diff --git a/.gitignore b/.gitignore index 35c2cd6f..3c4fe014 100644 --- a/.gitignore +++ b/.gitignore @@ -17,4 +17,4 @@ pluralkit.*.conf *.DotSettings.user # Generated -logs/ \ No newline at end of file +logs/ diff --git a/PluralKit.Bot/Commands/CommandTree.cs b/PluralKit.Bot/Commands/CommandTree.cs index 6363c5e7..b76d6261 100644 --- a/PluralKit.Bot/Commands/CommandTree.cs +++ b/PluralKit.Bot/Commands/CommandTree.cs @@ -13,6 +13,7 @@ namespace PluralKit.Bot public static Command SystemNew = new Command("system new", "system new [name]", "Creates a new system"); public static Command SystemRename = new Command("system name", "system rename [name]", "Renames your system"); public static Command SystemDesc = new Command("system description", "system description [description]", "Changes your system's description"); + public static Command SystemColor = new Command("system color", "system color [color]", "Changes your system's color"); public static Command SystemTag = new Command("system tag", "system tag [tag]", "Changes your system's tag"); public static Command SystemAvatar = new Command("system icon", "system icon [url|@mention]", "Changes your system's icon"); public static Command SystemDelete = new Command("system delete", "system delete", "Deletes your system"); @@ -55,6 +56,7 @@ namespace PluralKit.Bot public static Command GroupRename = new Command("group rename", "group rename ", "Renames a group"); public static Command GroupDisplayName = new Command("group displayname", "group displayname [display name]", "Changes a group's display name"); public static Command GroupDesc = new Command("group description", "group description [description]", "Changes a group's description"); + public static Command GroupColor = new Command("group color", "group color [color]", "Changes a group's color"); public static Command GroupAdd = new Command("group add", "group add [member 2] [member 3...]", "Adds one or more members to a group"); public static Command GroupRemove = new Command("group remove", "group remove [member 2] [member 3...]", "Removes one or more members from a group"); public static Command GroupPrivacy = new Command("group privacy", "group privacy ", "Changes a group's privacy settings"); @@ -88,7 +90,7 @@ namespace PluralKit.Bot public static Command PermCheck = new Command("permcheck", "permcheck ", "Checks whether a server's permission setup is correct"); public static Command[] SystemCommands = { - SystemInfo, SystemNew, SystemRename, SystemTag, SystemDesc, SystemAvatar, SystemDelete, SystemTimezone, + SystemInfo, SystemNew, SystemRename, SystemTag, SystemDesc, SystemAvatar, SystemColor, SystemDelete, SystemTimezone, SystemList, SystemFronter, SystemFrontHistory, SystemFrontPercent, SystemPrivacy, SystemProxy }; @@ -101,7 +103,7 @@ namespace PluralKit.Bot public static Command[] GroupCommands = { GroupInfo, GroupList, GroupNew, GroupAdd, GroupRemove, GroupMemberList, GroupRename, GroupDesc, - GroupIcon, GroupPrivacy, GroupDelete + GroupIcon, GroupColor, GroupPrivacy, GroupDelete }; public static Command[] GroupCommandsTargeted = @@ -217,6 +219,8 @@ namespace PluralKit.Bot await ctx.Execute(SystemTag, m => m.Tag(ctx)); else if (ctx.Match("description", "desc", "bio")) await ctx.Execute(SystemDesc, m => m.Description(ctx)); + else if (ctx.Match("color", "colour")) + await ctx.Execute(SystemColor, m => m.Color(ctx)); else if (ctx.Match("avatar", "picture", "icon", "image", "pic", "pfp")) await ctx.Execute(SystemAvatar, m => m.Avatar(ctx)); else if (ctx.Match("delete", "remove", "destroy", "erase", "yeet")) @@ -397,6 +401,8 @@ namespace PluralKit.Bot await ctx.Execute(GroupDelete, g => g.DeleteGroup(ctx, target)); else if (ctx.Match("avatar", "picture", "icon", "image", "pic", "pfp")) await ctx.Execute(GroupIcon, g => g.GroupIcon(ctx, target)); + else if (ctx.Match("color", "colour")) + await ctx.Execute(GroupColor, g => g.GroupColor(ctx, target)); else if (!ctx.HasNext()) await ctx.Execute(GroupInfo, g => g.ShowGroupCard(ctx, target)); else diff --git a/PluralKit.Bot/Commands/Groups.cs b/PluralKit.Bot/Commands/Groups.cs index ce93f8fe..eb23c685 100644 --- a/PluralKit.Bot/Commands/Groups.cs +++ b/PluralKit.Bot/Commands/Groups.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Linq; using System.Text; +using System.Text.RegularExpressions; using System.Threading.Tasks; using Dapper; @@ -224,6 +225,53 @@ namespace PluralKit.Bot else await ShowIcon(); } + public async Task GroupColor(Context ctx, PKGroup target) + { + var color = ctx.RemainderOrNull(); + if (await ctx.MatchClear()) + { + ctx.CheckOwnGroup(target); + + var patch = new GroupPatch {Color = Partial.Null()}; + await _db.Execute(conn => _repo.UpdateGroup(conn, target.Id, patch)); + + await ctx.Reply($"{Emojis.Success} Group color cleared."); + } + else if (!ctx.HasNext()) + { + + if (target.Color == null) + if (ctx.System?.Id == target.System) + await ctx.Reply( + $"This group does not have a color set. To set one, type `pk;group {target.Reference()} color `."); + else + await ctx.Reply("This group does not have a color set."); + else + await ctx.Reply(embed: new EmbedBuilder() + .Title("Group color") + .Color(target.Color.ToDiscordColor()) + .Thumbnail(new($"https://fakeimg.pl/256x256/{target.Color}/?text=%20")) + .Description($"This group's color is **#{target.Color}**." + + (ctx.System?.Id == target.System ? $" To clear it, type `pk;group {target.Reference()} color -clear`." : "")) + .Build()); + } + else + { + ctx.CheckOwnGroup(target); + + if (color.StartsWith("#")) color = color.Substring(1); + if (!Regex.IsMatch(color, "^[0-9a-fA-F]{6}$")) throw Errors.InvalidColorError(color); + + var patch = new GroupPatch {Color = Partial.Present(color.ToLowerInvariant())}; + await _db.Execute(conn => _repo.UpdateGroup(conn, target.Id, patch)); + + await ctx.Reply(embed: new EmbedBuilder() + .Title($"{Emojis.Success} Group color changed.") + .Color(color.ToDiscordColor()) + .Thumbnail(new($"https://fakeimg.pl/256x256/{color}/?text=%20")) + .Build()); + } + } public async Task ListSystemGroups(Context ctx, PKSystem system) { @@ -263,7 +311,7 @@ namespace PluralKit.Bot } var title = system.Name != null ? $"Groups of {system.Name} (`{system.Hid}`)" : $"Groups of `{system.Hid}`"; - await ctx.Paginate(groups.ToAsyncEnumerable(), groups.Count, 25, title, Renderer); + await ctx.Paginate(groups.ToAsyncEnumerable(), groups.Count, 25, title, ctx.System.Color, Renderer); Task Renderer(EmbedBuilder eb, IEnumerable page) { @@ -342,7 +390,7 @@ namespace PluralKit.Bot if (opts.Search != null) title.Append($" matching **{opts.Search}**"); - await ctx.RenderMemberList(ctx.LookupContextFor(target.System), _db, target.System, title.ToString(), opts); + await ctx.RenderMemberList(ctx.LookupContextFor(target.System), _db, target.System, title.ToString(), target.Color, opts); } public enum AddRemoveOperation diff --git a/PluralKit.Bot/Commands/Lists/ContextListExt.cs b/PluralKit.Bot/Commands/Lists/ContextListExt.cs index acca3b5f..fa990c4b 100644 --- a/PluralKit.Bot/Commands/Lists/ContextListExt.cs +++ b/PluralKit.Bot/Commands/Lists/ContextListExt.cs @@ -78,7 +78,7 @@ namespace PluralKit.Bot return p; } - public static async Task RenderMemberList(this Context ctx, LookupContext lookupCtx, IDatabase db, SystemId system, string embedTitle, MemberListOptions opts) + public static async Task RenderMemberList(this Context ctx, LookupContext lookupCtx, IDatabase db, SystemId system, string embedTitle, string color, MemberListOptions opts) { // We take an IDatabase instead of a IPKConnection so we don't keep the handle open for the entire runtime // We wanna release it as soon as the member list is actually *fetched*, instead of potentially minutes later (paginate timeout) @@ -87,7 +87,7 @@ namespace PluralKit.Bot .ToList(); var itemsPerPage = opts.Type == ListType.Short ? 25 : 5; - await ctx.Paginate(members.ToAsyncEnumerable(), members.Count, itemsPerPage, embedTitle, Renderer); + await ctx.Paginate(members.ToAsyncEnumerable(), members.Count, itemsPerPage, embedTitle, color, Renderer); // Base renderer, dispatches based on type Task Renderer(EmbedBuilder eb, IEnumerable page) diff --git a/PluralKit.Bot/Commands/ServerConfig.cs b/PluralKit.Bot/Commands/ServerConfig.cs index e29db8d4..12641288 100644 --- a/PluralKit.Bot/Commands/ServerConfig.cs +++ b/PluralKit.Bot/Commands/ServerConfig.cs @@ -107,6 +107,7 @@ namespace PluralKit.Bot await ctx.Paginate(channels.ToAsyncEnumerable(), channels.Count, 25, $"Blacklisted channels for {ctx.Guild.Name}", + null, (eb, l) => { string CategoryName(ulong? id) => diff --git a/PluralKit.Bot/Commands/SystemEdit.cs b/PluralKit.Bot/Commands/SystemEdit.cs index 7895b932..915d3bd8 100644 --- a/PluralKit.Bot/Commands/SystemEdit.cs +++ b/PluralKit.Bot/Commands/SystemEdit.cs @@ -1,5 +1,6 @@ using System; using System.Linq; +using System.Text.RegularExpressions; using System.Threading.Tasks; using Myriad.Builders; @@ -91,6 +92,47 @@ namespace PluralKit.Bot await ctx.Reply($"{Emojis.Success} System description changed."); } } + + public async Task Color(Context ctx) { + ctx.CheckSystem(); + + if (await ctx.MatchClear()) + { + var patch = new SystemPatch {Color = Partial.Null()}; + await _db.Execute(conn => _repo.UpdateSystem(conn, ctx.System.Id, patch)); + + await ctx.Reply($"{Emojis.Success} System color cleared."); + } + else if (!ctx.HasNext()) + { + if (ctx.System.Color == null) + await ctx.Reply( + $"Your system does not have a color set. To set one, type `pk;system color `."); + else + await ctx.Reply(embed: new EmbedBuilder() + .Title("System color") + .Color(ctx.System.Color.ToDiscordColor()) + .Thumbnail(new($"https://fakeimg.pl/256x256/{ctx.System.Color}/?text=%20")) + .Description($"Your system's color is **#{ctx.System.Color}**. To clear it, type `pk;s color -clear`.") + .Build()); + } + else + { + var color = ctx.RemainderOrNull(); + + if (color.StartsWith("#")) color = color.Substring(1); + if (!Regex.IsMatch(color, "^[0-9a-fA-F]{6}$")) throw Errors.InvalidColorError(color); + + var patch = new SystemPatch {Color = Partial.Present(color.ToLowerInvariant())}; + await _db.Execute(conn => _repo.UpdateSystem(conn, ctx.System.Id, patch)); + + await ctx.Reply(embed: new EmbedBuilder() + .Title($"{Emojis.Success} Member color changed.") + .Color(color.ToDiscordColor()) + .Thumbnail(new($"https://fakeimg.pl/256x256/{color}/?text=%20")) + .Build()); + } + } public async Task Tag(Context ctx) { diff --git a/PluralKit.Bot/Commands/SystemFront.cs b/PluralKit.Bot/Commands/SystemFront.cs index 3f8cbc0b..cde6359d 100644 --- a/PluralKit.Bot/Commands/SystemFront.cs +++ b/PluralKit.Bot/Commands/SystemFront.cs @@ -69,6 +69,7 @@ namespace PluralKit.Bot totalSwitches, 10, embedTitle, + system.Color, async (builder, switches) => { var sb = new StringBuilder(); diff --git a/PluralKit.Bot/Commands/SystemList.cs b/PluralKit.Bot/Commands/SystemList.cs index 493e5bda..8a33e552 100644 --- a/PluralKit.Bot/Commands/SystemList.cs +++ b/PluralKit.Bot/Commands/SystemList.cs @@ -20,7 +20,7 @@ namespace PluralKit.Bot ctx.CheckSystemPrivacy(target, target.MemberListPrivacy); var opts = ctx.ParseMemberListOptions(ctx.LookupContextFor(target)); - await ctx.RenderMemberList(ctx.LookupContextFor(target), _db, target.Id, GetEmbedTitle(target, opts), opts); + await ctx.RenderMemberList(ctx.LookupContextFor(target), _db, target.Id, GetEmbedTitle(target, opts), target.Color, opts); } private string GetEmbedTitle(PKSystem target, MemberListOptions opts) diff --git a/PluralKit.Bot/Services/EmbedService.cs b/PluralKit.Bot/Services/EmbedService.cs index fd0a971a..2a9d5bff 100644 --- a/PluralKit.Bot/Services/EmbedService.cs +++ b/PluralKit.Bot/Services/EmbedService.cs @@ -53,11 +53,22 @@ namespace PluralKit.Bot { var memberCount = cctx.MatchPrivateFlag(ctx) ? await _repo.GetSystemMemberCount(conn, system.Id, PrivacyLevel.Public) : await _repo.GetSystemMemberCount(conn, system.Id); + uint color; + try + { + color = system.Color?.ToDiscordColor() ?? DiscordUtils.Gray; + } + catch (ArgumentException) + { + // There's no API for system colors yet, but defaulting to a blank color in advance can't be a bad idea + color = DiscordUtils.Gray; + } + var eb = new EmbedBuilder() .Title(system.Name) .Thumbnail(new(system.AvatarUrl)) .Footer(new($"System ID: {system.Hid} | Created on {system.Created.FormatZoned(system)}")) - .Color(DiscordUtils.Gray); + .Color(color); var latestSwitch = await _repo.GetLatestSwitch(conn, system.Id); if (latestSwitch != null && system.FrontPrivacy.CanAccess(ctx)) @@ -68,7 +79,10 @@ namespace PluralKit.Bot { } if (system.Tag != null) - eb.Field(new("Tag", system.Tag.EscapeMarkdown())); + eb.Field(new("Tag", system.Tag.EscapeMarkdown(), true)); + + if (!system.Color.EmptyOrNull()) eb.Field(new("Color", $"#{system.Color}", true)); + eb.Field(new("Linked accounts", string.Join("\n", users).Truncate(1000), true)); if (system.MemberListPrivacy.CanAccess(ctx)) @@ -187,20 +201,34 @@ namespace PluralKit.Bot { if (system.Name != null) nameField = $"{nameField} ({system.Name})"; + uint color; + try + { + color = target.Color?.ToDiscordColor() ?? DiscordUtils.Gray; + } + catch (ArgumentException) + { + // There's no API for group colors yet, but defaulting to a blank color regardless + color = DiscordUtils.Gray; + } + var eb = new EmbedBuilder() .Author(new(nameField, IconUrl: DiscordUtils.WorkaroundForUrlBug(target.IconFor(pctx)))) + .Color(color) .Footer(new($"System ID: {system.Hid} | Group ID: {target.Hid} | Created on {target.Created.FormatZoned(system)}")); if (target.DisplayName != null) - eb.Field(new("Display Name", target.DisplayName)); + eb.Field(new("Display Name", target.DisplayName, true)); + + if (!target.Color.EmptyOrNull()) eb.Field(new("Color", $"#{target.Color}", true)); if (target.ListPrivacy.CanAccess(pctx)) { if (memberCount == 0 && pctx == LookupContext.ByOwner) // Only suggest the add command if this is actually the owner lol - eb.Field(new("Members (0)", $"Add one with `pk;group {target.Reference()} add `!", true)); + eb.Field(new("Members (0)", $"Add one with `pk;group {target.Reference()} add `!", false)); else - eb.Field(new($"Members ({memberCount})", $"(see `pk;group {target.Reference()} list`)", true)); + eb.Field(new($"Members ({memberCount})", $"(see `pk;group {target.Reference()} list`)", false)); } if (target.DescriptionFor(pctx) is { } desc) @@ -299,7 +327,6 @@ namespace PluralKit.Bot { var eb = new EmbedBuilder() .Color(DiscordUtils.Gray) .Footer(new($"Since {breakdown.RangeStart.FormatZoned(tz)} ({actualPeriod.FormatDuration()} ago)")); - var maxEntriesToDisplay = 24; // max 25 fields allowed in embed - reserve 1 for "others" // We convert to a list of pairs so we can add the no-fronter value diff --git a/PluralKit.Bot/Utils/ContextUtils.cs b/PluralKit.Bot/Utils/ContextUtils.cs index 2472c098..9f24d176 100644 --- a/PluralKit.Bot/Utils/ContextUtils.cs +++ b/PluralKit.Bot/Utils/ContextUtils.cs @@ -96,7 +96,7 @@ namespace PluralKit.Bot { return string.Equals(msg.Content, expectedReply, StringComparison.InvariantCultureIgnoreCase); } - public static async Task Paginate(this Context ctx, IAsyncEnumerable items, int totalCount, int itemsPerPage, string title, Func, Task> renderer) { + public static async Task Paginate(this Context ctx, IAsyncEnumerable items, int totalCount, int itemsPerPage, string title, string color, Func, Task> renderer) { // TODO: make this generic enough we can use it in Choose below var buffer = new List(); @@ -111,6 +111,8 @@ namespace PluralKit.Bot { var eb = new EmbedBuilder(); eb.Title(pageCount > 1 ? $"[{page+1}/{pageCount}] {title}" : title); + if (color != null) + eb.Color(color.ToDiscordColor()); await renderer(eb, buffer.Skip(page*itemsPerPage).Take(itemsPerPage)); return eb.Build(); } diff --git a/PluralKit.Core/Database/Database.cs b/PluralKit.Core/Database/Database.cs index 72a8591e..043b9e68 100644 --- a/PluralKit.Core/Database/Database.cs +++ b/PluralKit.Core/Database/Database.cs @@ -19,7 +19,7 @@ namespace PluralKit.Core internal class Database: IDatabase { private const string RootPath = "PluralKit.Core.Database"; // "resource path" root for SQL files - private const int TargetSchemaVersion = 12; + private const int TargetSchemaVersion = 13; private readonly CoreConfig _config; private readonly ILogger _logger; diff --git a/PluralKit.Core/Database/Migrations/13.sql b/PluralKit.Core/Database/Migrations/13.sql new file mode 100644 index 00000000..0ada9d65 --- /dev/null +++ b/PluralKit.Core/Database/Migrations/13.sql @@ -0,0 +1,7 @@ +-- SCHEMA VERSION 13: 2021-03-28 -- +-- Add system and group colors -- + +alter table systems add column color char(6); +alter table groups add column color char(6); + +update info set schema_version = 13; \ No newline at end of file diff --git a/PluralKit.Core/Models/PKGroup.cs b/PluralKit.Core/Models/PKGroup.cs index 7a45e900..21bea358 100644 --- a/PluralKit.Core/Models/PKGroup.cs +++ b/PluralKit.Core/Models/PKGroup.cs @@ -13,6 +13,7 @@ namespace PluralKit.Core public string? DisplayName { get; private set; } public string? Description { get; private set; } public string? Icon { get; private set; } + public string? Color { get; private set; } public PrivacyLevel DescriptionPrivacy { get; private set; } public PrivacyLevel IconPrivacy { get; private set; } diff --git a/PluralKit.Core/Models/PKSystem.cs b/PluralKit.Core/Models/PKSystem.cs index 436f9cc9..88d42266 100644 --- a/PluralKit.Core/Models/PKSystem.cs +++ b/PluralKit.Core/Models/PKSystem.cs @@ -14,6 +14,7 @@ namespace PluralKit.Core { public string Description { get; } public string Tag { get; } public string AvatarUrl { get; } + public string Color { get; } public string Token { get; } public Instant Created { get; } public string UiTz { get; set; } diff --git a/PluralKit.Core/Models/Patch/GroupPatch.cs b/PluralKit.Core/Models/Patch/GroupPatch.cs index 2f154c90..ee624df8 100644 --- a/PluralKit.Core/Models/Patch/GroupPatch.cs +++ b/PluralKit.Core/Models/Patch/GroupPatch.cs @@ -7,6 +7,7 @@ namespace PluralKit.Core public Partial DisplayName { get; set; } public Partial Description { get; set; } public Partial Icon { get; set; } + public Partial Color { get; set; } public Partial DescriptionPrivacy { get; set; } public Partial IconPrivacy { get; set; } @@ -18,6 +19,7 @@ namespace PluralKit.Core .With("display_name", DisplayName) .With("description", Description) .With("icon", Icon) + .With("color", Color) .With("description_privacy", DescriptionPrivacy) .With("icon_privacy", IconPrivacy) .With("list_privacy", ListPrivacy) diff --git a/PluralKit.Core/Models/Patch/SystemPatch.cs b/PluralKit.Core/Models/Patch/SystemPatch.cs index 7c3a9551..0f787749 100644 --- a/PluralKit.Core/Models/Patch/SystemPatch.cs +++ b/PluralKit.Core/Models/Patch/SystemPatch.cs @@ -7,6 +7,7 @@ namespace PluralKit.Core public Partial Description { get; set; } public Partial Tag { get; set; } public Partial AvatarUrl { get; set; } + public Partial Color { get; set; } public Partial Token { get; set; } public Partial UiTz { get; set; } public Partial DescriptionPrivacy { get; set; } @@ -22,6 +23,7 @@ namespace PluralKit.Core .With("description", Description) .With("tag", Tag) .With("avatar_url", AvatarUrl) + .With("color", Color) .With("token", Token) .With("ui_tz", UiTz) .With("description_privacy", DescriptionPrivacy)