Add member avatar privacy
This commit is contained in:
parent
27c8100cac
commit
ae4e8f97d0
@ -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<string>("visibility").ParsePrivacy("member");
|
||||
if (o.ContainsKey("name_privacy")) member.NamePrivacy = o.Value<string>("name_privacy").ParsePrivacy("member");
|
||||
if (o.ContainsKey("description_privacy")) member.DescriptionPrivacy = o.Value<string>("description_privacy").ParsePrivacy("member");
|
||||
if (o.ContainsKey("avatar_privacy")) member.AvatarPrivacy = o.Value<string>("avatar_privacy").ParsePrivacy("member");
|
||||
if (o.ContainsKey("birthday_privacy")) member.BirthdayPrivacy = o.Value<string>("birthday_privacy").ParsePrivacy("member");
|
||||
if (o.ContainsKey("pronoun_privacy")) member.PronounPrivacy = o.Value<string>("pronoun_privacy").ParsePrivacy("member");
|
||||
// if (o.ContainsKey("color_privacy")) member.ColorPrivacy = o.Value<string>("color_privacy").ParsePrivacy("member");
|
||||
|
@ -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"
|
||||
|
@ -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)
|
||||
{
|
||||
|
@ -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 <member> privacy <subject> <level>`\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 <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();
|
||||
}
|
||||
|
||||
@ -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}).");
|
||||
}
|
||||
|
||||
|
@ -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",
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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; }
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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":
|
||||
|
@ -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 <member> privacy <subject> <level>
|
||||
|
||||
where `<member>` is the name or the id of a member in your system, `<subject>` is either `name`, `description`, `birthday`, `pronouns`, `metadata`, or `visiblity` corresponding to the options above, and `<level>` is either `public` or `private`. `<subject>` 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 `<member>` is the name or the id of a member in your system, `<subject>` is either `name`, `description`, `avatar`, `birthday`, `pronouns`, `metadata`, or `visiblity` corresponding to the options above, and `<level>` is either `public` or `private`. `<subject>` 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:
|
||||
|
||||
|
@ -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)
|
||||
|
Loading…
Reference in New Issue
Block a user