Added permissions module

This commit is contained in:
Toastie 2024-09-20 23:24:21 +12:00
parent d9b644d50e
commit 5505052af4
Signed by: toastie_t0ast
GPG key ID: 27F3B6855AFD40A4
15 changed files with 2151 additions and 0 deletions

View file

@ -0,0 +1,154 @@
#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();
}
}
}
}

View file

@ -0,0 +1,15 @@
#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"));
}

View file

@ -0,0 +1,141 @@
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();
}
}

View file

@ -0,0 +1,106 @@
#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();
}
}
}
}

View file

@ -0,0 +1,326 @@
#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();
}
}
}

View file

@ -0,0 +1,249 @@
#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,
};
}
}

View file

@ -0,0 +1,10 @@
#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; }
}

View file

@ -0,0 +1,77 @@
#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();
}
}
}

View file

@ -0,0 +1,92 @@
#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;
}
}

View file

@ -0,0 +1,11 @@
#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; }
}

View file

@ -0,0 +1,132 @@
#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;
}
}

View file

@ -0,0 +1,543 @@
#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();
}
}

View file

@ -0,0 +1,74 @@
#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);
}
}
}

View file

@ -0,0 +1,184 @@
#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);
}
}

View file

@ -0,0 +1,37 @@
#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();
}
}
}