#nullable disable using NadekoBot.Common.ModuleBehaviors; using NadekoBot.Db; using NadekoBot.Modules.Utility.Common; using NadekoBot.Modules.Utility.Common.Exceptions; using NadekoBot.Services.Database.Models; using System.Diagnostics; using System.Net; namespace NadekoBot.Modules.Utility.Services; public class StreamRoleService : IReadyExecutor, INService { private readonly DbService _db; private readonly DiscordSocketClient _client; private readonly ConcurrentDictionary _guildSettings; private readonly QueueRunner _queueRunner; public StreamRoleService(DiscordSocketClient client, DbService db, Bot bot) { _db = db; _client = client; _guildSettings = bot.AllGuildConfigs.ToDictionary(x => x.GuildId, x => x.StreamRole) .Where(x => x.Value is { Enabled: true }) .ToConcurrent(); _client.PresenceUpdated += OnPresenceUpdate; _queueRunner = new QueueRunner(); } private Task OnPresenceUpdate(SocketUser user, SocketPresence oldPresence, SocketPresence newPresence) { _ = Task.Run(async () => { if (oldPresence.Activities.Count != newPresence.Activities.Count) { var guildUsers = _client.Guilds .Select(x => x.GetUser(user.Id)); foreach (var guildUser in guildUsers) { if (_guildSettings.TryGetValue(guildUser.Guild.Id, out var s)) await RescanUser(guildUser, s); } } }); return Task.CompletedTask; } public Task OnReadyAsync() => Task.WhenAll(_client.Guilds.Select(RescanUsers).WhenAll(), _queueRunner.RunAsync()); /// /// Adds or removes a user from a blacklist or a whitelist in the specified guild. /// /// List type /// Guild /// Add or rem action /// User's Id /// User's name#discrim /// Whether the operation was successful public async Task ApplyListAction( StreamRoleListType listType, IGuild guild, AddRemove action, ulong userId, string userName) { ArgumentNullException.ThrowIfNull(userName, nameof(userName)); var success = false; await using (var uow = _db.GetDbContext()) { var streamRoleSettings = uow.GetStreamRoleSettings(guild.Id); if (listType == StreamRoleListType.Whitelist) { var userObj = new StreamRoleWhitelistedUser { UserId = userId, Username = userName }; if (action == AddRemove.Rem) { var toDelete = streamRoleSettings.Whitelist.FirstOrDefault(x => x.Equals(userObj)); if (toDelete is not null) { uow.Remove(toDelete); success = true; } } else success = streamRoleSettings.Whitelist.Add(userObj); } else { var userObj = new StreamRoleBlacklistedUser { UserId = userId, Username = userName }; if (action == AddRemove.Rem) { var toRemove = streamRoleSettings.Blacklist.FirstOrDefault(x => x.Equals(userObj)); if (toRemove is not null) success = streamRoleSettings.Blacklist.Remove(toRemove); } else success = streamRoleSettings.Blacklist.Add(userObj); } await uow.SaveChangesAsync(); UpdateCache(guild.Id, streamRoleSettings); } if (success) await RescanUsers(guild); return success; } /// /// Sets keyword on a guild and updates the cache. /// /// Guild Id /// Keyword to set /// The keyword set public async Task SetKeyword(IGuild guild, string keyword) { keyword = keyword?.Trim().ToLowerInvariant(); await using (var uow = _db.GetDbContext()) { var streamRoleSettings = uow.GetStreamRoleSettings(guild.Id); streamRoleSettings.Keyword = keyword; UpdateCache(guild.Id, streamRoleSettings); await uow.SaveChangesAsync(); } await RescanUsers(guild); return keyword; } /// /// Gets the currently set keyword on a guild. /// /// Guild Id /// The keyword set public string GetKeyword(ulong guildId) { if (_guildSettings.TryGetValue(guildId, out var outSetting)) return outSetting.Keyword; StreamRoleSettings setting; using (var uow = _db.GetDbContext()) { setting = uow.GetStreamRoleSettings(guildId); } UpdateCache(guildId, setting); return setting.Keyword; } /// /// Sets the role to monitor, and a role to which to add to /// the user who starts streaming in the monitored role. /// /// Role to monitor /// Role to add to the user public async Task SetStreamRole(IRole fromRole, IRole addRole) { ArgumentNullException.ThrowIfNull(fromRole, nameof(fromRole)); ArgumentNullException.ThrowIfNull(addRole, nameof(addRole)); StreamRoleSettings setting; await using (var uow = _db.GetDbContext()) { var streamRoleSettings = uow.GetStreamRoleSettings(fromRole.Guild.Id); streamRoleSettings.Enabled = true; streamRoleSettings.AddRoleId = addRole.Id; streamRoleSettings.FromRoleId = fromRole.Id; setting = streamRoleSettings; await uow.SaveChangesAsync(); } UpdateCache(fromRole.Guild.Id, setting); foreach (var usr in await fromRole.GetMembersAsync()) { await RescanUser(usr, setting, addRole); } } /// /// Stops the stream role feature on the specified guild. /// /// Guild /// Whether to rescan users public async Task StopStreamRole(IGuild guild, bool cleanup = false) { await using (var uow = _db.GetDbContext()) { var streamRoleSettings = uow.GetStreamRoleSettings(guild.Id); streamRoleSettings.Enabled = false; streamRoleSettings.AddRoleId = 0; streamRoleSettings.FromRoleId = 0; await uow.SaveChangesAsync(); } if (_guildSettings.TryRemove(guild.Id, out _) && cleanup) await RescanUsers(guild); } private async ValueTask RescanUser(IGuildUser user, StreamRoleSettings setting, IRole addRole = null) => await _queueRunner.EnqueueAsync(() => RescanUserInternal(user, setting, addRole)); private async Task RescanUserInternal(IGuildUser user, StreamRoleSettings setting, IRole addRole = null) { if (user.IsBot) return; var g = (StreamingGame)user.Activities.FirstOrDefault(a => a is StreamingGame && (string.IsNullOrWhiteSpace(setting.Keyword) || a.Name.ToUpperInvariant().Contains(setting.Keyword.ToUpperInvariant()) || setting.Whitelist.Any(x => x.UserId == user.Id))); if (g is not null && setting.Enabled && setting.Blacklist.All(x => x.UserId != user.Id) && user.RoleIds.Contains(setting.FromRoleId)) { await _queueRunner.EnqueueAsync(async () => { try { addRole ??= user.Guild.GetRole(setting.AddRoleId); if (addRole is null) { await StopStreamRole(user.Guild); Log.Warning("Stream role in server {RoleId} no longer exists. Stopping", setting.AddRoleId); return; } //check if he doesn't have addrole already, to avoid errors if (!user.RoleIds.Contains(addRole.Id)) { await user.AddRoleAsync(addRole); Log.Information("Added stream role to user {User} in {Server} server", user.ToString(), user.Guild.ToString()); } } catch (HttpException ex) when (ex.HttpCode == HttpStatusCode.Forbidden) { await StopStreamRole(user.Guild); Log.Warning(ex, "Error adding stream role(s). Forcibly disabling stream role feature"); throw new StreamRolePermissionException(); } catch (Exception ex) { Log.Warning(ex, "Failed adding stream role"); } }); } else { //check if user is in the addrole if (user.RoleIds.Contains(setting.AddRoleId)) { await _queueRunner.EnqueueAsync(async () => { try { addRole ??= user.Guild.GetRole(setting.AddRoleId); if (addRole is null) { await StopStreamRole(user.Guild); Log.Warning( "Addrole doesn't exist in {GuildId} server. Forcibly disabling stream role feature", user.Guild.Id); return; } // need to check again in case queuer is taking too long to execute if (user.RoleIds.Contains(setting.AddRoleId)) { await user.RemoveRoleAsync(addRole); } Log.Information("Removed stream role from the user {User} in {Server} server", user.ToString(), user.Guild.ToString()); } catch (HttpException ex) { if (ex.HttpCode == HttpStatusCode.Forbidden) { await StopStreamRole(user.Guild); Log.Warning(ex, "Error removing stream role(s). Forcibly disabling stream role feature"); } } }); } } } private async Task RescanUsers(IGuild guild) { if (!_guildSettings.TryGetValue(guild.Id, out var setting)) return; var addRole = guild.GetRole(setting.AddRoleId); if (addRole is null) return; if (setting.Enabled) { var users = await guild.GetUsersAsync(CacheMode.CacheOnly); foreach (var usr in users.Where(x => x.RoleIds.Contains(setting.FromRoleId) || x.RoleIds.Contains(addRole.Id))) { if (usr is { } x) await RescanUser(x, setting, addRole); } } } private void UpdateCache(ulong guildId, StreamRoleSettings setting) => _guildSettings.AddOrUpdate(guildId, _ => setting, (_, _) => setting); }