Merge branch 'feature/granular-privacy'

This commit is contained in:
Ske 2020-06-20 16:25:08 +02:00
commit ff7b728762
39 changed files with 692 additions and 164 deletions

View file

@ -279,6 +279,9 @@ namespace PluralKit.Bot
public LookupContext LookupContextFor(SystemId systemId) =>
System?.Id == systemId ? LookupContext.ByOwner : LookupContext.ByNonOwner;
public LookupContext LookupContextFor(PKMember target) =>
System?.Id == target.System ? LookupContext.ByOwner : LookupContext.ByNonOwner;
public Context CheckSystemPrivacy(PKSystem target, PrivacyLevel level)
{
if (level.CanAccess(LookupContextFor(target))) return this;

View file

@ -76,7 +76,7 @@ namespace PluralKit.Bot
ctx.CheckOwnMember(member);
await UpdateAutoproxy(ctx, AutoproxyMode.Member, member.Id);
await ctx.Reply($"{Emojis.Success} Autoproxy set to **{member.Name}** in this server.");
await ctx.Reply($"{Emojis.Success} Autoproxy set to **{member.NameFor(ctx)}** in this server.");
}
private async Task<DiscordEmbed> CreateAutoproxyStatusEmbed(Context ctx)
@ -103,14 +103,14 @@ namespace PluralKit.Bot
{
if (relevantMember == null)
throw new ArgumentException("Attempted to print member autoproxy status, but the linked member ID wasn't found in the database. Should be handled appropriately.");
eb.WithDescription($"Autoproxy is currently set to **front mode** in this server. The current (first) fronter is **{relevantMember.Name.EscapeMarkdown()}** (`{relevantMember.Hid}`). To disable, type `pk;autoproxy off`.");
eb.WithDescription($"Autoproxy is currently set to **front mode** in this server. The current (first) fronter is **{relevantMember.NameFor(ctx).EscapeMarkdown()}** (`{relevantMember.Hid}`). To disable, type `pk;autoproxy off`.");
}
break;
}
// AutoproxyMember is never null if Mode is Member, this is just to make the compiler shut up
case AutoproxyMode.Member when relevantMember != null: {
eb.WithDescription($"Autoproxy is active for member **{relevantMember.Name}** (`{relevantMember.Hid}`) in this server. To disable, type `pk;autoproxy off`.");
eb.WithDescription($"Autoproxy is active for member **{relevantMember.NameFor(ctx)}** (`{relevantMember.Hid}`) in this server. To disable, type `pk;autoproxy off`.");
break;
}
case AutoproxyMode.Latch:

View file

@ -24,8 +24,8 @@ namespace PluralKit.Bot
public static Command SystemFronter = new Command("system fronter", "system [system] fronter", "Shows a system's fronter(s)");
public static Command SystemFrontHistory = new Command("system fronthistory", "system [system] fronthistory", "Shows a system's front history");
public static Command SystemFrontPercent = new Command("system frontpercent", "system [system] frontpercent [timespan]", "Shows a system's front breakdown");
public static Command SystemPrivacy = new Command("system privacy", "system privacy <description|members|fronter|fronthistory> <public|private>", "Changes your system's privacy settings");
public static Command SystemPing = new Command("system ping", "system ping <enable|disable>", "Changes your system's ping preferences");
public static Command SystemPrivacy = new Command("system privacy", "system privacy <description|members|fronter|fronthistory|all> <public|private>", "Changes your system's privacy settings");
public static Command Autoproxy = new Command("autoproxy", "autoproxy [off|front|latch|member]", "Sets your system's autoproxy mode for this server");
public static Command MemberInfo = new Command("member", "member <member>", "Looks up information about a member");
public static Command MemberNew = new Command("member new", "member new <name>", "Creates a new member");
@ -42,7 +42,7 @@ namespace PluralKit.Bot
public static Command MemberServerName = new Command("member servername", "member <member> servername [server name]", "Changes a member's display name in the current server");
public static Command MemberKeepProxy = new Command("member keepproxy", "member <member> keepproxy [on|off]", "Sets whether to include a member's proxy tags when proxying");
public static Command MemberRandom = new Command("random", "random", "Looks up a random member from your system");
public static Command MemberPrivacy = new Command("member privacy", "member <member> privacy [on|off]", "Sets whether a member is private or public");
public static Command MemberPrivacy = new Command("member privacy", "member <member> privacy <name|description|birthday|pronouns|metadata|visibility|all> <public|private>", "Changes a members's privacy settings");
public static Command Switch = new Command("switch", "switch <member> [member 2] [member 3...]", "Registers a switch");
public static Command SwitchOut = new Command("switch out", "switch out", "Registers a switch with no members");
public static Command SwitchMove = new Command("switch move", "switch move <date/time>", "Moves the latest switch in time");
@ -72,7 +72,7 @@ namespace PluralKit.Bot
public static Command[] MemberCommands = {
MemberInfo, MemberNew, MemberRename, MemberDisplayName, MemberServerName, MemberDesc, MemberPronouns,
MemberColor, MemberBirthday, MemberProxy, MemberKeepProxy, MemberDelete, MemberAvatar, MemberServerAvatar,
MemberColor, MemberBirthday, MemberProxy, MemberKeepProxy, MemberDelete, MemberAvatar, MemberServerAvatar, MemberPrivacy,
MemberRandom
};
@ -297,9 +297,9 @@ namespace PluralKit.Bot
await ctx.Execute<MemberEdit>(MemberKeepProxy, m => m.KeepProxy(ctx, target));
else if (ctx.Match("privacy"))
await ctx.Execute<MemberEdit>(MemberPrivacy, m => m.Privacy(ctx, target, null));
else if (ctx.Match("private", "hidden"))
else if (ctx.Match("private", "hidden", "hide"))
await ctx.Execute<MemberEdit>(MemberPrivacy, m => m.Privacy(ctx, target, PrivacyLevel.Private));
else if (ctx.Match("public", "shown"))
else if (ctx.Match("public", "shown", "show"))
await ctx.Execute<MemberEdit>(MemberPrivacy, m => m.Privacy(ctx, target, PrivacyLevel.Public));
else if (!ctx.HasNext()) // Bare command
await ctx.Execute<Member>(MemberInfo, m => m.ViewMember(ctx, target));

View file

@ -28,7 +28,7 @@ namespace PluralKit.Bot
// Warn if there's already a member by this name
var existingMember = await _data.GetMemberByName(ctx.System, memberName);
if (existingMember != null) {
var msg = await ctx.Reply($"{Emojis.Warn} You already have a member in your system with the name \"{existingMember.Name.SanitizeMentions()}\" (with ID `{existingMember.Hid}`). Do you want to create another member with the same name?");
var msg = await ctx.Reply($"{Emojis.Warn} You already have a member in your system with the name \"{existingMember.NameFor(ctx).SanitizeMentions()}\" (with ID `{existingMember.Hid}`). Do you want to create another member with the same name?");
if (!await ctx.PromptYesNo(msg)) throw new PKError("Member creation cancelled.");
}
@ -59,7 +59,7 @@ namespace PluralKit.Bot
//Maybe move this somewhere else in the file structure since it doesn't need to get created at every command
// TODO: don't buffer these, find something else to do ig
var members = await _data.GetSystemMembers(ctx.System).Where(m => m.MemberPrivacy == PrivacyLevel.Public).ToListAsync();
var members = await _data.GetSystemMembers(ctx.System).Where(m => m.MemberVisibility == PrivacyLevel.Public).ToListAsync();
if (members == null || !members.Any())
throw Errors.NoMembersError;
var randInt = randGen.Next(members.Count);

View file

@ -47,7 +47,8 @@ namespace PluralKit.Bot
var cmd = location == AvatarLocation.Server ? "serveravatar" : "avatar";
var currentValue = location == AvatarLocation.Member ? target.AvatarUrl : guildData?.AvatarUrl;
if (string.IsNullOrEmpty(currentValue))
var canAccess = location != AvatarLocation.Member || target.AvatarPrivacy.CanAccess(ctx.LookupContextFor(target));
if (string.IsNullOrEmpty(currentValue) || !canAccess)
{
if (location == AvatarLocation.Member)
{
@ -61,7 +62,7 @@ namespace PluralKit.Bot
}
var eb = new DiscordEmbedBuilder()
.WithTitle($"{target.Name.SanitizeMentions()}'s {field}")
.WithTitle($"{target.NameFor(ctx).SanitizeMentions()}'s {field}")
.WithImageUrl(currentValue);
if (target.System == ctx.System?.Id)
eb.WithDescription($"To clear, use `pk;member {target.Hid} {cmd} clear`.");

View file

@ -1,5 +1,7 @@
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System;
using Dapper;
@ -33,7 +35,7 @@ namespace PluralKit.Bot
// Warn if there's already a member by this name
var existingMember = await _data.GetMemberByName(ctx.System, newName);
if (existingMember != null) {
var msg = await ctx.Reply($"{Emojis.Warn} You already have a member in your system with the name \"{existingMember.Name.SanitizeMentions()}\" (`{existingMember.Hid}`). Do you want to rename this member to that name too?");
var msg = await ctx.Reply($"{Emojis.Warn} You already have a member in your system with the name \"{existingMember.NameFor(ctx).SanitizeMentions()}\" (`{existingMember.Hid}`). Do you want to rename this member to that name too?");
if (!await ctx.PromptYesNo(msg)) throw new PKError("Member renaming cancelled.");
}
@ -53,12 +55,6 @@ namespace PluralKit.Bot
}
}
private void CheckReadMemberPermission(Context ctx, PKMember target)
{
if (!target.MemberPrivacy.CanAccess(ctx.LookupContextFor(target.System)))
throw Errors.LookupNotAllowed;
}
private void CheckEditMemberPermission(Context ctx, PKMember target)
{
if (target.System != ctx.System?.Id) throw Errors.NotOwnMemberError;
@ -78,7 +74,8 @@ namespace PluralKit.Bot
}
else if (!ctx.HasNext())
{
CheckReadMemberPermission(ctx, target);
if (!target.DescriptionPrivacy.CanAccess(ctx.LookupContextFor(target.System)))
throw Errors.LookupNotAllowed;
if (target.Description == null)
if (ctx.System?.Id == target.System)
await ctx.Reply($"This member does not have a description set. To set one, type `pk;member {target.Hid} description <description>`.");
@ -119,14 +116,15 @@ namespace PluralKit.Bot
}
else if (!ctx.HasNext())
{
CheckReadMemberPermission(ctx, target);
if (!target.PronounPrivacy.CanAccess(ctx.LookupContextFor(target.System)))
throw Errors.LookupNotAllowed;
if (target.Pronouns == null)
if (ctx.System?.Id == target.System)
await ctx.Reply($"This member does not have pronouns set. To set some, type `pk;member {target.Hid} pronouns <pronouns>`.");
else
await ctx.Reply("This member does not have pronouns set.");
else
await ctx.Reply($"**{target.Name.SanitizeMentions()}**'s pronouns are **{target.Pronouns.SanitizeMentions()}**."
await ctx.Reply($"**{target.NameFor(ctx).SanitizeMentions()}**'s pronouns are **{target.Pronouns.SanitizeMentions()}**."
+ (ctx.System?.Id == target.System ? $" To clear them, type `pk;member {target.Hid} pronouns -clear`." : ""));
}
else
@ -155,7 +153,8 @@ namespace PluralKit.Bot
}
else if (!ctx.HasNext())
{
CheckReadMemberPermission(ctx, target);
// if (!target.ColorPrivacy.CanAccess(ctx.LookupContextFor(target.System)))
// throw Errors.LookupNotAllowed;
if (target.Color == null)
if (ctx.System?.Id == target.System)
@ -199,7 +198,8 @@ namespace PluralKit.Bot
}
else if (!ctx.HasNext())
{
CheckReadMemberPermission(ctx, target);
if (!target.BirthdayPrivacy.CanAccess(ctx.LookupContextFor(target.System)))
throw Errors.LookupNotAllowed;
if (target.Birthday == null)
await ctx.Reply("This member does not have a birthdate set."
@ -223,6 +223,8 @@ namespace PluralKit.Bot
private async Task<DiscordEmbedBuilder> CreateMemberNameInfoEmbed(Context ctx, PKMember target)
{
var lcx = ctx.LookupContextFor(target);
MemberGuildSettings memberGuildConfig = null;
if (ctx.Guild != null)
memberGuildConfig = await _db.Execute(c => c.QueryOrInsertMemberGuildConfig(ctx.Guild.Id, target.Id));
@ -231,14 +233,17 @@ namespace PluralKit.Bot
.WithFooter($"Member ID: {target.Hid} | Active name in bold. Server name overrides display name, which overrides base name.");
if (target.DisplayName == null && memberGuildConfig?.DisplayName == null)
eb.AddField($"Name", $"**{target.Name}**");
eb.AddField("Name", $"**{target.NameFor(ctx)}**");
else
eb.AddField("Name", target.Name);
if (target.DisplayName != null && memberGuildConfig?.DisplayName == null)
eb.AddField($"Display Name", $"**{target.DisplayName}**");
else
eb.AddField("Display Name", target.DisplayName ?? "*(none)*");
eb.AddField("Name", target.NameFor(ctx));
if (target.NamePrivacy.CanAccess(lcx))
{
if (target.DisplayName != null && memberGuildConfig?.DisplayName == null)
eb.AddField("Display Name", $"**{target.DisplayName}**");
else
eb.AddField("Display Name", target.DisplayName ?? "*(none)*");
}
if (ctx.Guild != null)
{
@ -272,7 +277,7 @@ namespace PluralKit.Bot
target.DisplayName = null;
await _data.SaveMember(target);
await PrintSuccess($"{Emojis.Success} Member display name cleared. This member will now be proxied using their member name \"{target.Name.SanitizeMentions()}\".");
await PrintSuccess($"{Emojis.Success} Member display name cleared. This member will now be proxied using their member name \"{target.NameFor(ctx).SanitizeMentions()}\".");
}
else if (!ctx.HasNext())
{
@ -309,7 +314,7 @@ namespace PluralKit.Bot
if (target.DisplayName != null)
await ctx.Reply($"{Emojis.Success} Member server name cleared. This member will now be proxied using their global display name \"{target.DisplayName.SanitizeMentions()}\" in this server ({ctx.Guild.Name.SanitizeMentions()}).");
else
await ctx.Reply($"{Emojis.Success} Member server name cleared. This member will now be proxied using their member name \"{target.Name.SanitizeMentions()}\" in this server ({ctx.Guild.Name.SanitizeMentions()}).");
await ctx.Reply($"{Emojis.Success} Member server name cleared. This member will now be proxied using their member name \"{target.NameFor(ctx).SanitizeMentions()}\" in this server ({ctx.Guild.Name.SanitizeMentions()}).");
}
else if (!ctx.HasNext())
{
@ -360,34 +365,122 @@ namespace PluralKit.Bot
await ctx.Reply($"{Emojis.Success} Member proxy tags will now not be included in the resulting message when proxying.");
}
private DiscordEmbed CreatePrivacyEmbed(Context ctx, PKMember member)
{
string PrivacyLevelString(PrivacyLevel level) => level switch
{
PrivacyLevel.Private => "**Private** (visible only when queried by you)",
PrivacyLevel.Public => "**Public** (visible to everyone)",
_ => throw new ArgumentOutOfRangeException(nameof(level), level, null)
};
var eb = new DiscordEmbedBuilder()
.WithTitle($"Current privacy settings for {member.NameFor(ctx)}")
.AddField("Name (replaces name with display name if member has one)",PrivacyLevelString(member.NamePrivacy))
.AddField("Description", PrivacyLevelString(member.DescriptionPrivacy))
.AddField("Avatar", PrivacyLevelString(member.AvatarPrivacy))
.AddField("Birthday", PrivacyLevelString(member.BirthdayPrivacy))
.AddField("Pronouns", PrivacyLevelString(member.PronounPrivacy))
// .AddField("Color", PrivacyLevelString(target.ColorPrivacy))
.AddField("Meta (message count, last front, last message)", PrivacyLevelString(member.MetadataPrivacy))
.AddField("Visibility", PrivacyLevelString(member.MemberVisibility))
.WithDescription("To edit privacy settings, use the command:\n`pk;member <member> privacy <subject> <level>`\n\n- `subject` is one of `name`, `description`, `avatar`, `birthday`, `pronouns`, `created`, `messages`, `visibility`, or `all`\n- `level` is either `public` or `private`.");
return eb.Build();
}
public async Task Privacy(Context ctx, PKMember target, PrivacyLevel? newValueFromCommand)
{
if (ctx.System == null) throw Errors.NoSystemError;
if (target.System != ctx.System.Id) throw Errors.NotOwnMemberError;
PrivacyLevel newValue;
if (ctx.Match("private", "hide", "hidden", "on", "enable", "yes")) newValue = PrivacyLevel.Private;
else if (ctx.Match("public", "show", "shown", "displayed", "off", "disable", "no")) newValue = PrivacyLevel.Public;
else if (ctx.HasNext()) throw new PKSyntaxError("You must pass either \"private\" or \"public\".");
// If we're getting a value from command (eg. "pk;m <name> private" == always private, "pk;m <name> public == always public"), use that instead of parsing
else if (newValueFromCommand != null) newValue = newValueFromCommand.Value;
else
// Display privacy settings
if (!ctx.HasNext() && newValueFromCommand == null)
{
if (target.MemberPrivacy == PrivacyLevel.Public)
await ctx.Reply("This member's privacy is currently set to **public**. This member will show up in member lists and will return all information when queried by other accounts.");
else
await ctx.Reply("This member's privacy is currently set to **private**. This member will not show up in member lists and will return limited information when queried by other accounts.");
await ctx.Reply(embed: CreatePrivacyEmbed(ctx, target));
return;
}
// Get guild settings (mostly for warnings and such)
MemberGuildSettings guildSettings = null;
if (ctx.Guild != null)
guildSettings = await _db.Execute(c => c.QueryOrInsertMemberGuildConfig(ctx.Guild.Id, target.Id));
target.MemberPrivacy = newValue;
await _data.SaveMember(target);
// Set Privacy Settings
PrivacyLevel PopPrivacyLevel(string subjectName)
{
if (ctx.Match("public", "show", "shown", "visible"))
return PrivacyLevel.Public;
if (newValue == PrivacyLevel.Private)
await ctx.Reply($"{Emojis.Success} Member privacy set to **private**. This member will no longer show up in member lists and will return limited information when queried by other accounts.");
if (ctx.Match("private", "hide", "hidden"))
return PrivacyLevel.Private;
if (!ctx.HasNext())
throw new PKSyntaxError($"You must pass a privacy level for `{subjectName}` (`public` or `private`)");
throw new PKSyntaxError($"Invalid privacy level `{ctx.PopArgument().SanitizeMentions()}` (must be `public` or `private`).");
}
// See if we have a subject given
PrivacyLevel newLevel;
if (PrivacyUtils.TryParseMemberPrivacy(ctx.PeekArgument(), out var subject))
{
// We peeked before, pop it now
ctx.PopArgument();
// Read the privacy level from args
newLevel = PopPrivacyLevel(subject.Name());
// Set the level on the given subject
target.SetPrivacy(subject, newLevel);
await _data.SaveMember(target);
// Print response
var explanation = (subject, newLevel) switch
{
(MemberPrivacySubject.Name, PrivacyLevel.Private) => "This member's name is now hidden from other systems, and will be replaced by the member's display name.",
(MemberPrivacySubject.Description, PrivacyLevel.Private) => "This member's description is now hidden from other systems.",
(MemberPrivacySubject.Avatar, PrivacyLevel.Private) => "This member's avatar is now hidden from other systems.",
(MemberPrivacySubject.Birthday, PrivacyLevel.Private) => "This member's birthday is now hidden from other systems.",
(MemberPrivacySubject.Pronouns, PrivacyLevel.Private) => "This member's pronouns are now hidden from other systems.",
(MemberPrivacySubject.Metadata, PrivacyLevel.Private) => "This member's metadata (eg. created timestamp, message count, etc) is now hidden from other systems.",
(MemberPrivacySubject.Visibility, PrivacyLevel.Private) => "This member is now hidden from member lists.",
(MemberPrivacySubject.Name, PrivacyLevel.Public) => "This member's name is no longer hidden from other systems.",
(MemberPrivacySubject.Description, PrivacyLevel.Public) => "This member's description is no longer hidden from other systems.",
(MemberPrivacySubject.Avatar, PrivacyLevel.Public) => "This member's avatar is no longer hidden from other systems.",
(MemberPrivacySubject.Birthday, PrivacyLevel.Public) => "This member's birthday is no longer hidden from other systems.",
(MemberPrivacySubject.Pronouns, PrivacyLevel.Public) => "This member's pronouns are no longer hidden other systems.",
(MemberPrivacySubject.Metadata, PrivacyLevel.Public) => "This member's metadata (eg. created timestamp, message count, etc) is no longer hidden from other systems.",
(MemberPrivacySubject.Visibility, PrivacyLevel.Public) => "This member is no longer hidden from member lists.",
_ => throw new InvalidOperationException($"Invalid subject/level tuple ({subject}, {newLevel})")
};
await ctx.Reply($"{Emojis.Success} {target.NameFor(ctx).SanitizeMentions()}'s {subject.Name()} has been set to **{newLevel.Name()}**. {explanation}");
}
else if (ctx.Match("all") || newValueFromCommand != null)
{
newLevel = newValueFromCommand ?? PopPrivacyLevel("all");
target.SetAllPrivacy(newLevel);
await _data.SaveMember(target);
if(newLevel == PrivacyLevel.Private)
await ctx.Reply($"All {target.NameFor(ctx).SanitizeMentions()}'s privacy settings have been set to **{newLevel.Name()}**. Other accounts will now see nothing on the member card.");
else
await ctx.Reply($"All {target.NameFor(ctx).SanitizeMentions()}'s privacy settings have been set to **{newLevel.Name()}**. Other accounts will now see everything on the member card.");
}
else
await ctx.Reply($"{Emojis.Success} Member privacy set to **public**. This member will now show up in member lists and will return all information when queried by other accounts.");
{
var subjectList = "`name`, `description`, `avatar`, `birthday`, `pronouns`, `metadata`, `visibility`, or `all`";
throw new PKSyntaxError($"Invalid privacy subject `{ctx.PopArgument().SanitizeMentions()}` (must be {subjectList}).");
}
// Name privacy only works given a display name
if (subject == MemberPrivacySubject.Name && newLevel == PrivacyLevel.Private && target.DisplayName == null)
await ctx.Reply($"{Emojis.Warn} This member does not have a display name set, and name privacy **will not take effect**.");
// Avatar privacy doesn't apply when proxying if no server avatar is set
if (subject == MemberPrivacySubject.Avatar && newLevel == PrivacyLevel.Private &&
guildSettings?.AvatarUrl == null)
await ctx.Reply($"{Emojis.Warn} This member does not have a server avatar set, so *proxying* will **still show the member avatar**. If you want to hide your avatar when proxying here, set a server avatar: `pk;member {target.Hid} serveravatar`");
}
public async Task Delete(Context ctx, PKMember target)
@ -395,7 +488,7 @@ namespace PluralKit.Bot
if (ctx.System == null) throw Errors.NoSystemError;
if (target.System != ctx.System.Id) throw Errors.NotOwnMemberError;
await ctx.Reply($"{Emojis.Warn} Are you sure you want to delete \"{target.Name.SanitizeMentions()}\"? If so, reply to this message with the member's ID (`{target.Hid}`). __***This cannot be undone!***__");
await ctx.Reply($"{Emojis.Warn} Are you sure you want to delete \"{target.NameFor(ctx).SanitizeMentions()}\"? If so, reply to this message with the member's ID (`{target.Hid}`). __***This cannot be undone!***__");
if (!await ctx.ConfirmWithReply(target.Hid)) throw Errors.MemberDeleteCancelled;
await _data.DeleteMember(target);
await ctx.Reply($"{Emojis.Success} Member deleted.");

View file

@ -36,7 +36,7 @@ namespace PluralKit.Bot
if (conflicts.Count <= 0) return true;
var conflictList = conflicts.Select(m => $"- **{m.Name}**");
var conflictList = conflicts.Select(m => $"- **{m.NameFor(ctx)}**");
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);

View file

@ -62,7 +62,7 @@ namespace PluralKit.Bot
var lastSwitchMembers = _data.GetSwitchMembers(lastSwitch);
// Make sure the requested switch isn't identical to the last one
if (await lastSwitchMembers.Select(m => m.Id).SequenceEqualAsync(members.Select(m => m.Id).ToAsyncEnumerable()))
throw Errors.SameSwitch(members);
throw Errors.SameSwitch(members, ctx.LookupContextFor(ctx.System));
}
await _data.AddSwitch(ctx.System.Id, members);
@ -70,7 +70,7 @@ namespace PluralKit.Bot
if (members.Count == 0)
await ctx.Reply($"{Emojis.Success} Switch-out registered.");
else
await ctx.Reply($"{Emojis.Success} Switch registered. Current fronter is now {string.Join(", ", members.Select(m => m.Name)).SanitizeMentions()}.");
await ctx.Reply($"{Emojis.Success} Switch registered. Current fronter is now {string.Join(", ", members.Select(m => m.NameFor(ctx))).SanitizeMentions()}.");
}
public async Task SwitchMove(Context ctx)
@ -102,7 +102,7 @@ namespace PluralKit.Bot
// Now we can actually do the move, yay!
// But, we do a prompt to confirm.
var lastSwitchMembers = _data.GetSwitchMembers(lastTwoSwitches[0]);
var lastSwitchMemberStr = string.Join(", ", await lastSwitchMembers.Select(m => m.Name).ToListAsync());
var lastSwitchMemberStr = string.Join(", ", await lastSwitchMembers.Select(m => m.NameFor(ctx)).ToListAsync());
var lastSwitchTimeStr = DateTimeFormats.ZonedDateTimeFormat.Format(lastTwoSwitches[0].Timestamp.InZone(ctx.System.Zone));
var lastSwitchDeltaStr = DateTimeFormats.DurationFormat.Format(SystemClock.Instance.GetCurrentInstant() - lastTwoSwitches[0].Timestamp);
var newSwitchTimeStr = DateTimeFormats.ZonedDateTimeFormat.Format(time);
@ -137,7 +137,7 @@ namespace PluralKit.Bot
if (lastTwoSwitches.Count == 0) throw Errors.NoRegisteredSwitches;
var lastSwitchMembers = _data.GetSwitchMembers(lastTwoSwitches[0]);
var lastSwitchMemberStr = string.Join(", ", await lastSwitchMembers.Select(m => m.Name).ToListAsync());
var lastSwitchMemberStr = string.Join(", ", await lastSwitchMembers.Select(m => m.NameFor(ctx)).ToListAsync());
var lastSwitchDeltaStr = DateTimeFormats.DurationFormat.Format(SystemClock.Instance.GetCurrentInstant() - lastTwoSwitches[0].Timestamp);
DiscordMessage msg;
@ -149,7 +149,7 @@ namespace PluralKit.Bot
else
{
var secondSwitchMembers = _data.GetSwitchMembers(lastTwoSwitches[1]);
var secondSwitchMemberStr = string.Join(", ", await secondSwitchMembers.Select(m => m.Name).ToListAsync());
var secondSwitchMemberStr = string.Join(", ", await secondSwitchMembers.Select(m => m.NameFor(ctx)).ToListAsync());
var secondSwitchDeltaStr = DateTimeFormats.DurationFormat.Format(SystemClock.Instance.GetCurrentInstant() - lastTwoSwitches[1].Timestamp);
msg = await ctx.Reply(
$"{Emojis.Warn} This will delete the latest switch ({lastSwitchMemberStr.SanitizeMentions()}, {lastSwitchDeltaStr} ago). The next latest switch is {secondSwitchMemberStr.SanitizeMentions()} ({secondSwitchDeltaStr} ago). Is this okay?");

View file

@ -262,7 +262,7 @@ namespace PluralKit.Bot
.AddField("Member list", PrivacyLevelString(ctx.System.MemberListPrivacy))
.AddField("Current fronter(s)", PrivacyLevelString(ctx.System.FrontPrivacy))
.AddField("Front/switch history", PrivacyLevelString(ctx.System.FrontHistoryPrivacy))
.WithDescription("To edit privacy settings, use the command:\n`pk;system privacy <subject> <level>`\n\n- `subject` is one of `description`, `list`, `front` or `fronthistory`\n- `level` is either `public` or `private`.");
.WithDescription("To edit privacy settings, use the command:\n`pk;system privacy <subject> <level>`\n\n- `subject` is one of `description`, `list`, `front`, `fronthistory`, or `all` \n- `level` is either `public` or `private`.");
await ctx.Reply(embed: eb.Build());
return;
}
@ -289,7 +289,7 @@ namespace PluralKit.Bot
}
string levelStr, levelExplanation, subjectStr;
var subjectList = "`description`, `members`, `front` or `fronthistory`";
var subjectList = "`description`, `members`, `front`, `fronthistory`, or `all`";
if (ctx.Match("description", "desc", "text", "info"))
{
subjectStr = "description";
@ -310,10 +310,27 @@ namespace PluralKit.Bot
subjectStr = "front history";
ctx.System.FrontHistoryPrivacy = PopPrivacyLevel("fronthistory", out levelStr, out levelExplanation);
}
else if (ctx.Match("all")){
subjectStr = "all";
PrivacyLevel level = PopPrivacyLevel("all", out levelStr, out levelExplanation);
ctx.System.DescriptionPrivacy = level;
ctx.System.MemberListPrivacy = level;
ctx.System.FrontPrivacy = level;
ctx.System.FrontHistoryPrivacy = level;
}
else
throw new PKSyntaxError($"Invalid privacy subject `{ctx.PopArgument().SanitizeMentions()}` (must be {subjectList}).");
await _data.SaveSystem(ctx.System);
if(subjectStr == "all"){
if(levelStr == "private")
await ctx.Reply($"All of your systems privacy settings have been set to **{levelStr}**. Other accounts will now see nothing on the member card.");
else
await ctx.Reply($"All of your systems privacy have been set to **{levelStr}**. Other accounts will now see everything on the member card.");
}
//Handle other subjects
else
await ctx.Reply($"System {subjectStr} privacy has been set to **{levelStr}**. Other accounts will now {levelExplanation} your system {subjectStr}.");
}

View file

@ -39,7 +39,7 @@ namespace PluralKit.Bot
var sw = await _data.GetLatestSwitch(system.Id);
if (sw == null) throw Errors.NoRegisteredSwitches;
await ctx.Reply(embed: await _embeds.CreateFronterEmbed(sw, system.Zone));
await ctx.Reply(embed: await _embeds.CreateFronterEmbed(sw, system.Zone, ctx.LookupContextFor(system)));
}
public async Task SystemFrontHistory(Context ctx, PKSystem system)
@ -68,7 +68,7 @@ namespace PluralKit.Bot
var sw = entry.ThisSwitch;
// Fetch member list and format
var members = await _data.GetSwitchMembers(sw).ToListAsync();
var membersStr = members.Any() ? string.Join(", ", members.Select(m => m.Name)) : "no fronter";
var membersStr = members.Any() ? string.Join(", ", members.Select(m => m.NameFor(ctx))) : "no fronter";
var switchSince = SystemClock.Instance.GetCurrentInstant() - sw.Timestamp;
@ -113,7 +113,7 @@ namespace PluralKit.Bot
if (rangeStart.Value.ToInstant() > now) throw Errors.FrontPercentTimeInFuture;
var frontpercent = await _data.GetFrontBreakdown(system, rangeStart.Value.ToInstant(), now);
await ctx.Reply(embed: await _embeds.CreateFrontPercentEmbed(frontpercent, system.Zone));
await ctx.Reply(embed: await _embeds.CreateFrontPercentEmbed(frontpercent, system.Zone, ctx.LookupContextFor(system)));
}
}
}

View file

@ -26,7 +26,7 @@ namespace PluralKit.Bot
var renderer = GetRendererFor(ctx);
var opts = GetOptions(ctx, target);
var members = (await _db.Execute(c => opts.Execute(c, target))).ToList();
var members = (await _db.Execute(c => opts.Execute(c, target, ctx.LookupContextFor(target)))).ToList();
await ctx.Paginate(
members.ToAsyncEnumerable(),
members.Count,
@ -35,7 +35,7 @@ namespace PluralKit.Bot
(eb, ms) =>
{
eb.WithFooter($"{opts.CreateFilterString()}. {members.Count} results.");
renderer.RenderPage(eb, ctx.System?.Zone ?? DateTimeZone.Utc, ms);
renderer.RenderPage(eb, ctx.System?.Zone ?? DateTimeZone.Utc, ms, ctx.LookupContextFor(target));
return Task.CompletedTask;
});
}

View file

@ -68,11 +68,11 @@ namespace PluralKit.Bot {
public static PKError MemberLinkCancelled => new PKError("Member link cancelled.");
public static PKError MemberUnlinkCancelled => new PKError("Member unlink cancelled.");
public static PKError SameSwitch(ICollection<PKMember> members)
public static PKError SameSwitch(ICollection<PKMember> members, LookupContext ctx)
{
if (members.Count == 0) return new PKError("There's already no one in front.");
if (members.Count == 1) return new PKError($"Member {members.First().Name.SanitizeMentions()} is already fronting.");
return new PKError($"Members {string.Join(", ", members.Select(m => m.Name.SanitizeMentions()))} are already fronting.");
if (members.Count == 1) return new PKError($"Member {members.First().NameFor(ctx).SanitizeMentions()} is already fronting.");
return new PKError($"Members {string.Join(", ", members.Select(m => m.NameFor(ctx).SanitizeMentions()))} are already fronting.");
}
public static PKError DuplicateSwitchMembers => new PKError("Duplicate members in member list.");

View file

@ -108,14 +108,14 @@ namespace PluralKit.Bot
{
// If the system has pings enabled, go ahead
var embed = new DiscordEmbedBuilder().WithDescription($"[Jump to pinged message]({evt.Message.JumpLink})");
await evt.Channel.SendMessageAsync($"Psst, **{msg.Member.DisplayName ?? msg.Member.Name}** (<@{msg.Message.Sender}>), you have been pinged by <@{evt.User.Id}>.", embed: embed.Build());
await evt.Channel.SendMessageAsync($"Psst, **{msg.Member.DisplayName()}** (<@{msg.Message.Sender}>), you have been pinged by <@{evt.User.Id}>.", embed: embed.Build());
}
else
{
// If not, tell them in DMs (if we can)
try
{
await guildUser.SendMessageAsync($"{Emojis.Error} {msg.Member.DisplayName ?? msg.Member.Name}'s system has disabled reaction pings. If you want to mention them anyway, you can copy/paste the following message:");
await guildUser.SendMessageAsync($"{Emojis.Error} {msg.Member.DisplayName()}'s system has disabled reaction pings. If you want to mention them anyway, you can copy/paste the following message:");
await guildUser.SendMessageAsync($"`<@{msg.Message.Sender}>`");
}
catch (UnauthorizedException) { }

View file

@ -11,6 +11,6 @@ namespace PluralKit.Bot
public interface IListRenderer
{
int MembersPerPage { get; }
void RenderPage(DiscordEmbedBuilder eb, DateTimeZone zone, IEnumerable<ListedMember> members);
void RenderPage(DiscordEmbedBuilder eb, DateTimeZone zone, IEnumerable<ListedMember> members, LookupContext ctx);
}
}

View file

@ -20,25 +20,25 @@ namespace PluralKit.Bot
_fields = fields;
}
public void RenderPage(DiscordEmbedBuilder eb, DateTimeZone zone, IEnumerable<ListedMember> members)
public void RenderPage(DiscordEmbedBuilder eb, DateTimeZone zone, IEnumerable<ListedMember> members, LookupContext ctx)
{
string FormatTimestamp(Instant timestamp) => DateTimeFormats.ZonedDateTimeFormat.Format(timestamp.InZone(zone));
foreach (var m in members)
{
var profile = $"**ID**: {m.Hid}";
if (_fields.ShowDisplayName && m.DisplayName != null) profile += $"\n**Display name**: {m.DisplayName}";
if (_fields.ShowPronouns && m.Pronouns != null) profile += $"\n**Pronouns**: {m.Pronouns}";
if (_fields.ShowBirthday && m.Birthday != null) profile += $"\n**Birthdate**: {m.BirthdayString}";
if (_fields.ShowDisplayName && m.DisplayName != null && m.NamePrivacy.CanAccess(ctx)) profile += $"\n**Display name**: {m.DisplayName}";
if (_fields.ShowPronouns && m.Pronouns != null && m.PronounPrivacy.CanAccess(ctx)) profile += $"\n**Pronouns**: {m.Pronouns}";
if (_fields.ShowBirthday && m.Birthday != null && m.BirthdayPrivacy.CanAccess(ctx)) profile += $"\n**Birthdate**: {m.BirthdayString}";
if (_fields.ShowProxyTags && m.ProxyTags.Count > 0) profile += $"\n**Proxy tags:** {m.ProxyTagsString()}";
if (_fields.ShowMessageCount && m.MessageCount > 0) profile += $"\n**Message count:** {m.MessageCount}";
if (_fields.ShowLastMessage && m.LastMessage != null) profile += $"\n**Last message:** {FormatTimestamp(DiscordUtils.SnowflakeToInstant(m.LastMessage.Value))}";
if (_fields.ShowLastSwitch && m.LastSwitchTime != null) profile += $"\n**Last switched in:** {FormatTimestamp(m.LastSwitchTime.Value)}";
if (_fields.ShowDescription && m.Description != null) profile += $"\n\n{m.Description}";
if (_fields.ShowPrivacy && m.MemberPrivacy == PrivacyLevel.Private)
profile += "\n*(this member is private)*";
if (_fields.ShowMessageCount && m.MessageCount > 0 && m.MetadataPrivacy.CanAccess(ctx)) profile += $"\n**Message count:** {m.MessageCount}";
if (_fields.ShowLastMessage && m.LastMessage != null && m.MetadataPrivacy.CanAccess(ctx)) profile += $"\n**Last message:** {FormatTimestamp(DiscordUtils.SnowflakeToInstant(m.LastMessage.Value))}";
if (_fields.ShowLastSwitch && m.LastSwitchTime != null && m.MetadataPrivacy.CanAccess(ctx)) profile += $"\n**Last switched in:** {FormatTimestamp(m.LastSwitchTime.Value)}";
if (_fields.ShowDescription && m.Description != null && m.DescriptionPrivacy.CanAccess(ctx)) profile += $"\n\n{m.Description}";
if (_fields.ShowPrivacy && m.MemberVisibility == PrivacyLevel.Private)
profile += "\n*(this member is hidden)*";
eb.AddField(m.Name, profile.Truncate(1024));
eb.AddField(m.NameFor(ctx), profile.Truncate(1024));
}
}

View file

@ -13,7 +13,7 @@ namespace PluralKit.Bot
{
public int MembersPerPage => 25;
public void RenderPage(DiscordEmbedBuilder eb, DateTimeZone timezone, IEnumerable<ListedMember> members)
public void RenderPage(DiscordEmbedBuilder eb, DateTimeZone timezone, IEnumerable<ListedMember> members, LookupContext ctx)
{
string RenderLine(ListedMember m)
{
@ -22,11 +22,10 @@ namespace PluralKit.Bot
var proxyTagsString = m.ProxyTagsString().SanitizeMentions();
if (proxyTagsString.Length > 100) // arbitrary threshold for now, tweak?
proxyTagsString = "tags too long, see member card";
return $"[`{m.Hid}`] **{m.Name.SanitizeMentions()}** *({proxyTagsString})*";
return $"[`{m.Hid}`] **{m.NameFor(ctx).SanitizeMentions()}** *({proxyTagsString})*";
}
return $"[`{m.Hid}`] **{m.Name.SanitizeMentions()}**";
return $"[`{m.Hid}`] **{m.NameFor(ctx).SanitizeMentions()}**";
}
var buf = new StringBuilder();

View file

@ -52,14 +52,14 @@ namespace PluralKit.Bot
return str.ToString();
}
public async Task<IEnumerable<ListedMember>> Execute(IPKConnection conn, PKSystem system)
public async Task<IEnumerable<ListedMember>> Execute(IPKConnection conn, PKSystem system, LookupContext ctx)
{
var filtered = await QueryWithFilter(conn, system);
return Sort(filtered);
var filtered = await QueryWithFilter(conn, system, ctx);
return Sort(filtered, ctx);
}
private Task<IEnumerable<ListedMember>> QueryWithFilter(IPKConnection conn, PKSystem system) =>
conn.QueryMemberList(system.Id, PrivacyFilter switch
private Task<IEnumerable<ListedMember>> QueryWithFilter(IPKConnection conn, PKSystem system, LookupContext ctx) =>
conn.QueryMemberList(system.Id, ctx, PrivacyFilter switch
{
PrivacyFilter.PrivateOnly => PrivacyLevel.Private,
PrivacyFilter.PublicOnly => PrivacyLevel.Public,
@ -67,7 +67,7 @@ namespace PluralKit.Bot
_ => throw new ArgumentOutOfRangeException($"Unknown privacy filter {PrivacyFilter}")
}, Filter, SearchInDescription);
private IEnumerable<ListedMember> Sort(IEnumerable<ListedMember> input)
private IEnumerable<ListedMember> Sort(IEnumerable<ListedMember> input, LookupContext ctx)
{
IComparer<T> ReverseMaybe<T>(IComparer<T> c) =>
Reverse ? Comparer<T>.Create((a, b) => c.Compare(b, a)) : c;
@ -78,7 +78,7 @@ namespace PluralKit.Bot
// As for the OrderByDescending HasValue calls: https://www.jerriepelser.com/blog/orderby-with-null-values/
// We want nulls last no matter what, even if orders are reversed
SortProperty.Hid => input.OrderBy(m => m.Hid, ReverseMaybe(culture)),
SortProperty.Name => input.OrderBy(m => m.Name, ReverseMaybe(culture)),
SortProperty.Name => input.OrderBy(m => m.NameFor(ctx), ReverseMaybe(culture)),
SortProperty.CreationDate => input.OrderBy(m => m.Created, ReverseMaybe(Comparer<Instant>.Default)),
SortProperty.MessageCount => input.OrderByDescending(m => m.MessageCount, ReverseMaybe(Comparer<int>.Default)),
SortProperty.DisplayName => input
@ -96,7 +96,7 @@ namespace PluralKit.Bot
_ => throw new ArgumentOutOfRangeException($"Unknown sort property {SortProperty}")
})
// Lastly, add a by-name fallback order for collisions (generally hits w/ lots of null values)
.ThenBy(m => m.Name, culture);
.ThenBy(m => m.NameFor(ctx), culture);
}
public static SortFilterOptions FromFlags(Context ctx)

View file

@ -26,6 +26,8 @@ namespace PluralKit.Bot {
_db = db;
}
public async Task<DiscordEmbed> CreateSystemEmbed(DiscordClient client, PKSystem system, LookupContext ctx) {
var accounts = await _data.GetSystemAccounts(system);
@ -45,7 +47,7 @@ namespace PluralKit.Bot {
var switchMembers = await _data.GetSwitchMembers(latestSwitch).ToListAsync();
if (switchMembers.Count > 0)
eb.AddField("Fronter".ToQuantity(switchMembers.Count(), ShowQuantityAs.None),
string.Join(", ", switchMembers.Select(m => m.Name)));
string.Join(", ", switchMembers.Select(m => m.NameFor(ctx))));
}
if (system.Tag != null) eb.AddField("Tag", system.Tag.EscapeMarkdown());
@ -68,9 +70,10 @@ namespace PluralKit.Bot {
public DiscordEmbed CreateLoggedMessageEmbed(PKSystem system, PKMember member, ulong messageId, ulong originalMsgId, DiscordUser sender, string content, DiscordChannel channel) {
// TODO: pronouns in ?-reacted response using this card
var timestamp = DiscordUtils.SnowflakeToInstant(messageId);
var name = member.NameFor(LookupContext.ByNonOwner);
return new DiscordEmbedBuilder()
.WithAuthor($"#{channel.Name}: {member.Name}", iconUrl: DiscordUtils.WorkaroundForUrlBug(member.AvatarUrl))
.WithThumbnailUrl(member.AvatarUrl)
.WithAuthor($"#{channel.Name}: {name}", iconUrl: DiscordUtils.WorkaroundForUrlBug(member.AvatarFor(LookupContext.ByNonOwner)))
.WithThumbnailUrl(member.AvatarFor(LookupContext.ByNonOwner))
.WithDescription(content?.NormalizeLineEndSpacing())
.WithFooter($"System ID: {system.Hid} | Member ID: {member.Hid} | Sender: {sender.Username}#{sender.Discriminator} ({sender.Id}) | Message ID: {messageId} | Original Message ID: {originalMsgId}")
.WithTimestamp(timestamp.ToDateTimeOffset())
@ -79,8 +82,11 @@ namespace PluralKit.Bot {
public async Task<DiscordEmbed> CreateMemberEmbed(PKSystem system, PKMember member, DiscordGuild guild, LookupContext ctx)
{
var name = member.Name;
if (system.Name != null) name = $"{member.Name} ({system.Name})";
// 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
@ -97,20 +103,21 @@ namespace PluralKit.Bot {
var guildSettings = guild != null ? await _db.Execute(c => c.QueryOrInsertMemberGuildConfig(guild.Id, member.Id)) : null;
var guildDisplayName = guildSettings?.DisplayName;
var avatar = guildSettings?.AvatarUrl ?? member.AvatarUrl;
var avatar = guildSettings?.AvatarUrl ?? member.AvatarFor(ctx);
var proxyTagsStr = string.Join('\n', member.ProxyTags.Select(t => $"`{t.ProxyString}`"));
var eb = new DiscordEmbedBuilder()
// TODO: add URL of website when that's up
.WithAuthor(name, iconUrl: DiscordUtils.WorkaroundForUrlBug(avatar))
.WithColor(member.MemberPrivacy.CanAccess(ctx) ? color : DiscordUtils.Gray)
.WithFooter($"System ID: {system.Hid} | Member ID: {member.Hid} | Created on {DateTimeFormats.ZonedDateTimeFormat.Format(member.Created.InZone(system.Zone))}");
// .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 {DateTimeFormats.ZonedDateTimeFormat.Format(member.Created.InZone(system.Zone))}":"")}");
var description = "";
if (member.MemberPrivacy == PrivacyLevel.Private) description += "*(this member is private)*\n";
if (member.MemberVisibility == PrivacyLevel.Private) description += "*(this member is hidden)*\n";
if (guildSettings?.AvatarUrl != null)
if (member.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";
@ -118,31 +125,38 @@ namespace PluralKit.Bot {
if (avatar != null) eb.WithThumbnailUrl(avatar);
if (!member.DisplayName.EmptyOrNull()) eb.AddField("Display Name", member.DisplayName.Truncate(1024), true);
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.Birthday != null && member.MemberPrivacy.CanAccess(ctx)) eb.AddField("Birthdate", member.BirthdayString, true);
if (!member.Pronouns.EmptyOrNull() && member.MemberPrivacy.CanAccess(ctx)) eb.AddField("Pronouns", member.Pronouns.Truncate(1024), true);
if (member.MessageCount > 0 && member.MemberPrivacy.CanAccess(ctx)) eb.AddField("Message Count", member.MessageCount.ToString(), true);
if (member.Birthday != null && member.BirthdayPrivacy.CanAccess(ctx)) eb.AddField("Birthdate", member.BirthdayString, true);
if (!member.Pronouns.EmptyOrNull() && member.PronounPrivacy.CanAccess(ctx)) eb.AddField("Pronouns", member.Pronouns.Truncate(1024), true);
if (member.MessageCount > 0 && member.MetadataPrivacy.CanAccess(ctx)) eb.AddField("Message Count", member.MessageCount.ToString(), true);
if (member.HasProxyTags) eb.AddField("Proxy Tags", string.Join('\n', proxyTagsStr).Truncate(1024), true);
if (!member.Color.EmptyOrNull() && member.MemberPrivacy.CanAccess(ctx)) eb.AddField("Color", $"#{member.Color}", true);
if (!member.Description.EmptyOrNull() && member.MemberPrivacy.CanAccess(ctx)) eb.AddField("Description", member.Description.NormalizeLineEndSpacing(), false);
// --- 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 (!member.Description.EmptyOrNull() && member.DescriptionPrivacy.CanAccess(ctx)) eb.AddField("Description", member.Description.NormalizeLineEndSpacing(), false);
return eb.Build();
}
public async Task<DiscordEmbed> CreateFronterEmbed(PKSwitch sw, DateTimeZone zone)
public async Task<DiscordEmbed> CreateFronterEmbed(PKSwitch sw, DateTimeZone zone, LookupContext ctx)
{
var members = await _data.GetSwitchMembers(sw).ToListAsync();
var timeSinceSwitch = SystemClock.Instance.GetCurrentInstant() - sw.Timestamp;
return new DiscordEmbedBuilder()
.WithColor(members.FirstOrDefault()?.Color?.ToDiscordColor() ?? DiscordUtils.Gray)
.AddField($"Current {"fronter".ToQuantity(members.Count, ShowQuantityAs.None)}", members.Count > 0 ? string.Join(", ", members.Select(m => m.Name)) : "*(no fronter)*")
.AddField($"Current {"fronter".ToQuantity(members.Count, ShowQuantityAs.None)}", members.Count > 0 ? string.Join(", ", members.Select(m => m.NameFor(ctx))) : "*(no fronter)*")
.AddField("Since", $"{DateTimeFormats.ZonedDateTimeFormat.Format(sw.Timestamp.InZone(zone))} ({DateTimeFormats.DurationFormat.Format(timeSinceSwitch)} ago)")
.Build();
}
public async Task<DiscordEmbed> CreateMessageInfoEmbed(DiscordClient client, FullMessage msg)
{
var ctx = LookupContext.ByNonOwner;
var channel = await client.GetChannelAsync(msg.Message.Channel);
var serverMsg = channel != null ? await channel.GetMessageAsync(msg.Message.Mid) : null;
@ -165,12 +179,12 @@ namespace PluralKit.Bot {
// Put it all together
var eb = new DiscordEmbedBuilder()
.WithAuthor(msg.Member.Name, iconUrl: DiscordUtils.WorkaroundForUrlBug(msg.Member.AvatarUrl))
.WithAuthor(msg.Member.NameFor(ctx), iconUrl: DiscordUtils.WorkaroundForUrlBug(msg.Member.AvatarFor(ctx)))
.WithDescription(serverMsg?.Content?.NormalizeLineEndSpacing() ?? "*(message contents deleted or inaccessible)*")
.WithImageUrl(serverMsg?.Attachments?.FirstOrDefault()?.Url)
.AddField("System",
msg.System.Name != null ? $"{msg.System.Name} (`{msg.System.Hid}`)" : $"`{msg.System.Hid}`", true)
.AddField("Member", $"{msg.Member.Name} (`{msg.Member.Hid}`)", true)
.AddField("Member", $"{msg.Member.NameFor(ctx)} (`{msg.Member.Hid}`)", true)
.AddField("Sent by", userStr, inline: true)
.WithTimestamp(DiscordUtils.SnowflakeToInstant(msg.Message.Mid).ToDateTimeOffset());
@ -181,7 +195,7 @@ namespace PluralKit.Bot {
return eb.Build();
}
public Task<DiscordEmbed> CreateFrontPercentEmbed(FrontBreakdown breakdown, DateTimeZone tz)
public Task<DiscordEmbed> CreateFrontPercentEmbed(FrontBreakdown breakdown, DateTimeZone tz, LookupContext ctx)
{
var actualPeriod = breakdown.RangeEnd - breakdown.RangeStart;
var eb = new DiscordEmbedBuilder()
@ -200,7 +214,7 @@ namespace PluralKit.Bot {
foreach (var pair in membersOrdered)
{
var frac = pair.Value / actualPeriod;
eb.AddField(pair.Key?.Name ?? "*(no fronter)*", $"{frac*100:F0}% ({DateTimeFormats.DurationFormat.Format(pair.Value)})");
eb.AddField(pair.Key?.NameFor(ctx) ?? "*(no fronter)*", $"{frac*100:F0}% ({DateTimeFormats.DurationFormat.Format(pair.Value)})");
}
if (membersOrdered.Count > maxEntriesToDisplay)

View file

@ -0,0 +1,16 @@
using PluralKit.Core;
namespace PluralKit.Bot
{
public static class ModelUtils
{
public static string NameFor(this PKMember member, Context ctx) =>
member.NameFor(ctx.LookupContextFor(member));
public static string AvatarFor(this PKMember member, Context ctx) =>
member.AvatarFor(ctx.LookupContextFor(member));
public static string DisplayName(this PKMember member) =>
member.DisplayName ?? member.Name;
}
}