Added Administration module
This commit is contained in:
parent
03ab232251
commit
ff653b5c57
59 changed files with 11797 additions and 0 deletions
499
src/EllieBot/Modules/Administration/Administration.cs
Normal file
499
src/EllieBot/Modules/Administration/Administration.cs
Normal file
|
@ -0,0 +1,499 @@
|
||||||
|
#nullable disable
|
||||||
|
using EllieBot.Common.TypeReaders.Models;
|
||||||
|
using EllieBot.Modules.Administration._common.results;
|
||||||
|
using EllieBot.Modules.Administration.Services;
|
||||||
|
|
||||||
|
namespace EllieBot.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 Response().Confirm(strs.imageonly_enable).SendAsync();
|
||||||
|
else
|
||||||
|
await Response().Pending(strs.imageonly_disable).SendAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
[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 Response().Confirm(strs.linkonly_enable).SendAsync();
|
||||||
|
else
|
||||||
|
await Response().Pending(strs.linkonly_disable).SendAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
[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 = _sender.CreateEmbed()
|
||||||
|
.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 Response().Embed(embed).SendAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
[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 Response().Confirm(strs.delmsg_on).SendAsync();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_service.DeleteMessagesOnCommand.TryRemove(ctx.Guild.Id);
|
||||||
|
await Response().Confirm(strs.delmsg_off).SendAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[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 Response().Confirm(strs.delmsg_channel_off).SendAsync();
|
||||||
|
else if (s == State.Enable)
|
||||||
|
await Response().Confirm(strs.delmsg_channel_on).SendAsync();
|
||||||
|
else
|
||||||
|
await Response().Confirm(strs.delmsg_channel_inherit).SendAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Cmd]
|
||||||
|
[RequireContext(ContextType.Guild)]
|
||||||
|
[UserPerm(GuildPerm.DeafenMembers)]
|
||||||
|
[BotPerm(GuildPerm.DeafenMembers)]
|
||||||
|
public async Task Deafen(params IGuildUser[] users)
|
||||||
|
{
|
||||||
|
await _service.DeafenUsers(true, users);
|
||||||
|
await Response().Confirm(strs.deafen).SendAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Cmd]
|
||||||
|
[RequireContext(ContextType.Guild)]
|
||||||
|
[UserPerm(GuildPerm.DeafenMembers)]
|
||||||
|
[BotPerm(GuildPerm.DeafenMembers)]
|
||||||
|
public async Task UnDeafen(params IGuildUser[] users)
|
||||||
|
{
|
||||||
|
await _service.DeafenUsers(false, users);
|
||||||
|
await Response().Confirm(strs.undeafen).SendAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Cmd]
|
||||||
|
[RequireContext(ContextType.Guild)]
|
||||||
|
[UserPerm(GuildPerm.ManageChannels)]
|
||||||
|
[BotPerm(GuildPerm.ManageChannels)]
|
||||||
|
public async Task DelVoiChanl([Leftover] IVoiceChannel voiceChannel)
|
||||||
|
{
|
||||||
|
await voiceChannel.DeleteAsync();
|
||||||
|
await Response().Confirm(strs.delvoich(Format.Bold(voiceChannel.Name))).SendAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
[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 Response().Confirm(strs.createvoich(Format.Bold(ch.Name))).SendAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Cmd]
|
||||||
|
[RequireContext(ContextType.Guild)]
|
||||||
|
[UserPerm(GuildPerm.ManageChannels)]
|
||||||
|
[BotPerm(GuildPerm.ManageChannels)]
|
||||||
|
public async Task DelTxtChanl([Leftover] ITextChannel toDelete)
|
||||||
|
{
|
||||||
|
await toDelete.DeleteAsync(new RequestOptions()
|
||||||
|
{
|
||||||
|
AuditLogReason = $"Deleted by {ctx.User.Username}"
|
||||||
|
});
|
||||||
|
await Response().Confirm(strs.deltextchan(Format.Bold(toDelete.Name))).SendAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
[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 Response().Confirm(strs.createtextchan(Format.Bold(txtCh.Name))).SendAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
[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 Response().Confirm(strs.set_topic).SendAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
[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 Response().Confirm(strs.set_channel_name).SendAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Cmd]
|
||||||
|
[RequireContext(ContextType.Guild)]
|
||||||
|
[UserPerm(GuildPerm.ManageChannels)]
|
||||||
|
[BotPerm(GuildPerm.ManageChannels)]
|
||||||
|
public async Task AgeRestrictToggle()
|
||||||
|
{
|
||||||
|
var channel = (ITextChannel)ctx.Channel;
|
||||||
|
var isEnabled = channel.IsNsfw;
|
||||||
|
|
||||||
|
await channel.ModifyAsync(c => c.IsNsfw = !isEnabled);
|
||||||
|
|
||||||
|
if (isEnabled)
|
||||||
|
await Response().Confirm(strs.nsfw_set_false).SendAsync();
|
||||||
|
else
|
||||||
|
await Response().Confirm(strs.nsfw_set_true).SendAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
[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 Response().Error(strs.insuf_perms_u).SendAsync();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!botPerms.Has(ChannelPermission.ViewChannel))
|
||||||
|
{
|
||||||
|
await Response().Error(strs.insuf_perms_i).SendAsync();
|
||||||
|
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 Response().Error(strs.insuf_perms_u).SendAsync();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!botPerms.Has(ChannelPermission.ManageMessages))
|
||||||
|
{
|
||||||
|
await Response().Error(strs.insuf_perms_i).SendAsync();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
var msg = await channel.GetMessageAsync(messageId);
|
||||||
|
if (msg is null)
|
||||||
|
{
|
||||||
|
await Response().Error(strs.msg_not_found).SendAsync();
|
||||||
|
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 Response().Error(strs.time_too_long).SendAsync();
|
||||||
|
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 Response().Error(strs.not_found).SendAsync();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await t.DeleteAsync();
|
||||||
|
await ctx.OkAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Cmd]
|
||||||
|
[UserPerm(ChannelPerm.ManageMessages)]
|
||||||
|
public async Task AutoPublish()
|
||||||
|
{
|
||||||
|
if (ctx.Channel.GetChannelType() != ChannelType.News)
|
||||||
|
{
|
||||||
|
await Response().Error(strs.req_announcement_channel).SendAsync();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var newState = await _autoPubService.ToggleAutoPublish(ctx.Guild.Id, ctx.Channel.Id);
|
||||||
|
|
||||||
|
if (newState)
|
||||||
|
{
|
||||||
|
await Response().Confirm(strs.autopublish_enable).SendAsync();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await Response().Confirm(strs.autopublish_disable).SendAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[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 Response().Confirm(strs.bot_nick(Format.Bold(newNick) ?? "-")).SendAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
[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 Response().Error(strs.insuf_perms_i).SendAsync();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await gu.ModifyAsync(u => u.Nickname = newNick);
|
||||||
|
|
||||||
|
await Response()
|
||||||
|
.Confirm(strs.user_nick(Format.Bold(gu.ToString()), Format.Bold(newNick) ?? "-"))
|
||||||
|
.SendAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
[Cmd]
|
||||||
|
[RequireContext(ContextType.Guild)]
|
||||||
|
[UserPerm(GuildPermission.ManageGuild)]
|
||||||
|
[BotPerm(GuildPermission.ManageGuild)]
|
||||||
|
public async Task SetServerBanner([Leftover] string img = null)
|
||||||
|
{
|
||||||
|
// Tier2 or higher is required to set a banner.
|
||||||
|
if (ctx.Guild.PremiumTier is PremiumTier.Tier1 or PremiumTier.None) return;
|
||||||
|
|
||||||
|
var result = await _service.SetServerBannerAsync(ctx.Guild, img);
|
||||||
|
|
||||||
|
switch (result)
|
||||||
|
{
|
||||||
|
case SetServerBannerResult.Success:
|
||||||
|
await Response().Confirm(strs.set_srvr_banner).SendAsync();
|
||||||
|
break;
|
||||||
|
case SetServerBannerResult.InvalidFileType:
|
||||||
|
await Response().Error(strs.srvr_banner_invalid).SendAsync();
|
||||||
|
break;
|
||||||
|
case SetServerBannerResult.Toolarge:
|
||||||
|
await Response().Error(strs.srvr_banner_too_large).SendAsync();
|
||||||
|
break;
|
||||||
|
case SetServerBannerResult.InvalidURL:
|
||||||
|
await Response().Error(strs.srvr_banner_invalid_url).SendAsync();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new ArgumentOutOfRangeException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Cmd]
|
||||||
|
[RequireContext(ContextType.Guild)]
|
||||||
|
[UserPerm(GuildPermission.ManageGuild)]
|
||||||
|
[BotPerm(GuildPermission.ManageGuild)]
|
||||||
|
public async Task SetServerIcon([Leftover] string img = null)
|
||||||
|
{
|
||||||
|
var result = await _service.SetServerIconAsync(ctx.Guild, img);
|
||||||
|
|
||||||
|
switch (result)
|
||||||
|
{
|
||||||
|
case SetServerIconResult.Success:
|
||||||
|
await Response().Confirm(strs.set_srvr_icon).SendAsync();
|
||||||
|
break;
|
||||||
|
case SetServerIconResult.InvalidFileType:
|
||||||
|
await Response().Error(strs.srvr_banner_invalid).SendAsync();
|
||||||
|
break;
|
||||||
|
case SetServerIconResult.InvalidURL:
|
||||||
|
await Response().Error(strs.srvr_banner_invalid_url).SendAsync();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new ArgumentOutOfRangeException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
205
src/EllieBot/Modules/Administration/AdministrationService.cs
Normal file
205
src/EllieBot/Modules/Administration/AdministrationService.cs
Normal file
|
@ -0,0 +1,205 @@
|
||||||
|
#nullable disable
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using EllieBot.Db.Models;
|
||||||
|
using EllieBot.Modules.Administration._common.results;
|
||||||
|
|
||||||
|
namespace EllieBot.Modules.Administration.Services;
|
||||||
|
|
||||||
|
public class AdministrationService : IEService
|
||||||
|
{
|
||||||
|
public ConcurrentHashSet<ulong> DeleteMessagesOnCommand { get; }
|
||||||
|
public ConcurrentDictionary<ulong, bool> DeleteMessagesOnCommandChannels { get; }
|
||||||
|
|
||||||
|
private readonly DbService _db;
|
||||||
|
private readonly IReplacementService _repSvc;
|
||||||
|
private readonly ILogCommandService _logService;
|
||||||
|
private readonly IHttpClientFactory _httpFactory;
|
||||||
|
|
||||||
|
public AdministrationService(
|
||||||
|
IBot bot,
|
||||||
|
CommandHandler cmdHandler,
|
||||||
|
DbService db,
|
||||||
|
IReplacementService repSvc,
|
||||||
|
ILogCommandService logService,
|
||||||
|
IHttpClientFactory factory)
|
||||||
|
{
|
||||||
|
_db = db;
|
||||||
|
_repSvc = repSvc;
|
||||||
|
_logService = logService;
|
||||||
|
_httpFactory = factory;
|
||||||
|
|
||||||
|
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 repCtx = new ReplacementContext(context);
|
||||||
|
|
||||||
|
var text = SmartText.CreateFrom(input);
|
||||||
|
text = await _repSvc.ReplaceAsync(text, repCtx);
|
||||||
|
|
||||||
|
await umsg.EditAsync(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<SetServerBannerResult> SetServerBannerAsync(IGuild guild, string img)
|
||||||
|
{
|
||||||
|
if (!IsValidUri(img)) return SetServerBannerResult.InvalidURL;
|
||||||
|
|
||||||
|
var uri = new Uri(img);
|
||||||
|
|
||||||
|
using var http = _httpFactory.CreateClient();
|
||||||
|
using var sr = await http.GetAsync(uri, HttpCompletionOption.ResponseHeadersRead);
|
||||||
|
|
||||||
|
if (!sr.IsImage()) return SetServerBannerResult.InvalidFileType;
|
||||||
|
|
||||||
|
if (sr.GetContentLength() > 8.Megabytes())
|
||||||
|
{
|
||||||
|
return SetServerBannerResult.Toolarge;
|
||||||
|
}
|
||||||
|
|
||||||
|
await using var imageStream = await sr.Content.ReadAsStreamAsync();
|
||||||
|
|
||||||
|
await guild.ModifyAsync(x => x.Banner = new Image(imageStream));
|
||||||
|
return SetServerBannerResult.Success;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<SetServerIconResult> SetServerIconAsync(IGuild guild, string img)
|
||||||
|
{
|
||||||
|
if (!IsValidUri(img)) return SetServerIconResult.InvalidURL;
|
||||||
|
|
||||||
|
var uri = new Uri(img);
|
||||||
|
|
||||||
|
using var http = _httpFactory.CreateClient();
|
||||||
|
using var sr = await http.GetAsync(uri, HttpCompletionOption.ResponseHeadersRead);
|
||||||
|
|
||||||
|
if (!sr.IsImage()) return SetServerIconResult.InvalidFileType;
|
||||||
|
|
||||||
|
await using var imageStream = await sr.Content.ReadAsStreamAsync();
|
||||||
|
|
||||||
|
await guild.ModifyAsync(x => x.Icon = new Image(imageStream));
|
||||||
|
return SetServerIconResult.Success;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool IsValidUri(string img) => !string.IsNullOrWhiteSpace(img) && Uri.IsWellFormedUriString(img, UriKind.Absolute);
|
||||||
|
}
|
|
@ -0,0 +1,60 @@
|
||||||
|
#nullable disable
|
||||||
|
using EllieBot.Modules.Administration.Services;
|
||||||
|
|
||||||
|
namespace EllieBot.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 Response().Error(strs.hierarchy).SendAsync();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var roles = await _service.ToggleAarAsync(ctx.Guild.Id, role.Id);
|
||||||
|
if (roles.Count == 0)
|
||||||
|
await Response().Confirm(strs.aar_disabled).SendAsync();
|
||||||
|
else if (roles.Contains(role.Id))
|
||||||
|
await AutoAssignRole();
|
||||||
|
else
|
||||||
|
await Response().Confirm(strs.aar_role_removed(Format.Bold(role.ToString()))).SendAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Cmd]
|
||||||
|
[RequireContext(ContextType.Guild)]
|
||||||
|
[UserPerm(GuildPerm.ManageRoles)]
|
||||||
|
[BotPerm(GuildPerm.ManageRoles)]
|
||||||
|
public async Task AutoAssignRole()
|
||||||
|
{
|
||||||
|
if (!_service.TryGetRoles(ctx.Guild.Id, out var roles))
|
||||||
|
{
|
||||||
|
await Response().Confirm(strs.aar_none).SendAsync();
|
||||||
|
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 Response()
|
||||||
|
.Confirm(strs.aar_roles(
|
||||||
|
'\n' + existing.Select(x => Format.Bold(x.ToString())).Join(",\n")))
|
||||||
|
.SendAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,158 @@
|
||||||
|
#nullable disable
|
||||||
|
using EllieBot.Db.Models;
|
||||||
|
using System.Net;
|
||||||
|
using System.Threading.Channels;
|
||||||
|
using LinqToDB;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace EllieBot.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(',');
|
||||||
|
}
|
89
src/EllieBot/Modules/Administration/AutoPublishService.cs
Normal file
89
src/EllieBot/Modules/Administration/AutoPublishService.cs
Normal file
|
@ -0,0 +1,89 @@
|
||||||
|
#nullable disable
|
||||||
|
using LinqToDB;
|
||||||
|
using LinqToDB.EntityFrameworkCore;
|
||||||
|
using EllieBot.Common.ModuleBehaviors;
|
||||||
|
using EllieBot.Db.Models;
|
||||||
|
|
||||||
|
namespace EllieBot.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
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// todo GUILDS
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,74 @@
|
||||||
|
using EllieBot.Modules.Administration.DangerousCommands;
|
||||||
|
|
||||||
|
namespace EllieBot.Modules.Administration;
|
||||||
|
|
||||||
|
public partial class Administration
|
||||||
|
{
|
||||||
|
[Group]
|
||||||
|
public partial class CleanupCommands : CleanupModuleBase
|
||||||
|
{
|
||||||
|
private readonly ICleanupService _svc;
|
||||||
|
private readonly IBotCredsProvider _creds;
|
||||||
|
|
||||||
|
public CleanupCommands(ICleanupService svc, IBotCredsProvider creds)
|
||||||
|
{
|
||||||
|
_svc = svc;
|
||||||
|
_creds = creds;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Cmd]
|
||||||
|
[OwnerOnly]
|
||||||
|
[RequireContext(ContextType.DM)]
|
||||||
|
public async Task CleanupGuildData()
|
||||||
|
{
|
||||||
|
var result = await _svc.DeleteMissingGuildDataAsync();
|
||||||
|
|
||||||
|
if (result is null)
|
||||||
|
{
|
||||||
|
await ctx.ErrorAsync();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await Response()
|
||||||
|
.Confirm($"{result.GuildCount} guilds' data remain in the database.")
|
||||||
|
.SendAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Cmd]
|
||||||
|
[RequireContext(ContextType.Guild)]
|
||||||
|
[UserPerm(GuildPerm.Administrator)]
|
||||||
|
public async Task Keep()
|
||||||
|
{
|
||||||
|
var result = await _svc.KeepGuild(Context.Guild.Id);
|
||||||
|
|
||||||
|
await Response().Text("This guild's bot data will be saved.").SendAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Cmd]
|
||||||
|
[OwnerOnly]
|
||||||
|
public async Task LeaveUnkeptServers(int startShardId, int shardMultiplier = 3000)
|
||||||
|
{
|
||||||
|
var keptGuildCount = await _svc.GetKeptGuildCount();
|
||||||
|
|
||||||
|
var response = await PromptUserConfirmAsync(new EmbedBuilder()
|
||||||
|
.WithDescription($"""
|
||||||
|
Do you want the bot to leave all unkept servers?
|
||||||
|
|
||||||
|
There are currently {keptGuildCount} kept servers.
|
||||||
|
|
||||||
|
**This is a highly destructive and irreversible action.**
|
||||||
|
"""));
|
||||||
|
|
||||||
|
if (!response)
|
||||||
|
return;
|
||||||
|
|
||||||
|
for (var shardId = startShardId; shardId < _creds.GetCreds().TotalShards; shardId++)
|
||||||
|
{
|
||||||
|
await _svc.StartLeavingUnkeptServers(shardId);
|
||||||
|
await Task.Delay(shardMultiplier * 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
await ctx.OkAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,271 @@
|
||||||
|
using LinqToDB;
|
||||||
|
using LinqToDB.Data;
|
||||||
|
using LinqToDB.EntityFrameworkCore;
|
||||||
|
using LinqToDB.Mapping;
|
||||||
|
using EllieBot.Common.ModuleBehaviors;
|
||||||
|
using EllieBot.Db.Models;
|
||||||
|
|
||||||
|
namespace EllieBot.Modules.Administration.DangerousCommands;
|
||||||
|
|
||||||
|
public sealed class CleanupService : ICleanupService, IReadyExecutor, IEService
|
||||||
|
{
|
||||||
|
private TypedKey<KeepReport> _cleanupReportKey = new("cleanup:report");
|
||||||
|
private TypedKey<bool> _cleanupTriggerKey = new("cleanup:trigger");
|
||||||
|
|
||||||
|
private TypedKey<int> _keepTriggerKey = new("keep:trigger");
|
||||||
|
|
||||||
|
private readonly IPubSub _pubSub;
|
||||||
|
private readonly DiscordSocketClient _client;
|
||||||
|
private ConcurrentDictionary<int, ulong[]> guildIds = new();
|
||||||
|
private readonly IBotCredsProvider _creds;
|
||||||
|
private readonly DbService _db;
|
||||||
|
|
||||||
|
public CleanupService(
|
||||||
|
IPubSub pubSub,
|
||||||
|
DiscordSocketClient client,
|
||||||
|
IBotCredsProvider creds,
|
||||||
|
DbService db)
|
||||||
|
{
|
||||||
|
_pubSub = pubSub;
|
||||||
|
_client = client;
|
||||||
|
_creds = creds;
|
||||||
|
_db = db;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task OnReadyAsync()
|
||||||
|
{
|
||||||
|
await _pubSub.Sub(_cleanupTriggerKey, OnCleanupTrigger);
|
||||||
|
await _pubSub.Sub(_keepTriggerKey, InternalTriggerKeep);
|
||||||
|
|
||||||
|
_client.JoinedGuild += ClientOnJoinedGuild;
|
||||||
|
|
||||||
|
if (_client.ShardId == 0)
|
||||||
|
await _pubSub.Sub(_cleanupReportKey, OnKeepReport);
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool keepTriggered = false;
|
||||||
|
|
||||||
|
private async ValueTask InternalTriggerKeep(int shardId)
|
||||||
|
{
|
||||||
|
if (_client.ShardId != shardId)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (keepTriggered)
|
||||||
|
return;
|
||||||
|
|
||||||
|
keepTriggered = true;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var allGuildIds = _client.Guilds.Select(x => x.Id).ToArray();
|
||||||
|
|
||||||
|
HashSet<ulong> dontDelete;
|
||||||
|
await using (var db = _db.GetDbContext())
|
||||||
|
{
|
||||||
|
await using var ctx = db.CreateLinqToDBContext();
|
||||||
|
var table = ctx.CreateTable<KeptGuilds>(tableOptions: TableOptions.CheckExistence);
|
||||||
|
|
||||||
|
var dontDeleteList = await table
|
||||||
|
.Where(x => allGuildIds.Contains(x.GuildId))
|
||||||
|
.Select(x => x.GuildId)
|
||||||
|
.ToListAsyncLinqToDB();
|
||||||
|
|
||||||
|
dontDelete = dontDeleteList.ToHashSet();
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.Information("Leaving {RemainingCount} guilds, 1 every second. {DontDeleteCount} will remain",
|
||||||
|
allGuildIds.Length - dontDelete.Count,
|
||||||
|
dontDelete.Count);
|
||||||
|
|
||||||
|
foreach (var guildId in allGuildIds)
|
||||||
|
{
|
||||||
|
if (dontDelete.Contains(guildId))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
await Task.Delay(1016);
|
||||||
|
|
||||||
|
SocketGuild? guild = null;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
guild = _client.GetGuild(guildId);
|
||||||
|
|
||||||
|
if (guild is null)
|
||||||
|
{
|
||||||
|
Log.Warning("Unable to find guild {GuildId}", guildId);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
await guild.LeaveAsync();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Log.Warning("Unable to leave guild {GuildName} [{GuildId}]: {ErrorMessage}",
|
||||||
|
guild?.Name,
|
||||||
|
guildId,
|
||||||
|
ex.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
keepTriggered = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<KeepResult?> DeleteMissingGuildDataAsync()
|
||||||
|
{
|
||||||
|
guildIds = new();
|
||||||
|
var totalShards = _creds.GetCreds().TotalShards;
|
||||||
|
await _pubSub.Pub(_cleanupTriggerKey, true);
|
||||||
|
var counter = 0;
|
||||||
|
while (guildIds.Keys.Count < totalShards)
|
||||||
|
{
|
||||||
|
await Task.Delay(1000);
|
||||||
|
counter++;
|
||||||
|
|
||||||
|
if (counter >= 5)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (guildIds.Keys.Count < totalShards)
|
||||||
|
return default;
|
||||||
|
|
||||||
|
var allIds = guildIds.SelectMany(x => x.Value)
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
await using var ctx = _db.GetDbContext();
|
||||||
|
await using var linqCtx = ctx.CreateLinqToDBContext();
|
||||||
|
await using var tempTable = linqCtx.CreateTempTable<CleanupId>();
|
||||||
|
|
||||||
|
foreach (var chunk in allIds.Chunk(20000))
|
||||||
|
{
|
||||||
|
await tempTable.BulkCopyAsync(chunk.Select(x => new CleanupId()
|
||||||
|
{
|
||||||
|
GuildId = x
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// delete guild configs
|
||||||
|
await ctx.GetTable<GuildConfig>()
|
||||||
|
.Where(x => !tempTable.Select(x => x.GuildId)
|
||||||
|
.Contains(x.GuildId))
|
||||||
|
.DeleteAsync();
|
||||||
|
|
||||||
|
// delete guild xp
|
||||||
|
await ctx.GetTable<UserXpStats>()
|
||||||
|
.Where(x => !tempTable.Select(x => x.GuildId)
|
||||||
|
.Contains(x.GuildId))
|
||||||
|
.DeleteAsync();
|
||||||
|
|
||||||
|
// delete expressions
|
||||||
|
await ctx.GetTable<EllieExpression>()
|
||||||
|
.Where(x => x.GuildId != null
|
||||||
|
&& !tempTable.Select(x => x.GuildId)
|
||||||
|
.Contains(x.GuildId.Value))
|
||||||
|
.DeleteAsync();
|
||||||
|
|
||||||
|
// delete quotes
|
||||||
|
await ctx.GetTable<Quote>()
|
||||||
|
.Where(x => !tempTable.Select(x => x.GuildId)
|
||||||
|
.Contains(x.GuildId))
|
||||||
|
.DeleteAsync();
|
||||||
|
|
||||||
|
// delete planted currencies
|
||||||
|
await ctx.GetTable<PlantedCurrency>()
|
||||||
|
.Where(x => !tempTable.Select(x => x.GuildId)
|
||||||
|
.Contains(x.GuildId))
|
||||||
|
.DeleteAsync();
|
||||||
|
|
||||||
|
// delete image only channels
|
||||||
|
await ctx.GetTable<ImageOnlyChannel>()
|
||||||
|
.Where(x => !tempTable.Select(x => x.GuildId)
|
||||||
|
.Contains(x.GuildId))
|
||||||
|
.DeleteAsync();
|
||||||
|
|
||||||
|
// delete reaction roles
|
||||||
|
await ctx.GetTable<ReactionRoleV2>()
|
||||||
|
.Where(x => !tempTable.Select(x => x.GuildId)
|
||||||
|
.Contains(x.GuildId))
|
||||||
|
.DeleteAsync();
|
||||||
|
|
||||||
|
// delete ignored users
|
||||||
|
await ctx.GetTable<DiscordPermOverride>()
|
||||||
|
.Where(x => x.GuildId != null
|
||||||
|
&& !tempTable.Select(x => x.GuildId)
|
||||||
|
.Contains(x.GuildId.Value))
|
||||||
|
.DeleteAsync();
|
||||||
|
|
||||||
|
// delete perm overrides
|
||||||
|
await ctx.GetTable<DiscordPermOverride>()
|
||||||
|
.Where(x => x.GuildId != null
|
||||||
|
&& !tempTable.Select(x => x.GuildId)
|
||||||
|
.Contains(x.GuildId.Value))
|
||||||
|
.DeleteAsync();
|
||||||
|
|
||||||
|
// delete repeaters
|
||||||
|
await ctx.GetTable<Repeater>()
|
||||||
|
.Where(x => !tempTable.Select(x => x.GuildId)
|
||||||
|
.Contains(x.GuildId))
|
||||||
|
.DeleteAsync();
|
||||||
|
|
||||||
|
return new()
|
||||||
|
{
|
||||||
|
GuildCount = guildIds.Keys.Count,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> KeepGuild(ulong guildId)
|
||||||
|
{
|
||||||
|
await using var db = _db.GetDbContext();
|
||||||
|
await using var ctx = db.CreateLinqToDBContext();
|
||||||
|
var table = ctx.CreateTable<KeptGuilds>(tableOptions: TableOptions.CheckExistence);
|
||||||
|
if (await table.AnyAsyncLinqToDB(x => x.GuildId == guildId))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
await table.InsertAsync(() => new()
|
||||||
|
{
|
||||||
|
GuildId = guildId
|
||||||
|
});
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<int> GetKeptGuildCount()
|
||||||
|
{
|
||||||
|
await using var db = _db.GetDbContext();
|
||||||
|
await using var ctx = db.CreateLinqToDBContext();
|
||||||
|
var table = ctx.CreateTable<KeptGuilds>(tableOptions: TableOptions.CheckExistence);
|
||||||
|
return await table.CountAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task StartLeavingUnkeptServers(int shardId)
|
||||||
|
=> await _pubSub.Pub(_keepTriggerKey, shardId);
|
||||||
|
|
||||||
|
private ValueTask OnKeepReport(KeepReport report)
|
||||||
|
{
|
||||||
|
guildIds[report.ShardId] = report.GuildIds;
|
||||||
|
return default;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ClientOnJoinedGuild(SocketGuild arg)
|
||||||
|
{
|
||||||
|
await KeepGuild(arg.Id);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ValueTask OnCleanupTrigger(bool arg)
|
||||||
|
{
|
||||||
|
_pubSub.Pub(_cleanupReportKey,
|
||||||
|
new KeepReport()
|
||||||
|
{
|
||||||
|
ShardId = _client.ShardId,
|
||||||
|
GuildIds = _client.GetGuildIds(),
|
||||||
|
});
|
||||||
|
|
||||||
|
return default;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class KeptGuilds
|
||||||
|
{
|
||||||
|
[PrimaryKey]
|
||||||
|
public ulong GuildId { get; set; }
|
||||||
|
}
|
|
@ -0,0 +1,164 @@
|
||||||
|
#nullable disable
|
||||||
|
using System.Globalization;
|
||||||
|
using CsvHelper;
|
||||||
|
using CsvHelper.Configuration;
|
||||||
|
using EllieBot.Modules.Gambling;
|
||||||
|
using EllieBot.Modules.Administration.Services;
|
||||||
|
using EllieBot.Modules.Xp;
|
||||||
|
|
||||||
|
namespace EllieBot.Modules.Administration;
|
||||||
|
|
||||||
|
public partial class Administration
|
||||||
|
{
|
||||||
|
[Group]
|
||||||
|
[OwnerOnly]
|
||||||
|
[NoPublicBot]
|
||||||
|
public partial class DangerousCommands : CleanupModuleBase
|
||||||
|
{
|
||||||
|
private readonly DangerousCommandsService _ds;
|
||||||
|
private readonly IGamblingCleanupService _gcs;
|
||||||
|
private readonly IXpCleanupService _xcs;
|
||||||
|
|
||||||
|
public DangerousCommands(
|
||||||
|
DangerousCommandsService ds,
|
||||||
|
IGamblingCleanupService gcs,
|
||||||
|
IXpCleanupService xcs)
|
||||||
|
{
|
||||||
|
_ds = ds;
|
||||||
|
_gcs = gcs;
|
||||||
|
_xcs = xcs;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Cmd]
|
||||||
|
[OwnerOnly]
|
||||||
|
public Task SqlSelect([Leftover] string sql)
|
||||||
|
{
|
||||||
|
var result = _ds.SelectSql(sql);
|
||||||
|
|
||||||
|
return Response()
|
||||||
|
.Paginated()
|
||||||
|
.Items(result.Results)
|
||||||
|
.PageSize(20)
|
||||||
|
.Page((items, _) =>
|
||||||
|
{
|
||||||
|
if (!items.Any())
|
||||||
|
return _sender.CreateEmbed().WithErrorColor().WithFooter(sql).WithDescription("-");
|
||||||
|
|
||||||
|
return _sender.CreateEmbed()
|
||||||
|
.WithOkColor()
|
||||||
|
.WithFooter(sql)
|
||||||
|
.WithTitle(string.Join(" ║ ", result.ColumnNames))
|
||||||
|
.WithDescription(string.Join('\n', items.Select(x => string.Join(" ║ ", x))));
|
||||||
|
})
|
||||||
|
.SendAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Cmd]
|
||||||
|
[OwnerOnly]
|
||||||
|
public async Task SqlSelectCsv([Leftover] string sql)
|
||||||
|
{
|
||||||
|
var result = _ds.SelectSql(sql);
|
||||||
|
|
||||||
|
// create a file stream and write the data as csv
|
||||||
|
using var ms = new MemoryStream();
|
||||||
|
await using var sw = new StreamWriter(ms);
|
||||||
|
await using var csv = new CsvWriter(sw,
|
||||||
|
new CsvConfiguration(CultureInfo.InvariantCulture)
|
||||||
|
{
|
||||||
|
Delimiter = ","
|
||||||
|
});
|
||||||
|
|
||||||
|
foreach (var cn in result.ColumnNames)
|
||||||
|
{
|
||||||
|
csv.WriteField(cn);
|
||||||
|
}
|
||||||
|
|
||||||
|
await csv.NextRecordAsync();
|
||||||
|
|
||||||
|
foreach (var row in result.Results)
|
||||||
|
{
|
||||||
|
foreach (var field in row)
|
||||||
|
{
|
||||||
|
csv.WriteField(field);
|
||||||
|
}
|
||||||
|
|
||||||
|
await csv.NextRecordAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
await csv.FlushAsync();
|
||||||
|
ms.Position = 0;
|
||||||
|
|
||||||
|
// send the file
|
||||||
|
await ctx.Channel.SendFileAsync(ms, $"query_result_{DateTime.UtcNow.Ticks}.csv");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Cmd]
|
||||||
|
[OwnerOnly]
|
||||||
|
public async Task SqlExec([Leftover] string sql)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var embed = _sender.CreateEmbed()
|
||||||
|
.WithTitle(GetText(strs.sql_confirm_exec))
|
||||||
|
.WithDescription(Format.Code(sql));
|
||||||
|
|
||||||
|
if (!await PromptUserConfirmAsync(embed))
|
||||||
|
return;
|
||||||
|
|
||||||
|
var res = await _ds.ExecuteSql(sql);
|
||||||
|
await Response().Confirm(res.ToString()).SendAsync();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
await Response().Error(ex.ToString()).SendAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Cmd]
|
||||||
|
[OwnerOnly]
|
||||||
|
public async Task PurgeUser(ulong userId)
|
||||||
|
{
|
||||||
|
var embed = _sender.CreateEmbed()
|
||||||
|
.WithDescription(GetText(strs.purge_user_confirm(Format.Bold(userId.ToString()))));
|
||||||
|
|
||||||
|
if (!await PromptUserConfirmAsync(embed))
|
||||||
|
return;
|
||||||
|
|
||||||
|
await _ds.PurgeUserAsync(userId);
|
||||||
|
await ctx.OkAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Cmd]
|
||||||
|
[OwnerOnly]
|
||||||
|
public Task PurgeUser([Leftover] IUser user)
|
||||||
|
=> PurgeUser(user.Id);
|
||||||
|
|
||||||
|
[Cmd]
|
||||||
|
[OwnerOnly]
|
||||||
|
public Task DeleteXp()
|
||||||
|
=> ConfirmActionInternalAsync("Delete Xp", () => _xcs.DeleteXp());
|
||||||
|
|
||||||
|
|
||||||
|
[Cmd]
|
||||||
|
[OwnerOnly]
|
||||||
|
public Task DeleteWaifus()
|
||||||
|
=> ConfirmActionInternalAsync("Delete Waifus", () => _gcs.DeleteWaifus());
|
||||||
|
|
||||||
|
[Cmd]
|
||||||
|
[OwnerOnly]
|
||||||
|
public async Task DeleteWaifu(IUser user)
|
||||||
|
=> await DeleteWaifu(user.Id);
|
||||||
|
|
||||||
|
[Cmd]
|
||||||
|
[OwnerOnly]
|
||||||
|
public Task DeleteWaifu(ulong userId)
|
||||||
|
=> ConfirmActionInternalAsync($"Delete Waifu {userId}", () => _gcs.DeleteWaifu(userId));
|
||||||
|
|
||||||
|
|
||||||
|
[Cmd]
|
||||||
|
[OwnerOnly]
|
||||||
|
public Task DeleteCurrency()
|
||||||
|
=> ConfirmActionInternalAsync("Delete Currency", () => _gcs.DeleteCurrency());
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,103 @@
|
||||||
|
#nullable disable
|
||||||
|
using LinqToDB;
|
||||||
|
using LinqToDB.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using EllieBot.Db.Models;
|
||||||
|
|
||||||
|
namespace EllieBot.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; }
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace EllieBot.Modules.Administration.DangerousCommands;
|
||||||
|
|
||||||
|
public sealed class CleanupId
|
||||||
|
{
|
||||||
|
[Key]
|
||||||
|
public ulong GuildId { get; set; }
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
namespace EllieBot.Modules.Administration.DangerousCommands;
|
||||||
|
|
||||||
|
public interface ICleanupService
|
||||||
|
{
|
||||||
|
Task<KeepResult?> DeleteMissingGuildDataAsync();
|
||||||
|
Task<bool> KeepGuild(ulong guildId);
|
||||||
|
Task<int> GetKeptGuildCount();
|
||||||
|
Task StartLeavingUnkeptServers(int shardId);
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
namespace EllieBot.Modules.Administration.DangerousCommands;
|
||||||
|
|
||||||
|
public sealed class KeepReport
|
||||||
|
{
|
||||||
|
public required int ShardId { get; init; }
|
||||||
|
public required ulong[] GuildIds { get; init; }
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
namespace EllieBot.Modules.Administration.DangerousCommands;
|
||||||
|
|
||||||
|
public sealed class KeepResult
|
||||||
|
{
|
||||||
|
public required int GuildCount { get; init; }
|
||||||
|
}
|
|
@ -0,0 +1,36 @@
|
||||||
|
#nullable disable
|
||||||
|
using EllieBot.Modules.Administration.Services;
|
||||||
|
|
||||||
|
namespace EllieBot.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 Response().Error(strs.not_in_voice).SendAsync();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var id = _service.ToggleGameVoiceChannel(ctx.Guild.Id, vch.Id);
|
||||||
|
|
||||||
|
if (id is null)
|
||||||
|
await Response().Confirm(strs.gvc_disabled).SendAsync();
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_service.GameVoiceChannels.Add(vch.Id);
|
||||||
|
await Response().Confirm(strs.gvc_enabled(Format.Bold(vch.Name))).SendAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,125 @@
|
||||||
|
#nullable disable
|
||||||
|
namespace EllieBot.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;
|
||||||
|
}
|
||||||
|
}
|
226
src/EllieBot/Modules/Administration/GreetBye/GreetCommands.cs
Normal file
226
src/EllieBot/Modules/Administration/GreetBye/GreetCommands.cs
Normal file
|
@ -0,0 +1,226 @@
|
||||||
|
namespace EllieBot.Modules.Administration;
|
||||||
|
|
||||||
|
public partial class Administration
|
||||||
|
{
|
||||||
|
[Group]
|
||||||
|
public partial class GreetCommands : EllieModule<GreetService>
|
||||||
|
{
|
||||||
|
[Cmd]
|
||||||
|
[RequireContext(ContextType.Guild)]
|
||||||
|
[UserPerm(GuildPerm.ManageGuild)]
|
||||||
|
public Task Boost()
|
||||||
|
=> Toggle(GreetType.Boost);
|
||||||
|
|
||||||
|
[Cmd]
|
||||||
|
[RequireContext(ContextType.Guild)]
|
||||||
|
[UserPerm(GuildPerm.ManageGuild)]
|
||||||
|
public Task BoostDel(int timer = 30)
|
||||||
|
=> SetDel(GreetType.Boost, timer);
|
||||||
|
|
||||||
|
[Cmd]
|
||||||
|
[RequireContext(ContextType.Guild)]
|
||||||
|
[UserPerm(GuildPerm.ManageGuild)]
|
||||||
|
public Task BoostMsg([Leftover] string? text = null)
|
||||||
|
=> SetMsg(GreetType.Boost, text);
|
||||||
|
|
||||||
|
[Cmd]
|
||||||
|
[RequireContext(ContextType.Guild)]
|
||||||
|
[UserPerm(GuildPerm.ManageGuild)]
|
||||||
|
public Task Greet()
|
||||||
|
=> Toggle(GreetType.Greet);
|
||||||
|
|
||||||
|
[Cmd]
|
||||||
|
[RequireContext(ContextType.Guild)]
|
||||||
|
[UserPerm(GuildPerm.ManageGuild)]
|
||||||
|
public Task GreetDel(int timer = 30)
|
||||||
|
=> SetDel(GreetType.Greet, timer);
|
||||||
|
|
||||||
|
[Cmd]
|
||||||
|
[RequireContext(ContextType.Guild)]
|
||||||
|
[UserPerm(GuildPerm.ManageGuild)]
|
||||||
|
public Task GreetMsg([Leftover] string? text = null)
|
||||||
|
=> SetMsg(GreetType.Greet, text);
|
||||||
|
|
||||||
|
[Cmd]
|
||||||
|
[RequireContext(ContextType.Guild)]
|
||||||
|
[UserPerm(GuildPerm.ManageGuild)]
|
||||||
|
public Task GreetDm()
|
||||||
|
=> Toggle(GreetType.GreetDm);
|
||||||
|
|
||||||
|
[Cmd]
|
||||||
|
[RequireContext(ContextType.Guild)]
|
||||||
|
[UserPerm(GuildPerm.ManageGuild)]
|
||||||
|
public Task GreetDmMsg([Leftover] string? text = null)
|
||||||
|
=> SetMsg(GreetType.GreetDm, text);
|
||||||
|
|
||||||
|
[Cmd]
|
||||||
|
[RequireContext(ContextType.Guild)]
|
||||||
|
[UserPerm(GuildPerm.ManageGuild)]
|
||||||
|
public Task Bye()
|
||||||
|
=> Toggle(GreetType.Bye);
|
||||||
|
|
||||||
|
[Cmd]
|
||||||
|
[RequireContext(ContextType.Guild)]
|
||||||
|
[UserPerm(GuildPerm.ManageGuild)]
|
||||||
|
public Task ByeDel(int timer = 30)
|
||||||
|
=> SetDel(GreetType.Bye, timer);
|
||||||
|
|
||||||
|
[Cmd]
|
||||||
|
[RequireContext(ContextType.Guild)]
|
||||||
|
[UserPerm(GuildPerm.ManageGuild)]
|
||||||
|
public Task ByeMsg([Leftover] string? text = null)
|
||||||
|
=> SetMsg(GreetType.Bye, text);
|
||||||
|
|
||||||
|
[Cmd]
|
||||||
|
[RequireContext(ContextType.Guild)]
|
||||||
|
[UserPerm(GuildPerm.ManageGuild)]
|
||||||
|
public Task GreetTest([Leftover] IGuildUser? user = null)
|
||||||
|
=> Test(GreetType.Greet, user);
|
||||||
|
|
||||||
|
[Cmd]
|
||||||
|
[RequireContext(ContextType.Guild)]
|
||||||
|
[UserPerm(GuildPerm.ManageGuild)]
|
||||||
|
public Task GreetDmTest([Leftover] IGuildUser? user = null)
|
||||||
|
=> Test(GreetType.GreetDm, user);
|
||||||
|
|
||||||
|
[Cmd]
|
||||||
|
[RequireContext(ContextType.Guild)]
|
||||||
|
[UserPerm(GuildPerm.ManageGuild)]
|
||||||
|
[Ratelimit(5)]
|
||||||
|
public Task ByeTest([Leftover] IGuildUser? user = null)
|
||||||
|
=> Test(GreetType.Bye, user);
|
||||||
|
|
||||||
|
[Cmd]
|
||||||
|
[RequireContext(ContextType.Guild)]
|
||||||
|
[UserPerm(GuildPerm.ManageGuild)]
|
||||||
|
[Ratelimit(5)]
|
||||||
|
public Task BoostTest([Leftover] IGuildUser? user = null)
|
||||||
|
=> Test(GreetType.Boost, user);
|
||||||
|
|
||||||
|
|
||||||
|
public async Task Toggle(GreetType type)
|
||||||
|
{
|
||||||
|
var enabled = await _service.SetGreet(ctx.Guild.Id, ctx.Channel.Id, type);
|
||||||
|
|
||||||
|
if (enabled)
|
||||||
|
await Response()
|
||||||
|
.Confirm(
|
||||||
|
type switch
|
||||||
|
{
|
||||||
|
GreetType.Boost => strs.boost_on,
|
||||||
|
GreetType.Greet => strs.greet_on,
|
||||||
|
GreetType.Bye => strs.bye_on,
|
||||||
|
GreetType.GreetDm => strs.greetdm_on,
|
||||||
|
_ => strs.error
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.SendAsync();
|
||||||
|
else
|
||||||
|
await Response()
|
||||||
|
.Pending(
|
||||||
|
type switch
|
||||||
|
{
|
||||||
|
GreetType.Boost => strs.boost_off,
|
||||||
|
GreetType.Greet => strs.greet_off,
|
||||||
|
GreetType.Bye => strs.bye_off,
|
||||||
|
GreetType.GreetDm => strs.greetdm_off,
|
||||||
|
_ => strs.error
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.SendAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public async Task SetDel(GreetType type, int timer)
|
||||||
|
{
|
||||||
|
if (timer is < 0 or > 600)
|
||||||
|
return;
|
||||||
|
|
||||||
|
await _service.SetDeleteTimer(ctx.Guild.Id, type, timer);
|
||||||
|
|
||||||
|
if (timer > 0)
|
||||||
|
await Response()
|
||||||
|
.Confirm(
|
||||||
|
type switch
|
||||||
|
{
|
||||||
|
GreetType.Boost => strs.boostdel_on(timer),
|
||||||
|
GreetType.Greet => strs.greetdel_on(timer),
|
||||||
|
GreetType.Bye => strs.byedel_on(timer),
|
||||||
|
_ => strs.error
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.SendAsync();
|
||||||
|
else
|
||||||
|
await Response()
|
||||||
|
.Pending(
|
||||||
|
type switch
|
||||||
|
{
|
||||||
|
GreetType.Boost => strs.boostdel_off,
|
||||||
|
GreetType.Greet => strs.greetdel_off,
|
||||||
|
GreetType.Bye => strs.byedel_off,
|
||||||
|
_ => strs.error
|
||||||
|
})
|
||||||
|
.SendAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public async Task SetMsg(GreetType type, string? text = null)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(text))
|
||||||
|
{
|
||||||
|
var conf = await _service.GetGreetSettingsAsync(ctx.Guild.Id, type);
|
||||||
|
var msg = conf?.MessageText ?? GreetService.GetDefaultGreet(type);
|
||||||
|
await Response()
|
||||||
|
.Confirm(
|
||||||
|
type switch
|
||||||
|
{
|
||||||
|
GreetType.Boost => strs.boostmsg_cur(msg),
|
||||||
|
GreetType.Greet => strs.greetmsg_cur(msg),
|
||||||
|
GreetType.Bye => strs.byemsg_cur(msg),
|
||||||
|
GreetType.GreetDm => strs.greetdmmsg_cur(msg),
|
||||||
|
_ => strs.error
|
||||||
|
})
|
||||||
|
.SendAsync();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var isEnabled = await _service.SetMessage(ctx.Guild.Id, type, text);
|
||||||
|
|
||||||
|
await Response()
|
||||||
|
.Confirm(type switch
|
||||||
|
{
|
||||||
|
GreetType.Boost => strs.boostmsg_new,
|
||||||
|
GreetType.Greet => strs.greetmsg_new,
|
||||||
|
GreetType.Bye => strs.byemsg_new,
|
||||||
|
GreetType.GreetDm => strs.greetdmmsg_new,
|
||||||
|
_ => strs.error
|
||||||
|
})
|
||||||
|
.SendAsync();
|
||||||
|
|
||||||
|
|
||||||
|
if (!isEnabled)
|
||||||
|
{
|
||||||
|
var cmdName = type switch
|
||||||
|
{
|
||||||
|
GreetType.Greet => "greet",
|
||||||
|
GreetType.Bye => "bye",
|
||||||
|
GreetType.Boost => "boost",
|
||||||
|
GreetType.GreetDm => "greetdm",
|
||||||
|
_ => "unknown_command"
|
||||||
|
};
|
||||||
|
|
||||||
|
await Response().Pending(strs.boostmsg_enable($"`{prefix}{cmdName}`")).SendAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task Test(GreetType type, IGuildUser? user = null)
|
||||||
|
{
|
||||||
|
user ??= (IGuildUser)ctx.User;
|
||||||
|
|
||||||
|
await _service.Test(ctx.Guild.Id, type, (ITextChannel)ctx.Channel, user);
|
||||||
|
var conf = await _service.GetGreetSettingsAsync(ctx.Guild.Id, type);
|
||||||
|
if (conf?.IsEnabled is not true)
|
||||||
|
await Response().Pending(strs.boostmsg_enable($"`{prefix}boost`")).SendAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
480
src/EllieBot/Modules/Administration/GreetBye/GreetService.cs
Normal file
480
src/EllieBot/Modules/Administration/GreetBye/GreetService.cs
Normal file
|
@ -0,0 +1,480 @@
|
||||||
|
using LinqToDB;
|
||||||
|
using LinqToDB.EntityFrameworkCore;
|
||||||
|
using LinqToDB.Tools;
|
||||||
|
using EllieBot.Common.ModuleBehaviors;
|
||||||
|
using System.Threading.Channels;
|
||||||
|
|
||||||
|
namespace EllieBot.Services;
|
||||||
|
|
||||||
|
public class GreetService : IEService, IReadyExecutor
|
||||||
|
{
|
||||||
|
private readonly DbService _db;
|
||||||
|
|
||||||
|
private ConcurrentDictionary<GreetType, ConcurrentHashSet<ulong>> _enabled = new();
|
||||||
|
|
||||||
|
private readonly DiscordSocketClient _client;
|
||||||
|
|
||||||
|
private readonly IReplacementService _repSvc;
|
||||||
|
private readonly IBotCache _cache;
|
||||||
|
private readonly IMessageSenderService _sender;
|
||||||
|
|
||||||
|
private readonly Channel<(GreetSettings, IUser, ITextChannel?)> _greetQueue =
|
||||||
|
Channel.CreateBounded<(GreetSettings, IUser, ITextChannel?)>(
|
||||||
|
new BoundedChannelOptions(60)
|
||||||
|
{
|
||||||
|
FullMode = BoundedChannelFullMode.DropOldest
|
||||||
|
});
|
||||||
|
|
||||||
|
public GreetService(
|
||||||
|
DiscordSocketClient client,
|
||||||
|
DbService db,
|
||||||
|
IMessageSenderService sender,
|
||||||
|
IReplacementService repSvc,
|
||||||
|
IBotCache cache
|
||||||
|
)
|
||||||
|
{
|
||||||
|
_db = db;
|
||||||
|
_client = client;
|
||||||
|
_repSvc = repSvc;
|
||||||
|
_cache = cache;
|
||||||
|
_sender = sender;
|
||||||
|
|
||||||
|
|
||||||
|
foreach (var type in Enum.GetValues<GreetType>())
|
||||||
|
{
|
||||||
|
_enabled[type] = new();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task OnReadyAsync()
|
||||||
|
{
|
||||||
|
// cache all enabled guilds
|
||||||
|
await using (var uow = _db.GetDbContext())
|
||||||
|
{
|
||||||
|
var guilds = _client.Guilds.Select(x => x.Id).ToList();
|
||||||
|
var enabled = await uow.GetTable<GreetSettings>()
|
||||||
|
.Where(x => x.GuildId.In(guilds))
|
||||||
|
.Where(x => x.IsEnabled)
|
||||||
|
.Select(x => new
|
||||||
|
{
|
||||||
|
x.GuildId,
|
||||||
|
x.GreetType
|
||||||
|
})
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
foreach (var e in enabled)
|
||||||
|
{
|
||||||
|
_enabled[e.GreetType].Add(e.GuildId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_client.UserJoined += OnUserJoined;
|
||||||
|
_client.UserLeft += OnUserLeft;
|
||||||
|
|
||||||
|
_client.LeftGuild += OnClientLeftGuild;
|
||||||
|
|
||||||
|
_client.GuildMemberUpdated += ClientOnGuildMemberUpdated;
|
||||||
|
|
||||||
|
var timer = new PeriodicTimer(TimeSpan.FromSeconds(2));
|
||||||
|
while (await timer.WaitForNextTickAsync())
|
||||||
|
{
|
||||||
|
var (conf, user, ch) = await _greetQueue.Reader.ReadAsync();
|
||||||
|
await GreetUsers(conf, ch, user);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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))
|
||||||
|
{
|
||||||
|
_ = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
var conf = await GetGreetSettingsAsync(newUser.Guild.Id, GreetType.Boost);
|
||||||
|
|
||||||
|
if (conf is null || !conf.IsEnabled)
|
||||||
|
return;
|
||||||
|
|
||||||
|
ITextChannel? channel = null;
|
||||||
|
if (conf.ChannelId is { } cid)
|
||||||
|
channel = newUser.Guild.GetTextChannel(cid);
|
||||||
|
|
||||||
|
if (channel is null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
await GreetUsers(conf, channel, newUser);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task OnClientLeftGuild(SocketGuild guild)
|
||||||
|
{
|
||||||
|
foreach (var gt in Enum.GetValues<GreetType>())
|
||||||
|
{
|
||||||
|
_enabled[gt].TryRemove(guild.Id);
|
||||||
|
}
|
||||||
|
|
||||||
|
await using var uow = _db.GetDbContext();
|
||||||
|
await uow.GetTable<GreetSettings>()
|
||||||
|
.Where(x => x.GuildId == guild.Id)
|
||||||
|
.DeleteAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Task OnUserLeft(SocketGuild guild, SocketUser user)
|
||||||
|
{
|
||||||
|
_ = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var conf = await GetGreetSettingsAsync(guild.Id, GreetType.Bye);
|
||||||
|
|
||||||
|
if (conf is null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var channel = guild.TextChannels.FirstOrDefault(c => c.Id == conf.ChannelId);
|
||||||
|
|
||||||
|
if (channel is null) //maybe warn the server owner that the channel is missing
|
||||||
|
{
|
||||||
|
await SetGreet(guild.Id, null, GreetType.Bye, false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await _greetQueue.Writer.WriteAsync((conf, user, channel));
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// ignored
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly TypedKey<GreetSettings?> _greetSettingsKey = new("greet_settings");
|
||||||
|
|
||||||
|
public async Task<GreetSettings?> GetGreetSettingsAsync(ulong gid, GreetType type)
|
||||||
|
=> await _cache.GetOrAddAsync<GreetSettings?>(_greetSettingsKey,
|
||||||
|
() => InternalGetGreetSettingsAsync(gid, type),
|
||||||
|
TimeSpan.FromSeconds(3));
|
||||||
|
|
||||||
|
private async Task<GreetSettings?> InternalGetGreetSettingsAsync(ulong gid, GreetType type)
|
||||||
|
{
|
||||||
|
await using var uow = _db.GetDbContext();
|
||||||
|
var res = await uow.GetTable<GreetSettings>()
|
||||||
|
.Where(x => x.GuildId == gid && x.GreetType == type)
|
||||||
|
.FirstOrDefaultAsync();
|
||||||
|
|
||||||
|
if (res is not null)
|
||||||
|
res.MessageText ??= GetDefaultGreet(type);
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task GreetUsers(GreetSettings conf, ITextChannel? channel, IUser user)
|
||||||
|
{
|
||||||
|
if (conf.GreetType == GreetType.GreetDm)
|
||||||
|
{
|
||||||
|
if (user is not IGuildUser gu)
|
||||||
|
return;
|
||||||
|
|
||||||
|
await GreetDmUserInternal(conf, gu);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (channel is null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var repCtx = new ReplacementContext(client: _client,
|
||||||
|
guild: channel.Guild,
|
||||||
|
channel: channel,
|
||||||
|
user: user);
|
||||||
|
|
||||||
|
var text = SmartText.CreateFrom(conf.MessageText);
|
||||||
|
text = await _repSvc.ReplaceAsync(text, repCtx);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var toDelete = await _sender.Response(channel).Text(text).Sanitize(false).SendAsync();
|
||||||
|
if (conf.AutoDeleteTimer > 0)
|
||||||
|
toDelete.DeleteAfter(conf.AutoDeleteTimer);
|
||||||
|
}
|
||||||
|
catch (HttpException ex) when (ex.DiscordCode is DiscordErrorCode.InsufficientPermissions
|
||||||
|
or DiscordErrorCode.MissingPermissions
|
||||||
|
or 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, GreetType.Greet, false);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Log.Warning(ex, "Error embeding greet message");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private async Task<bool> GreetDmUser(GreetSettings conf, IGuildUser user)
|
||||||
|
{
|
||||||
|
var completionSource = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||||
|
await _greetQueue.Writer.WriteAsync((conf, user, null));
|
||||||
|
return await completionSource.Task;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<bool> GreetDmUserInternal(GreetSettings conf, IGuildUser user)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var repCtx = new ReplacementContext(client: _client, guild: user.Guild, user: user);
|
||||||
|
var smartText = SmartText.CreateFrom(conf.MessageText);
|
||||||
|
smartText = await _repSvc.ReplaceAsync(smartText, repCtx);
|
||||||
|
|
||||||
|
if (smartText is SmartPlainText pt)
|
||||||
|
{
|
||||||
|
smartText = new SmartEmbedText()
|
||||||
|
{
|
||||||
|
Description = pt.Text
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (smartText is SmartEmbedText set)
|
||||||
|
{
|
||||||
|
smartText = set with
|
||||||
|
{
|
||||||
|
Footer = CreateFooterSource(user)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
else if (smartText 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
|
||||||
|
smartText = seta with
|
||||||
|
{
|
||||||
|
Embeds =
|
||||||
|
[
|
||||||
|
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 _sender.Response(user).Text(smartText).Sanitize(false).SendAsync();
|
||||||
|
}
|
||||||
|
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 = await GetGreetSettingsAsync(user.GuildId, GreetType.Greet);
|
||||||
|
|
||||||
|
if (conf is not null && conf.IsEnabled && conf.ChannelId is { } channelId)
|
||||||
|
{
|
||||||
|
var channel = await user.Guild.GetTextChannelAsync(channelId);
|
||||||
|
if (channel is not null)
|
||||||
|
{
|
||||||
|
await _greetQueue.Writer.WriteAsync((conf, user, channel));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var confDm = await GetGreetSettingsAsync(user.GuildId, GreetType.GreetDm);
|
||||||
|
|
||||||
|
if (confDm?.IsEnabled ?? false)
|
||||||
|
await GreetDmUser(confDm, user);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// ignored
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static string GetDefaultGreet(GreetType greetType)
|
||||||
|
=> greetType switch
|
||||||
|
{
|
||||||
|
GreetType.Boost => "%user.mention% has boosted the server!",
|
||||||
|
GreetType.Greet => "%user.mention% has joined the server!",
|
||||||
|
GreetType.Bye => "%user.name% has left the server!",
|
||||||
|
GreetType.GreetDm => "Welcome to the server %user.name%",
|
||||||
|
_ => "%user.name% did something new!"
|
||||||
|
};
|
||||||
|
|
||||||
|
public async Task<bool> SetGreet(
|
||||||
|
ulong guildId,
|
||||||
|
ulong? channelId,
|
||||||
|
GreetType greetType,
|
||||||
|
bool? value = null)
|
||||||
|
{
|
||||||
|
await using var uow = _db.GetDbContext();
|
||||||
|
var q = uow.GetTable<GreetSettings>();
|
||||||
|
|
||||||
|
if(value is null)
|
||||||
|
value = !_enabled[greetType].Contains(guildId);
|
||||||
|
|
||||||
|
if (value is { } v)
|
||||||
|
{
|
||||||
|
await q
|
||||||
|
.InsertOrUpdateAsync(() => new()
|
||||||
|
{
|
||||||
|
GuildId = guildId,
|
||||||
|
GreetType = greetType,
|
||||||
|
IsEnabled = v,
|
||||||
|
ChannelId = channelId,
|
||||||
|
},
|
||||||
|
(old) => new()
|
||||||
|
{
|
||||||
|
IsEnabled = v,
|
||||||
|
ChannelId = channelId,
|
||||||
|
},
|
||||||
|
() => new()
|
||||||
|
{
|
||||||
|
GuildId = guildId,
|
||||||
|
GreetType = greetType,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value is true)
|
||||||
|
{
|
||||||
|
_enabled[greetType].Add(guildId);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
_enabled[greetType].TryRemove(guildId);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public async Task<bool> SetMessage(ulong guildId, GreetType greetType, string? message)
|
||||||
|
{
|
||||||
|
await using (var uow = _db.GetDbContext())
|
||||||
|
{
|
||||||
|
await uow.GetTable<GreetSettings>()
|
||||||
|
.InsertOrUpdateAsync(() => new()
|
||||||
|
{
|
||||||
|
GuildId = guildId,
|
||||||
|
GreetType = greetType,
|
||||||
|
MessageText = message
|
||||||
|
},
|
||||||
|
x => new()
|
||||||
|
{
|
||||||
|
MessageText = message
|
||||||
|
},
|
||||||
|
() => new()
|
||||||
|
{
|
||||||
|
GuildId = guildId,
|
||||||
|
GreetType = greetType
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var conf = await GetGreetSettingsAsync(guildId, greetType);
|
||||||
|
|
||||||
|
return conf?.IsEnabled ?? false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> SetDeleteTimer(ulong guildId, GreetType greetType, int timer)
|
||||||
|
{
|
||||||
|
if (timer < 0 || timer > 3600)
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(timer));
|
||||||
|
|
||||||
|
await using (var uow = _db.GetDbContext())
|
||||||
|
{
|
||||||
|
await uow.GetTable<GreetSettings>()
|
||||||
|
.InsertOrUpdateAsync(() => new()
|
||||||
|
{
|
||||||
|
GuildId = guildId,
|
||||||
|
GreetType = greetType,
|
||||||
|
AutoDeleteTimer = timer,
|
||||||
|
},
|
||||||
|
x => new()
|
||||||
|
{
|
||||||
|
AutoDeleteTimer = timer
|
||||||
|
},
|
||||||
|
() => new()
|
||||||
|
{
|
||||||
|
GuildId = guildId,
|
||||||
|
GreetType = greetType
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var conf = await GetGreetSettingsAsync(guildId, greetType);
|
||||||
|
|
||||||
|
return conf?.IsEnabled ?? false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public async Task<bool> Test(
|
||||||
|
ulong guildId,
|
||||||
|
GreetType type,
|
||||||
|
IMessageChannel channel,
|
||||||
|
IGuildUser user)
|
||||||
|
{
|
||||||
|
var conf = await GetGreetSettingsAsync(guildId, type);
|
||||||
|
if (conf is null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
await SendMessage(conf, channel, user);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> SendMessage(GreetSettings conf, IMessageChannel channel, IGuildUser user)
|
||||||
|
{
|
||||||
|
if (conf.GreetType == GreetType.GreetDm)
|
||||||
|
{
|
||||||
|
await _greetQueue.Writer.WriteAsync((conf, user, channel as ITextChannel));
|
||||||
|
return await GreetDmUser(conf, user);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (channel is not ITextChannel ch)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
await GreetUsers(conf, ch, user);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,21 @@
|
||||||
|
namespace EllieBot.Services;
|
||||||
|
|
||||||
|
public enum GreetType
|
||||||
|
{
|
||||||
|
Greet,
|
||||||
|
GreetDm,
|
||||||
|
Bye,
|
||||||
|
Boost,
|
||||||
|
}
|
||||||
|
|
||||||
|
public class GreetSettings
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
|
||||||
|
public ulong GuildId { get; set; }
|
||||||
|
public GreetType GreetType { get; set; }
|
||||||
|
public string? MessageText { get; set; }
|
||||||
|
public bool IsEnabled { get; set; }
|
||||||
|
public ulong? ChannelId { get; set; }
|
||||||
|
public int AutoDeleteTimer { get; set; }
|
||||||
|
}
|
|
@ -0,0 +1,95 @@
|
||||||
|
using LinqToDB;
|
||||||
|
using LinqToDB.EntityFrameworkCore;
|
||||||
|
using EllieBot.Common.ModuleBehaviors;
|
||||||
|
using EllieBot.Db.Models;
|
||||||
|
using System.Threading.Channels;
|
||||||
|
|
||||||
|
namespace EllieBot.Modules.Administration.Honeypot;
|
||||||
|
|
||||||
|
public sealed class HoneyPotService : IHoneyPotService, IReadyExecutor, IExecNoCommand, IEService
|
||||||
|
{
|
||||||
|
private readonly DbService _db;
|
||||||
|
private readonly CommandHandler _handler;
|
||||||
|
|
||||||
|
private ConcurrentHashSet<ulong> _channels = new();
|
||||||
|
|
||||||
|
private Channel<SocketGuildUser> _punishments = Channel.CreateBounded<SocketGuildUser>(
|
||||||
|
new BoundedChannelOptions(100)
|
||||||
|
{
|
||||||
|
FullMode = BoundedChannelFullMode.DropOldest,
|
||||||
|
SingleReader = true,
|
||||||
|
SingleWriter = false,
|
||||||
|
});
|
||||||
|
|
||||||
|
public HoneyPotService(DbService db, CommandHandler handler)
|
||||||
|
{
|
||||||
|
_db = db;
|
||||||
|
_handler = handler;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> ToggleHoneypotChannel(ulong guildId, ulong channelId)
|
||||||
|
{
|
||||||
|
await using var uow = _db.GetDbContext();
|
||||||
|
|
||||||
|
var deleted = await uow.HoneyPotChannels
|
||||||
|
.Where(x => x.GuildId == guildId)
|
||||||
|
.DeleteWithOutputAsync();
|
||||||
|
|
||||||
|
if (deleted.Length > 0)
|
||||||
|
{
|
||||||
|
_channels.TryRemove(deleted[0].ChannelId);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
await uow.HoneyPotChannels
|
||||||
|
.ToLinqToDBTable()
|
||||||
|
.InsertAsync(() => new HoneypotChannel
|
||||||
|
{
|
||||||
|
GuildId = guildId,
|
||||||
|
ChannelId = channelId
|
||||||
|
});
|
||||||
|
|
||||||
|
_channels.Add(channelId);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task OnReadyAsync()
|
||||||
|
{
|
||||||
|
await using var uow = _db.GetDbContext();
|
||||||
|
|
||||||
|
var channels = await uow.HoneyPotChannels
|
||||||
|
.Select(x => x.ChannelId)
|
||||||
|
.ToListAsyncLinqToDB();
|
||||||
|
|
||||||
|
_channels = new(channels);
|
||||||
|
|
||||||
|
while (await _punishments.Reader.WaitToReadAsync())
|
||||||
|
{
|
||||||
|
while (_punishments.Reader.TryRead(out var user))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Log.Information("Honeypot caught user {User} [{UserId}]", user, user.Id);
|
||||||
|
await user.BanAsync(pruneDays: 1);
|
||||||
|
await user.Guild.RemoveBanAsync(user.Id);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Log.Warning(e, "Failed banning {User} due to {Error}", user, e.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
await Task.Delay(1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task ExecOnNoCommandAsync(IGuild guild, IUserMessage msg)
|
||||||
|
{
|
||||||
|
if (_channels.Contains(msg.Channel.Id) && msg.Author is SocketGuildUser sgu)
|
||||||
|
{
|
||||||
|
if (!sgu.GuildPermissions.BanMembers)
|
||||||
|
await _punishments.Writer.WriteAsync(sgu);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,29 @@
|
||||||
|
using EllieBot.Modules.Administration.Honeypot;
|
||||||
|
|
||||||
|
namespace EllieBot.Modules.Administration;
|
||||||
|
|
||||||
|
public partial class Administration
|
||||||
|
{
|
||||||
|
[Group]
|
||||||
|
public partial class HoneypotCommands : EllieModule
|
||||||
|
{
|
||||||
|
private readonly IHoneyPotService _service;
|
||||||
|
|
||||||
|
public HoneypotCommands(IHoneyPotService service)
|
||||||
|
=> _service = service;
|
||||||
|
|
||||||
|
[Cmd]
|
||||||
|
[RequireContext(ContextType.Guild)]
|
||||||
|
[RequireUserPermission(GuildPermission.Administrator)]
|
||||||
|
[RequireBotPermission(GuildPermission.BanMembers)]
|
||||||
|
public async Task Honeypot()
|
||||||
|
{
|
||||||
|
var enabled = await _service.ToggleHoneypotChannel(ctx.Guild.Id, ctx.Channel.Id);
|
||||||
|
|
||||||
|
if (enabled)
|
||||||
|
await Response().Confirm(strs.honeypot_on).SendAsync();
|
||||||
|
else
|
||||||
|
await Response().Confirm(strs.honeypot_off).SendAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
namespace EllieBot.Modules.Administration.Honeypot;
|
||||||
|
|
||||||
|
public interface IHoneyPotService
|
||||||
|
{
|
||||||
|
public Task<bool> ToggleHoneypotChannel(ulong guildId, ulong channelId);
|
||||||
|
}
|
235
src/EllieBot/Modules/Administration/ImageOnlyChannelService.cs
Normal file
235
src/EllieBot/Modules/Administration/ImageOnlyChannelService.cs
Normal file
|
@ -0,0 +1,235 @@
|
||||||
|
#nullable disable
|
||||||
|
using LinqToDB;
|
||||||
|
using Microsoft.Extensions.Caching.Memory;
|
||||||
|
using EllieBot.Common.ModuleBehaviors;
|
||||||
|
using System.Net;
|
||||||
|
using System.Threading.Channels;
|
||||||
|
using EllieBot.Db.Models;
|
||||||
|
|
||||||
|
namespace EllieBot.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 or 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;
|
||||||
|
}
|
||||||
|
}
|
264
src/EllieBot/Modules/Administration/LocalizationCommands.cs
Normal file
264
src/EllieBot/Modules/Administration/LocalizationCommands.cs
Normal file
|
@ -0,0 +1,264 @@
|
||||||
|
#nullable disable
|
||||||
|
using System.Globalization;
|
||||||
|
|
||||||
|
namespace EllieBot.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 Response().Confirm(strs.lang_set_show(Format.Bold(Culture.ToString()),
|
||||||
|
Format.Bold(Culture.NativeName))).SendAsync();
|
||||||
|
|
||||||
|
[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 CultureInfo(name);
|
||||||
|
if (!_supportedLocales.ContainsKey(ci.Name))
|
||||||
|
{
|
||||||
|
await LanguagesList();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_localization.SetGuildCulture(ctx.Guild, ci);
|
||||||
|
}
|
||||||
|
|
||||||
|
var nativeName = ci.NativeName;
|
||||||
|
if (ci.Name == "ts-TS")
|
||||||
|
nativeName = _supportedLocales[ci.Name];
|
||||||
|
await Response().Confirm(strs.lang_set(Format.Bold(ci.ToString()), Format.Bold(nativeName))).SendAsync();
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
await Response().Error(strs.lang_set_fail).SendAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Cmd]
|
||||||
|
public async Task LanguageSetDefault()
|
||||||
|
{
|
||||||
|
var cul = _localization.DefaultCultureInfo;
|
||||||
|
await Response().Error(strs.lang_set_bot_show(cul, cul.NativeName)).SendAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Cmd]
|
||||||
|
[OwnerOnly]
|
||||||
|
public async Task LanguageSetDefault(string name)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
CultureInfo ci;
|
||||||
|
if (name.Trim().ToLowerInvariant() == "default")
|
||||||
|
{
|
||||||
|
_localization.ResetDefaultCulture();
|
||||||
|
ci = _localization.DefaultCultureInfo;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ci = new CultureInfo(name);
|
||||||
|
if (!_supportedLocales.ContainsKey(ci.Name))
|
||||||
|
{
|
||||||
|
await LanguagesList();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_localization.SetDefaultCulture(ci);
|
||||||
|
}
|
||||||
|
|
||||||
|
await Response().Confirm(strs.lang_set_bot(Format.Bold(ci.ToString()),
|
||||||
|
Format.Bold(ci.NativeName))).SendAsync();
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
await Response().Error(strs.lang_set_fail).SendAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Cmd]
|
||||||
|
public async Task LanguagesList()
|
||||||
|
=> await Response().Embed(_sender.CreateEmbed()
|
||||||
|
.WithOkColor()
|
||||||
|
.WithTitle(GetText(strs.lang_list))
|
||||||
|
.WithDescription(string.Join("\n",
|
||||||
|
_supportedLocales.Select(
|
||||||
|
x => $"{Format.Code(x.Key),-10} => {x.Value}")))).SendAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* 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" }
|
||||||
|
*/
|
231
src/EllieBot/Modules/Administration/Mute/MuteCommands.cs
Normal file
231
src/EllieBot/Modules/Administration/Mute/MuteCommands.cs
Normal file
|
@ -0,0 +1,231 @@
|
||||||
|
#nullable disable
|
||||||
|
using EllieBot.Common.TypeReaders.Models;
|
||||||
|
using EllieBot.Modules.Administration.Services;
|
||||||
|
|
||||||
|
namespace EllieBot.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 Response().Error(strs.mute_perms).SendAsync();
|
||||||
|
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 Response().Confirm(strs.mute_role(Format.Code(muteRole.Name))).SendAsync();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ctx.User.Id != ctx.Guild.OwnerId
|
||||||
|
&& role.Position >= ((SocketGuildUser)ctx.User).Roles.Max(x => x.Position))
|
||||||
|
{
|
||||||
|
await Response().Error(strs.insuf_perms_u).SendAsync();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await _service.SetMuteRoleAsync(ctx.Guild.Id, role.Name);
|
||||||
|
|
||||||
|
await Response().Confirm(strs.mute_role_set).SendAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
[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 Response().Confirm(strs.user_muted(Format.Bold(target.ToString()))).SendAsync();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Log.Warning(ex, "Exception in the mute command");
|
||||||
|
await Response().Error(strs.mute_error).SendAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[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 Response().Confirm(strs.user_muted_time(Format.Bold(user.ToString()),
|
||||||
|
(int)time.Time.TotalMinutes)).SendAsync();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Log.Warning(ex, "Error in mute command");
|
||||||
|
await Response().Error(strs.mute_error).SendAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[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 Response().Confirm(strs.user_unmuted(Format.Bold(user.ToString()))).SendAsync();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
await Response().Error(strs.mute_error).SendAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[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 Response().Confirm(strs.user_chat_mute(Format.Bold(user.ToString()))).SendAsync();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Log.Warning(ex, "Exception in the chatmute command");
|
||||||
|
await Response().Error(strs.mute_error).SendAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[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 Response().Confirm(strs.user_chat_mute_time(Format.Bold(user.ToString()),
|
||||||
|
(int)time.Time.TotalMinutes)).SendAsync();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Log.Warning(ex, "Error in chatmute command");
|
||||||
|
await Response().Error(strs.mute_error).SendAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[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 Response().Confirm(strs.user_chat_unmute(Format.Bold(user.ToString()))).SendAsync();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
await Response().Error(strs.mute_error).SendAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[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 Response().Confirm(strs.user_voice_mute(Format.Bold(user.ToString()))).SendAsync();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
await Response().Error(strs.mute_error).SendAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[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 Response().Confirm(strs.user_voice_mute_time(Format.Bold(user.ToString()),
|
||||||
|
(int)time.Time.TotalMinutes)).SendAsync();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
await Response().Error(strs.mute_error).SendAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[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 Response().Confirm(strs.user_voice_unmute(Format.Bold(user.ToString()))).SendAsync();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
await Response().Error(strs.mute_error).SendAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
503
src/EllieBot/Modules/Administration/Mute/MuteService.cs
Normal file
503
src/EllieBot/Modules/Administration/Mute/MuteService.cs
Normal file
|
@ -0,0 +1,503 @@
|
||||||
|
#nullable disable
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using EllieBot.Db.Models;
|
||||||
|
|
||||||
|
namespace EllieBot.Modules.Administration.Services;
|
||||||
|
|
||||||
|
public enum MuteType
|
||||||
|
{
|
||||||
|
Voice,
|
||||||
|
Chat,
|
||||||
|
All
|
||||||
|
}
|
||||||
|
|
||||||
|
public class MuteService : IEService
|
||||||
|
{
|
||||||
|
public enum TimerType { Mute, Ban, AddRole }
|
||||||
|
|
||||||
|
private static readonly OverwritePermissions _denyOverwrite = new(addReactions: PermValue.Deny,
|
||||||
|
sendMessages: PermValue.Deny,
|
||||||
|
sendMessagesInThreads: PermValue.Deny,
|
||||||
|
attachFiles: PermValue.Deny);
|
||||||
|
|
||||||
|
public event Action<IGuildUser, IUser, MuteType, string> UserMuted = delegate { };
|
||||||
|
public event Action<IGuildUser, IUser, MuteType, string> UserUnmuted = delegate { };
|
||||||
|
|
||||||
|
public ConcurrentDictionary<ulong, string> GuildMuteRoles { get; }
|
||||||
|
public ConcurrentDictionary<ulong, ConcurrentHashSet<ulong>> MutedUsers { get; }
|
||||||
|
|
||||||
|
public ConcurrentDictionary<ulong, ConcurrentDictionary<(ulong, TimerType), Timer>> UnTimers { get; } = new();
|
||||||
|
|
||||||
|
private readonly DiscordSocketClient _client;
|
||||||
|
private readonly DbService _db;
|
||||||
|
private readonly IMessageSenderService _sender;
|
||||||
|
|
||||||
|
public MuteService(DiscordSocketClient client, DbService db, IMessageSenderService sender)
|
||||||
|
{
|
||||||
|
_client = client;
|
||||||
|
_db = db;
|
||||||
|
_sender = sender;
|
||||||
|
|
||||||
|
using (var uow = db.GetDbContext())
|
||||||
|
{
|
||||||
|
var guildIds = client.Guilds.Select(x => x.Id).ToList();
|
||||||
|
var configs = uow.Set<GuildConfig>()
|
||||||
|
.AsNoTracking()
|
||||||
|
.AsSplitQuery()
|
||||||
|
.Include(x => x.MutedUsers)
|
||||||
|
.Include(x => x.UnbanTimer)
|
||||||
|
.Include(x => x.UnmuteTimers)
|
||||||
|
.Include(x => x.UnroleTimer)
|
||||||
|
.Where(x => guildIds.Contains(x.GuildId))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
GuildMuteRoles = configs.Where(c => !string.IsNullOrWhiteSpace(c.MuteRoleName))
|
||||||
|
.ToDictionary(c => c.GuildId, c => c.MuteRoleName)
|
||||||
|
.ToConcurrent();
|
||||||
|
|
||||||
|
MutedUsers = new(configs.ToDictionary(k => k.GuildId,
|
||||||
|
v => new ConcurrentHashSet<ulong>(v.MutedUsers.Select(m => m.UserId))));
|
||||||
|
|
||||||
|
var max = TimeSpan.FromDays(49);
|
||||||
|
|
||||||
|
foreach (var conf in configs)
|
||||||
|
{
|
||||||
|
foreach (var x in conf.UnmuteTimers)
|
||||||
|
{
|
||||||
|
TimeSpan after;
|
||||||
|
if (x.UnmuteAt - TimeSpan.FromMinutes(2) <= DateTime.UtcNow)
|
||||||
|
after = TimeSpan.FromMinutes(2);
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var unmute = x.UnmuteAt - DateTime.UtcNow;
|
||||||
|
after = unmute > max ? max : unmute;
|
||||||
|
}
|
||||||
|
|
||||||
|
StartUn_Timer(conf.GuildId, x.UserId, after, TimerType.Mute);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var x in conf.UnbanTimer)
|
||||||
|
{
|
||||||
|
TimeSpan after;
|
||||||
|
if (x.UnbanAt - TimeSpan.FromMinutes(2) <= DateTime.UtcNow)
|
||||||
|
after = TimeSpan.FromMinutes(2);
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var unban = x.UnbanAt - DateTime.UtcNow;
|
||||||
|
after = unban > max ? max : unban;
|
||||||
|
}
|
||||||
|
|
||||||
|
StartUn_Timer(conf.GuildId, x.UserId, after, TimerType.Ban);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var x in conf.UnroleTimer)
|
||||||
|
{
|
||||||
|
TimeSpan after;
|
||||||
|
if (x.UnbanAt - TimeSpan.FromMinutes(2) <= DateTime.UtcNow)
|
||||||
|
after = TimeSpan.FromMinutes(2);
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var unban = x.UnbanAt - DateTime.UtcNow;
|
||||||
|
after = unban > max ? max : unban;
|
||||||
|
}
|
||||||
|
|
||||||
|
StartUn_Timer(conf.GuildId, x.UserId, after, TimerType.AddRole, x.RoleId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_client.UserJoined += Client_UserJoined;
|
||||||
|
}
|
||||||
|
|
||||||
|
UserMuted += OnUserMuted;
|
||||||
|
UserUnmuted += OnUserUnmuted;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnUserMuted(
|
||||||
|
IGuildUser user,
|
||||||
|
IUser mod,
|
||||||
|
MuteType type,
|
||||||
|
string reason)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(reason))
|
||||||
|
return;
|
||||||
|
|
||||||
|
_ = Task.Run(() => _sender.Response(user)
|
||||||
|
.Embed(_sender.CreateEmbed()
|
||||||
|
.WithDescription($"You've been muted in {user.Guild} server")
|
||||||
|
.AddField("Mute Type", type.ToString())
|
||||||
|
.AddField("Moderator", mod.ToString())
|
||||||
|
.AddField("Reason", reason))
|
||||||
|
.SendAsync());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnUserUnmuted(
|
||||||
|
IGuildUser user,
|
||||||
|
IUser mod,
|
||||||
|
MuteType type,
|
||||||
|
string reason)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(reason))
|
||||||
|
return;
|
||||||
|
|
||||||
|
_ = Task.Run(() => _sender.Response(user)
|
||||||
|
.Embed(_sender.CreateEmbed()
|
||||||
|
.WithDescription($"You've been unmuted in {user.Guild} server")
|
||||||
|
.AddField("Unmute Type", type.ToString())
|
||||||
|
.AddField("Moderator", mod.ToString())
|
||||||
|
.AddField("Reason", reason))
|
||||||
|
.SendAsync());
|
||||||
|
}
|
||||||
|
|
||||||
|
private Task Client_UserJoined(IGuildUser usr)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
MutedUsers.TryGetValue(usr.Guild.Id, out var muted);
|
||||||
|
|
||||||
|
if (muted is null || !muted.Contains(usr.Id))
|
||||||
|
return Task.CompletedTask;
|
||||||
|
_ = Task.Run(() => MuteUser(usr, _client.CurrentUser, reason: "Sticky mute"));
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Log.Warning(ex, "Error in MuteService UserJoined event");
|
||||||
|
}
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task SetMuteRoleAsync(ulong guildId, string name)
|
||||||
|
{
|
||||||
|
await using var uow = _db.GetDbContext();
|
||||||
|
var config = uow.GuildConfigsForId(guildId, set => set);
|
||||||
|
config.MuteRoleName = name;
|
||||||
|
GuildMuteRoles.AddOrUpdate(guildId, name, (_, _) => name);
|
||||||
|
await uow.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task MuteUser(
|
||||||
|
IGuildUser usr,
|
||||||
|
IUser mod,
|
||||||
|
MuteType type = MuteType.All,
|
||||||
|
string reason = "")
|
||||||
|
{
|
||||||
|
if (type == MuteType.All)
|
||||||
|
{
|
||||||
|
try { await usr.ModifyAsync(x => x.Mute = true); }
|
||||||
|
catch { }
|
||||||
|
|
||||||
|
var muteRole = await GetMuteRole(usr.Guild);
|
||||||
|
if (!usr.RoleIds.Contains(muteRole.Id))
|
||||||
|
await usr.AddRoleAsync(muteRole);
|
||||||
|
StopTimer(usr.GuildId, usr.Id, TimerType.Mute);
|
||||||
|
await using (var uow = _db.GetDbContext())
|
||||||
|
{
|
||||||
|
var config = uow.GuildConfigsForId(usr.Guild.Id,
|
||||||
|
set => set.Include(gc => gc.MutedUsers).Include(gc => gc.UnmuteTimers));
|
||||||
|
config.MutedUsers.Add(new()
|
||||||
|
{
|
||||||
|
UserId = usr.Id
|
||||||
|
});
|
||||||
|
if (MutedUsers.TryGetValue(usr.Guild.Id, out var muted))
|
||||||
|
muted.Add(usr.Id);
|
||||||
|
|
||||||
|
config.UnmuteTimers.RemoveWhere(x => x.UserId == usr.Id);
|
||||||
|
|
||||||
|
await uow.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
UserMuted(usr, mod, MuteType.All, reason);
|
||||||
|
}
|
||||||
|
else if (type == MuteType.Voice)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await usr.ModifyAsync(x => x.Mute = true);
|
||||||
|
UserMuted(usr, mod, MuteType.Voice, reason);
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
}
|
||||||
|
else if (type == MuteType.Chat)
|
||||||
|
{
|
||||||
|
await usr.AddRoleAsync(await GetMuteRole(usr.Guild));
|
||||||
|
UserMuted(usr, mod, MuteType.Chat, reason);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task UnmuteUser(
|
||||||
|
ulong guildId,
|
||||||
|
ulong usrId,
|
||||||
|
IUser mod,
|
||||||
|
MuteType type = MuteType.All,
|
||||||
|
string reason = "")
|
||||||
|
{
|
||||||
|
var usr = _client.GetGuild(guildId)?.GetUser(usrId);
|
||||||
|
if (type == MuteType.All)
|
||||||
|
{
|
||||||
|
StopTimer(guildId, usrId, TimerType.Mute);
|
||||||
|
await using (var uow = _db.GetDbContext())
|
||||||
|
{
|
||||||
|
var config = uow.GuildConfigsForId(guildId,
|
||||||
|
set => set.Include(gc => gc.MutedUsers).Include(gc => gc.UnmuteTimers));
|
||||||
|
var match = new MutedUserId
|
||||||
|
{
|
||||||
|
UserId = usrId
|
||||||
|
};
|
||||||
|
var toRemove = config.MutedUsers.FirstOrDefault(x => x.Equals(match));
|
||||||
|
if (toRemove is not null)
|
||||||
|
uow.Remove(toRemove);
|
||||||
|
if (MutedUsers.TryGetValue(guildId, out var muted))
|
||||||
|
muted.TryRemove(usrId);
|
||||||
|
|
||||||
|
config.UnmuteTimers.RemoveWhere(x => x.UserId == usrId);
|
||||||
|
|
||||||
|
await uow.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (usr is not null)
|
||||||
|
{
|
||||||
|
try { await usr.ModifyAsync(x => x.Mute = false); }
|
||||||
|
catch { }
|
||||||
|
|
||||||
|
try { await usr.RemoveRoleAsync(await GetMuteRole(usr.Guild)); }
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
/*ignore*/
|
||||||
|
}
|
||||||
|
|
||||||
|
UserUnmuted(usr, mod, MuteType.All, reason);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (type == MuteType.Voice)
|
||||||
|
{
|
||||||
|
if (usr is null)
|
||||||
|
return;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await usr.ModifyAsync(x => x.Mute = false);
|
||||||
|
UserUnmuted(usr, mod, MuteType.Voice, reason);
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
}
|
||||||
|
else if (type == MuteType.Chat)
|
||||||
|
{
|
||||||
|
if (usr is null)
|
||||||
|
return;
|
||||||
|
await usr.RemoveRoleAsync(await GetMuteRole(usr.Guild));
|
||||||
|
UserUnmuted(usr, mod, MuteType.Chat, reason);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IRole> GetMuteRole(IGuild guild)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(guild);
|
||||||
|
|
||||||
|
const string defaultMuteRoleName = "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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,83 @@
|
||||||
|
#nullable disable
|
||||||
|
using EllieBot.Common.TypeReaders;
|
||||||
|
|
||||||
|
namespace EllieBot.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 Response().Confirm(strs.perm_override_reset).SendAsync();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var aggregatePerms = perms.Aggregate((acc, seed) => seed | acc);
|
||||||
|
await _service.AddOverride(ctx.Guild.Id, cmd.Name, aggregatePerms);
|
||||||
|
|
||||||
|
await Response()
|
||||||
|
.Confirm(strs.perm_override(Format.Bold(aggregatePerms.ToString()),
|
||||||
|
Format.Code(cmd.Name)))
|
||||||
|
.SendAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Cmd]
|
||||||
|
[RequireContext(ContextType.Guild)]
|
||||||
|
[UserPerm(GuildPerm.Administrator)]
|
||||||
|
public async Task DiscordPermOverrideReset()
|
||||||
|
{
|
||||||
|
var result = await PromptUserConfirmAsync(_sender.CreateEmbed()
|
||||||
|
.WithOkColor()
|
||||||
|
.WithDescription(GetText(strs.perm_override_all_confirm)));
|
||||||
|
|
||||||
|
if (!result)
|
||||||
|
return;
|
||||||
|
|
||||||
|
await _service.ClearAllOverrides(ctx.Guild.Id);
|
||||||
|
|
||||||
|
await Response().Confirm(strs.perm_override_all).SendAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Cmd]
|
||||||
|
[RequireContext(ContextType.Guild)]
|
||||||
|
[UserPerm(GuildPerm.Administrator)]
|
||||||
|
public async Task DiscordPermOverrideList(int page = 1)
|
||||||
|
{
|
||||||
|
if (--page < 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var allOverrides = await _service.GetAllOverrides(ctx.Guild.Id);
|
||||||
|
|
||||||
|
await Response()
|
||||||
|
.Paginated()
|
||||||
|
.Items(allOverrides)
|
||||||
|
.PageSize(9)
|
||||||
|
.CurrentPage(page)
|
||||||
|
.Page((items, _) =>
|
||||||
|
{
|
||||||
|
var eb = _sender.CreateEmbed().WithTitle(GetText(strs.perm_overrides)).WithOkColor();
|
||||||
|
|
||||||
|
if (items.Count == 0)
|
||||||
|
eb.WithDescription(GetText(strs.perm_override_page_none));
|
||||||
|
else
|
||||||
|
{
|
||||||
|
eb.WithDescription(items.Select(ov => $"{ov.Command} => {ov.Perm.ToString()}")
|
||||||
|
.Join("\n"));
|
||||||
|
}
|
||||||
|
|
||||||
|
return eb;
|
||||||
|
})
|
||||||
|
.SendAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,68 @@
|
||||||
|
#nullable disable
|
||||||
|
using EllieBot.Modules.Administration.Services;
|
||||||
|
|
||||||
|
namespace EllieBot.Modules.Administration;
|
||||||
|
|
||||||
|
public partial class Administration
|
||||||
|
{
|
||||||
|
[Group]
|
||||||
|
public partial class PlayingRotateCommands : EllieModule<PlayingRotateService>
|
||||||
|
{
|
||||||
|
[Cmd]
|
||||||
|
[OwnerOnly]
|
||||||
|
public async Task RotatePlaying()
|
||||||
|
{
|
||||||
|
if (_service.ToggleRotatePlaying())
|
||||||
|
await Response().Confirm(strs.ropl_enabled).SendAsync();
|
||||||
|
else
|
||||||
|
await Response().Confirm(strs.ropl_disabled).SendAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
[Cmd]
|
||||||
|
[OwnerOnly]
|
||||||
|
public Task AddPlaying([Leftover] string status)
|
||||||
|
=> AddPlaying(ActivityType.CustomStatus, status);
|
||||||
|
|
||||||
|
[Cmd]
|
||||||
|
[OwnerOnly]
|
||||||
|
public async Task AddPlaying(ActivityType statusType, [Leftover] string status)
|
||||||
|
{
|
||||||
|
await _service.AddPlaying(statusType, status);
|
||||||
|
|
||||||
|
await Response().Confirm(strs.ropl_added).SendAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Cmd]
|
||||||
|
[OwnerOnly]
|
||||||
|
public async Task ListPlaying()
|
||||||
|
{
|
||||||
|
var statuses = _service.GetRotatingStatuses();
|
||||||
|
|
||||||
|
if (!statuses.Any())
|
||||||
|
await Response().Error(strs.ropl_not_set).SendAsync();
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var i = 1;
|
||||||
|
await Response()
|
||||||
|
.Confirm(strs.ropl_list(string.Join("\n\t",
|
||||||
|
statuses.Select(rs => $"`{i++}.` *{rs.Type}* {rs.Status}"))))
|
||||||
|
.SendAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Cmd]
|
||||||
|
[OwnerOnly]
|
||||||
|
public async Task RemovePlaying(int index)
|
||||||
|
{
|
||||||
|
index -= 1;
|
||||||
|
|
||||||
|
var msg = await _service.RemovePlayingAsync(index);
|
||||||
|
|
||||||
|
if (msg is null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
await Response().Confirm(strs.reprm(msg)).SendAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,109 @@
|
||||||
|
#nullable disable
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using EllieBot.Common.ModuleBehaviors;
|
||||||
|
using EllieBot.Db.Models;
|
||||||
|
|
||||||
|
namespace EllieBot.Modules.Administration.Services;
|
||||||
|
|
||||||
|
public sealed class PlayingRotateService : IEService, IReadyExecutor
|
||||||
|
{
|
||||||
|
private readonly BotConfigService _bss;
|
||||||
|
private readonly SelfService _selfService;
|
||||||
|
private readonly IReplacementService _repService;
|
||||||
|
// 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,
|
||||||
|
IReplacementService repService)
|
||||||
|
{
|
||||||
|
_db = db;
|
||||||
|
_bss = bss;
|
||||||
|
_selfService = selfService;
|
||||||
|
_repService = repService;
|
||||||
|
_client = client;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
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 = await _repService.ReplaceAsync(playingStatus.Status, new (client: _client));
|
||||||
|
await _selfService.SetActivityAsync(statusText, (ActivityType)playingStatus.Type);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Log.Warning(ex, "Rotating playing status errored: {ErrorMessage}", ex.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<string> RemovePlayingAsync(int index)
|
||||||
|
{
|
||||||
|
ArgumentOutOfRangeException.ThrowIfNegative(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 = (EllieBot.Db.DbActivityType)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();
|
||||||
|
}
|
||||||
|
}
|
57
src/EllieBot/Modules/Administration/Prefix/PrefixCommands.cs
Normal file
57
src/EllieBot/Modules/Administration/Prefix/PrefixCommands.cs
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
#nullable disable
|
||||||
|
namespace EllieBot.Modules.Administration;
|
||||||
|
|
||||||
|
public partial class Administration
|
||||||
|
{
|
||||||
|
[Group]
|
||||||
|
public partial class PrefixCommands : EllieModule
|
||||||
|
{
|
||||||
|
public enum Set
|
||||||
|
{
|
||||||
|
Set
|
||||||
|
}
|
||||||
|
|
||||||
|
[Cmd]
|
||||||
|
[Priority(1)]
|
||||||
|
public async Task Prefix()
|
||||||
|
=> await Response().Confirm(strs.prefix_current(Format.Code(_cmdHandler.GetPrefix(ctx.Guild)))).SendAsync();
|
||||||
|
|
||||||
|
[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 Response().Confirm(strs.prefix_new(Format.Code(oldPrefix), Format.Code(newPrefix))).SendAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Cmd]
|
||||||
|
[OwnerOnly]
|
||||||
|
public async Task DefPrefix([Leftover] string toSet = null)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(toSet))
|
||||||
|
{
|
||||||
|
await Response().Confirm(strs.defprefix_current(_cmdHandler.GetPrefix())).SendAsync();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var oldPrefix = _cmdHandler.GetPrefix();
|
||||||
|
var newPrefix = _cmdHandler.SetDefaultPrefix(toSet);
|
||||||
|
|
||||||
|
await Response().Confirm(strs.defprefix_new(Format.Code(oldPrefix), Format.Code(newPrefix))).SendAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,292 @@
|
||||||
|
#nullable disable
|
||||||
|
using EllieBot.Common.TypeReaders.Models;
|
||||||
|
using EllieBot.Modules.Administration.Services;
|
||||||
|
using EllieBot.Db.Models;
|
||||||
|
|
||||||
|
namespace EllieBot.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 Response().Confirm(strs.prot_disable("Anti-Alt")).SendAsync();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await Response().Confirm(strs.protection_not_running("Anti-Alt")).SendAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
[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 Response().Confirm(strs.prot_disable("Anti-Raid")).SendAsync();
|
||||||
|
return Response().Pending(strs.protection_not_running("Anti-Raid")).SendAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
[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 Response().Error(strs.punishment_unsupported(action)).SendAsync();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (userThreshold is < 2 or > 30)
|
||||||
|
{
|
||||||
|
await Response().Error(strs.raid_cnt(2, 30)).SendAsync();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (seconds is < 2 or > 300)
|
||||||
|
{
|
||||||
|
await Response().Error(strs.raid_time(2, 300)).SendAsync();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (punishTime is not null)
|
||||||
|
{
|
||||||
|
if (!_service.IsDurationAllowed(action))
|
||||||
|
await Response().Error(strs.prot_cant_use_time).SendAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
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 Response()
|
||||||
|
.Confirm(GetText(strs.prot_enable("Anti-Raid")),
|
||||||
|
$"{ctx.User.Mention} {GetAntiRaidString(stats)}")
|
||||||
|
.SendAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Cmd]
|
||||||
|
[RequireContext(ContextType.Guild)]
|
||||||
|
[UserPerm(GuildPerm.Administrator)]
|
||||||
|
public Task AntiSpam()
|
||||||
|
{
|
||||||
|
if (_service.TryStopAntiSpam(ctx.Guild.Id))
|
||||||
|
return Response().Confirm(strs.prot_disable("Anti-Spam")).SendAsync();
|
||||||
|
return Response().Pending(strs.protection_not_running("Anti-Spam")).SendAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
[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 Response().Error(strs.prot_cant_use_time).SendAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
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 Response()
|
||||||
|
.Confirm(GetText(strs.prot_enable("Anti-Spam")),
|
||||||
|
$"{ctx.User.Mention} {GetAntiSpamString(stats)}")
|
||||||
|
.SendAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
[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 Response().Error(strs.protection_not_running("Anti-Spam")).SendAsync();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (added.Value)
|
||||||
|
await Response().Confirm(strs.spam_ignore("Anti-Spam")).SendAsync();
|
||||||
|
else
|
||||||
|
await Response().Confirm(strs.spam_not_ignore("Anti-Spam")).SendAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
[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 Response().Confirm(strs.prot_none).SendAsync();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var embed = _sender.CreateEmbed().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 Response().Embed(embed).SendAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,498 @@
|
||||||
|
#nullable disable
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using EllieBot.Db.Models;
|
||||||
|
using System.Threading.Channels;
|
||||||
|
|
||||||
|
namespace EllieBot.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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,52 @@
|
||||||
|
#nullable disable
|
||||||
|
using EllieBot.Db.Models;
|
||||||
|
|
||||||
|
namespace EllieBot.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);
|
||||||
|
}
|
|
@ -0,0 +1,13 @@
|
||||||
|
#nullable disable
|
||||||
|
using EllieBot.Db.Models;
|
||||||
|
|
||||||
|
namespace EllieBot.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; }
|
||||||
|
}
|
|
@ -0,0 +1,64 @@
|
||||||
|
#nullable disable
|
||||||
|
namespace EllieBot.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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
227
src/EllieBot/Modules/Administration/Prune/PruneCommands.cs
Normal file
227
src/EllieBot/Modules/Administration/Prune/PruneCommands.cs
Normal file
|
@ -0,0 +1,227 @@
|
||||||
|
#nullable disable
|
||||||
|
using CommandLine;
|
||||||
|
using EllieBot.Modules.Administration.Services;
|
||||||
|
|
||||||
|
namespace EllieBot.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();
|
||||||
|
|
||||||
|
var progressMsg = await Response().Pending(strs.prune_progress(0, 100)).SendAsync();
|
||||||
|
var progress = GetProgressTracker(progressMsg);
|
||||||
|
|
||||||
|
PruneResult result;
|
||||||
|
if (opts.Safe)
|
||||||
|
result = await _service.PruneWhere((ITextChannel)ctx.Channel,
|
||||||
|
100,
|
||||||
|
x => x.Author.Id == user.Id && !x.IsPinned,
|
||||||
|
progress,
|
||||||
|
opts.After);
|
||||||
|
else
|
||||||
|
result = await _service.PruneWhere((ITextChannel)ctx.Channel,
|
||||||
|
100,
|
||||||
|
x => x.Author.Id == user.Id,
|
||||||
|
progress,
|
||||||
|
opts.After);
|
||||||
|
|
||||||
|
ctx.Message.DeleteAfter(3);
|
||||||
|
|
||||||
|
await SendResult(result);
|
||||||
|
await progressMsg.DeleteAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
|
||||||
|
var progressMsg = await Response().Pending(strs.prune_progress(0, count)).SendAsync();
|
||||||
|
var progress = GetProgressTracker(progressMsg);
|
||||||
|
|
||||||
|
PruneResult result;
|
||||||
|
if (opts.Safe)
|
||||||
|
result = await _service.PruneWhere((ITextChannel)ctx.Channel,
|
||||||
|
count,
|
||||||
|
x => !x.IsPinned && x.Id != progressMsg.Id,
|
||||||
|
progress,
|
||||||
|
opts.After);
|
||||||
|
else
|
||||||
|
result = await _service.PruneWhere((ITextChannel)ctx.Channel,
|
||||||
|
count,
|
||||||
|
x => x.Id != progressMsg.Id,
|
||||||
|
progress,
|
||||||
|
opts.After);
|
||||||
|
|
||||||
|
await SendResult(result);
|
||||||
|
await progressMsg.DeleteAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private IProgress<(int, int)> GetProgressTracker(IUserMessage progressMsg)
|
||||||
|
{
|
||||||
|
var progress = new Progress<(int, int)>(async (x) =>
|
||||||
|
{
|
||||||
|
var (deleted, total) = x;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await progressMsg.ModifyAsync(props =>
|
||||||
|
{
|
||||||
|
props.Embed = _sender.CreateEmbed()
|
||||||
|
.WithPendingColor()
|
||||||
|
.WithDescription(GetText(strs.prune_progress(deleted, total)))
|
||||||
|
.Build();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return progress;
|
||||||
|
}
|
||||||
|
|
||||||
|
//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);
|
||||||
|
|
||||||
|
var progressMsg = await Response().Pending(strs.prune_progress(0, count)).SendAsync();
|
||||||
|
var progress = GetProgressTracker(progressMsg);
|
||||||
|
|
||||||
|
PruneResult result;
|
||||||
|
if (opts.Safe)
|
||||||
|
{
|
||||||
|
result = await _service.PruneWhere((ITextChannel)ctx.Channel,
|
||||||
|
count,
|
||||||
|
m => m.Author.Id == userId && DateTime.UtcNow - m.CreatedAt < _twoWeeks && !m.IsPinned,
|
||||||
|
progress,
|
||||||
|
opts.After
|
||||||
|
);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
result = await _service.PruneWhere((ITextChannel)ctx.Channel,
|
||||||
|
count,
|
||||||
|
m => m.Author.Id == userId && DateTime.UtcNow - m.CreatedAt < _twoWeeks,
|
||||||
|
progress,
|
||||||
|
opts.After
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await SendResult(result);
|
||||||
|
await progressMsg.DeleteAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Cmd]
|
||||||
|
[RequireContext(ContextType.Guild)]
|
||||||
|
[UserPerm(ChannelPerm.ManageMessages)]
|
||||||
|
[BotPerm(ChannelPerm.ManageMessages)]
|
||||||
|
public async Task PruneCancel()
|
||||||
|
{
|
||||||
|
var ok = await _service.CancelAsync(ctx.Guild.Id);
|
||||||
|
|
||||||
|
if (!ok)
|
||||||
|
{
|
||||||
|
await Response().Error(strs.prune_not_found).SendAsync();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
await Response().Confirm(strs.prune_cancelled).SendAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private async Task SendResult(PruneResult result)
|
||||||
|
{
|
||||||
|
switch (result)
|
||||||
|
{
|
||||||
|
case PruneResult.Success:
|
||||||
|
break;
|
||||||
|
case PruneResult.AlreadyRunning:
|
||||||
|
var msg = await Response().Pending(strs.prune_already_running).SendAsync();
|
||||||
|
msg.DeleteAfter(5);
|
||||||
|
break;
|
||||||
|
case PruneResult.FeatureLimit:
|
||||||
|
var msg2 = await Response().Pending(strs.feature_limit_reached_owner).SendAsync();
|
||||||
|
msg2.DeleteAfter(10);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
Log.Error("Unhandled result received in prune: {Result}", result);
|
||||||
|
await Response().Error(strs.error_occured).SendAsync();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
9
src/EllieBot/Modules/Administration/Prune/PruneResult.cs
Normal file
9
src/EllieBot/Modules/Administration/Prune/PruneResult.cs
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
#nullable disable
|
||||||
|
namespace EllieBot.Modules.Administration.Services;
|
||||||
|
|
||||||
|
public enum PruneResult
|
||||||
|
{
|
||||||
|
Success,
|
||||||
|
AlreadyRunning,
|
||||||
|
FeatureLimit,
|
||||||
|
}
|
114
src/EllieBot/Modules/Administration/Prune/PruneService.cs
Normal file
114
src/EllieBot/Modules/Administration/Prune/PruneService.cs
Normal file
|
@ -0,0 +1,114 @@
|
||||||
|
#nullable disable
|
||||||
|
using EllieBot.Modules.Patronage;
|
||||||
|
|
||||||
|
namespace EllieBot.Modules.Administration.Services;
|
||||||
|
|
||||||
|
public class PruneService : IEService
|
||||||
|
{
|
||||||
|
//channelids where prunes are currently occuring
|
||||||
|
private readonly ConcurrentDictionary<ulong, CancellationTokenSource> _pruningGuilds = new();
|
||||||
|
private readonly TimeSpan _twoWeeks = TimeSpan.FromDays(14);
|
||||||
|
private readonly ILogCommandService _logService;
|
||||||
|
private readonly IPatronageService _ps;
|
||||||
|
|
||||||
|
public PruneService(ILogCommandService logService, IPatronageService ps)
|
||||||
|
{
|
||||||
|
_logService = logService;
|
||||||
|
_ps = ps;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<PruneResult> PruneWhere(
|
||||||
|
ITextChannel channel,
|
||||||
|
int amount,
|
||||||
|
Func<IMessage, bool> predicate,
|
||||||
|
IProgress<(int deleted, int total)> progress,
|
||||||
|
ulong? after = null
|
||||||
|
)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(channel, nameof(channel));
|
||||||
|
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(amount);
|
||||||
|
|
||||||
|
var originalAmount = amount;
|
||||||
|
|
||||||
|
using var cancelSource = new CancellationTokenSource();
|
||||||
|
if (!_pruningGuilds.TryAdd(channel.GuildId, cancelSource))
|
||||||
|
return PruneResult.AlreadyRunning;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!await _ps.LimitHitAsync(LimitedFeatureName.Prune, channel.Guild.OwnerId))
|
||||||
|
{
|
||||||
|
return PruneResult.FeatureLimit;
|
||||||
|
}
|
||||||
|
|
||||||
|
var now = DateTime.UtcNow;
|
||||||
|
IMessage[] msgs;
|
||||||
|
IMessage lastMessage = null;
|
||||||
|
|
||||||
|
while (amount > 0 && !cancelSource.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
var dled = lastMessage is null
|
||||||
|
? await channel.GetMessagesAsync(50).FlattenAsync()
|
||||||
|
: await channel.GetMessagesAsync(lastMessage, Direction.Before, 50).FlattenAsync();
|
||||||
|
|
||||||
|
msgs = dled
|
||||||
|
.Where(predicate)
|
||||||
|
.Where(x => after is not ulong a || x.Id > a)
|
||||||
|
.Take(amount)
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
if (!msgs.Any())
|
||||||
|
return PruneResult.Success;
|
||||||
|
|
||||||
|
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);
|
||||||
|
amount -= msgs.Length;
|
||||||
|
progress.Report((originalAmount - amount, originalAmount));
|
||||||
|
await Task.Delay(2000, cancelSource.Token);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var group in singleDeletable.Chunk(5))
|
||||||
|
{
|
||||||
|
await group.Select(x => x.DeleteAsync()).WhenAll();
|
||||||
|
amount -= 5;
|
||||||
|
progress.Report((originalAmount - amount, originalAmount));
|
||||||
|
await Task.Delay(5000, cancelSource.Token);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
//ignore
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_pruningGuilds.TryRemove(channel.GuildId, out _);
|
||||||
|
}
|
||||||
|
|
||||||
|
return PruneResult.Success;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> CancelAsync(ulong guildId)
|
||||||
|
{
|
||||||
|
if (!_pruningGuilds.TryRemove(guildId, out var source))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
await source.CancelAsync();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,51 @@
|
||||||
|
#nullable disable
|
||||||
|
using EllieBot.Db.Models;
|
||||||
|
using OneOf;
|
||||||
|
using OneOf.Types;
|
||||||
|
|
||||||
|
namespace EllieBot.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, Error>> 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);
|
||||||
|
}
|
174
src/EllieBot/Modules/Administration/Role/ReactionRoleCommands.cs
Normal file
174
src/EllieBot/Modules/Administration/Role/ReactionRoleCommands.cs
Normal file
|
@ -0,0 +1,174 @@
|
||||||
|
using EllieBot.Modules.Administration.Services;
|
||||||
|
|
||||||
|
namespace EllieBot.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 ReRoAdd(
|
||||||
|
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 Response().Error(strs.rero_message_not_found).SendAsync();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ctx.User.Id != ctx.Guild.OwnerId
|
||||||
|
&& ((IGuildUser)ctx.User).GetRoles().Max(x => x.Position) <= role.Position)
|
||||||
|
{
|
||||||
|
await Response().Error(strs.hierarchy).SendAsync();
|
||||||
|
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(),
|
||||||
|
async fl =>
|
||||||
|
{
|
||||||
|
_ = msg.RemoveReactionAsync(emote, ctx.Client.CurrentUser);
|
||||||
|
await Response().Pending(strs.feature_limit_reached_owner).SendAsync();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Cmd]
|
||||||
|
[RequireContext(ContextType.Guild)]
|
||||||
|
[UserPerm(GuildPerm.ManageRoles)]
|
||||||
|
[BotPerm(GuildPerm.ManageRoles)]
|
||||||
|
public async Task ReRoList(int page = 1)
|
||||||
|
{
|
||||||
|
if (--page < 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var allReros = await _rero.GetReactionRolesAsync(ctx.Guild.Id);
|
||||||
|
|
||||||
|
await Response()
|
||||||
|
.Paginated()
|
||||||
|
.Items(allReros.OrderBy(x => x.Group).ToList())
|
||||||
|
.PageSize(10)
|
||||||
|
.CurrentPage(page)
|
||||||
|
.Page((items, _) =>
|
||||||
|
{
|
||||||
|
var embed = _sender.CreateEmbed()
|
||||||
|
.WithOkColor();
|
||||||
|
|
||||||
|
var content = string.Empty;
|
||||||
|
foreach (var g in items
|
||||||
|
.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;
|
||||||
|
})
|
||||||
|
.SendAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Cmd]
|
||||||
|
[RequireContext(ContextType.Guild)]
|
||||||
|
[UserPerm(GuildPerm.ManageRoles)]
|
||||||
|
[BotPerm(GuildPerm.ManageRoles)]
|
||||||
|
public async Task ReRoRemove(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 ReRoDeleteAll()
|
||||||
|
{
|
||||||
|
await _rero.RemoveAllReactionRoles(ctx.Guild.Id);
|
||||||
|
await ctx.OkAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Cmd]
|
||||||
|
[RequireContext(ContextType.Guild)]
|
||||||
|
[UserPerm(GuildPerm.ManageRoles)]
|
||||||
|
[BotPerm(GuildPerm.ManageRoles)]
|
||||||
|
[Ratelimit(60)]
|
||||||
|
public async Task ReRoTransfer(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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
404
src/EllieBot/Modules/Administration/Role/ReactionRolesService.cs
Normal file
404
src/EllieBot/Modules/Administration/Role/ReactionRolesService.cs
Normal file
|
@ -0,0 +1,404 @@
|
||||||
|
#nullable disable
|
||||||
|
using LinqToDB;
|
||||||
|
using LinqToDB.EntityFrameworkCore;
|
||||||
|
using EllieBot.Common.ModuleBehaviors;
|
||||||
|
using EllieBot.Modules.Patronage;
|
||||||
|
using EllieBot.Db.Models;
|
||||||
|
using OneOf.Types;
|
||||||
|
using OneOf;
|
||||||
|
|
||||||
|
namespace EllieBot.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;
|
||||||
|
|
||||||
|
public ReactionRolesService(
|
||||||
|
DiscordSocketClient client,
|
||||||
|
IPatronageService ps,
|
||||||
|
DbService db,
|
||||||
|
IBotCredentials creds)
|
||||||
|
{
|
||||||
|
_db = db;
|
||||||
|
_client = client;
|
||||||
|
_creds = creds;
|
||||||
|
_ps = ps;
|
||||||
|
_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, new RequestOptions()
|
||||||
|
{
|
||||||
|
AuditLogReason = $"Reaction role"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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()
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
|
||||||
|
if (exclusive.Any())
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await user.RemoveRolesAsync(exclusive,
|
||||||
|
new RequestOptions()
|
||||||
|
{
|
||||||
|
AuditLogReason = "Reaction role exclusive group"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
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, new()
|
||||||
|
{
|
||||||
|
AuditLogReason = "Reaction role"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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, Error>> AddReactionRole(
|
||||||
|
IGuild guild,
|
||||||
|
IMessage msg,
|
||||||
|
string emote,
|
||||||
|
IRole role,
|
||||||
|
int group = 0,
|
||||||
|
int levelReq = 0)
|
||||||
|
{
|
||||||
|
ArgumentOutOfRangeException.ThrowIfNegative(group);
|
||||||
|
|
||||||
|
ArgumentOutOfRangeException.ThrowIfNegative(levelReq);
|
||||||
|
|
||||||
|
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 limit = await _ps.GetUserLimit(LimitedFeatureName.ReactionRole, guild.OwnerId);
|
||||||
|
|
||||||
|
if (!_creds.IsOwner(guild.OwnerId) && (activeReactionRoles >= limit.Quota && limit.Quota >= 0))
|
||||||
|
{
|
||||||
|
return new Error();
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
_ => [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();
|
||||||
|
}
|
||||||
|
}
|
209
src/EllieBot/Modules/Administration/Role/RoleCommands.cs
Normal file
209
src/EllieBot/Modules/Administration/Role/RoleCommands.cs
Normal file
|
@ -0,0 +1,209 @@
|
||||||
|
#nullable disable
|
||||||
|
using SixLabors.ImageSharp.PixelFormats;
|
||||||
|
using Color = SixLabors.ImageSharp.Color;
|
||||||
|
|
||||||
|
namespace EllieBot.Modules.Administration;
|
||||||
|
|
||||||
|
public partial class Administration
|
||||||
|
{
|
||||||
|
public partial class RoleCommands : EllieModule
|
||||||
|
{
|
||||||
|
public enum Exclude
|
||||||
|
{
|
||||||
|
Excl
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly IServiceProvider _services;
|
||||||
|
private StickyRolesService _stickyRoleSvc;
|
||||||
|
|
||||||
|
public RoleCommands(IServiceProvider services, StickyRolesService stickyRoleSvc)
|
||||||
|
{
|
||||||
|
_services = services;
|
||||||
|
_stickyRoleSvc = stickyRoleSvc;
|
||||||
|
}
|
||||||
|
|
||||||
|
[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, new RequestOptions()
|
||||||
|
{
|
||||||
|
AuditLogReason = $"Added by [{ctx.User.Username}]"
|
||||||
|
});
|
||||||
|
|
||||||
|
await Response().Confirm(strs.setrole(Format.Bold(roleToAdd.Name),
|
||||||
|
Format.Bold(targetUser.ToString()))).SendAsync();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Log.Warning(ex, "Error in setrole command");
|
||||||
|
await Response().Error(strs.setrole_err).SendAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[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 Response().Confirm(strs.remrole(Format.Bold(roleToRemove.Name),
|
||||||
|
Format.Bold(targetUser.ToString()))).SendAsync();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
await Response().Error(strs.remrole_err).SendAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[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 Response().Error(strs.renrole_perms).SendAsync();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await roleToEdit.ModifyAsync(g => g.Name = newname);
|
||||||
|
await Response().Confirm(strs.renrole).SendAsync();
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
await Response().Error(strs.renrole_err).SendAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[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 Response().Confirm(strs.rar(Format.Bold(user.ToString()))).SendAsync();
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
await Response().Error(strs.rar_err).SendAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[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 Response().Confirm(strs.cr(Format.Bold(r.Name))).SendAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
[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 Response().Confirm(strs.dr(Format.Bold(role.Name))).SendAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
[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 Response().Confirm(strs.rolehoist_enabled(Format.Bold(role.Name))).SendAsync();
|
||||||
|
else
|
||||||
|
await Response().Confirm(strs.rolehoist_disabled(Format.Bold(role.Name))).SendAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Cmd]
|
||||||
|
[RequireContext(ContextType.Guild)]
|
||||||
|
[Priority(1)]
|
||||||
|
public async Task RoleColor([Leftover] IRole role)
|
||||||
|
=> await Response().Confirm("Role Color", role.Color.RawValue.ToString("x6")).SendAsync();
|
||||||
|
|
||||||
|
[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 Response().Confirm(strs.rc(Format.Bold(role.Name))).SendAsync();
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
await Response().Error(strs.rc_perms).SendAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Cmd]
|
||||||
|
[RequireContext(ContextType.Guild)]
|
||||||
|
[UserPerm(GuildPerm.Administrator)]
|
||||||
|
[BotPerm(GuildPerm.ManageRoles)]
|
||||||
|
public async Task StickyRoles()
|
||||||
|
{
|
||||||
|
var newState = await _stickyRoleSvc.ToggleStickyRoles(ctx.Guild.Id);
|
||||||
|
|
||||||
|
if (newState)
|
||||||
|
{
|
||||||
|
await Response().Confirm(strs.sticky_roles_enabled).SendAsync();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await Response().Confirm(strs.sticky_roles_disabled).SendAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
138
src/EllieBot/Modules/Administration/Role/StickyRolesService.cs
Normal file
138
src/EllieBot/Modules/Administration/Role/StickyRolesService.cs
Normal file
|
@ -0,0 +1,138 @@
|
||||||
|
#nullable disable
|
||||||
|
using LinqToDB;
|
||||||
|
using LinqToDB.EntityFrameworkCore;
|
||||||
|
using EllieBot.Db.Models;
|
||||||
|
using EllieBot.Common.ModuleBehaviors;
|
||||||
|
|
||||||
|
namespace EllieBot.Modules.Administration;
|
||||||
|
|
||||||
|
public sealed class StickyRolesService : IEService, IReadyExecutor
|
||||||
|
{
|
||||||
|
private readonly DiscordSocketClient _client;
|
||||||
|
private readonly IBotCredentials _creds;
|
||||||
|
private readonly DbService _db;
|
||||||
|
private HashSet<ulong> _stickyRoles = new();
|
||||||
|
|
||||||
|
public StickyRolesService(
|
||||||
|
DiscordSocketClient client,
|
||||||
|
IBotCredentials creds,
|
||||||
|
DbService db)
|
||||||
|
{
|
||||||
|
_client = client;
|
||||||
|
_creds = creds;
|
||||||
|
_db = db;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public async Task OnReadyAsync()
|
||||||
|
{
|
||||||
|
await using (var ctx = _db.GetDbContext())
|
||||||
|
{
|
||||||
|
_stickyRoles = (await ctx
|
||||||
|
.Set<GuildConfig>()
|
||||||
|
.ToLinqToDBTable()
|
||||||
|
.Where(x => Linq2DbExpressions.GuildOnShard(x.GuildId,
|
||||||
|
_creds.TotalShards,
|
||||||
|
_client.ShardId))
|
||||||
|
.Where(x => x.StickyRoles)
|
||||||
|
.Select(x => x.GuildId)
|
||||||
|
.ToListAsync())
|
||||||
|
.ToHashSet();
|
||||||
|
}
|
||||||
|
|
||||||
|
_client.UserJoined += ClientOnUserJoined;
|
||||||
|
_client.UserLeft += ClientOnUserLeft;
|
||||||
|
|
||||||
|
// cleanup old ones every hour
|
||||||
|
// 30 days retention
|
||||||
|
if (_client.ShardId == 0)
|
||||||
|
{
|
||||||
|
using var timer = new PeriodicTimer(TimeSpan.FromHours(1));
|
||||||
|
while (await timer.WaitForNextTickAsync())
|
||||||
|
{
|
||||||
|
await using var ctx = _db.GetDbContext();
|
||||||
|
await ctx.GetTable<StickyRole>()
|
||||||
|
.Where(x => x.DateAdded < DateTime.UtcNow - TimeSpan.FromDays(30))
|
||||||
|
.DeleteAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Task ClientOnUserLeft(SocketGuild guild, SocketUser user)
|
||||||
|
{
|
||||||
|
if (user is not SocketGuildUser gu)
|
||||||
|
return Task.CompletedTask;
|
||||||
|
|
||||||
|
if (!_stickyRoles.Contains(guild.Id))
|
||||||
|
return Task.CompletedTask;
|
||||||
|
|
||||||
|
_ = Task.Run(async () => await SaveRolesAsync(guild.Id, gu.Id, gu.Roles));
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task SaveRolesAsync(ulong guildId, ulong userId, IReadOnlyCollection<SocketRole> guRoles)
|
||||||
|
{
|
||||||
|
await using var ctx = _db.GetDbContext();
|
||||||
|
await ctx.GetTable<StickyRole>()
|
||||||
|
.InsertAsync(() => new()
|
||||||
|
{
|
||||||
|
GuildId = guildId,
|
||||||
|
UserId = userId,
|
||||||
|
RoleIds = string.Join(',',
|
||||||
|
guRoles.Where(x => !x.IsEveryone && !x.IsManaged).Select(x => x.Id.ToString())),
|
||||||
|
DateAdded = DateTime.UtcNow
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private Task ClientOnUserJoined(SocketGuildUser user)
|
||||||
|
{
|
||||||
|
if (!_stickyRoles.Contains(user.Guild.Id))
|
||||||
|
return Task.CompletedTask;
|
||||||
|
|
||||||
|
_ = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
var roles = await GetRolesAsync(user.Guild.Id, user.Id);
|
||||||
|
|
||||||
|
await user.AddRolesAsync(roles);
|
||||||
|
});
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<ulong[]> GetRolesAsync(ulong guildId, ulong userId)
|
||||||
|
{
|
||||||
|
await using var ctx = _db.GetDbContext();
|
||||||
|
var stickyRolesEntry = await ctx
|
||||||
|
.GetTable<StickyRole>()
|
||||||
|
.Where(x => x.GuildId == guildId && x.UserId == userId)
|
||||||
|
.DeleteWithOutputAsync();
|
||||||
|
|
||||||
|
if (stickyRolesEntry is { Length: > 0 })
|
||||||
|
{
|
||||||
|
return stickyRolesEntry[0].GetRoleIds();
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> ToggleStickyRoles(ulong guildId, bool? newState = null)
|
||||||
|
{
|
||||||
|
await using var ctx = _db.GetDbContext();
|
||||||
|
var config = ctx.GuildConfigsForId(guildId, set => set);
|
||||||
|
|
||||||
|
config.StickyRoles = newState ?? !config.StickyRoles;
|
||||||
|
await ctx.SaveChangesAsync();
|
||||||
|
|
||||||
|
if (config.StickyRoles)
|
||||||
|
{
|
||||||
|
_stickyRoles.Add(guildId);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_stickyRoles.Remove(guildId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return config.StickyRoles;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,169 @@
|
||||||
|
using System.Net.Http.Json;
|
||||||
|
using System.Text;
|
||||||
|
using EllieBot.Common.ModuleBehaviors;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace EllieBot.Modules.Administration.Self;
|
||||||
|
|
||||||
|
public sealed class ToastielabReleaseModel
|
||||||
|
{
|
||||||
|
[JsonPropertyName("tag_name")]
|
||||||
|
public required string TagName { get; init; }
|
||||||
|
}
|
||||||
|
public sealed class CheckForUpdatesService : IEService, IReadyExecutor
|
||||||
|
{
|
||||||
|
private readonly BotConfigService _bcs;
|
||||||
|
private readonly IBotCredsProvider _bcp;
|
||||||
|
private readonly IHttpClientFactory _httpFactory;
|
||||||
|
private readonly DiscordSocketClient _client;
|
||||||
|
private readonly IMessageSenderService _sender;
|
||||||
|
|
||||||
|
|
||||||
|
private const string RELEASES_URL = "https://toastielab.dev/Emotions-stuff/elliebot/releases";
|
||||||
|
|
||||||
|
public CheckForUpdatesService(
|
||||||
|
BotConfigService bcs,
|
||||||
|
IBotCredsProvider bcp,
|
||||||
|
IHttpClientFactory httpFactory,
|
||||||
|
DiscordSocketClient client,
|
||||||
|
IMessageSenderService sender)
|
||||||
|
{
|
||||||
|
_bcs = bcs;
|
||||||
|
_bcp = bcp;
|
||||||
|
_httpFactory = httpFactory;
|
||||||
|
_client = client;
|
||||||
|
_sender = sender;
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
{
|
||||||
|
using var http = _httpFactory.CreateClient();
|
||||||
|
var toastielabRelease = (await http.GetFromJsonAsync<ToastielabReleaseModel[]>(RELEASES_URL))
|
||||||
|
?.FirstOrDefault();
|
||||||
|
|
||||||
|
if (toastielabRelease?.TagName is null)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var latest = toastielabRelease.TagName;
|
||||||
|
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/elliebot/raw/branch/v5/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 = _sender.CreateEmbed()
|
||||||
|
.WithOkColor()
|
||||||
|
.WithAuthor($"EllieBot v{latest} Released!")
|
||||||
|
.WithTitle("Changelog")
|
||||||
|
.WithUrl("https://toastielab.dev/Emotions-stuff/elliebot/src/branch/v5/CHANGELOG.md")
|
||||||
|
.WithDescription(thisVersionChangelog.TrimTo(4096))
|
||||||
|
.WithFooter(
|
||||||
|
"You may disable these messages by typing '.conf bot checkforupdates false'");
|
||||||
|
|
||||||
|
await _sender.Response(user).Embed(eb).SendAsync();
|
||||||
|
})
|
||||||
|
.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());
|
||||||
|
}
|
||||||
|
}
|
597
src/EllieBot/Modules/Administration/Self/SelfCommands.cs
Normal file
597
src/EllieBot/Modules/Administration/Self/SelfCommands.cs
Normal file
|
@ -0,0 +1,597 @@
|
||||||
|
#nullable disable
|
||||||
|
using Discord.Rest;
|
||||||
|
using EllieBot.Modules.Administration.Services;
|
||||||
|
using EllieBot.Db.Models;
|
||||||
|
using Ellie.Common.Marmalade;
|
||||||
|
|
||||||
|
namespace EllieBot.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 IMarmaladeLoaderService _marmaladeLoader;
|
||||||
|
private readonly ICoordinator _coord;
|
||||||
|
private readonly DbService _db;
|
||||||
|
|
||||||
|
public SelfCommands(
|
||||||
|
DiscordSocketClient client,
|
||||||
|
DbService db,
|
||||||
|
IBotStrings strings,
|
||||||
|
ICoordinator coord,
|
||||||
|
IMarmaladeLoaderService marmaladeLoader)
|
||||||
|
{
|
||||||
|
_client = client;
|
||||||
|
_db = db;
|
||||||
|
_strings = strings;
|
||||||
|
_coord = coord;
|
||||||
|
_marmaladeLoader = marmaladeLoader;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
[Cmd]
|
||||||
|
[RequireContext(ContextType.Guild)]
|
||||||
|
[OwnerOnly]
|
||||||
|
public Task CacheUsers()
|
||||||
|
=> CacheUsers(ctx.Guild);
|
||||||
|
|
||||||
|
[Cmd]
|
||||||
|
[OwnerOnly]
|
||||||
|
public async Task CacheUsers(IGuild guild)
|
||||||
|
{
|
||||||
|
var downloadUsersTask = guild.DownloadUsersAsync();
|
||||||
|
var message = await Response().Pending(strs.cache_users_pending).SendAsync();
|
||||||
|
|
||||||
|
await downloadUsersTask;
|
||||||
|
|
||||||
|
var users = (await guild.GetUsersAsync(CacheMode.CacheOnly))
|
||||||
|
.Cast<IUser>()
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
var (added, updated) = await _service.RefreshUsersAsync(users);
|
||||||
|
|
||||||
|
await message.ModifyAsync(x =>
|
||||||
|
x.Embed = _sender.CreateEmbed()
|
||||||
|
.WithDescription(GetText(strs.cache_users_done(added, updated)))
|
||||||
|
.WithOkColor()
|
||||||
|
.Build()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
[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 Response().Error(strs.error_occured).SendAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[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 Response()
|
||||||
|
.Embed(_sender.CreateEmbed()
|
||||||
|
.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))
|
||||||
|
.SendAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
[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 Response().Confirm(strs.autocmd_add(Format.Code(Format.Sanitize(cmdText)), cmd.Interval)).SendAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
[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 Response().Error(strs.startcmdlist_none).SendAsync();
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var i = 0;
|
||||||
|
await Response()
|
||||||
|
.Confirm(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)))
|
||||||
|
.SendAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[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 Response().Error(strs.autocmdlist_none).SendAsync();
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var i = 0;
|
||||||
|
await Response()
|
||||||
|
.Confirm(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)))
|
||||||
|
.SendAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 Response().Confirm($"⏲ {miliseconds}ms").SendAsync();
|
||||||
|
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 Response().Error(strs.acrm_fail).SendAsync();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await ctx.OkAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Cmd]
|
||||||
|
[RequireContext(ContextType.Guild)]
|
||||||
|
[OwnerOnly]
|
||||||
|
public async Task StartupCommandRemove([Leftover] int index)
|
||||||
|
{
|
||||||
|
if (!_service.RemoveStartupCommand(--index, out _))
|
||||||
|
await Response().Error(strs.scrm_fail).SendAsync();
|
||||||
|
else
|
||||||
|
await Response().Confirm(strs.scrm).SendAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Cmd]
|
||||||
|
[RequireContext(ContextType.Guild)]
|
||||||
|
[UserPerm(GuildPerm.Administrator)]
|
||||||
|
[OwnerOnly]
|
||||||
|
public async Task StartupCommandsClear()
|
||||||
|
{
|
||||||
|
_service.ClearStartupCommands();
|
||||||
|
|
||||||
|
await Response().Confirm(strs.startcmds_cleared).SendAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Cmd]
|
||||||
|
[OwnerOnly]
|
||||||
|
public async Task ForwardMessages()
|
||||||
|
{
|
||||||
|
var enabled = _service.ForwardMessages();
|
||||||
|
|
||||||
|
if (enabled)
|
||||||
|
await Response().Confirm(strs.fwdm_start).SendAsync();
|
||||||
|
else
|
||||||
|
await Response().Pending(strs.fwdm_stop).SendAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Cmd]
|
||||||
|
[OwnerOnly]
|
||||||
|
public async Task ForwardToAll()
|
||||||
|
{
|
||||||
|
var enabled = _service.ForwardToAll();
|
||||||
|
|
||||||
|
if (enabled)
|
||||||
|
await Response().Confirm(strs.fwall_start).SendAsync();
|
||||||
|
else
|
||||||
|
await Response().Pending(strs.fwall_stop).SendAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Cmd]
|
||||||
|
[RequireContext(ContextType.Guild)]
|
||||||
|
[OwnerOnly]
|
||||||
|
public async Task ForwardToChannel()
|
||||||
|
{
|
||||||
|
var enabled = _service.ForwardToChannel(ctx.Channel.Id);
|
||||||
|
|
||||||
|
if (enabled)
|
||||||
|
await Response().Confirm(strs.fwch_start).SendAsync();
|
||||||
|
else
|
||||||
|
await Response().Pending(strs.fwch_stop).SendAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
[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 Response()
|
||||||
|
.Paginated()
|
||||||
|
.Items(allShardStrings)
|
||||||
|
.PageSize(25)
|
||||||
|
.CurrentPage(page)
|
||||||
|
.Page((items, _) =>
|
||||||
|
{
|
||||||
|
var str = string.Join("\n", items);
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(str))
|
||||||
|
str = GetText(strs.no_shards_on_page);
|
||||||
|
|
||||||
|
return _sender.CreateEmbed().WithOkColor().WithDescription($"{status}\n\n{str}");
|
||||||
|
})
|
||||||
|
.SendAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
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 Response().Confirm(strs.shard_reconnecting(Format.Bold("#" + shardId))).SendAsync();
|
||||||
|
else
|
||||||
|
await Response().Error(strs.no_shard_id).SendAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
[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 Response().Confirm(strs.deleted_x_servers(toLeave.Count)).SendAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Cmd]
|
||||||
|
[OwnerOnly]
|
||||||
|
public async Task Die(bool graceful = false)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _client.SetStatusAsync(UserStatus.Invisible);
|
||||||
|
_ = _client.StopAsync();
|
||||||
|
await Response().Confirm(strs.shutting_down).SendAsync();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// ignored
|
||||||
|
}
|
||||||
|
|
||||||
|
await Task.Delay(2000);
|
||||||
|
_coord.Die(graceful);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Cmd]
|
||||||
|
[OwnerOnly]
|
||||||
|
public async Task Restart()
|
||||||
|
{
|
||||||
|
var success = _coord.RestartBot();
|
||||||
|
if (!success)
|
||||||
|
{
|
||||||
|
await Response().Error(strs.restart_fail).SendAsync();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try { await Response().Confirm(strs.restarting).SendAsync(); }
|
||||||
|
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 Response().Confirm(strs.bot_name(Format.Bold(newName))).SendAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Cmd]
|
||||||
|
[OwnerOnly]
|
||||||
|
public async Task SetStatus([Leftover] SettableUserStatus status)
|
||||||
|
{
|
||||||
|
await _client.SetStatusAsync(SettableUserStatusToUserStatus(status));
|
||||||
|
|
||||||
|
await Response().Confirm(strs.bot_status(Format.Bold(status.ToString()))).SendAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Cmd]
|
||||||
|
[OwnerOnly]
|
||||||
|
public async Task SetAvatar([Leftover] string img = null)
|
||||||
|
{
|
||||||
|
var success = await _service.SetAvatar(img);
|
||||||
|
|
||||||
|
if (success)
|
||||||
|
await Response().Confirm(strs.set_avatar).SendAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Cmd]
|
||||||
|
[OwnerOnly]
|
||||||
|
public async Task SetBanner([Leftover] string img = null)
|
||||||
|
{
|
||||||
|
var success = await _service.SetBanner(img);
|
||||||
|
|
||||||
|
if (success)
|
||||||
|
await Response().Confirm(strs.set_banner).SendAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Cmd]
|
||||||
|
[OwnerOnly]
|
||||||
|
public async Task SetActivity(ActivityType? type, [Leftover] string game = null)
|
||||||
|
{
|
||||||
|
// var rep = new ReplacementBuilder().WithDefault(Context).Build();
|
||||||
|
|
||||||
|
var repCtx = new ReplacementContext(ctx);
|
||||||
|
await _service.SetActivityAsync(game is null ? game : await repSvc.ReplaceAsync(game, repCtx), type);
|
||||||
|
|
||||||
|
await Response().Confirm(strs.set_activity).SendAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Cmd]
|
||||||
|
[OwnerOnly]
|
||||||
|
public Task SetActivity([Leftover] string game = null)
|
||||||
|
=> SetActivity(null, game);
|
||||||
|
|
||||||
|
public class SetActivityOptions
|
||||||
|
{
|
||||||
|
public ActivityType? Type { get; set; }
|
||||||
|
public string Game { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[Cmd]
|
||||||
|
[OwnerOnly]
|
||||||
|
public async Task SetStream(string url, [Leftover] string name = null)
|
||||||
|
{
|
||||||
|
name ??= "";
|
||||||
|
|
||||||
|
await _service.SetStreamAsync(name, url);
|
||||||
|
|
||||||
|
await Response().Confirm(strs.set_stream).SendAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum SendWhere
|
||||||
|
{
|
||||||
|
User = 0,
|
||||||
|
U = 0,
|
||||||
|
Usr = 0,
|
||||||
|
|
||||||
|
Channel = 1,
|
||||||
|
Ch = 1,
|
||||||
|
Chan = 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
[Cmd]
|
||||||
|
[OwnerOnly]
|
||||||
|
public async Task Send(SendWhere to, ulong id, [Leftover] SmartText text)
|
||||||
|
{
|
||||||
|
var ch = to switch
|
||||||
|
{
|
||||||
|
SendWhere.User => await ((await _client.Rest.GetUserAsync(id))?.CreateDMChannelAsync()
|
||||||
|
?? Task.FromResult<RestDMChannel>(null)),
|
||||||
|
SendWhere.Channel => await _client.Rest.GetChannelAsync(id) as IMessageChannel,
|
||||||
|
_ => null
|
||||||
|
};
|
||||||
|
|
||||||
|
if (ch is null)
|
||||||
|
{
|
||||||
|
await Response().Error(strs.invalid_format).SendAsync();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
var repCtx = new ReplacementContext(ctx);
|
||||||
|
text = await repSvc.ReplaceAsync(text, repCtx);
|
||||||
|
await Response().Channel(ch).Text(text).SendAsync();
|
||||||
|
|
||||||
|
await ctx.OkAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Cmd]
|
||||||
|
[OwnerOnly]
|
||||||
|
public async Task StringsReload()
|
||||||
|
{
|
||||||
|
_strings.Reload();
|
||||||
|
await _marmaladeLoader.ReloadStrings();
|
||||||
|
await Response().Confirm(strs.bot_strings_reloaded).SendAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
[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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
494
src/EllieBot/Modules/Administration/Self/SelfService.cs
Normal file
494
src/EllieBot/Modules/Administration/Self/SelfService.cs
Normal file
|
@ -0,0 +1,494 @@
|
||||||
|
#nullable disable
|
||||||
|
using EllieBot.Common.ModuleBehaviors;
|
||||||
|
using EllieBot.Db.Models;
|
||||||
|
using System.Collections.Immutable;
|
||||||
|
using LinqToDB;
|
||||||
|
using LinqToDB.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace EllieBot.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 IMessageSenderService _sender;
|
||||||
|
|
||||||
|
//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,
|
||||||
|
IMessageSenderService sender)
|
||||||
|
{
|
||||||
|
_cmdHandler = cmdHandler;
|
||||||
|
_db = db;
|
||||||
|
_strings = strings;
|
||||||
|
_client = client;
|
||||||
|
_creds = creds;
|
||||||
|
_httpFactory = factory;
|
||||||
|
_bss = bss;
|
||||||
|
_pubSub = pubSub;
|
||||||
|
_sender = sender;
|
||||||
|
_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 _sender.Response(ownerCh).Confirm(title, toSend).SendAsync();
|
||||||
|
}
|
||||||
|
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 _sender.Response(ch).Confirm(title, toSend).SendAsync();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
Log.Warning("Error forwarding message to the channel");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var firstOwnerChannel = ownerChannels.Values.First();
|
||||||
|
if (firstOwnerChannel.Recipient.Id != msg.Author.Id)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _sender.Response(firstOwnerChannel).Confirm(title, toSend).SendAsync();
|
||||||
|
}
|
||||||
|
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 async Task<bool> SetBanner(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;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sr.GetContentLength() > 8.Megabytes())
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
await using var imageStream = await sr.Content.ReadAsStreamAsync();
|
||||||
|
|
||||||
|
await _client.CurrentUser.ModifyAsync(x => x.Banner = new Image(imageStream));
|
||||||
|
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
|
||||||
|
{
|
||||||
|
if (data.Type is { } activityType)
|
||||||
|
await _client.SetGameAsync(data.Name, data.Link, activityType);
|
||||||
|
else
|
||||||
|
await _client.SetCustomStatusAsync(data.Name);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Log.Warning(ex, "Error setting activity");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
public Task SetActivityAsync(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; }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds the specified <paramref name="users"/> to the database. If a database user with placeholder name
|
||||||
|
/// and discriminator is present in <paramref name="users"/>, their name and discriminator get updated accordingly.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="ctx">This database context.</param>
|
||||||
|
/// <param name="users">The users to add or update in the database.</param>
|
||||||
|
/// <returns>A tuple with the amount of new users added and old users updated.</returns>
|
||||||
|
public async Task<(long UsersAdded, long UsersUpdated)> RefreshUsersAsync(List<IUser> users)
|
||||||
|
{
|
||||||
|
await using var ctx = _db.GetDbContext();
|
||||||
|
var presentDbUsers = await ctx.GetTable<DiscordUser>()
|
||||||
|
.Select(x => new
|
||||||
|
{
|
||||||
|
x.UserId,
|
||||||
|
x.Username,
|
||||||
|
x.Discriminator
|
||||||
|
})
|
||||||
|
.Where(x => users.Select(y => y.Id).Contains(x.UserId))
|
||||||
|
.ToArrayAsyncEF();
|
||||||
|
|
||||||
|
var usersToAdd = users
|
||||||
|
.Where(x => !presentDbUsers.Select(x => x.UserId).Contains(x.Id))
|
||||||
|
.Select(x => new DiscordUser()
|
||||||
|
{
|
||||||
|
UserId = x.Id,
|
||||||
|
AvatarId = x.AvatarId,
|
||||||
|
Username = x.Username,
|
||||||
|
Discriminator = x.Discriminator
|
||||||
|
});
|
||||||
|
|
||||||
|
var added = (await ctx.BulkCopyAsync(usersToAdd)).RowsCopied;
|
||||||
|
var toUpdateUserIds = presentDbUsers
|
||||||
|
.Where(x => x.Username == "Unknown" && x.Discriminator == "????")
|
||||||
|
.Select(x => x.UserId)
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
foreach (var user in users.Where(x => toUpdateUserIds.Contains(x.Id)))
|
||||||
|
{
|
||||||
|
await ctx.GetTable<DiscordUser>()
|
||||||
|
.Where(x => x.UserId == user.Id)
|
||||||
|
.UpdateAsync(x => new DiscordUser()
|
||||||
|
{
|
||||||
|
Username = user.Username,
|
||||||
|
Discriminator = user.Discriminator,
|
||||||
|
|
||||||
|
// .award tends to set AvatarId and DateAdded to NULL, so account for that.
|
||||||
|
AvatarId = user.AvatarId,
|
||||||
|
DateAdded = x.DateAdded ?? DateTime.UtcNow
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return (added, toUpdateUserIds.Length);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,239 @@
|
||||||
|
#nullable disable
|
||||||
|
using EllieBot.Modules.Administration.Services;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace EllieBot.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 Response().Confirm(strs.adsarm_enable(prefix)).SendAsync();
|
||||||
|
else
|
||||||
|
await Response().Confirm(strs.adsarm_disable(prefix)).SendAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
[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 Response()
|
||||||
|
.Confirm(strs.role_added(Format.Bold(role.Name), Format.Bold(group.ToString())))
|
||||||
|
.SendAsync();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
await Response().Error(strs.role_in_list(Format.Bold(role.Name))).SendAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
[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 Response()
|
||||||
|
.Confirm(strs.group_name_added(Format.Bold(group.ToString()), Format.Bold(name)))
|
||||||
|
.SendAsync();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
await Response().Confirm(strs.group_name_removed(Format.Bold(group.ToString()))).SendAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
[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 Response().Error(strs.self_assign_not).SendAsync();
|
||||||
|
else
|
||||||
|
await Response().Confirm(strs.self_assign_rem(Format.Bold(role.Name))).SendAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Cmd]
|
||||||
|
[RequireContext(ContextType.Guild)]
|
||||||
|
public async Task Lsar(int page = 1)
|
||||||
|
{
|
||||||
|
if (--page < 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var (exclusive, roles, groups) = _service.GetRoles(ctx.Guild);
|
||||||
|
|
||||||
|
await Response()
|
||||||
|
.Paginated()
|
||||||
|
.Items(roles.OrderBy(x => x.Model.Group).ToList())
|
||||||
|
.PageSize(20)
|
||||||
|
.CurrentPage(page)
|
||||||
|
.Page((items, _) =>
|
||||||
|
{
|
||||||
|
var rolesStr = new StringBuilder();
|
||||||
|
var roleGroups = items
|
||||||
|
.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 _sender.CreateEmbed()
|
||||||
|
.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));
|
||||||
|
})
|
||||||
|
.SendAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Cmd]
|
||||||
|
[RequireContext(ContextType.Guild)]
|
||||||
|
[UserPerm(GuildPerm.ManageRoles)]
|
||||||
|
[BotPerm(GuildPerm.ManageRoles)]
|
||||||
|
public async Task Togglexclsar()
|
||||||
|
{
|
||||||
|
var areExclusive = _service.ToggleEsar(ctx.Guild.Id);
|
||||||
|
if (areExclusive)
|
||||||
|
await Response().Confirm(strs.self_assign_excl).SendAsync();
|
||||||
|
else
|
||||||
|
await Response().Confirm(strs.self_assign_no_excl).SendAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
[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 Response().Error(strs.self_assign_not).SendAsync();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await Response()
|
||||||
|
.Confirm(strs.self_assign_level_req(Format.Bold(role.Name),
|
||||||
|
Format.Bold(level.ToString())))
|
||||||
|
.SendAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
[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 Response().Error(strs.self_assign_not).SendAsync();
|
||||||
|
else if (result == SelfAssignedRolesService.AssignResult.ErrLvlReq)
|
||||||
|
msg = await Response().Error(strs.self_assign_not_level(Format.Bold(extra.ToString()))).SendAsync();
|
||||||
|
else if (result == SelfAssignedRolesService.AssignResult.ErrAlreadyHave)
|
||||||
|
msg = await Response().Error(strs.self_assign_already(Format.Bold(role.Name))).SendAsync();
|
||||||
|
else if (result == SelfAssignedRolesService.AssignResult.ErrNotPerms)
|
||||||
|
msg = await Response().Error(strs.self_assign_perms).SendAsync();
|
||||||
|
else
|
||||||
|
msg = await Response().Confirm(strs.self_assign_success(Format.Bold(role.Name))).SendAsync();
|
||||||
|
|
||||||
|
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 Response().Error(strs.self_assign_not).SendAsync();
|
||||||
|
else if (result == SelfAssignedRolesService.RemoveResult.ErrNotHave)
|
||||||
|
msg = await Response().Error(strs.self_assign_not_have(Format.Bold(role.Name))).SendAsync();
|
||||||
|
else if (result == SelfAssignedRolesService.RemoveResult.ErrNotPerms)
|
||||||
|
msg = await Response().Error(strs.self_assign_perms).SendAsync();
|
||||||
|
else
|
||||||
|
msg = await Response().Confirm(strs.self_assign_remove(Format.Bold(role.Name))).SendAsync();
|
||||||
|
|
||||||
|
if (autoDelete)
|
||||||
|
{
|
||||||
|
msg.DeleteAfter(3);
|
||||||
|
ctx.Message.DeleteAfter(3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,233 @@
|
||||||
|
#nullable disable
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using EllieBot.Db.Models;
|
||||||
|
|
||||||
|
namespace EllieBot.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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,25 @@
|
||||||
|
using EllieBot.Db.Models;
|
||||||
|
|
||||||
|
namespace EllieBot.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
|
@ -0,0 +1,175 @@
|
||||||
|
using EllieBot.Common.TypeReaders.Models;
|
||||||
|
using EllieBot.Db.Models;
|
||||||
|
|
||||||
|
namespace EllieBot.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 Response().Confirm(strs.log_all).SendAsync();
|
||||||
|
else
|
||||||
|
await Response().Confirm(strs.log_disabled).SendAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
[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 = _sender.CreateEmbed()
|
||||||
|
.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 Response().Embed(eb).SendAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
[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 Response()
|
||||||
|
.Confirm(
|
||||||
|
strs.log_ignore_chan(Format.Bold(target.Mention + "(" + target.Id + ")")))
|
||||||
|
.SendAsync();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await Response()
|
||||||
|
.Confirm(
|
||||||
|
strs.log_not_ignore_chan(Format.Bold(target.Mention + "(" + target.Id + ")")))
|
||||||
|
.SendAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[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 Response()
|
||||||
|
.Confirm(strs.log_ignore_user(Format.Bold(target.Mention + "(" + target.Id + ")")))
|
||||||
|
.SendAsync();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await Response()
|
||||||
|
.Confirm(strs.log_not_ignore_user(Format.Bold(target.Mention + "(" + target.Id + ")")))
|
||||||
|
.SendAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[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 Response().Confirm(Format.Bold(GetText(strs.log_events)) + "\n" + str).SendAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
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.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 Response().Confirm(strs.log(Format.Bold(type.ToString()))).SendAsync();
|
||||||
|
else
|
||||||
|
await Response().Confirm(strs.log_stop(Format.Bold(type.ToString()))).SendAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
14
src/EllieBot/Modules/Administration/Ticket/TicketCommands.cs
Normal file
14
src/EllieBot/Modules/Administration/Ticket/TicketCommands.cs
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
// namespace EllieBot.Modules.Administration;
|
||||||
|
//
|
||||||
|
// public partial class Administration
|
||||||
|
// {
|
||||||
|
// [Group]
|
||||||
|
// public partial class TicketCommands : EllieModule
|
||||||
|
// {
|
||||||
|
// [Cmd]
|
||||||
|
// public async Task Ticket()
|
||||||
|
// {
|
||||||
|
//
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
|
@ -0,0 +1,94 @@
|
||||||
|
#nullable disable
|
||||||
|
using EllieBot.Db.Models;
|
||||||
|
using EllieBot.Common.ModuleBehaviors;
|
||||||
|
|
||||||
|
namespace EllieBot.Modules.Administration.Services;
|
||||||
|
|
||||||
|
public sealed class GuildTimezoneService : ITimezoneService, IReadyExecutor, IEService
|
||||||
|
{
|
||||||
|
private readonly ConcurrentDictionary<ulong, TimeZoneInfo> _timezones;
|
||||||
|
private readonly DbService _db;
|
||||||
|
private readonly IReplacementPatternStore _repStore;
|
||||||
|
|
||||||
|
public GuildTimezoneService(IBot bot, DbService db, IReplacementPatternStore repStore)
|
||||||
|
{
|
||||||
|
_timezones = bot.AllGuildConfigs.Select(GetTimzezoneTuple)
|
||||||
|
.Where(x => x.Timezone is not null)
|
||||||
|
.ToDictionary(x => x.GuildId, x => x.Timezone)
|
||||||
|
.ToConcurrent();
|
||||||
|
|
||||||
|
_db = db;
|
||||||
|
_repStore = repStore;
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
public Task OnReadyAsync()
|
||||||
|
{
|
||||||
|
_repStore.Register("%server.time%",
|
||||||
|
(IGuild g) =>
|
||||||
|
{
|
||||||
|
var to = TimeZoneInfo.Local;
|
||||||
|
if (g is not null)
|
||||||
|
{
|
||||||
|
to = GetTimeZoneOrDefault(g.Id) ?? TimeZoneInfo.Local;
|
||||||
|
}
|
||||||
|
|
||||||
|
return TimeZoneInfo.ConvertTime(DateTime.UtcNow, TimeZoneInfo.Utc, to).ToString("HH:mm ")
|
||||||
|
+ to.StandardName.GetInitials();
|
||||||
|
});
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,78 @@
|
||||||
|
#nullable disable
|
||||||
|
using EllieBot.Modules.Administration.Services;
|
||||||
|
|
||||||
|
namespace EllieBot.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}";
|
||||||
|
})
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
|
||||||
|
await Response()
|
||||||
|
.Paginated()
|
||||||
|
.Items(timezoneStrings)
|
||||||
|
.PageSize(timezonesPerPage)
|
||||||
|
.CurrentPage(page)
|
||||||
|
.Page((items, _) => _sender.CreateEmbed()
|
||||||
|
.WithOkColor()
|
||||||
|
.WithTitle(GetText(strs.timezones_available))
|
||||||
|
.WithDescription(string.Join("\n", items)))
|
||||||
|
.SendAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Cmd]
|
||||||
|
[RequireContext(ContextType.Guild)]
|
||||||
|
public async Task Timezone()
|
||||||
|
=> await Response().Confirm(strs.timezone_guild(_service.GetTimeZoneOrUtc(ctx.Guild.Id))).SendAsync();
|
||||||
|
|
||||||
|
[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 Response().Error(strs.timezone_not_found).SendAsync();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_service.SetTimeZone(ctx.Guild.Id, tz);
|
||||||
|
|
||||||
|
await Response().Confirm(tz.ToString()).SendAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,986 @@
|
||||||
|
#nullable disable
|
||||||
|
using CommandLine;
|
||||||
|
using EllieBot.Common.TypeReaders.Models;
|
||||||
|
using EllieBot.Modules.Administration.Services;
|
||||||
|
using EllieBot.Db.Models;
|
||||||
|
|
||||||
|
namespace EllieBot.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 Response().Error(strs.hierarchy).SendAsync();
|
||||||
|
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 _sender.Response(user)
|
||||||
|
.Embed(_sender.CreateEmbed()
|
||||||
|
.WithErrorColor()
|
||||||
|
.WithDescription(GetText(strs.warned_on(ctx.Guild.ToString())))
|
||||||
|
.AddField(GetText(strs.moderator), ctx.User.ToString())
|
||||||
|
.AddField(GetText(strs.reason), reason ?? "-"))
|
||||||
|
.SendAsync();
|
||||||
|
}
|
||||||
|
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 = _sender.CreateEmbed().WithErrorColor()
|
||||||
|
.WithDescription(GetText(strs.cant_apply_punishment));
|
||||||
|
|
||||||
|
if (dmFailed)
|
||||||
|
errorEmbed.WithFooter("⚠️ " + GetText(strs.unable_to_dm_user));
|
||||||
|
|
||||||
|
await Response().Embed(errorEmbed).SendAsync();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var embed = _sender.CreateEmbed().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 Response().Embed(embed).SendAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
[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 Response().Confirm(strs.warns_dont_expire).SendAsync();
|
||||||
|
else
|
||||||
|
await Response().Error(strs.warns_expire_in(expireDays)).SendAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
[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 Response().Confirm(strs.warn_expire_reset).SendAsync();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (opts.Delete)
|
||||||
|
await Response().Confirm(strs.warn_expire_set_delete(Format.Bold(days.ToString()))).SendAsync();
|
||||||
|
else
|
||||||
|
await Response().Confirm(strs.warn_expire_set_clear(Format.Bold(days.ToString()))).SendAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
[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 Response()
|
||||||
|
.Paginated()
|
||||||
|
.Items(allWarnings)
|
||||||
|
.PageSize(9)
|
||||||
|
.CurrentPage(inputPage)
|
||||||
|
.Page((warnings, page) =>
|
||||||
|
{
|
||||||
|
var user = (ctx.Guild as SocketGuild)?.GetUser(userId)?.ToString() ?? userId.ToString();
|
||||||
|
var embed = _sender.CreateEmbed().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;
|
||||||
|
})
|
||||||
|
.SendAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Cmd]
|
||||||
|
[RequireContext(ContextType.Guild)]
|
||||||
|
[UserPerm(GuildPerm.BanMembers)]
|
||||||
|
public async Task WarnlogAll(int page = 1)
|
||||||
|
{
|
||||||
|
if (--page < 0)
|
||||||
|
return;
|
||||||
|
var allWarnings = _service.WarnlogAll(ctx.Guild.Id);
|
||||||
|
|
||||||
|
await Response()
|
||||||
|
.Paginated()
|
||||||
|
.Items(allWarnings)
|
||||||
|
.PageSize(15)
|
||||||
|
.CurrentPage(page)
|
||||||
|
.Page((warnings, _) =>
|
||||||
|
{
|
||||||
|
var ws = warnings
|
||||||
|
.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 _sender.CreateEmbed()
|
||||||
|
.WithOkColor()
|
||||||
|
.WithTitle(GetText(strs.warnings_list))
|
||||||
|
.WithDescription(string.Join("\n", ws));
|
||||||
|
})
|
||||||
|
.SendAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Cmd]
|
||||||
|
[RequireContext(ContextType.Guild)]
|
||||||
|
[UserPerm(GuildPerm.Administrator)]
|
||||||
|
public Task WarnDelete(IGuildUser user, int index)
|
||||||
|
=> WarnDelete(user.Id, index);
|
||||||
|
|
||||||
|
[Cmd]
|
||||||
|
[RequireContext(ContextType.Guild)]
|
||||||
|
[UserPerm(GuildPerm.Administrator)]
|
||||||
|
public async Task WarnDelete(ulong userId, int index)
|
||||||
|
{
|
||||||
|
if (--index < 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var warn = await _service.WarnDelete(userId, index);
|
||||||
|
|
||||||
|
if (warn is null)
|
||||||
|
{
|
||||||
|
await Response().Error(strs.warning_not_found).SendAsync();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await Response().Confirm(strs.warning_deleted(Format.Bold(index.ToString()))).SendAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
[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 Response().Error(strs.warnings_cleared(userStr)).SendAsync();
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (success)
|
||||||
|
await Response().Confirm(strs.warning_cleared(Format.Bold(index.ToString()), userStr)).SendAsync();
|
||||||
|
else
|
||||||
|
await Response().Error(strs.warning_clear_fail).SendAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[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 Response().Error(strs.role_too_high).SendAsync();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var success = _service.WarnPunish(ctx.Guild.Id, number, punish, time, role);
|
||||||
|
|
||||||
|
if (!success)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (time is null)
|
||||||
|
{
|
||||||
|
await Response()
|
||||||
|
.Confirm(strs.warn_punish_set(Format.Bold(punish.ToString()),
|
||||||
|
Format.Bold(number.ToString())))
|
||||||
|
.SendAsync();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await Response()
|
||||||
|
.Confirm(strs.warn_punish_set_timed(Format.Bold(punish.ToString()),
|
||||||
|
Format.Bold(number.ToString()),
|
||||||
|
Format.Bold(time.Input)))
|
||||||
|
.SendAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[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 Response()
|
||||||
|
.Confirm(strs.warn_punish_set(Format.Bold(punish.ToString()),
|
||||||
|
Format.Bold(number.ToString())))
|
||||||
|
.SendAsync();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await Response()
|
||||||
|
.Confirm(strs.warn_punish_set_timed(Format.Bold(punish.ToString()),
|
||||||
|
Format.Bold(number.ToString()),
|
||||||
|
Format.Bold(time.Input)))
|
||||||
|
.SendAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Cmd]
|
||||||
|
[RequireContext(ContextType.Guild)]
|
||||||
|
[UserPerm(GuildPerm.BanMembers)]
|
||||||
|
public async Task WarnPunish(int number)
|
||||||
|
{
|
||||||
|
if (!_service.WarnPunishRemove(ctx.Guild.Id, number))
|
||||||
|
return;
|
||||||
|
|
||||||
|
await Response().Confirm(strs.warn_punish_rem(Format.Bold(number.ToString()))).SendAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
[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 Response().Confirm(GetText(strs.warn_punish_list), list).SendAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
[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 smartText =
|
||||||
|
await _service.GetBanUserDmEmbed(Context, guildUser, defaultMessage, msg, time.Time);
|
||||||
|
if (smartText is not null)
|
||||||
|
await Response().User(guildUser).Text(smartText).SendAsync();
|
||||||
|
}
|
||||||
|
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 = _sender.CreateEmbed()
|
||||||
|
.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.ToPrettyStringHm(),
|
||||||
|
true);
|
||||||
|
|
||||||
|
if (dmFailed)
|
||||||
|
toSend.WithFooter("⚠️ " + GetText(strs.unable_to_dm_user));
|
||||||
|
|
||||||
|
await Response().Embed(toSend).SendAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
[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 Response().Embed(_sender.CreateEmbed()
|
||||||
|
.WithOkColor()
|
||||||
|
.WithTitle("⛔️ " + GetText(strs.banned_user))
|
||||||
|
.AddField("ID", userId.ToString(), true))
|
||||||
|
.SendAsync();
|
||||||
|
}
|
||||||
|
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 = await _service.GetBanUserDmEmbed(Context, user, defaultMessage, msg, null);
|
||||||
|
if (embed is not null)
|
||||||
|
await Response().User(user).Text(embed).SendAsync();
|
||||||
|
}
|
||||||
|
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 = _sender.CreateEmbed()
|
||||||
|
.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 Response().Embed(toSend).SendAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Cmd]
|
||||||
|
[RequireContext(ContextType.Guild)]
|
||||||
|
[UserPerm(GuildPerm.BanMembers)]
|
||||||
|
[BotPerm(GuildPerm.BanMembers)]
|
||||||
|
public async Task BanPrune(int days)
|
||||||
|
{
|
||||||
|
if (days < 0 || days > 7)
|
||||||
|
{
|
||||||
|
await Response().Error(strs.invalid_input).SendAsync();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await _service.SetBanPruneAsync(ctx.Guild.Id, days);
|
||||||
|
|
||||||
|
if (days == 0)
|
||||||
|
await Response().Confirm(strs.ban_prune_disabled).SendAsync();
|
||||||
|
else
|
||||||
|
await Response().Confirm(strs.ban_prune(days)).SendAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
[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 Response().Confirm(strs.banmsg_default).SendAsync();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await Response().Confirm(template).SendAsync();
|
||||||
|
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 smartText = await _service.GetBanUserDmEmbed(Context,
|
||||||
|
(IGuildUser)ctx.User,
|
||||||
|
defaultMessage,
|
||||||
|
reason,
|
||||||
|
duration);
|
||||||
|
|
||||||
|
if (smartText is null)
|
||||||
|
await Response().Confirm(strs.banmsg_disabled).SendAsync();
|
||||||
|
else
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await Response().User(ctx.User).Text(smartText).SendAsync();
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
await Response().Error(strs.unable_to_dm_user).SendAsync();
|
||||||
|
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 Response().Error(strs.user_not_found).SendAsync();
|
||||||
|
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 Response().Error(strs.user_not_found).SendAsync();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await UnbanInternal(bun.User);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task UnbanInternal(IUser user)
|
||||||
|
{
|
||||||
|
await ctx.Guild.RemoveBanAsync(user);
|
||||||
|
|
||||||
|
await Response().Confirm(strs.unbanned_user(Format.Bold(user.ToString()))).SendAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
[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 Response()
|
||||||
|
.Channel(await user.CreateDMChannelAsync())
|
||||||
|
.Error(strs.sbdm(Format.Bold(ctx.Guild.Name), msg))
|
||||||
|
.SendAsync();
|
||||||
|
}
|
||||||
|
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 = _sender.CreateEmbed()
|
||||||
|
.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 Response().Embed(toSend).SendAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
[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 Response()
|
||||||
|
.Channel(await user.CreateDMChannelAsync())
|
||||||
|
.Error(GetText(strs.kickdm(Format.Bold(ctx.Guild.Name), msg)))
|
||||||
|
.SendAsync();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
dmFailed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
await user.KickAsync((ctx.User + " | " + msg).TrimTo(512));
|
||||||
|
|
||||||
|
var toSend = _sender.CreateEmbed()
|
||||||
|
.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 Response().Embed(toSend).SendAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
[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 _sender.Response(user)
|
||||||
|
.Embed(_sender.CreateEmbed()
|
||||||
|
.WithPendingColor()
|
||||||
|
.WithDescription(dmMessage))
|
||||||
|
.SendAsync();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
dmFailed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
await user.SetTimeOutAsync(time.Time);
|
||||||
|
|
||||||
|
var toSend = _sender.CreateEmbed()
|
||||||
|
.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 Response().Embed(toSend).SendAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
[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 = _sender.CreateEmbed()
|
||||||
|
.WithDescription(GetText(strs.mass_ban_in_progress(banning.Count)))
|
||||||
|
.AddField(GetText(strs.invalid(missing.Count)), missStr)
|
||||||
|
.WithPendingColor();
|
||||||
|
|
||||||
|
var banningMessage = await Response().Embed(toSend).SendAsync();
|
||||||
|
|
||||||
|
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 = _sender.CreateEmbed()
|
||||||
|
.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 = Response()
|
||||||
|
.Embed(_sender.CreateEmbed()
|
||||||
|
.WithDescription(
|
||||||
|
GetText(strs.mass_kill_in_progress(bans.Count())))
|
||||||
|
.AddField(GetText(strs.invalid(missing)), missStr)
|
||||||
|
.WithPendingColor())
|
||||||
|
.SendAsync();
|
||||||
|
|
||||||
|
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 = _sender.CreateEmbed()
|
||||||
|
.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()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,629 @@
|
||||||
|
#nullable disable
|
||||||
|
using LinqToDB;
|
||||||
|
using LinqToDB.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using EllieBot.Common.ModuleBehaviors;
|
||||||
|
using EllieBot.Common.TypeReaders.Models;
|
||||||
|
using EllieBot.Modules.Permissions.Services;
|
||||||
|
using EllieBot.Db.Models;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace EllieBot.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;
|
||||||
|
private readonly IReplacementService _repSvc;
|
||||||
|
|
||||||
|
public event Func<Warning, Task> OnUserWarned = static delegate { return Task.CompletedTask; };
|
||||||
|
|
||||||
|
public UserPunishService(
|
||||||
|
MuteService mute,
|
||||||
|
DbService db,
|
||||||
|
BlacklistService blacklistService,
|
||||||
|
BotConfigService bcs,
|
||||||
|
DiscordSocketClient client,
|
||||||
|
IReplacementService repSvc)
|
||||||
|
{
|
||||||
|
_mute = mute;
|
||||||
|
_db = db;
|
||||||
|
_blacklistService = blacklistService;
|
||||||
|
_bcs = bcs;
|
||||||
|
_client = client;
|
||||||
|
_repSvc = repSvc;
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(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 toClear = await uow.GetTable<Warning>()
|
||||||
|
.Where(x => uow.GetTable<GuildConfig>()
|
||||||
|
.Count(y => y.GuildId == x.GuildId
|
||||||
|
&& y.WarnExpireHours > 0
|
||||||
|
&& y.WarnExpireAction == WarnExpireAction.Clear)
|
||||||
|
> 0
|
||||||
|
&& x.Forgiven == false
|
||||||
|
&& x.DateAdded
|
||||||
|
< DateTime.UtcNow.AddHours(-uow.GetTable<GuildConfig>()
|
||||||
|
.Where(y => x.GuildId == y.GuildId)
|
||||||
|
.Select(y => y.WarnExpireHours)
|
||||||
|
.First()))
|
||||||
|
.Select(x => x.Id)
|
||||||
|
.ToListAsyncLinqToDB();
|
||||||
|
|
||||||
|
var cleared = await uow.GetTable<Warning>()
|
||||||
|
.Where(x => toClear.Contains(x.Id))
|
||||||
|
.UpdateAsync(_ => new()
|
||||||
|
{
|
||||||
|
Forgiven = true,
|
||||||
|
ForgivenBy = "expiry"
|
||||||
|
});
|
||||||
|
|
||||||
|
var toDelete = await uow.GetTable<Warning>()
|
||||||
|
.Where(x => uow.GetTable<GuildConfig>()
|
||||||
|
.Count(y => y.GuildId == x.GuildId
|
||||||
|
&& y.WarnExpireHours > 0
|
||||||
|
&& y.WarnExpireAction == WarnExpireAction.Delete)
|
||||||
|
> 0
|
||||||
|
&& x.DateAdded
|
||||||
|
< DateTime.UtcNow.AddHours(-uow.GetTable<GuildConfig>()
|
||||||
|
.Where(y => x.GuildId == y.GuildId)
|
||||||
|
.Select(y => y.WarnExpireHours)
|
||||||
|
.First()))
|
||||||
|
.Select(x => x.Id)
|
||||||
|
.ToListAsyncLinqToDB();
|
||||||
|
|
||||||
|
var deleted = await uow.GetTable<Warning>()
|
||||||
|
.Where(x => toDelete.Contains(x.Id))
|
||||||
|
.DeleteAsync();
|
||||||
|
|
||||||
|
if (cleared > 0 || deleted > 0)
|
||||||
|
{
|
||||||
|
Log.Information("Cleared {ClearedWarnings} warnings and deleted {DeletedWarnings} warnings due to expiry",
|
||||||
|
cleared,
|
||||||
|
toDelete.Count);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 Task<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 async Task<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 repCtx = new ReplacementContext(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");
|
||||||
|
|
||||||
|
|
||||||
|
// 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 await _repSvc.ReplaceAsync(output, repCtx);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Warning> WarnDelete(ulong userId, int index)
|
||||||
|
{
|
||||||
|
await using var uow = _db.GetDbContext();
|
||||||
|
|
||||||
|
var warn = await uow.GetTable<Warning>()
|
||||||
|
.Where(x => x.UserId == userId)
|
||||||
|
.OrderByDescending(x => x.DateAdded)
|
||||||
|
.Skip(index)
|
||||||
|
.FirstOrDefaultAsyncLinqToDB();
|
||||||
|
|
||||||
|
if (warn is not null)
|
||||||
|
{
|
||||||
|
await uow.GetTable<Warning>()
|
||||||
|
.Where(x => x.Id == warn.Id)
|
||||||
|
.DeleteAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
return warn;
|
||||||
|
}
|
||||||
|
}
|
77
src/EllieBot/Modules/Administration/VcRole/VcRoleCommands.cs
Normal file
77
src/EllieBot/Modules/Administration/VcRole/VcRoleCommands.cs
Normal file
|
@ -0,0 +1,77 @@
|
||||||
|
#nullable disable
|
||||||
|
using EllieBot.Modules.Administration.Services;
|
||||||
|
|
||||||
|
namespace EllieBot.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 Response().Confirm(strs.vcrole_removed(Format.Bold(vcId.ToString()))).SendAsync();
|
||||||
|
else
|
||||||
|
await Response().Error(strs.vcrole_not_found).SendAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
[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 Response().Error(strs.must_be_in_voice).SendAsync();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (role is null)
|
||||||
|
{
|
||||||
|
if (_service.RemoveVcRole(ctx.Guild.Id, vc.Id))
|
||||||
|
await Response().Confirm(strs.vcrole_removed(Format.Bold(vc.Name))).SendAsync();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_service.AddVcRole(ctx.Guild.Id, role, vc.Id);
|
||||||
|
await Response().Confirm(strs.vcrole_added(Format.Bold(vc.Name), Format.Bold(role.Name))).SendAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[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 Response().Embed(_sender.CreateEmbed()
|
||||||
|
.WithOkColor()
|
||||||
|
.WithTitle(GetText(strs.vc_role_list))
|
||||||
|
.WithDescription(text)).SendAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
207
src/EllieBot/Modules/Administration/VcRole/VcRoleService.cs
Normal file
207
src/EllieBot/Modules/Administration/VcRole/VcRoleService.cs
Normal file
|
@ -0,0 +1,207 @@
|
||||||
|
#nullable disable
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using EllieBot.Db.Models;
|
||||||
|
|
||||||
|
namespace EllieBot.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)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(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));
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
namespace EllieBot.Modules.Administration._common.results;
|
||||||
|
|
||||||
|
public enum SetServerBannerResult
|
||||||
|
{
|
||||||
|
Success,
|
||||||
|
InvalidFileType,
|
||||||
|
Toolarge,
|
||||||
|
InvalidURL
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
namespace EllieBot.Modules.Administration._common.results;
|
||||||
|
|
||||||
|
public enum SetServerIconResult
|
||||||
|
{
|
||||||
|
Success,
|
||||||
|
InvalidFileType,
|
||||||
|
InvalidURL
|
||||||
|
}
|
Loading…
Reference in a new issue