Compare commits

..

11 commits

Author SHA1 Message Date
asleepyskye
981546647a WIP: port fronters embed to CV2
Some checks failed
Build and push Docker image / .net docker build (push) Has been cancelled
.net checks / run .net tests (push) Has been cancelled
.net checks / dotnet-format (push) Has been cancelled
Build and push Rust service Docker images / rust docker build (push) Has been cancelled
rust checks / cargo fmt (push) Has been cancelled
2025-11-07 09:57:35 -05:00
asleepyskye
83f866384a feat(bot): use discord timestamps on cv2 cards 2025-11-07 09:20:23 -05:00
asleepyskye
0983179240 fix(bot): add allowed mentions to msg info replies
Some checks failed
Build and push Docker image / .net docker build (push) Has been cancelled
.net checks / run .net tests (push) Has been cancelled
.net checks / dotnet-format (push) Has been cancelled
2025-10-24 21:26:33 -04:00
asleepyskye
49ce00e675 fix(bot): check for null avatar in msg info 2025-10-24 19:55:53 -04:00
asleepyskye
83f2d33c3d feat(bot): port message info embeds to cv2
Some checks are pending
Build and push Docker image / .net docker build (push) Waiting to run
.net checks / run .net tests (push) Waiting to run
.net checks / dotnet-format (push) Waiting to run
2025-10-24 10:23:38 -04:00
Jake Fulmine
14f11bd1e9 fix(bot): take member name privacy into account when viewing member groups
Some checks failed
Build and push Docker image / .net docker build (push) Has been cancelled
.net checks / run .net tests (push) Has been cancelled
.net checks / dotnet-format (push) Has been cancelled
2025-09-20 02:46:29 +02:00
asleepyskye
39179f8e3a fix(gateway): properly check for reconnect
Some checks failed
Build and push Rust service Docker images / rust docker build (push) Has been cancelled
rust checks / cargo fmt (push) Has been cancelled
2025-09-19 09:30:20 -04:00
alyssa
24361d9d2b fix(bot): correctly check for existence of current system in embeds
Some checks are pending
Build and push Docker image / .net docker build (push) Waiting to run
.net checks / run .net tests (push) Waiting to run
.net checks / dotnet-format (push) Waiting to run
2025-09-18 23:32:00 +00:00
asleepyskye
9c99a0bc02 chore: update faq for cv2 2025-09-18 08:06:55 -04:00
asleepyskye
ebf8a40369 fix(bot): fix utility admin command
Some checks failed
Build and push Docker image / .net docker build (push) Has been cancelled
.net checks / run .net tests (push) Has been cancelled
.net checks / dotnet-format (push) Has been cancelled
2025-09-09 11:32:13 -04:00
asleepyskye
8bca02032f feat(bot): add utility admin command 2025-09-09 10:43:00 -04:00
13 changed files with 264 additions and 158 deletions

View file

@ -43,23 +43,6 @@ public class ApplicationCommandProxiedMessage
if (channel == null)
showContent = false;
// var embeds = new List<Embed>();
// var guild = await _cache.GetGuild(ctx.GuildId);
// if (msg.Member != null)
// embeds.Add(await _embeds.CreateMemberEmbed(
// msg.System,
// msg.Member,
// guild,
// ctx.Config,
// LookupContext.ByNonOwner,
// DateTimeZone.Utc
// ));
// embeds.Add(await _embeds.CreateMessageInfoEmbed(msg, showContent, ctx.Config));
// await ctx.Reply(embeds: embeds.ToArray());
var components = new List<MessageComponent>();
var guild = await _cache.GetGuild(ctx.GuildId);
if (msg.Member != null)
@ -71,7 +54,10 @@ public class ApplicationCommandProxiedMessage
LookupContext.ByNonOwner,
DateTimeZone.Utc
));
components.Add(new MessageComponent()
{
Type = ComponentType.Separator
});
components.AddRange(await _embeds.CreateMessageInfoMessageComponents(msg, showContent, ctx.Config));
await ctx.Reply(components: components.ToArray());
}
@ -83,11 +69,7 @@ public class ApplicationCommandProxiedMessage
if (msg == null)
throw Errors.MessageNotFound(messageId);
var embeds = new List<Embed>();
embeds.Add(await _embeds.CreateCommandMessageInfoEmbed(msg, true));
await ctx.Reply(embeds: embeds.ToArray());
await ctx.Reply(components: await _embeds.CreateCommandMessageInfoMessageComponents(msg, true));
}
public async Task DeleteMessage(InteractionContext ctx)

View file

@ -181,6 +181,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

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

@ -601,6 +601,13 @@ public class Config
public async Task FronterListFormat(Context ctx)
{
if (!ctx.HasNext())
{
var msg = $"Format of the fronter list is currently **{ctx.Config.FronterListFormat}**.";
await ctx.Reply(msg);
return;
}
var badInputError = "Valid list format settings are `short` or `full`.";
if (ctx.Match("full", "f"))
{
@ -613,8 +620,6 @@ public class Config
await ctx.Reply($"Fronter lists are now formatted as `short`");
}
else throw new PKError(badInputError);
}
public async Task ProxySwitch(Context ctx)

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

@ -442,56 +442,7 @@ public class ProxiedMessage
return;
}
MessageComponent authorInfo;
var author = user != null
? $"{user.Username}#{user.Discriminator}"
: $"Deleted user ${message.Message.Sender}";
var avatarUrl = user?.AvatarUrl();
var authorString = $"{author}\n**ID: **`{message.Message.Sender.ToString()}`";
if (user != null && avatarUrl != "")
{
authorInfo = new MessageComponent()
{
Type = ComponentType.Section,
Components = [
new MessageComponent()
{
Type = ComponentType.Text,
Content = authorString
}
],
Accessory = new MessageComponent()
{
Type = ComponentType.Thumbnail,
Media = new ComponentMedia()
{
Url = avatarUrl
}
}
};
}
else
{
authorInfo = new MessageComponent()
{
Type = ComponentType.Text,
Content = authorString
};
}
MessageComponent container = new MessageComponent()
{
Type = ComponentType.Container,
Components = [
authorInfo,
]
};
await ctx.Reply(components: [new MessageComponent()
{
Type = ComponentType.Text,
Content = user != null ? $"{user.Mention()} ({user.Id})" : $"*(deleted user {message.Message.Sender})*"
},container]);
await ctx.Reply(components: await _embeds.CreateAuthorMessageComponents(user, message));
return;
}
@ -533,6 +484,11 @@ public class ProxiedMessage
else if (!await ctx.CheckPermissionsInGuildChannel(channel, PermissionSet.ViewChannel))
showContent = false;
await ctx.Reply(embed: await _embeds.CreateCommandMessageInfoEmbed(msg, showContent));
if (ctx.MatchFlag("show-embed", "se"))
{
await ctx.Reply(embed: await _embeds.CreateCommandMessageInfoEmbed(msg, showContent));
return;
}
await ctx.Reply(components: await _embeds.CreateCommandMessageInfoMessageComponents(msg, showContent));
}
}

View file

@ -186,22 +186,6 @@ public class ReactionAdded: IEventHandler<MessageReactionAddEvent>
{
var dm = await _dmCache.GetOrCreateDmChannel(evt.UserId);
// var embeds = new List<Embed>();
// if (msg.Member != null)
// embeds.Add(await _embeds.CreateMemberEmbed(
// msg.System,
// msg.Member,
// guild,
// config,
// LookupContext.ByNonOwner,
// DateTimeZone.Utc
// ));
// embeds.Add(await _embeds.CreateMessageInfoEmbed(msg, true, config));
// await _rest.CreateMessage(dm, new MessageRequest { Embeds = embeds.ToArray() });
var components = new List<MessageComponent>();
if (msg.Member != null)
components.AddRange(await _embeds.CreateMemberMessageComponents(
@ -212,9 +196,12 @@ public class ReactionAdded: IEventHandler<MessageReactionAddEvent>
LookupContext.ByNonOwner,
DateTimeZone.Utc
));
components.Add(new MessageComponent()
{
Type = ComponentType.Separator
});
components.AddRange(await _embeds.CreateMessageInfoMessageComponents(msg, true, config));
await _rest.CreateMessage(dm, new MessageRequest { Components = components.ToArray(), Flags = Message.MessageFlags.IsComponentsV2 });
await _rest.CreateMessage(dm, new MessageRequest { Components = components.ToArray(), Flags = Message.MessageFlags.IsComponentsV2, AllowedMentions = new AllowedMentions() });
}
catch (ForbiddenException) { } // No permissions to DM, can't check for this :(

View file

@ -57,7 +57,7 @@ public class EmbedService
var countctx = LookupContext.ByNonOwner;
if (cctx.MatchFlag("a", "all"))
{
if (system.Id == cctx.System.Id)
if (system.Id == cctx.System?.Id)
countctx = LookupContext.ByOwner;
else
throw Errors.LookupNotAllowed;
@ -89,7 +89,7 @@ public class EmbedService
if (system.MemberListPrivacy.CanAccess(ctx))
{
headerText += $"\n**Members:** {memberCount}";
if (system.Id == cctx.System.Id)
if (system.Id == cctx.System?.Id)
if (memberCount > 0)
headerText += $" (see `{cctx.DefaultPrefix}system list`)";
else
@ -192,7 +192,7 @@ public class EmbedService
new MessageComponent()
{
Type = ComponentType.Text,
Content = $"-# System ID: `{system.DisplayHid(cctx.Config)}`\n-# Created: {system.Created.FormatZoned(cctx.Zone)}",
Content = $"-# System ID: `{system.DisplayHid(cctx.Config)}`\n-# Created: {DiscordUtils.InstantToTimestampString(system.Created)}",
},
],
Accessory = new MessageComponent()
@ -215,7 +215,7 @@ public class EmbedService
var countctx = LookupContext.ByNonOwner;
if (cctx.MatchFlag("a", "all"))
{
if (system.Id == cctx.System.Id)
if (system.Id == cctx.System?.Id)
countctx = LookupContext.ByOwner;
else
throw Errors.LookupNotAllowed;
@ -469,7 +469,7 @@ public class EmbedService
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)}" : "")}",
Content = $"-# System ID: `{system.DisplayHid(ccfg)}` \u2219 Member ID: `{member.DisplayHid(ccfg)}`{(member.MetadataPrivacy.CanAccess(ctx) ? $"\n-# Created: {DiscordUtils.InstantToTimestampString(member.Created)}" : "")}",
},
],
Accessory = new MessageComponent()
@ -570,7 +570,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;
@ -588,7 +588,7 @@ public class EmbedService
if (target.ListPrivacy.CanAccess(pctx))
{
headerText += $"\n**Members:** {memberCount}";
if (system.Id == ctx.System.Id && memberCount == 0)
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`)";
@ -659,7 +659,7 @@ public class EmbedService
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)}" : "")}",
Content = $"-# System ID: `{system.DisplayHid(ctx.Config)}` \u2219 Group ID: `{target.DisplayHid(ctx.Config)}`{(target.MetadataPrivacy.CanAccess(pctx) ? $"\n-# Created: {DiscordUtils.InstantToTimestampString(target.Created)}" : "")}",
},
],
Accessory = new MessageComponent()
@ -680,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;
@ -820,7 +820,7 @@ public class EmbedService
new MessageComponent()
{
Type = ComponentType.Text,
Content = $"**Since:** <t:{sw.Timestamp.ToUnixTimeSeconds()}> (<t:{sw.Timestamp.ToUnixTimeSeconds()}:R>)"
Content = $"**Since:** {DiscordUtils.InstantToTimestampString(sw.Timestamp)} (<t:{sw.Timestamp.ToUnixTimeSeconds()}:R>)"
}
]
},
@ -928,7 +928,7 @@ public class EmbedService
memberList.Add(new MessageComponent()
{
Type = ComponentType.Text,
Content = $"**Since:** <t:{sw.Timestamp.ToUnixTimeSeconds()}> (<t:{sw.Timestamp.ToUnixTimeSeconds()}:R>)"
Content = $"**Since:** {DiscordUtils.InstantToTimestampString(sw.Timestamp)} (<t:{sw.Timestamp.ToUnixTimeSeconds()}:R>)"
});
return [
new MessageComponent(){
@ -1023,7 +1023,7 @@ public class EmbedService
};
var avatarURL = msg.Member?.AvatarFor(ctx).TryGetCleanCdnUrl();
MessageComponent header = avatarURL == "" ? authorData : new MessageComponent()
MessageComponent header = (avatarURL == "" || avatarURL == null) ? authorData : new MessageComponent()
{
Type = ComponentType.Section,
Components = [authorData],
@ -1042,39 +1042,53 @@ public class EmbedService
{
Type = ComponentType.Separator,
Spacing = 2
},
new MessageComponent()
}
];
if (content != "")
{
body.Add(new MessageComponent()
{
Type = ComponentType.Text,
Content = content
}
];
});
}
if (showContent)
{
var url = serverMsg?.Attachments?.FirstOrDefault()?.Url;
if (url != null && url != "")
body.Add(new MessageComponent()
if (serverMsg != null)
{
var media = new List<ComponentMediaItem>();
foreach (Message.Attachment attachment in serverMsg?.Attachments)
{
Type = ComponentType.MediaGallery,
Items = [new ComponentMediaItem()
var url = attachment.Url;
if (url != null && url != "")
media.Add(new ComponentMediaItem()
{
Media = new ComponentMedia()
{
Url = url
}
});
}
if (media.Count > 0)
body.Add(new MessageComponent()
{
Media = new ComponentMedia(){
Url = url
}
}]
});
Type = ComponentType.MediaGallery,
Items = media.ToArray()
});
}
}
MessageComponent footer = new MessageComponent()
{
Type = ComponentType.Text,
Content = $"-# Original Message ID: {msg.Message.OriginalMid} · <t:{DiscordUtils.SnowflakeToTimestamp(msg.Message.Mid)}:t>"
Content = $"-# Original Message ID: {msg.Message.OriginalMid} · {DiscordUtils.InstantToTimestampString(DiscordUtils.SnowflakeToInstant(msg.Message.Mid))}"
};
return [
new MessageComponent(){
Type = ComponentType.Container,
new MessageComponent()
{
Type = ComponentType.Container,
Components = [
header,
..body
@ -1169,6 +1183,106 @@ public class EmbedService
return eb.Build();
}
public async Task<MessageComponent[]> CreateAuthorMessageComponents(User? user, FullMessage msg)
{
MessageComponent authorInfo;
var author = user != null
? $"{user.Username}#{user.Discriminator}"
: $"Deleted user ${msg.Message.Sender}";
var avatarUrl = user?.AvatarUrl();
var authorString = $"{author}\n**ID: **`{msg.Message.Sender.ToString()}`";
if (user != null && avatarUrl != "")
{
authorInfo = new MessageComponent()
{
Type = ComponentType.Section,
Components = [
new MessageComponent()
{
Type = ComponentType.Text,
Content = authorString
}
],
Accessory = new MessageComponent()
{
Type = ComponentType.Thumbnail,
Media = new ComponentMedia()
{
Url = avatarUrl
}
}
};
}
else
{
authorInfo = new MessageComponent()
{
Type = ComponentType.Text,
Content = authorString
};
}
MessageComponent container = new MessageComponent()
{
Type = ComponentType.Container,
Components = [
authorInfo,
]
};
return (
[
new MessageComponent()
{
Type = ComponentType.Text,
Content = user != null ? $"{user.Mention()} ({user.Id})" : $"*(deleted user {msg.Message.Sender})*"
},
container
]
);
}
public async Task<MessageComponent[]> CreateCommandMessageInfoMessageComponents(Core.CommandMessage msg, bool showContent)
{
var content = "*(command message deleted or inaccessible)*";
if (showContent)
{
var discordMessage = await _rest.GetMessageOrNull(msg.Channel, msg.OriginalMid);
if (discordMessage != null)
content = discordMessage.Content;
}
List<MessageComponent> body = [
new MessageComponent()
{
Type = ComponentType.Text,
Content = $"### Command response message\n**Original message:** https://discord.com/channels/{msg.Guild}/{msg.Channel}/{msg.OriginalMid}\n**Sent By:** <@{msg.Sender}>"
},
new MessageComponent()
{
Type = ComponentType.Separator,
},
new MessageComponent()
{
Type = ComponentType.Text,
Content = content
},
];
MessageComponent footer = new MessageComponent()
{
Type = ComponentType.Text,
Content = $"-# Original Message ID: {msg.OriginalMid} · {DiscordUtils.InstantToTimestampString(DiscordUtils.SnowflakeToInstant(msg.OriginalMid))}"
};
return [
new MessageComponent(){
Type = ComponentType.Container,
Components = [
..body
]
},
footer
];
}
public async Task<Embed> CreateCommandMessageInfoEmbed(Core.CommandMessage msg, bool showContent)
{
var content = "*(command message deleted or inaccessible)*";

View file

@ -45,6 +45,9 @@ public static class DiscordUtils
public static ulong InstantToSnowflake(Instant time) =>
(ulong)(time - Instant.FromUtc(2015, 1, 1, 0, 0, 0)).TotalMilliseconds << 22;
public static string InstantToTimestampString(Instant time) =>
$"<t:{time.ToUnixTimeSeconds()}:f>";
public static async Task CreateReactionsBulk(this DiscordApiClient rest, Message msg, string[] reactions)
{
foreach (var reaction in reactions)

View file

@ -5,6 +5,7 @@ using Autofac;
using Myriad.Cache;
using Myriad.Gateway;
using Myriad.Rest;
using Myriad.Rest.Types;
using Myriad.Types;
using PluralKit.Core;
@ -76,13 +77,14 @@ public class InteractionContext
});
}
public async Task Reply(MessageComponent[] components = null)
public async Task Reply(MessageComponent[] components = null, AllowedMentions? mentions = null)
{
await Respond(InteractionResponse.ResponseType.ChannelMessageWithSource,
new InteractionApplicationCommandCallbackData
{
Components = components,
Flags = Message.MessageFlags.Ephemeral | Message.MessageFlags.IsComponentsV2
Flags = Message.MessageFlags.Ephemeral | Message.MessageFlags.IsComponentsV2,
AllowedMentions = mentions ?? new AllowedMentions()
});
}

View file

@ -6,7 +6,7 @@ use std::sync::Arc;
use tokio::sync::mpsc::Sender;
use tracing::{error, info, warn};
use twilight_gateway::{
CloseFrame, ConfigBuilder, Event, EventTypeFlags, Message, Shard, ShardId, create_iterator,
ConfigBuilder, Event, EventTypeFlags, Message, Shard, ShardId, create_iterator,
};
use twilight_model::gateway::{
Intents,
@ -118,8 +118,11 @@ pub async fn runner(
Message::Close(frame) => {
let mut state_event = ShardStateEvent::Closed;
let close_code = if let Some(close) = frame {
if close == CloseFrame::RESUME {
state_event = ShardStateEvent::Reconnect;
match close.code {
4000..=4003 | 4005..=4009 => {
state_event = ShardStateEvent::Reconnect;
}
_ => {}
}
close.code.to_string()
} else {
@ -176,32 +179,45 @@ pub async fn runner(
)
.increment(1);
// update shard state and discord cache
if matches!(event, Event::Ready(_)) || matches!(event, Event::Resumed) {
if let Err(error) = tx_state.try_send((
shard.id(),
ShardStateEvent::Other,
Some(event.clone()),
None,
)) {
tracing::error!(?error, "error updating shard state");
// check for shard status events
match event {
Event::Ready(_) | Event::Resumed => {
if let Err(error) = tx_state.try_send((
shard.id(),
ShardStateEvent::Other,
Some(event.clone()),
None,
)) {
tracing::error!(?error, "error updating shard state");
}
}
}
// need to do heartbeat separately, to get the latency
let latency_num = shard
.latency()
.recent()
.first()
.map_or_else(|| 0, |d| d.as_millis()) as i32;
if let Event::GatewayHeartbeatAck = event
&& let Err(error) = tx_state.try_send((
shard.id(),
ShardStateEvent::Heartbeat,
Some(event.clone()),
Some(latency_num),
))
{
tracing::error!(?error, "error updating shard state for latency");
Event::GatewayReconnect => {
if let Err(error) = tx_state.try_send((
shard.id(),
ShardStateEvent::Reconnect,
Some(event.clone()),
None,
)) {
tracing::error!(?error, "error updating shard state for reconnect");
}
}
Event::GatewayHeartbeatAck => {
// need to do heartbeat separately, to get the latency
let latency_num = shard
.latency()
.recent()
.first()
.map_or_else(|| 0, |d| d.as_millis()) as i32;
if let Err(error) = tx_state.try_send((
shard.id(),
ShardStateEvent::Heartbeat,
Some(event.clone()),
Some(latency_num),
)) {
tracing::error!(?error, "error updating shard state for latency");
}
}
_ => {}
}
if let Event::Ready(_) = event {

View file

@ -1,6 +1,6 @@
-- database version 54
-- add config option for list format
-- add short/full option for fronter list
alter table system_config add column fronter_list_format text default 'short';
alter table system_config add column fronter_list_format int default 0;
update info set schema_version = 54;

View file

@ -99,4 +99,9 @@ It is not possible to edit messages via ID. Please use the full link, or reply t
You cannot reply-@ a proxied messages due to their nature as webhooks. If you want to "reply-@" a proxied message, you must react to the message with 🔔, 🛎, or 🏓. This will send a message from PluralKit that reads "Psst, MEMBER (@User), you have been pinged by @You", which will ping the Discord account behind the proxied message.
### Why do most of PluralKit's messages look blank or empty?
A lot of PluralKit's command responses use Discord embeds. If you can't see them, it's likely you have embeds turned off. To change this, go into your discord settings and find the tab "Chat" under "App Settings". Find the setting "Show embeds and preview website links" and turn it on. If it's already on, try turning it off and then on again.
PluralKit now uses Discord's "Components V2" for system/member/group cards - if the cards no longer show, your Discord app is too old to show the new components, and you should update it.
A temporary workaround to show the old version of the cards exists as the -show-embed (or -se) flag to pk;system / pk;member / pk;group - however, we will be removing the old embed-based cards in the future (and as such, we will not add a config option to always use the old cards).
Please read the announcement post for more details: <https://pluralkit.me/posts/2025-09-08-components-v2/>
Some of PluralKit's command responses still use Discord embeds. If you can't see them, it's likely you have embeds turned off. To change this, go into your discord settings and find the tab "Chat" under "App Settings". Find the setting "Show embeds and preview website links" and turn it on. If it's already on, try turning it off and then on again.