Merge branch 'main' into rust-command-parser

This commit is contained in:
libglfw 2024-11-02 15:16:29 -07:00
commit 77276c15e6
119 changed files with 4837 additions and 1017 deletions

View file

@ -63,14 +63,14 @@ public class ApplicationCommandProxiedMessage
var messageId = ctx.Event.Data!.TargetId!.Value;
// check for command messages
var (authorId, channelId) = await ctx.Services.Resolve<CommandMessageService>().GetCommandMessage(messageId);
if (authorId != null)
var cmessage = await ctx.Services.Resolve<CommandMessageService>().GetCommandMessage(messageId);
if (cmessage != null)
{
if (authorId != ctx.User.Id)
if (cmessage.AuthorId != ctx.User.Id)
throw new PKError("You can only delete command messages queried by this account.");
var isDM = (await _repo.GetDmChannel(ctx.User!.Id)) == channelId;
await DeleteMessageInner(ctx, channelId!.Value, messageId, isDM);
var isDM = (await _repo.GetDmChannel(ctx.User!.Id)) == cmessage.ChannelId;
await DeleteMessageInner(ctx, cmessage.GuildId, cmessage.ChannelId, messageId, isDM);
return;
}
@ -78,10 +78,10 @@ public class ApplicationCommandProxiedMessage
var message = await ctx.Repository.GetFullMessage(messageId);
if (message != null)
{
if (message.System?.Id != ctx.System.Id && message.Message.Sender != ctx.User.Id)
if (message.Message.Sender != ctx.User.Id && (ctx.System != null && message.System?.Id != ctx.System.Id))
throw new PKError("You can only delete your own messages.");
await DeleteMessageInner(ctx, message.Message.Channel, message.Message.Mid, false);
await DeleteMessageInner(ctx, message.Message.Guild ?? 0, message.Message.Channel, message.Message.Mid, false);
return;
}
@ -89,9 +89,9 @@ public class ApplicationCommandProxiedMessage
throw Errors.MessageNotFound(messageId);
}
internal async Task DeleteMessageInner(InteractionContext ctx, ulong channelId, ulong messageId, bool isDM = false)
internal async Task DeleteMessageInner(InteractionContext ctx, ulong guildId, ulong channelId, ulong messageId, bool isDM = false)
{
if (!((await _cache.BotPermissionsIn(channelId)).HasFlag(PermissionSet.ManageMessages) || isDM))
if (!((await _cache.BotPermissionsIn(guildId, channelId)).HasFlag(PermissionSet.ManageMessages) || isDM))
throw new PKError("PluralKit does not have the *Manage Messages* permission in this channel, and thus cannot delete the message."
+ " Please contact a server administrator to remedy this.");
@ -110,7 +110,7 @@ public class ApplicationCommandProxiedMessage
// (if not, PK shouldn't send messages on their behalf)
var member = await _rest.GetGuildMember(ctx.GuildId, ctx.User.Id);
var requiredPerms = PermissionSet.ViewChannel | PermissionSet.SendMessages;
if (member == null || !(await _cache.PermissionsForMemberInChannel(ctx.ChannelId, member)).HasFlag(requiredPerms))
if (member == null || !(await _cache.PermissionsForMemberInChannel(ctx.GuildId, ctx.ChannelId, member)).HasFlag(requiredPerms))
{
throw new PKError("You do not have permission to send messages in this channel.");
};

View file

@ -101,9 +101,7 @@ public class Bot
{
// we HandleGatewayEvent **before** getting the own user, because the own user is set in HandleGatewayEvent for ReadyEvent
await _cache.HandleGatewayEvent(evt);
await _cache.TryUpdateSelfMember(_config.ClientId, evt);
await OnEventReceivedInner(shardId, evt);
}
@ -175,7 +173,16 @@ public class Bot
}
using var _ = LogContext.PushProperty("EventId", Guid.NewGuid());
using var __ = LogContext.Push(await serviceScope.Resolve<SerilogGatewayEnricherFactory>().GetEnricher(shardId, evt));
// this fails when cache lookup fails, so put it in a try-catch
try
{
using var __ = LogContext.Push(await serviceScope.Resolve<SerilogGatewayEnricherFactory>().GetEnricher(shardId, evt));
}
catch (Exception exc)
{
await HandleError(handler, evt, serviceScope, exc);
}
_logger.Verbose("Received gateway event: {@Event}", evt);
try
@ -243,7 +250,7 @@ public class Bot
if (!exc.ShowToUser()) return;
// Once we've sent it to Sentry, report it to the user (if we have permission to)
var reportChannel = handler.ErrorChannelFor(evt, _config.ClientId);
var (guildId, reportChannel) = handler.ErrorChannelFor(evt, _config.ClientId);
if (reportChannel == null)
{
if (evt is InteractionCreateEvent ice && ice.Type == Interaction.InteractionType.ApplicationCommand)
@ -251,7 +258,7 @@ public class Bot
return;
}
var botPerms = await _cache.BotPermissionsIn(reportChannel.Value);
var botPerms = await _cache.BotPermissionsIn(guildId ?? 0, reportChannel.Value);
if (botPerms.HasFlag(PermissionSet.SendMessages | PermissionSet.EmbedLinks))
await _errorMessageService.SendErrorMessage(reportChannel.Value, sentryEvent.EventId.ToString());
}

View file

@ -20,7 +20,9 @@ public class BotConfig
public string? GatewayQueueUrl { get; set; }
public bool UseRedisRatelimiter { get; set; } = false;
public bool UseRedisCache { get; set; } = false;
public string? HttpCacheUrl { get; set; }
public bool HttpUseInnerCache { get; set; } = false;
public string? RedisGatewayUrl { get; set; }

View file

@ -136,4 +136,11 @@ public static class BotMetrics
DurationUnit = TimeUnit.Seconds,
Context = "Bot"
};
public static MeterOptions CacheDebug => new()
{
Name = "Bad responses to cache lookups",
Context = "Bot",
MeasurementUnit = Unit.Calls
};
}

View file

@ -82,6 +82,7 @@ public partial class CommandTree
public static Command SwitchMove = new Command("switch move", "switch move <date/time>", "Moves the latest switch in time");
public static Command SwitchEdit = new Command("switch edit", "switch edit <member> [member 2] [member 3...]", "Edits the members in the latest switch");
public static Command SwitchEditOut = new Command("switch edit out", "switch edit out", "Turns the latest switch into a switch-out");
public static Command SwitchCopy = new Command("switch copy", "switch copy <member> [member 2] [member 3...]", "Makes a new switch with the listed members added");
public static Command SwitchDelete = new Command("switch delete", "switch delete", "Deletes the latest switch");
public static Command SwitchDeleteAll = new Command("switch delete", "switch delete all", "Deletes all logged switches");
public static Command Link = new Command("link", "link <account>", "Links your system to another account");
@ -92,6 +93,7 @@ public partial class CommandTree
public static Command Export = new Command("export", "export", "Exports system information to a data file");
public static Command Help = new Command("help", "help", "Shows help information about PluralKit");
public static Command Explain = new Command("explain", "explain", "Explains the basics of systems and proxying");
public static Command Dashboard = new Command("dashboard", "dashboard", "Get a link to the PluralKit dashboard");
public static Command Message = new Command("message", "message <id|link> [delete|author]", "Looks up a proxied message");
public static Command MessageEdit = new Command("edit", "edit [link] <text>", "Edit a previously proxied message");
public static Command MessageReproxy = new Command("reproxy", "reproxy [link] <member>", "Reproxy a previously proxied message using a different member");
@ -137,7 +139,7 @@ public partial class CommandTree
public static Command[] SwitchCommands =
{
Switch, SwitchOut, SwitchMove, SwitchEdit, SwitchEditOut, SwitchDelete, SwitchDeleteAll
Switch, SwitchOut, SwitchMove, SwitchEdit, SwitchEditOut, SwitchDelete, SwitchDeleteAll, SwitchCopy
};
public static Command[] ConfigCommands =

View file

@ -18,7 +18,7 @@ public partial class CommandTree
return CommandHelpRoot(ctx);
if (ctx.Match("ap", "autoproxy", "auto"))
return HandleAutoproxyCommand(ctx);
if (ctx.Match("config", "cfg"))
if (ctx.Match("config", "cfg", "configure"))
return HandleConfigCommand(ctx);
if (ctx.Match("list", "find", "members", "search", "query", "l", "f", "fd", "ls"))
return ctx.Execute<SystemList>(SystemList, m => m.MemberList(ctx, ctx.System));
@ -105,6 +105,8 @@ public partial class CommandTree
return ctx.Execute<Random>(GroupRandom, r => r.Group(ctx, ctx.System));
else
return ctx.Execute<Random>(MemberRandom, m => m.Member(ctx, ctx.System));
if (ctx.Match("dashboard", "dash"))
return ctx.Execute<Help>(Dashboard, m => m.Dashboard(ctx));
// remove compiler warning
return ctx.Reply(
@ -430,13 +432,15 @@ public partial class CommandTree
await ctx.Execute<Switch>(SwitchEdit, m => m.SwitchEdit(ctx));
else if (ctx.Match("delete", "remove", "erase", "cancel", "yeet"))
await ctx.Execute<Switch>(SwitchDelete, m => m.SwitchDelete(ctx));
else if (ctx.Match("copy", "add", "duplicate", "dupe"))
await ctx.Execute<Switch>(SwitchCopy, m => m.SwitchEdit(ctx, true));
else if (ctx.Match("commands", "help"))
await PrintCommandList(ctx, "switching", SwitchCommands);
else if (ctx.HasNext()) // there are following arguments
await ctx.Execute<Switch>(Switch, m => m.SwitchDo(ctx));
else
await PrintCommandNotFoundError(ctx, Switch, SwitchOut, SwitchMove, SwitchEdit, SwitchEditOut,
SwitchDelete, SystemFronter, SystemFrontHistory);
SwitchDelete, SwitchCopy, SystemFronter, SystemFrontHistory);
}
private async Task CommandHelpRoot(Context ctx)
@ -544,6 +548,8 @@ public partial class CommandTree
return ctx.Execute<Config>(null, m => m.HidDisplayCaps(ctx));
if (ctx.MatchMultiple(new[] { "pad" }, new[] { "id", "ids" }) || ctx.MatchMultiple(new[] { "id" }, new[] { "pad", "padding" }) || ctx.Match("idpad", "padid", "padids"))
return ctx.Execute<Config>(null, m => m.HidListPadding(ctx));
if (ctx.MatchMultiple(new[] { "member", "group" }, new[] { "limit" }) || ctx.Match("limit"))
return ctx.Execute<Config>(null, m => m.LimitUpdate(ctx));
// todo: maybe add the list of configuration keys here?
return ctx.Reply($"{Emojis.Error} Could not find a setting with that name. Please see `pk;commands config` for the list of possible config settings.");

View file

@ -72,7 +72,7 @@ public class Context
public readonly int ShardId;
public readonly Cluster Cluster;
public Task<PermissionSet> BotPermissions => Cache.BotPermissionsIn(Channel.Id);
public Task<PermissionSet> BotPermissions => Cache.BotPermissionsIn(Guild?.Id ?? 0, Channel.Id);
public Task<PermissionSet> UserPermissions => Cache.PermissionsForMCE((MessageCreateEvent)Message);
@ -110,7 +110,7 @@ public class Context
// {
// Sensitive information that might want to be deleted by :x: reaction is typically in an embed format (member cards, for example)
// but since we can, we just store all sent messages for possible deletion
await _commandMessageService.RegisterMessage(msg.Id, msg.ChannelId, Author.Id);
await _commandMessageService.RegisterMessage(msg.Id, Guild?.Id ?? 0, msg.ChannelId, Author.Id);
// }
return msg;

View file

@ -91,8 +91,12 @@ public static class ContextArgumentsExt
public static bool MatchClear(this Context ctx)
=> ctx.Match("clear", "reset", "default") || ctx.MatchFlag("c", "clear");
public static bool MatchRaw(this Context ctx) =>
ctx.Match("r", "raw") || ctx.MatchFlag("r", "raw");
public static ReplyFormat MatchFormat(this Context ctx)
{
if (ctx.Match("r", "raw") || ctx.MatchFlag("r", "raw")) return ReplyFormat.Raw;
if (ctx.Match("pt", "plaintext") || ctx.MatchFlag("pt", "plaintext")) return ReplyFormat.Plaintext;
return ReplyFormat.Standard;
}
public static bool MatchToggle(this Context ctx, bool? defaultValue = null)
{
@ -184,4 +188,11 @@ public static class ContextArgumentsExt
return groups;
}
}
public enum ReplyFormat
{
Standard,
Raw,
Plaintext
}

View file

@ -188,7 +188,8 @@ public static class ContextEntityArgumentsExt
if (!MentionUtils.TryParseChannel(ctx.PeekArgument(), out var id))
return null;
var channel = await ctx.Cache.TryGetChannel(id);
// todo: match channels in other guilds
var channel = await ctx.Cache.TryGetChannel(ctx.Guild!.Id, id);
if (channel == null)
channel = await ctx.Rest.GetChannelOrNull(id);
if (channel == null)

View file

@ -1,8 +1,12 @@
using System.Text.RegularExpressions;
using Humanizer;
using Dapper;
using SqlKata;
using Myriad.Builders;
using Myriad.Extensions;
using Myriad.Cache;
using Myriad.Rest;
using Myriad.Types;
@ -14,11 +18,64 @@ public class Admin
{
private readonly BotConfig _botConfig;
private readonly DiscordApiClient _rest;
private readonly IDiscordCache _cache;
public Admin(BotConfig botConfig, DiscordApiClient rest)
public Admin(BotConfig botConfig, DiscordApiClient rest, IDiscordCache cache)
{
_botConfig = botConfig;
_rest = rest;
_cache = cache;
}
public async Task<Embed> CreateEmbed(Context ctx, PKSystem system)
{
string UntilLimit(int count, int limit)
{
var brackets = new List<int> { 10, 25, 50, 100 };
if (count == limit)
return "(at limit)";
foreach (var x in brackets)
{
if (limit - x <= count)
return $"(approx. {x} to limit)";
}
return "";
}
Task<(ulong Id, User? User)[]> GetUsers(IEnumerable<ulong> ids)
{
async Task<(ulong Id, User? User)> Inner(ulong id)
{
var user = await _cache.GetOrFetchUser(_rest, id);
return (id, user);
}
return Task.WhenAll(ids.Select(Inner));
}
var config = await ctx.Repository.GetSystemConfig(system.Id);
// Fetch/render info for all accounts simultaneously
var accounts = await ctx.Repository.GetSystemAccounts(system.Id);
var users = (await GetUsers(accounts)).Select(x => x.User?.NameAndMention() ?? $"(deleted: `{x.Id}`)");
var eb = new EmbedBuilder()
.Title("System info")
.Color(DiscordUtils.Green)
.Field(new Embed.Field("System ID", $"`{system.Hid}`"))
.Field(new Embed.Field("Linked accounts", string.Join("\n", users).Truncate(1000)));
var memberLimit = config.MemberLimitOverride ?? Limits.MaxMemberCount;
var memberCount = await ctx.Repository.GetSystemMemberCount(system.Id);
eb.Field(new Embed.Field("Member limit", $"{memberLimit} {UntilLimit(memberCount, memberLimit)}", true));
var groupLimit = config.GroupLimitOverride ?? Limits.MaxGroupCount;
var groupCount = await ctx.Repository.GetSystemGroupCount(system.Id);
eb.Field(new Embed.Field("Group limit", $"{groupLimit} {UntilLimit(groupCount, groupLimit)}", true));
return eb.Build();
}
public async Task UpdateSystemId(Context ctx)
@ -37,6 +94,8 @@ public class Admin
if (existingSystem != null)
throw new PKError($"Another system already exists with ID `{newHid}`.");
await ctx.Reply(null, await CreateEmbed(ctx, target));
if (!await ctx.PromptYesNo($"Change system ID of `{target.Hid}` to `{newHid}`?", "Change"))
throw new PKError("ID change cancelled.");
@ -60,6 +119,9 @@ public class Admin
if (existingMember != null)
throw new PKError($"Another member already exists with ID `{newHid}`.");
var system = await ctx.Repository.GetSystem(target.System);
await ctx.Reply(null, await CreateEmbed(ctx, system));
if (!await ctx.PromptYesNo(
$"Change member ID of **{target.NameFor(LookupContext.ByNonOwner)}** (`{target.Hid}`) to `{newHid}`?",
"Change"
@ -86,6 +148,9 @@ public class Admin
if (existingGroup != null)
throw new PKError($"Another group already exists with ID `{newHid}`.");
var system = await ctx.Repository.GetSystem(target.System);
await ctx.Reply(null, await CreateEmbed(ctx, system));
if (!await ctx.PromptYesNo($"Change group ID of **{target.Name}** (`{target.Hid}`) to `{newHid}`?",
"Change"
))
@ -103,6 +168,8 @@ public class Admin
if (target == null)
throw new PKError("Unknown system.");
await ctx.Reply(null, await CreateEmbed(ctx, target));
if (!await ctx.PromptYesNo($"Reroll system ID `{target.Hid}`?", "Reroll"))
throw new PKError("ID change cancelled.");
@ -124,6 +191,9 @@ public class Admin
if (target == null)
throw new PKError("Unknown member.");
var system = await ctx.Repository.GetSystem(target.System);
await ctx.Reply(null, await CreateEmbed(ctx, system));
if (!await ctx.PromptYesNo(
$"Reroll member ID for **{target.NameFor(LookupContext.ByNonOwner)}** (`{target.Hid}`)?",
"Reroll"
@ -148,6 +218,9 @@ public class Admin
if (target == null)
throw new PKError("Unknown group.");
var system = await ctx.Repository.GetSystem(target.System);
await ctx.Reply(null, await CreateEmbed(ctx, system));
if (!await ctx.PromptYesNo($"Reroll group ID for **{target.Name}** (`{target.Hid}`)?",
"Change"
))
@ -176,7 +249,7 @@ public class Admin
var currentLimit = config.MemberLimitOverride ?? Limits.MaxMemberCount;
if (!ctx.HasNext())
{
await ctx.Reply($"Current member limit is **{currentLimit}** members.");
await ctx.Reply(null, await CreateEmbed(ctx, target));
return;
}
@ -184,6 +257,7 @@ public class Admin
if (!int.TryParse(newLimitStr, out var newLimit))
throw new PKError($"Couldn't parse `{newLimitStr}` as number.");
await ctx.Reply(null, await CreateEmbed(ctx, target));
if (!await ctx.PromptYesNo($"Update member limit from **{currentLimit}** to **{newLimit}**?", "Update"))
throw new PKError("Member limit change cancelled.");
@ -204,7 +278,7 @@ public class Admin
var currentLimit = config.GroupLimitOverride ?? Limits.MaxGroupCount;
if (!ctx.HasNext())
{
await ctx.Reply($"Current group limit is **{currentLimit}** groups.");
await ctx.Reply(null, await CreateEmbed(ctx, target));
return;
}
@ -212,6 +286,7 @@ public class Admin
if (!int.TryParse(newLimitStr, out var newLimit))
throw new PKError($"Couldn't parse `{newLimitStr}` as number.");
await ctx.Reply(null, await CreateEmbed(ctx, target));
if (!await ctx.PromptYesNo($"Update group limit from **{currentLimit}** to **{newLimit}**?", "Update"))
throw new PKError("Group limit change cancelled.");
@ -243,6 +318,7 @@ public class Admin
throw Errors.AccountInOtherSystem(existingAccount, ctx.Config);
var system = await ctx.Repository.GetSystem(systemId.Value!);
await ctx.Reply(null, await CreateEmbed(ctx, system));
if (!await ctx.PromptYesNo($"Associate account {account.NameAndMention()} with system `{system.Hid}`?", "Recover account"))
throw new PKError("System recovery cancelled.");

View file

@ -143,6 +143,7 @@ public class Checks
var error = "Channel not found or you do not have permissions to access it.";
// todo: this breaks if channel is not in cache and bot does not have View Channel permissions
// with new cache it breaks if channel is not in current guild
var channel = await ctx.MatchChannel();
if (channel == null || channel.GuildId == null)
throw new PKError(error);
@ -156,7 +157,8 @@ public class Checks
if (!await ctx.CheckPermissionsInGuildChannel(channel, PermissionSet.ViewChannel))
throw new PKError(error);
var botPermissions = await _cache.BotPermissionsIn(channel.Id);
// todo: permcheck channel outside of guild?
var botPermissions = await _cache.BotPermissionsIn(ctx.Guild.Id, channel.Id);
// We use a bitfield so we can set individual permission bits
ulong missingPermissions = 0;
@ -231,11 +233,11 @@ public class Checks
var channel = await _rest.GetChannelOrNull(channelId.Value);
if (channel == null)
throw new PKError("Unable to get the channel associated with this message.");
var rootChannel = await _cache.GetRootChannel(channel.Id);
if (channel.GuildId == null)
throw new PKError("PluralKit is not able to proxy messages in DMs.");
var rootChannel = await _cache.GetRootChannel(channel.GuildId!.Value, channel.Id);
// using channel.GuildId here since _rest.GetMessage() doesn't return the GuildId
var context = await ctx.Repository.GetMessageContext(msg.Author.Id, channel.GuildId.Value, rootChannel.Id, msg.ChannelId);
var members = (await ctx.Repository.GetProxyMembers(msg.Author.Id, channel.GuildId.Value)).ToList();

View file

@ -536,4 +536,10 @@ public class Config
}
else throw new PKError(badInputError);
}
public Task LimitUpdate(Context ctx)
{
throw new PKError("You cannot update your own member or group limits. If you need a limit update, please join the " +
"support server and ask in #bot-support: https://discord.gg/PczBt78");
}
}

View file

@ -53,41 +53,55 @@ public class GroupMember
public async Task ListMemberGroups(Context ctx, PKMember target)
{
var pctx = ctx.DirectLookupContextFor(target.System);
var targetSystem = await ctx.Repository.GetSystem(target.System);
var opts = ctx.ParseListOptions(ctx.DirectLookupContextFor(target.System), ctx.LookupContextFor(target.System));
opts.MemberFilter = target.Id;
var groups = await ctx.Repository.GetMemberGroups(target.Id)
.Where(g => g.Visibility.CanAccess(pctx))
.OrderBy(g => (g.DisplayName ?? g.Name), StringComparer.InvariantCultureIgnoreCase)
.ToListAsync();
var description = "";
var msg = "";
if (groups.Count == 0)
description = "This member has no groups.";
else
description = string.Join("\n", groups.Select(g => $"[`{g.DisplayHid(ctx.Config, isList: true)}`] **{g.DisplayName ?? g.Name}**"));
if (pctx == LookupContext.ByOwner)
var title = new StringBuilder($"Groups containing {target.Name} (`{target.DisplayHid(ctx.Config)}`) in ");
if (ctx.Guild != null)
{
msg +=
$"\n\nTo add this member to one or more groups, use `pk;m {target.Reference(ctx)} group add <group> [group 2] [group 3...]`";
if (groups.Count > 0)
msg +=
$"\nTo remove this member from one or more groups, use `pk;m {target.Reference(ctx)} group remove <group> [group 2] [group 3...]`";
var guildSettings = await ctx.Repository.GetSystemGuild(ctx.Guild.Id, targetSystem.Id);
if (guildSettings.DisplayName != null)
title.Append($"{guildSettings.DisplayName} (`{targetSystem.DisplayHid(ctx.Config)}`)");
else if (targetSystem.NameFor(ctx) != null)
title.Append($"{targetSystem.NameFor(ctx)} (`{targetSystem.DisplayHid(ctx.Config)}`)");
else
title.Append($"`{targetSystem.DisplayHid(ctx.Config)}`");
}
else
{
if (targetSystem.NameFor(ctx) != null)
title.Append($"{targetSystem.NameFor(ctx)} (`{targetSystem.DisplayHid(ctx.Config)}`)");
else
title.Append($"`{targetSystem.DisplayHid(ctx.Config)}`");
}
if (opts.Search != null)
title.Append($" matching **{opts.Search.Truncate(100)}**");
await ctx.Reply(msg, new EmbedBuilder().Title($"{target.Name}'s groups").Description(description).Build());
await ctx.RenderGroupList(ctx.LookupContextFor(target.System), target.System, title.ToString(),
target.Color, opts);
}
public async Task AddRemoveMembers(Context ctx, PKGroup target, Groups.AddRemoveOperation op)
{
ctx.CheckOwnGroup(target);
var members = (await ctx.ParseMemberList(ctx.System.Id))
List<MemberId> members;
if (ctx.MatchFlag("all", "a"))
{
members = (await ctx.Database.Execute(conn => conn.QueryMemberList(target.System,
new DatabaseViewsExt.ListQueryOptions { })))
.Select(m => m.Id)
.Distinct()
.ToList();
}
else
{
members = (await ctx.ParseMemberList(ctx.System.Id))
.Select(m => m.Id)
.Distinct()
.ToList();
}
var existingMembersInGroup = (await ctx.Database.Execute(conn => conn.QueryMemberList(target.System,
new DatabaseViewsExt.ListQueryOptions { GroupFilter = target.Id })))
@ -127,7 +141,7 @@ public class GroupMember
var targetSystem = await GetGroupSystem(ctx, target);
ctx.CheckSystemPrivacy(targetSystem.Id, target.ListPrivacy);
var opts = ctx.ParseListOptions(ctx.DirectLookupContextFor(target.System));
var opts = ctx.ParseListOptions(ctx.DirectLookupContextFor(target.System), ctx.LookupContextFor(target.System));
opts.GroupFilter = target.Id;
var title = new StringBuilder($"Members of {target.DisplayName ?? target.Name} (`{target.DisplayHid(ctx.Config)}`) in ");

View file

@ -132,40 +132,47 @@ public class Groups
// No perms check, display name isn't covered by member privacy
if (ctx.MatchRaw())
{
var format = ctx.MatchFormat();
// if there's nothing next or what's next is "raw"/"plaintext" we're doing a query, so check for null
if (!ctx.HasNext(false) || format != ReplyFormat.Standard)
if (target.DisplayName == null)
{
await ctx.Reply(noDisplayNameSetMessage);
else
await ctx.Reply($"```\n{target.DisplayName}\n```");
return;
}
if (format == ReplyFormat.Raw)
{
await ctx.Reply($"```\n{target.DisplayName}\n```");
return;
}
if (format == ReplyFormat.Plaintext)
{
var eb = new EmbedBuilder()
.Description($"Showing displayname for group {target.Reference(ctx)}");
await ctx.Reply(target.DisplayName, embed: eb.Build());
return;
}
if (!ctx.HasNext(false))
{
if (target.DisplayName == null)
{
await ctx.Reply(noDisplayNameSetMessage);
}
else
{
var eb = new EmbedBuilder()
.Field(new Embed.Field("Name", target.Name))
.Field(new Embed.Field("Display Name", target.DisplayName));
var eb = new EmbedBuilder()
.Field(new Embed.Field("Name", target.Name))
.Field(new Embed.Field("Display Name", target.DisplayName));
var reference = target.Reference(ctx);
var reference = target.Reference(ctx);
if (ctx.System?.Id == target.System)
eb.Description(
$"To change display name, type `pk;group {reference} displayname <display name>`.\n"
+ $"To clear it, type `pk;group {reference} displayname -clear`.\n"
+ $"To print the raw display name, type `pk;group {reference} displayname -raw`.");
if (ctx.System?.Id == target.System)
eb.Description(
$"To change display name, type `pk;group {reference} displayname <display name>`.\n"
+ $"To clear it, type `pk;group {reference} displayname -clear`.\n"
+ $"To print the raw display name, type `pk;group {reference} displayname -raw`.");
if (ctx.System?.Id == target.System)
eb.Footer(new Embed.EmbedFooter($"Using {target.DisplayName.Length}/{Limits.MaxGroupNameLength} characters."));
if (ctx.System?.Id == target.System)
eb.Footer(new Embed.EmbedFooter($"Using {target.DisplayName.Length}/{Limits.MaxGroupNameLength} characters."));
await ctx.Reply(embed: eb.Build());
}
await ctx.Reply(embed: eb.Build());
return;
}
@ -184,6 +191,8 @@ public class Groups
else
{
var newDisplayName = ctx.RemainderOrNull(false).NormalizeLineEndSpacing();
if (newDisplayName.Length > Limits.MaxGroupNameLength)
throw new PKError($"Group name too long ({newDisplayName.Length}/{Limits.MaxGroupNameLength} characters).");
var patch = new GroupPatch { DisplayName = Partial<string>.Present(newDisplayName) };
await ctx.Repository.UpdateGroup(target.Id, patch);
@ -201,30 +210,41 @@ public class Groups
noDescriptionSetMessage +=
$" To set one, type `pk;group {target.Reference(ctx)} description <description>`.";
if (ctx.MatchRaw())
{
var format = ctx.MatchFormat();
// if there's nothing next or what's next is "raw"/"plaintext" we're doing a query, so check for null
if (!ctx.HasNext(false) || format != ReplyFormat.Standard)
if (target.Description == null)
{
await ctx.Reply(noDescriptionSetMessage);
else
await ctx.Reply($"```\n{target.Description}\n```");
return;
}
if (format == ReplyFormat.Raw)
{
await ctx.Reply($"```\n{target.Description}\n```");
return;
}
if (format == ReplyFormat.Plaintext)
{
var eb = new EmbedBuilder()
.Description($"Showing description for group {target.Reference(ctx)}");
await ctx.Reply(target.Description, embed: eb.Build());
return;
}
if (!ctx.HasNext(false))
{
if (target.Description == null)
await ctx.Reply(noDescriptionSetMessage);
else
await ctx.Reply(embed: new EmbedBuilder()
.Title("Group description")
.Description(target.Description)
.Field(new Embed.Field("\u200B",
$"To print the description with formatting, type `pk;group {target.Reference(ctx)} description -raw`."
+ (ctx.System?.Id == target.System
? $" To clear it, type `pk;group {target.Reference(ctx)} description -clear`."
: "")
+ $" Using {target.Description.Length}/{Limits.MaxDescriptionLength} characters."))
.Build());
await ctx.Reply(embed: new EmbedBuilder()
.Title("Group description")
.Description(target.Description)
.Field(new Embed.Field("\u200B",
$"To print the description with formatting, type `pk;group {target.Reference(ctx)} description -raw`."
+ (ctx.System?.Id == target.System
? $" To clear it, type `pk;group {target.Reference(ctx)} description -clear`."
: "")
+ $" Using {target.Description.Length}/{Limits.MaxDescriptionLength} characters."))
.Build());
return;
}
@ -385,7 +405,7 @@ public class Groups
public async Task GroupColor(Context ctx, PKGroup target)
{
var isOwnSystem = ctx.System?.Id == target.System;
var matchedRaw = ctx.MatchRaw();
var matchedFormat = ctx.MatchFormat();
var matchedClear = ctx.MatchClear();
if (!isOwnSystem || !(ctx.HasNext() || matchedClear))
@ -393,8 +413,10 @@ public class Groups
if (target.Color == null)
await ctx.Reply(
"This group does not have a color set." + (isOwnSystem ? $" To set one, type `pk;group {target.Reference(ctx)} color <color>`." : ""));
else if (matchedRaw)
else if (matchedFormat == ReplyFormat.Raw)
await ctx.Reply("```\n#" + target.Color + "\n```");
else if (matchedFormat == ReplyFormat.Plaintext)
await ctx.Reply(target.Color);
else
await ctx.Reply(embed: new EmbedBuilder()
.Title("Group color")
@ -446,7 +468,7 @@ public class Groups
// - ParseListOptions checks list access privacy and sets the privacy filter (which members show up in list)
// - RenderGroupList checks the indivual privacy for each member (NameFor, etc)
// the own system is always allowed to look up their list
var opts = ctx.ParseListOptions(ctx.DirectLookupContextFor(system.Id));
var opts = ctx.ParseListOptions(ctx.DirectLookupContextFor(system.Id), ctx.LookupContextFor(system.Id));
await ctx.RenderGroupList(
ctx.LookupContextFor(system.Id),
system.Id,
@ -460,8 +482,8 @@ public class Groups
{
var title = new StringBuilder("Groups of ");
if (target.Name != null)
title.Append($"{target.Name} (`{target.DisplayHid(ctx.Config)}`)");
if (target.NameFor(ctx) != null)
title.Append($"{target.NameFor(ctx)} (`{target.DisplayHid(ctx.Config)}`)");
else
title.Append($"`{target.DisplayHid(ctx.Config)}`");

View file

@ -17,6 +17,26 @@ public class Help
private static Dictionary<string, Embed.Field[]> helpEmbedPages = new Dictionary<string, Embed.Field[]>
{
{
"default",
new Embed.Field[]
{
new
(
"System Recovery",
"In the case of your Discord account getting lost or deleted, the PluralKit staff can help you recover your system. "
+ "In order to do so, we will need your **PluralKit token**. This is the *only* way you can prove ownership so we can help you recover your system. "
+ "To get it, run `pk;token` and then store it in a safe place.\n\n"
+ "Keep your token safe, if other people get access to it they can also use it to access your system. "
+ "If your token is ever compromised run `pk;token refresh` to invalidate the old token and get a new one."
),
new
(
"Use the buttons below to see more info!",
""
)
}
},
{
"basicinfo",
new Embed.Field[]
@ -31,7 +51,7 @@ public class Help
(
"Why are people's names saying [APP] or [BOT] next to them?",
"These people are not actually apps or bots, this is just a Discord limitation. See [the documentation](https://pluralkit.me/guide#proxying) for an in-depth explanation."
),
)
}
},
{
@ -137,7 +157,9 @@ public class Help
public Task HelpRoot(Context ctx)
=> ctx.Rest.CreateMessage(ctx.Channel.Id, new MessageRequest
{
Embeds = new[] { helpEmbed with { Description = helpEmbed.Description + "\n\n**Use the buttons below to see more info!**" } },
Content = $"{Emojis.Warn} If you cannot see the rest of this message see [the FAQ](<https://pluralkit.me/faq/#why-do-most-of-pluralkit-s-messages-look-blank-or-empty>)",
Embeds = new[] { helpEmbed with { Description = helpEmbed.Description,
Fields = helpEmbedPages.GetValueOrDefault("default") } },
Components = new[] { helpPageButtons(ctx.Author.Id) },
});
@ -151,7 +173,7 @@ public class Help
if (ctx.Event.Message.Components.First().Components.Where(x => x.CustomId == ctx.CustomId).First().Style == ButtonStyle.Primary)
return ctx.Respond(InteractionResponse.ResponseType.UpdateMessage, new()
{
Embeds = new[] { helpEmbed with { Description = helpEmbed.Description + "\n\n**Use the buttons below to see more info!**" } },
Embeds = new[] { helpEmbed with { Fields = helpEmbedPages.GetValueOrDefault("default") } },
Components = new[] { buttons }
});
@ -172,4 +194,6 @@ public class Help
});
public Task Explain(Context ctx) => ctx.Reply(explanation);
public Task Dashboard(Context ctx) => ctx.Reply("The PluralKit dashboard is at <https://dash.pluralkit.me>");
}

View file

@ -11,7 +11,7 @@ namespace PluralKit.Bot;
public static class ContextListExt
{
public static ListOptions ParseListOptions(this Context ctx, LookupContext lookupCtx)
public static ListOptions ParseListOptions(this Context ctx, LookupContext directLookupCtx, LookupContext lookupContext)
{
var p = new ListOptions();
@ -55,10 +55,13 @@ public static class ContextListExt
if (ctx.MatchFlag("private-only", "po")) p.PrivacyFilter = PrivacyLevel.Private;
// PERM CHECK: If we're trying to access non-public members of another system, error
if (p.PrivacyFilter != PrivacyLevel.Public && lookupCtx != LookupContext.ByOwner)
if (p.PrivacyFilter != PrivacyLevel.Public && directLookupCtx != LookupContext.ByOwner)
// TODO: should this just return null instead of throwing or something? >.>
throw Errors.NotOwnInfo;
//this is for searching
p.Context = lookupContext;
// Additional fields to include in the search results
if (ctx.MatchFlag("with-last-switch", "with-last-fronted", "with-last-front", "wls", "wlf"))
p.IncludeLastSwitch = true;
@ -124,11 +127,14 @@ public static class ContextListExt
void ShortRenderer(EmbedBuilder eb, IEnumerable<ListedMember> page)
{
// if there are both 5 and 6 character Hids they should be padded to align correctly.
var shouldPad = page.Any(x => x.Hid.Length > 5);
// We may end up over the description character limit
// so run it through a helper that "makes it work" :)
eb.WithSimpleLineContent(page.Select(m =>
{
var ret = $"[`{m.DisplayHid(ctx.Config, isList: true)}`] **{m.NameFor(ctx)}** ";
var ret = $"[`{m.DisplayHid(ctx.Config, isList: true, shouldPad: shouldPad)}`] **{m.NameFor(ctx)}** ";
if (opts.IncludeMessageCount && m.MessageCountFor(lookupCtx) is { } count)
ret += $"({count} messages)";
@ -234,11 +240,14 @@ public static class ContextListExt
void ShortRenderer(EmbedBuilder eb, IEnumerable<ListedGroup> page)
{
// if there are both 5 and 6 character Hids they should be padded to align correctly.
var shouldPad = page.Any(x => x.Hid.Length > 5);
// We may end up over the description character limit
// so run it through a helper that "makes it work" :)
eb.WithSimpleLineContent(page.Select(g =>
{
var ret = $"[`{g.DisplayHid(ctx.Config, isList: true)}`] **{g.NameFor(ctx)}** ";
var ret = $"[`{g.DisplayHid(ctx.Config, isList: true, shouldPad: shouldPad)}`] **{g.NameFor(ctx)}** ";
switch (opts.SortProperty)
{

View file

@ -28,7 +28,9 @@ public class ListOptions
public bool Reverse { get; set; }
public PrivacyLevel? PrivacyFilter { get; set; } = PrivacyLevel.Public;
public LookupContext Context { get; set; } = LookupContext.ByNonOwner;
public GroupId? GroupFilter { get; set; }
public MemberId? MemberFilter { get; set; }
public string? Search { get; set; }
public bool SearchDescription { get; set; }
@ -96,8 +98,10 @@ public class ListOptions
{
PrivacyFilter = PrivacyFilter,
GroupFilter = GroupFilter,
MemberFilter = MemberFilter,
Search = Search,
SearchDescription = SearchDescription
SearchDescription = SearchDescription,
Context = Context
};
}

View file

@ -70,30 +70,41 @@ public class MemberEdit
noDescriptionSetMessage +=
$" To set one, type `pk;member {target.Reference(ctx)} description <description>`.";
if (ctx.MatchRaw())
{
var format = ctx.MatchFormat();
// if there's nothing next or what's next is "raw"/"plaintext" we're doing a query, so check for null
if (!ctx.HasNext(false) || format != ReplyFormat.Standard)
if (target.Description == null)
{
await ctx.Reply(noDescriptionSetMessage);
else
await ctx.Reply($"```\n{target.Description}\n```");
return;
}
if (format == ReplyFormat.Raw)
{
await ctx.Reply($"```\n{target.Description}\n```");
return;
}
if (format == ReplyFormat.Plaintext)
{
var eb = new EmbedBuilder()
.Description($"Showing description for member {target.Reference(ctx)}");
await ctx.Reply(target.Description, embed: eb.Build());
return;
}
if (!ctx.HasNext(false))
{
if (target.Description == null)
await ctx.Reply(noDescriptionSetMessage);
else
await ctx.Reply(embed: new EmbedBuilder()
.Title("Member description")
.Description(target.Description)
.Field(new Embed.Field("\u200B",
$"To print the description with formatting, type `pk;member {target.Reference(ctx)} description -raw`."
+ (ctx.System?.Id == target.System
? $" To clear it, type `pk;member {target.Reference(ctx)} description -clear`."
: "")
+ $" Using {target.Description.Length}/{Limits.MaxDescriptionLength} characters."))
.Build());
await ctx.Reply(embed: new EmbedBuilder()
.Title("Member description")
.Description(target.Description)
.Field(new Embed.Field("\u200B",
$"To print the description with formatting, type `pk;member {target.Reference(ctx)} description -raw`."
+ (ctx.System?.Id == target.System
? $" To clear it, type `pk;member {target.Reference(ctx)} description -clear`."
: "")
+ $" Using {target.Description.Length}/{Limits.MaxDescriptionLength} characters."))
.Build());
return;
}
@ -126,26 +137,37 @@ public class MemberEdit
ctx.CheckSystemPrivacy(target.System, target.PronounPrivacy);
if (ctx.MatchRaw())
{
var format = ctx.MatchFormat();
// if there's nothing next or what's next is "raw"/"plaintext" we're doing a query, so check for null
if (!ctx.HasNext(false) || format != ReplyFormat.Standard)
if (target.Pronouns == null)
{
await ctx.Reply(noPronounsSetMessage);
else
await ctx.Reply($"```\n{target.Pronouns}\n```");
return;
}
if (format == ReplyFormat.Raw)
{
await ctx.Reply($"```\n{target.Pronouns}\n```");
return;
}
if (format == ReplyFormat.Plaintext)
{
var eb = new EmbedBuilder()
.Description($"Showing pronouns for member {target.Reference(ctx)}");
await ctx.Reply(target.Pronouns, embed: eb.Build());
return;
}
if (!ctx.HasNext(false))
{
if (target.Pronouns == null)
await ctx.Reply(noPronounsSetMessage);
else
await ctx.Reply(
$"**{target.NameFor(ctx)}**'s pronouns are **{target.Pronouns}**.\nTo print the pronouns with formatting, type `pk;member {target.Reference(ctx)} pronouns -raw`."
+ (ctx.System?.Id == target.System
? $" To clear them, type `pk;member {target.Reference(ctx)} pronouns -clear`."
: "")
+ $" Using {target.Pronouns.Length}/{Limits.MaxPronounsLength} characters.");
await ctx.Reply(
$"**{target.NameFor(ctx)}**'s pronouns are **{target.Pronouns}**.\nTo print the pronouns with formatting, type `pk;member {target.Reference(ctx)} pronouns -raw`."
+ (ctx.System?.Id == target.System
? $" To clear them, type `pk;member {target.Reference(ctx)} pronouns -clear`."
: "")
+ $" Using {target.Pronouns.Length}/{Limits.MaxPronounsLength} characters.");
return;
}
@ -232,7 +254,7 @@ public class MemberEdit
public async Task Color(Context ctx, PKMember target)
{
var isOwnSystem = ctx.System?.Id == target.System;
var matchedRaw = ctx.MatchRaw();
var matchedFormat = ctx.MatchFormat();
var matchedClear = ctx.MatchClear();
if (!isOwnSystem || !(ctx.HasNext() || matchedClear))
@ -240,8 +262,10 @@ public class MemberEdit
if (target.Color == null)
await ctx.Reply(
"This member does not have a color set." + (isOwnSystem ? $" To set one, type `pk;member {target.Reference(ctx)} color <color>`." : ""));
else if (matchedRaw)
else if (matchedFormat == ReplyFormat.Raw)
await ctx.Reply("```\n#" + target.Color + "\n```");
else if (matchedFormat == ReplyFormat.Plaintext)
await ctx.Reply(target.Color);
else
await ctx.Reply(embed: new EmbedBuilder()
.Title("Member color")
@ -388,12 +412,26 @@ public class MemberEdit
// No perms check, display name isn't covered by member privacy
if (ctx.MatchRaw())
{
var format = ctx.MatchFormat();
// if what's next is "raw"/"plaintext" we need to check for null
if (format != ReplyFormat.Standard)
if (target.DisplayName == null)
{
await ctx.Reply(noDisplayNameSetMessage);
else
await ctx.Reply($"```\n{target.DisplayName}\n```");
return;
}
if (format == ReplyFormat.Raw)
{
await ctx.Reply($"```\n{target.DisplayName}\n```");
return;
}
if (format == ReplyFormat.Plaintext)
{
var eb = new EmbedBuilder()
.Description($"Showing displayname for member {target.Reference(ctx)}");
await ctx.Reply(target.DisplayName, embed: eb.Build());
return;
}
@ -450,12 +488,26 @@ public class MemberEdit
// No perms check, display name isn't covered by member privacy
var memberGuildConfig = await ctx.Repository.GetMemberGuild(ctx.Guild.Id, target.Id);
if (ctx.MatchRaw())
{
var format = ctx.MatchFormat();
// if what's next is "raw"/"plaintext" we need to check for null
if (format != ReplyFormat.Standard)
if (memberGuildConfig.DisplayName == null)
{
await ctx.Reply(noServerNameSetMessage);
else
await ctx.Reply($"```\n{memberGuildConfig.DisplayName}\n```");
return;
}
if (format == ReplyFormat.Raw)
{
await ctx.Reply($"```\n{memberGuildConfig.DisplayName}\n```");
return;
}
if (format == ReplyFormat.Plaintext)
{
var eb = new EmbedBuilder()
.Description($"Showing servername for member {target.Reference(ctx)}");
await ctx.Reply(memberGuildConfig.DisplayName, embed: eb.Build());
return;
}

View file

@ -63,7 +63,7 @@ public class MemberProxy
if (!ctx.HasNext(false))
throw new PKSyntaxError("You must pass an example proxy to add (eg. `[text]` or `J:text`).");
var tagToAdd = ParseProxyTags(ctx.RemainderOrNull(false));
var tagToAdd = ParseProxyTags(ctx.RemainderOrNull(false).NormalizeLineEndSpacing());
if (tagToAdd.IsEmpty) throw Errors.EmptyProxyTags(target, ctx);
if (target.ProxyTags.Contains(tagToAdd))
throw Errors.ProxyTagAlreadyExists(tagToAdd, target);
@ -87,10 +87,17 @@ public class MemberProxy
if (!ctx.HasNext(false))
throw new PKSyntaxError("You must pass a proxy tag to remove (eg. `[text]` or `J:text`).");
var tagToRemove = ParseProxyTags(ctx.RemainderOrNull(false));
var remainder = ctx.RemainderOrNull(false);
var tagToRemove = ParseProxyTags(remainder.NormalizeLineEndSpacing());
if (tagToRemove.IsEmpty) throw Errors.EmptyProxyTags(target, ctx);
if (!target.ProxyTags.Contains(tagToRemove))
throw Errors.ProxyTagDoesNotExist(tagToRemove, target);
{
// Legacy support for when line endings weren't normalized
tagToRemove = ParseProxyTags(remainder);
if (!target.ProxyTags.Contains(tagToRemove))
throw Errors.ProxyTagDoesNotExist(tagToRemove, target);
}
var newTags = target.ProxyTags.ToList();
newTags.Remove(tagToRemove);
@ -102,7 +109,7 @@ public class MemberProxy
// Subcommand: bare proxy tag given
else
{
var requestedTag = ParseProxyTags(ctx.RemainderOrNull(false));
var requestedTag = ParseProxyTags(ctx.RemainderOrNull(false).NormalizeLineEndSpacing());
if (requestedTag.IsEmpty) throw Errors.EmptyProxyTags(target, ctx);
// This is mostly a legacy command, so it's gonna warn if there's

View file

@ -218,7 +218,7 @@ public class ProxiedMessage
try
{
var editedMsg =
await _webhookExecutor.EditWebhookMessage(msg.Channel, msg.Mid, newContent, clearEmbeds);
await _webhookExecutor.EditWebhookMessage(msg.Guild ?? 0, msg.Channel, msg.Mid, newContent, clearEmbeds);
if (ctx.Guild == null)
await _rest.CreateReaction(ctx.Channel.Id, ctx.Message.Id, new Emoji { Name = Emojis.Success });
@ -352,7 +352,9 @@ public class ProxiedMessage
else if (!await ctx.CheckPermissionsInGuildChannel(channel, PermissionSet.ViewChannel))
showContent = false;
if (ctx.MatchRaw())
var format = ctx.MatchFormat();
if (format != ReplyFormat.Standard)
{
var discordMessage = await _rest.GetMessageOrNull(message.Message.Channel, message.Message.Mid);
if (discordMessage == null || !showContent)
@ -365,21 +367,32 @@ public class ProxiedMessage
return;
}
await ctx.Reply($"```{content}```");
if (Regex.IsMatch(content, "```.*```", RegexOptions.Singleline))
if (format == ReplyFormat.Raw)
{
var stream = new MemoryStream(Encoding.UTF8.GetBytes(content));
await ctx.Rest.CreateMessage(
ctx.Channel.Id,
new MessageRequest
{
Content = $"{Emojis.Warn} Message contains codeblocks, raw source sent as an attachment."
},
new[] { new MultipartFile("message.txt", stream, null, null, null) });
await ctx.Reply($"```{content}```");
if (Regex.IsMatch(content, "```.*```", RegexOptions.Singleline))
{
var stream = new MemoryStream(Encoding.UTF8.GetBytes(content));
await ctx.Rest.CreateMessage(
ctx.Channel.Id,
new MessageRequest
{
Content = $"{Emojis.Warn} Message contains codeblocks, raw source sent as an attachment."
},
new[] { new MultipartFile("message.txt", stream, null, null, null) });
}
return;
}
if (format == ReplyFormat.Plaintext)
{
var eb = new EmbedBuilder()
.Description($"Showing contents of message {message.Message.Mid}");
await ctx.Reply(content, embed: eb.Build());
return;
}
return;
}
if (isDelete)
@ -387,7 +400,7 @@ public class ProxiedMessage
if (!showContent)
throw new PKError(noShowContentError);
if (message.System?.Id != ctx.System.Id && message.Message.Sender != ctx.Author.Id)
if (message.Message.Sender != ctx.Author.Id && (ctx.System != null && message.System?.Id != ctx.System.Id))
throw new PKError("You can only delete your own messages.");
await ctx.Rest.DeleteMessage(message.Message.Channel, message.Message.Mid);
@ -423,14 +436,14 @@ public class ProxiedMessage
private async Task DeleteCommandMessage(Context ctx, ulong messageId)
{
var (authorId, channelId) = await ctx.Services.Resolve<CommandMessageService>().GetCommandMessage(messageId);
if (authorId == null)
var cmessage = await ctx.Services.Resolve<CommandMessageService>().GetCommandMessage(messageId);
if (cmessage == null)
throw Errors.MessageNotFound(messageId);
if (authorId != ctx.Author.Id)
if (cmessage!.AuthorId != ctx.Author.Id)
throw new PKError("You can only delete command messages queried by this account.");
await ctx.Rest.DeleteMessage(channelId!.Value, messageId);
await ctx.Rest.DeleteMessage(cmessage.ChannelId, messageId);
if (ctx.Guild != null)
await ctx.Rest.DeleteMessage(ctx.Message);

View file

@ -67,7 +67,7 @@ public class Random
{
ctx.CheckSystemPrivacy(group.System, group.ListPrivacy);
var opts = ctx.ParseListOptions(ctx.DirectLookupContextFor(group.System));
var opts = ctx.ParseListOptions(ctx.DirectLookupContextFor(group.System), ctx.LookupContextFor(group.System));
opts.GroupFilter = group.Id;
var members = await ctx.Database.Execute(conn => conn.QueryMemberList(group.System, opts.ToQueryOptions()));

View file

@ -49,7 +49,7 @@ public class ServerConfig
if (channel.Type != Channel.ChannelType.GuildText && channel.Type != Channel.ChannelType.GuildPublicThread && channel.Type != Channel.ChannelType.GuildPrivateThread)
throw new PKError("PluralKit cannot log messages to this type of channel.");
var perms = await _cache.BotPermissionsIn(channel.Id);
var perms = await _cache.BotPermissionsIn(ctx.Guild.Id, channel.Id);
if (!perms.HasFlag(PermissionSet.SendMessages))
throw new PKError("PluralKit is missing **Send Messages** permissions in the new log channel.");
if (!perms.HasFlag(PermissionSet.EmbedLinks))
@ -104,7 +104,7 @@ public class ServerConfig
// Resolve all channels from the cache and order by position
var channels = (await Task.WhenAll(blacklist.Blacklist
.Select(id => _cache.TryGetChannel(id))))
.Select(id => _cache.TryGetChannel(ctx.Guild.Id, id))))
.Where(c => c != null)
.OrderBy(c => c.Position)
.ToList();
@ -121,7 +121,7 @@ public class ServerConfig
async (eb, l) =>
{
async Task<string> CategoryName(ulong? id) =>
id != null ? (await _cache.GetChannel(id.Value)).Name : "(no category)";
id != null ? (await _cache.GetChannel(ctx.Guild.Id, id.Value)).Name : "(no category)";
ulong? lastCategory = null;
@ -153,8 +153,9 @@ public class ServerConfig
var config = await ctx.Repository.GetGuild(ctx.Guild.Id);
// Resolve all channels from the cache and order by position
// todo: GetAllChannels?
var channels = (await Task.WhenAll(config.LogBlacklist
.Select(id => _cache.TryGetChannel(id))))
.Select(id => _cache.TryGetChannel(ctx.Guild.Id, id))))
.Where(c => c != null)
.OrderBy(c => c.Position)
.ToList();
@ -171,7 +172,7 @@ public class ServerConfig
async (eb, l) =>
{
async Task<string> CategoryName(ulong? id) =>
id != null ? (await _cache.GetChannel(id.Value)).Name : "(no category)";
id != null ? (await _cache.GetChannel(ctx.Guild.Id, id.Value)).Name : "(no category)";
ulong? lastCategory = null;
@ -204,7 +205,8 @@ public class ServerConfig
var affectedChannels = new List<Channel>();
if (ctx.Match("all"))
affectedChannels = (await _cache.GetGuildChannels(ctx.Guild.Id))
.Where(x => x.Type == Channel.ChannelType.GuildText).ToList();
// All the channel types you can proxy in
.Where(x => DiscordUtils.IsValidGuildChannel(x)).ToList();
else if (!ctx.HasNext()) throw new PKSyntaxError("You must pass one or more #channels.");
else
while (ctx.HasNext())

View file

@ -7,6 +7,7 @@ namespace PluralKit.Bot;
public class Switch
{
public async Task SwitchDo(Context ctx)
{
ctx.CheckSystem();
@ -103,12 +104,69 @@ public class Switch
await ctx.Reply($"{Emojis.Success} Switch moved to <t:{newSwitchTime}> ({newSwitchDeltaStr} ago).");
}
public async Task SwitchEdit(Context ctx)
public async Task SwitchEdit(Context ctx, bool newSwitch = false)
{
ctx.CheckSystem();
var members = await ctx.ParseMemberList(ctx.System.Id);
await DoEditCommand(ctx, members);
var newMembers = await ctx.ParseMemberList(ctx.System.Id);
await using var conn = await ctx.Database.Obtain();
var currentSwitch = await ctx.Repository.GetLatestSwitch(ctx.System.Id);
if (currentSwitch == null)
throw Errors.NoRegisteredSwitches;
var currentSwitchMembers = await ctx.Repository.GetSwitchMembers(conn, currentSwitch.Id).ToListAsync().AsTask();
if (ctx.MatchFlag("first", "f"))
newMembers = FirstInSwitch(newMembers[0], currentSwitchMembers);
else if (ctx.MatchFlag("remove", "r"))
newMembers = RemoveFromSwitch(newMembers, currentSwitchMembers);
else if (ctx.MatchFlag("append", "a"))
newMembers = AppendToSwitch(newMembers, currentSwitchMembers);
else if (ctx.MatchFlag("prepend", "p"))
newMembers = PrependToSwitch(newMembers, currentSwitchMembers);
if (newSwitch)
{
// if there's no edit flag, assume we're appending
if (!ctx.MatchFlag("first", "f", "remove", "r", "append", "a", "prepend", "p"))
newMembers = AppendToSwitch(newMembers, currentSwitchMembers);
await DoSwitchCommand(ctx, newMembers);
}
else
await DoEditCommand(ctx, newMembers);
}
public List<PKMember> PrependToSwitch(List<PKMember> members, List<PKMember> currentSwitchMembers)
{
members.AddRange(currentSwitchMembers);
return members;
}
public List<PKMember> AppendToSwitch(List<PKMember> members, List<PKMember> currentSwitchMembers)
{
currentSwitchMembers.AddRange(members);
members = currentSwitchMembers;
return members;
}
public List<PKMember> RemoveFromSwitch(List<PKMember> members, List<PKMember> currentSwitchMembers)
{
var memberIds = members.Select(m => m.Id.Value);
currentSwitchMembers = currentSwitchMembers.Where(m => !memberIds.Contains(m.Id.Value)).ToList();
members = currentSwitchMembers;
return members;
}
public List<PKMember> FirstInSwitch(PKMember member, List<PKMember> currentSwitchMembers)
{
currentSwitchMembers = currentSwitchMembers.Where(m => m.Id != member.Id).ToList();
var members = new List<PKMember> { member };
members.AddRange(currentSwitchMembers);
return members;
}
public async Task SwitchEditOut(Context ctx)

View file

@ -2,6 +2,9 @@ using PluralKit.Core;
namespace PluralKit.Bot;
using Myriad.Builders;
using Myriad.Types;
public class System
{
private readonly EmbedService _embeds;
@ -29,9 +32,25 @@ public class System
var system = await ctx.Repository.CreateSystem(systemName);
await ctx.Repository.AddAccount(system.Id, ctx.Author.Id);
// TODO: better message, perhaps embed like in groups?
await ctx.Reply(
$"{Emojis.Success} Your system has been created. Type `pk;system` to view it, and type `pk;system help` for more information about commands you can use now. Now that you have that set up, check out the getting started guide on setting up members and proxies: <https://pluralkit.me/start>");
var eb = new EmbedBuilder()
.Title(
$"{Emojis.Success} Your system has been created.")
.Field(new Embed.Field("Getting Started",
"New to PK? Check out our Getting Started guide on setting up members and proxies: https://pluralkit.me/start\n" +
"Otherwise, type `pk;system` to view your system and `pk;system help` for more information about commands you can use."))
.Field(new Embed.Field($"{Emojis.Warn} Notice {Emojis.Warn}", "PluralKit is a bot meant to help you share information about your system. " +
"Member descriptions are meant to be the equivalent to a Discord About Me. Because of this, any info you put in PK is **public by default**.\n" +
"Note that this does **not** include message content, only member fields. For more information, check out " +
"[the privacy section of the user guide](https://pluralkit.me/guide/#privacy). "))
.Field(new Embed.Field("System Recovery", "In the case of your Discord account getting lost or deleted, the PluralKit staff can help you recover your system. " +
"In order to do so, we will need your **PluralKit token**. This is the *only* way you can prove ownership so we can help you recover your system. " +
"To get it, run `pk;token` and then store it in a safe place.\n\n" +
"Keep your token safe, if other people get access to it they can also use it to access your system. " +
"If your token is ever compromised run `pk;token refresh` to invalidate the old token and get a new one."))
.Field(new Embed.Field("Questions?",
"Please join the PK server https://discord.gg/PczBt78 if you have any questions, we're happy to help"));
await ctx.Reply($"{Emojis.Warn} If you cannot see the rest of this message see [the FAQ](<https://pluralkit.me/faq/#why-do-most-of-pluralkit-s-messages-look-blank-or-empty>)", eb.Build());
}
public async Task DisplayId(Context ctx, PKSystem target)

View file

@ -37,24 +37,35 @@ public class SystemEdit
if (isOwnSystem)
noNameSetMessage += " Type `pk;system name <name>` to set one.";
if (ctx.MatchRaw())
{
if (target.Name != null)
await ctx.Reply($"```\n{target.Name}\n```");
else
var format = ctx.MatchFormat();
// if there's nothing next or what's next is "raw"/"plaintext" we're doing a query, so check for null
if (!ctx.HasNext(false) || format != ReplyFormat.Standard)
if (target.Name == null)
{
await ctx.Reply(noNameSetMessage);
return;
}
if (format == ReplyFormat.Raw)
{
await ctx.Reply($"` ``\n{target.Name}\n` ``");
return;
}
if (format == ReplyFormat.Plaintext)
{
var eb = new EmbedBuilder()
.Description($"Showing name for system {target.DisplayHid()}");
await ctx.Reply(target.Name, embed: eb.Build());
return;
}
if (!ctx.HasNext(false))
{
if (target.Name != null)
await ctx.Reply(
$"{(isOwnSystem ? "Your" : "This")} system's name is currently **{target.Name}**."
+ (isOwnSystem ? " Type `pk;system name -clear` to clear it." : "")
+ $" Using {target.Name.Length}/{Limits.MaxSystemNameLength} characters.");
else
await ctx.Reply(noNameSetMessage);
await ctx.Reply(
$"{(isOwnSystem ? "Your" : "This")} system's name is currently **{target.Name}**."
+ (isOwnSystem ? " Type `pk;system name -clear` to clear it." : "")
+ $" Using {target.Name.Length}/{Limits.MaxSystemNameLength} characters.");
return;
}
@ -91,24 +102,35 @@ public class SystemEdit
var settings = await ctx.Repository.GetSystemGuild(ctx.Guild.Id, target.Id);
if (ctx.MatchRaw())
{
if (settings.DisplayName != null)
await ctx.Reply($"```\n{settings.DisplayName}\n```");
else
var format = ctx.MatchFormat();
// if there's nothing next or what's next is "raw"/"plaintext" we're doing a query, so check for null
if (!ctx.HasNext(false) || format != ReplyFormat.Standard)
if (settings.DisplayName == null)
{
await ctx.Reply(noNameSetMessage);
return;
}
if (format == ReplyFormat.Raw)
{
await ctx.Reply($"` ``\n{settings.DisplayName}\n` ``");
return;
}
if (format == ReplyFormat.Plaintext)
{
var eb = new EmbedBuilder()
.Description($"Showing servername for system {target.DisplayHid()}");
await ctx.Reply(settings.DisplayName, embed: eb.Build());
return;
}
if (!ctx.HasNext(false))
{
if (settings.DisplayName != null)
await ctx.Reply(
$"{(isOwnSystem ? "Your" : "This")} system's name for this server is currently **{settings.DisplayName}**."
+ (isOwnSystem ? " Type `pk;system servername -clear` to clear it." : "")
+ $" Using {settings.DisplayName.Length}/{Limits.MaxSystemNameLength} characters.");
else
await ctx.Reply(noNameSetMessage);
await ctx.Reply(
$"{(isOwnSystem ? "Your" : "This")} system's name for this server is currently **{settings.DisplayName}**."
+ (isOwnSystem ? " Type `pk;system servername -clear` to clear it." : "")
+ $" Using {settings.DisplayName.Length}/{Limits.MaxSystemNameLength} characters.");
return;
}
@ -143,28 +165,39 @@ public class SystemEdit
if (isOwnSystem)
noDescriptionSetMessage += " To set one, type `pk;s description <description>`.";
if (ctx.MatchRaw())
{
var format = ctx.MatchFormat();
// if there's nothing next or what's next is "raw"/"plaintext" we're doing a query, so check for null
if (!ctx.HasNext(false) || format != ReplyFormat.Standard)
if (target.Description == null)
{
await ctx.Reply(noDescriptionSetMessage);
else
await ctx.Reply($"```\n{target.Description}\n```");
return;
}
if (format == ReplyFormat.Raw)
{
await ctx.Reply($"` ``\n{target.Description}\n` ``");
return;
}
if (format == ReplyFormat.Plaintext)
{
var eb = new EmbedBuilder()
.Description($"Showing description for system {target.DisplayHid()}");
await ctx.Reply(target.Description, embed: eb.Build());
return;
}
if (!ctx.HasNext(false))
{
if (target.Description == null)
await ctx.Reply(noDescriptionSetMessage);
else
await ctx.Reply(embed: new EmbedBuilder()
.Title("System description")
.Description(target.Description)
.Footer(new Embed.EmbedFooter(
"To print the description with formatting, type `pk;s description -raw`."
+ (isOwnSystem ? " To clear it, type `pk;s description -clear`. To change it, type `pk;s description <new description>`." : "")
+ $" Using {target.Description.Length}/{Limits.MaxDescriptionLength} characters."))
.Build());
await ctx.Reply(embed: new EmbedBuilder()
.Title("System description")
.Description(target.Description)
.Footer(new Embed.EmbedFooter(
"To print the description with formatting, type `pk;s description -raw`."
+ (isOwnSystem ? " To clear it, type `pk;s description -clear`. To change it, type `pk;s description <new description>`." : "")
+ $" Using {target.Description.Length}/{Limits.MaxDescriptionLength} characters."))
.Build());
return;
}
@ -191,7 +224,7 @@ public class SystemEdit
public async Task Color(Context ctx, PKSystem target)
{
var isOwnSystem = ctx.System?.Id == target.Id;
var matchedRaw = ctx.MatchRaw();
var matchedFormat = ctx.MatchFormat();
var matchedClear = ctx.MatchClear();
if (!isOwnSystem || !(ctx.HasNext() || matchedClear))
@ -199,8 +232,10 @@ public class SystemEdit
if (target.Color == null)
await ctx.Reply(
"This system does not have a color set." + (isOwnSystem ? " To set one, type `pk;system color <color>`." : ""));
else if (matchedRaw)
else if (matchedFormat == ReplyFormat.Raw)
await ctx.Reply("```\n#" + target.Color + "\n```");
else if (matchedFormat == ReplyFormat.Plaintext)
await ctx.Reply(target.Color);
else
await ctx.Reply(embed: new EmbedBuilder()
.Title("System color")
@ -246,22 +281,33 @@ public class SystemEdit
? "You currently have no system tag set. To set one, type `pk;s tag <tag>`."
: "This system currently has no system tag set.";
if (ctx.MatchRaw())
{
var format = ctx.MatchFormat();
// if there's nothing next or what's next is "raw"/"plaintext" we're doing a query, so check for null
if (!ctx.HasNext(false) || format != ReplyFormat.Standard)
if (target.Tag == null)
{
await ctx.Reply(noTagSetMessage);
else
await ctx.Reply($"```\n{target.Tag}\n```");
return;
}
if (format == ReplyFormat.Raw)
{
await ctx.Reply($"```\n{target.Tag}\n```");
return;
}
if (format == ReplyFormat.Plaintext)
{
var eb = new EmbedBuilder()
.Description($"Showing tag for system {target.DisplayHid()}");
await ctx.Reply(target.Tag, embed: eb.Build());
return;
}
if (!ctx.HasNext(false))
{
if (target.Tag == null)
await ctx.Reply(noTagSetMessage);
else
await ctx.Reply($"{(isOwnSystem ? "Your" : "This system's")} current system tag is {target.Tag.AsCode()}."
+ (isOwnSystem ? "To change it, type `pk;s tag <tag>`. To clear it, type `pk;s tag -clear`." : ""));
await ctx.Reply($"{(isOwnSystem ? "Your" : "This system's")} current system tag is {target.Tag.AsCode()}."
+ (isOwnSystem ? "To change it, type `pk;s tag <tag>`. To clear it, type `pk;s tag -clear`." : ""));
return;
}
@ -296,15 +342,22 @@ public class SystemEdit
var settings = await ctx.Repository.GetSystemGuild(ctx.Guild.Id, target.Id);
async Task Show(bool raw = false)
async Task Show(ReplyFormat format = ReplyFormat.Standard)
{
if (settings.Tag != null)
{
if (raw)
if (format == ReplyFormat.Raw)
{
await ctx.Reply($"```{settings.Tag}```");
return;
}
if (format == ReplyFormat.Plaintext)
{
var eb = new EmbedBuilder()
.Description($"Showing servertag for system {target.DisplayHid()}");
await ctx.Reply(settings.Tag, embed: eb.Build());
return;
}
var msg = $"Your current system tag in '{ctx.Guild.Name}' is {settings.Tag.AsCode()}";
if (!settings.TagEnabled)
@ -400,8 +453,8 @@ public class SystemEdit
await EnableDisable(false);
else if (ctx.Match("enable") || ctx.MatchFlag("enable"))
await EnableDisable(true);
else if (ctx.MatchRaw())
await Show(true);
else if (ctx.MatchFormat() != ReplyFormat.Standard)
await Show(ctx.MatchFormat());
else if (!ctx.HasNext(false))
await Show();
else
@ -418,24 +471,35 @@ public class SystemEdit
if (isOwnSystem)
noPronounsSetMessage += " To set some, type `pk;system pronouns <pronouns>`";
if (ctx.MatchRaw())
{
var format = ctx.MatchFormat();
// if there's nothing next or what's next is "raw"/"plaintext" we're doing a query, so check for null
if (!ctx.HasNext(false) || format != ReplyFormat.Standard)
if (target.Pronouns == null)
{
await ctx.Reply(noPronounsSetMessage);
else
await ctx.Reply($"```\n{target.Pronouns}\n```");
return;
}
if (format == ReplyFormat.Raw)
{
await ctx.Reply($"```\n{target.Pronouns}\n```");
return;
}
if (format == ReplyFormat.Plaintext)
{
var eb = new EmbedBuilder()
.Description($"Showing pronouns for system {target.DisplayHid()}");
await ctx.Reply(target.Pronouns, embed: eb.Build());
return;
}
if (!ctx.HasNext(false))
{
if (target.Pronouns == null)
await ctx.Reply(noPronounsSetMessage);
else
await ctx.Reply($"{(isOwnSystem ? "Your" : "This system's")} current pronouns are **{target.Pronouns}**.\nTo print the pronouns with formatting, type `pk;system pronouns -raw`."
+ (isOwnSystem ? " To clear them, type `pk;system pronouns -clear`."
: "")
+ $" Using {target.Pronouns.Length}/{Limits.MaxPronounsLength} characters.");
await ctx.Reply($"{(isOwnSystem ? "Your" : "This system's")} current pronouns are **{target.Pronouns}**.\nTo print the pronouns with formatting, type `pk;system pronouns -raw`."
+ (isOwnSystem ? " To clear them, type `pk;system pronouns -clear`."
: "")
+ $" Using {target.Pronouns.Length}/{Limits.MaxPronounsLength} characters.");
return;
}

View file

@ -17,7 +17,7 @@ public class SystemList
// - ParseListOptions checks list access privacy and sets the privacy filter (which members show up in list)
// - RenderMemberList checks the indivual privacy for each member (NameFor, etc)
// the own system is always allowed to look up their list
var opts = ctx.ParseListOptions(ctx.DirectLookupContextFor(target.Id));
var opts = ctx.ParseListOptions(ctx.DirectLookupContextFor(target.Id), ctx.LookupContextFor(target.Id));
await ctx.RenderMemberList(
ctx.LookupContextFor(target.Id),
target.Id,

View file

@ -6,5 +6,5 @@ public interface IEventHandler<in T> where T : IGatewayEvent
{
Task Handle(int shardId, T evt);
ulong? ErrorChannelFor(T evt, ulong userId) => null;
(ulong?, ulong?) ErrorChannelFor(T evt, ulong userId) => (null, null);
}

View file

@ -52,7 +52,7 @@ public class MessageCreated: IEventHandler<MessageCreateEvent>
_dmCache = dmCache;
}
public ulong? ErrorChannelFor(MessageCreateEvent evt, ulong userId) => evt.ChannelId;
public (ulong?, ulong?) ErrorChannelFor(MessageCreateEvent evt, ulong userId) => (evt.GuildId, evt.ChannelId);
private bool IsDuplicateMessage(Message msg) =>
// We consider a message duplicate if it has the same ID as the previous message that hit the gateway
_lastMessageCache.GetLastMessage(msg.ChannelId)?.Current.Id == msg.Id;
@ -63,7 +63,7 @@ public class MessageCreated: IEventHandler<MessageCreateEvent>
if (evt.Type != Message.MessageType.Default && evt.Type != Message.MessageType.Reply) return;
if (IsDuplicateMessage(evt)) return;
var botPermissions = await _cache.BotPermissionsIn(evt.ChannelId);
var botPermissions = await _cache.BotPermissionsIn(evt.GuildId ?? 0, evt.ChannelId);
if (!botPermissions.HasFlag(PermissionSet.SendMessages)) return;
// spawn off saving the private channel into another thread
@ -71,8 +71,8 @@ public class MessageCreated: IEventHandler<MessageCreateEvent>
_ = _dmCache.TrySavePrivateChannel(evt);
var guild = evt.GuildId != null ? await _cache.GetGuild(evt.GuildId.Value) : null;
var channel = await _cache.GetChannel(evt.ChannelId);
var rootChannel = await _cache.GetRootChannel(evt.ChannelId);
var channel = await _cache.GetChannel(evt.GuildId ?? 0, evt.ChannelId);
var rootChannel = await _cache.GetRootChannel(evt.GuildId ?? 0, evt.ChannelId);
// Log metrics and message info
_metrics.Measure.Meter.Mark(BotMetrics.MessagesReceived);
@ -90,7 +90,8 @@ public class MessageCreated: IEventHandler<MessageCreateEvent>
if (await TryHandleCommand(shardId, evt, guild, channel))
return;
await TryHandleProxy(evt, guild, channel, rootChannel.Id, botPermissions);
if (evt.GuildId != null)
await TryHandleProxy(evt, guild, channel, rootChannel.Id, botPermissions);
}
private async Task TryHandleLogClean(Channel channel, MessageCreateEvent evt)

View file

@ -52,10 +52,12 @@ public class MessageEdited: IEventHandler<MessageUpdateEvent>
if (!evt.Content.HasValue || !evt.Author.HasValue || !evt.Member.HasValue)
return;
var channel = await _cache.GetChannel(evt.ChannelId);
var guildIdMaybe = evt.GuildId.HasValue ? evt.GuildId.Value ?? 0 : 0;
var channel = await _cache.GetChannel(guildIdMaybe, evt.ChannelId); // todo: is this correct for message update?
if (!DiscordUtils.IsValidGuildChannel(channel))
return;
var rootChannel = await _cache.GetRootChannel(channel.Id);
var rootChannel = await _cache.GetRootChannel(guildIdMaybe, channel.Id);
var guild = await _cache.GetGuild(channel.GuildId!.Value);
var lastMessage = _lastMessageCache.GetLastMessage(evt.ChannelId)?.Current;
@ -69,7 +71,7 @@ public class MessageEdited: IEventHandler<MessageUpdateEvent>
ctx = await _repo.GetMessageContext(evt.Author.Value!.Id, channel.GuildId!.Value, rootChannel.Id, evt.ChannelId);
var equivalentEvt = await GetMessageCreateEvent(evt, lastMessage, channel);
var botPermissions = await _cache.BotPermissionsIn(channel.Id);
var botPermissions = await _cache.BotPermissionsIn(guildIdMaybe, channel.Id);
try
{
@ -91,7 +93,7 @@ public class MessageEdited: IEventHandler<MessageUpdateEvent>
private async Task<MessageCreateEvent> GetMessageCreateEvent(MessageUpdateEvent evt, CachedMessage lastMessage,
Channel channel)
{
var referencedMessage = await GetReferencedMessage(evt.ChannelId, lastMessage.ReferencedMessage);
var referencedMessage = await GetReferencedMessage(evt.GuildId.HasValue ? evt.GuildId.Value ?? 0 : 0, evt.ChannelId, lastMessage.ReferencedMessage);
var messageReference = lastMessage.ReferencedMessage != null
? new Message.Reference(channel.GuildId, evt.ChannelId, lastMessage.ReferencedMessage.Value)
@ -118,12 +120,12 @@ public class MessageEdited: IEventHandler<MessageUpdateEvent>
return equivalentEvt;
}
private async Task<Message?> GetReferencedMessage(ulong channelId, ulong? referencedMessageId)
private async Task<Message?> GetReferencedMessage(ulong guildId, ulong channelId, ulong? referencedMessageId)
{
if (referencedMessageId == null)
return null;
var botPermissions = await _cache.BotPermissionsIn(channelId);
var botPermissions = await _cache.BotPermissionsIn(guildId, channelId);
if (!botPermissions.HasFlag(PermissionSet.ReadMessageHistory))
{
_logger.Warning(

View file

@ -62,7 +62,7 @@ public class ReactionAdded: IEventHandler<MessageReactionAddEvent>
// but we aren't able to get DMs from bots anyway, so it's not really needed
if (evt.GuildId != null && (evt.Member?.User?.Bot ?? false)) return;
var channel = await _cache.GetChannel(evt.ChannelId);
var channel = await _cache.GetChannel(evt.GuildId ?? 0, evt.ChannelId);
// check if it's a command message first
// since this can happen in DMs as well
@ -75,10 +75,10 @@ public class ReactionAdded: IEventHandler<MessageReactionAddEvent>
return;
}
var (authorId, _) = await _commandMessageService.GetCommandMessage(evt.MessageId);
if (authorId != null)
var cmessage = await _commandMessageService.GetCommandMessage(evt.MessageId);
if (cmessage != null)
{
await HandleCommandDeleteReaction(evt, authorId.Value, false);
await HandleCommandDeleteReaction(evt, cmessage.AuthorId, false);
return;
}
}
@ -123,7 +123,7 @@ public class ReactionAdded: IEventHandler<MessageReactionAddEvent>
private async ValueTask HandleProxyDeleteReaction(MessageReactionAddEvent evt, PKMessage msg)
{
if (!(await _cache.BotPermissionsIn(evt.ChannelId)).HasFlag(PermissionSet.ManageMessages))
if (!(await _cache.BotPermissionsIn(evt.GuildId ?? 0, evt.ChannelId)).HasFlag(PermissionSet.ManageMessages))
return;
var isSameSystem = msg.Member != null && await _repo.IsMemberOwnedByAccount(msg.Member.Value, evt.UserId);
@ -150,7 +150,7 @@ public class ReactionAdded: IEventHandler<MessageReactionAddEvent>
if (authorId != null && authorId != evt.UserId)
return;
if (!((await _cache.BotPermissionsIn(evt.ChannelId)).HasFlag(PermissionSet.ManageMessages) || isDM))
if (!((await _cache.BotPermissionsIn(evt.GuildId ?? 0, evt.ChannelId)).HasFlag(PermissionSet.ManageMessages) || isDM))
return;
// todo: don't try to delete the user's own messages in DMs
@ -206,14 +206,14 @@ public class ReactionAdded: IEventHandler<MessageReactionAddEvent>
private async ValueTask HandlePingReaction(MessageReactionAddEvent evt, FullMessage msg)
{
if (!(await _cache.BotPermissionsIn(evt.ChannelId)).HasFlag(PermissionSet.ManageMessages))
if (!(await _cache.BotPermissionsIn(evt.GuildId ?? 0, evt.ChannelId)).HasFlag(PermissionSet.ManageMessages))
return;
// Check if the "pinger" has permission to send messages in this channel
// (if not, PK shouldn't send messages on their behalf)
var member = await _rest.GetGuildMember(evt.GuildId!.Value, evt.UserId);
var requiredPerms = PermissionSet.ViewChannel | PermissionSet.SendMessages;
if (member == null || !(await _cache.PermissionsForMemberInChannel(evt.ChannelId, member)).HasFlag(requiredPerms)) return;
if (member == null || !(await _cache.PermissionsForMemberInChannel(evt.GuildId ?? 0, evt.ChannelId, member)).HasFlag(requiredPerms)) return;
if (msg.Member == null) return;
@ -266,7 +266,7 @@ public class ReactionAdded: IEventHandler<MessageReactionAddEvent>
private async Task TryRemoveOriginalReaction(MessageReactionAddEvent evt)
{
if ((await _cache.BotPermissionsIn(evt.ChannelId)).HasFlag(PermissionSet.ManageMessages))
if ((await _cache.BotPermissionsIn(evt.GuildId ?? 0, evt.ChannelId)).HasFlag(PermissionSet.ManageMessages))
await _rest.DeleteUserReaction(evt.ChannelId, evt.MessageId, evt.Emoji, evt.UserId);
}
}

View file

@ -42,10 +42,10 @@ public class Init
using var _ = SentrySdk.Init(opts =>
{
opts.Dsn = services.Resolve<CoreConfig>().SentryUrl;
opts.Dsn = services.Resolve<CoreConfig>().SentryUrl ?? "";
opts.Release = BuildInfoService.FullVersion;
opts.AutoSessionTracking = true;
opts.DisableTaskUnobservedTaskExceptionCapture();
// opts.DisableTaskUnobservedTaskExceptionCapture();
});
var config = services.Resolve<BotConfig>();
@ -56,8 +56,6 @@ public class Init
await redis.InitAsync(coreConfig);
var cache = services.Resolve<IDiscordCache>();
if (cache is RedisDiscordCache)
await (cache as RedisDiscordCache).InitAsync(coreConfig.RedisAddr);
if (config.Cluster == null)
{

View file

@ -84,20 +84,32 @@ public class YesNoPrompt: BaseInteractive
var queue = _ctx.Services.Resolve<HandlerQueue<MessageCreateEvent>>();
var messageDispatch = queue.WaitFor(MessagePredicate, Timeout, cts.Token);
async Task WaitForMessage()
{
try
{
await queue.WaitFor(MessagePredicate, Timeout, cts.Token);
}
catch (TimeoutException e)
{
if (e.Message != "HandlerQueue#WaitFor timed out")
throw;
}
}
await Start();
cts.Token.Register(() => _tcs.TrySetException(new TimeoutException("Action timed out")));
var messageDispatch = WaitForMessage();
cts.Token.Register(() => _tcs.TrySetException(new TimeoutException("YesNoPrompt timed out")));
try
{
var doneTask = await Task.WhenAny(_tcs.Task, messageDispatch);
if (doneTask == messageDispatch)
await Finish();
}
finally
{
await Finish();
Cleanup();
}
}

View file

@ -48,8 +48,25 @@ public class BotModule: Module
{
var botConfig = c.Resolve<BotConfig>();
if (botConfig.UseRedisCache)
return new RedisDiscordCache(c.Resolve<ILogger>(), botConfig.ClientId);
if (botConfig.HttpCacheUrl != null)
{
var cache = new HttpDiscordCache(c.Resolve<ILogger>(),
c.Resolve<HttpClient>(), botConfig.HttpCacheUrl, botConfig.Cluster?.TotalShards ?? 1, botConfig.ClientId, botConfig.HttpUseInnerCache);
var metrics = c.Resolve<IMetrics>();
cache.OnDebug += (_, ev) =>
{
var (remote, key) = ev;
metrics.Measure.Meter.Mark(BotMetrics.CacheDebug, new MetricTags(
new[] { "remote", "key" },
new[] { remote.ToString(), key }
));
};
return cache;
}
return new MemoryDiscordCache(botConfig.ClientId);
}).AsSelf().SingleInstance();
builder.RegisterType<PrivateChannelService>().AsSelf().SingleInstance();

View file

@ -27,7 +27,7 @@
<PackageReference Include="Grpc.Net.ClientFactory" Version="2.32.0" />
<PackageReference Include="Grpc.Tools" Version="2.47.0" PrivateAssets="all" />
<PackageReference Include="Humanizer.Core" Version="2.8.26" />
<PackageReference Include="Sentry" Version="3.11.1" />
<PackageReference Include="Sentry" Version="4.12.1" />
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.5" />
</ItemGroup>

View file

@ -59,7 +59,7 @@ public class ProxyService
public async Task<bool> HandleIncomingMessage(MessageCreateEvent message, MessageContext ctx,
Guild guild, Channel channel, bool allowAutoproxy, PermissionSet botPermissions)
{
var rootChannel = await _cache.GetRootChannel(message.ChannelId);
var rootChannel = await _cache.GetRootChannel(message.GuildId!.Value, message.ChannelId);
if (!ShouldProxy(channel, rootChannel, message, ctx))
return false;
@ -111,31 +111,10 @@ public class ProxyService
return true;
}
#pragma warning disable CA1822 // Mark members as static
internal bool CanProxyInChannel(Channel ch, bool isRootChannel = false)
#pragma warning restore CA1822 // Mark members as static
{
// this is explicitly selecting known channel types so that when Discord add new
// ones, users don't get flooded with error codes if that new channel type doesn't
// support a feature we need for proxying
return ch.Type switch
{
Channel.ChannelType.GuildText => true,
Channel.ChannelType.GuildPublicThread => true,
Channel.ChannelType.GuildPrivateThread => true,
Channel.ChannelType.GuildNews => true,
Channel.ChannelType.GuildNewsThread => true,
Channel.ChannelType.GuildVoice => true,
Channel.ChannelType.GuildStageVoice => true,
Channel.ChannelType.GuildForum => isRootChannel,
Channel.ChannelType.GuildMedia => isRootChannel,
_ => false,
};
}
// Proxy checks that give user errors
public async Task<string> CanProxy(Channel channel, Channel rootChannel, Message msg, MessageContext ctx)
{
if (!(CanProxyInChannel(channel) && CanProxyInChannel(rootChannel, true)))
if (!DiscordUtils.IsValidGuildChannel(channel))
return $"PluralKit cannot proxy messages in this type of channel.";
// Check if the message does not go over any Discord Nitro limits
@ -159,6 +138,7 @@ public class ProxyService
return null;
}
// Proxy checks that don't give user errors unless `pk;debug proxy` is used
public bool ShouldProxy(Channel channel, Channel rootChannel, Message msg, MessageContext ctx)
{
// Make sure author has a system
@ -189,9 +169,9 @@ public class ProxyService
throw new ProxyChecksFailedException(
"Your system has proxying disabled in this server. Type `pk;proxy on` to enable it.");
// Make sure we have either an attachment or message content
// Make sure we have an attachment, message content, or poll
var isMessageBlank = msg.Content == null || msg.Content.Trim().Length == 0;
if (isMessageBlank && msg.Attachments.Length == 0)
if (isMessageBlank && msg.Attachments.Length == 0 && msg.Poll == null)
throw new ProxyChecksFailedException("Message cannot be blank.");
if (msg.Activity != null)
@ -227,8 +207,8 @@ public class ProxyService
var content = match.ProxyContent;
if (!allowEmbeds) content = content.BreakLinkEmbeds();
var messageChannel = await _cache.GetChannel(trigger.ChannelId);
var rootChannel = await _cache.GetRootChannel(trigger.ChannelId);
var messageChannel = await _cache.GetChannel(trigger.GuildId!.Value, trigger.ChannelId);
var rootChannel = await _cache.GetRootChannel(trigger.GuildId!.Value, trigger.ChannelId);
var threadId = messageChannel.IsThread() ? messageChannel.Id : (ulong?)null;
var guild = await _cache.GetGuild(trigger.GuildId.Value);
var guildMember = await _rest.GetGuildMember(trigger.GuildId!.Value, trigger.Author.Id);
@ -242,6 +222,7 @@ public class ProxyService
GuildId = trigger.GuildId!.Value,
ChannelId = rootChannel.Id,
ThreadId = threadId,
MessageId = trigger.Id,
Name = await FixSameName(messageChannel.Id, ctx, match.Member),
AvatarUrl = AvatarUtils.TryRewriteCdnUrl(match.Member.ProxyAvatar(ctx)),
Content = content,
@ -252,6 +233,7 @@ public class ProxyService
AllowEveryone = allowEveryone,
Flags = trigger.Flags.HasFlag(Message.MessageFlags.VoiceMessage) ? Message.MessageFlags.VoiceMessage : null,
Tts = tts,
Poll = trigger.Poll,
});
await HandleProxyExecutedActions(ctx, autoproxySettings, trigger, proxyMessage, match);
}
@ -310,6 +292,7 @@ public class ProxyService
GuildId = guild.Id,
ChannelId = rootChannel.Id,
ThreadId = threadId,
MessageId = originalMsg.Id,
Name = match.Member.ProxyName(ctx),
AvatarUrl = AvatarUtils.TryRewriteCdnUrl(match.Member.ProxyAvatar(ctx)),
Content = match.ProxyContent!,
@ -320,6 +303,7 @@ public class ProxyService
AllowEveryone = allowEveryone,
Flags = originalMsg.Flags.HasFlag(Message.MessageFlags.VoiceMessage) ? Message.MessageFlags.VoiceMessage : null,
Tts = tts,
Poll = originalMsg.Poll,
});

View file

@ -9,22 +9,35 @@ public class AvatarHostingService
private readonly BotConfig _config;
private readonly HttpClient _client;
public AvatarHostingService(BotConfig config, HttpClient client)
public AvatarHostingService(BotConfig config)
{
_config = config;
_client = client;
_client = new HttpClient
{
Timeout = TimeSpan.FromSeconds(10),
};
}
public async Task<ParsedImage> TryRehostImage(ParsedImage input, RehostedImageType type, ulong userId, PKSystem? system)
{
var uploaded = await TryUploadAvatar(input.Url, type, userId, system);
if (uploaded != null)
try
{
// todo: make new image type called Cdn?
return new ParsedImage { Url = uploaded, Source = AvatarSource.HostedCdn };
}
var uploaded = await TryUploadAvatar(input.Url, type, userId, system);
if (uploaded != null)
{
// todo: make new image type called Cdn?
return new ParsedImage { Url = uploaded, Source = AvatarSource.HostedCdn };
}
return input;
return input;
}
catch (TaskCanceledException e)
{
// don't show an internal error to users
if (e.Message.Contains("HttpClient.Timeout"))
throw new PKError("Temporary error setting image, please try again later");
throw;
}
}
public async Task<string?> TryUploadAvatar(string? avatarUrl, RehostedImageType type, ulong userId, PKSystem? system)

View file

@ -18,7 +18,7 @@ public class CommandMessageService
_logger = logger.ForContext<CommandMessageService>();
}
public async Task RegisterMessage(ulong messageId, ulong channelId, ulong authorId)
public async Task RegisterMessage(ulong messageId, ulong guildId, ulong channelId, ulong authorId)
{
if (_redis.Connection == null) return;
@ -27,17 +27,19 @@ public class CommandMessageService
messageId, authorId, channelId
);
await _redis.Connection.GetDatabase().StringSetAsync(messageId.ToString(), $"{authorId}-{channelId}", expiry: CommandMessageRetention);
await _redis.Connection.GetDatabase().StringSetAsync(messageId.ToString(), $"{authorId}-{channelId}-{guildId}", expiry: CommandMessageRetention);
}
public async Task<(ulong?, ulong?)> GetCommandMessage(ulong messageId)
public async Task<CommandMessage?> GetCommandMessage(ulong messageId)
{
var str = await _redis.Connection.GetDatabase().StringGetAsync(messageId.ToString());
if (str.HasValue)
{
var split = ((string)str).Split("-");
return (ulong.Parse(split[0]), ulong.Parse(split[1]));
return new CommandMessage(ulong.Parse(split[0]), ulong.Parse(split[1]), ulong.Parse(split[2]));
}
return (null, null);
return null;
}
}
}
public record CommandMessage(ulong AuthorId, ulong ChannelId, ulong GuildId);

View file

@ -263,7 +263,7 @@ public class EmbedService
else if (system.NameFor(ctx) != null)
nameField = $"{nameField} ({system.NameFor(ctx)})";
else
nameField = $"{nameField} ({system.Name})";
nameField = $"{nameField}";
var eb = new EmbedBuilder()
.Author(new Embed.EmbedAuthor(nameField, IconUrl: target.IconFor(pctx), Url: $"https://dash.pluralkit.me/profile/g/{target.Hid}"))
@ -336,7 +336,7 @@ public class EmbedService
public async Task<Embed> CreateMessageInfoEmbed(FullMessage msg, bool showContent, SystemConfig? ccfg = null)
{
var channel = await _cache.GetOrFetchChannel(_rest, msg.Message.Channel);
var channel = await _cache.GetOrFetchChannel(_rest, msg.Message.Guild ?? 0, msg.Message.Channel);
var ctx = LookupContext.ByNonOwner;
var serverMsg = await _rest.GetMessageOrNull(msg.Message.Channel, msg.Message.Mid);
@ -403,14 +403,15 @@ public class EmbedService
var roles = memberInfo?.Roles?.ToList();
if (roles != null && roles.Count > 0 && showContent)
{
var rolesString = string.Join(", ", (await Task.WhenAll(roles
.Select(async id =>
var guild = await _cache.GetGuild(channel.GuildId!.Value);
var rolesString = string.Join(", ", (roles
.Select(id =>
{
var role = await _cache.TryGetRole(id);
var role = Array.Find(guild.Roles, r => r.Id == id);
if (role != null)
return role;
return new Role { Name = "*(unknown role)*", Position = 0 };
})))
}))
.OrderByDescending(role => role.Position)
.Select(role => role.Name));
eb.Field(new Embed.Field($"Account roles ({roles.Count})", rolesString.Truncate(1024)));

View file

@ -42,7 +42,7 @@ public class LogChannelService
if (logChannelId == null)
return;
var triggerChannel = await _cache.GetChannel(proxiedMessage.Channel);
var triggerChannel = await _cache.GetChannel(proxiedMessage.Guild!.Value, proxiedMessage.Channel);
var member = await _repo.GetMember(proxiedMessage.Member!.Value);
var system = await _repo.GetSystem(member.System);
@ -63,7 +63,7 @@ public class LogChannelService
return null;
var guildId = proxiedMessage.Guild ?? trigger.GuildId.Value;
var rootChannel = await _cache.GetRootChannel(trigger.ChannelId);
var rootChannel = await _cache.GetRootChannel(guildId, trigger.ChannelId);
// get log channel info from the database
var guild = await _repo.GetGuild(guildId);
@ -109,7 +109,7 @@ public class LogChannelService
private async Task<Channel?> FindLogChannel(ulong guildId, ulong channelId)
{
// TODO: fetch it directly on cache miss?
if (await _cache.TryGetChannel(channelId) is Channel channel)
if (await _cache.TryGetChannel(guildId, channelId) is Channel channel)
return channel;
if (await _rest.GetChannelOrNull(channelId) is Channel restChannel)

View file

@ -23,6 +23,7 @@ public class LoggerCleanService
private static readonly Regex _basicRegex = new("(\\d{17,19})");
private static readonly Regex _dynoRegex = new("Message ID: (\\d{17,19})");
private static readonly Regex _carlRegex = new("Message ID: (\\d{17,19})");
private static readonly Regex _makiRegex = new("Message ID: (\\d{17,19})");
private static readonly Regex _circleRegex = new("\\(`(\\d{17,19})`\\)");
private static readonly Regex _loggerARegex = new("Message = (\\d{17,19})");
private static readonly Regex _loggerBRegex = new("MessageID:(\\d{17,19})");
@ -60,6 +61,7 @@ public class LoggerCleanService
new LoggerBot("Dyno#8389", 470724017205149701, ExtractDyno), // webhook
new LoggerBot("Dyno#5714", 470723870270160917, ExtractDyno), // webhook
new LoggerBot("Dyno#1961", 347378323418251264, ExtractDyno), // webhook
new LoggerBot("Maki", 563434444321587202, ExtractMaki), // webhook
new LoggerBot("Auttaja", 242730576195354624, ExtractAuttaja), // webhook
new LoggerBot("GenericBot", 295329346590343168, ExtractGenericBot),
new LoggerBot("blargbot", 134133271750639616, ExtractBlargBot),
@ -98,10 +100,10 @@ public class LoggerCleanService
public async ValueTask HandleLoggerBotCleanup(Message msg)
{
var channel = await _cache.GetChannel(msg.ChannelId);
var channel = await _cache.GetChannel(msg.GuildId!.Value, msg.ChannelId!);
if (channel.Type != Channel.ChannelType.GuildText) return;
if (!(await _cache.BotPermissionsIn(channel.Id)).HasFlag(PermissionSet.ManageMessages)) return;
if (!(await _cache.BotPermissionsIn(msg.GuildId!.Value, channel.Id)).HasFlag(PermissionSet.ManageMessages)) return;
// If this message is from a *webhook*, check if the application ID matches one of the bots we know
// If it's from a *bot*, check the bot ID to see if we know it.
@ -231,6 +233,15 @@ public class LoggerCleanService
return match.Success ? ulong.Parse(match.Groups[1].Value) : null;
}
private static ulong? ExtractMaki(Message msg)
{
// Embed, Message Author Name field: "Message Deleted", footer is "Message ID: [id]"
var embed = msg.Embeds?.FirstOrDefault();
if (embed.Author?.Name == null || embed?.Footer == null || !embed.Author.Name.StartsWith("Message Deleted")) return null;
var match = _makiRegex.Match(embed.Footer.Text ?? "");
return match.Success ? ulong.Parse(match.Groups[1].Value) : null;
}
private static FuzzyExtractResult? ExtractCircle(Message msg)
{
// Like Auttaja, Circle has both embed and compact modes, but the regex works for both.

View file

@ -54,33 +54,6 @@ public class PeriodicStatCollector
var stopwatch = new Stopwatch();
stopwatch.Start();
// Aggregate guild/channel stats
var guildCount = 0;
var channelCount = 0;
// No LINQ today, sorry
await foreach (var guild in _cache.GetAllGuilds())
{
guildCount++;
foreach (var channel in await _cache.GetGuildChannels(guild.Id))
if (DiscordUtils.IsValidGuildChannel(channel))
channelCount++;
}
if (_config.UseRedisMetrics)
{
var db = _redis.Connection.GetDatabase();
await db.HashSetAsync("pluralkit:cluster_stats", new StackExchange.Redis.HashEntry[] {
new(_botConfig.Cluster.NodeIndex, JsonConvert.SerializeObject(new ClusterMetricInfo
{
GuildCount = guildCount,
ChannelCount = channelCount,
DatabaseConnectionCount = _countHolder.ConnectionCount,
WebhookCacheSize = _webhookCache.CacheSize,
})),
});
}
// Process info
var process = Process.GetCurrentProcess();
_metrics.Measure.Gauge.SetValue(CoreMetrics.ProcessPhysicalMemory, process.WorkingSet64);

View file

@ -4,6 +4,8 @@ using App.Metrics;
using Humanizer;
using NodaTime.Text;
using Myriad.Cache;
using Myriad.Extensions;
using Myriad.Rest;
@ -35,6 +37,7 @@ public record ProxyRequest
public ulong GuildId { get; init; }
public ulong ChannelId { get; init; }
public ulong? ThreadId { get; init; }
public ulong MessageId { get; init; }
public string Name { get; init; }
public string? AvatarUrl { get; init; }
public string? Content { get; init; }
@ -45,6 +48,7 @@ public record ProxyRequest
public bool AllowEveryone { get; init; }
public Message.MessageFlags? Flags { get; init; }
public bool Tts { get; init; }
public Message.MessagePoll? Poll { get; init; }
}
public class WebhookExecutorService
@ -83,7 +87,7 @@ public class WebhookExecutorService
return webhookMessage;
}
public async Task<Message> EditWebhookMessage(ulong channelId, ulong messageId, string newContent, bool clearEmbeds = false)
public async Task<Message> EditWebhookMessage(ulong guildId, ulong channelId, ulong messageId, string newContent, bool clearEmbeds = false)
{
var allowedMentions = newContent.ParseMentions() with
{
@ -92,7 +96,7 @@ public class WebhookExecutorService
};
ulong? threadId = null;
var channel = await _cache.GetOrFetchChannel(_rest, channelId);
var channel = await _cache.GetOrFetchChannel(_rest, guildId, channelId);
if (channel.IsThread())
{
threadId = channelId;
@ -154,6 +158,26 @@ public class WebhookExecutorService
}).ToArray();
}
if (req.Poll is Message.MessagePoll poll)
{
int? duration = null;
if (poll.Expiry is string expiry)
{
var then = OffsetDateTimePattern.ExtendedIso.Parse(expiry).Value.ToInstant();
var now = DiscordUtils.SnowflakeToInstant(req.MessageId);
// in theory .TotalHours should be exact, but just in case
duration = (int)Math.Round((then - now).TotalMinutes / 60.0);
}
webhookReq.Poll = new ExecuteWebhookRequest.WebhookPoll
{
Question = poll.Question,
Answers = poll.Answers,
Duration = duration,
AllowMultiselect = poll.AllowMultiselect,
LayoutType = poll.LayoutType
};
}
Message webhookMessage;
using (_metrics.Measure.Timer.Time(BotMetrics.WebhookResponseTime))
{

View file

@ -49,17 +49,17 @@ public static class MiscUtils
if (e is WebhookExecutionErrorOnDiscordsEnd) return false;
// Socket errors are *not our problem*
if (e.GetBaseException() is SocketException) return false;
// if (e.GetBaseException() is SocketException) return false;
// Tasks being cancelled for whatver reason are, you guessed it, also not our problem.
if (e is TaskCanceledException) return false;
// if (e is TaskCanceledException) return false;
// Sometimes Discord just times everything out.
if (e is TimeoutException) return false;
// if (e is TimeoutException) return false;
if (e is UnknownDiscordRequestException tde && tde.Message == "Request Timeout") return false;
// HTTP/2 streams are complicated and break sometimes.
if (e is HttpRequestException) return false;
// if (e is HttpRequestException) return false;
// This may expanded at some point.
return true;

View file

@ -29,14 +29,14 @@ public static class ModelUtils
public static string DisplayHid(this PKSystem system, SystemConfig? cfg = null, bool isList = false) => HidTransform(system.Hid, cfg, isList);
public static string DisplayHid(this PKGroup group, SystemConfig? cfg = null, bool isList = false) => HidTransform(group.Hid, cfg, isList);
public static string DisplayHid(this PKMember member, SystemConfig? cfg = null, bool isList = false) => HidTransform(member.Hid, cfg, isList);
private static string HidTransform(string hid, SystemConfig? cfg = null, bool isList = false) =>
public static string DisplayHid(this PKGroup group, SystemConfig? cfg = null, bool isList = false, bool shouldPad = true) => HidTransform(group.Hid, cfg, isList, shouldPad);
public static string DisplayHid(this PKMember member, SystemConfig? cfg = null, bool isList = false, bool shouldPad = true) => HidTransform(member.Hid, cfg, isList, shouldPad);
private static string HidTransform(string hid, SystemConfig? cfg = null, bool isList = false, bool shouldPad = true) =>
HidUtils.HidTransform(
hid,
cfg != null && cfg.HidDisplaySplit,
cfg != null && cfg.HidDisplayCaps,
isList ? (cfg?.HidListPadding ?? SystemConfig.HidPadFormat.None) : SystemConfig.HidPadFormat.None // padding only on lists
isList && shouldPad ? (cfg?.HidListPadding ?? SystemConfig.HidPadFormat.None) : SystemConfig.HidPadFormat.None // padding only on lists
);
private static string EntityReference(string hid, string name)

View file

@ -38,9 +38,11 @@ public class SerilogGatewayEnricherFactory
{
props.Add(new LogEventProperty("ChannelId", new ScalarValue(channel.Value)));
if (await _cache.TryGetChannel(channel.Value) != null)
var guildIdForCache = guild != null ? guild.Value : 0;
if (await _cache.TryGetChannel(guildIdForCache, channel.Value) != null)
{
var botPermissions = await _cache.BotPermissionsIn(channel.Value);
var botPermissions = await _cache.BotPermissionsIn(guildIdForCache, channel.Value);
props.Add(new LogEventProperty("BotPermissions", new ScalarValue(botPermissions)));
}
}

View file

@ -36,9 +36,9 @@
},
"Sentry": {
"type": "Direct",
"requested": "[3.11.1, )",
"resolved": "3.11.1",
"contentHash": "T/NLfs6MMkUSYsPEDajB9ad0124T18I0uUod5MNOev3iwjvcnIEQBrStEX2olbIxzqfvGXzQ/QFqTfA2ElLPlA=="
"requested": "[4.12.1, )",
"resolved": "4.12.1",
"contentHash": "OLf7885OKHWLaTLTyw884mwOT4XKCWj2Hz5Wuz/TJemJqXwCIdIljkJBIoeHviRUPvtB7ulDgeYXf/Z7ScToSA=="
},
"SixLabors.ImageSharp": {
"type": "Direct",