From 92a5be6bcb32a7c6e340eb73bb9420a940e49327 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Mon, 11 Mar 2024 18:18:33 -0500 Subject: [PATCH] Handle sync legacy labels (#2291) * Handle sync legacy labels * Remap values on read * Filter out double-written legacy label values * Better naming, fix types --- packages/api/src/bsky-agent.ts | 75 +++++++++++ packages/api/tests/moderation-prefs.test.ts | 130 +++++++++++++++++++- 2 files changed, 204 insertions(+), 1 deletion(-) diff --git a/packages/api/src/bsky-agent.ts b/packages/api/src/bsky-agent.ts index 6f0bf88e03b..c3a10359f42 100644 --- a/packages/api/src/bsky-agent.ts +++ b/packages/api/src/bsky-agent.ts @@ -432,6 +432,10 @@ export class BskyAgent extends AtpAgent { } } + prefs.moderationPrefs.labels = remapLegacyLabels( + prefs.moderationPrefs.labels, + ) + // automatically configure the client this.configureLabelersHeader(prefsArrayToLabelerDids(res.data.preferences)) @@ -510,6 +514,8 @@ export class BskyAgent extends AtpAgent { pref.label === key && pref.labelerDid === labelerDid, ) + let legacyLabelPref: AppBskyActorDefs.ContentLabelPref | undefined + if (labelPref) { labelPref.visibility = value } else { @@ -520,6 +526,40 @@ export class BskyAgent extends AtpAgent { visibility: value, } } + + if (AppBskyActorDefs.isContentLabelPref(labelPref)) { + // is global + if (!labelPref.labelerDid) { + const legacyLabelValue = { + 'graphic-media': 'gore', + porn: 'nsfw', + sexual: 'suggestive', + }[labelPref.label] + + // if it's a legacy label, double-write the legacy label + if (legacyLabelValue) { + legacyLabelPref = prefs.findLast( + (pref) => + AppBskyActorDefs.isContentLabelPref(pref) && + AppBskyActorDefs.validateContentLabelPref(pref).success && + pref.label === legacyLabelValue && + pref.labelerDid === undefined, + ) as AppBskyActorDefs.ContentLabelPref | undefined + + if (legacyLabelPref) { + legacyLabelPref.visibility = value + } else { + legacyLabelPref = { + $type: 'app.bsky.actor.defs#contentLabelPref', + label: legacyLabelValue, + labelerDid: undefined, + visibility: value, + } + } + } + } + } + return prefs .filter( (pref) => @@ -527,6 +567,17 @@ export class BskyAgent extends AtpAgent { !(pref.label === key && pref.labelerDid === labelerDid), ) .concat([labelPref]) + .filter((pref) => { + if (!legacyLabelPref) return true + return ( + !AppBskyActorDefs.isContentLabelPref(pref) || + !( + pref.label === legacyLabelPref.label && + pref.labelerDid === undefined + ) + ) + }) + .concat(legacyLabelPref ? [legacyLabelPref] : []) }) } @@ -861,6 +912,30 @@ function adjustLegacyContentLabelPref( return { ...pref, visibility } } +/** + * Re-maps legacy labels to new labels on READ. Does not save these changes to + * the user's preferences. + */ +function remapLegacyLabels( + labels: BskyPreferences['moderationPrefs']['labels'], +) { + const _labels = { ...labels } + const legacyToNewMap: Record = { + gore: 'graphic-media', + nsfw: 'porn', + suggestive: 'sexual', + } + + for (const labelName in _labels) { + const newLabelName = legacyToNewMap[labelName]! + if (newLabelName) { + _labels[newLabelName] = _labels[labelName] + } + } + + return _labels +} + /** * A helper to get the currently enabled labelers from the full preferences array */ diff --git a/packages/api/tests/moderation-prefs.test.ts b/packages/api/tests/moderation-prefs.test.ts index fcfe36400f7..0a9a768ce0c 100644 --- a/packages/api/tests/moderation-prefs.test.ts +++ b/packages/api/tests/moderation-prefs.test.ts @@ -175,7 +175,7 @@ describe('agent', () => { interests: { tags: [] }, moderationPrefs: { adultContentEnabled: false, - labels: { ...DEFAULT_LABEL_SETTINGS, porn: 'ignore' }, + labels: { ...DEFAULT_LABEL_SETTINGS, porn: 'ignore', nsfw: 'ignore' }, labelers: [ { did: 'did:plc:other', @@ -204,4 +204,132 @@ describe('agent', () => { }, }) }) + + it(`updates label pref`, async () => { + const agent = new BskyAgent({ service: network.pds.url }) + + await agent.createAccount({ + handle: 'user8.test', + email: 'user8@test.com', + password: 'password', + }) + + await agent.addLabeler('did:plc:other') + await agent.setContentLabelPref('porn', 'ignore') + await agent.setContentLabelPref('porn', 'ignore', 'did:plc:other') + await agent.setContentLabelPref('porn', 'hide') + await agent.setContentLabelPref('porn', 'hide', 'did:plc:other') + + const { moderationPrefs } = await agent.getPreferences() + const labeler = moderationPrefs.labelers.find( + (l) => l.did === 'did:plc:other', + ) + + expect(moderationPrefs.labels.porn).toEqual('hide') + expect(labeler?.labels?.porn).toEqual('hide') + }) + + it(`double-write for legacy: 'graphic-media' in sync with 'gore'`, async () => { + const agent = new BskyAgent({ service: network.pds.url }) + + await agent.createAccount({ + handle: 'user9.test', + email: 'user9@test.com', + password: 'password', + }) + + await agent.setContentLabelPref('graphic-media', 'hide') + const a = await agent.getPreferences() + + expect(a.moderationPrefs.labels.gore).toEqual('hide') + expect(a.moderationPrefs.labels['graphic-media']).toEqual('hide') + + await agent.setContentLabelPref('graphic-media', 'warn') + const b = await agent.getPreferences() + + expect(b.moderationPrefs.labels.gore).toEqual('warn') + expect(b.moderationPrefs.labels['graphic-media']).toEqual('warn') + }) + + it(`double-write for legacy: 'porn' in sync with 'nsfw'`, async () => { + const agent = new BskyAgent({ service: network.pds.url }) + + await agent.createAccount({ + handle: 'user10.test', + email: 'user10@test.com', + password: 'password', + }) + + await agent.setContentLabelPref('porn', 'hide') + const a = await agent.getPreferences() + + expect(a.moderationPrefs.labels.nsfw).toEqual('hide') + expect(a.moderationPrefs.labels.porn).toEqual('hide') + + await agent.setContentLabelPref('porn', 'warn') + const b = await agent.getPreferences() + + expect(b.moderationPrefs.labels.nsfw).toEqual('warn') + expect(b.moderationPrefs.labels.porn).toEqual('warn') + }) + + it(`double-write for legacy: 'sexual' in sync with 'suggestive'`, async () => { + const agent = new BskyAgent({ service: network.pds.url }) + + await agent.createAccount({ + handle: 'user11.test', + email: 'user11@test.com', + password: 'password', + }) + + await agent.setContentLabelPref('sexual', 'hide') + const a = await agent.getPreferences() + + expect(a.moderationPrefs.labels.sexual).toEqual('hide') + expect(a.moderationPrefs.labels.suggestive).toEqual('hide') + + await agent.setContentLabelPref('sexual', 'warn') + const b = await agent.getPreferences() + + expect(b.moderationPrefs.labels.sexual).toEqual('warn') + expect(b.moderationPrefs.labels.suggestive).toEqual('warn') + }) + + it(`double-write for legacy: filters out existing old label pref if double-written`, async () => { + const agent = new BskyAgent({ service: network.pds.url }) + + await agent.createAccount({ + handle: 'user12.test', + email: 'user12@test.com', + password: 'password', + }) + + await agent.setContentLabelPref('nsfw', 'hide') + await agent.setContentLabelPref('porn', 'hide') + const a = await agent.app.bsky.actor.getPreferences({}) + + const nsfwSettings = a.data.preferences.filter( + (pref) => pref.label === 'nsfw', + ) + expect(nsfwSettings.length).toEqual(1) + }) + + it(`remaps old values to new on read`, async () => { + const agent = new BskyAgent({ service: network.pds.url }) + + await agent.createAccount({ + handle: 'user13.test', + email: 'user13@test.com', + password: 'password', + }) + + await agent.setContentLabelPref('nsfw', 'hide') + await agent.setContentLabelPref('gore', 'hide') + await agent.setContentLabelPref('suggestive', 'hide') + const a = await agent.getPreferences() + + expect(a.moderationPrefs.labels.porn).toEqual('hide') + expect(a.moderationPrefs.labels['graphic-media']).toEqual('hide') + expect(a.moderationPrefs.labels['sexual']).toEqual('hide') + }) })