Compare commits
No commits in common. "5505052af4c6b3cbe9e32040f7668acef1787fe2" and "f18808fb1c38af1f5f5dbb3bb09f7dafcea9bb70" have entirely different histories.
5505052af4
...
f18808fb1c
27 changed files with 0 additions and 3470 deletions
|
@ -1,154 +0,0 @@
|
||||||
#nullable disable
|
|
||||||
using EllieBot.Modules.Permissions.Services;
|
|
||||||
using EllieBot.Db.Models;
|
|
||||||
|
|
||||||
namespace EllieBot.Modules.Permissions;
|
|
||||||
|
|
||||||
public partial class Permissions
|
|
||||||
{
|
|
||||||
[Group]
|
|
||||||
public partial class BlacklistCommands : EllieModule<BlacklistService>
|
|
||||||
{
|
|
||||||
private readonly DiscordSocketClient _client;
|
|
||||||
|
|
||||||
public BlacklistCommands(DiscordSocketClient client)
|
|
||||||
=> _client = client;
|
|
||||||
|
|
||||||
private async Task ListBlacklistInternal(string title, BlacklistType type, int page = 0)
|
|
||||||
{
|
|
||||||
ArgumentOutOfRangeException.ThrowIfNegative(page);
|
|
||||||
|
|
||||||
var list = _service.GetBlacklist();
|
|
||||||
var allItems = await list.Where(x => x.Type == type)
|
|
||||||
.Select(i =>
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
return Task.FromResult(i.Type switch
|
|
||||||
{
|
|
||||||
BlacklistType.Channel => Format.Code(i.ItemId.ToString())
|
|
||||||
+ " "
|
|
||||||
+ (_client.GetChannel(i.ItemId)?.ToString()
|
|
||||||
?? ""),
|
|
||||||
BlacklistType.User => Format.Code(i.ItemId.ToString())
|
|
||||||
+ " "
|
|
||||||
+ ((_client.GetUser(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 Task.FromResult(Format.Code(i.ItemId.ToString()));
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.WhenAll();
|
|
||||||
|
|
||||||
await Response()
|
|
||||||
.Paginated()
|
|
||||||
.Items(allItems)
|
|
||||||
.PageSize(10)
|
|
||||||
.CurrentPage(page)
|
|
||||||
.Page((pageItems, _) =>
|
|
||||||
{
|
|
||||||
if (pageItems.Count == 0)
|
|
||||||
return _sender.CreateEmbed()
|
|
||||||
.WithOkColor()
|
|
||||||
.WithTitle(title)
|
|
||||||
.WithDescription(GetText(strs.empty_page));
|
|
||||||
|
|
||||||
return _sender.CreateEmbed()
|
|
||||||
.WithTitle(title)
|
|
||||||
.WithDescription(allItems.Join('\n'))
|
|
||||||
.WithOkColor();
|
|
||||||
})
|
|
||||||
.SendAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
[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)
|
|
||||||
await _service.Blacklist(type, id);
|
|
||||||
else
|
|
||||||
await _service.UnBlacklist(type, id);
|
|
||||||
|
|
||||||
if (action == AddRemove.Add)
|
|
||||||
{
|
|
||||||
await Response()
|
|
||||||
.Confirm(strs.blacklisted(Format.Code(type.ToString()),
|
|
||||||
Format.Code(id.ToString())))
|
|
||||||
.SendAsync();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
await Response()
|
|
||||||
.Confirm(strs.unblacklisted(Format.Code(type.ToString()),
|
|
||||||
Format.Code(id.ToString())))
|
|
||||||
.SendAsync();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,15 +0,0 @@
|
||||||
#nullable disable
|
|
||||||
using EllieBot.Common.TypeReaders;
|
|
||||||
using static EllieBot.Common.TypeReaders.TypeReaderResult;
|
|
||||||
|
|
||||||
namespace EllieBot.Modules.Permissions;
|
|
||||||
|
|
||||||
public class CleverbotResponseCmdCdTypeReader : EllieTypeReader<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"));
|
|
||||||
}
|
|
|
@ -1,141 +0,0 @@
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using EllieBot.Common.ModuleBehaviors;
|
|
||||||
|
|
||||||
namespace EllieBot.Modules.Permissions.Services;
|
|
||||||
|
|
||||||
public sealed class CmdCdService : IExecPreCommand, IReadyExecutor, IEService
|
|
||||||
{
|
|
||||||
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(IBot 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)
|
|
||||||
{
|
|
||||||
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(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();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,106 +0,0 @@
|
||||||
#nullable disable
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using EllieBot.Common.TypeReaders;
|
|
||||||
using EllieBot.Modules.Permissions.Services;
|
|
||||||
using EllieBot.Db.Models;
|
|
||||||
|
|
||||||
namespace EllieBot.Modules.Permissions;
|
|
||||||
|
|
||||||
public partial class Permissions
|
|
||||||
{
|
|
||||||
[Group]
|
|
||||||
public partial class CmdCdsCommands : EllieModule
|
|
||||||
{
|
|
||||||
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 Response().Error(strs.invalid_second_param_between(0, 3600)).SendAsync();
|
|
||||||
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 Response().Confirm(strs.cmdcd_cleared(Format.Bold(name))).SendAsync();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
await Response().Confirm(strs.cmdcd_add(Format.Bold(name), Format.Bold(secs.ToString()))).SendAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
[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 localSet = _service.GetCommandCooldowns(ctx.Guild.Id);
|
|
||||||
|
|
||||||
if (!localSet.Any())
|
|
||||||
await Response().Confirm(strs.cmdcd_none).SendAsync();
|
|
||||||
else
|
|
||||||
{
|
|
||||||
await Response()
|
|
||||||
.Paginated()
|
|
||||||
.Items(localSet)
|
|
||||||
.PageSize(15)
|
|
||||||
.CurrentPage(page)
|
|
||||||
.Page((items, _) =>
|
|
||||||
{
|
|
||||||
var output = items.Select(x =>
|
|
||||||
$"{Format.Code(x.CommandName)}: {x.Seconds}s");
|
|
||||||
|
|
||||||
return _sender.CreateEmbed()
|
|
||||||
.WithOkColor()
|
|
||||||
.WithDescription(output.Join("\n"));
|
|
||||||
})
|
|
||||||
.SendAsync();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,326 +0,0 @@
|
||||||
#nullable disable
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using EllieBot.Modules.Permissions.Services;
|
|
||||||
using EllieBot.Db.Models;
|
|
||||||
|
|
||||||
namespace EllieBot.Modules.Permissions;
|
|
||||||
|
|
||||||
public partial class Permissions
|
|
||||||
{
|
|
||||||
[Group]
|
|
||||||
public partial class FilterCommands : EllieModule<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 Response().Confirm(strs.fw_cleared).SendAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
[Cmd]
|
|
||||||
[RequireContext(ContextType.Guild)]
|
|
||||||
public async Task FilterList()
|
|
||||||
{
|
|
||||||
var embed = _sender.CreateEmbed()
|
|
||||||
.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 Response().Embed(embed).SendAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
[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 Response().Confirm(strs.invite_filter_server_on).SendAsync();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_service.InviteFilteringServers.TryRemove(channel.Guild.Id);
|
|
||||||
await Response().Confirm(strs.invite_filter_server_off).SendAsync();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[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 Response().Confirm(strs.invite_filter_channel_on).SendAsync();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_service.InviteFilteringChannels.TryRemove(channel.Id);
|
|
||||||
await Response().Confirm(strs.invite_filter_channel_off).SendAsync();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[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 Response().Confirm(strs.link_filter_server_on).SendAsync();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_service.LinkFilteringServers.TryRemove(channel.Guild.Id);
|
|
||||||
await Response().Confirm(strs.link_filter_server_off).SendAsync();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[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 Response().Confirm(strs.link_filter_channel_on).SendAsync();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_service.LinkFilteringChannels.TryRemove(channel.Id);
|
|
||||||
await Response().Confirm(strs.link_filter_channel_off).SendAsync();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[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 Response().Confirm(strs.word_filter_server_on).SendAsync();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_service.WordFilteringServers.TryRemove(channel.Guild.Id);
|
|
||||||
await Response().Confirm(strs.word_filter_server_off).SendAsync();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[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 Response().Confirm(strs.word_filter_channel_on).SendAsync();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_service.WordFilteringChannels.TryRemove(channel.Id);
|
|
||||||
await Response().Confirm(strs.word_filter_channel_off).SendAsync();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[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 Response().Confirm(strs.filter_word_add(Format.Code(word))).SendAsync();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
filteredWords.TryRemove(word);
|
|
||||||
await Response().Confirm(strs.filter_word_remove(Format.Code(word))).SendAsync();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[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 Response()
|
|
||||||
.Paginated()
|
|
||||||
.Items(fws)
|
|
||||||
.PageSize(10)
|
|
||||||
.CurrentPage(page)
|
|
||||||
.Page((items, _) => _sender.CreateEmbed()
|
|
||||||
.WithTitle(GetText(strs.filter_word_list))
|
|
||||||
.WithDescription(string.Join("\n", items))
|
|
||||||
.WithOkColor())
|
|
||||||
.SendAsync();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,249 +0,0 @@
|
||||||
#nullable disable
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using EllieBot.Common.ModuleBehaviors;
|
|
||||||
using EllieBot.Db.Models;
|
|
||||||
|
|
||||||
namespace EllieBot.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 user has manage messages perm, don't filter
|
|
||||||
if (usrMsg.Channel is ITextChannel ch && usrMsg.Author is IGuildUser gu && gu.GetPermissions(ch).ManageMessages)
|
|
||||||
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 user has manage messages perm, don't filter
|
|
||||||
if (usrMsg.Channel is ITextChannel ch && usrMsg.Author is IGuildUser gu && gu.GetPermissions(ch).ManageMessages)
|
|
||||||
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,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,10 +0,0 @@
|
||||||
#nullable disable
|
|
||||||
namespace EllieBot.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; }
|
|
||||||
}
|
|
|
@ -1,77 +0,0 @@
|
||||||
#nullable disable
|
|
||||||
using EllieBot.Common.TypeReaders;
|
|
||||||
using EllieBot.Modules.Permissions.Services;
|
|
||||||
|
|
||||||
namespace EllieBot.Modules.Permissions;
|
|
||||||
|
|
||||||
public partial class Permissions
|
|
||||||
{
|
|
||||||
[Group]
|
|
||||||
public partial class GlobalPermissionCommands : EllieModule
|
|
||||||
{
|
|
||||||
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 Response().Error(strs.lgp_none).SendAsync();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var embed = _sender.CreateEmbed().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 Response().Embed(embed).SendAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
[Cmd]
|
|
||||||
[OwnerOnly]
|
|
||||||
public async Task GlobalModule(ModuleOrExpr module)
|
|
||||||
{
|
|
||||||
var moduleName = module.Name.ToLowerInvariant();
|
|
||||||
|
|
||||||
var added = _service.ToggleModule(moduleName);
|
|
||||||
|
|
||||||
if (added)
|
|
||||||
{
|
|
||||||
await Response().Confirm(strs.gmod_add(Format.Bold(module.Name))).SendAsync();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await Response().Confirm(strs.gmod_remove(Format.Bold(module.Name))).SendAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
[Cmd]
|
|
||||||
[OwnerOnly]
|
|
||||||
public async Task GlobalCommand(CommandOrExprInfo cmd)
|
|
||||||
{
|
|
||||||
var commandName = cmd.Name.ToLowerInvariant();
|
|
||||||
var added = _service.ToggleCommand(commandName);
|
|
||||||
|
|
||||||
if (added)
|
|
||||||
{
|
|
||||||
await Response().Confirm(strs.gcmd_add(Format.Bold(cmd.Name))).SendAsync();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await Response().Confirm(strs.gcmd_remove(Format.Bold(cmd.Name))).SendAsync();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,92 +0,0 @@
|
||||||
#nullable disable
|
|
||||||
using EllieBot.Common.ModuleBehaviors;
|
|
||||||
|
|
||||||
namespace EllieBot.Modules.Permissions.Services;
|
|
||||||
|
|
||||||
public class GlobalPermissionService : IExecPreCommand, IEService
|
|
||||||
{
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,11 +0,0 @@
|
||||||
#nullable disable
|
|
||||||
using EllieBot.Db.Models;
|
|
||||||
|
|
||||||
namespace EllieBot.Modules.Permissions.Common;
|
|
||||||
|
|
||||||
public class PermissionCache
|
|
||||||
{
|
|
||||||
public string PermRole { get; set; }
|
|
||||||
public bool Verbose { get; set; } = true;
|
|
||||||
public PermissionsCollection<Permissionv2> Permissions { get; set; }
|
|
||||||
}
|
|
|
@ -1,132 +0,0 @@
|
||||||
#nullable disable
|
|
||||||
using EllieBot.Db.Models;
|
|
||||||
|
|
||||||
namespace EllieBot.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;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,543 +0,0 @@
|
||||||
#nullable disable
|
|
||||||
using EllieBot.Common.TypeReaders;
|
|
||||||
using EllieBot.Common.TypeReaders.Models;
|
|
||||||
using EllieBot.Modules.Permissions.Common;
|
|
||||||
using EllieBot.Modules.Permissions.Services;
|
|
||||||
using EllieBot.Db.Models;
|
|
||||||
|
|
||||||
namespace EllieBot.Modules.Permissions;
|
|
||||||
|
|
||||||
public partial class Permissions : EllieModule<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 Response().Confirm(strs.verbose_true).SendAsync();
|
|
||||||
else
|
|
||||||
await Response().Confirm(strs.verbose_false).SendAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
[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 Response().Confirm(strs.permrole_not_set).SendAsync();
|
|
||||||
else
|
|
||||||
await Response().Confirm(strs.permrole(Format.Bold(role.ToString()))).SendAsync();
|
|
||||||
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 Response().Confirm(strs.permrole_changed(Format.Bold(role.Name))).SendAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
[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 Response().Confirm(strs.permrole_reset).SendAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
[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 Response().Confirm(toSend).SendAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
[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 Response()
|
|
||||||
.Confirm(strs.removed(index + 1,
|
|
||||||
Format.Code(p.GetCommand(prefix, (SocketGuild)ctx.Guild))))
|
|
||||||
.SendAsync();
|
|
||||||
}
|
|
||||||
catch (IndexOutOfRangeException)
|
|
||||||
{
|
|
||||||
await Response().Error(strs.perm_out_of_range).SendAsync();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[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 Response().Error(strs.perm_not_found(++from)).SendAsync();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!toFound)
|
|
||||||
{
|
|
||||||
await Response().Error(strs.perm_not_found(++to)).SendAsync();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
fromPerm = permsCol[from];
|
|
||||||
|
|
||||||
permsCol.RemoveAt(from);
|
|
||||||
permsCol.Insert(to, fromPerm);
|
|
||||||
await uow.SaveChangesAsync();
|
|
||||||
_service.UpdateCache(config);
|
|
||||||
}
|
|
||||||
|
|
||||||
await Response()
|
|
||||||
.Confirm(strs.moved_permission(
|
|
||||||
Format.Code(fromPerm.GetCommand(prefix, (SocketGuild)ctx.Guild)),
|
|
||||||
++from,
|
|
||||||
++to))
|
|
||||||
.SendAsync();
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
catch (Exception e) when (e is ArgumentOutOfRangeException or IndexOutOfRangeException)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await Response().Confirm(strs.perm_out_of_range).SendAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
[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 Response().Confirm(strs.sx_enable(Format.Code(command.Name), GetText(strs.of_command))).SendAsync();
|
|
||||||
else
|
|
||||||
await Response().Confirm(strs.sx_disable(Format.Code(command.Name), GetText(strs.of_command))).SendAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
[Cmd]
|
|
||||||
[RequireContext(ContextType.Guild)]
|
|
||||||
public async Task SrvrMdl(ModuleOrExpr 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 Response().Confirm(strs.sx_enable(Format.Code(module.Name), GetText(strs.of_module))).SendAsync();
|
|
||||||
else
|
|
||||||
await Response().Confirm(strs.sx_disable(Format.Code(module.Name), GetText(strs.of_module))).SendAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
[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 Response()
|
|
||||||
.Confirm(strs.ux_enable(Format.Code(command.Name),
|
|
||||||
GetText(strs.of_command),
|
|
||||||
Format.Code(user.ToString())))
|
|
||||||
.SendAsync();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
await Response()
|
|
||||||
.Confirm(strs.ux_disable(Format.Code(command.Name),
|
|
||||||
GetText(strs.of_command),
|
|
||||||
Format.Code(user.ToString())))
|
|
||||||
.SendAsync();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[Cmd]
|
|
||||||
[RequireContext(ContextType.Guild)]
|
|
||||||
public async Task UsrMdl(ModuleOrExpr 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 Response()
|
|
||||||
.Confirm(strs.ux_enable(Format.Code(module.Name),
|
|
||||||
GetText(strs.of_module),
|
|
||||||
Format.Code(user.ToString())))
|
|
||||||
.SendAsync();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
await Response()
|
|
||||||
.Confirm(strs.ux_disable(Format.Code(module.Name),
|
|
||||||
GetText(strs.of_module),
|
|
||||||
Format.Code(user.ToString())))
|
|
||||||
.SendAsync();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[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 Response()
|
|
||||||
.Confirm(strs.rx_enable(Format.Code(command.Name),
|
|
||||||
GetText(strs.of_command),
|
|
||||||
Format.Code(role.Name)))
|
|
||||||
.SendAsync();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
await Response()
|
|
||||||
.Confirm(strs.rx_disable(Format.Code(command.Name),
|
|
||||||
GetText(strs.of_command),
|
|
||||||
Format.Code(role.Name)))
|
|
||||||
.SendAsync();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[Cmd]
|
|
||||||
[RequireContext(ContextType.Guild)]
|
|
||||||
public async Task RoleMdl(ModuleOrExpr 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 Response()
|
|
||||||
.Confirm(strs.rx_enable(Format.Code(module.Name),
|
|
||||||
GetText(strs.of_module),
|
|
||||||
Format.Code(role.Name)))
|
|
||||||
.SendAsync();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
await Response()
|
|
||||||
.Confirm(strs.rx_disable(Format.Code(module.Name),
|
|
||||||
GetText(strs.of_module),
|
|
||||||
Format.Code(role.Name)))
|
|
||||||
.SendAsync();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[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 Response()
|
|
||||||
.Confirm(strs.cx_enable(Format.Code(command.Name),
|
|
||||||
GetText(strs.of_command),
|
|
||||||
Format.Code(chnl.Name)))
|
|
||||||
.SendAsync();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
await Response()
|
|
||||||
.Confirm(strs.cx_disable(Format.Code(command.Name),
|
|
||||||
GetText(strs.of_command),
|
|
||||||
Format.Code(chnl.Name)))
|
|
||||||
.SendAsync();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[Cmd]
|
|
||||||
[RequireContext(ContextType.Guild)]
|
|
||||||
public async Task ChnlMdl(ModuleOrExpr 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 Response()
|
|
||||||
.Confirm(strs.cx_enable(Format.Code(module.Name),
|
|
||||||
GetText(strs.of_module),
|
|
||||||
Format.Code(chnl.Name)))
|
|
||||||
.SendAsync();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
await Response()
|
|
||||||
.Confirm(strs.cx_disable(Format.Code(module.Name),
|
|
||||||
GetText(strs.of_module),
|
|
||||||
Format.Code(chnl.Name)))
|
|
||||||
.SendAsync();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[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 Response().Confirm(strs.acm_enable(Format.Code(chnl.Name))).SendAsync();
|
|
||||||
else
|
|
||||||
await Response().Confirm(strs.acm_disable(Format.Code(chnl.Name))).SendAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
[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 Response().Confirm(strs.arm_enable(Format.Code(role.Name))).SendAsync();
|
|
||||||
else
|
|
||||||
await Response().Confirm(strs.arm_disable(Format.Code(role.Name))).SendAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
[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 Response().Confirm(strs.aum_enable(Format.Code(user.ToString()))).SendAsync();
|
|
||||||
else
|
|
||||||
await Response().Confirm(strs.aum_disable(Format.Code(user.ToString()))).SendAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
[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 Response().Confirm(strs.asm_enable).SendAsync();
|
|
||||||
else
|
|
||||||
await Response().Confirm(strs.asm_disable).SendAsync();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,74 +0,0 @@
|
||||||
#nullable disable
|
|
||||||
namespace EllieBot.Modules.Permissions.Common;
|
|
||||||
|
|
||||||
public class PermissionsCollection<T> : IndexedCollection<T>
|
|
||||||
where T : class, IIndexed
|
|
||||||
{
|
|
||||||
public override T this[int index]
|
|
||||||
{
|
|
||||||
get => Source[index];
|
|
||||||
set
|
|
||||||
{
|
|
||||||
lock (_localLocker)
|
|
||||||
{
|
|
||||||
if (index == 0) // can't set first element. It's always allow all
|
|
||||||
throw new IndexOutOfRangeException(nameof(index));
|
|
||||||
base[index] = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private readonly object _localLocker = new();
|
|
||||||
|
|
||||||
public PermissionsCollection(IEnumerable<T> source)
|
|
||||||
: base(source)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public static implicit operator List<T>(PermissionsCollection<T> x)
|
|
||||||
=> x.Source;
|
|
||||||
|
|
||||||
public override void Clear()
|
|
||||||
{
|
|
||||||
lock (_localLocker)
|
|
||||||
{
|
|
||||||
var first = Source[0];
|
|
||||||
base.Clear();
|
|
||||||
Source[0] = first;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public override bool Remove(T item)
|
|
||||||
{
|
|
||||||
bool removed;
|
|
||||||
lock (_localLocker)
|
|
||||||
{
|
|
||||||
if (Source.IndexOf(item) == 0)
|
|
||||||
throw new ArgumentException("You can't remove first permsission (allow all)");
|
|
||||||
removed = base.Remove(item);
|
|
||||||
}
|
|
||||||
|
|
||||||
return removed;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void Insert(int index, T item)
|
|
||||||
{
|
|
||||||
lock (_localLocker)
|
|
||||||
{
|
|
||||||
if (index == 0) // can't insert on first place. Last item is always allow all.
|
|
||||||
throw new IndexOutOfRangeException(nameof(index));
|
|
||||||
base.Insert(index, item);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void RemoveAt(int index)
|
|
||||||
{
|
|
||||||
lock (_localLocker)
|
|
||||||
{
|
|
||||||
if (index == 0) // you can't remove first permission (allow all)
|
|
||||||
throw new IndexOutOfRangeException(nameof(index));
|
|
||||||
|
|
||||||
base.RemoveAt(index);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,184 +0,0 @@
|
||||||
#nullable disable
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using EllieBot.Common.ModuleBehaviors;
|
|
||||||
using EllieBot.Modules.Permissions.Common;
|
|
||||||
using EllieBot.Db.Models;
|
|
||||||
|
|
||||||
namespace EllieBot.Modules.Permissions.Services;
|
|
||||||
|
|
||||||
public class PermissionService : IExecPreCommand, IEService
|
|
||||||
{
|
|
||||||
public int Priority { get; } = 0;
|
|
||||||
|
|
||||||
//guildid, root permission
|
|
||||||
public ConcurrentDictionary<ulong, PermissionCache> Cache { get; } = new();
|
|
||||||
|
|
||||||
private readonly DbService _db;
|
|
||||||
private readonly CommandHandler _cmd;
|
|
||||||
private readonly IBotStrings _strings;
|
|
||||||
private readonly IMessageSenderService _sender;
|
|
||||||
|
|
||||||
public PermissionService(
|
|
||||||
DiscordSocketClient client,
|
|
||||||
DbService db,
|
|
||||||
CommandHandler cmd,
|
|
||||||
IBotStrings strings,
|
|
||||||
IMessageSenderService sender)
|
|
||||||
{
|
|
||||||
_db = db;
|
|
||||||
_cmd = cmd;
|
|
||||||
_strings = strings;
|
|
||||||
_sender = sender;
|
|
||||||
|
|
||||||
using var uow = _db.GetDbContext();
|
|
||||||
foreach (var x in uow.Set<GuildConfig>().PermissionsForAll(client.Guilds.ToArray().Select(x => x.Id).ToList()))
|
|
||||||
{
|
|
||||||
Cache.TryAdd(x.GuildId,
|
|
||||||
new()
|
|
||||||
{
|
|
||||||
Verbose = x.VerbosePermissions,
|
|
||||||
PermRole = x.PermissionRole,
|
|
||||||
Permissions = new(x.Permissions)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public PermissionCache GetCacheFor(ulong guildId)
|
|
||||||
{
|
|
||||||
if (!Cache.TryGetValue(guildId, out var pc))
|
|
||||||
{
|
|
||||||
using (var uow = _db.GetDbContext())
|
|
||||||
{
|
|
||||||
var config = uow.GuildConfigsForId(guildId, set => set.Include(x => x.Permissions));
|
|
||||||
UpdateCache(config);
|
|
||||||
}
|
|
||||||
|
|
||||||
Cache.TryGetValue(guildId, out pc);
|
|
||||||
if (pc is null)
|
|
||||||
throw new("Cache is null.");
|
|
||||||
}
|
|
||||||
|
|
||||||
return pc;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task AddPermissions(ulong guildId, params Permissionv2[] perms)
|
|
||||||
{
|
|
||||||
await using var uow = _db.GetDbContext();
|
|
||||||
var config = uow.GcWithPermissionsFor(guildId);
|
|
||||||
//var orderedPerms = new PermissionsCollection<Permissionv2>(config.Permissions);
|
|
||||||
var max = config.Permissions.Max(x => x.Index); //have to set its index to be the highest
|
|
||||||
foreach (var perm in perms)
|
|
||||||
{
|
|
||||||
perm.Index = ++max;
|
|
||||||
config.Permissions.Add(perm);
|
|
||||||
}
|
|
||||||
|
|
||||||
await uow.SaveChangesAsync();
|
|
||||||
UpdateCache(config);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void UpdateCache(GuildConfig config)
|
|
||||||
=> Cache.AddOrUpdate(config.GuildId,
|
|
||||||
new PermissionCache
|
|
||||||
{
|
|
||||||
Permissions = new(config.Permissions),
|
|
||||||
PermRole = config.PermissionRole,
|
|
||||||
Verbose = config.VerbosePermissions
|
|
||||||
},
|
|
||||||
(_, old) =>
|
|
||||||
{
|
|
||||||
old.Permissions = new(config.Permissions);
|
|
||||||
old.PermRole = config.PermissionRole;
|
|
||||||
old.Verbose = config.VerbosePermissions;
|
|
||||||
return old;
|
|
||||||
});
|
|
||||||
|
|
||||||
public async Task<bool> ExecPreCommandAsync(ICommandContext ctx, string moduleName, CommandInfo command)
|
|
||||||
{
|
|
||||||
var guild = ctx.Guild;
|
|
||||||
var msg = ctx.Message;
|
|
||||||
var user = ctx.User;
|
|
||||||
var channel = ctx.Channel;
|
|
||||||
var commandName = command.Name.ToLowerInvariant();
|
|
||||||
|
|
||||||
if (guild is null)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
var resetCommand = commandName == "resetperms";
|
|
||||||
|
|
||||||
var pc = GetCacheFor(guild.Id);
|
|
||||||
if (!resetCommand
|
|
||||||
&& !pc.Permissions.CheckPermissions(msg.Author, msg.Channel, commandName, moduleName, out var index))
|
|
||||||
{
|
|
||||||
if (pc.Verbose)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await _sender.Response(channel)
|
|
||||||
.Error(_strings.GetText(strs.perm_prevent(index + 1,
|
|
||||||
Format.Bold(pc.Permissions[index]
|
|
||||||
.GetCommand(_cmd.GetPrefix(guild), (SocketGuild)guild))),
|
|
||||||
guild.Id))
|
|
||||||
.SendAsync();
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
if (moduleName == nameof(Permissions))
|
|
||||||
{
|
|
||||||
if (user is not IGuildUser guildUser)
|
|
||||||
return true;
|
|
||||||
|
|
||||||
if (guildUser.GuildPermissions.Administrator)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
var permRole = pc.PermRole;
|
|
||||||
if (!ulong.TryParse(permRole, out var rid))
|
|
||||||
rid = 0;
|
|
||||||
string returnMsg;
|
|
||||||
IRole role;
|
|
||||||
if (string.IsNullOrWhiteSpace(permRole) || (role = guild.GetRole(rid)) is null)
|
|
||||||
{
|
|
||||||
returnMsg = "You need Admin permissions in order to use permission commands.";
|
|
||||||
if (pc.Verbose)
|
|
||||||
{
|
|
||||||
try { await _sender.Response(channel).Error(returnMsg).SendAsync(); }
|
|
||||||
catch { }
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!guildUser.RoleIds.Contains(rid))
|
|
||||||
{
|
|
||||||
returnMsg = $"You need the {Format.Bold(role.Name)} role in order to use permission commands.";
|
|
||||||
if (pc.Verbose)
|
|
||||||
{
|
|
||||||
try { await _sender.Response(channel).Error(returnMsg).SendAsync(); }
|
|
||||||
catch { }
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task Reset(ulong guildId)
|
|
||||||
{
|
|
||||||
await using var uow = _db.GetDbContext();
|
|
||||||
var config = uow.GcWithPermissionsFor(guildId);
|
|
||||||
config.Permissions = Permissionv2.GetDefaultPermlist;
|
|
||||||
await uow.SaveChangesAsync();
|
|
||||||
UpdateCache(config);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,37 +0,0 @@
|
||||||
#nullable disable
|
|
||||||
using EllieBot.Modules.Permissions.Services;
|
|
||||||
|
|
||||||
namespace EllieBot.Modules.Permissions;
|
|
||||||
|
|
||||||
public partial class Permissions
|
|
||||||
{
|
|
||||||
[Group]
|
|
||||||
public partial class ResetPermissionsCommands : EllieModule
|
|
||||||
{
|
|
||||||
private readonly GlobalPermissionService _gps;
|
|
||||||
private readonly PermissionService _perms;
|
|
||||||
|
|
||||||
public ResetPermissionsCommands(GlobalPermissionService gps, PermissionService perms)
|
|
||||||
{
|
|
||||||
_gps = gps;
|
|
||||||
_perms = perms;
|
|
||||||
}
|
|
||||||
|
|
||||||
[Cmd]
|
|
||||||
[RequireContext(ContextType.Guild)]
|
|
||||||
[UserPerm(GuildPerm.Administrator)]
|
|
||||||
public async Task ResetPerms()
|
|
||||||
{
|
|
||||||
await _perms.Reset(ctx.Guild.Id);
|
|
||||||
await Response().Confirm(strs.perms_reset).SendAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
[Cmd]
|
|
||||||
[OwnerOnly]
|
|
||||||
public async Task ResetGlobalPerms()
|
|
||||||
{
|
|
||||||
await _gps.Reset();
|
|
||||||
await Response().Confirm(strs.global_perms_reset).SendAsync();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,164 +0,0 @@
|
||||||
#nullable disable
|
|
||||||
using Microsoft.Extensions.Configuration;
|
|
||||||
using EllieBot.Common.Yml;
|
|
||||||
|
|
||||||
namespace EllieBot.Services;
|
|
||||||
|
|
||||||
public sealed class BotCredsProvider : IBotCredsProvider
|
|
||||||
{
|
|
||||||
private const string CREDS_FILE_NAME = "creds.yml";
|
|
||||||
private const string CREDS_EXAMPLE_FILE_NAME = "creds_example.yml";
|
|
||||||
|
|
||||||
private string CredsPath { get; }
|
|
||||||
|
|
||||||
private string CredsExamplePath { get; }
|
|
||||||
|
|
||||||
private readonly int? _totalShards;
|
|
||||||
|
|
||||||
|
|
||||||
private readonly Creds _creds = new();
|
|
||||||
private readonly IConfigurationRoot _config;
|
|
||||||
|
|
||||||
|
|
||||||
private readonly object _reloadLock = new();
|
|
||||||
|
|
||||||
public BotCredsProvider(int? totalShards = null, string credPath = null)
|
|
||||||
{
|
|
||||||
_totalShards = totalShards;
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(credPath))
|
|
||||||
{
|
|
||||||
CredsPath = credPath;
|
|
||||||
CredsExamplePath = Path.Combine(Path.GetDirectoryName(credPath), CREDS_EXAMPLE_FILE_NAME);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
CredsPath = Path.Combine(Directory.GetCurrentDirectory(), CREDS_FILE_NAME);
|
|
||||||
CredsExamplePath = Path.Combine(Directory.GetCurrentDirectory(), CREDS_EXAMPLE_FILE_NAME);
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (!File.Exists(CredsExamplePath))
|
|
||||||
File.WriteAllText(CredsExamplePath, Yaml.Serializer.Serialize(_creds));
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
// this can fail in docker containers
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
MigrateCredentials();
|
|
||||||
|
|
||||||
if (!File.Exists(CredsPath))
|
|
||||||
{
|
|
||||||
Log.Warning(
|
|
||||||
"{CredsPath} is missing. Attempting to load creds from environment variables prefixed with 'EllieBot_'. Example is in {CredsExamplePath}",
|
|
||||||
CredsPath,
|
|
||||||
CredsExamplePath);
|
|
||||||
}
|
|
||||||
|
|
||||||
_config = new ConfigurationBuilder().AddYamlFile(CredsPath, false, true)
|
|
||||||
.AddEnvironmentVariables("EllieBot_")
|
|
||||||
.Build();
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Console.WriteLine(ex.ToString());
|
|
||||||
}
|
|
||||||
|
|
||||||
Reload();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Reload()
|
|
||||||
{
|
|
||||||
lock (_reloadLock)
|
|
||||||
{
|
|
||||||
_creds.OwnerIds.Clear();
|
|
||||||
_config.Bind(_creds);
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(_creds.Token))
|
|
||||||
{
|
|
||||||
Log.Error("Token is missing from creds.yml or Environment variables.\nAdd it and restart the program");
|
|
||||||
Helpers.ReadErrorAndExit(5);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(_creds.RestartCommand?.Cmd)
|
|
||||||
|| string.IsNullOrWhiteSpace(_creds.RestartCommand?.Args))
|
|
||||||
{
|
|
||||||
if (Environment.OSVersion.Platform == PlatformID.Unix)
|
|
||||||
{
|
|
||||||
_creds.RestartCommand = new RestartConfig()
|
|
||||||
{
|
|
||||||
Args = "dotnet",
|
|
||||||
Cmd = "EllieBot.dll -- {0}"
|
|
||||||
};
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_creds.RestartCommand = new RestartConfig()
|
|
||||||
{
|
|
||||||
Args = "EllieBot.exe",
|
|
||||||
Cmd = "{0}"
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(_creds.RedisOptions))
|
|
||||||
_creds.RedisOptions = "127.0.0.1,syncTimeout=3000";
|
|
||||||
|
|
||||||
// replace the old generated key with the shared key
|
|
||||||
if (string.IsNullOrWhiteSpace(_creds.CoinmarketcapApiKey)
|
|
||||||
|| _creds.CoinmarketcapApiKey.StartsWith("e79ec505-0913"))
|
|
||||||
_creds.CoinmarketcapApiKey = "3077537c-7dfb-4d97-9a60-56fc9a9f5035";
|
|
||||||
|
|
||||||
_creds.TotalShards = _totalShards ?? _creds.TotalShards;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void ModifyCredsFile(Action<IBotCredentials> func)
|
|
||||||
{
|
|
||||||
var ymlData = File.ReadAllText(CREDS_FILE_NAME);
|
|
||||||
var creds = Yaml.Deserializer.Deserialize<Creds>(ymlData);
|
|
||||||
|
|
||||||
func(creds);
|
|
||||||
|
|
||||||
ymlData = Yaml.Serializer.Serialize(creds);
|
|
||||||
File.WriteAllText(CREDS_FILE_NAME, ymlData);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void MigrateCredentials()
|
|
||||||
{
|
|
||||||
if (File.Exists(CREDS_FILE_NAME))
|
|
||||||
{
|
|
||||||
var creds = Yaml.Deserializer.Deserialize<Creds>(File.ReadAllText(CREDS_FILE_NAME));
|
|
||||||
if (creds.Version <= 5)
|
|
||||||
{
|
|
||||||
creds.BotCache = BotCacheImplemenation.Redis;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (creds.Version <= 6)
|
|
||||||
{
|
|
||||||
creds.Version = 7;
|
|
||||||
File.WriteAllText(CREDS_FILE_NAME, Yaml.Serializer.Serialize(creds));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (creds.Version <= 8)
|
|
||||||
{
|
|
||||||
creds.Version = 9;
|
|
||||||
File.WriteAllText(CREDS_FILE_NAME, Yaml.Serializer.Serialize(creds));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public IBotCredentials GetCreds()
|
|
||||||
{
|
|
||||||
lock (_reloadLock)
|
|
||||||
{
|
|
||||||
return _creds;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,229 +0,0 @@
|
||||||
#nullable disable
|
|
||||||
using Google;
|
|
||||||
using Google.Apis.Services;
|
|
||||||
using Google.Apis.Urlshortener.v1;
|
|
||||||
using Google.Apis.YouTube.v3;
|
|
||||||
using Newtonsoft.Json.Linq;
|
|
||||||
using System.Net;
|
|
||||||
using System.Text.RegularExpressions;
|
|
||||||
using System.Xml;
|
|
||||||
|
|
||||||
namespace EllieBot.Services;
|
|
||||||
|
|
||||||
public sealed partial class GoogleApiService : IGoogleApiService, IEService
|
|
||||||
{
|
|
||||||
private static readonly Regex
|
|
||||||
_plRegex = new(@"(?:youtu\.be\/|list=)(?<id>[\da-zA-Z\-_]*)", RegexOptions.Compiled);
|
|
||||||
|
|
||||||
|
|
||||||
private readonly YouTubeService _yt;
|
|
||||||
private readonly UrlshortenerService _sh;
|
|
||||||
|
|
||||||
//private readonly Regex YtVideoIdRegex = new Regex(@"(?:youtube\.com\/\S*(?:(?:\/e(?:mbed))?\/|watch\?(?:\S*?&?v\=))|youtu\.be\/)(?<id>[a-zA-Z0-9_-]{6,11})", RegexOptions.Compiled);
|
|
||||||
private readonly IBotCredsProvider _creds;
|
|
||||||
private readonly IHttpClientFactory _httpFactory;
|
|
||||||
|
|
||||||
public GoogleApiService(IBotCredsProvider creds, IHttpClientFactory factory) : this()
|
|
||||||
{
|
|
||||||
_creds = creds;
|
|
||||||
_httpFactory = factory;
|
|
||||||
|
|
||||||
var bcs = new BaseClientService.Initializer
|
|
||||||
{
|
|
||||||
ApplicationName = "Ellie Bot",
|
|
||||||
ApiKey = _creds.GetCreds().GoogleApiKey
|
|
||||||
};
|
|
||||||
|
|
||||||
_yt = new(bcs);
|
|
||||||
_sh = new(bcs);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<IEnumerable<string>> GetPlaylistIdsByKeywordsAsync(string keywords, int count = 1)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(keywords))
|
|
||||||
throw new ArgumentNullException(nameof(keywords));
|
|
||||||
|
|
||||||
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(count);
|
|
||||||
|
|
||||||
var match = _plRegex.Match(keywords);
|
|
||||||
if (match.Length > 1)
|
|
||||||
return new[] { match.Groups["id"].Value };
|
|
||||||
var query = _yt.Search.List("snippet");
|
|
||||||
query.MaxResults = count;
|
|
||||||
query.Type = "playlist";
|
|
||||||
query.Q = keywords;
|
|
||||||
|
|
||||||
return (await query.ExecuteAsync()).Items.Select(i => i.Id.PlaylistId);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<IEnumerable<string>> GetRelatedVideosAsync(string id, int count = 2, string user = null)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(id))
|
|
||||||
throw new ArgumentNullException(nameof(id));
|
|
||||||
|
|
||||||
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(count);
|
|
||||||
|
|
||||||
var query = _yt.Search.List("snippet");
|
|
||||||
query.MaxResults = count;
|
|
||||||
query.Q = id;
|
|
||||||
// query.RelatedToVideoId = id;
|
|
||||||
query.Type = "video";
|
|
||||||
query.QuotaUser = user;
|
|
||||||
// bad workaround as there's no replacement for related video querying right now.
|
|
||||||
// Query youtube with the id of the video, take a second video in the results
|
|
||||||
// skip the first one as that's probably the same video.
|
|
||||||
return (await query.ExecuteAsync()).Items.Select(i => "https://www.youtube.com/watch?v=" + i.Id.VideoId).Skip(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<IEnumerable<string>> GetVideoLinksByKeywordAsync(string keywords, int count = 1)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(keywords))
|
|
||||||
throw new ArgumentNullException(nameof(keywords));
|
|
||||||
|
|
||||||
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(count);
|
|
||||||
|
|
||||||
var query = _yt.Search.List("snippet");
|
|
||||||
query.MaxResults = count;
|
|
||||||
query.Q = keywords;
|
|
||||||
query.Type = "video";
|
|
||||||
query.SafeSearch = SearchResource.ListRequest.SafeSearchEnum.Strict;
|
|
||||||
return (await query.ExecuteAsync()).Items.Select(i => "https://www.youtube.com/watch?v=" + i.Id.VideoId);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<IEnumerable<(string Name, string Id, string Url, string Thumbnail)>> GetVideoInfosByKeywordAsync(
|
|
||||||
string keywords,
|
|
||||||
int count = 1)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(keywords))
|
|
||||||
throw new ArgumentNullException(nameof(keywords));
|
|
||||||
|
|
||||||
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(count);
|
|
||||||
|
|
||||||
var query = _yt.Search.List("snippet");
|
|
||||||
query.MaxResults = count;
|
|
||||||
query.Q = keywords;
|
|
||||||
query.Type = "video";
|
|
||||||
return (await query.ExecuteAsync()).Items.Select(i
|
|
||||||
=> (i.Snippet.Title.TrimTo(50),
|
|
||||||
i.Id.VideoId,
|
|
||||||
"https://www.youtube.com/watch?v=" + i.Id.VideoId,
|
|
||||||
i.Snippet.Thumbnails.High.Url));
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task<string> ShortenUrl(Uri url)
|
|
||||||
=> ShortenUrl(url.ToString());
|
|
||||||
|
|
||||||
public async Task<string> ShortenUrl(string url)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(url))
|
|
||||||
throw new ArgumentNullException(nameof(url));
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(_creds.GetCreds().GoogleApiKey))
|
|
||||||
return url;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var response = await _sh.Url.Insert(new()
|
|
||||||
{
|
|
||||||
LongUrl = url
|
|
||||||
})
|
|
||||||
.ExecuteAsync();
|
|
||||||
return response.Id;
|
|
||||||
}
|
|
||||||
catch (GoogleApiException ex) when (ex.HttpStatusCode == HttpStatusCode.Forbidden)
|
|
||||||
{
|
|
||||||
return url;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Log.Warning(ex, "Error shortening URL");
|
|
||||||
return url;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<IEnumerable<string>> GetPlaylistTracksAsync(string playlistId, int count = 50)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(playlistId))
|
|
||||||
throw new ArgumentNullException(nameof(playlistId));
|
|
||||||
|
|
||||||
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(count);
|
|
||||||
|
|
||||||
string nextPageToken = null;
|
|
||||||
|
|
||||||
var toReturn = new List<string>(count);
|
|
||||||
|
|
||||||
do
|
|
||||||
{
|
|
||||||
var toGet = count > 50 ? 50 : count;
|
|
||||||
count -= toGet;
|
|
||||||
|
|
||||||
var query = _yt.PlaylistItems.List("contentDetails");
|
|
||||||
query.MaxResults = toGet;
|
|
||||||
query.PlaylistId = playlistId;
|
|
||||||
query.PageToken = nextPageToken;
|
|
||||||
|
|
||||||
var data = await query.ExecuteAsync();
|
|
||||||
|
|
||||||
toReturn.AddRange(data.Items.Select(i => i.ContentDetails.VideoId));
|
|
||||||
nextPageToken = data.NextPageToken;
|
|
||||||
} while (count > 0 && !string.IsNullOrWhiteSpace(nextPageToken));
|
|
||||||
|
|
||||||
return toReturn;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<IReadOnlyDictionary<string, TimeSpan>> GetVideoDurationsAsync(IEnumerable<string> videoIds)
|
|
||||||
{
|
|
||||||
var videoIdsList = videoIds as List<string> ?? videoIds.ToList();
|
|
||||||
|
|
||||||
var toReturn = new Dictionary<string, TimeSpan>();
|
|
||||||
|
|
||||||
if (!videoIdsList.Any())
|
|
||||||
return toReturn;
|
|
||||||
var remaining = videoIdsList.Count;
|
|
||||||
|
|
||||||
do
|
|
||||||
{
|
|
||||||
var toGet = remaining > 50 ? 50 : remaining;
|
|
||||||
remaining -= toGet;
|
|
||||||
|
|
||||||
var q = _yt.Videos.List("contentDetails");
|
|
||||||
q.Id = string.Join(",", videoIdsList.Take(toGet));
|
|
||||||
videoIdsList = videoIdsList.Skip(toGet).ToList();
|
|
||||||
var items = (await q.ExecuteAsync()).Items;
|
|
||||||
foreach (var i in items)
|
|
||||||
toReturn.Add(i.Id, XmlConvert.ToTimeSpan(i.ContentDetails.Duration));
|
|
||||||
} while (remaining > 0);
|
|
||||||
|
|
||||||
return toReturn;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<string> Translate(string sourceText, string sourceLanguage, string targetLanguage)
|
|
||||||
{
|
|
||||||
string text;
|
|
||||||
|
|
||||||
if (!Languages.ContainsKey(sourceLanguage) || !Languages.ContainsKey(targetLanguage))
|
|
||||||
throw new ArgumentException(nameof(sourceLanguage) + "/" + nameof(targetLanguage));
|
|
||||||
|
|
||||||
|
|
||||||
var url = new Uri(string.Format(
|
|
||||||
"https://translate.googleapis.com/translate_a/single?client=gtx&sl={0}&tl={1}&dt=t&q={2}",
|
|
||||||
ConvertToLanguageCode(sourceLanguage),
|
|
||||||
ConvertToLanguageCode(targetLanguage),
|
|
||||||
WebUtility.UrlEncode(sourceText)));
|
|
||||||
using (var http = _httpFactory.CreateClient())
|
|
||||||
{
|
|
||||||
http.DefaultRequestHeaders.Add("user-agent",
|
|
||||||
"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2228.0 Safari/537.36");
|
|
||||||
text = await http.GetStringAsync(url);
|
|
||||||
}
|
|
||||||
|
|
||||||
return string.Concat(JArray.Parse(text)[0].Select(x => x[0]));
|
|
||||||
}
|
|
||||||
|
|
||||||
private string ConvertToLanguageCode(string language)
|
|
||||||
{
|
|
||||||
Languages.TryGetValue(language, out var mode);
|
|
||||||
return mode;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,160 +0,0 @@
|
||||||
namespace EllieBot.Services;
|
|
||||||
|
|
||||||
public sealed partial class GoogleApiService
|
|
||||||
{
|
|
||||||
private const string SUPPORTED = """
|
|
||||||
afrikaans af
|
|
||||||
albanian sq
|
|
||||||
amharic am
|
|
||||||
arabic ar
|
|
||||||
armenian hy
|
|
||||||
assamese as
|
|
||||||
aymara ay
|
|
||||||
azerbaijani az
|
|
||||||
bambara bm
|
|
||||||
basque eu
|
|
||||||
belarusian be
|
|
||||||
bengali bn
|
|
||||||
bhojpuri bho
|
|
||||||
bosnian bs
|
|
||||||
bulgarian bg
|
|
||||||
catalan ca
|
|
||||||
cebuano ceb
|
|
||||||
chinese zh-CN
|
|
||||||
chinese-trad zh-TW
|
|
||||||
corsican co
|
|
||||||
croatian hr
|
|
||||||
czech cs
|
|
||||||
danish da
|
|
||||||
dhivehi dv
|
|
||||||
dogri doi
|
|
||||||
dutch nl
|
|
||||||
english en
|
|
||||||
esperanto eo
|
|
||||||
estonian et
|
|
||||||
ewe ee
|
|
||||||
filipino fil
|
|
||||||
finnish fi
|
|
||||||
french fr
|
|
||||||
frisian fy
|
|
||||||
galician gl
|
|
||||||
georgian ka
|
|
||||||
german de
|
|
||||||
greek el
|
|
||||||
guarani gn
|
|
||||||
gujarati gu
|
|
||||||
haitian ht
|
|
||||||
hausa ha
|
|
||||||
hawaiian haw
|
|
||||||
hebrew he
|
|
||||||
hindi hi
|
|
||||||
hmong hmn
|
|
||||||
hungarian hu
|
|
||||||
icelandic is
|
|
||||||
igbo ig
|
|
||||||
ilocano ilo
|
|
||||||
indonesian id
|
|
||||||
irish ga
|
|
||||||
italian it
|
|
||||||
japanese ja
|
|
||||||
javanese jv
|
|
||||||
kannada kn
|
|
||||||
kazakh kk
|
|
||||||
khmer km
|
|
||||||
kinyarwanda rw
|
|
||||||
konkani gom
|
|
||||||
korean ko
|
|
||||||
krio kri
|
|
||||||
kurdish ku
|
|
||||||
kurdish-sor ckb
|
|
||||||
kyrgyz ky
|
|
||||||
lao lo
|
|
||||||
latin la
|
|
||||||
latvian lv
|
|
||||||
lingala ln
|
|
||||||
lithuanian lt
|
|
||||||
luganda lg
|
|
||||||
luxembourgish lb
|
|
||||||
macedonian mk
|
|
||||||
maithili mai
|
|
||||||
malagasy mg
|
|
||||||
malay ms
|
|
||||||
malayalam ml
|
|
||||||
maltese mt
|
|
||||||
maori mi
|
|
||||||
marathi mr
|
|
||||||
meiteilon mni-Mtei
|
|
||||||
mizo lus
|
|
||||||
mongolian mn
|
|
||||||
myanmar my
|
|
||||||
nepali ne
|
|
||||||
norwegian no
|
|
||||||
nyanja ny
|
|
||||||
odia or
|
|
||||||
oromo om
|
|
||||||
pashto ps
|
|
||||||
persian fa
|
|
||||||
polish pl
|
|
||||||
portuguese pt
|
|
||||||
punjabi pa
|
|
||||||
quechua qu
|
|
||||||
romanian ro
|
|
||||||
russian ru
|
|
||||||
samoan sm
|
|
||||||
sanskrit sa
|
|
||||||
scots gd
|
|
||||||
sepedi nso
|
|
||||||
serbian sr
|
|
||||||
sesotho st
|
|
||||||
shona sn
|
|
||||||
sindhi sd
|
|
||||||
sinhala si
|
|
||||||
slovak sk
|
|
||||||
slovenian sl
|
|
||||||
somali so
|
|
||||||
spanish es
|
|
||||||
sundanese su
|
|
||||||
swahili sw
|
|
||||||
swedish sv
|
|
||||||
tagalog tl
|
|
||||||
tajik tg
|
|
||||||
tamil ta
|
|
||||||
tatar tt
|
|
||||||
telugu te
|
|
||||||
thai th
|
|
||||||
tigrinya ti
|
|
||||||
tsonga ts
|
|
||||||
turkish tr
|
|
||||||
turkmen tk
|
|
||||||
twi ak
|
|
||||||
ukrainian uk
|
|
||||||
urdu ur
|
|
||||||
uyghur ug
|
|
||||||
uzbek uz
|
|
||||||
vietnamese vi
|
|
||||||
welsh cy
|
|
||||||
xhosa xh
|
|
||||||
yiddish yi
|
|
||||||
yoruba yo
|
|
||||||
zulu zu
|
|
||||||
""";
|
|
||||||
|
|
||||||
|
|
||||||
public IReadOnlyDictionary<string, string> Languages { get; }
|
|
||||||
|
|
||||||
private GoogleApiService()
|
|
||||||
{
|
|
||||||
var langs = SUPPORTED.Split("\n")
|
|
||||||
.Select(x => x.Split(' '))
|
|
||||||
.ToDictionary(x => x[0].Trim(), x => x[1].Trim());
|
|
||||||
|
|
||||||
foreach (var (_, v) in langs.ToArray())
|
|
||||||
{
|
|
||||||
langs.Add(v, v);
|
|
||||||
}
|
|
||||||
|
|
||||||
Languages = langs;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,71 +0,0 @@
|
||||||
namespace EllieBot.Services;
|
|
||||||
|
|
||||||
public sealed class ImageCache : IImageCache, IEService
|
|
||||||
{
|
|
||||||
private readonly IBotCache _cache;
|
|
||||||
private readonly ImagesConfig _ic;
|
|
||||||
private readonly Random _rng;
|
|
||||||
private readonly IHttpClientFactory _httpFactory;
|
|
||||||
|
|
||||||
public ImageCache(
|
|
||||||
IBotCache cache,
|
|
||||||
ImagesConfig ic,
|
|
||||||
IHttpClientFactory httpFactory)
|
|
||||||
{
|
|
||||||
_cache = cache;
|
|
||||||
_ic = ic;
|
|
||||||
_httpFactory = httpFactory;
|
|
||||||
_rng = new EllieRandom();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static TypedKey<byte[]> GetImageKey(Uri url)
|
|
||||||
=> new($"image:{url}");
|
|
||||||
|
|
||||||
public async Task<byte[]?> GetImageDataAsync(Uri url)
|
|
||||||
=> await _cache.GetOrAddAsync(
|
|
||||||
GetImageKey(url),
|
|
||||||
async () =>
|
|
||||||
{
|
|
||||||
if (url.IsFile)
|
|
||||||
{
|
|
||||||
return await File.ReadAllBytesAsync(url.LocalPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
using var http = _httpFactory.CreateClient();
|
|
||||||
var bytes = await http.GetByteArrayAsync(url);
|
|
||||||
return bytes;
|
|
||||||
},
|
|
||||||
expiry: TimeSpan.FromHours(48));
|
|
||||||
|
|
||||||
private async Task<byte[]?> GetRandomImageDataAsync(Uri[] urls)
|
|
||||||
{
|
|
||||||
if (urls.Length == 0)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
var url = urls[_rng.Next(0, urls.Length)];
|
|
||||||
|
|
||||||
var data = await GetImageDataAsync(url);
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task<byte[]?> GetHeadsImageAsync()
|
|
||||||
=> GetRandomImageDataAsync(_ic.Data.Coins.Heads);
|
|
||||||
|
|
||||||
public Task<byte[]?> GetTailsImageAsync()
|
|
||||||
=> GetRandomImageDataAsync(_ic.Data.Coins.Tails);
|
|
||||||
|
|
||||||
public Task<byte[]?> GetCurrencyImageAsync()
|
|
||||||
=> GetRandomImageDataAsync(_ic.Data.Currency);
|
|
||||||
|
|
||||||
public Task<byte[]?> GetXpBackgroundImageAsync()
|
|
||||||
=> GetImageDataAsync(_ic.Data.Xp.Bg);
|
|
||||||
|
|
||||||
public Task<byte[]?> GetDiceAsync(int num)
|
|
||||||
=> GetImageDataAsync(_ic.Data.Dice[num]);
|
|
||||||
|
|
||||||
public Task<byte[]?> GetSlotEmojiAsync(int number)
|
|
||||||
=> GetImageDataAsync(_ic.Data.Slots.Emojis[number]);
|
|
||||||
|
|
||||||
public Task<byte[]?> GetSlotBgAsync()
|
|
||||||
=> GetImageDataAsync(_ic.Data.Slots.Bg);
|
|
||||||
}
|
|
|
@ -1,108 +0,0 @@
|
||||||
using EllieBot.Common.Pokemon;
|
|
||||||
using EllieBot.Modules.Games.Common.Trivia;
|
|
||||||
using System.Text.Json;
|
|
||||||
using System.Text.Json.Serialization;
|
|
||||||
|
|
||||||
namespace EllieBot.Services;
|
|
||||||
|
|
||||||
public sealed class LocalDataCache : ILocalDataCache, IEService
|
|
||||||
{
|
|
||||||
private const string POKEMON_ABILITIES_FILE = "data/pokemon/pokemon_abilities.json";
|
|
||||||
private const string POKEMON_LIST_FILE = "data/pokemon/pokemon_list.json";
|
|
||||||
private const string POKEMON_MAP_PATH = "data/pokemon/name-id_map.json";
|
|
||||||
private const string QUESTIONS_FILE = "data/trivia_questions.json";
|
|
||||||
|
|
||||||
private readonly IBotCache _cache;
|
|
||||||
|
|
||||||
private readonly JsonSerializerOptions _opts = new JsonSerializerOptions()
|
|
||||||
{
|
|
||||||
AllowTrailingCommas = true,
|
|
||||||
NumberHandling = JsonNumberHandling.AllowReadingFromString,
|
|
||||||
PropertyNameCaseInsensitive = true
|
|
||||||
};
|
|
||||||
|
|
||||||
public LocalDataCache(IBotCache cache)
|
|
||||||
=> _cache = cache;
|
|
||||||
|
|
||||||
private async Task<T?> GetOrCreateCachedDataAsync<T>(
|
|
||||||
TypedKey<T> key,
|
|
||||||
string fileName)
|
|
||||||
=> await _cache.GetOrAddAsync(key,
|
|
||||||
async () =>
|
|
||||||
{
|
|
||||||
if (!File.Exists(fileName))
|
|
||||||
{
|
|
||||||
Log.Warning($"{fileName} is missing. Relevant data can't be loaded");
|
|
||||||
return default;
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await using var stream = File.OpenRead(fileName);
|
|
||||||
return await JsonSerializer.DeserializeAsync<T>(stream, _opts);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Log.Error(ex,
|
|
||||||
"Error reading {FileName} file: {ErrorMessage}",
|
|
||||||
fileName,
|
|
||||||
ex.Message);
|
|
||||||
|
|
||||||
return default;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
private static TypedKey<IReadOnlyDictionary<string, SearchPokemon>> _pokemonListKey
|
|
||||||
= new("pokemon:list");
|
|
||||||
|
|
||||||
public async Task<IReadOnlyDictionary<string, SearchPokemon>?> GetPokemonsAsync()
|
|
||||||
=> await GetOrCreateCachedDataAsync(_pokemonListKey, POKEMON_LIST_FILE);
|
|
||||||
|
|
||||||
|
|
||||||
private static TypedKey<IReadOnlyDictionary<string, SearchPokemonAbility>> _pokemonAbilitiesKey
|
|
||||||
= new("pokemon:abilities");
|
|
||||||
|
|
||||||
public async Task<IReadOnlyDictionary<string, SearchPokemonAbility>?> GetPokemonAbilitiesAsync()
|
|
||||||
=> await GetOrCreateCachedDataAsync(_pokemonAbilitiesKey, POKEMON_ABILITIES_FILE);
|
|
||||||
|
|
||||||
|
|
||||||
private static TypedKey<IReadOnlyDictionary<int, string>> _pokeMapKey
|
|
||||||
= new("pokemon:ab_map2"); // 2 because ab_map was storing arrays
|
|
||||||
|
|
||||||
public async Task<IReadOnlyDictionary<int, string>?> GetPokemonMapAsync()
|
|
||||||
=> await _cache.GetOrAddAsync(_pokeMapKey,
|
|
||||||
async () =>
|
|
||||||
{
|
|
||||||
var fileName = POKEMON_MAP_PATH;
|
|
||||||
if (!File.Exists(fileName))
|
|
||||||
{
|
|
||||||
Log.Warning($"{fileName} is missing. Relevant data can't be loaded");
|
|
||||||
return default;
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await using var stream = File.OpenRead(fileName);
|
|
||||||
var arr = await JsonSerializer.DeserializeAsync<PokemonNameId[]>(stream, _opts);
|
|
||||||
|
|
||||||
return (IReadOnlyDictionary<int, string>?)arr?.ToDictionary(x => x.Id, x => x.Name);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Log.Error(ex,
|
|
||||||
"Error reading {FileName} file: {ErrorMessage}",
|
|
||||||
fileName,
|
|
||||||
ex.Message);
|
|
||||||
|
|
||||||
return default;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
private static TypedKey<TriviaQuestionModel[]> _triviaKey
|
|
||||||
= new("trivia:questions");
|
|
||||||
|
|
||||||
public async Task<TriviaQuestionModel[]?> GetTriviaQuestionsAsync()
|
|
||||||
=> await GetOrCreateCachedDataAsync(_triviaKey, QUESTIONS_FILE);
|
|
||||||
}
|
|
|
@ -1,120 +0,0 @@
|
||||||
#nullable disable
|
|
||||||
using Newtonsoft.Json;
|
|
||||||
using System.Globalization;
|
|
||||||
|
|
||||||
namespace EllieBot.Services;
|
|
||||||
|
|
||||||
public class Localization : ILocalization
|
|
||||||
{
|
|
||||||
private static readonly Dictionary<string, CommandData> _commandData =
|
|
||||||
JsonConvert.DeserializeObject<Dictionary<string, CommandData>>(
|
|
||||||
File.ReadAllText("./data/strings/commands/commands.en-US.json"));
|
|
||||||
|
|
||||||
private readonly ConcurrentDictionary<ulong, CultureInfo> _guildCultureInfos;
|
|
||||||
|
|
||||||
public IDictionary<ulong, CultureInfo> GuildCultureInfos
|
|
||||||
=> _guildCultureInfos;
|
|
||||||
|
|
||||||
public CultureInfo DefaultCultureInfo
|
|
||||||
=> _bss.Data.DefaultLocale;
|
|
||||||
|
|
||||||
private readonly BotConfigService _bss;
|
|
||||||
private readonly DbService _db;
|
|
||||||
|
|
||||||
public Localization(BotConfigService bss, Bot bot, DbService db)
|
|
||||||
{
|
|
||||||
_bss = bss;
|
|
||||||
_db = db;
|
|
||||||
|
|
||||||
var cultureInfoNames = bot.AllGuildConfigs.ToDictionary(x => x.GuildId, x => x.Locale);
|
|
||||||
|
|
||||||
_guildCultureInfos = new(cultureInfoNames
|
|
||||||
.ToDictionary(x => x.Key,
|
|
||||||
x =>
|
|
||||||
{
|
|
||||||
CultureInfo cultureInfo = null;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (x.Value is null)
|
|
||||||
return null;
|
|
||||||
cultureInfo = new(x.Value);
|
|
||||||
}
|
|
||||||
catch { }
|
|
||||||
|
|
||||||
return cultureInfo;
|
|
||||||
})
|
|
||||||
.Where(x => x.Value is not null));
|
|
||||||
}
|
|
||||||
|
|
||||||
public void SetGuildCulture(IGuild guild, CultureInfo ci)
|
|
||||||
=> SetGuildCulture(guild.Id, ci);
|
|
||||||
|
|
||||||
public void SetGuildCulture(ulong guildId, CultureInfo ci)
|
|
||||||
{
|
|
||||||
if (ci.Name == _bss.Data.DefaultLocale.Name)
|
|
||||||
{
|
|
||||||
RemoveGuildCulture(guildId);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
using (var uow = _db.GetDbContext())
|
|
||||||
{
|
|
||||||
var gc = uow.GuildConfigsForId(guildId, set => set);
|
|
||||||
gc.Locale = ci.Name;
|
|
||||||
uow.SaveChanges();
|
|
||||||
}
|
|
||||||
|
|
||||||
_guildCultureInfos.AddOrUpdate(guildId, ci, (_, _) => ci);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void RemoveGuildCulture(IGuild guild)
|
|
||||||
=> RemoveGuildCulture(guild.Id);
|
|
||||||
|
|
||||||
public void RemoveGuildCulture(ulong guildId)
|
|
||||||
{
|
|
||||||
if (_guildCultureInfos.TryRemove(guildId, out _))
|
|
||||||
{
|
|
||||||
using var uow = _db.GetDbContext();
|
|
||||||
var gc = uow.GuildConfigsForId(guildId, set => set);
|
|
||||||
gc.Locale = null;
|
|
||||||
uow.SaveChanges();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void SetDefaultCulture(CultureInfo ci)
|
|
||||||
=> _bss.ModifyConfig(bs =>
|
|
||||||
{
|
|
||||||
bs.DefaultLocale = ci;
|
|
||||||
});
|
|
||||||
|
|
||||||
public void ResetDefaultCulture()
|
|
||||||
=> SetDefaultCulture(CultureInfo.CurrentCulture);
|
|
||||||
|
|
||||||
public CultureInfo GetCultureInfo(IGuild guild)
|
|
||||||
=> GetCultureInfo(guild?.Id);
|
|
||||||
|
|
||||||
public CultureInfo GetCultureInfo(ulong? guildId)
|
|
||||||
{
|
|
||||||
if (guildId is null || !GuildCultureInfos.TryGetValue(guildId.Value, out var info) || info is null)
|
|
||||||
return _bss.Data.DefaultLocale;
|
|
||||||
|
|
||||||
return info;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static CommandData LoadCommand(string key)
|
|
||||||
{
|
|
||||||
_commandData.TryGetValue(key, out var toReturn);
|
|
||||||
|
|
||||||
if (toReturn is null)
|
|
||||||
{
|
|
||||||
return new()
|
|
||||||
{
|
|
||||||
Cmd = key,
|
|
||||||
Desc = key,
|
|
||||||
Usage = [key]
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return toReturn;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,28 +0,0 @@
|
||||||
using EllieBot.Common.JsonConverters;
|
|
||||||
using System.Text.Json;
|
|
||||||
|
|
||||||
namespace EllieBot.Common;
|
|
||||||
|
|
||||||
public class JsonSeria : ISeria
|
|
||||||
{
|
|
||||||
private readonly JsonSerializerOptions _serializerOptions = new()
|
|
||||||
{
|
|
||||||
IncludeFields = true,
|
|
||||||
Converters =
|
|
||||||
{
|
|
||||||
new Rgba32Converter(),
|
|
||||||
new CultureInfoConverter()
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
public byte[] Serialize<T>(T data)
|
|
||||||
=> JsonSerializer.SerializeToUtf8Bytes(data, _serializerOptions);
|
|
||||||
|
|
||||||
public T? Deserialize<T>(byte[]? data)
|
|
||||||
{
|
|
||||||
if (data is null)
|
|
||||||
return default;
|
|
||||||
|
|
||||||
return JsonSerializer.Deserialize<T>(data, _serializerOptions);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,57 +0,0 @@
|
||||||
using StackExchange.Redis;
|
|
||||||
|
|
||||||
namespace EllieBot.Common;
|
|
||||||
|
|
||||||
public sealed class RedisPubSub : IPubSub
|
|
||||||
{
|
|
||||||
private readonly IBotCredentials _creds;
|
|
||||||
private readonly ConnectionMultiplexer _multi;
|
|
||||||
private readonly ISeria _serializer;
|
|
||||||
|
|
||||||
public RedisPubSub(ConnectionMultiplexer multi, ISeria serializer, IBotCredentials creds)
|
|
||||||
{
|
|
||||||
_multi = multi;
|
|
||||||
_serializer = serializer;
|
|
||||||
_creds = creds;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task Pub<TData>(in TypedKey<TData> key, TData data)
|
|
||||||
where TData : notnull
|
|
||||||
{
|
|
||||||
var serialized = _serializer.Serialize(data);
|
|
||||||
return _multi.GetSubscriber()
|
|
||||||
.PublishAsync(new RedisChannel($"{_creds.RedisKey()}:{key.Key}", RedisChannel.PatternMode.Literal),
|
|
||||||
serialized,
|
|
||||||
CommandFlags.FireAndForget);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task Sub<TData>(in TypedKey<TData> key, Func<TData, ValueTask> action)
|
|
||||||
where TData : notnull
|
|
||||||
{
|
|
||||||
var eventName = key.Key;
|
|
||||||
|
|
||||||
async void OnSubscribeHandler(RedisChannel _, RedisValue data)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var dataObj = _serializer.Deserialize<TData>(data);
|
|
||||||
if (dataObj is not null)
|
|
||||||
await action(dataObj);
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Log.Warning("Publishing event {EventName} with a null value. This is not allowed",
|
|
||||||
eventName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Log.Error("Error handling the event {EventName}: {ErrorMessage}", eventName, ex.Message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return _multi.GetSubscriber()
|
|
||||||
.SubscribeAsync(
|
|
||||||
new RedisChannel($"{_creds.RedisKey()}:{eventName}", RedisChannel.PatternMode.Literal),
|
|
||||||
OnSubscribeHandler);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,39 +0,0 @@
|
||||||
using EllieBot.Common.Configs;
|
|
||||||
using EllieBot.Common.Yml;
|
|
||||||
using System.Text.RegularExpressions;
|
|
||||||
using YamlDotNet.Serialization;
|
|
||||||
|
|
||||||
namespace EllieBot.Common;
|
|
||||||
|
|
||||||
public class YamlSeria : IConfigSeria
|
|
||||||
{
|
|
||||||
private static readonly Regex _codePointRegex =
|
|
||||||
new(@"(\\U(?<code>[a-zA-Z0-9]{8})|\\u(?<code>[a-zA-Z0-9]{4})|\\x(?<code>[a-zA-Z0-9]{2}))",
|
|
||||||
RegexOptions.Compiled);
|
|
||||||
|
|
||||||
private readonly IDeserializer _deserializer;
|
|
||||||
private readonly ISerializer _serializer;
|
|
||||||
|
|
||||||
public YamlSeria()
|
|
||||||
{
|
|
||||||
_serializer = Yaml.Serializer;
|
|
||||||
_deserializer = Yaml.Deserializer;
|
|
||||||
}
|
|
||||||
|
|
||||||
public string Serialize<T>(T obj)
|
|
||||||
where T : notnull
|
|
||||||
{
|
|
||||||
var escapedOutput = _serializer.Serialize(obj);
|
|
||||||
var output = _codePointRegex.Replace(escapedOutput,
|
|
||||||
me =>
|
|
||||||
{
|
|
||||||
var str = me.Groups["code"].Value;
|
|
||||||
var newString = str.UnescapeUnicodeCodePoint();
|
|
||||||
return newString;
|
|
||||||
});
|
|
||||||
return output;
|
|
||||||
}
|
|
||||||
|
|
||||||
public T Deserialize<T>(string data)
|
|
||||||
=> _deserializer.Deserialize<T>(data);
|
|
||||||
}
|
|
|
@ -1,120 +0,0 @@
|
||||||
using OneOf;
|
|
||||||
using OneOf.Types;
|
|
||||||
using StackExchange.Redis;
|
|
||||||
using System.Text.Json;
|
|
||||||
using System.Text.Json.Serialization;
|
|
||||||
|
|
||||||
namespace EllieBot.Common;
|
|
||||||
|
|
||||||
public sealed class RedisBotCache : IBotCache
|
|
||||||
{
|
|
||||||
private static readonly Type[] _supportedTypes =
|
|
||||||
[
|
|
||||||
typeof(bool), typeof(int), typeof(uint), typeof(long),
|
|
||||||
typeof(ulong), typeof(float), typeof(double),
|
|
||||||
typeof(string), typeof(byte[]), typeof(ReadOnlyMemory<byte>), typeof(Memory<byte>),
|
|
||||||
typeof(RedisValue)
|
|
||||||
];
|
|
||||||
|
|
||||||
private static readonly JsonSerializerOptions _opts = new()
|
|
||||||
{
|
|
||||||
PropertyNameCaseInsensitive = true,
|
|
||||||
NumberHandling = JsonNumberHandling.AllowReadingFromString,
|
|
||||||
AllowTrailingCommas = true,
|
|
||||||
IgnoreReadOnlyProperties = false,
|
|
||||||
};
|
|
||||||
private readonly ConnectionMultiplexer _conn;
|
|
||||||
|
|
||||||
public RedisBotCache(ConnectionMultiplexer conn)
|
|
||||||
{
|
|
||||||
_conn = conn;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async ValueTask<bool> AddAsync<T>(TypedKey<T> key, T value, TimeSpan? expiry = null, bool overwrite = true)
|
|
||||||
{
|
|
||||||
// if a null value is passed, remove the key
|
|
||||||
if (value is null)
|
|
||||||
{
|
|
||||||
await RemoveAsync(key);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var db = _conn.GetDatabase();
|
|
||||||
RedisValue val = IsSupportedType(typeof(T))
|
|
||||||
? RedisValue.Unbox(value)
|
|
||||||
: JsonSerializer.Serialize(value, _opts);
|
|
||||||
|
|
||||||
var success = await db.StringSetAsync(key.Key,
|
|
||||||
val,
|
|
||||||
expiry: expiry,
|
|
||||||
keepTtl: true,
|
|
||||||
when: overwrite ? When.Always : When.NotExists);
|
|
||||||
|
|
||||||
return success;
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool IsSupportedType(Type type)
|
|
||||||
{
|
|
||||||
if (type.IsGenericType)
|
|
||||||
{
|
|
||||||
var typeDef = type.GetGenericTypeDefinition();
|
|
||||||
if (typeDef == typeof(Nullable<>))
|
|
||||||
return IsSupportedType(type.GenericTypeArguments[0]);
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var t in _supportedTypes)
|
|
||||||
{
|
|
||||||
if (type == t)
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async ValueTask<OneOf<T, None>> GetAsync<T>(TypedKey<T> key)
|
|
||||||
{
|
|
||||||
var db = _conn.GetDatabase();
|
|
||||||
var val = await db.StringGetAsync(key.Key);
|
|
||||||
if (val == default)
|
|
||||||
return new None();
|
|
||||||
|
|
||||||
if (IsSupportedType(typeof(T)))
|
|
||||||
return (T)((IConvertible)val).ToType(typeof(T), null);
|
|
||||||
|
|
||||||
return JsonSerializer.Deserialize<T>(val.ToString(), _opts)!;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async ValueTask<bool> RemoveAsync<T>(TypedKey<T> key)
|
|
||||||
{
|
|
||||||
var db = _conn.GetDatabase();
|
|
||||||
|
|
||||||
return await db.KeyDeleteAsync(key.Key);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async ValueTask<T?> GetOrAddAsync<T>(TypedKey<T> key, Func<Task<T?>> createFactory, TimeSpan? expiry = null)
|
|
||||||
{
|
|
||||||
var result = await GetAsync(key);
|
|
||||||
|
|
||||||
return await result.Match<Task<T?>>(
|
|
||||||
v => Task.FromResult<T?>(v),
|
|
||||||
async _ =>
|
|
||||||
{
|
|
||||||
var factoryValue = await createFactory();
|
|
||||||
|
|
||||||
if (factoryValue is null)
|
|
||||||
return default;
|
|
||||||
|
|
||||||
await AddAsync(key, factoryValue, expiry);
|
|
||||||
|
|
||||||
// get again to make sure it's the cached value
|
|
||||||
// and not the late factory value, in case there's a race condition
|
|
||||||
|
|
||||||
var newResult = await GetAsync(key);
|
|
||||||
|
|
||||||
// it's fine to do this, it should blow up if something went wrong.
|
|
||||||
return newResult.Match<T?>(
|
|
||||||
v => v,
|
|
||||||
_ => default);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,91 +0,0 @@
|
||||||
#nullable disable
|
|
||||||
using StackExchange.Redis;
|
|
||||||
using System.Text.Json;
|
|
||||||
using System.Web;
|
|
||||||
|
|
||||||
namespace EllieBot.Services;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Uses <see cref="IStringsSource" /> to load strings into redis hash (only on Shard 0)
|
|
||||||
/// and retrieves them from redis via <see cref="GetText" />
|
|
||||||
/// </summary>
|
|
||||||
public class RedisBotStringsProvider : IBotStringsProvider
|
|
||||||
{
|
|
||||||
private const string COMMANDS_KEY = "commands_v5";
|
|
||||||
|
|
||||||
private readonly ConnectionMultiplexer _redis;
|
|
||||||
private readonly IStringsSource _source;
|
|
||||||
private readonly IBotCredentials _creds;
|
|
||||||
|
|
||||||
public RedisBotStringsProvider(
|
|
||||||
ConnectionMultiplexer redis,
|
|
||||||
DiscordSocketClient discordClient,
|
|
||||||
IStringsSource source,
|
|
||||||
IBotCredentials creds)
|
|
||||||
{
|
|
||||||
_redis = redis;
|
|
||||||
_source = source;
|
|
||||||
_creds = creds;
|
|
||||||
|
|
||||||
if (discordClient.ShardId == 0)
|
|
||||||
Reload();
|
|
||||||
}
|
|
||||||
|
|
||||||
public string GetText(string localeName, string key)
|
|
||||||
{
|
|
||||||
var value = _redis.GetDatabase().HashGet($"{_creds.RedisKey()}:responses:{localeName}", key);
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
public CommandStrings GetCommandStrings(string localeName, string commandName)
|
|
||||||
{
|
|
||||||
string examplesStr = _redis.GetDatabase()
|
|
||||||
.HashGet($"{_creds.RedisKey()}:{COMMANDS_KEY}:{localeName}",
|
|
||||||
$"{commandName}::examples");
|
|
||||||
if (examplesStr == default)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
var descStr = _redis.GetDatabase()
|
|
||||||
.HashGet($"{_creds.RedisKey()}:{COMMANDS_KEY}:{localeName}", $"{commandName}::desc");
|
|
||||||
if (descStr == default)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
var ex = examplesStr.Split('&').Map(HttpUtility.UrlDecode);
|
|
||||||
|
|
||||||
var paramsStr = _redis.GetDatabase()
|
|
||||||
.HashGet($"{_creds.RedisKey()}:{COMMANDS_KEY}:{localeName}", $"{commandName}::params");
|
|
||||||
if (paramsStr == default)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
return new()
|
|
||||||
{
|
|
||||||
Examples = ex,
|
|
||||||
Params = JsonSerializer.Deserialize<Dictionary<string, CommandStringParam>[]>(paramsStr),
|
|
||||||
Desc = descStr
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Reload()
|
|
||||||
{
|
|
||||||
var redisDb = _redis.GetDatabase();
|
|
||||||
foreach (var (localeName, localeStrings) in _source.GetResponseStrings())
|
|
||||||
{
|
|
||||||
var hashFields = localeStrings.Select(x => new HashEntry(x.Key, x.Value)).ToArray();
|
|
||||||
|
|
||||||
redisDb.HashSet($"{_creds.RedisKey()}:responses:{localeName}", hashFields);
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var (localeName, localeStrings) in _source.GetCommandStrings())
|
|
||||||
{
|
|
||||||
var hashFields = localeStrings
|
|
||||||
.Select(x => new HashEntry($"{x.Key}::examples",
|
|
||||||
string.Join('&', x.Value.Examples.Map(HttpUtility.UrlEncode))))
|
|
||||||
.Concat(localeStrings.Select(x => new HashEntry($"{x.Key}::desc", x.Value.Desc)))
|
|
||||||
.Concat(localeStrings.Select(x
|
|
||||||
=> new HashEntry($"{x.Key}::params", JsonSerializer.Serialize(x.Value.Params))))
|
|
||||||
.ToArray();
|
|
||||||
|
|
||||||
redisDb.HashSet($"{_creds.RedisKey()}:{COMMANDS_KEY}:{localeName}", hashFields);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,132 +0,0 @@
|
||||||
#nullable disable
|
|
||||||
using Grpc.Core;
|
|
||||||
using Grpc.Net.Client;
|
|
||||||
using EllieBot.Common.ModuleBehaviors;
|
|
||||||
using EllieBot.Coordinator;
|
|
||||||
|
|
||||||
namespace EllieBot.Services;
|
|
||||||
|
|
||||||
public class RemoteGrpcCoordinator : ICoordinator, IReadyExecutor
|
|
||||||
{
|
|
||||||
private readonly Coordinator.Coordinator.CoordinatorClient _coordClient;
|
|
||||||
private readonly DiscordSocketClient _client;
|
|
||||||
|
|
||||||
public RemoteGrpcCoordinator(IBotCredentials creds, DiscordSocketClient client)
|
|
||||||
{
|
|
||||||
var coordUrl = string.IsNullOrWhiteSpace(creds.CoordinatorUrl) ? "http://localhost:3442" : creds.CoordinatorUrl;
|
|
||||||
|
|
||||||
var channel = GrpcChannel.ForAddress(coordUrl);
|
|
||||||
_coordClient = new(channel);
|
|
||||||
_client = client;
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool RestartBot()
|
|
||||||
{
|
|
||||||
_coordClient.RestartAllShards(new());
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Die(bool graceful)
|
|
||||||
=> _coordClient.Die(new()
|
|
||||||
{
|
|
||||||
Graceful = graceful
|
|
||||||
});
|
|
||||||
|
|
||||||
public bool RestartShard(int shardId)
|
|
||||||
{
|
|
||||||
_coordClient.RestartShard(new()
|
|
||||||
{
|
|
||||||
ShardId = shardId
|
|
||||||
});
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public IList<ShardStatus> GetAllShardStatuses()
|
|
||||||
{
|
|
||||||
var res = _coordClient.GetAllStatuses(new());
|
|
||||||
|
|
||||||
return res.Statuses.ToArray()
|
|
||||||
.Map(s => new ShardStatus
|
|
||||||
{
|
|
||||||
ConnectionState = FromCoordConnState(s.State),
|
|
||||||
GuildCount = s.GuildCount,
|
|
||||||
ShardId = s.ShardId,
|
|
||||||
LastUpdate = s.LastUpdate.ToDateTime()
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public int GetGuildCount()
|
|
||||||
{
|
|
||||||
var res = _coordClient.GetAllStatuses(new());
|
|
||||||
|
|
||||||
return res.Statuses.Sum(x => x.GuildCount);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task Reload()
|
|
||||||
=> await _coordClient.ReloadAsync(new());
|
|
||||||
|
|
||||||
public Task OnReadyAsync()
|
|
||||||
{
|
|
||||||
Task.Run(async () =>
|
|
||||||
{
|
|
||||||
var gracefulImminent = false;
|
|
||||||
while (true)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var reply = await _coordClient.HeartbeatAsync(new()
|
|
||||||
{
|
|
||||||
State = ToCoordConnState(_client.ConnectionState),
|
|
||||||
GuildCount =
|
|
||||||
_client.ConnectionState == ConnectionState.Connected ? _client.Guilds.Count : 0,
|
|
||||||
ShardId = _client.ShardId
|
|
||||||
},
|
|
||||||
deadline: DateTime.UtcNow + TimeSpan.FromSeconds(10));
|
|
||||||
gracefulImminent = reply.GracefulImminent;
|
|
||||||
}
|
|
||||||
catch (RpcException ex)
|
|
||||||
{
|
|
||||||
if (!gracefulImminent)
|
|
||||||
{
|
|
||||||
Log.Warning(ex,
|
|
||||||
"Hearbeat failed and graceful shutdown was not expected: {Message}",
|
|
||||||
ex.Message);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
Log.Information("Coordinator is restarting gracefully. Waiting...");
|
|
||||||
await Task.Delay(30_000);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Log.Error(ex, "Unexpected heartbeat exception: {Message}", ex.Message);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
await Task.Delay(7500);
|
|
||||||
}
|
|
||||||
|
|
||||||
Environment.Exit(5);
|
|
||||||
});
|
|
||||||
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
|
|
||||||
private ConnState ToCoordConnState(ConnectionState state)
|
|
||||||
=> state switch
|
|
||||||
{
|
|
||||||
ConnectionState.Connecting => ConnState.Connecting,
|
|
||||||
ConnectionState.Connected => ConnState.Connected,
|
|
||||||
_ => ConnState.Disconnected
|
|
||||||
};
|
|
||||||
|
|
||||||
private ConnectionState FromCoordConnState(ConnState state)
|
|
||||||
=> state switch
|
|
||||||
{
|
|
||||||
ConnState.Connecting => ConnectionState.Connecting,
|
|
||||||
ConnState.Connected => ConnectionState.Connected,
|
|
||||||
_ => ConnectionState.Disconnected
|
|
||||||
};
|
|
||||||
}
|
|
Loading…
Reference in a new issue