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