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 RemoveUser(ulong userId);
public ValueTask RemoveRole(ulong guildId, ulong roleId); public ValueTask RemoveRole(ulong guildId, ulong roleId);
public ValueTask<Guild?> GetGuild(ulong guildId); public bool TryGetGuild(ulong guildId, out Guild guild);
public ValueTask<Channel?> GetChannel(ulong channelId); public bool TryGetChannel(ulong channelId, out Channel channel);
public ValueTask<User?> GetUser(ulong userId); public bool TryGetUser(ulong userId, out User user);
public ValueTask<Role?> GetRole(ulong roleId); public bool TryGetRole(ulong roleId, out Role role);
public IAsyncEnumerable<Guild> GetAllGuilds(); 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; 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() public async IAsyncEnumerable<Guild> GetAllGuilds()
{ {
@ -124,12 +137,12 @@ namespace Myriad.Cache
yield return guild.Guild; yield return guild.Guild;
} }
public ValueTask<IEnumerable<Channel>> GetGuildChannels(ulong guildId) public IEnumerable<Channel> GetGuildChannels(ulong guildId)
{ {
if (!_guilds.TryGetValue(guildId, out var guild)) if (!_guilds.TryGetValue(guildId, out var guild))
throw new ArgumentException("Guild not found", nameof(guildId)); 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) => 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 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 namespace Myriad.Extensions
{ {
public class MessageExtensions public static class MessageExtensions
{ {
} }
} }

View File

@ -1,7 +1,9 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks;
using Myriad.Cache;
using Myriad.Gateway; using Myriad.Gateway;
using Myriad.Types; using Myriad.Types;
@ -9,18 +11,40 @@ namespace Myriad.Extensions
{ {
public static class PermissionExtensions 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) => public static PermissionSet EveryonePermissions(this Guild guild) =>
guild.Roles.FirstOrDefault(r => r.Id == guild.Id)?.Permissions ?? PermissionSet.Dm; guild.Roles.FirstOrDefault(r => r.Id == guild.Id)?.Permissions ?? PermissionSet.Dm;
public static PermissionSet PermissionsFor(Guild guild, Channel channel, MessageCreateEvent msg) => 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, public static PermissionSet PermissionsFor(Guild guild, Channel channel, ulong userId,
ICollection<ulong> roleIds) ICollection<ulong>? roleIds)
{ {
if (channel.Type == Channel.ChannelType.Dm) if (channel.Type == Channel.ChannelType.Dm)
return PermissionSet.Dm; return PermissionSet.Dm;
if (roleIds == null)
throw new ArgumentException($"User roles must be specified for guild channels");
var perms = GuildPermissions(guild, userId, roleIds); var perms = GuildPermissions(guild, userId, roleIds);
perms = ApplyChannelOverwrites(perms, channel, userId, roleIds); perms = ApplyChannelOverwrites(perms, channel, userId, roleIds);
@ -36,9 +60,6 @@ namespace Myriad.Extensions
return perms; 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) public static PermissionSet GuildPermissions(this Guild guild, ulong userId, ICollection<ulong> roleIds)
{ {
if (guild.OwnerId == userId) if (guild.OwnerId == userId)
@ -51,7 +72,7 @@ namespace Myriad.Extensions
perms |= role.Permissions; perms |= role.Permissions;
} }
if (perms.Has(PermissionSet.Administrator)) if (perms.HasFlag(PermissionSet.Administrator))
return PermissionSet.All; return PermissionSet.All;
return perms; return perms;

View File

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

View File

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

View File

@ -8,6 +8,6 @@ namespace Myriad.Rest.Types.Requests
public object? Nonce { get; set; } public object? Nonce { get; set; }
public bool Tts { get; set; } public bool Tts { get; set; }
public AllowedMentions AllowedMentions { 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? Name { get; init; }
public string? Topic { get; init; } public string? Topic { get; init; }
public bool? Nsfw { get; init; } public bool? Nsfw { get; init; }
public long? ParentId { get; init; } public ulong? ParentId { get; init; }
public Overwrite[]? PermissionOverwrites { get; init; } public Overwrite[]? PermissionOverwrites { get; init; }
public record Overwrite public record Overwrite

View File

@ -8,14 +8,17 @@ using Autofac;
using DSharpPlus; using DSharpPlus;
using DSharpPlus.Entities; using DSharpPlus.Entities;
using DSharpPlus.Net;
using Myriad.Cache;
using Myriad.Extensions; using Myriad.Extensions;
using Myriad.Gateway; using Myriad.Gateway;
using Myriad.Rest.Types.Requests;
using Myriad.Types; using Myriad.Types;
using PluralKit.Core; using PluralKit.Core;
using Permissions = DSharpPlus.Permissions; using DiscordApiClient = Myriad.Rest.DiscordApiClient;
namespace PluralKit.Bot namespace PluralKit.Bot
{ {
@ -24,6 +27,7 @@ namespace PluralKit.Bot
private readonly ILifetimeScope _provider; private readonly ILifetimeScope _provider;
private readonly DiscordRestClient _rest; private readonly DiscordRestClient _rest;
private readonly DiscordApiClient _newRest;
private readonly DiscordShardedClient _client; private readonly DiscordShardedClient _client;
private readonly DiscordClient _shard = null; private readonly DiscordClient _shard = null;
private readonly Shard _shardNew; private readonly Shard _shardNew;
@ -42,6 +46,7 @@ namespace PluralKit.Bot
private readonly PKSystem _senderSystem; private readonly PKSystem _senderSystem;
private readonly IMetrics _metrics; private readonly IMetrics _metrics;
private readonly CommandMessageService _commandMessageService; private readonly CommandMessageService _commandMessageService;
private readonly IDiscordCache _cache;
private Command _currentCommand; private Command _currentCommand;
@ -57,24 +62,25 @@ namespace PluralKit.Bot
_senderSystem = senderSystem; _senderSystem = senderSystem;
_messageContext = messageContext; _messageContext = messageContext;
_botMember = botMember; _botMember = botMember;
_cache = provider.Resolve<IDiscordCache>();
_db = provider.Resolve<IDatabase>(); _db = provider.Resolve<IDatabase>();
_repo = provider.Resolve<ModelRepository>(); _repo = provider.Resolve<ModelRepository>();
_metrics = provider.Resolve<IMetrics>(); _metrics = provider.Resolve<IMetrics>();
_provider = provider; _provider = provider;
_commandMessageService = provider.Resolve<CommandMessageService>(); _commandMessageService = provider.Resolve<CommandMessageService>();
_parameters = new Parameters(message.Content.Substring(commandParseOffset)); _parameters = new Parameters(message.Content.Substring(commandParseOffset));
_newRest = provider.Resolve<DiscordApiClient>();
_botPermissions = message.GuildId != null _botPermissions = _cache.PermissionsFor(message.ChannelId, shard.User!.Id, botMember!);
? PermissionExtensions.PermissionsFor(guild!, channel, shard.User?.Id ?? default, botMember!.Roles) _userPermissions = _cache.PermissionsFor(message);
: PermissionSet.Dm;
_userPermissions = message.GuildId != null
? PermissionExtensions.PermissionsFor(guild!, channel, message.Author.Id, message.Member!.Roles)
: PermissionSet.Dm;
} }
public IDiscordCache Cache => _cache;
public DiscordUser Author => _message.Author; public DiscordUser Author => _message.Author;
public DiscordChannel Channel => _message.Channel; public DiscordChannel Channel => _message.Channel;
public Channel ChannelNew => _channel; public Channel ChannelNew => _channel;
public User AuthorNew => _messageNew.Author;
public DiscordMessage Message => _message; public DiscordMessage Message => _message;
public Message MessageNew => _messageNew; public Message MessageNew => _messageNew;
public DiscordGuild Guild => _message.Channel.Guild; public DiscordGuild Guild => _message.Channel.Guild;
@ -95,24 +101,44 @@ namespace PluralKit.Bot
internal IDatabase Database => _db; internal IDatabase Database => _db;
internal ModelRepository Repository => _repo; 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. // 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."); 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."); 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) if (embed != null)
{ {
// Sensitive information that might want to be deleted by :x: reaction is typically in an embed format (member cards, for example) // 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 // 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) public async Task Execute<T>(Command commandDef, Func<T, Task> handler)

View File

@ -1,5 +1,7 @@
using DSharpPlus; using DSharpPlus;
using Myriad.Types;
using PluralKit.Core; using PluralKit.Core;
namespace PluralKit.Bot namespace PluralKit.Bot
@ -8,7 +10,7 @@ namespace PluralKit.Bot
{ {
public static Context CheckGuildContext(this Context ctx) 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."); throw new PKError("This command can not be run in a DM.");
} }
@ -46,12 +48,9 @@ namespace PluralKit.Bot
return ctx; 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 if ((ctx.UserPermissions & neededPerms) != neededPerms)
// message received event...
var hasPerms = ctx.Channel.PermissionsInSync(ctx.Author);
if ((hasPerms & neededPerms) != neededPerms)
throw new PKError($"You must have the \"{permissionName}\" permission in this server to use this command."); throw new PKError($"You must have the \"{permissionName}\" permission in this server to use this command.");
return ctx; return ctx;
} }

View File

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

View File

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

View File

@ -25,7 +25,7 @@ namespace PluralKit.Bot
if (location == AvatarLocation.Server) if (location == AvatarLocation.Server)
{ {
if (target.AvatarUrl != null) 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 else
await ctx.Reply($"{Emojis.Success} Member server avatar cleared. This member now has no avatar."); 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."); 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 cmd = location == AvatarLocation.Server ? "serveravatar" : "avatar";
var eb = new DiscordEmbedBuilder() var eb = new DiscordEmbedBuilder()
@ -69,14 +69,14 @@ namespace PluralKit.Bot
public async Task ServerAvatar(Context ctx, PKMember target) public async Task ServerAvatar(Context ctx, PKMember target)
{ {
ctx.CheckGuildContext(); 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); await AvatarCommandTree(AvatarLocation.Server, ctx, target, guildData);
} }
public async Task Avatar(Context ctx, PKMember target) public async Task Avatar(Context ctx, PKMember target)
{ {
var guildData = ctx.Guild != null ? var guildData = ctx.GuildNew != null ?
await _db.Execute(c => _repo.GetMemberGuild(c, ctx.Guild.Id, target.Id)) await _db.Execute(c => _repo.GetMemberGuild(c, ctx.GuildNew.Id, target.Id))
: null; : null;
await AvatarCommandTree(AvatarLocation.Member, ctx, target, guildData); await AvatarCommandTree(AvatarLocation.Member, ctx, target, guildData);
@ -119,8 +119,8 @@ namespace PluralKit.Bot
var serverFrag = location switch var serverFrag = location switch
{ {
AvatarLocation.Server => $" This avatar will now be used when proxying in this server (**{ctx.Guild.Name}**).", 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.Guild.Name}**), and thus changing the global avatar will have no effect here.", 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: case AvatarLocation.Server:
var serverPatch = new MemberGuildPatch { AvatarUrl = url }; 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: case AvatarLocation.Member:
var memberPatch = new MemberPatch { AvatarUrl = url }; var memberPatch = new MemberPatch { AvatarUrl = url };
return _db.Execute(c => _repo.UpdateMember(c, target.Id, memberPatch)); 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.Threading.Tasks;
using System; using System;
using Dapper;
using DSharpPlus.Entities; using DSharpPlus.Entities;
using NodaTime; 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 (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 (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) 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); var lcx = ctx.LookupContextFor(target);
MemberGuildSettings memberGuildConfig = null; MemberGuildSettings memberGuildConfig = null;
if (ctx.Guild != null) if (ctx.GuildNew != null)
memberGuildConfig = await _db.Execute(c => _repo.GetMemberGuild(c, ctx.Guild.Id, target.Id)); memberGuildConfig = await _db.Execute(c => _repo.GetMemberGuild(c, ctx.GuildNew.Id, target.Id));
var eb = new DiscordEmbedBuilder().WithTitle($"Member names") 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."); .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)*"); eb.AddField("Display Name", target.DisplayName ?? "*(none)*");
} }
if (ctx.Guild != null) if (ctx.GuildNew != null)
{ {
if (memberGuildConfig?.DisplayName != 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 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; return eb;
@ -264,11 +261,11 @@ namespace PluralKit.Bot
async Task PrintSuccess(string text) async Task PrintSuccess(string text)
{ {
var successStr = 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) 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); await ctx.Reply(successStr);
@ -313,12 +310,12 @@ namespace PluralKit.Bot
ctx.CheckOwnMember(target); ctx.CheckOwnMember(target);
var patch = new MemberGuildPatch {DisplayName = null}; 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) 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 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()) else if (!ctx.HasNext())
{ {
@ -335,9 +332,9 @@ namespace PluralKit.Bot
var newServerName = ctx.RemainderOrNull(); var newServerName = ctx.RemainderOrNull();
var patch = new MemberGuildPatch {DisplayName = newServerName}; 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) // Get guild settings (mostly for warnings and such)
MemberGuildSettings guildSettings = null; MemberGuildSettings guildSettings = null;
if (ctx.Guild != null) if (ctx.GuildNew != null)
guildSettings = await _db.Execute(c => _repo.GetMemberGuild(c, ctx.Guild.Id, target.Id)); guildSettings = await _db.Execute(c => _repo.GetMemberGuild(c, ctx.GuildNew.Id, target.Id));
async Task SetAll(PrivacyLevel level) async Task SetAll(PrivacyLevel level)
{ {

View File

@ -6,29 +6,37 @@ using System.Threading.Tasks;
using DSharpPlus; using DSharpPlus;
using DSharpPlus.Entities; using DSharpPlus.Entities;
using Myriad.Cache;
using Myriad.Extensions;
using Myriad.Types;
using PluralKit.Core; using PluralKit.Core;
using Permissions = DSharpPlus.Permissions;
namespace PluralKit.Bot namespace PluralKit.Bot
{ {
public class ServerConfig public class ServerConfig
{ {
private readonly IDatabase _db; private readonly IDatabase _db;
private readonly ModelRepository _repo; private readonly ModelRepository _repo;
private readonly IDiscordCache _cache;
private readonly LoggerCleanService _cleanService; private readonly LoggerCleanService _cleanService;
public ServerConfig(LoggerCleanService cleanService, IDatabase db, ModelRepository repo) public ServerConfig(LoggerCleanService cleanService, IDatabase db, ModelRepository repo, IDiscordCache cache)
{ {
_cleanService = cleanService; _cleanService = cleanService;
_db = db; _db = db;
_repo = repo; _repo = repo;
_cache = cache;
} }
public async Task SetLogChannel(Context ctx) 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")) 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."); await ctx.Reply($"{Emojis.Success} Proxy logging channel cleared.");
return; return;
} }
@ -36,36 +44,36 @@ namespace PluralKit.Bot
if (!ctx.HasNext()) if (!ctx.HasNext())
throw new PKSyntaxError("You must pass a #channel to set, or `clear` to clear it."); 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(); var channelString = ctx.PeekArgument();
channel = await ctx.MatchChannel(); 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}; 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}."); await ctx.Reply($"{Emojis.Success} Proxy logging channel set to #{channel.Name}.");
} }
public async Task SetLogEnabled(Context ctx, bool enable) 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")) 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 if (!ctx.HasNext()) throw new PKSyntaxError("You must pass one or more #channels.");
else while (ctx.HasNext()) else while (ctx.HasNext())
{ {
var channelString = ctx.PeekArgument(); var channelString = ctx.PeekArgument();
var channel = await ctx.MatchChannel(); 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); affectedChannels.Add(channel);
} }
ulong? logChannel = null; ulong? logChannel = null;
await using (var conn = await _db.Obtain()) 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; logChannel = config.LogChannel;
var blacklist = config.LogBlacklist.ToHashSet(); var blacklist = config.LogBlacklist.ToHashSet();
if (enable) if (enable)
@ -74,7 +82,7 @@ namespace PluralKit.Bot
blacklist.UnionWith(affectedChannels.Select(c => c.Id)); blacklist.UnionWith(affectedChannels.Select(c => c.Id));
var patch = new GuildPatch {LogBlacklist = blacklist.ToArray()}; 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( await ctx.Reply(
@ -84,13 +92,13 @@ namespace PluralKit.Bot
public async Task ShowBlacklisted(Context ctx) 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 // Resolve all channels from the cache and order by position
var channels = blacklist.Blacklist var channels = blacklist.Blacklist
.Select(id => ctx.Guild.GetChannel(id)) .Select(id => _cache.GetChannelOrNull(id))
.Where(c => c != null) .Where(c => c != null)
.OrderBy(c => c.Position) .OrderBy(c => c.Position)
.ToList(); .ToList();
@ -102,26 +110,29 @@ namespace PluralKit.Bot
} }
await ctx.Paginate(channels.ToAsyncEnumerable(), channels.Count, 25, await ctx.Paginate(channels.ToAsyncEnumerable(), channels.Count, 25,
$"Blacklisted channels for {ctx.Guild.Name}", $"Blacklisted channels for {ctx.GuildNew.Name}",
(eb, l) => (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(); var fieldValue = new StringBuilder();
foreach (var channel in l) 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(); fieldValue.Clear();
} }
else fieldValue.Append("\n"); else fieldValue.Append("\n");
fieldValue.Append(channel.Mention); fieldValue.Append(channel.Mention());
lastCategory = channel.Parent; lastCategory = channel.ParentId;
} }
eb.AddField(lastCategory?.Name ?? "(no category)", fieldValue.ToString()); eb.AddField(CategoryName(lastCategory), fieldValue.ToString());
return Task.CompletedTask; return Task.CompletedTask;
}); });
@ -129,23 +140,23 @@ namespace PluralKit.Bot
public async Task SetBlacklisted(Context ctx, bool shouldAdd) 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")) 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 if (!ctx.HasNext()) throw new PKSyntaxError("You must pass one or more #channels.");
else while (ctx.HasNext()) else while (ctx.HasNext())
{ {
var channelString = ctx.PeekArgument(); var channelString = ctx.PeekArgument();
var channel = await ctx.MatchChannel(); 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); affectedChannels.Add(channel);
} }
await using (var conn = await _db.Obtain()) 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(); var blacklist = guild.Blacklist.ToHashSet();
if (shouldAdd) if (shouldAdd)
blacklist.UnionWith(affectedChannels.Select(c => c.Id)); blacklist.UnionWith(affectedChannels.Select(c => c.Id));
@ -153,7 +164,7 @@ namespace PluralKit.Bot
blacklist.ExceptWith(affectedChannels.Select(c => c.Id)); blacklist.ExceptWith(affectedChannels.Select(c => c.Id));
var patch = new GuildPatch {Blacklist = blacklist.ToArray()}; 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."); 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) 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())); 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") .WithTitle("Log cleanup settings")
.AddField("Supported bots", botList); .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) if (guildCfg.LogCleanupEnabled)
eb.WithDescription("Log cleanup is currently **on** for this server. To disable it, type `pk;logclean off`."); eb.WithDescription("Log cleanup is currently **on** for this server. To disable it, type `pk;logclean off`.");
else else
@ -186,7 +197,7 @@ namespace PluralKit.Bot
} }
var patch = new GuildPatch {LogCleanupEnabled = newValue}; 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) 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."); 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.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using DSharpPlus.Entities;
using NodaTime; using NodaTime;
using NodaTime.TimeZones; using NodaTime.TimeZones;

View File

@ -2,9 +2,6 @@ using System;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Dapper;
using DSharpPlus;
using DSharpPlus.Entities; using DSharpPlus.Entities;
using NodaTime; using NodaTime;
@ -13,8 +10,6 @@ using NodaTime.TimeZones;
using PluralKit.Core; using PluralKit.Core;
using Sentry.Protocol;
namespace PluralKit.Bot namespace PluralKit.Bot
{ {
public class SystemEdit public class SystemEdit
@ -196,7 +191,7 @@ namespace PluralKit.Bot
public async Task SystemProxy(Context ctx) public async Task SystemProxy(Context ctx)
{ {
ctx.CheckSystem().CheckGuildContext(); 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; bool newValue;
if (ctx.Match("on", "enabled", "true", "yes")) newValue = true; if (ctx.Match("on", "enabled", "true", "yes")) newValue = true;
@ -212,12 +207,12 @@ namespace PluralKit.Bot
} }
var patch = new SystemGuildPatch {ProxyEnabled = newValue}; 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) 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 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) public async Task SystemTimezone(Context ctx)

View File

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

View File

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

View File

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

View File

@ -5,9 +5,13 @@ using System.Threading.Tasks;
using DSharpPlus; using DSharpPlus;
using DSharpPlus.Entities; using DSharpPlus.Entities;
using DSharpPlus.Exceptions;
using Humanizer; using Humanizer;
using Myriad.Cache;
using Myriad.Rest;
using Myriad.Types;
using NodaTime; using NodaTime;
using PluralKit.Core; using PluralKit.Core;
@ -18,54 +22,79 @@ namespace PluralKit.Bot {
private readonly IDatabase _db; private readonly IDatabase _db;
private readonly ModelRepository _repo; private readonly ModelRepository _repo;
private readonly DiscordShardedClient _client; 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; _client = client;
_db = db; _db = db;
_repo = repo; _repo = repo;
_cache = cache;
_rest = rest;
} }
public async Task<DiscordEmbed> CreateSystemEmbed(Context cctx, PKSystem system, LookupContext ctx) 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<Embed> CreateSystemEmbed(Context cctx, PKSystem system, LookupContext ctx)
{ {
await using var conn = await _db.Obtain(); await using var conn = await _db.Obtain();
// Fetch/render info for all accounts simultaneously // Fetch/render info for all accounts simultaneously
var accounts = await _repo.GetSystemAccounts(conn, system.Id); 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 memberCount = cctx.MatchPrivateFlag(ctx) ? await _repo.GetSystemMemberCount(conn, system.Id, PrivacyLevel.Public) : await _repo.GetSystemMemberCount(conn, system.Id);
var eb = new DiscordEmbedBuilder() var embed = new Embed
.WithColor(DiscordUtils.Gray) {
.WithTitle(system.Name ?? null) Title = system.Name,
.WithThumbnail(system.AvatarUrl) Thumbnail = new(system.AvatarUrl),
.WithFooter($"System ID: {system.Hid} | Created on {system.Created.FormatZoned(system)}"); 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); var latestSwitch = await _repo.GetLatestSwitch(conn, system.Id);
if (latestSwitch != null && system.FrontPrivacy.CanAccess(ctx)) if (latestSwitch != null && system.FrontPrivacy.CanAccess(ctx))
{ {
var switchMembers = await _repo.GetSwitchMembers(conn, latestSwitch.Id).ToListAsync(); var switchMembers = await _repo.GetSwitchMembers(conn, latestSwitch.Id).ToListAsync();
if (switchMembers.Count > 0) if (switchMembers.Count > 0)
eb.AddField("Fronter".ToQuantity(switchMembers.Count(), ShowQuantityAs.None), fields.Add(new("Fronter".ToQuantity(switchMembers.Count, ShowQuantityAs.None), string.Join(", ", switchMembers.Select(m => m.NameFor(ctx)))));
string.Join(", ", switchMembers.Select(m => m.NameFor(ctx))));
} }
if (system.Tag != null) eb.AddField("Tag", system.Tag.EscapeMarkdown()); if (system.Tag != null)
eb.AddField("Linked accounts", string.Join("\n", users).Truncate(1000), true); 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 (system.MemberListPrivacy.CanAccess(ctx))
{ {
if (memberCount > 0) 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 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) 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) { 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) private async Task<Channel?> FindLogChannel(ulong guildId, ulong channelId)
{ {
// TODO: fetch it directly on cache miss? // 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 // 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); _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 using var conn = await _db.Obtain();
await conn.ExecuteAsync("update servers set log_channel = null where id = @Guild", await conn.ExecuteAsync("update servers set log_channel = null where id = @Guild",
new {Guild = guildId}); new {Guild = guildId});
}
return channel; return null;
} }
} }
} }

View File

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

View File

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

View File

@ -12,6 +12,8 @@ using DSharpPlus.Entities;
using DSharpPlus.EventArgs; using DSharpPlus.EventArgs;
using DSharpPlus.Exceptions; using DSharpPlus.Exceptions;
using Myriad.Extensions;
using Myriad.Rest.Types;
using Myriad.Types; using Myriad.Types;
using NodaTime; using NodaTime;
@ -51,6 +53,11 @@ namespace PluralKit.Bot
return $"{user.Username}#{user.Discriminator} ({user.Mention})"; 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 // We funnel all "permissions from DiscordMember" calls through here
// This way we can ensure we do the read permission correction everywhere // This way we can ensure we do the read permission correction everywhere
private static Permissions PermissionsInGuild(DiscordChannel channel, DiscordMember member) private static Permissions PermissionsInGuild(DiscordChannel channel, DiscordMember member)
@ -75,19 +82,6 @@ namespace PluralKit.Bot
roleIdCache.RemoveAll(x => invalidRoleIds.Contains(x)); 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. // 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) public static Permissions PermissionsInSync(this DiscordChannel channel, DiscordUser user)
@ -194,23 +188,27 @@ namespace PluralKit.Bot
return false; 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>(); var users = USER_MENTION.Matches(input).Select(x => ulong.Parse(x.Groups[1].Value));
mentions.AddRange(USER_MENTION.Matches(input) var roles = ROLE_MENTION.Matches(input).Select(x => ulong.Parse(x.Groups[1].Value));
.Select(x => new UserMention(ulong.Parse(x.Groups[1].Value)) as IMention)); var everyone = EVERYONE_HERE_MENTION.IsMatch(input);
// Only allow role mentions through where the role is actually listed as *mentionable* return new AllowedMentions
// (ie. any user can @ them, regardless of permissions) {
// Still let the allowEveryone flag override this though (privileged users can @ *any* role) Users = users.ToArray(),
// Original fix by Gwen Roles = roles.ToArray(),
mentions.AddRange(ROLE_MENTION.Matches(input) Parse = everyone ? new[] {AllowedMentions.ParseType.Everyone} : null
.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) public static AllowedMentions RemoveUnmentionableRoles(this AllowedMentions mentions, Guild guild)
mentions.Add(new EveryoneMention()); {
return mentions; 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) public static string EscapeMarkdown(this string input)