Add embed builder, some more ported classes

This commit is contained in:
Ske 2020-12-23 02:19:02 +01:00
parent 05334f0d25
commit f6fb8204bb
13 changed files with 305 additions and 189 deletions

View File

@ -0,0 +1,86 @@
using System.Collections.Generic;
using Myriad.Types;
namespace Myriad.Builders
{
public class EmbedBuilder
{
private Embed _embed = new();
private readonly List<Embed.Field> _fields = new();
public EmbedBuilder Title(string? title)
{
_embed = _embed with {Title = title};
return this;
}
public EmbedBuilder Description(string? description)
{
_embed = _embed with { Description = description};
return this;
}
public EmbedBuilder Url(string? url)
{
_embed = _embed with {Url = url};
return this;
}
public EmbedBuilder Color(uint? color)
{
_embed = _embed with {Color = color};
return this;
}
public EmbedBuilder Footer(Embed.EmbedFooter? footer)
{
_embed = _embed with {
Footer = footer
};
return this;
}
public EmbedBuilder Image(Embed.EmbedImage? image)
{
_embed = _embed with {
Image = image
};
return this;
}
public EmbedBuilder Thumbnail(Embed.EmbedThumbnail? thumbnail)
{
_embed = _embed with {
Thumbnail = thumbnail
};
return this;
}
public EmbedBuilder Author(Embed.EmbedAuthor? author)
{
_embed = _embed with {
Author = author
};
return this;
}
public EmbedBuilder Timestamp(string? timestamp)
{
_embed = _embed with {
Timestamp = timestamp
};
return this;
}
public EmbedBuilder Field(Embed.Field field)
{
_fields.Add(field);
return this;
}
public Embed Build() =>
_embed with { Fields = _fields.ToArray() };
}
}

View File

@ -1,6 +1,14 @@
namespace Myriad.Extensions using Myriad.Gateway;
using Myriad.Types;
namespace Myriad.Extensions
{ {
public static class MessageExtensions public static class MessageExtensions
{ {
public static string JumpLink(this Message msg) =>
$"https://discord.com/channels/{msg.GuildId}/{msg.ChannelId}/{msg.Id}";
public static string JumpLink(this MessageReactionAddEvent msg) =>
$"https://discord.com/channels/{msg.GuildId}/{msg.ChannelId}/{msg.MessageId}";
} }
} }

View File

@ -39,6 +39,10 @@ namespace Myriad.Rest
public Task<User?> GetUser(ulong id) => public Task<User?> GetUser(ulong id) =>
_client.Get<User>($"/users/{id}", ("GetUser", default)); _client.Get<User>($"/users/{id}", ("GetUser", default));
public Task<GuildMember?> GetGuildMember(ulong guildId, ulong userId) =>
_client.Get<GuildMember>($"/guilds/{guildId}/members/{userId}",
("GetGuildMember", guildId));
public Task<Message> CreateMessage(ulong channelId, MessageRequest request) => public Task<Message> CreateMessage(ulong channelId, MessageRequest request) =>
_client.Post<Message>($"/channels/{channelId}/messages", ("CreateMessage", channelId), request)!; _client.Post<Message>($"/channels/{channelId}/messages", ("CreateMessage", channelId), request)!;
@ -110,7 +114,7 @@ namespace Myriad.Rest
public Task<Message> ExecuteWebhook(ulong webhookId, string webhookToken, ExecuteWebhookRequest request, public Task<Message> ExecuteWebhook(ulong webhookId, string webhookToken, ExecuteWebhookRequest request,
MultipartFile[]? files = null) => MultipartFile[]? files = null) =>
_client.PostMultipart<Message>($"/webhooks/{webhookId}/{webhookToken}", _client.PostMultipart<Message>($"/webhooks/{webhookId}/{webhookToken}?wait=true",
("ExecuteWebhook", webhookId), request, files)!; ("ExecuteWebhook", webhookId), request, files)!;
private static string EncodeEmoji(Emoji emoji) => private static string EncodeEmoji(Emoji emoji) =>

View File

@ -1,6 +1,4 @@
using System.Collections.Generic; namespace Myriad.Types
namespace Myriad.Types
{ {
public record Embed public record Embed
{ {

View File

@ -11,6 +11,7 @@ using App.Metrics;
using Autofac; using Autofac;
using Myriad.Cache; using Myriad.Cache;
using Myriad.Extensions;
using Myriad.Gateway; using Myriad.Gateway;
using Myriad.Rest; using Myriad.Rest;
using Myriad.Types; using Myriad.Types;
@ -75,7 +76,18 @@ namespace PluralKit.Bot
}, null, timeTillNextWholeMinute, TimeSpan.FromMinutes(1)); }, null, timeTillNextWholeMinute, TimeSpan.FromMinutes(1));
} }
public GuildMemberPartial? BotMemberIn(ulong guildId) => _guildMembers.GetValueOrDefault(guildId); public PermissionSet PermissionsIn(ulong channelId)
{
var channel = _cache.GetChannel(channelId);
if (channel.GuildId != null)
{
var member = _guildMembers.GetValueOrDefault(channel.GuildId.Value);
return _cache.PermissionsFor(channelId, _cluster.User?.Id ?? default, member?.Roles);
}
return PermissionSet.Dm;
}
private async Task OnEventReceived(Shard shard, IGatewayEvent evt) private async Task OnEventReceived(Shard shard, IGatewayEvent evt)
{ {

View File

@ -37,7 +37,6 @@ namespace PluralKit.Bot
private readonly Message _messageNew; private readonly Message _messageNew;
private readonly Parameters _parameters; private readonly Parameters _parameters;
private readonly MessageContext _messageContext; private readonly MessageContext _messageContext;
private readonly GuildMemberPartial? _botMember;
private readonly PermissionSet _botPermissions; private readonly PermissionSet _botPermissions;
private readonly PermissionSet _userPermissions; private readonly PermissionSet _userPermissions;
@ -51,7 +50,7 @@ namespace PluralKit.Bot
private Command _currentCommand; private Command _currentCommand;
public Context(ILifetimeScope provider, Shard shard, Guild? guild, Channel channel, MessageCreateEvent message, int commandParseOffset, public Context(ILifetimeScope provider, Shard shard, Guild? guild, Channel channel, MessageCreateEvent message, int commandParseOffset,
PKSystem senderSystem, MessageContext messageContext, GuildMemberPartial? botMember) PKSystem senderSystem, MessageContext messageContext, PermissionSet botPermissions)
{ {
_rest = provider.Resolve<DiscordRestClient>(); _rest = provider.Resolve<DiscordRestClient>();
_client = provider.Resolve<DiscordShardedClient>(); _client = provider.Resolve<DiscordShardedClient>();
@ -61,7 +60,6 @@ namespace PluralKit.Bot
_channel = channel; _channel = channel;
_senderSystem = senderSystem; _senderSystem = senderSystem;
_messageContext = messageContext; _messageContext = messageContext;
_botMember = botMember;
_cache = provider.Resolve<IDiscordCache>(); _cache = provider.Resolve<IDiscordCache>();
_db = provider.Resolve<IDatabase>(); _db = provider.Resolve<IDatabase>();
_repo = provider.Resolve<ModelRepository>(); _repo = provider.Resolve<ModelRepository>();
@ -71,7 +69,7 @@ namespace PluralKit.Bot
_parameters = new Parameters(message.Content.Substring(commandParseOffset)); _parameters = new Parameters(message.Content.Substring(commandParseOffset));
_newRest = provider.Resolve<DiscordApiClient>(); _newRest = provider.Resolve<DiscordApiClient>();
_botPermissions = _cache.PermissionsFor(message.ChannelId, shard.User!.Id, botMember!); _botPermissions = botPermissions;
_userPermissions = _cache.PermissionsFor(message); _userPermissions = _cache.PermissionsFor(message);
} }

View File

@ -1,8 +1,6 @@
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using DSharpPlus;
using Humanizer; using Humanizer;
using PluralKit.Core; using PluralKit.Core;
@ -120,14 +118,6 @@ namespace PluralKit.Bot
public static Command[] BlacklistCommands = {BlacklistAdd, BlacklistRemove, BlacklistShow}; public static Command[] BlacklistCommands = {BlacklistAdd, BlacklistRemove, BlacklistShow};
private DiscordShardedClient _client;
public CommandTree(DiscordShardedClient client)
{
_client = client;
}
public Task ExecuteCommand(Context ctx) public Task ExecuteCommand(Context ctx)
{ {
if (ctx.Match("system", "s")) if (ctx.Match("system", "s"))

View File

@ -114,7 +114,7 @@ namespace PluralKit.Bot
try try
{ {
var system = ctx.SystemId != null ? await _db.Execute(c => _repo.GetSystem(c, ctx.SystemId.Value)) : null; var system = ctx.SystemId != null ? await _db.Execute(c => _repo.GetSystem(c, ctx.SystemId.Value)) : null;
await _tree.ExecuteCommand(new Context(_services, shard, guild, channel, evt, cmdStart, system, ctx, _bot.BotMemberIn(channel.GuildId!.Value))); await _tree.ExecuteCommand(new Context(_services, shard, guild, channel, evt, cmdStart, system, ctx, _bot.PermissionsIn(channel.Id)));
} }
catch (PKError) catch (PKError)
{ {
@ -147,8 +147,7 @@ namespace PluralKit.Bot
private async ValueTask<bool> TryHandleProxy(Shard shard, MessageCreateEvent evt, Guild guild, Channel channel, MessageContext ctx) private async ValueTask<bool> TryHandleProxy(Shard shard, MessageCreateEvent evt, Guild guild, Channel channel, MessageContext ctx)
{ {
var botMember = _bot.BotMemberIn(channel.GuildId!.Value); var botPermissions = _bot.PermissionsIn(channel.Id);
var botPermissions = PermissionExtensions.PermissionsFor(guild, channel, shard.User!.Id, botMember!.Roles);
try try
{ {

View File

@ -34,7 +34,7 @@ namespace PluralKit.Bot
{ {
await Task.Delay(MessageDeleteDelay); await Task.Delay(MessageDeleteDelay);
// TODO // TODO
// await _db.Execute(c => _repo.DeleteMessage(c, evt.Message.Id)); await _db.Execute(c => _repo.DeleteMessage(c, evt.Id));
} }
// Fork a task to delete the message after a short delay // Fork a task to delete the message after a short delay
@ -49,9 +49,10 @@ namespace PluralKit.Bot
async Task Inner() async Task Inner()
{ {
await Task.Delay(MessageDeleteDelay); await Task.Delay(MessageDeleteDelay);
// TODO
// _logger.Information("Bulk deleting {Count} messages in channel {Channel}", evt.Messages.Count, evt.Channel.Id); _logger.Information("Bulk deleting {Count} messages in channel {Channel}",
// await _db.Execute(c => _repo.DeleteMessagesBulk(c, evt.Messages.Select(m => m.Id).ToList())); evt.Ids.Length, evt.ChannelId);
await _db.Execute(c => _repo.DeleteMessagesBulk(c, evt.Ids));
} }
_ = Inner(); _ = Inner();

View File

@ -1,11 +1,13 @@
using System.Threading.Tasks; using System.Threading.Tasks;
using DSharpPlus; using Myriad.Builders;
using DSharpPlus.Entities; using Myriad.Cache;
using DSharpPlus.EventArgs; using Myriad.Extensions;
using DSharpPlus.Exceptions;
using Myriad.Gateway; using Myriad.Gateway;
using Myriad.Rest;
using Myriad.Rest.Exceptions;
using Myriad.Rest.Types;
using Myriad.Types;
using PluralKit.Core; using PluralKit.Core;
@ -18,37 +20,42 @@ namespace PluralKit.Bot
private readonly IDatabase _db; private readonly IDatabase _db;
private readonly ModelRepository _repo; private readonly ModelRepository _repo;
private readonly CommandMessageService _commandMessageService; private readonly CommandMessageService _commandMessageService;
private readonly EmbedService _embeds;
private readonly ILogger _logger; private readonly ILogger _logger;
private readonly IDiscordCache _cache;
private readonly Bot _bot;
private readonly DiscordApiClient _rest;
public ReactionAdded(EmbedService embeds, ILogger logger, IDatabase db, ModelRepository repo, CommandMessageService commandMessageService) public ReactionAdded(ILogger logger, IDatabase db, ModelRepository repo, CommandMessageService commandMessageService, IDiscordCache cache, Bot bot, DiscordApiClient rest)
{ {
_embeds = embeds;
_db = db; _db = db;
_repo = repo; _repo = repo;
_commandMessageService = commandMessageService; _commandMessageService = commandMessageService;
_cache = cache;
_bot = bot;
_rest = rest;
_logger = logger.ForContext<ReactionAdded>(); _logger = logger.ForContext<ReactionAdded>();
} }
public async Task Handle(Shard shard, MessageReactionAddEvent evt) public async Task Handle(Shard shard, MessageReactionAddEvent evt)
{ {
// await TryHandleProxyMessageReactions(shard, evt); await TryHandleProxyMessageReactions(evt);
} }
private async ValueTask TryHandleProxyMessageReactions(DiscordClient shard, MessageReactionAddEventArgs evt) private async ValueTask TryHandleProxyMessageReactions(MessageReactionAddEvent evt)
{ {
// Sometimes we get events from users that aren't in the user cache // Sometimes we get events from users that aren't in the user cache
// In that case we get a "broken" user object (where eg. calling IsBot throws an exception)
// We just ignore all of those for now, should be quite rare... // We just ignore all of those for now, should be quite rare...
if (!shard.TryGetCachedUser(evt.User.Id, out _)) return; if (!_cache.TryGetUser(evt.UserId, out var user))
return;
var channel = _cache.GetChannel(evt.ChannelId);
// check if it's a command message first // check if it's a command message first
// since this can happen in DMs as well // since this can happen in DMs as well
if (evt.Emoji.Name == "\u274c") if (evt.Emoji.Name == "\u274c")
{ {
await using var conn = await _db.Obtain(); await using var conn = await _db.Obtain();
var commandMsg = await _commandMessageService.GetCommandMessage(conn, evt.Message.Id); var commandMsg = await _commandMessageService.GetCommandMessage(conn, evt.MessageId);
if (commandMsg != null) if (commandMsg != null)
{ {
await HandleCommandDeleteReaction(evt, commandMsg); await HandleCommandDeleteReaction(evt, commandMsg);
@ -57,10 +64,10 @@ namespace PluralKit.Bot
} }
// Only proxies in guild text channels // Only proxies in guild text channels
if (evt.Channel == null || evt.Channel.Type != ChannelType.Text) return; if (channel.Type != Channel.ChannelType.GuildText) return;
// Ignore reactions from bots (we can't DM them anyway) // Ignore reactions from bots (we can't DM them anyway)
if (evt.User.IsBot) return; if (user.Bot) return;
switch (evt.Emoji.Name) switch (evt.Emoji.Name)
{ {
@ -68,7 +75,7 @@ namespace PluralKit.Bot
case "\u274C": // Red X case "\u274C": // Red X
{ {
await using var conn = await _db.Obtain(); await using var conn = await _db.Obtain();
var msg = await _repo.GetMessage(conn, evt.Message.Id); var msg = await _repo.GetMessage(conn, evt.MessageId);
if (msg != null) if (msg != null)
await HandleProxyDeleteReaction(evt, msg); await HandleProxyDeleteReaction(evt, msg);
@ -78,9 +85,9 @@ namespace PluralKit.Bot
case "\u2754": // White question mark case "\u2754": // White question mark
{ {
await using var conn = await _db.Obtain(); await using var conn = await _db.Obtain();
var msg = await _repo.GetMessage(conn, evt.Message.Id); var msg = await _repo.GetMessage(conn, evt.MessageId);
if (msg != null) if (msg != null)
await HandleQueryReaction(shard, evt, msg); await HandleQueryReaction(evt, msg);
break; break;
} }
@ -92,7 +99,7 @@ namespace PluralKit.Bot
case "\u2757": // Exclamation mark case "\u2757": // Exclamation mark
{ {
await using var conn = await _db.Obtain(); await using var conn = await _db.Obtain();
var msg = await _repo.GetMessage(conn, evt.Message.Id); var msg = await _repo.GetMessage(conn, evt.MessageId);
if (msg != null) if (msg != null)
await HandlePingReaction(evt, msg); await HandlePingReaction(evt, msg);
break; break;
@ -100,37 +107,39 @@ namespace PluralKit.Bot
} }
} }
private async ValueTask HandleProxyDeleteReaction(MessageReactionAddEventArgs evt, FullMessage msg) private async ValueTask HandleProxyDeleteReaction(MessageReactionAddEvent evt, FullMessage msg)
{ {
if (!evt.Channel.BotHasAllPermissions(Permissions.ManageMessages)) return; if (!_bot.PermissionsIn(evt.ChannelId).HasFlag(PermissionSet.ManageMessages))
return;
// Can only delete your own message // Can only delete your own message
if (msg.Message.Sender != evt.User.Id) return; if (msg.Message.Sender != evt.UserId) return;
try try
{ {
await evt.Message.DeleteAsync(); await _rest.DeleteMessage(evt.ChannelId, evt.MessageId);
} }
catch (NotFoundException) catch (NotFoundException)
{ {
// Message was deleted by something/someone else before we got to it // Message was deleted by something/someone else before we got to it
} }
await _db.Execute(c => _repo.DeleteMessage(c, evt.Message.Id)); await _db.Execute(c => _repo.DeleteMessage(c, evt.MessageId));
} }
private async ValueTask HandleCommandDeleteReaction(MessageReactionAddEventArgs evt, CommandMessage msg) private async ValueTask HandleCommandDeleteReaction(MessageReactionAddEvent evt, CommandMessage msg)
{ {
if (!evt.Channel.BotHasAllPermissions(Permissions.ManageMessages) && evt.Channel.Guild != null) // TODO: why does the bot need manage messages if it's deleting its own messages??
if (!_bot.PermissionsIn(evt.ChannelId).HasFlag(PermissionSet.ManageMessages))
return; return;
// Can only delete your own message // Can only delete your own message
if (msg.AuthorId != evt.User.Id) if (msg.AuthorId != evt.UserId)
return; return;
try try
{ {
await evt.Message.DeleteAsync(); await _rest.DeleteMessage(evt.ChannelId, evt.MessageId);
} }
catch (NotFoundException) catch (NotFoundException)
{ {
@ -140,44 +149,52 @@ namespace PluralKit.Bot
// No need to delete database row here, it'll get deleted by the once-per-minute scheduled task. // No need to delete database row here, it'll get deleted by the once-per-minute scheduled task.
} }
private async ValueTask HandleQueryReaction(DiscordClient shard, MessageReactionAddEventArgs evt, FullMessage msg) private async ValueTask HandleQueryReaction(MessageReactionAddEvent evt, FullMessage msg)
{ {
// Try to DM the user info about the message // Try to DM the user info about the message
var member = await evt.Guild.GetMember(evt.User.Id); // var member = await evt.Guild.GetMember(evt.User.Id);
try try
{ {
await member.SendMessageAsync(embed: await _embeds.CreateMemberEmbed(msg.System, msg.Member, evt.Guild, LookupContext.ByNonOwner)); // TODO: how to DM?
await member.SendMessageAsync(embed: await _embeds.CreateMessageInfoEmbed(shard, msg)); // await member.SendMessageAsync(embed: await _embeds.CreateMemberEmbed(msg.System, msg.Member, evt.Guild, LookupContext.ByNonOwner));
// await member.SendMessageAsync(embed: await _embeds.CreateMessageInfoEmbed(shard, msg));
} }
catch (UnauthorizedException) { } // No permissions to DM, can't check for this :( catch (UnauthorizedException) { } // No permissions to DM, can't check for this :(
await TryRemoveOriginalReaction(evt); await TryRemoveOriginalReaction(evt);
} }
private async ValueTask HandlePingReaction(MessageReactionAddEventArgs evt, FullMessage msg) private async ValueTask HandlePingReaction(MessageReactionAddEvent evt, FullMessage msg)
{ {
if (!evt.Channel.BotHasAllPermissions(Permissions.SendMessages)) return; if (!_bot.PermissionsIn(evt.ChannelId).HasFlag(PermissionSet.ManageMessages))
return;
// Check if the "pinger" has permission to send messages in this channel // Check if the "pinger" has permission to send messages in this channel
// (if not, PK shouldn't send messages on their behalf) // (if not, PK shouldn't send messages on their behalf)
var guildUser = await evt.Guild.GetMember(evt.User.Id); var member = await _rest.GetGuildMember(evt.GuildId!.Value, evt.UserId);
var requiredPerms = Permissions.AccessChannels | Permissions.SendMessages; var requiredPerms = PermissionSet.ViewChannel | PermissionSet.SendMessages;
if (guildUser == null || (guildUser.PermissionsIn(evt.Channel) & requiredPerms) != requiredPerms) return; if (member == null || !_cache.PermissionsFor(evt.ChannelId, member).HasFlag(requiredPerms)) return;
if (msg.System.PingsEnabled) if (msg.System.PingsEnabled)
{ {
// If the system has pings enabled, go ahead // If the system has pings enabled, go ahead
var embed = new DiscordEmbedBuilder().WithDescription($"[Jump to pinged message]({evt.Message.JumpLink})"); var embed = new EmbedBuilder().Description($"[Jump to pinged message]({evt.JumpLink()})");
await evt.Channel.SendMessageFixedAsync($"Psst, **{msg.Member.DisplayName()}** (<@{msg.Message.Sender}>), you have been pinged by <@{evt.User.Id}>.", embed: embed.Build(), await _rest.CreateMessage(evt.ChannelId, new()
new IMention[] {new UserMention(msg.Message.Sender) }); {
Content =
$"Psst, **{msg.Member.DisplayName()}** (<@{msg.Message.Sender}>), you have been pinged by <@{evt.UserId}>.",
Embed = embed.Build(),
AllowedMentions = new AllowedMentions {Users = new[] {msg.Message.Sender}}
});
} }
else else
{ {
// If not, tell them in DMs (if we can) // If not, tell them in DMs (if we can)
try try
{ {
await guildUser.SendMessageFixedAsync($"{Emojis.Error} {msg.Member.DisplayName()}'s system has disabled reaction pings. If you want to mention them anyway, you can copy/paste the following message:"); // todo: how to dm
await guildUser.SendMessageFixedAsync($"<@{msg.Message.Sender}>".AsCode()); // await guildUser.SendMessageFixedAsync($"{Emojis.Error} {msg.Member.DisplayName()}'s system has disabled reaction pings. If you want to mention them anyway, you can copy/paste the following message:");
// await guildUser.SendMessageFixedAsync($"<@{msg.Message.Sender}>".AsCode());
} }
catch (UnauthorizedException) { } catch (UnauthorizedException) { }
} }
@ -185,21 +202,10 @@ namespace PluralKit.Bot
await TryRemoveOriginalReaction(evt); await TryRemoveOriginalReaction(evt);
} }
private async Task TryRemoveOriginalReaction(MessageReactionAddEventArgs evt) private async Task TryRemoveOriginalReaction(MessageReactionAddEvent evt)
{ {
try if (_bot.PermissionsIn(evt.ChannelId).HasFlag(PermissionSet.ManageMessages))
{ await _rest.DeleteOwnReaction(evt.ChannelId, evt.MessageId, evt.Emoji);
if (evt.Channel.BotHasAllPermissions(Permissions.ManageMessages))
await evt.Message.DeleteReactionAsync(evt.Emoji, evt.User);
}
catch (UnauthorizedException)
{
var botPerms = evt.Channel.BotPermissions();
// So, in some cases (see Sentry issue 11K) the above check somehow doesn't work, and
// Discord returns a 403 Unauthorized. TODO: figure out the root cause here instead of a workaround
_logger.Warning("Attempted to remove reaction {Emoji} from user {User} on message {Channel}/{Message}, but got 403. Bot has permissions {Permissions} according to itself.",
evt.Emoji.Id, evt.User.Id, evt.Channel.Id, evt.Message.Id, botPerms);
}
} }
} }
} }

View File

@ -8,6 +8,7 @@ using DSharpPlus.Entities;
using Humanizer; using Humanizer;
using Myriad.Builders;
using Myriad.Cache; using Myriad.Cache;
using Myriad.Rest; using Myriad.Rest;
using Myriad.Types; using Myriad.Types;
@ -62,55 +63,52 @@ namespace PluralKit.Bot {
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 embed = new Embed var eb = new EmbedBuilder()
{ .Title(system.Name)
Title = system.Name, .Thumbnail(new(system.AvatarUrl))
Thumbnail = new(system.AvatarUrl), .Footer(new($"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);
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)
fields.Add(new("Fronter".ToQuantity(switchMembers.Count, ShowQuantityAs.None), string.Join(", ", switchMembers.Select(m => m.NameFor(ctx))))); eb.Field(new("Fronter".ToQuantity(switchMembers.Count, ShowQuantityAs.None), string.Join(", ", switchMembers.Select(m => m.NameFor(ctx)))));
} }
if (system.Tag != null) if (system.Tag != null)
fields.Add(new("Tag", system.Tag.EscapeMarkdown())); eb.Field(new("Tag", system.Tag.EscapeMarkdown()));
fields.Add(new("Linked accounts", string.Join("\n", users).Truncate(1000), true)); eb.Field(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)
fields.Add(new($"Members ({memberCount})", $"(see `pk;system {system.Hid} list` or `pk;system {system.Hid} list full`)", true)); eb.Field(new($"Members ({memberCount})", $"(see `pk;system {system.Hid} list` or `pk;system {system.Hid} list full`)", true));
else else
fields.Add(new($"Members ({memberCount})", "Add one with `pk;member new`!", true)); eb.Field(new($"Members ({memberCount})", "Add one with `pk;member new`!", true));
} }
if (system.DescriptionFor(ctx) is { } desc) if (system.DescriptionFor(ctx) is { } desc)
fields.Add(new("Description", desc.NormalizeLineEndSpacing().Truncate(1024), false)); eb.Field(new("Description", desc.NormalizeLineEndSpacing().Truncate(1024), false));
return embed with { Fields = fields.ToArray() }; return eb.Build();
} }
public DiscordEmbed CreateLoggedMessageEmbed(PKSystem system, PKMember member, ulong messageId, ulong originalMsgId, DiscordUser sender, string content, DiscordChannel channel) { public Embed CreateLoggedMessageEmbed(PKSystem system, PKMember member, ulong messageId, ulong originalMsgId, User sender, string content, Channel channel) {
// TODO: pronouns in ?-reacted response using this card // TODO: pronouns in ?-reacted response using this card
var timestamp = DiscordUtils.SnowflakeToInstant(messageId); var timestamp = DiscordUtils.SnowflakeToInstant(messageId);
var name = member.NameFor(LookupContext.ByNonOwner); var name = member.NameFor(LookupContext.ByNonOwner);
return new DiscordEmbedBuilder() return new EmbedBuilder()
.WithAuthor($"#{channel.Name}: {name}", iconUrl: DiscordUtils.WorkaroundForUrlBug(member.AvatarFor(LookupContext.ByNonOwner))) .Author(new($"#{channel.Name}: {name}", IconUrl: DiscordUtils.WorkaroundForUrlBug(member.AvatarFor(LookupContext.ByNonOwner))))
.WithThumbnail(member.AvatarFor(LookupContext.ByNonOwner)) .Thumbnail(new(member.AvatarFor(LookupContext.ByNonOwner)))
.WithDescription(content?.NormalizeLineEndSpacing()) .Description(content?.NormalizeLineEndSpacing())
.WithFooter($"System ID: {system.Hid} | Member ID: {member.Hid} | Sender: {sender.Username}#{sender.Discriminator} ({sender.Id}) | Message ID: {messageId} | Original Message ID: {originalMsgId}") .Footer(new($"System ID: {system.Hid} | Member ID: {member.Hid} | Sender: {sender.Username}#{sender.Discriminator} ({sender.Id}) | Message ID: {messageId} | Original Message ID: {originalMsgId}"))
.WithTimestamp(timestamp.ToDateTimeOffset()) .Timestamp(timestamp.ToDateTimeOffset().ToString("O"))
.Build(); .Build();
} }
public async Task<DiscordEmbed> CreateMemberEmbed(PKSystem system, PKMember member, DiscordGuild guild, LookupContext ctx) public async Task<Embed> CreateMemberEmbed(PKSystem system, PKMember member, DiscordGuild guild, LookupContext ctx)
{ {
// string FormatTimestamp(Instant timestamp) => DateTimeFormats.ZonedDateTimeFormat.Format(timestamp.InZone(system.Zone)); // string FormatTimestamp(Instant timestamp) => DateTimeFormats.ZonedDateTimeFormat.Format(timestamp.InZone(system.Zone));
@ -142,12 +140,13 @@ namespace PluralKit.Bot {
.OrderBy(g => g.Name, StringComparer.InvariantCultureIgnoreCase) .OrderBy(g => g.Name, StringComparer.InvariantCultureIgnoreCase)
.ToListAsync(); .ToListAsync();
var eb = new DiscordEmbedBuilder() var eb = new EmbedBuilder()
// TODO: add URL of website when that's up // TODO: add URL of website when that's up
.WithAuthor(name, iconUrl: DiscordUtils.WorkaroundForUrlBug(avatar)) .Author(new(name, IconUrl: DiscordUtils.WorkaroundForUrlBug(avatar)))
// .WithColor(member.ColorPrivacy.CanAccess(ctx) ? color : DiscordUtils.Gray) // .WithColor(member.ColorPrivacy.CanAccess(ctx) ? color : DiscordUtils.Gray)
.WithColor(color) .Color((uint?) color.Value)
.WithFooter($"System ID: {system.Hid} | Member ID: {member.Hid} {(member.MetadataPrivacy.CanAccess(ctx) ? $"| Created on {member.Created.FormatZoned(system)}":"")}"); .Footer(new(
$"System ID: {system.Hid} | Member ID: {member.Hid} {(member.MetadataPrivacy.CanAccess(ctx) ? $"| Created on {member.Created.FormatZoned(system)}" : "")}"));
var description = ""; var description = "";
if (member.MemberVisibility == PrivacyLevel.Private) description += "*(this member is hidden)*\n"; if (member.MemberVisibility == PrivacyLevel.Private) description += "*(this member is hidden)*\n";
@ -156,21 +155,21 @@ namespace PluralKit.Bot {
description += $"*(this member has a server-specific avatar set; [click here]({member.AvatarUrl}) to see the global avatar)*\n"; description += $"*(this member has a server-specific avatar set; [click here]({member.AvatarUrl}) to see the global avatar)*\n";
else else
description += "*(this member has a server-specific avatar set)*\n"; description += "*(this member has a server-specific avatar set)*\n";
if (description != "") eb.WithDescription(description); if (description != "") eb.Description(description);
if (avatar != null) eb.WithThumbnail(avatar); if (avatar != null) eb.Thumbnail(new(avatar));
if (!member.DisplayName.EmptyOrNull() && member.NamePrivacy.CanAccess(ctx)) eb.AddField("Display Name", member.DisplayName.Truncate(1024), true); if (!member.DisplayName.EmptyOrNull() && member.NamePrivacy.CanAccess(ctx)) eb.Field(new("Display Name", member.DisplayName.Truncate(1024), true));
if (guild != null && guildDisplayName != null) eb.AddField($"Server Nickname (for {guild.Name})", guildDisplayName.Truncate(1024), true); if (guild != null && guildDisplayName != null) eb.Field(new($"Server Nickname (for {guild.Name})", guildDisplayName.Truncate(1024), true));
if (member.BirthdayFor(ctx) != null) eb.AddField("Birthdate", member.BirthdayString, true); if (member.BirthdayFor(ctx) != null) eb.Field(new("Birthdate", member.BirthdayString, true));
if (member.PronounsFor(ctx) is {} pronouns && !string.IsNullOrWhiteSpace(pronouns)) eb.AddField("Pronouns", pronouns.Truncate(1024), true); if (member.PronounsFor(ctx) is {} pronouns && !string.IsNullOrWhiteSpace(pronouns)) eb.Field(new("Pronouns", pronouns.Truncate(1024), true));
if (member.MessageCountFor(ctx) is {} count && count > 0) eb.AddField("Message Count", member.MessageCount.ToString(), true); if (member.MessageCountFor(ctx) is {} count && count > 0) eb.Field(new("Message Count", member.MessageCount.ToString(), true));
if (member.HasProxyTags) eb.AddField("Proxy Tags", member.ProxyTagsString("\n").Truncate(1024), true); if (member.HasProxyTags) eb.Field(new("Proxy Tags", member.ProxyTagsString("\n").Truncate(1024), true));
// --- For when this gets added to the member object itself or however they get added // --- For when this gets added to the member object itself or however they get added
// if (member.LastMessage != null && member.MetadataPrivacy.CanAccess(ctx)) eb.AddField("Last message:" FormatTimestamp(DiscordUtils.SnowflakeToInstant(m.LastMessage.Value))); // if (member.LastMessage != null && member.MetadataPrivacy.CanAccess(ctx)) eb.AddField("Last message:" FormatTimestamp(DiscordUtils.SnowflakeToInstant(m.LastMessage.Value)));
// if (member.LastSwitchTime != null && m.MetadataPrivacy.CanAccess(ctx)) eb.AddField("Last switched in:", FormatTimestamp(member.LastSwitchTime.Value)); // if (member.LastSwitchTime != null && m.MetadataPrivacy.CanAccess(ctx)) eb.AddField("Last switched in:", FormatTimestamp(member.LastSwitchTime.Value));
// if (!member.Color.EmptyOrNull() && member.ColorPrivacy.CanAccess(ctx)) eb.AddField("Color", $"#{member.Color}", true); // if (!member.Color.EmptyOrNull() && member.ColorPrivacy.CanAccess(ctx)) eb.AddField("Color", $"#{member.Color}", true);
if (!member.Color.EmptyOrNull()) eb.AddField("Color", $"#{member.Color}", true); if (!member.Color.EmptyOrNull()) eb.Field(new("Color", $"#{member.Color}", true));
if (groups.Count > 0) if (groups.Count > 0)
{ {
@ -178,15 +177,16 @@ namespace PluralKit.Bot {
var content = groups.Count > 5 var content = groups.Count > 5
? string.Join(", ", groups.Select(g => g.DisplayName ?? g.Name)) ? string.Join(", ", groups.Select(g => g.DisplayName ?? g.Name))
: string.Join("\n", groups.Select(g => $"[`{g.Hid}`] **{g.DisplayName ?? g.Name}**")); : string.Join("\n", groups.Select(g => $"[`{g.Hid}`] **{g.DisplayName ?? g.Name}**"));
eb.AddField($"Groups ({groups.Count})", content.Truncate(1000)); eb.Field(new($"Groups ({groups.Count})", content.Truncate(1000)));
} }
if (member.DescriptionFor(ctx) is {} desc) eb.AddField("Description", member.Description.NormalizeLineEndSpacing(), false); if (member.DescriptionFor(ctx) is {} desc)
eb.Field(new("Description", member.Description.NormalizeLineEndSpacing(), false));
return eb.Build(); return eb.Build();
} }
public async Task<DiscordEmbed> CreateGroupEmbed(Context ctx, PKSystem system, PKGroup target) public async Task<Embed> CreateGroupEmbed(Context ctx, PKSystem system, PKGroup target)
{ {
await using var conn = await _db.Obtain(); await using var conn = await _db.Obtain();
@ -197,43 +197,43 @@ namespace PluralKit.Bot {
if (system.Name != null) if (system.Name != null)
nameField = $"{nameField} ({system.Name})"; nameField = $"{nameField} ({system.Name})";
var eb = new DiscordEmbedBuilder() var eb = new EmbedBuilder()
.WithAuthor(nameField, iconUrl: DiscordUtils.WorkaroundForUrlBug(target.IconFor(pctx))) .Author(new(nameField, IconUrl: DiscordUtils.WorkaroundForUrlBug(target.IconFor(pctx))))
.WithFooter($"System ID: {system.Hid} | Group ID: {target.Hid} | Created on {target.Created.FormatZoned(system)}"); .Footer(new($"System ID: {system.Hid} | Group ID: {target.Hid} | Created on {target.Created.FormatZoned(system)}"));
if (target.DisplayName != null) if (target.DisplayName != null)
eb.AddField("Display Name", target.DisplayName); eb.Field(new("Display Name", target.DisplayName));
if (target.ListPrivacy.CanAccess(pctx)) if (target.ListPrivacy.CanAccess(pctx))
{ {
if (memberCount == 0 && pctx == LookupContext.ByOwner) if (memberCount == 0 && pctx == LookupContext.ByOwner)
// Only suggest the add command if this is actually the owner lol // Only suggest the add command if this is actually the owner lol
eb.AddField("Members (0)", $"Add one with `pk;group {target.Reference()} add <member>`!", true); eb.Field(new("Members (0)", $"Add one with `pk;group {target.Reference()} add <member>`!", true));
else else
eb.AddField($"Members ({memberCount})", $"(see `pk;group {target.Reference()} list`)", true); eb.Field(new($"Members ({memberCount})", $"(see `pk;group {target.Reference()} list`)", true));
} }
if (target.DescriptionFor(pctx) is {} desc) if (target.DescriptionFor(pctx) is { } desc)
eb.AddField("Description", desc); eb.Field(new("Description", desc));
if (target.IconFor(pctx) is {} icon) if (target.IconFor(pctx) is {} icon)
eb.WithThumbnail(icon); eb.Thumbnail(new(icon));
return eb.Build(); return eb.Build();
} }
public async Task<DiscordEmbed> CreateFronterEmbed(PKSwitch sw, DateTimeZone zone, LookupContext ctx) public async Task<Embed> CreateFronterEmbed(PKSwitch sw, DateTimeZone zone, LookupContext ctx)
{ {
var members = await _db.Execute(c => _repo.GetSwitchMembers(c, sw.Id).ToListAsync().AsTask()); var members = await _db.Execute(c => _repo.GetSwitchMembers(c, sw.Id).ToListAsync().AsTask());
var timeSinceSwitch = SystemClock.Instance.GetCurrentInstant() - sw.Timestamp; var timeSinceSwitch = SystemClock.Instance.GetCurrentInstant() - sw.Timestamp;
return new DiscordEmbedBuilder() return new EmbedBuilder()
.WithColor(members.FirstOrDefault()?.Color?.ToDiscordColor() ?? DiscordUtils.Gray) .Color((uint?) (members.FirstOrDefault()?.Color?.ToDiscordColor()?.Value ?? DiscordUtils.Gray.Value))
.AddField($"Current {"fronter".ToQuantity(members.Count, ShowQuantityAs.None)}", members.Count > 0 ? string.Join(", ", members.Select(m => m.NameFor(ctx))) : "*(no fronter)*") .Field(new($"Current {"fronter".ToQuantity(members.Count, ShowQuantityAs.None)}", members.Count > 0 ? string.Join(", ", members.Select(m => m.NameFor(ctx))) : "*(no fronter)*"))
.AddField("Since", $"{sw.Timestamp.FormatZoned(zone)} ({timeSinceSwitch.FormatDuration()} ago)") .Field(new("Since", $"{sw.Timestamp.FormatZoned(zone)} ({timeSinceSwitch.FormatDuration()} ago)"))
.Build(); .Build();
} }
public async Task<DiscordEmbed> CreateMessageInfoEmbed(DiscordClient client, FullMessage msg) public async Task<Embed> CreateMessageInfoEmbed(DiscordClient client, FullMessage msg)
{ {
var ctx = LookupContext.ByNonOwner; var ctx = LookupContext.ByNonOwner;
var channel = await _client.GetChannel(msg.Message.Channel); var channel = await _client.GetChannel(msg.Message.Channel);
@ -257,32 +257,32 @@ namespace PluralKit.Bot {
else userStr = $"*(deleted user {msg.Message.Sender})*"; else userStr = $"*(deleted user {msg.Message.Sender})*";
// Put it all together // Put it all together
var eb = new DiscordEmbedBuilder() var eb = new EmbedBuilder()
.WithAuthor(msg.Member.NameFor(ctx), iconUrl: DiscordUtils.WorkaroundForUrlBug(msg.Member.AvatarFor(ctx))) .Author(new(msg.Member.NameFor(ctx), IconUrl: DiscordUtils.WorkaroundForUrlBug(msg.Member.AvatarFor(ctx))))
.WithDescription(serverMsg?.Content?.NormalizeLineEndSpacing() ?? "*(message contents deleted or inaccessible)*") .Description(serverMsg?.Content?.NormalizeLineEndSpacing() ?? "*(message contents deleted or inaccessible)*")
.WithImageUrl(serverMsg?.Attachments?.FirstOrDefault()?.Url) .Image(new(serverMsg?.Attachments?.FirstOrDefault()?.Url))
.AddField("System", .Field(new("System",
msg.System.Name != null ? $"{msg.System.Name} (`{msg.System.Hid}`)" : $"`{msg.System.Hid}`", true) msg.System.Name != null ? $"{msg.System.Name} (`{msg.System.Hid}`)" : $"`{msg.System.Hid}`", true))
.AddField("Member", $"{msg.Member.NameFor(ctx)} (`{msg.Member.Hid}`)", true) .Field(new("Member", $"{msg.Member.NameFor(ctx)} (`{msg.Member.Hid}`)", true))
.AddField("Sent by", userStr, inline: true) .Field(new("Sent by", userStr, true))
.WithTimestamp(DiscordUtils.SnowflakeToInstant(msg.Message.Mid).ToDateTimeOffset()); .Timestamp(DiscordUtils.SnowflakeToInstant(msg.Message.Mid).ToDateTimeOffset().ToString("O"));
var roles = memberInfo?.Roles?.ToList(); var roles = memberInfo?.Roles?.ToList();
if (roles != null && roles.Count > 0) if (roles != null && roles.Count > 0)
{ {
var rolesString = string.Join(", ", roles.Select(role => role.Name)); var rolesString = string.Join(", ", roles.Select(role => role.Name));
eb.AddField($"Account roles ({roles.Count})", rolesString.Truncate(1024)); eb.Field(new($"Account roles ({roles.Count})", rolesString.Truncate(1024)));
} }
return eb.Build(); return eb.Build();
} }
public Task<DiscordEmbed> CreateFrontPercentEmbed(FrontBreakdown breakdown, DateTimeZone tz, LookupContext ctx) public Task<Embed> CreateFrontPercentEmbed(FrontBreakdown breakdown, DateTimeZone tz, LookupContext ctx)
{ {
var actualPeriod = breakdown.RangeEnd - breakdown.RangeStart; var actualPeriod = breakdown.RangeEnd - breakdown.RangeStart;
var eb = new DiscordEmbedBuilder() var eb = new EmbedBuilder()
.WithColor(DiscordUtils.Gray) .Color((uint?) DiscordUtils.Gray.Value)
.WithFooter($"Since {breakdown.RangeStart.FormatZoned(tz)} ({actualPeriod.FormatDuration()} ago)"); .Footer(new($"Since {breakdown.RangeStart.FormatZoned(tz)} ({actualPeriod.FormatDuration()} ago)"));
var maxEntriesToDisplay = 24; // max 25 fields allowed in embed - reserve 1 for "others" var maxEntriesToDisplay = 24; // max 25 fields allowed in embed - reserve 1 for "others"
@ -296,15 +296,15 @@ namespace PluralKit.Bot {
foreach (var pair in membersOrdered) foreach (var pair in membersOrdered)
{ {
var frac = pair.Value / actualPeriod; var frac = pair.Value / actualPeriod;
eb.AddField(pair.Key?.NameFor(ctx) ?? "*(no fronter)*", $"{frac*100:F0}% ({pair.Value.FormatDuration()})"); eb.Field(new(pair.Key?.NameFor(ctx) ?? "*(no fronter)*", $"{frac*100:F0}% ({pair.Value.FormatDuration()})"));
} }
if (membersOrdered.Count > maxEntriesToDisplay) if (membersOrdered.Count > maxEntriesToDisplay)
{ {
eb.AddField("(others)", eb.Field(new("(others)",
membersOrdered.Skip(maxEntriesToDisplay) membersOrdered.Skip(maxEntriesToDisplay)
.Aggregate(Duration.Zero, (prod, next) => prod + next.Value) .Aggregate(Duration.Zero, (prod, next) => prod + next.Value)
.FormatDuration(), true); .FormatDuration(), true));
} }
return Task.FromResult(eb.Build()); return Task.FromResult(eb.Build());

View File

@ -4,7 +4,8 @@ using System.Threading.Tasks;
using App.Metrics; using App.Metrics;
using DSharpPlus.Entities; using Myriad.Builders;
using Myriad.Rest;
using NodaTime; using NodaTime;
@ -19,54 +20,61 @@ namespace PluralKit.Bot
private readonly IMetrics _metrics; private readonly IMetrics _metrics;
private readonly ILogger _logger; private readonly ILogger _logger;
private readonly DiscordApiClient _rest;
public ErrorMessageService(IMetrics metrics, ILogger logger) public ErrorMessageService(IMetrics metrics, ILogger logger, DiscordApiClient rest)
{ {
_metrics = metrics; _metrics = metrics;
_logger = logger; _logger = logger;
_rest = rest;
} }
public async Task SendErrorMessage(DiscordChannel channel, string errorId) public async Task SendErrorMessage(ulong channelId, string errorId)
{ {
var now = SystemClock.Instance.GetCurrentInstant(); var now = SystemClock.Instance.GetCurrentInstant();
if (!ShouldSendErrorMessage(channel, now)) if (!ShouldSendErrorMessage(channelId, now))
{ {
_logger.Warning("Rate limited sending error message to {ChannelId} with error code {ErrorId}", channel.Id, errorId); _logger.Warning("Rate limited sending error message to {ChannelId} with error code {ErrorId}", channelId, errorId);
_metrics.Measure.Meter.Mark(BotMetrics.ErrorMessagesSent, "throttled"); _metrics.Measure.Meter.Mark(BotMetrics.ErrorMessagesSent, "throttled");
return; return;
} }
var embed = new DiscordEmbedBuilder() var embed = new EmbedBuilder()
.WithColor(new DiscordColor(0xE74C3C)) .Color(0xE74C3C)
.WithTitle("Internal error occurred") .Title("Internal error occurred")
.WithDescription("For support, please send the error code above in **#bug-reports-and-errors** on **[the support server *(click to join)*](https://discord.gg/PczBt78)** with a description of what you were doing at the time.") .Description("For support, please send the error code above in **#bug-reports-and-errors** on **[the support server *(click to join)*](https://discord.gg/PczBt78)** with a description of what you were doing at the time.")
.WithFooter(errorId) .Footer(new(errorId))
.WithTimestamp(now.ToDateTimeOffset()); .Timestamp(now.ToDateTimeOffset().ToString("O"));
try try
{ {
await channel.SendMessageAsync($"> **Error code:** `{errorId}`", embed: embed.Build()); await _rest.CreateMessage(channelId, new()
_logger.Information("Sent error message to {ChannelId} with error code {ErrorId}", channel.Id, errorId); {
Content = $"> **Error code:** `{errorId}`",
Embed = embed.Build()
});
_logger.Information("Sent error message to {ChannelId} with error code {ErrorId}", channelId, errorId);
_metrics.Measure.Meter.Mark(BotMetrics.ErrorMessagesSent, "sent"); _metrics.Measure.Meter.Mark(BotMetrics.ErrorMessagesSent, "sent");
} }
catch (Exception e) catch (Exception e)
{ {
_logger.Error(e, "Error sending error message to {ChannelId}", channel.Id); _logger.Error(e, "Error sending error message to {ChannelId}", channelId);
_metrics.Measure.Meter.Mark(BotMetrics.ErrorMessagesSent, "failed"); _metrics.Measure.Meter.Mark(BotMetrics.ErrorMessagesSent, "failed");
throw; throw;
} }
} }
private bool ShouldSendErrorMessage(DiscordChannel channel, Instant now) private bool ShouldSendErrorMessage(ulong channelId, Instant now)
{ {
if (_lastErrorInChannel.TryGetValue(channel.Id, out var lastErrorTime)) if (_lastErrorInChannel.TryGetValue(channelId, out var lastErrorTime))
{ {
var interval = now - lastErrorTime; var interval = now - lastErrorTime;
if (interval < MinErrorInterval) if (interval < MinErrorInterval)
return false; return false;
} }
_lastErrorInChannel[channel.Id] = now; _lastErrorInChannel[channelId] = now;
return true; return true;
} }
} }

View File

@ -3,6 +3,7 @@ using System.Threading.Tasks;
using Dapper; using Dapper;
using Myriad.Cache; using Myriad.Cache;
using Myriad.Extensions;
using Myriad.Rest; using Myriad.Rest;
using Myriad.Types; using Myriad.Types;
@ -18,14 +19,16 @@ namespace PluralKit.Bot {
private readonly ILogger _logger; private readonly ILogger _logger;
private readonly IDiscordCache _cache; private readonly IDiscordCache _cache;
private readonly DiscordApiClient _rest; private readonly DiscordApiClient _rest;
private readonly Bot _bot;
public LogChannelService(EmbedService embed, ILogger logger, IDatabase db, ModelRepository repo, IDiscordCache cache, DiscordApiClient rest) public LogChannelService(EmbedService embed, ILogger logger, IDatabase db, ModelRepository repo, IDiscordCache cache, DiscordApiClient rest, Bot bot)
{ {
_embed = embed; _embed = embed;
_db = db; _db = db;
_repo = repo; _repo = repo;
_cache = cache; _cache = cache;
_rest = rest; _rest = rest;
_bot = bot;
_logger = logger.ForContext<LogChannelService>(); _logger = logger.ForContext<LogChannelService>();
} }
@ -37,24 +40,27 @@ namespace PluralKit.Bot {
var logChannel = await FindLogChannel(trigger.GuildId!.Value, ctx.LogChannel.Value); var logChannel = await FindLogChannel(trigger.GuildId!.Value, ctx.LogChannel.Value);
if (logChannel == null || logChannel.Type != Channel.ChannelType.GuildText) return; if (logChannel == null || logChannel.Type != Channel.ChannelType.GuildText) return;
var triggerChannel = _cache.GetChannel(trigger.ChannelId);
// Check bot permissions // Check bot permissions
// if (!logChannel.BotHasAllPermissions(Permissions.SendMessages | Permissions.EmbedLinks)) var perms = _bot.PermissionsIn(logChannel.Id);
// { if (!perms.HasFlag(PermissionSet.SendMessages | PermissionSet.EmbedLinks))
// _logger.Information( {
// "Does not have permission to proxy log, ignoring (channel: {ChannelId}, guild: {GuildId}, bot permissions: {BotPermissions})", _logger.Information(
// ctx.LogChannel.Value, trigger.GuildId!.Value, trigger.Channel.BotPermissions()); "Does not have permission to proxy log, ignoring (channel: {ChannelId}, guild: {GuildId}, bot permissions: {BotPermissions})",
// return; ctx.LogChannel.Value, trigger.GuildId!.Value, perms);
// } return;
// }
// Send embed! // Send embed!
// TODO: fix? // TODO: fix?
// await using var conn = await _db.Obtain(); await using var conn = await _db.Obtain();
// var embed = _embed.CreateLoggedMessageEmbed(await _repo.GetSystem(conn, ctx.SystemId.Value), var embed = _embed.CreateLoggedMessageEmbed(await _repo.GetSystem(conn, ctx.SystemId.Value),
// await _repo.GetMember(conn, proxy.Member.Id), hookMessage, trigger.Id, trigger.Author, proxy.Content, await _repo.GetMember(conn, proxy.Member.Id), hookMessage, trigger.Id, trigger.Author, proxy.Content,
// trigger.Channel); triggerChannel);
// var url = $"https://discord.com/channels/{trigger.Channel.GuildId}/{trigger.ChannelId}/{hookMessage}"; var url = $"https://discord.com/channels/{trigger.GuildId}/{trigger.ChannelId}/{hookMessage}";
// await logChannel.SendMessageFixedAsync(content: url, embed: embed); await _rest.CreateMessage(logChannel.Id, new() {Content = url, Embed = embed});
} }
private async Task<Channel?> FindLogChannel(ulong guildId, ulong channelId) private async Task<Channel?> FindLogChannel(ulong guildId, ulong channelId)