#nullable disable using Microsoft.EntityFrameworkCore; using EllieBot.Db.Models; namespace EllieBot.Modules.Administration.Services; public enum MuteType { Voice, Chat, All } public class MuteService : IEService { public enum TimerType { Mute, Ban, AddRole } private static readonly OverwritePermissions _denyOverwrite = new(addReactions: PermValue.Deny, sendMessages: PermValue.Deny, sendMessagesInThreads: PermValue.Deny, attachFiles: PermValue.Deny); public event Action<IGuildUser, IUser, MuteType, string> UserMuted = delegate { }; public event Action<IGuildUser, IUser, MuteType, string> UserUnmuted = delegate { }; public ConcurrentDictionary<ulong, string> GuildMuteRoles { get; } public ConcurrentDictionary<ulong, ConcurrentHashSet<ulong>> MutedUsers { get; } public ConcurrentDictionary<ulong, ConcurrentDictionary<(ulong, TimerType), Timer>> UnTimers { get; } = new(); private readonly DiscordSocketClient _client; private readonly DbService _db; private readonly IMessageSenderService _sender; public MuteService(DiscordSocketClient client, DbService db, IMessageSenderService sender) { _client = client; _db = db; _sender = sender; using (var uow = db.GetDbContext()) { var guildIds = client.Guilds.Select(x => x.Id).ToList(); var configs = uow.Set<GuildConfig>() .AsNoTracking() .AsSplitQuery() .Include(x => x.MutedUsers) .Include(x => x.UnbanTimer) .Include(x => x.UnmuteTimers) .Include(x => x.UnroleTimer) .Where(x => guildIds.Contains(x.GuildId)) .ToList(); GuildMuteRoles = configs.Where(c => !string.IsNullOrWhiteSpace(c.MuteRoleName)) .ToDictionary(c => c.GuildId, c => c.MuteRoleName) .ToConcurrent(); MutedUsers = new(configs.ToDictionary(k => k.GuildId, v => new ConcurrentHashSet<ulong>(v.MutedUsers.Select(m => m.UserId)))); var max = TimeSpan.FromDays(49); foreach (var conf in configs) { foreach (var x in conf.UnmuteTimers) { TimeSpan after; if (x.UnmuteAt - TimeSpan.FromMinutes(2) <= DateTime.UtcNow) after = TimeSpan.FromMinutes(2); else { var unmute = x.UnmuteAt - DateTime.UtcNow; after = unmute > max ? max : unmute; } StartUn_Timer(conf.GuildId, x.UserId, after, TimerType.Mute); } foreach (var x in conf.UnbanTimer) { TimeSpan after; if (x.UnbanAt - TimeSpan.FromMinutes(2) <= DateTime.UtcNow) after = TimeSpan.FromMinutes(2); else { var unban = x.UnbanAt - DateTime.UtcNow; after = unban > max ? max : unban; } StartUn_Timer(conf.GuildId, x.UserId, after, TimerType.Ban); } foreach (var x in conf.UnroleTimer) { TimeSpan after; if (x.UnbanAt - TimeSpan.FromMinutes(2) <= DateTime.UtcNow) after = TimeSpan.FromMinutes(2); else { var unban = x.UnbanAt - DateTime.UtcNow; after = unban > max ? max : unban; } StartUn_Timer(conf.GuildId, x.UserId, after, TimerType.AddRole, x.RoleId); } } _client.UserJoined += Client_UserJoined; } UserMuted += OnUserMuted; UserUnmuted += OnUserUnmuted; } private void OnUserMuted( IGuildUser user, IUser mod, MuteType type, string reason) { if (string.IsNullOrWhiteSpace(reason)) return; _ = Task.Run(() => _sender.Response(user) .Embed(_sender.CreateEmbed(user?.GuildId) .WithDescription($"You've been muted in {user.Guild} server") .AddField("Mute Type", type.ToString()) .AddField("Moderator", mod.ToString()) .AddField("Reason", reason)) .SendAsync()); } private void OnUserUnmuted( IGuildUser user, IUser mod, MuteType type, string reason) { if (string.IsNullOrWhiteSpace(reason)) return; _ = Task.Run(() => _sender.Response(user) .Embed(_sender.CreateEmbed(user.GuildId) .WithDescription($"You've been unmuted in {user.Guild} server") .AddField("Unmute Type", type.ToString()) .AddField("Moderator", mod.ToString()) .AddField("Reason", reason)) .SendAsync()); } private Task Client_UserJoined(IGuildUser usr) { try { MutedUsers.TryGetValue(usr.Guild.Id, out var muted); if (muted is null || !muted.Contains(usr.Id)) return Task.CompletedTask; _ = Task.Run(() => MuteUser(usr, _client.CurrentUser, reason: "Sticky mute")); } catch (Exception ex) { Log.Warning(ex, "Error in MuteService UserJoined event"); } return Task.CompletedTask; } public async Task SetMuteRoleAsync(ulong guildId, string name) { await using var uow = _db.GetDbContext(); var config = uow.GuildConfigsForId(guildId, set => set); config.MuteRoleName = name; GuildMuteRoles.AddOrUpdate(guildId, name, (_, _) => name); await uow.SaveChangesAsync(); } public async Task MuteUser( IGuildUser usr, IUser mod, MuteType type = MuteType.All, string reason = "") { if (type == MuteType.All) { try { await usr.ModifyAsync(x => x.Mute = true); } catch { } var muteRole = await GetMuteRole(usr.Guild); if (!usr.RoleIds.Contains(muteRole.Id)) await usr.AddRoleAsync(muteRole); StopTimer(usr.GuildId, usr.Id, TimerType.Mute); await using (var uow = _db.GetDbContext()) { var config = uow.GuildConfigsForId(usr.Guild.Id, set => set.Include(gc => gc.MutedUsers).Include(gc => gc.UnmuteTimers)); config.MutedUsers.Add(new() { UserId = usr.Id }); if (MutedUsers.TryGetValue(usr.Guild.Id, out var muted)) muted.Add(usr.Id); config.UnmuteTimers.RemoveWhere(x => x.UserId == usr.Id); await uow.SaveChangesAsync(); } UserMuted(usr, mod, MuteType.All, reason); } else if (type == MuteType.Voice) { try { await usr.ModifyAsync(x => x.Mute = true); UserMuted(usr, mod, MuteType.Voice, reason); } catch { } } else if (type == MuteType.Chat) { await usr.AddRoleAsync(await GetMuteRole(usr.Guild)); UserMuted(usr, mod, MuteType.Chat, reason); } } public async Task UnmuteUser( ulong guildId, ulong usrId, IUser mod, MuteType type = MuteType.All, string reason = "") { var usr = _client.GetGuild(guildId)?.GetUser(usrId); if (type == MuteType.All) { StopTimer(guildId, usrId, TimerType.Mute); await using (var uow = _db.GetDbContext()) { var config = uow.GuildConfigsForId(guildId, set => set.Include(gc => gc.MutedUsers).Include(gc => gc.UnmuteTimers)); var match = new MutedUserId { UserId = usrId }; var toRemove = config.MutedUsers.FirstOrDefault(x => x.Equals(match)); if (toRemove is not null) uow.Remove(toRemove); if (MutedUsers.TryGetValue(guildId, out var muted)) muted.TryRemove(usrId); config.UnmuteTimers.RemoveWhere(x => x.UserId == usrId); await uow.SaveChangesAsync(); } if (usr is not null) { try { await usr.ModifyAsync(x => x.Mute = false); } catch { } try { await usr.RemoveRoleAsync(await GetMuteRole(usr.Guild)); } catch { /*ignore*/ } UserUnmuted(usr, mod, MuteType.All, reason); } } else if (type == MuteType.Voice) { if (usr is null) return; try { await usr.ModifyAsync(x => x.Mute = false); UserUnmuted(usr, mod, MuteType.Voice, reason); } catch { } } else if (type == MuteType.Chat) { if (usr is null) return; await usr.RemoveRoleAsync(await GetMuteRole(usr.Guild)); UserUnmuted(usr, mod, MuteType.Chat, reason); } } public async Task<IRole> GetMuteRole(IGuild guild) { ArgumentNullException.ThrowIfNull(guild); const string defaultMuteRoleName = "ellie-mute"; var muteRoleName = GuildMuteRoles.GetOrAdd(guild.Id, defaultMuteRoleName); var muteRole = guild.Roles.FirstOrDefault(r => r.Name == muteRoleName); if (muteRole is null) //if it doesn't exist, create it { try { muteRole = await guild.CreateRoleAsync(muteRoleName, isMentionable: false); } catch { //if creations fails, maybe the name is not correct, find default one, if doesn't work, create default one muteRole = guild.Roles.FirstOrDefault(r => r.Name == muteRoleName) ?? await guild.CreateRoleAsync(defaultMuteRoleName, isMentionable: false); } } foreach (var toOverwrite in await guild.GetTextChannelsAsync()) { try { if (!toOverwrite.PermissionOverwrites.Any(x => x.TargetId == muteRole.Id && x.TargetType == PermissionTarget.Role)) { await toOverwrite.AddPermissionOverwriteAsync(muteRole, _denyOverwrite); await Task.Delay(200); } } catch { // ignored } } return muteRole; } public async Task TimedMute( IGuildUser user, IUser mod, TimeSpan after, MuteType muteType = MuteType.All, string reason = "") { await MuteUser(user, mod, muteType, reason); // mute the user. This will also remove any previous unmute timers await using (var uow = _db.GetDbContext()) { var config = uow.GuildConfigsForId(user.GuildId, set => set.Include(x => x.UnmuteTimers)); config.UnmuteTimers.Add(new() { UserId = user.Id, UnmuteAt = DateTime.UtcNow + after }); // add teh unmute timer to the database uow.SaveChanges(); } StartUn_Timer(user.GuildId, user.Id, after, TimerType.Mute); // start the timer } public async Task TimedBan( IGuild guild, ulong userId, TimeSpan after, string reason, int pruneDays) { await guild.AddBanAsync(userId, pruneDays, reason); await using (var uow = _db.GetDbContext()) { var config = uow.GuildConfigsForId(guild.Id, set => set.Include(x => x.UnbanTimer)); config.UnbanTimer.Add(new() { UserId = userId, UnbanAt = DateTime.UtcNow + after }); // add teh unmute timer to the database await uow.SaveChangesAsync(); } StartUn_Timer(guild.Id, userId, after, TimerType.Ban); // start the timer } public async Task TimedRole( IGuildUser user, TimeSpan after, string reason, IRole role) { await user.AddRoleAsync(role); await using (var uow = _db.GetDbContext()) { var config = uow.GuildConfigsForId(user.GuildId, set => set.Include(x => x.UnroleTimer)); config.UnroleTimer.Add(new() { UserId = user.Id, UnbanAt = DateTime.UtcNow + after, RoleId = role.Id }); // add teh unmute timer to the database uow.SaveChanges(); } StartUn_Timer(user.GuildId, user.Id, after, TimerType.AddRole, role.Id); // start the timer } public void StartUn_Timer( ulong guildId, ulong userId, TimeSpan after, TimerType type, ulong? roleId = null) { //load the unmute timers for this guild var userUnTimers = UnTimers.GetOrAdd(guildId, new ConcurrentDictionary<(ulong, TimerType), Timer>()); //unmute timer to be added var toAdd = new Timer(async _ => { if (type == TimerType.Ban) { try { RemoveTimerFromDb(guildId, userId, type); StopTimer(guildId, userId, type); var guild = _client.GetGuild(guildId); // load the guild if (guild is not null) await guild.RemoveBanAsync(userId); } catch (Exception ex) { Log.Warning(ex, "Couldn't unban user {UserId} in guild {GuildId}", userId, guildId); } } else if (type == TimerType.AddRole) { try { if (roleId is null) return; RemoveTimerFromDb(guildId, userId, type); StopTimer(guildId, userId, type); var guild = _client.GetGuild(guildId); var user = guild?.GetUser(userId); var role = guild?.GetRole(roleId.Value); if (guild is not null && user is not null && user.Roles.Contains(role)) await user.RemoveRoleAsync(role); } catch (Exception ex) { Log.Warning(ex, "Couldn't remove role from user {UserId} in guild {GuildId}", userId, guildId); } } else { try { // unmute the user, this will also remove the timer from the db await UnmuteUser(guildId, userId, _client.CurrentUser, reason: "Timed mute expired"); } catch (Exception ex) { RemoveTimerFromDb(guildId, userId, type); // if unmute errored, just remove unmute from db Log.Warning(ex, "Couldn't unmute user {UserId} in guild {GuildId}", userId, guildId); } } }, null, after, Timeout.InfiniteTimeSpan); //add it, or stop the old one and add this one userUnTimers.AddOrUpdate((userId, type), _ => toAdd, (_, old) => { old.Change(Timeout.Infinite, Timeout.Infinite); return toAdd; }); } public void StopTimer(ulong guildId, ulong userId, TimerType type) { if (!UnTimers.TryGetValue(guildId, out var userTimer)) return; if (userTimer.TryRemove((userId, type), out var removed)) removed.Change(Timeout.Infinite, Timeout.Infinite); } private void RemoveTimerFromDb(ulong guildId, ulong userId, TimerType type) { using var uow = _db.GetDbContext(); object toDelete; if (type == TimerType.Mute) { var config = uow.GuildConfigsForId(guildId, set => set.Include(x => x.UnmuteTimers)); toDelete = config.UnmuteTimers.FirstOrDefault(x => x.UserId == userId); } else { var config = uow.GuildConfigsForId(guildId, set => set.Include(x => x.UnbanTimer)); toDelete = config.UnbanTimer.FirstOrDefault(x => x.UserId == userId); } if (toDelete is not null) uow.Remove(toDelete); uow.SaveChanges(); } }