Added Ellie.Bot.Modules.Administration

Signed-off-by: Emotion <emotion@emotionchild.com>
This commit is contained in:
Toastie 2023-08-16 01:25:10 +12:00 committed by Emotion
parent f7467ae23f
commit 99f97f63a7
No known key found for this signature in database
GPG key ID: D7D3E4C27A98C37B
47 changed files with 10754 additions and 1 deletions

View file

@ -0,0 +1,405 @@
#nullable disable
using Ellie.Common.TypeReaders.Models;
using Ellie.Modules.Administration.Services;
namespace Ellie.Modules.Administration;
public partial class Administration : EllieModule<AdministrationService>
{
public enum Channel
{
Channel,
Ch,
Chnl,
Chan
}
public enum List
{
List = 0,
Ls = 0
}
public enum Server
{
Server
}
public enum State
{
Enable,
Disable,
Inherit
}
private readonly SomethingOnlyChannelService _somethingOnly;
private readonly AutoPublishService _autoPubService;
public Administration(SomethingOnlyChannelService somethingOnly, AutoPublishService autoPubService)
{
_somethingOnly = somethingOnly;
_autoPubService = autoPubService;
}
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.Administrator)]
[BotPerm(GuildPerm.ManageGuild)]
public async Task ImageOnlyChannel(StoopidTime time = null)
{
var newValue = await _somethingOnly.ToggleImageOnlyChannelAsync(ctx.Guild.Id, ctx.Channel.Id);
if (newValue)
await ReplyConfirmLocalizedAsync(strs.imageonly_enable);
else
await ReplyPendingLocalizedAsync(strs.imageonly_disable);
}
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.Administrator)]
[BotPerm(GuildPerm.ManageGuild)]
public async Task LinkOnlyChannel(StoopidTime time = null)
{
var newValue = await _somethingOnly.ToggleLinkOnlyChannelAsync(ctx.Guild.Id, ctx.Channel.Id);
if (newValue)
await ReplyConfirmLocalizedAsync(strs.linkonly_enable);
else
await ReplyPendingLocalizedAsync(strs.linkonly_disable);
}
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(ChannelPerm.ManageChannels)]
[BotPerm(ChannelPerm.ManageChannels)]
public async Task Slowmode(StoopidTime time = null)
{
var seconds = (int?)time?.Time.TotalSeconds ?? 0;
if (time is not null && (time.Time < TimeSpan.FromSeconds(0) || time.Time > TimeSpan.FromHours(6)))
return;
await ((ITextChannel)ctx.Channel).ModifyAsync(tcp =>
{
tcp.SlowModeInterval = seconds;
});
await ctx.OkAsync();
}
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.Administrator)]
[BotPerm(GuildPerm.ManageMessages)]
[Priority(2)]
public async Task Delmsgoncmd(List _)
{
var guild = (SocketGuild)ctx.Guild;
var (enabled, channels) = _service.GetDelMsgOnCmdData(ctx.Guild.Id);
var embed = _eb.Create()
.WithOkColor()
.WithTitle(GetText(strs.server_delmsgoncmd))
.WithDescription(enabled ? "✅" : "❌");
var str = string.Join("\n",
channels.Select(x =>
{
var ch = guild.GetChannel(x.ChannelId)?.ToString() ?? x.ChannelId.ToString();
var prefixSign = x.State ? "✅ " : "❌ ";
return prefixSign + ch;
}));
if (string.IsNullOrWhiteSpace(str))
str = "-";
embed.AddField(GetText(strs.channel_delmsgoncmd), str);
await ctx.Channel.EmbedAsync(embed);
}
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.Administrator)]
[BotPerm(GuildPerm.ManageMessages)]
[Priority(1)]
public async Task Delmsgoncmd(Server _ = Server.Server)
{
if (_service.ToggleDeleteMessageOnCommand(ctx.Guild.Id))
{
_service.DeleteMessagesOnCommand.Add(ctx.Guild.Id);
await ReplyConfirmLocalizedAsync(strs.delmsg_on);
}
else
{
_service.DeleteMessagesOnCommand.TryRemove(ctx.Guild.Id);
await ReplyConfirmLocalizedAsync(strs.delmsg_off);
}
}
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.Administrator)]
[BotPerm(GuildPerm.ManageMessages)]
[Priority(0)]
public Task Delmsgoncmd(Channel _, State s, ITextChannel ch)
=> Delmsgoncmd(_, s, ch.Id);
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.Administrator)]
[BotPerm(GuildPerm.ManageMessages)]
[Priority(1)]
public async Task Delmsgoncmd(Channel _, State s, ulong? chId = null)
{
var actualChId = chId ?? ctx.Channel.Id;
await _service.SetDelMsgOnCmdState(ctx.Guild.Id, actualChId, s);
if (s == State.Disable)
await ReplyConfirmLocalizedAsync(strs.delmsg_channel_off);
else if (s == State.Enable)
await ReplyConfirmLocalizedAsync(strs.delmsg_channel_on);
else
await ReplyConfirmLocalizedAsync(strs.delmsg_channel_inherit);
}
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.DeafenMembers)]
[BotPerm(GuildPerm.DeafenMembers)]
public async Task Deafen(params IGuildUser[] users)
{
await _service.DeafenUsers(true, users);
await ReplyConfirmLocalizedAsync(strs.deafen);
}
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.DeafenMembers)]
[BotPerm(GuildPerm.DeafenMembers)]
public async Task UnDeafen(params IGuildUser[] users)
{
await _service.DeafenUsers(false, users);
await ReplyConfirmLocalizedAsync(strs.undeafen);
}
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageChannels)]
[BotPerm(GuildPerm.ManageChannels)]
public async Task DelVoiChanl([Leftover] IVoiceChannel voiceChannel)
{
await voiceChannel.DeleteAsync();
await ReplyConfirmLocalizedAsync(strs.delvoich(Format.Bold(voiceChannel.Name)));
}
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageChannels)]
[BotPerm(GuildPerm.ManageChannels)]
public async Task CreatVoiChanl([Leftover] string channelName)
{
var ch = await ctx.Guild.CreateVoiceChannelAsync(channelName);
await ReplyConfirmLocalizedAsync(strs.createvoich(Format.Bold(ch.Name)));
}
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageChannels)]
[BotPerm(GuildPerm.ManageChannels)]
public async Task DelTxtChanl([Leftover] ITextChannel toDelete)
{
await toDelete.DeleteAsync();
await ReplyConfirmLocalizedAsync(strs.deltextchan(Format.Bold(toDelete.Name)));
}
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageChannels)]
[BotPerm(GuildPerm.ManageChannels)]
public async Task CreaTxtChanl([Leftover] string channelName)
{
var txtCh = await ctx.Guild.CreateTextChannelAsync(channelName);
await ReplyConfirmLocalizedAsync(strs.createtextchan(Format.Bold(txtCh.Name)));
}
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageChannels)]
[BotPerm(GuildPerm.ManageChannels)]
public async Task SetTopic([Leftover] string topic = null)
{
var channel = (ITextChannel)ctx.Channel;
topic ??= "";
await channel.ModifyAsync(c => c.Topic = topic);
await ReplyConfirmLocalizedAsync(strs.set_topic);
}
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageChannels)]
[BotPerm(GuildPerm.ManageChannels)]
public async Task SetChanlName([Leftover] string name)
{
var channel = (ITextChannel)ctx.Channel;
await channel.ModifyAsync(c => c.Name = name);
await ReplyConfirmLocalizedAsync(strs.set_channel_name);
}
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageChannels)]
[BotPerm(GuildPerm.ManageChannels)]
public async Task NsfwToggle()
{
var channel = (ITextChannel)ctx.Channel;
var isEnabled = channel.IsNsfw;
await channel.ModifyAsync(c => c.IsNsfw = !isEnabled);
if (isEnabled)
await ReplyConfirmLocalizedAsync(strs.nsfw_set_false);
else
await ReplyConfirmLocalizedAsync(strs.nsfw_set_true);
}
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(ChannelPerm.ManageMessages)]
[Priority(0)]
public Task Edit(ulong messageId, [Leftover] string text)
=> Edit((ITextChannel)ctx.Channel, messageId, text);
[Cmd]
[RequireContext(ContextType.Guild)]
[Priority(1)]
public async Task Edit(ITextChannel channel, ulong messageId, [Leftover] string text)
{
var userPerms = ((SocketGuildUser)ctx.User).GetPermissions(channel);
var botPerms = ((SocketGuild)ctx.Guild).CurrentUser.GetPermissions(channel);
if (!userPerms.Has(ChannelPermission.ManageMessages))
{
await ReplyErrorLocalizedAsync(strs.insuf_perms_u);
return;
}
if (!botPerms.Has(ChannelPermission.ViewChannel))
{
await ReplyErrorLocalizedAsync(strs.insuf_perms_i);
return;
}
await _service.EditMessage(ctx, channel, messageId, text);
}
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(ChannelPerm.ManageMessages)]
[BotPerm(ChannelPerm.ManageMessages)]
public Task Delete(ulong messageId, StoopidTime time = null)
=> Delete((ITextChannel)ctx.Channel, messageId, time);
[Cmd]
[RequireContext(ContextType.Guild)]
public async Task Delete(ITextChannel channel, ulong messageId, StoopidTime time = null)
=> await InternalMessageAction(channel, messageId, time, msg => msg.DeleteAsync());
private async Task InternalMessageAction(
ITextChannel channel,
ulong messageId,
StoopidTime time,
Func<IMessage, Task> func)
{
var userPerms = ((SocketGuildUser)ctx.User).GetPermissions(channel);
var botPerms = ((SocketGuild)ctx.Guild).CurrentUser.GetPermissions(channel);
if (!userPerms.Has(ChannelPermission.ManageMessages))
{
await ReplyErrorLocalizedAsync(strs.insuf_perms_u);
return;
}
if (!botPerms.Has(ChannelPermission.ManageMessages))
{
await ReplyErrorLocalizedAsync(strs.insuf_perms_i);
return;
}
var msg = await channel.GetMessageAsync(messageId);
if (msg is null)
{
await ReplyErrorLocalizedAsync(strs.msg_not_found);
return;
}
if (time is null)
await msg.DeleteAsync();
else if (time.Time <= TimeSpan.FromDays(7))
{
_ = Task.Run(async () =>
{
await Task.Delay(time.Time);
await msg.DeleteAsync();
});
}
else
{
await ReplyErrorLocalizedAsync(strs.time_too_long);
return;
}
await ctx.OkAsync();
}
[Cmd]
[BotPerm(ChannelPermission.CreatePublicThreads)]
[UserPerm(ChannelPermission.CreatePublicThreads)]
public async Task ThreadCreate([Leftover] string name)
{
if (ctx.Channel is not SocketTextChannel stc)
return;
await stc.CreateThreadAsync(name, message: ctx.Message.ReferencedMessage);
await ctx.OkAsync();
}
[Cmd]
[BotPerm(ChannelPermission.ManageThreads)]
[UserPerm(ChannelPermission.ManageThreads)]
public async Task ThreadDelete([Leftover] string name)
{
if (ctx.Channel is not SocketTextChannel stc)
return;
var t = stc.Threads.FirstOrDefault(x => string.Equals(x.Name, name, StringComparison.InvariantCultureIgnoreCase));
if (t is null)
{
await ReplyErrorLocalizedAsync(strs.not_found);
return;
}
await t.DeleteAsync();
await ctx.OkAsync();
}
[Cmd]
[UserPerm(ChannelPerm.ManageMessages)]
public async Task AutoPublish()
{
if (ctx.Channel.GetChannelType() != ChannelType.News)
{
await ReplyErrorLocalizedAsync(strs.req_announcement_channel);
return;
}
var newState = await _autoPubService.ToggleAutoPublish(ctx.Guild.Id, ctx.Channel.Id);
if (newState)
{
await ReplyConfirmLocalizedAsync(strs.autopublish_enable);
}
else
{
await ReplyConfirmLocalizedAsync(strs.autopublish_disable);
}
}
}

View file

@ -0,0 +1,158 @@
#nullable disable
using Microsoft.EntityFrameworkCore;
using Ellie.Db;
using Ellie.Services.Database.Models;
namespace Ellie.Modules.Administration.Services;
public class AdministrationService : IEService
{
public ConcurrentHashSet<ulong> DeleteMessagesOnCommand { get; }
public ConcurrentDictionary<ulong, bool> DeleteMessagesOnCommandChannels { get; }
private readonly DbService _db;
private readonly ILogCommandService _logService;
public AdministrationService(
IBot bot,
CommandHandler cmdHandler,
DbService db,
ILogCommandService logService)
{
_db = db;
_logService = logService;
DeleteMessagesOnCommand = new(bot.AllGuildConfigs.Where(g => g.DeleteMessageOnCommand).Select(g => g.GuildId));
DeleteMessagesOnCommandChannels = new(bot.AllGuildConfigs.SelectMany(x => x.DelMsgOnCmdChannels)
.ToDictionary(x => x.ChannelId, x => x.State)
.ToConcurrent());
cmdHandler.CommandExecuted += DelMsgOnCmd_Handler;
}
public (bool DelMsgOnCmd, IEnumerable<DelMsgOnCmdChannel> channels) GetDelMsgOnCmdData(ulong guildId)
{
using var uow = _db.GetDbContext();
var conf = uow.GuildConfigsForId(guildId, set => set.Include(x => x.DelMsgOnCmdChannels));
return (conf.DeleteMessageOnCommand, conf.DelMsgOnCmdChannels);
}
private Task DelMsgOnCmd_Handler(IUserMessage msg, CommandInfo cmd)
{
if (msg.Channel is not ITextChannel channel)
return Task.CompletedTask;
_ = Task.Run(async () =>
{
//wat ?!
if (DeleteMessagesOnCommandChannels.TryGetValue(channel.Id, out var state))
{
if (state && cmd.Name != "prune" && cmd.Name != "pick")
{
_logService.AddDeleteIgnore(msg.Id);
try { await msg.DeleteAsync(); }
catch { }
}
//if state is false, that means do not do it
}
else if (DeleteMessagesOnCommand.Contains(channel.Guild.Id) && cmd.Name != "prune" && cmd.Name != "pick")
{
_logService.AddDeleteIgnore(msg.Id);
try { await msg.DeleteAsync(); }
catch { }
}
});
return Task.CompletedTask;
}
public bool ToggleDeleteMessageOnCommand(ulong guildId)
{
bool enabled;
using var uow = _db.GetDbContext();
var conf = uow.GuildConfigsForId(guildId, set => set);
enabled = conf.DeleteMessageOnCommand = !conf.DeleteMessageOnCommand;
uow.SaveChanges();
return enabled;
}
public async Task SetDelMsgOnCmdState(ulong guildId, ulong chId, Administration.State newState)
{
await using (var uow = _db.GetDbContext())
{
var conf = uow.GuildConfigsForId(guildId, set => set.Include(x => x.DelMsgOnCmdChannels));
var old = conf.DelMsgOnCmdChannels.FirstOrDefault(x => x.ChannelId == chId);
if (newState == Administration.State.Inherit)
{
if (old is not null)
{
conf.DelMsgOnCmdChannels.Remove(old);
uow.Remove(old);
}
}
else
{
if (old is null)
{
old = new()
{
ChannelId = chId
};
conf.DelMsgOnCmdChannels.Add(old);
}
old.State = newState == Administration.State.Enable;
DeleteMessagesOnCommandChannels[chId] = newState == Administration.State.Enable;
}
await uow.SaveChangesAsync();
}
if (newState == Administration.State.Disable)
{
}
else if (newState == Administration.State.Enable)
DeleteMessagesOnCommandChannels[chId] = true;
else
DeleteMessagesOnCommandChannels.TryRemove(chId, out _);
}
public async Task DeafenUsers(bool value, params IGuildUser[] users)
{
if (!users.Any())
return;
foreach (var u in users)
{
try
{
await u.ModifyAsync(usr => usr.Deaf = value);
}
catch
{
// ignored
}
}
}
public async Task EditMessage(
ICommandContext context,
ITextChannel chanl,
ulong messageId,
string input)
{
var msg = await chanl.GetMessageAsync(messageId);
if (msg is not IUserMessage umsg || msg.Author.Id != context.Client.CurrentUser.Id)
return;
var rep = new ReplacementBuilder().WithDefault(context).Build();
var text = SmartText.CreateFrom(input);
text = rep.Replace(text);
await umsg.EditAsync(text);
}
}

View file

@ -0,0 +1,58 @@
#nullable disable
using Ellie.Modules.Administration.Services;
namespace Ellie.Modules.Administration;
public partial class Administration
{
[Group]
public partial class AutoAssignRoleCommands : EllieModule<AutoAssignRoleService>
{
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageRoles)]
[BotPerm(GuildPerm.ManageRoles)]
public async Task AutoAssignRole([Leftover] IRole role)
{
var guser = (IGuildUser)ctx.User;
if (role.Id == ctx.Guild.EveryoneRole.Id)
return;
// the user can't aar the role which is higher or equal to his highest role
if (ctx.User.Id != guser.Guild.OwnerId && guser.GetRoles().Max(x => x.Position) <= role.Position)
{
await ReplyErrorLocalizedAsync(strs.hierarchy);
return;
}
var roles = await _service.ToggleAarAsync(ctx.Guild.Id, role.Id);
if (roles.Count == 0)
await ReplyConfirmLocalizedAsync(strs.aar_disabled);
else if (roles.Contains(role.Id))
await AutoAssignRole();
else
await ReplyConfirmLocalizedAsync(strs.aar_role_removed(Format.Bold(role.ToString())));
}
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageRoles)]
[BotPerm(GuildPerm.ManageRoles)]
public async Task AutoAssignRole()
{
if (!_service.TryGetRoles(ctx.Guild.Id, out var roles))
{
await ReplyConfirmLocalizedAsync(strs.aar_none);
return;
}
var existing = roles.Select(rid => ctx.Guild.GetRole(rid)).Where(r => r is not null).ToList();
if (existing.Count != roles.Count)
await _service.SetAarRolesAsync(ctx.Guild.Id, existing.Select(x => x.Id));
await ReplyConfirmLocalizedAsync(strs.aar_roles(
'\n' + existing.Select(x => Format.Bold(x.ToString())).Join(",\n")));
}
}
}

View file

@ -0,0 +1,159 @@
#nullable disable
using Ellie.Services.Database.Models;
using System.Net;
using System.Threading.Channels;
using LinqToDB;
using Microsoft.EntityFrameworkCore;
using Ellie.Db;
namespace Ellie.Modules.Administration.Services;
public sealed class AutoAssignRoleService : IEService
{
private readonly DiscordSocketClient _client;
private readonly DbService _db;
//guildid/roleid
private readonly ConcurrentDictionary<ulong, IReadOnlyList<ulong>> _autoAssignableRoles;
private readonly Channel<SocketGuildUser> _assignQueue = Channel.CreateBounded<SocketGuildUser>(
new BoundedChannelOptions(100)
{
FullMode = BoundedChannelFullMode.DropOldest,
SingleReader = true,
SingleWriter = false
});
public AutoAssignRoleService(DiscordSocketClient client, IBot bot, DbService db)
{
_client = client;
_db = db;
_autoAssignableRoles = bot.AllGuildConfigs.Where(x => !string.IsNullOrWhiteSpace(x.AutoAssignRoleIds))
.ToDictionary<GuildConfig, ulong, IReadOnlyList<ulong>>(k => k.GuildId,
v => v.GetAutoAssignableRoles())
.ToConcurrent();
_ = Task.Run(async () =>
{
while (true)
{
var user = await _assignQueue.Reader.ReadAsync();
if (!_autoAssignableRoles.TryGetValue(user.Guild.Id, out var savedRoleIds))
continue;
try
{
var roleIds = savedRoleIds.Select(roleId => user.Guild.GetRole(roleId))
.Where(x => x is not null)
.ToList();
if (roleIds.Any())
{
await user.AddRolesAsync(roleIds);
await Task.Delay(250);
}
else
{
Log.Warning(
"Disabled 'Auto assign role' feature on {GuildName} [{GuildId}] server the roles dont exist",
user.Guild.Name,
user.Guild.Id);
await DisableAarAsync(user.Guild.Id);
}
}
catch (HttpException ex) when (ex.HttpCode == HttpStatusCode.Forbidden)
{
Log.Warning(
"Disabled 'Auto assign role' feature on {GuildName} [{GuildId}] server because I don't have role management permissions",
user.Guild.Name,
user.Guild.Id);
await DisableAarAsync(user.Guild.Id);
}
catch (Exception ex)
{
Log.Warning(ex, "Error in aar. Probably one of the roles doesn't exist");
}
}
});
_client.UserJoined += OnClientOnUserJoined;
_client.RoleDeleted += OnClientRoleDeleted;
}
private async Task OnClientRoleDeleted(SocketRole role)
{
if (_autoAssignableRoles.TryGetValue(role.Guild.Id, out var roles) && roles.Contains(role.Id))
await ToggleAarAsync(role.Guild.Id, role.Id);
}
private async Task OnClientOnUserJoined(SocketGuildUser user)
{
if (_autoAssignableRoles.TryGetValue(user.Guild.Id, out _))
await _assignQueue.Writer.WriteAsync(user);
}
public async Task<IReadOnlyList<ulong>> ToggleAarAsync(ulong guildId, ulong roleId)
{
await using var uow = _db.GetDbContext();
var gc = uow.GuildConfigsForId(guildId, set => set);
var roles = gc.GetAutoAssignableRoles();
if (!roles.Remove(roleId) && roles.Count < 3)
roles.Add(roleId);
gc.SetAutoAssignableRoles(roles);
await uow.SaveChangesAsync();
if (roles.Count > 0)
_autoAssignableRoles[guildId] = roles;
else
_autoAssignableRoles.TryRemove(guildId, out _);
return roles;
}
public async Task DisableAarAsync(ulong guildId)
{
await using var uow = _db.GetDbContext();
await uow.Set<GuildConfig>().AsNoTracking()
.Where(x => x.GuildId == guildId)
.UpdateAsync(_ => new()
{
AutoAssignRoleIds = null
});
_autoAssignableRoles.TryRemove(guildId, out _);
await uow.SaveChangesAsync();
}
public async Task SetAarRolesAsync(ulong guildId, IEnumerable<ulong> newRoles)
{
await using var uow = _db.GetDbContext();
var gc = uow.GuildConfigsForId(guildId, set => set);
gc.SetAutoAssignableRoles(newRoles);
await uow.SaveChangesAsync();
}
public bool TryGetRoles(ulong guildId, out IReadOnlyList<ulong> roles)
=> _autoAssignableRoles.TryGetValue(guildId, out roles);
}
public static class GuildConfigExtensions
{
public static List<ulong> GetAutoAssignableRoles(this GuildConfig gc)
{
if (string.IsNullOrWhiteSpace(gc.AutoAssignRoleIds))
return new();
return gc.AutoAssignRoleIds.Split(',').Select(ulong.Parse).ToList();
}
public static void SetAutoAssignableRoles(this GuildConfig gc, IEnumerable<ulong> roles)
=> gc.AutoAssignRoleIds = roles.Join(',');
}

View file

@ -0,0 +1,87 @@
#nullable disable
using LinqToDB;
using LinqToDB.EntityFrameworkCore;
using Ellie.Common.ModuleBehaviors;
using Ellie.Db.Models;
namespace Ellie.Modules.Administration.Services;
public class AutoPublishService : IExecNoCommand, IReadyExecutor, IEService
{
private readonly DbService _db;
private readonly DiscordSocketClient _client;
private readonly IBotCredsProvider _creds;
private ConcurrentDictionary<ulong, ulong> _enabled;
public AutoPublishService(DbService db, DiscordSocketClient client, IBotCredsProvider creds)
{
_db = db;
_client = client;
_creds = creds;
}
public async Task ExecOnNoCommandAsync(IGuild guild, IUserMessage msg)
{
if (guild is null)
return;
if (msg.Channel.GetChannelType() != ChannelType.News)
return;
if (!_enabled.TryGetValue(guild.Id, out var cid) || cid != msg.Channel.Id)
return;
await msg.CrosspostAsync(new RequestOptions()
{
RetryMode = RetryMode.AlwaysFail
});
}
public async Task OnReadyAsync()
{
var creds = _creds.GetCreds();
await using var ctx = _db.GetDbContext();
var items = await ctx.GetTable<AutoPublishChannel>()
.Where(x => Linq2DbExpressions.GuildOnShard(x.GuildId, creds.TotalShards, _client.ShardId))
.ToListAsyncLinqToDB();
_enabled = items
.ToDictionary(x => x.GuildId, x => x.ChannelId)
.ToConcurrent();
}
public async Task<bool> ToggleAutoPublish(ulong guildId, ulong channelId)
{
await using var ctx = _db.GetDbContext();
var deleted = await ctx.GetTable<AutoPublishChannel>()
.DeleteAsync(x => x.GuildId == guildId && x.ChannelId == channelId);
if (deleted != 0)
{
_enabled.TryRemove(guildId, out _);
return false;
}
await ctx.GetTable<AutoPublishChannel>()
.InsertOrUpdateAsync(() => new()
{
GuildId = guildId,
ChannelId = channelId,
DateAdded = DateTime.UtcNow,
},
old => new()
{
ChannelId = channelId,
DateAdded = DateTime.UtcNow,
},
() => new()
{
GuildId = guildId
});
_enabled[guildId] = channelId;
return true;
}
}

View file

@ -0,0 +1,80 @@
#nullable disable
using Ellie.Modules.Administration.Services;
#if !GLOBAL_ELLIE
namespace Ellie.Modules.Administration
{
public partial class Administration
{
[Group]
[OwnerOnly]
public partial class DangerousCommands : EllieModule<DangerousCommandsService>
{
[Cmd]
[OwnerOnly]
public Task SqlSelect([Leftover] string sql)
{
var result = _service.SelectSql(sql);
return ctx.SendPaginatedConfirmAsync(0,
cur =>
{
var items = result.Results.Skip(cur * 20).Take(20).ToList();
if (!items.Any())
return _eb.Create().WithErrorColor().WithFooter(sql).WithDescription("-");
return _eb.Create()
.WithOkColor()
.WithFooter(sql)
.WithTitle(string.Join(" ║ ", result.ColumnNames))
.WithDescription(string.Join('\n', items.Select(x => string.Join(" ║ ", x))));
},
result.Results.Count,
20);
}
[Cmd]
[OwnerOnly]
public async Task SqlExec([Leftover] string sql)
{
try
{
var embed = _eb.Create()
.WithTitle(GetText(strs.sql_confirm_exec))
.WithDescription(Format.Code(sql));
if (!await PromptUserConfirmAsync(embed))
return;
var res = await _service.ExecuteSql(sql);
await SendConfirmAsync(res.ToString());
}
catch (Exception ex)
{
await SendErrorAsync(ex.ToString());
}
}
[Cmd]
[OwnerOnly]
public async Task PurgeUser(ulong userId)
{
var embed = _eb.Create()
.WithDescription(GetText(strs.purge_user_confirm(Format.Bold(userId.ToString()))));
if (!await PromptUserConfirmAsync(embed))
return;
await _service.PurgeUserAsync(userId);
await ctx.OkAsync();
}
[Cmd]
[OwnerOnly]
public Task PurgeUser([Leftover] IUser user)
=> PurgeUser(user.Id);
}
}
}
#endif

View file

@ -0,0 +1,104 @@
#nullable disable
using LinqToDB;
using LinqToDB.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using Ellie.Db.Models;
using Ellie.Services.Database.Models;
namespace Ellie.Modules.Administration.Services;
public class DangerousCommandsService : IEService
{
private readonly DbService _db;
public DangerousCommandsService(DbService db)
=> _db = db;
public async Task<int> ExecuteSql(string sql)
{
int res;
await using var uow = _db.GetDbContext();
res = await uow.Database.ExecuteSqlRawAsync(sql);
return res;
}
public SelectResult SelectSql(string sql)
{
var result = new SelectResult
{
ColumnNames = new(),
Results = new()
};
using var uow = _db.GetDbContext();
var conn = uow.Database.GetDbConnection();
conn.Open();
using var cmd = conn.CreateCommand();
cmd.CommandText = sql;
using var reader = cmd.ExecuteReader();
if (reader.HasRows)
{
for (var i = 0; i < reader.FieldCount; i++)
result.ColumnNames.Add(reader.GetName(i));
while (reader.Read())
{
var obj = new object[reader.FieldCount];
reader.GetValues(obj);
result.Results.Add(obj.Select(x => x.ToString()).ToArray());
}
}
return result;
}
public async Task PurgeUserAsync(ulong userId)
{
await using var uow = _db.GetDbContext();
// get waifu info
var wi = await uow.Set<WaifuInfo>().FirstOrDefaultAsyncEF(x => x.Waifu.UserId == userId);
// if it exists, delete waifu related things
if (wi is not null)
{
// remove updates which have new or old as this waifu
await uow.Set<WaifuUpdate>().DeleteAsync(wu => wu.New.UserId == userId || wu.Old.UserId == userId);
// delete all items this waifu owns
await uow.Set<WaifuItem>().DeleteAsync(x => x.WaifuInfoId == wi.Id);
// all waifus this waifu claims are released
await uow.Set<WaifuInfo>()
.AsQueryable()
.Where(x => x.Claimer.UserId == userId)
.UpdateAsync(x => new()
{
ClaimerId = null
});
// all affinities set to this waifu are reset
await uow.Set<WaifuInfo>()
.AsQueryable()
.Where(x => x.Affinity.UserId == userId)
.UpdateAsync(x => new()
{
AffinityId = null
});
}
// delete guild xp
await uow.Set<UserXpStats>().DeleteAsync(x => x.UserId == userId);
// delete currency transactions
await uow.Set<CurrencyTransaction>().DeleteAsync(x => x.UserId == userId);
// delete user, currency, and clubs go away with it
await uow.Set<DiscordUser>().DeleteAsync(u => u.UserId == userId);
}
public class SelectResult
{
public List<string> ColumnNames { get; set; }
public List<string[]> Results { get; set; }
}
}

View file

@ -1,10 +1,24 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Discord.Net" Version="3.202.0" />
<PackageReference Include="Microsoft.Extensions.Http" Version="7.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Ellie.Bot.Common\Ellie.Bot.Common.csproj" />
<ProjectReference Include="..\Ellie.Bot.Modules.Gambling\Ellie.Bot.Modules.Gambling.csproj" />
</ItemGroup>
<ItemGroup>
<Folder Include="UserPunish\_common\" />
<Folder Include="UserPunish\_common\db\" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,36 @@
#nullable disable
using Ellie.Modules.Administration.Services;
namespace Ellie.Modules.Administration;
public partial class Administration
{
[Group]
public partial class GameVoiceChannelCommands : EllieModule<GameVoiceChannelService>
{
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.Administrator)]
[BotPerm(GuildPerm.MoveMembers)]
public async Task GameVoiceChannel()
{
var vch = ((IGuildUser)ctx.User).VoiceChannel;
if (vch is null)
{
await ReplyErrorLocalizedAsync(strs.not_in_voice);
return;
}
var id = _service.ToggleGameVoiceChannel(ctx.Guild.Id, vch.Id);
if (id is null)
await ReplyConfirmLocalizedAsync(strs.gvc_disabled);
else
{
_service.GameVoiceChannels.Add(vch.Id);
await ReplyConfirmLocalizedAsync(strs.gvc_enabled(Format.Bold(vch.Name)));
}
}
}
}

View file

@ -0,0 +1,127 @@
#nullable disable
using Ellie.Db;
namespace Ellie.Modules.Administration.Services;
public class GameVoiceChannelService : IEService
{
public ConcurrentHashSet<ulong> GameVoiceChannels { get; }
private readonly DbService _db;
private readonly DiscordSocketClient _client;
public GameVoiceChannelService(DiscordSocketClient client, DbService db, IBot bot)
{
_db = db;
_client = client;
GameVoiceChannels = new(bot.AllGuildConfigs
.Where(gc => gc.GameVoiceChannel is not null)
.Select(gc => gc.GameVoiceChannel!.Value));
_client.UserVoiceStateUpdated += OnUserVoiceStateUpdated;
_client.PresenceUpdated += OnPresenceUpdate;
}
private Task OnPresenceUpdate(SocketUser socketUser, SocketPresence before, SocketPresence after)
{
_ = Task.Run(async () =>
{
try
{
if (socketUser is not SocketGuildUser newUser)
return;
// if the user is in the voice channel and that voice channel is gvc
if (newUser.VoiceChannel is not { } vc
|| !GameVoiceChannels.Contains(vc.Id))
return;
//if the activity has changed, and is a playi1ng activity
foreach (var activity in after.Activities)
{
if (activity is { Type: ActivityType.Playing })
//trigger gvc
{
if (await TriggerGvc(newUser, activity.Name))
return;
}
}
}
catch (Exception ex)
{
Log.Warning(ex, "Error running GuildMemberUpdated in gvc");
}
});
return Task.CompletedTask;
}
public ulong? ToggleGameVoiceChannel(ulong guildId, ulong vchId)
{
ulong? id;
using var uow = _db.GetDbContext();
var gc = uow.GuildConfigsForId(guildId, set => set);
if (gc.GameVoiceChannel == vchId)
{
GameVoiceChannels.TryRemove(vchId);
id = gc.GameVoiceChannel = null;
}
else
{
if (gc.GameVoiceChannel is not null)
GameVoiceChannels.TryRemove(gc.GameVoiceChannel.Value);
GameVoiceChannels.Add(vchId);
id = gc.GameVoiceChannel = vchId;
}
uow.SaveChanges();
return id;
}
private Task OnUserVoiceStateUpdated(SocketUser usr, SocketVoiceState oldState, SocketVoiceState newState)
{
_ = Task.Run(async () =>
{
try
{
if (usr is not SocketGuildUser gUser)
return;
if (newState.VoiceChannel is null)
return;
if (!GameVoiceChannels.Contains(newState.VoiceChannel.Id))
return;
foreach (var game in gUser.Activities.Select(x => x.Name))
{
if (await TriggerGvc(gUser, game))
return;
}
}
catch (Exception ex)
{
Log.Warning(ex, "Error running VoiceStateUpdate in gvc");
}
});
return Task.CompletedTask;
}
private async Task<bool> TriggerGvc(SocketGuildUser gUser, string game)
{
if (string.IsNullOrWhiteSpace(game))
return false;
game = game.TrimTo(50)!.ToLowerInvariant();
var vch = gUser.Guild.VoiceChannels.FirstOrDefault(x => x.Name.ToLowerInvariant() == game);
if (vch is null)
return false;
await Task.Delay(1000);
await gUser.ModifyAsync(gu => gu.Channel = vch);
return true;
}
}

View file

@ -0,0 +1,229 @@
namespace Ellie.Modules.Administration;
public partial class Administration
{
[Group]
public partial class GreetCommands : EllieModule<GreetService>
{
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageGuild)]
public async Task Boost()
{
var enabled = await _service.ToggleBoost(ctx.Guild.Id, ctx.Channel.Id);
if (enabled)
await ReplyConfirmLocalizedAsync(strs.boost_on);
else
await ReplyPendingLocalizedAsync(strs.boost_off);
}
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageGuild)]
public async Task BoostDel(int timer = 30)
{
if (timer is < 0 or > 600)
return;
await _service.SetBoostDel(ctx.Guild.Id, timer);
if (timer > 0)
await ReplyConfirmLocalizedAsync(strs.boostdel_on(timer));
else
await ReplyPendingLocalizedAsync(strs.boostdel_off);
}
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageGuild)]
public async Task BoostMsg([Leftover] string? text = null)
{
if (string.IsNullOrWhiteSpace(text))
{
var boostMessage = _service.GetBoostMessage(ctx.Guild.Id);
await ReplyConfirmLocalizedAsync(strs.boostmsg_cur(boostMessage?.SanitizeMentions()));
return;
}
var sendBoostEnabled = _service.SetBoostMessage(ctx.Guild.Id, ref text);
await ReplyConfirmLocalizedAsync(strs.boostmsg_new);
if (!sendBoostEnabled)
await ReplyPendingLocalizedAsync(strs.boostmsg_enable($"`{prefix}boost`"));
}
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageGuild)]
public async Task GreetDel(int timer = 30)
{
if (timer is < 0 or > 600)
return;
await _service.SetGreetDel(ctx.Guild.Id, timer);
if (timer > 0)
await ReplyConfirmLocalizedAsync(strs.greetdel_on(timer));
else
await ReplyPendingLocalizedAsync(strs.greetdel_off);
}
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageGuild)]
public async Task Greet()
{
var enabled = await _service.SetGreet(ctx.Guild.Id, ctx.Channel.Id);
if (enabled)
await ReplyConfirmLocalizedAsync(strs.greet_on);
else
await ReplyPendingLocalizedAsync(strs.greet_off);
}
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageGuild)]
public async Task GreetMsg([Leftover] string? text = null)
{
if (string.IsNullOrWhiteSpace(text))
{
var greetMsg = _service.GetGreetMsg(ctx.Guild.Id);
await ReplyConfirmLocalizedAsync(strs.greetmsg_cur(greetMsg?.SanitizeMentions()));
return;
}
var sendGreetEnabled = _service.SetGreetMessage(ctx.Guild.Id, ref text);
await ReplyConfirmLocalizedAsync(strs.greetmsg_new);
if (!sendGreetEnabled)
await ReplyPendingLocalizedAsync(strs.greetmsg_enable($"`{prefix}greet`"));
}
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageGuild)]
public async Task GreetDm()
{
var enabled = await _service.SetGreetDm(ctx.Guild.Id);
if (enabled)
await ReplyConfirmLocalizedAsync(strs.greetdm_on);
else
await ReplyConfirmLocalizedAsync(strs.greetdm_off);
}
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageGuild)]
public async Task GreetDmMsg([Leftover] string? text = null)
{
if (string.IsNullOrWhiteSpace(text))
{
var dmGreetMsg = _service.GetDmGreetMsg(ctx.Guild.Id);
await ReplyConfirmLocalizedAsync(strs.greetdmmsg_cur(dmGreetMsg?.SanitizeMentions()));
return;
}
var sendGreetEnabled = _service.SetGreetDmMessage(ctx.Guild.Id, ref text);
await ReplyConfirmLocalizedAsync(strs.greetdmmsg_new);
if (!sendGreetEnabled)
await ReplyPendingLocalizedAsync(strs.greetdmmsg_enable($"`{prefix}greetdm`"));
}
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageGuild)]
public async Task Bye()
{
var enabled = await _service.SetBye(ctx.Guild.Id, ctx.Channel.Id);
if (enabled)
await ReplyConfirmLocalizedAsync(strs.bye_on);
else
await ReplyConfirmLocalizedAsync(strs.bye_off);
}
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageGuild)]
public async Task ByeMsg([Leftover] string? text = null)
{
if (string.IsNullOrWhiteSpace(text))
{
var byeMsg = _service.GetByeMessage(ctx.Guild.Id);
await ReplyConfirmLocalizedAsync(strs.byemsg_cur(byeMsg?.SanitizeMentions()));
return;
}
var sendByeEnabled = _service.SetByeMessage(ctx.Guild.Id, ref text);
await ReplyConfirmLocalizedAsync(strs.byemsg_new);
if (!sendByeEnabled)
await ReplyPendingLocalizedAsync(strs.byemsg_enable($"`{prefix}bye`"));
}
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageGuild)]
public async Task ByeDel(int timer = 30)
{
await _service.SetByeDel(ctx.Guild.Id, timer);
if (timer > 0)
await ReplyConfirmLocalizedAsync(strs.byedel_on(timer));
else
await ReplyPendingLocalizedAsync(strs.byedel_off);
}
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageGuild)]
[Ratelimit(5)]
public async Task ByeTest([Leftover] IGuildUser? user = null)
{
user ??= (IGuildUser)ctx.User;
await _service.ByeTest((ITextChannel)ctx.Channel, user);
var enabled = _service.GetByeEnabled(ctx.Guild.Id);
if (!enabled)
await ReplyPendingLocalizedAsync(strs.byemsg_enable($"`{prefix}bye`"));
}
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageGuild)]
[Ratelimit(5)]
public async Task GreetTest([Leftover] IGuildUser? user = null)
{
user ??= (IGuildUser)ctx.User;
await _service.GreetTest((ITextChannel)ctx.Channel, user);
var enabled = _service.GetGreetEnabled(ctx.Guild.Id);
if (!enabled)
await ReplyPendingLocalizedAsync(strs.greetmsg_enable($"`{prefix}greet`"));
}
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageGuild)]
[Ratelimit(5)]
public async Task GreetDmTest([Leftover] IGuildUser? user = null)
{
user ??= (IGuildUser)ctx.User;
var success = await _service.GreetDmTest(user);
if (success)
await ctx.OkAsync();
else
await ctx.WarningAsync();
var enabled = _service.GetGreetDmEnabled(ctx.Guild.Id);
if (!enabled)
await ReplyPendingLocalizedAsync(strs.greetdmmsg_enable($"`{prefix}greetdm`"));
}
}
}

View file

@ -0,0 +1,71 @@
namespace Ellie.Services;
public class GreetGrouper<T>
{
private readonly Dictionary<ulong, HashSet<T>> _group;
private readonly object _locker = new();
public GreetGrouper()
=> _group = new();
/// <summary>
/// Creates a group, if group already exists, adds the specified user
/// </summary>
/// <param name="guildId">Id of the server for which to create group for</param>
/// <param name="toAddIfExists">User to add if group already exists</param>
/// <returns></returns>
public bool CreateOrAdd(ulong guildId, T toAddIfExists)
{
lock (_locker)
{
if (_group.TryGetValue(guildId, out var list))
{
list.Add(toAddIfExists);
return false;
}
_group[guildId] = new();
return true;
}
}
/// <summary>
/// Remove the specified amount of items from the group. If all items are removed, group will be removed.
/// </summary>
/// <param name="guildId">Id of the group</param>
/// <param name="count">Maximum number of items to retrieve</param>
/// <param name="items">Items retrieved</param>
/// <returns>Whether the group has no more items left and is deleted</returns>
public bool ClearGroup(ulong guildId, int count, out IReadOnlyCollection<T> items)
{
lock (_locker)
{
if (_group.TryGetValue(guildId, out var set))
{
// if we want more than there are, return everything
if (count >= set.Count)
{
items = set;
_group.Remove(guildId);
return true;
}
// if there are more in the group than what's needed
// take the requested number, remove them from the set
// and return them
var toReturn = set.TakeWhile(_ => count-- != 0).ToList();
foreach (var item in toReturn)
set.Remove(item);
items = toReturn;
// returning falsemeans group is not yet deleted
// because there are items left
return false;
}
items = Array.Empty<T>();
return true;
}
}
}

View file

@ -0,0 +1,619 @@
using Ellie.Common.ModuleBehaviors;
using Ellie.Db;
using Ellie.Services.Database.Models;
using System.Threading.Channels;
namespace Ellie.Services;
public class GreetService : IEService, IReadyExecutor
{
public bool GroupGreets
=> _bss.Data.GroupGreets;
private readonly DbService _db;
private readonly ConcurrentDictionary<ulong, GreetSettings> _guildConfigsCache;
private readonly DiscordSocketClient _client;
private readonly GreetGrouper<IGuildUser> _greets = new();
private readonly GreetGrouper<IUser> _byes = new();
private readonly BotConfigService _bss;
public GreetService(
DiscordSocketClient client,
IBot bot,
DbService db,
BotConfigService bss)
{
_db = db;
_client = client;
_bss = bss;
_guildConfigsCache = new(bot.AllGuildConfigs.ToDictionary(g => g.GuildId, GreetSettings.Create));
_client.UserJoined += OnUserJoined;
_client.UserLeft += OnUserLeft;
bot.JoinedGuild += OnBotJoinedGuild;
_client.LeftGuild += OnClientLeftGuild;
_client.GuildMemberUpdated += ClientOnGuildMemberUpdated;
}
public async Task OnReadyAsync()
{
while (true)
{
var (conf, user, compl) = await _greetDmQueue.Reader.ReadAsync();
var res = await GreetDmUserInternal(conf, user);
compl.TrySetResult(res);
await Task.Delay(2000);
}
}
private Task ClientOnGuildMemberUpdated(Cacheable<SocketGuildUser, ulong> optOldUser, SocketGuildUser newUser)
{
// if user is a new booster
// or boosted again the same server
if ((optOldUser.Value is { PremiumSince: null } && newUser is { PremiumSince: not null })
|| (optOldUser.Value?.PremiumSince is { } oldDate
&& newUser.PremiumSince is { } newDate
&& newDate > oldDate))
{
var conf = GetOrAddSettingsForGuild(newUser.Guild.Id);
if (!conf.SendBoostMessage)
return Task.CompletedTask;
_ = Task.Run(TriggerBoostMessage(conf, newUser));
}
return Task.CompletedTask;
}
private Func<Task> TriggerBoostMessage(GreetSettings conf, SocketGuildUser user)
=> async () =>
{
var channel = user.Guild.GetTextChannel(conf.BoostMessageChannelId);
if (channel is null)
return;
if (string.IsNullOrWhiteSpace(conf.BoostMessage))
return;
var toSend = SmartText.CreateFrom(conf.BoostMessage);
var rep = new ReplacementBuilder().WithDefault(user, channel, user.Guild, _client).Build();
try
{
var toDelete = await channel.SendAsync(rep.Replace(toSend));
if (conf.BoostMessageDeleteAfter > 0)
toDelete.DeleteAfter(conf.BoostMessageDeleteAfter);
}
catch (Exception ex)
{
Log.Error(ex, "Error sending boost message");
}
};
private Task OnClientLeftGuild(SocketGuild arg)
{
_guildConfigsCache.TryRemove(arg.Id, out _);
return Task.CompletedTask;
}
private Task OnBotJoinedGuild(GuildConfig gc)
{
_guildConfigsCache[gc.GuildId] = GreetSettings.Create(gc);
return Task.CompletedTask;
}
private Task OnUserLeft(SocketGuild guild, SocketUser user)
{
_ = Task.Run(async () =>
{
try
{
var conf = GetOrAddSettingsForGuild(guild.Id);
if (!conf.SendChannelByeMessage)
return;
var channel = guild.TextChannels.FirstOrDefault(c => c.Id == conf.ByeMessageChannelId);
if (channel is null) //maybe warn the server owner that the channel is missing
return;
if (GroupGreets)
{
// if group is newly created, greet that user right away,
// but any user which joins in the next 5 seconds will
// be greeted in a group greet
if (_byes.CreateOrAdd(guild.Id, user))
{
// greet single user
await ByeUsers(conf, channel, new[] { user });
var groupClear = false;
while (!groupClear)
{
await Task.Delay(5000);
groupClear = _byes.ClearGroup(guild.Id, 5, out var toBye);
await ByeUsers(conf, channel, toBye);
}
}
}
else
await ByeUsers(conf, channel, new[] { user });
}
catch
{
// ignored
}
});
return Task.CompletedTask;
}
public string? GetDmGreetMsg(ulong id)
{
using var uow = _db.GetDbContext();
return uow.GuildConfigsForId(id, set => set).DmGreetMessageText;
}
public string? GetGreetMsg(ulong gid)
{
using var uow = _db.GetDbContext();
return uow.GuildConfigsForId(gid, set => set).ChannelGreetMessageText;
}
public string? GetBoostMessage(ulong gid)
{
using var uow = _db.GetDbContext();
return uow.GuildConfigsForId(gid, set => set).BoostMessage;
}
private Task ByeUsers(GreetSettings conf, ITextChannel channel, IUser user)
=> ByeUsers(conf, channel, new[] { user });
private async Task ByeUsers(GreetSettings conf, ITextChannel channel, IReadOnlyCollection<IUser> users)
{
if (!users.Any())
return;
var rep = new ReplacementBuilder().WithChannel(channel)
.WithClient(_client)
.WithServer(_client, (SocketGuild)channel.Guild)
.WithManyUsers(users)
.Build();
var text = SmartText.CreateFrom(conf.ChannelByeMessageText);
text = rep.Replace(text);
try
{
var toDelete = await channel.SendAsync(text);
if (conf.AutoDeleteByeMessagesTimer > 0)
toDelete.DeleteAfter(conf.AutoDeleteByeMessagesTimer);
}
catch (HttpException ex) when (ex.DiscordCode == DiscordErrorCode.InsufficientPermissions || ex.DiscordCode == DiscordErrorCode.UnknownChannel)
{
Log.Warning(ex, "Missing permissions to send a bye message, the bye message will be disabled on server: {GuildId}", channel.GuildId);
await SetBye(channel.GuildId, channel.Id, false);
}
catch (Exception ex)
{
Log.Warning(ex, "Error embeding bye message");
}
}
private Task GreetUsers(GreetSettings conf, ITextChannel channel, IGuildUser user)
=> GreetUsers(conf, channel, new[] { user });
private async Task GreetUsers(GreetSettings conf, ITextChannel channel, IReadOnlyCollection<IGuildUser> users)
{
if (users.Count == 0)
return;
var rep = new ReplacementBuilder().WithChannel(channel)
.WithClient(_client)
.WithServer(_client, (SocketGuild)channel.Guild)
.WithManyUsers(users)
.Build();
var text = SmartText.CreateFrom(conf.ChannelGreetMessageText);
text = rep.Replace(text);
try
{
var toDelete = await channel.SendAsync(text);
if (conf.AutoDeleteGreetMessagesTimer > 0)
toDelete.DeleteAfter(conf.AutoDeleteGreetMessagesTimer);
}
catch (HttpException ex) when (ex.DiscordCode == DiscordErrorCode.InsufficientPermissions || ex.DiscordCode == DiscordErrorCode.UnknownChannel)
{
Log.Warning(ex, "Missing permissions to send a bye message, the greet message will be disabled on server: {GuildId}", channel.GuildId);
await SetGreet(channel.GuildId, channel.Id, false);
}
catch (Exception ex)
{
Log.Warning(ex, "Error embeding greet message");
}
}
private readonly Channel<(GreetSettings, IGuildUser, TaskCompletionSource<bool>)> _greetDmQueue =
Channel.CreateBounded<(GreetSettings, IGuildUser, TaskCompletionSource<bool>)>(new BoundedChannelOptions(60)
{
// The limit of 60 users should be only hit when there's a raid. In that case
// probably the best thing to do is to drop newest (raiding) users
FullMode = BoundedChannelFullMode.DropNewest
});
private async Task<bool> GreetDmUser(GreetSettings conf, IGuildUser user)
{
var completionSource = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
await _greetDmQueue.Writer.WriteAsync((conf, user, completionSource));
return await completionSource.Task;
}
private async Task<bool> GreetDmUserInternal(GreetSettings conf, IGuildUser user)
{
try
{
var rep = new ReplacementBuilder()
.WithUser(user)
.WithServer(_client, (SocketGuild)user.Guild)
.Build();
var text = SmartText.CreateFrom(conf.DmGreetMessageText);
text = rep.Replace(text);
if (text is SmartPlainText pt)
{
text = new SmartEmbedText()
{
Description = pt.Text
};
}
if (text is SmartEmbedText set)
{
text = set with
{
Footer = CreateFooterSource(user)
};
}
else if (text is SmartEmbedTextArray seta)
{
// if the greet dm message is a text array
var ebElem = seta.Embeds.LastOrDefault();
if (ebElem is null)
{
// if there are no embeds, add an embed with the footer
text = seta with
{
Embeds = new[]
{
new SmartEmbedArrayElementText()
{
Footer = CreateFooterSource(user)
}
}
};
}
else
{
// if the maximum amount of embeds is reached, edit the last embed
if (seta.Embeds.Length >= 10)
{
seta.Embeds[^1] = seta.Embeds[^1] with
{
Footer = CreateFooterSource(user)
};
}
else
{
// if there is less than 10 embeds, add an embed with footer only
seta.Embeds = seta.Embeds.Append(new SmartEmbedArrayElementText()
{
Footer = CreateFooterSource(user)
}).ToArray();
}
}
}
await user.SendAsync(text);
}
catch
{
return false;
}
return true;
}
private static SmartTextEmbedFooter CreateFooterSource(IGuildUser user)
=> new()
{
Text = $"This message was sent from {user.Guild} server.",
IconUrl = user.Guild.IconUrl
};
private Task OnUserJoined(IGuildUser user)
{
_ = Task.Run(async () =>
{
try
{
var conf = GetOrAddSettingsForGuild(user.GuildId);
if (conf.SendChannelGreetMessage)
{
var channel = await user.Guild.GetTextChannelAsync(conf.GreetMessageChannelId);
if (channel is not null)
{
if (GroupGreets)
{
// if group is newly created, greet that user right away,
// but any user which joins in the next 5 seconds will
// be greeted in a group greet
if (_greets.CreateOrAdd(user.GuildId, user))
{
// greet single user
await GreetUsers(conf, channel, new[] { user });
var groupClear = false;
while (!groupClear)
{
await Task.Delay(5000);
groupClear = _greets.ClearGroup(user.GuildId, 5, out var toGreet);
await GreetUsers(conf, channel, toGreet);
}
}
}
else
await GreetUsers(conf, channel, new[] { user });
}
}
if (conf.SendDmGreetMessage)
await GreetDmUser(conf, user);
}
catch
{
// ignored
}
});
return Task.CompletedTask;
}
public string? GetByeMessage(ulong gid)
{
using var uow = _db.GetDbContext();
return uow.GuildConfigsForId(gid, set => set).ChannelByeMessageText;
}
public GreetSettings GetOrAddSettingsForGuild(ulong guildId)
{
if (_guildConfigsCache.TryGetValue(guildId, out var settings))
return settings;
using (var uow = _db.GetDbContext())
{
var gc = uow.GuildConfigsForId(guildId, set => set);
settings = GreetSettings.Create(gc);
}
_guildConfigsCache.TryAdd(guildId, settings);
return settings;
}
public async Task<bool> SetGreet(ulong guildId, ulong channelId, bool? value = null)
{
await using var uow = _db.GetDbContext();
var conf = uow.GuildConfigsForId(guildId, set => set);
var enabled = conf.SendChannelGreetMessage = value ?? !conf.SendChannelGreetMessage;
conf.GreetMessageChannelId = channelId;
var toAdd = GreetSettings.Create(conf);
_guildConfigsCache[guildId] = toAdd;
await uow.SaveChangesAsync();
return enabled;
}
public bool SetGreetMessage(ulong guildId, ref string message)
{
message = message.SanitizeMentions();
if (string.IsNullOrWhiteSpace(message))
throw new ArgumentNullException(nameof(message));
using var uow = _db.GetDbContext();
var conf = uow.GuildConfigsForId(guildId, set => set);
conf.ChannelGreetMessageText = message;
var greetMsgEnabled = conf.SendChannelGreetMessage;
var toAdd = GreetSettings.Create(conf);
_guildConfigsCache.AddOrUpdate(guildId, toAdd, (_, _) => toAdd);
uow.SaveChanges();
return greetMsgEnabled;
}
public async Task<bool> SetGreetDm(ulong guildId, bool? value = null)
{
await using var uow = _db.GetDbContext();
var conf = uow.GuildConfigsForId(guildId, set => set);
var enabled = conf.SendDmGreetMessage = value ?? !conf.SendDmGreetMessage;
var toAdd = GreetSettings.Create(conf);
_guildConfigsCache[guildId] = toAdd;
await uow.SaveChangesAsync();
return enabled;
}
public bool SetGreetDmMessage(ulong guildId, ref string? message)
{
message = message?.SanitizeMentions();
if (string.IsNullOrWhiteSpace(message))
throw new ArgumentNullException(nameof(message));
using var uow = _db.GetDbContext();
var conf = uow.GuildConfigsForId(guildId, set => set);
conf.DmGreetMessageText = message;
var toAdd = GreetSettings.Create(conf);
_guildConfigsCache[guildId] = toAdd;
uow.SaveChanges();
return conf.SendDmGreetMessage;
}
public async Task<bool> SetBye(ulong guildId, ulong channelId, bool? value = null)
{
await using var uow = _db.GetDbContext();
var conf = uow.GuildConfigsForId(guildId, set => set);
var enabled = conf.SendChannelByeMessage = value ?? !conf.SendChannelByeMessage;
conf.ByeMessageChannelId = channelId;
var toAdd = GreetSettings.Create(conf);
_guildConfigsCache[guildId] = toAdd;
await uow.SaveChangesAsync();
return enabled;
}
public bool SetByeMessage(ulong guildId, ref string? message)
{
message = message?.SanitizeMentions();
if (string.IsNullOrWhiteSpace(message))
throw new ArgumentNullException(nameof(message));
using var uow = _db.GetDbContext();
var conf = uow.GuildConfigsForId(guildId, set => set);
conf.ChannelByeMessageText = message;
var toAdd = GreetSettings.Create(conf);
_guildConfigsCache[guildId] = toAdd;
uow.SaveChanges();
return conf.SendChannelByeMessage;
}
public async Task SetByeDel(ulong guildId, int timer)
{
if (timer is < 0 or > 600)
return;
await using var uow = _db.GetDbContext();
var conf = uow.GuildConfigsForId(guildId, set => set);
conf.AutoDeleteByeMessagesTimer = timer;
var toAdd = GreetSettings.Create(conf);
_guildConfigsCache[guildId] = toAdd;
await uow.SaveChangesAsync();
}
public async Task SetGreetDel(ulong guildId, int timer)
{
if (timer is < 0 or > 600)
return;
await using var uow = _db.GetDbContext();
var conf = uow.GuildConfigsForId(guildId, set => set);
conf.AutoDeleteGreetMessagesTimer = timer;
var toAdd = GreetSettings.Create(conf);
_guildConfigsCache[guildId] = toAdd;
await uow.SaveChangesAsync();
}
public bool SetBoostMessage(ulong guildId, ref string message)
{
message = message.SanitizeMentions();
using var uow = _db.GetDbContext();
var conf = uow.GuildConfigsForId(guildId, set => set);
conf.BoostMessage = message;
var toAdd = GreetSettings.Create(conf);
_guildConfigsCache[guildId] = toAdd;
uow.SaveChanges();
return conf.SendBoostMessage;
}
public async Task SetBoostDel(ulong guildId, int timer)
{
if (timer is < 0 or > 600)
throw new ArgumentOutOfRangeException(nameof(timer));
await using var uow = _db.GetDbContext();
var conf = uow.GuildConfigsForId(guildId, set => set);
conf.BoostMessageDeleteAfter = timer;
var toAdd = GreetSettings.Create(conf);
_guildConfigsCache[guildId] = toAdd;
await uow.SaveChangesAsync();
}
public async Task<bool> ToggleBoost(ulong guildId, ulong channelId)
{
await using var uow = _db.GetDbContext();
var conf = uow.GuildConfigsForId(guildId, set => set);
conf.SendBoostMessage = !conf.SendBoostMessage;
conf.BoostMessageChannelId = channelId;
await uow.SaveChangesAsync();
var toAdd = GreetSettings.Create(conf);
_guildConfigsCache[guildId] = toAdd;
return conf.SendBoostMessage;
}
#region Get Enabled Status
public bool GetGreetDmEnabled(ulong guildId)
{
using var uow = _db.GetDbContext();
var conf = uow.GuildConfigsForId(guildId, set => set);
return conf.SendDmGreetMessage;
}
public bool GetGreetEnabled(ulong guildId)
{
using var uow = _db.GetDbContext();
var conf = uow.GuildConfigsForId(guildId, set => set);
return conf.SendChannelGreetMessage;
}
public bool GetByeEnabled(ulong guildId)
{
using var uow = _db.GetDbContext();
var conf = uow.GuildConfigsForId(guildId, set => set);
return conf.SendChannelByeMessage;
}
#endregion
#region Test Messages
public Task ByeTest(ITextChannel channel, IGuildUser user)
{
var conf = GetOrAddSettingsForGuild(user.GuildId);
return ByeUsers(conf, channel, user);
}
public Task GreetTest(ITextChannel channel, IGuildUser user)
{
var conf = GetOrAddSettingsForGuild(user.GuildId);
return GreetUsers(conf, channel, user);
}
public Task<bool> GreetDmTest(IGuildUser user)
{
var conf = GetOrAddSettingsForGuild(user.GuildId);
return GreetDmUser(conf, user);
}
#endregion
}

View file

@ -0,0 +1,45 @@
using Ellie.Services.Database.Models;
namespace Ellie.Services;
public class GreetSettings
{
public int AutoDeleteGreetMessagesTimer { get; set; }
public int AutoDeleteByeMessagesTimer { get; set; }
public ulong GreetMessageChannelId { get; set; }
public ulong ByeMessageChannelId { get; set; }
public bool SendDmGreetMessage { get; set; }
public string? DmGreetMessageText { get; set; }
public bool SendChannelGreetMessage { get; set; }
public string? ChannelGreetMessageText { get; set; }
public bool SendChannelByeMessage { get; set; }
public string? ChannelByeMessageText { get; set; }
public bool SendBoostMessage { get; set; }
public string? BoostMessage { get; set; }
public int BoostMessageDeleteAfter { get; set; }
public ulong BoostMessageChannelId { get; set; }
public static GreetSettings Create(GuildConfig g)
=> new()
{
AutoDeleteByeMessagesTimer = g.AutoDeleteByeMessagesTimer,
AutoDeleteGreetMessagesTimer = g.AutoDeleteGreetMessagesTimer,
GreetMessageChannelId = g.GreetMessageChannelId,
ByeMessageChannelId = g.ByeMessageChannelId,
SendDmGreetMessage = g.SendDmGreetMessage,
DmGreetMessageText = g.DmGreetMessageText,
SendChannelGreetMessage = g.SendChannelGreetMessage,
ChannelGreetMessageText = g.ChannelGreetMessageText,
SendChannelByeMessage = g.SendChannelByeMessage,
ChannelByeMessageText = g.ChannelByeMessageText,
SendBoostMessage = g.SendBoostMessage,
BoostMessage = g.BoostMessage,
BoostMessageDeleteAfter = g.BoostMessageDeleteAfter,
BoostMessageChannelId = g.BoostMessageChannelId
};
}

View file

@ -0,0 +1,235 @@
#nullable disable
using LinqToDB;
using Microsoft.Extensions.Caching.Memory;
using Ellie.Common.ModuleBehaviors;
using System.Net;
using System.Threading.Channels;
using Ellie.Services.Database.Models;
namespace Ellie.Modules.Administration.Services;
public sealed class SomethingOnlyChannelService : IExecOnMessage
{
public int Priority { get; } = 0;
private readonly IMemoryCache _ticketCache;
private readonly DiscordSocketClient _client;
private readonly DbService _db;
private readonly ConcurrentDictionary<ulong, ConcurrentHashSet<ulong>> _imageOnly;
private readonly ConcurrentDictionary<ulong, ConcurrentHashSet<ulong>> _linkOnly;
private readonly Channel<IUserMessage> _deleteQueue = Channel.CreateBounded<IUserMessage>(
new BoundedChannelOptions(100)
{
FullMode = BoundedChannelFullMode.DropOldest,
SingleReader = true,
SingleWriter = false
});
public SomethingOnlyChannelService(IMemoryCache ticketCache, DiscordSocketClient client, DbService db)
{
_ticketCache = ticketCache;
_client = client;
_db = db;
using var uow = _db.GetDbContext();
_imageOnly = uow.Set<ImageOnlyChannel>()
.Where(x => x.Type == OnlyChannelType.Image)
.ToList()
.GroupBy(x => x.GuildId)
.ToDictionary(x => x.Key, x => new ConcurrentHashSet<ulong>(x.Select(y => y.ChannelId)))
.ToConcurrent();
_linkOnly = uow.Set<ImageOnlyChannel>()
.Where(x => x.Type == OnlyChannelType.Link)
.ToList()
.GroupBy(x => x.GuildId)
.ToDictionary(x => x.Key, x => new ConcurrentHashSet<ulong>(x.Select(y => y.ChannelId)))
.ToConcurrent();
_ = Task.Run(DeleteQueueRunner);
_client.ChannelDestroyed += ClientOnChannelDestroyed;
}
private async Task ClientOnChannelDestroyed(SocketChannel ch)
{
if (ch is not IGuildChannel gch)
return;
if (_imageOnly.TryGetValue(gch.GuildId, out var channels) && channels.TryRemove(ch.Id))
await ToggleImageOnlyChannelAsync(gch.GuildId, ch.Id, true);
}
private async Task DeleteQueueRunner()
{
while (true)
{
var toDelete = await _deleteQueue.Reader.ReadAsync();
try
{
await toDelete.DeleteAsync();
await Task.Delay(1000);
}
catch (HttpException ex) when (ex.HttpCode == HttpStatusCode.Forbidden)
{
// disable if bot can't delete messages in the channel
await ToggleImageOnlyChannelAsync(((ITextChannel)toDelete.Channel).GuildId, toDelete.Channel.Id, true);
}
}
}
public async Task<bool> ToggleImageOnlyChannelAsync(ulong guildId, ulong channelId, bool forceDisable = false)
{
var newState = false;
await using var uow = _db.GetDbContext();
if (forceDisable || (_imageOnly.TryGetValue(guildId, out var channels) && channels.TryRemove(channelId)))
{
await uow.Set<ImageOnlyChannel>().DeleteAsync(x => x.ChannelId == channelId && x.Type == OnlyChannelType.Image);
}
else
{
await uow.Set<ImageOnlyChannel>().DeleteAsync(x => x.ChannelId == channelId);
uow.Set<ImageOnlyChannel>().Add(new()
{
GuildId = guildId,
ChannelId = channelId,
Type = OnlyChannelType.Image
});
if (_linkOnly.TryGetValue(guildId, out var chs))
chs.TryRemove(channelId);
channels = _imageOnly.GetOrAdd(guildId, new ConcurrentHashSet<ulong>());
channels.Add(channelId);
newState = true;
}
await uow.SaveChangesAsync();
return newState;
}
public async Task<bool> ToggleLinkOnlyChannelAsync(ulong guildId, ulong channelId, bool forceDisable = false)
{
var newState = false;
await using var uow = _db.GetDbContext();
if (forceDisable || (_linkOnly.TryGetValue(guildId, out var channels) && channels.TryRemove(channelId)))
{
await uow.Set<ImageOnlyChannel>().DeleteAsync(x => x.ChannelId == channelId && x.Type == OnlyChannelType.Link);
}
else
{
await uow.Set<ImageOnlyChannel>().DeleteAsync(x => x.ChannelId == channelId);
uow.Set<ImageOnlyChannel>().Add(new()
{
GuildId = guildId,
ChannelId = channelId,
Type = OnlyChannelType.Link
});
if (_imageOnly.TryGetValue(guildId, out var chs))
chs.TryRemove(channelId);
channels = _linkOnly.GetOrAdd(guildId, new ConcurrentHashSet<ulong>());
channels.Add(channelId);
newState = true;
}
await uow.SaveChangesAsync();
return newState;
}
public async Task<bool> ExecOnMessageAsync(IGuild guild, IUserMessage msg)
{
if (msg.Channel is not ITextChannel tch)
return false;
if (_imageOnly.TryGetValue(tch.GuildId, out var chs) && chs.Contains(msg.Channel.Id))
return await HandleOnlyChannel(tch, msg, OnlyChannelType.Image);
if (_linkOnly.TryGetValue(tch.GuildId, out chs) && chs.Contains(msg.Channel.Id))
return await HandleOnlyChannel(tch, msg, OnlyChannelType.Link);
return false;
}
private async Task<bool> HandleOnlyChannel(ITextChannel tch, IUserMessage msg, OnlyChannelType type)
{
if (type == OnlyChannelType.Image)
{
if (msg.Attachments.Any(x => x is { Height: > 0, Width: > 0 }))
return false;
}
else
{
if (msg.Content.TryGetUrlPath(out _))
return false;
}
var user = await tch.Guild.GetUserAsync(msg.Author.Id)
?? await _client.Rest.GetGuildUserAsync(tch.GuildId, msg.Author.Id);
if (user is null)
return false;
// ignore owner and admin
if (user.Id == tch.Guild.OwnerId || user.GuildPermissions.Administrator)
{
Log.Information("{Type}-Only Channel: Ignoring owner od admin ({ChannelId})", type, msg.Channel.Id);
return false;
}
// ignore users higher in hierarchy
var botUser = await tch.Guild.GetCurrentUserAsync();
if (user.GetRoles().Max(x => x.Position) >= botUser.GetRoles().Max(x => x.Position))
return false;
if (!botUser.GetPermissions(tch).ManageChannel)
{
if(type == OnlyChannelType.Image)
await ToggleImageOnlyChannelAsync(tch.GuildId, tch.Id, true);
else
await ToggleImageOnlyChannelAsync(tch.GuildId, tch.Id, true);
return false;
}
var shouldLock = AddUserTicket(tch.GuildId, msg.Author.Id);
if (shouldLock)
{
await tch.AddPermissionOverwriteAsync(msg.Author, new(sendMessages: PermValue.Deny));
Log.Warning("{Type}-Only Channel: User {User} [{UserId}] has been banned from typing in the channel [{ChannelId}]",
type,
msg.Author,
msg.Author.Id,
msg.Channel.Id);
}
try
{
await _deleteQueue.Writer.WriteAsync(msg);
}
catch (Exception ex)
{
Log.Error(ex, "Error deleting message {MessageId} in image-only channel {ChannelId}", msg.Id, tch.Id);
}
return true;
}
private bool AddUserTicket(ulong guildId, ulong userId)
{
var old = _ticketCache.GetOrCreate($"{guildId}_{userId}",
entry =>
{
entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromDays(1);
return 0;
});
_ticketCache.Set($"{guildId}_{userId}", ++old);
// if this is the third time that the user posts a
// non image in an image-only channel on this server
return old > 2;
}
}

View file

@ -0,0 +1,250 @@
#nullable disable
using System.Globalization;
namespace Ellie.Modules.Administration;
public partial class Administration
{
[Group]
public partial class LocalizationCommands : EllieModule
{
private static readonly IReadOnlyDictionary<string, string> _supportedLocales = new Dictionary<string, string>
{
{ "ar", "العربية" },
{ "zh-TW", "繁體中文, 台灣" },
{ "zh-CN", "简体中文, 中华人民共和国" },
{ "nl-NL", "Nederlands, Nederland" },
{ "en-US", "English, United States" },
{ "fr-FR", "Français, France" },
{ "cs-CZ", "Čeština, Česká republika" },
{ "da-DK", "Dansk, Danmark" },
{ "de-DE", "Deutsch, Deutschland" },
{ "he-IL", "עברית, ישראל" },
{ "hu-HU", "Magyar, Magyarország" },
{ "id-ID", "Bahasa Indonesia, Indonesia" },
{ "it-IT", "Italiano, Italia" },
{ "ja-JP", "日本語, 日本" },
{ "ko-KR", "한국어, 대한민국" },
{ "nb-NO", "Norsk, Norge" },
{ "pl-PL", "Polski, Polska" },
{ "pt-BR", "Português Brasileiro, Brasil" },
{ "ro-RO", "Română, România" },
{ "ru-RU", "Русский, Россия" },
{ "sr-Cyrl-RS", "Српски, Србија" },
{ "es-ES", "Español, España" },
{ "sv-SE", "Svenska, Sverige" },
{ "tr-TR", "Türkçe, Türkiye" },
{ "ts-TS", "Tsundere, You Baka" },
{ "uk-UA", "Українська, Україна" }
};
[Cmd]
[RequireContext(ContextType.Guild)]
[Priority(0)]
public async Task LanguageSet()
=> await ReplyConfirmLocalizedAsync(strs.lang_set_show(Format.Bold(Culture.ToString()),
Format.Bold(Culture.NativeName)));
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.Administrator)]
[Priority(1)]
public async Task LanguageSet(string name)
{
try
{
CultureInfo ci;
if (name.Trim().ToLowerInvariant() == "default")
{
_localization.RemoveGuildCulture(ctx.Guild);
ci = _localization.DefaultCultureInfo;
}
else
{
ci = new(name);
_localization.SetGuildCulture(ctx.Guild, ci);
}
await ReplyConfirmLocalizedAsync(strs.lang_set(Format.Bold(ci.ToString()), Format.Bold(ci.NativeName)));
}
catch (Exception)
{
await ReplyErrorLocalizedAsync(strs.lang_set_fail);
}
}
[Cmd]
public async Task LanguageSetDefault()
{
var cul = _localization.DefaultCultureInfo;
await ReplyErrorLocalizedAsync(strs.lang_set_bot_show(cul, cul.NativeName));
}
[Cmd]
[OwnerOnly]
public async Task LanguageSetDefault(string name)
{
try
{
CultureInfo ci;
if (name.Trim().ToLowerInvariant() == "default")
{
_localization.ResetDefaultCulture();
ci = _localization.DefaultCultureInfo;
}
else
{
ci = new(name);
_localization.SetDefaultCulture(ci);
}
await ReplyConfirmLocalizedAsync(strs.lang_set_bot(Format.Bold(ci.ToString()),
Format.Bold(ci.NativeName)));
}
catch (Exception)
{
await ReplyErrorLocalizedAsync(strs.lang_set_fail);
}
}
[Cmd]
public async Task LanguagesList()
=> await ctx.Channel.EmbedAsync(_eb.Create()
.WithOkColor()
.WithTitle(GetText(strs.lang_list))
.WithDescription(string.Join("\n",
_supportedLocales.Select(
x => $"{Format.Code(x.Key),-10} => {x.Value}"))));
}
}
/* list of language codes for reference.
* taken from https://github.com/dotnet/coreclr/blob/ee5862c6a257e60e263537d975ab6c513179d47f/src/mscorlib/src/System/Globalization/CultureData.cs#L192
{ "029", "en-029" },
{ "AE", "ar-AE" },
{ "AF", "prs-AF" },
{ "AL", "sq-AL" },
{ "AM", "hy-AM" },
{ "AR", "es-AR" },
{ "AT", "de-AT" },
{ "AU", "en-AU" },
{ "AZ", "az-Cyrl-AZ" },
{ "BA", "bs-Latn-BA" },
{ "BD", "bn-BD" },
{ "BE", "nl-BE" },
{ "BG", "bg-BG" },
{ "BH", "ar-BH" },
{ "BN", "ms-BN" },
{ "BO", "es-BO" },
{ "BR", "pt-BR" },
{ "BY", "be-BY" },
{ "BZ", "en-BZ" },
{ "CA", "en-CA" },
{ "CH", "it-CH" },
{ "CL", "es-CL" },
{ "CN", "zh-CN" },
{ "CO", "es-CO" },
{ "CR", "es-CR" },
{ "CS", "sr-Cyrl-CS" },
{ "CZ", "cs-CZ" },
{ "DE", "de-DE" },
{ "DK", "da-DK" },
{ "DO", "es-DO" },
{ "DZ", "ar-DZ" },
{ "EC", "es-EC" },
{ "EE", "et-EE" },
{ "EG", "ar-EG" },
{ "ES", "es-ES" },
{ "ET", "am-ET" },
{ "FI", "fi-FI" },
{ "FO", "fo-FO" },
{ "FR", "fr-FR" },
{ "GB", "en-GB" },
{ "GE", "ka-GE" },
{ "GL", "kl-GL" },
{ "GR", "el-GR" },
{ "GT", "es-GT" },
{ "HK", "zh-HK" },
{ "HN", "es-HN" },
{ "HR", "hr-HR" },
{ "HU", "hu-HU" },
{ "ID", "id-ID" },
{ "IE", "en-IE" },
{ "IL", "he-IL" },
{ "IN", "hi-IN" },
{ "IQ", "ar-IQ" },
{ "IR", "fa-IR" },
{ "IS", "is-IS" },
{ "IT", "it-IT" },
{ "IV", "" },
{ "JM", "en-JM" },
{ "JO", "ar-JO" },
{ "JP", "ja-JP" },
{ "KE", "sw-KE" },
{ "KG", "ky-KG" },
{ "KH", "km-KH" },
{ "KR", "ko-KR" },
{ "KW", "ar-KW" },
{ "KZ", "kk-KZ" },
{ "LA", "lo-LA" },
{ "LB", "ar-LB" },
{ "LI", "de-LI" },
{ "LK", "si-LK" },
{ "LT", "lt-LT" },
{ "LU", "lb-LU" },
{ "LV", "lv-LV" },
{ "LY", "ar-LY" },
{ "MA", "ar-MA" },
{ "MC", "fr-MC" },
{ "ME", "sr-Latn-ME" },
{ "MK", "mk-MK" },
{ "MN", "mn-MN" },
{ "MO", "zh-MO" },
{ "MT", "mt-MT" },
{ "MV", "dv-MV" },
{ "MX", "es-MX" },
{ "MY", "ms-MY" },
{ "NG", "ig-NG" },
{ "NI", "es-NI" },
{ "NL", "nl-NL" },
{ "NO", "nn-NO" },
{ "NP", "ne-NP" },
{ "NZ", "en-NZ" },
{ "OM", "ar-OM" },
{ "PA", "es-PA" },
{ "PE", "es-PE" },
{ "PH", "en-PH" },
{ "PK", "ur-PK" },
{ "PL", "pl-PL" },
{ "PR", "es-PR" },
{ "PT", "pt-PT" },
{ "PY", "es-PY" },
{ "QA", "ar-QA" },
{ "RO", "ro-RO" },
{ "RS", "sr-Latn-RS" },
{ "RU", "ru-RU" },
{ "RW", "rw-RW" },
{ "SA", "ar-SA" },
{ "SE", "sv-SE" },
{ "SG", "zh-SG" },
{ "SI", "sl-SI" },
{ "SK", "sk-SK" },
{ "SN", "wo-SN" },
{ "SV", "es-SV" },
{ "SY", "ar-SY" },
{ "TH", "th-TH" },
{ "TJ", "tg-Cyrl-TJ" },
{ "TM", "tk-TM" },
{ "TN", "ar-TN" },
{ "TR", "tr-TR" },
{ "TT", "en-TT" },
{ "TW", "zh-TW" },
{ "UA", "uk-UA" },
{ "US", "en-US" },
{ "UY", "es-UY" },
{ "UZ", "uz-Cyrl-UZ" },
{ "VE", "es-VE" },
{ "VN", "vi-VN" },
{ "YE", "ar-YE" },
{ "ZA", "af-ZA" },
{ "ZW", "en-ZW" }
*/

View file

@ -0,0 +1,231 @@
#nullable disable
using Ellie.Common.TypeReaders.Models;
using Ellie.Modules.Administration.Services;
namespace Ellie.Modules.Administration;
public partial class Administration
{
[Group]
public partial class MuteCommands : EllieModule<MuteService>
{
private async Task<bool> VerifyMutePermissions(IGuildUser runnerUser, IGuildUser targetUser)
{
var runnerUserRoles = runnerUser.GetRoles();
var targetUserRoles = targetUser.GetRoles();
if (runnerUser.Id != ctx.Guild.OwnerId
&& runnerUserRoles.Max(x => x.Position) <= targetUserRoles.Max(x => x.Position))
{
await ReplyErrorLocalizedAsync(strs.mute_perms);
return false;
}
return true;
}
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageRoles)]
public async Task MuteRole([Leftover] IRole role = null)
{
if (role is null)
{
var muteRole = await _service.GetMuteRole(ctx.Guild);
await ReplyConfirmLocalizedAsync(strs.mute_role(Format.Code(muteRole.Name)));
return;
}
if (ctx.User.Id != ctx.Guild.OwnerId
&& role.Position >= ((SocketGuildUser)ctx.User).Roles.Max(x => x.Position))
{
await ReplyErrorLocalizedAsync(strs.insuf_perms_u);
return;
}
await _service.SetMuteRoleAsync(ctx.Guild.Id, role.Name);
await ReplyConfirmLocalizedAsync(strs.mute_role_set);
}
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageRoles | GuildPerm.MuteMembers)]
[Priority(0)]
public async Task Mute(IGuildUser target, [Leftover] string reason = "")
{
try
{
if (!await VerifyMutePermissions((IGuildUser)ctx.User, target))
return;
await _service.MuteUser(target, ctx.User, reason: reason);
await ReplyConfirmLocalizedAsync(strs.user_muted(Format.Bold(target.ToString())));
}
catch (Exception ex)
{
Log.Warning(ex, "Exception in the mute command");
await ReplyErrorLocalizedAsync(strs.mute_error);
}
}
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageRoles | GuildPerm.MuteMembers)]
[Priority(1)]
public async Task Mute(StoopidTime time, IGuildUser user, [Leftover] string reason = "")
{
if (time.Time < TimeSpan.FromMinutes(1) || time.Time > TimeSpan.FromDays(49))
return;
try
{
if (!await VerifyMutePermissions((IGuildUser)ctx.User, user))
return;
await _service.TimedMute(user, ctx.User, time.Time, reason: reason);
await ReplyConfirmLocalizedAsync(strs.user_muted_time(Format.Bold(user.ToString()),
(int)time.Time.TotalMinutes));
}
catch (Exception ex)
{
Log.Warning(ex, "Error in mute command");
await ReplyErrorLocalizedAsync(strs.mute_error);
}
}
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageRoles | GuildPerm.MuteMembers)]
public async Task Unmute(IGuildUser user, [Leftover] string reason = "")
{
try
{
await _service.UnmuteUser(user.GuildId, user.Id, ctx.User, reason: reason);
await ReplyConfirmLocalizedAsync(strs.user_unmuted(Format.Bold(user.ToString())));
}
catch
{
await ReplyErrorLocalizedAsync(strs.mute_error);
}
}
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageRoles)]
[Priority(0)]
public async Task ChatMute(IGuildUser user, [Leftover] string reason = "")
{
try
{
if (!await VerifyMutePermissions((IGuildUser)ctx.User, user))
return;
await _service.MuteUser(user, ctx.User, MuteType.Chat, reason);
await ReplyConfirmLocalizedAsync(strs.user_chat_mute(Format.Bold(user.ToString())));
}
catch (Exception ex)
{
Log.Warning(ex, "Exception in the chatmute command");
await ReplyErrorLocalizedAsync(strs.mute_error);
}
}
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageRoles)]
[Priority(1)]
public async Task ChatMute(StoopidTime time, IGuildUser user, [Leftover] string reason = "")
{
if (time.Time < TimeSpan.FromMinutes(1) || time.Time > TimeSpan.FromDays(49))
return;
try
{
if (!await VerifyMutePermissions((IGuildUser)ctx.User, user))
return;
await _service.TimedMute(user, ctx.User, time.Time, MuteType.Chat, reason);
await ReplyConfirmLocalizedAsync(strs.user_chat_mute_time(Format.Bold(user.ToString()),
(int)time.Time.TotalMinutes));
}
catch (Exception ex)
{
Log.Warning(ex, "Error in chatmute command");
await ReplyErrorLocalizedAsync(strs.mute_error);
}
}
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageRoles)]
public async Task ChatUnmute(IGuildUser user, [Leftover] string reason = "")
{
try
{
await _service.UnmuteUser(user.Guild.Id, user.Id, ctx.User, MuteType.Chat, reason);
await ReplyConfirmLocalizedAsync(strs.user_chat_unmute(Format.Bold(user.ToString())));
}
catch
{
await ReplyErrorLocalizedAsync(strs.mute_error);
}
}
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.MuteMembers)]
[Priority(0)]
public async Task VoiceMute(IGuildUser user, [Leftover] string reason = "")
{
try
{
if (!await VerifyMutePermissions((IGuildUser)ctx.User, user))
return;
await _service.MuteUser(user, ctx.User, MuteType.Voice, reason);
await ReplyConfirmLocalizedAsync(strs.user_voice_mute(Format.Bold(user.ToString())));
}
catch
{
await ReplyErrorLocalizedAsync(strs.mute_error);
}
}
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.MuteMembers)]
[Priority(1)]
public async Task VoiceMute(StoopidTime time, IGuildUser user, [Leftover] string reason = "")
{
if (time.Time < TimeSpan.FromMinutes(1) || time.Time > TimeSpan.FromDays(49))
return;
try
{
if (!await VerifyMutePermissions((IGuildUser)ctx.User, user))
return;
await _service.TimedMute(user, ctx.User, time.Time, MuteType.Voice, reason);
await ReplyConfirmLocalizedAsync(strs.user_voice_mute_time(Format.Bold(user.ToString()),
(int)time.Time.TotalMinutes));
}
catch
{
await ReplyErrorLocalizedAsync(strs.mute_error);
}
}
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.MuteMembers)]
public async Task VoiceUnmute(IGuildUser user, [Leftover] string reason = "")
{
try
{
await _service.UnmuteUser(user.GuildId, user.Id, ctx.User, MuteType.Voice, reason);
await ReplyConfirmLocalizedAsync(strs.user_voice_unmute(Format.Bold(user.ToString())));
}
catch
{
await ReplyErrorLocalizedAsync(strs.mute_error);
}
}
}
}

View file

@ -0,0 +1,505 @@
#nullable disable
using Microsoft.EntityFrameworkCore;
using Ellie.Db;
using Ellie.Services.Database.Models;
namespace Ellie.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 IEmbedBuilderService _eb;
public MuteService(DiscordSocketClient client, DbService db, IEmbedBuilderService eb)
{
_client = client;
_db = db;
_eb = eb;
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(() => user.SendMessageAsync(embed: _eb.Create()
.WithDescription(
$"You've been muted in {user.Guild} server")
.AddField("Mute Type", type.ToString())
.AddField("Moderator", mod.ToString())
.AddField("Reason", reason)
.Build()));
}
private void OnUserUnmuted(
IGuildUser user,
IUser mod,
MuteType type,
string reason)
{
if (string.IsNullOrWhiteSpace(reason))
return;
_ = Task.Run(() => user.SendMessageAsync(embed: _eb.Create()
.WithDescription(
$"You've been unmuted in {user.Guild} server")
.AddField("Unmute Type", type.ToString())
.AddField("Moderator", mod.ToString())
.AddField("Reason", reason)
.Build()));
}
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)
{
if (guild is null)
throw new ArgumentNullException(nameof(guild));
const string defaultMuteRoleName = "nadeko-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();
}
}

View file

@ -0,0 +1,80 @@
#nullable disable
using Ellie.Common.TypeReaders;
namespace Ellie.Modules.Administration;
public partial class Administration
{
[Group]
public partial class DiscordPermOverrideCommands : EllieModule<DiscordPermOverrideService>
{
// override stats, it should require that the user has managessages guild permission
// .po 'stats' add user guild managemessages
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.Administrator)]
public async Task DiscordPermOverride(CommandOrExprInfo cmd, params GuildPerm[] perms)
{
if (perms is null || perms.Length == 0)
{
await _service.RemoveOverride(ctx.Guild.Id, cmd.Name);
await ReplyConfirmLocalizedAsync(strs.perm_override_reset);
return;
}
var aggregatePerms = perms.Aggregate((acc, seed) => seed | acc);
await _service.AddOverride(ctx.Guild.Id, cmd.Name, aggregatePerms);
await ReplyConfirmLocalizedAsync(strs.perm_override(Format.Bold(aggregatePerms.ToString()),
Format.Code(cmd.Name)));
}
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.Administrator)]
public async Task DiscordPermOverrideReset()
{
var result = await PromptUserConfirmAsync(_eb.Create()
.WithOkColor()
.WithDescription(GetText(strs.perm_override_all_confirm)));
if (!result)
return;
await _service.ClearAllOverrides(ctx.Guild.Id);
await ReplyConfirmLocalizedAsync(strs.perm_override_all);
}
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.Administrator)]
public async Task DiscordPermOverrideList(int page = 1)
{
if (--page < 0)
return;
var overrides = await _service.GetAllOverrides(ctx.Guild.Id);
await ctx.SendPaginatedConfirmAsync(page,
curPage =>
{
var eb = _eb.Create().WithTitle(GetText(strs.perm_overrides)).WithOkColor();
var thisPageOverrides = overrides.Skip(9 * curPage).Take(9).ToList();
if (thisPageOverrides.Count == 0)
eb.WithDescription(GetText(strs.perm_override_page_none));
else
{
eb.WithDescription(thisPageOverrides.Select(ov => $"{ov.Command} => {ov.Perm.ToString()}")
.Join("\n"));
}
return eb;
},
overrides.Count,
9);
}
}
}

View file

@ -0,0 +1,60 @@
#nullable disable
using Ellie.Modules.Administration.Services;
namespace Ellie.Modules.Administration;
public partial class Administration
{
[Group]
public partial class PlayingRotateCommands : EllieModule<PlayingRotateService>
{
[Cmd]
[OwnerOnly]
public async Task RotatePlaying()
{
if (_service.ToggleRotatePlaying())
await ReplyConfirmLocalizedAsync(strs.ropl_enabled);
else
await ReplyConfirmLocalizedAsync(strs.ropl_disabled);
}
[Cmd]
[OwnerOnly]
public async Task AddPlaying(ActivityType t, [Leftover] string status)
{
await _service.AddPlaying(t, status);
await ReplyConfirmLocalizedAsync(strs.ropl_added);
}
[Cmd]
[OwnerOnly]
public async Task ListPlaying()
{
var statuses = _service.GetRotatingStatuses();
if (!statuses.Any())
await ReplyErrorLocalizedAsync(strs.ropl_not_set);
else
{
var i = 1;
await ReplyConfirmLocalizedAsync(strs.ropl_list(string.Join("\n\t",
statuses.Select(rs => $"`{i++}.` *{rs.Type}* {rs.Status}"))));
}
}
[Cmd]
[OwnerOnly]
public async Task RemovePlaying(int index)
{
index -= 1;
var msg = await _service.RemovePlayingAsync(index);
if (msg is null)
return;
await ReplyConfirmLocalizedAsync(strs.reprm(msg));
}
}
}

View file

@ -0,0 +1,109 @@
#nullable disable
using Microsoft.EntityFrameworkCore;
using Ellie.Common.ModuleBehaviors;
using Ellie.Services.Database.Models;
namespace Ellie.Modules.Administration.Services;
public sealed class PlayingRotateService : IEService, IReadyExecutor
{
private readonly BotConfigService _bss;
private readonly SelfService _selfService;
private readonly Replacer _rep;
private readonly DbService _db;
private readonly DiscordSocketClient _client;
public PlayingRotateService(
DiscordSocketClient client,
DbService db,
BotConfigService bss,
IEnumerable<IPlaceholderProvider> phProviders,
SelfService selfService)
{
_db = db;
_bss = bss;
_selfService = selfService;
_client = client;
if (client.ShardId == 0)
_rep = new ReplacementBuilder().WithClient(client).WithProviders(phProviders).Build();
}
public async Task OnReadyAsync()
{
if (_client.ShardId != 0)
return;
using var timer = new PeriodicTimer(TimeSpan.FromMinutes(1));
var index = 0;
while (await timer.WaitForNextTickAsync())
{
try
{
if (!_bss.Data.RotateStatuses)
continue;
IReadOnlyList<RotatingPlayingStatus> rotatingStatuses;
await using (var uow = _db.GetDbContext())
{
rotatingStatuses = uow.Set<RotatingPlayingStatus>().AsNoTracking().OrderBy(x => x.Id).ToList();
}
if (rotatingStatuses.Count == 0)
continue;
var playingStatus = index >= rotatingStatuses.Count
? rotatingStatuses[index = 0]
: rotatingStatuses[index++];
var statusText = _rep.Replace(playingStatus.Status);
await _selfService.SetGameAsync(statusText, (ActivityType)playingStatus.Type);
}
catch (Exception ex)
{
Log.Warning(ex, "Rotating playing status errored: {ErrorMessage}", ex.Message);
}
}
}
public async Task<string> RemovePlayingAsync(int index)
{
if (index < 0)
throw new ArgumentOutOfRangeException(nameof(index));
await using var uow = _db.GetDbContext();
var toRemove = await uow.Set<RotatingPlayingStatus>().AsQueryable().AsNoTracking().Skip(index).FirstOrDefaultAsync();
if (toRemove is null)
return null;
uow.Remove(toRemove);
await uow.SaveChangesAsync();
return toRemove.Status;
}
public async Task AddPlaying(ActivityType activityType, string status)
{
await using var uow = _db.GetDbContext();
var toAdd = new RotatingPlayingStatus
{
Status = status,
Type = (Ellie.Bot.Db.ActivityType)activityType
};
uow.Add(toAdd);
await uow.SaveChangesAsync();
}
public bool ToggleRotatePlaying()
{
var enabled = false;
_bss.ModifyConfig(bs => { enabled = bs.RotateStatuses = !bs.RotateStatuses; });
return enabled;
}
public IReadOnlyList<RotatingPlayingStatus> GetRotatingStatuses()
{
using var uow = _db.GetDbContext();
return uow.Set<RotatingPlayingStatus>().AsNoTracking().ToList();
}
}

View file

@ -0,0 +1,57 @@
#nullable disable
namespace Ellie.Modules.Administration;
public partial class Administration
{
[Group]
public partial class PrefixCommands : EllieModule
{
public enum Set
{
Set
}
[Cmd]
[Priority(1)]
public async Task Prefix()
=> await ReplyConfirmLocalizedAsync(strs.prefix_current(Format.Code(_cmdHandler.GetPrefix(ctx.Guild))));
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.Administrator)]
[Priority(0)]
public Task Prefix(Set _, [Leftover] string newPrefix)
=> Prefix(newPrefix);
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.Administrator)]
[Priority(0)]
public async Task Prefix([Leftover] string toSet)
{
if (string.IsNullOrWhiteSpace(prefix))
return;
var oldPrefix = prefix;
var newPrefix = _cmdHandler.SetPrefix(ctx.Guild, toSet);
await ReplyConfirmLocalizedAsync(strs.prefix_new(Format.Code(oldPrefix), Format.Code(newPrefix)));
}
[Cmd]
[OwnerOnly]
public async Task DefPrefix([Leftover] string toSet = null)
{
if (string.IsNullOrWhiteSpace(toSet))
{
await ReplyConfirmLocalizedAsync(strs.defprefix_current(_cmdHandler.GetPrefix()));
return;
}
var oldPrefix = _cmdHandler.GetPrefix();
var newPrefix = _cmdHandler.SetDefaultPrefix(toSet);
await ReplyConfirmLocalizedAsync(strs.defprefix_new(Format.Code(oldPrefix), Format.Code(newPrefix)));
}
}
}

View file

@ -0,0 +1,288 @@
#nullable disable
using Ellie.Common.TypeReaders.Models;
using Ellie.Modules.Administration.Services;
using Ellie.Services.Database.Models;
namespace Ellie.Modules.Administration;
public partial class Administration
{
[Group]
public partial class ProtectionCommands : EllieModule<ProtectionService>
{
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.Administrator)]
public async Task AntiAlt()
{
if (await _service.TryStopAntiAlt(ctx.Guild.Id))
{
await ReplyConfirmLocalizedAsync(strs.prot_disable("Anti-Alt"));
return;
}
await ReplyConfirmLocalizedAsync(strs.protection_not_running("Anti-Alt"));
}
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.Administrator)]
public async Task AntiAlt(
StoopidTime minAge,
PunishmentAction action,
[Leftover] StoopidTime punishTime = null)
{
var minAgeMinutes = (int)minAge.Time.TotalMinutes;
var punishTimeMinutes = (int?)punishTime?.Time.TotalMinutes ?? 0;
if (minAgeMinutes < 1 || punishTimeMinutes < 0)
return;
var minutes = (int?)punishTime?.Time.TotalMinutes ?? 0;
if (action is PunishmentAction.TimeOut && minutes < 1)
minutes = 1;
await _service.StartAntiAltAsync(ctx.Guild.Id,
minAgeMinutes,
action,
minutes);
await ctx.OkAsync();
}
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.Administrator)]
public async Task AntiAlt(StoopidTime minAge, PunishmentAction action, [Leftover] IRole role)
{
var minAgeMinutes = (int)minAge.Time.TotalMinutes;
if (minAgeMinutes < 1)
return;
if (action == PunishmentAction.TimeOut)
return;
await _service.StartAntiAltAsync(ctx.Guild.Id, minAgeMinutes, action, roleId: role.Id);
await ctx.OkAsync();
}
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.Administrator)]
public Task AntiRaid()
{
if (_service.TryStopAntiRaid(ctx.Guild.Id))
return ReplyConfirmLocalizedAsync(strs.prot_disable("Anti-Raid"));
return ReplyPendingLocalizedAsync(strs.protection_not_running("Anti-Raid"));
}
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.Administrator)]
[Priority(1)]
public Task AntiRaid(
int userThreshold,
int seconds,
PunishmentAction action,
[Leftover] StoopidTime punishTime)
=> InternalAntiRaid(userThreshold, seconds, action, punishTime);
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.Administrator)]
[Priority(2)]
public Task AntiRaid(int userThreshold, int seconds, PunishmentAction action)
=> InternalAntiRaid(userThreshold, seconds, action);
private async Task InternalAntiRaid(
int userThreshold,
int seconds = 10,
PunishmentAction action = PunishmentAction.Mute,
StoopidTime punishTime = null)
{
if (action == PunishmentAction.AddRole)
{
await ReplyErrorLocalizedAsync(strs.punishment_unsupported(action));
return;
}
if (userThreshold is < 2 or > 30)
{
await ReplyErrorLocalizedAsync(strs.raid_cnt(2, 30));
return;
}
if (seconds is < 2 or > 300)
{
await ReplyErrorLocalizedAsync(strs.raid_time(2, 300));
return;
}
if (punishTime is not null)
{
if (!_service.IsDurationAllowed(action))
await ReplyErrorLocalizedAsync(strs.prot_cant_use_time);
}
var time = (int?)punishTime?.Time.TotalMinutes ?? 0;
if (time is < 0 or > 60 * 24)
return;
if(action is PunishmentAction.TimeOut && time < 1)
return;
var stats = await _service.StartAntiRaidAsync(ctx.Guild.Id, userThreshold, seconds, action, time);
if (stats is null)
return;
await SendConfirmAsync(GetText(strs.prot_enable("Anti-Raid")),
$"{ctx.User.Mention} {GetAntiRaidString(stats)}");
}
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.Administrator)]
public Task AntiSpam()
{
if (_service.TryStopAntiSpam(ctx.Guild.Id))
return ReplyConfirmLocalizedAsync(strs.prot_disable("Anti-Spam"));
return ReplyPendingLocalizedAsync(strs.protection_not_running("Anti-Spam"));
}
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.Administrator)]
[Priority(0)]
public Task AntiSpam(int messageCount, PunishmentAction action, [Leftover] IRole role)
{
if (action != PunishmentAction.AddRole)
return Task.CompletedTask;
return InternalAntiSpam(messageCount, action, null, role);
}
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.Administrator)]
[Priority(1)]
public Task AntiSpam(int messageCount, PunishmentAction action, [Leftover] StoopidTime punishTime)
=> InternalAntiSpam(messageCount, action, punishTime);
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.Administrator)]
[Priority(2)]
public Task AntiSpam(int messageCount, PunishmentAction action)
=> InternalAntiSpam(messageCount, action);
private async Task InternalAntiSpam(
int messageCount,
PunishmentAction action,
StoopidTime timeData = null,
IRole role = null)
{
if (messageCount is < 2 or > 10)
return;
if (timeData is not null)
{
if (!_service.IsDurationAllowed(action))
await ReplyErrorLocalizedAsync(strs.prot_cant_use_time);
}
var time = (int?)timeData?.Time.TotalMinutes ?? 0;
if (time is < 0 or > 60 * 24)
return;
if (action is PunishmentAction.TimeOut && time < 1)
return;
var stats = await _service.StartAntiSpamAsync(ctx.Guild.Id, messageCount, action, time, role?.Id);
await SendConfirmAsync(GetText(strs.prot_enable("Anti-Spam")),
$"{ctx.User.Mention} {GetAntiSpamString(stats)}");
}
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.Administrator)]
public async Task AntispamIgnore()
{
var added = await _service.AntiSpamIgnoreAsync(ctx.Guild.Id, ctx.Channel.Id);
if (added is null)
{
await ReplyErrorLocalizedAsync(strs.protection_not_running("Anti-Spam"));
return;
}
if (added.Value)
await ReplyConfirmLocalizedAsync(strs.spam_ignore("Anti-Spam"));
else
await ReplyConfirmLocalizedAsync(strs.spam_not_ignore("Anti-Spam"));
}
[Cmd]
[RequireContext(ContextType.Guild)]
public async Task AntiList()
{
var (spam, raid, alt) = _service.GetAntiStats(ctx.Guild.Id);
if (spam is null && raid is null && alt is null)
{
await ReplyConfirmLocalizedAsync(strs.prot_none);
return;
}
var embed = _eb.Create().WithOkColor().WithTitle(GetText(strs.prot_active));
if (spam is not null)
embed.AddField("Anti-Spam", GetAntiSpamString(spam).TrimTo(1024), true);
if (raid is not null)
embed.AddField("Anti-Raid", GetAntiRaidString(raid).TrimTo(1024), true);
if (alt is not null)
embed.AddField("Anti-Alt", GetAntiAltString(alt), true);
await ctx.Channel.EmbedAsync(embed);
}
private string GetAntiAltString(AntiAltStats alt)
=> GetText(strs.anti_alt_status(Format.Bold(alt.MinAge.ToString(@"dd\d\ hh\h\ mm\m\ ")),
Format.Bold(alt.Action.ToString()),
Format.Bold(alt.Counter.ToString())));
private string GetAntiSpamString(AntiSpamStats stats)
{
var settings = stats.AntiSpamSettings;
var ignoredString = string.Join(", ", settings.IgnoredChannels.Select(c => $"<#{c.ChannelId}>"));
if (string.IsNullOrWhiteSpace(ignoredString))
ignoredString = "none";
var add = string.Empty;
if (settings.MuteTime > 0)
add = $" ({TimeSpan.FromMinutes(settings.MuteTime):hh\\hmm\\m})";
return GetText(strs.spam_stats(Format.Bold(settings.MessageThreshold.ToString()),
Format.Bold(settings.Action + add),
ignoredString));
}
private string GetAntiRaidString(AntiRaidStats stats)
{
var actionString = Format.Bold(stats.AntiRaidSettings.Action.ToString());
if (stats.AntiRaidSettings.PunishDuration > 0)
actionString += $" **({TimeSpan.FromMinutes(stats.AntiRaidSettings.PunishDuration):hh\\hmm\\m})**";
return GetText(strs.raid_stats(Format.Bold(stats.AntiRaidSettings.UserThreshold.ToString()),
Format.Bold(stats.AntiRaidSettings.Seconds.ToString()),
actionString));
}
}
}

View file

@ -0,0 +1,499 @@
#nullable disable
using Microsoft.EntityFrameworkCore;
using Ellie.Db;
using Ellie.Services.Database.Models;
using System.Threading.Channels;
namespace Ellie.Modules.Administration.Services;
public class ProtectionService : IEService
{
public event Func<PunishmentAction, ProtectionType, IGuildUser[], Task> OnAntiProtectionTriggered = delegate
{
return Task.CompletedTask;
};
private readonly ConcurrentDictionary<ulong, AntiRaidStats> _antiRaidGuilds = new();
private readonly ConcurrentDictionary<ulong, AntiSpamStats> _antiSpamGuilds = new();
private readonly ConcurrentDictionary<ulong, AntiAltStats> _antiAltGuilds = new();
private readonly DiscordSocketClient _client;
private readonly MuteService _mute;
private readonly DbService _db;
private readonly UserPunishService _punishService;
private readonly Channel<PunishQueueItem> _punishUserQueue =
Channel.CreateUnbounded<PunishQueueItem>(new()
{
SingleReader = true,
SingleWriter = false
});
public ProtectionService(
DiscordSocketClient client,
IBot bot,
MuteService mute,
DbService db,
UserPunishService punishService)
{
_client = client;
_mute = mute;
_db = db;
_punishService = punishService;
var ids = client.GetGuildIds();
using (var uow = db.GetDbContext())
{
var configs = uow.Set<GuildConfig>()
.AsQueryable()
.Include(x => x.AntiRaidSetting)
.Include(x => x.AntiSpamSetting)
.ThenInclude(x => x.IgnoredChannels)
.Include(x => x.AntiAltSetting)
.Where(x => ids.Contains(x.GuildId))
.ToList();
foreach (var gc in configs)
Initialize(gc);
}
_client.MessageReceived += HandleAntiSpam;
_client.UserJoined += HandleUserJoined;
bot.JoinedGuild += _bot_JoinedGuild;
_client.LeftGuild += _client_LeftGuild;
_ = Task.Run(RunQueue);
}
private async Task RunQueue()
{
while (true)
{
var item = await _punishUserQueue.Reader.ReadAsync();
var muteTime = item.MuteTime;
var gu = item.User;
try
{
await _punishService.ApplyPunishment(gu.Guild,
gu,
_client.CurrentUser,
item.Action,
muteTime,
item.RoleId,
$"{item.Type} Protection");
}
catch (Exception ex)
{
Log.Warning(ex, "Error in punish queue: {Message}", ex.Message);
}
finally
{
await Task.Delay(1000);
}
}
}
private Task _client_LeftGuild(SocketGuild guild)
{
_ = Task.Run(async () =>
{
TryStopAntiRaid(guild.Id);
TryStopAntiSpam(guild.Id);
await TryStopAntiAlt(guild.Id);
});
return Task.CompletedTask;
}
private Task _bot_JoinedGuild(GuildConfig gc)
{
using var uow = _db.GetDbContext();
var gcWithData = uow.GuildConfigsForId(gc.GuildId,
set => set.Include(x => x.AntiRaidSetting)
.Include(x => x.AntiAltSetting)
.Include(x => x.AntiSpamSetting)
.ThenInclude(x => x.IgnoredChannels));
Initialize(gcWithData);
return Task.CompletedTask;
}
private void Initialize(GuildConfig gc)
{
var raid = gc.AntiRaidSetting;
var spam = gc.AntiSpamSetting;
if (raid is not null)
{
var raidStats = new AntiRaidStats
{
AntiRaidSettings = raid
};
_antiRaidGuilds[gc.GuildId] = raidStats;
}
if (spam is not null)
{
_antiSpamGuilds[gc.GuildId] = new()
{
AntiSpamSettings = spam
};
}
var alt = gc.AntiAltSetting;
if (alt is not null)
_antiAltGuilds[gc.GuildId] = new(alt);
}
private Task HandleUserJoined(SocketGuildUser user)
{
if (user.IsBot)
return Task.CompletedTask;
_antiRaidGuilds.TryGetValue(user.Guild.Id, out var maybeStats);
_antiAltGuilds.TryGetValue(user.Guild.Id, out var maybeAlts);
if (maybeStats is null && maybeAlts is null)
return Task.CompletedTask;
_ = Task.Run(async () =>
{
if (maybeAlts is { } alts)
{
if (user.CreatedAt != default)
{
var diff = DateTime.UtcNow - user.CreatedAt.UtcDateTime;
if (diff < alts.MinAge)
{
alts.Increment();
await PunishUsers(alts.Action,
ProtectionType.Alting,
alts.ActionDurationMinutes,
alts.RoleId,
user);
return;
}
}
}
try
{
if (maybeStats is not { } stats || !stats.RaidUsers.Add(user))
return;
++stats.UsersCount;
if (stats.UsersCount >= stats.AntiRaidSettings.UserThreshold)
{
var users = stats.RaidUsers.ToArray();
stats.RaidUsers.Clear();
var settings = stats.AntiRaidSettings;
await PunishUsers(settings.Action, ProtectionType.Raiding, settings.PunishDuration, null, users);
}
await Task.Delay(1000 * stats.AntiRaidSettings.Seconds);
stats.RaidUsers.TryRemove(user);
--stats.UsersCount;
}
catch
{
// ignored
}
});
return Task.CompletedTask;
}
private Task HandleAntiSpam(SocketMessage arg)
{
if (arg is not SocketUserMessage msg || msg.Author.IsBot)
return Task.CompletedTask;
if (msg.Channel is not ITextChannel channel)
return Task.CompletedTask;
_ = Task.Run(async () =>
{
try
{
if (!_antiSpamGuilds.TryGetValue(channel.Guild.Id, out var spamSettings)
|| spamSettings.AntiSpamSettings.IgnoredChannels.Contains(new()
{
ChannelId = channel.Id
}))
return;
var stats = spamSettings.UserStats.AddOrUpdate(msg.Author.Id,
_ => new(msg),
(_, old) =>
{
old.ApplyNextMessage(msg);
return old;
});
if (stats.Count >= spamSettings.AntiSpamSettings.MessageThreshold)
{
if (spamSettings.UserStats.TryRemove(msg.Author.Id, out stats))
{
var settings = spamSettings.AntiSpamSettings;
await PunishUsers(settings.Action,
ProtectionType.Spamming,
settings.MuteTime,
settings.RoleId,
(IGuildUser)msg.Author);
}
}
}
catch
{
// ignored
}
});
return Task.CompletedTask;
}
private async Task PunishUsers(
PunishmentAction action,
ProtectionType pt,
int muteTime,
ulong? roleId,
params IGuildUser[] gus)
{
Log.Information("[{PunishType}] - Punishing [{Count}] users with [{PunishAction}] in {GuildName} guild",
pt,
gus.Length,
action,
gus[0].Guild.Name);
foreach (var gu in gus)
{
await _punishUserQueue.Writer.WriteAsync(new()
{
Action = action,
Type = pt,
User = gu,
MuteTime = muteTime,
RoleId = roleId
});
}
_ = OnAntiProtectionTriggered(action, pt, gus);
}
public async Task<AntiRaidStats> StartAntiRaidAsync(
ulong guildId,
int userThreshold,
int seconds,
PunishmentAction action,
int minutesDuration)
{
var g = _client.GetGuild(guildId);
await _mute.GetMuteRole(g);
if (action == PunishmentAction.AddRole)
return null;
if (!IsDurationAllowed(action))
minutesDuration = 0;
var stats = new AntiRaidStats
{
AntiRaidSettings = new()
{
Action = action,
Seconds = seconds,
UserThreshold = userThreshold,
PunishDuration = minutesDuration
}
};
_antiRaidGuilds.AddOrUpdate(guildId, stats, (_, _) => stats);
await using var uow = _db.GetDbContext();
var gc = uow.GuildConfigsForId(guildId, set => set.Include(x => x.AntiRaidSetting));
gc.AntiRaidSetting = stats.AntiRaidSettings;
await uow.SaveChangesAsync();
return stats;
}
public bool TryStopAntiRaid(ulong guildId)
{
if (_antiRaidGuilds.TryRemove(guildId, out _))
{
using var uow = _db.GetDbContext();
var gc = uow.GuildConfigsForId(guildId, set => set.Include(x => x.AntiRaidSetting));
gc.AntiRaidSetting = null;
uow.SaveChanges();
return true;
}
return false;
}
public bool TryStopAntiSpam(ulong guildId)
{
if (_antiSpamGuilds.TryRemove(guildId, out _))
{
using var uow = _db.GetDbContext();
var gc = uow.GuildConfigsForId(guildId,
set => set.Include(x => x.AntiSpamSetting).ThenInclude(x => x.IgnoredChannels));
gc.AntiSpamSetting = null;
uow.SaveChanges();
return true;
}
return false;
}
public async Task<AntiSpamStats> StartAntiSpamAsync(
ulong guildId,
int messageCount,
PunishmentAction action,
int punishDurationMinutes,
ulong? roleId)
{
var g = _client.GetGuild(guildId);
await _mute.GetMuteRole(g);
if (!IsDurationAllowed(action))
punishDurationMinutes = 0;
var stats = new AntiSpamStats
{
AntiSpamSettings = new()
{
Action = action,
MessageThreshold = messageCount,
MuteTime = punishDurationMinutes,
RoleId = roleId
}
};
stats = _antiSpamGuilds.AddOrUpdate(guildId,
stats,
(_, old) =>
{
stats.AntiSpamSettings.IgnoredChannels = old.AntiSpamSettings.IgnoredChannels;
return stats;
});
await using var uow = _db.GetDbContext();
var gc = uow.GuildConfigsForId(guildId, set => set.Include(x => x.AntiSpamSetting));
if (gc.AntiSpamSetting is not null)
{
gc.AntiSpamSetting.Action = stats.AntiSpamSettings.Action;
gc.AntiSpamSetting.MessageThreshold = stats.AntiSpamSettings.MessageThreshold;
gc.AntiSpamSetting.MuteTime = stats.AntiSpamSettings.MuteTime;
gc.AntiSpamSetting.RoleId = stats.AntiSpamSettings.RoleId;
}
else
gc.AntiSpamSetting = stats.AntiSpamSettings;
await uow.SaveChangesAsync();
return stats;
}
public async Task<bool?> AntiSpamIgnoreAsync(ulong guildId, ulong channelId)
{
var obj = new AntiSpamIgnore
{
ChannelId = channelId
};
bool added;
await using var uow = _db.GetDbContext();
var gc = uow.GuildConfigsForId(guildId,
set => set.Include(x => x.AntiSpamSetting).ThenInclude(x => x.IgnoredChannels));
var spam = gc.AntiSpamSetting;
if (spam is null)
return null;
if (spam.IgnoredChannels.Add(obj)) // if adding to db is successful
{
if (_antiSpamGuilds.TryGetValue(guildId, out var temp))
temp.AntiSpamSettings.IgnoredChannels.Add(obj); // add to local cache
added = true;
}
else
{
var toRemove = spam.IgnoredChannels.First(x => x.ChannelId == channelId);
uow.Set<AntiSpamIgnore>().Remove(toRemove); // remove from db
if (_antiSpamGuilds.TryGetValue(guildId, out var temp))
temp.AntiSpamSettings.IgnoredChannels.Remove(toRemove); // remove from local cache
added = false;
}
await uow.SaveChangesAsync();
return added;
}
public (AntiSpamStats, AntiRaidStats, AntiAltStats) GetAntiStats(ulong guildId)
{
_antiRaidGuilds.TryGetValue(guildId, out var antiRaidStats);
_antiSpamGuilds.TryGetValue(guildId, out var antiSpamStats);
_antiAltGuilds.TryGetValue(guildId, out var antiAltStats);
return (antiSpamStats, antiRaidStats, antiAltStats);
}
public bool IsDurationAllowed(PunishmentAction action)
{
switch (action)
{
case PunishmentAction.Ban:
case PunishmentAction.Mute:
case PunishmentAction.ChatMute:
case PunishmentAction.VoiceMute:
case PunishmentAction.AddRole:
case PunishmentAction.TimeOut:
return true;
default:
return false;
}
}
public async Task StartAntiAltAsync(
ulong guildId,
int minAgeMinutes,
PunishmentAction action,
int actionDurationMinutes = 0,
ulong? roleId = null)
{
await using var uow = _db.GetDbContext();
var gc = uow.GuildConfigsForId(guildId, set => set.Include(x => x.AntiAltSetting));
gc.AntiAltSetting = new()
{
Action = action,
ActionDurationMinutes = actionDurationMinutes,
MinAge = TimeSpan.FromMinutes(minAgeMinutes),
RoleId = roleId
};
await uow.SaveChangesAsync();
_antiAltGuilds[guildId] = new(gc.AntiAltSetting);
}
public async Task<bool> TryStopAntiAlt(ulong guildId)
{
if (!_antiAltGuilds.TryRemove(guildId, out _))
return false;
await using var uow = _db.GetDbContext();
var gc = uow.GuildConfigsForId(guildId, set => set.Include(x => x.AntiAltSetting));
gc.AntiAltSetting = null;
await uow.SaveChangesAsync();
return true;
}
}

View file

@ -0,0 +1,52 @@
#nullable disable
using Ellie.Services.Database.Models;
namespace Ellie.Modules.Administration;
public enum ProtectionType
{
Raiding,
Spamming,
Alting
}
public class AntiRaidStats
{
public AntiRaidSetting AntiRaidSettings { get; set; }
public int UsersCount { get; set; }
public ConcurrentHashSet<IGuildUser> RaidUsers { get; set; } = new();
}
public class AntiSpamStats
{
public AntiSpamSetting AntiSpamSettings { get; set; }
public ConcurrentDictionary<ulong, UserSpamStats> UserStats { get; set; } = new();
}
public class AntiAltStats
{
public PunishmentAction Action
=> _setting.Action;
public int ActionDurationMinutes
=> _setting.ActionDurationMinutes;
public ulong? RoleId
=> _setting.RoleId;
public TimeSpan MinAge
=> _setting.MinAge;
public int Counter
=> counter;
private readonly AntiAltSetting _setting;
private int counter;
public AntiAltStats(AntiAltSetting setting)
=> _setting = setting;
public void Increment()
=> Interlocked.Increment(ref counter);
}

View file

@ -0,0 +1,13 @@
#nullable disable
using Ellie.Services.Database.Models;
namespace Ellie.Modules.Administration;
public class PunishQueueItem
{
public PunishmentAction Action { get; set; }
public ProtectionType Type { get; set; }
public int MuteTime { get; set; }
public ulong? RoleId { get; set; }
public IGuildUser User { get; set; }
}

View file

@ -0,0 +1,64 @@
#nullable disable
namespace Ellie.Modules.Administration;
public sealed class UserSpamStats
{
public int Count
{
get
{
lock (_applyLock)
{
Cleanup();
return _messageTracker.Count;
}
}
}
private string lastMessage;
private readonly Queue<DateTime> _messageTracker;
private readonly object _applyLock = new();
private readonly TimeSpan _maxTime = TimeSpan.FromMinutes(30);
public UserSpamStats(IUserMessage msg)
{
lastMessage = msg.Content.ToUpperInvariant();
_messageTracker = new();
ApplyNextMessage(msg);
}
public void ApplyNextMessage(IUserMessage message)
{
var upperMsg = message.Content.ToUpperInvariant();
lock (_applyLock)
{
if (upperMsg != lastMessage || (string.IsNullOrWhiteSpace(upperMsg) && message.Attachments.Any()))
{
// if it's a new message, reset spam counter
lastMessage = upperMsg;
_messageTracker.Clear();
}
_messageTracker.Enqueue(DateTime.UtcNow);
}
}
private void Cleanup()
{
lock (_applyLock)
{
while (_messageTracker.TryPeek(out var dateTime))
{
if (DateTime.UtcNow - dateTime < _maxTime)
break;
_messageTracker.Dequeue();
}
}
}
}

View file

@ -0,0 +1,114 @@
#nullable disable
using CommandLine;
using Ellie.Modules.Administration.Services;
namespace Ellie.Modules.Administration;
public partial class Administration
{
[Group]
public partial class PruneCommands : EllieModule<PruneService>
{
private static readonly TimeSpan _twoWeeks = TimeSpan.FromDays(14);
public sealed class PruneOptions : IEllieCommandOptions
{
[Option(shortName: 's', longName: "safe", Default = false, HelpText = "Whether pinned messages should be deleted.", Required = false)]
public bool Safe { get; set; }
[Option(shortName: 'a', longName: "after", Default = null, HelpText = "Prune only messages after the specified message ID.", Required = false)]
public ulong? After { get; set; }
public void NormalizeOptions()
{
}
}
//deletes her own messages, no perm required
[Cmd]
[RequireContext(ContextType.Guild)]
[EllieOptions<PruneOptions>]
public async Task Prune(params string[] args)
{
var (opts, _) = OptionsParser.ParseFrom(new PruneOptions(), args);
var user = await ctx.Guild.GetCurrentUserAsync();
if (opts.Safe)
await _service.PruneWhere((ITextChannel)ctx.Channel, 100, x => x.Author.Id == user.Id && !x.IsPinned, opts.After);
else
await _service.PruneWhere((ITextChannel)ctx.Channel, 100, x => x.Author.Id == user.Id, opts.After);
ctx.Message.DeleteAfter(3);
}
// prune x
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(ChannelPerm.ManageMessages)]
[BotPerm(ChannelPerm.ManageMessages)]
[EllieOptions<PruneOptions>]
[Priority(1)]
public async Task Prune(int count, params string[] args)
{
count++;
if (count < 1)
return;
if (count > 1000)
count = 1000;
var (opts, _) = OptionsParser.ParseFrom<PruneOptions>(new PruneOptions(), args);
if (opts.Safe)
await _service.PruneWhere((ITextChannel)ctx.Channel, count, x => !x.IsPinned, opts.After);
else
await _service.PruneWhere((ITextChannel)ctx.Channel, count, _ => true, opts.After);
}
//prune @user [x]
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(ChannelPerm.ManageMessages)]
[BotPerm(ChannelPerm.ManageMessages)]
[EllieOptions<PruneOptions>]
[Priority(0)]
public Task Prune(IGuildUser user, int count = 100, params string[] args)
=> Prune(user.Id, count, args);
//prune userid [x]
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(ChannelPerm.ManageMessages)]
[BotPerm(ChannelPerm.ManageMessages)]
[EllieOptions<PruneOptions>]
[Priority(0)]
public async Task Prune(ulong userId, int count = 100, params string[] args)
{
if (userId == ctx.User.Id)
count++;
if (count < 1)
return;
if (count > 1000)
count = 1000;
var (opts, _) = OptionsParser.ParseFrom<PruneOptions>(new PruneOptions(), args);
if (opts.Safe)
{
await _service.PruneWhere((ITextChannel)ctx.Channel,
count,
m => m.Author.Id == userId && DateTime.UtcNow - m.CreatedAt < _twoWeeks && !m.IsPinned,
opts.After);
}
else
{
await _service.PruneWhere((ITextChannel)ctx.Channel,
count,
m => m.Author.Id == userId && DateTime.UtcNow - m.CreatedAt < _twoWeeks,
opts.After);
}
}
}
}

View file

@ -0,0 +1,89 @@
#nullable disable
namespace Ellie.Modules.Administration.Services;
public class PruneService : IEService
{
//channelids where prunes are currently occuring
private readonly ConcurrentHashSet<ulong> _pruningGuilds = new();
private readonly TimeSpan _twoWeeks = TimeSpan.FromDays(14);
private readonly ILogCommandService _logService;
public PruneService(ILogCommandService logService)
=> _logService = logService;
public async Task PruneWhere(ITextChannel channel, int amount, Func<IMessage, bool> predicate, ulong? after = null)
{
ArgumentNullException.ThrowIfNull(channel, nameof(channel));
if (amount <= 0)
throw new ArgumentOutOfRangeException(nameof(amount));
if (!_pruningGuilds.Add(channel.GuildId))
return;
try
{
var now = DateTime.UtcNow;
IMessage[] msgs;
IMessage lastMessage = null;
var dled = await channel.GetMessagesAsync(50).FlattenAsync();
msgs = dled
.Where(predicate)
.Where(x => after is ulong a ? x.Id > a : true)
.Take(amount)
.ToArray();
while (amount > 0 && msgs.Any())
{
lastMessage = msgs[^1];
var bulkDeletable = new List<IMessage>();
var singleDeletable = new List<IMessage>();
foreach (var x in msgs)
{
_logService.AddDeleteIgnore(x.Id);
if (now - x.CreatedAt < _twoWeeks)
bulkDeletable.Add(x);
else
singleDeletable.Add(x);
}
if (bulkDeletable.Count > 0)
{
await channel.DeleteMessagesAsync(bulkDeletable);
await Task.Delay(2000);
}
foreach (var group in singleDeletable.Chunk(5))
{
await group.Select(x => x.DeleteAsync()).WhenAll();
await Task.Delay(5000);
}
//this isn't good, because this still work as if i want to remove only specific user's messages from the last
//100 messages, Maybe this needs to be reduced by msgs.Length instead of 100
amount -= 50;
if (amount > 0)
{
dled = await channel.GetMessagesAsync(lastMessage, Direction.Before, 50).FlattenAsync();
msgs = dled
.Where(predicate)
.Where(x => after is ulong a ? x.Id > a : true)
.Take(amount)
.ToArray();
}
}
}
catch
{
//ignore
}
finally
{
_pruningGuilds.TryRemove(channel.GuildId);
}
}
}

View file

@ -0,0 +1,52 @@
#nullable disable
using Ellie.Modules.Patronage;
using Ellie.Services.Database.Models;
using OneOf;
using OneOf.Types;
namespace Ellie.Modules.Administration.Services;
public interface IReactionRoleService
{
/// <summary>
/// Adds a single reaction role
/// </summary>
/// <param name="guild">Guild where to add a reaction role</param>
/// <param name="msg">Message to which to add a reaction role</param>
/// <param name="emote"></param>
/// <param name="role"></param>
/// <param name="group"></param>
/// <param name="levelReq"></param>
/// <returns>The result of the operation</returns>
Task<OneOf<Success, FeatureLimit>> AddReactionRole(
IGuild guild,
IMessage msg,
string emote,
IRole role,
int group = 0,
int levelReq = 0);
/// <summary>
/// Get all reaction roles on the specified server
/// </summary>
/// <param name="guildId"></param>
/// <returns></returns>
Task<IReadOnlyCollection<ReactionRoleV2>> GetReactionRolesAsync(ulong guildId);
/// <summary>
/// Remove reaction roles on the specified message
/// </summary>
/// <param name="guildId"></param>
/// <param name="messageId"></param>
/// <returns></returns>
Task<bool> RemoveReactionRoles(ulong guildId, ulong messageId);
/// <summary>
/// Remove all reaction roles in the specified server
/// </summary>
/// <param name="guildId"></param>
/// <returns></returns>
Task<int> RemoveAllReactionRoles(ulong guildId);
Task<IReadOnlyCollection<IEmote>> TransferReactionRolesAsync(ulong guildId, ulong fromMessageId, ulong toMessageId);
}

View file

@ -0,0 +1,171 @@
using Ellie.Modules.Administration.Services;
namespace Ellie.Modules.Administration;
public partial class Administration
{
public partial class ReactionRoleCommands : EllieModule
{
private readonly IReactionRoleService _rero;
public ReactionRoleCommands(IReactionRoleService rero)
{
_rero = rero;
}
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageRoles)]
[BotPerm(GuildPerm.ManageRoles)]
public async Task ReactionRoleAdd(
ulong messageId,
string emoteStr,
IRole role,
int group = 0,
int levelReq = 0)
{
if (group < 0)
return;
if (levelReq < 0)
return;
var msg = await ctx.Channel.GetMessageAsync(messageId);
if (msg is null)
{
await ReplyErrorLocalizedAsync(strs.not_found);
return;
}
if (ctx.User.Id != ctx.Guild.OwnerId && ((IGuildUser)ctx.User).GetRoles().Max(x => x.Position) <= role.Position)
{
await ReplyErrorLocalizedAsync(strs.hierarchy);
return;
}
var emote = emoteStr.ToIEmote();
await msg.AddReactionAsync(emote);
var res = await _rero.AddReactionRole(ctx.Guild,
msg,
emoteStr,
role,
group,
levelReq);
await res.Match(
_ => ctx.OkAsync(),
fl =>
{
_ = msg.RemoveReactionAsync(emote, ctx.Client.CurrentUser);
return !fl.IsPatronLimit
? ReplyErrorLocalizedAsync(strs.limit_reached(fl.Quota))
: ReplyPendingLocalizedAsync(strs.feature_limit_reached_owner(fl.Quota, fl.Name));
});
}
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageRoles)]
[BotPerm(GuildPerm.ManageRoles)]
public async Task ReactionRolesList(int page = 1)
{
if (--page < 0)
return;
var reros = await _rero.GetReactionRolesAsync(ctx.Guild.Id);
await ctx.SendPaginatedConfirmAsync(page, curPage =>
{
var embed = _eb.Create(ctx)
.WithOkColor();
var content = string.Empty;
foreach (var g in reros.OrderBy(x => x.Group)
.Skip(curPage * 10)
.GroupBy(x => x.MessageId)
.OrderBy(x => x.Key))
{
var messageId = g.Key;
content +=
$"[{messageId}](https://discord.com/channels/{ctx.Guild.Id}/{g.First().ChannelId}/{g.Key})\n";
var groupGroups = g.GroupBy(x => x.Group);
foreach (var ggs in groupGroups)
{
content += $"`< {(g.Key == 0 ? ("Not Exclusive (Group 0)") : ($"Group {ggs.Key}"))} >`\n";
foreach (var rero in ggs)
{
content +=
$"\t{rero.Emote} -> {(ctx.Guild.GetRole(rero.RoleId)?.Mention ?? "<missing role>")}";
if (rero.LevelReq > 0)
content += $" (lvl {rero.LevelReq}+)";
content += '\n';
}
}
}
embed.WithDescription(string.IsNullOrWhiteSpace(content)
? "There are no reaction roles on this server"
: content);
return embed;
}, reros.Count, 10);
}
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageRoles)]
[BotPerm(GuildPerm.ManageRoles)]
public async Task ReactionRolesRemove(ulong messageId)
{
var succ = await _rero.RemoveReactionRoles(ctx.Guild.Id, messageId);
if (succ)
await ctx.OkAsync();
else
await ctx.ErrorAsync();
}
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageRoles)]
[BotPerm(GuildPerm.ManageRoles)]
public async Task ReactionRolesDeleteAll()
{
await _rero.RemoveAllReactionRoles(ctx.Guild.Id);
await ctx.OkAsync();
}
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageRoles)]
[BotPerm(GuildPerm.ManageRoles)]
[Ratelimit(60)]
public async Task ReactionRolesTransfer(ulong fromMessageId, ulong toMessageId)
{
var msg = await ctx.Channel.GetMessageAsync(toMessageId);
if (msg is null)
{
await ctx.ErrorAsync();
return;
}
var reactions = await _rero.TransferReactionRolesAsync(ctx.Guild.Id, fromMessageId, toMessageId);
if (reactions.Count == 0)
{
await ctx.ErrorAsync();
}
else
{
foreach (var r in reactions)
{
await msg.AddReactionAsync(r);
}
}
}
}
}

View file

@ -0,0 +1,396 @@
#nullable disable
using LinqToDB;
using LinqToDB.EntityFrameworkCore;
using Ellie.Common.ModuleBehaviors;
using Ellie.Db;
using Ellie.Modules.Patronage;
using Ellie.Services.Database.Models;
using OneOf.Types;
using OneOf;
namespace Ellie.Modules.Administration.Services;
public sealed class ReactionRolesService : IReadyExecutor, IEService, IReactionRoleService
{
private readonly DbService _db;
private readonly DiscordSocketClient _client;
private readonly IBotCredentials _creds;
private ConcurrentDictionary<ulong, List<ReactionRoleV2>> _cache;
private readonly object _cacheLock = new();
private readonly SemaphoreSlim _assignementLock = new(1, 1);
private readonly IPatronageService _ps;
private static readonly FeatureLimitKey _reroFLKey = new()
{
Key = "rero:max_count",
PrettyName = "Reaction Role"
};
public ReactionRolesService(
DiscordSocketClient client,
DbService db,
IBotCredentials creds,
IPatronageService ps)
{
_db = db;
_ps = ps;
_client = client;
_creds = creds;
_cache = new();
}
public async Task OnReadyAsync()
{
await using var uow = _db.GetDbContext();
var reros = await uow.GetTable<ReactionRoleV2>()
.Where(
x => Linq2DbExpressions.GuildOnShard(x.GuildId, _creds.TotalShards, _client.ShardId))
.ToListAsyncLinqToDB();
foreach (var group in reros.GroupBy(x => x.MessageId))
{
_cache[group.Key] = group.ToList();
}
_client.ReactionAdded += ClientOnReactionAdded;
_client.ReactionRemoved += ClientOnReactionRemoved;
}
private async Task<(IGuildUser, IRole)> GetUserAndRoleAsync(
ulong userId,
ReactionRoleV2 rero)
{
var guild = _client.GetGuild(rero.GuildId);
var role = guild?.GetRole(rero.RoleId);
if (role is null)
return default;
var user = guild.GetUser(userId) as IGuildUser
?? await _client.Rest.GetGuildUserAsync(guild.Id, userId);
if (user is null)
return default;
return (user, role);
}
private Task ClientOnReactionRemoved(
Cacheable<IUserMessage, ulong> cmsg,
Cacheable<IMessageChannel, ulong> ch,
SocketReaction r)
{
if (!_cache.TryGetValue(cmsg.Id, out var reros))
return Task.CompletedTask;
_ = Task.Run(async () =>
{
var emote = await GetFixedEmoteAsync(cmsg, r.Emote);
var rero = reros.FirstOrDefault(x => x.Emote == emote.Name
|| x.Emote == emote.ToString());
if (rero is null)
return;
var (user, role) = await GetUserAndRoleAsync(r.UserId, rero);
if (user.IsBot)
return;
await _assignementLock.WaitAsync();
try
{
if (user.RoleIds.Contains(role.Id))
{
await user.RemoveRoleAsync(role.Id);
}
}
finally
{
_assignementLock.Release();
}
});
return Task.CompletedTask;
}
// had to add this because for some reason, reactionremoved event's reaction doesn't have IsAnimated set,
// causing the .ToString() to be wrong on animated custom emotes
private async Task<IEmote> GetFixedEmoteAsync(
Cacheable<IUserMessage, ulong> cmsg,
IEmote inputEmote)
{
// this should only run for emote
if (inputEmote is not Emote e)
return inputEmote;
// try to get the message and pull
var msg = await cmsg.GetOrDownloadAsync();
var emote = msg.Reactions.Keys.FirstOrDefault(x => e.Equals(x));
return emote ?? inputEmote;
}
private Task ClientOnReactionAdded(
Cacheable<IUserMessage, ulong> msg,
Cacheable<IMessageChannel, ulong> ch,
SocketReaction r)
{
if (!_cache.TryGetValue(msg.Id, out var reros))
return Task.CompletedTask;
_ = Task.Run(async () =>
{
var rero = reros.FirstOrDefault(x => x.Emote == r.Emote.Name || x.Emote == r.Emote.ToString());
if (rero is null)
return;
var (user, role) = await GetUserAndRoleAsync(r.UserId, rero);
if (user.IsBot)
return;
await _assignementLock.WaitAsync();
try
{
if (!user.RoleIds.Contains(role.Id))
{
// first check if there is a level requirement
// and if there is, make sure user satisfies it
if (rero.LevelReq > 0)
{
await using var ctx = _db.GetDbContext();
var levelData = await ctx.GetTable<UserXpStats>()
.GetLevelDataFor(user.GuildId, user.Id);
if (levelData.Level < rero.LevelReq)
return;
}
// remove all other roles from the same group from the user
// execept in group 0, which is a special, non-exclusive group
if (rero.Group != 0)
{
var exclusive = reros
.Where(x => x.Group == rero.Group && x.RoleId != role.Id)
.Select(x => x.RoleId)
.Distinct();
try { await user.RemoveRolesAsync(exclusive); }
catch { }
// remove user's previous reaction
try
{
var m = await msg.GetOrDownloadAsync();
if (m is not null)
{
var reactToRemove = m.Reactions
.FirstOrDefault(x => x.Key.ToString() != r.Emote.ToString())
.Key;
if (reactToRemove is not null)
{
await m.RemoveReactionAsync(reactToRemove, user);
}
}
}
catch
{
}
}
await user.AddRoleAsync(role.Id);
}
}
finally
{
_assignementLock.Release();
}
});
return Task.CompletedTask;
}
/// <summary>
/// Adds a single reaction role
/// </summary>
/// <param name="guild">Guild where to add a reaction role</param>
/// <param name="msg">Message to which to add a reaction role</param>
/// <param name="emote"></param>
/// <param name="role"></param>
/// <param name="group"></param>
/// <param name="levelReq"></param>
/// <returns>The result of the operation</returns>
public async Task<OneOf<Success, FeatureLimit>> AddReactionRole(
IGuild guild,
IMessage msg,
string emote,
IRole role,
int group = 0,
int levelReq = 0)
{
if (group < 0)
throw new ArgumentOutOfRangeException(nameof(group));
if (levelReq < 0)
throw new ArgumentOutOfRangeException(nameof(group));
await using var ctx = _db.GetDbContext();
await using var tran = await ctx.Database.BeginTransactionAsync();
var activeReactionRoles = await ctx.GetTable<ReactionRoleV2>()
.Where(x => x.GuildId == guild.Id)
.CountAsync();
var result = await _ps.TryGetFeatureLimitAsync(_reroFLKey, guild.OwnerId, 50);
if (result.Quota != -1 && activeReactionRoles >= result.Quota)
return result;
await ctx.GetTable<ReactionRoleV2>()
.InsertOrUpdateAsync(() => new()
{
GuildId = guild.Id,
ChannelId = msg.Channel.Id,
MessageId = msg.Id,
Emote = emote,
RoleId = role.Id,
Group = group,
LevelReq = levelReq
},
(old) => new()
{
RoleId = role.Id,
Group = group,
LevelReq = levelReq
},
() => new()
{
MessageId = msg.Id,
Emote = emote,
});
await tran.CommitAsync();
var obj = new ReactionRoleV2()
{
GuildId = guild.Id,
MessageId = msg.Id,
Emote = emote,
RoleId = role.Id,
Group = group,
LevelReq = levelReq
};
lock (_cacheLock)
{
_cache.AddOrUpdate(msg.Id,
_ => new()
{
obj
},
(_, list) =>
{
list.RemoveAll(x => x.Emote == emote);
list.Add(obj);
return list;
});
}
return new Success();
}
/// <summary>
/// Get all reaction roles on the specified server
/// </summary>
/// <param name="guildId"></param>
/// <returns></returns>
public async Task<IReadOnlyCollection<ReactionRoleV2>> GetReactionRolesAsync(ulong guildId)
{
await using var ctx = _db.GetDbContext();
return await ctx.GetTable<ReactionRoleV2>()
.Where(x => x.GuildId == guildId)
.ToListAsync();
}
/// <summary>
/// Remove reaction roles on the specified message
/// </summary>
/// <param name="guildId"></param>
/// <param name="messageId"></param>
/// <returns></returns>
public async Task<bool> RemoveReactionRoles(ulong guildId, ulong messageId)
{
// guildid is used for quick index lookup
await using var ctx = _db.GetDbContext();
var changed = await ctx.GetTable<ReactionRoleV2>()
.Where(x => x.GuildId == guildId && x.MessageId == messageId)
.DeleteAsync();
_cache.TryRemove(messageId, out _);
if (changed == 0)
return false;
return true;
}
/// <summary>
/// Remove all reaction roles in the specified server
/// </summary>
/// <param name="guildId"></param>
/// <returns></returns>
public async Task<int> RemoveAllReactionRoles(ulong guildId)
{
await using var ctx = _db.GetDbContext();
var output = await ctx.GetTable<ReactionRoleV2>()
.Where(x => x.GuildId == guildId)
.DeleteWithOutputAsync(x => x.MessageId);
lock (_cacheLock)
{
foreach (var o in output)
{
_cache.TryRemove(o, out _);
}
}
return output.Length;
}
public async Task<IReadOnlyCollection<IEmote>> TransferReactionRolesAsync(
ulong guildId,
ulong fromMessageId,
ulong toMessageId)
{
await using var ctx = _db.GetDbContext();
var updated = ctx.GetTable<ReactionRoleV2>()
.Where(x => x.GuildId == guildId && x.MessageId == fromMessageId)
.UpdateWithOutput(old => new()
{
MessageId = toMessageId
},
(old, neu) => neu);
lock (_cacheLock)
{
if (_cache.TryRemove(fromMessageId, out var data))
{
if (_cache.TryGetValue(toMessageId, out var newData))
{
newData.AddRange(data);
}
else
{
_cache[toMessageId] = data;
}
}
}
return updated.Select(x => x.Emote.ToIEmote()).ToList();
}
}

View file

@ -0,0 +1,184 @@
#nullable disable
using SixLabors.ImageSharp.PixelFormats;
using Color = SixLabors.ImageSharp.Color;
namespace Ellie.Modules.Administration;
public partial class Administration
{
public partial class RoleCommands : EllieModule
{
public enum Exclude { Excl }
private readonly IServiceProvider _services;
public RoleCommands(IServiceProvider services)
{
_services = services;
}
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageRoles)]
[BotPerm(GuildPerm.ManageRoles)]
public async Task SetRole(IGuildUser targetUser, [Leftover] IRole roleToAdd)
{
var runnerUser = (IGuildUser)ctx.User;
var runnerMaxRolePosition = runnerUser.GetRoles().Max(x => x.Position);
if (ctx.User.Id != ctx.Guild.OwnerId && runnerMaxRolePosition <= roleToAdd.Position)
return;
try
{
await targetUser.AddRoleAsync(roleToAdd);
await ReplyConfirmLocalizedAsync(strs.setrole(Format.Bold(roleToAdd.Name),
Format.Bold(targetUser.ToString())));
}
catch (Exception ex)
{
Log.Warning(ex, "Error in setrole command");
await ReplyErrorLocalizedAsync(strs.setrole_err);
}
}
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageRoles)]
[BotPerm(GuildPerm.ManageRoles)]
public async Task RemoveRole(IGuildUser targetUser, [Leftover] IRole roleToRemove)
{
var runnerUser = (IGuildUser)ctx.User;
if (ctx.User.Id != runnerUser.Guild.OwnerId
&& runnerUser.GetRoles().Max(x => x.Position) <= roleToRemove.Position)
return;
try
{
await targetUser.RemoveRoleAsync(roleToRemove);
await ReplyConfirmLocalizedAsync(strs.remrole(Format.Bold(roleToRemove.Name),
Format.Bold(targetUser.ToString())));
}
catch
{
await ReplyErrorLocalizedAsync(strs.remrole_err);
}
}
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageRoles)]
[BotPerm(GuildPerm.ManageRoles)]
public async Task RenameRole(IRole roleToEdit, [Leftover] string newname)
{
var guser = (IGuildUser)ctx.User;
if (ctx.User.Id != guser.Guild.OwnerId && guser.GetRoles().Max(x => x.Position) <= roleToEdit.Position)
return;
try
{
if (roleToEdit.Position > (await ctx.Guild.GetCurrentUserAsync()).GetRoles().Max(r => r.Position))
{
await ReplyErrorLocalizedAsync(strs.renrole_perms);
return;
}
await roleToEdit.ModifyAsync(g => g.Name = newname);
await ReplyConfirmLocalizedAsync(strs.renrole);
}
catch (Exception)
{
await ReplyErrorLocalizedAsync(strs.renrole_err);
}
}
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageRoles)]
[BotPerm(GuildPerm.ManageRoles)]
public async Task RemoveAllRoles([Leftover] IGuildUser user)
{
var guser = (IGuildUser)ctx.User;
var userRoles = user.GetRoles().Where(x => !x.IsManaged && x != x.Guild.EveryoneRole).ToList();
if (user.Id == ctx.Guild.OwnerId
|| (ctx.User.Id != ctx.Guild.OwnerId
&& guser.GetRoles().Max(x => x.Position) <= userRoles.Max(x => x.Position)))
return;
try
{
await user.RemoveRolesAsync(userRoles);
await ReplyConfirmLocalizedAsync(strs.rar(Format.Bold(user.ToString())));
}
catch (Exception)
{
await ReplyErrorLocalizedAsync(strs.rar_err);
}
}
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageRoles)]
[BotPerm(GuildPerm.ManageRoles)]
public async Task CreateRole([Leftover] string roleName = null)
{
if (string.IsNullOrWhiteSpace(roleName))
return;
var r = await ctx.Guild.CreateRoleAsync(roleName, isMentionable: false);
await ReplyConfirmLocalizedAsync(strs.cr(Format.Bold(r.Name)));
}
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageRoles)]
[BotPerm(GuildPerm.ManageRoles)]
public async Task DeleteRole([Leftover] IRole role)
{
var guser = (IGuildUser)ctx.User;
if (ctx.User.Id != guser.Guild.OwnerId && guser.GetRoles().Max(x => x.Position) <= role.Position)
return;
await role.DeleteAsync();
await ReplyConfirmLocalizedAsync(strs.dr(Format.Bold(role.Name)));
}
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageRoles)]
[BotPerm(GuildPerm.ManageRoles)]
public async Task RoleHoist([Leftover] IRole role)
{
var newHoisted = !role.IsHoisted;
await role.ModifyAsync(r => r.Hoist = newHoisted);
if (newHoisted)
await ReplyConfirmLocalizedAsync(strs.rolehoist_enabled(Format.Bold(role.Name)));
else
await ReplyConfirmLocalizedAsync(strs.rolehoist_disabled(Format.Bold(role.Name)));
}
[Cmd]
[RequireContext(ContextType.Guild)]
[Priority(1)]
public async Task RoleColor([Leftover] IRole role)
=> await SendConfirmAsync("Role Color", role.Color.RawValue.ToString("x6"));
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageRoles)]
[BotPerm(GuildPerm.ManageRoles)]
[Priority(0)]
public async Task RoleColor(Color color, [Leftover] IRole role)
{
try
{
var rgba32 = color.ToPixel<Rgba32>();
await role.ModifyAsync(r => r.Color = new Discord.Color(rgba32.R, rgba32.G, rgba32.B));
await ReplyConfirmLocalizedAsync(strs.rc(Format.Bold(role.Name)));
}
catch (Exception)
{
await ReplyErrorLocalizedAsync(strs.rc_perms);
}
}
}
}

View file

@ -0,0 +1,153 @@
using System.Net.Http.Json;
using System.Text;
using Ellie.Common.ModuleBehaviors;
namespace NadekoBot.Modules.Administration.Self;
public sealed class CheckForUpdatesService : IEService, IReadyExecutor
{
private readonly BotConfigService _bcs;
private readonly IBotCredsProvider _bcp;
private readonly IHttpClientFactory _httpFactory;
private readonly DiscordSocketClient _client;
private readonly IEmbedBuilderService _ebs;
public CheckForUpdatesService(BotConfigService bcs, IBotCredsProvider bcp, IHttpClientFactory httpFactory,
DiscordSocketClient client, IEmbedBuilderService ebs)
{
_bcs = bcs;
_bcp = bcp;
_httpFactory = httpFactory;
_client = client;
_ebs = ebs;
}
public async Task OnReadyAsync()
{
if (_client.ShardId != 0)
return;
using var timer = new PeriodicTimer(TimeSpan.FromHours(1));
while (await timer.WaitForNextTickAsync())
{
var conf = _bcs.Data;
if (!conf.CheckForUpdates)
continue;
try
{
const string URL = "https://cdn.elliebot.net/cmds/versions.json";
using var http = _httpFactory.CreateClient();
var versions = await http.GetFromJsonAsync<string[]>(URL);
if (versions is null)
continue;
var latest = versions[0];
var latestVersion = Version.Parse(latest);
var lastKnownVersion = GetLastKnownVersion();
if (lastKnownVersion is null)
{
UpdateLastKnownVersion(latestVersion);
continue;
}
if (latestVersion > lastKnownVersion)
{
UpdateLastKnownVersion(latestVersion);
// pull changelog
var changelog = await http.GetStringAsync("https://toastielab.dev/Emotions-stuff/Ellie/src/branch/main/CHANGELOG.md");
var thisVersionChangelog = GetVersionChangelog(latestVersion, changelog);
if (string.IsNullOrWhiteSpace(thisVersionChangelog))
{
Log.Warning("New version {BotVersion} was found but changelog is unavailable",
thisVersionChangelog);
continue;
}
var creds = _bcp.GetCreds();
await creds.OwnerIds
.Select(async x =>
{
var user = await _client.GetUserAsync(x);
if (user is null)
return;
var eb = _ebs.Create()
.WithOkColor()
.WithAuthor($"Ellie v{latestVersion} Released!")
.WithTitle("Changelog")
.WithUrl("https://toastielab.dev/Emotions-stuff/Ellie/src/branch/main/CHANGELOG.md")
.WithDescription(thisVersionChangelog.TrimTo(4096))
.WithFooter("You may disable these messages by typing '.conf bot checkforupdates false'");
await user.EmbedAsync(eb);
}).WhenAll();
}
}
catch (Exception ex)
{
Log.Error(ex, "Error while checking for new bot release: {ErrorMessage}", ex.Message);
}
}
}
private string? GetVersionChangelog(Version latestVersion, string changelog)
{
var clSpan = changelog.AsSpan();
var sb = new StringBuilder();
var started = false;
foreach (var line in clSpan.EnumerateLines())
{
// if we're at the current version, keep reading lines and adding to the output
if (started)
{
// if we got to previous version, end
if (line.StartsWith("## ["))
break;
// if we're reading a new segment, reformat it to print it better to discord
if (line.StartsWith("### "))
{
sb.AppendLine(Format.Bold(line.ToString()));
}
else
{
sb.AppendLine(line.ToString());
}
continue;
}
if (line.StartsWith($"## [{latestVersion.ToString()}]"))
{
started = true;
continue;
}
}
return sb.ToString();
}
private const string LAST_KNOWN_VERSION_PATH = "data/last_known_version.txt";
private Version? GetLastKnownVersion()
{
if (!File.Exists(LAST_KNOWN_VERSION_PATH))
return null;
return Version.TryParse(File.ReadAllText(LAST_KNOWN_VERSION_PATH), out var ver)
? ver
: null;
}
private void UpdateLastKnownVersion(Version version)
{
File.WriteAllText("data/last_known_version.txt", version.ToString());
}
}

View file

@ -0,0 +1,572 @@
#nullable disable
using Ellie.Marmalade;
using Ellie.Modules.Administration.Services;
using Ellie.Services.Database.Models;
namespace Ellie.Modules.Administration;
public partial class Administration
{
[Group]
public partial class SelfCommands : EllieModule<SelfService>
{
public enum SettableUserStatus
{
Online,
Invisible,
Idle,
Dnd
}
private readonly DiscordSocketClient _client;
private readonly IBotStrings _strings;
private readonly IMarmaladeLoaderSevice _marmaladeLoader;
private readonly ICoordinator _coord;
public SelfCommands(
DiscordSocketClient client,
IBotStrings strings,
ICoordinator coord,
IMarmaladeLoaderSevice marmaladeLoader)
{
_client = client;
_strings = strings;
_coord = coord;
_marmaladeLoader = marmaladeLoader;
}
[Cmd]
[OwnerOnly]
public async Task DoAs(IUser user, [Leftover] string message)
{
if (ctx.User is not IGuildUser { GuildPermissions.Administrator: true })
return;
if (ctx.Guild is SocketGuild sg && ctx.Channel is ISocketMessageChannel ch
&& ctx.Message is SocketUserMessage msg)
{
var fakeMessage = new DoAsUserMessage(msg, user, message);
await _cmdHandler.TryRunCommand(sg, ch, fakeMessage);
}
else
{
await ReplyErrorLocalizedAsync(strs.error_occured);
}
}
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.Administrator)]
[OwnerOnly]
public async Task StartupCommandAdd([Leftover] string cmdText)
{
if (cmdText.StartsWith(prefix + "die", StringComparison.InvariantCulture))
return;
var guser = (IGuildUser)ctx.User;
var cmd = new AutoCommand
{
CommandText = cmdText,
ChannelId = ctx.Channel.Id,
ChannelName = ctx.Channel.Name,
GuildId = ctx.Guild?.Id,
GuildName = ctx.Guild?.Name,
VoiceChannelId = guser.VoiceChannel?.Id,
VoiceChannelName = guser.VoiceChannel?.Name,
Interval = 0
};
_service.AddNewAutoCommand(cmd);
await ctx.Channel.EmbedAsync(_eb.Create()
.WithOkColor()
.WithTitle(GetText(strs.scadd))
.AddField(GetText(strs.server),
cmd.GuildId is null ? "-" : $"{cmd.GuildName}/{cmd.GuildId}",
true)
.AddField(GetText(strs.channel), $"{cmd.ChannelName}/{cmd.ChannelId}", true)
.AddField(GetText(strs.command_text), cmdText));
}
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.Administrator)]
[OwnerOnly]
public async Task AutoCommandAdd(int interval, [Leftover] string cmdText)
{
if (cmdText.StartsWith(prefix + "die", StringComparison.InvariantCulture))
return;
if (interval < 5)
return;
var guser = (IGuildUser)ctx.User;
var cmd = new AutoCommand
{
CommandText = cmdText,
ChannelId = ctx.Channel.Id,
ChannelName = ctx.Channel.Name,
GuildId = ctx.Guild?.Id,
GuildName = ctx.Guild?.Name,
VoiceChannelId = guser.VoiceChannel?.Id,
VoiceChannelName = guser.VoiceChannel?.Name,
Interval = interval
};
_service.AddNewAutoCommand(cmd);
await ReplyConfirmLocalizedAsync(strs.autocmd_add(Format.Code(Format.Sanitize(cmdText)), cmd.Interval));
}
[Cmd]
[RequireContext(ContextType.Guild)]
[OwnerOnly]
public async Task StartupCommandsList(int page = 1)
{
if (page-- < 1)
return;
var scmds = _service.GetStartupCommands().Skip(page * 5).Take(5).ToList();
if (scmds.Count == 0)
await ReplyErrorLocalizedAsync(strs.startcmdlist_none);
else
{
var i = 0;
await SendConfirmAsync(text: string.Join("\n",
scmds.Select(x => $@"```css
#{++i + (page * 5)}
[{GetText(strs.server)}]: {(x.GuildId.HasValue ? $"{x.GuildName} #{x.GuildId}" : "-")}
[{GetText(strs.channel)}]: {x.ChannelName} #{x.ChannelId}
[{GetText(strs.command_text)}]: {x.CommandText}```")),
title: string.Empty,
footer: GetText(strs.page(page + 1)));
}
}
[Cmd]
[RequireContext(ContextType.Guild)]
[OwnerOnly]
public async Task AutoCommandsList(int page = 1)
{
if (page-- < 1)
return;
var scmds = _service.GetAutoCommands().Skip(page * 5).Take(5).ToList();
if (!scmds.Any())
await ReplyErrorLocalizedAsync(strs.autocmdlist_none);
else
{
var i = 0;
await SendConfirmAsync(text: string.Join("\n",
scmds.Select(x => $@"```css
#{++i + (page * 5)}
[{GetText(strs.server)}]: {(x.GuildId.HasValue ? $"{x.GuildName} #{x.GuildId}" : "-")}
[{GetText(strs.channel)}]: {x.ChannelName} #{x.ChannelId}
{GetIntervalText(x.Interval)}
[{GetText(strs.command_text)}]: {x.CommandText}```")),
title: string.Empty,
footer: GetText(strs.page(page + 1)));
}
}
private string GetIntervalText(int interval)
=> $"[{GetText(strs.interval)}]: {interval}";
[Cmd]
[OwnerOnly]
public async Task Wait(int miliseconds)
{
if (miliseconds <= 0)
return;
ctx.Message.DeleteAfter(0);
try
{
var msg = await SendConfirmAsync($"⏲ {miliseconds}ms");
msg.DeleteAfter(miliseconds / 1000);
}
catch { }
await Task.Delay(miliseconds);
}
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.Administrator)]
[OwnerOnly]
public async Task AutoCommandRemove([Leftover] int index)
{
if (!_service.RemoveAutoCommand(--index, out _))
{
await ReplyErrorLocalizedAsync(strs.acrm_fail);
return;
}
await ctx.OkAsync();
}
[Cmd]
[RequireContext(ContextType.Guild)]
[OwnerOnly]
public async Task StartupCommandRemove([Leftover] int index)
{
if (!_service.RemoveStartupCommand(--index, out _))
await ReplyErrorLocalizedAsync(strs.scrm_fail);
else
await ReplyConfirmLocalizedAsync(strs.scrm);
}
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.Administrator)]
[OwnerOnly]
public async Task StartupCommandsClear()
{
_service.ClearStartupCommands();
await ReplyConfirmLocalizedAsync(strs.startcmds_cleared);
}
[Cmd]
[OwnerOnly]
public async Task ForwardMessages()
{
var enabled = _service.ForwardMessages();
if (enabled)
await ReplyConfirmLocalizedAsync(strs.fwdm_start);
else
await ReplyPendingLocalizedAsync(strs.fwdm_stop);
}
[Cmd]
[OwnerOnly]
public async Task ForwardToAll()
{
var enabled = _service.ForwardToAll();
if (enabled)
await ReplyConfirmLocalizedAsync(strs.fwall_start);
else
await ReplyPendingLocalizedAsync(strs.fwall_stop);
}
[Cmd]
[RequireContext(ContextType.Guild)]
[OwnerOnly]
public async Task ForwardToChannel()
{
var enabled = _service.ForwardToChannel(ctx.Channel.Id);
if (enabled)
await ReplyConfirmLocalizedAsync(strs.fwch_start);
else
await ReplyPendingLocalizedAsync(strs.fwch_stop);
}
[Cmd]
public async Task ShardStats(int page = 1)
{
if (--page < 0)
return;
var statuses = _coord.GetAllShardStatuses();
var status = string.Join(" : ",
statuses.Select(x => (ConnectionStateToEmoji(x), x))
.GroupBy(x => x.Item1)
.Select(x => $"`{x.Count()} {x.Key}`")
.ToArray());
var allShardStrings = statuses.Select(st =>
{
var timeDiff = DateTime.UtcNow - st.LastUpdate;
var stateStr = ConnectionStateToEmoji(st);
var maxGuildCountLength =
statuses.Max(x => x.GuildCount).ToString().Length;
return $"`{stateStr} "
+ $"| #{st.ShardId.ToString().PadBoth(3)} "
+ $"| {timeDiff:mm\\:ss} "
+ $"| {st.GuildCount.ToString().PadBoth(maxGuildCountLength)} `";
})
.ToArray();
await ctx.SendPaginatedConfirmAsync(page,
curPage =>
{
var str = string.Join("\n", allShardStrings.Skip(25 * curPage).Take(25));
if (string.IsNullOrWhiteSpace(str))
str = GetText(strs.no_shards_on_page);
return _eb.Create().WithOkColor().WithDescription($"{status}\n\n{str}");
},
allShardStrings.Length,
25);
}
private static string ConnectionStateToEmoji(ShardStatus status)
{
var timeDiff = DateTime.UtcNow - status.LastUpdate;
return status.ConnectionState switch
{
ConnectionState.Disconnected => "🔻",
_ when timeDiff > TimeSpan.FromSeconds(30) => " ❗ ",
ConnectionState.Connected => "✅",
_ => " ⏳"
};
}
[Cmd]
[OwnerOnly]
public async Task RestartShard(int shardId)
{
var success = _coord.RestartShard(shardId);
if (success)
await ReplyConfirmLocalizedAsync(strs.shard_reconnecting(Format.Bold("#" + shardId)));
else
await ReplyErrorLocalizedAsync(strs.no_shard_id);
}
[Cmd]
[OwnerOnly]
public Task Leave([Leftover] string guildStr)
=> _service.LeaveGuild(guildStr);
[Cmd]
[OwnerOnly]
public async Task DeleteEmptyServers()
{
await ctx.Channel.TriggerTypingAsync();
var toLeave = _client.Guilds
.Where(s => s.MemberCount == 1 && s.Users.Count == 1)
.ToList();
foreach (var server in toLeave)
{
try
{
await server.DeleteAsync();
Log.Information("Deleted server {ServerName} [{ServerId}]",
server.Name,
server.Id);
}
catch (Exception ex)
{
Log.Warning(ex,
"Error leaving server {ServerName} [{ServerId}]",
server.Name,
server.Id);
}
}
await ReplyConfirmLocalizedAsync(strs.deleted_x_servers(toLeave.Count));
}
[Cmd]
[OwnerOnly]
public async Task Die(bool graceful = false)
{
try
{
await _client.SetStatusAsync(UserStatus.Invisible);
_ = _client.StopAsync();
await ReplyConfirmLocalizedAsync(strs.shutting_down);
}
catch
{
// ignored
}
await Task.Delay(2000);
_coord.Die(graceful);
}
[Cmd]
[OwnerOnly]
public async Task Restart()
{
var success = _coord.RestartBot();
if (!success)
{
await ReplyErrorLocalizedAsync(strs.restart_fail);
return;
}
try { await ReplyConfirmLocalizedAsync(strs.restarting); }
catch { }
}
[Cmd]
[OwnerOnly]
public async Task SetName([Leftover] string newName)
{
if (string.IsNullOrWhiteSpace(newName))
return;
try
{
await _client.CurrentUser.ModifyAsync(u => u.Username = newName);
}
catch (RateLimitedException)
{
Log.Warning("You've been ratelimited. Wait 2 hours to change your name");
}
await ReplyConfirmLocalizedAsync(strs.bot_name(Format.Bold(newName)));
}
[Cmd]
[UserPerm(GuildPerm.ManageNicknames)]
[BotPerm(GuildPerm.ChangeNickname)]
[Priority(0)]
public async Task SetNick([Leftover] string newNick = null)
{
if (string.IsNullOrWhiteSpace(newNick))
return;
var curUser = await ctx.Guild.GetCurrentUserAsync();
await curUser.ModifyAsync(u => u.Nickname = newNick);
await ReplyConfirmLocalizedAsync(strs.bot_nick(Format.Bold(newNick) ?? "-"));
}
[Cmd]
[BotPerm(GuildPerm.ManageNicknames)]
[UserPerm(GuildPerm.ManageNicknames)]
[Priority(1)]
public async Task SetNick(IGuildUser gu, [Leftover] string newNick = null)
{
var sg = (SocketGuild)ctx.Guild;
if (sg.OwnerId == gu.Id
|| gu.GetRoles().Max(r => r.Position) >= sg.CurrentUser.GetRoles().Max(r => r.Position))
{
await ReplyErrorLocalizedAsync(strs.insuf_perms_i);
return;
}
await gu.ModifyAsync(u => u.Nickname = newNick);
await ReplyConfirmLocalizedAsync(strs.user_nick(Format.Bold(gu.ToString()), Format.Bold(newNick) ?? "-"));
}
[Cmd]
[OwnerOnly]
public async Task SetStatus([Leftover] SettableUserStatus status)
{
await _client.SetStatusAsync(SettableUserStatusToUserStatus(status));
await ReplyConfirmLocalizedAsync(strs.bot_status(Format.Bold(status.ToString())));
}
[Cmd]
[OwnerOnly]
public async Task SetAvatar([Leftover] string img = null)
{
var success = await _service.SetAvatar(img);
if (success)
await ReplyConfirmLocalizedAsync(strs.set_avatar);
}
[Cmd]
[OwnerOnly]
public async Task SetGame(ActivityType type, [Leftover] string game = null)
{
var rep = new ReplacementBuilder().WithDefault(Context).Build();
await _service.SetGameAsync(game is null ? game : rep.Replace(game), type);
await ReplyConfirmLocalizedAsync(strs.set_game);
}
[Cmd]
[OwnerOnly]
public async Task SetStream(string url, [Leftover] string name = null)
{
name ??= "";
await _service.SetStreamAsync(name, url);
await ReplyConfirmLocalizedAsync(strs.set_stream);
}
[Cmd]
[OwnerOnly]
public async Task Send(string where, [Leftover] SmartText text = null)
{
var ids = where.Split('|');
if (ids.Length != 2)
return;
var sid = ulong.Parse(ids[0]);
var server = _client.Guilds.FirstOrDefault(s => s.Id == sid);
if (server is null)
return;
var rep = new ReplacementBuilder().WithDefault(Context).Build();
if (ids[1].ToUpperInvariant().StartsWith("C:", StringComparison.InvariantCulture))
{
var cid = ulong.Parse(ids[1][2..]);
var ch = server.TextChannels.FirstOrDefault(c => c.Id == cid);
if (ch is null)
return;
text = rep.Replace(text);
await ch.SendAsync(text);
}
else if (ids[1].ToUpperInvariant().StartsWith("U:", StringComparison.InvariantCulture))
{
var uid = ulong.Parse(ids[1][2..]);
var user = server.Users.FirstOrDefault(u => u.Id == uid);
if (user is null)
return;
var ch = await user.CreateDMChannelAsync();
text = rep.Replace(text);
await ch.SendAsync(text);
}
else
{
await ReplyErrorLocalizedAsync(strs.invalid_format);
return;
}
await ReplyConfirmLocalizedAsync(strs.message_sent);
}
[Cmd]
[OwnerOnly]
public async Task StringsReload()
{
_strings.Reload();
await _marmaladeLoader.ReloadStrings();
await ReplyConfirmLocalizedAsync(strs.bot_strings_reloaded);
}
[Cmd]
[OwnerOnly]
public async Task CoordReload()
{
await _coord.Reload();
await ctx.OkAsync();
}
private static UserStatus SettableUserStatusToUserStatus(SettableUserStatus sus)
{
switch (sus)
{
case SettableUserStatus.Online:
return UserStatus.Online;
case SettableUserStatus.Invisible:
return UserStatus.Invisible;
case SettableUserStatus.Idle:
return UserStatus.AFK;
case SettableUserStatus.Dnd:
return UserStatus.DoNotDisturb;
}
return UserStatus.Online;
}
}
}

View file

@ -0,0 +1,399 @@
#nullable disable
using Microsoft.EntityFrameworkCore;
using Ellie.Common.ModuleBehaviors;
using Ellie.Services.Database.Models;
using System.Collections.Immutable;
namespace Ellie.Modules.Administration.Services;
public sealed class SelfService : IExecNoCommand, IReadyExecutor, IEService
{
private readonly CommandHandler _cmdHandler;
private readonly DbService _db;
private readonly IBotStrings _strings;
private readonly DiscordSocketClient _client;
private readonly IBotCredentials _creds;
private ImmutableDictionary<ulong, IDMChannel> ownerChannels =
new Dictionary<ulong, IDMChannel>().ToImmutableDictionary();
private ConcurrentDictionary<ulong?, ConcurrentDictionary<int, Timer>> autoCommands = new();
private readonly IHttpClientFactory _httpFactory;
private readonly BotConfigService _bss;
private readonly IPubSub _pubSub;
private readonly IEmbedBuilderService _eb;
//keys
private readonly TypedKey<ActivityPubData> _activitySetKey;
private readonly TypedKey<string> _guildLeaveKey;
public SelfService(
DiscordSocketClient client,
CommandHandler cmdHandler,
DbService db,
IBotStrings strings,
IBotCredentials creds,
IHttpClientFactory factory,
BotConfigService bss,
IPubSub pubSub,
IEmbedBuilderService eb)
{
_cmdHandler = cmdHandler;
_db = db;
_strings = strings;
_client = client;
_creds = creds;
_httpFactory = factory;
_bss = bss;
_pubSub = pubSub;
_eb = eb;
_activitySetKey = new("activity.set");
_guildLeaveKey = new("guild.leave");
HandleStatusChanges();
_pubSub.Sub(_guildLeaveKey,
async input =>
{
var guildStr = input.ToString().Trim().ToUpperInvariant();
if (string.IsNullOrWhiteSpace(guildStr))
return;
var server = _client.Guilds.FirstOrDefault(g => g.Id.ToString() == guildStr
|| g.Name.Trim().ToUpperInvariant() == guildStr);
if (server is null)
return;
if (server.OwnerId != _client.CurrentUser.Id)
{
await server.LeaveAsync();
Log.Information("Left server {Name} [{Id}]", server.Name, server.Id);
}
else
{
await server.DeleteAsync();
Log.Information("Deleted server {Name} [{Id}]", server.Name, server.Id);
}
});
}
public async Task OnReadyAsync()
{
await using var uow = _db.GetDbContext();
autoCommands = uow.Set<AutoCommand>().AsNoTracking()
.Where(x => x.Interval >= 5)
.AsEnumerable()
.GroupBy(x => x.GuildId)
.ToDictionary(x => x.Key,
y => y.ToDictionary(x => x.Id, TimerFromAutoCommand).ToConcurrent())
.ToConcurrent();
var startupCommands = uow.Set<AutoCommand>().AsNoTracking().Where(x => x.Interval == 0);
foreach (var cmd in startupCommands)
{
try
{
await ExecuteCommand(cmd);
}
catch
{
}
}
if (_client.ShardId == 0)
await LoadOwnerChannels();
}
private Timer TimerFromAutoCommand(AutoCommand x)
=> new(async obj => await ExecuteCommand((AutoCommand)obj), x, x.Interval * 1000, x.Interval * 1000);
private async Task ExecuteCommand(AutoCommand cmd)
{
try
{
if (cmd.GuildId is null)
return;
var guildShard = (int)((cmd.GuildId.Value >> 22) % (ulong)_creds.TotalShards);
if (guildShard != _client.ShardId)
return;
var prefix = _cmdHandler.GetPrefix(cmd.GuildId);
//if someone already has .die as their startup command, ignore it
if (cmd.CommandText.StartsWith(prefix + "die", StringComparison.InvariantCulture))
return;
await _cmdHandler.ExecuteExternal(cmd.GuildId, cmd.ChannelId, cmd.CommandText);
}
catch (Exception ex)
{
Log.Warning(ex, "Error in SelfService ExecuteCommand");
}
}
public void AddNewAutoCommand(AutoCommand cmd)
{
using (var uow = _db.GetDbContext())
{
uow.Set<AutoCommand>().Add(cmd);
uow.SaveChanges();
}
if (cmd.Interval >= 5)
{
var autos = autoCommands.GetOrAdd(cmd.GuildId, new ConcurrentDictionary<int, Timer>());
autos.AddOrUpdate(cmd.Id,
_ => TimerFromAutoCommand(cmd),
(_, old) =>
{
old.Change(Timeout.Infinite, Timeout.Infinite);
return TimerFromAutoCommand(cmd);
});
}
}
public IEnumerable<AutoCommand> GetStartupCommands()
{
using var uow = _db.GetDbContext();
return uow.Set<AutoCommand>().AsNoTracking().Where(x => x.Interval == 0).OrderBy(x => x.Id).ToList();
}
public IEnumerable<AutoCommand> GetAutoCommands()
{
using var uow = _db.GetDbContext();
return uow.Set<AutoCommand>().AsNoTracking().Where(x => x.Interval >= 5).OrderBy(x => x.Id).ToList();
}
private async Task LoadOwnerChannels()
{
var channels = await _creds.OwnerIds.Select(id =>
{
var user = _client.GetUser(id);
if (user is null)
return Task.FromResult<IDMChannel>(null);
return user.CreateDMChannelAsync();
})
.WhenAll();
ownerChannels = channels.Where(x => x is not null)
.ToDictionary(x => x.Recipient.Id, x => x)
.ToImmutableDictionary();
if (!ownerChannels.Any())
{
Log.Warning(
"No owner channels created! Make sure you've specified the correct OwnerId in the creds.yml file and invited the bot to a Discord server");
}
else
{
Log.Information("Created {OwnerChannelCount} out of {TotalOwnerChannelCount} owner message channels",
ownerChannels.Count,
_creds.OwnerIds.Count);
}
}
public Task LeaveGuild(string guildStr)
=> _pubSub.Pub(_guildLeaveKey, guildStr);
// forwards dms
public async Task ExecOnNoCommandAsync(IGuild guild, IUserMessage msg)
{
var bs = _bss.Data;
if (msg.Channel is IDMChannel && bs.ForwardMessages && (ownerChannels.Any() || bs.ForwardToChannel is not null))
{
var title = _strings.GetText(strs.dm_from) + $" [{msg.Author}]({msg.Author.Id})";
var attachamentsTxt = _strings.GetText(strs.attachments);
var toSend = msg.Content;
if (msg.Attachments.Count > 0)
{
toSend += $"\n\n{Format.Code(attachamentsTxt)}:\n"
+ string.Join("\n", msg.Attachments.Select(a => a.ProxyUrl));
}
if (bs.ForwardToAllOwners)
{
var allOwnerChannels = ownerChannels.Values;
foreach (var ownerCh in allOwnerChannels.Where(ch => ch.Recipient.Id != msg.Author.Id))
{
try
{
await ownerCh.SendConfirmAsync(_eb, title, toSend);
}
catch
{
Log.Warning("Can't contact owner with id {OwnerId}", ownerCh.Recipient.Id);
}
}
}
else if (bs.ForwardToChannel is ulong cid)
{
try
{
if (_client.GetChannel(cid) is ITextChannel ch)
await ch.SendConfirmAsync(_eb, title, toSend);
}
catch
{
Log.Warning("Error forwarding message to the channel");
}
}
else
{
var firstOwnerChannel = ownerChannels.Values.First();
if (firstOwnerChannel.Recipient.Id != msg.Author.Id)
{
try
{
await firstOwnerChannel.SendConfirmAsync(_eb, title, toSend);
}
catch
{
// ignored
}
}
}
}
}
public bool RemoveStartupCommand(int index, out AutoCommand cmd)
{
using var uow = _db.GetDbContext();
cmd = uow.Set<AutoCommand>().AsNoTracking().Where(x => x.Interval == 0).Skip(index).FirstOrDefault();
if (cmd is not null)
{
uow.Remove(cmd);
uow.SaveChanges();
return true;
}
return false;
}
public bool RemoveAutoCommand(int index, out AutoCommand cmd)
{
using var uow = _db.GetDbContext();
cmd = uow.Set<AutoCommand>().AsNoTracking().Where(x => x.Interval >= 5).Skip(index).FirstOrDefault();
if (cmd is not null)
{
uow.Remove(cmd);
if (autoCommands.TryGetValue(cmd.GuildId, out var autos))
{
if (autos.TryRemove(cmd.Id, out var timer))
timer.Change(Timeout.Infinite, Timeout.Infinite);
}
uow.SaveChanges();
return true;
}
return false;
}
public async Task<bool> SetAvatar(string img)
{
if (string.IsNullOrWhiteSpace(img))
return false;
if (!Uri.IsWellFormedUriString(img, UriKind.Absolute))
return false;
var uri = new Uri(img);
using var http = _httpFactory.CreateClient();
using var sr = await http.GetAsync(uri, HttpCompletionOption.ResponseHeadersRead);
if (!sr.IsImage())
return false;
// i can't just do ReadAsStreamAsync because dicord.net's image poops itself
var imgData = await sr.Content.ReadAsByteArrayAsync();
await using var imgStream = imgData.ToStream();
await _client.CurrentUser.ModifyAsync(u => u.Avatar = new Image(imgStream));
return true;
}
public void ClearStartupCommands()
{
using var uow = _db.GetDbContext();
var toRemove = uow.Set<AutoCommand>().AsNoTracking().Where(x => x.Interval == 0);
uow.Set<AutoCommand>().RemoveRange(toRemove);
uow.SaveChanges();
}
public bool ForwardMessages()
{
var isForwarding = false;
_bss.ModifyConfig(config => { isForwarding = config.ForwardMessages = !config.ForwardMessages; });
return isForwarding;
}
public bool ForwardToAll()
{
var isToAll = false;
_bss.ModifyConfig(config => { isToAll = config.ForwardToAllOwners = !config.ForwardToAllOwners; });
return isToAll;
}
public bool ForwardToChannel(ulong? channelId)
{
using var uow = _db.GetDbContext();
_bss.ModifyConfig(config =>
{
config.ForwardToChannel = channelId == config.ForwardToChannel
? null
: channelId;
});
return channelId is not null;
}
private void HandleStatusChanges()
=> _pubSub.Sub(_activitySetKey,
async data =>
{
try
{
await _client.SetGameAsync(data.Name, data.Link, data.Type);
}
catch (Exception ex)
{
Log.Warning(ex, "Error setting activity");
}
});
public Task SetGameAsync(string game, ActivityType type)
=> _pubSub.Pub(_activitySetKey,
new()
{
Name = game,
Link = null,
Type = type
});
public Task SetStreamAsync(string name, string link)
=> _pubSub.Pub(_activitySetKey,
new()
{
Name = name,
Link = link,
Type = ActivityType.Streaming
});
private sealed class ActivityPubData
{
public string Name { get; init; }
public string Link { get; init; }
public ActivityType Type { get; init; }
}
}

View file

@ -0,0 +1,234 @@
#nullable disable
using Ellie.Modules.Administration.Services;
using System.Text;
namespace Ellie.Modules.Administration;
public partial class Administration
{
[Group]
public partial class SelfAssignedRolesCommands : EllieModule<SelfAssignedRolesService>
{
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageMessages)]
[BotPerm(GuildPerm.ManageMessages)]
public async Task AdSarm()
{
var newVal = _service.ToggleAdSarm(ctx.Guild.Id);
if (newVal)
await ReplyConfirmLocalizedAsync(strs.adsarm_enable(prefix));
else
await ReplyConfirmLocalizedAsync(strs.adsarm_disable(prefix));
}
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageRoles)]
[BotPerm(GuildPerm.ManageRoles)]
[Priority(1)]
public Task Asar([Leftover] IRole role)
=> Asar(0, role);
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageRoles)]
[BotPerm(GuildPerm.ManageRoles)]
[Priority(0)]
public async Task Asar(int group, [Leftover] IRole role)
{
var guser = (IGuildUser)ctx.User;
if (ctx.User.Id != guser.Guild.OwnerId && guser.GetRoles().Max(x => x.Position) <= role.Position)
return;
var succ = _service.AddNew(ctx.Guild.Id, role, group);
if (succ)
{
await ReplyConfirmLocalizedAsync(strs.role_added(Format.Bold(role.Name),
Format.Bold(@group.ToString())));
}
else
await ReplyErrorLocalizedAsync(strs.role_in_list(Format.Bold(role.Name)));
}
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageRoles)]
[BotPerm(GuildPerm.ManageRoles)]
[Priority(0)]
public async Task Sargn(int group, [Leftover] string name = null)
{
var set = await _service.SetNameAsync(ctx.Guild.Id, group, name);
if (set)
{
await ReplyConfirmLocalizedAsync(
strs.group_name_added(Format.Bold(@group.ToString()), Format.Bold(name)));
}
else
await ReplyConfirmLocalizedAsync(strs.group_name_removed(Format.Bold(group.ToString())));
}
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageRoles)]
public async Task Rsar([Leftover] IRole role)
{
var guser = (IGuildUser)ctx.User;
if (ctx.User.Id != guser.Guild.OwnerId && guser.GetRoles().Max(x => x.Position) <= role.Position)
return;
var success = _service.RemoveSar(role.Guild.Id, role.Id);
if (!success)
await ReplyErrorLocalizedAsync(strs.self_assign_not);
else
await ReplyConfirmLocalizedAsync(strs.self_assign_rem(Format.Bold(role.Name)));
}
[Cmd]
[RequireContext(ContextType.Guild)]
public async Task Lsar(int page = 1)
{
if (--page < 0)
return;
var (exclusive, roles, groups) = _service.GetRoles(ctx.Guild);
await ctx.SendPaginatedConfirmAsync(page,
cur =>
{
var rolesStr = new StringBuilder();
var roleGroups = roles.OrderBy(x => x.Model.Group)
.Skip(cur * 20)
.Take(20)
.GroupBy(x => x.Model.Group)
.OrderBy(x => x.Key);
foreach (var kvp in roleGroups)
{
string groupNameText;
if (!groups.TryGetValue(kvp.Key, out var name))
groupNameText = Format.Bold(GetText(strs.self_assign_group(kvp.Key)));
else
groupNameText = Format.Bold($"{kvp.Key} - {name.TrimTo(25, true)}");
rolesStr.AppendLine("\t\t\t\t ⟪" + groupNameText + "⟫");
foreach (var (model, role) in kvp.AsEnumerable())
{
if (role is null)
{
}
else
{
// first character is invisible space
if (model.LevelRequirement == 0)
rolesStr.AppendLine(" " + role.Name);
else
rolesStr.AppendLine(" " + role.Name + $" (lvl {model.LevelRequirement}+)");
}
}
rolesStr.AppendLine();
}
return _eb.Create()
.WithOkColor()
.WithTitle(Format.Bold(GetText(strs.self_assign_list(roles.Count()))))
.WithDescription(rolesStr.ToString())
.WithFooter(exclusive
? GetText(strs.self_assign_are_exclusive)
: GetText(strs.self_assign_are_not_exclusive));
},
roles.Count(),
20);
}
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageRoles)]
[BotPerm(GuildPerm.ManageRoles)]
public async Task Togglexclsar()
{
var areExclusive = _service.ToggleEsar(ctx.Guild.Id);
if (areExclusive)
await ReplyConfirmLocalizedAsync(strs.self_assign_excl);
else
await ReplyConfirmLocalizedAsync(strs.self_assign_no_excl);
}
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageRoles)]
[BotPerm(GuildPerm.ManageRoles)]
public async Task RoleLevelReq(int level, [Leftover] IRole role)
{
if (level < 0)
return;
var succ = _service.SetLevelReq(ctx.Guild.Id, role, level);
if (!succ)
{
await ReplyErrorLocalizedAsync(strs.self_assign_not);
return;
}
await ReplyConfirmLocalizedAsync(strs.self_assign_level_req(Format.Bold(role.Name),
Format.Bold(level.ToString())));
}
[Cmd]
[RequireContext(ContextType.Guild)]
public async Task Iam([Leftover] IRole role)
{
var guildUser = (IGuildUser)ctx.User;
var (result, autoDelete, extra) = await _service.Assign(guildUser, role);
IUserMessage msg;
if (result == SelfAssignedRolesService.AssignResult.ErrNotAssignable)
msg = await ReplyErrorLocalizedAsync(strs.self_assign_not);
else if (result == SelfAssignedRolesService.AssignResult.ErrLvlReq)
msg = await ReplyErrorLocalizedAsync(strs.self_assign_not_level(Format.Bold(extra.ToString())));
else if (result == SelfAssignedRolesService.AssignResult.ErrAlreadyHave)
msg = await ReplyErrorLocalizedAsync(strs.self_assign_already(Format.Bold(role.Name)));
else if (result == SelfAssignedRolesService.AssignResult.ErrNotPerms)
msg = await ReplyErrorLocalizedAsync(strs.self_assign_perms);
else
msg = await ReplyConfirmLocalizedAsync(strs.self_assign_success(Format.Bold(role.Name)));
if (autoDelete)
{
msg.DeleteAfter(3);
ctx.Message.DeleteAfter(3);
}
}
[Cmd]
[RequireContext(ContextType.Guild)]
public async Task Iamnot([Leftover] IRole role)
{
var guildUser = (IGuildUser)ctx.User;
var (result, autoDelete) = await _service.Remove(guildUser, role);
IUserMessage msg;
if (result == SelfAssignedRolesService.RemoveResult.ErrNotAssignable)
msg = await ReplyErrorLocalizedAsync(strs.self_assign_not);
else if (result == SelfAssignedRolesService.RemoveResult.ErrNotHave)
msg = await ReplyErrorLocalizedAsync(strs.self_assign_not_have(Format.Bold(role.Name)));
else if (result == SelfAssignedRolesService.RemoveResult.ErrNotPerms)
msg = await ReplyErrorLocalizedAsync(strs.self_assign_perms);
else
msg = await ReplyConfirmLocalizedAsync(strs.self_assign_remove(Format.Bold(role.Name)));
if (autoDelete)
{
msg.DeleteAfter(3);
ctx.Message.DeleteAfter(3);
}
}
}
}

View file

@ -0,0 +1,234 @@
#nullable disable
using Microsoft.EntityFrameworkCore;
using Ellie.Db;
using Ellie.Services.Database.Models;
namespace Ellie.Modules.Administration.Services;
public class SelfAssignedRolesService : IEService
{
public enum AssignResult
{
Assigned, // successfully removed
ErrNotAssignable, // not assignable (error)
ErrAlreadyHave, // you already have that role (error)
ErrNotPerms, // bot doesn't have perms (error)
ErrLvlReq // you are not required level (error)
}
public enum RemoveResult
{
Removed, // successfully removed
ErrNotAssignable, // not assignable (error)
ErrNotHave, // you don't have a role you want to remove (error)
ErrNotPerms // bot doesn't have perms (error)
}
private readonly DbService _db;
public SelfAssignedRolesService(DbService db)
=> _db = db;
public bool AddNew(ulong guildId, IRole role, int group)
{
using var uow = _db.GetDbContext();
var roles = uow.Set<SelfAssignedRole>().GetFromGuild(guildId);
if (roles.Any(s => s.RoleId == role.Id && s.GuildId == role.Guild.Id))
return false;
uow.Set<SelfAssignedRole>().Add(new()
{
Group = group,
RoleId = role.Id,
GuildId = role.Guild.Id
});
uow.SaveChanges();
return true;
}
public bool ToggleAdSarm(ulong guildId)
{
bool newval;
using var uow = _db.GetDbContext();
var config = uow.GuildConfigsForId(guildId, set => set);
newval = config.AutoDeleteSelfAssignedRoleMessages = !config.AutoDeleteSelfAssignedRoleMessages;
uow.SaveChanges();
return newval;
}
public async Task<(AssignResult Result, bool AutoDelete, object extra)> Assign(IGuildUser guildUser, IRole role)
{
LevelStats userLevelData;
await using (var uow = _db.GetDbContext())
{
var stats = uow.GetOrCreateUserXpStats(guildUser.Guild.Id, guildUser.Id);
userLevelData = new(stats.Xp + stats.AwardedXp);
}
var (autoDelete, exclusive, roles) = GetAdAndRoles(guildUser.Guild.Id);
var theRoleYouWant = roles.FirstOrDefault(r => r.RoleId == role.Id);
if (theRoleYouWant is null)
return (AssignResult.ErrNotAssignable, autoDelete, null);
if (theRoleYouWant.LevelRequirement > userLevelData.Level)
return (AssignResult.ErrLvlReq, autoDelete, theRoleYouWant.LevelRequirement);
if (guildUser.RoleIds.Contains(role.Id))
return (AssignResult.ErrAlreadyHave, autoDelete, null);
var roleIds = roles.Where(x => x.Group == theRoleYouWant.Group).Select(x => x.RoleId).ToArray();
if (exclusive)
{
var sameRoles = guildUser.RoleIds.Where(r => roleIds.Contains(r));
foreach (var roleId in sameRoles)
{
var sameRole = guildUser.Guild.GetRole(roleId);
if (sameRole is not null)
{
try
{
await guildUser.RemoveRoleAsync(sameRole);
await Task.Delay(300);
}
catch
{
// ignored
}
}
}
}
try
{
await guildUser.AddRoleAsync(role);
}
catch (Exception ex)
{
return (AssignResult.ErrNotPerms, autoDelete, ex);
}
return (AssignResult.Assigned, autoDelete, null);
}
public async Task<bool> SetNameAsync(ulong guildId, int group, string name)
{
var set = false;
await using var uow = _db.GetDbContext();
var gc = uow.GuildConfigsForId(guildId, y => y.Include(x => x.SelfAssignableRoleGroupNames));
var toUpdate = gc.SelfAssignableRoleGroupNames.FirstOrDefault(x => x.Number == group);
if (string.IsNullOrWhiteSpace(name))
{
if (toUpdate is not null)
gc.SelfAssignableRoleGroupNames.Remove(toUpdate);
}
else if (toUpdate is null)
{
gc.SelfAssignableRoleGroupNames.Add(new()
{
Name = name,
Number = group
});
set = true;
}
else
{
toUpdate.Name = name;
set = true;
}
await uow.SaveChangesAsync();
return set;
}
public async Task<(RemoveResult Result, bool AutoDelete)> Remove(IGuildUser guildUser, IRole role)
{
var (autoDelete, _, roles) = GetAdAndRoles(guildUser.Guild.Id);
if (roles.FirstOrDefault(r => r.RoleId == role.Id) is null)
return (RemoveResult.ErrNotAssignable, autoDelete);
if (!guildUser.RoleIds.Contains(role.Id))
return (RemoveResult.ErrNotHave, autoDelete);
try
{
await guildUser.RemoveRoleAsync(role);
}
catch (Exception)
{
return (RemoveResult.ErrNotPerms, autoDelete);
}
return (RemoveResult.Removed, autoDelete);
}
public bool RemoveSar(ulong guildId, ulong roleId)
{
bool success;
using var uow = _db.GetDbContext();
success = uow.Set<SelfAssignedRole>().DeleteByGuildAndRoleId(guildId, roleId);
uow.SaveChanges();
return success;
}
public (bool AutoDelete, bool Exclusive, IReadOnlyCollection<SelfAssignedRole>) GetAdAndRoles(ulong guildId)
{
using var uow = _db.GetDbContext();
var gc = uow.GuildConfigsForId(guildId, set => set);
var autoDelete = gc.AutoDeleteSelfAssignedRoleMessages;
var exclusive = gc.ExclusiveSelfAssignedRoles;
var roles = uow.Set<SelfAssignedRole>().GetFromGuild(guildId);
return (autoDelete, exclusive, roles);
}
public bool SetLevelReq(ulong guildId, IRole role, int level)
{
using var uow = _db.GetDbContext();
var roles = uow.Set<SelfAssignedRole>().GetFromGuild(guildId);
var sar = roles.FirstOrDefault(x => x.RoleId == role.Id);
if (sar is not null)
{
sar.LevelRequirement = level;
uow.SaveChanges();
}
else
return false;
return true;
}
public bool ToggleEsar(ulong guildId)
{
bool areExclusive;
using var uow = _db.GetDbContext();
var config = uow.GuildConfigsForId(guildId, set => set);
areExclusive = config.ExclusiveSelfAssignedRoles = !config.ExclusiveSelfAssignedRoles;
uow.SaveChanges();
return areExclusive;
}
public (bool Exclusive, IReadOnlyCollection<(SelfAssignedRole Model, IRole Role)> Roles, IDictionary<int, string>
GroupNames
) GetRoles(IGuild guild)
{
var exclusive = false;
IReadOnlyCollection<(SelfAssignedRole Model, IRole Role)> roles;
IDictionary<int, string> groupNames;
using (var uow = _db.GetDbContext())
{
var gc = uow.GuildConfigsForId(guild.Id, set => set.Include(x => x.SelfAssignableRoleGroupNames));
exclusive = gc.ExclusiveSelfAssignedRoles;
groupNames = gc.SelfAssignableRoleGroupNames.ToDictionary(x => x.Number, x => x.Name);
var roleModels = uow.Set<SelfAssignedRole>().GetFromGuild(guild.Id);
roles = roleModels.Select(x => (Model: x, Role: guild.GetRole(x.RoleId)))
.ToList();
uow.Set<SelfAssignedRole>().RemoveRange(roles.Where(x => x.Role is null).Select(x => x.Model).ToArray());
uow.SaveChanges();
}
return (exclusive, roles.Where(x => x.Role is not null).ToList(), groupNames);
}
}

View file

@ -0,0 +1,25 @@
using Ellie.Services.Database.Models;
namespace Ellie.Modules.Administration;
public sealed class DummyLogCommandService : ILogCommandService
#if GLOBAL_ELLIE
, IEService
#endif
{
public void AddDeleteIgnore(ulong xId)
{
}
public Task LogServer(ulong guildId, ulong channelId, bool actionValue)
=> Task.CompletedTask;
public bool LogIgnore(ulong guildId, ulong itemId, IgnoredItemType itemType)
=> false;
public LogSetting? GetGuildLogSettings(ulong guildId)
=> default;
public bool Log(ulong guildId, ulong? channelId, LogType type)
=> false;
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,171 @@
using Ellie.Common.TypeReaders.Models;
using Ellie.Services.Database.Models;
namespace Ellie.Modules.Administration;
public partial class Administration
{
[Group]
[NoPublicBot]
public partial class LogCommands : EllieModule<ILogCommandService>
{
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.Administrator)]
[OwnerOnly]
public async Task LogServer(PermissionAction action)
{
await _service.LogServer(ctx.Guild.Id, ctx.Channel.Id, action.Value);
if (action.Value)
await ReplyConfirmLocalizedAsync(strs.log_all);
else
await ReplyConfirmLocalizedAsync(strs.log_disabled);
}
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.Administrator)]
[OwnerOnly]
public async Task LogIgnore()
{
var settings = _service.GetGuildLogSettings(ctx.Guild.Id);
var chs = settings?.LogIgnores.Where(x => x.ItemType == IgnoredItemType.Channel).ToList()
?? new List<IgnoredLogItem>();
var usrs = settings?.LogIgnores.Where(x => x.ItemType == IgnoredItemType.User).ToList()
?? new List<IgnoredLogItem>();
var eb = _eb.Create(ctx)
.WithOkColor()
.AddField(GetText(strs.log_ignored_channels),
chs.Count == 0
? "-"
: string.Join('\n', chs.Select(x => $"{x.LogItemId} | <#{x.LogItemId}>")))
.AddField(GetText(strs.log_ignored_users),
usrs.Count == 0
? "-"
: string.Join('\n', usrs.Select(x => $"{x.LogItemId} | <@{x.LogItemId}>")));
await ctx.Channel.EmbedAsync(eb);
}
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.Administrator)]
[OwnerOnly]
public async Task LogIgnore([Leftover] ITextChannel target)
{
var removed = _service.LogIgnore(ctx.Guild.Id, target.Id, IgnoredItemType.Channel);
if (!removed)
{
await ReplyConfirmLocalizedAsync(
strs.log_ignore_chan(Format.Bold(target.Mention + "(" + target.Id + ")")));
}
else
{
await ReplyConfirmLocalizedAsync(
strs.log_not_ignore_chan(Format.Bold(target.Mention + "(" + target.Id + ")")));
}
}
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.Administrator)]
[OwnerOnly]
public async Task LogIgnore([Leftover] IUser target)
{
var removed = _service.LogIgnore(ctx.Guild.Id, target.Id, IgnoredItemType.User);
if (!removed)
{
await ReplyConfirmLocalizedAsync(
strs.log_ignore_user(Format.Bold(target.Mention + "(" + target.Id + ")")));
}
else
{
await ReplyConfirmLocalizedAsync(
strs.log_not_ignore_user(Format.Bold(target.Mention + "(" + target.Id + ")")));
}
}
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.Administrator)]
[OwnerOnly]
public async Task LogEvents()
{
var logSetting = _service.GetGuildLogSettings(ctx.Guild.Id);
var str = string.Join("\n",
Enum.GetNames<LogType>()
.Select(x =>
{
var val = logSetting is null ? null : GetLogProperty(logSetting, Enum.Parse<LogType>(x));
if (val is not null)
return $"{Format.Bold(x)} <#{val}>";
return Format.Bold(x);
}));
await SendConfirmAsync(Format.Bold(GetText(strs.log_events)) + "\n" + str);
}
private static ulong? GetLogProperty(LogSetting l, LogType type)
{
switch (type)
{
case LogType.Other:
return l.LogOtherId;
case LogType.MessageUpdated:
return l.MessageUpdatedId;
case LogType.MessageDeleted:
return l.MessageDeletedId;
case LogType.UserJoined:
return l.UserJoinedId;
case LogType.UserLeft:
return l.UserLeftId;
case LogType.UserBanned:
return l.UserBannedId;
case LogType.UserUnbanned:
return l.UserUnbannedId;
case LogType.UserUpdated:
return l.UserUpdatedId;
case LogType.ChannelCreated:
return l.ChannelCreatedId;
case LogType.ChannelDestroyed:
return l.ChannelDestroyedId;
case LogType.ChannelUpdated:
return l.ChannelUpdatedId;
case LogType.UserPresence:
return l.LogUserPresenceId;
case LogType.VoicePresence:
return l.LogVoicePresenceId;
case LogType.VoicePresenceTts:
return l.LogVoicePresenceTTSId;
case LogType.UserMuted:
return l.UserMutedId;
case LogType.UserWarned:
return l.LogWarnsId;
case LogType.ThreadDeleted:
return l.ThreadDeletedId;
case LogType.ThreadCreated:
return l.ThreadCreatedId;
default:
return null;
}
}
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.Administrator)]
[OwnerOnly]
public async Task Log(LogType type)
{
var val = _service.Log(ctx.Guild.Id, ctx.Channel.Id, type);
if (val)
await ReplyConfirmLocalizedAsync(strs.log(Format.Bold(type.ToString())));
else
await ReplyConfirmLocalizedAsync(strs.log_stop(Format.Bold(type.ToString())));
}
}
}

View file

@ -0,0 +1,78 @@
#nullable disable
using Ellie.Db;
using Ellie.Services.Database.Models;
namespace Ellie.Modules.Administration.Services;
public sealed class GuildTimezoneService : ITimezoneService, IEService
{
public static ConcurrentDictionary<ulong, GuildTimezoneService> AllServices { get; } = new();
private readonly ConcurrentDictionary<ulong, TimeZoneInfo> _timezones;
private readonly DbService _db;
public GuildTimezoneService(DiscordSocketClient client, IBot bot, DbService db)
{
_timezones = bot.AllGuildConfigs.Select(GetTimzezoneTuple)
.Where(x => x.Timezone is not null)
.ToDictionary(x => x.GuildId, x => x.Timezone)
.ToConcurrent();
var curUser = client.CurrentUser;
if (curUser is not null)
AllServices.TryAdd(curUser.Id, this);
_db = db;
bot.JoinedGuild += Bot_JoinedGuild;
}
private Task Bot_JoinedGuild(GuildConfig arg)
{
var (guildId, tz) = GetTimzezoneTuple(arg);
if (tz is not null)
_timezones.TryAdd(guildId, tz);
return Task.CompletedTask;
}
private static (ulong GuildId, TimeZoneInfo Timezone) GetTimzezoneTuple(GuildConfig x)
{
TimeZoneInfo tz;
try
{
if (x.TimeZoneId is null)
tz = null;
else
tz = TimeZoneInfo.FindSystemTimeZoneById(x.TimeZoneId);
}
catch
{
tz = null;
}
return (x.GuildId, Timezone: tz);
}
public TimeZoneInfo GetTimeZoneOrDefault(ulong? guildId)
{
if (guildId is ulong gid && _timezones.TryGetValue(gid, out var tz))
return tz;
return null;
}
public void SetTimeZone(ulong guildId, TimeZoneInfo tz)
{
using var uow = _db.GetDbContext();
var gc = uow.GuildConfigsForId(guildId, set => set);
gc.TimeZoneId = tz?.Id;
uow.SaveChanges();
if (tz is null)
_timezones.TryRemove(guildId, out tz);
else
_timezones.AddOrUpdate(guildId, tz, (_, _) => tz);
}
public TimeZoneInfo GetTimeZoneOrUtc(ulong? guildId)
=> GetTimeZoneOrDefault(guildId) ?? TimeZoneInfo.Utc;
}

View file

@ -0,0 +1,75 @@
#nullable disable
using Ellie.Modules.Administration.Services;
namespace Ellie.Modules.Administration;
public partial class Administration
{
[Group]
public partial class TimeZoneCommands : EllieModule<GuildTimezoneService>
{
[Cmd]
[RequireContext(ContextType.Guild)]
public async Task Timezones(int page = 1)
{
page--;
if (page is < 0 or > 20)
return;
var timezones = TimeZoneInfo.GetSystemTimeZones().OrderBy(x => x.BaseUtcOffset).ToArray();
var timezonesPerPage = 20;
var curTime = DateTimeOffset.UtcNow;
var i = 0;
var timezoneStrings = timezones.Select(x => (x, ++i % 2 == 0))
.Select(data =>
{
var (tzInfo, flip) = data;
var nameStr = $"{tzInfo.Id,-30}";
var offset = curTime.ToOffset(tzInfo.GetUtcOffset(curTime))
.ToString("zzz");
if (flip)
return $"{offset} {Format.Code(nameStr)}";
return $"{Format.Code(offset)} {nameStr}";
});
await ctx.SendPaginatedConfirmAsync(page,
curPage => _eb.Create()
.WithOkColor()
.WithTitle(GetText(strs.timezones_available))
.WithDescription(string.Join("\n",
timezoneStrings.Skip(curPage * timezonesPerPage).Take(timezonesPerPage))),
timezones.Length,
timezonesPerPage);
}
[Cmd]
[RequireContext(ContextType.Guild)]
public async Task Timezone()
=> await ReplyConfirmLocalizedAsync(strs.timezone_guild(_service.GetTimeZoneOrUtc(ctx.Guild.Id)));
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.Administrator)]
public async Task Timezone([Leftover] string id)
{
TimeZoneInfo tz;
try { tz = TimeZoneInfo.FindSystemTimeZoneById(id); }
catch { tz = null; }
if (tz is null)
{
await ReplyErrorLocalizedAsync(strs.timezone_not_found);
return;
}
_service.SetTimeZone(ctx.Guild.Id, tz);
await SendConfirmAsync(tz.ToString());
}
}
}

View file

@ -0,0 +1,932 @@
#nullable disable
using CommandLine;
using Humanizer.Localisation;
using Ellie.Common.TypeReaders.Models;
using Ellie.Modules.Administration.Services;
using Ellie.Services.Database.Models;
namespace Ellie.Modules.Administration;
public partial class Administration
{
[Group]
public partial class UserPunishCommands : EllieModule<UserPunishService>
{
public enum AddRole
{
AddRole
}
private readonly MuteService _mute;
public UserPunishCommands(MuteService mute)
{
_mute = mute;
}
private async Task<bool> CheckRoleHierarchy(IGuildUser target)
{
var curUser = ((SocketGuild)ctx.Guild).CurrentUser;
var ownerId = ctx.Guild.OwnerId;
var modMaxRole = ((IGuildUser)ctx.User).GetRoles().Max(r => r.Position);
var targetMaxRole = target.GetRoles().Max(r => r.Position);
var botMaxRole = curUser.GetRoles().Max(r => r.Position);
// bot can't punish a user who is higher in the hierarchy. Discord will return 403
// moderator can be owner, in which case role hierarchy doesn't matter
// otherwise, moderator has to have a higher role
if (botMaxRole <= targetMaxRole
|| (ctx.User.Id != ownerId && targetMaxRole >= modMaxRole)
|| target.Id == ownerId)
{
await ReplyErrorLocalizedAsync(strs.hierarchy);
return false;
}
return true;
}
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.BanMembers)]
public Task Warn(IGuildUser user, [Leftover] string reason = null)
=> Warn(1, user, reason);
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.BanMembers)]
public async Task Warn(int weight, IGuildUser user, [Leftover] string reason = null)
{
if (weight <= 0)
return;
if (!await CheckRoleHierarchy(user))
return;
var dmFailed = false;
try
{
await user.EmbedAsync(_eb.Create()
.WithErrorColor()
.WithDescription(GetText(strs.warned_on(ctx.Guild.ToString())))
.AddField(GetText(strs.moderator), ctx.User.ToString())
.AddField(GetText(strs.reason), reason ?? "-"));
}
catch
{
dmFailed = true;
}
WarningPunishment punishment;
try
{
punishment = await _service.Warn(ctx.Guild, user.Id, ctx.User, weight, reason);
}
catch (Exception ex)
{
Log.Warning(ex, "Exception occured while warning a user");
var errorEmbed = _eb.Create().WithErrorColor().WithDescription(GetText(strs.cant_apply_punishment));
if (dmFailed)
errorEmbed.WithFooter("⚠️ " + GetText(strs.unable_to_dm_user));
await ctx.Channel.EmbedAsync(errorEmbed);
return;
}
var embed = _eb.Create().WithOkColor();
if (punishment is null)
embed.WithDescription(GetText(strs.user_warned(Format.Bold(user.ToString()))));
else
{
embed.WithDescription(GetText(strs.user_warned_and_punished(Format.Bold(user.ToString()),
Format.Bold(punishment.Punishment.ToString()))));
}
if (dmFailed)
embed.WithFooter("⚠️ " + GetText(strs.unable_to_dm_user));
await ctx.Channel.EmbedAsync(embed);
}
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.Administrator)]
[EllieOptions<WarnExpireOptions>]
[Priority(1)]
public async Task WarnExpire()
{
var expireDays = await _service.GetWarnExpire(ctx.Guild.Id);
if (expireDays == 0)
await ReplyConfirmLocalizedAsync(strs.warns_dont_expire);
else
await ReplyErrorLocalizedAsync(strs.warns_expire_in(expireDays));
}
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.Administrator)]
[EllieOptions<WarnExpireOptions>]
[Priority(2)]
public async Task WarnExpire(int days, params string[] args)
{
if (days is < 0 or > 366)
return;
var opts = OptionsParser.ParseFrom<WarnExpireOptions>(args);
await ctx.Channel.TriggerTypingAsync();
await _service.WarnExpireAsync(ctx.Guild.Id, days, opts.Delete);
if (days == 0)
{
await ReplyConfirmLocalizedAsync(strs.warn_expire_reset);
return;
}
if (opts.Delete)
await ReplyConfirmLocalizedAsync(strs.warn_expire_set_delete(Format.Bold(days.ToString())));
else
await ReplyConfirmLocalizedAsync(strs.warn_expire_set_clear(Format.Bold(days.ToString())));
}
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.BanMembers)]
[Priority(2)]
public Task Warnlog(int page, [Leftover] IGuildUser user = null)
{
user ??= (IGuildUser)ctx.User;
return Warnlog(page, user.Id);
}
[Cmd]
[RequireContext(ContextType.Guild)]
[Priority(3)]
public Task Warnlog(IGuildUser user = null)
{
user ??= (IGuildUser)ctx.User;
return ctx.User.Id == user.Id || ((IGuildUser)ctx.User).GuildPermissions.BanMembers
? Warnlog(user.Id)
: Task.CompletedTask;
}
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.BanMembers)]
[Priority(0)]
public Task Warnlog(int page, ulong userId)
=> InternalWarnlog(userId, page - 1);
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.BanMembers)]
[Priority(1)]
public Task Warnlog(ulong userId)
=> InternalWarnlog(userId, 0);
private async Task InternalWarnlog(ulong userId, int inputPage)
{
if (inputPage < 0)
return;
var allWarnings = _service.UserWarnings(ctx.Guild.Id, userId);
await ctx.SendPaginatedConfirmAsync(inputPage,
page =>
{
var warnings = allWarnings.Skip(page * 9).Take(9).ToArray();
var user = (ctx.Guild as SocketGuild)?.GetUser(userId)?.ToString() ?? userId.ToString();
var embed = _eb.Create().WithOkColor().WithTitle(GetText(strs.warnlog_for(user)));
if (!warnings.Any())
embed.WithDescription(GetText(strs.warnings_none));
else
{
var descText = GetText(strs.warn_count(
Format.Bold(warnings.Where(x => !x.Forgiven).Sum(x => x.Weight).ToString()),
Format.Bold(warnings.Sum(x => x.Weight).ToString())));
embed.WithDescription(descText);
var i = page * 9;
foreach (var w in warnings)
{
i++;
var name = GetText(strs.warned_on_by(w.DateAdded?.ToString("dd.MM.yyy"),
w.DateAdded?.ToString("HH:mm"),
w.Moderator));
if (w.Forgiven)
name = $"{Format.Strikethrough(name)} {GetText(strs.warn_cleared_by(w.ForgivenBy))}";
embed.AddField($"#`{i}` " + name,
Format.Code(GetText(strs.warn_weight(w.Weight))) + '\n' + w.Reason.TrimTo(1000));
}
}
return embed;
},
allWarnings.Length,
9);
}
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.BanMembers)]
public async Task WarnlogAll(int page = 1)
{
if (--page < 0)
return;
var warnings = _service.WarnlogAll(ctx.Guild.Id);
await ctx.SendPaginatedConfirmAsync(page,
curPage =>
{
var ws = warnings.Skip(curPage * 15)
.Take(15)
.ToArray()
.Select(x =>
{
var all = x.Count();
var forgiven = x.Count(y => y.Forgiven);
var total = all - forgiven;
var usr = ((SocketGuild)ctx.Guild).GetUser(x.Key);
return (usr?.ToString() ?? x.Key.ToString())
+ $" | {total} ({all} - {forgiven})";
});
return _eb.Create()
.WithOkColor()
.WithTitle(GetText(strs.warnings_list))
.WithDescription(string.Join("\n", ws));
},
warnings.Length,
15);
}
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.BanMembers)]
public Task Warnclear(IGuildUser user, int index = 0)
=> Warnclear(user.Id, index);
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.BanMembers)]
public async Task Warnclear(ulong userId, int index = 0)
{
if (index < 0)
return;
var success = await _service.WarnClearAsync(ctx.Guild.Id, userId, index, ctx.User.ToString());
var userStr = Format.Bold((ctx.Guild as SocketGuild)?.GetUser(userId)?.ToString() ?? userId.ToString());
if (index == 0)
await ReplyErrorLocalizedAsync(strs.warnings_cleared(userStr));
else
{
if (success)
await ReplyConfirmLocalizedAsync(strs.warning_cleared(Format.Bold(index.ToString()), userStr));
else
await ReplyErrorLocalizedAsync(strs.warning_clear_fail);
}
}
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.BanMembers)]
[Priority(1)]
public async Task WarnPunish(
int number,
AddRole _,
IRole role,
StoopidTime time = null)
{
var punish = PunishmentAction.AddRole;
if (ctx.Guild.OwnerId != ctx.User.Id
&& role.Position >= ((IGuildUser)ctx.User).GetRoles().Max(x => x.Position))
{
await ReplyErrorLocalizedAsync(strs.role_too_high);
return;
}
var success = _service.WarnPunish(ctx.Guild.Id, number, punish, time, role);
if (!success)
return;
if (time is null)
{
await ReplyConfirmLocalizedAsync(strs.warn_punish_set(Format.Bold(punish.ToString()),
Format.Bold(number.ToString())));
}
else
{
await ReplyConfirmLocalizedAsync(strs.warn_punish_set_timed(Format.Bold(punish.ToString()),
Format.Bold(number.ToString()),
Format.Bold(time.Input)));
}
}
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.BanMembers)]
public async Task WarnPunish(int number, PunishmentAction punish, StoopidTime time = null)
{
// this should never happen. Addrole has its own method with higher priority
// also disallow warn punishment for getting warned
if (punish is PunishmentAction.AddRole or PunishmentAction.Warn)
return;
// you must specify the time for timeout
if (punish is PunishmentAction.TimeOut && time is null)
return;
var success = _service.WarnPunish(ctx.Guild.Id, number, punish, time);
if (!success)
return;
if (time is null)
{
await ReplyConfirmLocalizedAsync(strs.warn_punish_set(Format.Bold(punish.ToString()),
Format.Bold(number.ToString())));
}
else
{
await ReplyConfirmLocalizedAsync(strs.warn_punish_set_timed(Format.Bold(punish.ToString()),
Format.Bold(number.ToString()),
Format.Bold(time.Input)));
}
}
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.BanMembers)]
public async Task WarnPunish(int number)
{
if (!_service.WarnPunishRemove(ctx.Guild.Id, number))
return;
await ReplyConfirmLocalizedAsync(strs.warn_punish_rem(Format.Bold(number.ToString())));
}
[Cmd]
[RequireContext(ContextType.Guild)]
public async Task WarnPunishList()
{
var ps = _service.WarnPunishList(ctx.Guild.Id);
string list;
if (ps.Any())
{
list = string.Join("\n",
ps.Select(x
=> $"{x.Count} -> {x.Punishment} {(x.Punishment == PunishmentAction.AddRole ? $"<@&{x.RoleId}>" : "")} {(x.Time <= 0 ? "" : x.Time + "m")} "));
}
else
list = GetText(strs.warnpl_none);
await SendConfirmAsync(GetText(strs.warn_punish_list), list);
}
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.BanMembers)]
[BotPerm(GuildPerm.BanMembers)]
[Priority(1)]
public Task Ban(StoopidTime time, IUser user, [Leftover] string msg = null)
=> Ban(time, user.Id, msg);
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.BanMembers)]
[BotPerm(GuildPerm.BanMembers)]
[Priority(0)]
public async Task Ban(StoopidTime time, ulong userId, [Leftover] string msg = null)
{
if (time.Time > TimeSpan.FromDays(49))
return;
var guildUser = await ((DiscordSocketClient)Context.Client).Rest.GetGuildUserAsync(ctx.Guild.Id, userId);
if (guildUser is not null && !await CheckRoleHierarchy(guildUser))
return;
var dmFailed = false;
if (guildUser is not null)
{
try
{
var defaultMessage = GetText(strs.bandm(Format.Bold(ctx.Guild.Name), msg));
var embed = _service.GetBanUserDmEmbed(Context, guildUser, defaultMessage, msg, time.Time);
if (embed is not null)
await guildUser.SendAsync(embed);
}
catch
{
dmFailed = true;
}
}
var user = await ctx.Client.GetUserAsync(userId);
var banPrune = await _service.GetBanPruneAsync(ctx.Guild.Id) ?? 7;
await _mute.TimedBan(ctx.Guild, userId, time.Time, (ctx.User + " | " + msg).TrimTo(512), banPrune);
var toSend = _eb.Create()
.WithOkColor()
.WithTitle("⛔️ " + GetText(strs.banned_user))
.AddField(GetText(strs.username), user?.ToString() ?? userId.ToString(), true)
.AddField("ID", userId.ToString(), true)
.AddField(GetText(strs.duration),
time.Time.Humanize(3, minUnit: TimeUnit.Minute, culture: Culture),
true);
if (dmFailed)
toSend.WithFooter("⚠️ " + GetText(strs.unable_to_dm_user));
await ctx.Channel.EmbedAsync(toSend);
}
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.BanMembers)]
[BotPerm(GuildPerm.BanMembers)]
[Priority(0)]
public async Task Ban(ulong userId, [Leftover] string msg = null)
{
var user = await ((DiscordSocketClient)Context.Client).Rest.GetGuildUserAsync(ctx.Guild.Id, userId);
if (user is null)
{
var banPrune = await _service.GetBanPruneAsync(ctx.Guild.Id) ?? 7;
await ctx.Guild.AddBanAsync(userId, banPrune, (ctx.User + " | " + msg).TrimTo(512));
await ctx.Channel.EmbedAsync(_eb.Create()
.WithOkColor()
.WithTitle("⛔️ " + GetText(strs.banned_user))
.AddField("ID", userId.ToString(), true));
}
else
await Ban(user, msg);
}
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.BanMembers)]
[BotPerm(GuildPerm.BanMembers)]
[Priority(2)]
public async Task Ban(IGuildUser user, [Leftover] string msg = null)
{
if (!await CheckRoleHierarchy(user))
return;
var dmFailed = false;
try
{
var defaultMessage = GetText(strs.bandm(Format.Bold(ctx.Guild.Name), msg));
var embed = _service.GetBanUserDmEmbed(Context, user, defaultMessage, msg, null);
if (embed is not null)
await user.SendAsync(embed);
}
catch
{
dmFailed = true;
}
var banPrune = await _service.GetBanPruneAsync(ctx.Guild.Id) ?? 7;
await ctx.Guild.AddBanAsync(user, banPrune, (ctx.User + " | " + msg).TrimTo(512));
var toSend = _eb.Create()
.WithOkColor()
.WithTitle("⛔️ " + GetText(strs.banned_user))
.AddField(GetText(strs.username), user.ToString(), true)
.AddField("ID", user.Id.ToString(), true);
if (dmFailed)
toSend.WithFooter("⚠️ " + GetText(strs.unable_to_dm_user));
await ctx.Channel.EmbedAsync(toSend);
}
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.BanMembers)]
[BotPerm(GuildPerm.BanMembers)]
public async Task BanPrune(int days)
{
if (days < 0 || days > 7)
{
await ReplyErrorLocalizedAsync(strs.invalid_input);
return;
}
await _service.SetBanPruneAsync(ctx.Guild.Id, days);
if (days == 0)
await ReplyConfirmLocalizedAsync(strs.ban_prune_disabled);
else
await ReplyConfirmLocalizedAsync(strs.ban_prune(days));
}
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.BanMembers)]
[BotPerm(GuildPerm.BanMembers)]
public async Task BanMessage([Leftover] string message = null)
{
if (message is null)
{
var template = _service.GetBanTemplate(ctx.Guild.Id);
if (template is null)
{
await ReplyConfirmLocalizedAsync(strs.banmsg_default);
return;
}
await SendConfirmAsync(template);
return;
}
_service.SetBanTemplate(ctx.Guild.Id, message);
await ctx.OkAsync();
}
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.BanMembers)]
[BotPerm(GuildPerm.BanMembers)]
public async Task BanMsgReset()
{
_service.SetBanTemplate(ctx.Guild.Id, null);
await ctx.OkAsync();
}
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.BanMembers)]
[BotPerm(GuildPerm.BanMembers)]
[Priority(0)]
public Task BanMessageTest([Leftover] string reason = null)
=> InternalBanMessageTest(reason, null);
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.BanMembers)]
[BotPerm(GuildPerm.BanMembers)]
[Priority(1)]
public Task BanMessageTest(StoopidTime duration, [Leftover] string reason = null)
=> InternalBanMessageTest(reason, duration.Time);
private async Task InternalBanMessageTest(string reason, TimeSpan? duration)
{
var defaultMessage = GetText(strs.bandm(Format.Bold(ctx.Guild.Name), reason));
var embed = _service.GetBanUserDmEmbed(Context, (IGuildUser)ctx.User, defaultMessage, reason, duration);
if (embed is null)
await ConfirmLocalizedAsync(strs.banmsg_disabled);
else
{
try
{
await ctx.User.SendAsync(embed);
}
catch (Exception)
{
await ReplyErrorLocalizedAsync(strs.unable_to_dm_user);
return;
}
await ctx.OkAsync();
}
}
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.BanMembers)]
[BotPerm(GuildPerm.BanMembers)]
public async Task Unban([Leftover] string user)
{
var bans = await ctx.Guild.GetBansAsync().FlattenAsync();
var bun = bans.FirstOrDefault(x => x.User.ToString()!.ToLowerInvariant() == user.ToLowerInvariant());
if (bun is null)
{
await ReplyErrorLocalizedAsync(strs.user_not_found);
return;
}
await UnbanInternal(bun.User);
}
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.BanMembers)]
[BotPerm(GuildPerm.BanMembers)]
public async Task Unban(ulong userId)
{
var bun = await ctx.Guild.GetBanAsync(userId);
if (bun is null)
{
await ReplyErrorLocalizedAsync(strs.user_not_found);
return;
}
await UnbanInternal(bun.User);
}
private async Task UnbanInternal(IUser user)
{
await ctx.Guild.RemoveBanAsync(user);
await ReplyConfirmLocalizedAsync(strs.unbanned_user(Format.Bold(user.ToString())));
}
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.KickMembers | GuildPerm.ManageMessages)]
[BotPerm(GuildPerm.BanMembers)]
public Task Softban(IGuildUser user, [Leftover] string msg = null)
=> SoftbanInternal(user, msg);
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.KickMembers | GuildPerm.ManageMessages)]
[BotPerm(GuildPerm.BanMembers)]
public async Task Softban(ulong userId, [Leftover] string msg = null)
{
var user = await ((DiscordSocketClient)Context.Client).Rest.GetGuildUserAsync(ctx.Guild.Id, userId);
if (user is null)
return;
await SoftbanInternal(user, msg);
}
private async Task SoftbanInternal(IGuildUser user, [Leftover] string msg = null)
{
if (!await CheckRoleHierarchy(user))
return;
var dmFailed = false;
try
{
await user.SendErrorAsync(_eb, GetText(strs.sbdm(Format.Bold(ctx.Guild.Name), msg)));
}
catch
{
dmFailed = true;
}
var banPrune = await _service.GetBanPruneAsync(ctx.Guild.Id) ?? 7;
await ctx.Guild.AddBanAsync(user, banPrune, ("Softban | " + ctx.User + " | " + msg).TrimTo(512));
try { await ctx.Guild.RemoveBanAsync(user); }
catch { await ctx.Guild.RemoveBanAsync(user); }
var toSend = _eb.Create()
.WithOkColor()
.WithTitle("☣ " + GetText(strs.sb_user))
.AddField(GetText(strs.username), user.ToString(), true)
.AddField("ID", user.Id.ToString(), true);
if (dmFailed)
toSend.WithFooter("⚠️ " + GetText(strs.unable_to_dm_user));
await ctx.Channel.EmbedAsync(toSend);
}
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.KickMembers)]
[BotPerm(GuildPerm.KickMembers)]
[Priority(1)]
public Task Kick(IGuildUser user, [Leftover] string msg = null)
=> KickInternal(user, msg);
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.KickMembers)]
[BotPerm(GuildPerm.KickMembers)]
[Priority(0)]
public async Task Kick(ulong userId, [Leftover] string msg = null)
{
var user = await ((DiscordSocketClient)Context.Client).Rest.GetGuildUserAsync(ctx.Guild.Id, userId);
if (user is null)
return;
await KickInternal(user, msg);
}
private async Task KickInternal(IGuildUser user, string msg = null)
{
if (!await CheckRoleHierarchy(user))
return;
var dmFailed = false;
try
{
await user.SendErrorAsync(_eb, GetText(strs.kickdm(Format.Bold(ctx.Guild.Name), msg)));
}
catch
{
dmFailed = true;
}
await user.KickAsync((ctx.User + " | " + msg).TrimTo(512));
var toSend = _eb.Create()
.WithOkColor()
.WithTitle(GetText(strs.kicked_user))
.AddField(GetText(strs.username), user.ToString(), true)
.AddField("ID", user.Id.ToString(), true);
if (dmFailed)
toSend.WithFooter("⚠️ " + GetText(strs.unable_to_dm_user));
await ctx.Channel.EmbedAsync(toSend);
}
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ModerateMembers)]
[BotPerm(GuildPerm.ModerateMembers)]
[Priority(2)]
public async Task Timeout(IUser globalUser, StoopidTime time, [Leftover] string msg = null)
{
var user = await ctx.Guild.GetUserAsync(globalUser.Id);
if (user is null)
return;
if (!await CheckRoleHierarchy(user))
return;
var dmFailed = false;
try
{
var dmMessage = GetText(strs.timeoutdm(Format.Bold(ctx.Guild.Name), msg));
await user.EmbedAsync(_eb.Create(ctx)
.WithPendingColor()
.WithDescription(dmMessage));
}
catch
{
dmFailed = true;
}
await user.SetTimeOutAsync(time.Time);
var toSend = _eb.Create()
.WithOkColor()
.WithTitle("⏳ " + GetText(strs.timedout_user))
.AddField(GetText(strs.username), user.ToString(), true)
.AddField("ID", user.Id.ToString(), true);
if (dmFailed)
toSend.WithFooter("⚠️ " + GetText(strs.unable_to_dm_user));
await ctx.Channel.EmbedAsync(toSend);
}
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.BanMembers)]
[BotPerm(GuildPerm.BanMembers)]
[Ratelimit(30)]
public async Task MassBan(params string[] userStrings)
{
if (userStrings.Length == 0)
return;
var missing = new List<string>();
var banning = new HashSet<IUser>();
await ctx.Channel.TriggerTypingAsync();
foreach (var userStr in userStrings)
{
if (ulong.TryParse(userStr, out var userId))
{
IUser user = await ctx.Guild.GetUserAsync(userId)
?? await ((DiscordSocketClient)Context.Client).Rest.GetGuildUserAsync(ctx.Guild.Id,
userId);
if (user is null)
{
// if IGuildUser is null, try to get IUser
user = await ((DiscordSocketClient)Context.Client).Rest.GetUserAsync(userId);
// only add to missing if *still* null
if (user is null)
{
missing.Add(userStr);
continue;
}
}
//Hierachy checks only if the user is in the guild
if (user is IGuildUser gu && !await CheckRoleHierarchy(gu))
return;
banning.Add(user);
}
else
missing.Add(userStr);
}
var missStr = string.Join("\n", missing);
if (string.IsNullOrWhiteSpace(missStr))
missStr = "-";
var toSend = _eb.Create(ctx)
.WithDescription(GetText(strs.mass_ban_in_progress(banning.Count)))
.AddField(GetText(strs.invalid(missing.Count)), missStr)
.WithPendingColor();
var banningMessage = await ctx.Channel.EmbedAsync(toSend);
var banPrune = await _service.GetBanPruneAsync(ctx.Guild.Id) ?? 7;
foreach (var toBan in banning)
{
try
{
await ctx.Guild.AddBanAsync(toBan.Id, banPrune, $"{ctx.User} | Massban");
}
catch (Exception ex)
{
Log.Warning(ex, "Error banning {User} user in {GuildId} server", toBan.Id, ctx.Guild.Id);
}
}
await banningMessage.ModifyAsync(x => x.Embed = _eb.Create()
.WithDescription(
GetText(strs.mass_ban_completed(banning.Count())))
.AddField(GetText(strs.invalid(missing.Count)), missStr)
.WithOkColor()
.Build());
}
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.BanMembers)]
[BotPerm(GuildPerm.BanMembers)]
[OwnerOnly]
public async Task MassKill([Leftover] string people)
{
if (string.IsNullOrWhiteSpace(people))
return;
var (bans, missing) = _service.MassKill((SocketGuild)ctx.Guild, people);
var missStr = string.Join("\n", missing);
if (string.IsNullOrWhiteSpace(missStr))
missStr = "-";
//send a message but don't wait for it
var banningMessageTask = ctx.Channel.EmbedAsync(_eb.Create()
.WithDescription(
GetText(strs.mass_kill_in_progress(bans.Count())))
.AddField(GetText(strs.invalid(missing)), missStr)
.WithPendingColor());
var banPrune = await _service.GetBanPruneAsync(ctx.Guild.Id) ?? 7;
//do the banning
await Task.WhenAll(bans.Where(x => x.Id.HasValue)
.Select(x => ctx.Guild.AddBanAsync(x.Id.Value,
banPrune,
x.Reason,
new()
{
RetryMode = RetryMode.AlwaysRetry
})));
//wait for the message and edit it
var banningMessage = await banningMessageTask;
await banningMessage.ModifyAsync(x => x.Embed = _eb.Create()
.WithDescription(
GetText(strs.mass_kill_completed(bans.Count())))
.AddField(GetText(strs.invalid(missing)), missStr)
.WithOkColor()
.Build());
}
public class WarnExpireOptions : IEllieCommandOptions
{
[Option('d', "delete", Default = false, HelpText = "Delete warnings instead of clearing them.")]
public bool Delete { get; set; } = false;
public void NormalizeOptions()
{
}
}
}
}

View file

@ -0,0 +1,595 @@
#nullable disable
using LinqToDB;
using LinqToDB.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using Ellie.Common.ModuleBehaviors;
using Ellie.Common.TypeReaders.Models;
using Ellie.Db;
using Ellie.Modules.Permissions.Services;
using Ellie.Services.Database.Models;
using Newtonsoft.Json;
namespace Ellie.Modules.Administration.Services;
public class UserPunishService : IEService, IReadyExecutor
{
private readonly MuteService _mute;
private readonly DbService _db;
private readonly BlacklistService _blacklistService;
private readonly BotConfigService _bcs;
private readonly DiscordSocketClient _client;
public event Func<Warning, Task> OnUserWarned = static delegate { return Task.CompletedTask; };
public UserPunishService(
MuteService mute,
DbService db,
BlacklistService blacklistService,
BotConfigService bcs,
DiscordSocketClient client)
{
_mute = mute;
_db = db;
_blacklistService = blacklistService;
_bcs = bcs;
_client = client;
}
public async Task OnReadyAsync()
{
if (_client.ShardId != 0)
return;
using var expiryTimer = new PeriodicTimer(TimeSpan.FromHours(12));
do
{
try
{
await CheckAllWarnExpiresAsync();
}
catch (Exception ex)
{
Log.Error(ex, "Unexpected error while checking for warn expiries: {ErrorMessage}", ex.Message);
}
} while (await expiryTimer.WaitForNextTickAsync());
}
public async Task<WarningPunishment> Warn(
IGuild guild,
ulong userId,
IUser mod,
long weight,
string reason)
{
if (weight <= 0)
throw new ArgumentOutOfRangeException(nameof(weight));
var modName = mod.ToString();
if (string.IsNullOrWhiteSpace(reason))
reason = "-";
var guildId = guild.Id;
var warn = new Warning
{
UserId = userId,
GuildId = guildId,
Forgiven = false,
Reason = reason,
Moderator = modName,
Weight = weight
};
long previousCount;
List<WarningPunishment> ps;
await using (var uow = _db.GetDbContext())
{
ps = uow.GuildConfigsForId(guildId, set => set.Include(x => x.WarnPunishments)).WarnPunishments;
previousCount = uow.Set<Warning>().ForId(guildId, userId)
.Where(w => !w.Forgiven && w.UserId == userId)
.Sum(x => x.Weight);
uow.Set<Warning>().Add(warn);
await uow.SaveChangesAsync();
}
_ = OnUserWarned(warn);
var totalCount = previousCount + weight;
var p = ps.Where(x => x.Count > previousCount && x.Count <= totalCount)
.MaxBy(x => x.Count);
if (p is not null)
{
var user = await guild.GetUserAsync(userId);
if (user is null)
return null;
await ApplyPunishment(guild, user, mod, p.Punishment, p.Time, p.RoleId, "Warned too many times.");
return p;
}
return null;
}
public async Task ApplyPunishment(
IGuild guild,
IGuildUser user,
IUser mod,
PunishmentAction p,
int minutes,
ulong? roleId,
string reason)
{
if (!await CheckPermission(guild, p))
return;
int banPrune;
switch (p)
{
case PunishmentAction.Mute:
if (minutes == 0)
await _mute.MuteUser(user, mod, reason: reason);
else
await _mute.TimedMute(user, mod, TimeSpan.FromMinutes(minutes), reason: reason);
break;
case PunishmentAction.VoiceMute:
if (minutes == 0)
await _mute.MuteUser(user, mod, MuteType.Voice, reason);
else
await _mute.TimedMute(user, mod, TimeSpan.FromMinutes(minutes), MuteType.Voice, reason);
break;
case PunishmentAction.ChatMute:
if (minutes == 0)
await _mute.MuteUser(user, mod, MuteType.Chat, reason);
else
await _mute.TimedMute(user, mod, TimeSpan.FromMinutes(minutes), MuteType.Chat, reason);
break;
case PunishmentAction.Kick:
await user.KickAsync(reason);
break;
case PunishmentAction.Ban:
banPrune = await GetBanPruneAsync(user.GuildId) ?? 7;
if (minutes == 0)
await guild.AddBanAsync(user, reason: reason, pruneDays: banPrune);
else
await _mute.TimedBan(user.Guild, user.Id, TimeSpan.FromMinutes(minutes), reason, banPrune);
break;
case PunishmentAction.Softban:
banPrune = await GetBanPruneAsync(user.GuildId) ?? 7;
await guild.AddBanAsync(user, banPrune, $"Softban | {reason}");
try
{
await guild.RemoveBanAsync(user);
}
catch
{
await guild.RemoveBanAsync(user);
}
break;
case PunishmentAction.RemoveRoles:
await user.RemoveRolesAsync(user.GetRoles().Where(x => !x.IsManaged && x != x.Guild.EveryoneRole));
break;
case PunishmentAction.AddRole:
if (roleId is null)
return;
var role = guild.GetRole(roleId.Value);
if (role is not null)
{
if (minutes == 0)
await user.AddRoleAsync(role);
else
await _mute.TimedRole(user, TimeSpan.FromMinutes(minutes), reason, role);
}
else
{
Log.Warning("Can't find role {RoleId} on server {GuildId} to apply punishment",
roleId.Value,
guild.Id);
}
break;
case PunishmentAction.Warn:
await Warn(guild, user.Id, mod, 1, reason);
break;
case PunishmentAction.TimeOut:
await user.SetTimeOutAsync(TimeSpan.FromMinutes(minutes));
break;
}
}
/// <summary>
/// Used to prevent the bot from hitting 403's when it needs to
/// apply punishments with insufficient permissions
/// </summary>
/// <param name="guild">Guild the punishment is applied in</param>
/// <param name="punish">Punishment to apply</param>
/// <returns>Whether the bot has sufficient permissions</returns>
private async Task<bool> CheckPermission(IGuild guild, PunishmentAction punish)
{
var botUser = await guild.GetCurrentUserAsync();
switch (punish)
{
case PunishmentAction.Mute:
return botUser.GuildPermissions.MuteMembers && botUser.GuildPermissions.ManageRoles;
case PunishmentAction.Kick:
return botUser.GuildPermissions.KickMembers;
case PunishmentAction.Ban:
return botUser.GuildPermissions.BanMembers;
case PunishmentAction.Softban:
return botUser.GuildPermissions.BanMembers; // ban + unban
case PunishmentAction.RemoveRoles:
return botUser.GuildPermissions.ManageRoles;
case PunishmentAction.ChatMute:
return botUser.GuildPermissions.ManageRoles; // adds nadeko-mute role
case PunishmentAction.VoiceMute:
return botUser.GuildPermissions.MuteMembers;
case PunishmentAction.AddRole:
return botUser.GuildPermissions.ManageRoles;
case PunishmentAction.TimeOut:
return botUser.GuildPermissions.ModerateMembers;
default:
return true;
}
}
public async Task CheckAllWarnExpiresAsync()
{
await using var uow = _db.GetDbContext();
var cleared = await uow.Set<Warning>()
.Where(x => uow.Set<GuildConfig>()
.Any(y => y.GuildId == x.GuildId
&& y.WarnExpireHours > 0
&& y.WarnExpireAction == WarnExpireAction.Clear)
&& x.Forgiven == false
&& x.DateAdded
< DateTime.UtcNow.AddHours(-uow.Set<GuildConfig>()
.Where(y => x.GuildId == y.GuildId)
.Select(y => y.WarnExpireHours)
.First()))
.UpdateAsync(_ => new()
{
Forgiven = true,
ForgivenBy = "expiry"
});
var deleted = await uow.Set<Warning>()
.Where(x => uow.Set<GuildConfig>()
.Any(y => y.GuildId == x.GuildId
&& y.WarnExpireHours > 0
&& y.WarnExpireAction == WarnExpireAction.Delete)
&& x.DateAdded
< DateTime.UtcNow.AddHours(-uow.Set<GuildConfig>()
.Where(y => x.GuildId == y.GuildId)
.Select(y => y.WarnExpireHours)
.First()))
.DeleteAsync();
if (cleared > 0 || deleted > 0)
{
Log.Information("Cleared {ClearedWarnings} warnings and deleted {DeletedWarnings} warnings due to expiry",
cleared,
deleted);
}
await uow.SaveChangesAsync();
}
public async Task CheckWarnExpiresAsync(ulong guildId)
{
await using var uow = _db.GetDbContext();
var config = uow.GuildConfigsForId(guildId, inc => inc);
if (config.WarnExpireHours == 0)
return;
if (config.WarnExpireAction == WarnExpireAction.Clear)
{
await uow.Set<Warning>()
.Where(x => x.GuildId == guildId
&& x.Forgiven == false
&& x.DateAdded < DateTime.UtcNow.AddHours(-config.WarnExpireHours))
.UpdateAsync(_ => new()
{
Forgiven = true,
ForgivenBy = "expiry"
});
}
else if (config.WarnExpireAction == WarnExpireAction.Delete)
{
await uow.Set<Warning>()
.Where(x => x.GuildId == guildId
&& x.DateAdded < DateTime.UtcNow.AddHours(-config.WarnExpireHours))
.DeleteAsync();
}
await uow.SaveChangesAsync();
}
public Task<int> GetWarnExpire(ulong guildId)
{
using var uow = _db.GetDbContext();
var config = uow.GuildConfigsForId(guildId, set => set);
return Task.FromResult(config.WarnExpireHours / 24);
}
public async Task WarnExpireAsync(ulong guildId, int days, bool delete)
{
await using (var uow = _db.GetDbContext())
{
var config = uow.GuildConfigsForId(guildId, inc => inc);
config.WarnExpireHours = days * 24;
config.WarnExpireAction = delete ? WarnExpireAction.Delete : WarnExpireAction.Clear;
await uow.SaveChangesAsync();
// no need to check for warn expires
if (config.WarnExpireHours == 0)
return;
}
await CheckWarnExpiresAsync(guildId);
}
public IGrouping<ulong, Warning>[] WarnlogAll(ulong gid)
{
using var uow = _db.GetDbContext();
return uow.Set<Warning>().GetForGuild(gid).GroupBy(x => x.UserId).ToArray();
}
public Warning[] UserWarnings(ulong gid, ulong userId)
{
using var uow = _db.GetDbContext();
return uow.Set<Warning>().ForId(gid, userId);
}
public async Task<bool> WarnClearAsync(
ulong guildId,
ulong userId,
int index,
string moderator)
{
var toReturn = true;
await using var uow = _db.GetDbContext();
if (index == 0)
await uow.Set<Warning>().ForgiveAll(guildId, userId, moderator);
else
toReturn = uow.Set<Warning>().Forgive(guildId, userId, moderator, index - 1);
await uow.SaveChangesAsync();
return toReturn;
}
public bool WarnPunish(
ulong guildId,
int number,
PunishmentAction punish,
StoopidTime time,
IRole role = null)
{
// these 3 don't make sense with time
if (punish is PunishmentAction.Softban or PunishmentAction.Kick or PunishmentAction.RemoveRoles
&& time is not null)
return false;
if (number <= 0 || (time is not null && time.Time > TimeSpan.FromDays(49)))
return false;
if (punish is PunishmentAction.AddRole && role is null)
return false;
if (punish is PunishmentAction.TimeOut && time is null)
return false;
using var uow = _db.GetDbContext();
var ps = uow.GuildConfigsForId(guildId, set => set.Include(x => x.WarnPunishments)).WarnPunishments;
var toDelete = ps.Where(x => x.Count == number);
uow.RemoveRange(toDelete);
ps.Add(new()
{
Count = number,
Punishment = punish,
Time = (int?)time?.Time.TotalMinutes ?? 0,
RoleId = punish == PunishmentAction.AddRole ? role!.Id : default(ulong?)
});
uow.SaveChanges();
return true;
}
public bool WarnPunishRemove(ulong guildId, int number)
{
if (number <= 0)
return false;
using var uow = _db.GetDbContext();
var ps = uow.GuildConfigsForId(guildId, set => set.Include(x => x.WarnPunishments)).WarnPunishments;
var p = ps.FirstOrDefault(x => x.Count == number);
if (p is not null)
{
uow.Remove(p);
uow.SaveChanges();
}
return true;
}
public WarningPunishment[] WarnPunishList(ulong guildId)
{
using var uow = _db.GetDbContext();
return uow.GuildConfigsForId(guildId, gc => gc.Include(x => x.WarnPunishments))
.WarnPunishments.OrderBy(x => x.Count)
.ToArray();
}
public (IReadOnlyCollection<(string Original, ulong? Id, string Reason)> Bans, int Missing) MassKill(
SocketGuild guild,
string people)
{
var gusers = guild.Users;
//get user objects and reasons
var bans = people.Split("\n")
.Select(x =>
{
var split = x.Trim().Split(" ");
var reason = string.Join(" ", split.Skip(1));
if (ulong.TryParse(split[0], out var id))
return (Original: split[0], Id: id, Reason: reason);
return (Original: split[0],
gusers.FirstOrDefault(u => u.ToString().ToLowerInvariant() == x)?.Id,
Reason: reason);
})
.ToArray();
//if user is null, means that person couldn't be found
var missing = bans.Count(x => !x.Id.HasValue);
//get only data for found users
var found = bans.Where(x => x.Id.HasValue).Select(x => x.Id.Value).ToList();
_blacklistService.BlacklistUsers(found);
return (bans, missing);
}
public string GetBanTemplate(ulong guildId)
{
using var uow = _db.GetDbContext();
var template = uow.Set<BanTemplate>().AsQueryable().FirstOrDefault(x => x.GuildId == guildId);
return template?.Text;
}
public void SetBanTemplate(ulong guildId, string text)
{
using var uow = _db.GetDbContext();
var template = uow.Set<BanTemplate>().AsQueryable().FirstOrDefault(x => x.GuildId == guildId);
if (text is null)
{
if (template is null)
return;
uow.Remove(template);
}
else if (template is null)
{
uow.Set<BanTemplate>().Add(new()
{
GuildId = guildId,
Text = text
});
}
else
template.Text = text;
uow.SaveChanges();
}
public async Task SetBanPruneAsync(ulong guildId, int? pruneDays)
{
await using var ctx = _db.GetDbContext();
await ctx.Set<BanTemplate>()
.ToLinqToDBTable()
.InsertOrUpdateAsync(() => new()
{
GuildId = guildId,
Text = null,
DateAdded = DateTime.UtcNow,
PruneDays = pruneDays
},
old => new()
{
PruneDays = pruneDays
},
() => new()
{
GuildId = guildId
});
}
public async Task<int?> GetBanPruneAsync(ulong guildId)
{
await using var ctx = _db.GetDbContext();
return await ctx.Set<BanTemplate>()
.Where(x => x.GuildId == guildId)
.Select(x => x.PruneDays)
.FirstOrDefaultAsyncLinqToDB();
}
public SmartText GetBanUserDmEmbed(
ICommandContext context,
IGuildUser target,
string defaultMessage,
string banReason,
TimeSpan? duration)
=> GetBanUserDmEmbed((DiscordSocketClient)context.Client,
(SocketGuild)context.Guild,
(IGuildUser)context.User,
target,
defaultMessage,
banReason,
duration);
public SmartText GetBanUserDmEmbed(
DiscordSocketClient client,
SocketGuild guild,
IGuildUser moderator,
IGuildUser target,
string defaultMessage,
string banReason,
TimeSpan? duration)
{
var template = GetBanTemplate(guild.Id);
banReason = string.IsNullOrWhiteSpace(banReason) ? "-" : banReason;
var replacer = new ReplacementBuilder().WithServer(client, guild)
.WithOverride("%ban.mod%", () => moderator.ToString())
.WithOverride("%ban.mod.fullname%", () => moderator.ToString())
.WithOverride("%ban.mod.name%", () => moderator.Username)
.WithOverride("%ban.mod.discrim%", () => moderator.Discriminator)
.WithOverride("%ban.user%", () => target.ToString())
.WithOverride("%ban.user.fullname%", () => target.ToString())
.WithOverride("%ban.user.name%", () => target.Username)
.WithOverride("%ban.user.discrim%", () => target.Discriminator)
.WithOverride("%reason%", () => banReason)
.WithOverride("%ban.reason%", () => banReason)
.WithOverride("%ban.duration%",
() => duration?.ToString(@"d\.hh\:mm") ?? "perma")
.Build();
// if template isn't set, use the old message style
if (string.IsNullOrWhiteSpace(template))
{
template = JsonConvert.SerializeObject(new
{
color = _bcs.Data.Color.Error.PackedValue >> 8,
description = defaultMessage
});
}
// if template is set to "-" do not dm the user
else if (template == "-")
return default;
// if template is an embed, send that embed with replacements
// otherwise, treat template as a regular string with replacements
else if (SmartText.CreateFrom(template) is not { IsEmbed: true } or { IsEmbedArray: true })
{
template = JsonConvert.SerializeObject(new
{
color = _bcs.Data.Color.Error.PackedValue >> 8,
description = template
});
}
var output = SmartText.CreateFrom(template);
return replacer.Replace(output);
}
}

View file

@ -0,0 +1,77 @@
#nullable disable
using Ellie.Modules.Administration.Services;
namespace Ellie.Modules.Administration;
public partial class Administration
{
[Group]
public partial class VcRoleCommands : EllieModule<VcRoleService>
{
[Cmd]
[UserPerm(GuildPerm.ManageRoles)]
[BotPerm(GuildPerm.ManageRoles)]
[RequireContext(ContextType.Guild)]
public async Task VcRoleRm(ulong vcId)
{
if (_service.RemoveVcRole(ctx.Guild.Id, vcId))
await ReplyConfirmLocalizedAsync(strs.vcrole_removed(Format.Bold(vcId.ToString())));
else
await ReplyErrorLocalizedAsync(strs.vcrole_not_found);
}
[Cmd]
[UserPerm(GuildPerm.ManageRoles)]
[BotPerm(GuildPerm.ManageRoles)]
[RequireContext(ContextType.Guild)]
public async Task VcRole([Leftover] IRole role = null)
{
var user = (IGuildUser)ctx.User;
var vc = user.VoiceChannel;
if (vc is null || vc.GuildId != user.GuildId)
{
await ReplyErrorLocalizedAsync(strs.must_be_in_voice);
return;
}
if (role is null)
{
if (_service.RemoveVcRole(ctx.Guild.Id, vc.Id))
await ReplyConfirmLocalizedAsync(strs.vcrole_removed(Format.Bold(vc.Name)));
}
else
{
_service.AddVcRole(ctx.Guild.Id, role, vc.Id);
await ReplyConfirmLocalizedAsync(strs.vcrole_added(Format.Bold(vc.Name), Format.Bold(role.Name)));
}
}
[Cmd]
[RequireContext(ContextType.Guild)]
public async Task VcRoleList()
{
var guild = (SocketGuild)ctx.Guild;
string text;
if (_service.VcRoles.TryGetValue(ctx.Guild.Id, out var roles))
{
if (!roles.Any())
text = GetText(strs.no_vcroles);
else
{
text = string.Join("\n",
roles.Select(x
=> $"{Format.Bold(guild.GetVoiceChannel(x.Key)?.Name ?? x.Key.ToString())} => {x.Value}"));
}
}
else
text = GetText(strs.no_vcroles);
await ctx.Channel.EmbedAsync(_eb.Create()
.WithOkColor()
.WithTitle(GetText(strs.vc_role_list))
.WithDescription(text));
}
}
}

View file

@ -0,0 +1,209 @@
#nullable disable
using Microsoft.EntityFrameworkCore;
using Ellie.Db;
using Ellie.Services.Database.Models;
namespace Ellie.Modules.Administration.Services;
public class VcRoleService : IEService
{
public ConcurrentDictionary<ulong, ConcurrentDictionary<ulong, IRole>> VcRoles { get; }
public ConcurrentDictionary<ulong, System.Collections.Concurrent.ConcurrentQueue<(bool, IGuildUser, IRole)>> ToAssign { get; }
private readonly DbService _db;
private readonly DiscordSocketClient _client;
public VcRoleService(DiscordSocketClient client, IBot bot, DbService db)
{
_db = db;
_client = client;
_client.UserVoiceStateUpdated += ClientOnUserVoiceStateUpdated;
VcRoles = new();
ToAssign = new();
using (var uow = db.GetDbContext())
{
var guildIds = client.Guilds.Select(x => x.Id).ToList();
uow.Set<GuildConfig>()
.AsQueryable()
.Include(x => x.VcRoleInfos)
.Where(x => guildIds.Contains(x.GuildId))
.AsEnumerable()
.Select(InitializeVcRole)
.WhenAll();
}
Task.Run(async () =>
{
while (true)
{
Task Selector(System.Collections.Concurrent.ConcurrentQueue<(bool, IGuildUser, IRole)> queue)
{
return Task.Run(async () =>
{
while (queue.TryDequeue(out var item))
{
var (add, user, role) = item;
try
{
if (add)
{
if (!user.RoleIds.Contains(role.Id))
await user.AddRoleAsync(role);
}
else
{
if (user.RoleIds.Contains(role.Id))
await user.RemoveRoleAsync(role);
}
}
catch
{
}
await Task.Delay(250);
}
});
}
await ToAssign.Values.Select(Selector).Append(Task.Delay(1000)).WhenAll();
}
});
_client.LeftGuild += _client_LeftGuild;
bot.JoinedGuild += Bot_JoinedGuild;
}
private Task Bot_JoinedGuild(GuildConfig arg)
{
// includeall no longer loads vcrole
// need to load new guildconfig with vc role included
using (var uow = _db.GetDbContext())
{
var configWithVcRole = uow.GuildConfigsForId(arg.GuildId, set => set.Include(x => x.VcRoleInfos));
_ = InitializeVcRole(configWithVcRole);
}
return Task.CompletedTask;
}
private Task _client_LeftGuild(SocketGuild arg)
{
VcRoles.TryRemove(arg.Id, out _);
ToAssign.TryRemove(arg.Id, out _);
return Task.CompletedTask;
}
private async Task InitializeVcRole(GuildConfig gconf)
{
var g = _client.GetGuild(gconf.GuildId);
if (g is null)
return;
var infos = new ConcurrentDictionary<ulong, IRole>();
var missingRoles = new List<VcRoleInfo>();
VcRoles.AddOrUpdate(gconf.GuildId, infos, delegate { return infos; });
foreach (var ri in gconf.VcRoleInfos)
{
var role = g.GetRole(ri.RoleId);
if (role is null)
{
missingRoles.Add(ri);
continue;
}
infos.TryAdd(ri.VoiceChannelId, role);
}
if (missingRoles.Any())
{
await using var uow = _db.GetDbContext();
uow.RemoveRange(missingRoles);
await uow.SaveChangesAsync();
Log.Warning("Removed {MissingRoleCount} missing roles from {ServiceName}",
missingRoles.Count,
nameof(VcRoleService));
}
}
public void AddVcRole(ulong guildId, IRole role, ulong vcId)
{
if (role is null)
throw new ArgumentNullException(nameof(role));
var guildVcRoles = VcRoles.GetOrAdd(guildId, new ConcurrentDictionary<ulong, IRole>());
guildVcRoles.AddOrUpdate(vcId, role, (_, _) => role);
using var uow = _db.GetDbContext();
var conf = uow.GuildConfigsForId(guildId, set => set.Include(x => x.VcRoleInfos));
var toDelete = conf.VcRoleInfos.FirstOrDefault(x => x.VoiceChannelId == vcId); // remove old one
if (toDelete is not null)
uow.Remove(toDelete);
conf.VcRoleInfos.Add(new()
{
VoiceChannelId = vcId,
RoleId = role.Id
}); // add new one
uow.SaveChanges();
}
public bool RemoveVcRole(ulong guildId, ulong vcId)
{
if (!VcRoles.TryGetValue(guildId, out var guildVcRoles))
return false;
if (!guildVcRoles.TryRemove(vcId, out _))
return false;
using var uow = _db.GetDbContext();
var conf = uow.GuildConfigsForId(guildId, set => set.Include(x => x.VcRoleInfos));
var toRemove = conf.VcRoleInfos.Where(x => x.VoiceChannelId == vcId).ToList();
uow.RemoveRange(toRemove);
uow.SaveChanges();
return true;
}
private Task ClientOnUserVoiceStateUpdated(SocketUser usr, SocketVoiceState oldState, SocketVoiceState newState)
{
if (usr is not SocketGuildUser gusr)
return Task.CompletedTask;
var oldVc = oldState.VoiceChannel;
var newVc = newState.VoiceChannel;
_ = Task.Run(() =>
{
try
{
if (oldVc != newVc)
{
ulong guildId;
guildId = newVc?.Guild.Id ?? oldVc.Guild.Id;
if (VcRoles.TryGetValue(guildId, out var guildVcRoles))
{
//remove old
if (oldVc is not null && guildVcRoles.TryGetValue(oldVc.Id, out var role))
Assign(false, gusr, role);
//add new
if (newVc is not null && guildVcRoles.TryGetValue(newVc.Id, out role))
Assign(true, gusr, role);
}
}
}
catch (Exception ex)
{
Log.Warning(ex, "Error in VcRoleService VoiceStateUpdate");
}
});
return Task.CompletedTask;
}
private void Assign(bool v, SocketGuildUser gusr, IRole role)
{
var queue = ToAssign.GetOrAdd(gusr.Guild.Id, new System.Collections.Concurrent.ConcurrentQueue<(bool, IGuildUser, IRole)>());
queue.Enqueue((v, gusr, role));
}
}