diff --git a/PluralKit.API/Controllers/v1/JsonModelExt.cs b/PluralKit.API/Controllers/v1/JsonModelExt.cs index d8f39a8d..ee2c4132 100644 --- a/PluralKit.API/Controllers/v1/JsonModelExt.cs +++ b/PluralKit.API/Controllers/v1/JsonModelExt.cs @@ -50,7 +50,7 @@ namespace PluralKit.API o.Add("display_name", member.NamePrivacy.CanAccess(ctx) ? member.DisplayName : null); o.Add("birthday", member.BirthdayPrivacy.CanAccess(ctx) && member.Birthday.HasValue ? DateTimeFormats.DateExportFormat.Format(member.Birthday.Value) : null); o.Add("pronouns", member.PronounPrivacy.CanAccess(ctx) ? member.Pronouns : null); - o.Add("avatar_url", member.AvatarUrl); + o.Add("avatar_url", member.AvatarPrivacy.CanAccess(ctx) ? member.AvatarUrl : null); o.Add("description", member.DescriptionPrivacy.CanAccess(ctx) ? member.Description : null); var tagArray = new JArray(); @@ -67,6 +67,7 @@ namespace PluralKit.API o.Add("description_privacy", ctx == LookupContext.ByOwner ? (member.DescriptionPrivacy == PrivacyLevel.Private ? "private" : "public") : null); o.Add("birthday_privacy", ctx == LookupContext.ByOwner ? (member.BirthdayPrivacy == PrivacyLevel.Private ? "private" : "public") : null); o.Add("pronoun_privacy", ctx == LookupContext.ByOwner ? (member.PronounPrivacy == PrivacyLevel.Private ? "private" : "public") : null); + o.Add("avatar_privacy", ctx == LookupContext.ByOwner ? (member.AvatarPrivacy == PrivacyLevel.Private ? "private" : "public") : null); // o.Add("color_privacy", ctx == LookupContext.ByOwner ? (member.ColorPrivacy == PrivacyLevel.Private ? "private" : "public") : null); o.Add("metadata_privacy", ctx == LookupContext.ByOwner ? (member.MetadataPrivacy == PrivacyLevel.Private ? "private" : "public") : null); @@ -122,6 +123,7 @@ namespace PluralKit.API member.MemberVisibility = plevel; member.NamePrivacy = plevel; + member.AvatarPrivacy = plevel; member.DescriptionPrivacy = plevel; member.BirthdayPrivacy = plevel; member.PronounPrivacy = plevel; @@ -133,6 +135,7 @@ namespace PluralKit.API if (o.ContainsKey("visibility")) member.MemberVisibility = o.Value("visibility").ParsePrivacy("member"); if (o.ContainsKey("name_privacy")) member.NamePrivacy = o.Value("name_privacy").ParsePrivacy("member"); if (o.ContainsKey("description_privacy")) member.DescriptionPrivacy = o.Value("description_privacy").ParsePrivacy("member"); + if (o.ContainsKey("avatar_privacy")) member.AvatarPrivacy = o.Value("avatar_privacy").ParsePrivacy("member"); if (o.ContainsKey("birthday_privacy")) member.BirthdayPrivacy = o.Value("birthday_privacy").ParsePrivacy("member"); if (o.ContainsKey("pronoun_privacy")) member.PronounPrivacy = o.Value("pronoun_privacy").ParsePrivacy("member"); // if (o.ContainsKey("color_privacy")) member.ColorPrivacy = o.Value("color_privacy").ParsePrivacy("member"); diff --git a/PluralKit.API/openapi.yaml b/PluralKit.API/openapi.yaml index b1b1775e..33653f3c 100644 --- a/PluralKit.API/openapi.yaml +++ b/PluralKit.API/openapi.yaml @@ -625,6 +625,19 @@ components: Because of this, there is no way for an unauthorized user to tell the difference between a private description and a `null` description - this is intentional. example: public + avatar_privacy: + allOf: + - $ref: "#/components/schemas/PrivacySetting" + - description: | + The member's current avatar privacy setting, either "public" or "private". + + If this is set to "private", the field `avatar_url` will be returned as `null` on all requests not authorized with this system's token. + + In addition, this field will be returned as `null` if the request is not authorized with this system's token. + + Because of this, there is no way for an unauthorized user to tell the difference between a private avatar and a `null` avatar - this is intentional. + example: public + pronouns_privacy: allOf: - $ref: "#/components/schemas/PrivacySetting" diff --git a/PluralKit.Bot/Commands/MemberAvatar.cs b/PluralKit.Bot/Commands/MemberAvatar.cs index c8bfbb19..b2b8dbcc 100644 --- a/PluralKit.Bot/Commands/MemberAvatar.cs +++ b/PluralKit.Bot/Commands/MemberAvatar.cs @@ -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) { diff --git a/PluralKit.Bot/Commands/MemberEdit.cs b/PluralKit.Bot/Commands/MemberEdit.cs index 07db4880..38219eef 100644 --- a/PluralKit.Bot/Commands/MemberEdit.cs +++ b/PluralKit.Bot/Commands/MemberEdit.cs @@ -378,12 +378,13 @@ namespace PluralKit.Bot .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 privacy `\n\n- `subject` is one of `name`, `description`, `birthday`, `pronouns`, `created`, `messages`, `visibility`, or `all`\n- `level` is either `public` or `private`."); + .WithDescription("To edit privacy settings, use the command:\n`pk;member privacy `\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(); } @@ -432,6 +433,7 @@ namespace PluralKit.Bot { (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.", @@ -439,6 +441,7 @@ namespace PluralKit.Bot (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.", @@ -462,7 +465,7 @@ namespace PluralKit.Bot } else { - var subjectList = "`name`, `description`, `birthday`, `pronouns`, `metadata`, `visibility`, or `all`"; + var subjectList = "`name`, `description`, `avatar`, `birthday`, `pronouns`, `metadata`, `visibility`, or `all`"; throw new PKSyntaxError($"Invalid privacy subject `{ctx.PopArgument().SanitizeMentions()}` (must be {subjectList})."); } diff --git a/PluralKit.Bot/Services/EmbedService.cs b/PluralKit.Bot/Services/EmbedService.cs index eaf48dea..4443a4f5 100644 --- a/PluralKit.Bot/Services/EmbedService.cs +++ b/PluralKit.Bot/Services/EmbedService.cs @@ -72,8 +72,8 @@ namespace PluralKit.Bot { var timestamp = DiscordUtils.SnowflakeToInstant(messageId); var name = member.NameFor(LookupContext.ByNonOwner); return new DiscordEmbedBuilder() - .WithAuthor($"#{channel.Name}: {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()) @@ -103,7 +103,7 @@ 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}`")); @@ -117,7 +117,7 @@ namespace PluralKit.Bot { var description = ""; 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"; @@ -179,7 +179,7 @@ namespace PluralKit.Bot { // Put it all together var eb = new DiscordEmbedBuilder() - .WithAuthor(msg.Member.NameFor(ctx), 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", diff --git a/PluralKit.Bot/Utils/ModelUtils.cs b/PluralKit.Bot/Utils/ModelUtils.cs index 7e90042e..28dc3927 100644 --- a/PluralKit.Bot/Utils/ModelUtils.cs +++ b/PluralKit.Bot/Utils/ModelUtils.cs @@ -7,6 +7,9 @@ namespace PluralKit.Bot 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; } diff --git a/PluralKit.Core/Database/Migrations/8.sql b/PluralKit.Core/Database/Migrations/8.sql index 366a9309..0a56dfaa 100644 --- a/PluralKit.Core/Database/Migrations/8.sql +++ b/PluralKit.Core/Database/Migrations/8.sql @@ -2,6 +2,7 @@ -- Create new columns -- alter table members add column description_privacy integer check (description_privacy in (1, 2)) not null default 1; alter table members add column name_privacy integer check (name_privacy in (1, 2)) not null default 1; +alter table members add column avatar_privacy integer check (avatar_privacy in (1, 2)) not null default 1; alter table members add column birthday_privacy integer check (birthday_privacy in (1, 2)) not null default 1; alter table members add column pronoun_privacy integer check (pronoun_privacy in (1, 2)) not null default 1; alter table members add column metadata_privacy integer check (metadata_privacy in (1, 2)) not null default 1; @@ -10,6 +11,7 @@ alter table members add column metadata_privacy integer check (metadata_privacy -- Transfer existing settings -- update members set description_privacy = member_privacy; update members set name_privacy = member_privacy; +update members set avatar_privacy = member_privacy; update members set birthday_privacy = member_privacy; update members set pronoun_privacy = member_privacy; update members set metadata_privacy = member_privacy; diff --git a/PluralKit.Core/Models/ModelExtensions.cs b/PluralKit.Core/Models/ModelExtensions.cs index 55856bf9..47d2cc61 100644 --- a/PluralKit.Core/Models/ModelExtensions.cs +++ b/PluralKit.Core/Models/ModelExtensions.cs @@ -4,5 +4,8 @@ { public static string NameFor(this PKMember member, LookupContext ctx) => member.NamePrivacy.CanAccess(ctx) ? member.Name : member.DisplayName ?? member.Name; + + public static string AvatarFor(this PKMember member, LookupContext ctx) => + member.AvatarPrivacy.CanAccess(ctx) ? member.AvatarUrl : null; } } \ No newline at end of file diff --git a/PluralKit.Core/Models/PKMember.cs b/PluralKit.Core/Models/PKMember.cs index 01eb004c..97c620ef 100644 --- a/PluralKit.Core/Models/PKMember.cs +++ b/PluralKit.Core/Models/PKMember.cs @@ -25,6 +25,7 @@ namespace PluralKit.Core { public PrivacyLevel MemberVisibility { get; set; } public PrivacyLevel DescriptionPrivacy { get; set; } + public PrivacyLevel AvatarPrivacy { get; set; } public PrivacyLevel NamePrivacy { get; set; } //ignore setting if no display name is set public PrivacyLevel BirthdayPrivacy { get; set; } public PrivacyLevel PronounPrivacy { get; set; } diff --git a/PluralKit.Core/Services/PostgresDataStore.cs b/PluralKit.Core/Services/PostgresDataStore.cs index cf3490c7..fd4bc579 100644 --- a/PluralKit.Core/Services/PostgresDataStore.cs +++ b/PluralKit.Core/Services/PostgresDataStore.cs @@ -150,7 +150,7 @@ namespace PluralKit.Core { public async Task SaveMember(PKMember member) { using (var conn = await _conn.Obtain()) - await conn.ExecuteAsync("update members set name = @Name, display_name = @DisplayName, description = @Description, color = @Color, avatar_url = @AvatarUrl, birthday = @Birthday, pronouns = @Pronouns, proxy_tags = @ProxyTags, keep_proxy = @KeepProxy, member_visibility = @MemberVisibility, description_privacy = @DescriptionPrivacy, name_privacy = @NamePrivacy, birthday_privacy = @BirthdayPrivacy, pronoun_privacy = @PronounPrivacy, metadata_privacy = @MetadataPrivacy where id = @Id", member); + await conn.ExecuteAsync("update members set name = @Name, display_name = @DisplayName, description = @Description, color = @Color, avatar_url = @AvatarUrl, birthday = @Birthday, pronouns = @Pronouns, proxy_tags = @ProxyTags, keep_proxy = @KeepProxy, member_visibility = @MemberVisibility, description_privacy = @DescriptionPrivacy, name_privacy = @NamePrivacy, avatar_privacy = @AvatarPrivacy, birthday_privacy = @BirthdayPrivacy, pronoun_privacy = @PronounPrivacy, metadata_privacy = @MetadataPrivacy where id = @Id", member); _logger.Information("Updated member {@Member}", member); } diff --git a/PluralKit.Core/Utils/PrivacyUtils.cs b/PluralKit.Core/Utils/PrivacyUtils.cs index a1952274..8cbe079b 100644 --- a/PluralKit.Core/Utils/PrivacyUtils.cs +++ b/PluralKit.Core/Utils/PrivacyUtils.cs @@ -6,6 +6,7 @@ namespace PluralKit.Core Visibility, Name, Description, + Avatar, Birthday, Pronouns, Metadata @@ -17,6 +18,7 @@ namespace PluralKit.Core { MemberPrivacySubject.Name => "name", MemberPrivacySubject.Description => "description", + MemberPrivacySubject.Avatar => "avatar", MemberPrivacySubject.Pronouns => "pronouns", MemberPrivacySubject.Birthday => "birthday", MemberPrivacySubject.Metadata => "metadata", @@ -31,6 +33,7 @@ namespace PluralKit.Core { MemberPrivacySubject.Name => member.NamePrivacy = level, MemberPrivacySubject.Description => member.DescriptionPrivacy = level, + MemberPrivacySubject.Avatar => member.AvatarPrivacy = level, MemberPrivacySubject.Pronouns => member.PronounPrivacy = level, MemberPrivacySubject.Birthday => member.BirthdayPrivacy= level, MemberPrivacySubject.Metadata => member.MetadataPrivacy = level, @@ -43,6 +46,7 @@ namespace PluralKit.Core { member.NamePrivacy = level; member.DescriptionPrivacy = level; + member.AvatarPrivacy = level; member.PronounPrivacy = level; member.BirthdayPrivacy = level; member.MetadataPrivacy = level; @@ -62,6 +66,12 @@ namespace PluralKit.Core case "info": subject = MemberPrivacySubject.Description; break; + case "avatar": + case "pfp": + case "pic": + case "icon": + subject = MemberPrivacySubject.Avatar; + break; case "birthday": case "birth": case "bday": diff --git a/docs/2-user-guide.md b/docs/2-user-guide.md index 0057f4cd..db14fd5b 100644 --- a/docs/2-user-guide.md +++ b/docs/2-user-guide.md @@ -449,10 +449,11 @@ For example: When the **member list** is **private**, other users will not be able to view the full member list of your system, but they can still query individual members given their 5-letter ID. If **current fronter** is private, but **front history** isn't, someone can still see the current fronter by looking at the history (this combination doesn't make much sense). ### Member privacy -There are also six options for configuring member privacy; +There are also seven options for configuring member privacy; - Name - Description +- Avatar - Birthday - Pronouns - Metadata *(message count, creation date, etc)* @@ -468,8 +469,8 @@ To update a members privacy you can use the command: member privacy -where `` is the name or the id of a member in your system, `` is either `name`, `description`, `birthday`, `pronouns`, `metadata`, or `visiblity` corresponding to the options above, and `` is either `public` or `private`. `` can also be `all` in order to change all subjects at once. -`metatdata` will affect the message count, the date created, the last fronted, and the last message information. +where `` is the name or the id of a member in your system, `` is either `name`, `description`, `avatar`, `birthday`, `pronouns`, `metadata`, or `visiblity` corresponding to the options above, and `` is either `public` or `private`. `` can also be `all` in order to change all subjects at once. +`metadata` will affect the message count, the date created, the last fronted, and the last message information. For example: diff --git a/docs/4-api-documentation.md b/docs/4-api-documentation.md index fefb14ab..5c059bc7 100644 --- a/docs/4-api-documentation.md +++ b/docs/4-api-documentation.md @@ -68,6 +68,7 @@ The following three models (usually represented in JSON format) represent the va |visibility|string?|Yes|Patching with `private` will set it to private; `public` or `null` will set it to public.| |name_privacy|string?|Yes|Patching with `private` will set it to private; `public` or `null` will set it to public.| |description_privacy|string?|Yes|Patching with `private` will set it to private; `public` or `null` will set it to public.| +|avatar_privacy|string?|Yes|Patching with `private` will set it to private; `public` or `null` will set it to public.| |birthday_privacy|string?|Yes|Patching with `private` will set it to private; `public` or `null` will set it to public.| |pronoun_privacy|string?|Yes|Patching with `private` will set it to private; `public` or `null` will set it to public.| |metadata_privacy|string?|Yes|Patching with `private` will set it to private; `public` or `null` will set it to public.| @@ -234,6 +235,7 @@ If the system has chosen to hide its current fronters, this will return `403 For "visibility": null, "name_privacy": null, "description_privacy": null, + "avatar_privacy": null, "birthday_privacy": null, "pronoun_privacy": null, "metadata_privacy": null, @@ -322,6 +324,7 @@ If this member is marked private, and the request isn't authenticated with the m "visibility": "public", "name_privacy": "public", "description_privacy": "private", + "avatar_privacy": "private", "birthday_privacy": "private", "pronoun_privacy": "public", "metadata_privacy": "public" @@ -349,6 +352,7 @@ Creates a new member with the information given. Missing fields (except for name "visibility": "public", "name_privacy": "public", "description_privacy": "private", + "avatar_privacy": "private", "birthday_privacy": "private", "pronoun_privacy": "public", "metadata_privacy": "private" @@ -400,6 +404,7 @@ Edits a member's information. Missing fields will keep their current values. Wil "visibility": "public", "name_privacy": "public", "description_privacy": "private", + "avatar_privacy": "private", "birthday_privacy": "private", "pronoun_privacy": "public", "metadata_privacy": "private" @@ -424,6 +429,7 @@ Edits a member's information. Missing fields will keep their current values. Wil "visibility": "public", "name_privacy": "public", "description_privacy": "private", + "avatar_privacy": "private", "birthday_privacy": "private", "pronoun_privacy": "public", "metadata_privacy": "private" @@ -505,6 +511,7 @@ The returned system and member's privacy settings will be respected, and as such "visibility": "public", "name_privacy": "public", "description_privacy": "private", + "avatar_privacy": "private", "birthday_privacy": "private", "pronoun_privacy": "public", "metadata_privacy": "private" @@ -514,7 +521,7 @@ The returned system and member's privacy settings will be respected, and as such ## Version history * 2020-06-17 (v1.1) - * The API now has values for granular member privacy. The new fields are as follows: `visibility`, `name_privacy`, `description_privacy`, `birthday_privacy`, `pronoun_privacy`, `metadata_privacy`. All are strings and accept the values of `public`, `private` and `null` + * The API now has values for granular member privacy. The new fields are as follows: `visibility`, `name_privacy`, `description_privacy`, `avatar_privacy`, `birthday_privacy`, `pronoun_privacy`, `metadata_privacy`. All are strings and accept the values of `public`, `private` and `null`. * The `privacy` field has now been deprecated and should not be used. It's still returned (mirroring the `visibility` field), and writing to it will write to *all privacy options*. * 2020-05-07 * The API (v1) is now formally(ish) defined with OpenAPI v3.0. [The definition file can be found here.](https://github.com/xSke/PluralKit/blob/master/PluralKit.API/openapi.yaml)