Update muted words handling, add attributes (#2276)
* Sketch proposal for additional muted words attributes * Rename ttl -> expiresAt * Feedback * Codegen * Refactor muted words methods to integrate new attributes * Add changeset * Use datetime format * Simplify migration * Fix tests * Format * Re-integrate tests * Let the lock cook * Fix comments * Integrate mute words enhancements (#2643) * Check expiry when comparing mute words * Check actors when comparing * Tweak lex, condegen * Integrate new prop * Remove fake timers (cherry picked from commit ad31910560ce938e3ff64944d46355c64635ebf8) * Update changeset * Prevent deleting value when updating * Include missing test * Add default * Apply default 'all' value to existing mute words to satisfy Typescript * Fix types in tests * Fix types on new tests
This commit is contained in:
parent
803d1b6c0d
commit
77c5306d2a
.changeset
lexicons/app/bsky/actor
packages
api
src
tests
bsky/src/lexicon
ozone/src/lexicon
pds/src/lexicon
8
.changeset/rotten-moose-switch.md
Normal file
8
.changeset/rotten-moose-switch.md
Normal file
@ -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`, `actorTarget`, and `expiresAt`. Adds and updates methods in API SDK for better management of muted words.
|
@ -330,6 +330,7 @@
|
||||
"description": "A word that the account owner has muted.",
|
||||
"required": ["value", "targets"],
|
||||
"properties": {
|
||||
"id": { "type": "string" },
|
||||
"value": {
|
||||
"type": "string",
|
||||
"description": "The muted word itself.",
|
||||
@ -343,6 +344,17 @@
|
||||
"type": "ref",
|
||||
"ref": "app.bsky.actor.defs#mutedWordTarget"
|
||||
}
|
||||
},
|
||||
"actorTarget": {
|
||||
"type": "string",
|
||||
"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",
|
||||
"format": "datetime",
|
||||
"description": "The date and time at which the muted word will expire and no longer be applied."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -9,6 +9,7 @@ import {
|
||||
AppBskyLabelerDefs,
|
||||
ComAtprotoRepoPutRecord,
|
||||
} from './client'
|
||||
import { MutedWord } from './client/types/app/bsky/actor/defs'
|
||||
import {
|
||||
BskyPreferences,
|
||||
BskyFeedViewPreference,
|
||||
@ -477,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
|
||||
@ -937,7 +946,19 @@ export class BskyAgent extends AtpAgent {
|
||||
})
|
||||
}
|
||||
|
||||
async upsertMutedWords(newMutedWords: AppBskyActorDefs.MutedWord[]) {
|
||||
/**
|
||||
* Add a muted word to user preferences.
|
||||
*/
|
||||
async addMutedWord(
|
||||
mutedWord: Pick<
|
||||
MutedWord,
|
||||
'value' | 'targets' | 'actorTarget' | 'expiresAt'
|
||||
>,
|
||||
) {
|
||||
const sanitizedValue = sanitizeMutedWordValue(mutedWord.value)
|
||||
|
||||
if (!sanitizedValue) return
|
||||
|
||||
await updatePreferences(this, (prefs: AppBskyActorDefs.Preferences) => {
|
||||
let mutedWordsPref = prefs.findLast(
|
||||
(pref) =>
|
||||
@ -945,40 +966,27 @@ export class BskyAgent extends AtpAgent {
|
||||
AppBskyActorDefs.validateMutedWordsPref(pref).success,
|
||||
)
|
||||
|
||||
const newMutedWord: AppBskyActorDefs.MutedWord = {
|
||||
id: TID.nextStr(),
|
||||
value: sanitizedValue,
|
||||
targets: mutedWord.targets || [],
|
||||
actorTarget: mutedWord.actorTarget || 'all',
|
||||
expiresAt: mutedWord.expiresAt || undefined,
|
||||
}
|
||||
|
||||
if (mutedWordsPref && AppBskyActorDefs.isMutedWordsPref(mutedWordsPref)) {
|
||||
for (const updatedWord of newMutedWords) {
|
||||
let foundMatch = false
|
||||
const sanitizedUpdatedValue = sanitizeMutedWordValue(
|
||||
updatedWord.value,
|
||||
)
|
||||
mutedWordsPref.items.push(newMutedWord)
|
||||
|
||||
// was trimmed down to an empty string e.g. single `#`
|
||||
if (!sanitizedUpdatedValue) continue
|
||||
|
||||
for (const existingItem of mutedWordsPref.items) {
|
||||
if (existingItem.value === sanitizedUpdatedValue) {
|
||||
existingItem.targets = Array.from(
|
||||
new Set([...existingItem.targets, ...updatedWord.targets]),
|
||||
)
|
||||
foundMatch = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
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],
|
||||
}
|
||||
}
|
||||
|
||||
@ -990,6 +998,28 @@ export class BskyAgent extends AtpAgent {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience method to add muted words to user preferences
|
||||
*/
|
||||
async addMutedWords(newMutedWords: AppBskyActorDefs.MutedWord[]) {
|
||||
await Promise.all(newMutedWords.map((word) => this.addMutedWord(word)))
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated use `addMutedWords` or `addMutedWord` instead
|
||||
*/
|
||||
async upsertMutedWords(
|
||||
mutedWords: Pick<
|
||||
MutedWord,
|
||||
'value' | 'targets' | 'actorTarget' | 'expiresAt'
|
||||
>[],
|
||||
) {
|
||||
await this.addMutedWords(mutedWords)
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a muted word in user preferences.
|
||||
*/
|
||||
async updateMutedWord(mutedWord: AppBskyActorDefs.MutedWord) {
|
||||
await updatePreferences(this, (prefs: AppBskyActorDefs.Preferences) => {
|
||||
const mutedWordsPref = prefs.findLast(
|
||||
@ -999,22 +1029,48 @@ 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 = mutedWordsPref.items.map((existingItem) => {
|
||||
const match = matchMutedWord(existingItem, mutedWord)
|
||||
|
||||
if (match) {
|
||||
const updated = {
|
||||
...existingItem,
|
||||
...mutedWord,
|
||||
}
|
||||
return {
|
||||
id: existingItem.id || TID.nextStr(),
|
||||
value:
|
||||
sanitizeMutedWordValue(updated.value) || existingItem.value,
|
||||
targets: updated.targets || [],
|
||||
actorTarget: updated.actorTarget || 'all',
|
||||
expiresAt: updated.expiresAt || undefined,
|
||||
}
|
||||
} else {
|
||||
return existingItem
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* Migrate any old muted words that don't have an id
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
async removeMutedWord(mutedWord: AppBskyActorDefs.MutedWord) {
|
||||
await updatePreferences(this, (prefs: AppBskyActorDefs.Preferences) => {
|
||||
const mutedWordsPref = prefs.findLast(
|
||||
@ -1025,22 +1081,39 @@ 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 match = matchMutedWord(mutedWordsPref.items[i], mutedWord)
|
||||
|
||||
if (match) {
|
||||
mutedWordsPref.items.splice(i, 1)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrate any old muted words that don't have an id
|
||||
*/
|
||||
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 remove muted words from user preferences
|
||||
*/
|
||||
async removeMutedWords(mutedWords: AppBskyActorDefs.MutedWord[]) {
|
||||
await Promise.all(mutedWords.map((word) => this.removeMutedWord(word)))
|
||||
}
|
||||
|
||||
async hidePost(postUri: string) {
|
||||
await updateHiddenPost(this, postUri, 'hide')
|
||||
}
|
||||
@ -1369,3 +1442,24 @@ 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 || TID.nextStr(),
|
||||
}))
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
@ -4341,6 +4341,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.',
|
||||
@ -4355,6 +4358,19 @@ export const schemaDict = {
|
||||
ref: 'lex:app.bsky.actor.defs#mutedWordTarget',
|
||||
},
|
||||
},
|
||||
actorTarget: {
|
||||
type: 'string',
|
||||
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',
|
||||
format: 'datetime',
|
||||
description:
|
||||
'The date and time at which the muted word will expire and no longer be applied.',
|
||||
},
|
||||
},
|
||||
},
|
||||
mutedWordsPref: {
|
||||
|
@ -370,10 +370,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[]
|
||||
/** 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
|
||||
}
|
||||
|
||||
|
@ -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[])
|
||||
@ -48,6 +50,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
|
||||
|
@ -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
|
||||
|
@ -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,591 @@ 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'],
|
||||
actorTarget: 'all',
|
||||
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!.actorTarget).toEqual('all')
|
||||
expect(word!.expiresAt).toEqual(expiresAt)
|
||||
})
|
||||
|
||||
it('single-hash #, no insert', async () => {
|
||||
await agent.addMutedWord({
|
||||
value: '#',
|
||||
targets: [],
|
||||
actorTarget: 'all',
|
||||
})
|
||||
const { moderationPrefs } = await agent.getPreferences()
|
||||
|
||||
// sanitized to empty string, not inserted
|
||||
expect(moderationPrefs.mutedWords.length).toEqual(0)
|
||||
})
|
||||
|
||||
it('multi-hash ##, inserts #', async () => {
|
||||
await agent.addMutedWord({
|
||||
value: '##',
|
||||
targets: [],
|
||||
actorTarget: 'all',
|
||||
})
|
||||
const { moderationPrefs } = await agent.getPreferences()
|
||||
expect(
|
||||
moderationPrefs.mutedWords.find((m) => m.value === '#'),
|
||||
).toBeTruthy()
|
||||
})
|
||||
|
||||
it('multi-hash ##hashtag, inserts #hashtag', async () => {
|
||||
await agent.addMutedWord({
|
||||
value: '##hashtag',
|
||||
targets: [],
|
||||
actorTarget: 'all',
|
||||
})
|
||||
const { moderationPrefs } = await agent.getPreferences()
|
||||
expect(
|
||||
moderationPrefs.mutedWords.find((w) => w.value === '#hashtag'),
|
||||
).toBeTruthy()
|
||||
})
|
||||
|
||||
it('hash emoji #️⃣, inserts #️⃣', async () => {
|
||||
await agent.addMutedWord({
|
||||
value: '#️⃣',
|
||||
targets: [],
|
||||
actorTarget: 'all',
|
||||
})
|
||||
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: [],
|
||||
actorTarget: 'all',
|
||||
})
|
||||
const { moderationPrefs } = await agent.getPreferences()
|
||||
expect(
|
||||
moderationPrefs.mutedWords.find((m) => m.value === '#️⃣'),
|
||||
).toBeTruthy()
|
||||
})
|
||||
|
||||
it('hash emoji with double leading hash ###️⃣, inserts ##️⃣', async () => {
|
||||
await agent.addMutedWord({
|
||||
value: '###️⃣',
|
||||
targets: [],
|
||||
actorTarget: 'all',
|
||||
})
|
||||
const { moderationPrefs } = await agent.getPreferences()
|
||||
expect(
|
||||
moderationPrefs.mutedWords.find((m) => m.value === '##️⃣'),
|
||||
).toBeTruthy()
|
||||
})
|
||||
|
||||
it(`includes apostrophes e.g. Bluesky's`, async () => {
|
||||
await agent.addMutedWord({
|
||||
value: `Bluesky's`,
|
||||
targets: [],
|
||||
actorTarget: 'all',
|
||||
})
|
||||
const { mutedWords } = (await agent.getPreferences()).moderationPrefs
|
||||
|
||||
expect(mutedWords.find((m) => m.value === `Bluesky's`)).toBeTruthy()
|
||||
})
|
||||
|
||||
describe(`invalid characters`, () => {
|
||||
it('#<zws>, no insert', async () => {
|
||||
await agent.addMutedWord({
|
||||
value: '#',
|
||||
targets: [],
|
||||
actorTarget: 'all',
|
||||
})
|
||||
const { moderationPrefs } = await agent.getPreferences()
|
||||
expect(moderationPrefs.mutedWords.length).toEqual(0)
|
||||
})
|
||||
|
||||
it('#<zws>ab, inserts ab', async () => {
|
||||
await agent.addMutedWord({
|
||||
value: '#ab',
|
||||
targets: [],
|
||||
actorTarget: 'all',
|
||||
})
|
||||
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: [],
|
||||
actorTarget: 'all',
|
||||
})
|
||||
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: [],
|
||||
actorTarget: 'all',
|
||||
})
|
||||
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: [],
|
||||
actorTarget: 'all',
|
||||
})
|
||||
const { moderationPrefs } = await agent.getPreferences()
|
||||
expect(moderationPrefs.mutedWords.length).toEqual(0)
|
||||
})
|
||||
|
||||
it(`' trim ', inserts 'trim'`, async () => {
|
||||
await agent.addMutedWord({
|
||||
value: ' trim ',
|
||||
targets: [],
|
||||
actorTarget: 'all',
|
||||
})
|
||||
const { moderationPrefs } = await agent.getPreferences()
|
||||
expect(
|
||||
moderationPrefs.mutedWords.find((m) => m.value === 'trim'),
|
||||
).toBeTruthy()
|
||||
})
|
||||
})
|
||||
// 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'],
|
||||
})
|
||||
await agent.updateMutedWord({
|
||||
value: 'tag_then_both',
|
||||
targets: ['content', 'tag'],
|
||||
})
|
||||
await agent.updateMutedWord({ value: 'tag_then_none', targets: [] })
|
||||
await agent.updateMutedWord({ value: 'no_exist', targets: ['tag'] })
|
||||
const { mutedWords } = (await agent.getPreferences()).moderationPrefs
|
||||
describe('addMutedWords', () => {
|
||||
it('inserts happen sequentially, no clobbering', async () => {
|
||||
await agent.addMutedWords([
|
||||
{ value: 'a', targets: ['content'], actorTarget: 'all' },
|
||||
{ value: 'b', targets: ['content'], actorTarget: 'all' },
|
||||
{ value: 'c', targets: ['content'], actorTarget: 'all' },
|
||||
])
|
||||
|
||||
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()
|
||||
const { moderationPrefs } = await agent.getPreferences()
|
||||
|
||||
expect(moderationPrefs.mutedWords.length).toEqual(3)
|
||||
})
|
||||
})
|
||||
|
||||
it('updateMutedWord with #, does not update', async () => {
|
||||
await agent.upsertMutedWords([
|
||||
{
|
||||
value: '#just_a_tag',
|
||||
describe('upsertMutedWords (deprecated)', () => {
|
||||
it('no longer upserts, calls addMutedWords', async () => {
|
||||
await agent.upsertMutedWords([
|
||||
{ value: 'both', targets: ['content'], actorTarget: 'all' },
|
||||
])
|
||||
await agent.upsertMutedWords([
|
||||
{ value: 'both', targets: ['tag'], actorTarget: 'all' },
|
||||
])
|
||||
|
||||
const { moderationPrefs } = await agent.getPreferences()
|
||||
|
||||
expect(moderationPrefs.mutedWords.length).toEqual(2)
|
||||
})
|
||||
})
|
||||
|
||||
describe('updateMutedWord', () => {
|
||||
it(`word doesn't exist, no update or insert`, async () => {
|
||||
await agent.updateMutedWord({
|
||||
value: 'word',
|
||||
targets: ['tag', 'content'],
|
||||
actorTarget: 'all',
|
||||
})
|
||||
const { moderationPrefs } = await agent.getPreferences()
|
||||
expect(moderationPrefs.mutedWords.length).toEqual(0)
|
||||
})
|
||||
|
||||
it('updates and sanitizes new value', async () => {
|
||||
await agent.addMutedWord({
|
||||
value: 'value',
|
||||
targets: ['content'],
|
||||
actorTarget: 'all',
|
||||
})
|
||||
|
||||
const a = await agent.getPreferences()
|
||||
const word = a.moderationPrefs.mutedWords.find(
|
||||
(m) => m.value === 'value',
|
||||
)
|
||||
|
||||
await agent.updateMutedWord({
|
||||
...word!,
|
||||
value: '#new value',
|
||||
})
|
||||
|
||||
const b = await agent.getPreferences()
|
||||
const updatedWord = b.moderationPrefs.mutedWords.find(
|
||||
(m) => m.id === word!.id,
|
||||
)
|
||||
|
||||
expect(updatedWord!.value).toEqual('new value')
|
||||
expect(updatedWord).toHaveProperty('targets', ['content'])
|
||||
})
|
||||
|
||||
it('updates targets', async () => {
|
||||
await agent.addMutedWord({
|
||||
value: 'word',
|
||||
targets: ['tag'],
|
||||
},
|
||||
])
|
||||
await agent.updateMutedWord({
|
||||
value: '#just_a_tag',
|
||||
targets: ['tag', 'content'],
|
||||
})
|
||||
const { mutedWords } = (await agent.getPreferences()).moderationPrefs
|
||||
expect(mutedWords.find((m) => m.value === 'just_a_tag')).toStrictEqual({
|
||||
value: 'just_a_tag',
|
||||
targets: ['tag'],
|
||||
})
|
||||
})
|
||||
actorTarget: 'all',
|
||||
})
|
||||
|
||||
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
|
||||
const a = await agent.getPreferences()
|
||||
const word = a.moderationPrefs.mutedWords.find(
|
||||
(m) => m.value === 'word',
|
||||
)
|
||||
|
||||
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()
|
||||
})
|
||||
await agent.updateMutedWord({
|
||||
...word!,
|
||||
targets: ['content'],
|
||||
})
|
||||
|
||||
it('removeMutedWord with #, no match, no removal', async () => {
|
||||
await agent.removeMutedWord({ value: '#hashtag', targets: [] })
|
||||
const { mutedWords } = (await agent.getPreferences()).moderationPrefs
|
||||
|
||||
// was inserted with #hashtag, but we don't sanitize on remove
|
||||
expect(mutedWords.find((m) => m.value === 'hashtag')).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
|
||||
|
||||
// sanitized to empty string, not inserted
|
||||
expect(end.mutedWords.length).toEqual(length)
|
||||
})
|
||||
|
||||
it('multi-hash ##', async () => {
|
||||
await agent.upsertMutedWords([{ value: '##', targets: [] }])
|
||||
const { mutedWords } = (await agent.getPreferences()).moderationPrefs
|
||||
|
||||
expect(mutedWords.find((m) => m.value === '#')).toBeTruthy()
|
||||
})
|
||||
|
||||
it('multi-hash ##hashtag', async () => {
|
||||
await agent.upsertMutedWords([{ value: '##hashtag', targets: [] }])
|
||||
const a = (await agent.getPreferences()).moderationPrefs
|
||||
|
||||
expect(a.mutedWords.find((w) => w.value === '#hashtag')).toBeTruthy()
|
||||
|
||||
await agent.removeMutedWord({ value: '#hashtag', targets: [] })
|
||||
const b = (await agent.getPreferences()).moderationPrefs
|
||||
|
||||
expect(b.mutedWords.find((w) => w.value === '#hashtag')).toBeFalsy()
|
||||
})
|
||||
|
||||
it('hash emoji #️⃣', async () => {
|
||||
await agent.upsertMutedWords([{ value: '#️⃣', targets: [] }])
|
||||
const { mutedWords } = (await agent.getPreferences()).moderationPrefs
|
||||
|
||||
expect(mutedWords.find((m) => m.value === '#️⃣')).toBeTruthy()
|
||||
|
||||
await agent.removeMutedWord({ value: '#️⃣', targets: [] })
|
||||
const end = (await agent.getPreferences()).moderationPrefs
|
||||
|
||||
expect(end.mutedWords.find((m) => m.value === '#️⃣')).toBeFalsy()
|
||||
})
|
||||
|
||||
it('hash emoji ##️⃣', async () => {
|
||||
await agent.upsertMutedWords([{ value: '##️⃣', targets: [] }])
|
||||
const { mutedWords } = (await agent.getPreferences()).moderationPrefs
|
||||
|
||||
expect(mutedWords.find((m) => m.value === '#️⃣')).toBeTruthy()
|
||||
|
||||
await agent.removeMutedWord({ value: '#️⃣', targets: [] })
|
||||
const end = (await agent.getPreferences()).moderationPrefs
|
||||
|
||||
expect(end.mutedWords.find((m) => m.value === '#️⃣')).toBeFalsy()
|
||||
})
|
||||
|
||||
it('hash emoji ###️⃣', async () => {
|
||||
await agent.upsertMutedWords([{ value: '###️⃣', targets: [] }])
|
||||
const { mutedWords } = (await agent.getPreferences()).moderationPrefs
|
||||
|
||||
expect(mutedWords.find((m) => m.value === '##️⃣')).toBeTruthy()
|
||||
|
||||
await agent.removeMutedWord({ value: '##️⃣', targets: [] })
|
||||
const end = (await agent.getPreferences()).moderationPrefs
|
||||
|
||||
expect(end.mutedWords.find((m) => m.value === '##️⃣')).toBeFalsy()
|
||||
})
|
||||
|
||||
it(`apostrophe: Bluesky's`, async () => {
|
||||
await agent.upsertMutedWords([{ value: `Bluesky's`, targets: [] }])
|
||||
const { mutedWords } = (await agent.getPreferences()).moderationPrefs
|
||||
|
||||
expect(mutedWords.find((m) => m.value === `Bluesky's`)).toBeTruthy()
|
||||
})
|
||||
|
||||
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
|
||||
|
||||
expect(mutedWords.length).toEqual(length)
|
||||
})
|
||||
|
||||
it('newline', async () => {
|
||||
await agent.upsertMutedWords([
|
||||
{ value: 'test value\n with newline', targets: [] },
|
||||
])
|
||||
const { mutedWords } = (await agent.getPreferences()).moderationPrefs
|
||||
const b = await agent.getPreferences()
|
||||
|
||||
expect(
|
||||
mutedWords.find((m) => m.value === 'test value with newline'),
|
||||
).toBeTruthy()
|
||||
b.moderationPrefs.mutedWords.find((m) => m.id === word!.id),
|
||||
).toHaveProperty('targets', ['content'])
|
||||
})
|
||||
|
||||
it('newline(s)', async () => {
|
||||
await agent.upsertMutedWords([
|
||||
{ value: 'test value\n\r with newline', targets: [] },
|
||||
])
|
||||
const { mutedWords } = (await agent.getPreferences()).moderationPrefs
|
||||
it('updates actorTarget', async () => {
|
||||
await agent.addMutedWord({
|
||||
value: 'value',
|
||||
targets: ['content'],
|
||||
actorTarget: 'all',
|
||||
})
|
||||
|
||||
const a = await agent.getPreferences()
|
||||
const word = a.moderationPrefs.mutedWords.find(
|
||||
(m) => m.value === 'value',
|
||||
)
|
||||
|
||||
await agent.updateMutedWord({
|
||||
...word!,
|
||||
actorTarget: 'exclude-following',
|
||||
})
|
||||
|
||||
const b = await agent.getPreferences()
|
||||
|
||||
expect(
|
||||
mutedWords.find((m) => m.value === 'test value with newline'),
|
||||
b.moderationPrefs.mutedWords.find((m) => m.id === word!.id),
|
||||
).toHaveProperty('actorTarget', 'exclude-following')
|
||||
})
|
||||
|
||||
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,
|
||||
actorTarget: 'all',
|
||||
})
|
||||
|
||||
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(`doesn't update if value is sanitized to be falsy`, async () => {
|
||||
await agent.addMutedWord({
|
||||
value: 'rug',
|
||||
targets: ['content'],
|
||||
actorTarget: 'all',
|
||||
})
|
||||
|
||||
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', () => {
|
||||
it('removes word', async () => {
|
||||
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(word!)
|
||||
|
||||
const b = await agent.getPreferences()
|
||||
|
||||
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'],
|
||||
actorTarget: 'all',
|
||||
})
|
||||
const a = await agent.getPreferences()
|
||||
const word = a.moderationPrefs.mutedWords.find(
|
||||
(m) => m.value === 'word',
|
||||
)
|
||||
|
||||
await agent.removeMutedWord({
|
||||
value: 'another',
|
||||
targets: [],
|
||||
actorTarget: 'all',
|
||||
})
|
||||
|
||||
const b = await agent.getPreferences()
|
||||
|
||||
expect(
|
||||
b.moderationPrefs.mutedWords.find((m) => m.id === word!.id),
|
||||
).toBeTruthy()
|
||||
})
|
||||
})
|
||||
|
||||
it('empty space', async () => {
|
||||
await agent.upsertMutedWords([{ value: ' ', targets: [] }])
|
||||
const { mutedWords } = (await agent.getPreferences()).moderationPrefs
|
||||
describe('removeMutedWords', () => {
|
||||
it(`removes sequentially, no clobbering`, async () => {
|
||||
await agent.addMutedWords([
|
||||
{ value: 'a', targets: ['content'], actorTarget: 'all ' },
|
||||
{ value: 'b', targets: ['content'], actorTarget: 'all ' },
|
||||
{ value: 'c', targets: ['content'], actorTarget: 'all ' },
|
||||
])
|
||||
|
||||
expect(mutedWords.find((m) => m.value === ' ')).toBeFalsy()
|
||||
const a = await agent.getPreferences()
|
||||
await agent.removeMutedWords(a.moderationPrefs.mutedWords)
|
||||
const b = await agent.getPreferences()
|
||||
|
||||
expect(b.moderationPrefs.mutedWords.length).toEqual(0)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('leading/trailing space', async () => {
|
||||
await agent.upsertMutedWords([{ value: ' trim ', targets: [] }])
|
||||
const { mutedWords } = (await agent.getPreferences()).moderationPrefs
|
||||
describe('legacy muted words', () => {
|
||||
let agent: BskyAgent
|
||||
|
||||
expect(mutedWords.find((m) => m.value === 'trim')).toBeTruthy()
|
||||
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,
|
||||
actorTarget: 'all',
|
||||
}
|
||||
|
||||
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'],
|
||||
actorTarget: 'all',
|
||||
})
|
||||
|
||||
{
|
||||
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'], actorTarget: 'all' },
|
||||
])
|
||||
|
||||
{
|
||||
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'],
|
||||
actorTarget: 'all',
|
||||
})
|
||||
await addLegacyMutedWord({
|
||||
value: 'word2',
|
||||
targets: ['tag'],
|
||||
actorTarget: 'all',
|
||||
})
|
||||
|
||||
await agent.updateMutedWord({
|
||||
value: 'word',
|
||||
targets: ['tag'],
|
||||
actorTarget: 'all',
|
||||
})
|
||||
|
||||
{
|
||||
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'],
|
||||
actorTarget: 'all',
|
||||
})
|
||||
await addLegacyMutedWord({
|
||||
value: 'word2',
|
||||
targets: ['tag'],
|
||||
actorTarget: 'all',
|
||||
})
|
||||
|
||||
await agent.removeMutedWord({
|
||||
value: 'word',
|
||||
targets: ['tag'],
|
||||
actorTarget: 'all',
|
||||
})
|
||||
|
||||
{
|
||||
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()
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
@ -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: [],
|
||||
@ -683,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: [
|
||||
{
|
||||
@ -709,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: [
|
||||
{
|
||||
@ -753,7 +869,9 @@ describe(`hasMutedWord`, () => {
|
||||
adultContentEnabled: false,
|
||||
labels: {},
|
||||
labelers: [],
|
||||
mutedWords: [{ value: 'words', targets: ['content'] }],
|
||||
mutedWords: [
|
||||
{ value: 'words', targets: ['content'], actorTarget: 'all' },
|
||||
],
|
||||
hiddenPosts: [],
|
||||
},
|
||||
labelDefs: {},
|
||||
@ -780,7 +898,9 @@ describe(`hasMutedWord`, () => {
|
||||
adultContentEnabled: false,
|
||||
labels: {},
|
||||
labelers: [],
|
||||
mutedWords: [{ value: 'words', targets: ['content'] }],
|
||||
mutedWords: [
|
||||
{ value: 'words', targets: ['content'], actorTarget: 'all' },
|
||||
],
|
||||
hiddenPosts: [],
|
||||
},
|
||||
labelDefs: {},
|
||||
@ -811,7 +931,9 @@ describe(`hasMutedWord`, () => {
|
||||
adultContentEnabled: false,
|
||||
labels: {},
|
||||
labelers: [],
|
||||
mutedWords: [{ value: 'words', targets: ['tags'] }],
|
||||
mutedWords: [
|
||||
{ value: 'words', targets: ['tags'], actorTarget: 'all' },
|
||||
],
|
||||
hiddenPosts: [],
|
||||
},
|
||||
labelDefs: {},
|
||||
@ -820,4 +942,140 @@ describe(`hasMutedWord`, () => {
|
||||
expect(res.causes.length).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe(`timed mute words`, () => {
|
||||
it(`non-expired word`, () => {
|
||||
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(),
|
||||
actorTarget: 'all',
|
||||
},
|
||||
],
|
||||
hiddenPosts: [],
|
||||
},
|
||||
labelDefs: {},
|
||||
},
|
||||
)
|
||||
|
||||
expect(res.causes[0].type).toBe('mute-word')
|
||||
})
|
||||
|
||||
it(`expired word`, () => {
|
||||
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(),
|
||||
actorTarget: 'all',
|
||||
},
|
||||
],
|
||||
hiddenPosts: [],
|
||||
},
|
||||
labelDefs: {},
|
||||
},
|
||||
)
|
||||
|
||||
expect(res.causes.length).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
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')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
@ -4341,6 +4341,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.',
|
||||
@ -4355,6 +4358,19 @@ export const schemaDict = {
|
||||
ref: 'lex:app.bsky.actor.defs#mutedWordTarget',
|
||||
},
|
||||
},
|
||||
actorTarget: {
|
||||
type: 'string',
|
||||
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',
|
||||
format: 'datetime',
|
||||
description:
|
||||
'The date and time at which the muted word will expire and no longer be applied.',
|
||||
},
|
||||
},
|
||||
},
|
||||
mutedWordsPref: {
|
||||
|
@ -370,10 +370,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[]
|
||||
/** 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
|
||||
}
|
||||
|
||||
|
@ -4341,6 +4341,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.',
|
||||
@ -4355,6 +4358,19 @@ export const schemaDict = {
|
||||
ref: 'lex:app.bsky.actor.defs#mutedWordTarget',
|
||||
},
|
||||
},
|
||||
actorTarget: {
|
||||
type: 'string',
|
||||
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',
|
||||
format: 'datetime',
|
||||
description:
|
||||
'The date and time at which the muted word will expire and no longer be applied.',
|
||||
},
|
||||
},
|
||||
},
|
||||
mutedWordsPref: {
|
||||
|
@ -370,10 +370,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[]
|
||||
/** 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
|
||||
}
|
||||
|
||||
|
@ -4341,6 +4341,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.',
|
||||
@ -4355,6 +4358,19 @@ export const schemaDict = {
|
||||
ref: 'lex:app.bsky.actor.defs#mutedWordTarget',
|
||||
},
|
||||
},
|
||||
actorTarget: {
|
||||
type: 'string',
|
||||
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',
|
||||
format: 'datetime',
|
||||
description:
|
||||
'The date and time at which the muted word will expire and no longer be applied.',
|
||||
},
|
||||
},
|
||||
},
|
||||
mutedWordsPref: {
|
||||
|
@ -370,10 +370,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[]
|
||||
/** 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
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user