mirror of
				https://gitlab.com/Kwoth/nadekobot.git
				synced 2025-11-04 00:34:26 -05: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();
 | 
			
		||||
 
 | 
			
		||||
@@ -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")]
 | 
			
		||||
    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)
 | 
			
		||||
@@ -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,12 +314,10 @@ public partial class Administration : NadekoModule<AdministrationService>
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (time is null)
 | 
			
		||||
        {
 | 
			
		||||
            await msg.DeleteAsync();
 | 
			
		||||
        }
 | 
			
		||||
        else if (time.Time <= TimeSpan.FromDays(7))
 | 
			
		||||
        {
 | 
			
		||||
            _= Task.Run(async () =>
 | 
			
		||||
            _ = Task.Run(async () =>
 | 
			
		||||
            {
 | 
			
		||||
                await Task.Delay(time.Time);
 | 
			
		||||
                await msg.DeleteAsync();
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
                                                               .FailingStreams
 | 
			
		||||
                                                               .Where(fs => DateTime.UtcNow - fs.ErroringSince
 | 
			
		||||
                                                                            > duration)
 | 
			
		||||
                                                               .Select(fs => new StreamDataKey(prov.Value.Platform,
 | 
			
		||||
                                                                   fs.Item1)))
 | 
			
		||||
                                       .ToList();
 | 
			
		||||
        var toReturn = _streamProviders
 | 
			
		||||
                       .SelectMany(prov => prov.Value
 | 
			
		||||
                                               .FailingStreams
 | 
			
		||||
                                               .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,19 +71,21 @@ public class NotifChecker
 | 
			
		||||
                                                entry => entry.AsEnumerable()
 | 
			
		||||
                                                              .ToDictionary(x => x.Key.Name, x => x.Value));
 | 
			
		||||
 | 
			
		||||
                    var newStreamData = await oldStreamDataDict.Select(x =>
 | 
			
		||||
                                                               {
 | 
			
		||||
                                                                   // get all stream data for the streams of this type
 | 
			
		||||
                                                                   if (_streamProviders.TryGetValue(x.Key,
 | 
			
		||||
                                                                           out var provider))
 | 
			
		||||
                                                                       return provider.GetStreamDataAsync(x.Value
 | 
			
		||||
                                                                           .Select(entry => entry.Key)
 | 
			
		||||
                                                                           .ToList());
 | 
			
		||||
                    var newStreamData = await oldStreamDataDict
 | 
			
		||||
                                              .Select(x =>
 | 
			
		||||
                                              {
 | 
			
		||||
                                                  // get all stream data for the streams of this type
 | 
			
		||||
                                                  if (_streamProviders.TryGetValue(x.Key,
 | 
			
		||||
                                                          out var provider))
 | 
			
		||||
                                                      return provider.GetStreamDataAsync(x.Value
 | 
			
		||||
                                                          .Select(entry => entry.Key)
 | 
			
		||||
                                                          .ToList());
 | 
			
		||||
 | 
			
		||||
                                                                   // this means there's no provider for this stream data, (and there was before?)
 | 
			
		||||
                                                                   return Task.FromResult(new List<StreamData>());
 | 
			
		||||
                                                               })
 | 
			
		||||
                                                               .WhenAll();
 | 
			
		||||
                                                  // this means there's no provider for this stream data, (and there was before?)
 | 
			
		||||
                                                  return Task.FromResult<IReadOnlyCollection<StreamData>>(
 | 
			
		||||
                                                      new List<StreamData>());
 | 
			
		||||
                                              })
 | 
			
		||||
                                              .WhenAll();
 | 
			
		||||
 | 
			
		||||
                    var newlyOnline = 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));
 | 
			
		||||
 | 
			
		||||
@@ -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