More restructuring

This commit is contained in:
Kwoth
2023-03-26 14:44:25 +02:00
parent 01f70f0a24
commit 308ba36b2e
319 changed files with 681 additions and 218 deletions

View File

@@ -1,51 +0,0 @@
#nullable disable
using NadekoBot.Modules.Administration.Services;
namespace NadekoBot.Common.TypeReaders;
public sealed class GuildDateTimeTypeReader : NadekoTypeReader<GuildDateTime>
{
private readonly GuildTimezoneService _gts;
public GuildDateTimeTypeReader(GuildTimezoneService gts)
=> _gts = gts;
public override ValueTask<TypeReaderResult<GuildDateTime>> ReadAsync(ICommandContext context, string input)
{
var gdt = Parse(context.Guild.Id, input);
if (gdt is null)
{
return new(TypeReaderResult.FromError<GuildDateTime>(CommandError.ParseFailed,
"Input string is in an incorrect format."));
}
return new(TypeReaderResult.FromSuccess(gdt));
}
private GuildDateTime Parse(ulong guildId, string input)
{
if (!DateTime.TryParse(input, out var dt))
return null;
var tz = _gts.GetTimeZoneOrUtc(guildId);
return new(tz, dt);
}
}
public class GuildDateTime
{
public TimeZoneInfo Timezone { get; }
public DateTime CurrentGuildTime { get; }
public DateTime InputTime { get; }
public DateTime InputTimeUtc { get; }
public GuildDateTime(TimeZoneInfo guildTimezone, DateTime inputTime)
{
var now = DateTime.UtcNow;
Timezone = guildTimezone;
CurrentGuildTime = TimeZoneInfo.ConvertTime(now, TimeZoneInfo.Utc, Timezone);
InputTime = inputTime;
InputTimeUtc = TimeZoneInfo.ConvertTime(inputTime, Timezone, TimeZoneInfo.Utc);
}
}

View File

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

View File

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

View File

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

View File

@@ -1,160 +0,0 @@
#nullable disable
using LinqToDB;
using Microsoft.EntityFrameworkCore;
using NadekoBot.Db;
using NadekoBot.Services.Database.Models;
using System.Net;
using System.Threading.Channels;
using Nadeko.Common;
namespace NadekoBot.Modules.Administration.Services;
public sealed class AutoAssignRoleService : INService
{
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, Bot 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.GuildConfigs.AsNoTracking()
.Where(x => x.GuildId == guildId)
.UpdateAsync(_ => new()
{
AutoAssignRoleIds = null
});
_autoAssignableRoles.TryRemove(guildId, out _);
await uow.SaveChangesAsync();
}
public async Task SetAarRolesAsync(ulong guildId, IEnumerable<ulong> newRoles)
{
await using var uow = _db.GetDbContext();
var gc = uow.GuildConfigsForId(guildId, set => set);
gc.SetAutoAssignableRoles(newRoles);
await uow.SaveChangesAsync();
}
public bool TryGetRoles(ulong guildId, out IReadOnlyList<ulong> roles)
=> _autoAssignableRoles.TryGetValue(guildId, out roles);
}
public static class GuildConfigExtensions
{
public static List<ulong> GetAutoAssignableRoles(this GuildConfig gc)
{
if (string.IsNullOrWhiteSpace(gc.AutoAssignRoleIds))
return new();
return gc.AutoAssignRoleIds.Split(',').Select(ulong.Parse).ToList();
}
public static void SetAutoAssignableRoles(this GuildConfig gc, IEnumerable<ulong> roles)
=> gc.AutoAssignRoleIds = roles.Join(',');
}

View File

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

View File

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

View File

@@ -1,172 +0,0 @@
#nullable disable
using LinqToDB;
using LinqToDB.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using NadekoBot.Db.Models;
using NadekoBot.Services.Database.Models;
namespace NadekoBot.Modules.Administration.Services;
public class DangerousCommandsService : INService
{
private readonly DbService _db;
public DangerousCommandsService(DbService db)
=> _db = db;
public async Task DeleteXp()
{
await using var ctx = _db.GetDbContext();
await ctx.DiscordUser.UpdateAsync(_ => new DiscordUser()
{
ClubId = null,
// IsClubAdmin = false,
TotalXp = 0
});
await ctx.UserXpStats.DeleteAsync();
await ctx.ClubApplicants.DeleteAsync();
await ctx.ClubBans.DeleteAsync();
await ctx.Clubs.DeleteAsync();
await ctx.SaveChangesAsync();
}
public async Task DeleteWaifus()
{
await using var ctx = _db.GetDbContext();
await ctx.WaifuUpdates.DeleteAsync();
await ctx.WaifuItem.DeleteAsync();
await ctx.WaifuInfo.DeleteAsync();
await ctx.SaveChangesAsync();
}
public async Task DeleteWaifu(ulong userId)
{
await using var ctx = _db.GetDbContext();
await ctx.WaifuUpdates
.Where(x => x.User.UserId == userId)
.DeleteAsync();
await ctx.WaifuItem
.Where(x => x.WaifuInfo.Waifu.UserId == userId)
.DeleteAsync();
await ctx.WaifuInfo
.Where(x => x.Claimer.UserId == userId)
.UpdateAsync(old => new WaifuInfo()
{
ClaimerId = null,
});
await ctx.WaifuInfo
.Where(x => x.Waifu.UserId == userId)
.DeleteAsync();
await ctx.SaveChangesAsync();
}
public async Task DeletePlaylists()
{
await using var ctx = _db.GetDbContext();
await ctx.MusicPlaylists.DeleteAsync();
await ctx.SaveChangesAsync();
}
public async Task DeleteCurrency()
{
await using var ctx = _db.GetDbContext();
await ctx.DiscordUser.UpdateAsync(_ => new DiscordUser()
{
CurrencyAmount = 0
});
await ctx.CurrencyTransactions.DeleteAsync();
await ctx.PlantedCurrency.DeleteAsync();
await ctx.BankUsers.DeleteAsync();
await ctx.SaveChangesAsync();
}
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.WaifuUpdates.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.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.DiscordUser.DeleteAsync(u => u.UserId == userId);
}
public class SelectResult
{
public List<string> ColumnNames { get; set; }
public List<string[]> Results { get; set; }
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,235 +0,0 @@
#nullable disable
using LinqToDB;
using Microsoft.Extensions.Caching.Memory;
using NadekoBot.Common.ModuleBehaviors;
using System.Net;
using System.Threading.Channels;
using NadekoBot.Services.Database.Models;
namespace NadekoBot.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.ImageOnlyChannels
.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.ImageOnlyChannels
.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.ImageOnlyChannels.DeleteAsync(x => x.ChannelId == channelId && x.Type == OnlyChannelType.Image);
}
else
{
await uow.ImageOnlyChannels.DeleteAsync(x => x.ChannelId == channelId);
uow.ImageOnlyChannels.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.ImageOnlyChannels.DeleteAsync(x => x.ChannelId == channelId && x.Type == OnlyChannelType.Link);
}
else
{
await uow.ImageOnlyChannels.DeleteAsync(x => x.ChannelId == channelId);
uow.ImageOnlyChannels.Add(new()
{
GuildId = guildId,
ChannelId = channelId,
Type = OnlyChannelType.Link
});
if (_imageOnly.TryGetValue(guildId, out var chs))
chs.TryRemove(channelId);
channels = _linkOnly.GetOrAdd(guildId, new ConcurrentHashSet<ulong>());
channels.Add(channelId);
newState = true;
}
await uow.SaveChangesAsync();
return newState;
}
public async Task<bool> ExecOnMessageAsync(IGuild guild, IUserMessage msg)
{
if (msg.Channel is not ITextChannel tch)
return false;
if (_imageOnly.TryGetValue(tch.GuildId, out var chs) && chs.Contains(msg.Channel.Id))
return await HandleOnlyChannel(tch, msg, OnlyChannelType.Image);
if (_linkOnly.TryGetValue(tch.GuildId, out chs) && chs.Contains(msg.Channel.Id))
return await HandleOnlyChannel(tch, msg, OnlyChannelType.Link);
return false;
}
private async Task<bool> HandleOnlyChannel(ITextChannel tch, IUserMessage msg, OnlyChannelType type)
{
if (type == OnlyChannelType.Image)
{
if (msg.Attachments.Any(x => x is { Height: > 0, Width: > 0 }))
return false;
}
else
{
if (msg.Content.TryGetUrlPath(out _))
return false;
}
var user = await tch.Guild.GetUserAsync(msg.Author.Id)
?? await _client.Rest.GetGuildUserAsync(tch.GuildId, msg.Author.Id);
if (user is null)
return false;
// ignore owner and admin
if (user.Id == tch.Guild.OwnerId || user.GuildPermissions.Administrator)
{
Log.Information("{Type}-Only Channel: Ignoring owner od admin ({ChannelId})", type, msg.Channel.Id);
return false;
}
// ignore users higher in hierarchy
var botUser = await tch.Guild.GetCurrentUserAsync();
if (user.GetRoles().Max(x => x.Position) >= botUser.GetRoles().Max(x => x.Position))
return false;
if (!botUser.GetPermissions(tch).ManageChannel)
{
if(type == OnlyChannelType.Image)
await ToggleImageOnlyChannelAsync(tch.GuildId, tch.Id, true);
else
await ToggleImageOnlyChannelAsync(tch.GuildId, tch.Id, true);
return false;
}
var shouldLock = AddUserTicket(tch.GuildId, msg.Author.Id);
if (shouldLock)
{
await tch.AddPermissionOverwriteAsync(msg.Author, new(sendMessages: PermValue.Deny));
Log.Warning("{Type}-Only Channel: User {User} [{UserId}] has been banned from typing in the channel [{ChannelId}]",
type,
msg.Author,
msg.Author.Id,
msg.Channel.Id);
}
try
{
await _deleteQueue.Writer.WriteAsync(msg);
}
catch (Exception ex)
{
Log.Error(ex, "Error deleting message {MessageId} in image-only channel {ChannelId}", msg.Id, tch.Id);
}
return true;
}
private bool AddUserTicket(ulong guildId, ulong userId)
{
var old = _ticketCache.GetOrCreate($"{guildId}_{userId}",
entry =>
{
entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromDays(1);
return 0;
});
_ticketCache.Set($"{guildId}_{userId}", ++old);
// if this is the third time that the user posts a
// non image in an image-only channel on this server
return old > 2;
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,136 +0,0 @@
#nullable disable
using Microsoft.EntityFrameworkCore;
using Nadeko.Common;
using NadekoBot.Common.ModuleBehaviors;
using NadekoBot.Services.Database.Models;
namespace NadekoBot.Modules.Administration.Services;
public class DiscordPermOverrideService : INService, IExecPreCommand, IDiscordPermOverrideService
{
public int Priority { get; } = int.MaxValue;
private readonly DbService _db;
private readonly IServiceProvider _services;
private readonly ConcurrentDictionary<(ulong, string), DiscordPermOverride> _overrides;
public DiscordPermOverrideService(DbService db, IServiceProvider services)
{
_db = db;
_services = services;
using var uow = _db.GetDbContext();
_overrides = uow.DiscordPermOverrides.AsNoTracking()
.AsEnumerable()
.ToDictionary(o => (o.GuildId ?? 0, o.Command), o => o)
.ToConcurrent();
}
public bool TryGetOverrides(ulong guildId, string commandName, out Nadeko.Bot.Db.GuildPerm? perm)
{
commandName = commandName.ToLowerInvariant();
if (_overrides.TryGetValue((guildId, commandName), out var dpo))
{
perm = dpo.Perm;
return true;
}
perm = null;
return false;
}
public Task<PreconditionResult> ExecuteOverrides(
ICommandContext ctx,
CommandInfo command,
GuildPerm perms,
IServiceProvider services)
{
var rupa = new RequireUserPermissionAttribute(perms);
return rupa.CheckPermissionsAsync(ctx, command, services);
}
public async Task AddOverride(ulong guildId, string commandName, GuildPerm perm)
{
commandName = commandName.ToLowerInvariant();
await using var uow = _db.GetDbContext();
var over = await uow.Set<DiscordPermOverride>()
.AsQueryable()
.FirstOrDefaultAsync(x => x.GuildId == guildId && commandName == x.Command);
if (over is null)
{
uow.Set<DiscordPermOverride>()
.Add(over = new()
{
Command = commandName,
Perm = (Nadeko.Bot.Db.GuildPerm)perm,
GuildId = guildId
});
}
else
over.Perm = (Nadeko.Bot.Db.GuildPerm)perm;
_overrides[(guildId, commandName)] = over;
await uow.SaveChangesAsync();
}
public async Task ClearAllOverrides(ulong guildId)
{
await using var uow = _db.GetDbContext();
var overrides = await uow.Set<DiscordPermOverride>()
.AsQueryable()
.AsNoTracking()
.Where(x => x.GuildId == guildId)
.ToListAsync();
uow.RemoveRange(overrides);
await uow.SaveChangesAsync();
foreach (var over in overrides)
_overrides.TryRemove((guildId, over.Command), out _);
}
public async Task RemoveOverride(ulong guildId, string commandName)
{
commandName = commandName.ToLowerInvariant();
await using var uow = _db.GetDbContext();
var over = await uow.Set<DiscordPermOverride>()
.AsQueryable()
.AsNoTracking()
.FirstOrDefaultAsync(x => x.GuildId == guildId && x.Command == commandName);
if (over is null)
return;
uow.Remove(over);
await uow.SaveChangesAsync();
_overrides.TryRemove((guildId, commandName), out _);
}
public async Task<List<DiscordPermOverride>> GetAllOverrides(ulong guildId)
{
await using var uow = _db.GetDbContext();
return await uow.Set<DiscordPermOverride>()
.AsQueryable()
.AsNoTracking()
.Where(x => x.GuildId == guildId)
.ToListAsync();
}
public async Task<bool> ExecPreCommandAsync(ICommandContext context, string moduleName, CommandInfo command)
{
if (TryGetOverrides(context.Guild?.Id ?? 0, command.Name, out var perm) && perm is not null)
{
var result =
await new RequireUserPermissionAttribute((GuildPermission)perm).CheckPermissionsAsync(context,
command,
_services);
return !result.IsSuccess;
}
return false;
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,114 +0,0 @@
#nullable disable
using CommandLine;
using NadekoBot.Modules.Administration.Services;
namespace NadekoBot.Modules.Administration;
public partial class Administration
{
[Group]
public partial class PruneCommands : NadekoModule<PruneService>
{
private static readonly TimeSpan _twoWeeks = TimeSpan.FromDays(14);
public sealed class PruneOptions : INadekoCommandOptions
{
[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)]
[NadekoOptions<PruneOptions>]
public async Task Prune(params string[] args)
{
var (opts, _) = OptionsParser.ParseFrom(new PruneOptions(), args);
var user = await ctx.Guild.GetCurrentUserAsync();
if (opts.Safe)
await _service.PruneWhere((ITextChannel)ctx.Channel, 100, x => x.Author.Id == user.Id && !x.IsPinned, opts.After);
else
await _service.PruneWhere((ITextChannel)ctx.Channel, 100, x => x.Author.Id == user.Id, opts.After);
ctx.Message.DeleteAfter(3);
}
// prune x
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(ChannelPerm.ManageMessages)]
[BotPerm(ChannelPerm.ManageMessages)]
[NadekoOptions<PruneOptions>]
[Priority(1)]
public async Task Prune(int count, params string[] args)
{
count++;
if (count < 1)
return;
if (count > 1000)
count = 1000;
var (opts, _) = OptionsParser.ParseFrom<PruneOptions>(new PruneOptions(), args);
if (opts.Safe)
await _service.PruneWhere((ITextChannel)ctx.Channel, count, x => !x.IsPinned, opts.After);
else
await _service.PruneWhere((ITextChannel)ctx.Channel, count, _ => true, opts.After);
}
//prune @user [x]
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(ChannelPerm.ManageMessages)]
[BotPerm(ChannelPerm.ManageMessages)]
[NadekoOptions<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)]
[NadekoOptions<PruneOptions>]
[Priority(0)]
public async Task Prune(ulong userId, int count = 100, params string[] args)
{
if (userId == ctx.User.Id)
count++;
if (count < 1)
return;
if (count > 1000)
count = 1000;
var (opts, _) = OptionsParser.ParseFrom<PruneOptions>(new PruneOptions(), args);
if (opts.Safe)
{
await _service.PruneWhere((ITextChannel)ctx.Channel,
count,
m => m.Author.Id == userId && DateTime.UtcNow - m.CreatedAt < _twoWeeks && !m.IsPinned,
opts.After);
}
else
{
await _service.PruneWhere((ITextChannel)ctx.Channel,
count,
m => m.Author.Id == userId && DateTime.UtcNow - m.CreatedAt < _twoWeeks,
opts.After);
}
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,235 +0,0 @@
#nullable disable
using Microsoft.EntityFrameworkCore;
using NadekoBot.Db;
using NadekoBot.Modules.Xp;
using NadekoBot.Services.Database.Models;
namespace NadekoBot.Modules.Administration.Services;
public class SelfAssignedRolesService : INService
{
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.SelfAssignableRoles.GetFromGuild(guildId);
if (roles.Any(s => s.RoleId == role.Id && s.GuildId == role.Guild.Id))
return false;
uow.SelfAssignableRoles.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.SelfAssignableRoles.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.SelfAssignableRoles.GetFromGuild(guildId);
return (autoDelete, exclusive, roles);
}
public bool SetLevelReq(ulong guildId, IRole role, int level)
{
using var uow = _db.GetDbContext();
var roles = uow.SelfAssignableRoles.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.SelfAssignableRoles.GetFromGuild(guild.Id);
roles = roleModels.Select(x => (Model: x, Role: guild.GetRole(x.RoleId)))
.ToList();
uow.SelfAssignableRoles.RemoveRange(roles.Where(x => x.Role is null).Select(x => x.Model).ToArray());
uow.SaveChanges();
}
return (exclusive, roles.Where(x => x.Role is not null).ToList(), groupNames);
}
}

View File

@@ -1,25 +0,0 @@
using NadekoBot.Services.Database.Models;
namespace NadekoBot.Modules.Administration;
public sealed class DummyLogCommandService : ILogCommandService
#if GLOBAL_NADEKO
, INService
#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;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,13 +0,0 @@
#nullable disable
namespace NadekoBot.Modules.Help;
internal class CommandJsonObject
{
public string[] Aliases { get; set; }
public string Description { get; set; }
public string[] Usage { get; set; }
public string Submodule { get; set; }
public string Module { get; set; }
public List<string> Options { get; set; }
public string[] Requirements { get; set; }
}

View File

@@ -1,26 +0,0 @@
#nullable disable
using CommandLine;
namespace NadekoBot.Modules.Help.Common;
public class CommandsOptions : INadekoCommandOptions
{
public enum ViewType
{
Hide,
Cross,
All
}
[Option('v',
"view",
Required = false,
Default = ViewType.Hide,
HelpText =
"Specifies how to output the list of commands. 0 - Hide commands which you can't use, 1 - Cross out commands which you can't use, 2 - Show all.")]
public ViewType View { get; set; } = ViewType.Hide;
public void NormalizeOptions()
{
}
}

View File

@@ -1,565 +0,0 @@
#nullable disable
using Amazon.S3;
using Nadeko.Medusa;
using NadekoBot.Modules.Help.Common;
using NadekoBot.Modules.Help.Services;
using NadekoBot.Modules.Permissions.Services;
using Newtonsoft.Json;
using System.Text;
using System.Text.Json;
using Nadeko.Common;
using JsonSerializer = System.Text.Json.JsonSerializer;
namespace NadekoBot.Modules.Help;
public partial class Help : NadekoModule<HelpService>
{
public const string PATREON_URL = "https://patreon.com/nadekobot";
public const string PAYPAL_URL = "https://paypal.me/Kwoth";
private readonly CommandService _cmds;
private readonly BotConfigService _bss;
private readonly GlobalPermissionService _perms;
private readonly IServiceProvider _services;
private readonly DiscordSocketClient _client;
private readonly IBotStrings _strings;
private readonly AsyncLazy<ulong> _lazyClientId;
private readonly IMedusaLoaderService _medusae;
public Help(
GlobalPermissionService perms,
CommandService cmds,
BotConfigService bss,
IServiceProvider services,
DiscordSocketClient client,
IBotStrings strings,
IMedusaLoaderService medusae)
{
_cmds = cmds;
_bss = bss;
_perms = perms;
_services = services;
_client = client;
_strings = strings;
_medusae = medusae;
_lazyClientId = new(async () => (await _client.GetApplicationInfoAsync()).Id);
}
public async Task<SmartText> GetHelpString()
{
var botSettings = _bss.Data;
if (string.IsNullOrWhiteSpace(botSettings.HelpText) || botSettings.HelpText == "-")
return default;
var clientId = await _lazyClientId.Value;
var r = new ReplacementBuilder().WithDefault(Context)
.WithOverride("{0}", () => clientId.ToString())
.WithOverride("{1}", () => prefix)
.WithOverride("%prefix%", () => prefix)
.WithOverride("%bot.prefix%", () => prefix)
.Build();
var text = SmartText.CreateFrom(botSettings.HelpText);
return r.Replace(text);
}
[Cmd]
public async Task Modules(int page = 1)
{
if (--page < 0)
return;
var topLevelModules = _cmds.Modules.GroupBy(m => m.GetTopLevelModule())
.Where(m => !_perms.BlockedModules.Contains(m.Key.Name.ToLowerInvariant()))
.Select(x => x.Key)
.ToList();
await ctx.SendPaginatedConfirmAsync(page,
cur =>
{
var embed = _eb.Create().WithOkColor().WithTitle(GetText(strs.list_of_modules));
var localModules = topLevelModules.Skip(12 * cur).Take(12).ToList();
if (!localModules.Any())
{
embed = embed.WithOkColor().WithDescription(GetText(strs.module_page_empty));
return embed;
}
localModules.OrderBy(module => module.Name)
.ToList()
.ForEach(module => embed.AddField($"{GetModuleEmoji(module.Name)} {module.Name}",
GetModuleDescription(module.Name)
+ "\n"
+ Format.Code(GetText(strs.module_footer(prefix, module.Name.ToLowerInvariant()))),
true));
return embed;
},
topLevelModules.Count(),
12,
false);
}
private string GetModuleDescription(string moduleName)
{
var key = GetModuleLocStr(moduleName);
if (key.Key == strs.module_description_missing.Key)
{
var desc = _medusae
.GetLoadedMedusae(Culture)
.FirstOrDefault(m => m.Sneks
.Any(x => x.Name.Equals(moduleName,
StringComparison.InvariantCultureIgnoreCase)))
?.Description;
if (desc is not null)
return desc;
}
return GetText(key);
}
private LocStr GetModuleLocStr(string moduleName)
{
switch (moduleName.ToLowerInvariant())
{
case "help":
return strs.module_description_help;
case "administration":
return strs.module_description_administration;
case "expressions":
return strs.module_description_expressions;
case "searches":
return strs.module_description_searches;
case "utility":
return strs.module_description_utility;
case "games":
return strs.module_description_games;
case "gambling":
return strs.module_description_gambling;
case "music":
return strs.module_description_music;
case "nsfw":
return strs.module_description_nsfw;
case "permissions":
return strs.module_description_permissions;
case "xp":
return strs.module_description_xp;
case "medusa":
return strs.module_description_medusa;
default:
return strs.module_description_missing;
}
}
private string GetModuleEmoji(string moduleName)
{
moduleName = moduleName.ToLowerInvariant();
switch (moduleName)
{
case "help":
return "❓";
case "administration":
return "🛠️";
case "expressions":
return "🗣️";
case "searches":
return "🔍";
case "utility":
return "🔧";
case "games":
return "🎲";
case "gambling":
return "💰";
case "music":
return "🎶";
case "nsfw":
return "😳";
case "permissions":
return "🚓";
case "xp":
return "📝";
default:
return "📖";
}
}
[Cmd]
[NadekoOptions<CommandsOptions>]
public async Task Commands(string module = null, params string[] args)
{
module = module?.Trim().ToUpperInvariant();
if (string.IsNullOrWhiteSpace(module))
{
await Modules();
return;
}
var (opts, _) = OptionsParser.ParseFrom(new CommandsOptions(), args);
// Find commands for that module
// don't show commands which are blocked
// order by name
var cmds = _cmds.Commands
.Where(c => c.Module.GetTopLevelModule()
.Name.ToUpperInvariant()
.StartsWith(module, StringComparison.InvariantCulture))
.Where(c => !_perms.BlockedCommands.Contains(c.Aliases[0].ToLowerInvariant()))
.OrderBy(c => c.Aliases[0])
.DistinctBy(x => x.Aliases[0])
.ToList();
// check preconditions for all commands, but only if it's not 'all'
// because all will show all commands anyway, no need to check
var succ = new HashSet<CommandInfo>();
if (opts.View != CommandsOptions.ViewType.All)
{
succ = new((await cmds.Select(async x =>
{
var pre = await x.CheckPreconditionsAsync(Context, _services);
return (Cmd: x, Succ: pre.IsSuccess);
})
.WhenAll()).Where(x => x.Succ)
.Select(x => x.Cmd));
if (opts.View == CommandsOptions.ViewType.Hide)
// if hidden is specified, completely remove these commands from the list
cmds = cmds.Where(x => succ.Contains(x)).ToList();
}
var cmdsWithGroup = cmds.GroupBy(c => c.Module.GetGroupName())
.OrderBy(x => x.Key == x.First().Module.Name ? int.MaxValue : x.Count())
.ToList();
if (cmdsWithGroup.Count == 0)
{
if (opts.View != CommandsOptions.ViewType.Hide)
await ReplyErrorLocalizedAsync(strs.module_not_found);
else
await ReplyErrorLocalizedAsync(strs.module_not_found_or_cant_exec);
return;
}
var cnt = 0;
var groups = cmdsWithGroup.GroupBy(_ => cnt++ / 48).ToArray();
var embed = _eb.Create().WithOkColor();
foreach (var g in groups)
{
var last = g.Count();
for (var i = 0; i < last; i++)
{
var transformed = g.ElementAt(i)
.Select(x =>
{
//if cross is specified, and the command doesn't satisfy the requirements, cross it out
if (opts.View == CommandsOptions.ViewType.Cross)
{
return
$"{(succ.Contains(x) ? "" : "")}{prefix + x.Aliases.First(),-15} {"[" + x.Aliases.Skip(1).FirstOrDefault() + "]",-8}";
}
return
$"{prefix + x.Aliases.First(),-15} {"[" + x.Aliases.Skip(1).FirstOrDefault() + "]",-8}";
});
if (i == last - 1 && (i + 1) % 2 != 0)
{
transformed = transformed.Chunk(2)
.Select(x =>
{
if (x.Count() == 1)
return $"{x.First()}";
return string.Concat(x);
});
}
embed.AddField(g.ElementAt(i).Key, "```css\n" + string.Join("\n", transformed) + "\n```", true);
}
}
embed.WithFooter(GetText(strs.commands_instr(prefix)));
await ctx.Channel.EmbedAsync(embed);
}
private async Task Group(ModuleInfo group)
{
var eb = _eb.Create(ctx)
.WithTitle(GetText(strs.cmd_group_commands(group.Name)))
.WithOkColor();
foreach (var cmd in group.Commands)
{
eb.AddField(prefix + cmd.Aliases.First(), cmd.RealSummary(_strings, _medusae, Culture, prefix));
}
await ctx.Channel.EmbedAsync(eb);
}
[Cmd]
[Priority(0)]
public async Task H([Leftover] string fail)
{
var prefixless =
_cmds.Commands.FirstOrDefault(x => x.Aliases.Any(cmdName => cmdName.ToLowerInvariant() == fail));
if (prefixless is not null)
{
await H(prefixless);
return;
}
if (fail.StartsWith(prefix))
fail = fail.Substring(prefix.Length);
var group = _cmds.Modules
.SelectMany(x => x.Submodules)
.Where(x => !string.IsNullOrWhiteSpace(x.Group))
.FirstOrDefault(x => x.Group.Equals(fail, StringComparison.InvariantCultureIgnoreCase));
if (group is not null)
{
await Group(group);
return;
}
await ReplyErrorLocalizedAsync(strs.command_not_found);
}
[Cmd]
[Priority(1)]
public async Task H([Leftover] CommandInfo com = null)
{
var channel = ctx.Channel;
if (com is null)
{
var ch = channel is ITextChannel ? await ctx.User.CreateDMChannelAsync() : channel;
try
{
var data = await GetHelpString();
if (data == default)
return;
await ch.SendAsync(data);
try { await ctx.OkAsync(); }
catch { } // ignore if bot can't react
}
catch (Exception)
{
await ReplyErrorLocalizedAsync(strs.cant_dm);
}
return;
}
var embed = _service.GetCommandHelp(com, ctx.Guild);
await channel.EmbedAsync(embed);
}
[Cmd]
[OwnerOnly]
public async Task GenCmdList()
{
_ = ctx.Channel.TriggerTypingAsync();
// order commands by top level module name
// and make a dictionary of <ModuleName, Array<JsonCommandData>>
var cmdData = _cmds.Commands.GroupBy(x => x.Module.GetTopLevelModule().Name)
.OrderBy(x => x.Key)
.ToDictionary(x => x.Key,
x => x.DistinctBy(c => c.Aliases.First())
.Select(com =>
{
List<string> optHelpStr = null;
var opt = HelpService.GetNadekoOptionType(com.Attributes);
if (opt is not null)
optHelpStr = HelpService.GetCommandOptionHelpList(opt);
return new CommandJsonObject
{
Aliases = com.Aliases.Select(alias => prefix + alias).ToArray(),
Description = com.RealSummary(_strings, _medusae, Culture, prefix),
Usage = com.RealRemarksArr(_strings, _medusae, Culture, prefix),
Submodule = com.Module.Name,
Module = com.Module.GetTopLevelModule().Name,
Options = optHelpStr,
Requirements = HelpService.GetCommandRequirements(com)
};
})
.ToList());
var readableData = JsonConvert.SerializeObject(cmdData, Formatting.Indented);
var uploadData = JsonConvert.SerializeObject(cmdData, Formatting.None);
// for example https://nyc.digitaloceanspaces.com (without your space name)
var serviceUrl = Environment.GetEnvironmentVariable("do_spaces_address");
// generate spaces access key on https://cloud.digitalocean.com/account/api/tokens
// you will get 2 keys, first, shorter one is id, longer one is secret
var accessKey = Environment.GetEnvironmentVariable("do_access_key_id");
var secretAcccessKey = Environment.GetEnvironmentVariable("do_access_key_secret");
// if all env vars are set, upload the unindented file (to save space) there
if (!(serviceUrl is null || accessKey is null || secretAcccessKey is null))
{
var config = new AmazonS3Config
{
ServiceURL = serviceUrl
};
using var dlClient = new AmazonS3Client(accessKey, secretAcccessKey, config);
using (var client = new AmazonS3Client(accessKey, secretAcccessKey, config))
{
await client.PutObjectAsync(new()
{
BucketName = "nadeko-pictures",
ContentType = "application/json",
ContentBody = uploadData,
// either use a path provided in the argument or the default one for public nadeko, other/cmds.json
Key = $"cmds/{StatsService.BOT_VERSION}.json",
CannedACL = S3CannedACL.PublicRead
});
}
var versionListString = "[]";
try
{
using var oldVersionObject = await dlClient.GetObjectAsync(new()
{
BucketName = "nadeko-pictures",
Key = "cmds/versions.json"
});
await using var ms = new MemoryStream();
await oldVersionObject.ResponseStream.CopyToAsync(ms);
versionListString = Encoding.UTF8.GetString(ms.ToArray());
}
catch (Exception)
{
Log.Information("No old version list found. Creating a new one");
}
var versionList = JsonSerializer.Deserialize<List<string>>(versionListString);
if (versionList is not null && !versionList.Contains(StatsService.BOT_VERSION))
{
// save the file with new version added
// versionList.Add(StatsService.BotVersion);
versionListString = JsonSerializer.Serialize(versionList.Prepend(StatsService.BOT_VERSION),
new JsonSerializerOptions
{
WriteIndented = true
});
// upload the updated version list
using var client = new AmazonS3Client(accessKey, secretAcccessKey, config);
await client.PutObjectAsync(new()
{
BucketName = "nadeko-pictures",
ContentType = "application/json",
ContentBody = versionListString,
// either use a path provided in the argument or the default one for public nadeko, other/cmds.json
Key = "cmds/versions.json",
CannedACL = S3CannedACL.PublicRead
});
}
else
{
Log.Warning(
"Version {Version} already exists in the version file. " + "Did you forget to increment it?",
StatsService.BOT_VERSION);
}
}
// also send the file, but indented one, to chat
await using var rDataStream = new MemoryStream(Encoding.ASCII.GetBytes(readableData));
await ctx.Channel.SendFileAsync(rDataStream, "cmds.json", GetText(strs.commandlist_regen));
}
[Cmd]
public async Task Guide()
=> await ConfirmLocalizedAsync(strs.guide("https://nadeko.bot/commands",
"https://nadekobot.readthedocs.io/en/latest/"));
private Task SelfhostAction(SocketMessageComponent smc, object _)
=> smc.RespondConfirmAsync(_eb,
"""
- In case you don't want or cannot Donate to NadekoBot project, but you
- NadekoBot is a completely free and fully [open source](https://gitlab.com/kwoth/nadekobot) project which means you can run your own "selfhosted" instance on your computer or server for free.
*Keep in mind that running the bot on your computer means that the bot will be offline when you turn off your computer*
- You can find the selfhosting guides by using the `.guide` command and clicking on the second link that pops up.
- If you decide to selfhost the bot, still consider [supporting the project](https://patreon.com/join/nadekobot) to keep the development going :)
""",
true);
[Cmd]
[OnlyPublicBot]
public async Task Donate()
{
// => new NadekoInteractionData(new Emoji("🖥️"), "donate:selfhosting", "Selfhosting");
var selfhostInter = _inter.Create(ctx.User.Id,
new SimpleInteraction<object>(new ButtonBuilder(
emote: new Emoji("🖥️"),
customId: "donate:selfhosting",
label: "Selfhosting"),
SelfhostAction));
var eb = _eb.Create(ctx)
.WithOkColor()
.WithTitle("Thank you for considering to donate to the NadekoBot project!");
eb
.WithDescription("NadekoBot relies on donations to keep the servers, services and APIs running.\n"
+ "Donating will give you access to some exclusive features. You can read about them on the [patreon page](https://patreon.com/join/nadekobot)")
.AddField("Donation Instructions",
$@"
🗒️ Before pledging it is recommended to open your DMs as Nadeko will send you a welcome message with instructions after you pledge has been processed and confirmed.
**Step 1:** ❤️ Pledge on Patreon ❤️
`1.` Go to <https://patreon.com/join/nadekobot> and choose a tier.
`2.` Make sure your payment is processed and accepted.
**Step 2** 🤝 Connect your Discord account 🤝
`1.` Go to your profile settings on Patreon and connect your Discord account to it.
*please make sure you're logged into the correct Discord account*
If you do not know how to do it, you may follow instructions in this link:
<https://support.patreon.com/hc/en-us/articles/212052266-How-do-I-connect-Discord-to-Patreon-Patron->
**Step 3** ⏰ Wait a short while (usually 1-3 minutes) ⏰
Nadeko will DM you the welcome instructions, and you may start using the patron-only commands and features!
🎉 **Enjoy!** 🎉
")
.AddField("Troubleshooting",
"""
*In case you didn't receive the rewards within 5 minutes:*
`1.` Make sure your DMs are open to everyone. Maybe your pledge was processed successfully but the bot was unable to DM you. Use the `.patron` command to check your status.
`2.` Make sure you've connected the CORRECT Discord account. Quite often users log in to different Discord accounts in their browser. You may also try disconnecting and reconnecting your account.
`3.` Make sure your payment has been processed and not declined by Patreon.
`4.` If any of the previous steps don't help, you can join the nadeko support server <https://discord.nadeko.bot> and ask for help in the #help channel
""");
try
{
await (await ctx.User.CreateDMChannelAsync()).EmbedAsync(eb, inter: selfhostInter);
_ = ctx.OkAsync();
}
catch
{
await ReplyErrorLocalizedAsync(strs.cant_dm);
}
}
}

View File

@@ -1,203 +0,0 @@
#nullable disable
using CommandLine;
using Nadeko.Common;
using Nadeko.Medusa;
using NadekoBot.Common.ModuleBehaviors;
using NadekoBot.Modules.Administration.Services;
namespace NadekoBot.Modules.Help.Services;
public class HelpService : IExecNoCommand, INService
{
private readonly CommandHandler _ch;
private readonly IBotStrings _strings;
private readonly DiscordPermOverrideService _dpos;
private readonly BotConfigService _bss;
private readonly IEmbedBuilderService _eb;
private readonly ILocalization _loc;
private readonly IMedusaLoaderService _medusae;
public HelpService(
CommandHandler ch,
IBotStrings strings,
DiscordPermOverrideService dpos,
BotConfigService bss,
IEmbedBuilderService eb,
ILocalization loc,
IMedusaLoaderService medusae)
{
_ch = ch;
_strings = strings;
_dpos = dpos;
_bss = bss;
_eb = eb;
_loc = loc;
_medusae = medusae;
}
public Task ExecOnNoCommandAsync(IGuild guild, IUserMessage msg)
{
var settings = _bss.Data;
if (guild is null)
{
if (string.IsNullOrWhiteSpace(settings.DmHelpText) || settings.DmHelpText == "-")
return Task.CompletedTask;
// only send dm help text if it contains one of the keywords, if they're specified
// if they're not, then reply to every DM
if (settings.DmHelpTextKeywords is not null && !settings.DmHelpTextKeywords.Any(k => msg.Content.Contains(k)))
return Task.CompletedTask;
var rep = new ReplacementBuilder().WithOverride("%prefix%", () => _bss.Data.Prefix)
.WithOverride("%bot.prefix%", () => _bss.Data.Prefix)
.WithUser(msg.Author)
.Build();
var text = SmartText.CreateFrom(settings.DmHelpText);
text = rep.Replace(text);
return msg.Channel.SendAsync(text);
}
return Task.CompletedTask;
}
public IEmbedBuilder GetCommandHelp(CommandInfo com, IGuild guild)
{
var prefix = _ch.GetPrefix(guild);
var str = $"**`{prefix + com.Aliases.First()}`**";
var alias = com.Aliases.Skip(1).FirstOrDefault();
if (alias is not null)
str += $" **/ `{prefix + alias}`**";
var culture = _loc.GetCultureInfo(guild);
var em = _eb.Create()
.AddField(str, $"{com.RealSummary(_strings, _medusae, culture, prefix)}", true);
_dpos.TryGetOverrides(guild?.Id ?? 0, com.Name, out var overrides);
var reqs = GetCommandRequirements(com, (GuildPermission?)overrides);
if (reqs.Any())
em.AddField(GetText(strs.requires, guild), string.Join("\n", reqs));
em.AddField(_strings.GetText(strs.usage),
string.Join("\n", com.RealRemarksArr(_strings,_medusae, culture, prefix).Map(arg => Format.Code(arg))))
.WithFooter(GetText(strs.module(com.Module.GetTopLevelModule().Name), guild))
.WithOkColor();
var opt = GetNadekoOptionType(com.Attributes);
if (opt is not null)
{
var hs = GetCommandOptionHelp(opt);
if (!string.IsNullOrWhiteSpace(hs))
em.AddField(GetText(strs.options, guild), hs);
}
return em;
}
public static Type GetNadekoOptionType(IEnumerable<Attribute> attributes)
=> attributes
.Select(a => a.GetType())
.Where(a => a.IsGenericType
&& a.GetGenericTypeDefinition() == typeof(NadekoOptionsAttribute<>))
.Select(a => a.GenericTypeArguments[0])
.FirstOrDefault();
public static string GetCommandOptionHelp(Type opt)
{
var strs = GetCommandOptionHelpList(opt);
return string.Join("\n", strs);
}
public static List<string> GetCommandOptionHelpList(Type opt)
{
var strs = opt.GetProperties()
.Select(x => x.GetCustomAttributes(true).FirstOrDefault(a => a is OptionAttribute))
.Where(x => x is not null)
.Cast<OptionAttribute>()
.Select(x =>
{
var toReturn = $"`--{x.LongName}`";
if (!string.IsNullOrWhiteSpace(x.ShortName))
toReturn += $" (`-{x.ShortName}`)";
toReturn += $" {x.HelpText} ";
return toReturn;
})
.ToList();
return strs;
}
public static string[] GetCommandRequirements(CommandInfo cmd, GuildPerm? overrides = null)
{
var toReturn = new List<string>();
if (cmd.Preconditions.Any(x => x is OwnerOnlyAttribute))
toReturn.Add("Bot Owner Only");
if(cmd.Preconditions.Any(x => x is NoPublicBotAttribute)
|| cmd.Module
.Preconditions
.Any(x => x is NoPublicBotAttribute)
|| cmd.Module.GetTopLevelModule()
.Preconditions
.Any(x => x is NoPublicBotAttribute))
toReturn.Add("No Public Bot");
if (cmd.Preconditions
.Any(x => x is OnlyPublicBotAttribute)
|| cmd.Module
.Preconditions
.Any(x => x is OnlyPublicBotAttribute)
|| cmd.Module.GetTopLevelModule()
.Preconditions
.Any(x => x is OnlyPublicBotAttribute))
toReturn.Add("Only Public Bot");
var userPermString = cmd.Preconditions
.Where(ca => ca is UserPermAttribute)
.Cast<UserPermAttribute>()
.Select(userPerm =>
{
if (userPerm.ChannelPermission is { } cPerm)
return GetPreconditionString(cPerm);
if (userPerm.GuildPermission is { } gPerm)
return GetPreconditionString(gPerm);
return string.Empty;
})
.Where(x => !string.IsNullOrWhiteSpace(x))
.Join('\n');
if (overrides is null)
{
if (!string.IsNullOrWhiteSpace(userPermString))
toReturn.Add(userPermString);
}
else
{
if (!string.IsNullOrWhiteSpace(userPermString))
toReturn.Add(Format.Strikethrough(userPermString));
toReturn.Add(GetPreconditionString(overrides.Value));
}
return toReturn.ToArray();
}
public static string GetPreconditionString(ChannelPerm perm)
=> (perm + " Channel Permission").Replace("Guild", "Server");
public static string GetPreconditionString(GuildPerm perm)
=> (perm + " Server Permission").Replace("Guild", "Server");
private string GetText(LocStr str, IGuild guild)
=> _strings.GetText(str, guild?.Id);
}

View File

@@ -1,758 +0,0 @@
#nullable disable
using NadekoBot.Modules.Music.Services;
using NadekoBot.Services.Database.Models;
namespace NadekoBot.Modules.Music;
[NoPublicBot]
public sealed partial class Music : NadekoModule<IMusicService>
{
public enum All { All = -1 }
public enum InputRepeatType
{
N = 0, No = 0, None = 0,
T = 1, Track = 1, S = 1, Song = 1,
Q = 2, Queue = 2, Playlist = 2, Pl = 2
}
public const string MUSIC_ICON_URL = "http://i.imgur.com/nhKS3PT.png";
private const int LQ_ITEMS_PER_PAGE = 9;
private static readonly SemaphoreSlim _voiceChannelLock = new(1, 1);
private readonly ILogCommandService _logService;
public Music(ILogCommandService logService)
=> _logService = logService;
private async Task<bool> ValidateAsync()
{
var user = (IGuildUser)ctx.User;
var userVoiceChannelId = user.VoiceChannel?.Id;
if (userVoiceChannelId is null)
{
await ReplyErrorLocalizedAsync(strs.must_be_in_voice);
return false;
}
var currentUser = await ctx.Guild.GetCurrentUserAsync();
if (currentUser.VoiceChannel?.Id != userVoiceChannelId)
{
await ReplyErrorLocalizedAsync(strs.not_with_bot_in_voice);
return false;
}
return true;
}
private async Task EnsureBotInVoiceChannelAsync(ulong voiceChannelId, IGuildUser botUser = null)
{
botUser ??= await ctx.Guild.GetCurrentUserAsync();
await _voiceChannelLock.WaitAsync();
try
{
if (botUser.VoiceChannel?.Id is null || !_service.TryGetMusicPlayer(ctx.Guild.Id, out _))
await _service.JoinVoiceChannelAsync(ctx.Guild.Id, voiceChannelId);
}
finally
{
_voiceChannelLock.Release();
}
}
private async Task<bool> QueuePreconditionInternalAsync()
{
var user = (IGuildUser)ctx.User;
var voiceChannelId = user.VoiceChannel?.Id;
if (voiceChannelId is null)
{
await ReplyErrorLocalizedAsync(strs.must_be_in_voice);
return false;
}
_ = ctx.Channel.TriggerTypingAsync();
var botUser = await ctx.Guild.GetCurrentUserAsync();
await EnsureBotInVoiceChannelAsync(voiceChannelId!.Value, botUser);
if (botUser.VoiceChannel?.Id != voiceChannelId)
{
await ReplyErrorLocalizedAsync(strs.not_with_bot_in_voice);
return false;
}
return true;
}
private async Task QueueByQuery(string query, bool asNext = false, MusicPlatform? forcePlatform = null)
{
var succ = await QueuePreconditionInternalAsync();
if (!succ)
return;
var mp = await _service.GetOrCreateMusicPlayerAsync((ITextChannel)ctx.Channel);
if (mp is null)
{
await ReplyErrorLocalizedAsync(strs.no_player);
return;
}
var (trackInfo, index) = await mp.TryEnqueueTrackAsync(query, ctx.User.ToString(), asNext, forcePlatform);
if (trackInfo is null)
{
await ReplyErrorLocalizedAsync(strs.track_not_found);
return;
}
try
{
var embed = _eb.Create()
.WithOkColor()
.WithAuthor(GetText(strs.queued_track) + " #" + (index + 1), MUSIC_ICON_URL)
.WithDescription($"{trackInfo.PrettyName()}\n{GetText(strs.queue)} ")
.WithFooter(trackInfo.Platform.ToString());
if (!string.IsNullOrWhiteSpace(trackInfo.Thumbnail))
embed.WithThumbnailUrl(trackInfo.Thumbnail);
var queuedMessage = await _service.SendToOutputAsync(ctx.Guild.Id, embed);
queuedMessage?.DeleteAfter(10, _logService);
if (mp.IsStopped)
{
var msg = await ReplyPendingLocalizedAsync(strs.queue_stopped(Format.Code(prefix + "play")));
msg.DeleteAfter(10, _logService);
}
}
catch
{
// ignored
}
}
private async Task MoveToIndex(int index)
{
if (--index < 0)
return;
var succ = await QueuePreconditionInternalAsync();
if (!succ)
return;
var mp = await _service.GetOrCreateMusicPlayerAsync((ITextChannel)ctx.Channel);
if (mp is null)
{
await ReplyErrorLocalizedAsync(strs.no_player);
return;
}
mp.MoveTo(index);
}
// join vc
[Cmd]
[RequireContext(ContextType.Guild)]
public async Task Join()
{
var user = (IGuildUser)ctx.User;
var voiceChannelId = user.VoiceChannel?.Id;
if (voiceChannelId is null)
{
await ReplyErrorLocalizedAsync(strs.must_be_in_voice);
return;
}
await _service.JoinVoiceChannelAsync(user.GuildId, voiceChannelId.Value);
}
// leave vc (destroy)
[Cmd]
[RequireContext(ContextType.Guild)]
public async Task Destroy()
{
var valid = await ValidateAsync();
if (!valid)
return;
await _service.LeaveVoiceChannelAsync(ctx.Guild.Id);
}
// play - no args = next
[Cmd]
[RequireContext(ContextType.Guild)]
[Priority(2)]
public Task Play()
=> Next();
// play - index = skip to that index
[Cmd]
[RequireContext(ContextType.Guild)]
[Priority(1)]
public Task Play(int index)
=> MoveToIndex(index);
// play - query = q(query)
[Cmd]
[RequireContext(ContextType.Guild)]
[Priority(0)]
public Task Play([Leftover] string query)
=> QueueByQuery(query);
[Cmd]
[RequireContext(ContextType.Guild)]
public Task Queue([Leftover] string query)
=> QueueByQuery(query);
[Cmd]
[RequireContext(ContextType.Guild)]
public Task QueueNext([Leftover] string query)
=> QueueByQuery(query, true);
[Cmd]
[RequireContext(ContextType.Guild)]
public async Task Volume(int vol)
{
if (vol is < 0 or > 100)
{
await ReplyErrorLocalizedAsync(strs.volume_input_invalid);
return;
}
var valid = await ValidateAsync();
if (!valid)
return;
await _service.SetVolumeAsync(ctx.Guild.Id, vol);
await ReplyConfirmLocalizedAsync(strs.volume_set(vol));
}
[Cmd]
[RequireContext(ContextType.Guild)]
public async Task Next()
{
var valid = await ValidateAsync();
if (!valid)
return;
var success = await _service.PlayAsync(ctx.Guild.Id, ((IGuildUser)ctx.User).VoiceChannel.Id);
if (!success)
await ReplyErrorLocalizedAsync(strs.no_player);
}
// list queue, relevant page
[Cmd]
[RequireContext(ContextType.Guild)]
public async Task ListQueue()
{
// show page with the current track
if (!_service.TryGetMusicPlayer(ctx.Guild.Id, out var mp))
{
await ReplyErrorLocalizedAsync(strs.no_player);
return;
}
await ListQueue((mp.CurrentIndex / LQ_ITEMS_PER_PAGE) + 1);
}
// list queue, specify page
[Cmd]
[RequireContext(ContextType.Guild)]
public async Task ListQueue(int page)
{
if (--page < 0)
return;
IReadOnlyCollection<IQueuedTrackInfo> tracks;
if (!_service.TryGetMusicPlayer(ctx.Guild.Id, out var mp) || (tracks = mp.GetQueuedTracks()).Count == 0)
{
await ReplyErrorLocalizedAsync(strs.no_player);
return;
}
IEmbedBuilder PrintAction(int curPage)
{
var desc = string.Empty;
var current = mp.GetCurrentTrack(out var currentIndex);
if (current is not null)
desc = $"`🔊` {current.PrettyFullName()}\n\n" + desc;
var repeatType = mp.Repeat;
var add = string.Empty;
if (mp.IsStopped)
add += Format.Bold(GetText(strs.queue_stopped(Format.Code(prefix + "play")))) + "\n";
// var mps = mp.MaxPlaytimeSeconds;
// if (mps > 0)
// add += Format.Bold(GetText(strs.song_skips_after(TimeSpan.FromSeconds(mps).ToString("HH\\:mm\\:ss")))) + "\n";
if (repeatType == PlayerRepeatType.Track)
add += "🔂 " + GetText(strs.repeating_track) + "\n";
else
{
if (mp.AutoPlay)
add += "↪ " + GetText(strs.autoplaying) + "\n";
// if (mp.FairPlay && !mp.Autoplay)
// add += " " + GetText(strs.fairplay) + "\n";
if (repeatType == PlayerRepeatType.Queue)
add += "🔁 " + GetText(strs.repeating_queue) + "\n";
}
desc += tracks.Skip(LQ_ITEMS_PER_PAGE * curPage)
.Take(LQ_ITEMS_PER_PAGE)
.Select((v, index) =>
{
index += LQ_ITEMS_PER_PAGE * curPage;
if (index == currentIndex)
return $"**⇒**`{index + 1}.` {v.PrettyFullName()}";
return $"`{index + 1}.` {v.PrettyFullName()}";
})
.Join('\n');
if (!string.IsNullOrWhiteSpace(add))
desc = add + "\n" + desc;
var embed = _eb.Create()
.WithAuthor(GetText(strs.player_queue(curPage + 1, (tracks.Count / LQ_ITEMS_PER_PAGE) + 1)),
MUSIC_ICON_URL)
.WithDescription(desc)
.WithFooter($" {mp.PrettyVolume()} | 🎶 {tracks.Count} | ⌛ {mp.PrettyTotalTime()} ")
.WithOkColor();
return embed;
}
await ctx.SendPaginatedConfirmAsync(page, PrintAction, tracks.Count, LQ_ITEMS_PER_PAGE, false);
}
// search
[Cmd]
[RequireContext(ContextType.Guild)]
public async Task QueueSearch([Leftover] string query)
{
_ = ctx.Channel.TriggerTypingAsync();
var videos = await _service.SearchVideosAsync(query);
if (videos.Count == 0)
{
await ReplyErrorLocalizedAsync(strs.track_not_found);
return;
}
var resultsString = videos.Select((x, i) => $"`{i + 1}.`\n\t{Format.Bold(x.Title)}\n\t{x.Url}").Join('\n');
var msg = await SendConfirmAsync(resultsString);
try
{
var input = await GetUserInputAsync(ctx.User.Id, ctx.Channel.Id);
if (input is null || !int.TryParse(input, out var index) || (index -= 1) < 0 || index >= videos.Count)
{
_logService.AddDeleteIgnore(msg.Id);
try
{
await msg.DeleteAsync();
}
catch
{
}
return;
}
query = videos[index].Url;
await Play(query);
}
finally
{
_logService.AddDeleteIgnore(msg.Id);
try
{
await msg.DeleteAsync();
}
catch
{
}
}
}
[Cmd]
[RequireContext(ContextType.Guild)]
[Priority(1)]
public async Task TrackRemove(int index)
{
if (index < 1)
{
await ReplyErrorLocalizedAsync(strs.removed_track_error);
return;
}
var valid = await ValidateAsync();
if (!valid)
return;
if (!_service.TryGetMusicPlayer(ctx.Guild.Id, out var mp))
{
await ReplyErrorLocalizedAsync(strs.no_player);
return;
}
if (!mp.TryRemoveTrackAt(index - 1, out var track))
{
await ReplyErrorLocalizedAsync(strs.removed_track_error);
return;
}
var embed = _eb.Create()
.WithAuthor(GetText(strs.removed_track) + " #" + index, MUSIC_ICON_URL)
.WithDescription(track.PrettyName())
.WithFooter(track.PrettyInfo())
.WithErrorColor();
await _service.SendToOutputAsync(ctx.Guild.Id, embed);
}
[Cmd]
[RequireContext(ContextType.Guild)]
[Priority(0)]
public async Task TrackRemove(All _ = All.All)
{
var valid = await ValidateAsync();
if (!valid)
return;
if (!_service.TryGetMusicPlayer(ctx.Guild.Id, out var mp))
{
await ReplyErrorLocalizedAsync(strs.no_player);
return;
}
mp.Clear();
await ReplyConfirmLocalizedAsync(strs.queue_cleared);
}
[Cmd]
[RequireContext(ContextType.Guild)]
public async Task Stop()
{
var valid = await ValidateAsync();
if (!valid)
return;
if (!_service.TryGetMusicPlayer(ctx.Guild.Id, out var mp))
{
await ReplyErrorLocalizedAsync(strs.no_player);
return;
}
mp.Stop();
}
private PlayerRepeatType InputToDbType(InputRepeatType type)
=> type switch
{
InputRepeatType.None => PlayerRepeatType.None,
InputRepeatType.Queue => PlayerRepeatType.Queue,
InputRepeatType.Track => PlayerRepeatType.Track,
_ => PlayerRepeatType.Queue
};
[Cmd]
[RequireContext(ContextType.Guild)]
public async Task QueueRepeat(InputRepeatType type = InputRepeatType.Queue)
{
var valid = await ValidateAsync();
if (!valid)
return;
await _service.SetRepeatAsync(ctx.Guild.Id, InputToDbType(type));
if (type == InputRepeatType.None)
await ReplyConfirmLocalizedAsync(strs.repeating_none);
else if (type == InputRepeatType.Queue)
await ReplyConfirmLocalizedAsync(strs.repeating_queue);
else
await ReplyConfirmLocalizedAsync(strs.repeating_track);
}
[Cmd]
[RequireContext(ContextType.Guild)]
public async Task Pause()
{
var valid = await ValidateAsync();
if (!valid)
return;
if (!_service.TryGetMusicPlayer(ctx.Guild.Id, out var mp) || mp.GetCurrentTrack(out _) is null)
{
await ReplyErrorLocalizedAsync(strs.no_player);
return;
}
mp.TogglePause();
}
[Cmd]
[RequireContext(ContextType.Guild)]
public Task Radio(string radioLink)
=> QueueByQuery(radioLink, false, MusicPlatform.Radio);
[Cmd]
[RequireContext(ContextType.Guild)]
[OwnerOnly]
public Task Local([Leftover] string path)
=> QueueByQuery(path, false, MusicPlatform.Local);
[Cmd]
[RequireContext(ContextType.Guild)]
[OwnerOnly]
public async Task LocalPlaylist([Leftover] string dirPath)
{
if (string.IsNullOrWhiteSpace(dirPath))
return;
var user = (IGuildUser)ctx.User;
var voiceChannelId = user.VoiceChannel?.Id;
if (voiceChannelId is null)
{
await ReplyErrorLocalizedAsync(strs.must_be_in_voice);
return;
}
_ = ctx.Channel.TriggerTypingAsync();
var botUser = await ctx.Guild.GetCurrentUserAsync();
await EnsureBotInVoiceChannelAsync(voiceChannelId!.Value, botUser);
if (botUser.VoiceChannel?.Id != voiceChannelId)
{
await ReplyErrorLocalizedAsync(strs.not_with_bot_in_voice);
return;
}
var mp = await _service.GetOrCreateMusicPlayerAsync((ITextChannel)ctx.Channel);
if (mp is null)
{
await ReplyErrorLocalizedAsync(strs.no_player);
return;
}
await _service.EnqueueDirectoryAsync(mp, dirPath, ctx.User.ToString());
await ReplyConfirmLocalizedAsync(strs.dir_queue_complete);
}
[Cmd]
[RequireContext(ContextType.Guild)]
public async Task TrackMove(int from, int to)
{
if (--from < 0 || --to < 0 || from == to)
{
await ReplyErrorLocalizedAsync(strs.invalid_input);
return;
}
var valid = await ValidateAsync();
if (!valid)
return;
var mp = await _service.GetOrCreateMusicPlayerAsync((ITextChannel)ctx.Channel);
if (mp is null)
{
await ReplyErrorLocalizedAsync(strs.no_player);
return;
}
var track = mp.MoveTrack(from, to);
if (track is null)
{
await ReplyErrorLocalizedAsync(strs.invalid_input);
return;
}
var embed = _eb.Create()
.WithTitle(track.Title.TrimTo(65))
.WithAuthor(GetText(strs.track_moved), MUSIC_ICON_URL)
.AddField(GetText(strs.from_position), $"#{from + 1}", true)
.AddField(GetText(strs.to_position), $"#{to + 1}", true)
.WithOkColor();
if (Uri.IsWellFormedUriString(track.Url, UriKind.Absolute))
embed.WithUrl(track.Url);
await ctx.Channel.EmbedAsync(embed);
}
[Cmd]
[RequireContext(ContextType.Guild)]
public Task SoundCloudQueue([Leftover] string query)
=> QueueByQuery(query, false, MusicPlatform.SoundCloud);
[Cmd]
[RequireContext(ContextType.Guild)]
public async Task SoundCloudPl([Leftover] string playlist)
{
if (string.IsNullOrWhiteSpace(playlist))
return;
var succ = await QueuePreconditionInternalAsync();
if (!succ)
return;
var mp = await _service.GetOrCreateMusicPlayerAsync((ITextChannel)ctx.Channel);
if (mp is null)
{
await ReplyErrorLocalizedAsync(strs.no_player);
return;
}
_ = ctx.Channel.TriggerTypingAsync();
await _service.EnqueueSoundcloudPlaylistAsync(mp, playlist, ctx.User.ToString());
await ctx.OkAsync();
}
[Cmd]
[RequireContext(ContextType.Guild)]
public async Task Playlist([Leftover] string playlistQuery)
{
if (string.IsNullOrWhiteSpace(playlistQuery))
return;
var succ = await QueuePreconditionInternalAsync();
if (!succ)
return;
var mp = await _service.GetOrCreateMusicPlayerAsync((ITextChannel)ctx.Channel);
if (mp is null)
{
await ReplyErrorLocalizedAsync(strs.no_player);
return;
}
_ = ctx.Channel.TriggerTypingAsync();
var queuedCount = await _service.EnqueueYoutubePlaylistAsync(mp, playlistQuery, ctx.User.ToString());
if (queuedCount == 0)
{
await ReplyErrorLocalizedAsync(strs.no_search_results);
return;
}
await ctx.OkAsync();
}
[Cmd]
[RequireContext(ContextType.Guild)]
public async Task NowPlaying()
{
var mp = await _service.GetOrCreateMusicPlayerAsync((ITextChannel)ctx.Channel);
if (mp is null)
{
await ReplyErrorLocalizedAsync(strs.no_player);
return;
}
var currentTrack = mp.GetCurrentTrack(out _);
if (currentTrack is null)
return;
var embed = _eb.Create()
.WithOkColor()
.WithAuthor(GetText(strs.now_playing), MUSIC_ICON_URL)
.WithDescription(currentTrack.PrettyName())
.WithThumbnailUrl(currentTrack.Thumbnail)
.WithFooter(
$"{mp.PrettyVolume()} | {mp.PrettyTotalTime()} | {currentTrack.Platform} | {currentTrack.Queuer}");
await ctx.Channel.EmbedAsync(embed);
}
[Cmd]
[RequireContext(ContextType.Guild)]
public async Task PlaylistShuffle()
{
var valid = await ValidateAsync();
if (!valid)
return;
var mp = await _service.GetOrCreateMusicPlayerAsync((ITextChannel)ctx.Channel);
if (mp is null)
{
await ReplyErrorLocalizedAsync(strs.no_player);
return;
}
mp.ShuffleQueue();
await ReplyConfirmLocalizedAsync(strs.queue_shuffled);
}
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageMessages)]
public async Task SetMusicChannel()
{
await _service.SetMusicChannelAsync(ctx.Guild.Id, ctx.Channel.Id);
await ReplyConfirmLocalizedAsync(strs.set_music_channel);
}
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageMessages)]
public async Task UnsetMusicChannel()
{
await _service.SetMusicChannelAsync(ctx.Guild.Id, null);
await ReplyConfirmLocalizedAsync(strs.unset_music_channel);
}
[Cmd]
[RequireContext(ContextType.Guild)]
public async Task AutoDisconnect()
{
var newState = await _service.ToggleAutoDisconnectAsync(ctx.Guild.Id);
if (newState)
await ReplyConfirmLocalizedAsync(strs.autodc_enable);
else
await ReplyConfirmLocalizedAsync(strs.autodc_disable);
}
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.Administrator)]
public async Task MusicQuality()
{
var quality = await _service.GetMusicQualityAsync(ctx.Guild.Id);
await ReplyConfirmLocalizedAsync(strs.current_music_quality(Format.Bold(quality.ToString())));
}
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.Administrator)]
public async Task MusicQuality(QualityPreset preset)
{
await _service.SetMusicQualityAsync(ctx.Guild.Id, preset);
await ReplyConfirmLocalizedAsync(strs.music_quality_set(Format.Bold(preset.ToString())));
}
[Cmd]
[RequireContext(ContextType.Guild)]
public async Task QueueAutoPlay()
{
var newValue = await _service.ToggleQueueAutoPlayAsync(ctx.Guild.Id);
if (newValue)
await ReplyConfirmLocalizedAsync(strs.music_autoplay_on);
else
await ReplyConfirmLocalizedAsync(strs.music_autoplay_off);
}
}

View File

@@ -1,229 +0,0 @@
#nullable disable
using NadekoBot.Db;
using NadekoBot.Modules.Music.Services;
using NadekoBot.Services.Database.Models;
namespace NadekoBot.Modules.Music;
public sealed partial class Music
{
[Group]
public sealed partial class PlaylistCommands : NadekoModule<IMusicService>
{
private static readonly SemaphoreSlim _playlistLock = new(1, 1);
private readonly DbService _db;
private readonly IBotCredentials _creds;
public PlaylistCommands(DbService db, IBotCredentials creds)
{
_db = db;
_creds = creds;
}
private async Task EnsureBotInVoiceChannelAsync(ulong voiceChannelId, IGuildUser botUser = null)
{
botUser ??= await ctx.Guild.GetCurrentUserAsync();
await _voiceChannelLock.WaitAsync();
try
{
if (botUser.VoiceChannel?.Id is null || !_service.TryGetMusicPlayer(ctx.Guild.Id, out _))
await _service.JoinVoiceChannelAsync(ctx.Guild.Id, voiceChannelId);
}
finally
{
_voiceChannelLock.Release();
}
}
[Cmd]
[RequireContext(ContextType.Guild)]
public async Task Playlists([Leftover] int num = 1)
{
if (num <= 0)
return;
List<MusicPlaylist> playlists;
await using (var uow = _db.GetDbContext())
{
playlists = uow.MusicPlaylists.GetPlaylistsOnPage(num);
}
var embed = _eb.Create(ctx)
.WithAuthor(GetText(strs.playlists_page(num)), MUSIC_ICON_URL)
.WithDescription(string.Join("\n",
playlists.Select(r => GetText(strs.playlists(r.Id, r.Name, r.Author, r.Songs.Count)))))
.WithOkColor();
await ctx.Channel.EmbedAsync(embed);
}
[Cmd]
[RequireContext(ContextType.Guild)]
public async Task DeletePlaylist([Leftover] int id)
{
var success = false;
try
{
await using var uow = _db.GetDbContext();
var pl = uow.MusicPlaylists.FirstOrDefault(x => x.Id == id);
if (pl is not null)
{
if (_creds.IsOwner(ctx.User) || pl.AuthorId == ctx.User.Id)
{
uow.MusicPlaylists.Remove(pl);
await uow.SaveChangesAsync();
success = true;
}
}
}
catch (Exception ex)
{
Log.Warning(ex, "Error deleting playlist");
}
if (!success)
await ReplyErrorLocalizedAsync(strs.playlist_delete_fail);
else
await ReplyConfirmLocalizedAsync(strs.playlist_deleted);
}
[Cmd]
[RequireContext(ContextType.Guild)]
public async Task PlaylistShow(int id, int page = 1)
{
if (page-- < 1)
return;
MusicPlaylist mpl;
await using (var uow = _db.GetDbContext())
{
mpl = uow.MusicPlaylists.GetWithSongs(id);
}
await ctx.SendPaginatedConfirmAsync(page,
cur =>
{
var i = 0;
var str = string.Join("\n",
mpl.Songs.Skip(cur * 20)
.Take(20)
.Select(x => $"`{++i}.` [{x.Title.TrimTo(45)}]({x.Query}) `{x.Provider}`"));
return _eb.Create().WithTitle($"\"{mpl.Name}\" by {mpl.Author}").WithOkColor().WithDescription(str);
},
mpl.Songs.Count,
20);
}
[Cmd]
[RequireContext(ContextType.Guild)]
public async Task Save([Leftover] string name)
{
if (!_service.TryGetMusicPlayer(ctx.Guild.Id, out var mp))
{
await ReplyErrorLocalizedAsync(strs.no_player);
return;
}
var songs = mp.GetQueuedTracks()
.Select(s => new PlaylistSong
{
Provider = s.Platform.ToString(),
ProviderType = (MusicType)s.Platform,
Title = s.Title,
Query = s.Platform == MusicPlatform.Local ? s.GetStreamUrl().Result!.Trim('"') : s.Url
})
.ToList();
MusicPlaylist playlist;
await using (var uow = _db.GetDbContext())
{
playlist = new()
{
Name = name,
Author = ctx.User.Username,
AuthorId = ctx.User.Id,
Songs = songs.ToList()
};
uow.MusicPlaylists.Add(playlist);
await uow.SaveChangesAsync();
}
await ctx.Channel.EmbedAsync(_eb.Create()
.WithOkColor()
.WithTitle(GetText(strs.playlist_saved))
.AddField(GetText(strs.name), name)
.AddField(GetText(strs.id), playlist.Id.ToString()));
}
[Cmd]
[RequireContext(ContextType.Guild)]
public async Task Load([Leftover] int id)
{
// expensive action, 1 at a time
await _playlistLock.WaitAsync();
try
{
var user = (IGuildUser)ctx.User;
var voiceChannelId = user.VoiceChannel?.Id;
if (voiceChannelId is null)
{
await ReplyErrorLocalizedAsync(strs.must_be_in_voice);
return;
}
_ = ctx.Channel.TriggerTypingAsync();
var botUser = await ctx.Guild.GetCurrentUserAsync();
await EnsureBotInVoiceChannelAsync(voiceChannelId!.Value, botUser);
if (botUser.VoiceChannel?.Id != voiceChannelId)
{
await ReplyErrorLocalizedAsync(strs.not_with_bot_in_voice);
return;
}
var mp = await _service.GetOrCreateMusicPlayerAsync((ITextChannel)ctx.Channel);
if (mp is null)
{
await ReplyErrorLocalizedAsync(strs.no_player);
return;
}
MusicPlaylist mpl;
await using (var uow = _db.GetDbContext())
{
mpl = uow.MusicPlaylists.GetWithSongs(id);
}
if (mpl is null)
{
await ReplyErrorLocalizedAsync(strs.playlist_id_not_found);
return;
}
IUserMessage msg = null;
try
{
msg = await ctx.Channel.SendMessageAsync(
GetText(strs.attempting_to_queue(Format.Bold(mpl.Songs.Count.ToString()))));
}
catch (Exception)
{
}
await mp.EnqueueManyAsync(mpl.Songs.Select(x => (x.Query, (MusicPlatform)x.ProviderType)),
ctx.User.ToString());
if (msg is not null)
await msg.ModifyAsync(m => m.Content = GetText(strs.playlist_queue_complete));
}
finally
{
_playlistLock.Release();
}
}
}
}

View File

@@ -1,218 +0,0 @@
#nullable disable
using Ayu.Discord.Voice;
using System.Reflection;
namespace NadekoBot.Modules.Music.Services;
public sealed class AyuVoiceStateService : INService
{
// public delegate Task VoiceProxyUpdatedDelegate(ulong guildId, IVoiceProxy proxy);
// public event VoiceProxyUpdatedDelegate OnVoiceProxyUpdate = delegate { return Task.CompletedTask; };
private readonly ConcurrentDictionary<ulong, IVoiceProxy> _voiceProxies = new();
private readonly ConcurrentDictionary<ulong, SemaphoreSlim> _voiceGatewayLocks = new();
private readonly DiscordSocketClient _client;
private readonly MethodInfo _sendVoiceStateUpdateMethodInfo;
private readonly object _dnetApiClient;
private readonly ulong _currentUserId;
public AyuVoiceStateService(DiscordSocketClient client)
{
_client = client;
_currentUserId = _client.CurrentUser.Id;
var prop = _client.GetType()
.GetProperties(BindingFlags.NonPublic | BindingFlags.Instance)
.First(x => x.Name == "ApiClient" && x.PropertyType.Name == "DiscordSocketApiClient");
_dnetApiClient = prop.GetValue(_client, null);
_sendVoiceStateUpdateMethodInfo = _dnetApiClient.GetType()
.GetMethod("SendVoiceStateUpdateAsync",
new[]
{
typeof(ulong), typeof(ulong?), typeof(bool),
typeof(bool), typeof(RequestOptions)
});
_client.LeftGuild += ClientOnLeftGuild;
}
private Task ClientOnLeftGuild(SocketGuild guild)
{
if (_voiceProxies.TryRemove(guild.Id, out var proxy))
{
proxy.StopGateway();
proxy.SetGateway(null);
}
return Task.CompletedTask;
}
private Task InvokeSendVoiceStateUpdateAsync(
ulong guildId,
ulong? channelId = null,
bool isDeafened = false,
bool isMuted = false)
// return _voiceStateUpdate(guildId, channelId, isDeafened, isMuted);
=> (Task)_sendVoiceStateUpdateMethodInfo.Invoke(_dnetApiClient,
new object[] { guildId, channelId, isMuted, isDeafened, null });
private Task SendLeaveVoiceChannelInternalAsync(ulong guildId)
=> InvokeSendVoiceStateUpdateAsync(guildId);
private Task SendJoinVoiceChannelInternalAsync(ulong guildId, ulong channelId)
=> InvokeSendVoiceStateUpdateAsync(guildId, channelId);
private SemaphoreSlim GetVoiceGatewayLock(ulong guildId)
=> _voiceGatewayLocks.GetOrAdd(guildId, new SemaphoreSlim(1, 1));
private async Task LeaveVoiceChannelInternalAsync(ulong guildId)
{
var complete = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
Task OnUserVoiceStateUpdated(SocketUser user, SocketVoiceState oldState, SocketVoiceState newState)
{
if (user is SocketGuildUser guildUser && guildUser.Guild.Id == guildId && newState.VoiceChannel?.Id is null)
complete.TrySetResult(true);
return Task.CompletedTask;
}
try
{
_client.UserVoiceStateUpdated += OnUserVoiceStateUpdated;
if (_voiceProxies.TryGetValue(guildId, out var proxy))
{
_ = proxy.StopGateway();
proxy.SetGateway(null);
}
await SendLeaveVoiceChannelInternalAsync(guildId);
await Task.WhenAny(Task.Delay(1500), complete.Task);
}
finally
{
_client.UserVoiceStateUpdated -= OnUserVoiceStateUpdated;
}
}
public async Task LeaveVoiceChannel(ulong guildId)
{
var gwLock = GetVoiceGatewayLock(guildId);
await gwLock.WaitAsync();
try
{
await LeaveVoiceChannelInternalAsync(guildId);
}
finally
{
gwLock.Release();
}
}
private async Task<IVoiceProxy> InternalConnectToVcAsync(ulong guildId, ulong channelId)
{
var voiceStateUpdatedSource =
new TaskCompletionSource<string>(TaskCreationOptions.RunContinuationsAsynchronously);
var voiceServerUpdatedSource =
new TaskCompletionSource<SocketVoiceServer>(TaskCreationOptions.RunContinuationsAsynchronously);
Task OnUserVoiceStateUpdated(SocketUser user, SocketVoiceState oldState, SocketVoiceState newState)
{
if (user is SocketGuildUser guildUser && guildUser.Guild.Id == guildId)
{
if (newState.VoiceChannel?.Id == channelId)
voiceStateUpdatedSource.TrySetResult(newState.VoiceSessionId);
voiceStateUpdatedSource.TrySetResult(null);
}
return Task.CompletedTask;
}
Task OnVoiceServerUpdated(SocketVoiceServer data)
{
if (data.Guild.Id == guildId)
voiceServerUpdatedSource.TrySetResult(data);
return Task.CompletedTask;
}
try
{
_client.VoiceServerUpdated += OnVoiceServerUpdated;
_client.UserVoiceStateUpdated += OnUserVoiceStateUpdated;
await SendJoinVoiceChannelInternalAsync(guildId, channelId);
// create a delay task, how much to wait for gateway response
using var cts = new CancellationTokenSource();
var delayTask = Task.Delay(2500, cts.Token);
// either delay or successful voiceStateUpdate
var maybeUpdateTask = Task.WhenAny(delayTask, voiceStateUpdatedSource.Task);
// either delay or successful voiceServerUpdate
var maybeServerTask = Task.WhenAny(delayTask, voiceServerUpdatedSource.Task);
// wait for both to end (max 1s) and check if either of them is a delay task
var results = await Task.WhenAll(maybeUpdateTask, maybeServerTask);
if (results[0] == delayTask || results[1] == delayTask)
// if either is delay, return null - connection unsuccessful
return null;
else
cts.Cancel();
// if both are succesful, that means we can safely get
// the values from completion sources
var session = await voiceStateUpdatedSource.Task;
// session can be null. Means we disconnected, or connected to the wrong channel (?!)
if (session is null)
return null;
var voiceServerData = await voiceServerUpdatedSource.Task;
VoiceGateway CreateVoiceGatewayLocal()
{
return new(guildId, _currentUserId, session, voiceServerData.Token, voiceServerData.Endpoint);
}
var current = _voiceProxies.AddOrUpdate(guildId,
_ => new VoiceProxy(CreateVoiceGatewayLocal()),
(gid, currentProxy) =>
{
_ = currentProxy.StopGateway();
currentProxy.SetGateway(CreateVoiceGatewayLocal());
return currentProxy;
});
_ = current.StartGateway(); // don't await, this blocks until gateway is closed
return current;
}
finally
{
_client.VoiceServerUpdated -= OnVoiceServerUpdated;
_client.UserVoiceStateUpdated -= OnUserVoiceStateUpdated;
}
}
public async Task<IVoiceProxy> JoinVoiceChannel(ulong guildId, ulong channelId, bool forceReconnect = true)
{
var gwLock = GetVoiceGatewayLock(guildId);
await gwLock.WaitAsync();
try
{
await LeaveVoiceChannelInternalAsync(guildId);
return await InternalConnectToVcAsync(guildId, channelId);
}
finally
{
gwLock.Release();
}
}
public bool TryGetProxy(ulong guildId, out IVoiceProxy proxy)
=> _voiceProxies.TryGetValue(guildId, out proxy);
}

View File

@@ -1,36 +0,0 @@
using NadekoBot.Services.Database.Models;
using System.Diagnostics.CodeAnalysis;
namespace NadekoBot.Modules.Music.Services;
public interface IMusicService : IPlaceholderProvider
{
/// <summary>
/// Leave voice channel in the specified guild if it's connected to one
/// </summary>
/// <param name="guildId">Id of the guild</param>
public Task LeaveVoiceChannelAsync(ulong guildId);
/// <summary>
/// Joins the voice channel with the specified id
/// </summary>
/// <param name="guildId">Id of the guild where the voice channel is</param>
/// <param name="voiceChannelId">Id of the voice channel</param>
public Task JoinVoiceChannelAsync(ulong guildId, ulong voiceChannelId);
Task<IMusicPlayer?> GetOrCreateMusicPlayerAsync(ITextChannel contextChannel);
bool TryGetMusicPlayer(ulong guildId, [MaybeNullWhen(false)] out IMusicPlayer musicPlayer);
Task<int> EnqueueYoutubePlaylistAsync(IMusicPlayer mp, string playlistId, string queuer);
Task EnqueueDirectoryAsync(IMusicPlayer mp, string dirPath, string queuer);
Task<int> EnqueueSoundcloudPlaylistAsync(IMusicPlayer mp, string playlist, string queuer);
Task<IUserMessage?> SendToOutputAsync(ulong guildId, IEmbedBuilder embed);
Task<bool> PlayAsync(ulong guildId, ulong voiceChannelId);
Task<IList<(string Title, string Url)>> SearchVideosAsync(string query);
Task<bool> SetMusicChannelAsync(ulong guildId, ulong? channelId);
Task SetRepeatAsync(ulong guildId, PlayerRepeatType repeatType);
Task SetVolumeAsync(ulong guildId, int value);
Task<bool> ToggleAutoDisconnectAsync(ulong guildId);
Task<QualityPreset> GetMusicQualityAsync(ulong guildId);
Task SetMusicQualityAsync(ulong guildId, QualityPreset preset);
Task<bool> ToggleQueueAutoPlayAsync(ulong guildId);
}

View File

@@ -1,461 +0,0 @@
using NadekoBot.Db;
using NadekoBot.Services.Database.Models;
using System.Diagnostics.CodeAnalysis;
using Nadeko.Common;
namespace NadekoBot.Modules.Music.Services;
public sealed class MusicService : IMusicService
{
private readonly AyuVoiceStateService _voiceStateService;
private readonly ITrackResolveProvider _trackResolveProvider;
private readonly DbService _db;
private readonly IYoutubeResolver _ytResolver;
private readonly ILocalTrackResolver _localResolver;
private readonly ISoundcloudResolver _scResolver;
private readonly DiscordSocketClient _client;
private readonly IBotStrings _strings;
private readonly IGoogleApiService _googleApiService;
private readonly YtLoader _ytLoader;
private readonly IEmbedBuilderService _eb;
private readonly ConcurrentDictionary<ulong, IMusicPlayer> _players;
private readonly ConcurrentDictionary<ulong, (ITextChannel Default, ITextChannel? Override)> _outputChannels;
private readonly ConcurrentDictionary<ulong, MusicPlayerSettings> _settings;
public MusicService(
AyuVoiceStateService voiceStateService,
ITrackResolveProvider trackResolveProvider,
DbService db,
IYoutubeResolver ytResolver,
ILocalTrackResolver localResolver,
ISoundcloudResolver scResolver,
DiscordSocketClient client,
IBotStrings strings,
IGoogleApiService googleApiService,
YtLoader ytLoader,
IEmbedBuilderService eb)
{
_voiceStateService = voiceStateService;
_trackResolveProvider = trackResolveProvider;
_db = db;
_ytResolver = ytResolver;
_localResolver = localResolver;
_scResolver = scResolver;
_client = client;
_strings = strings;
_googleApiService = googleApiService;
_ytLoader = ytLoader;
_eb = eb;
_players = new();
_outputChannels = new ConcurrentDictionary<ulong, (ITextChannel, ITextChannel?)>();
_settings = new();
_client.LeftGuild += ClientOnLeftGuild;
}
private void DisposeMusicPlayer(IMusicPlayer musicPlayer)
{
musicPlayer.Kill();
_ = Task.Delay(10_000).ContinueWith(_ => musicPlayer.Dispose());
}
private void RemoveMusicPlayer(ulong guildId)
{
_outputChannels.TryRemove(guildId, out _);
if (_players.TryRemove(guildId, out var mp))
DisposeMusicPlayer(mp);
}
private Task ClientOnLeftGuild(SocketGuild guild)
{
RemoveMusicPlayer(guild.Id);
return Task.CompletedTask;
}
public async Task LeaveVoiceChannelAsync(ulong guildId)
{
RemoveMusicPlayer(guildId);
await _voiceStateService.LeaveVoiceChannel(guildId);
}
public Task JoinVoiceChannelAsync(ulong guildId, ulong voiceChannelId)
=> _voiceStateService.JoinVoiceChannel(guildId, voiceChannelId);
public async Task<IMusicPlayer?> GetOrCreateMusicPlayerAsync(ITextChannel contextChannel)
{
var newPLayer = await CreateMusicPlayerInternalAsync(contextChannel.GuildId, contextChannel);
if (newPLayer is null)
return null;
return _players.GetOrAdd(contextChannel.GuildId, newPLayer);
}
public bool TryGetMusicPlayer(ulong guildId, [MaybeNullWhen(false)] out IMusicPlayer musicPlayer)
=> _players.TryGetValue(guildId, out musicPlayer);
public async Task<int> EnqueueYoutubePlaylistAsync(IMusicPlayer mp, string query, string queuer)
{
var count = 0;
await foreach (var track in _ytResolver.ResolveTracksFromPlaylistAsync(query))
{
if (mp.IsKilled)
break;
mp.EnqueueTrack(track, queuer);
++count;
}
return count;
}
public async Task EnqueueDirectoryAsync(IMusicPlayer mp, string dirPath, string queuer)
{
await foreach (var track in _localResolver.ResolveDirectoryAsync(dirPath))
{
if (mp.IsKilled)
break;
mp.EnqueueTrack(track, queuer);
}
}
public async Task<int> EnqueueSoundcloudPlaylistAsync(IMusicPlayer mp, string playlist, string queuer)
{
var i = 0;
await foreach (var track in _scResolver.ResolvePlaylistAsync(playlist))
{
if (mp.IsKilled)
break;
mp.EnqueueTrack(track, queuer);
++i;
}
return i;
}
private async Task<IMusicPlayer?> CreateMusicPlayerInternalAsync(ulong guildId, ITextChannel defaultChannel)
{
var queue = new MusicQueue();
var resolver = _trackResolveProvider;
if (!_voiceStateService.TryGetProxy(guildId, out var proxy))
return null;
var settings = await GetSettingsInternalAsync(guildId);
ITextChannel? overrideChannel = null;
if (settings.MusicChannelId is { } channelId)
{
overrideChannel = _client.GetGuild(guildId)?.GetTextChannel(channelId);
if (overrideChannel is null)
Log.Warning("Saved music output channel doesn't exist, falling back to current channel");
}
_outputChannels[guildId] = (defaultChannel, overrideChannel);
var mp = new MusicPlayer(queue,
resolver,
proxy,
_googleApiService,
settings.QualityPreset,
settings.AutoPlay);
mp.SetRepeat(settings.PlayerRepeat);
if (settings.Volume is >= 0 and <= 100)
mp.SetVolume(settings.Volume);
else
Log.Error("Saved Volume is outside of valid range >= 0 && <=100 ({Volume})", settings.Volume);
mp.OnCompleted += OnTrackCompleted(guildId);
mp.OnStarted += OnTrackStarted(guildId);
mp.OnQueueStopped += OnQueueStopped(guildId);
return mp;
}
public async Task<IUserMessage?> SendToOutputAsync(ulong guildId, IEmbedBuilder embed)
{
if (_outputChannels.TryGetValue(guildId, out var chan))
{
var msg = await (chan.Override ?? chan.Default).EmbedAsync(embed);
return msg;
}
return null;
}
private Func<IMusicPlayer, IQueuedTrackInfo, Task> OnTrackCompleted(ulong guildId)
{
IUserMessage? lastFinishedMessage = null;
return async (mp, trackInfo) =>
{
_ = lastFinishedMessage?.DeleteAsync();
var embed = _eb.Create()
.WithOkColor()
.WithAuthor(GetText(guildId, strs.finished_track), Music.MUSIC_ICON_URL)
.WithDescription(trackInfo.PrettyName())
.WithFooter(trackInfo.PrettyTotalTime());
lastFinishedMessage = await SendToOutputAsync(guildId, embed);
};
}
private Func<IMusicPlayer, IQueuedTrackInfo, int, Task> OnTrackStarted(ulong guildId)
{
IUserMessage? lastPlayingMessage = null;
return async (mp, trackInfo, index) =>
{
_ = lastPlayingMessage?.DeleteAsync();
var embed = _eb.Create()
.WithOkColor()
.WithAuthor(GetText(guildId, strs.playing_track(index + 1)), Music.MUSIC_ICON_URL)
.WithDescription(trackInfo.PrettyName())
.WithFooter($"{mp.PrettyVolume()} | {trackInfo.PrettyInfo()}");
lastPlayingMessage = await SendToOutputAsync(guildId, embed);
};
}
private Func<IMusicPlayer, Task> OnQueueStopped(ulong guildId)
=> _ =>
{
if (_settings.TryGetValue(guildId, out var settings))
{
if (settings.AutoDisconnect)
return LeaveVoiceChannelAsync(guildId);
}
return Task.CompletedTask;
};
// this has to be done because dragging bot to another vc isn't supported yet
public async Task<bool> PlayAsync(ulong guildId, ulong voiceChannelId)
{
if (!TryGetMusicPlayer(guildId, out var mp))
return false;
if (mp.IsStopped)
{
if (!_voiceStateService.TryGetProxy(guildId, out var proxy)
|| proxy.State == VoiceProxy.VoiceProxyState.Stopped)
await JoinVoiceChannelAsync(guildId, voiceChannelId);
}
mp.Next();
return true;
}
private async Task<IList<(string Title, string Url)>> SearchYtLoaderVideosAsync(string query)
{
var result = await _ytLoader.LoadResultsAsync(query);
return result.Select(x => (x.Title, x.Url)).ToList();
}
private async Task<IList<(string Title, string Url)>> SearchGoogleApiVideosAsync(string query)
{
var result = await _googleApiService.GetVideoInfosByKeywordAsync(query, 5);
return result.Select(x => (x.Name, x.Url)).ToList();
}
public async Task<IList<(string Title, string Url)>> SearchVideosAsync(string query)
{
try
{
IList<(string, string)> videos = await SearchYtLoaderVideosAsync(query);
if (videos.Count > 0)
return videos;
}
catch (Exception ex)
{
Log.Warning("Failed geting videos with YtLoader: {ErrorMessage}", ex.Message);
}
try
{
return await SearchGoogleApiVideosAsync(query);
}
catch (Exception ex)
{
Log.Warning("Failed getting video results with Google Api. "
+ "Probably google api key missing: {ErrorMessage}",
ex.Message);
}
return Array.Empty<(string, string)>();
}
private string GetText(ulong guildId, LocStr str)
=> _strings.GetText(str, guildId);
public IEnumerable<(string Name, Func<string> Func)> GetPlaceholders()
{
// random track that's playing
yield return ("%music.playing%", () =>
{
var randomPlayingTrack = _players.Select(x => x.Value.GetCurrentTrack(out _))
.Where(x => x is not null)
.Shuffle()
.FirstOrDefault();
if (randomPlayingTrack is null)
return "-";
return randomPlayingTrack.Title;
});
// number of servers currently listening to music
yield return ("%music.servers%", () =>
{
var count = _players.Select(x => x.Value.GetCurrentTrack(out _)).Count(x => x is not null);
return count.ToString();
});
yield return ("%music.queued%", () =>
{
var count = _players.Sum(x => x.Value.GetQueuedTracks().Count);
return count.ToString();
});
}
#region Settings
private async Task<MusicPlayerSettings> GetSettingsInternalAsync(ulong guildId)
{
if (_settings.TryGetValue(guildId, out var settings))
return settings;
await using var uow = _db.GetDbContext();
var toReturn = _settings[guildId] = await uow.MusicPlayerSettings.ForGuildAsync(guildId);
await uow.SaveChangesAsync();
return toReturn;
}
private async Task ModifySettingsInternalAsync<TState>(
ulong guildId,
Action<MusicPlayerSettings, TState> action,
TState state)
{
await using var uow = _db.GetDbContext();
var ms = await uow.MusicPlayerSettings.ForGuildAsync(guildId);
action(ms, state);
await uow.SaveChangesAsync();
_settings[guildId] = ms;
}
public async Task<bool> SetMusicChannelAsync(ulong guildId, ulong? channelId)
{
if (channelId is null)
{
await UnsetMusicChannelAsync(guildId);
return true;
}
var channel = _client.GetGuild(guildId)?.GetTextChannel(channelId.Value);
if (channel is null)
return false;
await ModifySettingsInternalAsync(guildId,
(settings, chId) =>
{
settings.MusicChannelId = chId;
},
channelId);
_outputChannels.AddOrUpdate(guildId, (channel, channel), (_, old) => (old.Default, channel));
return true;
}
public async Task UnsetMusicChannelAsync(ulong guildId)
{
await ModifySettingsInternalAsync(guildId,
(settings, _) =>
{
settings.MusicChannelId = null;
},
(ulong?)null);
if (_outputChannels.TryGetValue(guildId, out var old))
_outputChannels[guildId] = (old.Default, null);
}
public async Task SetRepeatAsync(ulong guildId, PlayerRepeatType repeatType)
{
await ModifySettingsInternalAsync(guildId,
(settings, type) =>
{
settings.PlayerRepeat = type;
},
repeatType);
if (TryGetMusicPlayer(guildId, out var mp))
mp.SetRepeat(repeatType);
}
public async Task SetVolumeAsync(ulong guildId, int value)
{
if (value is < 0 or > 100)
throw new ArgumentOutOfRangeException(nameof(value));
await ModifySettingsInternalAsync(guildId,
(settings, newValue) =>
{
settings.Volume = newValue;
},
value);
if (TryGetMusicPlayer(guildId, out var mp))
mp.SetVolume(value);
}
public async Task<bool> ToggleAutoDisconnectAsync(ulong guildId)
{
var newState = false;
await ModifySettingsInternalAsync(guildId,
(settings, _) =>
{
newState = settings.AutoDisconnect = !settings.AutoDisconnect;
},
default(object));
return newState;
}
public async Task<QualityPreset> GetMusicQualityAsync(ulong guildId)
{
await using var uow = _db.GetDbContext();
var settings = await uow.MusicPlayerSettings.ForGuildAsync(guildId);
return settings.QualityPreset;
}
public Task SetMusicQualityAsync(ulong guildId, QualityPreset preset)
=> ModifySettingsInternalAsync(guildId,
(settings, _) =>
{
settings.QualityPreset = preset;
},
preset);
public async Task<bool> ToggleQueueAutoPlayAsync(ulong guildId)
{
var newValue = false;
await ModifySettingsInternalAsync(guildId,
(settings, _) => newValue = settings.AutoPlay = !settings.AutoPlay,
false);
if (TryGetMusicPlayer(guildId, out var mp))
mp.AutoPlay = newValue;
return newValue;
}
#endregion
}

View File

@@ -1,71 +0,0 @@
#nullable disable
namespace NadekoBot.Modules.Music.Services;
public sealed partial class YtLoader
{
public class InitRange
{
public string Start { get; set; }
public string End { get; set; }
}
public class IndexRange
{
public string Start { get; set; }
public string End { get; set; }
}
public class ColorInfo
{
public string Primaries { get; set; }
public string TransferCharacteristics { get; set; }
public string MatrixCoefficients { get; set; }
}
public class YtAdaptiveFormat
{
public int Itag { get; set; }
public string MimeType { get; set; }
public int Bitrate { get; set; }
public int Width { get; set; }
public int Height { get; set; }
public InitRange InitRange { get; set; }
public IndexRange IndexRange { get; set; }
public string LastModified { get; set; }
public string ContentLength { get; set; }
public string Quality { get; set; }
public int Fps { get; set; }
public string QualityLabel { get; set; }
public string ProjectionType { get; set; }
public int AverageBitrate { get; set; }
public ColorInfo ColorInfo { get; set; }
public string ApproxDurationMs { get; set; }
public string SignatureCipher { get; set; }
}
public abstract class TrackInfo
{
public abstract string Url { get; }
public abstract string Title { get; }
public abstract TimeSpan Duration { get; }
}
public sealed class YtTrackInfo : TrackInfo
{
private const string BASE_YOUTUBE_URL = "https://youtube.com/watch?v=";
public override string Url { get; }
public override string Title { get; }
public override TimeSpan Duration { get; }
private readonly string _videoId;
public YtTrackInfo(string title, string videoId, TimeSpan duration)
{
Title = title;
Url = BASE_YOUTUBE_URL + videoId;
Duration = duration;
_videoId = videoId;
}
}
}

View File

@@ -1,130 +0,0 @@
#nullable disable
using System.Globalization;
using System.Text;
using System.Text.Json;
namespace NadekoBot.Modules.Music.Services;
public sealed partial class YtLoader
{
private static readonly byte[] _ytResultInitialData = Encoding.UTF8.GetBytes("var ytInitialData = ");
private static readonly byte[] _ytResultJsonEnd = Encoding.UTF8.GetBytes(";<");
private static readonly string[] _durationFormats =
{
@"m\:ss", @"mm\:ss", @"h\:mm\:ss", @"hh\:mm\:ss", @"hhh\:mm\:ss"
};
private readonly IHttpClientFactory _httpFactory;
public YtLoader(IHttpClientFactory httpFactory)
=> _httpFactory = httpFactory;
// public async Task<TrackInfo> LoadTrackByIdAsync(string videoId)
// {
// using var http = new HttpClient();
// http.DefaultRequestHeaders.Add("X-YouTube-Client-Name", "1");
// http.DefaultRequestHeaders.Add("X-YouTube-Client-Version", "2.20210520.09.00");
// http.DefaultRequestHeaders.Add("Cookie", "CONSENT=YES+cb.20210530-19-p0.en+FX+071;");
//
// var responseString = await http.GetStringAsync($"https://youtube.com?" +
// $"pbj=1" +
// $"&hl=en" +
// $"&v=" + videoId);
//
// var jsonDoc = JsonDocument.Parse(responseString).RootElement;
// var elem = jsonDoc.EnumerateArray()
// .FirstOrDefault(x => x.TryGetProperty("page", out var elem) && elem.GetString() == "watch");
//
// var formatsJsonArray = elem.GetProperty("streamingdata")
// .GetProperty("formats")
// .GetRawText();
//
// var formats = JsonSerializer.Deserialize<List<YtAdaptiveFormat>>(formatsJsonArray);
// var result = formats
// .Where(x => x.MimeType.StartsWith("audio/"))
// .OrderByDescending(x => x.Bitrate)
// .FirstOrDefault();
//
// if (result is null)
// return null;
//
// return new YtTrackInfo("1", "2", TimeSpan.Zero);
// }
public async Task<IList<TrackInfo>> LoadResultsAsync(string query)
{
query = Uri.EscapeDataString(query);
using var http = _httpFactory.CreateClient();
http.DefaultRequestHeaders.Add("Cookie", "CONSENT=YES+cb.20210530-19-p0.en+FX+071;");
byte[] response;
try
{
response = await http.GetByteArrayAsync($"https://youtube.com/results?hl=en&search_query={query}");
}
catch (HttpRequestException ex)
{
Log.Warning("Unable to retrieve data with YtLoader: {ErrorMessage}", ex.Message);
return null;
}
// there is a lot of useless html above the script tag, however if html gets significantly reduced
// this will result in the json being cut off
var mem = GetScriptResponseSpan(response);
var root = JsonDocument.Parse(mem).RootElement;
using var tracksJsonItems = root
.GetProperty("contents")
.GetProperty("twoColumnSearchResultsRenderer")
.GetProperty("primaryContents")
.GetProperty("sectionListRenderer")
.GetProperty("contents")[0]
.GetProperty("itemSectionRenderer")
.GetProperty("contents")
.EnumerateArray();
var tracks = new List<TrackInfo>();
foreach (var track in tracksJsonItems)
{
if (!track.TryGetProperty("videoRenderer", out var elem))
continue;
var videoId = elem.GetProperty("videoId").GetString();
// var thumb = elem.GetProperty("thumbnail").GetProperty("thumbnails")[0].GetProperty("url").GetString();
var title = elem.GetProperty("title").GetProperty("runs")[0].GetProperty("text").GetString();
var durationString = elem.GetProperty("lengthText").GetProperty("simpleText").GetString();
if (!TimeSpan.TryParseExact(durationString,
_durationFormats,
CultureInfo.InvariantCulture,
out var duration))
{
Log.Warning("Cannot parse duration: {DurationString}", durationString);
continue;
}
tracks.Add(new YtTrackInfo(title, videoId, duration));
if (tracks.Count >= 5)
break;
}
return tracks;
}
private Memory<byte> GetScriptResponseSpan(byte[] response)
{
var responseSpan = response.AsSpan()[140_000..];
var startIndex = responseSpan.IndexOf(_ytResultInitialData);
if (startIndex == -1)
return null; // FUTURE try selecting html
startIndex += _ytResultInitialData.Length;
var endIndex =
140_000 + startIndex + responseSpan[(startIndex + 20_000)..].IndexOf(_ytResultJsonEnd) + 20_000;
startIndex += 140_000;
return response.AsMemory(startIndex, endIndex - startIndex);
}
}

View File

@@ -1,12 +0,0 @@
#nullable disable
namespace NadekoBot.Modules.Music;
public interface ICachableTrackData
{
string Id { get; set; }
string Url { get; set; }
string Thumbnail { get; set; }
public TimeSpan Duration { get; }
MusicPlatform Platform { get; set; }
string Title { get; set; }
}

View File

@@ -1,7 +0,0 @@
#nullable disable
namespace NadekoBot.Modules.Music;
public interface ILocalTrackResolver : IPlatformQueryResolver
{
IAsyncEnumerable<ITrackInfo> ResolveDirectoryAsync(string dirPath);
}

View File

@@ -1,40 +0,0 @@
using NadekoBot.Services.Database.Models;
namespace NadekoBot.Modules.Music;
public interface IMusicPlayer : IDisposable
{
float Volume { get; }
bool IsPaused { get; }
bool IsStopped { get; }
bool IsKilled { get; }
int CurrentIndex { get; }
public PlayerRepeatType Repeat { get; }
bool AutoPlay { get; set; }
void Stop();
void Clear();
IReadOnlyCollection<IQueuedTrackInfo> GetQueuedTracks();
IQueuedTrackInfo? GetCurrentTrack(out int index);
void Next();
bool MoveTo(int index);
void SetVolume(int newVolume);
void Kill();
bool TryRemoveTrackAt(int index, out IQueuedTrackInfo? trackInfo);
Task<(IQueuedTrackInfo? QueuedTrack, int Index)> TryEnqueueTrackAsync(
string query,
string queuer,
bool asNext,
MusicPlatform? forcePlatform = null);
Task EnqueueManyAsync(IEnumerable<(string Query, MusicPlatform Platform)> queries, string queuer);
bool TogglePause();
IQueuedTrackInfo? MoveTrack(int from, int to);
void EnqueueTrack(ITrackInfo track, string queuer);
void EnqueueTracks(IEnumerable<ITrackInfo> tracks, string queuer);
void SetRepeat(PlayerRepeatType type);
void ShuffleQueue();
}

View File

@@ -1,22 +0,0 @@
namespace NadekoBot.Modules.Music;
public interface IMusicQueue
{
int Index { get; }
int Count { get; }
IQueuedTrackInfo Enqueue(ITrackInfo trackInfo, string queuer, out int index);
IQueuedTrackInfo EnqueueNext(ITrackInfo song, string queuer, out int index);
void EnqueueMany(IEnumerable<ITrackInfo> tracks, string queuer);
public IReadOnlyCollection<IQueuedTrackInfo> List();
IQueuedTrackInfo? GetCurrent(out int index);
void Advance();
void Clear();
bool SetIndex(int index);
bool TryRemoveAt(int index, out IQueuedTrackInfo? trackInfo, out bool isCurrent);
void RemoveCurrent();
IQueuedTrackInfo? MoveTrack(int from, int to);
void Shuffle(Random rng);
bool IsLast();
}

View File

@@ -1,6 +0,0 @@
namespace NadekoBot.Modules.Music;
public interface IPlatformQueryResolver
{
Task<ITrackInfo?> ResolveByQueryAsync(string query);
}

View File

@@ -1,9 +0,0 @@
#nullable disable
namespace NadekoBot.Modules.Music;
public interface IQueuedTrackInfo : ITrackInfo
{
public ITrackInfo TrackInfo { get; }
public string Queuer { get; }
}

View File

@@ -1,6 +0,0 @@
#nullable disable
namespace NadekoBot.Modules.Music;
public interface IRadioResolver : IPlatformQueryResolver
{
}

View File

@@ -1,8 +0,0 @@
#nullable disable
namespace NadekoBot.Modules.Music;
public interface ISoundcloudResolver : IPlatformQueryResolver
{
bool IsSoundCloudLink(string url);
IAsyncEnumerable<ITrackInfo> ResolvePlaylistAsync(string playlist);
}

View File

@@ -1,25 +0,0 @@
namespace NadekoBot.Modules.Music;
public interface ITrackCacher
{
Task<string?> GetOrCreateStreamLink(
string id,
MusicPlatform platform,
Func<Task<(string StreamUrl, TimeSpan Expiry)>> streamUrlFactory);
Task CacheTrackDataAsync(ICachableTrackData data);
Task<ICachableTrackData?> GetCachedDataByIdAsync(string id, MusicPlatform platform);
Task<ICachableTrackData?> GetCachedDataByQueryAsync(string query, MusicPlatform platform);
Task CacheTrackDataByQueryAsync(string query, ICachableTrackData data);
Task CacheStreamUrlAsync(
string id,
MusicPlatform platform,
string url,
TimeSpan expiry);
Task<IReadOnlyCollection<string>> GetPlaylistTrackIdsAsync(string playlistId, MusicPlatform platform);
Task CachePlaylistTrackIdsAsync(string playlistId, MusicPlatform platform, IEnumerable<string> ids);
Task CachePlaylistIdByQueryAsync(string query, MusicPlatform platform, string playlistId);
Task<string?> GetPlaylistIdByQueryAsync(string query, MusicPlatform platform);
}

View File

@@ -1,12 +0,0 @@
namespace NadekoBot.Modules.Music;
public interface ITrackInfo
{
public string Id => string.Empty;
public string Title { get; }
public string Url { get; }
public string Thumbnail { get; }
public TimeSpan Duration { get; }
public MusicPlatform Platform { get; }
public ValueTask<string?> GetStreamUrl();
}

View File

@@ -1,6 +0,0 @@
namespace NadekoBot.Modules.Music;
public interface ITrackResolveProvider
{
Task<ITrackInfo?> QuerySongAsync(string query, MusicPlatform? forcePlatform);
}

View File

@@ -1,15 +0,0 @@
#nullable disable
using Ayu.Discord.Voice;
namespace NadekoBot.Modules.Music;
public interface IVoiceProxy
{
VoiceProxy.VoiceProxyState State { get; }
public bool SendPcmFrame(VoiceClient vc, Span<byte> data, int length);
public void SetGateway(VoiceGateway gateway);
Task StartSpeakingAsync();
Task StopSpeakingAsync();
public Task StartGateway();
Task StopGateway();
}

View File

@@ -1,11 +0,0 @@
using System.Text.RegularExpressions;
namespace NadekoBot.Modules.Music;
public interface IYoutubeResolver : IPlatformQueryResolver
{
public Regex YtVideoIdRegex { get; }
public Task<ITrackInfo?> ResolveByIdAsync(string id);
IAsyncEnumerable<ITrackInfo> ResolveTracksFromPlaylistAsync(string query);
Task<ITrackInfo?> ResolveByQueryAsync(string query, bool tryExtractingId);
}

View File

@@ -1,19 +0,0 @@
#nullable disable
using System.Text.Json.Serialization;
namespace NadekoBot.Modules.Music;
public sealed class CachableTrackData : ICachableTrackData
{
public string Title { get; set; } = string.Empty;
public string Id { get; set; } = string.Empty;
public string Url { get; set; } = string.Empty;
public string Thumbnail { get; set; } = string.Empty;
public double TotalDurationMs { get; set; }
[JsonIgnore]
public TimeSpan Duration
=> TimeSpan.FromMilliseconds(TotalDurationMs);
public MusicPlatform Platform { get; set; }
}

View File

@@ -1,95 +0,0 @@
#nullable disable
using System.Runtime.InteropServices;
namespace NadekoBot.Modules.Music.Common;
public sealed class MultimediaTimer : IDisposable
{
private LpTimeProcDelegate lpTimeProc;
private readonly uint _eventId;
private readonly Action<object> _callback;
private readonly object _state;
public MultimediaTimer(Action<object> callback, object state, int period)
{
if (period <= 0)
throw new ArgumentOutOfRangeException(nameof(period), "Period must be greater than 0");
_callback = callback;
_state = state;
lpTimeProc = CallbackInternal;
_eventId = timeSetEvent((uint)period, 1, lpTimeProc, 0, TimerMode.Periodic);
}
/// <summary>
/// The timeSetEvent function starts a specified timer event. The multimedia timer runs in its own thread.
/// After the event is activated, it calls the specified callback function or sets or pulses the specified
/// event object.
/// </summary>
/// <param name="uDelay">
/// Event delay, in milliseconds. If this value is not in the range of the minimum and
/// maximum event delays supported by the timer, the function returns an error.
/// </param>
/// <param name="uResolution">
/// Resolution of the timer event, in milliseconds. The resolution increases with
/// smaller values; a resolution of 0 indicates periodic events should occur with the greatest possible accuracy.
/// To reduce system overhead, however, you should use the maximum value appropriate for your application.
/// </param>
/// <param name="lpTimeProc">
/// Pointer to a callback function that is called once upon expiration of a single event or periodically upon
/// expiration of periodic events. If fuEvent specifies the TIME_CALLBACK_EVENT_SET or TIME_CALLBACK_EVENT_PULSE
/// flag, then the lpTimeProc parameter is interpreted as a handle to an event object. The event will be set or
/// pulsed upon completion of a single event or periodically upon completion of periodic events.
/// For any other value of fuEvent, the lpTimeProc parameter is a pointer to a callback function of type
/// LPTIMECALLBACK.
/// </param>
/// <param name="dwUser">User-supplied callback data.</param>
/// <param name="fuEvent"></param>
/// <returns>Timer event type. This parameter may include one of the following values.</returns>
[DllImport("Winmm.dll")]
private static extern uint timeSetEvent(
uint uDelay,
uint uResolution,
LpTimeProcDelegate lpTimeProc,
int dwUser,
TimerMode fuEvent);
/// <summary>
/// The timeKillEvent function cancels a specified timer event.
/// </summary>
/// <param name="uTimerId">
/// Identifier of the timer event to cancel.
/// This identifier was returned by the timeSetEvent function when the timer event was set up.
/// </param>
/// <returns>Returns TIMERR_NOERROR if successful or MMSYSERR_INVALPARAM if the specified timer event does not exist.</returns>
[DllImport("Winmm.dll")]
private static extern int timeKillEvent(uint uTimerId);
private void CallbackInternal(
uint uTimerId,
uint uMsg,
int dwUser,
int dw1,
int dw2)
=> _callback(_state);
public void Dispose()
{
lpTimeProc = default;
timeKillEvent(_eventId);
}
private delegate void LpTimeProcDelegate(
uint uTimerId,
uint uMsg,
int dwUser,
int dw1,
int dw2);
private enum TimerMode
{
OneShot,
Periodic
}
}

View File

@@ -1,57 +0,0 @@
#nullable disable
namespace NadekoBot.Modules.Music;
public static class MusicExtensions
{
public static string PrettyTotalTime(this IMusicPlayer mp)
{
long sum = 0;
foreach (var track in mp.GetQueuedTracks())
{
if (track.Duration == TimeSpan.MaxValue)
return "∞";
sum += track.Duration.Ticks;
}
var total = new TimeSpan(sum);
return total.ToString(@"hh\:mm\:ss");
}
public static string PrettyVolume(this IMusicPlayer mp)
=> $"🔉 {(int)(mp.Volume * 100)}%";
public static string PrettyName(this ITrackInfo trackInfo)
=> $"**[{trackInfo.Title.TrimTo(60).Replace("[", "\\[").Replace("]", "\\]")}]({trackInfo.Url.TrimTo(50, true)})**";
public static string PrettyInfo(this IQueuedTrackInfo trackInfo)
=> $"{trackInfo.PrettyTotalTime()} | {trackInfo.Platform} | {trackInfo.Queuer}";
public static string PrettyFullName(this IQueuedTrackInfo trackInfo)
=> $@"{trackInfo.PrettyName()}
`{trackInfo.PrettyTotalTime()} | {trackInfo.Platform} | {Format.Sanitize(trackInfo.Queuer.TrimTo(15))}`";
public static string PrettyTotalTime(this ITrackInfo trackInfo)
{
if (trackInfo.Duration == TimeSpan.Zero)
return "(?)";
if (trackInfo.Duration == TimeSpan.MaxValue)
return "∞";
if (trackInfo.Duration.TotalHours >= 1)
return trackInfo.Duration.ToString("""hh\:mm\:ss""");
return trackInfo.Duration.ToString("""mm\:ss""");
}
public static ICachableTrackData ToCachedData(this ITrackInfo trackInfo, string id)
=> new CachableTrackData
{
TotalDurationMs = trackInfo.Duration.TotalMilliseconds,
Id = id,
Thumbnail = trackInfo.Thumbnail,
Url = trackInfo.Url,
Platform = trackInfo.Platform,
Title = trackInfo.Title
};
}

View File

@@ -1,10 +0,0 @@
#nullable disable
namespace NadekoBot.Modules.Music;
public enum MusicPlatform
{
Radio,
Youtube,
Local,
SoundCloud
}

View File

@@ -1,529 +0,0 @@
using Ayu.Discord.Voice;
using NadekoBot.Services.Database.Models;
using System.ComponentModel;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using Nadeko.Common;
namespace NadekoBot.Modules.Music;
public sealed class MusicPlayer : IMusicPlayer
{
public event Func<IMusicPlayer, IQueuedTrackInfo, Task>? OnCompleted;
public event Func<IMusicPlayer, IQueuedTrackInfo, int, Task>? OnStarted;
public event Func<IMusicPlayer, Task>? OnQueueStopped;
public bool IsKilled { get; private set; }
public bool IsStopped { get; private set; }
public bool IsPaused { get; private set; }
public PlayerRepeatType Repeat { get; private set; }
public int CurrentIndex
=> _queue.Index;
public float Volume { get; private set; } = 1.0f;
private readonly AdjustVolumeDelegate _adjustVolume;
private readonly VoiceClient _vc;
private readonly IMusicQueue _queue;
private readonly ITrackResolveProvider _trackResolveProvider;
private readonly IVoiceProxy _proxy;
private readonly IGoogleApiService _googleApiService;
private readonly ISongBuffer _songBuffer;
private bool skipped;
private int? forceIndex;
private readonly Thread _thread;
private readonly Random _rng;
public bool AutoPlay { get; set; }
public MusicPlayer(
IMusicQueue queue,
ITrackResolveProvider trackResolveProvider,
IVoiceProxy proxy,
IGoogleApiService googleApiService,
QualityPreset qualityPreset,
bool autoPlay)
{
_queue = queue;
_trackResolveProvider = trackResolveProvider;
_proxy = proxy;
_googleApiService = googleApiService;
AutoPlay = autoPlay;
_rng = new NadekoRandom();
_vc = GetVoiceClient(qualityPreset);
if (_vc.BitDepth == 16)
_adjustVolume = AdjustVolumeInt16;
else
_adjustVolume = AdjustVolumeFloat32;
_songBuffer = new PoopyBufferImmortalized(_vc.InputLength);
_thread = new(async () =>
{
await PlayLoop();
});
_thread.Start();
}
private static VoiceClient GetVoiceClient(QualityPreset qualityPreset)
=> qualityPreset switch
{
QualityPreset.Highest => new(),
QualityPreset.High => new(SampleRate._48k, Bitrate._128k, Channels.Two, FrameDelay.Delay40),
QualityPreset.Medium => new(SampleRate._48k,
Bitrate._96k,
Channels.Two,
FrameDelay.Delay40,
BitDepthEnum.UInt16),
QualityPreset.Low => new(SampleRate._48k,
Bitrate._64k,
Channels.Two,
FrameDelay.Delay40,
BitDepthEnum.UInt16),
_ => throw new ArgumentOutOfRangeException(nameof(qualityPreset), qualityPreset, null)
};
private async Task PlayLoop()
{
var sw = new Stopwatch();
while (!IsKilled)
{
// wait until a song is available in the queue
// or until the queue is resumed
var track = _queue.GetCurrent(out var index);
if (track is null || IsStopped)
{
await Task.Delay(500);
continue;
}
if (skipped)
{
skipped = false;
_queue.Advance();
continue;
}
using var cancellationTokenSource = new CancellationTokenSource();
var token = cancellationTokenSource.Token;
try
{
// light up green in vc
_ = _proxy.StartSpeakingAsync();
_ = OnStarted?.Invoke(this, track, index);
// make sure song buffer is ready to be (re)used
_songBuffer.Reset();
var streamUrl = await track.GetStreamUrl();
// start up the data source
using var source = FfmpegTrackDataSource.CreateAsync(
_vc.BitDepth,
streamUrl,
track.Platform == MusicPlatform.Local);
// start moving data from the source into the buffer
// this method will return once the sufficient prebuffering is done
await _songBuffer.BufferAsync(source, token);
// // Implemenation with multimedia timer. Works but a hassle because no support for switching
// // vcs, as any error in copying will cancel the song. Also no idea how to use this as an option
// // for selfhosters.
// if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
// {
// var cancelSource = new CancellationTokenSource();
// var cancelToken = cancelSource.Token;
// using var timer = new MultimediaTimer(_ =>
// {
// if (IsStopped || IsKilled)
// {
// cancelSource.Cancel();
// return;
// }
//
// if (_skipped)
// {
// _skipped = false;
// cancelSource.Cancel();
// return;
// }
//
// if (IsPaused)
// return;
//
// try
// {
// // this should tolerate certain number of errors
// var result = CopyChunkToOutput(_songBuffer, _vc);
// if (!result)
// cancelSource.Cancel();
//
// }
// catch (Exception ex)
// {
// Log.Warning(ex, "Something went wrong sending voice data: {ErrorMessage}", ex.Message);
// cancelSource.Cancel();
// }
//
// }, null, 20);
//
// while(true)
// await Task.Delay(1000, cancelToken);
// }
// start sending data
var ticksPerMs = 1000f / Stopwatch.Frequency;
sw.Start();
Thread.Sleep(2);
var delay = sw.ElapsedTicks * ticksPerMs > 3f ? _vc.Delay - 16 : _vc.Delay - 3;
var errorCount = 0;
while (!IsStopped && !IsKilled)
{
// doing the skip this way instead of in the condition
// ensures that a song will for sure be skipped
if (skipped)
{
skipped = false;
break;
}
if (IsPaused)
{
await Task.Delay(200);
continue;
}
sw.Restart();
var ticks = sw.ElapsedTicks;
try
{
var result = CopyChunkToOutput(_songBuffer, _vc);
// if song is finished
if (result is null)
break;
if (result is true)
{
if (errorCount > 0)
{
_ = _proxy.StartSpeakingAsync();
errorCount = 0;
}
// FUTURE windows multimedia api
// wait for slightly less than the latency
Thread.Sleep(delay);
// and then spin out the rest
while ((sw.ElapsedTicks - ticks) * ticksPerMs <= _vc.Delay - 0.1f)
Thread.SpinWait(100);
}
else
{
// result is false is either when the gateway is being swapped
// or if the bot is reconnecting, or just disconnected for whatever reason
// tolerate up to 15x200ms of failures (3 seconds)
if (++errorCount <= 15)
{
await Task.Delay(200);
continue;
}
Log.Warning("Can't send data to voice channel");
IsStopped = true;
// if errors are happening for more than 3 seconds
// Stop the player
break;
}
}
catch (Exception ex)
{
Log.Warning(ex, "Something went wrong sending voice data: {ErrorMessage}", ex.Message);
}
}
}
catch (Win32Exception)
{
IsStopped = true;
Log.Error("Please install ffmpeg and make sure it's added to your "
+ "PATH environment variable before trying again");
}
catch (OperationCanceledException)
{
Log.Information("Song skipped");
}
catch (Exception ex)
{
Log.Error(ex, "Unknown error in music loop: {ErrorMessage}", ex.Message);
}
finally
{
cancellationTokenSource.Cancel();
// turn off green in vc
_ = OnCompleted?.Invoke(this, track);
if (AutoPlay && track.Platform == MusicPlatform.Youtube)
{
try
{
var relatedSongs = await _googleApiService.GetRelatedVideosAsync(track.TrackInfo.Id, 5);
var related = relatedSongs.Shuffle().FirstOrDefault();
if (related is not null)
{
var relatedTrack = await _trackResolveProvider.QuerySongAsync(related, MusicPlatform.Youtube);
if (relatedTrack is not null)
EnqueueTrack(relatedTrack, "Autoplay");
}
}
catch (Exception ex)
{
Log.Warning(ex, "Failed queueing a related song via autoplay");
}
}
HandleQueuePostTrack();
skipped = false;
_ = _proxy.StopSpeakingAsync();
await Task.Delay(100);
}
}
}
private bool? CopyChunkToOutput(ISongBuffer sb, VoiceClient vc)
{
var data = sb.Read(vc.InputLength, out var length);
// if nothing is read from the buffer, song is finished
if (data.Length == 0)
return null;
_adjustVolume(data, Volume);
return _proxy.SendPcmFrame(vc, data, length);
}
private void HandleQueuePostTrack()
{
if (forceIndex is { } index)
{
_queue.SetIndex(index);
forceIndex = null;
return;
}
var (repeat, isStopped) = (Repeat, IsStopped);
if (repeat == PlayerRepeatType.Track || isStopped)
return;
// if queue is being repeated, advance no matter what
if (repeat == PlayerRepeatType.None)
{
// if this is the last song,
// stop the queue
if (_queue.IsLast())
{
IsStopped = true;
OnQueueStopped?.Invoke(this);
return;
}
_queue.Advance();
return;
}
_queue.Advance();
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void AdjustVolumeInt16(Span<byte> audioSamples, float volume)
{
if (Math.Abs(volume - 1f) < 0.0001f)
return;
var samples = MemoryMarshal.Cast<byte, short>(audioSamples);
for (var i = 0; i < samples.Length; i++)
{
ref var sample = ref samples[i];
sample = (short)(sample * volume);
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void AdjustVolumeFloat32(Span<byte> audioSamples, float volume)
{
if (Math.Abs(volume - 1f) < 0.0001f)
return;
var samples = MemoryMarshal.Cast<byte, float>(audioSamples);
for (var i = 0; i < samples.Length; i++)
{
ref var sample = ref samples[i];
sample *= volume;
}
}
public async Task<(IQueuedTrackInfo? QueuedTrack, int Index)> TryEnqueueTrackAsync(
string query,
string queuer,
bool asNext,
MusicPlatform? forcePlatform = null)
{
var song = await _trackResolveProvider.QuerySongAsync(query, forcePlatform);
if (song is null)
return default;
int index;
if (asNext)
return (_queue.EnqueueNext(song, queuer, out index), index);
return (_queue.Enqueue(song, queuer, out index), index);
}
public async Task EnqueueManyAsync(IEnumerable<(string Query, MusicPlatform Platform)> queries, string queuer)
{
var errorCount = 0;
foreach (var chunk in queries.Chunk(5))
{
if (IsKilled)
break;
await chunk.Select(async data =>
{
var (query, platform) = data;
try
{
await TryEnqueueTrackAsync(query, queuer, false, platform);
errorCount = 0;
}
catch (Exception ex)
{
Log.Warning(ex, "Error resolving {MusicPlatform} Track {TrackQuery}", platform, query);
++errorCount;
}
})
.WhenAll();
await Task.Delay(1000);
// > 10 errors in a row = kill
if (errorCount > 10)
break;
}
}
public void EnqueueTrack(ITrackInfo track, string queuer)
=> _queue.Enqueue(track, queuer, out _);
public void EnqueueTracks(IEnumerable<ITrackInfo> tracks, string queuer)
=> _queue.EnqueueMany(tracks, queuer);
public void SetRepeat(PlayerRepeatType type)
=> Repeat = type;
public void ShuffleQueue()
=> _queue.Shuffle(_rng);
public void Stop()
=> IsStopped = true;
public void Clear()
{
_queue.Clear();
skipped = true;
}
public IReadOnlyCollection<IQueuedTrackInfo> GetQueuedTracks()
=> _queue.List();
public IQueuedTrackInfo? GetCurrentTrack(out int index)
=> _queue.GetCurrent(out index);
public void Next()
{
skipped = true;
IsStopped = false;
IsPaused = false;
}
public bool MoveTo(int index)
{
if (_queue.SetIndex(index))
{
forceIndex = index;
skipped = true;
IsStopped = false;
IsPaused = false;
return true;
}
return false;
}
public void SetVolume(int newVolume)
{
var normalizedVolume = newVolume / 100f;
if (normalizedVolume is < 0f or > 1f)
throw new ArgumentOutOfRangeException(nameof(newVolume), "Volume must be in range 0-100");
Volume = normalizedVolume;
}
public void Kill()
{
IsKilled = true;
IsStopped = true;
IsPaused = false;
skipped = true;
}
public bool TryRemoveTrackAt(int index, out IQueuedTrackInfo? trackInfo)
{
if (!_queue.TryRemoveAt(index, out trackInfo, out var isCurrent))
return false;
if (isCurrent)
skipped = true;
return true;
}
public bool TogglePause()
=> IsPaused = !IsPaused;
public IQueuedTrackInfo? MoveTrack(int from, int to)
=> _queue.MoveTrack(from, to);
public void Dispose()
{
IsKilled = true;
OnCompleted = null;
OnStarted = null;
OnQueueStopped = null;
_queue.Clear();
_songBuffer.Dispose();
_vc.Dispose();
}
private delegate void AdjustVolumeDelegate(Span<byte> data, float volume);
}

View File

@@ -1,316 +0,0 @@
namespace NadekoBot.Modules.Music;
public sealed partial class MusicQueue
{
private sealed class QueuedTrackInfo : IQueuedTrackInfo
{
public ITrackInfo TrackInfo { get; }
public string Queuer { get; }
public string Title
=> TrackInfo.Title;
public string Url
=> TrackInfo.Url;
public string Thumbnail
=> TrackInfo.Thumbnail;
public TimeSpan Duration
=> TrackInfo.Duration;
public MusicPlatform Platform
=> TrackInfo.Platform;
public QueuedTrackInfo(ITrackInfo trackInfo, string queuer)
{
TrackInfo = trackInfo;
Queuer = queuer;
}
public ValueTask<string?> GetStreamUrl()
=> TrackInfo.GetStreamUrl();
}
}
public sealed partial class MusicQueue : IMusicQueue
{
public int Index
{
get
{
// just make sure the internal logic runs first
// to make sure that some potential indermediate value is not returned
lock (_locker)
{
return index;
}
}
}
public int Count
{
get
{
lock (_locker)
{
return tracks.Count;
}
}
}
private LinkedList<QueuedTrackInfo> tracks;
private int index;
private readonly object _locker = new();
public MusicQueue()
{
index = 0;
tracks = new();
}
public IQueuedTrackInfo Enqueue(ITrackInfo trackInfo, string queuer, out int enqueuedAt)
{
lock (_locker)
{
var added = new QueuedTrackInfo(trackInfo, queuer);
enqueuedAt = tracks.Count;
tracks.AddLast(added);
return added;
}
}
public IQueuedTrackInfo EnqueueNext(ITrackInfo trackInfo, string queuer, out int trackIndex)
{
lock (_locker)
{
if (tracks.Count == 0)
return Enqueue(trackInfo, queuer, out trackIndex);
var currentNode = tracks.First!;
int i;
for (i = 1; i <= index; i++)
currentNode = currentNode.Next!; // can't be null because index is always in range of the count
var added = new QueuedTrackInfo(trackInfo, queuer);
trackIndex = i;
tracks.AddAfter(currentNode, added);
return added;
}
}
public void EnqueueMany(IEnumerable<ITrackInfo> toEnqueue, string queuer)
{
lock (_locker)
{
foreach (var track in toEnqueue)
{
var added = new QueuedTrackInfo(track, queuer);
tracks.AddLast(added);
}
}
}
public IReadOnlyCollection<IQueuedTrackInfo> List()
{
lock (_locker)
{
return tracks.ToList();
}
}
public IQueuedTrackInfo? GetCurrent(out int currentIndex)
{
lock (_locker)
{
currentIndex = index;
return tracks.ElementAtOrDefault(index);
}
}
public void Advance()
{
lock (_locker)
{
if (++index >= tracks.Count)
index = 0;
}
}
public void Clear()
{
lock (_locker)
{
tracks.Clear();
}
}
public bool SetIndex(int newIndex)
{
lock (_locker)
{
if (newIndex < 0 || newIndex >= tracks.Count)
return false;
index = newIndex;
return true;
}
}
private void RemoveAtInternal(int remoteAtIndex, out IQueuedTrackInfo trackInfo)
{
var removedNode = tracks.First!;
int i;
for (i = 0; i < remoteAtIndex; i++)
removedNode = removedNode.Next!;
trackInfo = removedNode.Value;
tracks.Remove(removedNode);
if (i <= index)
--index;
if (index < 0)
index = Count;
// if it was the last song in the queue
// // wrap back to start
// if (_index == Count)
// _index = 0;
// else if (i <= _index)
// if (_index == 0)
// _index = Count;
// else --_index;
}
public void RemoveCurrent()
{
lock (_locker)
{
if (index < tracks.Count)
RemoveAtInternal(index, out _);
}
}
public IQueuedTrackInfo? MoveTrack(int from, int to)
{
if (from < 0)
throw new ArgumentOutOfRangeException(nameof(from));
if (to < 0)
throw new ArgumentOutOfRangeException(nameof(to));
if (to == from)
throw new ArgumentException($"{nameof(from)} and {nameof(to)} must be different");
lock (_locker)
{
if (from >= Count || to >= Count)
return null;
// update current track index
if (from == index)
{
// if the song being moved is the current track
// it means that it will for sure end up on the destination
index = to;
}
else
{
// moving a track from below the current track means
// means it will drop down
if (from < index)
index--;
// moving a track to below the current track
// means it will rise up
if (to <= index)
index++;
// if both from and to are below _index - net change is + 1 - 1 = 0
// if from is below and to is above - net change is -1 (as the track is taken and put above)
// if from is above and to is below - net change is 1 (as the track is inserted under)
// if from is above and to is above - net change is 0
}
// get the node which needs to be moved
var fromNode = tracks.First!;
for (var i = 0; i < from; i++)
fromNode = fromNode.Next!;
// remove it from the queue
tracks.Remove(fromNode);
// if it needs to be added as a first node,
// add it directly and return
if (to == 0)
{
tracks.AddFirst(fromNode);
return fromNode.Value;
}
// else find the node at the index before the specified target
var addAfterNode = tracks.First!;
for (var i = 1; i < to; i++)
addAfterNode = addAfterNode.Next!;
// and add after it
tracks.AddAfter(addAfterNode, fromNode);
return fromNode.Value;
}
}
public void Shuffle(Random rng)
{
lock (_locker)
{
var list = tracks.ToList();
for (var i = 0; i < list.Count; i++)
{
var struck = rng.Next(i, list.Count);
(list[struck], list[i]) = (list[i], list[struck]);
// could preserving the index during shuffling be done better?
if (i == index)
index = struck;
else if (struck == index)
index = i;
}
tracks = new(list);
}
}
public bool IsLast()
{
lock (_locker)
{
return index == tracks.Count // if there are no tracks
|| index == tracks.Count - 1;
}
}
public bool TryRemoveAt(int remoteAt, out IQueuedTrackInfo? trackInfo, out bool isCurrent)
{
lock (_locker)
{
isCurrent = false;
trackInfo = null;
if (remoteAt < 0 || remoteAt >= tracks.Count)
return false;
if (remoteAt == index)
isCurrent = true;
RemoveAtInternal(remoteAt, out trackInfo);
return true;
}
}
}

View File

@@ -1,16 +0,0 @@
namespace NadekoBot.Modules.Music;
public sealed record RemoteTrackInfo(
string Id,
string Title,
string Url,
string Thumbnail,
TimeSpan Duration,
MusicPlatform Platform,
Func<Task<string?>> _streamFactory) : ITrackInfo
{
private readonly Func<Task<string?>> _streamFactory = _streamFactory;
public async ValueTask<string?> GetStreamUrl()
=> await _streamFactory();
}

View File

@@ -1,30 +0,0 @@
namespace NadekoBot.Modules.Music;
public sealed class SimpleTrackInfo : ITrackInfo
{
public string Title { get; }
public string Url { get; }
public string Thumbnail { get; }
public TimeSpan Duration { get; }
public MusicPlatform Platform { get; }
public string? StreamUrl { get; }
public SimpleTrackInfo(
string title,
string url,
string thumbnail,
TimeSpan duration,
MusicPlatform platform,
string streamUrl)
{
Title = title;
Url = url;
Thumbnail = thumbnail;
Duration = duration;
Platform = platform;
StreamUrl = streamUrl;
}
public ValueTask<string?> GetStreamUrl()
=> new(StreamUrl);
}

View File

@@ -1,105 +0,0 @@
namespace NadekoBot.Modules.Music;
public sealed class TrackCacher : ITrackCacher
{
private readonly IBotCache _cache;
public TrackCacher(IBotCache cache)
=> _cache = cache;
private TypedKey<string> GetStreamLinkKey(MusicPlatform platform, string id)
=> new($"music:stream:{platform}:{id}");
public async Task<string?> GetOrCreateStreamLink(
string id,
MusicPlatform platform,
Func<Task<(string StreamUrl, TimeSpan Expiry)>> streamUrlFactory)
{
var key = GetStreamLinkKey(platform, id);
var streamUrl = await _cache.GetOrDefaultAsync(key);
await _cache.RemoveAsync(key);
if (streamUrl == default)
{
(streamUrl, _) = await streamUrlFactory();
}
// make a new one for later use
_ = Task.Run(async () =>
{
(streamUrl, var expiry) = await streamUrlFactory();
await CacheStreamUrlAsync(id, platform, streamUrl, expiry);
});
return streamUrl;
}
public async Task CacheStreamUrlAsync(
string id,
MusicPlatform platform,
string url,
TimeSpan expiry)
=> await _cache.AddAsync(GetStreamLinkKey(platform, id), url, expiry);
// track data by id
private TypedKey<CachableTrackData> GetTrackDataKey(MusicPlatform platform, string id)
=> new($"music:track:{platform}:{id}");
public async Task CacheTrackDataAsync(ICachableTrackData data)
=> await _cache.AddAsync(GetTrackDataKey(data.Platform, data.Id), ToCachableTrackData(data));
private CachableTrackData ToCachableTrackData(ICachableTrackData data)
=> new CachableTrackData()
{
Id = data.Id,
Platform = data.Platform,
Thumbnail = data.Thumbnail,
Title = data.Title,
Url = data.Url,
};
public async Task<ICachableTrackData?> GetCachedDataByIdAsync(string id, MusicPlatform platform)
=> await _cache.GetOrDefaultAsync(GetTrackDataKey(platform, id));
// track data by query
private TypedKey<CachableTrackData> GetTrackDataQueryKey(MusicPlatform platform, string query)
=> new($"music:track:{platform}:q:{query}");
public async Task CacheTrackDataByQueryAsync(string query, ICachableTrackData data)
=> await Task.WhenAll(
_cache.AddAsync(GetTrackDataQueryKey(data.Platform, query), ToCachableTrackData(data)).AsTask(),
_cache.AddAsync(GetTrackDataKey(data.Platform, data.Id), ToCachableTrackData(data)).AsTask());
public async Task<ICachableTrackData?> GetCachedDataByQueryAsync(string query, MusicPlatform platform)
=> await _cache.GetOrDefaultAsync(GetTrackDataQueryKey(platform, query));
// playlist track ids by playlist id
private TypedKey<List<string>> GetPlaylistTracksCacheKey(string playlist, MusicPlatform platform)
=> new($"music:playlist_tracks:{platform}:{playlist}");
public async Task CachePlaylistTrackIdsAsync(string playlistId, MusicPlatform platform, IEnumerable<string> ids)
=> await _cache.AddAsync(GetPlaylistTracksCacheKey(playlistId, platform), ids.ToList());
public async Task<IReadOnlyCollection<string>> GetPlaylistTrackIdsAsync(string playlistId, MusicPlatform platform)
{
var result = await _cache.GetAsync(GetPlaylistTracksCacheKey(playlistId, platform));
if (result.TryGetValue(out var val))
return val;
return Array.Empty<string>();
}
// playlist id by query
private TypedKey<string> GetPlaylistCacheKey(string query, MusicPlatform platform)
=> new($"music:playlist_id:{platform}:{query}");
public async Task CachePlaylistIdByQueryAsync(string query, MusicPlatform platform, string playlistId)
=> await _cache.AddAsync(GetPlaylistCacheKey(query, platform), playlistId);
public async Task<string?> GetPlaylistIdByQueryAsync(string query, MusicPlatform platform)
=> await _cache.GetOrDefaultAsync(GetPlaylistCacheKey(query, platform));
}

View File

@@ -1,102 +0,0 @@
#nullable disable
using Ayu.Discord.Voice;
using Ayu.Discord.Voice.Models;
namespace NadekoBot.Modules.Music;
public sealed class VoiceProxy : IVoiceProxy
{
public enum VoiceProxyState
{
Created,
Started,
Stopped
}
private const int MAX_ERROR_COUNT = 20;
private const int DELAY_ON_ERROR_MILISECONDS = 200;
public VoiceProxyState State
=> gateway switch
{
{ Started: true, Stopped: false } => VoiceProxyState.Started,
{ Stopped: false } => VoiceProxyState.Created,
_ => VoiceProxyState.Stopped
};
private VoiceGateway gateway;
public VoiceProxy(VoiceGateway initial)
=> gateway = initial;
public bool SendPcmFrame(VoiceClient vc, Span<byte> data, int length)
{
try
{
var gw = gateway;
if (gw is null || gw.Stopped || !gw.Started)
return false;
vc.SendPcmFrame(gw, data, 0, length);
return true;
}
catch (Exception)
{
return false;
}
}
public async Task<bool> RunGatewayAction(Func<VoiceGateway, Task> action)
{
var errorCount = 0;
do
{
if (State == VoiceProxyState.Stopped)
break;
try
{
var gw = gateway;
if (gw is null || !gw.ConnectingFinished.Task.IsCompleted)
{
++errorCount;
await Task.Delay(DELAY_ON_ERROR_MILISECONDS);
Log.Debug("Gateway is not ready");
continue;
}
await action(gw);
errorCount = 0;
}
catch (Exception ex)
{
++errorCount;
await Task.Delay(DELAY_ON_ERROR_MILISECONDS);
Log.Debug(ex, "Error performing proxy gateway action");
}
} while (errorCount is > 0 and <= MAX_ERROR_COUNT);
return State != VoiceProxyState.Stopped && errorCount <= MAX_ERROR_COUNT;
}
public void SetGateway(VoiceGateway newGateway)
=> gateway = newGateway;
public Task StartSpeakingAsync()
=> RunGatewayAction(gw => gw.SendSpeakingAsync(VoiceSpeaking.State.Microphone));
public Task StopSpeakingAsync()
=> RunGatewayAction(gw => gw.SendSpeakingAsync(VoiceSpeaking.State.None));
public async Task StartGateway()
=> await gateway.Start();
public Task StopGateway()
{
if (gateway is { } gw)
return gw.StopAsync();
return Task.CompletedTask;
}
}

View File

@@ -1,123 +0,0 @@
using System.ComponentModel;
using System.Diagnostics;
using System.Text;
using Nadeko.Common;
namespace NadekoBot.Modules.Music.Resolvers;
public sealed class LocalTrackResolver : ILocalTrackResolver
{
private static readonly HashSet<string> _musicExtensions = new[]
{
".MP4", ".MP3", ".FLAC", ".OGG", ".WAV", ".WMA", ".WMV", ".AAC", ".MKV", ".WEBM", ".M4A", ".AA", ".AAX",
".ALAC", ".AIFF", ".MOV", ".FLV", ".OGG", ".M4V"
}.ToHashSet();
public async Task<ITrackInfo?> ResolveByQueryAsync(string query)
{
if (!File.Exists(query))
return null;
var trackDuration = await Ffprobe.GetTrackDurationAsync(query);
return new SimpleTrackInfo(Path.GetFileNameWithoutExtension(query),
$"https://google.com?q={Uri.EscapeDataString(Path.GetFileNameWithoutExtension(query))}",
"https://cdn.discordapp.com/attachments/155726317222887425/261850914783100928/1482522077_music.png",
trackDuration,
MusicPlatform.Local,
$"\"{Path.GetFullPath(query)}\"");
}
public async IAsyncEnumerable<ITrackInfo> ResolveDirectoryAsync(string dirPath)
{
DirectoryInfo dir;
try
{
dir = new(dirPath);
}
catch (Exception ex)
{
Log.Error(ex, "Specified directory {DirectoryPath} could not be opened", dirPath);
yield break;
}
var files = dir.EnumerateFiles()
.Where(x =>
{
if (!x.Attributes.HasFlag(FileAttributes.Hidden | FileAttributes.System)
&& _musicExtensions.Contains(x.Extension.ToUpperInvariant()))
return true;
return false;
})
.ToList();
var firstFile = files.FirstOrDefault()?.FullName;
if (firstFile is null)
yield break;
var firstData = await ResolveByQueryAsync(firstFile);
if (firstData is not null)
yield return firstData;
var fileChunks = files.Skip(1).Chunk(10);
foreach (var chunk in fileChunks)
{
var part = await chunk.Select(x => ResolveByQueryAsync(x.FullName)).WhenAll();
// nullable reference types being annoying
foreach (var p in part)
{
if (p is null)
continue;
yield return p;
}
}
}
}
public static class Ffprobe
{
public static async Task<TimeSpan> GetTrackDurationAsync(string query)
{
query = query.Replace("\"", "");
try
{
using var p = Process.Start(new ProcessStartInfo
{
FileName = "ffprobe",
Arguments =
$"-v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 -- \"{query}\"",
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
StandardOutputEncoding = Encoding.UTF8,
StandardErrorEncoding = Encoding.UTF8,
CreateNoWindow = true
});
if (p is null)
return TimeSpan.Zero;
var data = await p.StandardOutput.ReadToEndAsync();
if (double.TryParse(data, out var seconds))
return TimeSpan.FromSeconds(seconds);
var errorData = await p.StandardError.ReadToEndAsync();
if (!string.IsNullOrWhiteSpace(errorData))
Log.Warning("Ffprobe warning for file {FileName}: {ErrorMessage}", query, errorData);
return TimeSpan.Zero;
}
catch (Win32Exception)
{
Log.Warning("Ffprobe was likely not installed. Local song durations will show as (?)");
}
catch (Exception ex)
{
Log.Error(ex, "Unknown exception running ffprobe; {ErrorMessage}", ex.Message);
}
return TimeSpan.Zero;
}
}

View File

@@ -1,106 +0,0 @@
#nullable disable
using System.Text.RegularExpressions;
namespace NadekoBot.Modules.Music.Resolvers;
public class RadioResolver : IRadioResolver
{
private readonly Regex _plsRegex = new(@"File1=(?<url>.*?)\n", RegexOptions.Compiled);
private readonly Regex _m3URegex = new(@"(?<url>^[^#].*)", RegexOptions.Compiled | RegexOptions.Multiline);
private readonly Regex _asxRegex = new(@"<ref href=""(?<url>.*?)""", RegexOptions.Compiled);
private readonly Regex _xspfRegex = new(@"<location>(?<url>.*?)</location>", RegexOptions.Compiled);
public async Task<ITrackInfo> ResolveByQueryAsync(string query)
{
if (IsRadioLink(query))
query = await HandleStreamContainers(query);
return new SimpleTrackInfo(query.TrimTo(50),
query,
"https://cdn.discordapp.com/attachments/155726317222887425/261850925063340032/1482522097_radio.png",
TimeSpan.MaxValue,
MusicPlatform.Radio,
query);
}
public static bool IsRadioLink(string query)
=> (query.StartsWith("http", StringComparison.InvariantCulture)
|| query.StartsWith("ww", StringComparison.InvariantCulture))
&& (query.Contains(".pls") || query.Contains(".m3u") || query.Contains(".asx") || query.Contains(".xspf"));
private async Task<string> HandleStreamContainers(string query)
{
string file = null;
try
{
using var http = new HttpClient();
file = await http.GetStringAsync(query);
}
catch
{
return query;
}
if (query.Contains(".pls"))
{
try
{
var m = _plsRegex.Match(file);
var res = m.Groups["url"]?.ToString();
return res?.Trim();
}
catch
{
Log.Warning("Failed reading .pls:\n{PlsFile}", file);
return null;
}
}
if (query.Contains(".m3u"))
{
try
{
var m = _m3URegex.Match(file);
var res = m.Groups["url"]?.ToString();
return res?.Trim();
}
catch
{
Log.Warning("Failed reading .m3u:\n{M3uFile}", file);
return null;
}
}
if (query.Contains(".asx"))
{
try
{
var m = _asxRegex.Match(file);
var res = m.Groups["url"]?.ToString();
return res?.Trim();
}
catch
{
Log.Warning("Failed reading .asx:\n{AsxFile}", file);
return null;
}
}
if (query.Contains(".xspf"))
{
try
{
var m = _xspfRegex.Match(file);
var res = m.Groups["url"]?.ToString();
return res?.Trim();
}
catch
{
Log.Warning("Failed reading .xspf:\n{XspfFile}", file);
return null;
}
}
return query;
}
}

View File

@@ -1,85 +0,0 @@
using Newtonsoft.Json.Linq;
using System.Runtime.CompilerServices;
using System.Text.RegularExpressions;
using Nadeko.Common;
namespace NadekoBot.Modules.Music.Resolvers;
public sealed class SoundcloudResolver : ISoundcloudResolver
{
private readonly SoundCloudApiService _sc;
private readonly ITrackCacher _trackCacher;
private readonly IHttpClientFactory _httpFactory;
public SoundcloudResolver(SoundCloudApiService sc, ITrackCacher trackCacher, IHttpClientFactory httpFactory)
{
_sc = sc;
_trackCacher = trackCacher;
_httpFactory = httpFactory;
}
public bool IsSoundCloudLink(string url)
=> Regex.IsMatch(url, "(.*)(soundcloud.com|snd.sc)(.*)");
public async IAsyncEnumerable<ITrackInfo> ResolvePlaylistAsync(string playlist)
{
playlist = Uri.EscapeDataString(playlist);
using var http = _httpFactory.CreateClient();
var responseString = await http.GetStringAsync($"https://scapi.nadeko.bot/resolve?url={playlist}");
var scvids = JObject.Parse(responseString)["tracks"]?.ToObject<SoundCloudVideo[]>();
if (scvids is null)
yield break;
foreach (var videosChunk in scvids.Where(x => x.Streamable is true).Chunk(5))
{
var cachableTracks = videosChunk.Select(VideoModelToCachedData).ToList();
await cachableTracks.Select(_trackCacher.CacheTrackDataAsync).WhenAll();
foreach (var info in cachableTracks.Select(CachableDataToTrackInfo))
yield return info;
}
}
private ICachableTrackData VideoModelToCachedData(SoundCloudVideo svideo)
=> new CachableTrackData
{
Title = svideo.FullName,
Url = svideo.TrackLink,
Thumbnail = svideo.ArtworkUrl,
TotalDurationMs = svideo.Duration,
Id = svideo.Id.ToString(),
Platform = MusicPlatform.SoundCloud
};
private ITrackInfo CachableDataToTrackInfo(ICachableTrackData trackData)
=> new SimpleTrackInfo(trackData.Title,
trackData.Url,
trackData.Thumbnail,
trackData.Duration,
trackData.Platform,
GetStreamUrl(trackData.Id));
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private string GetStreamUrl(string trackId)
=> $"https://api.soundcloud.com/tracks/{trackId}/stream?client_id=368b0c85751007cd588d869d3ae61ac0";
public async Task<ITrackInfo?> ResolveByQueryAsync(string query)
{
var cached = await _trackCacher.GetCachedDataByQueryAsync(query, MusicPlatform.SoundCloud);
if (cached is not null)
return CachableDataToTrackInfo(cached);
var svideo = !IsSoundCloudLink(query)
? await _sc.GetVideoByQueryAsync(query)
: await _sc.ResolveVideoAsync(query);
if (svideo is null)
return null;
var cachableData = VideoModelToCachedData(svideo);
await _trackCacher.CacheTrackDataByQueryAsync(query, cachableData);
return CachableDataToTrackInfo(cachableData);
}
}

View File

@@ -1,56 +0,0 @@
namespace NadekoBot.Modules.Music;
public sealed class TrackResolveProvider : ITrackResolveProvider
{
private readonly IYoutubeResolver _ytResolver;
private readonly ILocalTrackResolver _localResolver;
private readonly ISoundcloudResolver _soundcloudResolver;
private readonly IRadioResolver _radioResolver;
public TrackResolveProvider(
IYoutubeResolver ytResolver,
ILocalTrackResolver localResolver,
ISoundcloudResolver soundcloudResolver,
IRadioResolver radioResolver)
{
_ytResolver = ytResolver;
_localResolver = localResolver;
_soundcloudResolver = soundcloudResolver;
_radioResolver = radioResolver;
}
public Task<ITrackInfo?> QuerySongAsync(string query, MusicPlatform? forcePlatform)
{
switch (forcePlatform)
{
case MusicPlatform.Radio:
return _radioResolver.ResolveByQueryAsync(query);
case MusicPlatform.Youtube:
return _ytResolver.ResolveByQueryAsync(query);
case MusicPlatform.Local:
return _localResolver.ResolveByQueryAsync(query);
case MusicPlatform.SoundCloud:
return _soundcloudResolver.ResolveByQueryAsync(query);
case null:
var match = _ytResolver.YtVideoIdRegex.Match(query);
if (match.Success)
return _ytResolver.ResolveByIdAsync(match.Groups["id"].Value);
else if (_soundcloudResolver.IsSoundCloudLink(query))
return _soundcloudResolver.ResolveByQueryAsync(query);
else if (Uri.TryCreate(query, UriKind.Absolute, out var uri) && uri.IsFile)
return _localResolver.ResolveByQueryAsync(uri.AbsolutePath);
else if (IsRadioLink(query))
return _radioResolver.ResolveByQueryAsync(query);
else
return _ytResolver.ResolveByQueryAsync(query, false);
default:
Log.Error("Unsupported platform: {MusicPlatform}", forcePlatform);
return Task.FromResult<ITrackInfo?>(null);
}
}
public static bool IsRadioLink(string query)
=> (query.StartsWith("http", StringComparison.InvariantCulture)
|| query.StartsWith("ww", StringComparison.InvariantCulture))
&& (query.Contains(".pls") || query.Contains(".m3u") || query.Contains(".asx") || query.Contains(".xspf"));
}

View File

@@ -1,315 +0,0 @@
using System.Globalization;
using System.Text.RegularExpressions;
using NadekoBot.Modules.Searches;
namespace NadekoBot.Modules.Music;
public sealed class YtdlYoutubeResolver : IYoutubeResolver
{
private static readonly string[] _durationFormats =
{
"ss", "m\\:ss", "mm\\:ss", "h\\:mm\\:ss", "hh\\:mm\\:ss", "hhh\\:mm\\:ss"
};
private static readonly Regex _expiryRegex = new(@"(?:[\?\&]expire\=(?<timestamp>\d+))");
private static readonly Regex _simplePlaylistRegex = new(@"&list=(?<id>[\w\-]{12,})", RegexOptions.Compiled);
public Regex YtVideoIdRegex { get; } =
new(@"(?:youtube\.com\/\S*(?:(?:\/e(?:mbed))?\/|watch\?(?:\S*?&?v\=))|youtu\.be\/)(?<id>[a-zA-Z0-9_-]{6,11})",
RegexOptions.Compiled);
private readonly ITrackCacher _trackCacher;
private readonly YtdlOperation _ytdlPlaylistOperation;
private readonly YtdlOperation _ytdlIdOperation;
private readonly YtdlOperation _ytdlSearchOperation;
private readonly IGoogleApiService _google;
public YtdlYoutubeResolver(ITrackCacher trackCacher, IGoogleApiService google, SearchesConfigService scs)
{
_trackCacher = trackCacher;
_google = google;
_ytdlPlaylistOperation = new("-4 "
+ "--geo-bypass "
+ "--encoding UTF8 "
+ "-f bestaudio "
+ "-e "
+ "--get-url "
+ "--get-id "
+ "--get-thumbnail "
+ "--get-duration "
+ "--no-check-certificate "
+ "-i "
+ "--yes-playlist "
+ "-- \"{0}\"", scs.Data.YtProvider != YoutubeSearcher.Ytdl);
_ytdlIdOperation = new("-4 "
+ "--geo-bypass "
+ "--encoding UTF8 "
+ "-f bestaudio "
+ "-e "
+ "--get-url "
+ "--get-id "
+ "--get-thumbnail "
+ "--get-duration "
+ "--no-check-certificate "
+ "-- \"{0}\"", scs.Data.YtProvider != YoutubeSearcher.Ytdl);
_ytdlSearchOperation = new("-4 "
+ "--geo-bypass "
+ "--encoding UTF8 "
+ "-f bestaudio "
+ "-e "
+ "--get-url "
+ "--get-id "
+ "--get-thumbnail "
+ "--get-duration "
+ "--no-check-certificate "
+ "--default-search "
+ "\"ytsearch:\" -- \"{0}\"", scs.Data.YtProvider != YoutubeSearcher.Ytdl);
}
private YtTrackData ResolveYtdlData(string ytdlOutputString)
{
if (string.IsNullOrWhiteSpace(ytdlOutputString))
return default;
var dataArray = ytdlOutputString.Trim().Split('\n');
if (dataArray.Length < 5)
{
Log.Information("Not enough data received: {YtdlData}", ytdlOutputString);
return default;
}
if (!TimeSpan.TryParseExact(dataArray[4], _durationFormats, CultureInfo.InvariantCulture, out var time))
time = TimeSpan.Zero;
var thumbnail = Uri.IsWellFormedUriString(dataArray[3], UriKind.Absolute) ? dataArray[3].Trim() : string.Empty;
return new(dataArray[0], dataArray[1], thumbnail, dataArray[2], time);
}
private ITrackInfo DataToInfo(in YtTrackData trackData)
=> new RemoteTrackInfo(
trackData.Id,
trackData.Title,
$"https://youtube.com/watch?v={trackData.Id}",
trackData.Thumbnail,
trackData.Duration,
MusicPlatform.Youtube,
CreateCacherFactory(trackData.Id));
private Func<Task<string?>> CreateCacherFactory(string id)
=> () => _trackCacher.GetOrCreateStreamLink(id,
MusicPlatform.Youtube,
async () => await ExtractNewStreamUrlAsync(id));
private static TimeSpan GetExpiry(string streamUrl)
{
var match = _expiryRegex.Match(streamUrl);
if (match.Success && double.TryParse(match.Groups["timestamp"].ToString(), out var timestamp))
{
var realExpiry = timestamp.ToUnixTimestamp() - DateTime.UtcNow;
if (realExpiry > TimeSpan.FromMinutes(60))
return realExpiry.Subtract(TimeSpan.FromMinutes(30));
return realExpiry;
}
return TimeSpan.FromHours(1);
}
private async Task<(string StreamUrl, TimeSpan Expiry)> ExtractNewStreamUrlAsync(string id)
{
var data = await _ytdlIdOperation.GetDataAsync(id);
var trackInfo = ResolveYtdlData(data);
if (string.IsNullOrWhiteSpace(trackInfo.StreamUrl))
return default;
return (trackInfo.StreamUrl!, GetExpiry(trackInfo.StreamUrl!));
}
public async Task<ITrackInfo?> ResolveByIdAsync(string id)
{
id = id.Trim();
var cachedData = await _trackCacher.GetCachedDataByIdAsync(id, MusicPlatform.Youtube);
if (cachedData is null)
{
Log.Information("Resolving youtube track by Id: {YoutubeId}", id);
var data = await _ytdlIdOperation.GetDataAsync(id);
var trackInfo = ResolveYtdlData(data);
if (string.IsNullOrWhiteSpace(trackInfo.Title))
return default;
var toReturn = DataToInfo(in trackInfo);
await Task.WhenAll(_trackCacher.CacheTrackDataAsync(toReturn.ToCachedData(id)),
CacheStreamUrlAsync(trackInfo));
return toReturn;
}
return DataToInfo(new(cachedData.Title, cachedData.Id, cachedData.Thumbnail, null, cachedData.Duration));
}
private Task CacheStreamUrlAsync(YtTrackData trackInfo)
=> _trackCacher.CacheStreamUrlAsync(trackInfo.Id,
MusicPlatform.Youtube,
trackInfo.StreamUrl!,
GetExpiry(trackInfo.StreamUrl!));
public async IAsyncEnumerable<ITrackInfo> ResolveTracksByPlaylistIdAsync(string playlistId)
{
Log.Information("Resolving youtube tracks from playlist: {PlaylistId}", playlistId);
var count = 0;
var ids = await _trackCacher.GetPlaylistTrackIdsAsync(playlistId, MusicPlatform.Youtube);
if (ids.Count > 0)
{
foreach (var id in ids)
{
var trackInfo = await ResolveByIdAsync(id);
if (trackInfo is null)
continue;
yield return trackInfo;
}
yield break;
}
var data = string.Empty;
var trackIds = new List<string>();
await foreach (var line in _ytdlPlaylistOperation.EnumerateDataAsync(playlistId))
{
data += line;
if (++count == 5)
{
var trackData = ResolveYtdlData(data);
data = string.Empty;
count = 0;
if (string.IsNullOrWhiteSpace(trackData.Id))
continue;
var info = DataToInfo(in trackData);
await Task.WhenAll(_trackCacher.CacheTrackDataAsync(info.ToCachedData(trackData.Id)),
CacheStreamUrlAsync(trackData));
trackIds.Add(trackData.Id);
yield return info;
}
else
data += Environment.NewLine;
}
await _trackCacher.CachePlaylistTrackIdsAsync(playlistId, MusicPlatform.Youtube, trackIds);
}
public async IAsyncEnumerable<ITrackInfo> ResolveTracksFromPlaylistAsync(string query)
{
string? playlistId;
// try to match playlist id inside the query, if a playlist url has been queried
var match = _simplePlaylistRegex.Match(query);
if (match.Success)
{
// if it's a success, just return from that playlist using the id
playlistId = match.Groups["id"].ToString();
await foreach (var track in ResolveTracksByPlaylistIdAsync(playlistId))
yield return track;
yield break;
}
// if a query is a search term, try the cache
playlistId = await _trackCacher.GetPlaylistIdByQueryAsync(query, MusicPlatform.Youtube);
if (playlistId is null)
{
// if it's not in the cache
// find playlist id by keyword using google api
try
{
var playlistIds = await _google.GetPlaylistIdsByKeywordsAsync(query);
playlistId = playlistIds.FirstOrDefault();
}
catch (Exception ex)
{
Log.Warning(ex, "Error Getting playlist id via GoogleApi");
}
// if query is not a playlist url
// and query result is not in the cache
// and api returns no values
// it means invalid input has been used,
// or google api key is not provided
if (playlistId is null)
yield break;
}
// cache the query -> playlist id for fast future lookup
await _trackCacher.CachePlaylistIdByQueryAsync(query, MusicPlatform.Youtube, playlistId);
await foreach (var track in ResolveTracksByPlaylistIdAsync(playlistId))
yield return track;
}
public Task<ITrackInfo?> ResolveByQueryAsync(string query)
=> ResolveByQueryAsync(query, true);
public async Task<ITrackInfo?> ResolveByQueryAsync(string query, bool tryResolving)
{
if (tryResolving)
{
var match = YtVideoIdRegex.Match(query);
if (match.Success)
return await ResolveByIdAsync(match.Groups["id"].Value);
}
Log.Information("Resolving youtube song by search term: {YoutubeQuery}", query);
var cachedData = await _trackCacher.GetCachedDataByQueryAsync(query, MusicPlatform.Youtube);
if (cachedData is null || string.IsNullOrWhiteSpace(cachedData.Title))
{
var stringData = await _ytdlSearchOperation.GetDataAsync(query);
var trackData = ResolveYtdlData(stringData);
var trackInfo = DataToInfo(trackData);
await Task.WhenAll(_trackCacher.CacheTrackDataByQueryAsync(query, trackInfo.ToCachedData(trackData.Id)),
CacheStreamUrlAsync(trackData));
return trackInfo;
}
return DataToInfo(new(cachedData.Title, cachedData.Id, cachedData.Thumbnail, null, cachedData.Duration));
}
private readonly struct YtTrackData
{
public readonly string Title;
public readonly string Id;
public readonly string Thumbnail;
public readonly string? StreamUrl;
public readonly TimeSpan Duration;
public YtTrackData(
string title,
string id,
string thumbnail,
string? streamUrl,
TimeSpan duration)
{
Title = title.Trim();
Id = id.Trim();
Thumbnail = thumbnail;
StreamUrl = streamUrl;
Duration = duration;
}
}
}

View File

@@ -1,144 +0,0 @@
#nullable disable
using Nadeko.Common;
using NadekoBot.Modules.Permissions.Services;
using NadekoBot.Services.Database.Models;
namespace NadekoBot.Modules.Permissions;
public partial class Permissions
{
[Group]
public partial class BlacklistCommands : NadekoModule<BlacklistService>
{
private readonly DiscordSocketClient _client;
public BlacklistCommands(DiscordSocketClient client)
=> _client = client;
private async Task ListBlacklistInternal(string title, BlacklistType type, int page = 0)
{
if (page < 0)
throw new ArgumentOutOfRangeException(nameof(page));
var list = _service.GetBlacklist();
var items = await list.Where(x => x.Type == type)
.Select(async i =>
{
try
{
return i.Type switch
{
BlacklistType.Channel => Format.Code(i.ItemId.ToString())
+ " "
+ (_client.GetChannel(i.ItemId)?.ToString()
?? ""),
BlacklistType.User => Format.Code(i.ItemId.ToString())
+ " "
+ ((await _client.Rest.GetUserAsync(i.ItemId))
?.ToString()
?? ""),
BlacklistType.Server => Format.Code(i.ItemId.ToString())
+ " "
+ (_client.GetGuild(i.ItemId)?.ToString() ?? ""),
_ => Format.Code(i.ItemId.ToString())
};
}
catch
{
Log.Warning("Can't get {BlacklistType} [{BlacklistItemId}]",
i.Type,
i.ItemId);
return Format.Code(i.ItemId.ToString());
}
})
.WhenAll();
await ctx.SendPaginatedConfirmAsync(page,
curPage =>
{
var pageItems = items.Skip(10 * curPage).Take(10).ToList();
if (pageItems.Count == 0)
return _eb.Create().WithOkColor().WithTitle(title).WithDescription(GetText(strs.empty_page));
return _eb.Create().WithTitle(title).WithDescription(pageItems.Join('\n')).WithOkColor();
},
items.Length,
10);
}
[Cmd]
[OwnerOnly]
public Task UserBlacklist(int page = 1)
{
if (--page < 0)
return Task.CompletedTask;
return ListBlacklistInternal(GetText(strs.blacklisted_users), BlacklistType.User, page);
}
[Cmd]
[OwnerOnly]
public Task ChannelBlacklist(int page = 1)
{
if (--page < 0)
return Task.CompletedTask;
return ListBlacklistInternal(GetText(strs.blacklisted_channels), BlacklistType.Channel, page);
}
[Cmd]
[OwnerOnly]
public Task ServerBlacklist(int page = 1)
{
if (--page < 0)
return Task.CompletedTask;
return ListBlacklistInternal(GetText(strs.blacklisted_servers), BlacklistType.Server, page);
}
[Cmd]
[OwnerOnly]
public Task UserBlacklist(AddRemove action, ulong id)
=> Blacklist(action, id, BlacklistType.User);
[Cmd]
[OwnerOnly]
public Task UserBlacklist(AddRemove action, IUser usr)
=> Blacklist(action, usr.Id, BlacklistType.User);
[Cmd]
[OwnerOnly]
public Task ChannelBlacklist(AddRemove action, ulong id)
=> Blacklist(action, id, BlacklistType.Channel);
[Cmd]
[OwnerOnly]
public Task ServerBlacklist(AddRemove action, ulong id)
=> Blacklist(action, id, BlacklistType.Server);
[Cmd]
[OwnerOnly]
public Task ServerBlacklist(AddRemove action, IGuild guild)
=> Blacklist(action, guild.Id, BlacklistType.Server);
private async Task Blacklist(AddRemove action, ulong id, BlacklistType type)
{
if (action == AddRemove.Add)
_service.Blacklist(type, id);
else
_service.UnBlacklist(type, id);
if (action == AddRemove.Add)
{
await ReplyConfirmLocalizedAsync(strs.blacklisted(Format.Code(type.ToString()),
Format.Code(id.ToString())));
}
else
{
await ReplyConfirmLocalizedAsync(strs.unblacklisted(Format.Code(type.ToString()),
Format.Code(id.ToString())));
}
}
}
}

View File

@@ -1,131 +0,0 @@
#nullable disable
using Microsoft.EntityFrameworkCore;
using NadekoBot.Common.ModuleBehaviors;
using NadekoBot.Db;
using NadekoBot.Services.Database.Models;
namespace NadekoBot.Modules.Permissions.Services;
public sealed class BlacklistService : IExecOnMessage
{
public int Priority
=> int.MaxValue;
private readonly DbService _db;
private readonly IPubSub _pubSub;
private readonly IBotCredentials _creds;
private IReadOnlyList<BlacklistEntry> blacklist;
private readonly TypedKey<BlacklistEntry[]> _blPubKey = new("blacklist.reload");
public BlacklistService(DbService db, IPubSub pubSub, IBotCredentials creds)
{
_db = db;
_pubSub = pubSub;
_creds = creds;
Reload(false);
_pubSub.Sub(_blPubKey, OnReload);
}
private ValueTask OnReload(BlacklistEntry[] newBlacklist)
{
blacklist = newBlacklist;
return default;
}
public Task<bool> ExecOnMessageAsync(IGuild guild, IUserMessage usrMsg)
{
foreach (var bl in blacklist)
{
if (guild is not null && bl.Type == BlacklistType.Server && bl.ItemId == guild.Id)
{
Log.Information("Blocked input from blacklisted guild: {GuildName} [{GuildId}]", guild.Name, guild.Id);
return Task.FromResult(true);
}
if (bl.Type == BlacklistType.Channel && bl.ItemId == usrMsg.Channel.Id)
{
Log.Information("Blocked input from blacklisted channel: {ChannelName} [{ChannelId}]",
usrMsg.Channel.Name,
usrMsg.Channel.Id);
return Task.FromResult(true);
}
if (bl.Type == BlacklistType.User && bl.ItemId == usrMsg.Author.Id)
{
Log.Information("Blocked input from blacklisted user: {UserName} [{UserId}]",
usrMsg.Author.ToString(),
usrMsg.Author.Id);
return Task.FromResult(true);
}
}
return Task.FromResult(false);
}
public IReadOnlyList<BlacklistEntry> GetBlacklist()
=> blacklist;
public void Reload(bool publish = true)
{
using var uow = _db.GetDbContext();
var toPublish = uow.Blacklist.AsNoTracking().ToArray();
blacklist = toPublish;
if (publish)
_pubSub.Pub(_blPubKey, toPublish);
}
public void Blacklist(BlacklistType type, ulong id)
{
if (_creds.OwnerIds.Contains(id))
return;
using var uow = _db.GetDbContext();
var item = new BlacklistEntry
{
ItemId = id,
Type = type
};
uow.Blacklist.Add(item);
uow.SaveChanges();
Reload();
}
public void UnBlacklist(BlacklistType type, ulong id)
{
using var uow = _db.GetDbContext();
var toRemove = uow.Blacklist.FirstOrDefault(bi => bi.ItemId == id && bi.Type == type);
if (toRemove is not null)
uow.Blacklist.Remove(toRemove);
uow.SaveChanges();
Reload();
}
public void BlacklistUsers(IReadOnlyCollection<ulong> toBlacklist)
{
using (var uow = _db.GetDbContext())
{
var bc = uow.Blacklist;
//blacklist the users
bc.AddRange(toBlacklist.Select(x => new BlacklistEntry
{
ItemId = x,
Type = BlacklistType.User
}));
//clear their currencies
uow.DiscordUser.RemoveFromMany(toBlacklist);
uow.SaveChanges();
}
Reload();
}
}

View File

@@ -1,15 +0,0 @@
#nullable disable
using NadekoBot.Common.TypeReaders;
using static NadekoBot.Common.TypeReaders.TypeReaderResult;
namespace NadekoBot.Modules.Permissions;
public class CleverbotResponseCmdCdTypeReader : NadekoTypeReader<CleverBotResponseStr>
{
public override ValueTask<TypeReaderResult<CleverBotResponseStr>> ReadAsync(
ICommandContext ctx,
string input)
=> input.ToLowerInvariant() == CleverBotResponseStr.CLEVERBOT_RESPONSE
? new(FromSuccess(new CleverBotResponseStr()))
: new(FromError<CleverBotResponseStr>(CommandError.ParseFailed, "Not a valid cleverbot"));
}

View File

@@ -1,143 +0,0 @@
using Microsoft.EntityFrameworkCore;
using NadekoBot.Common.ModuleBehaviors;
using NadekoBot.Db;
namespace NadekoBot.Modules.Permissions.Services;
public sealed class CmdCdService : IExecPreCommand, IReadyExecutor, INService
{
private readonly DbService _db;
private readonly ConcurrentDictionary<ulong, ConcurrentDictionary<string, int>> _settings = new();
private readonly ConcurrentDictionary<(ulong, string), ConcurrentDictionary<ulong, DateTime>> _activeCooldowns =
new();
public int Priority => 0;
public CmdCdService(Bot bot, DbService db)
{
_db = db;
_settings = bot
.AllGuildConfigs
.ToDictionary(x => x.GuildId, x => x.CommandCooldowns
.DistinctBy(x => x.CommandName.ToLowerInvariant())
.ToDictionary(c => c.CommandName, c => c.Seconds)
.ToConcurrent())
.ToConcurrent();
}
public Task<bool> ExecPreCommandAsync(ICommandContext context, string moduleName, CommandInfo command)
=> TryBlock(context.Guild, context.User, command.Name.ToLowerInvariant());
public Task<bool> TryBlock(IGuild? guild, IUser user, string commandName)
{
if (guild is null)
return Task.FromResult(false);
if (!_settings.TryGetValue(guild.Id, out var cooldownSettings))
return Task.FromResult(false);
if (!cooldownSettings.TryGetValue(commandName, out var cdSeconds))
return Task.FromResult(false);
var cooldowns = _activeCooldowns.GetOrAdd(
(guild.Id, commandName),
static _ => new());
// if user is not already on cooldown, add
if (cooldowns.TryAdd(user.Id, DateTime.UtcNow))
{
return Task.FromResult(false);
}
// if there is an entry, maybe it expired. Try to check if it expired and don't fail if it did
// - just update
if (cooldowns.TryGetValue(user.Id, out var oldValue))
{
var diff = DateTime.UtcNow - oldValue;
if (diff.TotalSeconds > cdSeconds)
{
if (cooldowns.TryUpdate(user.Id, DateTime.UtcNow, oldValue))
return Task.FromResult(false);
}
}
return Task.FromResult(true);
}
public async Task OnReadyAsync()
{
using var timer = new PeriodicTimer(TimeSpan.FromHours(1));
while (await timer.WaitForNextTickAsync())
{
// once per hour delete expired entries
foreach (var ((guildId, commandName), dict) in _activeCooldowns)
{
// if this pair no longer has associated config, that means it has been removed.
// remove all cooldowns
if (!_settings.TryGetValue(guildId, out var inner)
|| !inner.TryGetValue(commandName, out var cdSeconds))
{
_activeCooldowns.Remove((guildId, commandName), out _);
continue;
}
Cleanup(dict, cdSeconds);
}
}
}
private void Cleanup(ConcurrentDictionary<ulong, DateTime> dict, int cdSeconds)
{
var now = DateTime.UtcNow;
foreach (var (key, _) in dict.Where(x => (now - x.Value).TotalSeconds > cdSeconds).ToArray())
{
dict.TryRemove(key, out _);
}
}
public void ClearCooldowns(ulong guildId, string cmdName)
{
if (_settings.TryGetValue(guildId, out var dict))
dict.TryRemove(cmdName, out _);
_activeCooldowns.TryRemove((guildId, cmdName), out _);
using var ctx = _db.GetDbContext();
var gc = ctx.GuildConfigsForId(guildId, x => x.Include(x => x.CommandCooldowns));
gc.CommandCooldowns.RemoveWhere(x => x.CommandName == cmdName);
ctx.SaveChanges();
}
public void AddCooldown(ulong guildId, string name, int secs)
{
if (secs <= 0)
throw new ArgumentOutOfRangeException(nameof(secs));
var sett = _settings.GetOrAdd(guildId, static _ => new());
sett[name] = secs;
// force cleanup
if (_activeCooldowns.TryGetValue((guildId, name), out var dict))
Cleanup(dict, secs);
using var ctx = _db.GetDbContext();
var gc = ctx.GuildConfigsForId(guildId, x => x.Include(x => x.CommandCooldowns));
gc.CommandCooldowns.RemoveWhere(x => x.CommandName == name);
gc.CommandCooldowns.Add(new()
{
Seconds = secs,
CommandName = name
});
ctx.SaveChanges();
}
public IReadOnlyCollection<(string CommandName, int Seconds)> GetCommandCooldowns(ulong guildId)
{
if (!_settings.TryGetValue(guildId, out var dict))
return Array.Empty<(string, int)>();
return dict.Select(x => (x.Key, x.Value)).ToArray();
}
}

View File

@@ -1,105 +0,0 @@
#nullable disable
using Humanizer.Localisation;
using Microsoft.EntityFrameworkCore;
using NadekoBot.Common.TypeReaders;
using NadekoBot.Db;
using NadekoBot.Modules.Permissions.Services;
using NadekoBot.Services.Database.Models;
namespace NadekoBot.Modules.Permissions;
public partial class Permissions
{
[Group]
public partial class CmdCdsCommands : NadekoModule
{
private readonly DbService _db;
private readonly CmdCdService _service;
public CmdCdsCommands(CmdCdService service, DbService db)
{
_service = service;
_db = db;
}
private async Task CmdCooldownInternal(string cmdName, int secs)
{
var channel = (ITextChannel)ctx.Channel;
if (secs is < 0 or > 3600)
{
await ReplyErrorLocalizedAsync(strs.invalid_second_param_between(0, 3600));
return;
}
var name = cmdName.ToLowerInvariant();
await using (var uow = _db.GetDbContext())
{
var config = uow.GuildConfigsForId(channel.Guild.Id, set => set.Include(gc => gc.CommandCooldowns));
var toDelete = config.CommandCooldowns.FirstOrDefault(cc => cc.CommandName == name);
if (toDelete is not null)
uow.Set<CommandCooldown>().Remove(toDelete);
if (secs != 0)
{
var cc = new CommandCooldown
{
CommandName = name,
Seconds = secs
};
config.CommandCooldowns.Add(cc);
_service.AddCooldown(channel.Guild.Id, name, secs);
}
await uow.SaveChangesAsync();
}
if (secs == 0)
{
_service.ClearCooldowns(ctx.Guild.Id, cmdName);
await ReplyConfirmLocalizedAsync(strs.cmdcd_cleared(Format.Bold(name)));
}
else
await ReplyConfirmLocalizedAsync(strs.cmdcd_add(Format.Bold(name), Format.Bold(secs.ToString())));
}
[Cmd]
[RequireContext(ContextType.Guild)]
[Priority(0)]
public Task CmdCooldown(CleverBotResponseStr command, int secs)
=> CmdCooldownInternal(CleverBotResponseStr.CLEVERBOT_RESPONSE, secs);
[Cmd]
[RequireContext(ContextType.Guild)]
[Priority(1)]
public Task CmdCooldown(CommandOrExprInfo command, int secs)
=> CmdCooldownInternal(command.Name, secs);
[Cmd]
[RequireContext(ContextType.Guild)]
public async Task AllCmdCooldowns(int page = 1)
{
if (--page < 0)
return;
var channel = (ITextChannel)ctx.Channel;
var localSet = _service.GetCommandCooldowns(ctx.Guild.Id);
if (!localSet.Any())
await ReplyConfirmLocalizedAsync(strs.cmdcd_none);
else
{
await ctx.SendPaginatedConfirmAsync(page, curPage =>
{
var items = localSet.Skip(curPage * 15)
.Take(15)
.Select(x => $"{Format.Code(x.CommandName)}: {x.Seconds.Seconds().Humanize(maxUnit: TimeUnit.Second, culture: Culture)}");
return _eb.Create(ctx)
.WithOkColor()
.WithDescription(items.Join("\n"));
}, localSet.Count, 15);
}
}
}
}

View File

@@ -1,324 +0,0 @@
#nullable disable
using Microsoft.EntityFrameworkCore;
using NadekoBot.Db;
using NadekoBot.Modules.Permissions.Services;
using NadekoBot.Services.Database.Models;
namespace NadekoBot.Modules.Permissions;
public partial class Permissions
{
[Group]
public partial class FilterCommands : NadekoModule<FilterService>
{
private readonly DbService _db;
public FilterCommands(DbService db)
=> _db = db;
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.Administrator)]
public async Task FwClear()
{
_service.ClearFilteredWords(ctx.Guild.Id);
await ReplyConfirmLocalizedAsync(strs.fw_cleared);
}
[Cmd]
[RequireContext(ContextType.Guild)]
public async Task FilterList()
{
var embed = _eb.Create(ctx)
.WithOkColor()
.WithTitle("Server filter settings");
var config = await _service.GetFilterSettings(ctx.Guild.Id);
string GetEnabledEmoji(bool value)
=> value ? "\\🟢" : "\\🔴";
async Task<string> GetChannelListAsync(IReadOnlyCollection<ulong> channels)
{
var toReturn = (await channels
.Select(async cid =>
{
var ch = await ctx.Guild.GetChannelAsync(cid);
return ch is null
? $"{cid} *missing*"
: $"<#{cid}>";
})
.WhenAll())
.Join('\n');
if (string.IsNullOrWhiteSpace(toReturn))
return GetText(strs.no_channel_found);
return toReturn;
}
embed.AddField($"{GetEnabledEmoji(config.FilterLinksEnabled)} Filter Links",
await GetChannelListAsync(config.FilterLinksChannels));
embed.AddField($"{GetEnabledEmoji(config.FilterInvitesEnabled)} Filter Invites",
await GetChannelListAsync(config.FilterInvitesChannels));
await ctx.Channel.EmbedAsync(embed);
}
[Cmd]
[RequireContext(ContextType.Guild)]
public async Task SrvrFilterInv()
{
var channel = (ITextChannel)ctx.Channel;
bool enabled;
await using (var uow = _db.GetDbContext())
{
var config = uow.GuildConfigsForId(channel.Guild.Id, set => set);
enabled = config.FilterInvites = !config.FilterInvites;
await uow.SaveChangesAsync();
}
if (enabled)
{
_service.InviteFilteringServers.Add(channel.Guild.Id);
await ReplyConfirmLocalizedAsync(strs.invite_filter_server_on);
}
else
{
_service.InviteFilteringServers.TryRemove(channel.Guild.Id);
await ReplyConfirmLocalizedAsync(strs.invite_filter_server_off);
}
}
[Cmd]
[RequireContext(ContextType.Guild)]
public async Task ChnlFilterInv()
{
var channel = (ITextChannel)ctx.Channel;
FilterChannelId removed;
await using (var uow = _db.GetDbContext())
{
var config = uow.GuildConfigsForId(channel.Guild.Id,
set => set.Include(gc => gc.FilterInvitesChannelIds));
var match = new FilterChannelId
{
ChannelId = channel.Id
};
removed = config.FilterInvitesChannelIds.FirstOrDefault(fc => fc.Equals(match));
if (removed is null)
config.FilterInvitesChannelIds.Add(match);
else
uow.Remove(removed);
await uow.SaveChangesAsync();
}
if (removed is null)
{
_service.InviteFilteringChannels.Add(channel.Id);
await ReplyConfirmLocalizedAsync(strs.invite_filter_channel_on);
}
else
{
_service.InviteFilteringChannels.TryRemove(channel.Id);
await ReplyConfirmLocalizedAsync(strs.invite_filter_channel_off);
}
}
[Cmd]
[RequireContext(ContextType.Guild)]
public async Task SrvrFilterLin()
{
var channel = (ITextChannel)ctx.Channel;
bool enabled;
await using (var uow = _db.GetDbContext())
{
var config = uow.GuildConfigsForId(channel.Guild.Id, set => set);
enabled = config.FilterLinks = !config.FilterLinks;
await uow.SaveChangesAsync();
}
if (enabled)
{
_service.LinkFilteringServers.Add(channel.Guild.Id);
await ReplyConfirmLocalizedAsync(strs.link_filter_server_on);
}
else
{
_service.LinkFilteringServers.TryRemove(channel.Guild.Id);
await ReplyConfirmLocalizedAsync(strs.link_filter_server_off);
}
}
[Cmd]
[RequireContext(ContextType.Guild)]
public async Task ChnlFilterLin()
{
var channel = (ITextChannel)ctx.Channel;
FilterLinksChannelId removed;
await using (var uow = _db.GetDbContext())
{
var config =
uow.GuildConfigsForId(channel.Guild.Id, set => set.Include(gc => gc.FilterLinksChannelIds));
var match = new FilterLinksChannelId
{
ChannelId = channel.Id
};
removed = config.FilterLinksChannelIds.FirstOrDefault(fc => fc.Equals(match));
if (removed is null)
config.FilterLinksChannelIds.Add(match);
else
uow.Remove(removed);
await uow.SaveChangesAsync();
}
if (removed is null)
{
_service.LinkFilteringChannels.Add(channel.Id);
await ReplyConfirmLocalizedAsync(strs.link_filter_channel_on);
}
else
{
_service.LinkFilteringChannels.TryRemove(channel.Id);
await ReplyConfirmLocalizedAsync(strs.link_filter_channel_off);
}
}
[Cmd]
[RequireContext(ContextType.Guild)]
public async Task SrvrFilterWords()
{
var channel = (ITextChannel)ctx.Channel;
bool enabled;
await using (var uow = _db.GetDbContext())
{
var config = uow.GuildConfigsForId(channel.Guild.Id, set => set);
enabled = config.FilterWords = !config.FilterWords;
await uow.SaveChangesAsync();
}
if (enabled)
{
_service.WordFilteringServers.Add(channel.Guild.Id);
await ReplyConfirmLocalizedAsync(strs.word_filter_server_on);
}
else
{
_service.WordFilteringServers.TryRemove(channel.Guild.Id);
await ReplyConfirmLocalizedAsync(strs.word_filter_server_off);
}
}
[Cmd]
[RequireContext(ContextType.Guild)]
public async Task ChnlFilterWords()
{
var channel = (ITextChannel)ctx.Channel;
FilterWordsChannelId removed;
await using (var uow = _db.GetDbContext())
{
var config =
uow.GuildConfigsForId(channel.Guild.Id, set => set.Include(gc => gc.FilterWordsChannelIds));
var match = new FilterWordsChannelId
{
ChannelId = channel.Id
};
removed = config.FilterWordsChannelIds.FirstOrDefault(fc => fc.Equals(match));
if (removed is null)
config.FilterWordsChannelIds.Add(match);
else
uow.Remove(removed);
await uow.SaveChangesAsync();
}
if (removed is null)
{
_service.WordFilteringChannels.Add(channel.Id);
await ReplyConfirmLocalizedAsync(strs.word_filter_channel_on);
}
else
{
_service.WordFilteringChannels.TryRemove(channel.Id);
await ReplyConfirmLocalizedAsync(strs.word_filter_channel_off);
}
}
[Cmd]
[RequireContext(ContextType.Guild)]
public async Task FilterWord([Leftover] string word)
{
var channel = (ITextChannel)ctx.Channel;
word = word?.Trim().ToLowerInvariant();
if (string.IsNullOrWhiteSpace(word))
return;
FilteredWord removed;
await using (var uow = _db.GetDbContext())
{
var config = uow.GuildConfigsForId(channel.Guild.Id, set => set.Include(gc => gc.FilteredWords));
removed = config.FilteredWords.FirstOrDefault(fw => fw.Word.Trim().ToLowerInvariant() == word);
if (removed is null)
{
config.FilteredWords.Add(new()
{
Word = word
});
}
else
uow.Remove(removed);
await uow.SaveChangesAsync();
}
var filteredWords =
_service.ServerFilteredWords.GetOrAdd(channel.Guild.Id, new ConcurrentHashSet<string>());
if (removed is null)
{
filteredWords.Add(word);
await ReplyConfirmLocalizedAsync(strs.filter_word_add(Format.Code(word)));
}
else
{
filteredWords.TryRemove(word);
await ReplyConfirmLocalizedAsync(strs.filter_word_remove(Format.Code(word)));
}
}
[Cmd]
[RequireContext(ContextType.Guild)]
public async Task LstFilterWords(int page = 1)
{
page--;
if (page < 0)
return;
var channel = (ITextChannel)ctx.Channel;
_service.ServerFilteredWords.TryGetValue(channel.Guild.Id, out var fwHash);
var fws = fwHash.ToArray();
await ctx.SendPaginatedConfirmAsync(page,
curPage => _eb.Create()
.WithTitle(GetText(strs.filter_word_list))
.WithDescription(string.Join("\n", fws.Skip(curPage * 10).Take(10)))
.WithOkColor(),
fws.Length,
10);
}
}
}

View File

@@ -1,242 +0,0 @@
#nullable disable
using AngleSharp.Dom;
using Microsoft.EntityFrameworkCore;
using NadekoBot.Common.ModuleBehaviors;
using NadekoBot.Db;
using NadekoBot.Services.Database.Models;
namespace NadekoBot.Modules.Permissions.Services;
public sealed class FilterService : IExecOnMessage
{
public ConcurrentHashSet<ulong> InviteFilteringChannels { get; }
public ConcurrentHashSet<ulong> InviteFilteringServers { get; }
//serverid, filteredwords
public ConcurrentDictionary<ulong, ConcurrentHashSet<string>> ServerFilteredWords { get; }
public ConcurrentHashSet<ulong> WordFilteringChannels { get; }
public ConcurrentHashSet<ulong> WordFilteringServers { get; }
public ConcurrentHashSet<ulong> LinkFilteringChannels { get; }
public ConcurrentHashSet<ulong> LinkFilteringServers { get; }
public int Priority
=> int.MaxValue - 1;
private readonly DbService _db;
public FilterService(DiscordSocketClient client, DbService db)
{
_db = db;
using (var uow = db.GetDbContext())
{
var ids = client.GetGuildIds();
var configs = uow.Set<GuildConfig>()
.AsQueryable()
.Include(x => x.FilteredWords)
.Include(x => x.FilterLinksChannelIds)
.Include(x => x.FilterWordsChannelIds)
.Include(x => x.FilterInvitesChannelIds)
.Where(gc => ids.Contains(gc.GuildId))
.ToList();
InviteFilteringServers = new(configs.Where(gc => gc.FilterInvites).Select(gc => gc.GuildId));
InviteFilteringChannels =
new(configs.SelectMany(gc => gc.FilterInvitesChannelIds.Select(fci => fci.ChannelId)));
LinkFilteringServers = new(configs.Where(gc => gc.FilterLinks).Select(gc => gc.GuildId));
LinkFilteringChannels =
new(configs.SelectMany(gc => gc.FilterLinksChannelIds.Select(fci => fci.ChannelId)));
var dict = configs.ToDictionary(gc => gc.GuildId,
gc => new ConcurrentHashSet<string>(gc.FilteredWords.Select(fw => fw.Word).Distinct()));
ServerFilteredWords = new(dict);
var serverFiltering = configs.Where(gc => gc.FilterWords);
WordFilteringServers = new(serverFiltering.Select(gc => gc.GuildId));
WordFilteringChannels =
new(configs.SelectMany(gc => gc.FilterWordsChannelIds.Select(fwci => fwci.ChannelId)));
}
client.MessageUpdated += (oldData, newMsg, channel) =>
{
_ = Task.Run(() =>
{
var guild = (channel as ITextChannel)?.Guild;
if (guild is null || newMsg is not IUserMessage usrMsg)
return Task.CompletedTask;
return ExecOnMessageAsync(guild, usrMsg);
});
return Task.CompletedTask;
};
}
public ConcurrentHashSet<string> FilteredWordsForChannel(ulong channelId, ulong guildId)
{
var words = new ConcurrentHashSet<string>();
if (WordFilteringChannels.Contains(channelId))
ServerFilteredWords.TryGetValue(guildId, out words);
return words;
}
public void ClearFilteredWords(ulong guildId)
{
using var uow = _db.GetDbContext();
var gc = uow.GuildConfigsForId(guildId,
set => set.Include(x => x.FilteredWords).Include(x => x.FilterWordsChannelIds));
WordFilteringServers.TryRemove(guildId);
ServerFilteredWords.TryRemove(guildId, out _);
foreach (var c in gc.FilterWordsChannelIds)
WordFilteringChannels.TryRemove(c.ChannelId);
gc.FilterWords = false;
gc.FilteredWords.Clear();
gc.FilterWordsChannelIds.Clear();
uow.SaveChanges();
}
public ConcurrentHashSet<string> FilteredWordsForServer(ulong guildId)
{
var words = new ConcurrentHashSet<string>();
if (WordFilteringServers.Contains(guildId))
ServerFilteredWords.TryGetValue(guildId, out words);
return words;
}
public async Task<bool> ExecOnMessageAsync(IGuild guild, IUserMessage msg)
{
if (msg.Author is not IGuildUser gu || gu.GuildPermissions.Administrator)
return false;
var results = await Task.WhenAll(FilterInvites(guild, msg), FilterWords(guild, msg), FilterLinks(guild, msg));
return results.Any(x => x);
}
private async Task<bool> FilterWords(IGuild guild, IUserMessage usrMsg)
{
if (guild is null)
return false;
if (usrMsg is null)
return false;
var filteredChannelWords =
FilteredWordsForChannel(usrMsg.Channel.Id, guild.Id) ?? new ConcurrentHashSet<string>();
var filteredServerWords = FilteredWordsForServer(guild.Id) ?? new ConcurrentHashSet<string>();
var wordsInMessage = usrMsg.Content.ToLowerInvariant().Split(' ');
if (filteredChannelWords.Count != 0 || filteredServerWords.Count != 0)
{
foreach (var word in wordsInMessage)
{
if (filteredChannelWords.Contains(word) || filteredServerWords.Contains(word))
{
Log.Information("User {UserName} [{UserId}] used a filtered word in {ChannelId} channel",
usrMsg.Author.ToString(),
usrMsg.Author.Id,
usrMsg.Channel.Id);
try
{
await usrMsg.DeleteAsync();
}
catch (HttpException ex)
{
Log.Warning(ex,
"I do not have permission to filter words in channel with id {Id}",
usrMsg.Channel.Id);
}
return true;
}
}
}
return false;
}
private async Task<bool> FilterInvites(IGuild guild, IUserMessage usrMsg)
{
if (guild is null)
return false;
if (usrMsg is null)
return false;
if ((InviteFilteringChannels.Contains(usrMsg.Channel.Id) || InviteFilteringServers.Contains(guild.Id))
&& usrMsg.Content.IsDiscordInvite())
{
Log.Information("User {UserName} [{UserId}] sent a filtered invite to {ChannelId} channel",
usrMsg.Author.ToString(),
usrMsg.Author.Id,
usrMsg.Channel.Id);
try
{
await usrMsg.DeleteAsync();
return true;
}
catch (HttpException ex)
{
Log.Warning(ex,
"I do not have permission to filter invites in channel with id {Id}",
usrMsg.Channel.Id);
return true;
}
}
return false;
}
private async Task<bool> FilterLinks(IGuild guild, IUserMessage usrMsg)
{
if (guild is null)
return false;
if (usrMsg is null)
return false;
if ((LinkFilteringChannels.Contains(usrMsg.Channel.Id) || LinkFilteringServers.Contains(guild.Id))
&& usrMsg.Content.TryGetUrlPath(out _))
{
Log.Information("User {UserName} [{UserId}] sent a filtered link to {ChannelId} channel",
usrMsg.Author.ToString(),
usrMsg.Author.Id,
usrMsg.Channel.Id);
try
{
await usrMsg.DeleteAsync();
return true;
}
catch (HttpException ex)
{
Log.Warning(ex, "I do not have permission to filter links in channel with id {Id}", usrMsg.Channel.Id);
return true;
}
}
return false;
}
public async Task<ServerFilterSettings> GetFilterSettings(ulong guildId)
{
await using var uow = _db.GetDbContext();
var gc = uow.GuildConfigsForId(guildId, set => set
.Include(x => x.FilterInvitesChannelIds)
.Include(x => x.FilterLinksChannelIds));
return new()
{
FilterInvitesChannels = gc.FilterInvitesChannelIds.Map(x => x.ChannelId),
FilterLinksChannels = gc.FilterLinksChannelIds.Map(x => x.ChannelId),
FilterInvitesEnabled = gc.FilterInvites,
FilterLinksEnabled = gc.FilterLinks,
};
}
}

View File

@@ -1,10 +0,0 @@
#nullable disable
namespace NadekoBot.Modules.Permissions.Services;
public readonly struct ServerFilterSettings
{
public bool FilterInvitesEnabled { get; init; }
public bool FilterLinksEnabled { get; init; }
public IReadOnlyCollection<ulong> FilterInvitesChannels { get; init; }
public IReadOnlyCollection<ulong> FilterLinksChannels { get; init; }
}

View File

@@ -1,77 +0,0 @@
#nullable disable
using NadekoBot.Common.TypeReaders;
using NadekoBot.Modules.Permissions.Services;
namespace NadekoBot.Modules.Permissions;
public partial class Permissions
{
[Group]
public partial class GlobalPermissionCommands : NadekoModule
{
private readonly GlobalPermissionService _service;
private readonly DbService _db;
public GlobalPermissionCommands(GlobalPermissionService service, DbService db)
{
_service = service;
_db = db;
}
[Cmd]
[OwnerOnly]
public async Task GlobalPermList()
{
var blockedModule = _service.BlockedModules;
var blockedCommands = _service.BlockedCommands;
if (!blockedModule.Any() && !blockedCommands.Any())
{
await ReplyErrorLocalizedAsync(strs.lgp_none);
return;
}
var embed = _eb.Create().WithOkColor();
if (blockedModule.Any())
embed.AddField(GetText(strs.blocked_modules), string.Join("\n", _service.BlockedModules));
if (blockedCommands.Any())
embed.AddField(GetText(strs.blocked_commands), string.Join("\n", _service.BlockedCommands));
await ctx.Channel.EmbedAsync(embed);
}
[Cmd]
[OwnerOnly]
public async Task GlobalModule(ModuleOrCrInfo module)
{
var moduleName = module.Name.ToLowerInvariant();
var added = _service.ToggleModule(moduleName);
if (added)
{
await ReplyConfirmLocalizedAsync(strs.gmod_add(Format.Bold(module.Name)));
return;
}
await ReplyConfirmLocalizedAsync(strs.gmod_remove(Format.Bold(module.Name)));
}
[Cmd]
[OwnerOnly]
public async Task GlobalCommand(CommandOrExprInfo cmd)
{
var commandName = cmd.Name.ToLowerInvariant();
var added = _service.ToggleCommand(commandName);
if (added)
{
await ReplyConfirmLocalizedAsync(strs.gcmd_add(Format.Bold(cmd.Name)));
return;
}
await ReplyConfirmLocalizedAsync(strs.gcmd_remove(Format.Bold(cmd.Name)));
}
}
}

View File

@@ -1,92 +0,0 @@
#nullable disable
using NadekoBot.Common.ModuleBehaviors;
namespace NadekoBot.Modules.Permissions.Services;
public class GlobalPermissionService : IExecPreCommand, INService
{
public int Priority { get; } = 0;
public HashSet<string> BlockedCommands
=> _bss.Data.Blocked.Commands;
public HashSet<string> BlockedModules
=> _bss.Data.Blocked.Modules;
private readonly BotConfigService _bss;
public GlobalPermissionService(BotConfigService bss)
=> _bss = bss;
public Task<bool> ExecPreCommandAsync(ICommandContext ctx, string moduleName, CommandInfo command)
{
var settings = _bss.Data;
var commandName = command.Name.ToLowerInvariant();
if (commandName != "resetglobalperms"
&& (settings.Blocked.Commands.Contains(commandName)
|| settings.Blocked.Modules.Contains(moduleName.ToLowerInvariant())))
return Task.FromResult(true);
return Task.FromResult(false);
}
/// <summary>
/// Toggles module blacklist
/// </summary>
/// <param name="moduleName">Lowercase module name</param>
/// <returns>Whether the module is added</returns>
public bool ToggleModule(string moduleName)
{
var added = false;
_bss.ModifyConfig(bs =>
{
if (bs.Blocked.Modules.Add(moduleName))
added = true;
else
{
bs.Blocked.Modules.Remove(moduleName);
added = false;
}
});
return added;
}
/// <summary>
/// Toggles command blacklist
/// </summary>
/// <param name="commandName">Lowercase command name</param>
/// <returns>Whether the command is added</returns>
public bool ToggleCommand(string commandName)
{
var added = false;
_bss.ModifyConfig(bs =>
{
if (bs.Blocked.Commands.Add(commandName))
added = true;
else
{
bs.Blocked.Commands.Remove(commandName);
added = false;
}
});
return added;
}
/// <summary>
/// Resets all global permissions
/// </summary>
public Task Reset()
{
_bss.ModifyConfig(bs =>
{
bs.Blocked.Commands.Clear();
bs.Blocked.Modules.Clear();
});
return Task.CompletedTask;
}
}

View File

@@ -1,11 +0,0 @@
#nullable disable
using NadekoBot.Services.Database.Models;
namespace NadekoBot.Modules.Permissions.Common;
public class PermissionCache
{
public string PermRole { get; set; }
public bool Verbose { get; set; } = true;
public PermissionsCollection<Permissionv2> Permissions { get; set; }
}

View File

@@ -1,132 +0,0 @@
#nullable disable
using NadekoBot.Services.Database.Models;
namespace NadekoBot.Modules.Permissions.Common;
public static class PermissionExtensions
{
public static bool CheckPermissions(
this IEnumerable<Permissionv2> permsEnumerable,
IUser user,
IMessageChannel message,
string commandName,
string moduleName,
out int permIndex)
{
var perms = permsEnumerable as List<Permissionv2> ?? permsEnumerable.ToList();
for (var i = perms.Count - 1; i >= 0; i--)
{
var perm = perms[i];
var result = perm.CheckPermission(user, message, commandName, moduleName);
if (result is null)
continue;
permIndex = i;
return result.Value;
}
permIndex = -1; //defaut behaviour
return true;
}
//null = not applicable
//true = applicable, allowed
//false = applicable, not allowed
public static bool? CheckPermission(
this Permissionv2 perm,
IUser user,
IMessageChannel channel,
string commandName,
string moduleName)
{
if (!((perm.SecondaryTarget == SecondaryPermissionType.Command
&& string.Equals(perm.SecondaryTargetName, commandName, StringComparison.InvariantCultureIgnoreCase))
|| (perm.SecondaryTarget == SecondaryPermissionType.Module
&& string.Equals(perm.SecondaryTargetName, moduleName, StringComparison.InvariantCultureIgnoreCase))
|| perm.SecondaryTarget == SecondaryPermissionType.AllModules))
return null;
var guildUser = user as IGuildUser;
switch (perm.PrimaryTarget)
{
case PrimaryPermissionType.User:
if (perm.PrimaryTargetId == user.Id)
return perm.State;
break;
case PrimaryPermissionType.Channel:
if (perm.PrimaryTargetId == channel.Id)
return perm.State;
break;
case PrimaryPermissionType.Role:
if (guildUser is null)
break;
if (guildUser.RoleIds.Contains(perm.PrimaryTargetId))
return perm.State;
break;
case PrimaryPermissionType.Server:
if (guildUser is null)
break;
return perm.State;
}
return null;
}
public static string GetCommand(this Permissionv2 perm, string prefix, SocketGuild guild = null)
{
var com = string.Empty;
switch (perm.PrimaryTarget)
{
case PrimaryPermissionType.User:
com += "u";
break;
case PrimaryPermissionType.Channel:
com += "c";
break;
case PrimaryPermissionType.Role:
com += "r";
break;
case PrimaryPermissionType.Server:
com += "s";
break;
}
switch (perm.SecondaryTarget)
{
case SecondaryPermissionType.Module:
com += "m";
break;
case SecondaryPermissionType.Command:
com += "c";
break;
case SecondaryPermissionType.AllModules:
com = "a" + com + "m";
break;
}
var secName = perm.SecondaryTarget == SecondaryPermissionType.Command && !perm.IsCustomCommand
? prefix + perm.SecondaryTargetName
: perm.SecondaryTargetName;
com += " " + (perm.SecondaryTargetName != "*" ? secName + " " : "") + (perm.State ? "enable" : "disable") + " ";
switch (perm.PrimaryTarget)
{
case PrimaryPermissionType.User:
com += guild?.GetUser(perm.PrimaryTargetId)?.ToString() ?? $"<@{perm.PrimaryTargetId}>";
break;
case PrimaryPermissionType.Channel:
com += $"<#{perm.PrimaryTargetId}>";
break;
case PrimaryPermissionType.Role:
com += guild?.GetRole(perm.PrimaryTargetId)?.ToString() ?? $"<@&{perm.PrimaryTargetId}>";
break;
case PrimaryPermissionType.Server:
break;
}
return prefix + com;
}
}

View File

@@ -1,516 +0,0 @@
#nullable disable
using NadekoBot.Common.TypeReaders;
using NadekoBot.Common.TypeReaders.Models;
using NadekoBot.Db;
using NadekoBot.Modules.Permissions.Common;
using NadekoBot.Modules.Permissions.Services;
using NadekoBot.Services.Database.Models;
namespace NadekoBot.Modules.Permissions;
public partial class Permissions : NadekoModule<PermissionService>
{
public enum Reset { Reset }
private readonly DbService _db;
public Permissions(DbService db)
=> _db = db;
[Cmd]
[RequireContext(ContextType.Guild)]
public async Task Verbose(PermissionAction action = null)
{
await using (var uow = _db.GetDbContext())
{
var config = uow.GcWithPermissionsFor(ctx.Guild.Id);
if (action is null)
action = new(!config.VerbosePermissions); // New behaviour, can toggle.
config.VerbosePermissions = action.Value;
await uow.SaveChangesAsync();
_service.UpdateCache(config);
}
if (action.Value)
await ReplyConfirmLocalizedAsync(strs.verbose_true);
else
await ReplyConfirmLocalizedAsync(strs.verbose_false);
}
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.Administrator)]
[Priority(0)]
public async Task PermRole([Leftover] IRole role = null)
{
if (role is not null && role == role.Guild.EveryoneRole)
return;
if (role is null)
{
var cache = _service.GetCacheFor(ctx.Guild.Id);
if (!ulong.TryParse(cache.PermRole, out var roleId)
|| (role = ((SocketGuild)ctx.Guild).GetRole(roleId)) is null)
await ReplyConfirmLocalizedAsync(strs.permrole_not_set);
else
await ReplyConfirmLocalizedAsync(strs.permrole(Format.Bold(role.ToString())));
return;
}
await using (var uow = _db.GetDbContext())
{
var config = uow.GcWithPermissionsFor(ctx.Guild.Id);
config.PermissionRole = role.Id.ToString();
uow.SaveChanges();
_service.UpdateCache(config);
}
await ReplyConfirmLocalizedAsync(strs.permrole_changed(Format.Bold(role.Name)));
}
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.Administrator)]
[Priority(1)]
public async Task PermRole(Reset _)
{
await using (var uow = _db.GetDbContext())
{
var config = uow.GcWithPermissionsFor(ctx.Guild.Id);
config.PermissionRole = null;
await uow.SaveChangesAsync();
_service.UpdateCache(config);
}
await ReplyConfirmLocalizedAsync(strs.permrole_reset);
}
[Cmd]
[RequireContext(ContextType.Guild)]
public async Task ListPerms(int page = 1)
{
if (page < 1)
return;
IList<Permissionv2> perms;
if (_service.Cache.TryGetValue(ctx.Guild.Id, out var permCache))
perms = permCache.Permissions.Source.ToList();
else
perms = Permissionv2.GetDefaultPermlist;
var startPos = 20 * (page - 1);
var toSend = Format.Bold(GetText(strs.page(page)))
+ "\n\n"
+ string.Join("\n",
perms.Reverse()
.Skip(startPos)
.Take(20)
.Select(p =>
{
var str =
$"`{p.Index + 1}.` {Format.Bold(p.GetCommand(prefix, (SocketGuild)ctx.Guild))}";
if (p.Index == 0)
str += $" [{GetText(strs.uneditable)}]";
return str;
}));
await ctx.Channel.SendMessageAsync(toSend);
}
[Cmd]
[RequireContext(ContextType.Guild)]
public async Task RemovePerm(int index)
{
index -= 1;
if (index < 0)
return;
try
{
Permissionv2 p;
await using (var uow = _db.GetDbContext())
{
var config = uow.GcWithPermissionsFor(ctx.Guild.Id);
var permsCol = new PermissionsCollection<Permissionv2>(config.Permissions);
p = permsCol[index];
permsCol.RemoveAt(index);
uow.Remove(p);
await uow.SaveChangesAsync();
_service.UpdateCache(config);
}
await ReplyConfirmLocalizedAsync(strs.removed(index + 1,
Format.Code(p.GetCommand(prefix, (SocketGuild)ctx.Guild))));
}
catch (IndexOutOfRangeException)
{
await ReplyErrorLocalizedAsync(strs.perm_out_of_range);
}
}
[Cmd]
[RequireContext(ContextType.Guild)]
public async Task MovePerm(int from, int to)
{
from -= 1;
to -= 1;
if (!(from == to || from < 0 || to < 0))
{
try
{
Permissionv2 fromPerm;
await using (var uow = _db.GetDbContext())
{
var config = uow.GcWithPermissionsFor(ctx.Guild.Id);
var permsCol = new PermissionsCollection<Permissionv2>(config.Permissions);
var fromFound = @from < permsCol.Count;
var toFound = to < permsCol.Count;
if (!fromFound)
{
await ReplyErrorLocalizedAsync(strs.perm_not_found(++@from));
return;
}
if (!toFound)
{
await ReplyErrorLocalizedAsync(strs.perm_not_found(++to));
return;
}
fromPerm = permsCol[@from];
permsCol.RemoveAt(@from);
permsCol.Insert(to, fromPerm);
await uow.SaveChangesAsync();
_service.UpdateCache(config);
}
await ReplyConfirmLocalizedAsync(strs.moved_permission(
Format.Code(fromPerm.GetCommand(prefix, (SocketGuild)ctx.Guild)),
++@from,
++to));
return;
}
catch (Exception e) when (e is ArgumentOutOfRangeException or IndexOutOfRangeException)
{
}
}
await ReplyConfirmLocalizedAsync(strs.perm_out_of_range);
}
[Cmd]
[RequireContext(ContextType.Guild)]
public async Task SrvrCmd(CommandOrExprInfo command, PermissionAction action)
{
await _service.AddPermissions(ctx.Guild.Id,
new Permissionv2
{
PrimaryTarget = PrimaryPermissionType.Server,
PrimaryTargetId = 0,
SecondaryTarget = SecondaryPermissionType.Command,
SecondaryTargetName = command.Name.ToLowerInvariant(),
State = action.Value,
IsCustomCommand = command.IsCustom
});
if (action.Value)
await ReplyConfirmLocalizedAsync(strs.sx_enable(Format.Code(command.Name), GetText(strs.of_command)));
else
await ReplyConfirmLocalizedAsync(strs.sx_disable(Format.Code(command.Name), GetText(strs.of_command)));
}
[Cmd]
[RequireContext(ContextType.Guild)]
public async Task SrvrMdl(ModuleOrCrInfo module, PermissionAction action)
{
await _service.AddPermissions(ctx.Guild.Id,
new Permissionv2
{
PrimaryTarget = PrimaryPermissionType.Server,
PrimaryTargetId = 0,
SecondaryTarget = SecondaryPermissionType.Module,
SecondaryTargetName = module.Name.ToLowerInvariant(),
State = action.Value
});
if (action.Value)
await ReplyConfirmLocalizedAsync(strs.sx_enable(Format.Code(module.Name), GetText(strs.of_module)));
else
await ReplyConfirmLocalizedAsync(strs.sx_disable(Format.Code(module.Name), GetText(strs.of_module)));
}
[Cmd]
[RequireContext(ContextType.Guild)]
public async Task UsrCmd(CommandOrExprInfo command, PermissionAction action, [Leftover] IGuildUser user)
{
await _service.AddPermissions(ctx.Guild.Id,
new Permissionv2
{
PrimaryTarget = PrimaryPermissionType.User,
PrimaryTargetId = user.Id,
SecondaryTarget = SecondaryPermissionType.Command,
SecondaryTargetName = command.Name.ToLowerInvariant(),
State = action.Value,
IsCustomCommand = command.IsCustom
});
if (action.Value)
{
await ReplyConfirmLocalizedAsync(strs.ux_enable(Format.Code(command.Name),
GetText(strs.of_command),
Format.Code(user.ToString())));
}
else
{
await ReplyConfirmLocalizedAsync(strs.ux_disable(Format.Code(command.Name),
GetText(strs.of_command),
Format.Code(user.ToString())));
}
}
[Cmd]
[RequireContext(ContextType.Guild)]
public async Task UsrMdl(ModuleOrCrInfo module, PermissionAction action, [Leftover] IGuildUser user)
{
await _service.AddPermissions(ctx.Guild.Id,
new Permissionv2
{
PrimaryTarget = PrimaryPermissionType.User,
PrimaryTargetId = user.Id,
SecondaryTarget = SecondaryPermissionType.Module,
SecondaryTargetName = module.Name.ToLowerInvariant(),
State = action.Value
});
if (action.Value)
{
await ReplyConfirmLocalizedAsync(strs.ux_enable(Format.Code(module.Name),
GetText(strs.of_module),
Format.Code(user.ToString())));
}
else
{
await ReplyConfirmLocalizedAsync(strs.ux_disable(Format.Code(module.Name),
GetText(strs.of_module),
Format.Code(user.ToString())));
}
}
[Cmd]
[RequireContext(ContextType.Guild)]
public async Task RoleCmd(CommandOrExprInfo command, PermissionAction action, [Leftover] IRole role)
{
if (role == role.Guild.EveryoneRole)
return;
await _service.AddPermissions(ctx.Guild.Id,
new Permissionv2
{
PrimaryTarget = PrimaryPermissionType.Role,
PrimaryTargetId = role.Id,
SecondaryTarget = SecondaryPermissionType.Command,
SecondaryTargetName = command.Name.ToLowerInvariant(),
State = action.Value,
IsCustomCommand = command.IsCustom
});
if (action.Value)
{
await ReplyConfirmLocalizedAsync(strs.rx_enable(Format.Code(command.Name),
GetText(strs.of_command),
Format.Code(role.Name)));
}
else
{
await ReplyConfirmLocalizedAsync(strs.rx_disable(Format.Code(command.Name),
GetText(strs.of_command),
Format.Code(role.Name)));
}
}
[Cmd]
[RequireContext(ContextType.Guild)]
public async Task RoleMdl(ModuleOrCrInfo module, PermissionAction action, [Leftover] IRole role)
{
if (role == role.Guild.EveryoneRole)
return;
await _service.AddPermissions(ctx.Guild.Id,
new Permissionv2
{
PrimaryTarget = PrimaryPermissionType.Role,
PrimaryTargetId = role.Id,
SecondaryTarget = SecondaryPermissionType.Module,
SecondaryTargetName = module.Name.ToLowerInvariant(),
State = action.Value
});
if (action.Value)
{
await ReplyConfirmLocalizedAsync(strs.rx_enable(Format.Code(module.Name),
GetText(strs.of_module),
Format.Code(role.Name)));
}
else
{
await ReplyConfirmLocalizedAsync(strs.rx_disable(Format.Code(module.Name),
GetText(strs.of_module),
Format.Code(role.Name)));
}
}
[Cmd]
[RequireContext(ContextType.Guild)]
public async Task ChnlCmd(CommandOrExprInfo command, PermissionAction action, [Leftover] ITextChannel chnl)
{
await _service.AddPermissions(ctx.Guild.Id,
new Permissionv2
{
PrimaryTarget = PrimaryPermissionType.Channel,
PrimaryTargetId = chnl.Id,
SecondaryTarget = SecondaryPermissionType.Command,
SecondaryTargetName = command.Name.ToLowerInvariant(),
State = action.Value,
IsCustomCommand = command.IsCustom
});
if (action.Value)
{
await ReplyConfirmLocalizedAsync(strs.cx_enable(Format.Code(command.Name),
GetText(strs.of_command),
Format.Code(chnl.Name)));
}
else
{
await ReplyConfirmLocalizedAsync(strs.cx_disable(Format.Code(command.Name),
GetText(strs.of_command),
Format.Code(chnl.Name)));
}
}
[Cmd]
[RequireContext(ContextType.Guild)]
public async Task ChnlMdl(ModuleOrCrInfo module, PermissionAction action, [Leftover] ITextChannel chnl)
{
await _service.AddPermissions(ctx.Guild.Id,
new Permissionv2
{
PrimaryTarget = PrimaryPermissionType.Channel,
PrimaryTargetId = chnl.Id,
SecondaryTarget = SecondaryPermissionType.Module,
SecondaryTargetName = module.Name.ToLowerInvariant(),
State = action.Value
});
if (action.Value)
{
await ReplyConfirmLocalizedAsync(strs.cx_enable(Format.Code(module.Name),
GetText(strs.of_module),
Format.Code(chnl.Name)));
}
else
{
await ReplyConfirmLocalizedAsync(strs.cx_disable(Format.Code(module.Name),
GetText(strs.of_module),
Format.Code(chnl.Name)));
}
}
[Cmd]
[RequireContext(ContextType.Guild)]
public async Task AllChnlMdls(PermissionAction action, [Leftover] ITextChannel chnl)
{
await _service.AddPermissions(ctx.Guild.Id,
new Permissionv2
{
PrimaryTarget = PrimaryPermissionType.Channel,
PrimaryTargetId = chnl.Id,
SecondaryTarget = SecondaryPermissionType.AllModules,
SecondaryTargetName = "*",
State = action.Value
});
if (action.Value)
await ReplyConfirmLocalizedAsync(strs.acm_enable(Format.Code(chnl.Name)));
else
await ReplyConfirmLocalizedAsync(strs.acm_disable(Format.Code(chnl.Name)));
}
[Cmd]
[RequireContext(ContextType.Guild)]
public async Task AllRoleMdls(PermissionAction action, [Leftover] IRole role)
{
if (role == role.Guild.EveryoneRole)
return;
await _service.AddPermissions(ctx.Guild.Id,
new Permissionv2
{
PrimaryTarget = PrimaryPermissionType.Role,
PrimaryTargetId = role.Id,
SecondaryTarget = SecondaryPermissionType.AllModules,
SecondaryTargetName = "*",
State = action.Value
});
if (action.Value)
await ReplyConfirmLocalizedAsync(strs.arm_enable(Format.Code(role.Name)));
else
await ReplyConfirmLocalizedAsync(strs.arm_disable(Format.Code(role.Name)));
}
[Cmd]
[RequireContext(ContextType.Guild)]
public async Task AllUsrMdls(PermissionAction action, [Leftover] IUser user)
{
await _service.AddPermissions(ctx.Guild.Id,
new Permissionv2
{
PrimaryTarget = PrimaryPermissionType.User,
PrimaryTargetId = user.Id,
SecondaryTarget = SecondaryPermissionType.AllModules,
SecondaryTargetName = "*",
State = action.Value
});
if (action.Value)
await ReplyConfirmLocalizedAsync(strs.aum_enable(Format.Code(user.ToString())));
else
await ReplyConfirmLocalizedAsync(strs.aum_disable(Format.Code(user.ToString())));
}
[Cmd]
[RequireContext(ContextType.Guild)]
public async Task AllSrvrMdls(PermissionAction action)
{
var newPerm = new Permissionv2
{
PrimaryTarget = PrimaryPermissionType.Server,
PrimaryTargetId = 0,
SecondaryTarget = SecondaryPermissionType.AllModules,
SecondaryTargetName = "*",
State = action.Value
};
var allowUser = new Permissionv2
{
PrimaryTarget = PrimaryPermissionType.User,
PrimaryTargetId = ctx.User.Id,
SecondaryTarget = SecondaryPermissionType.AllModules,
SecondaryTargetName = "*",
State = true
};
await _service.AddPermissions(ctx.Guild.Id, newPerm, allowUser);
if (action.Value)
await ReplyConfirmLocalizedAsync(strs.asm_enable);
else
await ReplyConfirmLocalizedAsync(strs.asm_disable);
}
}

Some files were not shown because too many files have changed in this diff Show More