Initial implementation of trovo.live notifications support for .stadd

This commit is contained in:
Kwoth
2022-01-31 00:47:37 +01:00
parent cd812304f7
commit 0f36242597
19 changed files with 318 additions and 84 deletions

View File

@@ -6,7 +6,8 @@ public static class CommandNameLoadHelper
{ {
private static readonly IDeserializer _deserializer = new Deserializer(); 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") 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) 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() ? aliases.Skip(1).ToArray()
: Array.Empty<string>(); : Array.Empty<string>();
public static string GetCommandNameFor(string methodName) public static string GetCommandNameFor(string methodName)
{ {
methodName = methodName.ToLowerInvariant(); 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] ? aliases[0]
: methodName; : methodName;
return toReturn; return toReturn;

View File

@@ -7,7 +7,7 @@ public sealed class OwnerOnlyAttribute : PreconditionAttribute
{ {
public override Task<PreconditionResult> CheckPermissionsAsync( public override Task<PreconditionResult> CheckPermissionsAsync(
ICommandContext context, ICommandContext context,
CommandInfo executingCommand, CommandInfo command,
IServiceProvider services) IServiceProvider services)
{ {
var creds = services.GetRequiredService<IBotCredsProvider>().GetCreds(); var creds = services.GetRequiredService<IBotCredsProvider>().GetCreds();

View File

@@ -1,5 +1,6 @@
#nullable disable #nullable disable
#pragma warning disable all #pragma warning disable
#pragma warning disable *
// License MIT // License MIT
// Source: https://github.com/i3arnon/ConcurrentHashSet // 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> /// <returns>true if the <see cref="ConcurrentHashSet{T}" /> contains the item; otherwise, false.</returns>
public bool Contains(T item) 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. // 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. // 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]. // 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) while (current is not null)
{ {
@@ -444,29 +445,29 @@ public sealed class ConcurrentHashSet<T> : IReadOnlyCollection<T>, ICollection<T
var hashcode = _comparer.GetHashCode(item); var hashcode = _comparer.GetHashCode(item);
while (true) 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. // If the table just got resized, we may not be holding the right lock, and must retry.
// This should be a rare occurrence. // This should be a rare occurrence.
if (tables != this.tables) continue; if (localTables != this.tables) continue;
Node previous = null; 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 (hashcode == current.Hashcode && _comparer.Equals(current.Item, item))
{ {
if (previous is null) if (previous is null)
Volatile.Write(ref tables.Buckets[bucketNo], current.Next); Volatile.Write(ref localTables.Buckets[bucketNo], current.Next);
else else
previous.Next = current.Next; previous.Next = current.Next;
tables.CountPerLock[lockNo]--; localTables.CountPerLock[lockNo]--;
return true; return true;
} }
@@ -489,25 +490,25 @@ public sealed class ConcurrentHashSet<T> : IReadOnlyCollection<T>, ICollection<T
{ {
while (true) 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);
var resizeDesired = false; var resizeDesired = false;
var lockTaken = false; var lockTaken = false;
try try
{ {
if (acquireLock) 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. // If the table just got resized, we may not be holding the right lock, and must retry.
// This should be a rare occurrence. // This should be a rare occurrence.
if (tables != this.tables) continue; if (localTables != this.tables) continue;
// Try to find this item in the bucket // Try to find this item in the bucket
Node previous = null; 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 (hashcode == current.Hashcode && _comparer.Equals(current.Item, item))
return false; 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. // 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 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. // 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. // 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 finally
{ {
if (lockTaken) 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 // - 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. // 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; return true;
} }
@@ -569,7 +570,7 @@ public sealed class ConcurrentHashSet<T> : IReadOnlyCollection<T>, ICollection<T
Debug.Assert(lockNo >= 0 && lockNo < lockCount); Debug.Assert(lockNo >= 0 && lockNo < lockCount);
} }
private void GrowTable(Tables tables) private void GrowTable(Tables localTables)
{ {
const int maxArrayLength = 0X7FEFFFFF; const int maxArrayLength = 0X7FEFFFFF;
var locksAcquired = 0; var locksAcquired = 0;
@@ -579,7 +580,7 @@ public sealed class ConcurrentHashSet<T> : IReadOnlyCollection<T>, ICollection<T
AcquireLocks(0, 1, ref locksAcquired); AcquireLocks(0, 1, ref locksAcquired);
// Make sure nobody resized the table while we were waiting for lock 0: // 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 // 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, // was adjusted). If we ever decide to do table shrinking, or replace the table for other reasons,
// we will have to revisit this logic. // 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. // Compute the (approx.) total size. Use an Int64 accumulation variable to avoid an overflow.
long approxCount = 0; 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 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; budget = 2 * budget;
if (budget < 0) budget = int.MaxValue; if (budget < 0) budget = int.MaxValue;
@@ -609,7 +610,7 @@ public sealed class ConcurrentHashSet<T> : IReadOnlyCollection<T>, ICollection<T
checked checked
{ {
// Double the size of the buckets table and add one, so that we have an odd integer. // 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 // Now, we only need to check odd integers, and find the first that is not divisible
// by 3, 5 or 7. // 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 // 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 // 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]; newLocks = new object[localTables.Locks.Length * 2];
Array.Copy(tables.Locks, 0, newLocks, 0, tables.Locks.Length); Array.Copy(localTables.Locks, 0, newLocks, 0, localTables.Locks.Length);
for (var i = tables.Locks.Length; i < newLocks.Length; i++) newLocks[i] = new(); for (var i = localTables.Locks.Length; i < newLocks.Length; i++) newLocks[i] = new();
} }
var newBuckets = new Node[newLength]; var newBuckets = new Node[newLength];
var newCountPerLock = new int[newLocks.Length]; var newCountPerLock = new int[newLocks.Length];
// Copy all data into a new table, creating new nodes for all elements // 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) while (current is not null)
{ {
var next = current.Next; var next = current.Next;

View File

@@ -42,8 +42,8 @@ public class IndexedCollection<T> : IList<T>
} }
} }
public int IndexOf([NotNull] T item) public int IndexOf(T item)
=> item.Index; => item?.Index ?? -1;
public IEnumerator<T> GetEnumerator() public IEnumerator<T> GetEnumerator()
=> Source.GetEnumerator(); => Source.GetEnumerator();

View File

@@ -66,6 +66,10 @@ Used for cryptocurrency related commands.")]
[Comment(@"Api key used for Osu related commands. Obtain this key at https://osu.ppy.sh/p/api")] [Comment(@"Api key used for Osu related commands. Obtain this key at https://osu.ppy.sh/p/api")]
public string OsuApiKey { get; set; } 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. [Comment(@"Command and args which will be used to restart the bot.
Only used if bot is executed directly (NOT through the coordinator) Only used if bot is executed directly (NOT through the coordinator)
@@ -188,11 +192,11 @@ This should be equivalent to the DiscordsKey in your NadekoBot.Votes api appsett
public int TotalShards { get; set; } = 1; public int TotalShards { get; set; } = 1;
public string PatreonAccessToken { get; set; } = string.Empty; public string PatreonAccessToken { get; set; } = string.Empty;
public string PatreonCampaignId { get; set; } = "334038"; 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 ShardRunCommand { get; set; } = string.Empty;
public string ShardRunArguments { 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 MiningProxyUrl { get; set; } = string.Empty;
public string MiningProxyCreds { get; set; } = string.Empty; public string MiningProxyCreds { get; set; } = string.Empty;

View File

@@ -20,6 +20,7 @@ public interface IBotCredentials
string LocationIqApiKey { get; } string LocationIqApiKey { get; }
string TimezoneDbApiKey { get; } string TimezoneDbApiKey { get; }
string CoinmarketcapApiKey { get; } string CoinmarketcapApiKey { get; }
string TrovoClientId { get; }
string CoordinatorUrl { get; set; } string CoordinatorUrl { get; set; }
} }

View File

@@ -11,7 +11,8 @@ public class FollowedStream : DbEntity
Twitch = 0, Twitch = 0,
Picarto = 3, Picarto = 3,
Youtube = 4, Youtube = 4,
Facebook = 5 Facebook = 5,
Trovo = 6
} }
public ulong GuildId { get; set; } public ulong GuildId { get; set; }

View File

@@ -314,12 +314,10 @@ public partial class Administration : NadekoModule<AdministrationService>
} }
if (time is null) if (time is null)
{
await msg.DeleteAsync(); await msg.DeleteAsync();
}
else if (time.Time <= TimeSpan.FromDays(7)) else if (time.Time <= TimeSpan.FromDays(7))
{ {
_= Task.Run(async () => _ = Task.Run(async () =>
{ {
await Task.Delay(time.Time); await Task.Delay(time.Time);
await msg.DeleteAsync(); await msg.DeleteAsync();

View File

@@ -39,7 +39,7 @@ public sealed class StreamNotificationService : INService
DiscordSocketClient client, DiscordSocketClient client,
IBotStrings strings, IBotStrings strings,
ConnectionMultiplexer redis, ConnectionMultiplexer redis,
IBotCredentials creds, IBotCredsProvider creds,
IHttpClientFactory httpFactory, IHttpClientFactory httpFactory,
Bot bot, Bot bot,
IPubSub pubSub, IPubSub pubSub,
@@ -50,7 +50,7 @@ public sealed class StreamNotificationService : INService
_strings = strings; _strings = strings;
_pubSub = pubSub; _pubSub = pubSub;
_eb = eb; _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"); _streamsOnlineKey = new("streams.online");
_streamsOfflineKey = new("streams.offline"); _streamsOfflineKey = new("streams.offline");

View File

@@ -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; }
}

View File

@@ -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; }
}

View File

@@ -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; }
}

View File

@@ -17,6 +17,7 @@ public class NotifChecker
public NotifChecker( public NotifChecker(
IHttpClientFactory httpClientFactory, IHttpClientFactory httpClientFactory,
IBotCredsProvider credsProvider,
ConnectionMultiplexer multi, ConnectionMultiplexer multi,
string uniqueCacheKey, string uniqueCacheKey,
bool isMaster) bool isMaster)
@@ -26,7 +27,8 @@ public class NotifChecker
_streamProviders = new() _streamProviders = new()
{ {
{ FollowedStream.FType.Twitch, new TwitchProvider(httpClientFactory) }, { 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(); _offlineBuffer = new();
if (isMaster) CacheClearAllData(); if (isMaster) CacheClearAllData();
@@ -35,17 +37,20 @@ public class NotifChecker
// gets all streams which have been failing for more than the provided timespan // gets all streams which have been failing for more than the provided timespan
public IEnumerable<StreamDataKey> GetFailingStreams(TimeSpan duration, bool remove = false) public IEnumerable<StreamDataKey> GetFailingStreams(TimeSpan duration, bool remove = false)
{ {
var toReturn = _streamProviders.SelectMany(prov => prov.Value var toReturn = _streamProviders
.FailingStreams .SelectMany(prov => prov.Value
.Where(fs => DateTime.UtcNow - fs.ErroringSince .FailingStreams
> duration) .Where(fs => DateTime.UtcNow - fs.Value > duration)
.Select(fs => new StreamDataKey(prov.Value.Platform, .Select(fs => new StreamDataKey(prov.Value.Platform, fs.Key)))
fs.Item1))) .ToList();
.ToList();
if (remove) if (remove)
{
foreach (var toBeRemoved in toReturn) foreach (var toBeRemoved in toReturn)
{
_streamProviders[toBeRemoved.Type].ClearErrorsFor(toBeRemoved.Name); _streamProviders[toBeRemoved.Type].ClearErrorsFor(toBeRemoved.Name);
}
}
return toReturn; return toReturn;
} }
@@ -54,6 +59,7 @@ public class NotifChecker
=> Task.Run(async () => => Task.Run(async () =>
{ {
while (true) while (true)
{
try try
{ {
var allStreamData = CacheGetAllData(); var allStreamData = CacheGetAllData();
@@ -65,19 +71,21 @@ public class NotifChecker
entry => entry.AsEnumerable() entry => entry.AsEnumerable()
.ToDictionary(x => x.Key.Name, x => x.Value)); .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, // get all stream data for the streams of this type
out var provider)) if (_streamProviders.TryGetValue(x.Key,
return provider.GetStreamDataAsync(x.Value out var provider))
.Select(entry => entry.Key) return provider.GetStreamDataAsync(x.Value
.ToList()); .Select(entry => entry.Key)
.ToList());
// this means there's no provider for this stream data, (and there was before?) // 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(); })
.WhenAll();
var newlyOnline = new List<StreamData>(); var newlyOnline = new List<StreamData>();
var newlyOffline = new List<StreamData>(); var newlyOffline = new List<StreamData>();
@@ -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)); 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); Log.Error(ex, "Error getting stream notifications: {ErrorMessage}", ex.Message);
} }
}
}); });
public bool CacheAddData(StreamDataKey key, StreamData? data, bool replace) public bool CacheAddData(StreamDataKey key, StreamData? data, bool replace)

View File

@@ -46,10 +46,10 @@ public class PicartoProvider : Provider
return data.FirstOrDefault(); 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) if (logins.Count == 0)
return new(); return new List<StreamData>();
using var http = _httpClientFactory.CreateClient(); using var http = _httpClientFactory.CreateClient();
var toReturn = new List<StreamData>(); var toReturn = new List<StreamData>();

View File

@@ -15,9 +15,10 @@ public abstract class Provider
/// <summary> /// <summary>
/// Gets the stream usernames which fail to execute due to an error, and when they started throwing errors. /// 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. /// This can happen if stream name is invalid, or if the stream doesn't exist anymore.
/// Override to provide a custom implementation
/// </summary> /// </summary>
public IEnumerable<(string Login, DateTime ErroringSince)> FailingStreams public virtual IReadOnlyDictionary<string, DateTime> FailingStreams
=> _failingStreams.Select(entry => (entry.Key, entry.Value)).ToList(); => _failingStreams;
/// <summary> /// <summary>
/// When was the first time the stream continually had errors while being retrieved /// When was the first time the stream continually had errors while being retrieved
@@ -50,8 +51,13 @@ public abstract class Provider
/// </summary> /// </summary>
/// <param name="usernames">List of ids/usernames</param> /// <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> /// <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) /// <summary>
=> _failingStreams.TryRemove(login, out _); /// 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();
} }

View File

@@ -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;
}
}

View File

@@ -12,11 +12,17 @@ public class TwitchProvider : Provider
public override FollowedStream.FType Platform public override FollowedStream.FType Platform
=> FollowedStream.FType.Twitch; => FollowedStream.FType.Twitch;
public override IReadOnlyDictionary<string, DateTime> FailingStreams
=> _failingStreams;
private readonly IHttpClientFactory _httpClientFactory; private readonly IHttpClientFactory _httpClientFactory;
public TwitchProvider(IHttpClientFactory httpClientFactory) public TwitchProvider(IHttpClientFactory httpClientFactory)
=> _httpClientFactory = httpClientFactory; => _httpClientFactory = httpClientFactory;
public override void ClearErrorsFor(string login)
=> _failingStreams.TryRemove(login, out _);
public override Task<bool> IsValidUrl(string url) public override Task<bool> IsValidUrl(string url)
{ {
var match = Regex.Match(url); var match = Regex.Match(url);
@@ -41,15 +47,18 @@ public class TwitchProvider : Provider
public override async Task<StreamData?> GetStreamDataAsync(string id) 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(); 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) if (logins.Count == 0)
return new(); return new List<StreamData>();
using var http = _httpClientFactory.CreateClient(); using var http = _httpClientFactory.CreateClient();
http.DefaultRequestHeaders.Add("Client-Id", "67w6z9i09xv2uoojdm9l0wsyph4hxo6"); http.DefaultRequestHeaders.Add("Client-Id", "67w6z9i09xv2uoojdm9l0wsyph4hxo6");
@@ -70,7 +79,11 @@ public class TwitchProvider : Provider
// get stream data // get stream data
var str = await http.GetStringAsync($"https://api.twitch.tv/kraken/streams/{user.Id}"); 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 stream is null, user is not streaming
if (resObj?.Stream is null) if (resObj?.Stream is null)

View File

@@ -9,7 +9,8 @@
<RunWorkingDirectory>$(MSBuildProjectDirectory)</RunWorkingDirectory> <RunWorkingDirectory>$(MSBuildProjectDirectory)</RunWorkingDirectory>
<OutputType>exe</OutputType> <OutputType>exe</OutputType>
<ApplicationIcon>nadeko_icon.ico</ApplicationIcon> <ApplicationIcon>nadeko_icon.ico</ApplicationIcon>
<NoWarn>CS1066</NoWarn> <NoWarn>CS1066,CA1069</NoWarn>
<AnalysisMode>Recommended</AnalysisMode>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>

View File

@@ -119,6 +119,12 @@ public sealed class BotCredsProvider : IBotCredsProvider
var jsonCredentialsFileText = File.ReadAllText(OldCredsJsonPath); var jsonCredentialsFileText = File.ReadAllText(OldCredsJsonPath);
var oldCreds = JsonConvert.DeserializeObject<Creds.Old>(jsonCredentialsFileText); 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 var creds = new Creds
{ {
Version = 1, Version = 1,