mirror of
https://gitlab.com/Kwoth/nadekobot.git
synced 2025-09-10 17:28:27 -04:00
Initial implementation of trovo.live notifications support for .stadd
This commit is contained in:
@@ -6,7 +6,8 @@ public static class CommandNameLoadHelper
|
||||
{
|
||||
private static readonly IDeserializer _deserializer = new Deserializer();
|
||||
|
||||
public static Lazy<Dictionary<string, string[]>> LazyCommandAliases = new(() => LoadAliases());
|
||||
private static readonly Lazy<Dictionary<string, string[]>> _lazyCommandAliases
|
||||
= new(() => LoadAliases());
|
||||
|
||||
public static Dictionary<string, string[]> LoadAliases(string aliasesFilePath = "data/aliases.yml")
|
||||
{
|
||||
@@ -15,14 +16,14 @@ public static class CommandNameLoadHelper
|
||||
}
|
||||
|
||||
public static string[] GetAliasesFor(string methodName)
|
||||
=> LazyCommandAliases.Value.TryGetValue(methodName.ToLowerInvariant(), out var aliases) && aliases.Length > 1
|
||||
=> _lazyCommandAliases.Value.TryGetValue(methodName.ToLowerInvariant(), out var aliases) && aliases.Length > 1
|
||||
? aliases.Skip(1).ToArray()
|
||||
: Array.Empty<string>();
|
||||
|
||||
public static string GetCommandNameFor(string methodName)
|
||||
{
|
||||
methodName = methodName.ToLowerInvariant();
|
||||
var toReturn = LazyCommandAliases.Value.TryGetValue(methodName, out var aliases) && aliases.Length > 0
|
||||
var toReturn = _lazyCommandAliases.Value.TryGetValue(methodName, out var aliases) && aliases.Length > 0
|
||||
? aliases[0]
|
||||
: methodName;
|
||||
return toReturn;
|
||||
|
@@ -7,7 +7,7 @@ public sealed class OwnerOnlyAttribute : PreconditionAttribute
|
||||
{
|
||||
public override Task<PreconditionResult> CheckPermissionsAsync(
|
||||
ICommandContext context,
|
||||
CommandInfo executingCommand,
|
||||
CommandInfo command,
|
||||
IServiceProvider services)
|
||||
{
|
||||
var creds = services.GetRequiredService<IBotCredsProvider>().GetCreds();
|
||||
|
@@ -1,5 +1,6 @@
|
||||
#nullable disable
|
||||
#pragma warning disable all
|
||||
#pragma warning disable
|
||||
#pragma warning disable *
|
||||
// License MIT
|
||||
// Source: https://github.com/i3arnon/ConcurrentHashSet
|
||||
|
||||
@@ -332,16 +333,16 @@ public sealed class ConcurrentHashSet<T> : IReadOnlyCollection<T>, ICollection<T
|
||||
/// <returns>true if the <see cref="ConcurrentHashSet{T}" /> contains the item; otherwise, false.</returns>
|
||||
public bool Contains(T item)
|
||||
{
|
||||
var hashcode = _comparer.GetHashCode(item);
|
||||
var hashcode = _comparer.GetHashCode(item!);
|
||||
|
||||
// We must capture the _buckets field in a local variable. It is set to a new table on each table resize.
|
||||
var tables = this.tables;
|
||||
var localTables = this.tables;
|
||||
|
||||
var bucketNo = GetBucket(hashcode, tables.Buckets.Length);
|
||||
var bucketNo = GetBucket(hashcode, localTables.Buckets.Length);
|
||||
|
||||
// We can get away w/out a lock here.
|
||||
// The Volatile.Read ensures that the load of the fields of 'n' doesn't move before the load from buckets[i].
|
||||
var current = Volatile.Read(ref tables.Buckets[bucketNo]);
|
||||
var current = Volatile.Read(ref localTables.Buckets[bucketNo]);
|
||||
|
||||
while (current is not null)
|
||||
{
|
||||
@@ -444,29 +445,29 @@ public sealed class ConcurrentHashSet<T> : IReadOnlyCollection<T>, ICollection<T
|
||||
var hashcode = _comparer.GetHashCode(item);
|
||||
while (true)
|
||||
{
|
||||
var tables = this.tables;
|
||||
var localTables = this.tables;
|
||||
|
||||
GetBucketAndLockNo(hashcode, out var bucketNo, out var lockNo, tables.Buckets.Length, tables.Locks.Length);
|
||||
GetBucketAndLockNo(hashcode, out var bucketNo, out var lockNo, localTables.Buckets.Length, localTables.Locks.Length);
|
||||
|
||||
lock (tables.Locks[lockNo])
|
||||
lock (localTables.Locks[lockNo])
|
||||
{
|
||||
// If the table just got resized, we may not be holding the right lock, and must retry.
|
||||
// This should be a rare occurrence.
|
||||
if (tables != this.tables) continue;
|
||||
if (localTables != this.tables) continue;
|
||||
|
||||
Node previous = null;
|
||||
for (var current = tables.Buckets[bucketNo]; current is not null; current = current.Next)
|
||||
for (var current = localTables.Buckets[bucketNo]; current is not null; current = current.Next)
|
||||
{
|
||||
Debug.Assert((previous is null && current == tables.Buckets[bucketNo]) || previous.Next == current);
|
||||
Debug.Assert((previous is null && current == localTables.Buckets[bucketNo]) || previous!.Next == current);
|
||||
|
||||
if (hashcode == current.Hashcode && _comparer.Equals(current.Item, item))
|
||||
{
|
||||
if (previous is null)
|
||||
Volatile.Write(ref tables.Buckets[bucketNo], current.Next);
|
||||
Volatile.Write(ref localTables.Buckets[bucketNo], current.Next);
|
||||
else
|
||||
previous.Next = current.Next;
|
||||
|
||||
tables.CountPerLock[lockNo]--;
|
||||
localTables.CountPerLock[lockNo]--;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -489,25 +490,25 @@ public sealed class ConcurrentHashSet<T> : IReadOnlyCollection<T>, ICollection<T
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
var tables = this.tables;
|
||||
GetBucketAndLockNo(hashcode, out var bucketNo, out var lockNo, tables.Buckets.Length, tables.Locks.Length);
|
||||
var localTables = this.tables;
|
||||
GetBucketAndLockNo(hashcode, out var bucketNo, out var lockNo, localTables.Buckets.Length, localTables.Locks.Length);
|
||||
|
||||
var resizeDesired = false;
|
||||
var lockTaken = false;
|
||||
try
|
||||
{
|
||||
if (acquireLock)
|
||||
Monitor.Enter(tables.Locks[lockNo], ref lockTaken);
|
||||
Monitor.Enter(localTables.Locks[lockNo], ref lockTaken);
|
||||
|
||||
// If the table just got resized, we may not be holding the right lock, and must retry.
|
||||
// This should be a rare occurrence.
|
||||
if (tables != this.tables) continue;
|
||||
if (localTables != this.tables) continue;
|
||||
|
||||
// Try to find this item in the bucket
|
||||
Node previous = null;
|
||||
for (var current = tables.Buckets[bucketNo]; current is not null; current = current.Next)
|
||||
for (var current = localTables.Buckets[bucketNo]; current is not null; current = current.Next)
|
||||
{
|
||||
Debug.Assert((previous is null && current == tables.Buckets[bucketNo]) || previous.Next == current);
|
||||
Debug.Assert((previous is null && current == localTables.Buckets[bucketNo]) || previous!.Next == current);
|
||||
if (hashcode == current.Hashcode && _comparer.Equals(current.Item, item))
|
||||
return false;
|
||||
|
||||
@@ -515,10 +516,10 @@ public sealed class ConcurrentHashSet<T> : IReadOnlyCollection<T>, ICollection<T
|
||||
}
|
||||
|
||||
// The item was not found in the bucket. Insert the new item.
|
||||
Volatile.Write(ref tables.Buckets[bucketNo], new(item, hashcode, tables.Buckets[bucketNo]));
|
||||
Volatile.Write(ref localTables.Buckets[bucketNo], new(item, hashcode, localTables.Buckets[bucketNo]));
|
||||
checked
|
||||
{
|
||||
tables.CountPerLock[lockNo]++;
|
||||
localTables.CountPerLock[lockNo]++;
|
||||
}
|
||||
|
||||
//
|
||||
@@ -526,12 +527,12 @@ public sealed class ConcurrentHashSet<T> : IReadOnlyCollection<T>, ICollection<T
|
||||
// It is also possible that GrowTable will increase the budget but won't resize the bucket table.
|
||||
// That happens if the bucket table is found to be poorly utilized due to a bad hash function.
|
||||
//
|
||||
if (tables.CountPerLock[lockNo] > budget) resizeDesired = true;
|
||||
if (localTables.CountPerLock[lockNo] > budget) resizeDesired = true;
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (lockTaken)
|
||||
Monitor.Exit(tables.Locks[lockNo]);
|
||||
Monitor.Exit(localTables.Locks[lockNo]);
|
||||
}
|
||||
|
||||
//
|
||||
@@ -542,7 +543,7 @@ public sealed class ConcurrentHashSet<T> : IReadOnlyCollection<T>, ICollection<T
|
||||
// - As a result, it is possible that GrowTable will be called unnecessarily. But, GrowTable will obtain lock 0
|
||||
// and then verify that the table we passed to it as the argument is still the current table.
|
||||
//
|
||||
if (resizeDesired) GrowTable(tables);
|
||||
if (resizeDesired) GrowTable(localTables);
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -569,7 +570,7 @@ public sealed class ConcurrentHashSet<T> : IReadOnlyCollection<T>, ICollection<T
|
||||
Debug.Assert(lockNo >= 0 && lockNo < lockCount);
|
||||
}
|
||||
|
||||
private void GrowTable(Tables tables)
|
||||
private void GrowTable(Tables localTables)
|
||||
{
|
||||
const int maxArrayLength = 0X7FEFFFFF;
|
||||
var locksAcquired = 0;
|
||||
@@ -579,7 +580,7 @@ public sealed class ConcurrentHashSet<T> : IReadOnlyCollection<T>, ICollection<T
|
||||
AcquireLocks(0, 1, ref locksAcquired);
|
||||
|
||||
// Make sure nobody resized the table while we were waiting for lock 0:
|
||||
if (tables != this.tables)
|
||||
if (localTables != this.tables)
|
||||
// We assume that since the table reference is different, it was already resized (or the budget
|
||||
// was adjusted). If we ever decide to do table shrinking, or replace the table for other reasons,
|
||||
// we will have to revisit this logic.
|
||||
@@ -587,12 +588,12 @@ public sealed class ConcurrentHashSet<T> : IReadOnlyCollection<T>, ICollection<T
|
||||
|
||||
// Compute the (approx.) total size. Use an Int64 accumulation variable to avoid an overflow.
|
||||
long approxCount = 0;
|
||||
for (var i = 0; i < tables.CountPerLock.Length; i++) approxCount += tables.CountPerLock[i];
|
||||
for (var i = 0; i < localTables.CountPerLock.Length; i++) approxCount += localTables.CountPerLock[i];
|
||||
|
||||
//
|
||||
// If the bucket array is too empty, double the budget instead of resizing the table
|
||||
//
|
||||
if (approxCount < tables.Buckets.Length / 4)
|
||||
if (approxCount < localTables.Buckets.Length / 4)
|
||||
{
|
||||
budget = 2 * budget;
|
||||
if (budget < 0) budget = int.MaxValue;
|
||||
@@ -609,7 +610,7 @@ public sealed class ConcurrentHashSet<T> : IReadOnlyCollection<T>, ICollection<T
|
||||
checked
|
||||
{
|
||||
// Double the size of the buckets table and add one, so that we have an odd integer.
|
||||
newLength = (tables.Buckets.Length * 2) + 1;
|
||||
newLength = (localTables.Buckets.Length * 2) + 1;
|
||||
|
||||
// Now, we only need to check odd integers, and find the first that is not divisible
|
||||
// by 3, 5 or 7.
|
||||
@@ -638,25 +639,25 @@ public sealed class ConcurrentHashSet<T> : IReadOnlyCollection<T>, ICollection<T
|
||||
}
|
||||
|
||||
// Now acquire all other locks for the table
|
||||
AcquireLocks(1, tables.Locks.Length, ref locksAcquired);
|
||||
AcquireLocks(1, localTables.Locks.Length, ref locksAcquired);
|
||||
|
||||
var newLocks = tables.Locks;
|
||||
var newLocks = localTables.Locks;
|
||||
|
||||
// Add more locks
|
||||
if (_growLockArray && tables.Locks.Length < MAX_LOCK_NUMBER)
|
||||
if (_growLockArray && localTables.Locks.Length < MAX_LOCK_NUMBER)
|
||||
{
|
||||
newLocks = new object[tables.Locks.Length * 2];
|
||||
Array.Copy(tables.Locks, 0, newLocks, 0, tables.Locks.Length);
|
||||
for (var i = tables.Locks.Length; i < newLocks.Length; i++) newLocks[i] = new();
|
||||
newLocks = new object[localTables.Locks.Length * 2];
|
||||
Array.Copy(localTables.Locks, 0, newLocks, 0, localTables.Locks.Length);
|
||||
for (var i = localTables.Locks.Length; i < newLocks.Length; i++) newLocks[i] = new();
|
||||
}
|
||||
|
||||
var newBuckets = new Node[newLength];
|
||||
var newCountPerLock = new int[newLocks.Length];
|
||||
|
||||
// Copy all data into a new table, creating new nodes for all elements
|
||||
for (var i = 0; i < tables.Buckets.Length; i++)
|
||||
for (var i = 0; i < localTables.Buckets.Length; i++)
|
||||
{
|
||||
var current = tables.Buckets[i];
|
||||
var current = localTables.Buckets[i];
|
||||
while (current is not null)
|
||||
{
|
||||
var next = current.Next;
|
||||
|
@@ -42,8 +42,8 @@ public class IndexedCollection<T> : IList<T>
|
||||
}
|
||||
}
|
||||
|
||||
public int IndexOf([NotNull] T item)
|
||||
=> item.Index;
|
||||
public int IndexOf(T item)
|
||||
=> item?.Index ?? -1;
|
||||
|
||||
public IEnumerator<T> GetEnumerator()
|
||||
=> Source.GetEnumerator();
|
||||
|
@@ -67,6 +67,10 @@ Used for cryptocurrency related commands.")]
|
||||
[Comment(@"Api key used for Osu related commands. Obtain this key at https://osu.ppy.sh/p/api")]
|
||||
public string OsuApiKey { get; set; }
|
||||
|
||||
[Comment(@"Optional Trovo client id.
|
||||
You should only use this if Trovo notifications stopped working or you're getting ratelimit errors.")]
|
||||
public string TrovoClientId { get; set; }
|
||||
|
||||
[Comment(@"Command and args which will be used to restart the bot.
|
||||
Only used if bot is executed directly (NOT through the coordinator)
|
||||
placeholders:
|
||||
@@ -188,11 +192,11 @@ This should be equivalent to the DiscordsKey in your NadekoBot.Votes api appsett
|
||||
public int TotalShards { get; set; } = 1;
|
||||
public string PatreonAccessToken { get; set; } = string.Empty;
|
||||
public string PatreonCampaignId { get; set; } = "334038";
|
||||
public RestartConfig RestartCommand { get; set; } = null;
|
||||
public RestartConfig RestartCommand { get; set; }
|
||||
|
||||
public string ShardRunCommand { get; set; } = string.Empty;
|
||||
public string ShardRunArguments { get; set; } = string.Empty;
|
||||
public int? ShardRunPort { get; set; } = null;
|
||||
public int? ShardRunPort { get; set; }
|
||||
public string MiningProxyUrl { get; set; } = string.Empty;
|
||||
public string MiningProxyCreds { get; set; } = string.Empty;
|
||||
|
||||
|
@@ -20,6 +20,7 @@ public interface IBotCredentials
|
||||
string LocationIqApiKey { get; }
|
||||
string TimezoneDbApiKey { get; }
|
||||
string CoinmarketcapApiKey { get; }
|
||||
string TrovoClientId { get; }
|
||||
string CoordinatorUrl { get; set; }
|
||||
}
|
||||
|
||||
|
@@ -11,7 +11,8 @@ public class FollowedStream : DbEntity
|
||||
Twitch = 0,
|
||||
Picarto = 3,
|
||||
Youtube = 4,
|
||||
Facebook = 5
|
||||
Facebook = 5,
|
||||
Trovo = 6
|
||||
}
|
||||
|
||||
public ulong GuildId { get; set; }
|
||||
|
@@ -314,9 +314,7 @@ public partial class Administration : NadekoModule<AdministrationService>
|
||||
}
|
||||
|
||||
if (time is null)
|
||||
{
|
||||
await msg.DeleteAsync();
|
||||
}
|
||||
else if (time.Time <= TimeSpan.FromDays(7))
|
||||
{
|
||||
_ = Task.Run(async () =>
|
||||
|
@@ -39,7 +39,7 @@ public sealed class StreamNotificationService : INService
|
||||
DiscordSocketClient client,
|
||||
IBotStrings strings,
|
||||
ConnectionMultiplexer redis,
|
||||
IBotCredentials creds,
|
||||
IBotCredsProvider creds,
|
||||
IHttpClientFactory httpFactory,
|
||||
Bot bot,
|
||||
IPubSub pubSub,
|
||||
@@ -50,7 +50,7 @@ public sealed class StreamNotificationService : INService
|
||||
_strings = strings;
|
||||
_pubSub = pubSub;
|
||||
_eb = eb;
|
||||
_streamTracker = new(httpFactory, redis, creds.RedisKey(), client.ShardId == 0);
|
||||
_streamTracker = new(httpFactory, creds, redis, creds.GetCreds().RedisKey(), client.ShardId == 0);
|
||||
|
||||
_streamsOnlineKey = new("streams.online");
|
||||
_streamsOfflineKey = new("streams.offline");
|
||||
|
@@ -0,0 +1,61 @@
|
||||
#nullable disable
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace NadekoBot.Modules.Searches.Common.StreamNotifications.Providers;
|
||||
|
||||
public class TrovoGetUsersResponse
|
||||
{
|
||||
[JsonPropertyName("is_live")]
|
||||
public bool IsLive { get; set; }
|
||||
|
||||
[JsonPropertyName("category_id")]
|
||||
public string CategoryId { get; set; }
|
||||
|
||||
[JsonPropertyName("category_name")]
|
||||
public string CategoryName { get; set; }
|
||||
|
||||
[JsonPropertyName("live_title")]
|
||||
public string LiveTitle { get; set; }
|
||||
|
||||
[JsonPropertyName("audi_type")]
|
||||
public string AudiType { get; set; }
|
||||
|
||||
[JsonPropertyName("language_code")]
|
||||
public string LanguageCode { get; set; }
|
||||
|
||||
[JsonPropertyName("thumbnail")]
|
||||
public string Thumbnail { get; set; }
|
||||
|
||||
[JsonPropertyName("current_viewers")]
|
||||
public int CurrentViewers { get; set; }
|
||||
|
||||
[JsonPropertyName("followers")]
|
||||
public int Followers { get; set; }
|
||||
|
||||
[JsonPropertyName("streamer_info")]
|
||||
public string StreamerInfo { get; set; }
|
||||
|
||||
[JsonPropertyName("profile_pic")]
|
||||
public string ProfilePic { get; set; }
|
||||
|
||||
[JsonPropertyName("channel_url")]
|
||||
public string ChannelUrl { get; set; }
|
||||
|
||||
[JsonPropertyName("created_at")]
|
||||
public string CreatedAt { get; set; }
|
||||
|
||||
[JsonPropertyName("subscriber_num")]
|
||||
public int SubscriberNum { get; set; }
|
||||
|
||||
[JsonPropertyName("username")]
|
||||
public string Username { get; set; }
|
||||
|
||||
[JsonPropertyName("social_links")]
|
||||
public List<TrovoSocialLink> SocialLinks { get; set; }
|
||||
|
||||
[JsonPropertyName("started_at")]
|
||||
public string StartedAt { get; set; }
|
||||
|
||||
[JsonPropertyName("ended_at")]
|
||||
public string EndedAt { get; set; }
|
||||
}
|
@@ -0,0 +1,10 @@
|
||||
#nullable disable
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace NadekoBot.Modules.Searches.Common.StreamNotifications.Providers;
|
||||
|
||||
public class TrovoRequestData
|
||||
{
|
||||
[JsonPropertyName("channel_id")]
|
||||
public string ChannelId { get; set; }
|
||||
}
|
@@ -0,0 +1,13 @@
|
||||
#nullable disable
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace NadekoBot.Modules.Searches.Common.StreamNotifications.Providers;
|
||||
|
||||
public class TrovoSocialLink
|
||||
{
|
||||
[JsonPropertyName("type")]
|
||||
public string Type { get; set; }
|
||||
|
||||
[JsonPropertyName("url")]
|
||||
public string Url { get; set; }
|
||||
}
|
@@ -17,6 +17,7 @@ public class NotifChecker
|
||||
|
||||
public NotifChecker(
|
||||
IHttpClientFactory httpClientFactory,
|
||||
IBotCredsProvider credsProvider,
|
||||
ConnectionMultiplexer multi,
|
||||
string uniqueCacheKey,
|
||||
bool isMaster)
|
||||
@@ -26,7 +27,8 @@ public class NotifChecker
|
||||
_streamProviders = new()
|
||||
{
|
||||
{ FollowedStream.FType.Twitch, new TwitchProvider(httpClientFactory) },
|
||||
{ FollowedStream.FType.Picarto, new PicartoProvider(httpClientFactory) }
|
||||
{ FollowedStream.FType.Picarto, new PicartoProvider(httpClientFactory) },
|
||||
{ FollowedStream.FType.Trovo, new TrovoProvider(httpClientFactory, credsProvider) }
|
||||
};
|
||||
_offlineBuffer = new();
|
||||
if (isMaster) CacheClearAllData();
|
||||
@@ -35,17 +37,20 @@ public class NotifChecker
|
||||
// gets all streams which have been failing for more than the provided timespan
|
||||
public IEnumerable<StreamDataKey> GetFailingStreams(TimeSpan duration, bool remove = false)
|
||||
{
|
||||
var toReturn = _streamProviders.SelectMany(prov => prov.Value
|
||||
var toReturn = _streamProviders
|
||||
.SelectMany(prov => prov.Value
|
||||
.FailingStreams
|
||||
.Where(fs => DateTime.UtcNow - fs.ErroringSince
|
||||
> duration)
|
||||
.Select(fs => new StreamDataKey(prov.Value.Platform,
|
||||
fs.Item1)))
|
||||
.Where(fs => DateTime.UtcNow - fs.Value > duration)
|
||||
.Select(fs => new StreamDataKey(prov.Value.Platform, fs.Key)))
|
||||
.ToList();
|
||||
|
||||
if (remove)
|
||||
{
|
||||
foreach (var toBeRemoved in toReturn)
|
||||
{
|
||||
_streamProviders[toBeRemoved.Type].ClearErrorsFor(toBeRemoved.Name);
|
||||
}
|
||||
}
|
||||
|
||||
return toReturn;
|
||||
}
|
||||
@@ -54,6 +59,7 @@ public class NotifChecker
|
||||
=> Task.Run(async () =>
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
try
|
||||
{
|
||||
var allStreamData = CacheGetAllData();
|
||||
@@ -65,7 +71,8 @@ public class NotifChecker
|
||||
entry => entry.AsEnumerable()
|
||||
.ToDictionary(x => x.Key.Name, x => x.Value));
|
||||
|
||||
var newStreamData = await oldStreamDataDict.Select(x =>
|
||||
var newStreamData = await oldStreamDataDict
|
||||
.Select(x =>
|
||||
{
|
||||
// get all stream data for the streams of this type
|
||||
if (_streamProviders.TryGetValue(x.Key,
|
||||
@@ -75,7 +82,8 @@ public class NotifChecker
|
||||
.ToList());
|
||||
|
||||
// this means there's no provider for this stream data, (and there was before?)
|
||||
return Task.FromResult(new List<StreamData>());
|
||||
return Task.FromResult<IReadOnlyCollection<StreamData>>(
|
||||
new List<StreamData>());
|
||||
})
|
||||
.WhenAll();
|
||||
|
||||
@@ -124,7 +132,10 @@ public class NotifChecker
|
||||
}
|
||||
}
|
||||
|
||||
var tasks = new List<Task> { Task.Delay(30_000) };
|
||||
var tasks = new List<Task>
|
||||
{
|
||||
Task.Delay(30_000)
|
||||
};
|
||||
|
||||
if (newlyOnline.Count > 0) tasks.Add(OnStreamsOnline(newlyOnline));
|
||||
|
||||
@@ -136,6 +147,7 @@ public class NotifChecker
|
||||
{
|
||||
Log.Error(ex, "Error getting stream notifications: {ErrorMessage}", ex.Message);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
public bool CacheAddData(StreamDataKey key, StreamData? data, bool replace)
|
||||
|
@@ -46,10 +46,10 @@ public class PicartoProvider : Provider
|
||||
return data.FirstOrDefault();
|
||||
}
|
||||
|
||||
public override async Task<List<StreamData>> GetStreamDataAsync(List<string> logins)
|
||||
public override async Task<IReadOnlyCollection<StreamData>> GetStreamDataAsync(List<string> logins)
|
||||
{
|
||||
if (logins.Count == 0)
|
||||
return new();
|
||||
return new List<StreamData>();
|
||||
|
||||
using var http = _httpClientFactory.CreateClient();
|
||||
var toReturn = new List<StreamData>();
|
||||
|
@@ -15,9 +15,10 @@ public abstract class Provider
|
||||
/// <summary>
|
||||
/// Gets the stream usernames which fail to execute due to an error, and when they started throwing errors.
|
||||
/// This can happen if stream name is invalid, or if the stream doesn't exist anymore.
|
||||
/// Override to provide a custom implementation
|
||||
/// </summary>
|
||||
public IEnumerable<(string Login, DateTime ErroringSince)> FailingStreams
|
||||
=> _failingStreams.Select(entry => (entry.Key, entry.Value)).ToList();
|
||||
public virtual IReadOnlyDictionary<string, DateTime> FailingStreams
|
||||
=> _failingStreams;
|
||||
|
||||
/// <summary>
|
||||
/// When was the first time the stream continually had errors while being retrieved
|
||||
@@ -50,8 +51,13 @@ public abstract class Provider
|
||||
/// </summary>
|
||||
/// <param name="usernames">List of ids/usernames</param>
|
||||
/// <returns><see cref="StreamData" /> of all users, in the same order. Null for every id/user not found.</returns>
|
||||
public abstract Task<List<StreamData>> GetStreamDataAsync(List<string> usernames);
|
||||
public abstract Task<IReadOnlyCollection<StreamData>> GetStreamDataAsync(List<string> usernames);
|
||||
|
||||
public void ClearErrorsFor(string login)
|
||||
=> _failingStreams.TryRemove(login, out _);
|
||||
/// <summary>
|
||||
/// Unmark the stream as errored. You should override this method
|
||||
/// if you've overridden the <see cref="FailingStreams"/> property.
|
||||
/// </summary>
|
||||
/// <param name="login"></param>
|
||||
public virtual void ClearErrorsFor(string login)
|
||||
=> _failingStreams.Clear();
|
||||
}
|
@@ -0,0 +1,106 @@
|
||||
using NadekoBot.Db.Models;
|
||||
using System.Net.Http.Json;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace NadekoBot.Modules.Searches.Common.StreamNotifications.Providers;
|
||||
|
||||
public class TrovoProvider : Provider
|
||||
{
|
||||
private readonly IHttpClientFactory _httpClientFactory;
|
||||
|
||||
public override FollowedStream.FType Platform
|
||||
=> FollowedStream.FType.Trovo;
|
||||
|
||||
private readonly Regex _urlRegex
|
||||
= new Regex(@"trovo.live\/(?<channel>[\w\d\-_]+)/?", RegexOptions.Compiled);
|
||||
|
||||
private readonly IBotCredsProvider _creds;
|
||||
|
||||
|
||||
public TrovoProvider(IHttpClientFactory httpClientFactory, IBotCredsProvider creds)
|
||||
=> (_httpClientFactory, _creds) = (httpClientFactory, creds);
|
||||
|
||||
public override Task<bool> IsValidUrl(string url)
|
||||
=> Task.FromResult(_urlRegex.IsMatch(url));
|
||||
|
||||
public override Task<StreamData?> GetStreamDataByUrlAsync(string url)
|
||||
{
|
||||
var match = _urlRegex.Match(url);
|
||||
if (match.Length == 0)
|
||||
return Task.FromResult(default(StreamData?));
|
||||
|
||||
return GetStreamDataAsync(match.Groups["channel"].Value);
|
||||
}
|
||||
|
||||
public override async Task<StreamData?> GetStreamDataAsync(string id)
|
||||
{
|
||||
using var http = _httpClientFactory.CreateClient();
|
||||
|
||||
var trovoClientId = _creds.GetCreds().TrovoClientId;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(trovoClientId))
|
||||
trovoClientId = "waiting for key";
|
||||
|
||||
|
||||
http.DefaultRequestHeaders.Clear();
|
||||
http.DefaultRequestHeaders.Add("Accept", "application/json");
|
||||
http.DefaultRequestHeaders.Add("Client-ID", trovoClientId);
|
||||
|
||||
// trovo ratelimit is very generous (1200 per minute)
|
||||
// so there is no need for ratelimit checks atm
|
||||
try
|
||||
{
|
||||
var res = await http.PostAsJsonAsync(
|
||||
$"https://open-api.trovo.live/openplatform/channels/id",
|
||||
new TrovoRequestData()
|
||||
{
|
||||
ChannelId = id
|
||||
});
|
||||
|
||||
res.EnsureSuccessStatusCode();
|
||||
|
||||
var data = await res.Content.ReadFromJsonAsync<TrovoGetUsersResponse>();
|
||||
|
||||
if (data is null)
|
||||
{
|
||||
Log.Warning("An empty response received while retrieving stream data for trovo.live/{TrovoId}", id);
|
||||
_failingStreams.TryAdd(id, DateTime.UtcNow);
|
||||
return null;
|
||||
}
|
||||
|
||||
return new()
|
||||
{
|
||||
IsLive = data.IsLive,
|
||||
Game = data.CategoryName,
|
||||
Name = data.Username,
|
||||
Title = data.LiveTitle,
|
||||
Viewers = data.CurrentViewers,
|
||||
AvatarUrl = data.ProfilePic,
|
||||
StreamType = FollowedStream.FType.Picarto,
|
||||
StreamUrl = data.ChannelUrl,
|
||||
UniqueName = data.Username,
|
||||
Preview = data.Thumbnail,
|
||||
};
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Warning(ex, "Error retrieving stream data for trovo.live/{TrovoId}", id);
|
||||
_failingStreams.TryAdd(id, DateTime.UtcNow);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public override async Task<IReadOnlyCollection<StreamData>> GetStreamDataAsync(List<string> usernames)
|
||||
{
|
||||
var results = new List<StreamData>(usernames.Count);
|
||||
foreach (var chunk in usernames.Chunk(10)
|
||||
.Select(x => x.Select(GetStreamDataAsync)))
|
||||
{
|
||||
var chunkResults = await Task.WhenAll(chunk);
|
||||
results.AddRange(chunkResults.Where(x => x is not null)!);
|
||||
await Task.Delay(1000);
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
}
|
@@ -12,11 +12,17 @@ public class TwitchProvider : Provider
|
||||
public override FollowedStream.FType Platform
|
||||
=> FollowedStream.FType.Twitch;
|
||||
|
||||
public override IReadOnlyDictionary<string, DateTime> FailingStreams
|
||||
=> _failingStreams;
|
||||
|
||||
private readonly IHttpClientFactory _httpClientFactory;
|
||||
|
||||
public TwitchProvider(IHttpClientFactory httpClientFactory)
|
||||
=> _httpClientFactory = httpClientFactory;
|
||||
|
||||
public override void ClearErrorsFor(string login)
|
||||
=> _failingStreams.TryRemove(login, out _);
|
||||
|
||||
public override Task<bool> IsValidUrl(string url)
|
||||
{
|
||||
var match = Regex.Match(url);
|
||||
@@ -41,15 +47,18 @@ public class TwitchProvider : Provider
|
||||
|
||||
public override async Task<StreamData?> GetStreamDataAsync(string id)
|
||||
{
|
||||
var data = await GetStreamDataAsync(new List<string> { id });
|
||||
var data = await GetStreamDataAsync(new List<string>
|
||||
{
|
||||
id
|
||||
});
|
||||
|
||||
return data.FirstOrDefault();
|
||||
}
|
||||
|
||||
public override async Task<List<StreamData>> GetStreamDataAsync(List<string> logins)
|
||||
public override async Task<IReadOnlyCollection<StreamData>> GetStreamDataAsync(List<string> logins)
|
||||
{
|
||||
if (logins.Count == 0)
|
||||
return new();
|
||||
return new List<StreamData>();
|
||||
|
||||
using var http = _httpClientFactory.CreateClient();
|
||||
http.DefaultRequestHeaders.Add("Client-Id", "67w6z9i09xv2uoojdm9l0wsyph4hxo6");
|
||||
@@ -70,7 +79,11 @@ public class TwitchProvider : Provider
|
||||
|
||||
// get stream data
|
||||
var str = await http.GetStringAsync($"https://api.twitch.tv/kraken/streams/{user.Id}");
|
||||
var resObj = JsonConvert.DeserializeAnonymousType(str, new { Stream = new TwitchResponseV5.Stream() });
|
||||
var resObj = JsonConvert.DeserializeAnonymousType(str,
|
||||
new
|
||||
{
|
||||
Stream = new TwitchResponseV5.Stream()
|
||||
});
|
||||
|
||||
// if stream is null, user is not streaming
|
||||
if (resObj?.Stream is null)
|
||||
|
@@ -9,7 +9,8 @@
|
||||
<RunWorkingDirectory>$(MSBuildProjectDirectory)</RunWorkingDirectory>
|
||||
<OutputType>exe</OutputType>
|
||||
<ApplicationIcon>nadeko_icon.ico</ApplicationIcon>
|
||||
<NoWarn>CS1066</NoWarn>
|
||||
<NoWarn>CS1066,CA1069</NoWarn>
|
||||
<AnalysisMode>Recommended</AnalysisMode>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
@@ -119,6 +119,12 @@ public sealed class BotCredsProvider : IBotCredsProvider
|
||||
var jsonCredentialsFileText = File.ReadAllText(OldCredsJsonPath);
|
||||
var oldCreds = JsonConvert.DeserializeObject<Creds.Old>(jsonCredentialsFileText);
|
||||
|
||||
if (oldCreds is null)
|
||||
{
|
||||
Log.Error("Error while reading old credentials file. Make sure that the file is formatted correctly");
|
||||
return;
|
||||
}
|
||||
|
||||
var creds = new Creds
|
||||
{
|
||||
Version = 1,
|
||||
|
Reference in New Issue
Block a user