public async Task Proxy(Context ctx, PKMember target) { ctx.CheckSystem().CheckOwnMember(target); ProxyTag ParseProxyTags(string exampleProxy) { // // Make sure there's one and only one instance of "text" in the example proxy given var prefixAndSuffix = exampleProxy.Split("text"); if (prefixAndSuffix.Length == 1) { prefixAndSuffix = prefixAndSuffix[0].Split("TEXT"); } if (prefixAndSuffix.Length < 2) { throw Errors.ProxyMustHaveText; } if (prefixAndSuffix.Length > 2) { throw Errors.ProxyMultipleText; } return(new ProxyTag(prefixAndSuffix[0], prefixAndSuffix[1])); } async Task <bool> WarnOnConflict(ProxyTag newTag) { var query = "select * from (select *, (unnest(proxy_tags)).prefix as prefix, (unnest(proxy_tags)).suffix as suffix from members where system = @System) as _ where prefix is not distinct from @Prefix and suffix is not distinct from @Suffix and id != @Existing"; var conflicts = (await _db.Execute(conn => conn.QueryAsync <PKMember>(query, new { Prefix = newTag.Prefix, Suffix = newTag.Suffix, Existing = target.Id, system = target.System }))).ToList(); if (conflicts.Count <= 0) { return(true); } var conflictList = conflicts.Select(m => $"- **{m.NameFor(ctx)}**"); var msg = $"{Emojis.Warn} The following members have conflicting proxy tags:\n{string.Join('\n', conflictList)}\nDo you want to proceed anyway?"; return(await ctx.PromptYesNo(msg, "Proceed")); } // "Sub"command: clear flag if (await ctx.MatchClear()) { // If we already have multiple tags, this would clear everything, so prompt that if (target.ProxyTags.Count > 1) { var msg = $"{Emojis.Warn} You already have multiple proxy tags set: {target.ProxyTagsString()}\nDo you want to clear them all?"; if (!await ctx.PromptYesNo(msg, "Clear")) { throw Errors.GenericCancelled(); } } var patch = new MemberPatch { ProxyTags = Partial <ProxyTag[]> .Present(new ProxyTag[0]) }; await _db.Execute(conn => _repo.UpdateMember(conn, target.Id, patch)); await ctx.Reply($"{Emojis.Success} Proxy tags cleared."); } // "Sub"command: no arguments; will print proxy tags else if (!ctx.HasNext(skipFlags: false)) { if (target.ProxyTags.Count == 0) { await ctx.Reply("This member does not have any proxy tags."); } else { await ctx.Reply($"This member's proxy tags are:\n{target.ProxyTagsString("\n")}"); } } // Subcommand: "add" else if (ctx.Match("add", "append")) { if (!ctx.HasNext(skipFlags: false)) { throw new PKSyntaxError("You must pass an example proxy to add (eg. `[text]` or `J:text`)."); } var tagToAdd = ParseProxyTags(ctx.RemainderOrNull(skipFlags: false)); if (tagToAdd.IsEmpty) { throw Errors.EmptyProxyTags(target); } if (target.ProxyTags.Contains(tagToAdd)) { throw Errors.ProxyTagAlreadyExists(tagToAdd, target); } if (!await WarnOnConflict(tagToAdd)) { throw Errors.GenericCancelled(); } var newTags = target.ProxyTags.ToList(); newTags.Add(tagToAdd); var patch = new MemberPatch { ProxyTags = Partial <ProxyTag[]> .Present(newTags.ToArray()) }; await _db.Execute(conn => _repo.UpdateMember(conn, target.Id, patch)); await ctx.Reply($"{Emojis.Success} Added proxy tags {tagToAdd.ProxyString.AsCode()}."); } // Subcommand: "remove" else if (ctx.Match("remove", "delete")) { if (!ctx.HasNext(skipFlags: false)) { throw new PKSyntaxError("You must pass a proxy tag to remove (eg. `[text]` or `J:text`)."); } var tagToRemove = ParseProxyTags(ctx.RemainderOrNull(skipFlags: false)); if (tagToRemove.IsEmpty) { throw Errors.EmptyProxyTags(target); } if (!target.ProxyTags.Contains(tagToRemove)) { throw Errors.ProxyTagDoesNotExist(tagToRemove, target); } var newTags = target.ProxyTags.ToList(); newTags.Remove(tagToRemove); var patch = new MemberPatch { ProxyTags = Partial <ProxyTag[]> .Present(newTags.ToArray()) }; await _db.Execute(conn => _repo.UpdateMember(conn, target.Id, patch)); await ctx.Reply($"{Emojis.Success} Removed proxy tags {tagToRemove.ProxyString.AsCode()}."); } // Subcommand: bare proxy tag given else { var requestedTag = ParseProxyTags(ctx.RemainderOrNull(skipFlags: false)); if (requestedTag.IsEmpty) { throw Errors.EmptyProxyTags(target); } // This is mostly a legacy command, so it's gonna warn if there's // already more than one proxy tag. if (target.ProxyTags.Count > 1) { var msg = $"This member already has more than one proxy tag set: {target.ProxyTagsString()}\nDo you want to replace them?"; if (!await ctx.PromptYesNo(msg, "Replace")) { throw Errors.GenericCancelled(); } } if (!await WarnOnConflict(requestedTag)) { throw Errors.GenericCancelled(); } var newTags = new[] { requestedTag }; var patch = new MemberPatch { ProxyTags = Partial <ProxyTag[]> .Present(newTags) }; await _db.Execute(conn => _repo.UpdateMember(conn, target.Id, patch)); await ctx.Reply($"{Emojis.Success} Member proxy tags set to {requestedTag.ProxyString.AsCode()}."); } }
public async Task <DiscordEmbed> CreateMemberEmbed(PKSystem system, PKMember member, DiscordGuild guild, LookupContext ctx) { // string FormatTimestamp(Instant timestamp) => DateTimeFormats.ZonedDateTimeFormat.Format(timestamp.InZone(system.Zone)); var name = member.NameFor(ctx); if (system.Name != null) { name = $"{name} ({system.Name})"; } DiscordColor color; try { color = member.Color?.ToDiscordColor() ?? DiscordUtils.Gray; } catch (ArgumentException) { // Bad API use can cause an invalid color string // TODO: fix that in the API // for now we just default to a blank color, yolo color = DiscordUtils.Gray; } await using var conn = await _db.Obtain(); var guildSettings = guild != null ? await conn.QueryOrInsertMemberGuildConfig(guild.Id, member.Id) : null; var guildDisplayName = guildSettings?.DisplayName; var avatar = guildSettings?.AvatarUrl ?? member.AvatarFor(ctx); var groups = (await conn.QueryMemberGroups(member.Id)).Where(g => g.Visibility.CanAccess(ctx)).ToList(); var eb = new DiscordEmbedBuilder() // TODO: add URL of website when that's up .WithAuthor(name, iconUrl: DiscordUtils.WorkaroundForUrlBug(avatar)) // .WithColor(member.ColorPrivacy.CanAccess(ctx) ? color : DiscordUtils.Gray) .WithColor(color) .WithFooter($"System ID: {system.Hid} | Member ID: {member.Hid} {(member.MetadataPrivacy.CanAccess(ctx) ? $"| Created on {member.Created.FormatZoned(system)}":"")}"); var description = ""; if (member.MemberVisibility == PrivacyLevel.Private) { description += "*(this member is hidden)*\n"; } if (guildSettings?.AvatarUrl != null) { if (member.AvatarFor(ctx) != null) { description += $"*(this member has a server-specific avatar set; [click here]({member.AvatarUrl}) to see the global avatar)*\n"; } else { description += "*(this member has a server-specific avatar set)*\n"; } } if (description != "") { eb.WithDescription(description); } if (avatar != null) { eb.WithThumbnail(avatar); } if (!member.DisplayName.EmptyOrNull() && member.NamePrivacy.CanAccess(ctx)) { eb.AddField("Display Name", member.DisplayName.Truncate(1024), true); } if (guild != null && guildDisplayName != null) { eb.AddField($"Server Nickname (for {guild.Name})", guildDisplayName.Truncate(1024), true); } if (member.BirthdayFor(ctx) != null) { eb.AddField("Birthdate", member.BirthdayString, true); } if (member.PronounsFor(ctx) is {} pronouns&& !string.IsNullOrWhiteSpace(pronouns)) { eb.AddField("Pronouns", pronouns.Truncate(1024), true); } if (member.MessageCountFor(ctx) is {} count&& count > 0) { eb.AddField("Message Count", member.MessageCount.ToString(), true); } if (member.HasProxyTags) { eb.AddField("Proxy Tags", member.ProxyTagsString("\n").Truncate(1024), true); } // --- For when this gets added to the member object itself or however they get added // if (member.LastMessage != null && member.MetadataPrivacy.CanAccess(ctx)) eb.AddField("Last message:" FormatTimestamp(DiscordUtils.SnowflakeToInstant(m.LastMessage.Value))); // if (member.LastSwitchTime != null && m.MetadataPrivacy.CanAccess(ctx)) eb.AddField("Last switched in:", FormatTimestamp(member.LastSwitchTime.Value)); // if (!member.Color.EmptyOrNull() && member.ColorPrivacy.CanAccess(ctx)) eb.AddField("Color", $"#{member.Color}", true); if (!member.Color.EmptyOrNull()) { eb.AddField("Color", $"#{member.Color}", true); } 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.Hid}`] **{g.DisplayName ?? g.Name}**")); eb.AddField($"Groups ({groups.Count})", content.Truncate(1000)); } if (member.DescriptionFor(ctx) is {} desc) { eb.AddField("Description", member.Description.NormalizeLineEndSpacing(), false); } return(eb.Build()); }
public static PKError ProxyTagDoesNotExist(ProxyTag tagToRemove, PKMember member) => new PKError($"That member does not have the proxy tag {tagToRemove.ProxyString.AsCode()}. The member currently has these tags: {member.ProxyTagsString()}");
public static PKError LegacyAlreadyHasProxyTag(ProxyTag requested, PKMember member) => new PKError($"This member already has more than one proxy tag set: {member.ProxyTagsString()}\nConsider using the {$"pk;member {member.Reference()} proxy add {requested.ProxyString}".AsCode()} command instead.");
public async Task Proxy(Context ctx, PKMember target) { if (ctx.System == null) { throw Errors.NoSystemError; } if (target.System != ctx.System.Id) { throw Errors.NotOwnMemberError; } ProxyTag ParseProxyTags(string exampleProxy) { // // Make sure there's one and only one instance of "text" in the example proxy given var prefixAndSuffix = exampleProxy.Split("text"); if (prefixAndSuffix.Length < 2) { throw Errors.ProxyMustHaveText; } if (prefixAndSuffix.Length > 2) { throw Errors.ProxyMultipleText; } return(new ProxyTag(prefixAndSuffix[0], prefixAndSuffix[1])); } async Task <bool> WarnOnConflict(ProxyTag newTag) { var conflicts = (await _data.GetConflictingProxies(ctx.System, newTag)) .Where(m => m.Id != target.Id) .ToList(); if (conflicts.Count <= 0) { return(true); } var conflictList = conflicts.Select(m => $"- **{m.Name}**"); var msg = await ctx.Reply( $"{Emojis.Warn} The following members have conflicting proxy tags:\n{string.Join('\n', conflictList)}\nDo you want to proceed anyway?"); return(await ctx.PromptYesNo(msg)); } // "Sub"command: no arguments clearing // Also matches the pseudo-subcommand "text" which is equivalent to empty proxy tags on both sides. if (!ctx.HasNext() || ctx.Match("clear", "purge", "clean", "removeall")) { // If we already have multiple tags, this would clear everything, so prompt that if (target.ProxyTags.Count > 1) { var msg = await ctx.Reply( $"{Emojis.Warn} You already have multiple proxy tags set: {target.ProxyTagsString()}\nDo you want to clear them all?"); if (!await ctx.PromptYesNo(msg)) { throw Errors.GenericCancelled(); } } target.ProxyTags = new ProxyTag[] { }; await _data.SaveMember(target); await ctx.Reply($"{Emojis.Success} Proxy tags cleared."); } // Subcommand: "add" else if (ctx.Match("add", "append")) { if (!ctx.HasNext()) { throw new PKSyntaxError("You must pass an example proxy to add (eg. `[text]` or `J:text`)."); } var tagToAdd = ParseProxyTags(ctx.RemainderOrNull()); if (tagToAdd.IsEmpty) { throw Errors.EmptyProxyTags(target); } if (target.ProxyTags.Contains(tagToAdd)) { throw Errors.ProxyTagAlreadyExists(tagToAdd, target); } if (!await WarnOnConflict(tagToAdd)) { throw Errors.GenericCancelled(); } // It's not guaranteed the list's mutable, so we force it to be target.ProxyTags = target.ProxyTags.ToList(); target.ProxyTags.Add(tagToAdd); await _data.SaveMember(target); await ctx.Reply($"{Emojis.Success} Added proxy tags `{tagToAdd.ProxyString.SanitizeMentions()}`."); } // Subcommand: "remove" else if (ctx.Match("remove", "delete")) { if (!ctx.HasNext()) { throw new PKSyntaxError("You must pass a proxy tag to remove (eg. `[text]` or `J:text`)."); } var tagToRemove = ParseProxyTags(ctx.RemainderOrNull()); if (tagToRemove.IsEmpty) { throw Errors.EmptyProxyTags(target); } if (!target.ProxyTags.Contains(tagToRemove)) { throw Errors.ProxyTagDoesNotExist(tagToRemove, target); } // It's not guaranteed the list's mutable, so we force it to be target.ProxyTags = target.ProxyTags.ToList(); target.ProxyTags.Remove(tagToRemove); await _data.SaveMember(target); await ctx.Reply($"{Emojis.Success} Removed proxy tags `{tagToRemove.ProxyString.SanitizeMentions()}`."); } // Subcommand: bare proxy tag given else { if (!ctx.HasNext()) { throw new PKSyntaxError("You must pass an example proxy to set (eg. `[text]` or `J:text`)."); } var requestedTag = ParseProxyTags(ctx.RemainderOrNull()); if (requestedTag.IsEmpty) { throw Errors.EmptyProxyTags(target); } // This is mostly a legacy command, so it's gonna warn if there's // already more than one proxy tag. if (target.ProxyTags.Count > 1) { var msg = await ctx.Reply($"This member already has more than one proxy tag set: {target.ProxyTagsString().SanitizeMentions()}\nDo you want to replace them?"); if (!await ctx.PromptYesNo(msg)) { throw Errors.GenericCancelled(); } } if (!await WarnOnConflict(requestedTag)) { throw Errors.GenericCancelled(); } target.ProxyTags = new[] { requestedTag }; await _data.SaveMember(target); await ctx.Reply($"{Emojis.Success} Member proxy tags set to `{requestedTag.ProxyString.SanitizeMentions()}`."); } }
public static PKError ProxyTagAlreadyExists(ProxyTag tagToAdd, PKMember member) => new PKError($"That member already has the proxy tag {tagToAdd.ProxyString.AsCode()}. The member currently has these tags: {member.ProxyTagsString()}");
public static PKError LegacyAlreadyHasProxyTag(ProxyTag requested, PKMember member) => new PKError($"This member already has more than one proxy tag set: {member.ProxyTagsString().SanitizeMentions()}\nConsider using the `pk;member {member.Hid} proxy add {requested.ProxyString.SanitizeMentions()}` command instead.");
public async Task <Embed> CreateMemberEmbed(PKSystem system, PKMember member, Guild guild, LookupContext ctx, DateTimeZone zone) { // string FormatTimestamp(Instant timestamp) => DateTimeFormats.ZonedDateTimeFormat.Format(timestamp.InZone(system.Zone)); var name = member.NameFor(ctx); if (system.Name != null) { name = $"{name} ({system.Name})"; } uint color; try { color = member.Color?.ToDiscordColor() ?? DiscordUtils.Gray; } catch (ArgumentException) { // Bad API use can cause an invalid color string // this is now fixed in the API, but might still have some remnants in the database // so we just default to a blank color, yolo color = DiscordUtils.Gray; } var guildSettings = guild != null ? await _repo.GetMemberGuild(guild.Id, member.Id) : null; var guildDisplayName = guildSettings?.DisplayName; 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 eb = new EmbedBuilder() .Author(new Embed.EmbedAuthor(name, IconUrl: avatar.TryGetCleanCdnUrl(), Url: $"https://dash.pluralkit.me/profile/m/{member.Hid}")) // .WithColor(member.ColorPrivacy.CanAccess(ctx) ? color : DiscordUtils.Gray) .Color(color) .Footer(new Embed.EmbedFooter( $"System ID: {system.Hid} | Member ID: {member.Hid} {(member.MetadataPrivacy.CanAccess(ctx) ? $"| Created on {member.Created.FormatZoned(zone)}" : "")}")); if (member.DescriptionPrivacy.CanAccess(ctx)) { eb.Image(new Embed.EmbedImage(member.BannerImage)); } var description = ""; if (member.MemberVisibility == PrivacyLevel.Private) { description += "*(this member is hidden)*\n"; } if (guildSettings?.AvatarUrl != null) { if (member.AvatarFor(ctx) != null) { description += $"*(this member has a server-specific avatar set; [click here]({member.AvatarUrl.TryGetCleanCdnUrl()}) to see the global avatar)*\n"; } else { description += "*(this member has a server-specific avatar set)*\n"; } } if (description != "") { eb.Description(description); } if (avatar != null) { eb.Thumbnail(new Embed.EmbedThumbnail(avatar.TryGetCleanCdnUrl())); } if (!member.DisplayName.EmptyOrNull() && member.NamePrivacy.CanAccess(ctx)) { eb.Field(new Embed.Field("Display Name", member.DisplayName.Truncate(1024), true)); } if (guild != null && guildDisplayName != null) { eb.Field(new Embed.Field($"Server Nickname (for {guild.Name})", guildDisplayName.Truncate(1024), true)); } if (member.BirthdayFor(ctx) != null) { eb.Field(new Embed.Field("Birthdate", member.BirthdayString, true)); } if (member.PronounsFor(ctx) is { } pronouns&& !string.IsNullOrWhiteSpace(pronouns)) { eb.Field(new Embed.Field("Pronouns", pronouns.Truncate(1024), true)); } if (member.MessageCountFor(ctx) is { } count&& count > 0) { eb.Field(new Embed.Field("Message Count", member.MessageCount.ToString(), true)); } if (member.HasProxyTags) { eb.Field(new Embed.Field("Proxy Tags", member.ProxyTagsString("\n").Truncate(1024), true)); } // --- For when this gets added to the member object itself or however they get added // if (member.LastMessage != null && member.MetadataPrivacy.CanAccess(ctx)) eb.AddField("Last message:" FormatTimestamp(DiscordUtils.SnowflakeToInstant(m.LastMessage.Value))); // if (member.LastSwitchTime != null && m.MetadataPrivacy.CanAccess(ctx)) eb.AddField("Last switched in:", FormatTimestamp(member.LastSwitchTime.Value)); // if (!member.Color.EmptyOrNull() && member.ColorPrivacy.CanAccess(ctx)) eb.AddField("Color", $"#{member.Color}", true); if (!member.Color.EmptyOrNull()) { eb.Field(new Embed.Field("Color", $"#{member.Color}", true)); } 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.Hid}`] **{g.DisplayName ?? g.Name}**")); eb.Field(new Embed.Field($"Groups ({groups.Count})", content.Truncate(1000))); } if (member.DescriptionFor(ctx) is { } desc) { eb.Field(new Embed.Field("Description", member.Description.NormalizeLineEndSpacing())); } return(eb.Build()); }
public static PKError ProxyTagDoesNotExist(ProxyTag tagToRemove, PKMember member) => new PKError($"That member does not have the proxy tag `{tagToRemove.ProxyString.SanitizeMentions()}`. The member currently has these tags: {member.ProxyTagsString().SanitizeMentions()}");
public static PKError ProxyTagAlreadyExists(ProxyTag tagToAdd, PKMember member) => new PKError($"That member already has the proxy tag `{tagToAdd.ProxyString.SanitizeMentions()}`. The member currently has these tags: {member.ProxyTagsString().SanitizeMentions()}");
public static PKError LegacyAlreadyHasProxyTag(ProxyTag requested, PKMember member) => new PKError($"This member already has more than one proxy tag set: {member.ProxyTagsString()}\nConsider using the ``pk;member {member.Hid} proxy add {requested.ProxyString.EscapeBacktickPair()}`` command instead.");
public static PKError ProxyTagDoesNotExist(ProxyTag tagToRemove, PKMember member) => new PKError($"That member does not have the proxy tag ``{tagToRemove.ProxyString.EscapeBacktickPair()}``. The member currently has these tags: {member.ProxyTagsString()}");
public static PKError ProxyTagAlreadyExists(ProxyTag tagToAdd, PKMember member) => new PKError($"That member already has the proxy tag ``{tagToAdd.ProxyString.EscapeBacktickPair()}``. The member currently has these tags: {member.ProxyTagsString()}");