From 429ea11a6d677ce33d5b64996810bf154068adaf Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Tue, 5 Mar 2024 16:27:18 -0600 Subject: [PATCH 01/22] Sketch proposal for additional muted words attributes --- lexicons/app/bsky/actor/defs.json | 12 ++- packages/api/package.json | 3 +- packages/api/src/bsky-agent.ts | 125 +++++++++++++++++++++--------- pnpm-lock.yaml | 15 ++++ 4 files changed, 116 insertions(+), 39 deletions(-) diff --git a/lexicons/app/bsky/actor/defs.json b/lexicons/app/bsky/actor/defs.json index b3cfe2e1967..af04ce6879c 100644 --- a/lexicons/app/bsky/actor/defs.json +++ b/lexicons/app/bsky/actor/defs.json @@ -245,8 +245,9 @@ "mutedWord": { "type": "object", "description": "A word that the account owner has muted.", - "required": ["value", "targets"], + "required": ["id", "value", "targets"], "properties": { + "id": { "type": "string" }, "value": { "type": "string", "description": "The muted word itself.", @@ -260,6 +261,15 @@ "type": "ref", "ref": "app.bsky.actor.defs#mutedWordTarget" } + }, + "actors": { + "type": "array", + "description": "The accounts for which this muted word applies.", + "items": { "type": "string", "format": "did" } + }, + "ttl": { + "type": "string", + "description": "The time-to-live for the muted word." } } }, diff --git a/packages/api/package.json b/packages/api/package.json index 8a9e7b0a760..d3bde6fafce 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -36,11 +36,12 @@ "multiformats": "^9.9.0", "tlds": "^1.234.0", "typed-emitter": "^2.1.0", + "uuid": "^9.0.1", "zod": "^3.21.4" }, "devDependencies": { - "@atproto/lex-cli": "workspace:^", "@atproto/dev-env": "workspace:^", + "@atproto/lex-cli": "workspace:^", "common-tags": "^1.8.2", "get-port": "^6.1.2" } diff --git a/packages/api/src/bsky-agent.ts b/packages/api/src/bsky-agent.ts index e51347bca7e..841dd119eae 100644 --- a/packages/api/src/bsky-agent.ts +++ b/packages/api/src/bsky-agent.ts @@ -1,4 +1,5 @@ import { AtUri, ensureValidDid } from '@atproto/syntax' +import { v4 as uuid } from 'uuid' import { AtpAgent } from './agent' import { AppBskyFeedPost, @@ -767,7 +768,11 @@ export class BskyAgent extends AtpAgent { }) } - async upsertMutedWords(newMutedWords: AppBskyActorDefs.MutedWord[]) { + async addMutedWord(mutedWord: AppBskyActorDefs.MutedWord) { + const sanitizedValue = sanitizeMutedWordValue(mutedWord.value) + + if (!sanitizedValue) return + await updatePreferences(this, (prefs: AppBskyActorDefs.Preferences) => { let mutedWordsPref = prefs.findLast( (pref) => @@ -775,40 +780,27 @@ export class BskyAgent extends AtpAgent { AppBskyActorDefs.validateMutedWordsPref(pref).success, ) - if (mutedWordsPref && AppBskyActorDefs.isMutedWordsPref(mutedWordsPref)) { - for (const updatedWord of newMutedWords) { - let foundMatch = false - const sanitizedUpdatedValue = sanitizeMutedWordValue( - updatedWord.value, - ) - - // was trimmed down to an empty string e.g. single `#` - if (!sanitizedUpdatedValue) continue + const newMutedWord: AppBskyActorDefs.MutedWord = { + id: uuid(), + value: sanitizedValue, + targets: mutedWord.targets || [], + actors: mutedWord.actors || [], + ttl: mutedWord.ttl || undefined, + } - for (const existingItem of mutedWordsPref.items) { - if (existingItem.value === sanitizedUpdatedValue) { - existingItem.targets = Array.from( - new Set([...existingItem.targets, ...updatedWord.targets]), - ) - foundMatch = true - break - } - } + if (mutedWordsPref && AppBskyActorDefs.isMutedWordsPref(mutedWordsPref)) { + mutedWordsPref.items.push(newMutedWord) - if (!foundMatch) { - mutedWordsPref.items.push({ - ...updatedWord, - value: sanitizedUpdatedValue, - }) - } - } + /** + * Migrate any old muted words that don't have an id + */ + mutedWordsPref.items = migrateLegacyMutedWordsItems( + mutedWordsPref.items, + ) } else { // if the pref doesn't exist, create it mutedWordsPref = { - items: newMutedWords.map((w) => ({ - ...w, - value: sanitizeMutedWordValue(w.value), - })), + items: [newMutedWord], } } @@ -820,6 +812,15 @@ export class BskyAgent extends AtpAgent { }) } + /** + * @deprecated use `addMutedWord` instead + */ + async upsertMutedWords(newMutedWords: AppBskyActorDefs.MutedWord[]) { + for (const word of newMutedWords) { + await this.addMutedWord(word) + } + } + async updateMutedWord(mutedWord: AppBskyActorDefs.MutedWord) { await updatePreferences(this, (prefs: AppBskyActorDefs.Preferences) => { const mutedWordsPref = prefs.findLast( @@ -829,12 +830,38 @@ export class BskyAgent extends AtpAgent { ) if (mutedWordsPref && AppBskyActorDefs.isMutedWordsPref(mutedWordsPref)) { - for (const existingItem of mutedWordsPref.items) { - if (existingItem.value === mutedWord.value) { - existingItem.targets = mutedWord.targets - break + mutedWordsPref.items.map((existingItem) => { + // id is undefined in legacy implementation + const existingId = existingItem.id as string | undefined + // prefer matching based on id + const matchById = existingId && existingId === mutedWord.id + // handle legacy case where id is not set + const legacyMatchByValue = + !existingId && existingItem.value === mutedWord.value + + if (matchById || legacyMatchByValue) { + const updated = { + ...existingItem, + ...mutedWord, + } + return { + id: existingId || uuid(), + value: updated.value, + targets: updated.targets || [], + actors: updated.actors || [], + ttl: updated.ttl || undefined, + } + } else { + return existingItem } - } + }) + + /** + * Migrate any old muted words that don't have an id + */ + mutedWordsPref.items = migrateLegacyMutedWordsItems( + mutedWordsPref.items, + ) } return prefs @@ -855,12 +882,27 @@ export class BskyAgent extends AtpAgent { if (mutedWordsPref && AppBskyActorDefs.isMutedWordsPref(mutedWordsPref)) { for (let i = 0; i < mutedWordsPref.items.length; i++) { - const existing = mutedWordsPref.items[i] - if (existing.value === mutedWord.value) { + const existingItem = mutedWordsPref.items[i] + // id is undefined in legacy implementation + const existingId = existingItem.id as string | undefined + // prefer matching based on id + const matchById = existingId && existingId === mutedWord.id + // handle legacy case where id is not set + const legacyMatchByValue = + !existingId && existingItem.value === mutedWord.value + + if (matchById || legacyMatchByValue) { mutedWordsPref.items.splice(i, 1) break } } + + /** + * Migrate any old muted words that don't have an id + */ + mutedWordsPref.items = migrateLegacyMutedWordsItems( + mutedWordsPref.items, + ) } return prefs @@ -1043,3 +1085,12 @@ function isBskyPrefs(v: any): v is BskyPreferences { function isModPrefs(v: any): v is ModerationPrefs { return v && typeof v === 'object' && 'labelers' in v } + +function migrateLegacyMutedWordsItems(items: AppBskyActorDefs.MutedWord[]) { + return items.map((item) => ({ + ...item, + id: item.id || uuid(), + actors: item.actors || [], + ttl: item.ttl || undefined, + })) +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f54d3f47947..a20adf0b7be 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -113,6 +113,9 @@ importers: typed-emitter: specifier: ^2.1.0 version: 2.1.0 + uuid: + specifier: ^9.0.1 + version: 9.0.1 zod: specifier: ^3.21.4 version: 3.21.4 @@ -123,6 +126,9 @@ importers: '@atproto/lex-cli': specifier: workspace:^ version: link:../lex-cli + '@types/uuid': + specifier: ^9.0.8 + version: 9.0.8 common-tags: specifier: ^1.8.2 version: 1.8.2 @@ -6034,6 +6040,10 @@ packages: resolution: {integrity: sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==} dev: true + /@types/uuid@9.0.8: + resolution: {integrity: sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==} + dev: true + /@types/ws@8.5.4: resolution: {integrity: sha512-zdQDHKUgcX/zBc4GrwsE/7dVdAD8JR4EuiAXiiUhhfyIJXXb2+PrGshFyeXWQPMmmZ2XxgaqclgpIC7eTXc1mg==} dependencies: @@ -11830,6 +11840,11 @@ packages: hasBin: true dev: false + /uuid@9.0.1: + resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==} + hasBin: true + dev: false + /v8-compile-cache-lib@3.0.1: resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} dev: true From d1226ca2eda84b012bb3abecf095991b9d73d146 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Tue, 5 Mar 2024 16:50:59 -0600 Subject: [PATCH 02/22] Rename ttl -> expiresAt --- lexicons/app/bsky/actor/defs.json | 4 ++-- packages/api/src/bsky-agent.ts | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/lexicons/app/bsky/actor/defs.json b/lexicons/app/bsky/actor/defs.json index af04ce6879c..4dc876db4a3 100644 --- a/lexicons/app/bsky/actor/defs.json +++ b/lexicons/app/bsky/actor/defs.json @@ -267,9 +267,9 @@ "description": "The accounts for which this muted word applies.", "items": { "type": "string", "format": "did" } }, - "ttl": { + "expiresAt": { "type": "string", - "description": "The time-to-live for the muted word." + "description": "The date and time at which the muted word will expire and no longer be applied." } } }, diff --git a/packages/api/src/bsky-agent.ts b/packages/api/src/bsky-agent.ts index 841dd119eae..b9f93cfabd6 100644 --- a/packages/api/src/bsky-agent.ts +++ b/packages/api/src/bsky-agent.ts @@ -785,7 +785,7 @@ export class BskyAgent extends AtpAgent { value: sanitizedValue, targets: mutedWord.targets || [], actors: mutedWord.actors || [], - ttl: mutedWord.ttl || undefined, + expiresAt: mutedWord.expiresAt || undefined, } if (mutedWordsPref && AppBskyActorDefs.isMutedWordsPref(mutedWordsPref)) { @@ -849,7 +849,7 @@ export class BskyAgent extends AtpAgent { value: updated.value, targets: updated.targets || [], actors: updated.actors || [], - ttl: updated.ttl || undefined, + expiresAt: updated.expiresAt || undefined, } } else { return existingItem @@ -1091,6 +1091,6 @@ function migrateLegacyMutedWordsItems(items: AppBskyActorDefs.MutedWord[]) { ...item, id: item.id || uuid(), actors: item.actors || [], - ttl: item.ttl || undefined, + expiresAt: item.expiresAt || undefined, })) } From c5ed4339a054e658592ac7cbb0712c7c11908715 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Tue, 5 Mar 2024 17:09:56 -0600 Subject: [PATCH 03/22] Feedback --- lexicons/app/bsky/actor/defs.json | 2 +- packages/api/package.json | 1 - packages/api/src/bsky-agent.ts | 8 ++++---- pnpm-lock.yaml | 15 --------------- 4 files changed, 5 insertions(+), 21 deletions(-) diff --git a/lexicons/app/bsky/actor/defs.json b/lexicons/app/bsky/actor/defs.json index 4dc876db4a3..862690cb03c 100644 --- a/lexicons/app/bsky/actor/defs.json +++ b/lexicons/app/bsky/actor/defs.json @@ -245,7 +245,7 @@ "mutedWord": { "type": "object", "description": "A word that the account owner has muted.", - "required": ["id", "value", "targets"], + "required": ["value", "targets"], "properties": { "id": { "type": "string" }, "value": { diff --git a/packages/api/package.json b/packages/api/package.json index d3bde6fafce..59785e34f5c 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -36,7 +36,6 @@ "multiformats": "^9.9.0", "tlds": "^1.234.0", "typed-emitter": "^2.1.0", - "uuid": "^9.0.1", "zod": "^3.21.4" }, "devDependencies": { diff --git a/packages/api/src/bsky-agent.ts b/packages/api/src/bsky-agent.ts index b9f93cfabd6..e4c6af6768a 100644 --- a/packages/api/src/bsky-agent.ts +++ b/packages/api/src/bsky-agent.ts @@ -1,5 +1,5 @@ import { AtUri, ensureValidDid } from '@atproto/syntax' -import { v4 as uuid } from 'uuid' +import {TID} from '@atproto/common-web' import { AtpAgent } from './agent' import { AppBskyFeedPost, @@ -781,7 +781,7 @@ export class BskyAgent extends AtpAgent { ) const newMutedWord: AppBskyActorDefs.MutedWord = { - id: uuid(), + id: TID.nextStr(), value: sanitizedValue, targets: mutedWord.targets || [], actors: mutedWord.actors || [], @@ -845,7 +845,7 @@ export class BskyAgent extends AtpAgent { ...mutedWord, } return { - id: existingId || uuid(), + id: existingId || TID.nextStr(), value: updated.value, targets: updated.targets || [], actors: updated.actors || [], @@ -1089,7 +1089,7 @@ function isModPrefs(v: any): v is ModerationPrefs { function migrateLegacyMutedWordsItems(items: AppBskyActorDefs.MutedWord[]) { return items.map((item) => ({ ...item, - id: item.id || uuid(), + id: item.id || TID.nextStr(), actors: item.actors || [], expiresAt: item.expiresAt || undefined, })) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a20adf0b7be..f54d3f47947 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -113,9 +113,6 @@ importers: typed-emitter: specifier: ^2.1.0 version: 2.1.0 - uuid: - specifier: ^9.0.1 - version: 9.0.1 zod: specifier: ^3.21.4 version: 3.21.4 @@ -126,9 +123,6 @@ importers: '@atproto/lex-cli': specifier: workspace:^ version: link:../lex-cli - '@types/uuid': - specifier: ^9.0.8 - version: 9.0.8 common-tags: specifier: ^1.8.2 version: 1.8.2 @@ -6040,10 +6034,6 @@ packages: resolution: {integrity: sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==} dev: true - /@types/uuid@9.0.8: - resolution: {integrity: sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==} - dev: true - /@types/ws@8.5.4: resolution: {integrity: sha512-zdQDHKUgcX/zBc4GrwsE/7dVdAD8JR4EuiAXiiUhhfyIJXXb2+PrGshFyeXWQPMmmZ2XxgaqclgpIC7eTXc1mg==} dependencies: @@ -11840,11 +11830,6 @@ packages: hasBin: true dev: false - /uuid@9.0.1: - resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==} - hasBin: true - dev: false - /v8-compile-cache-lib@3.0.1: resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} dev: true From 452bacece96f30500f5b4c2ea7caa82b30c8fedb Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Tue, 5 Mar 2024 17:13:18 -0600 Subject: [PATCH 04/22] Codegen --- packages/api/src/client/lexicons.ts | 16 ++++++++++++++++ .../api/src/client/types/app/bsky/actor/defs.ts | 5 +++++ packages/bsky/src/lexicon/lexicons.ts | 16 ++++++++++++++++ .../src/lexicon/types/app/bsky/actor/defs.ts | 5 +++++ packages/ozone/src/lexicon/lexicons.ts | 16 ++++++++++++++++ .../src/lexicon/types/app/bsky/actor/defs.ts | 5 +++++ packages/pds/src/lexicon/lexicons.ts | 16 ++++++++++++++++ .../pds/src/lexicon/types/app/bsky/actor/defs.ts | 5 +++++ 8 files changed, 84 insertions(+) diff --git a/packages/api/src/client/lexicons.ts b/packages/api/src/client/lexicons.ts index c1f266c1ecf..8e3e5f0de85 100644 --- a/packages/api/src/client/lexicons.ts +++ b/packages/api/src/client/lexicons.ts @@ -3924,6 +3924,9 @@ export const schemaDict = { description: 'A word that the account owner has muted.', required: ['value', 'targets'], properties: { + id: { + type: 'string', + }, value: { type: 'string', description: 'The muted word itself.', @@ -3938,6 +3941,19 @@ export const schemaDict = { ref: 'lex:app.bsky.actor.defs#mutedWordTarget', }, }, + actors: { + type: 'array', + description: 'The accounts for which this muted word applies.', + items: { + type: 'string', + format: 'did', + }, + }, + expiresAt: { + type: 'string', + description: + 'The date and time at which the muted word will expire and no longer be applied.', + }, }, }, mutedWordsPref: { diff --git a/packages/api/src/client/types/app/bsky/actor/defs.ts b/packages/api/src/client/types/app/bsky/actor/defs.ts index 4243002b862..d878abf6d79 100644 --- a/packages/api/src/client/types/app/bsky/actor/defs.ts +++ b/packages/api/src/client/types/app/bsky/actor/defs.ts @@ -283,10 +283,15 @@ export type MutedWordTarget = 'content' | 'tag' | (string & {}) /** A word that the account owner has muted. */ export interface MutedWord { + id?: string /** The muted word itself. */ value: string /** The intended targets of the muted word. */ targets: MutedWordTarget[] + /** The accounts for which this muted word applies. */ + actors?: string[] + /** The date and time at which the muted word will expire and no longer be applied. */ + expiresAt?: string [k: string]: unknown } diff --git a/packages/bsky/src/lexicon/lexicons.ts b/packages/bsky/src/lexicon/lexicons.ts index 7407c3a961c..7a92a450bed 100644 --- a/packages/bsky/src/lexicon/lexicons.ts +++ b/packages/bsky/src/lexicon/lexicons.ts @@ -3924,6 +3924,9 @@ export const schemaDict = { description: 'A word that the account owner has muted.', required: ['value', 'targets'], properties: { + id: { + type: 'string', + }, value: { type: 'string', description: 'The muted word itself.', @@ -3938,6 +3941,19 @@ export const schemaDict = { ref: 'lex:app.bsky.actor.defs#mutedWordTarget', }, }, + actors: { + type: 'array', + description: 'The accounts for which this muted word applies.', + items: { + type: 'string', + format: 'did', + }, + }, + expiresAt: { + type: 'string', + description: + 'The date and time at which the muted word will expire and no longer be applied.', + }, }, }, mutedWordsPref: { diff --git a/packages/bsky/src/lexicon/types/app/bsky/actor/defs.ts b/packages/bsky/src/lexicon/types/app/bsky/actor/defs.ts index 7bd87c6e953..70e3715565e 100644 --- a/packages/bsky/src/lexicon/types/app/bsky/actor/defs.ts +++ b/packages/bsky/src/lexicon/types/app/bsky/actor/defs.ts @@ -283,10 +283,15 @@ export type MutedWordTarget = 'content' | 'tag' | (string & {}) /** A word that the account owner has muted. */ export interface MutedWord { + id?: string /** The muted word itself. */ value: string /** The intended targets of the muted word. */ targets: MutedWordTarget[] + /** The accounts for which this muted word applies. */ + actors?: string[] + /** The date and time at which the muted word will expire and no longer be applied. */ + expiresAt?: string [k: string]: unknown } diff --git a/packages/ozone/src/lexicon/lexicons.ts b/packages/ozone/src/lexicon/lexicons.ts index c1f266c1ecf..8e3e5f0de85 100644 --- a/packages/ozone/src/lexicon/lexicons.ts +++ b/packages/ozone/src/lexicon/lexicons.ts @@ -3924,6 +3924,9 @@ export const schemaDict = { description: 'A word that the account owner has muted.', required: ['value', 'targets'], properties: { + id: { + type: 'string', + }, value: { type: 'string', description: 'The muted word itself.', @@ -3938,6 +3941,19 @@ export const schemaDict = { ref: 'lex:app.bsky.actor.defs#mutedWordTarget', }, }, + actors: { + type: 'array', + description: 'The accounts for which this muted word applies.', + items: { + type: 'string', + format: 'did', + }, + }, + expiresAt: { + type: 'string', + description: + 'The date and time at which the muted word will expire and no longer be applied.', + }, }, }, mutedWordsPref: { diff --git a/packages/ozone/src/lexicon/types/app/bsky/actor/defs.ts b/packages/ozone/src/lexicon/types/app/bsky/actor/defs.ts index 7bd87c6e953..70e3715565e 100644 --- a/packages/ozone/src/lexicon/types/app/bsky/actor/defs.ts +++ b/packages/ozone/src/lexicon/types/app/bsky/actor/defs.ts @@ -283,10 +283,15 @@ export type MutedWordTarget = 'content' | 'tag' | (string & {}) /** A word that the account owner has muted. */ export interface MutedWord { + id?: string /** The muted word itself. */ value: string /** The intended targets of the muted word. */ targets: MutedWordTarget[] + /** The accounts for which this muted word applies. */ + actors?: string[] + /** The date and time at which the muted word will expire and no longer be applied. */ + expiresAt?: string [k: string]: unknown } diff --git a/packages/pds/src/lexicon/lexicons.ts b/packages/pds/src/lexicon/lexicons.ts index c1f266c1ecf..8e3e5f0de85 100644 --- a/packages/pds/src/lexicon/lexicons.ts +++ b/packages/pds/src/lexicon/lexicons.ts @@ -3924,6 +3924,9 @@ export const schemaDict = { description: 'A word that the account owner has muted.', required: ['value', 'targets'], properties: { + id: { + type: 'string', + }, value: { type: 'string', description: 'The muted word itself.', @@ -3938,6 +3941,19 @@ export const schemaDict = { ref: 'lex:app.bsky.actor.defs#mutedWordTarget', }, }, + actors: { + type: 'array', + description: 'The accounts for which this muted word applies.', + items: { + type: 'string', + format: 'did', + }, + }, + expiresAt: { + type: 'string', + description: + 'The date and time at which the muted word will expire and no longer be applied.', + }, }, }, mutedWordsPref: { diff --git a/packages/pds/src/lexicon/types/app/bsky/actor/defs.ts b/packages/pds/src/lexicon/types/app/bsky/actor/defs.ts index 7bd87c6e953..70e3715565e 100644 --- a/packages/pds/src/lexicon/types/app/bsky/actor/defs.ts +++ b/packages/pds/src/lexicon/types/app/bsky/actor/defs.ts @@ -283,10 +283,15 @@ export type MutedWordTarget = 'content' | 'tag' | (string & {}) /** A word that the account owner has muted. */ export interface MutedWord { + id?: string /** The muted word itself. */ value: string /** The intended targets of the muted word. */ targets: MutedWordTarget[] + /** The accounts for which this muted word applies. */ + actors?: string[] + /** The date and time at which the muted word will expire and no longer be applied. */ + expiresAt?: string [k: string]: unknown } From 8759cb6d46ab8c55dfe1332e49ffb0a2e5a5a9ae Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Wed, 6 Mar 2024 15:42:17 -0600 Subject: [PATCH 05/22] Refactor muted words methods to integrate new attributes --- packages/api/src/bsky-agent.ts | 103 ++++++++++++++++++++++----------- 1 file changed, 70 insertions(+), 33 deletions(-) diff --git a/packages/api/src/bsky-agent.ts b/packages/api/src/bsky-agent.ts index e4c6af6768a..0636d68c2cf 100644 --- a/packages/api/src/bsky-agent.ts +++ b/packages/api/src/bsky-agent.ts @@ -8,6 +8,7 @@ import { AppBskyLabelerDefs, ComAtprotoRepoPutRecord, } from './client' +import { MutedWord } from './client/types/app/bsky/actor/defs' import { BskyPreferences, BskyFeedViewPreference, @@ -768,7 +769,13 @@ export class BskyAgent extends AtpAgent { }) } - async addMutedWord(mutedWord: AppBskyActorDefs.MutedWord) { + /** + * Add a muted word to user preferences. If called in succession, this + * method must be called sequentially, not in parallel. + */ + async addMutedWord( + mutedWord: Pick, + ) { const sanitizedValue = sanitizeMutedWordValue(mutedWord.value) if (!sanitizedValue) return @@ -813,14 +820,25 @@ export class BskyAgent extends AtpAgent { } /** - * @deprecated use `addMutedWord` instead + * Convenience method to sequentially add muted words to user preferences */ - async upsertMutedWords(newMutedWords: AppBskyActorDefs.MutedWord[]) { + async addMutedWords(newMutedWords: AppBskyActorDefs.MutedWord[]) { for (const word of newMutedWords) { await this.addMutedWord(word) } } + /** + * @deprecated use `addMutedWords` or `addMutedWord` instead + */ + async upsertMutedWords(mutedWords: AppBskyActorDefs.MutedWord[]) { + await this.addMutedWords(mutedWords) + } + + /** + * Update a muted word in user preferences. If called in succession, this + * method must be called sequentially, not in parallel. + */ async updateMutedWord(mutedWord: AppBskyActorDefs.MutedWord) { await updatePreferences(this, (prefs: AppBskyActorDefs.Preferences) => { const mutedWordsPref = prefs.findLast( @@ -830,23 +848,17 @@ export class BskyAgent extends AtpAgent { ) if (mutedWordsPref && AppBskyActorDefs.isMutedWordsPref(mutedWordsPref)) { - mutedWordsPref.items.map((existingItem) => { - // id is undefined in legacy implementation - const existingId = existingItem.id as string | undefined - // prefer matching based on id - const matchById = existingId && existingId === mutedWord.id - // handle legacy case where id is not set - const legacyMatchByValue = - !existingId && existingItem.value === mutedWord.value - - if (matchById || legacyMatchByValue) { + mutedWordsPref.items = mutedWordsPref.items.map((existingItem) => { + const match = matchMutedWord(existingItem, mutedWord) + + if (match) { const updated = { ...existingItem, ...mutedWord, } return { - id: existingId || TID.nextStr(), - value: updated.value, + id: existingItem.id || TID.nextStr(), + value: sanitizeMutedWordValue(updated.value), targets: updated.targets || [], actors: updated.actors || [], expiresAt: updated.expiresAt || undefined, @@ -862,16 +874,22 @@ export class BskyAgent extends AtpAgent { mutedWordsPref.items = migrateLegacyMutedWordsItems( mutedWordsPref.items, ) + + return prefs + .filter((p) => !AppBskyActorDefs.isMutedWordsPref(p)) + .concat([ + { ...mutedWordsPref, $type: 'app.bsky.actor.defs#mutedWordsPref' }, + ]) } return prefs - .filter((p) => !AppBskyActorDefs.isMutedWordsPref(p)) - .concat([ - { ...mutedWordsPref, $type: 'app.bsky.actor.defs#mutedWordsPref' }, - ]) }) } + /** + * Remove a muted word from user preferences. If called in succession, this + * method must be called sequentially, not in parallel. + */ async removeMutedWord(mutedWord: AppBskyActorDefs.MutedWord) { await updatePreferences(this, (prefs: AppBskyActorDefs.Preferences) => { const mutedWordsPref = prefs.findLast( @@ -882,16 +900,9 @@ export class BskyAgent extends AtpAgent { if (mutedWordsPref && AppBskyActorDefs.isMutedWordsPref(mutedWordsPref)) { for (let i = 0; i < mutedWordsPref.items.length; i++) { - const existingItem = mutedWordsPref.items[i] - // id is undefined in legacy implementation - const existingId = existingItem.id as string | undefined - // prefer matching based on id - const matchById = existingId && existingId === mutedWord.id - // handle legacy case where id is not set - const legacyMatchByValue = - !existingId && existingItem.value === mutedWord.value - - if (matchById || legacyMatchByValue) { + const match = matchMutedWord(mutedWordsPref.items[i], mutedWord) + + if (match) { mutedWordsPref.items.splice(i, 1) break } @@ -903,16 +914,28 @@ export class BskyAgent extends AtpAgent { mutedWordsPref.items = migrateLegacyMutedWordsItems( mutedWordsPref.items, ) + + return prefs + .filter((p) => !AppBskyActorDefs.isMutedWordsPref(p)) + .concat([ + { ...mutedWordsPref, $type: 'app.bsky.actor.defs#mutedWordsPref' }, + ]) } return prefs - .filter((p) => !AppBskyActorDefs.isMutedWordsPref(p)) - .concat([ - { ...mutedWordsPref, $type: 'app.bsky.actor.defs#mutedWordsPref' }, - ]) }) } + /** + * Convenience method to sequentially remove muted words from user + * preferences + */ + async removeMutedWords(mutedWords: AppBskyActorDefs.MutedWord[]) { + for (const word of mutedWords) { + await this.removeMutedWord(word) + } + } + async hidePost(postUri: string) { await updateHiddenPost(this, postUri, 'hide') } @@ -1094,3 +1117,17 @@ function migrateLegacyMutedWordsItems(items: AppBskyActorDefs.MutedWord[]) { expiresAt: item.expiresAt || undefined, })) } + +function matchMutedWord( + existingWord: AppBskyActorDefs.MutedWord, + newWord: AppBskyActorDefs.MutedWord, +): boolean { + // id is undefined in legacy implementation + const existingId = existingWord.id + // prefer matching based on id + const matchById = existingId && existingId === newWord.id + // handle legacy case where id is not set + const legacyMatchByValue = !existingId && existingWord.value === newWord.value + + return matchById || legacyMatchByValue +} From 83b708772731c8df6f6eab4bfdc0734829102711 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Wed, 6 Mar 2024 16:40:24 -0600 Subject: [PATCH 06/22] Add changeset --- .changeset/rotten-moose-switch.md | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 .changeset/rotten-moose-switch.md diff --git a/.changeset/rotten-moose-switch.md b/.changeset/rotten-moose-switch.md new file mode 100644 index 00000000000..5736de3b224 --- /dev/null +++ b/.changeset/rotten-moose-switch.md @@ -0,0 +1,8 @@ +--- +'@atproto/ozone': patch +'@atproto/bsky': patch +'@atproto/api': patch +'@atproto/pds': patch +--- + +Updates muted words lexicons to include new attributes `id`, `actors`, and `expiresAt`. Adds and updates methods in API SDK for better management of muted words. From 03d8a70750965b498b2d2aa60f01d3637c6c8dfe Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Wed, 6 Mar 2024 16:49:18 -0600 Subject: [PATCH 07/22] Use datetime format --- lexicons/app/bsky/actor/defs.json | 1 + packages/api/src/client/lexicons.ts | 1 + packages/bsky/src/lexicon/lexicons.ts | 1 + packages/ozone/src/lexicon/lexicons.ts | 1 + packages/pds/src/lexicon/lexicons.ts | 1 + 5 files changed, 5 insertions(+) diff --git a/lexicons/app/bsky/actor/defs.json b/lexicons/app/bsky/actor/defs.json index 862690cb03c..e9557facbec 100644 --- a/lexicons/app/bsky/actor/defs.json +++ b/lexicons/app/bsky/actor/defs.json @@ -269,6 +269,7 @@ }, "expiresAt": { "type": "string", + "format": "datetime", "description": "The date and time at which the muted word will expire and no longer be applied." } } diff --git a/packages/api/src/client/lexicons.ts b/packages/api/src/client/lexicons.ts index 8e3e5f0de85..136126d9167 100644 --- a/packages/api/src/client/lexicons.ts +++ b/packages/api/src/client/lexicons.ts @@ -3951,6 +3951,7 @@ export const schemaDict = { }, expiresAt: { type: 'string', + format: 'datetime', description: 'The date and time at which the muted word will expire and no longer be applied.', }, diff --git a/packages/bsky/src/lexicon/lexicons.ts b/packages/bsky/src/lexicon/lexicons.ts index 7a92a450bed..c2336f2ed4e 100644 --- a/packages/bsky/src/lexicon/lexicons.ts +++ b/packages/bsky/src/lexicon/lexicons.ts @@ -3951,6 +3951,7 @@ export const schemaDict = { }, expiresAt: { type: 'string', + format: 'datetime', description: 'The date and time at which the muted word will expire and no longer be applied.', }, diff --git a/packages/ozone/src/lexicon/lexicons.ts b/packages/ozone/src/lexicon/lexicons.ts index 8e3e5f0de85..136126d9167 100644 --- a/packages/ozone/src/lexicon/lexicons.ts +++ b/packages/ozone/src/lexicon/lexicons.ts @@ -3951,6 +3951,7 @@ export const schemaDict = { }, expiresAt: { type: 'string', + format: 'datetime', description: 'The date and time at which the muted word will expire and no longer be applied.', }, diff --git a/packages/pds/src/lexicon/lexicons.ts b/packages/pds/src/lexicon/lexicons.ts index 8e3e5f0de85..136126d9167 100644 --- a/packages/pds/src/lexicon/lexicons.ts +++ b/packages/pds/src/lexicon/lexicons.ts @@ -3951,6 +3951,7 @@ export const schemaDict = { }, expiresAt: { type: 'string', + format: 'datetime', description: 'The date and time at which the muted word will expire and no longer be applied.', }, From 07a1553107bd0c92b1fa2746534d0f55e0765d4a Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Wed, 6 Mar 2024 16:53:33 -0600 Subject: [PATCH 08/22] Simplify migration --- packages/api/src/bsky-agent.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/api/src/bsky-agent.ts b/packages/api/src/bsky-agent.ts index 0636d68c2cf..8492d735407 100644 --- a/packages/api/src/bsky-agent.ts +++ b/packages/api/src/bsky-agent.ts @@ -1113,8 +1113,6 @@ function migrateLegacyMutedWordsItems(items: AppBskyActorDefs.MutedWord[]) { return items.map((item) => ({ ...item, id: item.id || TID.nextStr(), - actors: item.actors || [], - expiresAt: item.expiresAt || undefined, })) } From ca359cafb4f1985ba9b4e59ce7d69b1a8dd6713b Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Wed, 13 Mar 2024 11:11:53 -0500 Subject: [PATCH 09/22] Fix tests --- packages/api/tests/bsky-agent.test.ts | 535 ++++++++++++++++++-------- 1 file changed, 377 insertions(+), 158 deletions(-) diff --git a/packages/api/tests/bsky-agent.test.ts b/packages/api/tests/bsky-agent.test.ts index bae98dfe65d..d07b167a5f3 100644 --- a/packages/api/tests/bsky-agent.test.ts +++ b/packages/api/tests/bsky-agent.test.ts @@ -4,6 +4,7 @@ import { ComAtprotoRepoPutRecord, AppBskyActorProfile, DEFAULT_LABEL_SETTINGS, + AppBskyActorDefs, } from '..' describe('agent', () => { @@ -1395,14 +1396,6 @@ describe('agent', () => { describe('muted words', () => { let agent: BskyAgent - const mutedWords = [ - { value: 'both', targets: ['content', 'tag'] }, - { value: 'content', targets: ['content'] }, - { value: 'tag', targets: ['tag'] }, - { value: 'tag_then_both', targets: ['tag'] }, - { value: 'tag_then_content', targets: ['tag'] }, - { value: 'tag_then_none', targets: ['tag'] }, - ] beforeAll(async () => { agent = new BskyAgent({ service: network.pds.url }) @@ -1413,207 +1406,433 @@ describe('agent', () => { }) }) - it('upsertMutedWords', async () => { - await agent.upsertMutedWords(mutedWords) - await agent.upsertMutedWords(mutedWords) // double - await expect(agent.getPreferences()).resolves.toHaveProperty( - 'moderationPrefs.mutedWords', - mutedWords, - ) + afterEach(async () => { + const { moderationPrefs } = await agent.getPreferences() + await agent.removeMutedWords(moderationPrefs.mutedWords) }) - it('upsertMutedWords with #', async () => { - await agent.upsertMutedWords([ - { value: 'hashtag', targets: ['content'] }, - ]) - // is sanitized to `hashtag` - await agent.upsertMutedWords([{ value: '#hashtag', targets: ['tag'] }]) + describe('addMutedWord', () => { + it('inserts', async () => { + const expiresAt = new Date(Date.now() + 6e3).toISOString() + await agent.addMutedWord({ + value: 'word', + targets: ['content'], + actors: [], + expiresAt, + }) + + const { moderationPrefs } = await agent.getPreferences() + const word = moderationPrefs.mutedWords.find((m) => m.value === 'word') + + expect(word!.id).toBeTruthy() + expect(word!.targets).toEqual(['content']) + expect(word!.actors).toEqual([]) + expect(word!.expiresAt).toEqual(expiresAt) + }) - const { mutedWords } = (await agent.getPreferences()).moderationPrefs + it('single-hash #, no insert', async () => { + await agent.addMutedWord({ value: '#', targets: [] }) + const { moderationPrefs } = await agent.getPreferences() - expect(mutedWords.find((m) => m.value === '#hashtag')).toBeFalsy() - // merged with existing - expect(mutedWords.find((m) => m.value === 'hashtag')).toStrictEqual({ - value: 'hashtag', - targets: ['content', 'tag'], + // sanitized to empty string, not inserted + expect(moderationPrefs.mutedWords.length).toEqual(0) }) - // only one added - expect(mutedWords.filter((m) => m.value === 'hashtag').length).toBe(1) - }) - it('updateMutedWord', async () => { - await agent.updateMutedWord({ - value: 'tag_then_content', - targets: ['content'], + it('multi-hash ##, inserts #', async () => { + await agent.addMutedWord({ value: '##', targets: [] }) + const { moderationPrefs } = await agent.getPreferences() + expect(moderationPrefs.mutedWords.find((m) => m.value === '#')).toBeTruthy() }) - await agent.updateMutedWord({ - value: 'tag_then_both', - targets: ['content', 'tag'], + + it('multi-hash ##hashtag, inserts #hashtag', async () => { + await agent.addMutedWord({ value: '##hashtag', targets: [] }) + const { moderationPrefs } = await agent.getPreferences() + expect(moderationPrefs.mutedWords.find((w) => w.value === '#hashtag')).toBeTruthy() }) - await agent.updateMutedWord({ value: 'tag_then_none', targets: [] }) - await agent.updateMutedWord({ value: 'no_exist', targets: ['tag'] }) - const { mutedWords } = (await agent.getPreferences()).moderationPrefs - - expect( - mutedWords.find((m) => m.value === 'tag_then_content'), - ).toHaveProperty('targets', ['content']) - expect( - mutedWords.find((m) => m.value === 'tag_then_both'), - ).toHaveProperty('targets', ['content', 'tag']) - expect( - mutedWords.find((m) => m.value === 'tag_then_none'), - ).toHaveProperty('targets', []) - expect(mutedWords.find((m) => m.value === 'no_exist')).toBeFalsy() - }) - it('updateMutedWord with #, does not update', async () => { - await agent.upsertMutedWords([ - { - value: '#just_a_tag', - targets: ['tag'], - }, - ]) - await agent.updateMutedWord({ - value: '#just_a_tag', - targets: ['tag', 'content'], + it('hash emoji #️⃣, inserts #️⃣', async () => { + await agent.addMutedWord({ value: '#️⃣', targets: [] }) + const { moderationPrefs } = await agent.getPreferences() + expect(moderationPrefs.mutedWords.find((m) => m.value === '#️⃣')).toBeTruthy() + }) + + it('hash emoji w/leading hash ##️⃣, inserts #️⃣', async () => { + await agent.addMutedWord({ value: '##️⃣', targets: [] }) + const { moderationPrefs } = await agent.getPreferences() + expect(moderationPrefs.mutedWords.find((m) => m.value === '#️⃣')).toBeTruthy() }) - const { mutedWords } = (await agent.getPreferences()).moderationPrefs - expect(mutedWords.find((m) => m.value === 'just_a_tag')).toStrictEqual({ - value: 'just_a_tag', - targets: ['tag'], + + it('hash emoji with double leading hash ###️⃣, inserts ##️⃣', async () => { + await agent.addMutedWord({ value: '###️⃣', targets: [] }) + const { moderationPrefs } = await agent.getPreferences() + expect(moderationPrefs.mutedWords.find((m) => m.value === '##️⃣')).toBeTruthy() }) - }) - it('removeMutedWord', async () => { - await agent.removeMutedWord({ value: 'tag_then_content', targets: [] }) - await agent.removeMutedWord({ value: 'tag_then_both', targets: [] }) - await agent.removeMutedWord({ value: 'tag_then_none', targets: [] }) - const { mutedWords } = (await agent.getPreferences()).moderationPrefs - - expect( - mutedWords.find((m) => m.value === 'tag_then_content'), - ).toBeFalsy() - expect(mutedWords.find((m) => m.value === 'tag_then_both')).toBeFalsy() - expect(mutedWords.find((m) => m.value === 'tag_then_none')).toBeFalsy() + describe(`invalid characters`, () => { + it('#, no insert', async () => { + await agent.addMutedWord({ value: '#​', targets: [] }) + const { moderationPrefs } = await agent.getPreferences() + expect(moderationPrefs.mutedWords.length).toEqual(0) + }) + + it('#ab, inserts ab', async () => { + await agent.addMutedWord({ value: '#​ab', targets: [] }) + const { moderationPrefs } = await agent.getPreferences() + expect(moderationPrefs.mutedWords.length).toEqual(1) + }) + + it('phrase with newline, inserts phrase without newline', async () => { + await agent.addMutedWord({ + value: 'test value\n with newline', + targets: [], + }) + const { moderationPrefs } = await agent.getPreferences() + expect( + moderationPrefs.mutedWords.find((m) => m.value === 'test value with newline'), + ).toBeTruthy() + }) + + it('phrase with newlines, inserts phrase without newlines', async () => { + await agent.addMutedWord({ + value: 'test value\n\r with newline', + targets: [], + }) + const { moderationPrefs } = await agent.getPreferences() + expect( + moderationPrefs.mutedWords.find((m) => m.value === 'test value with newline'), + ).toBeTruthy() + }) + + it('empty space, no insert', async () => { + await agent.addMutedWord({ value: ' ', targets: [] }) + const { moderationPrefs } = await agent.getPreferences() + expect(moderationPrefs.mutedWords.length).toEqual(0) + }) + + it(`' trim ', inserts 'trim'`, async () => { + await agent.addMutedWord({ value: ' trim ', targets: [] }) + const { moderationPrefs } = await agent.getPreferences() + expect(moderationPrefs.mutedWords.find((m) => m.value === 'trim')).toBeTruthy() + }) + }) }) - it('removeMutedWord with #, no match, no removal', async () => { - await agent.removeMutedWord({ value: '#hashtag', targets: [] }) - const { mutedWords } = (await agent.getPreferences()).moderationPrefs + describe('addMutedWords', () => { + it('inserts happen sequentially, no clobbering', async () => { + await agent.addMutedWords([ + { value: 'a', targets: ['content'] }, + { value: 'b', targets: ['content'] }, + { value: 'c', targets: ['content'] }, + ]) + + const { moderationPrefs } = await agent.getPreferences() - // was inserted with #hashtag, but we don't sanitize on remove - expect(mutedWords.find((m) => m.value === 'hashtag')).toBeTruthy() + expect(moderationPrefs.mutedWords.length).toEqual(3) + }) }) - it('single-hash #', async () => { - const prev = (await agent.getPreferences()).moderationPrefs - const length = prev.mutedWords.length - await agent.upsertMutedWords([{ value: '#', targets: [] }]) - const end = (await agent.getPreferences()).moderationPrefs + describe('upsertMutedWords (deprecated)', () => { + it('no longer upserts, calls addMutedWords', async () => { + await agent.upsertMutedWords([ + { value: 'both', targets: ['content'] }, + ]) + await agent.upsertMutedWords([{ value: 'both', targets: ['tag'] }]) + + const { moderationPrefs } = await agent.getPreferences() - // sanitized to empty string, not inserted - expect(end.mutedWords.length).toEqual(length) + expect(moderationPrefs.mutedWords.length).toEqual(2) + }) }) - it('multi-hash ##', async () => { - await agent.upsertMutedWords([{ value: '##', targets: [] }]) - const { mutedWords } = (await agent.getPreferences()).moderationPrefs + describe('updateMutedWord', () => { + it(`word doesn't exist, no update or insert`, async () => { + await agent.updateMutedWord({ + value: 'word', + targets: ['tag', 'content'], + }) + const { moderationPrefs } = await agent.getPreferences() + expect(moderationPrefs.mutedWords.length).toEqual(0) + }) - expect(mutedWords.find((m) => m.value === '#')).toBeTruthy() - }) + it('updates and sanitizes new value', async () => { + await agent.addMutedWord({ + value: 'value', + targets: ['content'], + }) - it('multi-hash ##hashtag', async () => { - await agent.upsertMutedWords([{ value: '##hashtag', targets: [] }]) - const a = (await agent.getPreferences()).moderationPrefs + const a = await agent.getPreferences() + const word = a.moderationPrefs.mutedWords.find((m) => m.value === 'value') - expect(a.mutedWords.find((w) => w.value === '#hashtag')).toBeTruthy() + await agent.updateMutedWord({ + ...word!, + value: '#new value', + }) - await agent.removeMutedWord({ value: '#hashtag', targets: [] }) - const b = (await agent.getPreferences()).moderationPrefs + const b = await agent.getPreferences() + const updatedWord = b.moderationPrefs.mutedWords.find((m) => m.id === word!.id) - expect(b.mutedWords.find((w) => w.value === '#hashtag')).toBeFalsy() - }) + expect(updatedWord!.value).toEqual('new value') + expect(updatedWord).toHaveProperty('targets', ['content']) + }) - it('hash emoji #️⃣', async () => { - await agent.upsertMutedWords([{ value: '#️⃣', targets: [] }]) - const { mutedWords } = (await agent.getPreferences()).moderationPrefs + it('updates targets', async () => { + await agent.addMutedWord({ + value: 'word', + targets: ['tag'], + }) - expect(mutedWords.find((m) => m.value === '#️⃣')).toBeTruthy() + const a = await agent.getPreferences() + const word = a.moderationPrefs.mutedWords.find((m) => m.value === 'word') - await agent.removeMutedWord({ value: '#️⃣', targets: [] }) - const end = (await agent.getPreferences()).moderationPrefs + await agent.updateMutedWord({ + ...word!, + targets: ['content'], + }) - expect(end.mutedWords.find((m) => m.value === '#️⃣')).toBeFalsy() - }) + const b = await agent.getPreferences() + + expect(b.moderationPrefs.mutedWords.find((m) => m.id === word!.id)).toHaveProperty( + 'targets', + ['content'], + ) + }) + + it('updates actors', async () => { + await agent.addMutedWord({ + value: 'value', + targets: ['content'], + actors: ['did:plc:fake'], + }) - it('hash emoji ##️⃣', async () => { - await agent.upsertMutedWords([{ value: '##️⃣', targets: [] }]) - const { mutedWords } = (await agent.getPreferences()).moderationPrefs + const a = await agent.getPreferences() + const word = a.moderationPrefs.mutedWords.find((m) => m.value === 'value') - expect(mutedWords.find((m) => m.value === '#️⃣')).toBeTruthy() + await agent.updateMutedWord({ + ...word!, + actors: ['did:plc:fake2'], + }) - await agent.removeMutedWord({ value: '#️⃣', targets: [] }) - const end = (await agent.getPreferences()).moderationPrefs + const b = await agent.getPreferences() - expect(end.mutedWords.find((m) => m.value === '#️⃣')).toBeFalsy() + expect(b.moderationPrefs.mutedWords.find((m) => m.id === word!.id)).toHaveProperty( + 'actors', + ['did:plc:fake2'], + ) + }) + + it('updates expiresAt', async () => { + const expiresAt = new Date(Date.now() + 6e3).toISOString() + const expiresAt2 = new Date(Date.now() + 10e3).toISOString() + await agent.addMutedWord({ + value: 'value', + targets: ['content'], + expiresAt, + }) + + const a = await agent.getPreferences() + const word = a.moderationPrefs.mutedWords.find((m) => m.value === 'value') + + await agent.updateMutedWord({ + ...word!, + expiresAt: expiresAt2, + }) + + const b = await agent.getPreferences() + + expect(b.moderationPrefs.mutedWords.find((m) => m.id === word!.id)).toHaveProperty( + 'expiresAt', + expiresAt2, + ) + }) }) - it('hash emoji ###️⃣', async () => { - await agent.upsertMutedWords([{ value: '###️⃣', targets: [] }]) - const { mutedWords } = (await agent.getPreferences()).moderationPrefs + describe('removeMutedWord', () => { + it('removes word', async () => { + await agent.addMutedWord({ value: 'word', targets: ['tag'] }) + const a = await agent.getPreferences() + const word = a.moderationPrefs.mutedWords.find((m) => m.value === 'word') - expect(mutedWords.find((m) => m.value === '##️⃣')).toBeTruthy() + await agent.removeMutedWord(word!) - await agent.removeMutedWord({ value: '##️⃣', targets: [] }) - const end = (await agent.getPreferences()).moderationPrefs + const b = await agent.getPreferences() - expect(end.mutedWords.find((m) => m.value === '##️⃣')).toBeFalsy() - }) + expect(b.moderationPrefs.mutedWords.find((m) => m.id === word!.id)).toBeFalsy() + }) - describe(`invalid characters`, () => { - it('zero width space', async () => { - const prev = (await agent.getPreferences()).moderationPrefs - const length = prev.mutedWords.length - await agent.upsertMutedWords([{ value: '#​', targets: [] }]) - const { mutedWords } = (await agent.getPreferences()).moderationPrefs + it(`word doesn't exist, no action`, async () => { + await agent.addMutedWord({ value: 'word', targets: ['tag'] }) + const a = await agent.getPreferences() + const word = a.moderationPrefs.mutedWords.find((m) => m.value === 'word') - expect(mutedWords.length).toEqual(length) + await agent.removeMutedWord({ value: 'another', targets: [] }) + + const b = await agent.getPreferences() + + expect(b.moderationPrefs.mutedWords.find((m) => m.id === word!.id)).toBeTruthy() }) + }) - it('newline', async () => { - await agent.upsertMutedWords([ - { value: 'test value\n with newline', targets: [] }, + describe('removeMutedWords', () => { + it(`removes sequentially, no clobbering`, async () => { + await agent.addMutedWords([ + { value: 'a', targets: ['content'] }, + { value: 'b', targets: ['content'] }, + { value: 'c', targets: ['content'] }, ]) - const { mutedWords } = (await agent.getPreferences()).moderationPrefs - expect( - mutedWords.find((m) => m.value === 'test value with newline'), - ).toBeTruthy() + const a = await agent.getPreferences() + await agent.removeMutedWords(a.moderationPrefs.mutedWords) + const b = await agent.getPreferences() + + expect(b.moderationPrefs.mutedWords.length).toEqual(0) }) + }) + }) - it('newline(s)', async () => { - await agent.upsertMutedWords([ - { value: 'test value\n\r with newline', targets: [] }, - ]) - const { mutedWords } = (await agent.getPreferences()).moderationPrefs + describe('legacy muted words', () => { + let agent: BskyAgent + + async function updatePreferences( + agent: BskyAgent, + cb: ( + prefs: AppBskyActorDefs.Preferences, + ) => AppBskyActorDefs.Preferences | false, + ) { + const res = await agent.app.bsky.actor.getPreferences({}) + const newPrefs = cb(res.data.preferences) + if (newPrefs === false) { + return + } + await agent.app.bsky.actor.putPreferences({ + preferences: newPrefs, + }) + } + + async function addLegacyMutedWord(mutedWord: AppBskyActorDefs.MutedWord) { + await updatePreferences(agent, (prefs) => { + let mutedWordsPref = prefs.findLast( + (pref) => + AppBskyActorDefs.isMutedWordsPref(pref) && + AppBskyActorDefs.validateMutedWordsPref(pref).success, + ) + + const newMutedWord: AppBskyActorDefs.MutedWord = { + value: mutedWord.value, + targets: mutedWord.targets, + } + + if ( + mutedWordsPref && + AppBskyActorDefs.isMutedWordsPref(mutedWordsPref) + ) { + mutedWordsPref.items.push(newMutedWord) + } else { + // if the pref doesn't exist, create it + mutedWordsPref = { + items: [newMutedWord], + } + } + + return prefs + .filter((p) => !AppBskyActorDefs.isMutedWordsPref(p)) + .concat([ + { + ...mutedWordsPref, + $type: 'app.bsky.actor.defs#mutedWordsPref', + }, + ]) + }) + } + + beforeAll(async () => { + agent = new BskyAgent({ service: network.pds.url }) + await agent.createAccount({ + handle: 'user8.test', + email: 'user8@test.com', + password: 'password', + }) + }) + + afterEach(async () => { + const { moderationPrefs } = await agent.getPreferences() + await agent.removeMutedWords(moderationPrefs.mutedWords) + }) + + describe(`upsertMutedWords (and addMutedWord)`, () => { + it(`adds new word, migrates old words`, async () => { + await addLegacyMutedWord({ + value: 'word', + targets: ['content'], + }) + + { + const { moderationPrefs } = await agent.getPreferences() + const word = moderationPrefs.mutedWords.find((w) => w.value === 'word') + expect(word).toBeTruthy() + expect(word!.id).toBeFalsy() + } + + await agent.upsertMutedWords([{ value: 'word2', targets: ['tag'] }]) + + { + const { moderationPrefs } = await agent.getPreferences() + const word = moderationPrefs.mutedWords.find((w) => w.value === 'word') + const word2 = moderationPrefs.mutedWords.find((w) => w.value === 'word2') - expect( - mutedWords.find((m) => m.value === 'test value with newline'), - ).toBeTruthy() + expect(word!.id).toBeTruthy() + expect(word2!.id).toBeTruthy() + } }) + }) + + describe(`updateMutedWord`, () => { + it(`updates legacy word, migrates old words`, async () => { + await addLegacyMutedWord({ + value: 'word', + targets: ['content'], + }) + await addLegacyMutedWord({ + value: 'word2', + targets: ['tag'], + }) - it('empty space', async () => { - await agent.upsertMutedWords([{ value: ' ', targets: [] }]) - const { mutedWords } = (await agent.getPreferences()).moderationPrefs + await agent.updateMutedWord({ value: 'word', targets: ['tag'] }) - expect(mutedWords.find((m) => m.value === ' ')).toBeFalsy() + { + const { moderationPrefs } = await agent.getPreferences() + const word = moderationPrefs.mutedWords.find((w) => w.value === 'word') + const word2 = moderationPrefs.mutedWords.find((w) => w.value === 'word2') + + expect(moderationPrefs.mutedWords.length).toEqual(2) + expect(word!.id).toBeTruthy() + expect(word!.targets).toEqual(['tag']) + expect(word2!.id).toBeTruthy() + } }) + }) - it('leading/trailing space', async () => { - await agent.upsertMutedWords([{ value: ' trim ', targets: [] }]) - const { mutedWords } = (await agent.getPreferences()).moderationPrefs + describe(`removeMutedWord`, () => { + it(`removes legacy word, migrates old words`, async () => { + await addLegacyMutedWord({ + value: 'word', + targets: ['content'], + }) + await addLegacyMutedWord({ + value: 'word2', + targets: ['tag'], + }) + + await agent.removeMutedWord({ value: 'word', targets: ['tag'] }) - expect(mutedWords.find((m) => m.value === 'trim')).toBeTruthy() + { + const { moderationPrefs } = await agent.getPreferences() + const word = moderationPrefs.mutedWords.find((w) => w.value === 'word') + const word2 = moderationPrefs.mutedWords.find((w) => w.value === 'word2') + + expect(moderationPrefs.mutedWords.length).toEqual(1) + expect(word).toBeFalsy() + expect(word2!.id).toBeTruthy() + } }) }) }) @@ -1625,8 +1844,8 @@ describe('agent', () => { beforeAll(async () => { agent = new BskyAgent({ service: network.pds.url }) await agent.createAccount({ - handle: 'user8.test', - email: 'user8@test.com', + handle: 'user9.test', + email: 'user9@test.com', password: 'password', }) }) From a7eb5bb9b23b7fd86f01ec795ee1b2c4ca7e03f7 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Wed, 13 Mar 2024 11:23:48 -0500 Subject: [PATCH 10/22] Format --- packages/api/src/bsky-agent.ts | 2 +- packages/api/tests/bsky-agent.test.ts | 121 ++++++++++++++++++-------- 2 files changed, 85 insertions(+), 38 deletions(-) diff --git a/packages/api/src/bsky-agent.ts b/packages/api/src/bsky-agent.ts index 8492d735407..7c04fadd9e9 100644 --- a/packages/api/src/bsky-agent.ts +++ b/packages/api/src/bsky-agent.ts @@ -1,5 +1,5 @@ import { AtUri, ensureValidDid } from '@atproto/syntax' -import {TID} from '@atproto/common-web' +import { TID } from '@atproto/common-web' import { AtpAgent } from './agent' import { AppBskyFeedPost, diff --git a/packages/api/tests/bsky-agent.test.ts b/packages/api/tests/bsky-agent.test.ts index d07b167a5f3..b45de766051 100644 --- a/packages/api/tests/bsky-agent.test.ts +++ b/packages/api/tests/bsky-agent.test.ts @@ -1422,7 +1422,9 @@ describe('agent', () => { }) const { moderationPrefs } = await agent.getPreferences() - const word = moderationPrefs.mutedWords.find((m) => m.value === 'word') + const word = moderationPrefs.mutedWords.find( + (m) => m.value === 'word', + ) expect(word!.id).toBeTruthy() expect(word!.targets).toEqual(['content']) @@ -1441,31 +1443,41 @@ describe('agent', () => { it('multi-hash ##, inserts #', async () => { await agent.addMutedWord({ value: '##', targets: [] }) const { moderationPrefs } = await agent.getPreferences() - expect(moderationPrefs.mutedWords.find((m) => m.value === '#')).toBeTruthy() + expect( + moderationPrefs.mutedWords.find((m) => m.value === '#'), + ).toBeTruthy() }) it('multi-hash ##hashtag, inserts #hashtag', async () => { await agent.addMutedWord({ value: '##hashtag', targets: [] }) const { moderationPrefs } = await agent.getPreferences() - expect(moderationPrefs.mutedWords.find((w) => w.value === '#hashtag')).toBeTruthy() + expect( + moderationPrefs.mutedWords.find((w) => w.value === '#hashtag'), + ).toBeTruthy() }) it('hash emoji #️⃣, inserts #️⃣', async () => { await agent.addMutedWord({ value: '#️⃣', targets: [] }) const { moderationPrefs } = await agent.getPreferences() - expect(moderationPrefs.mutedWords.find((m) => m.value === '#️⃣')).toBeTruthy() + expect( + moderationPrefs.mutedWords.find((m) => m.value === '#️⃣'), + ).toBeTruthy() }) it('hash emoji w/leading hash ##️⃣, inserts #️⃣', async () => { await agent.addMutedWord({ value: '##️⃣', targets: [] }) const { moderationPrefs } = await agent.getPreferences() - expect(moderationPrefs.mutedWords.find((m) => m.value === '#️⃣')).toBeTruthy() + expect( + moderationPrefs.mutedWords.find((m) => m.value === '#️⃣'), + ).toBeTruthy() }) it('hash emoji with double leading hash ###️⃣, inserts ##️⃣', async () => { await agent.addMutedWord({ value: '###️⃣', targets: [] }) const { moderationPrefs } = await agent.getPreferences() - expect(moderationPrefs.mutedWords.find((m) => m.value === '##️⃣')).toBeTruthy() + expect( + moderationPrefs.mutedWords.find((m) => m.value === '##️⃣'), + ).toBeTruthy() }) describe(`invalid characters`, () => { @@ -1488,7 +1500,9 @@ describe('agent', () => { }) const { moderationPrefs } = await agent.getPreferences() expect( - moderationPrefs.mutedWords.find((m) => m.value === 'test value with newline'), + moderationPrefs.mutedWords.find( + (m) => m.value === 'test value with newline', + ), ).toBeTruthy() }) @@ -1499,7 +1513,9 @@ describe('agent', () => { }) const { moderationPrefs } = await agent.getPreferences() expect( - moderationPrefs.mutedWords.find((m) => m.value === 'test value with newline'), + moderationPrefs.mutedWords.find( + (m) => m.value === 'test value with newline', + ), ).toBeTruthy() }) @@ -1512,7 +1528,9 @@ describe('agent', () => { it(`' trim ', inserts 'trim'`, async () => { await agent.addMutedWord({ value: ' trim ', targets: [] }) const { moderationPrefs } = await agent.getPreferences() - expect(moderationPrefs.mutedWords.find((m) => m.value === 'trim')).toBeTruthy() + expect( + moderationPrefs.mutedWords.find((m) => m.value === 'trim'), + ).toBeTruthy() }) }) }) @@ -1561,7 +1579,9 @@ describe('agent', () => { }) const a = await agent.getPreferences() - const word = a.moderationPrefs.mutedWords.find((m) => m.value === 'value') + const word = a.moderationPrefs.mutedWords.find( + (m) => m.value === 'value', + ) await agent.updateMutedWord({ ...word!, @@ -1569,7 +1589,9 @@ describe('agent', () => { }) const b = await agent.getPreferences() - const updatedWord = b.moderationPrefs.mutedWords.find((m) => m.id === word!.id) + const updatedWord = b.moderationPrefs.mutedWords.find( + (m) => m.id === word!.id, + ) expect(updatedWord!.value).toEqual('new value') expect(updatedWord).toHaveProperty('targets', ['content']) @@ -1582,7 +1604,9 @@ describe('agent', () => { }) const a = await agent.getPreferences() - const word = a.moderationPrefs.mutedWords.find((m) => m.value === 'word') + const word = a.moderationPrefs.mutedWords.find( + (m) => m.value === 'word', + ) await agent.updateMutedWord({ ...word!, @@ -1591,10 +1615,9 @@ describe('agent', () => { const b = await agent.getPreferences() - expect(b.moderationPrefs.mutedWords.find((m) => m.id === word!.id)).toHaveProperty( - 'targets', - ['content'], - ) + expect( + b.moderationPrefs.mutedWords.find((m) => m.id === word!.id), + ).toHaveProperty('targets', ['content']) }) it('updates actors', async () => { @@ -1605,7 +1628,9 @@ describe('agent', () => { }) const a = await agent.getPreferences() - const word = a.moderationPrefs.mutedWords.find((m) => m.value === 'value') + const word = a.moderationPrefs.mutedWords.find( + (m) => m.value === 'value', + ) await agent.updateMutedWord({ ...word!, @@ -1614,10 +1639,9 @@ describe('agent', () => { const b = await agent.getPreferences() - expect(b.moderationPrefs.mutedWords.find((m) => m.id === word!.id)).toHaveProperty( - 'actors', - ['did:plc:fake2'], - ) + expect( + b.moderationPrefs.mutedWords.find((m) => m.id === word!.id), + ).toHaveProperty('actors', ['did:plc:fake2']) }) it('updates expiresAt', async () => { @@ -1630,7 +1654,9 @@ describe('agent', () => { }) const a = await agent.getPreferences() - const word = a.moderationPrefs.mutedWords.find((m) => m.value === 'value') + const word = a.moderationPrefs.mutedWords.find( + (m) => m.value === 'value', + ) await agent.updateMutedWord({ ...word!, @@ -1639,10 +1665,9 @@ describe('agent', () => { const b = await agent.getPreferences() - expect(b.moderationPrefs.mutedWords.find((m) => m.id === word!.id)).toHaveProperty( - 'expiresAt', - expiresAt2, - ) + expect( + b.moderationPrefs.mutedWords.find((m) => m.id === word!.id), + ).toHaveProperty('expiresAt', expiresAt2) }) }) @@ -1650,25 +1675,33 @@ describe('agent', () => { it('removes word', async () => { await agent.addMutedWord({ value: 'word', targets: ['tag'] }) const a = await agent.getPreferences() - const word = a.moderationPrefs.mutedWords.find((m) => m.value === 'word') + const word = a.moderationPrefs.mutedWords.find( + (m) => m.value === 'word', + ) await agent.removeMutedWord(word!) const b = await agent.getPreferences() - expect(b.moderationPrefs.mutedWords.find((m) => m.id === word!.id)).toBeFalsy() + expect( + b.moderationPrefs.mutedWords.find((m) => m.id === word!.id), + ).toBeFalsy() }) it(`word doesn't exist, no action`, async () => { await agent.addMutedWord({ value: 'word', targets: ['tag'] }) const a = await agent.getPreferences() - const word = a.moderationPrefs.mutedWords.find((m) => m.value === 'word') + const word = a.moderationPrefs.mutedWords.find( + (m) => m.value === 'word', + ) await agent.removeMutedWord({ value: 'another', targets: [] }) const b = await agent.getPreferences() - expect(b.moderationPrefs.mutedWords.find((m) => m.id === word!.id)).toBeTruthy() + expect( + b.moderationPrefs.mutedWords.find((m) => m.id === word!.id), + ).toBeTruthy() }) }) @@ -1767,7 +1800,9 @@ describe('agent', () => { { const { moderationPrefs } = await agent.getPreferences() - const word = moderationPrefs.mutedWords.find((w) => w.value === 'word') + const word = moderationPrefs.mutedWords.find( + (w) => w.value === 'word', + ) expect(word).toBeTruthy() expect(word!.id).toBeFalsy() } @@ -1776,8 +1811,12 @@ describe('agent', () => { { const { moderationPrefs } = await agent.getPreferences() - const word = moderationPrefs.mutedWords.find((w) => w.value === 'word') - const word2 = moderationPrefs.mutedWords.find((w) => w.value === 'word2') + const word = moderationPrefs.mutedWords.find( + (w) => w.value === 'word', + ) + const word2 = moderationPrefs.mutedWords.find( + (w) => w.value === 'word2', + ) expect(word!.id).toBeTruthy() expect(word2!.id).toBeTruthy() @@ -1800,8 +1839,12 @@ describe('agent', () => { { const { moderationPrefs } = await agent.getPreferences() - const word = moderationPrefs.mutedWords.find((w) => w.value === 'word') - const word2 = moderationPrefs.mutedWords.find((w) => w.value === 'word2') + const word = moderationPrefs.mutedWords.find( + (w) => w.value === 'word', + ) + const word2 = moderationPrefs.mutedWords.find( + (w) => w.value === 'word2', + ) expect(moderationPrefs.mutedWords.length).toEqual(2) expect(word!.id).toBeTruthy() @@ -1826,8 +1869,12 @@ describe('agent', () => { { const { moderationPrefs } = await agent.getPreferences() - const word = moderationPrefs.mutedWords.find((w) => w.value === 'word') - const word2 = moderationPrefs.mutedWords.find((w) => w.value === 'word2') + const word = moderationPrefs.mutedWords.find( + (w) => w.value === 'word', + ) + const word2 = moderationPrefs.mutedWords.find( + (w) => w.value === 'word2', + ) expect(moderationPrefs.mutedWords.length).toEqual(1) expect(word).toBeFalsy() From 8a98ddae512428ca77ebdb54b41c4c2ab330ae9c Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Tue, 16 Jul 2024 12:47:32 -0700 Subject: [PATCH 11/22] Re-integrate tests --- packages/api/tests/bsky-agent.test.ts | 576 +++++++++++++++++++------- 1 file changed, 417 insertions(+), 159 deletions(-) diff --git a/packages/api/tests/bsky-agent.test.ts b/packages/api/tests/bsky-agent.test.ts index 0afdfa6403e..b2d2b742742 100644 --- a/packages/api/tests/bsky-agent.test.ts +++ b/packages/api/tests/bsky-agent.test.ts @@ -1751,14 +1751,6 @@ describe('agent', () => { describe('muted words', () => { let agent: BskyAgent - const mutedWords = [ - { value: 'both', targets: ['content', 'tag'] }, - { value: 'content', targets: ['content'] }, - { value: 'tag', targets: ['tag'] }, - { value: 'tag_then_both', targets: ['tag'] }, - { value: 'tag_then_content', targets: ['tag'] }, - { value: 'tag_then_none', targets: ['tag'] }, - ] beforeAll(async () => { agent = new BskyAgent({ service: network.pds.url }) @@ -1769,214 +1761,480 @@ describe('agent', () => { }) }) - it('upsertMutedWords', async () => { - await agent.upsertMutedWords(mutedWords) - await agent.upsertMutedWords(mutedWords) // double - await expect(agent.getPreferences()).resolves.toHaveProperty( - 'moderationPrefs.mutedWords', - mutedWords, - ) + afterEach(async () => { + const { moderationPrefs } = await agent.getPreferences() + await agent.removeMutedWords(moderationPrefs.mutedWords) }) - it('upsertMutedWords with #', async () => { - await agent.upsertMutedWords([ - { value: 'hashtag', targets: ['content'] }, - ]) - // is sanitized to `hashtag` - await agent.upsertMutedWords([{ value: '#hashtag', targets: ['tag'] }]) + describe('addMutedWord', () => { + it('inserts', async () => { + const expiresAt = new Date(Date.now() + 6e3).toISOString() + await agent.addMutedWord({ + value: 'word', + targets: ['content'], + actors: [], + expiresAt, + }) - const { mutedWords } = (await agent.getPreferences()).moderationPrefs + const { moderationPrefs } = await agent.getPreferences() + const word = moderationPrefs.mutedWords.find( + (m) => m.value === 'word', + ) - expect(mutedWords.find((m) => m.value === '#hashtag')).toBeFalsy() - // merged with existing - expect(mutedWords.find((m) => m.value === 'hashtag')).toStrictEqual({ - value: 'hashtag', - targets: ['content', 'tag'], + expect(word!.id).toBeTruthy() + expect(word!.targets).toEqual(['content']) + expect(word!.actors).toEqual([]) + expect(word!.expiresAt).toEqual(expiresAt) }) - // only one added - expect(mutedWords.filter((m) => m.value === 'hashtag').length).toBe(1) - }) - it('updateMutedWord', async () => { - await agent.updateMutedWord({ - value: 'tag_then_content', - targets: ['content'], + it('single-hash #, no insert', async () => { + await agent.addMutedWord({ value: '#', targets: [] }) + const { moderationPrefs } = await agent.getPreferences() + + // sanitized to empty string, not inserted + expect(moderationPrefs.mutedWords.length).toEqual(0) }) - await agent.updateMutedWord({ - value: 'tag_then_both', - targets: ['content', 'tag'], + + it('multi-hash ##, inserts #', async () => { + await agent.addMutedWord({ value: '##', targets: [] }) + const { moderationPrefs } = await agent.getPreferences() + expect( + moderationPrefs.mutedWords.find((m) => m.value === '#'), + ).toBeTruthy() }) - await agent.updateMutedWord({ value: 'tag_then_none', targets: [] }) - await agent.updateMutedWord({ value: 'no_exist', targets: ['tag'] }) - const { mutedWords } = (await agent.getPreferences()).moderationPrefs - - expect( - mutedWords.find((m) => m.value === 'tag_then_content'), - ).toHaveProperty('targets', ['content']) - expect( - mutedWords.find((m) => m.value === 'tag_then_both'), - ).toHaveProperty('targets', ['content', 'tag']) - expect( - mutedWords.find((m) => m.value === 'tag_then_none'), - ).toHaveProperty('targets', []) - expect(mutedWords.find((m) => m.value === 'no_exist')).toBeFalsy() - }) - - it('updateMutedWord with #, does not update', async () => { - await agent.upsertMutedWords([ - { - value: '#just_a_tag', - targets: ['tag'], - }, - ]) - await agent.updateMutedWord({ - value: '#just_a_tag', - targets: ['tag', 'content'], + + it('multi-hash ##hashtag, inserts #hashtag', async () => { + await agent.addMutedWord({ value: '##hashtag', targets: [] }) + const { moderationPrefs } = await agent.getPreferences() + expect( + moderationPrefs.mutedWords.find((w) => w.value === '#hashtag'), + ).toBeTruthy() }) - const { mutedWords } = (await agent.getPreferences()).moderationPrefs - expect(mutedWords.find((m) => m.value === 'just_a_tag')).toStrictEqual({ - value: 'just_a_tag', - targets: ['tag'], + + it('hash emoji #️⃣, inserts #️⃣', async () => { + await agent.addMutedWord({ value: '#️⃣', targets: [] }) + const { moderationPrefs } = await agent.getPreferences() + expect( + moderationPrefs.mutedWords.find((m) => m.value === '#️⃣'), + ).toBeTruthy() }) - }) - it('removeMutedWord', async () => { - await agent.removeMutedWord({ value: 'tag_then_content', targets: [] }) - await agent.removeMutedWord({ value: 'tag_then_both', targets: [] }) - await agent.removeMutedWord({ value: 'tag_then_none', targets: [] }) - const { mutedWords } = (await agent.getPreferences()).moderationPrefs + it('hash emoji w/leading hash ##️⃣, inserts #️⃣', async () => { + await agent.addMutedWord({ value: '##️⃣', targets: [] }) + const { moderationPrefs } = await agent.getPreferences() + expect( + moderationPrefs.mutedWords.find((m) => m.value === '#️⃣'), + ).toBeTruthy() + }) - expect( - mutedWords.find((m) => m.value === 'tag_then_content'), - ).toBeFalsy() - expect(mutedWords.find((m) => m.value === 'tag_then_both')).toBeFalsy() - expect(mutedWords.find((m) => m.value === 'tag_then_none')).toBeFalsy() - }) + it('hash emoji with double leading hash ###️⃣, inserts ##️⃣', async () => { + await agent.addMutedWord({ value: '###️⃣', targets: [] }) + const { moderationPrefs } = await agent.getPreferences() + expect( + moderationPrefs.mutedWords.find((m) => m.value === '##️⃣'), + ).toBeTruthy() + }) - it('removeMutedWord with #, no match, no removal', async () => { - await agent.removeMutedWord({ value: '#hashtag', targets: [] }) - const { mutedWords } = (await agent.getPreferences()).moderationPrefs + describe(`invalid characters`, () => { + it('#, no insert', async () => { + await agent.addMutedWord({ value: '#​', targets: [] }) + const { moderationPrefs } = await agent.getPreferences() + expect(moderationPrefs.mutedWords.length).toEqual(0) + }) + + it('#ab, inserts ab', async () => { + await agent.addMutedWord({ value: '#​ab', targets: [] }) + const { moderationPrefs } = await agent.getPreferences() + expect(moderationPrefs.mutedWords.length).toEqual(1) + }) + + it('phrase with newline, inserts phrase without newline', async () => { + await agent.addMutedWord({ + value: 'test value\n with newline', + targets: [], + }) + const { moderationPrefs } = await agent.getPreferences() + expect( + moderationPrefs.mutedWords.find( + (m) => m.value === 'test value with newline', + ), + ).toBeTruthy() + }) + + it('phrase with newlines, inserts phrase without newlines', async () => { + await agent.addMutedWord({ + value: 'test value\n\r with newline', + targets: [], + }) + const { moderationPrefs } = await agent.getPreferences() + expect( + moderationPrefs.mutedWords.find( + (m) => m.value === 'test value with newline', + ), + ).toBeTruthy() + }) - // was inserted with #hashtag, but we don't sanitize on remove - expect(mutedWords.find((m) => m.value === 'hashtag')).toBeTruthy() + it('empty space, no insert', async () => { + await agent.addMutedWord({ value: ' ', targets: [] }) + const { moderationPrefs } = await agent.getPreferences() + expect(moderationPrefs.mutedWords.length).toEqual(0) + }) + + it(`' trim ', inserts 'trim'`, async () => { + await agent.addMutedWord({ value: ' trim ', targets: [] }) + const { moderationPrefs } = await agent.getPreferences() + expect( + moderationPrefs.mutedWords.find((m) => m.value === 'trim'), + ).toBeTruthy() + }) + }) }) - it('single-hash #', async () => { - const prev = (await agent.getPreferences()).moderationPrefs - const length = prev.mutedWords.length - await agent.upsertMutedWords([{ value: '#', targets: [] }]) - const end = (await agent.getPreferences()).moderationPrefs + describe('addMutedWords', () => { + it('inserts happen sequentially, no clobbering', async () => { + await agent.addMutedWords([ + { value: 'a', targets: ['content'] }, + { value: 'b', targets: ['content'] }, + { value: 'c', targets: ['content'] }, + ]) + + const { moderationPrefs } = await agent.getPreferences() - // sanitized to empty string, not inserted - expect(end.mutedWords.length).toEqual(length) + expect(moderationPrefs.mutedWords.length).toEqual(3) + }) }) - it('multi-hash ##', async () => { - await agent.upsertMutedWords([{ value: '##', targets: [] }]) - const { mutedWords } = (await agent.getPreferences()).moderationPrefs + describe('upsertMutedWords (deprecated)', () => { + it('no longer upserts, calls addMutedWords', async () => { + await agent.upsertMutedWords([ + { value: 'both', targets: ['content'] }, + ]) + await agent.upsertMutedWords([{ value: 'both', targets: ['tag'] }]) + + const { moderationPrefs } = await agent.getPreferences() - expect(mutedWords.find((m) => m.value === '#')).toBeTruthy() + expect(moderationPrefs.mutedWords.length).toEqual(2) + }) }) - it('multi-hash ##hashtag', async () => { - await agent.upsertMutedWords([{ value: '##hashtag', targets: [] }]) - const a = (await agent.getPreferences()).moderationPrefs + describe('updateMutedWord', () => { + it(`word doesn't exist, no update or insert`, async () => { + await agent.updateMutedWord({ + value: 'word', + targets: ['tag', 'content'], + }) + const { moderationPrefs } = await agent.getPreferences() + expect(moderationPrefs.mutedWords.length).toEqual(0) + }) - expect(a.mutedWords.find((w) => w.value === '#hashtag')).toBeTruthy() + it('updates and sanitizes new value', async () => { + await agent.addMutedWord({ + value: 'value', + targets: ['content'], + }) - await agent.removeMutedWord({ value: '#hashtag', targets: [] }) - const b = (await agent.getPreferences()).moderationPrefs + const a = await agent.getPreferences() + const word = a.moderationPrefs.mutedWords.find( + (m) => m.value === 'value', + ) - expect(b.mutedWords.find((w) => w.value === '#hashtag')).toBeFalsy() - }) + await agent.updateMutedWord({ + ...word!, + value: '#new value', + }) - it('hash emoji #️⃣', async () => { - await agent.upsertMutedWords([{ value: '#️⃣', targets: [] }]) - const { mutedWords } = (await agent.getPreferences()).moderationPrefs + const b = await agent.getPreferences() + const updatedWord = b.moderationPrefs.mutedWords.find( + (m) => m.id === word!.id, + ) - expect(mutedWords.find((m) => m.value === '#️⃣')).toBeTruthy() + expect(updatedWord!.value).toEqual('new value') + expect(updatedWord).toHaveProperty('targets', ['content']) + }) - await agent.removeMutedWord({ value: '#️⃣', targets: [] }) - const end = (await agent.getPreferences()).moderationPrefs + it('updates targets', async () => { + await agent.addMutedWord({ + value: 'word', + targets: ['tag'], + }) - expect(end.mutedWords.find((m) => m.value === '#️⃣')).toBeFalsy() - }) + const a = await agent.getPreferences() + const word = a.moderationPrefs.mutedWords.find( + (m) => m.value === 'word', + ) - it('hash emoji ##️⃣', async () => { - await agent.upsertMutedWords([{ value: '##️⃣', targets: [] }]) - const { mutedWords } = (await agent.getPreferences()).moderationPrefs + await agent.updateMutedWord({ + ...word!, + targets: ['content'], + }) - expect(mutedWords.find((m) => m.value === '#️⃣')).toBeTruthy() + const b = await agent.getPreferences() - await agent.removeMutedWord({ value: '#️⃣', targets: [] }) - const end = (await agent.getPreferences()).moderationPrefs + expect( + b.moderationPrefs.mutedWords.find((m) => m.id === word!.id), + ).toHaveProperty('targets', ['content']) + }) - expect(end.mutedWords.find((m) => m.value === '#️⃣')).toBeFalsy() - }) + it('updates actors', async () => { + await agent.addMutedWord({ + value: 'value', + targets: ['content'], + actors: ['did:plc:fake'], + }) - it('hash emoji ###️⃣', async () => { - await agent.upsertMutedWords([{ value: '###️⃣', targets: [] }]) - const { mutedWords } = (await agent.getPreferences()).moderationPrefs + const a = await agent.getPreferences() + const word = a.moderationPrefs.mutedWords.find( + (m) => m.value === 'value', + ) - expect(mutedWords.find((m) => m.value === '##️⃣')).toBeTruthy() + await agent.updateMutedWord({ + ...word!, + actors: ['did:plc:fake2'], + }) - await agent.removeMutedWord({ value: '##️⃣', targets: [] }) - const end = (await agent.getPreferences()).moderationPrefs + const b = await agent.getPreferences() - expect(end.mutedWords.find((m) => m.value === '##️⃣')).toBeFalsy() - }) + expect( + b.moderationPrefs.mutedWords.find((m) => m.id === word!.id), + ).toHaveProperty('actors', ['did:plc:fake2']) + }) + + it('updates expiresAt', async () => { + const expiresAt = new Date(Date.now() + 6e3).toISOString() + const expiresAt2 = new Date(Date.now() + 10e3).toISOString() + await agent.addMutedWord({ + value: 'value', + targets: ['content'], + expiresAt, + }) + + const a = await agent.getPreferences() + const word = a.moderationPrefs.mutedWords.find( + (m) => m.value === 'value', + ) + + await agent.updateMutedWord({ + ...word!, + expiresAt: expiresAt2, + }) - it(`apostrophe: Bluesky's`, async () => { - await agent.upsertMutedWords([{ value: `Bluesky's`, targets: [] }]) - const { mutedWords } = (await agent.getPreferences()).moderationPrefs + const b = await agent.getPreferences() - expect(mutedWords.find((m) => m.value === `Bluesky's`)).toBeTruthy() + expect( + b.moderationPrefs.mutedWords.find((m) => m.id === word!.id), + ).toHaveProperty('expiresAt', expiresAt2) + }) }) - describe(`invalid characters`, () => { - it('zero width space', async () => { - const prev = (await agent.getPreferences()).moderationPrefs - const length = prev.mutedWords.length - await agent.upsertMutedWords([{ value: '#​', targets: [] }]) - const { mutedWords } = (await agent.getPreferences()).moderationPrefs + describe('removeMutedWord', () => { + it('removes word', async () => { + await agent.addMutedWord({ value: 'word', targets: ['tag'] }) + const a = await agent.getPreferences() + const word = a.moderationPrefs.mutedWords.find( + (m) => m.value === 'word', + ) + + await agent.removeMutedWord(word!) + + const b = await agent.getPreferences() - expect(mutedWords.length).toEqual(length) + expect( + b.moderationPrefs.mutedWords.find((m) => m.id === word!.id), + ).toBeFalsy() }) - it('newline', async () => { - await agent.upsertMutedWords([ - { value: 'test value\n with newline', targets: [] }, - ]) - const { mutedWords } = (await agent.getPreferences()).moderationPrefs + it(`word doesn't exist, no action`, async () => { + await agent.addMutedWord({ value: 'word', targets: ['tag'] }) + const a = await agent.getPreferences() + const word = a.moderationPrefs.mutedWords.find( + (m) => m.value === 'word', + ) + + await agent.removeMutedWord({ value: 'another', targets: [] }) + + const b = await agent.getPreferences() expect( - mutedWords.find((m) => m.value === 'test value with newline'), + b.moderationPrefs.mutedWords.find((m) => m.id === word!.id), ).toBeTruthy() }) + }) - it('newline(s)', async () => { - await agent.upsertMutedWords([ - { value: 'test value\n\r with newline', targets: [] }, + describe('removeMutedWords', () => { + it(`removes sequentially, no clobbering`, async () => { + await agent.addMutedWords([ + { value: 'a', targets: ['content'] }, + { value: 'b', targets: ['content'] }, + { value: 'c', targets: ['content'] }, ]) - const { mutedWords } = (await agent.getPreferences()).moderationPrefs - expect( - mutedWords.find((m) => m.value === 'test value with newline'), - ).toBeTruthy() + const a = await agent.getPreferences() + await agent.removeMutedWords(a.moderationPrefs.mutedWords) + const b = await agent.getPreferences() + + expect(b.moderationPrefs.mutedWords.length).toEqual(0) }) + }) + }) - it('empty space', async () => { - await agent.upsertMutedWords([{ value: ' ', targets: [] }]) - const { mutedWords } = (await agent.getPreferences()).moderationPrefs + describe('legacy muted words', () => { + let agent: BskyAgent - expect(mutedWords.find((m) => m.value === ' ')).toBeFalsy() + async function updatePreferences( + agent: BskyAgent, + cb: ( + prefs: AppBskyActorDefs.Preferences, + ) => AppBskyActorDefs.Preferences | false, + ) { + const res = await agent.app.bsky.actor.getPreferences({}) + const newPrefs = cb(res.data.preferences) + if (newPrefs === false) { + return + } + await agent.app.bsky.actor.putPreferences({ + preferences: newPrefs, }) + } - it('leading/trailing space', async () => { - await agent.upsertMutedWords([{ value: ' trim ', targets: [] }]) - const { mutedWords } = (await agent.getPreferences()).moderationPrefs + async function addLegacyMutedWord(mutedWord: AppBskyActorDefs.MutedWord) { + await updatePreferences(agent, (prefs) => { + let mutedWordsPref = prefs.findLast( + (pref) => + AppBskyActorDefs.isMutedWordsPref(pref) && + AppBskyActorDefs.validateMutedWordsPref(pref).success, + ) + + const newMutedWord: AppBskyActorDefs.MutedWord = { + value: mutedWord.value, + targets: mutedWord.targets, + } - expect(mutedWords.find((m) => m.value === 'trim')).toBeTruthy() + if ( + mutedWordsPref && + AppBskyActorDefs.isMutedWordsPref(mutedWordsPref) + ) { + mutedWordsPref.items.push(newMutedWord) + } else { + // if the pref doesn't exist, create it + mutedWordsPref = { + items: [newMutedWord], + } + } + + return prefs + .filter((p) => !AppBskyActorDefs.isMutedWordsPref(p)) + .concat([ + { + ...mutedWordsPref, + $type: 'app.bsky.actor.defs#mutedWordsPref', + }, + ]) + }) + } + + beforeAll(async () => { + agent = new BskyAgent({ service: network.pds.url }) + await agent.createAccount({ + handle: 'user7-1.test', + email: 'user7-1@test.com', + password: 'password', + }) + }) + + afterEach(async () => { + const { moderationPrefs } = await agent.getPreferences() + await agent.removeMutedWords(moderationPrefs.mutedWords) + }) + + describe(`upsertMutedWords (and addMutedWord)`, () => { + it(`adds new word, migrates old words`, async () => { + await addLegacyMutedWord({ + value: 'word', + targets: ['content'], + }) + + { + const { moderationPrefs } = await agent.getPreferences() + const word = moderationPrefs.mutedWords.find( + (w) => w.value === 'word', + ) + expect(word).toBeTruthy() + expect(word!.id).toBeFalsy() + } + + await agent.upsertMutedWords([{ value: 'word2', targets: ['tag'] }]) + + { + const { moderationPrefs } = await agent.getPreferences() + const word = moderationPrefs.mutedWords.find( + (w) => w.value === 'word', + ) + const word2 = moderationPrefs.mutedWords.find( + (w) => w.value === 'word2', + ) + + expect(word!.id).toBeTruthy() + expect(word2!.id).toBeTruthy() + } + }) + }) + + describe(`updateMutedWord`, () => { + it(`updates legacy word, migrates old words`, async () => { + await addLegacyMutedWord({ + value: 'word', + targets: ['content'], + }) + await addLegacyMutedWord({ + value: 'word2', + targets: ['tag'], + }) + + await agent.updateMutedWord({ value: 'word', targets: ['tag'] }) + + { + const { moderationPrefs } = await agent.getPreferences() + const word = moderationPrefs.mutedWords.find( + (w) => w.value === 'word', + ) + const word2 = moderationPrefs.mutedWords.find( + (w) => w.value === 'word2', + ) + + expect(moderationPrefs.mutedWords.length).toEqual(2) + expect(word!.id).toBeTruthy() + expect(word!.targets).toEqual(['tag']) + expect(word2!.id).toBeTruthy() + } + }) + }) + + describe(`removeMutedWord`, () => { + it(`removes legacy word, migrates old words`, async () => { + await addLegacyMutedWord({ + value: 'word', + targets: ['content'], + }) + await addLegacyMutedWord({ + value: 'word2', + targets: ['tag'], + }) + + await agent.removeMutedWord({ value: 'word', targets: ['tag'] }) + + { + const { moderationPrefs } = await agent.getPreferences() + const word = moderationPrefs.mutedWords.find( + (w) => w.value === 'word', + ) + const word2 = moderationPrefs.mutedWords.find( + (w) => w.value === 'word2', + ) + + expect(moderationPrefs.mutedWords.length).toEqual(1) + expect(word).toBeFalsy() + expect(word2!.id).toBeTruthy() + } }) }) }) From 69d5f2179253b9a24cb8b481355b1955d1d2fe71 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Tue, 16 Jul 2024 14:35:54 -0700 Subject: [PATCH 12/22] Let the lock cook --- packages/api/src/bsky-agent.ts | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/packages/api/src/bsky-agent.ts b/packages/api/src/bsky-agent.ts index 40f77c594a6..7d7d49d86f1 100644 --- a/packages/api/src/bsky-agent.ts +++ b/packages/api/src/bsky-agent.ts @@ -939,8 +939,7 @@ export class BskyAgent extends AtpAgent { } /** - * Add a muted word to user preferences. If called in succession, this - * method must be called sequentially, not in parallel. + * Add a muted word to user preferences. */ async addMutedWord( mutedWord: Pick, @@ -992,9 +991,7 @@ export class BskyAgent extends AtpAgent { * Convenience method to sequentially add muted words to user preferences */ async addMutedWords(newMutedWords: AppBskyActorDefs.MutedWord[]) { - for (const word of newMutedWords) { - await this.addMutedWord(word) - } + await Promise.all(newMutedWords.map((word) => this.addMutedWord(word))) } /** @@ -1056,8 +1053,7 @@ export class BskyAgent extends AtpAgent { } /** - * Remove a muted word from user preferences. If called in succession, this - * method must be called sequentially, not in parallel. + * Remove a muted word from user preferences. */ async removeMutedWord(mutedWord: AppBskyActorDefs.MutedWord) { await updatePreferences(this, (prefs: AppBskyActorDefs.Preferences) => { @@ -1096,13 +1092,10 @@ export class BskyAgent extends AtpAgent { } /** - * Convenience method to sequentially remove muted words from user - * preferences + * Convenience method to remove muted words from user preferences */ async removeMutedWords(mutedWords: AppBskyActorDefs.MutedWord[]) { - for (const word of mutedWords) { - await this.removeMutedWord(word) - } + await Promise.all(mutedWords.map((word) => this.removeMutedWord(word))) } async hidePost(postUri: string) { From e46566ac0d872f9a0f27fa75b54c35beb90756f5 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Tue, 16 Jul 2024 14:37:29 -0700 Subject: [PATCH 13/22] Fix comments --- packages/api/src/bsky-agent.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/api/src/bsky-agent.ts b/packages/api/src/bsky-agent.ts index 7d7d49d86f1..9e48c646d4d 100644 --- a/packages/api/src/bsky-agent.ts +++ b/packages/api/src/bsky-agent.ts @@ -988,7 +988,7 @@ export class BskyAgent extends AtpAgent { } /** - * Convenience method to sequentially add muted words to user preferences + * Convenience method to add muted words to user preferences */ async addMutedWords(newMutedWords: AppBskyActorDefs.MutedWord[]) { await Promise.all(newMutedWords.map((word) => this.addMutedWord(word))) @@ -1002,8 +1002,7 @@ export class BskyAgent extends AtpAgent { } /** - * Update a muted word in user preferences. If called in succession, this - * method must be called sequentially, not in parallel. + * Update a muted word in user preferences. */ async updateMutedWord(mutedWord: AppBskyActorDefs.MutedWord) { await updatePreferences(this, (prefs: AppBskyActorDefs.Preferences) => { From 6c5ba0d9b751967be29eef09f10ea50e97dde0da Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Wed, 17 Jul 2024 13:52:28 -0500 Subject: [PATCH 14/22] Integrate mute words enhancements (#2643) * Check expiry when comparing mute words * Check actors when comparing * Tweak lex, condegen * Integrate new prop --- lexicons/app/bsky/actor/defs.json | 8 +- packages/api/src/bsky-agent.ts | 9 +- packages/api/src/client/lexicons.ts | 12 +- .../src/client/types/app/bsky/actor/defs.ts | 4 +- packages/api/src/moderation/mutewords.ts | 11 ++ packages/api/src/moderation/subjects/post.ts | 15 ++ packages/api/tests/bsky-agent.test.ts | 12 +- .../api/tests/moderation-mutewords.test.ts | 140 ++++++++++++++++++ packages/bsky/src/lexicon/lexicons.ts | 12 +- .../src/lexicon/types/app/bsky/actor/defs.ts | 4 +- packages/ozone/src/lexicon/lexicons.ts | 12 +- .../src/lexicon/types/app/bsky/actor/defs.ts | 4 +- packages/pds/src/lexicon/lexicons.ts | 12 +- .../src/lexicon/types/app/bsky/actor/defs.ts | 4 +- 14 files changed, 210 insertions(+), 49 deletions(-) diff --git a/lexicons/app/bsky/actor/defs.json b/lexicons/app/bsky/actor/defs.json index 30d0f2b7249..07e42daef2c 100644 --- a/lexicons/app/bsky/actor/defs.json +++ b/lexicons/app/bsky/actor/defs.json @@ -345,10 +345,10 @@ "ref": "app.bsky.actor.defs#mutedWordTarget" } }, - "actors": { - "type": "array", - "description": "The accounts for which this muted word applies.", - "items": { "type": "string", "format": "did" } + "actorTarget": { + "type": "string", + "description": "Groups of users to apply the muted word to. If undefined, applies to all users.", + "knownValues": ["all", "exclude-following"] }, "expiresAt": { "type": "string", diff --git a/packages/api/src/bsky-agent.ts b/packages/api/src/bsky-agent.ts index 9e48c646d4d..add6820e9aa 100644 --- a/packages/api/src/bsky-agent.ts +++ b/packages/api/src/bsky-agent.ts @@ -942,7 +942,10 @@ export class BskyAgent extends AtpAgent { * Add a muted word to user preferences. */ async addMutedWord( - mutedWord: Pick, + mutedWord: Pick< + MutedWord, + 'value' | 'targets' | 'actorTarget' | 'expiresAt' + >, ) { const sanitizedValue = sanitizeMutedWordValue(mutedWord.value) @@ -959,7 +962,7 @@ export class BskyAgent extends AtpAgent { id: TID.nextStr(), value: sanitizedValue, targets: mutedWord.targets || [], - actors: mutedWord.actors || [], + actorTarget: mutedWord.actorTarget || 'all', expiresAt: mutedWord.expiresAt || undefined, } @@ -1025,7 +1028,7 @@ export class BskyAgent extends AtpAgent { id: existingItem.id || TID.nextStr(), value: sanitizeMutedWordValue(updated.value), targets: updated.targets || [], - actors: updated.actors || [], + actorTarget: updated.actorTarget || 'all', expiresAt: updated.expiresAt || undefined, } } else { diff --git a/packages/api/src/client/lexicons.ts b/packages/api/src/client/lexicons.ts index bb10ba58bc3..d05148b9d6f 100644 --- a/packages/api/src/client/lexicons.ts +++ b/packages/api/src/client/lexicons.ts @@ -4358,13 +4358,11 @@ export const schemaDict = { ref: 'lex:app.bsky.actor.defs#mutedWordTarget', }, }, - actors: { - type: 'array', - description: 'The accounts for which this muted word applies.', - items: { - type: 'string', - format: 'did', - }, + actorTarget: { + type: 'string', + description: + 'Groups of users to apply the muted word to. If undefined, applies to all users.', + knownValues: ['all', 'exclude-following'], }, expiresAt: { type: 'string', diff --git a/packages/api/src/client/types/app/bsky/actor/defs.ts b/packages/api/src/client/types/app/bsky/actor/defs.ts index 7dff2b33e03..6595811f9b7 100644 --- a/packages/api/src/client/types/app/bsky/actor/defs.ts +++ b/packages/api/src/client/types/app/bsky/actor/defs.ts @@ -375,8 +375,8 @@ export interface MutedWord { value: string /** The intended targets of the muted word. */ targets: MutedWordTarget[] - /** The accounts for which this muted word applies. */ - actors?: string[] + /** Groups of users to apply the muted word to. If undefined, applies to all users. */ + actorTarget?: 'all' | 'exclude-following' | (string & {}) /** The date and time at which the muted word will expire and no longer be applied. */ expiresAt?: string [k: string]: unknown diff --git a/packages/api/src/moderation/mutewords.ts b/packages/api/src/moderation/mutewords.ts index 8988f3dc2b6..e5579b20903 100644 --- a/packages/api/src/moderation/mutewords.ts +++ b/packages/api/src/moderation/mutewords.ts @@ -27,12 +27,14 @@ export function hasMutedWord({ facets, outlineTags, languages, + actor, }: { mutedWords: AppBskyActorDefs.MutedWord[] text: string facets?: AppBskyRichtextFacet.Main[] outlineTags?: string[] languages?: string[] + actor?: AppBskyActorDefs.ProfileView }) { const exception = LANGUAGE_EXCEPTIONS.includes(languages?.[0] || '') const tags = ([] as string[]) @@ -52,6 +54,15 @@ export function hasMutedWord({ const mutedWord = mute.value.toLowerCase() const postText = text.toLowerCase() + // expired, ignore + if (mute.expiresAt && mute.expiresAt < new Date().toISOString()) continue + + if ( + mute.actorTarget === 'exclude-following' && + Boolean(actor?.viewer?.following) + ) + continue + // `content` applies to tags as well if (tags.includes(mutedWord)) return true // rest of the checks are for `content` only diff --git a/packages/api/src/moderation/subjects/post.ts b/packages/api/src/moderation/subjects/post.ts index 1274a453e29..5d6e5c765a9 100644 --- a/packages/api/src/moderation/subjects/post.ts +++ b/packages/api/src/moderation/subjects/post.ts @@ -141,6 +141,8 @@ function checkMutedWords( return false } + const postAuthor = subject.author + if (AppBskyFeedPost.isRecord(subject.record)) { // post text if ( @@ -150,6 +152,7 @@ function checkMutedWords( facets: subject.record.facets, outlineTags: subject.record.tags, languages: subject.record.langs, + actor: postAuthor, }) ) { return true @@ -166,6 +169,7 @@ function checkMutedWords( mutedWords, text: image.alt, languages: subject.record.langs, + actor: postAuthor, }) ) { return true @@ -179,6 +183,7 @@ function checkMutedWords( if (AppBskyEmbedRecord.isViewRecord(subject.embed.record)) { if (AppBskyFeedPost.isRecord(subject.embed.record.value)) { const embeddedPost = subject.embed.record.value + const embedAuthor = subject.embed.record.author // quoted post text if ( @@ -188,6 +193,7 @@ function checkMutedWords( facets: embeddedPost.facets, outlineTags: embeddedPost.tags, languages: embeddedPost.langs, + actor: embedAuthor, }) ) { return true @@ -201,6 +207,7 @@ function checkMutedWords( mutedWords, text: image.alt, languages: embeddedPost.langs, + actor: embedAuthor, }) ) { return true @@ -216,6 +223,7 @@ function checkMutedWords( mutedWords, text: external.title + ' ' + external.description, languages: [], + actor: embedAuthor, }) ) { return true @@ -231,6 +239,7 @@ function checkMutedWords( mutedWords, text: external.title + ' ' + external.description, languages: [], + actor: embedAuthor, }) ) { return true @@ -247,6 +256,7 @@ function checkMutedWords( languages: AppBskyFeedPost.isRecord(embeddedPost.record) ? embeddedPost.langs : [], + actor: embedAuthor, }) ) { return true @@ -264,6 +274,7 @@ function checkMutedWords( mutedWords, text: external.title + ' ' + external.description, languages: [], + actor: postAuthor, }) ) { return true @@ -274,6 +285,8 @@ function checkMutedWords( AppBskyEmbedRecordWithMedia.isView(subject.embed) && AppBskyEmbedRecord.isViewRecord(subject.embed.record.record) ) { + const embedAuthor = subject.embed.record.record.author + // quoted post text if (AppBskyFeedPost.isRecord(subject.embed.record.record.value)) { const post = subject.embed.record.record.value @@ -284,6 +297,7 @@ function checkMutedWords( facets: post.facets, outlineTags: post.tags, languages: post.langs, + actor: embedAuthor, }) ) { return true @@ -300,6 +314,7 @@ function checkMutedWords( languages: AppBskyFeedPost.isRecord(subject.record) ? subject.record.langs : [], + actor: embedAuthor, }) ) { return true diff --git a/packages/api/tests/bsky-agent.test.ts b/packages/api/tests/bsky-agent.test.ts index b2d2b742742..0fd2240e826 100644 --- a/packages/api/tests/bsky-agent.test.ts +++ b/packages/api/tests/bsky-agent.test.ts @@ -1772,7 +1772,7 @@ describe('agent', () => { await agent.addMutedWord({ value: 'word', targets: ['content'], - actors: [], + actorTarget: 'all', expiresAt, }) @@ -1783,7 +1783,7 @@ describe('agent', () => { expect(word!.id).toBeTruthy() expect(word!.targets).toEqual(['content']) - expect(word!.actors).toEqual([]) + expect(word!.actorTarget).toEqual('all') expect(word!.expiresAt).toEqual(expiresAt) }) @@ -1975,11 +1975,11 @@ describe('agent', () => { ).toHaveProperty('targets', ['content']) }) - it('updates actors', async () => { + it('updates actorTarget', async () => { await agent.addMutedWord({ value: 'value', targets: ['content'], - actors: ['did:plc:fake'], + actorTarget: 'all', }) const a = await agent.getPreferences() @@ -1989,14 +1989,14 @@ describe('agent', () => { await agent.updateMutedWord({ ...word!, - actors: ['did:plc:fake2'], + actorTarget: 'exclude-following', }) const b = await agent.getPreferences() expect( b.moderationPrefs.mutedWords.find((m) => m.id === word!.id), - ).toHaveProperty('actors', ['did:plc:fake2']) + ).toHaveProperty('actorTarget', 'exclude-following') }) it('updates expiresAt', async () => { diff --git a/packages/api/tests/moderation-mutewords.test.ts b/packages/api/tests/moderation-mutewords.test.ts index 5416152aecb..3ccb77b5c74 100644 --- a/packages/api/tests/moderation-mutewords.test.ts +++ b/packages/api/tests/moderation-mutewords.test.ts @@ -766,4 +766,144 @@ describe(`hasMutedWord`, () => { expect(res.causes.length).toBe(0) }) }) + + describe(`timed mute words`, () => { + it(`non-expired word`, () => { + jest.useFakeTimers() + const now = Date.now() + + const res = moderatePost( + mock.postView({ + record: mock.post({ + text: 'Mute words!', + }), + author: mock.profileViewBasic({ + handle: 'bob.test', + displayName: 'Bob', + }), + labels: [], + }), + { + userDid: 'did:web:alice.test', + prefs: { + adultContentEnabled: false, + labels: {}, + labelers: [], + mutedWords: [ + { + value: 'words', + targets: ['content'], + expiresAt: new Date(now + 1e3).toISOString(), + }, + ], + hiddenPosts: [], + }, + labelDefs: {}, + }, + ) + + expect(res.causes[0].type).toBe('mute-word') + + jest.useRealTimers() + }) + + it(`expired word`, () => { + jest.useFakeTimers() + const now = Date.now() + + const res = moderatePost( + mock.postView({ + record: mock.post({ + text: 'Mute words!', + }), + author: mock.profileViewBasic({ + handle: 'bob.test', + displayName: 'Bob', + }), + labels: [], + }), + { + userDid: 'did:web:alice.test', + prefs: { + adultContentEnabled: false, + labels: {}, + labelers: [], + mutedWords: [ + { + value: 'words', + targets: ['content'], + expiresAt: new Date(now - 1e3).toISOString(), + }, + ], + hiddenPosts: [], + }, + labelDefs: {}, + }, + ) + + expect(res.causes.length).toBe(0) + + jest.useRealTimers() + }) + }) + + describe(`actor-based mute words`, () => { + const viewer = { + userDid: 'did:web:alice.test', + prefs: { + adultContentEnabled: false, + labels: {}, + labelers: [], + mutedWords: [ + { + value: 'words', + targets: ['content'], + actorTarget: 'exclude-following', + }, + ], + hiddenPosts: [], + }, + labelDefs: {}, + } + + it(`followed actor`, () => { + const res = moderatePost( + mock.postView({ + record: mock.post({ + text: 'Mute words!', + }), + author: mock.profileViewBasic({ + handle: 'bob.test', + displayName: 'Bob', + viewer: { + following: 'true', + }, + }), + labels: [], + }), + viewer, + ) + expect(res.causes.length).toBe(0) + }) + + it(`non-followed actor`, () => { + const res = moderatePost( + mock.postView({ + record: mock.post({ + text: 'Mute words!', + }), + author: mock.profileViewBasic({ + handle: 'carla.test', + displayName: 'Carla', + viewer: { + following: undefined, + }, + }), + labels: [], + }), + viewer, + ) + expect(res.causes[0].type).toBe('mute-word') + }) + }) }) diff --git a/packages/bsky/src/lexicon/lexicons.ts b/packages/bsky/src/lexicon/lexicons.ts index 68b3d1c301d..2e35e507879 100644 --- a/packages/bsky/src/lexicon/lexicons.ts +++ b/packages/bsky/src/lexicon/lexicons.ts @@ -4358,13 +4358,11 @@ export const schemaDict = { ref: 'lex:app.bsky.actor.defs#mutedWordTarget', }, }, - actors: { - type: 'array', - description: 'The accounts for which this muted word applies.', - items: { - type: 'string', - format: 'did', - }, + actorTarget: { + type: 'string', + description: + 'Groups of users to apply the muted word to. If undefined, applies to all users.', + knownValues: ['all', 'exclude-following'], }, expiresAt: { type: 'string', diff --git a/packages/bsky/src/lexicon/types/app/bsky/actor/defs.ts b/packages/bsky/src/lexicon/types/app/bsky/actor/defs.ts index 5f1a06f10b7..555fa037ee7 100644 --- a/packages/bsky/src/lexicon/types/app/bsky/actor/defs.ts +++ b/packages/bsky/src/lexicon/types/app/bsky/actor/defs.ts @@ -375,8 +375,8 @@ export interface MutedWord { value: string /** The intended targets of the muted word. */ targets: MutedWordTarget[] - /** The accounts for which this muted word applies. */ - actors?: string[] + /** Groups of users to apply the muted word to. If undefined, applies to all users. */ + actorTarget?: 'all' | 'exclude-following' | (string & {}) /** The date and time at which the muted word will expire and no longer be applied. */ expiresAt?: string [k: string]: unknown diff --git a/packages/ozone/src/lexicon/lexicons.ts b/packages/ozone/src/lexicon/lexicons.ts index bb10ba58bc3..d05148b9d6f 100644 --- a/packages/ozone/src/lexicon/lexicons.ts +++ b/packages/ozone/src/lexicon/lexicons.ts @@ -4358,13 +4358,11 @@ export const schemaDict = { ref: 'lex:app.bsky.actor.defs#mutedWordTarget', }, }, - actors: { - type: 'array', - description: 'The accounts for which this muted word applies.', - items: { - type: 'string', - format: 'did', - }, + actorTarget: { + type: 'string', + description: + 'Groups of users to apply the muted word to. If undefined, applies to all users.', + knownValues: ['all', 'exclude-following'], }, expiresAt: { type: 'string', diff --git a/packages/ozone/src/lexicon/types/app/bsky/actor/defs.ts b/packages/ozone/src/lexicon/types/app/bsky/actor/defs.ts index 5f1a06f10b7..555fa037ee7 100644 --- a/packages/ozone/src/lexicon/types/app/bsky/actor/defs.ts +++ b/packages/ozone/src/lexicon/types/app/bsky/actor/defs.ts @@ -375,8 +375,8 @@ export interface MutedWord { value: string /** The intended targets of the muted word. */ targets: MutedWordTarget[] - /** The accounts for which this muted word applies. */ - actors?: string[] + /** Groups of users to apply the muted word to. If undefined, applies to all users. */ + actorTarget?: 'all' | 'exclude-following' | (string & {}) /** The date and time at which the muted word will expire and no longer be applied. */ expiresAt?: string [k: string]: unknown diff --git a/packages/pds/src/lexicon/lexicons.ts b/packages/pds/src/lexicon/lexicons.ts index bb10ba58bc3..d05148b9d6f 100644 --- a/packages/pds/src/lexicon/lexicons.ts +++ b/packages/pds/src/lexicon/lexicons.ts @@ -4358,13 +4358,11 @@ export const schemaDict = { ref: 'lex:app.bsky.actor.defs#mutedWordTarget', }, }, - actors: { - type: 'array', - description: 'The accounts for which this muted word applies.', - items: { - type: 'string', - format: 'did', - }, + actorTarget: { + type: 'string', + description: + 'Groups of users to apply the muted word to. If undefined, applies to all users.', + knownValues: ['all', 'exclude-following'], }, expiresAt: { type: 'string', diff --git a/packages/pds/src/lexicon/types/app/bsky/actor/defs.ts b/packages/pds/src/lexicon/types/app/bsky/actor/defs.ts index 5f1a06f10b7..555fa037ee7 100644 --- a/packages/pds/src/lexicon/types/app/bsky/actor/defs.ts +++ b/packages/pds/src/lexicon/types/app/bsky/actor/defs.ts @@ -375,8 +375,8 @@ export interface MutedWord { value: string /** The intended targets of the muted word. */ targets: MutedWordTarget[] - /** The accounts for which this muted word applies. */ - actors?: string[] + /** Groups of users to apply the muted word to. If undefined, applies to all users. */ + actorTarget?: 'all' | 'exclude-following' | (string & {}) /** The date and time at which the muted word will expire and no longer be applied. */ expiresAt?: string [k: string]: unknown From 90e8343f6037a18e1222d24a41ed58cf4da54306 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Fri, 19 Jul 2024 14:00:25 -0700 Subject: [PATCH 15/22] Remove fake timers (cherry picked from commit ad31910560ce938e3ff64944d46355c64635ebf8) --- packages/api/tests/moderation-mutewords.test.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/packages/api/tests/moderation-mutewords.test.ts b/packages/api/tests/moderation-mutewords.test.ts index 3ccb77b5c74..2bdd8adec5c 100644 --- a/packages/api/tests/moderation-mutewords.test.ts +++ b/packages/api/tests/moderation-mutewords.test.ts @@ -769,7 +769,6 @@ describe(`hasMutedWord`, () => { describe(`timed mute words`, () => { it(`non-expired word`, () => { - jest.useFakeTimers() const now = Date.now() const res = moderatePost( @@ -803,12 +802,9 @@ describe(`hasMutedWord`, () => { ) expect(res.causes[0].type).toBe('mute-word') - - jest.useRealTimers() }) it(`expired word`, () => { - jest.useFakeTimers() const now = Date.now() const res = moderatePost( @@ -842,8 +838,6 @@ describe(`hasMutedWord`, () => { ) expect(res.causes.length).toBe(0) - - jest.useRealTimers() }) }) From 424df67cddad5b03d510f9a4bac99a3f24aac4b8 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Fri, 19 Jul 2024 14:08:07 -0700 Subject: [PATCH 16/22] Update changeset --- .changeset/rotten-moose-switch.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/rotten-moose-switch.md b/.changeset/rotten-moose-switch.md index 5736de3b224..0104fc08ec6 100644 --- a/.changeset/rotten-moose-switch.md +++ b/.changeset/rotten-moose-switch.md @@ -5,4 +5,4 @@ '@atproto/pds': patch --- -Updates muted words lexicons to include new attributes `id`, `actors`, and `expiresAt`. Adds and updates methods in API SDK for better management of muted words. +Updates muted words lexicons to include new attributes `id`, `actorTarget`, and `expiresAt`. Adds and updates methods in API SDK for better management of muted words. From 194a1fd9b36873fa1197ba3512b0d51770cd3388 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Fri, 19 Jul 2024 15:21:18 -0700 Subject: [PATCH 17/22] Prevent deleting value when updating --- packages/api/src/bsky-agent.ts | 10 ++++++++-- packages/api/tests/bsky-agent.test.ts | 23 +++++++++++++++++++++++ 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/packages/api/src/bsky-agent.ts b/packages/api/src/bsky-agent.ts index add6820e9aa..859f7d5a9b7 100644 --- a/packages/api/src/bsky-agent.ts +++ b/packages/api/src/bsky-agent.ts @@ -1000,7 +1000,12 @@ export class BskyAgent extends AtpAgent { /** * @deprecated use `addMutedWords` or `addMutedWord` instead */ - async upsertMutedWords(mutedWords: AppBskyActorDefs.MutedWord[]) { + async upsertMutedWords( + mutedWords: Pick< + MutedWord, + 'value' | 'targets' | 'actorTarget' | 'expiresAt' + >[], + ) { await this.addMutedWords(mutedWords) } @@ -1026,7 +1031,8 @@ export class BskyAgent extends AtpAgent { } return { id: existingItem.id || TID.nextStr(), - value: sanitizeMutedWordValue(updated.value), + value: + sanitizeMutedWordValue(updated.value) || existingItem.value, targets: updated.targets || [], actorTarget: updated.actorTarget || 'all', expiresAt: updated.expiresAt || undefined, diff --git a/packages/api/tests/bsky-agent.test.ts b/packages/api/tests/bsky-agent.test.ts index 0fd2240e826..a688a7e8f0e 100644 --- a/packages/api/tests/bsky-agent.test.ts +++ b/packages/api/tests/bsky-agent.test.ts @@ -2024,6 +2024,29 @@ describe('agent', () => { b.moderationPrefs.mutedWords.find((m) => m.id === word!.id), ).toHaveProperty('expiresAt', expiresAt2) }) + + it(`doesn't update if value is sanitized to be falsy`, async () => { + await agent.addMutedWord({ + value: 'rug', + targets: ['content'], + }) + + const a = await agent.getPreferences() + const word = a.moderationPrefs.mutedWords.find( + (m) => m.value === 'rug', + ) + + await agent.updateMutedWord({ + ...word!, + value: '', + }) + + const b = await agent.getPreferences() + + expect( + b.moderationPrefs.mutedWords.find((m) => m.id === word!.id), + ).toHaveProperty('value', 'rug') + }) }) describe('removeMutedWord', () => { From df20225622b650d3bc080637b8e773ce621656e0 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Tue, 23 Jul 2024 14:07:10 -0500 Subject: [PATCH 18/22] Include missing test --- packages/api/tests/bsky-agent.test.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/api/tests/bsky-agent.test.ts b/packages/api/tests/bsky-agent.test.ts index a688a7e8f0e..a78b429126e 100644 --- a/packages/api/tests/bsky-agent.test.ts +++ b/packages/api/tests/bsky-agent.test.ts @@ -1835,6 +1835,13 @@ describe('agent', () => { ).toBeTruthy() }) + it(`includes apostrophes e.g. Bluesky's`, async () => { + await agent.addMutedWord({ value: `Bluesky's`, targets: [] }) + const { mutedWords } = (await agent.getPreferences()).moderationPrefs + + expect(mutedWords.find((m) => m.value === `Bluesky's`)).toBeTruthy() + }) + describe(`invalid characters`, () => { it('#, no insert', async () => { await agent.addMutedWord({ value: '#​', targets: [] }) From c3124bb33b400ee1b138194a2bce71faff6d393b Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Fri, 26 Jul 2024 09:41:33 -0500 Subject: [PATCH 19/22] Add default --- lexicons/app/bsky/actor/defs.json | 3 ++- packages/api/src/client/lexicons.ts | 1 + packages/api/src/client/types/app/bsky/actor/defs.ts | 2 +- packages/bsky/src/lexicon/lexicons.ts | 1 + packages/bsky/src/lexicon/types/app/bsky/actor/defs.ts | 2 +- packages/ozone/src/lexicon/lexicons.ts | 1 + packages/ozone/src/lexicon/types/app/bsky/actor/defs.ts | 2 +- packages/pds/src/lexicon/lexicons.ts | 1 + packages/pds/src/lexicon/types/app/bsky/actor/defs.ts | 2 +- 9 files changed, 10 insertions(+), 5 deletions(-) diff --git a/lexicons/app/bsky/actor/defs.json b/lexicons/app/bsky/actor/defs.json index 07e42daef2c..6ba7aaa734a 100644 --- a/lexicons/app/bsky/actor/defs.json +++ b/lexicons/app/bsky/actor/defs.json @@ -348,7 +348,8 @@ "actorTarget": { "type": "string", "description": "Groups of users to apply the muted word to. If undefined, applies to all users.", - "knownValues": ["all", "exclude-following"] + "knownValues": ["all", "exclude-following"], + "default": "all" }, "expiresAt": { "type": "string", diff --git a/packages/api/src/client/lexicons.ts b/packages/api/src/client/lexicons.ts index d05148b9d6f..fedcb619b52 100644 --- a/packages/api/src/client/lexicons.ts +++ b/packages/api/src/client/lexicons.ts @@ -4363,6 +4363,7 @@ export const schemaDict = { description: 'Groups of users to apply the muted word to. If undefined, applies to all users.', knownValues: ['all', 'exclude-following'], + default: 'all', }, expiresAt: { type: 'string', diff --git a/packages/api/src/client/types/app/bsky/actor/defs.ts b/packages/api/src/client/types/app/bsky/actor/defs.ts index 6595811f9b7..d6c0de137b0 100644 --- a/packages/api/src/client/types/app/bsky/actor/defs.ts +++ b/packages/api/src/client/types/app/bsky/actor/defs.ts @@ -376,7 +376,7 @@ export interface MutedWord { /** The intended targets of the muted word. */ targets: MutedWordTarget[] /** Groups of users to apply the muted word to. If undefined, applies to all users. */ - actorTarget?: 'all' | 'exclude-following' | (string & {}) + actorTarget: 'all' | 'exclude-following' | (string & {}) /** The date and time at which the muted word will expire and no longer be applied. */ expiresAt?: string [k: string]: unknown diff --git a/packages/bsky/src/lexicon/lexicons.ts b/packages/bsky/src/lexicon/lexicons.ts index 2e35e507879..395ac581b61 100644 --- a/packages/bsky/src/lexicon/lexicons.ts +++ b/packages/bsky/src/lexicon/lexicons.ts @@ -4363,6 +4363,7 @@ export const schemaDict = { description: 'Groups of users to apply the muted word to. If undefined, applies to all users.', knownValues: ['all', 'exclude-following'], + default: 'all', }, expiresAt: { type: 'string', diff --git a/packages/bsky/src/lexicon/types/app/bsky/actor/defs.ts b/packages/bsky/src/lexicon/types/app/bsky/actor/defs.ts index 555fa037ee7..c7eadff70d7 100644 --- a/packages/bsky/src/lexicon/types/app/bsky/actor/defs.ts +++ b/packages/bsky/src/lexicon/types/app/bsky/actor/defs.ts @@ -376,7 +376,7 @@ export interface MutedWord { /** The intended targets of the muted word. */ targets: MutedWordTarget[] /** Groups of users to apply the muted word to. If undefined, applies to all users. */ - actorTarget?: 'all' | 'exclude-following' | (string & {}) + actorTarget: 'all' | 'exclude-following' | (string & {}) /** The date and time at which the muted word will expire and no longer be applied. */ expiresAt?: string [k: string]: unknown diff --git a/packages/ozone/src/lexicon/lexicons.ts b/packages/ozone/src/lexicon/lexicons.ts index d05148b9d6f..fedcb619b52 100644 --- a/packages/ozone/src/lexicon/lexicons.ts +++ b/packages/ozone/src/lexicon/lexicons.ts @@ -4363,6 +4363,7 @@ export const schemaDict = { description: 'Groups of users to apply the muted word to. If undefined, applies to all users.', knownValues: ['all', 'exclude-following'], + default: 'all', }, expiresAt: { type: 'string', diff --git a/packages/ozone/src/lexicon/types/app/bsky/actor/defs.ts b/packages/ozone/src/lexicon/types/app/bsky/actor/defs.ts index 555fa037ee7..c7eadff70d7 100644 --- a/packages/ozone/src/lexicon/types/app/bsky/actor/defs.ts +++ b/packages/ozone/src/lexicon/types/app/bsky/actor/defs.ts @@ -376,7 +376,7 @@ export interface MutedWord { /** The intended targets of the muted word. */ targets: MutedWordTarget[] /** Groups of users to apply the muted word to. If undefined, applies to all users. */ - actorTarget?: 'all' | 'exclude-following' | (string & {}) + actorTarget: 'all' | 'exclude-following' | (string & {}) /** The date and time at which the muted word will expire and no longer be applied. */ expiresAt?: string [k: string]: unknown diff --git a/packages/pds/src/lexicon/lexicons.ts b/packages/pds/src/lexicon/lexicons.ts index d05148b9d6f..fedcb619b52 100644 --- a/packages/pds/src/lexicon/lexicons.ts +++ b/packages/pds/src/lexicon/lexicons.ts @@ -4363,6 +4363,7 @@ export const schemaDict = { description: 'Groups of users to apply the muted word to. If undefined, applies to all users.', knownValues: ['all', 'exclude-following'], + default: 'all', }, expiresAt: { type: 'string', diff --git a/packages/pds/src/lexicon/types/app/bsky/actor/defs.ts b/packages/pds/src/lexicon/types/app/bsky/actor/defs.ts index 555fa037ee7..c7eadff70d7 100644 --- a/packages/pds/src/lexicon/types/app/bsky/actor/defs.ts +++ b/packages/pds/src/lexicon/types/app/bsky/actor/defs.ts @@ -376,7 +376,7 @@ export interface MutedWord { /** The intended targets of the muted word. */ targets: MutedWordTarget[] /** Groups of users to apply the muted word to. If undefined, applies to all users. */ - actorTarget?: 'all' | 'exclude-following' | (string & {}) + actorTarget: 'all' | 'exclude-following' | (string & {}) /** The date and time at which the muted word will expire and no longer be applied. */ expiresAt?: string [k: string]: unknown From ef736ccc03683911cfe95b28adcb9e725a65c680 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Fri, 26 Jul 2024 09:49:00 -0500 Subject: [PATCH 20/22] Apply default 'all' value to existing mute words to satisfy Typescript --- packages/api/src/bsky-agent.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/api/src/bsky-agent.ts b/packages/api/src/bsky-agent.ts index 859f7d5a9b7..b32390f6f66 100644 --- a/packages/api/src/bsky-agent.ts +++ b/packages/api/src/bsky-agent.ts @@ -478,6 +478,14 @@ export class BskyAgent extends AtpAgent { // eslint-disable-next-line @typescript-eslint/no-unused-vars const { $type, ...v } = pref prefs.moderationPrefs.mutedWords = v.items + + if (prefs.moderationPrefs.mutedWords.length) { + prefs.moderationPrefs.mutedWords = + prefs.moderationPrefs.mutedWords.map((word) => { + word.actorTarget = word.actorTarget || 'all' + return word + }) + } } else if ( AppBskyActorDefs.isHiddenPostsPref(pref) && AppBskyActorDefs.validateHiddenPostsPref(pref).success From e59c3d159090f499fc4fde9802163fa9c74b9656 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Fri, 26 Jul 2024 10:08:32 -0500 Subject: [PATCH 21/22] Fix types in tests --- packages/api/tests/bsky-agent.test.ts | 131 +++++++++-- .../api/tests/moderation-mutewords.test.ts | 216 ++++++++++++++---- 2 files changed, 274 insertions(+), 73 deletions(-) diff --git a/packages/api/tests/bsky-agent.test.ts b/packages/api/tests/bsky-agent.test.ts index a78b429126e..925c06e542a 100644 --- a/packages/api/tests/bsky-agent.test.ts +++ b/packages/api/tests/bsky-agent.test.ts @@ -1788,7 +1788,11 @@ describe('agent', () => { }) it('single-hash #, no insert', async () => { - await agent.addMutedWord({ value: '#', targets: [] }) + await agent.addMutedWord({ + value: '#', + targets: [], + actorTarget: 'all', + }) const { moderationPrefs } = await agent.getPreferences() // sanitized to empty string, not inserted @@ -1796,7 +1800,11 @@ describe('agent', () => { }) it('multi-hash ##, inserts #', async () => { - await agent.addMutedWord({ value: '##', targets: [] }) + await agent.addMutedWord({ + value: '##', + targets: [], + actorTarget: 'all', + }) const { moderationPrefs } = await agent.getPreferences() expect( moderationPrefs.mutedWords.find((m) => m.value === '#'), @@ -1804,7 +1812,11 @@ describe('agent', () => { }) it('multi-hash ##hashtag, inserts #hashtag', async () => { - await agent.addMutedWord({ value: '##hashtag', targets: [] }) + await agent.addMutedWord({ + value: '##hashtag', + targets: [], + actorTarget: 'all', + }) const { moderationPrefs } = await agent.getPreferences() expect( moderationPrefs.mutedWords.find((w) => w.value === '#hashtag'), @@ -1812,7 +1824,11 @@ describe('agent', () => { }) it('hash emoji #️⃣, inserts #️⃣', async () => { - await agent.addMutedWord({ value: '#️⃣', targets: [] }) + await agent.addMutedWord({ + value: '#️⃣', + targets: [], + actorTarget: 'all', + }) const { moderationPrefs } = await agent.getPreferences() expect( moderationPrefs.mutedWords.find((m) => m.value === '#️⃣'), @@ -1820,7 +1836,11 @@ describe('agent', () => { }) it('hash emoji w/leading hash ##️⃣, inserts #️⃣', async () => { - await agent.addMutedWord({ value: '##️⃣', targets: [] }) + await agent.addMutedWord({ + value: '##️⃣', + targets: [], + actorTarget: 'all', + }) const { moderationPrefs } = await agent.getPreferences() expect( moderationPrefs.mutedWords.find((m) => m.value === '#️⃣'), @@ -1828,7 +1848,11 @@ describe('agent', () => { }) it('hash emoji with double leading hash ###️⃣, inserts ##️⃣', async () => { - await agent.addMutedWord({ value: '###️⃣', targets: [] }) + await agent.addMutedWord({ + value: '###️⃣', + targets: [], + actorTarget: 'all', + }) const { moderationPrefs } = await agent.getPreferences() expect( moderationPrefs.mutedWords.find((m) => m.value === '##️⃣'), @@ -1836,7 +1860,11 @@ describe('agent', () => { }) it(`includes apostrophes e.g. Bluesky's`, async () => { - await agent.addMutedWord({ value: `Bluesky's`, targets: [] }) + await agent.addMutedWord({ + value: `Bluesky's`, + targets: [], + actorTarget: 'all', + }) const { mutedWords } = (await agent.getPreferences()).moderationPrefs expect(mutedWords.find((m) => m.value === `Bluesky's`)).toBeTruthy() @@ -1844,13 +1872,21 @@ describe('agent', () => { describe(`invalid characters`, () => { it('#, no insert', async () => { - await agent.addMutedWord({ value: '#​', targets: [] }) + await agent.addMutedWord({ + value: '#​', + targets: [], + actorTarget: 'all', + }) const { moderationPrefs } = await agent.getPreferences() expect(moderationPrefs.mutedWords.length).toEqual(0) }) it('#ab, inserts ab', async () => { - await agent.addMutedWord({ value: '#​ab', targets: [] }) + await agent.addMutedWord({ + value: '#​ab', + targets: [], + actorTarget: 'all', + }) const { moderationPrefs } = await agent.getPreferences() expect(moderationPrefs.mutedWords.length).toEqual(1) }) @@ -1859,6 +1895,7 @@ describe('agent', () => { await agent.addMutedWord({ value: 'test value\n with newline', targets: [], + actorTarget: 'all', }) const { moderationPrefs } = await agent.getPreferences() expect( @@ -1872,6 +1909,7 @@ describe('agent', () => { await agent.addMutedWord({ value: 'test value\n\r with newline', targets: [], + actorTarget: 'all', }) const { moderationPrefs } = await agent.getPreferences() expect( @@ -1882,13 +1920,21 @@ describe('agent', () => { }) it('empty space, no insert', async () => { - await agent.addMutedWord({ value: ' ', targets: [] }) + await agent.addMutedWord({ + value: ' ', + targets: [], + actorTarget: 'all', + }) const { moderationPrefs } = await agent.getPreferences() expect(moderationPrefs.mutedWords.length).toEqual(0) }) it(`' trim ', inserts 'trim'`, async () => { - await agent.addMutedWord({ value: ' trim ', targets: [] }) + await agent.addMutedWord({ + value: ' trim ', + targets: [], + actorTarget: 'all', + }) const { moderationPrefs } = await agent.getPreferences() expect( moderationPrefs.mutedWords.find((m) => m.value === 'trim'), @@ -1900,9 +1946,9 @@ describe('agent', () => { describe('addMutedWords', () => { it('inserts happen sequentially, no clobbering', async () => { await agent.addMutedWords([ - { value: 'a', targets: ['content'] }, - { value: 'b', targets: ['content'] }, - { value: 'c', targets: ['content'] }, + { value: 'a', targets: ['content'], actorTarget: 'all' }, + { value: 'b', targets: ['content'], actorTarget: 'all' }, + { value: 'c', targets: ['content'], actorTarget: 'all' }, ]) const { moderationPrefs } = await agent.getPreferences() @@ -1914,9 +1960,11 @@ describe('agent', () => { describe('upsertMutedWords (deprecated)', () => { it('no longer upserts, calls addMutedWords', async () => { await agent.upsertMutedWords([ - { value: 'both', targets: ['content'] }, + { value: 'both', targets: ['content'], actorTarget: 'all' }, + ]) + await agent.upsertMutedWords([ + { value: 'both', targets: ['tag'], actorTarget: 'all' }, ]) - await agent.upsertMutedWords([{ value: 'both', targets: ['tag'] }]) const { moderationPrefs } = await agent.getPreferences() @@ -1929,6 +1977,7 @@ describe('agent', () => { await agent.updateMutedWord({ value: 'word', targets: ['tag', 'content'], + actorTarget: 'all', }) const { moderationPrefs } = await agent.getPreferences() expect(moderationPrefs.mutedWords.length).toEqual(0) @@ -1938,6 +1987,7 @@ describe('agent', () => { await agent.addMutedWord({ value: 'value', targets: ['content'], + actorTarget: 'all', }) const a = await agent.getPreferences() @@ -1963,6 +2013,7 @@ describe('agent', () => { await agent.addMutedWord({ value: 'word', targets: ['tag'], + actorTarget: 'all', }) const a = await agent.getPreferences() @@ -2013,6 +2064,7 @@ describe('agent', () => { value: 'value', targets: ['content'], expiresAt, + actorTarget: 'all', }) const a = await agent.getPreferences() @@ -2036,6 +2088,7 @@ describe('agent', () => { await agent.addMutedWord({ value: 'rug', targets: ['content'], + actorTarget: 'all', }) const a = await agent.getPreferences() @@ -2058,7 +2111,11 @@ describe('agent', () => { describe('removeMutedWord', () => { it('removes word', async () => { - await agent.addMutedWord({ value: 'word', targets: ['tag'] }) + await agent.addMutedWord({ + value: 'word', + targets: ['tag'], + actorTarget: 'all', + }) const a = await agent.getPreferences() const word = a.moderationPrefs.mutedWords.find( (m) => m.value === 'word', @@ -2074,13 +2131,21 @@ describe('agent', () => { }) it(`word doesn't exist, no action`, async () => { - await agent.addMutedWord({ value: 'word', targets: ['tag'] }) + await agent.addMutedWord({ + value: 'word', + targets: ['tag'], + actorTarget: 'all', + }) const a = await agent.getPreferences() const word = a.moderationPrefs.mutedWords.find( (m) => m.value === 'word', ) - await agent.removeMutedWord({ value: 'another', targets: [] }) + await agent.removeMutedWord({ + value: 'another', + targets: [], + actorTarget: 'all', + }) const b = await agent.getPreferences() @@ -2093,9 +2158,9 @@ describe('agent', () => { describe('removeMutedWords', () => { it(`removes sequentially, no clobbering`, async () => { await agent.addMutedWords([ - { value: 'a', targets: ['content'] }, - { value: 'b', targets: ['content'] }, - { value: 'c', targets: ['content'] }, + { value: 'a', targets: ['content'], actorTarget: 'all ' }, + { value: 'b', targets: ['content'], actorTarget: 'all ' }, + { value: 'c', targets: ['content'], actorTarget: 'all ' }, ]) const a = await agent.getPreferences() @@ -2137,6 +2202,7 @@ describe('agent', () => { const newMutedWord: AppBskyActorDefs.MutedWord = { value: mutedWord.value, targets: mutedWord.targets, + actorTarget: 'all', } if ( @@ -2181,6 +2247,7 @@ describe('agent', () => { await addLegacyMutedWord({ value: 'word', targets: ['content'], + actorTarget: 'all', }) { @@ -2192,7 +2259,9 @@ describe('agent', () => { expect(word!.id).toBeFalsy() } - await agent.upsertMutedWords([{ value: 'word2', targets: ['tag'] }]) + await agent.upsertMutedWords([ + { value: 'word2', targets: ['tag'], actorTarget: 'all' }, + ]) { const { moderationPrefs } = await agent.getPreferences() @@ -2214,13 +2283,19 @@ describe('agent', () => { await addLegacyMutedWord({ value: 'word', targets: ['content'], + actorTarget: 'all', }) await addLegacyMutedWord({ value: 'word2', targets: ['tag'], + actorTarget: 'all', }) - await agent.updateMutedWord({ value: 'word', targets: ['tag'] }) + await agent.updateMutedWord({ + value: 'word', + targets: ['tag'], + actorTarget: 'all', + }) { const { moderationPrefs } = await agent.getPreferences() @@ -2244,13 +2319,19 @@ describe('agent', () => { await addLegacyMutedWord({ value: 'word', targets: ['content'], + actorTarget: 'all', }) await addLegacyMutedWord({ value: 'word2', targets: ['tag'], + actorTarget: 'all', }) - await agent.removeMutedWord({ value: 'word', targets: ['tag'] }) + await agent.removeMutedWord({ + value: 'word', + targets: ['tag'], + actorTarget: 'all', + }) { const { moderationPrefs } = await agent.getPreferences() diff --git a/packages/api/tests/moderation-mutewords.test.ts b/packages/api/tests/moderation-mutewords.test.ts index 2bdd8adec5c..26de3fe9872 100644 --- a/packages/api/tests/moderation-mutewords.test.ts +++ b/packages/api/tests/moderation-mutewords.test.ts @@ -11,7 +11,9 @@ describe(`hasMutedWord`, () => { rt.detectFacetsWithoutResolution() const match = hasMutedWord({ - mutedWords: [{ value: 'outlineTag', targets: ['tag'] }], + mutedWords: [ + { value: 'outlineTag', targets: ['tag'], actorTarget: 'all' }, + ], text: rt.text, facets: rt.facets, outlineTags: ['outlineTag'], @@ -27,7 +29,9 @@ describe(`hasMutedWord`, () => { rt.detectFacetsWithoutResolution() const match = hasMutedWord({ - mutedWords: [{ value: 'inlineTag', targets: ['tag'] }], + mutedWords: [ + { value: 'inlineTag', targets: ['tag'], actorTarget: 'all' }, + ], text: rt.text, facets: rt.facets, outlineTags: ['outlineTag'], @@ -43,7 +47,9 @@ describe(`hasMutedWord`, () => { rt.detectFacetsWithoutResolution() const match = hasMutedWord({ - mutedWords: [{ value: 'inlineTag', targets: ['content'] }], + mutedWords: [ + { value: 'inlineTag', targets: ['content'], actorTarget: 'all' }, + ], text: rt.text, facets: rt.facets, outlineTags: ['outlineTag'], @@ -59,7 +65,9 @@ describe(`hasMutedWord`, () => { rt.detectFacetsWithoutResolution() const match = hasMutedWord({ - mutedWords: [{ value: 'inlineTag', targets: ['tag'] }], + mutedWords: [ + { value: 'inlineTag', targets: ['tag'], actorTarget: 'all' }, + ], text: rt.text, facets: rt.facets, outlineTags: [], @@ -80,7 +88,7 @@ describe(`hasMutedWord`, () => { rt.detectFacetsWithoutResolution() const match = hasMutedWord({ - mutedWords: [{ value: '希', targets: ['content'] }], + mutedWords: [{ value: '希', targets: ['content'], actorTarget: 'all' }], text: rt.text, facets: rt.facets, outlineTags: [], @@ -96,7 +104,9 @@ describe(`hasMutedWord`, () => { rt.detectFacetsWithoutResolution() const match = hasMutedWord({ - mutedWords: [{ value: '☠︎', targets: ['content'] }], + mutedWords: [ + { value: '☠︎', targets: ['content'], actorTarget: 'all' }, + ], text: rt.text, facets: rt.facets, outlineTags: [], @@ -112,7 +122,9 @@ describe(`hasMutedWord`, () => { rt.detectFacetsWithoutResolution() const match = hasMutedWord({ - mutedWords: [{ value: 'politics', targets: ['content'] }], + mutedWords: [ + { value: 'politics', targets: ['content'], actorTarget: 'all' }, + ], text: rt.text, facets: rt.facets, outlineTags: [], @@ -128,7 +140,9 @@ describe(`hasMutedWord`, () => { rt.detectFacetsWithoutResolution() const match = hasMutedWord({ - mutedWords: [{ value: 'javascript', targets: ['content'] }], + mutedWords: [ + { value: 'javascript', targets: ['content'], actorTarget: 'all' }, + ], text: rt.text, facets: rt.facets, outlineTags: [], @@ -146,7 +160,9 @@ describe(`hasMutedWord`, () => { rt.detectFacetsWithoutResolution() const match = hasMutedWord({ - mutedWords: [{ value: 'javascript', targets: ['content'] }], + mutedWords: [ + { value: 'javascript', targets: ['content'], actorTarget: 'all' }, + ], text: rt.text, facets: rt.facets, outlineTags: [], @@ -162,7 +178,7 @@ describe(`hasMutedWord`, () => { rt.detectFacetsWithoutResolution() const match = hasMutedWord({ - mutedWords: [{ value: 'ai', targets: ['content'] }], + mutedWords: [{ value: 'ai', targets: ['content'], actorTarget: 'all' }], text: rt.text, facets: rt.facets, outlineTags: [], @@ -178,7 +194,9 @@ describe(`hasMutedWord`, () => { rt.detectFacetsWithoutResolution() const match = hasMutedWord({ - mutedWords: [{ value: 'brain', targets: ['content'] }], + mutedWords: [ + { value: 'brain', targets: ['content'], actorTarget: 'all' }, + ], text: rt.text, facets: rt.facets, outlineTags: [], @@ -194,7 +212,7 @@ describe(`hasMutedWord`, () => { rt.detectFacetsWithoutResolution() const match = hasMutedWord({ - mutedWords: [{ value: `:)`, targets: ['content'] }], + mutedWords: [{ value: `:)`, targets: ['content'], actorTarget: 'all' }], text: rt.text, facets: rt.facets, outlineTags: [], @@ -213,7 +231,9 @@ describe(`hasMutedWord`, () => { it(`match: yay!`, () => { const match = hasMutedWord({ - mutedWords: [{ value: 'yay!', targets: ['content'] }], + mutedWords: [ + { value: 'yay!', targets: ['content'], actorTarget: 'all' }, + ], text: rt.text, facets: rt.facets, outlineTags: [], @@ -224,7 +244,9 @@ describe(`hasMutedWord`, () => { it(`match: yay`, () => { const match = hasMutedWord({ - mutedWords: [{ value: 'yay', targets: ['content'] }], + mutedWords: [ + { value: 'yay', targets: ['content'], actorTarget: 'all' }, + ], text: rt.text, facets: rt.facets, outlineTags: [], @@ -242,7 +264,9 @@ describe(`hasMutedWord`, () => { it(`match: y!ppee`, () => { const match = hasMutedWord({ - mutedWords: [{ value: 'y!ppee', targets: ['content'] }], + mutedWords: [ + { value: 'y!ppee', targets: ['content'], actorTarget: 'all' }, + ], text: rt.text, facets: rt.facets, outlineTags: [], @@ -254,7 +278,9 @@ describe(`hasMutedWord`, () => { // single exclamation point, source has double it(`no match: y!ppee!`, () => { const match = hasMutedWord({ - mutedWords: [{ value: 'y!ppee!', targets: ['content'] }], + mutedWords: [ + { value: 'y!ppee!', targets: ['content'], actorTarget: 'all' }, + ], text: rt.text, facets: rt.facets, outlineTags: [], @@ -272,7 +298,9 @@ describe(`hasMutedWord`, () => { it(`match: Bluesky's`, () => { const match = hasMutedWord({ - mutedWords: [{ value: `Bluesky's`, targets: ['content'] }], + mutedWords: [ + { value: `Bluesky's`, targets: ['content'], actorTarget: 'all' }, + ], text: rt.text, facets: rt.facets, outlineTags: [], @@ -283,7 +311,9 @@ describe(`hasMutedWord`, () => { it(`match: Bluesky`, () => { const match = hasMutedWord({ - mutedWords: [{ value: 'Bluesky', targets: ['content'] }], + mutedWords: [ + { value: 'Bluesky', targets: ['content'], actorTarget: 'all' }, + ], text: rt.text, facets: rt.facets, outlineTags: [], @@ -294,7 +324,9 @@ describe(`hasMutedWord`, () => { it(`match: bluesky`, () => { const match = hasMutedWord({ - mutedWords: [{ value: 'bluesky', targets: ['content'] }], + mutedWords: [ + { value: 'bluesky', targets: ['content'], actorTarget: 'all' }, + ], text: rt.text, facets: rt.facets, outlineTags: [], @@ -305,7 +337,9 @@ describe(`hasMutedWord`, () => { it(`match: blueskys`, () => { const match = hasMutedWord({ - mutedWords: [{ value: 'blueskys', targets: ['content'] }], + mutedWords: [ + { value: 'blueskys', targets: ['content'], actorTarget: 'all' }, + ], text: rt.text, facets: rt.facets, outlineTags: [], @@ -323,7 +357,9 @@ describe(`hasMutedWord`, () => { it(`match: S@assy`, () => { const match = hasMutedWord({ - mutedWords: [{ value: 'S@assy', targets: ['content'] }], + mutedWords: [ + { value: 'S@assy', targets: ['content'], actorTarget: 'all' }, + ], text: rt.text, facets: rt.facets, outlineTags: [], @@ -334,7 +370,9 @@ describe(`hasMutedWord`, () => { it(`match: s@assy`, () => { const match = hasMutedWord({ - mutedWords: [{ value: 's@assy', targets: ['content'] }], + mutedWords: [ + { value: 's@assy', targets: ['content'], actorTarget: 'all' }, + ], text: rt.text, facets: rt.facets, outlineTags: [], @@ -353,7 +391,13 @@ describe(`hasMutedWord`, () => { // case insensitive it(`match: new york times`, () => { const match = hasMutedWord({ - mutedWords: [{ value: 'new york times', targets: ['content'] }], + mutedWords: [ + { + value: 'new york times', + targets: ['content'], + actorTarget: 'all', + }, + ], text: rt.text, facets: rt.facets, outlineTags: [], @@ -371,7 +415,9 @@ describe(`hasMutedWord`, () => { it(`match: !command`, () => { const match = hasMutedWord({ - mutedWords: [{ value: `!command`, targets: ['content'] }], + mutedWords: [ + { value: `!command`, targets: ['content'], actorTarget: 'all' }, + ], text: rt.text, facets: rt.facets, outlineTags: [], @@ -382,7 +428,9 @@ describe(`hasMutedWord`, () => { it(`match: command`, () => { const match = hasMutedWord({ - mutedWords: [{ value: `command`, targets: ['content'] }], + mutedWords: [ + { value: `command`, targets: ['content'], actorTarget: 'all' }, + ], text: rt.text, facets: rt.facets, outlineTags: [], @@ -398,7 +446,9 @@ describe(`hasMutedWord`, () => { rt.detectFacetsWithoutResolution() const match = hasMutedWord({ - mutedWords: [{ value: `!command`, targets: ['content'] }], + mutedWords: [ + { value: `!command`, targets: ['content'], actorTarget: 'all' }, + ], text: rt.text, facets: rt.facets, outlineTags: [], @@ -416,7 +466,9 @@ describe(`hasMutedWord`, () => { it(`match: e/acc`, () => { const match = hasMutedWord({ - mutedWords: [{ value: `e/acc`, targets: ['content'] }], + mutedWords: [ + { value: `e/acc`, targets: ['content'], actorTarget: 'all' }, + ], text: rt.text, facets: rt.facets, outlineTags: [], @@ -427,7 +479,9 @@ describe(`hasMutedWord`, () => { it(`match: acc`, () => { const match = hasMutedWord({ - mutedWords: [{ value: `acc`, targets: ['content'] }], + mutedWords: [ + { value: `acc`, targets: ['content'], actorTarget: 'all' }, + ], text: rt.text, facets: rt.facets, outlineTags: [], @@ -445,7 +499,9 @@ describe(`hasMutedWord`, () => { it(`match: super-bad`, () => { const match = hasMutedWord({ - mutedWords: [{ value: `super-bad`, targets: ['content'] }], + mutedWords: [ + { value: `super-bad`, targets: ['content'], actorTarget: 'all' }, + ], text: rt.text, facets: rt.facets, outlineTags: [], @@ -456,7 +512,9 @@ describe(`hasMutedWord`, () => { it(`match: super`, () => { const match = hasMutedWord({ - mutedWords: [{ value: `super`, targets: ['content'] }], + mutedWords: [ + { value: `super`, targets: ['content'], actorTarget: 'all' }, + ], text: rt.text, facets: rt.facets, outlineTags: [], @@ -467,7 +525,9 @@ describe(`hasMutedWord`, () => { it(`match: bad`, () => { const match = hasMutedWord({ - mutedWords: [{ value: `bad`, targets: ['content'] }], + mutedWords: [ + { value: `bad`, targets: ['content'], actorTarget: 'all' }, + ], text: rt.text, facets: rt.facets, outlineTags: [], @@ -478,7 +538,9 @@ describe(`hasMutedWord`, () => { it(`match: super bad`, () => { const match = hasMutedWord({ - mutedWords: [{ value: `super bad`, targets: ['content'] }], + mutedWords: [ + { value: `super bad`, targets: ['content'], actorTarget: 'all' }, + ], text: rt.text, facets: rt.facets, outlineTags: [], @@ -489,7 +551,9 @@ describe(`hasMutedWord`, () => { it(`match: superbad`, () => { const match = hasMutedWord({ - mutedWords: [{ value: `superbad`, targets: ['content'] }], + mutedWords: [ + { value: `superbad`, targets: ['content'], actorTarget: 'all' }, + ], text: rt.text, facets: rt.facets, outlineTags: [], @@ -508,7 +572,11 @@ describe(`hasMutedWord`, () => { it(`match: idk what this would be`, () => { const match = hasMutedWord({ mutedWords: [ - { value: `idk what this would be`, targets: ['content'] }, + { + value: `idk what this would be`, + targets: ['content'], + actorTarget: 'all', + }, ], text: rt.text, facets: rt.facets, @@ -522,7 +590,11 @@ describe(`hasMutedWord`, () => { // extra word const match = hasMutedWord({ mutedWords: [ - { value: `idk what this would be for`, targets: ['content'] }, + { + value: `idk what this would be for`, + targets: ['content'], + actorTarget: 'all', + }, ], text: rt.text, facets: rt.facets, @@ -535,7 +607,9 @@ describe(`hasMutedWord`, () => { it(`match: idk`, () => { // extra word const match = hasMutedWord({ - mutedWords: [{ value: `idk`, targets: ['content'] }], + mutedWords: [ + { value: `idk`, targets: ['content'], actorTarget: 'all' }, + ], text: rt.text, facets: rt.facets, outlineTags: [], @@ -546,7 +620,13 @@ describe(`hasMutedWord`, () => { it(`match: idkwhatthiswouldbe`, () => { const match = hasMutedWord({ - mutedWords: [{ value: `idkwhatthiswouldbe`, targets: ['content'] }], + mutedWords: [ + { + value: `idkwhatthiswouldbe`, + targets: ['content'], + actorTarget: 'all', + }, + ], text: rt.text, facets: rt.facets, outlineTags: [], @@ -564,7 +644,13 @@ describe(`hasMutedWord`, () => { it(`match: context(iykyk)`, () => { const match = hasMutedWord({ - mutedWords: [{ value: `context(iykyk)`, targets: ['content'] }], + mutedWords: [ + { + value: `context(iykyk)`, + targets: ['content'], + actorTarget: 'all', + }, + ], text: rt.text, facets: rt.facets, outlineTags: [], @@ -575,7 +661,9 @@ describe(`hasMutedWord`, () => { it(`match: context`, () => { const match = hasMutedWord({ - mutedWords: [{ value: `context`, targets: ['content'] }], + mutedWords: [ + { value: `context`, targets: ['content'], actorTarget: 'all' }, + ], text: rt.text, facets: rt.facets, outlineTags: [], @@ -586,7 +674,9 @@ describe(`hasMutedWord`, () => { it(`match: iykyk`, () => { const match = hasMutedWord({ - mutedWords: [{ value: `iykyk`, targets: ['content'] }], + mutedWords: [ + { value: `iykyk`, targets: ['content'], actorTarget: 'all' }, + ], text: rt.text, facets: rt.facets, outlineTags: [], @@ -597,7 +687,9 @@ describe(`hasMutedWord`, () => { it(`match: (iykyk)`, () => { const match = hasMutedWord({ - mutedWords: [{ value: `(iykyk)`, targets: ['content'] }], + mutedWords: [ + { value: `(iykyk)`, targets: ['content'], actorTarget: 'all' }, + ], text: rt.text, facets: rt.facets, outlineTags: [], @@ -615,7 +707,9 @@ describe(`hasMutedWord`, () => { it(`match: 🦋`, () => { const match = hasMutedWord({ - mutedWords: [{ value: `🦋`, targets: ['content'] }], + mutedWords: [ + { value: `🦋`, targets: ['content'], actorTarget: 'all' }, + ], text: rt.text, facets: rt.facets, outlineTags: [], @@ -635,7 +729,13 @@ describe(`hasMutedWord`, () => { it(`match: stop worrying`, () => { const match = hasMutedWord({ - mutedWords: [{ value: 'stop worrying', targets: ['content'] }], + mutedWords: [ + { + value: 'stop worrying', + targets: ['content'], + actorTarget: 'all', + }, + ], text: rt.text, facets: rt.facets, outlineTags: [], @@ -646,7 +746,13 @@ describe(`hasMutedWord`, () => { it(`match: turtles, or how`, () => { const match = hasMutedWord({ - mutedWords: [{ value: 'turtles, or how', targets: ['content'] }], + mutedWords: [ + { + value: 'turtles, or how', + targets: ['content'], + actorTarget: 'all', + }, + ], text: rt.text, facets: rt.facets, outlineTags: [], @@ -668,7 +774,13 @@ describe(`hasMutedWord`, () => { // internet it(`match: インターネット`, () => { const match = hasMutedWord({ - mutedWords: [{ value: 'インターネット', targets: ['content'] }], + mutedWords: [ + { + value: 'インターネット', + targets: ['content'], + actorTarget: 'all', + }, + ], text: rt.text, facets: rt.facets, outlineTags: [], @@ -699,7 +811,9 @@ describe(`hasMutedWord`, () => { adultContentEnabled: false, labels: {}, labelers: [], - mutedWords: [{ value: 'words', targets: ['content'] }], + mutedWords: [ + { value: 'words', targets: ['content'], actorTarget: 'all' }, + ], hiddenPosts: [], }, labelDefs: {}, @@ -726,7 +840,9 @@ describe(`hasMutedWord`, () => { adultContentEnabled: false, labels: {}, labelers: [], - mutedWords: [{ value: 'words', targets: ['content'] }], + mutedWords: [ + { value: 'words', targets: ['content'], actorTarget: 'all' }, + ], hiddenPosts: [], }, labelDefs: {}, @@ -757,7 +873,9 @@ describe(`hasMutedWord`, () => { adultContentEnabled: false, labels: {}, labelers: [], - mutedWords: [{ value: 'words', targets: ['tags'] }], + mutedWords: [ + { value: 'words', targets: ['tags'], actorTarget: 'all' }, + ], hiddenPosts: [], }, labelDefs: {}, @@ -793,6 +911,7 @@ describe(`hasMutedWord`, () => { value: 'words', targets: ['content'], expiresAt: new Date(now + 1e3).toISOString(), + actorTarget: 'all', }, ], hiddenPosts: [], @@ -829,6 +948,7 @@ describe(`hasMutedWord`, () => { value: 'words', targets: ['content'], expiresAt: new Date(now - 1e3).toISOString(), + actorTarget: 'all', }, ], hiddenPosts: [], From 3c2c06bf197e227b59811bb8efcaf4de87d23c2c Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Fri, 26 Jul 2024 11:17:16 -0500 Subject: [PATCH 22/22] Fix types on new tests --- packages/api/tests/moderation-mutewords.test.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/api/tests/moderation-mutewords.test.ts b/packages/api/tests/moderation-mutewords.test.ts index 922a94ecb95..681dddc73d8 100644 --- a/packages/api/tests/moderation-mutewords.test.ts +++ b/packages/api/tests/moderation-mutewords.test.ts @@ -795,7 +795,9 @@ describe(`hasMutedWord`, () => { describe(`facet with multiple features`, () => { it(`multiple tags`, () => { const match = hasMutedWord({ - mutedWords: [{ value: 'bad', targets: ['content'] }], + mutedWords: [ + { value: 'bad', targets: ['content'], actorTarget: 'all' }, + ], text: 'tags', facets: [ { @@ -821,7 +823,9 @@ describe(`hasMutedWord`, () => { it(`other features`, () => { const match = hasMutedWord({ - mutedWords: [{ value: 'bad', targets: ['content'] }], + mutedWords: [ + { value: 'bad', targets: ['content'], actorTarget: 'all' }, + ], text: 'test', facets: [ {