Converted enough to send the system card

This commit is contained in:
Ske 2020-12-22 16:55:13 +01:00
parent a6fbd869be
commit 05334f0d25
28 changed files with 343 additions and 198 deletions

View File

@ -17,12 +17,12 @@ namespace Myriad.Cache
public ValueTask RemoveUser(ulong userId);
public ValueTask RemoveRole(ulong guildId, ulong roleId);
public ValueTask<Guild?> GetGuild(ulong guildId);
public ValueTask<Channel?> GetChannel(ulong channelId);
public ValueTask<User?> GetUser(ulong userId);
public ValueTask<Role?> GetRole(ulong roleId);
public bool TryGetGuild(ulong guildId, out Guild guild);
public bool TryGetChannel(ulong channelId, out Channel channel);
public bool TryGetUser(ulong userId, out User user);
public bool TryGetRole(ulong roleId, out Role role);
public IAsyncEnumerable<Guild> GetAllGuilds();
public ValueTask<IEnumerable<Channel>> GetGuildChannels(ulong guildId);
public IEnumerable<Channel> GetGuildChannels(ulong guildId);
}
}

View File

@ -110,13 +110,26 @@ namespace Myriad.Cache
return default;
}
public ValueTask<Guild?> GetGuild(ulong guildId) => new(_guilds.GetValueOrDefault(guildId)?.Guild);
public bool TryGetGuild(ulong guildId, out Guild guild)
{
if (_guilds.TryGetValue(guildId, out var cg))
{
guild = cg.Guild;
return true;
}
public ValueTask<Channel?> GetChannel(ulong channelId) => new(_channels.GetValueOrDefault(channelId));
guild = null!;
return false;
}
public ValueTask<User?> GetUser(ulong userId) => new(_users.GetValueOrDefault(userId));
public bool TryGetChannel(ulong channelId, out Channel channel) =>
_channels.TryGetValue(channelId, out channel!);
public ValueTask<Role?> GetRole(ulong roleId) => new(_roles.GetValueOrDefault(roleId));
public bool TryGetUser(ulong userId, out User user) =>
_users.TryGetValue(userId, out user!);
public bool TryGetRole(ulong roleId, out Role role) =>
_roles.TryGetValue(roleId, out role!);
public async IAsyncEnumerable<Guild> GetAllGuilds()
{
@ -124,12 +137,12 @@ namespace Myriad.Cache
yield return guild.Guild;
}
public ValueTask<IEnumerable<Channel>> GetGuildChannels(ulong guildId)
public IEnumerable<Channel> GetGuildChannels(ulong guildId)
{
if (!_guilds.TryGetValue(guildId, out var guild))
throw new ArgumentException("Guild not found", nameof(guildId));
return new ValueTask<IEnumerable<Channel>>(guild.Channels.Keys.Select(c => _channels[c]));
return guild.Channels.Keys.Select(c => _channels[c]);
}
private CachedGuild SaveGuildRaw(Guild guild) =>

View File

@ -0,0 +1,45 @@
using System.Collections.Generic;
using Myriad.Cache;
using Myriad.Types;
namespace Myriad.Extensions
{
public static class CacheExtensions
{
public static Guild GetGuild(this IDiscordCache cache, ulong guildId)
{
if (!cache.TryGetGuild(guildId, out var guild))
throw new KeyNotFoundException($"Guild {guildId} not found in cache");
return guild;
}
public static Channel GetChannel(this IDiscordCache cache, ulong channelId)
{
if (!cache.TryGetChannel(channelId, out var channel))
throw new KeyNotFoundException($"Channel {channelId} not found in cache");
return channel;
}
public static Channel? GetChannelOrNull(this IDiscordCache cache, ulong channelId)
{
if (cache.TryGetChannel(channelId, out var channel))
return channel;
return null;
}
public static User GetUser(this IDiscordCache cache, ulong userId)
{
if (!cache.TryGetUser(userId, out var user))
throw new KeyNotFoundException($"User {userId} not found in cache");
return user;
}
public static Role GetRole(this IDiscordCache cache, ulong roleId)
{
if (!cache.TryGetRole(roleId, out var role))
throw new KeyNotFoundException($"User {roleId} not found in cache");
return role;
}
}
}

View File

@ -1,7 +1,9 @@
namespace Myriad.Extensions
using Myriad.Types;
namespace Myriad.Extensions
{
public static class ChannelExtensions
{
public static string Mention(this Channel channel) => $"<#{channel.Id}>";
}
}

View File

@ -0,0 +1,7 @@
namespace Myriad.Extensions
{
public static class GuildExtensions
{
}
}

View File

@ -1,7 +1,6 @@
namespace Myriad.Extensions
{
public class MessageExtensions
public static class MessageExtensions
{
}
}

View File

@ -1,7 +1,9 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Myriad.Cache;
using Myriad.Gateway;
using Myriad.Types;
@ -9,17 +11,39 @@ namespace Myriad.Extensions
{
public static class PermissionExtensions
{
public static PermissionSet PermissionsFor(this IDiscordCache cache, MessageCreateEvent message) =>
PermissionsFor(cache, message.ChannelId, message.Author.Id, message.Member?.Roles);
public static PermissionSet PermissionsFor(this IDiscordCache cache, ulong channelId, GuildMember member) =>
PermissionsFor(cache, channelId, member.User.Id, member.Roles);
public static PermissionSet PermissionsFor(this IDiscordCache cache, ulong channelId, ulong userId, GuildMemberPartial member) =>
PermissionsFor(cache, channelId, userId, member.Roles);
public static PermissionSet PermissionsFor(this IDiscordCache cache, ulong channelId, ulong userId, ICollection<ulong>? userRoles)
{
var channel = cache.GetChannel(channelId);
if (channel.GuildId == null)
return PermissionSet.Dm;
var guild = cache.GetGuild(channel.GuildId.Value);
return PermissionsFor(guild, channel, userId, userRoles);
}
public static PermissionSet EveryonePermissions(this Guild guild) =>
guild.Roles.FirstOrDefault(r => r.Id == guild.Id)?.Permissions ?? PermissionSet.Dm;
public static PermissionSet PermissionsFor(Guild guild, Channel channel, MessageCreateEvent msg) =>
PermissionsFor(guild, channel, msg.Author.Id, msg.Member!.Roles);
PermissionsFor(guild, channel, msg.Author.Id, msg.Member?.Roles);
public static PermissionSet PermissionsFor(Guild guild, Channel channel, ulong userId,
ICollection<ulong> roleIds)
ICollection<ulong>? roleIds)
{
if (channel.Type == Channel.ChannelType.Dm)
return PermissionSet.Dm;
if (roleIds == null)
throw new ArgumentException($"User roles must be specified for guild channels");
var perms = GuildPermissions(guild, userId, roleIds);
perms = ApplyChannelOverwrites(perms, channel, userId, roleIds);
@ -36,9 +60,6 @@ namespace Myriad.Extensions
return perms;
}
public static bool Has(this PermissionSet value, PermissionSet flag) =>
(value & flag) == flag;
public static PermissionSet GuildPermissions(this Guild guild, ulong userId, ICollection<ulong> roleIds)
{
if (guild.OwnerId == userId)
@ -51,7 +72,7 @@ namespace Myriad.Extensions
perms |= role.Permissions;
}
if (perms.Has(PermissionSet.Administrator))
if (perms.HasFlag(PermissionSet.Administrator))
return PermissionSet.All;
return perms;

View File

@ -4,6 +4,8 @@ namespace Myriad.Extensions
{
public static class UserExtensions
{
public static string Mention(this User user) => $"<@{user.Id}>";
public static string AvatarUrl(this User user) =>
$"https://cdn.discordapp.com/avatars/{user.Id}/{user.Avatar}.png";
}

View File

@ -11,9 +11,9 @@ namespace Myriad.Rest.Types
Everyone
}
public List<ParseType>? Parse { get; set; }
public List<ulong>? Users { get; set; }
public List<ulong>? Roles { get; set; }
public ParseType[]? Parse { get; set; }
public ulong[]? Users { get; set; }
public ulong[]? Roles { get; set; }
public bool RepliedUser { get; set; }
}
}

View File

@ -8,6 +8,6 @@ namespace Myriad.Rest.Types.Requests
public object? Nonce { get; set; }
public bool Tts { get; set; }
public AllowedMentions AllowedMentions { get; set; }
public Embed? Embeds { get; set; }
public Embed? Embed { get; set; }
}
}

View File

@ -20,7 +20,7 @@
public string? Name { get; init; }
public string? Topic { get; init; }
public bool? Nsfw { get; init; }
public long? ParentId { get; init; }
public ulong? ParentId { get; init; }
public Overwrite[]? PermissionOverwrites { get; init; }
public record Overwrite

View File

@ -8,14 +8,17 @@ using Autofac;
using DSharpPlus;
using DSharpPlus.Entities;
using DSharpPlus.Net;
using Myriad.Cache;
using Myriad.Extensions;
using Myriad.Gateway;
using Myriad.Rest.Types.Requests;
using Myriad.Types;
using PluralKit.Core;
using Permissions = DSharpPlus.Permissions;
using DiscordApiClient = Myriad.Rest.DiscordApiClient;
namespace PluralKit.Bot
{
@ -24,6 +27,7 @@ namespace PluralKit.Bot
private readonly ILifetimeScope _provider;
private readonly DiscordRestClient _rest;
private readonly DiscordApiClient _newRest;
private readonly DiscordShardedClient _client;
private readonly DiscordClient _shard = null;
private readonly Shard _shardNew;
@ -42,6 +46,7 @@ namespace PluralKit.Bot
private readonly PKSystem _senderSystem;
private readonly IMetrics _metrics;
private readonly CommandMessageService _commandMessageService;
private readonly IDiscordCache _cache;
private Command _currentCommand;
@ -57,24 +62,25 @@ namespace PluralKit.Bot
_senderSystem = senderSystem;
_messageContext = messageContext;
_botMember = botMember;
_cache = provider.Resolve<IDiscordCache>();
_db = provider.Resolve<IDatabase>();
_repo = provider.Resolve<ModelRepository>();
_metrics = provider.Resolve<IMetrics>();
_provider = provider;
_commandMessageService = provider.Resolve<CommandMessageService>();
_parameters = new Parameters(message.Content.Substring(commandParseOffset));
_newRest = provider.Resolve<DiscordApiClient>();
_botPermissions = message.GuildId != null
? PermissionExtensions.PermissionsFor(guild!, channel, shard.User?.Id ?? default, botMember!.Roles)
: PermissionSet.Dm;
_userPermissions = message.GuildId != null
? PermissionExtensions.PermissionsFor(guild!, channel, message.Author.Id, message.Member!.Roles)
: PermissionSet.Dm;
_botPermissions = _cache.PermissionsFor(message.ChannelId, shard.User!.Id, botMember!);
_userPermissions = _cache.PermissionsFor(message);
}
public IDiscordCache Cache => _cache;
public DiscordUser Author => _message.Author;
public DiscordChannel Channel => _message.Channel;
public Channel ChannelNew => _channel;
public User AuthorNew => _messageNew.Author;
public DiscordMessage Message => _message;
public Message MessageNew => _messageNew;
public DiscordGuild Guild => _message.Channel.Guild;
@ -95,24 +101,44 @@ namespace PluralKit.Bot
internal IDatabase Database => _db;
internal ModelRepository Repository => _repo;
public async Task<DiscordMessage> Reply(string text = null, DiscordEmbed embed = null, IEnumerable<IMention> mentions = null)
public Task<DiscordMessage> Reply(string text, DiscordEmbed embed,
IEnumerable<IMention>? mentions = null)
{
if (!this.BotHasAllPermissions(Permissions.SendMessages))
return Reply(text, (DiscordEmbed) null, mentions);
}
public Task<DiscordMessage> Reply(DiscordEmbed embed,
IEnumerable<IMention>? mentions = null)
{
return Reply(null, (DiscordEmbed) null, mentions);
}
public async Task<DiscordMessage> Reply(string text = null, Embed embed = null, IEnumerable<IMention>? mentions = null)
{
if (!BotPermissions.HasFlag(PermissionSet.SendMessages))
// Will be "swallowed" during the error handler anyway, this message is never shown.
throw new PKError("PluralKit does not have permission to send messages in this channel.");
if (embed != null && !this.BotHasAllPermissions(Permissions.EmbedLinks))
if (embed != null && !BotPermissions.HasFlag(PermissionSet.EmbedLinks))
throw new PKError("PluralKit does not have permission to send embeds in this channel. Please ensure I have the **Embed Links** permission enabled.");
var msg = await Channel.SendMessageFixedAsync(text, embed: embed, mentions: mentions);
var msg = await _newRest.CreateMessage(_channel.Id, new MessageRequest
{
Content = text,
Embed = embed
});
// TODO: embeds/mentions
// var msg = await Channel.SendMessageFixedAsync(text, embed: embed, mentions: mentions);
if (embed != null)
{
// Sensitive information that might want to be deleted by :x: reaction is typically in an embed format (member cards, for example)
// This may need to be changed at some point but works well enough for now
await _commandMessageService.RegisterMessage(msg.Id, Author.Id);
await _commandMessageService.RegisterMessage(msg.Id, AuthorNew.Id);
}
return msg;
// return msg;
return null;
}
public async Task Execute<T>(Command commandDef, Func<T, Task> handler)

View File

@ -1,5 +1,7 @@
using DSharpPlus;
using Myriad.Types;
using PluralKit.Core;
namespace PluralKit.Bot
@ -8,7 +10,7 @@ namespace PluralKit.Bot
{
public static Context CheckGuildContext(this Context ctx)
{
if (ctx.Channel.Guild != null) return ctx;
if (ctx.ChannelNew.GuildId != null) return ctx;
throw new PKError("This command can not be run in a DM.");
}
@ -46,12 +48,9 @@ namespace PluralKit.Bot
return ctx;
}
public static Context CheckAuthorPermission(this Context ctx, Permissions neededPerms, string permissionName)
public static Context CheckAuthorPermission(this Context ctx, PermissionSet neededPerms, string permissionName)
{
// TODO: can we always assume Author is a DiscordMember? I would think so, given they always come from a
// message received event...
var hasPerms = ctx.Channel.PermissionsInSync(ctx.Author);
if ((hasPerms & neededPerms) != neededPerms)
if ((ctx.UserPermissions & neededPerms) != neededPerms)
throw new PKError($"You must have the \"{permissionName}\" permission in this server to use this command.");
return ctx;
}

View File

@ -3,6 +3,8 @@
using DSharpPlus;
using DSharpPlus.Entities;
using Myriad.Types;
using PluralKit.Bot.Utils;
using PluralKit.Core;
@ -153,13 +155,16 @@ namespace PluralKit.Bot
return $"Group not found. Note that a group ID is 5 characters long.";
}
public static async Task<DiscordChannel> MatchChannel(this Context ctx)
public static async Task<Channel> MatchChannel(this Context ctx)
{
if (!MentionUtils.TryParseChannel(ctx.PeekArgument(), out var id))
return null;
if (!ctx.Cache.TryGetChannel(id, out var channel))
return null;
var channel = await ctx.Shard.GetChannel(id);
if (channel == null || !(channel.Type == ChannelType.Text || channel.Type == ChannelType.News)) return null;
if (!(channel.Type == Channel.ChannelType.GuildText || channel.Type == Channel.ChannelType.GuildText))
return null;
ctx.PopArgument();
return channel;

View File

@ -2,7 +2,6 @@ using System.Linq;
using System.Threading.Tasks;
using DSharpPlus;
using DSharpPlus.Exceptions;
using Humanizer;

View File

@ -25,7 +25,7 @@ namespace PluralKit.Bot
if (location == AvatarLocation.Server)
{
if (target.AvatarUrl != null)
await ctx.Reply($"{Emojis.Success} Member server avatar cleared. This member will now use the global avatar in this server (**{ctx.Guild.Name}**).");
await ctx.Reply($"{Emojis.Success} Member server avatar cleared. This member will now use the global avatar in this server (**{ctx.GuildNew.Name}**).");
else
await ctx.Reply($"{Emojis.Success} Member server avatar cleared. This member now has no avatar.");
}
@ -55,7 +55,7 @@ namespace PluralKit.Bot
throw new PKError($"This member does not have a server avatar set. Type `pk;member {target.Reference()} avatar` to see their global avatar.");
}
var field = location == AvatarLocation.Server ? $"server avatar (for {ctx.Guild.Name})" : "avatar";
var field = location == AvatarLocation.Server ? $"server avatar (for {ctx.GuildNew.Name})" : "avatar";
var cmd = location == AvatarLocation.Server ? "serveravatar" : "avatar";
var eb = new DiscordEmbedBuilder()
@ -69,14 +69,14 @@ namespace PluralKit.Bot
public async Task ServerAvatar(Context ctx, PKMember target)
{
ctx.CheckGuildContext();
var guildData = await _db.Execute(c => _repo.GetMemberGuild(c, ctx.Guild.Id, target.Id));
var guildData = await _db.Execute(c => _repo.GetMemberGuild(c, ctx.GuildNew.Id, target.Id));
await AvatarCommandTree(AvatarLocation.Server, ctx, target, guildData);
}
public async Task Avatar(Context ctx, PKMember target)
{
var guildData = ctx.Guild != null ?
await _db.Execute(c => _repo.GetMemberGuild(c, ctx.Guild.Id, target.Id))
var guildData = ctx.GuildNew != null ?
await _db.Execute(c => _repo.GetMemberGuild(c, ctx.GuildNew.Id, target.Id))
: null;
await AvatarCommandTree(AvatarLocation.Member, ctx, target, guildData);
@ -119,8 +119,8 @@ namespace PluralKit.Bot
var serverFrag = location switch
{
AvatarLocation.Server => $" This avatar will now be used when proxying in this server (**{ctx.Guild.Name}**).",
AvatarLocation.Member when targetGuildData?.AvatarUrl != null => $"\n{Emojis.Note} Note that this member *also* has a server-specific avatar set in this server (**{ctx.Guild.Name}**), and thus changing the global avatar will have no effect here.",
AvatarLocation.Server => $" This avatar will now be used when proxying in this server (**{ctx.GuildNew.Name}**).",
AvatarLocation.Member when targetGuildData?.AvatarUrl != null => $"\n{Emojis.Note} Note that this member *also* has a server-specific avatar set in this server (**{ctx.GuildNew.Name}**), and thus changing the global avatar will have no effect here.",
_ => ""
};
@ -145,7 +145,7 @@ namespace PluralKit.Bot
{
case AvatarLocation.Server:
var serverPatch = new MemberGuildPatch { AvatarUrl = url };
return _db.Execute(c => _repo.UpsertMemberGuild(c, target.Id, ctx.Guild.Id, serverPatch));
return _db.Execute(c => _repo.UpsertMemberGuild(c, target.Id, ctx.GuildNew.Id, serverPatch));
case AvatarLocation.Member:
var memberPatch = new MemberPatch { AvatarUrl = url };
return _db.Execute(c => _repo.UpdateMember(c, target.Id, memberPatch));

View File

@ -2,9 +2,6 @@ using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System;
using Dapper;
using DSharpPlus.Entities;
using NodaTime;
@ -49,11 +46,11 @@ namespace PluralKit.Bot
if (newName.Contains(" ")) await ctx.Reply($"{Emojis.Note} Note that this member's name now contains spaces. You will need to surround it with \"double quotes\" when using commands referring to it.");
if (target.DisplayName != null) await ctx.Reply($"{Emojis.Note} Note that this member has a display name set ({target.DisplayName}), and will be proxied using that name instead.");
if (ctx.Guild != null)
if (ctx.GuildNew != null)
{
var memberGuildConfig = await _db.Execute(c => _repo.GetMemberGuild(c, ctx.Guild.Id, target.Id));
var memberGuildConfig = await _db.Execute(c => _repo.GetMemberGuild(c, ctx.GuildNew.Id, target.Id));
if (memberGuildConfig.DisplayName != null)
await ctx.Reply($"{Emojis.Note} Note that this member has a server name set ({memberGuildConfig.DisplayName}) in this server ({ctx.Guild.Name}), and will be proxied using that name here.");
await ctx.Reply($"{Emojis.Note} Note that this member has a server name set ({memberGuildConfig.DisplayName}) in this server ({ctx.GuildNew.Name}), and will be proxied using that name here.");
}
}
@ -229,8 +226,8 @@ namespace PluralKit.Bot
var lcx = ctx.LookupContextFor(target);
MemberGuildSettings memberGuildConfig = null;
if (ctx.Guild != null)
memberGuildConfig = await _db.Execute(c => _repo.GetMemberGuild(c, ctx.Guild.Id, target.Id));
if (ctx.GuildNew != null)
memberGuildConfig = await _db.Execute(c => _repo.GetMemberGuild(c, ctx.GuildNew.Id, target.Id));
var eb = new DiscordEmbedBuilder().WithTitle($"Member names")
.WithFooter($"Member ID: {target.Hid} | Active name in bold. Server name overrides display name, which overrides base name.");
@ -248,12 +245,12 @@ namespace PluralKit.Bot
eb.AddField("Display Name", target.DisplayName ?? "*(none)*");
}
if (ctx.Guild != null)
if (ctx.GuildNew != null)
{
if (memberGuildConfig?.DisplayName != null)
eb.AddField($"Server Name (in {ctx.Guild.Name})", $"**{memberGuildConfig.DisplayName}**");
eb.AddField($"Server Name (in {ctx.GuildNew.Name})", $"**{memberGuildConfig.DisplayName}**");
else
eb.AddField($"Server Name (in {ctx.Guild.Name})", memberGuildConfig?.DisplayName ?? "*(none)*");
eb.AddField($"Server Name (in {ctx.GuildNew.Name})", memberGuildConfig?.DisplayName ?? "*(none)*");
}
return eb;
@ -264,11 +261,11 @@ namespace PluralKit.Bot
async Task PrintSuccess(string text)
{
var successStr = text;
if (ctx.Guild != null)
if (ctx.GuildNew != null)
{
var memberGuildConfig = await _db.Execute(c => _repo.GetMemberGuild(c, ctx.Guild.Id, target.Id));
var memberGuildConfig = await _db.Execute(c => _repo.GetMemberGuild(c, ctx.GuildNew.Id, target.Id));
if (memberGuildConfig.DisplayName != null)
successStr += $" However, this member has a server name set in this server ({ctx.Guild.Name}), and will be proxied using that name, \"{memberGuildConfig.DisplayName}\", here.";
successStr += $" However, this member has a server name set in this server ({ctx.GuildNew.Name}), and will be proxied using that name, \"{memberGuildConfig.DisplayName}\", here.";
}
await ctx.Reply(successStr);
@ -313,12 +310,12 @@ namespace PluralKit.Bot
ctx.CheckOwnMember(target);
var patch = new MemberGuildPatch {DisplayName = null};
await _db.Execute(conn => _repo.UpsertMemberGuild(conn, target.Id, ctx.Guild.Id, patch));
await _db.Execute(conn => _repo.UpsertMemberGuild(conn, target.Id, ctx.GuildNew.Id, patch));
if (target.DisplayName != null)
await ctx.Reply($"{Emojis.Success} Member server name cleared. This member will now be proxied using their global display name \"{target.DisplayName}\" in this server ({ctx.Guild.Name}).");
await ctx.Reply($"{Emojis.Success} Member server name cleared. This member will now be proxied using their global display name \"{target.DisplayName}\" in this server ({ctx.GuildNew.Name}).");
else
await ctx.Reply($"{Emojis.Success} Member server name cleared. This member will now be proxied using their member name \"{target.NameFor(ctx)}\" in this server ({ctx.Guild.Name}).");
await ctx.Reply($"{Emojis.Success} Member server name cleared. This member will now be proxied using their member name \"{target.NameFor(ctx)}\" in this server ({ctx.GuildNew.Name}).");
}
else if (!ctx.HasNext())
{
@ -335,9 +332,9 @@ namespace PluralKit.Bot
var newServerName = ctx.RemainderOrNull();
var patch = new MemberGuildPatch {DisplayName = newServerName};
await _db.Execute(conn => _repo.UpsertMemberGuild(conn, target.Id, ctx.Guild.Id, patch));
await _db.Execute(conn => _repo.UpsertMemberGuild(conn, target.Id, ctx.GuildNew.Id, patch));
await ctx.Reply($"{Emojis.Success} Member server name changed. This member will now be proxied using the name \"{newServerName}\" in this server ({ctx.Guild.Name}).");
await ctx.Reply($"{Emojis.Success} Member server name changed. This member will now be proxied using the name \"{newServerName}\" in this server ({ctx.GuildNew.Name}).");
}
}
@ -417,8 +414,8 @@ namespace PluralKit.Bot
// Get guild settings (mostly for warnings and such)
MemberGuildSettings guildSettings = null;
if (ctx.Guild != null)
guildSettings = await _db.Execute(c => _repo.GetMemberGuild(c, ctx.Guild.Id, target.Id));
if (ctx.GuildNew != null)
guildSettings = await _db.Execute(c => _repo.GetMemberGuild(c, ctx.GuildNew.Id, target.Id));
async Task SetAll(PrivacyLevel level)
{

View File

@ -6,29 +6,37 @@ using System.Threading.Tasks;
using DSharpPlus;
using DSharpPlus.Entities;
using Myriad.Cache;
using Myriad.Extensions;
using Myriad.Types;
using PluralKit.Core;
using Permissions = DSharpPlus.Permissions;
namespace PluralKit.Bot
{
public class ServerConfig
{
private readonly IDatabase _db;
private readonly ModelRepository _repo;
private readonly IDiscordCache _cache;
private readonly LoggerCleanService _cleanService;
public ServerConfig(LoggerCleanService cleanService, IDatabase db, ModelRepository repo)
public ServerConfig(LoggerCleanService cleanService, IDatabase db, ModelRepository repo, IDiscordCache cache)
{
_cleanService = cleanService;
_db = db;
_repo = repo;
_cache = cache;
}
public async Task SetLogChannel(Context ctx)
{
ctx.CheckGuildContext().CheckAuthorPermission(Permissions.ManageGuild, "Manage Server");
ctx.CheckGuildContext().CheckAuthorPermission(PermissionSet.ManageGuild, "Manage Server");
if (await ctx.MatchClear("the server log channel"))
{
await _db.Execute(conn => _repo.UpsertGuild(conn, ctx.Guild.Id, new GuildPatch {LogChannel = null}));
await _db.Execute(conn => _repo.UpsertGuild(conn, ctx.GuildNew.Id, new GuildPatch {LogChannel = null}));
await ctx.Reply($"{Emojis.Success} Proxy logging channel cleared.");
return;
}
@ -36,36 +44,36 @@ namespace PluralKit.Bot
if (!ctx.HasNext())
throw new PKSyntaxError("You must pass a #channel to set, or `clear` to clear it.");
DiscordChannel channel = null;
Channel channel = null;
var channelString = ctx.PeekArgument();
channel = await ctx.MatchChannel();
if (channel == null || channel.GuildId != ctx.Guild.Id) throw Errors.ChannelNotFound(channelString);
if (channel == null || channel.GuildId != ctx.GuildNew.Id) throw Errors.ChannelNotFound(channelString);
var patch = new GuildPatch {LogChannel = channel.Id};
await _db.Execute(conn => _repo.UpsertGuild(conn, ctx.Guild.Id, patch));
await _db.Execute(conn => _repo.UpsertGuild(conn, ctx.GuildNew.Id, patch));
await ctx.Reply($"{Emojis.Success} Proxy logging channel set to #{channel.Name}.");
}
public async Task SetLogEnabled(Context ctx, bool enable)
{
ctx.CheckGuildContext().CheckAuthorPermission(Permissions.ManageGuild, "Manage Server");
ctx.CheckGuildContext().CheckAuthorPermission(PermissionSet.ManageGuild, "Manage Server");
var affectedChannels = new List<DiscordChannel>();
var affectedChannels = new List<Channel>();
if (ctx.Match("all"))
affectedChannels = (await ctx.Guild.GetChannelsAsync()).Where(x => x.Type == ChannelType.Text).ToList();
affectedChannels = _cache.GetGuildChannels(ctx.GuildNew.Id).Where(x => x.Type == Channel.ChannelType.GuildText).ToList();
else if (!ctx.HasNext()) throw new PKSyntaxError("You must pass one or more #channels.");
else while (ctx.HasNext())
{
var channelString = ctx.PeekArgument();
var channel = await ctx.MatchChannel();
if (channel == null || channel.GuildId != ctx.Guild.Id) throw Errors.ChannelNotFound(channelString);
if (channel == null || channel.GuildId != ctx.GuildNew.Id) throw Errors.ChannelNotFound(channelString);
affectedChannels.Add(channel);
}
ulong? logChannel = null;
await using (var conn = await _db.Obtain())
{
var config = await _repo.GetGuild(conn, ctx.Guild.Id);
var config = await _repo.GetGuild(conn, ctx.GuildNew.Id);
logChannel = config.LogChannel;
var blacklist = config.LogBlacklist.ToHashSet();
if (enable)
@ -74,7 +82,7 @@ namespace PluralKit.Bot
blacklist.UnionWith(affectedChannels.Select(c => c.Id));
var patch = new GuildPatch {LogBlacklist = blacklist.ToArray()};
await _repo.UpsertGuild(conn, ctx.Guild.Id, patch);
await _repo.UpsertGuild(conn, ctx.GuildNew.Id, patch);
}
await ctx.Reply(
@ -84,13 +92,13 @@ namespace PluralKit.Bot
public async Task ShowBlacklisted(Context ctx)
{
ctx.CheckGuildContext().CheckAuthorPermission(Permissions.ManageGuild, "Manage Server");
ctx.CheckGuildContext().CheckAuthorPermission(PermissionSet.ManageGuild, "Manage Server");
var blacklist = await _db.Execute(c => _repo.GetGuild(c, ctx.Guild.Id));
var blacklist = await _db.Execute(c => _repo.GetGuild(c, ctx.GuildNew.Id));
// Resolve all channels from the cache and order by position
var channels = blacklist.Blacklist
.Select(id => ctx.Guild.GetChannel(id))
.Select(id => _cache.GetChannelOrNull(id))
.Where(c => c != null)
.OrderBy(c => c.Position)
.ToList();
@ -102,26 +110,29 @@ namespace PluralKit.Bot
}
await ctx.Paginate(channels.ToAsyncEnumerable(), channels.Count, 25,
$"Blacklisted channels for {ctx.Guild.Name}",
$"Blacklisted channels for {ctx.GuildNew.Name}",
(eb, l) =>
{
DiscordChannel lastCategory = null;
string CategoryName(ulong? id) =>
id != null ? _cache.GetChannel(id.Value).Name : "(no category)";
ulong? lastCategory = null;
var fieldValue = new StringBuilder();
foreach (var channel in l)
{
if (lastCategory != channel.Parent && fieldValue.Length > 0)
if (lastCategory != channel!.ParentId && fieldValue.Length > 0)
{
eb.AddField(lastCategory?.Name ?? "(no category)", fieldValue.ToString());
eb.AddField(CategoryName(lastCategory), fieldValue.ToString());
fieldValue.Clear();
}
else fieldValue.Append("\n");
fieldValue.Append(channel.Mention);
lastCategory = channel.Parent;
fieldValue.Append(channel.Mention());
lastCategory = channel.ParentId;
}
eb.AddField(lastCategory?.Name ?? "(no category)", fieldValue.ToString());
eb.AddField(CategoryName(lastCategory), fieldValue.ToString());
return Task.CompletedTask;
});
@ -129,23 +140,23 @@ namespace PluralKit.Bot
public async Task SetBlacklisted(Context ctx, bool shouldAdd)
{
ctx.CheckGuildContext().CheckAuthorPermission(Permissions.ManageGuild, "Manage Server");
ctx.CheckGuildContext().CheckAuthorPermission(PermissionSet.ManageGuild, "Manage Server");
var affectedChannels = new List<DiscordChannel>();
var affectedChannels = new List<Channel>();
if (ctx.Match("all"))
affectedChannels = (await ctx.Guild.GetChannelsAsync()).Where(x => x.Type == ChannelType.Text).ToList();
affectedChannels = _cache.GetGuildChannels(ctx.GuildNew.Id).Where(x => x.Type == Channel.ChannelType.GuildText).ToList();
else if (!ctx.HasNext()) throw new PKSyntaxError("You must pass one or more #channels.");
else while (ctx.HasNext())
{
var channelString = ctx.PeekArgument();
var channel = await ctx.MatchChannel();
if (channel == null || channel.GuildId != ctx.Guild.Id) throw Errors.ChannelNotFound(channelString);
if (channel == null || channel.GuildId != ctx.GuildNew.Id) throw Errors.ChannelNotFound(channelString);
affectedChannels.Add(channel);
}
await using (var conn = await _db.Obtain())
{
var guild = await _repo.GetGuild(conn, ctx.Guild.Id);
var guild = await _repo.GetGuild(conn, ctx.GuildNew.Id);
var blacklist = guild.Blacklist.ToHashSet();
if (shouldAdd)
blacklist.UnionWith(affectedChannels.Select(c => c.Id));
@ -153,7 +164,7 @@ namespace PluralKit.Bot
blacklist.ExceptWith(affectedChannels.Select(c => c.Id));
var patch = new GuildPatch {Blacklist = blacklist.ToArray()};
await _repo.UpsertGuild(conn, ctx.Guild.Id, patch);
await _repo.UpsertGuild(conn, ctx.GuildNew.Id, patch);
}
await ctx.Reply($"{Emojis.Success} Channels {(shouldAdd ? "added to" : "removed from")} the proxy blacklist.");
@ -161,7 +172,7 @@ namespace PluralKit.Bot
public async Task SetLogCleanup(Context ctx)
{
ctx.CheckGuildContext().CheckAuthorPermission(Permissions.ManageGuild, "Manage Server");
ctx.CheckGuildContext().CheckAuthorPermission(PermissionSet.ManageGuild, "Manage Server");
var botList = string.Join(", ", _cleanService.Bots.Select(b => b.Name).OrderBy(x => x.ToLowerInvariant()));
@ -176,7 +187,7 @@ namespace PluralKit.Bot
.WithTitle("Log cleanup settings")
.AddField("Supported bots", botList);
var guildCfg = await _db.Execute(c => _repo.GetGuild(c, ctx.Guild.Id));
var guildCfg = await _db.Execute(c => _repo.GetGuild(c, ctx.GuildNew.Id));
if (guildCfg.LogCleanupEnabled)
eb.WithDescription("Log cleanup is currently **on** for this server. To disable it, type `pk;logclean off`.");
else
@ -186,7 +197,7 @@ namespace PluralKit.Bot
}
var patch = new GuildPatch {LogCleanupEnabled = newValue};
await _db.Execute(conn => _repo.UpsertGuild(conn, ctx.Guild.Id, patch));
await _db.Execute(conn => _repo.UpsertGuild(conn, ctx.GuildNew.Id, patch));
if (newValue)
await ctx.Reply($"{Emojis.Success} Log cleanup has been **enabled** for this server. Messages deleted by PluralKit will now be cleaned up from logging channels managed by the following bots:\n- **{botList}**\n\n{Emojis.Note} Make sure PluralKit has the **Manage Messages** permission in the channels in question.\n{Emojis.Note} Also, make sure to blacklist the logging channel itself from the bots in question to prevent conflicts.");

View File

@ -2,8 +2,6 @@ using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using DSharpPlus.Entities;
using NodaTime;
using NodaTime.TimeZones;

View File

@ -2,9 +2,6 @@ using System;
using System.Linq;
using System.Threading.Tasks;
using Dapper;
using DSharpPlus;
using DSharpPlus.Entities;
using NodaTime;
@ -13,8 +10,6 @@ using NodaTime.TimeZones;
using PluralKit.Core;
using Sentry.Protocol;
namespace PluralKit.Bot
{
public class SystemEdit
@ -196,7 +191,7 @@ namespace PluralKit.Bot
public async Task SystemProxy(Context ctx)
{
ctx.CheckSystem().CheckGuildContext();
var gs = await _db.Execute(c => _repo.GetSystemGuild(c, ctx.Guild.Id, ctx.System.Id));
var gs = await _db.Execute(c => _repo.GetSystemGuild(c, ctx.GuildNew.Id, ctx.System.Id));
bool newValue;
if (ctx.Match("on", "enabled", "true", "yes")) newValue = true;
@ -212,12 +207,12 @@ namespace PluralKit.Bot
}
var patch = new SystemGuildPatch {ProxyEnabled = newValue};
await _db.Execute(conn => _repo.UpsertSystemGuild(conn, ctx.System.Id, ctx.Guild.Id, patch));
await _db.Execute(conn => _repo.UpsertSystemGuild(conn, ctx.System.Id, ctx.GuildNew.Id, patch));
if (newValue)
await ctx.Reply($"Message proxying in this server ({ctx.Guild.Name.EscapeMarkdown()}) is now **enabled** for your system.");
await ctx.Reply($"Message proxying in this server ({ctx.GuildNew.Name.EscapeMarkdown()}) is now **enabled** for your system.");
else
await ctx.Reply($"Message proxying in this server ({ctx.Guild.Name.EscapeMarkdown()}) is now **disabled** for your system.");
await ctx.Reply($"Message proxying in this server ({ctx.GuildNew.Name.EscapeMarkdown()}) is now **disabled** for your system.");
}
public async Task SystemTimezone(Context ctx)

View File

@ -1,5 +1,4 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

View File

@ -1,8 +1,6 @@
using System.Text;
using System.Threading.Tasks;
using NodaTime;
using PluralKit.Core;
namespace PluralKit.Bot

View File

@ -61,8 +61,8 @@ namespace PluralKit.Bot
if (evt.Type != Message.MessageType.Default) return;
if (IsDuplicateMessage(evt)) return;
var guild = evt.GuildId != null ? await _cache.GetGuild(evt.GuildId.Value) : null;
var channel = await _cache.GetChannel(evt.ChannelId);
var guild = evt.GuildId != null ? _cache.GetGuild(evt.GuildId.Value) : null;
var channel = _cache.GetChannel(evt.ChannelId);
// Log metrics and message info
_metrics.Measure.Meter.Mark(BotMetrics.MessagesReceived);
@ -89,8 +89,8 @@ namespace PluralKit.Bot
private async ValueTask<bool> TryHandleLogClean(MessageCreateEvent evt, MessageContext ctx)
{
var channel = await _cache.GetChannel(evt.ChannelId);
if (!evt.Author.Bot || channel!.Type != Channel.ChannelType.GuildText ||
var channel = _cache.GetChannel(evt.ChannelId);
if (!evt.Author.Bot || channel.Type != Channel.ChannelType.GuildText ||
!ctx.LogCleanupEnabled) return false;
await _loggerClean.HandleLoggerBotCleanup(evt);

View File

@ -5,9 +5,13 @@ using System.Threading.Tasks;
using DSharpPlus;
using DSharpPlus.Entities;
using DSharpPlus.Exceptions;
using Humanizer;
using Myriad.Cache;
using Myriad.Rest;
using Myriad.Types;
using NodaTime;
using PluralKit.Core;
@ -18,54 +22,79 @@ namespace PluralKit.Bot {
private readonly IDatabase _db;
private readonly ModelRepository _repo;
private readonly DiscordShardedClient _client;
private readonly IDiscordCache _cache;
private readonly DiscordApiClient _rest;
public EmbedService(DiscordShardedClient client, IDatabase db, ModelRepository repo)
public EmbedService(DiscordShardedClient client, IDatabase db, ModelRepository repo, IDiscordCache cache, DiscordApiClient rest)
{
_client = client;
_db = db;
_repo = repo;
_cache = cache;
_rest = rest;
}
private Task<(ulong Id, User? User)[]> GetUsers(IEnumerable<ulong> ids)
{
async Task<(ulong Id, User? User)> Inner(ulong id)
{
if (_cache.TryGetUser(id, out var cachedUser))
return (id, cachedUser);
var user = await _rest.GetUser(id);
if (user == null)
return (id, null);
// todo: move to "GetUserCached" helper
await _cache.SaveUser(user);
return (id, user);
}
return Task.WhenAll(ids.Select(Inner));
}
public async Task<DiscordEmbed> CreateSystemEmbed(Context cctx, PKSystem system, LookupContext ctx)
public async Task<Embed> CreateSystemEmbed(Context cctx, PKSystem system, LookupContext ctx)
{
await using var conn = await _db.Obtain();
// Fetch/render info for all accounts simultaneously
var accounts = await _repo.GetSystemAccounts(conn, system.Id);
var users = await Task.WhenAll(accounts.Select(async uid => (await cctx.Shard.GetUser(uid))?.NameAndMention() ?? $"(deleted account {uid})"));
var users = (await GetUsers(accounts)).Select(x => x.User?.NameAndMention() ?? $"(deleted account {x.Id})");
var memberCount = cctx.MatchPrivateFlag(ctx) ? await _repo.GetSystemMemberCount(conn, system.Id, PrivacyLevel.Public) : await _repo.GetSystemMemberCount(conn, system.Id);
var eb = new DiscordEmbedBuilder()
.WithColor(DiscordUtils.Gray)
.WithTitle(system.Name ?? null)
.WithThumbnail(system.AvatarUrl)
.WithFooter($"System ID: {system.Hid} | Created on {system.Created.FormatZoned(system)}");
var embed = new Embed
{
Title = system.Name,
Thumbnail = new(system.AvatarUrl),
Footer = new($"System ID: {system.Hid} | Created on {system.Created.FormatZoned(system)}"),
Color = (uint?) DiscordUtils.Gray.Value
};
var fields = new List<Embed.Field>();
var latestSwitch = await _repo.GetLatestSwitch(conn, system.Id);
if (latestSwitch != null && system.FrontPrivacy.CanAccess(ctx))
{
var switchMembers = await _repo.GetSwitchMembers(conn, latestSwitch.Id).ToListAsync();
if (switchMembers.Count > 0)
eb.AddField("Fronter".ToQuantity(switchMembers.Count(), ShowQuantityAs.None),
string.Join(", ", switchMembers.Select(m => m.NameFor(ctx))));
if (switchMembers.Count > 0)
fields.Add(new("Fronter".ToQuantity(switchMembers.Count, ShowQuantityAs.None), string.Join(", ", switchMembers.Select(m => m.NameFor(ctx)))));
}
if (system.Tag != null) eb.AddField("Tag", system.Tag.EscapeMarkdown());
eb.AddField("Linked accounts", string.Join("\n", users).Truncate(1000), true);
if (system.Tag != null)
fields.Add(new("Tag", system.Tag.EscapeMarkdown()));
fields.Add(new("Linked accounts", string.Join("\n", users).Truncate(1000), true));
if (system.MemberListPrivacy.CanAccess(ctx))
{
if (memberCount > 0)
eb.AddField($"Members ({memberCount})", $"(see `pk;system {system.Hid} list` or `pk;system {system.Hid} list full`)", true);
fields.Add(new($"Members ({memberCount})", $"(see `pk;system {system.Hid} list` or `pk;system {system.Hid} list full`)", true));
else
eb.AddField($"Members ({memberCount})", "Add one with `pk;member new`!", true);
fields.Add(new($"Members ({memberCount})", "Add one with `pk;member new`!", true));
}
if (system.DescriptionFor(ctx) is { } desc)
eb.AddField("Description", desc.NormalizeLineEndSpacing().Truncate(1024), false);
fields.Add(new("Description", desc.NormalizeLineEndSpacing().Truncate(1024), false));
return eb.Build();
return embed with { Fields = fields.ToArray() };
}
public DiscordEmbed CreateLoggedMessageEmbed(PKSystem system, PKMember member, ulong messageId, ulong originalMsgId, DiscordUser sender, string content, DiscordChannel channel) {

View File

@ -60,18 +60,16 @@ namespace PluralKit.Bot {
private async Task<Channel?> FindLogChannel(ulong guildId, ulong channelId)
{
// TODO: fetch it directly on cache miss?
var channel = await _cache.GetChannel(channelId);
if (_cache.TryGetChannel(channelId, out var channel))
return channel;
if (channel == null)
{
// Channel doesn't exist or we don't have permission to access it, let's remove it from the database too
_logger.Warning("Attempted to fetch missing log channel {LogChannel} for guild {Guild}, removing from database", channelId, guildId);
await using var conn = await _db.Obtain();
await conn.ExecuteAsync("update servers set log_channel = null where id = @Guild",
new {Guild = guildId});
}
// Channel doesn't exist or we don't have permission to access it, let's remove it from the database too
_logger.Warning("Attempted to fetch missing log channel {LogChannel} for guild {Guild}, removing from database", channelId, guildId);
await using var conn = await _db.Obtain();
await conn.ExecuteAsync("update servers set log_channel = null where id = @Guild",
new {Guild = guildId});
return channel;
return null;
}
}
}

View File

@ -9,6 +9,7 @@ using App.Metrics;
using Humanizer;
using Myriad.Cache;
using Myriad.Extensions;
using Myriad.Rest;
using Myriad.Rest.Types;
using Myriad.Rest.Types.Requests;
@ -77,20 +78,22 @@ namespace PluralKit.Bot
private async Task<Message> ExecuteWebhookInner(Webhook webhook, ProxyRequest req, bool hasRetried = false)
{
var guild = await _cache.GetGuild(req.GuildId)!;
var guild = _cache.GetGuild(req.GuildId);
var content = req.Content.Truncate(2000);
var allowedMentions = content.ParseMentions();
if (!req.AllowEveryone)
allowedMentions = allowedMentions.RemoveUnmentionableRoles(guild);
var webhookReq = new ExecuteWebhookRequest
{
Username = FixClyde(req.Name).Truncate(80),
Content = content,
AllowedMentions = null, // todo
AllowedMentions = allowedMentions,
AvatarUrl = !string.IsNullOrWhiteSpace(req.AvatarUrl) ? req.AvatarUrl : null,
Embeds = req.Embeds
};
// dwb.AddMentions(content.ParseAllMentions(guild, req.AllowEveryone));
MultipartFile[] files = null;
var attachmentChunks = ChunkAttachmentsOrThrow(req.Attachments, 8 * 1024 * 1024);
if (attachmentChunks.Count > 0)

View File

@ -11,10 +11,14 @@ using DSharpPlus.Entities;
using DSharpPlus.EventArgs;
using DSharpPlus.Exceptions;
using Myriad.Types;
using NodaTime;
using PluralKit.Core;
using Permissions = DSharpPlus.Permissions;
namespace PluralKit.Bot {
public static class ContextUtils {
public static async Task<bool> ConfirmClear(this Context ctx, string toClear)
@ -149,7 +153,8 @@ namespace PluralKit.Bot {
if (currentPage < 0) currentPage += pageCount;
// If we can, remove the user's reaction (so they can press again quickly)
if (ctx.BotHasAllPermissions(Permissions.ManageMessages)) await msg.DeleteReactionAsync(reaction.Emoji, reaction.User);
if (ctx.BotPermissions.HasFlag(PermissionSet.ManageMessages))
await msg.DeleteReactionAsync(reaction.Emoji, reaction.User);
// Edit the embed with the new page
var embed = await MakeEmbedForPage(currentPage);
@ -159,7 +164,8 @@ namespace PluralKit.Bot {
// "escape hatch", clean up as if we hit X
}
if (ctx.BotHasAllPermissions(Permissions.ManageMessages)) await msg.DeleteAllReactionsAsync();
if (ctx.BotPermissions.HasFlag(PermissionSet.ManageMessages))
await msg.DeleteAllReactionsAsync();
}
// If we get a "NotFound" error, the message has been deleted and thus not our problem
catch (NotFoundException) { }
@ -245,12 +251,7 @@ namespace PluralKit.Bot {
return items[Array.IndexOf(indicators, reaction.Emoji.Name)];
}
}
public static Permissions BotPermissions(this Context ctx) => ctx.Channel.BotPermissions();
public static bool BotHasAllPermissions(this Context ctx, Permissions permission) =>
ctx.Channel.BotHasAllPermissions(permission);
public static async Task BusyIndicator(this Context ctx, Func<Task> f, string emoji = "\u23f3" /* hourglass */)
{
await ctx.BusyIndicator<object>(async () =>
@ -265,8 +266,8 @@ namespace PluralKit.Bot {
var task = f();
// If we don't have permission to add reactions, don't bother, and just await the task normally.
var neededPermissions = Permissions.AddReactions | Permissions.ReadMessageHistory;
if ((ctx.BotPermissions() & neededPermissions) != neededPermissions) return await task;
var neededPermissions = PermissionSet.AddReactions | PermissionSet.ReadMessageHistory;
if ((ctx.BotPermissions & neededPermissions) != neededPermissions) return await task;
try
{

View File

@ -12,6 +12,8 @@ using DSharpPlus.Entities;
using DSharpPlus.EventArgs;
using DSharpPlus.Exceptions;
using Myriad.Extensions;
using Myriad.Rest.Types;
using Myriad.Types;
using NodaTime;
@ -50,6 +52,11 @@ namespace PluralKit.Bot
{
return $"{user.Username}#{user.Discriminator} ({user.Mention})";
}
public static string NameAndMention(this User user)
{
return $"{user.Username}#{user.Discriminator} ({user.Mention()})";
}
// We funnel all "permissions from DiscordMember" calls through here
// This way we can ensure we do the read permission correction everywhere
@ -74,20 +81,7 @@ namespace PluralKit.Bot
var invalidRoleIds = roleIdCache.Where(x => !currentRoleIds.Contains(x)).ToList();
roleIdCache.RemoveAll(x => invalidRoleIds.Contains(x));
}
public static async Task<Permissions> PermissionsIn(this DiscordChannel channel, DiscordUser user)
{
// Just delegates to PermissionsInSync, but handles the case of a non-member User in a guild properly
// This is a separate method because it requires an async call
if (channel.Guild != null && !(user is DiscordMember))
{
var member = await channel.Guild.GetMember(user.Id);
if (member != null)
return PermissionsInSync(channel, member);
}
return PermissionsInSync(channel, user);
}
// Same as PermissionsIn, but always synchronous. DiscordUser must be a DiscordMember if channel is in guild.
public static Permissions PermissionsInSync(this DiscordChannel channel, DiscordUser user)
@ -194,23 +188,27 @@ namespace PluralKit.Bot
return false;
}
public static IEnumerable<IMention> ParseAllMentions(this string input, Guild guild, bool allowEveryone = false)
public static AllowedMentions ParseMentions(this string input)
{
var mentions = new List<IMention>();
mentions.AddRange(USER_MENTION.Matches(input)
.Select(x => new UserMention(ulong.Parse(x.Groups[1].Value)) as IMention));
var users = USER_MENTION.Matches(input).Select(x => ulong.Parse(x.Groups[1].Value));
var roles = ROLE_MENTION.Matches(input).Select(x => ulong.Parse(x.Groups[1].Value));
var everyone = EVERYONE_HERE_MENTION.IsMatch(input);
return new AllowedMentions
{
Users = users.ToArray(),
Roles = roles.ToArray(),
Parse = everyone ? new[] {AllowedMentions.ParseType.Everyone} : null
};
}
// Only allow role mentions through where the role is actually listed as *mentionable*
// (ie. any user can @ them, regardless of permissions)
// Still let the allowEveryone flag override this though (privileged users can @ *any* role)
// Original fix by Gwen
mentions.AddRange(ROLE_MENTION.Matches(input)
.Select(x => ulong.Parse(x.Groups[1].Value))
.Where(x => allowEveryone || guild != null && (guild.Roles.FirstOrDefault(g => g.Id == x)?.Mentionable ?? false))
.Select(x => new RoleMention(x) as IMention));
if (EVERYONE_HERE_MENTION.IsMatch(input) && allowEveryone)
mentions.Add(new EveryoneMention());
return mentions;
public static AllowedMentions RemoveUnmentionableRoles(this AllowedMentions mentions, Guild guild)
{
return mentions with {
Roles = mentions.Roles
?.Where(id => guild.Roles.FirstOrDefault(r => r.Id == id)?.Mentionable == true)
.ToArray()
};
}
public static string EscapeMarkdown(this string input)