Merge remote-tracking branch 'upstream/main' into rust-command-parser

This commit is contained in:
dusk 2025-09-26 15:16:54 +00:00
commit b353dcbda2
No known key found for this signature in database
94 changed files with 2575 additions and 738 deletions

View file

@ -139,7 +139,8 @@ public class ApplicationCommandProxiedMessage
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.");
};
}
;
var config = await _repo.GetSystemConfig(msg.System.Id);

View file

@ -325,6 +325,8 @@ public partial class CommandTree
await ctx.Execute<Admin>(Admin, a => a.SystemRecover(ctx));
else if (ctx.Match("sd", "systemdelete"))
await ctx.Execute<Admin>(Admin, a => a.SystemDelete(ctx));
else if (ctx.Match("sendmsg", "sendmessage"))
await ctx.Execute<Admin>(Admin, a => a.SendAdminMessage(ctx));
else if (ctx.Match("al", "abuselog"))
await HandleAdminAbuseLogCommand(ctx);
else
@ -623,6 +625,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[] { "show" }, new[] { "color", "colour", "colors", "colours" }) || ctx.Match("showcolor", "showcolour", "showcolors", "showcolours", "colorcode", "colorhex"))
return ctx.Execute<Config>(null, m => m.CardShowColorHex(ctx));
if (ctx.MatchMultiple(new[] { "name" }, new[] { "format" }) || ctx.Match("nameformat", "nf"))
return ctx.Execute<Config>(null, m => m.NameFormat(ctx));
if (ctx.MatchMultiple(new[] { "member", "group" }, new[] { "limit" }) || ctx.Match("limit"))

View file

@ -117,6 +117,41 @@ public class Context
return msg;
}
public async Task<Message> Reply(MessageComponent[] components = null, AllowedMentions? mentions = null, MultipartFile[]? files = null)
{
var botPerms = await BotPermissions;
if (!botPerms.HasFlag(PermissionSet.SendMessages))
// Will be "swallowed" during the error handler anyway, this message is never shown.
throw new PKError("PluralKit does not have permission to send messages in this channel.");
if (files != null && !botPerms.HasFlag(PermissionSet.AttachFiles))
throw new PKError("PluralKit does not have permission to attach files in this channel. Please ensure I have the **Attach Files** permission enabled.");
var msg = await Rest.CreateMessage(Channel.Id, new MessageRequest
{
Components = components,
Flags = Message.MessageFlags.IsComponentsV2,
// Default to an empty allowed mentions object instead of null (which means no mentions allowed)
AllowedMentions = mentions ?? new AllowedMentions()
}, files: files);
// store log of sent message, so it can be queried or deleted later
// skip DMs as DM messages can always be deleted
if (Guild != null)
await Repository.AddCommandMessage(new Core.CommandMessage
{
Mid = msg.Id,
Guild = Guild!.Id,
Channel = Channel.Id,
Sender = Author.Id,
OriginalMid = Message.Id,
});
return msg;
}
public async Task Execute<T>(Command? commandDef, Func<T, Task> handler, bool deprecated = false)
{
if (deprecated && commandDef != null)

View file

@ -9,6 +9,8 @@ using Myriad.Extensions;
using Myriad.Cache;
using Myriad.Rest;
using Myriad.Types;
using Myriad.Rest.Types.Requests;
using Myriad.Rest.Exceptions;
using PluralKit.Core;
@ -19,12 +21,14 @@ public class Admin
private readonly BotConfig _botConfig;
private readonly DiscordApiClient _rest;
private readonly IDiscordCache _cache;
private readonly PrivateChannelService _dmCache;
public Admin(BotConfig botConfig, DiscordApiClient rest, IDiscordCache cache)
public Admin(BotConfig botConfig, DiscordApiClient rest, IDiscordCache cache, PrivateChannelService dmCache)
{
_botConfig = botConfig;
_rest = rest;
_cache = cache;
_dmCache = dmCache;
}
private Task<(ulong Id, User? User)[]> GetUsers(IEnumerable<ulong> ids)
@ -496,4 +500,34 @@ public class Admin
await ctx.Repository.DeleteAbuseLog(abuseLog.Id);
await ctx.Reply($"{Emojis.Success} Successfully deleted abuse log entry.");
}
public async Task SendAdminMessage(Context ctx)
{
ctx.AssertBotAdmin();
var account = await ctx.MatchUser();
if (account == null)
throw new PKError("You must pass an account to send an admin message to (either ID or @mention).");
if (!ctx.HasNext())
throw new PKError("You must provide a message to send.");
var content = ctx.RemainderOrNull(false).NormalizeLineEndSpacing();
var messageContent = $"## [Admin Message]\n\n{content}\n\nWe cannot read replies sent to this DM. If you wish to contact the staff team, please join the support server (<https://discord.gg/PczBt78>) or send us an email at <legal@pluralkit.me>.";
try
{
var dm = await _dmCache.GetOrCreateDmChannel(account.Id);
var msg = await ctx.Rest.CreateMessage(dm,
new MessageRequest { Content = messageContent }
);
}
catch (ForbiddenException)
{
await ctx.Reply(
$"{Emojis.Error} Error while sending DM.");
return;
}
await ctx.Reply($"{Emojis.Success} Successfully sent message.");
}
}

View file

@ -123,6 +123,13 @@ public class Config
"off"
));
items.Add(new(
"show color",
"Whether to show color codes in system/member/group cards",
EnabledDisabled(ctx.Config.CardShowColorHex),
"disabled"
));
items.Add(new(
"Proxy Switch",
"Switching behavior when proxy tags are used",
@ -575,6 +582,20 @@ public class Config
else throw new PKError(badInputError);
}
public async Task CardShowColorHex(Context ctx)
{
if (!ctx.HasNext())
{
var msg = $"Showing color codes on system/member/group cards is currently **{EnabledDisabled(ctx.Config.CardShowColorHex)}**.";
await ctx.Reply(msg);
return;
}
var newVal = ctx.MatchToggle(false);
await ctx.Repository.UpdateSystemConfig(ctx.System.Id, new() { CardShowColorHex = newVal });
await ctx.Reply($"Showing color codes on system/member/group cards is now {EnabledDisabled(newVal)}.");
}
public async Task ProxySwitch(Context ctx)
{
if (!ctx.HasNext())

View file

@ -57,7 +57,7 @@ public class GroupMember
var opts = ctx.ParseListOptions(ctx.DirectLookupContextFor(target.System), ctx.LookupContextFor(target.System));
opts.MemberFilter = target.Id;
var title = new StringBuilder($"Groups containing {target.Name} (`{target.DisplayHid(ctx.Config)}`) in ");
var title = new StringBuilder($"Groups containing {target.NameFor(ctx)} (`{target.DisplayHid(ctx.Config)}`) in ");
if (ctx.Guild != null)
{
var guildSettings = await ctx.Repository.GetSystemGuild(ctx.Guild.Id, targetSystem.Id);

View file

@ -520,7 +520,13 @@ public class Groups
public async Task ShowGroupCard(Context ctx, PKGroup target)
{
var system = await GetGroupSystem(ctx, target);
await ctx.Reply(embed: await _embeds.CreateGroupEmbed(ctx, system, target));
if (ctx.MatchFlag("show-embed", "se"))
{
await ctx.Reply(text: EmbedService.LEGACY_EMBED_WARNING, embed: await _embeds.CreateGroupEmbed(ctx, system, target));
return;
}
await ctx.Reply(components: await _embeds.CreateGroupMessageComponents(ctx, system, target));
}
public async Task GroupPrivacy(Context ctx, PKGroup target, PrivacyLevel? newValueFromCommand)

View file

@ -7,12 +7,94 @@ namespace PluralKit.Bot;
public class Help
{
public Task HelpRoot(Context ctx)
{
if (ctx.MatchFlag("show-embed", "se"))
return HelpRootOld(ctx);
return ctx.Reply(BuildComponents(ctx.Author.Id, Help.Description.Replace("{prefix}", ctx.DefaultPrefix), -1));
}
public static Task ButtonClick(InteractionContext ctx, string prefix)
{
if (!ctx.CustomId.Contains(ctx.User.Id.ToString()))
return ctx.Ignore();
if (ctx.CustomId.StartsWith("new-"))
{
Console.WriteLine($"{ctx.Event.Message.Components.First().Components.Length}");
if (ctx.Event.Message.Components.First().Components[1].Components.Where(x => x.CustomId == ctx.CustomId).First().Style == ButtonStyle.Primary)
return ctx.Respond(InteractionResponse.ResponseType.UpdateMessage, new()
{
Components = BuildComponents(ctx.User.Id, Help.Description.Replace("{prefix}", prefix), -1),
Flags = Message.MessageFlags.IsComponentsV2,
});
var text = helpEmbedPages.GetValueOrDefault(ctx.CustomId.Split("-")[3]).Select(
(item, index) => $"### {item.Name.Replace("{prefix}", prefix)}\n{item.Value.Replace("{prefix}", prefix)}"
).ToArray();
var index = Array.FindIndex(ctx.Event.Message.Components.First().Components[1].Components, x => x.CustomId == ctx.CustomId);
var components = BuildComponents(ctx.User.Id, Help.Description.Replace("{prefix}", prefix), index);
components.First().Components[ctx.Event.Message.Components.First().Components.Length - 1] = new MessageComponent()
{
Type = ComponentType.Text,
Content = String.Join("\n", text),
};
return ctx.Respond(InteractionResponse.ResponseType.UpdateMessage, new()
{
Components = components,
Flags = Message.MessageFlags.IsComponentsV2,
});
}
return ButtonClickOld(ctx, prefix);
}
private static MessageComponent[] BuildComponents(ulong userId, string textContent, int menuIndex)
{
return [
new MessageComponent()
{
Type = ComponentType.Container,
AccentColor = DiscordUtils.Blue,
Components = [
new MessageComponent()
{
Type = ComponentType.Text,
Content = "# PluralKit\n-# Use the buttons below to see more info!"
},
helpPageButtons(userId, "new-", menuIndex),
new MessageComponent()
{
Type = ComponentType.Separator,
},
new MessageComponent()
{
Type = ComponentType.Text,
Content = textContent,
},
],
},
new MessageComponent()
{
Type = ComponentType.Text,
Content = EmbedFooter("\n-# "),
},
];
}
///
private static string Description = "PluralKit is a bot designed for plural communities on Discord, and is open for anyone to use. It allows you to register systems, maintain system information, set up message proxying, log switches, and more.\n\n" +
"**System recovery:** in the case of your Discord account getting lost or deleted, the PluralKit staff can help you recover your system, **only if you save the system token from `{prefix}token`**. See [this FAQ entry](https://pluralkit.me/faq/#can-i-recover-my-system-if-i-lose-access-to-my-discord-account) for more details.\n\n" +
"If PluralKit is useful to you, please consider donating on [Patreon](https://patreon.com/pluralkit) or [Buy Me A Coffee](https://buymeacoffee.com/pluralkit).\n" +
"## Use the buttons below to see more info!";
"If PluralKit is useful to you, please consider donating on [Patreon](https://patreon.com/pluralkit) or [Buy Me A Coffee](https://buymeacoffee.com/pluralkit).";
public static string EmbedFooter = "-# PluralKit by @ske and contributors | Myriad design by @layl, icon by @tedkalashnikov, banner by @fulmine | GitHub: https://github.com/PluralKit/PluralKit/ | Website: https://pluralkit.me/";
private static string DescriptionOld = $"{Description}\n## Use the buttons below to see more info!";
public static string EmbedFooter(string linkSeparator) => $"-# PluralKit by @ske and contributors | Myriad design by @layl, icon by @tedkalashnikov, banner by @fulmine{linkSeparator}GitHub: https://github.com/PluralKit/PluralKit/ | Website: https://pluralkit.me/";
public static Embed helpEmbed = new()
{
@ -98,7 +180,7 @@ public class Help
}
};
private static MessageComponent helpPageButtons(ulong userId) => new MessageComponent
private static MessageComponent helpPageButtons(ulong userId, string pfx = "", int menuIndex = -1) => new MessageComponent
{
Type = ComponentType.ActionRow,
Components = new[]
@ -106,58 +188,54 @@ public class Help
new MessageComponent
{
Type = ComponentType.Button,
Style = ButtonStyle.Secondary,
Style = menuIndex == 0 ? ButtonStyle.Primary : ButtonStyle.Secondary,
Label = "Basic Info",
CustomId = $"help-menu-basicinfo-{userId}",
CustomId = $"{pfx}help-menu-basicinfo-{userId}",
Emoji = new() { Name = "\u2139" },
},
new()
{
Type = ComponentType.Button,
Style = ButtonStyle.Secondary,
Style = menuIndex == 1 ? ButtonStyle.Primary : ButtonStyle.Secondary,
Label = "Getting Started",
CustomId = $"help-menu-gettingstarted-{userId}",
CustomId = $"{pfx}help-menu-gettingstarted-{userId}",
Emoji = new() { Name = "\u2753", },
},
new()
{
Type = ComponentType.Button,
Style = ButtonStyle.Secondary,
Style = menuIndex == 2 ? ButtonStyle.Primary : ButtonStyle.Secondary,
Label = "Useful Tips",
CustomId = $"help-menu-usefultips-{userId}",
CustomId = $"{pfx}help-menu-usefultips-{userId}",
Emoji = new() { Name = "\U0001f4a1", },
},
new()
{
Type = ComponentType.Button,
Style = ButtonStyle.Secondary,
Style = menuIndex == 3 ? ButtonStyle.Primary : ButtonStyle.Secondary,
Label = "More Info",
CustomId = $"help-menu-moreinfo-{userId}",
CustomId = $"{pfx}help-menu-moreinfo-{userId}",
Emoji = new() { Id = 986379675066593330, },
}
}
};
public Task HelpRoot(Context ctx)
public Task HelpRootOld(Context ctx)
=> ctx.Rest.CreateMessage(ctx.Channel.Id, new MessageRequest
{
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 = Help.Description.Replace("{prefix}", ctx.DefaultPrefix), Fields = new Embed.Field[] { new("", EmbedFooter) } } },
Embeds = new[] { helpEmbed with { Description = Help.DescriptionOld.Replace("{prefix}", ctx.DefaultPrefix), Fields = new Embed.Field[] { new("", EmbedFooter(" | ")) } } },
Components = new[] { helpPageButtons(ctx.Author.Id) },
});
public static Task ButtonClick(InteractionContext ctx, string prefix)
public static Task ButtonClickOld(InteractionContext ctx, string prefix)
{
if (!ctx.CustomId.Contains(ctx.User.Id.ToString()))
return ctx.Ignore();
var buttons = helpPageButtons(ctx.User.Id);
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 = Help.Description.Replace("{prefix}", prefix), Fields = new Embed.Field[] { new("", EmbedFooter) } } },
Embeds = new[] { helpEmbed with { Description = Help.DescriptionOld.Replace("{prefix}", prefix), Fields = new Embed.Field[] { new("", EmbedFooter(" | ")) } } },
Components = new[] { buttons }
});
@ -167,7 +245,7 @@ public class Help
{
Embeds = new[] { helpEmbed with { Fields = helpEmbedPages.GetValueOrDefault(ctx.CustomId.Split("-")[2]).Select(
(item, index) => new Embed.Field(item.Name.Replace("{prefix}", prefix), item.Value.Replace("{prefix}", prefix))
).Append(new("", EmbedFooter)).ToArray() } },
).Append(new("", EmbedFooter(" | "))).ToArray() } },
Components = new[] { buttons }
});
}

View file

@ -123,8 +123,16 @@ public class Member
public async Task ViewMember(Context ctx, PKMember target)
{
var system = await ctx.Repository.GetSystem(target.System);
if (ctx.MatchFlag("show-embed", "se"))
{
await ctx.Reply(
text: EmbedService.LEGACY_EMBED_WARNING,
embed: await _embeds.CreateMemberEmbed(system, target, ctx.Guild, ctx.Config, ctx.LookupContextFor(system.Id), ctx.Zone));
return;
}
await ctx.Reply(
embed: await _embeds.CreateMemberEmbed(system, target, ctx.Guild, ctx.Config, ctx.LookupContextFor(system.Id), ctx.Zone));
components: await _embeds.CreateMemberMessageComponents(system, target, ctx.Guild, ctx.Config, ctx.LookupContextFor(system.Id), ctx.Zone));
}
public async Task Soulscream(Context ctx, PKMember target)

View file

@ -92,7 +92,7 @@ public class Misc
+ $"**{stats.db.switches:N0}** switches, **{stats.db.messages:N0}** messages\n" +
$"**{stats.db.guilds:N0}** servers with **{stats.db.channels:N0}** channels"));
embed.Field(new("", Help.EmbedFooter));
embed.Field(new("", Help.EmbedFooter(" | ")));
var uptime = ((DateTimeOffset)process.StartTime).ToUnixTimeSeconds();
embed.Description($"### PluralKit [{BuildInfoService.Version}](https://github.com/pluralkit/pluralkit/commit/{BuildInfoService.FullVersion})\n" +

View file

@ -36,8 +36,17 @@ public class Random
"This system has no members!");
var randInt = randGen.Next(members.Count);
await ctx.Reply(embed: await _embeds.CreateMemberEmbed(target, members[randInt], ctx.Guild,
ctx.Config, ctx.LookupContextFor(target.Id), ctx.Zone));
if (ctx.MatchFlag("show-embed", "se"))
{
await ctx.Reply(
text: EmbedService.LEGACY_EMBED_WARNING,
embed: await _embeds.CreateMemberEmbed(target, members[randInt], ctx.Guild, ctx.Config, ctx.LookupContextFor(target.Id), ctx.Zone));
return;
}
await ctx.Reply(
components: await _embeds.CreateMemberMessageComponents(target, members[randInt], ctx.Guild, ctx.Config, ctx.LookupContextFor(target.Id), ctx.Zone));
}
public async Task Group(Context ctx, PKSystem target)
@ -60,7 +69,17 @@ public class Random
$"This system has no groups!");
var randInt = randGen.Next(groups.Count());
await ctx.Reply(embed: await _embeds.CreateGroupEmbed(ctx, target, groups.ToArray()[randInt]));
if (ctx.MatchFlag("show-embed", "se"))
{
await ctx.Reply(
text: EmbedService.LEGACY_EMBED_WARNING,
embed: await _embeds.CreateGroupEmbed(ctx, target, groups.ToArray()[randInt]));
return;
}
await ctx.Reply(
components: await _embeds.CreateGroupMessageComponents(ctx, target, groups.ToArray()[randInt]));
}
public async Task GroupMember(Context ctx, PKGroup group)
@ -92,7 +111,16 @@ public class Random
system = await ctx.Repository.GetSystem(group.System);
var randInt = randGen.Next(ms.Count);
await ctx.Reply(embed: await _embeds.CreateMemberEmbed(system, ms[randInt], ctx.Guild,
ctx.Config, ctx.LookupContextFor(group.System), ctx.Zone));
if (ctx.MatchFlag("show-embed", "se"))
{
await ctx.Reply(
text: EmbedService.LEGACY_EMBED_WARNING,
embed: await _embeds.CreateMemberEmbed(system, ms[randInt], ctx.Guild, ctx.Config, ctx.LookupContextFor(system.Id), ctx.Zone));
return;
}
await ctx.Reply(
components: await _embeds.CreateMemberMessageComponents(system, ms[randInt], ctx.Guild, ctx.Config, ctx.LookupContextFor(system.Id), ctx.Zone));
}
}

View file

@ -17,8 +17,13 @@ public class System
public async Task Query(Context ctx, PKSystem system, bool all, bool @public, bool @private)
{
if (system == null) throw Errors.NoSystemError(ctx.DefaultPrefix);
if (ctx.MatchFlag("show-embed", "se"))
{
await ctx.Reply(text: EmbedService.LEGACY_EMBED_WARNING, embed: await _embeds.CreateSystemEmbed(ctx, system, ctx.LookupContextFor(system.Id), all));
return;
}
await ctx.Reply(embed: await _embeds.CreateSystemEmbed(ctx, system, ctx.LookupContextFor(system.Id, @private, @public), all));
await ctx.Reply(components: await _embeds.CreateSystemMessageComponents(ctx, system, ctx.LookupContextFor(system.Id)));
}
public async Task New(Context ctx, string? systemName)

View file

@ -61,6 +61,7 @@ public class InteractionCreated: IEventHandler<InteractionCreateEvent>
// got some unhandled command, log and ignore
_logger.Warning(@"Unhandled ApplicationCommand interaction: {EventId} {CommandName}", evt.Id, evt.Data?.Name);
break;
};
}
;
}
}

View file

@ -25,6 +25,6 @@
<ItemGroup>
<PackageReference Include="Humanizer.Core" Version="2.14.1" />
<PackageReference Include="Sentry" Version="4.13.0" />
<PackageReference Include="Watson.Lite" Version="6.3.5" />
<PackageReference Include="Watson.Lite" Version="6.3.12" />
</ItemGroup>
</Project>

View file

@ -15,17 +15,21 @@ namespace PluralKit.Bot;
public class EmbedService
{
public const string LEGACY_EMBED_WARNING = "\u26A0\uFE0F The \"legacy\" embeds for system/member/group cards are deprecated, and will be removed in future.";
private readonly IDiscordCache _cache;
private readonly IDatabase _db;
private readonly ModelRepository _repo;
private readonly DiscordApiClient _rest;
private readonly CoreConfig _coreConfig;
public EmbedService(IDatabase db, ModelRepository repo, IDiscordCache cache, DiscordApiClient rest)
public EmbedService(IDatabase db, ModelRepository repo, IDiscordCache cache, DiscordApiClient rest, CoreConfig coreConfig)
{
_db = db;
_repo = repo;
_cache = cache;
_rest = rest;
_coreConfig = coreConfig;
}
private Task<(ulong Id, User? User)[]> GetUsers(IEnumerable<ulong> ids)
@ -39,6 +43,169 @@ public class EmbedService
return Task.WhenAll(ids.Select(Inner));
}
public async Task<MessageComponent[]> CreateSystemMessageComponents(Context cctx, PKSystem system, LookupContext ctx)
{
// Fetch/render info for all accounts simultaneously
var accounts = await _repo.GetSystemAccounts(system.Id);
var users = (await GetUsers(accounts)).Select(x => x.User?.NameAndMention() ?? $"(deleted account {x.Id})");
var linkedAccounts = new MessageComponent()
{
Type = ComponentType.Text,
Content = "**Linked accounts:**\n" + string.Join("\n", users).Truncate(1000),
};
var countctx = LookupContext.ByNonOwner;
if (cctx.MatchFlag("a", "all"))
{
if (system.Id == cctx.System?.Id)
countctx = LookupContext.ByOwner;
else
throw Errors.LookupNotAllowed;
}
var memberCount = await _repo.GetSystemMemberCount(system.Id, countctx == LookupContext.ByOwner ? null : PrivacyLevel.Public);
var guildSettings = cctx.Guild != null ? await _repo.GetSystemGuild(cctx.Guild.Id, system.Id) : null;
var avatar = system.AvatarFor(ctx);
var headerText = "";
if (system.PronounPrivacy.CanAccess(ctx) && system.Pronouns != null)
headerText += $"\n**Pronouns:** {system.Pronouns}";
if (system.Tag != null)
headerText += $"\n**Tag:** {system.Tag.EscapeMarkdown()}";
if (cctx.Config != null && cctx.Config.CardShowColorHex && !system.Color.EmptyOrNull())
headerText += $"\n**Color:** #{system.Color}";
if (cctx.Guild != null)
{
if (guildSettings.Tag != null && guildSettings.TagEnabled)
headerText += $"\n**Tag (in server '{cctx.Guild.Name}'):** {guildSettings.Tag.EscapeMarkdown()}";
if (!guildSettings.TagEnabled)
headerText += $"\n**Tag (in server '{cctx.Guild.Name}'):** *(tag is disabled in this server)*";
}
if (system.MemberListPrivacy.CanAccess(ctx))
{
headerText += $"\n**Members:** {memberCount}";
if (system.Id == cctx.System?.Id)
if (memberCount > 0)
headerText += $" (see `{cctx.DefaultPrefix}system list`)";
else
headerText += $" (add one with `{cctx.DefaultPrefix}member new`!)";
else if (memberCount > 0)
headerText += $" (see `{cctx.DefaultPrefix}system {system.DisplayHid(cctx.Config)} list`)";
}
List<MessageComponent> switchComponent = [];
var latestSwitch = await _repo.GetLatestSwitch(system.Id);
if (latestSwitch != null && system.FrontPrivacy.CanAccess(ctx))
{
var switchMembers =
await _db.Execute(conn => _repo.GetSwitchMembers(conn, latestSwitch.Id)).ToListAsync();
if (switchMembers.Count > 0)
{
var memberStr = string.Join(", ", switchMembers.Select(m => m.NameFor(ctx)));
if (memberStr.Length > 200)
memberStr = $"(too many to show, see `{cctx.DefaultPrefix}system {system.DisplayHid(cctx.Config)} fronters`)";
switchComponent.Add(new()
{
Type = ComponentType.Text,
Content = $"**{"Current fronter".ToQuantity(switchMembers.Count, ShowQuantityAs.None)}:** {memberStr}",
});
}
}
List<MessageComponent> descComponents = [];
if (system.DescriptionFor(ctx) is { } desc && !string.IsNullOrWhiteSpace(desc))
{
descComponents.Add(new()
{
Type = ComponentType.Separator,
});
descComponents.Add(new()
{
Type = ComponentType.Text,
Content = desc.NormalizeLineEndSpacing().Truncate(1024),
});
}
if (system.BannerPrivacy.CanAccess(ctx) && !string.IsNullOrWhiteSpace(system.BannerImage))
descComponents.Add(new()
{
Type = ComponentType.MediaGallery,
Items = [new() { Media = new() { Url = system.BannerImage } }],
});
var systemName = (cctx.Guild != null && guildSettings?.DisplayName != null) ? guildSettings?.DisplayName! : system.NameFor(ctx);
var premiumText = ""; // TODO(iris): "\n\U0001F31F *PluralKit Premium supporter!*";
List<MessageComponent> header = [
new MessageComponent()
{
Type = ComponentType.Text,
Content = $"### {systemName ?? $"`{system.DisplayHid(cctx.Config)}`"}{premiumText}",
},
];
if (!string.IsNullOrWhiteSpace(headerText))
header.Add(new MessageComponent()
{
Type = ComponentType.Text,
Content = headerText,
});
if (cctx.Guild != null)
{
var guildAvatar = guildSettings.AvatarUrl.TryGetCleanCdnUrl();
if (!string.IsNullOrWhiteSpace(guildAvatar))
avatar = guildAvatar;
}
if (!string.IsNullOrWhiteSpace(avatar))
header = [
new MessageComponent()
{
Type = ComponentType.Section,
Components = [.. header],
Accessory = new MessageComponent()
{
Type = ComponentType.Thumbnail,
Media = new() { Url = avatar },
},
},
];
return [
new MessageComponent()
{
Type = ComponentType.Container,
AccentColor = system.Color?.ToDiscordColor(),
Components = [ ..header, ..switchComponent, linkedAccounts, ..descComponents ],
},
new MessageComponent()
{
Type = ComponentType.Section,
Components = [
new MessageComponent()
{
Type = ComponentType.Text,
Content = $"-# System ID: `{system.DisplayHid(cctx.Config)}`\n-# Created: {system.Created.FormatZoned(cctx.Zone)}",
},
],
Accessory = new MessageComponent()
{
Type = ComponentType.Button,
Style = ButtonStyle.Link,
Label = "View on dashboard",
Url = $"{_coreConfig.DashboardBaseUrl}/profile/s/{system.Hid}",
},
},
];
}
public async Task<Embed> CreateSystemEmbed(Context cctx, PKSystem system, LookupContext ctx, bool countctxByOwner)
{
// Fetch/render info for all accounts simultaneously
@ -48,7 +215,7 @@ public class EmbedService
var countctx = LookupContext.ByNonOwner;
if (countctxByOwner)
{
if (system.Id == cctx.System.Id)
if (system.Id == cctx.System?.Id)
countctx = LookupContext.ByOwner;
else
throw Errors.LookupNotAllowed;
@ -61,7 +228,7 @@ public class EmbedService
.Footer(new Embed.EmbedFooter(
$"System ID: {system.DisplayHid(cctx.Config)} | Created on {system.Created.FormatZoned(cctx.Zone)}"))
.Color(system.Color?.ToDiscordColor())
.Url($"https://dash.pluralkit.me/profile/s/{system.Hid}");
.Url($"{_coreConfig.DashboardBaseUrl}/profile/s/{system.Hid}");
var avatar = system.AvatarFor(ctx);
if (avatar != null)
@ -164,6 +331,158 @@ public class EmbedService
return embed.Build();
}
public async Task<MessageComponent[]> CreateMemberMessageComponents(PKSystem system, PKMember member, Guild guild, SystemConfig? ccfg, LookupContext ctx, DateTimeZone zone)
{
var name = member.NameFor(ctx);
var systemGuildSettings = guild != null ? await _repo.GetSystemGuild(guild.Id, system.Id) : null;
var systemName = (guild != null && systemGuildSettings?.DisplayName != null) ? systemGuildSettings?.DisplayName! : system.NameFor(ctx);
var guildSettings = guild != null ? await _repo.GetMemberGuild(guild.Id, member.Id) : null;
var guildDisplayName = guildSettings?.DisplayName;
var webhook_avatar = guildSettings?.AvatarUrl ?? member.WebhookAvatarFor(ctx) ?? member.AvatarFor(ctx);
var avatar = guildSettings?.AvatarUrl ?? member.AvatarFor(ctx);
var groups = await _repo.GetMemberGroups(member.Id)
.Where(g => g.Visibility.CanAccess(ctx))
.OrderBy(g => g.Name, StringComparer.InvariantCultureIgnoreCase)
.ToListAsync();
var headerText = "";
if (member.MemberVisibility == PrivacyLevel.Private)
headerText += "*(this member is hidden)*\n";
if (guildSettings?.AvatarUrl != null)
if (member.AvatarFor(ctx) != null)
headerText +=
$"*(this member has a server-specific avatar set; [click here]({member.AvatarUrl.TryGetCleanCdnUrl()}) to see the global avatar)*\n";
else
headerText += "*(this member has a server-specific avatar set)*\n";
if (!member.DisplayName.EmptyOrNull() && member.NamePrivacy.CanAccess(ctx))
headerText += $"\n**Display name:** {member.DisplayName.Truncate(1024)}";
if (guild != null && guildDisplayName != null)
headerText += $"\n**Server nickname (for '{guild.Name}'):** {guildDisplayName.Truncate(1024)}";
if (ccfg != null && ccfg.CardShowColorHex && !member.Color.EmptyOrNull())
headerText += $"\n**Color:** #{member.Color}";
if (member.PronounsFor(ctx) is { } pronouns && !string.IsNullOrWhiteSpace(pronouns))
headerText += $"\n**Pronouns:** {pronouns}";
if (member.BirthdayFor(ctx) != null)
headerText += $"\n**Birthday:** {member.BirthdayString}";
if (member.MessageCountFor(ctx) is { } count && count > 0)
headerText += $"\n**Message count:** {member.MessageCount}";
List<MessageComponent> extraData = [];
if (member.HasProxyTags && member.ProxyPrivacy.CanAccess(ctx))
{
extraData.Add(new MessageComponent
{
Type = ComponentType.Separator,
});
extraData.Add(new MessageComponent
{
Type = ComponentType.Text,
Content = $"**Proxy tags:**\n{member.ProxyTagsString("\n").Truncate(1024)}",
});
}
if (groups.Count > 0)
{
// More than 5 groups show in "compact" format without ID
var content = groups.Count > 5
? string.Join(", ", groups.Select(g => g.DisplayName ?? g.Name))
: string.Join("\n", groups.Select(g => $"[`{g.DisplayHid(ccfg, isList: true)}`] **{g.DisplayName ?? g.Name}**"));
extraData.Add(new MessageComponent
{
Type = ComponentType.Separator,
});
extraData.Add(new MessageComponent
{
Type = ComponentType.Text,
Content = $"**Groups ({groups.Count}):**\n{content.Truncate(1000)}",
});
}
List<MessageComponent> descComponents = [];
if (member.DescriptionFor(ctx) is { } desc && !string.IsNullOrWhiteSpace(desc))
{
descComponents.Add(new()
{
Type = ComponentType.Separator,
});
descComponents.Add(new()
{
Type = ComponentType.Text,
Content = desc.NormalizeLineEndSpacing().Truncate(1024),
});
}
if (member.BannerPrivacy.CanAccess(ctx) && !string.IsNullOrWhiteSpace(member.BannerImage))
descComponents.Add(new()
{
Type = ComponentType.MediaGallery,
Items = [new() { Media = new() { Url = member.BannerImage } }],
});
List<MessageComponent> header = [
new MessageComponent()
{
Type = ComponentType.Text,
Content = $"### {name}{(systemName != null ? $" ({systemName})" : "")}",
},
];
if (!string.IsNullOrWhiteSpace(headerText))
header.Add(new MessageComponent()
{
Type = ComponentType.Text,
Content = headerText,
});
if (!string.IsNullOrWhiteSpace(avatar))
header = [
new MessageComponent()
{
Type = ComponentType.Section,
Components = [.. header],
Accessory = new MessageComponent()
{
Type = ComponentType.Thumbnail,
Media = new() { Url = avatar },
},
},
];
return [
new MessageComponent()
{
Type = ComponentType.Container,
AccentColor = member.Color?.ToDiscordColor(),
Components = [ ..header, ..extraData, ..descComponents ],
},
new MessageComponent()
{
Type = ComponentType.Section,
Components = [
new MessageComponent()
{
Type = ComponentType.Text,
Content = $"-# System ID: `{system.DisplayHid(ccfg)}` \u2219 Member ID: `{member.DisplayHid(ccfg)}`{(member.MetadataPrivacy.CanAccess(ctx) ? $"\n-# Created: {member.Created.FormatZoned(zone)}" : "")}",
},
],
Accessory = new MessageComponent()
{
Type = ComponentType.Button,
Style = ButtonStyle.Link,
Label = "View on dashboard",
Url = $"{_coreConfig.DashboardBaseUrl}/profile/m/{member.Hid}",
},
},
];
}
public async Task<Embed> CreateMemberEmbed(PKSystem system, PKMember member, Guild guild, SystemConfig? ccfg, LookupContext ctx, DateTimeZone zone)
{
// string FormatTimestamp(Instant timestamp) => DateTimeFormats.ZonedDateTimeFormat.Format(timestamp.InZone(system.Zone));
@ -188,7 +507,7 @@ public class EmbedService
.ToListAsync();
var eb = new EmbedBuilder()
.Author(new Embed.EmbedAuthor(name, IconUrl: webhook_avatar.TryGetCleanCdnUrl(), Url: $"https://dash.pluralkit.me/profile/m/{member.Hid}"))
.Author(new Embed.EmbedAuthor(name, IconUrl: webhook_avatar.TryGetCleanCdnUrl(), Url: $"{_coreConfig.DashboardBaseUrl}/profile/m/{member.Hid}"))
// .WithColor(member.ColorPrivacy.CanAccess(ctx) ? color : null)
.Color(member.Color?.ToDiscordColor())
.Footer(new Embed.EmbedFooter(
@ -241,6 +560,119 @@ public class EmbedService
return eb.Build();
}
public async Task<MessageComponent[]> CreateGroupMessageComponents(Context ctx, PKSystem system, PKGroup target)
{
var pctx = ctx.LookupContextFor(system.Id);
var name = target.NameFor(ctx);
var systemGuildSettings = ctx.Guild != null ? await _repo.GetSystemGuild(ctx.Guild.Id, system.Id) : null;
var systemName = (ctx.Guild != null && systemGuildSettings?.DisplayName != null) ? systemGuildSettings?.DisplayName! : system.NameFor(ctx);
var countctx = LookupContext.ByNonOwner;
if (ctx.MatchFlag("a", "all"))
{
if (system.Id == ctx.System?.Id)
countctx = LookupContext.ByOwner;
else
throw Errors.LookupNotAllowed;
}
var memberCount = await _repo.GetGroupMemberCount(target.Id, countctx == LookupContext.ByOwner ? null : PrivacyLevel.Public);
var headerText = "";
if (target.NamePrivacy.CanAccess(pctx) && target.DisplayName != null)
headerText += $"\n**Display name:** {target.DisplayName}";
if (ctx.Config != null && ctx.Config.CardShowColorHex && !target.Color.EmptyOrNull())
headerText += $"\n**Color:** #{target.Color}";
if (target.ListPrivacy.CanAccess(pctx))
{
headerText += $"\n**Members:** {memberCount}";
if (system.Id == ctx.System?.Id && memberCount == 0)
headerText += $" (add one with `{ctx.DefaultPrefix}group {target.Reference(ctx)} add <member>`!)";
else if (memberCount > 0)
headerText += $" (see `{ctx.DefaultPrefix}group {target.Reference(ctx)} list`)";
}
List<MessageComponent> descComponents = [];
if (target.DescriptionFor(pctx) is { } desc && !string.IsNullOrWhiteSpace(desc))
{
descComponents.Add(new()
{
Type = ComponentType.Separator,
});
descComponents.Add(new()
{
Type = ComponentType.Text,
Content = desc.NormalizeLineEndSpacing().Truncate(1024),
});
}
if (target.BannerPrivacy.CanAccess(pctx) && !string.IsNullOrWhiteSpace(target.BannerImage))
descComponents.Add(new()
{
Type = ComponentType.MediaGallery,
Items = [new() { Media = new() { Url = target.BannerImage } }],
});
List<MessageComponent> header = [
new MessageComponent()
{
Type = ComponentType.Text,
Content = $"### {name}{(systemName != null ? $" ({systemName})" : "")}",
},
];
if (!string.IsNullOrWhiteSpace(headerText))
header.Add(new MessageComponent()
{
Type = ComponentType.Text,
Content = headerText,
});
if (target.IconFor(pctx) is { } icon)
header = [
new MessageComponent()
{
Type = ComponentType.Section,
Components = [.. header],
Accessory = new MessageComponent()
{
Type = ComponentType.Thumbnail,
Media = new() { Url = icon.TryGetCleanCdnUrl() },
},
},
];
return [
new MessageComponent()
{
Type = ComponentType.Container,
AccentColor = target.Color?.ToDiscordColor(),
Components = [ ..header, ..descComponents ],
},
new MessageComponent()
{
Type = ComponentType.Section,
Components = [
new MessageComponent()
{
Type = ComponentType.Text,
Content = $"-# System ID: `{system.DisplayHid(ctx.Config)}` \u2219 Group ID: `{target.DisplayHid(ctx.Config)}`{(target.MetadataPrivacy.CanAccess(pctx) ? $"\n-# Created: {target.Created.FormatZoned(ctx.Zone)}" : "")}",
},
],
Accessory = new MessageComponent()
{
Type = ComponentType.Button,
Style = ButtonStyle.Link,
Label = "View on dashboard",
Url = $"{_coreConfig.DashboardBaseUrl}/profile/g/{target.Hid}",
},
},
];
}
public async Task<Embed> CreateGroupEmbed(Context ctx, PKSystem system, PKGroup target)
{
var pctx = ctx.LookupContextFor(system.Id);
@ -248,7 +680,7 @@ public class EmbedService
var countctx = LookupContext.ByNonOwner;
if (ctx.MatchFlag("a", "all"))
{
if (system.Id == ctx.System.Id)
if (system.Id == ctx.System?.Id)
countctx = LookupContext.ByOwner;
else
throw Errors.LookupNotAllowed;
@ -266,7 +698,7 @@ public class EmbedService
nameField = $"{nameField}";
var eb = new EmbedBuilder()
.Author(new Embed.EmbedAuthor(nameField, IconUrl: target.IconFor(pctx), Url: $"https://dash.pluralkit.me/profile/g/{target.Hid}"))
.Author(new Embed.EmbedAuthor(nameField, IconUrl: target.IconFor(pctx), Url: $"{_coreConfig.DashboardBaseUrl}/profile/g/{target.Hid}"))
.Color(target.Color?.ToDiscordColor());
eb.Footer(new Embed.EmbedFooter($"System ID: {system.DisplayHid(ctx.Config)} | Group ID: {target.DisplayHid(ctx.Config)}{(target.MetadataPrivacy.CanAccess(pctx) ? $" | Created on {target.Created.FormatZoned(ctx.Zone)}" : "")}"));

View file

@ -116,7 +116,7 @@ public class ErrorMessageService
return new EmbedBuilder()
.Color(0xE74C3C)
.Title("Internal error occurred")
.Description($"For support, please send the error code above as text in {channelInfo} with a description of what you were doing at the time.")
.Description($"**If you need support,** please send/forward the error code above **as text** in {channelInfo} with a description of what you were doing at the time.")
.Footer(new Embed.EmbedFooter(errorId))
.Timestamp(now.ToDateTimeOffset().ToString("O"))
.Build();

View file

@ -1,4 +1,3 @@
using System.Text;
using System.Text.Json;
using Serilog;
@ -58,7 +57,7 @@ public class HttpListenerService
private async Task RuntimeConfigSet(HttpContextBase ctx)
{
var key = ctx.Request.Url.Parameters["key"];
var value = ReadStream(ctx.Request.Data, ctx.Request.ContentLength);
var value = ctx.Request.DataAsString;
await _runtimeConfig.Set(key, value);
await RuntimeConfigGet(ctx);
}
@ -77,7 +76,7 @@ public class HttpListenerService
var shardIdString = ctx.Request.Url.Parameters["shard_id"];
if (!int.TryParse(shardIdString, out var shardId)) return;
var packet = JsonSerializer.Deserialize<GatewayPacket>(ReadStream(ctx.Request.Data, ctx.Request.ContentLength), _jsonSerializerOptions);
var packet = JsonSerializer.Deserialize<GatewayPacket>(ctx.Request.DataAsString, _jsonSerializerOptions);
var evt = DeserializeEvent(shardId, packet.EventType!, (JsonElement)packet.Payload!);
if (evt != null)
{
@ -108,39 +107,4 @@ public class HttpListenerService
return null;
}
}
//temporary re-implementation of the ReadStream function found in WatsonWebserver.Lite, but with handling for closed connections
//https://github.com/dotnet/WatsonWebserver/issues/171
private static string ReadStream(Stream input, long contentLength)
{
if (input == null) throw new ArgumentNullException(nameof(input));
if (!input.CanRead) throw new InvalidOperationException("Input stream is not readable");
if (contentLength < 1) return "";
byte[] buffer = new byte[65536];
long bytesRemaining = contentLength;
using (MemoryStream ms = new MemoryStream())
{
int read;
while (bytesRemaining > 0)
{
read = input.Read(buffer, 0, buffer.Length);
if (read > 0)
{
ms.Write(buffer, 0, read);
bytesRemaining -= read;
}
else
{
throw new IOException("Connection closed before reading end of stream.");
}
}
if (ms.Length < 1) return null;
var str = Encoding.Default.GetString(ms.ToArray());
return str;
}
}
}

View file

@ -16,12 +16,12 @@
},
"Watson.Lite": {
"type": "Direct",
"requested": "[6.3.5, )",
"resolved": "6.3.5",
"contentHash": "YF8+se3IVenn8YlyNeb4wSJK6QMnVD0QHIOEiZ22wS4K2wkwoSDzWS+ZAjk1MaPeB+XO5gRoENUN//pOc+wI2g==",
"requested": "[6.3.12, )",
"resolved": "6.3.12",
"contentHash": "L/TfJadyOwK9bhhvOnEKXLeyDaTAn8v6hnYPVPIwQ7JlnUXDbDqsUa3qHFdFIHiuK8vMZOnDL7+k/mY10yxdYw==",
"dependencies": {
"CavemanTcp": "2.0.5",
"Watson.Core": "6.3.5"
"CavemanTcp": "2.0.9",
"Watson.Core": "6.3.12"
}
},
"App.Metrics": {
@ -119,8 +119,8 @@
},
"CavemanTcp": {
"type": "Transitive",
"resolved": "2.0.5",
"contentHash": "90wywmGpjrj26HMAkufYZwuZI8sVYB1mRwEdqugSR3kgDnPX+3l0jO86gwtFKsPvsEpsS4Dn/1EbhguzUxMU8Q=="
"resolved": "2.0.9",
"contentHash": "KgIwYhPhGkBTm+wwVAmWonkKPw4xYVnutzzlIeqOLcX1fti+8d+MEGTvbern1smf3S/UpjFjihkf6XRziTddzQ=="
},
"Dapper": {
"type": "Transitive",
@ -746,8 +746,8 @@
},
"Watson.Core": {
"type": "Transitive",
"resolved": "6.3.5",
"contentHash": "Y5YxKOCSLe2KDmfwvI/J0qApgmmZR77LwyoufRVfKH7GLdHiE7fY0IfoNxWTG7nNv8knBfgwyOxdehRm+4HaCg==",
"resolved": "6.3.12",
"contentHash": "lCWv+7rz++z/1ceu+aBdJYw+nO7u/dgXxVYrmdUO/3ylFeEbbPQP19MYjOJbhSQY4+bmQRex79YJ1IneLZhprA==",
"dependencies": {
"IpMatcher": "1.0.5",
"RegexMatcher": "1.0.9",