Compare commits

...

14 commits

Author SHA1 Message Date
Iris System
e720883ec1 feat(bot): add toggle for color codes on cv2 cards
Some checks failed
Build dashboard Docker image / dashboard docker build (push) Has been cancelled
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-09-05 14:41:50 +12:00
Iris System
a2417e5f06 fix(bot): smaller header for cv2 cards 2025-09-05 14:41:37 +12:00
Iris System
345d067096 feat(bot): use CV2 cards in random commands 2025-09-05 14:41:25 +12:00
Iris System
364c71c76c feat(bot): use button for cv2 cards dashboard link 2025-09-05 14:41:14 +12:00
Iris System
e031a261a0 fix(bot): add cv2 separator between proxy tags & groups 2025-09-05 14:41:03 +12:00
Iris System
0afcee108e feat(bot): allow querying legacy embeds with -se flag 2025-09-05 14:40:52 +12:00
Iris System
41478f0fef fix(bot): more null/empty-string checks in CV2 cards 2025-09-05 14:40:39 +12:00
Iris System
02b9783abe feat(bot): initial ComponentsV2 implementation 2025-09-05 14:40:23 +12:00
asleepyskye
2248403140 fix(flake): change systems url to default
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-01 21:33:16 -04:00
asleepyskye
b3eb108a13 chore(bot): update wording on error message 2025-09-01 21:23:24 -04:00
asleepyskye
0cd351de45 fix(gateway): add missing 'if' to shard state
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-08-31 22:46:51 -04:00
asleepyskye
65b40c498b chore(gateway): format
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-08-27 22:34:19 -04:00
asleepyskye
1378379e14 feat(gateway): add reconnect timestamp to shard state 2025-08-27 22:31:23 -04:00
asleepyskye
2fc5f2a9d9 chore: add link to new status page to docs, dash
Some checks failed
Build dashboard Docker image / dashboard docker build (push) Has been cancelled
2025-08-27 17:22:04 -04:00
30 changed files with 638 additions and 36 deletions

View file

@ -17,7 +17,7 @@ reqwest = { version = "0.12.7" , default-features = false, features = ["rustls-t
sentry = { version = "0.36.0", default-features = false, features = ["backtrace", "contexts", "panic", "debug-images", "reqwest", "rustls"] } # replace native-tls with rustls sentry = { version = "0.36.0", default-features = false, features = ["backtrace", "contexts", "panic", "debug-images", "reqwest", "rustls"] } # replace native-tls with rustls
serde = { version = "1.0.196", features = ["derive"] } serde = { version = "1.0.196", features = ["derive"] }
serde_json = "1.0.117" serde_json = "1.0.117"
sqlx = { version = "0.8.2", features = ["runtime-tokio", "postgres", "time", "macros", "uuid"] } sqlx = { version = "0.8.2", features = ["runtime-tokio", "postgres", "time", "chrono", "macros", "uuid"] }
tokio = { version = "1.36.0", features = ["full"] } tokio = { version = "1.36.0", features = ["full"] }
tracing = "0.1" tracing = "0.1"
tracing-subscriber = { version = "0.3.16", features = ["env-filter", "json"] } tracing-subscriber = { version = "0.3.16", features = ["env-filter", "json"] }

View file

@ -9,6 +9,7 @@ public record MessageRequest
public bool Tts { get; set; } public bool Tts { get; set; }
public AllowedMentions? AllowedMentions { get; set; } public AllowedMentions? AllowedMentions { get; set; }
public Embed[]? Embeds { get; set; } public Embed[]? Embeds { get; set; }
public Message.MessageFlags Flags { get; set; }
public MessageComponent[]? Components { get; set; } public MessageComponent[]? Components { get; set; }
public Message.Reference? MessageReference { get; set; } public Message.Reference? MessageReference { get; set; }
} }

View file

@ -0,0 +1,13 @@
namespace Myriad.Types;
public record ComponentMedia
{
public string? Url { get; init; }
}
public record ComponentMediaItem
{
public ComponentMedia Media { get; init; }
public string? Description { get; init; }
public bool Spoiler { get; init; } = false;
}

View file

@ -3,5 +3,12 @@ namespace Myriad.Types;
public enum ComponentType public enum ComponentType
{ {
ActionRow = 1, ActionRow = 1,
Button = 2 Button = 2,
StringSelect = 3,
Section = 9,
Text = 10,
Thumbnail = 11,
MediaGallery = 12,
Separator = 14,
Container = 17,
} }

View file

@ -5,9 +5,15 @@ public record MessageComponent
public ComponentType Type { get; init; } public ComponentType Type { get; init; }
public ButtonStyle? Style { get; set; } public ButtonStyle? Style { get; set; }
public string? Label { get; init; } public string? Label { get; init; }
public string? Content { get; init; }
public Emoji? Emoji { get; init; } public Emoji? Emoji { get; init; }
public string? CustomId { get; init; } public string? CustomId { get; init; }
public string? Url { get; init; } public string? Url { get; init; }
public bool? Disabled { get; init; } public bool? Disabled { get; init; }
public uint? AccentColor { get; init; }
public ComponentMedia? Media { get; init; }
public ComponentMediaItem[]? Items { get; init; }
public MessageComponent? Accessory { get; init; }
public MessageComponent[]? Components { get; init; } public MessageComponent[]? Components { get; init; }
} }

View file

@ -17,6 +17,7 @@ public record Message
Ephemeral = 1 << 6, Ephemeral = 1 << 6,
SuppressNotifications = 1 << 12, SuppressNotifications = 1 << 12,
VoiceMessage = 1 << 13, VoiceMessage = 1 << 13,
IsComponentsV2 = 1 << 15,
} }
public enum MessageType public enum MessageType
@ -73,8 +74,6 @@ public record Message
public MessagePoll? Poll { get; init; } public MessagePoll? Poll { get; init; }
// public MessageComponent[]? Components { get; init; }
public record Reference(ulong? GuildId, ulong? ChannelId, ulong? MessageId); public record Reference(ulong? GuildId, ulong? ChannelId, ulong? MessageId);
public record MessageActivity(int Type, string PartyId); public record MessageActivity(int Type, string PartyId);

View file

@ -139,7 +139,8 @@ public class ApplicationCommandProxiedMessage
if (member == null || !(await _cache.PermissionsForMemberInChannel(ctx.GuildId, 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."); throw new PKError("You do not have permission to send messages in this channel.");
}; }
;
var config = await _repo.GetSystemConfig(msg.System.Id); var config = await _repo.GetSystemConfig(msg.System.Id);

View file

@ -592,6 +592,8 @@ public partial class CommandTree
return ctx.Execute<Config>(null, m => m.HidDisplayCaps(ctx)); 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")) 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)); 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")) if (ctx.MatchMultiple(new[] { "name" }, new[] { "format" }) || ctx.Match("nameformat", "nf"))
return ctx.Execute<Config>(null, m => m.NameFormat(ctx)); return ctx.Execute<Config>(null, m => m.NameFormat(ctx));
if (ctx.MatchMultiple(new[] { "member", "group" }, new[] { "limit" }) || ctx.Match("limit")) if (ctx.MatchMultiple(new[] { "member", "group" }, new[] { "limit" }) || ctx.Match("limit"))

View file

@ -119,6 +119,41 @@ public class Context
return msg; 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) public async Task Execute<T>(Command? commandDef, Func<T, Task> handler, bool deprecated = false)
{ {
_currentCommand = commandDef; _currentCommand = commandDef;

View file

@ -123,6 +123,13 @@ public class Config
"off" "off"
)); ));
items.Add(new(
"show color",
"Whether to show color codes in system/member/group cards",
EnabledDisabled(ctx.Config.CardShowColorHex),
"disabled"
));
items.Add(new( items.Add(new(
"Proxy Switch", "Proxy Switch",
"Switching behavior when proxy tags are used", "Switching behavior when proxy tags are used",
@ -570,6 +577,20 @@ public class Config
else throw new PKError(badInputError); 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) public async Task ProxySwitch(Context ctx)
{ {
if (!ctx.HasNext()) if (!ctx.HasNext())

View file

@ -520,7 +520,13 @@ public class Groups
public async Task ShowGroupCard(Context ctx, PKGroup target) public async Task ShowGroupCard(Context ctx, PKGroup target)
{ {
var system = await GetGroupSystem(ctx, 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) public async Task GroupPrivacy(Context ctx, PKGroup target, PrivacyLevel? newValueFromCommand)

View file

@ -122,8 +122,16 @@ public class Member
public async Task ViewMember(Context ctx, PKMember target) public async Task ViewMember(Context ctx, PKMember target)
{ {
var system = await ctx.Repository.GetSystem(target.System); 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( 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) public async Task Soulscream(Context ctx, PKMember target)

View file

@ -36,8 +36,17 @@ public class Random
"This system has no members!"); "This system has no members!");
var randInt = randGen.Next(members.Count); 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) public async Task Group(Context ctx, PKSystem target)
@ -60,7 +69,17 @@ public class Random
$"This system has no groups!"); $"This system has no groups!");
var randInt = randGen.Next(groups.Count()); 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) public async Task GroupMember(Context ctx, PKGroup group)
@ -92,7 +111,16 @@ public class Random
system = await ctx.Repository.GetSystem(group.System); system = await ctx.Repository.GetSystem(group.System);
var randInt = randGen.Next(ms.Count); 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) public async Task Query(Context ctx, PKSystem system)
{ {
if (system == null) throw Errors.NoSystemError(ctx.DefaultPrefix); 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)));
return;
}
await ctx.Reply(embed: await _embeds.CreateSystemEmbed(ctx, system, ctx.LookupContextFor(system.Id))); await ctx.Reply(components: await _embeds.CreateSystemMessageComponents(ctx, system, ctx.LookupContextFor(system.Id)));
} }
public async Task New(Context ctx) public async Task New(Context ctx)

View file

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

View file

@ -15,17 +15,21 @@ namespace PluralKit.Bot;
public class EmbedService 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 IDiscordCache _cache;
private readonly IDatabase _db; private readonly IDatabase _db;
private readonly ModelRepository _repo; private readonly ModelRepository _repo;
private readonly DiscordApiClient _rest; 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; _db = db;
_repo = repo; _repo = repo;
_cache = cache; _cache = cache;
_rest = rest; _rest = rest;
_coreConfig = coreConfig;
} }
private Task<(ulong Id, User? User)[]> GetUsers(IEnumerable<ulong> ids) private Task<(ulong Id, User? User)[]> GetUsers(IEnumerable<ulong> ids)
@ -39,6 +43,169 @@ public class EmbedService
return Task.WhenAll(ids.Select(Inner)); 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.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)
{
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) public async Task<Embed> CreateSystemEmbed(Context cctx, PKSystem system, LookupContext ctx)
{ {
// Fetch/render info for all accounts simultaneously // Fetch/render info for all accounts simultaneously
@ -61,7 +228,7 @@ public class EmbedService
.Footer(new Embed.EmbedFooter( .Footer(new Embed.EmbedFooter(
$"System ID: {system.DisplayHid(cctx.Config)} | Created on {system.Created.FormatZoned(cctx.Zone)}")) $"System ID: {system.DisplayHid(cctx.Config)} | Created on {system.Created.FormatZoned(cctx.Zone)}"))
.Color(system.Color?.ToDiscordColor()) .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); var avatar = system.AvatarFor(ctx);
if (avatar != null) if (avatar != null)
@ -164,6 +331,158 @@ public class EmbedService
return embed.Build(); 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.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)
{
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) 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)); // string FormatTimestamp(Instant timestamp) => DateTimeFormats.ZonedDateTimeFormat.Format(timestamp.InZone(system.Zone));
@ -188,7 +507,7 @@ public class EmbedService
.ToListAsync(); .ToListAsync();
var eb = new EmbedBuilder() 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) // .WithColor(member.ColorPrivacy.CanAccess(ctx) ? color : null)
.Color(member.Color?.ToDiscordColor()) .Color(member.Color?.ToDiscordColor())
.Footer(new Embed.EmbedFooter( .Footer(new Embed.EmbedFooter(
@ -241,6 +560,119 @@ public class EmbedService
return eb.Build(); 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.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)
{
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) public async Task<Embed> CreateGroupEmbed(Context ctx, PKSystem system, PKGroup target)
{ {
var pctx = ctx.LookupContextFor(system.Id); var pctx = ctx.LookupContextFor(system.Id);
@ -266,7 +698,7 @@ public class EmbedService
nameField = $"{nameField}"; nameField = $"{nameField}";
var eb = new EmbedBuilder() 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()); .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)}" : "")}")); 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() return new EmbedBuilder()
.Color(0xE74C3C) .Color(0xE74C3C)
.Title("Internal error occurred") .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)) .Footer(new Embed.EmbedFooter(errorId))
.Timestamp(now.ToDateTimeOffset().ToString("O")) .Timestamp(now.ToDateTimeOffset().ToString("O"))
.Build(); .Build();

View file

@ -16,6 +16,7 @@ public class CoreConfig
public string? SeqLogUrl { get; set; } public string? SeqLogUrl { get; set; }
public string? DispatchProxyUrl { get; set; } public string? DispatchProxyUrl { get; set; }
public string? DispatchProxyToken { get; set; } public string? DispatchProxyToken { get; set; }
public string DashboardBaseUrl { get; set; } = "https://dash.pluralkit.local";
public LogEventLevel ConsoleLogLevel { get; set; } = LogEventLevel.Debug; public LogEventLevel ConsoleLogLevel { get; set; } = LogEventLevel.Debug;
public LogEventLevel ElasticLogLevel { get; set; } = LogEventLevel.Information; public LogEventLevel ElasticLogLevel { get; set; } = LogEventLevel.Information;

View file

@ -22,6 +22,7 @@ public class SystemConfigPatch: PatchObject
public Partial<bool> ProxyErrorMessageEnabled { get; set; } public Partial<bool> ProxyErrorMessageEnabled { get; set; }
public Partial<bool> HidDisplaySplit { get; set; } public Partial<bool> HidDisplaySplit { get; set; }
public Partial<bool> HidDisplayCaps { get; set; } public Partial<bool> HidDisplayCaps { get; set; }
public Partial<bool> CardShowColorHex { get; set; }
public Partial<string?> NameFormat { get; set; } public Partial<string?> NameFormat { get; set; }
public Partial<SystemConfig.HidPadFormat> HidListPadding { get; set; } public Partial<SystemConfig.HidPadFormat> HidListPadding { get; set; }
public Partial<SystemConfig.ProxySwitchAction> ProxySwitch { get; set; } public Partial<SystemConfig.ProxySwitchAction> ProxySwitch { get; set; }
@ -41,6 +42,7 @@ public class SystemConfigPatch: PatchObject
.With("hid_display_split", HidDisplaySplit) .With("hid_display_split", HidDisplaySplit)
.With("hid_display_caps", HidDisplayCaps) .With("hid_display_caps", HidDisplayCaps)
.With("hid_list_padding", HidListPadding) .With("hid_list_padding", HidListPadding)
.With("card_show_color_hex", CardShowColorHex)
.With("proxy_switch", ProxySwitch) .With("proxy_switch", ProxySwitch)
.With("name_format", NameFormat) .With("name_format", NameFormat)
); );
@ -107,6 +109,9 @@ public class SystemConfigPatch: PatchObject
if (HidListPadding.IsPresent) if (HidListPadding.IsPresent)
o.Add("hid_list_padding", HidListPadding.Value.ToUserString()); o.Add("hid_list_padding", HidListPadding.Value.ToUserString());
if (CardShowColorHex.IsPresent)
o.Add("card_show_color_hex", CardShowColorHex.Value);
if (ProxySwitch.IsPresent) if (ProxySwitch.IsPresent)
o.Add("proxy_switch", ProxySwitch.Value.ToUserString()); o.Add("proxy_switch", ProxySwitch.Value.ToUserString());
@ -150,6 +155,9 @@ public class SystemConfigPatch: PatchObject
if (o.ContainsKey("hid_display_caps")) if (o.ContainsKey("hid_display_caps"))
patch.HidDisplayCaps = o.Value<bool>("hid_display_caps"); patch.HidDisplayCaps = o.Value<bool>("hid_display_caps");
if (o.ContainsKey("card_show_color_hex"))
patch.CardShowColorHex = o.Value<bool>("card_show_color_hex");
if (o.ContainsKey("proxy_switch")) if (o.ContainsKey("proxy_switch"))
patch.ProxySwitch = o.Value<string>("proxy_switch") switch patch.ProxySwitch = o.Value<string>("proxy_switch") switch
{ {

View file

@ -23,6 +23,7 @@ public class SystemConfig
public bool ProxyErrorMessageEnabled { get; } public bool ProxyErrorMessageEnabled { get; }
public bool HidDisplaySplit { get; } public bool HidDisplaySplit { get; }
public bool HidDisplayCaps { get; } public bool HidDisplayCaps { get; }
public bool CardShowColorHex { get; }
public HidPadFormat HidListPadding { get; } public HidPadFormat HidListPadding { get; }
public ProxySwitchAction ProxySwitch { get; } public ProxySwitchAction ProxySwitch { get; }
public string NameFormat { get; } public string NameFormat { get; }
@ -60,6 +61,7 @@ public static class SystemConfigExt
o.Add("hid_display_split", cfg.HidDisplaySplit); o.Add("hid_display_split", cfg.HidDisplaySplit);
o.Add("hid_display_caps", cfg.HidDisplayCaps); o.Add("hid_display_caps", cfg.HidDisplayCaps);
o.Add("hid_list_padding", cfg.HidListPadding.ToUserString()); o.Add("hid_list_padding", cfg.HidListPadding.ToUserString());
o.Add("card_show_color_hex", cfg.CardShowColorHex);
o.Add("proxy_switch", cfg.ProxySwitch.ToUserString()); o.Add("proxy_switch", cfg.ProxySwitch.ToUserString());
o.Add("name_format", cfg.NameFormat); o.Add("name_format", cfg.NameFormat);

View file

@ -6,7 +6,7 @@ use std::sync::Arc;
use tokio::sync::mpsc::Sender; use tokio::sync::mpsc::Sender;
use tracing::{error, info, warn}; use tracing::{error, info, warn};
use twilight_gateway::{ use twilight_gateway::{
create_iterator, ConfigBuilder, Event, EventTypeFlags, Message, Shard, ShardId, create_iterator, CloseFrame, ConfigBuilder, Event, EventTypeFlags, Message, Shard, ShardId,
}; };
use twilight_model::gateway::{ use twilight_model::gateway::{
payload::outgoing::update_presence::UpdatePresencePayload, payload::outgoing::update_presence::UpdatePresencePayload,
@ -116,7 +116,11 @@ pub async fn runner(
let raw_event = match item { let raw_event = match item {
Ok(evt) => match evt { Ok(evt) => match evt {
Message::Close(frame) => { Message::Close(frame) => {
let mut state_event = ShardStateEvent::Closed;
let close_code = if let Some(close) = frame { let close_code = if let Some(close) = frame {
if close == CloseFrame::RESUME {
state_event = ShardStateEvent::Reconnect;
}
close.code.to_string() close.code.to_string()
} else { } else {
"unknown".to_string() "unknown".to_string()
@ -131,9 +135,7 @@ pub async fn runner(
) )
.increment(1); .increment(1);
if let Err(error) = if let Err(error) = tx_state.try_send((shard.id(), state_event, None, None)) {
tx_state.try_send((shard.id(), ShardStateEvent::Closed, None, None))
{
error!("failed to update shard state for socket closure: {error}"); error!("failed to update shard state for socket closure: {error}");
} }

View file

@ -86,7 +86,7 @@ impl ShardStateManager {
Ok(()) Ok(())
} }
pub async fn socket_closed(&self, shard_id: u32) -> anyhow::Result<()> { pub async fn socket_closed(&self, shard_id: u32, reconnect: bool) -> anyhow::Result<()> {
gauge!("pluralkit_gateway_shard_up").decrement(1); gauge!("pluralkit_gateway_shard_up").decrement(1);
let mut info = self let mut info = self
@ -97,6 +97,9 @@ impl ShardStateManager {
info.shard_id = shard_id as i32; info.shard_id = shard_id as i32;
info.cluster_id = Some(cluster_config().node_id as i32); info.cluster_id = Some(cluster_config().node_id as i32);
info.up = false; info.up = false;
if reconnect {
info.last_reconnect = chrono::offset::Utc::now().timestamp() as i32
}
info.disconnection_count += 1; info.disconnection_count += 1;
self.save_shard(shard_id, info).await?; self.save_shard(shard_id, info).await?;

View file

@ -109,8 +109,16 @@ async fn main() -> anyhow::Result<()> {
}; };
} }
ShardStateEvent::Closed => { ShardStateEvent::Closed => {
if let Err(error) = shard_state.socket_closed(shard_id.number()).await { if let Err(error) =
error!("failed to update shard state for heartbeat: {error}") shard_state.socket_closed(shard_id.number(), false).await
{
error!("failed to update shard state for closed: {error}")
};
}
ShardStateEvent::Reconnect => {
if let Err(error) = shard_state.socket_closed(shard_id.number(), true).await
{
error!("failed to update shard state for reconnect: {error}")
}; };
} }
ShardStateEvent::Other => { ShardStateEvent::Other => {
@ -121,7 +129,7 @@ async fn main() -> anyhow::Result<()> {
) )
.await .await
{ {
error!("failed to update shard state for heartbeat: {error}") error!("failed to update shard state for other evt: {error}")
}; };
} }
} }

View file

@ -8,11 +8,13 @@ pub struct ShardState {
/// unix timestamp /// unix timestamp
pub last_heartbeat: i32, pub last_heartbeat: i32,
pub last_connection: i32, pub last_connection: i32,
pub last_reconnect: i32,
pub cluster_id: Option<i32>, pub cluster_id: Option<i32>,
} }
pub enum ShardStateEvent { pub enum ShardStateEvent {
Closed, Closed,
Heartbeat, Heartbeat,
Reconnect,
Other, Other,
} }

View file

@ -0,0 +1,6 @@
-- database version 53
-- add toggle for showing color codes on cv2 cards
alter table system_config add column card_show_color_hex bool default false;
update info set schema_version = 53;

View file

@ -64,6 +64,10 @@ func main() {
createEmbed(rw, r) createEmbed(rw, r)
}) })
r.Get("/status", func(rw http.ResponseWriter, r *http.Request) {
http.Redirect(rw, r, "https://status.pluralkit.me/", http.StatusMovedPermanently)
})
http.ListenAndServe(":8080", r) http.ListenAndServe(":8080", r)
} }

View file

@ -53,7 +53,7 @@
<Link to="/profile" class="nav-link">Public</Link> <Link to="/profile" class="nav-link">Public</Link>
</NavItem> </NavItem>
<NavItem> <NavItem>
<Link to="/status" class="nav-link">Bot status</Link> <a href="https://status.pluralkit.me/" class="nav-link">Bot status</a>
</NavItem> </NavItem>
</Nav> </Nav>
</Collapse> </Collapse>

View file

@ -18,7 +18,7 @@ module.exports = {
}, },
themeConfig: { themeConfig: {
repo: 'PluralKit/PluralKit', repo: false,
docsDir: 'docs/content/', docsDir: 'docs/content/',
docsBranch: 'main', docsBranch: 'main',
editLinks: true, editLinks: true,
@ -29,7 +29,8 @@ module.exports = {
nav: [ nav: [
{ text: "Web dashboard", link: "https://dash.pluralkit.me" }, { text: "Web dashboard", link: "https://dash.pluralkit.me" },
{ text: "Support server", link: "https://discord.gg/PczBt78" }, { text: "Support server", link: "https://discord.gg/PczBt78" },
{ text: "Invite bot", link: "https://discord.com/oauth2/authorize?client_id=466378653216014359&scope=bot%20applications.commands&permissions=536995904" } { text: "Invite bot", link: "https://discord.com/oauth2/authorize?client_id=466378653216014359&scope=bot%20applications.commands&permissions=536995904" },
{ text: "Bot status", link: "https://status.pluralkit.me/" }
], ],
sidebar: [ sidebar: [
"/", "/",

10
flake.lock generated
View file

@ -297,16 +297,16 @@
}, },
"systems": { "systems": {
"locked": { "locked": {
"lastModified": 1680978846, "lastModified": 1681028828,
"narHash": "sha256-Gtqg8b/v49BFDpDetjclCYXm8mAnTrUzR0JnE2nv5aw=", "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems", "owner": "nix-systems",
"repo": "x86_64-linux", "repo": "default",
"rev": "2ecfcac5e15790ba6ce360ceccddb15ad16d08a8", "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github" "type": "github"
}, },
"original": { "original": {
"owner": "nix-systems", "owner": "nix-systems",
"repo": "x86_64-linux", "repo": "default",
"type": "github" "type": "github"
} }
}, },

View file

@ -4,7 +4,7 @@
inputs = { inputs = {
nixpkgs.url = "nixpkgs/nixpkgs-unstable"; nixpkgs.url = "nixpkgs/nixpkgs-unstable";
parts.url = "github:hercules-ci/flake-parts"; parts.url = "github:hercules-ci/flake-parts";
systems.url = "github:nix-systems/x86_64-linux"; systems.url = "github:nix-systems/default";
# process compose # process compose
process-compose.url = "github:Platonic-Systems/process-compose-flake"; process-compose.url = "github:Platonic-Systems/process-compose-flake";
services.url = "github:juspay/services-flake"; services.url = "github:juspay/services-flake";