From dbc312dd9dd138633bc534185bfd34a191edbbb3 Mon Sep 17 00:00:00 2001
From: Toastie <toastie@toastiet0ast.com>
Date: Thu, 6 Feb 2025 12:54:30 +1300
Subject: [PATCH] wip reimplementation of the voicexp

---
 src/EllieBot/EllieBot.csproj         |   2 +-
 src/EllieBot/Modules/Xp/XpService.cs | 190 +++++++++------------------
 2 files changed, 61 insertions(+), 131 deletions(-)

diff --git a/src/EllieBot/EllieBot.csproj b/src/EllieBot/EllieBot.csproj
index 72b6799..35d0275 100644
--- a/src/EllieBot/EllieBot.csproj
+++ b/src/EllieBot/EllieBot.csproj
@@ -154,7 +154,7 @@
     <PropertyGroup Condition=" '$(Configuration)' == 'GlobalEllie' ">
         <!-- Define trace doesn't seem to affect the build at all so I had to remove $(DefineConstants)-->
         <DefineTrace>false</DefineTrace>
-        <DefineConstants>GLOBAL_NADEKO</DefineConstants>
+        <DefineConstants>GLOBAL_ELLIE</DefineConstants>
         <NoWarn>$(NoWarn);CS1573;CS1591</NoWarn>
         <Optimize>true</Optimize>
         <DebugType>portable</DebugType>
diff --git a/src/EllieBot/Modules/Xp/XpService.cs b/src/EllieBot/Modules/Xp/XpService.cs
index 109d9b0..54884c3 100644
--- a/src/EllieBot/Modules/Xp/XpService.cs
+++ b/src/EllieBot/Modules/Xp/XpService.cs
@@ -140,6 +140,22 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand
 
         return;
 
+        async Task VoiceUpdateTimer()
+        {
+            using var timer = new PeriodicTimer(TimeSpan.FromMinutes(1));
+            while (await timer.WaitForNextTickAsync())
+            {
+                try
+                {
+                    await UpdateVoiceXp();
+                }
+                catch (Exception ex)
+                {
+                    Log.Error(ex, "Error updating voice xp");
+                }
+            }
+        }
+
         async Task UpdateTimer()
         {
             // todo a bigger loop that runs once every XpTimer
@@ -164,17 +180,46 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand
     /// </summary>
     private readonly ConcurrentHashSet<IGuildUser> _usersBatch = new();
 
+    private readonly ConcurrentHashSet<IGuildUser> _voiceXPBatch = new();
+
+    private async Task UpdateVoiceXp()
+    {
+        var xpAmount = (int)_xpConfig.Data.VoiceXpPerMinute;
+        var oldBatch = _voiceXPBatch.ToArray();
+        _voiceXPBatch.Clear();
+        var validUsers = new HashSet<IGuildUser>();
+
+        var guilds = _client.Guilds;
+
+        foreach (var g in guilds)
+            foreach (var vc in g.VoiceChannels)
+                foreach (var u in vc.ConnectedUsers)
+                    if (!u.IsMuted && !u.IsDeafened
+                                   && vc.ConnectedUsers.Count(x => !x.IsBot) > 1)
+                    {
+                        if (oldBatch.Contains(u))
+                            validUsers.Add(u);
+
+                        _voiceXPBatch.Add(u);
+                    }
+
+        await UpdateXpInternalAsync(validUsers.DistinctBy(x => x.Id).ToArray(), xpAmount);
+    }
+
     private async Task UpdateXp()
     {
         var xpAmount = _xpConfig.Data.XpPerMessage;
         var currentBatch = _usersBatch.ToArray();
         _usersBatch.Clear();
 
+        await UpdateXpInternalAsync(currentBatch, xpAmount);
+    }
+
+    private async Task UpdateXpInternalAsync(IGuildUser[] currentBatch, int xpAmount)
+    {
         if (currentBatch.Length == 0)
             return;
 
-        var ids = currentBatch.Select(x => x.Id).ToArray();
-
         await using var ctx = _db.GetDbContext();
         await using var lctx = ctx.CreateLinqToDBConnection();
 
@@ -492,7 +537,7 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand
             });
         }
 
-        uow.SaveChanges();
+        await uow.SaveChangesAsync();
     }
 
     public async Task<IReadOnlyCollection<UserXpStats>> GetGuildUserXps(ulong guildId, int page)
@@ -552,29 +597,16 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand
         return Task.CompletedTask;
     }
 
-    private Task Client_OnUserVoiceStateUpdated(SocketUser socketUser, SocketVoiceState before, SocketVoiceState after)
+    private async Task Client_OnUserVoiceStateUpdated(SocketUser socketUser, SocketVoiceState before,
+        SocketVoiceState after)
     {
         if (socketUser is not SocketGuildUser user || user.IsBot)
-            return Task.CompletedTask;
+            return;
 
-        _ = Task.Run(async () =>
+        if (after.VoiceChannel is not null)
         {
-            if (before.VoiceChannel is not null)
-                await ScanChannelForVoiceXp(before.VoiceChannel);
-
-            if (after.VoiceChannel is not null && after.VoiceChannel != before.VoiceChannel)
-            {
-                await ScanChannelForVoiceXp(after.VoiceChannel);
-            }
-            else if (after.VoiceChannel is null && before.VoiceChannel is not null)
-            {
-                // In this case, the user left the channel and the previous for loops didn't catch
-                // it because it wasn't in any new channel. So we need to get rid of it.
-                await UserLeftVoiceChannel(user, before.VoiceChannel);
-            }
-        });
-
-        return Task.CompletedTask;
+            await ScanChannelForVoiceXp(after.VoiceChannel);
+        }
     }
 
     private async Task ScanChannelForVoiceXp(SocketVoiceChannel channel)
@@ -582,27 +614,13 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand
         if (ShouldTrackVoiceChannel(channel))
         {
             foreach (var user in channel.ConnectedUsers)
-                await ScanUserForVoiceXp(user, channel);
-        }
-        else
-        {
-            foreach (var user in channel.ConnectedUsers)
-                await UserLeftVoiceChannel(user, channel);
+            {
+                if (UserParticipatingInVoiceChannel(user) && ShouldTrackXp(user, channel))
+                    await UserJoinedVoiceChannel(user);
+            }
         }
     }
 
-    /// <summary>
-    ///     Assumes that the channel itself is valid and adding xp.
-    /// </summary>
-    /// <param name="user"></param>
-    /// <param name="channel"></param>
-    private async Task ScanUserForVoiceXp(SocketGuildUser user, SocketVoiceChannel channel)
-    {
-        if (UserParticipatingInVoiceChannel(user) && ShouldTrackXp(user, channel))
-            await UserJoinedVoiceChannel(user);
-        else
-            await UserLeftVoiceChannel(user, channel);
-    }
 
     private bool ShouldTrackVoiceChannel(SocketVoiceChannel channel)
         => channel.ConnectedUsers.Where(UserParticipatingInVoiceChannel).Take(2).Count() >= 2;
@@ -619,84 +637,10 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand
 
         await _c.AddAsync(GetVoiceXpKey(user.Id),
             value,
-            TimeSpan.FromMinutes(_xpConfig.Data.VoiceMaxMinutes),
-            overwrite: false);
+            TimeSpan.FromMinutes(1),
+            overwrite: true);
     }
 
-    // private void UserJoinedVoiceChannel(SocketGuildUser user)
-    // {
-    //     var key = $"{_creds.RedisKey()}_user_xp_vc_join_{user.Id}";
-    //     var value = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
-    //
-    //     _cache.Redis.GetDatabase()
-    //         .StringSet(key,
-    //             value,
-    //             TimeSpan.FromMinutes(_xpConfig.Data.VoiceMaxMinutes),
-    //             when: When.NotExists);
-    // }
-
-    private async Task UserLeftVoiceChannel(SocketGuildUser user, SocketVoiceChannel channel)
-    {
-        var key = GetVoiceXpKey(user.Id);
-        var result = await _c.GetAsync(key);
-        if (!await _c.RemoveAsync(key))
-            return;
-
-        // Allow for if this function gets called multiple times when a user leaves a channel.
-        if (!result.TryGetValue(out var unixTime))
-            return;
-
-        var dateStart = DateTimeOffset.FromUnixTimeSeconds(unixTime);
-        var dateEnd = DateTimeOffset.UtcNow;
-        var minutes = (dateEnd - dateStart).TotalMinutes;
-        var xp = _xpConfig.Data.VoiceXpPerMinute * minutes;
-        var actualXp = (int)Math.Floor(xp);
-
-        if (actualXp > 0)
-        {
-            Log.Information("Adding {Amount} voice xp to {User}", actualXp, user.ToString());
-            await _xpGainQueue.Writer.WriteAsync(new()
-            {
-                Guild = channel.Guild,
-                User = user,
-                XpAmount = actualXp,
-                Channel = channel
-            });
-        }
-    }
-
-    /*
-     * private void UserLeftVoiceChannel(SocketGuildUser user, SocketVoiceChannel channel)
-    {
-        var key = $"{_creds.RedisKey()}_user_xp_vc_join_{user.Id}";
-        var value = _cache.Redis.GetDatabase().StringGet(key);
-        _cache.Redis.GetDatabase().KeyDelete(key);
-
-        // Allow for if this function gets called multiple times when a user leaves a channel.
-        if (value.IsNull)
-            return;
-
-        if (!value.TryParse(out long startUnixTime))
-            return;
-
-        var dateStart = DateTimeOffset.FromUnixTimeSeconds(startUnixTime);
-        var dateEnd = DateTimeOffset.UtcNow;
-        var minutes = (dateEnd - dateStart).TotalMinutes;
-        var xp = _xpConfig.Data.VoiceXpPerMinute * minutes;
-        var actualXp = (int)Math.Floor(xp);
-
-        if (actualXp > 0)
-        {
-            _addMessageXp.Enqueue(new()
-            {
-                Guild = channel.Guild,
-                User = user,
-                XpAmount = actualXp
-            });
-        }
-    }
-     */
-
     private bool ShouldTrackXp(SocketGuildUser user, IMessageChannel channel)
     {
         var channelId = channel.Id;
@@ -744,20 +688,6 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand
         return Task.CompletedTask;
     }
 
-    // public void AddXpDirectly(IGuildUser user, IMessageChannel channel, int amount)
-    // {
-    //     if (amount <= 0)
-    //         throw new ArgumentOutOfRangeException(nameof(amount));
-    //
-    //     _xpGainQueue.Writer.WriteAsync(new()
-    //     {
-    //         Guild = user.Guild,
-    //         Channel = channel,
-    //         User = user,
-    //         XpAmount = amount
-    //     });
-    // }
-
     public async Task<int> AddXpToUsersAsync(ulong guildId, long amount, params ulong[] userIds)
     {
         await using var ctx = _db.GetDbContext();