feat!: separate banner privacy from description privacy

This commit is contained in:
rladenson 2024-11-08 16:25:45 -07:00
parent 0c802ab0bd
commit a0f13b47af
15 changed files with 100 additions and 9 deletions

View file

@ -374,7 +374,7 @@ public class Groups
async Task ShowBannerImage()
{
ctx.CheckSystemPrivacy(target.System, target.DescriptionPrivacy);
ctx.CheckSystemPrivacy(target.System, target.BannerPrivacy);
if ((target.BannerImage?.Trim() ?? "").Length > 0)
{
@ -509,6 +509,7 @@ public class Groups
.Title($"Current privacy settings for {target.Name}")
.Field(new Embed.Field("Name", target.NamePrivacy.Explanation()))
.Field(new Embed.Field("Description", target.DescriptionPrivacy.Explanation()))
.Field(new Embed.Field("Description", target.BannerPrivacy.Explanation()))
.Field(new Embed.Field("Icon", target.IconPrivacy.Explanation()))
.Field(new Embed.Field("Member list", target.ListPrivacy.Explanation()))
.Field(new Embed.Field("Metadata (creation date)", target.MetadataPrivacy.Explanation()))
@ -539,6 +540,7 @@ public class Groups
{
GroupPrivacySubject.Name => "name privacy",
GroupPrivacySubject.Description => "description privacy",
GroupPrivacySubject.Banner => "banner privacy",
GroupPrivacySubject.Icon => "icon privacy",
GroupPrivacySubject.List => "member list",
GroupPrivacySubject.Metadata => "metadata",
@ -552,6 +554,8 @@ public class Groups
"This group's name is now hidden from other systems, and will be replaced by the group's display name.",
(GroupPrivacySubject.Description, PrivacyLevel.Private) =>
"This group's description is now hidden from other systems.",
(GroupPrivacySubject.Banner, PrivacyLevel.Private) =>
"This group's banner is now hidden from other systems.",
(GroupPrivacySubject.Icon, PrivacyLevel.Private) =>
"This group's icon is now hidden from other systems.",
(GroupPrivacySubject.Visibility, PrivacyLevel.Private) =>
@ -565,6 +569,8 @@ public class Groups
"This group's name is no longer hidden from other systems.",
(GroupPrivacySubject.Description, PrivacyLevel.Public) =>
"This group's description is no longer hidden from other systems.",
(GroupPrivacySubject.Banner, PrivacyLevel.Public) =>
"This group's banner is no longer hidden from other systems.",
(GroupPrivacySubject.Icon, PrivacyLevel.Public) =>
"This group's icon is no longer hidden from other systems.",
(GroupPrivacySubject.Visibility, PrivacyLevel.Public) =>

View file

@ -756,6 +756,7 @@ public class MemberEdit
.Field(new Embed.Field("Name (replaces name with display name if member has one)",
target.NamePrivacy.Explanation()))
.Field(new Embed.Field("Description", target.DescriptionPrivacy.Explanation()))
.Field(new Embed.Field("Banner", target.BannerPrivacy.Explanation()))
.Field(new Embed.Field("Avatar", target.AvatarPrivacy.Explanation()))
.Field(new Embed.Field("Birthday", target.BirthdayPrivacy.Explanation()))
.Field(new Embed.Field("Pronouns", target.PronounPrivacy.Explanation()))
@ -794,6 +795,7 @@ public class MemberEdit
{
MemberPrivacySubject.Name => "name privacy",
MemberPrivacySubject.Description => "description privacy",
MemberPrivacySubject.Banner => "banner privacy",
MemberPrivacySubject.Avatar => "avatar privacy",
MemberPrivacySubject.Pronouns => "pronoun privacy",
MemberPrivacySubject.Birthday => "birthday privacy",
@ -809,6 +811,8 @@ public class MemberEdit
"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.Banner, PrivacyLevel.Private) =>
"This member's banner is now hidden from other systems.",
(MemberPrivacySubject.Avatar, PrivacyLevel.Private) =>
"This member's avatar is now hidden from other systems.",
(MemberPrivacySubject.Birthday, PrivacyLevel.Private) =>
@ -826,6 +830,8 @@ public class MemberEdit
"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.Banner, PrivacyLevel.Public) =>
"This member's banner 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) =>

View file

@ -672,7 +672,7 @@ public class SystemEdit
public async Task BannerImage(Context ctx, PKSystem target)
{
ctx.CheckSystemPrivacy(target.Id, target.DescriptionPrivacy);
ctx.CheckSystemPrivacy(target.Id, target.BannerPrivacy);
var isOwnSystem = target.Id == ctx.System?.Id;
@ -835,6 +835,7 @@ public class SystemEdit
.Field(new Embed.Field("Name", target.NamePrivacy.Explanation()))
.Field(new Embed.Field("Avatar", target.AvatarPrivacy.Explanation()))
.Field(new Embed.Field("Description", target.DescriptionPrivacy.Explanation()))
.Field(new Embed.Field("Banner", target.BannerPrivacy.Explanation()))
.Field(new Embed.Field("Pronouns", target.PronounPrivacy.Explanation()))
.Field(new Embed.Field("Member list", target.MemberListPrivacy.Explanation()))
.Field(new Embed.Field("Group list", target.GroupListPrivacy.Explanation()))
@ -861,6 +862,7 @@ public class SystemEdit
SystemPrivacySubject.Name => "name",
SystemPrivacySubject.Avatar => "avatar",
SystemPrivacySubject.Description => "description",
SystemPrivacySubject.Banner => "banner",
SystemPrivacySubject.Pronouns => "pronouns",
SystemPrivacySubject.Front => "front",
SystemPrivacySubject.FrontHistory => "front history",

View file

@ -67,7 +67,7 @@ public class EmbedService
if (avatar != null)
eb.Thumbnail(new Embed.EmbedThumbnail(avatar));
if (system.DescriptionPrivacy.CanAccess(ctx))
if (system.BannerPrivacy.CanAccess(ctx))
eb.Image(new Embed.EmbedImage(system.BannerImage));
var latestSwitch = await _repo.GetLatestSwitch(system.Id);
@ -194,7 +194,7 @@ public class EmbedService
.Footer(new Embed.EmbedFooter(
$"System ID: {system.DisplayHid(ccfg)} | Member ID: {member.DisplayHid(ccfg)} {(member.MetadataPrivacy.CanAccess(ctx) ? $"| Created on {member.Created.FormatZoned(zone)}" : "")}"));
if (member.DescriptionPrivacy.CanAccess(ctx))
if (member.BannerPrivacy.CanAccess(ctx))
eb.Image(new Embed.EmbedImage(member.BannerImage));
var description = "";
@ -271,7 +271,7 @@ public class EmbedService
eb.Footer(new Embed.EmbedFooter($"System ID: {system.DisplayHid(ctx.Config)} | Group ID: {target.DisplayHid(ctx.Config)}{(target.MetadataPrivacy.CanAccess(pctx) ? $" | Created on {target.Created.FormatZoned(ctx.Zone)}" : "")}"));
if (target.DescriptionPrivacy.CanAccess(pctx))
if (target.BannerPrivacy.CanAccess(pctx))
eb.Image(new Embed.EmbedImage(target.BannerImage));
if (target.NamePrivacy.CanAccess(pctx) && target.DisplayName != null)

View file

@ -0,0 +1,16 @@
-- database version 45
-- adds banner privacy
alter table members add column banner_privacy int not null default 1;
alter table groups add column banner_privacy int not null default 1;
alter table systems add column banner_privacy int not null default 1;
alter table members add constraint members_banner_privacy_check check (banner_privacy = ANY (ARRAY[1,2]));
alter table groups add constraint groups_banner_privacy_check check (banner_privacy = ANY (ARRAY[1,2]));
alter table systems add constraint systems_banner_privacy_check check (banner_privacy = ANY (ARRAY[1,2]));
update members set banner_privacy = 2 where description_privacy = 2;
update groups set banner_privacy = 2 where description_privacy = 2;
update systems set banner_privacy = 2 where description_privacy = 2;
update info set schema_version = 45;

View file

@ -9,7 +9,7 @@ namespace PluralKit.Core;
internal class DatabaseMigrator
{
private const string RootPath = "PluralKit.Core.Database"; // "resource path" root for SQL files
private const int TargetSchemaVersion = 44;
private const int TargetSchemaVersion = 45;
private readonly ILogger _logger;
public DatabaseMigrator(ILogger logger)

View file

@ -51,6 +51,7 @@ public class PKGroup
public PrivacyLevel NamePrivacy { get; private set; }
public PrivacyLevel DescriptionPrivacy { get; private set; }
public PrivacyLevel BannerPrivacy { get; private set; }
public PrivacyLevel IconPrivacy { get; private set; }
public PrivacyLevel ListPrivacy { get; private set; }
public PrivacyLevel MetadataPrivacy { get; private set; }
@ -88,7 +89,7 @@ public static class PKGroupExt
o.Add("display_name", group.NamePrivacy.CanAccess(ctx) ? group.DisplayName : null);
o.Add("description", group.DescriptionPrivacy.Get(ctx, group.Description));
o.Add("icon", group.IconFor(ctx));
o.Add("banner", group.DescriptionPrivacy.Get(ctx, group.BannerImage));
o.Add("banner", group.BannerPrivacy.Get(ctx, group.BannerImage));
o.Add("color", group.Color);
o.Add("created", group.CreatedFor(ctx)?.FormatExport());
@ -102,6 +103,7 @@ public static class PKGroupExt
p.Add("name_privacy", group.NamePrivacy.ToJsonString());
p.Add("description_privacy", group.DescriptionPrivacy.ToJsonString());
p.Add("banner_privacy", group.BannerPrivacy.ToJsonString());
p.Add("icon_privacy", group.IconPrivacy.ToJsonString());
p.Add("list_privacy", group.ListPrivacy.ToJsonString());
p.Add("metadata_privacy", group.MetadataPrivacy.ToJsonString());

View file

@ -63,6 +63,7 @@ public class PKMember
public PrivacyLevel MemberVisibility { get; private set; }
public PrivacyLevel DescriptionPrivacy { get; private set; }
public PrivacyLevel BannerPrivacy { get; private set; }
public PrivacyLevel AvatarPrivacy { get; private set; }
public PrivacyLevel NamePrivacy { get; private set; } //ignore setting if no display name is set
public PrivacyLevel BirthdayPrivacy { get; private set; }
@ -140,7 +141,7 @@ public static class PKMemberExt
o.Add("pronouns", member.PronounsFor(ctx));
o.Add("avatar_url", member.AvatarFor(ctx).TryGetCleanCdnUrl());
o.Add("webhook_avatar_url", member.AvatarPrivacy.Get(ctx, member.WebhookAvatarUrl?.TryGetCleanCdnUrl()));
o.Add("banner", member.DescriptionPrivacy.Get(ctx, member.BannerImage).TryGetCleanCdnUrl());
o.Add("banner", member.BannerPrivacy.Get(ctx, member.BannerImage).TryGetCleanCdnUrl());
o.Add("description", member.DescriptionFor(ctx));
o.Add("created", member.CreatedFor(ctx)?.FormatExport());
o.Add("keep_proxy", member.KeepProxy);
@ -166,6 +167,7 @@ public static class PKMemberExt
p.Add("visibility", member.MemberVisibility.ToJsonString());
p.Add("name_privacy", member.NamePrivacy.ToJsonString());
p.Add("description_privacy", member.DescriptionPrivacy.ToJsonString());
p.Add("banner_privacy", member.BannerPrivacy.ToJsonString());
p.Add("birthday_privacy", member.BirthdayPrivacy.ToJsonString());
p.Add("pronoun_privacy", member.PronounPrivacy.ToJsonString());
p.Add("avatar_privacy", member.AvatarPrivacy.ToJsonString());

View file

@ -55,6 +55,7 @@ public class PKSystem
public PrivacyLevel NamePrivacy { get; }
public PrivacyLevel AvatarPrivacy { get; }
public PrivacyLevel DescriptionPrivacy { get; }
public PrivacyLevel BannerPrivacy { get; }
public PrivacyLevel MemberListPrivacy { get; }
public PrivacyLevel FrontPrivacy { get; }
public PrivacyLevel FrontHistoryPrivacy { get; }
@ -85,7 +86,7 @@ public static class PKSystemExt
o.Add("pronouns", system.PronounPrivacy.Get(ctx, system.Pronouns));
o.Add("avatar_url", system.AvatarFor(ctx));
o.Add("banner", system.DescriptionPrivacy.Get(ctx, system.BannerImage).TryGetCleanCdnUrl());
o.Add("banner", system.BannerPrivacy.Get(ctx, system.BannerImage).TryGetCleanCdnUrl());
o.Add("color", system.Color);
o.Add("created", system.Created.FormatExport());
@ -100,6 +101,7 @@ public static class PKSystemExt
p.Add("name_privacy", system.NamePrivacy.ToJsonString());
p.Add("avatar_privacy", system.AvatarPrivacy.ToJsonString());
p.Add("description_privacy", system.DescriptionPrivacy.ToJsonString());
p.Add("banner_privacy", system.BannerPrivacy.ToJsonString());
p.Add("pronoun_privacy", system.PronounPrivacy.ToJsonString());
p.Add("member_list_privacy", system.MemberListPrivacy.ToJsonString());
p.Add("group_list_privacy", system.GroupListPrivacy.ToJsonString());

View file

@ -17,6 +17,7 @@ public class GroupPatch: PatchObject
public Partial<PrivacyLevel> NamePrivacy { get; set; }
public Partial<PrivacyLevel> DescriptionPrivacy { get; set; }
public Partial<PrivacyLevel> BannerPrivacy { get; set; }
public Partial<PrivacyLevel> IconPrivacy { get; set; }
public Partial<PrivacyLevel> ListPrivacy { get; set; }
public Partial<PrivacyLevel> MetadataPrivacy { get; set; }
@ -32,6 +33,7 @@ public class GroupPatch: PatchObject
.With("color", Color)
.With("name_privacy", NamePrivacy)
.With("description_privacy", DescriptionPrivacy)
.With("banner_privacy", BannerPrivacy)
.With("icon_privacy", IconPrivacy)
.With("list_privacy", ListPrivacy)
.With("metadata_privacy", MetadataPrivacy)
@ -84,6 +86,9 @@ public class GroupPatch: PatchObject
if (privacy.ContainsKey("description_privacy"))
patch.DescriptionPrivacy = patch.ParsePrivacy(privacy, "description_privacy");
if (privacy.ContainsKey("banner_privacy"))
patch.BannerPrivacy = patch.ParsePrivacy(privacy, "banner_privacy");
if (privacy.ContainsKey("icon_privacy"))
patch.IconPrivacy = patch.ParsePrivacy(privacy, "icon_privacy");
@ -122,6 +127,7 @@ public class GroupPatch: PatchObject
if (
NamePrivacy.IsPresent
|| DescriptionPrivacy.IsPresent
|| BannerPrivacy.IsPresent
|| IconPrivacy.IsPresent
|| ListPrivacy.IsPresent
|| MetadataPrivacy.IsPresent
@ -136,6 +142,9 @@ public class GroupPatch: PatchObject
if (DescriptionPrivacy.IsPresent)
p.Add("description_privacy", DescriptionPrivacy.Value.ToJsonString());
if (BannerPrivacy.IsPresent)
p.Add("banner_privacy", BannerPrivacy.Value.ToJsonString());
if (IconPrivacy.IsPresent)
p.Add("icon_privacy", IconPrivacy.Value.ToJsonString());

View file

@ -27,6 +27,7 @@ public class MemberPatch: PatchObject
public Partial<PrivacyLevel> Visibility { get; set; }
public Partial<PrivacyLevel> NamePrivacy { get; set; }
public Partial<PrivacyLevel> DescriptionPrivacy { get; set; }
public Partial<PrivacyLevel> BannerPrivacy { get; set; }
public Partial<PrivacyLevel> PronounPrivacy { get; set; }
public Partial<PrivacyLevel> BirthdayPrivacy { get; set; }
public Partial<PrivacyLevel> AvatarPrivacy { get; set; }
@ -53,6 +54,7 @@ public class MemberPatch: PatchObject
.With("member_visibility", Visibility)
.With("name_privacy", NamePrivacy)
.With("description_privacy", DescriptionPrivacy)
.With("banner_privacy", BannerPrivacy)
.With("pronoun_privacy", PronounPrivacy)
.With("birthday_privacy", BirthdayPrivacy)
.With("avatar_privacy", AvatarPrivacy)
@ -140,6 +142,8 @@ public class MemberPatch: PatchObject
if (o.ContainsKey("name_privacy")) patch.NamePrivacy = patch.ParsePrivacy(o, "name_privacy");
if (o.ContainsKey("description_privacy"))
patch.DescriptionPrivacy = patch.ParsePrivacy(o, "description_privacy");
if (o.ContainsKey("banner_privacy"))
patch.BannerPrivacy = patch.ParsePrivacy(o, "banner_privacy");
if (o.ContainsKey("avatar_privacy"))
patch.AvatarPrivacy = patch.ParsePrivacy(o, "avatar_privacy");
if (o.ContainsKey("birthday_privacy"))
@ -172,6 +176,9 @@ public class MemberPatch: PatchObject
if (privacy.ContainsKey("description_privacy"))
patch.DescriptionPrivacy = patch.ParsePrivacy(privacy, "description_privacy");
if (privacy.ContainsKey("banner_privacy"))
patch.BannerPrivacy = patch.ParsePrivacy(privacy, "banner_privacy");
if (privacy.ContainsKey("avatar_privacy"))
patch.AvatarPrivacy = patch.ParsePrivacy(privacy, "avatar_privacy");
@ -233,6 +240,7 @@ public class MemberPatch: PatchObject
Visibility.IsPresent
|| NamePrivacy.IsPresent
|| DescriptionPrivacy.IsPresent
|| BannerPrivacy.IsPresent
|| PronounPrivacy.IsPresent
|| BirthdayPrivacy.IsPresent
|| AvatarPrivacy.IsPresent
@ -251,6 +259,9 @@ public class MemberPatch: PatchObject
if (DescriptionPrivacy.IsPresent)
p.Add("description_privacy", DescriptionPrivacy.Value.ToJsonString());
if (BannerPrivacy.IsPresent)
p.Add("banner_privacy", BannerPrivacy.Value.ToJsonString());
if (PronounPrivacy.IsPresent)
p.Add("pronoun_privacy", PronounPrivacy.Value.ToJsonString());

View file

@ -21,6 +21,7 @@ public class SystemPatch: PatchObject
public Partial<PrivacyLevel> NamePrivacy { get; set; }
public Partial<PrivacyLevel> AvatarPrivacy { get; set; }
public Partial<PrivacyLevel> DescriptionPrivacy { get; set; }
public Partial<PrivacyLevel> BannerPrivacy { get; set; }
public Partial<PrivacyLevel> MemberListPrivacy { get; set; }
public Partial<PrivacyLevel> GroupListPrivacy { get; set; }
public Partial<PrivacyLevel> FrontPrivacy { get; set; }
@ -42,6 +43,7 @@ public class SystemPatch: PatchObject
.With("name_privacy", NamePrivacy)
.With("avatar_privacy", AvatarPrivacy)
.With("description_privacy", DescriptionPrivacy)
.With("banner_privacy", BannerPrivacy)
.With("member_list_privacy", MemberListPrivacy)
.With("group_list_privacy", GroupListPrivacy)
.With("front_privacy", FrontPrivacy)
@ -86,6 +88,8 @@ public class SystemPatch: PatchObject
{
if (o.ContainsKey("description_privacy"))
patch.DescriptionPrivacy = patch.ParsePrivacy(o, "description_privacy");
if (o.ContainsKey("banner_privacy"))
patch.BannerPrivacy = patch.ParsePrivacy(o, "banner_privacy");
if (o.ContainsKey("member_list_privacy"))
patch.MemberListPrivacy = patch.ParsePrivacy(o, "member_list_privacy");
if (o.ContainsKey("front_privacy")) patch.FrontPrivacy = patch.ParsePrivacy(o, "front_privacy");
@ -106,6 +110,9 @@ public class SystemPatch: PatchObject
if (privacy.ContainsKey("description_privacy"))
patch.DescriptionPrivacy = patch.ParsePrivacy(privacy, "description_privacy");
if (privacy.ContainsKey("banner_privacy"))
patch.BannerPrivacy = patch.ParsePrivacy(privacy, "banner_privacy");
if (privacy.ContainsKey("pronoun_privacy"))
patch.PronounPrivacy = patch.ParsePrivacy(privacy, "pronoun_privacy");
@ -150,6 +157,7 @@ public class SystemPatch: PatchObject
NamePrivacy.IsPresent
|| AvatarPrivacy.IsPresent
|| DescriptionPrivacy.IsPresent
|| BannerPrivacy.IsPresent
|| PronounPrivacy.IsPresent
|| MemberListPrivacy.IsPresent
|| GroupListPrivacy.IsPresent
@ -168,6 +176,9 @@ public class SystemPatch: PatchObject
if (DescriptionPrivacy.IsPresent)
p.Add("description_privacy", DescriptionPrivacy.Value.ToJsonString());
if (BannerPrivacy.IsPresent)
p.Add("banner_privacy", BannerPrivacy.Value.ToJsonString());
if (PronounPrivacy.IsPresent)
p.Add("pronoun_privacy", PronounPrivacy.Value.ToJsonString());

View file

@ -4,6 +4,7 @@ public enum GroupPrivacySubject
{
Name,
Description,
Banner,
Icon,
List,
Metadata,
@ -19,6 +20,7 @@ public static class GroupPrivacyUtils
{
GroupPrivacySubject.Name => group.NamePrivacy = level,
GroupPrivacySubject.Description => group.DescriptionPrivacy = level,
GroupPrivacySubject.Banner => group.BannerPrivacy = level,
GroupPrivacySubject.Icon => group.IconPrivacy = level,
GroupPrivacySubject.List => group.ListPrivacy = level,
GroupPrivacySubject.Metadata => group.MetadataPrivacy = level,
@ -53,6 +55,12 @@ public static class GroupPrivacyUtils
case "intro":
subject = GroupPrivacySubject.Description;
break;
case "banner":
case "b":
case "splash":
case "cover":
subject = GroupPrivacySubject.Banner;
break;
case "avatar":
case "pfp":
case "pic":

View file

@ -5,6 +5,7 @@ public enum MemberPrivacySubject
Visibility,
Name,
Description,
Banner,
Avatar,
Birthday,
Pronouns,
@ -21,6 +22,7 @@ public static class MemberPrivacyUtils
{
MemberPrivacySubject.Name => member.NamePrivacy = level,
MemberPrivacySubject.Description => member.DescriptionPrivacy = level,
MemberPrivacySubject.Banner => member.BannerPrivacy = level,
MemberPrivacySubject.Avatar => member.AvatarPrivacy = level,
MemberPrivacySubject.Pronouns => member.PronounPrivacy = level,
MemberPrivacySubject.Birthday => member.BirthdayPrivacy = level,
@ -57,6 +59,12 @@ public static class MemberPrivacyUtils
case "intro":
subject = MemberPrivacySubject.Description;
break;
case "banner":
case "b":
case "splash":
case "cover":
subject = MemberPrivacySubject.Banner;
break;
case "avatar":
case "pfp":
case "pic":

View file

@ -5,6 +5,7 @@ public enum SystemPrivacySubject
Name,
Avatar,
Description,
Banner,
Pronouns,
MemberList,
GroupList,
@ -22,6 +23,7 @@ public static class SystemPrivacyUtils
SystemPrivacySubject.Name => system.NamePrivacy = level,
SystemPrivacySubject.Avatar => system.AvatarPrivacy = level,
SystemPrivacySubject.Description => system.DescriptionPrivacy = level,
SystemPrivacySubject.Banner => system.BannerPrivacy = level,
SystemPrivacySubject.Pronouns => system.PronounPrivacy = level,
SystemPrivacySubject.Front => system.FrontPrivacy = level,
SystemPrivacySubject.FrontHistory => system.FrontHistoryPrivacy = level,
@ -63,6 +65,12 @@ public static class SystemPrivacyUtils
case "intro":
subject = SystemPrivacySubject.Description;
break;
case "banner":
case "b":
case "splash":
case "cover":
subject = SystemPrivacySubject.Banner;
break;
case "pronouns":
case "pronoun":
case "prns":