diff --git a/.changeset/funny-elephants-listen.md b/.changeset/funny-elephants-listen.md new file mode 100644 index 00000000000..8135f76f989 --- /dev/null +++ b/.changeset/funny-elephants-listen.md @@ -0,0 +1,5 @@ +--- +'@atproto/api': patch +--- + +Add muted words/tags and hidden posts prefs and methods" diff --git a/lexicons/app/bsky/actor/defs.json b/lexicons/app/bsky/actor/defs.json index fa3772c4ff3..a260fc4ef5f 100644 --- a/lexicons/app/bsky/actor/defs.json +++ b/lexicons/app/bsky/actor/defs.json @@ -215,6 +215,58 @@ "description": "A list of tags which describe the account owner's interests gathered during onboarding." } } + }, + "mutedWordTarget": { + "type": "string", + "knownValues": ["content", "tag"], + "maxLength": 640, + "maxGraphemes": 64 + }, + "mutedWord": { + "type": "object", + "description": "A word that the account owner has muted.", + "required": ["value", "targets"], + "properties": { + "value": { + "type": "string", + "description": "The muted word itself.", + "maxLength": 10000, + "maxGraphemes": 1000 + }, + "targets": { + "type": "array", + "description": "The intended targets of the muted word.", + "items": { + "type": "ref", + "ref": "app.bsky.actor.defs#mutedWordTarget" + } + } + } + }, + "mutedWordsPref": { + "type": "object", + "required": ["items"], + "properties": { + "items": { + "type": "array", + "items": { + "type": "ref", + "ref": "app.bsky.actor.defs#mutedWord" + }, + "description": "A list of words the account owner has muted." + } + } + }, + "hiddenPostsPref": { + "type": "object", + "required": ["items"], + "properties": { + "items": { + "type": "array", + "items": { "type": "string", "format": "at-uri" }, + "description": "A list of URIs of posts the account owner has hidden." + } + } } } } diff --git a/packages/api/src/bsky-agent.ts b/packages/api/src/bsky-agent.ts index 606e06dcda8..291cf8608c4 100644 --- a/packages/api/src/bsky-agent.ts +++ b/packages/api/src/bsky-agent.ts @@ -327,6 +327,8 @@ export class BskyAgent extends AtpAgent { interests: { tags: [], }, + mutedWords: [], + hiddenPosts: [], } const res = await this.app.bsky.actor.getPreferences({}) for (const pref of res.data.preferences) { @@ -380,6 +382,20 @@ export class BskyAgent extends AtpAgent { // eslint-disable-next-line @typescript-eslint/no-unused-vars const { $type, ...v } = pref prefs.interests = { ...prefs.interests, ...v } + } else if ( + AppBskyActorDefs.isMutedWordsPref(pref) && + AppBskyActorDefs.validateMutedWordsPref(pref).success + ) { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { $type, ...v } = pref + prefs.mutedWords = v.items + } else if ( + AppBskyActorDefs.isHiddenPostsPref(pref) && + AppBskyActorDefs.validateHiddenPostsPref(pref).success + ) { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { $type, ...v } = pref + prefs.hiddenPosts = v.items } } return prefs @@ -548,6 +564,26 @@ export class BskyAgent extends AtpAgent { .concat([{ ...pref, $type: 'app.bsky.actor.defs#interestsPref' }]) }) } + + async upsertMutedWords(mutedWords: AppBskyActorDefs.MutedWord[]) { + await updateMutedWords(this, mutedWords, 'upsert') + } + + async updateMutedWord(mutedWord: AppBskyActorDefs.MutedWord) { + await updateMutedWords(this, [mutedWord], 'update') + } + + async removeMutedWord(mutedWord: AppBskyActorDefs.MutedWord) { + await updateMutedWords(this, [mutedWord], 'remove') + } + + async hidePost(postUri: string) { + await updateHiddenPost(this, postUri, 'hide') + } + + async unhidePost(postUri: string) { + await updateHiddenPost(this, postUri, 'unhide') + } } /** @@ -609,3 +645,103 @@ async function updateFeedPreferences( }) return res } + +/** + * A helper specifically for updating muted words preferences + */ +async function updateMutedWords( + agent: BskyAgent, + mutedWords: AppBskyActorDefs.MutedWord[], + action: 'upsert' | 'update' | 'remove', +) { + const sanitizeMutedWord = (word: AppBskyActorDefs.MutedWord) => ({ + value: word.value.replace(/^#/, ''), + targets: word.targets, + }) + + await updatePreferences(agent, (prefs: AppBskyActorDefs.Preferences) => { + let mutedWordsPref = prefs.findLast( + (pref) => + AppBskyActorDefs.isMutedWordsPref(pref) && + AppBskyActorDefs.validateMutedWordsPref(pref).success, + ) + + if (mutedWordsPref && AppBskyActorDefs.isMutedWordsPref(mutedWordsPref)) { + if (action === 'upsert' || action === 'update') { + for (const newItem of mutedWords) { + let foundMatch = false + + for (const existingItem of mutedWordsPref.items) { + if (existingItem.value === newItem.value) { + existingItem.targets = + action === 'upsert' + ? Array.from( + new Set([...existingItem.targets, ...newItem.targets]), + ) + : newItem.targets + foundMatch = true + break + } + } + + if (action === 'upsert' && !foundMatch) { + mutedWordsPref.items.push(sanitizeMutedWord(newItem)) + } + } + } else if (action === 'remove') { + for (const word of mutedWords) { + for (let i = 0; i < mutedWordsPref.items.length; i++) { + const existing = mutedWordsPref.items[i] + if (existing.value === sanitizeMutedWord(word).value) { + mutedWordsPref.items.splice(i, 1) + break + } + } + } + } + } else { + // if the pref doesn't exist, create it + if (action === 'upsert') { + mutedWordsPref = { + items: mutedWords.map(sanitizeMutedWord), + } + } + } + + return prefs + .filter((p) => !AppBskyActorDefs.isMutedWordsPref(p)) + .concat([ + { ...mutedWordsPref, $type: 'app.bsky.actor.defs#mutedWordsPref' }, + ]) + }) +} + +async function updateHiddenPost( + agent: BskyAgent, + postUri: string, + action: 'hide' | 'unhide', +) { + await updatePreferences(agent, (prefs: AppBskyActorDefs.Preferences) => { + let pref = prefs.findLast( + (pref) => + AppBskyActorDefs.isHiddenPostsPref(pref) && + AppBskyActorDefs.validateHiddenPostsPref(pref).success, + ) + if (pref && AppBskyActorDefs.isHiddenPostsPref(pref)) { + pref.items = + action === 'hide' + ? Array.from(new Set([...pref.items, postUri])) + : pref.items.filter((uri) => uri !== postUri) + } else { + if (action === 'hide') { + pref = { + $type: 'app.bsky.actor.defs#hiddenPostsPref', + items: [postUri], + } + } + } + return prefs + .filter((p) => !AppBskyActorDefs.isInterestsPref(p)) + .concat([{ ...pref, $type: 'app.bsky.actor.defs#hiddenPostsPref' }]) + }) +} diff --git a/packages/api/src/client/lexicons.ts b/packages/api/src/client/lexicons.ts index 01a2f683d77..c8a8ebde670 100644 --- a/packages/api/src/client/lexicons.ts +++ b/packages/api/src/client/lexicons.ts @@ -4974,6 +4974,62 @@ export const schemaDict = { }, }, }, + mutedWordTarget: { + type: 'string', + knownValues: ['content', 'tag'], + maxLength: 640, + maxGraphemes: 64, + }, + mutedWord: { + type: 'object', + description: 'A word that the account owner has muted.', + required: ['value', 'targets'], + properties: { + value: { + type: 'string', + description: 'The muted word itself.', + maxLength: 10000, + maxGraphemes: 1000, + }, + targets: { + type: 'array', + description: 'The intended targets of the muted word.', + items: { + type: 'ref', + ref: 'lex:app.bsky.actor.defs#mutedWordTarget', + }, + }, + }, + }, + mutedWordsPref: { + type: 'object', + required: ['items'], + properties: { + items: { + type: 'array', + items: { + type: 'ref', + ref: 'lex:app.bsky.actor.defs#mutedWord', + }, + description: 'A list of words the account owner has muted.', + }, + }, + }, + hiddenPostsPref: { + type: 'object', + required: ['items'], + properties: { + items: { + type: 'array', + items: { + type: 'string', + format: 'at-uri', + }, + description: + 'A list of URIs of posts the account owner has hidden.', + }, + }, + }, }, }, AppBskyActorGetPreferences: { @@ -6884,7 +6940,8 @@ export const schemaDict = { }, tags: { type: 'array', - description: 'Additional non-inline tags describing this post.', + description: + 'Additional hashtags, in addition to any included in post text and facets.', maxLength: 8, items: { 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 5f493576334..535cfe9094f 100644 --- a/packages/api/src/client/types/app/bsky/actor/defs.ts +++ b/packages/api/src/client/types/app/bsky/actor/defs.ts @@ -254,3 +254,62 @@ export function isInterestsPref(v: unknown): v is InterestsPref { export function validateInterestsPref(v: unknown): ValidationResult { return lexicons.validate('app.bsky.actor.defs#interestsPref', v) } + +export type MutedWordTarget = 'content' | 'tag' | (string & {}) + +/** A word that the account owner has muted. */ +export interface MutedWord { + /** The muted word itself. */ + value: string + /** The intended targets of the muted word. */ + targets: MutedWordTarget[] + [k: string]: unknown +} + +export function isMutedWord(v: unknown): v is MutedWord { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'app.bsky.actor.defs#mutedWord' + ) +} + +export function validateMutedWord(v: unknown): ValidationResult { + return lexicons.validate('app.bsky.actor.defs#mutedWord', v) +} + +export interface MutedWordsPref { + /** A list of words the account owner has muted. */ + items: MutedWord[] + [k: string]: unknown +} + +export function isMutedWordsPref(v: unknown): v is MutedWordsPref { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'app.bsky.actor.defs#mutedWordsPref' + ) +} + +export function validateMutedWordsPref(v: unknown): ValidationResult { + return lexicons.validate('app.bsky.actor.defs#mutedWordsPref', v) +} + +export interface HiddenPostsPref { + /** A list of URIs of posts the account owner has hidden. */ + items: string[] + [k: string]: unknown +} + +export function isHiddenPostsPref(v: unknown): v is HiddenPostsPref { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'app.bsky.actor.defs#hiddenPostsPref' + ) +} + +export function validateHiddenPostsPref(v: unknown): ValidationResult { + return lexicons.validate('app.bsky.actor.defs#hiddenPostsPref', v) +} diff --git a/packages/api/src/client/types/app/bsky/feed/post.ts b/packages/api/src/client/types/app/bsky/feed/post.ts index 401510f9ef9..0de5192af77 100644 --- a/packages/api/src/client/types/app/bsky/feed/post.ts +++ b/packages/api/src/client/types/app/bsky/feed/post.ts @@ -32,7 +32,7 @@ export interface Record { labels?: | ComAtprotoLabelDefs.SelfLabels | { $type: string; [k: string]: unknown } - /** Additional non-inline tags describing this post. */ + /** Additional hashtags, in addition to any included in post text and facets. */ tags?: string[] /** Client-declared timestamp when this post was originally created. */ createdAt: string diff --git a/packages/api/src/types.ts b/packages/api/src/types.ts index 3d6f73baa33..7e36e77de58 100644 --- a/packages/api/src/types.ts +++ b/packages/api/src/types.ts @@ -1,3 +1,4 @@ +import { AppBskyActorNS, AppBskyActorDefs } from './client' import { LabelPreference } from './moderation/types' /** @@ -119,4 +120,6 @@ export interface BskyPreferences { contentLabels: Record birthDate: Date | undefined interests: BskyInterestsPreference + mutedWords: AppBskyActorDefs.MutedWord[] + hiddenPosts: string[] } diff --git a/packages/api/tests/bsky-agent.test.ts b/packages/api/tests/bsky-agent.test.ts index 5f850b19e91..edaf3912ec7 100644 --- a/packages/api/tests/bsky-agent.test.ts +++ b/packages/api/tests/bsky-agent.test.ts @@ -239,6 +239,8 @@ describe('agent', () => { interests: { tags: [], }, + mutedWords: [], + hiddenPosts: [], }) await agent.setAdultContentEnabled(true) @@ -263,6 +265,8 @@ describe('agent', () => { interests: { tags: [], }, + mutedWords: [], + hiddenPosts: [], }) await agent.setAdultContentEnabled(false) @@ -287,6 +291,8 @@ describe('agent', () => { interests: { tags: [], }, + mutedWords: [], + hiddenPosts: [], }) await agent.setContentLabelPref('impersonation', 'warn') @@ -313,6 +319,8 @@ describe('agent', () => { interests: { tags: [], }, + mutedWords: [], + hiddenPosts: [], }) await agent.setContentLabelPref('spam', 'show') // will convert to 'ignore' @@ -341,6 +349,8 @@ describe('agent', () => { interests: { tags: [], }, + mutedWords: [], + hiddenPosts: [], }) await agent.addSavedFeed('at://bob.com/app.bsky.feed.generator/fake') @@ -371,6 +381,8 @@ describe('agent', () => { interests: { tags: [], }, + mutedWords: [], + hiddenPosts: [], }) await agent.addPinnedFeed('at://bob.com/app.bsky.feed.generator/fake') @@ -401,6 +413,8 @@ describe('agent', () => { interests: { tags: [], }, + mutedWords: [], + hiddenPosts: [], }) await agent.removePinnedFeed('at://bob.com/app.bsky.feed.generator/fake') @@ -431,6 +445,8 @@ describe('agent', () => { interests: { tags: [], }, + mutedWords: [], + hiddenPosts: [], }) await agent.removeSavedFeed('at://bob.com/app.bsky.feed.generator/fake') @@ -461,6 +477,8 @@ describe('agent', () => { interests: { tags: [], }, + mutedWords: [], + hiddenPosts: [], }) await agent.addPinnedFeed('at://bob.com/app.bsky.feed.generator/fake') @@ -491,6 +509,8 @@ describe('agent', () => { interests: { tags: [], }, + mutedWords: [], + hiddenPosts: [], }) await agent.addPinnedFeed('at://bob.com/app.bsky.feed.generator/fake2') @@ -527,6 +547,8 @@ describe('agent', () => { interests: { tags: [], }, + mutedWords: [], + hiddenPosts: [], }) await agent.removeSavedFeed('at://bob.com/app.bsky.feed.generator/fake') @@ -557,6 +579,8 @@ describe('agent', () => { interests: { tags: [], }, + mutedWords: [], + hiddenPosts: [], }) await agent.setPersonalDetails({ birthDate: '2023-09-11T18:05:42.556Z' }) @@ -587,6 +611,8 @@ describe('agent', () => { interests: { tags: [], }, + mutedWords: [], + hiddenPosts: [], }) await agent.setFeedViewPrefs('home', { hideReplies: true }) @@ -617,6 +643,8 @@ describe('agent', () => { interests: { tags: [], }, + mutedWords: [], + hiddenPosts: [], }) await agent.setFeedViewPrefs('home', { hideReplies: false }) @@ -647,6 +675,8 @@ describe('agent', () => { interests: { tags: [], }, + mutedWords: [], + hiddenPosts: [], }) await agent.setFeedViewPrefs('other', { hideReplies: true }) @@ -684,6 +714,8 @@ describe('agent', () => { interests: { tags: [], }, + mutedWords: [], + hiddenPosts: [], }) await agent.setThreadViewPrefs({ sort: 'random' }) @@ -721,6 +753,8 @@ describe('agent', () => { interests: { tags: [], }, + mutedWords: [], + hiddenPosts: [], }) await agent.setThreadViewPrefs({ sort: 'oldest' }) @@ -758,6 +792,8 @@ describe('agent', () => { interests: { tags: [], }, + mutedWords: [], + hiddenPosts: [], }) await agent.setInterestsPref({ tags: ['foo', 'bar'] }) @@ -795,6 +831,8 @@ describe('agent', () => { interests: { tags: ['foo', 'bar'], }, + mutedWords: [], + hiddenPosts: [], }) }) @@ -921,6 +959,8 @@ describe('agent', () => { interests: { tags: [], }, + mutedWords: [], + hiddenPosts: [], }) await agent.setAdultContentEnabled(false) @@ -950,6 +990,8 @@ describe('agent', () => { interests: { tags: [], }, + mutedWords: [], + hiddenPosts: [], }) await agent.setContentLabelPref('nsfw', 'hide') @@ -979,6 +1021,8 @@ describe('agent', () => { interests: { tags: [], }, + mutedWords: [], + hiddenPosts: [], }) await agent.addPinnedFeed('at://bob.com/app.bsky.feed.generator/fake') @@ -1008,6 +1052,8 @@ describe('agent', () => { interests: { tags: [], }, + mutedWords: [], + hiddenPosts: [], }) await agent.setPersonalDetails({ birthDate: '2023-09-11T18:05:42.556Z' }) @@ -1037,6 +1083,8 @@ describe('agent', () => { interests: { tags: [], }, + mutedWords: [], + hiddenPosts: [], }) await agent.setFeedViewPrefs('home', { @@ -1077,6 +1125,8 @@ describe('agent', () => { interests: { tags: [], }, + mutedWords: [], + hiddenPosts: [], }) const res = await agent.app.bsky.actor.getPreferences() @@ -1118,6 +1168,139 @@ describe('agent', () => { ].sort(byType), ) }) + + 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 }) + await agent.createAccount({ + handle: 'user7.test', + email: 'user7@test.com', + password: 'password', + }) + }) + + it('upsertMutedWords', async () => { + await agent.upsertMutedWords(mutedWords) + await agent.upsertMutedWords(mutedWords) // double + await expect(agent.getPreferences()).resolves.toHaveProperty( + 'mutedWords', + mutedWords, + ) + }) + + it('upsertMutedWords with #', async () => { + await agent.upsertMutedWords([{ value: '#hashtag', targets: ['tag'] }]) + const { mutedWords } = await agent.getPreferences() + expect(mutedWords.find((m) => m.value === '#hashtag')).toBeFalsy() + expect(mutedWords.find((m) => m.value === 'hashtag')).toBeTruthy() + }) + + 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() + + 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 #', async () => { + await agent.updateMutedWord({ + value: 'hashtag', + targets: ['tag', 'content'], + }) + const { mutedWords } = await agent.getPreferences() + expect(mutedWords.find((m) => m.value === 'hashtag')).toStrictEqual({ + value: 'hashtag', + targets: ['tag', 'content'], + }) + }) + + 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() + + 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('removeMutedWord with #', async () => { + await agent.removeMutedWord({ value: '#hashtag', targets: [] }) + const { mutedWords } = await agent.getPreferences() + + expect(mutedWords.find((m) => m.value === 'hashtag')).toBeFalsy() + }) + }) + + describe('hidden posts', () => { + let agent: BskyAgent + const postUri = 'at://did:plc:fake/app.bsky.feed.post/fake' + + beforeAll(async () => { + agent = new BskyAgent({ service: network.pds.url }) + await agent.createAccount({ + handle: 'user8.test', + email: 'user8@test.com', + password: 'password', + }) + }) + + it('hidePost', async () => { + await agent.hidePost(postUri) + await agent.hidePost(postUri) // double, should dedupe + await expect(agent.getPreferences()).resolves.toHaveProperty( + 'hiddenPosts', + [postUri], + ) + }) + + it('unhidePost', async () => { + await agent.unhidePost(postUri) + await expect(agent.getPreferences()).resolves.toHaveProperty( + 'hiddenPosts', + [], + ) + // no issues calling a second time + await agent.unhidePost(postUri) + await expect(agent.getPreferences()).resolves.toHaveProperty( + 'hiddenPosts', + [], + ) + }) + }) + + // end }) }) diff --git a/packages/bsky/src/lexicon/lexicons.ts b/packages/bsky/src/lexicon/lexicons.ts index 01a2f683d77..c8a8ebde670 100644 --- a/packages/bsky/src/lexicon/lexicons.ts +++ b/packages/bsky/src/lexicon/lexicons.ts @@ -4974,6 +4974,62 @@ export const schemaDict = { }, }, }, + mutedWordTarget: { + type: 'string', + knownValues: ['content', 'tag'], + maxLength: 640, + maxGraphemes: 64, + }, + mutedWord: { + type: 'object', + description: 'A word that the account owner has muted.', + required: ['value', 'targets'], + properties: { + value: { + type: 'string', + description: 'The muted word itself.', + maxLength: 10000, + maxGraphemes: 1000, + }, + targets: { + type: 'array', + description: 'The intended targets of the muted word.', + items: { + type: 'ref', + ref: 'lex:app.bsky.actor.defs#mutedWordTarget', + }, + }, + }, + }, + mutedWordsPref: { + type: 'object', + required: ['items'], + properties: { + items: { + type: 'array', + items: { + type: 'ref', + ref: 'lex:app.bsky.actor.defs#mutedWord', + }, + description: 'A list of words the account owner has muted.', + }, + }, + }, + hiddenPostsPref: { + type: 'object', + required: ['items'], + properties: { + items: { + type: 'array', + items: { + type: 'string', + format: 'at-uri', + }, + description: + 'A list of URIs of posts the account owner has hidden.', + }, + }, + }, }, }, AppBskyActorGetPreferences: { @@ -6884,7 +6940,8 @@ export const schemaDict = { }, tags: { type: 'array', - description: 'Additional non-inline tags describing this post.', + description: + 'Additional hashtags, in addition to any included in post text and facets.', maxLength: 8, items: { 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 a802f70f44b..50afff62a0f 100644 --- a/packages/bsky/src/lexicon/types/app/bsky/actor/defs.ts +++ b/packages/bsky/src/lexicon/types/app/bsky/actor/defs.ts @@ -254,3 +254,62 @@ export function isInterestsPref(v: unknown): v is InterestsPref { export function validateInterestsPref(v: unknown): ValidationResult { return lexicons.validate('app.bsky.actor.defs#interestsPref', v) } + +export type MutedWordTarget = 'content' | 'tag' | (string & {}) + +/** A word that the account owner has muted. */ +export interface MutedWord { + /** The muted word itself. */ + value: string + /** The intended targets of the muted word. */ + targets: MutedWordTarget[] + [k: string]: unknown +} + +export function isMutedWord(v: unknown): v is MutedWord { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'app.bsky.actor.defs#mutedWord' + ) +} + +export function validateMutedWord(v: unknown): ValidationResult { + return lexicons.validate('app.bsky.actor.defs#mutedWord', v) +} + +export interface MutedWordsPref { + /** A list of words the account owner has muted. */ + items: MutedWord[] + [k: string]: unknown +} + +export function isMutedWordsPref(v: unknown): v is MutedWordsPref { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'app.bsky.actor.defs#mutedWordsPref' + ) +} + +export function validateMutedWordsPref(v: unknown): ValidationResult { + return lexicons.validate('app.bsky.actor.defs#mutedWordsPref', v) +} + +export interface HiddenPostsPref { + /** A list of URIs of posts the account owner has hidden. */ + items: string[] + [k: string]: unknown +} + +export function isHiddenPostsPref(v: unknown): v is HiddenPostsPref { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'app.bsky.actor.defs#hiddenPostsPref' + ) +} + +export function validateHiddenPostsPref(v: unknown): ValidationResult { + return lexicons.validate('app.bsky.actor.defs#hiddenPostsPref', v) +} diff --git a/packages/bsky/src/lexicon/types/app/bsky/feed/post.ts b/packages/bsky/src/lexicon/types/app/bsky/feed/post.ts index c30825e118a..881e3d199aa 100644 --- a/packages/bsky/src/lexicon/types/app/bsky/feed/post.ts +++ b/packages/bsky/src/lexicon/types/app/bsky/feed/post.ts @@ -32,7 +32,7 @@ export interface Record { labels?: | ComAtprotoLabelDefs.SelfLabels | { $type: string; [k: string]: unknown } - /** Additional non-inline tags describing this post. */ + /** Additional hashtags, in addition to any included in post text and facets. */ tags?: string[] /** Client-declared timestamp when this post was originally created. */ createdAt: string diff --git a/packages/ozone/src/lexicon/lexicons.ts b/packages/ozone/src/lexicon/lexicons.ts index 01a2f683d77..c8a8ebde670 100644 --- a/packages/ozone/src/lexicon/lexicons.ts +++ b/packages/ozone/src/lexicon/lexicons.ts @@ -4974,6 +4974,62 @@ export const schemaDict = { }, }, }, + mutedWordTarget: { + type: 'string', + knownValues: ['content', 'tag'], + maxLength: 640, + maxGraphemes: 64, + }, + mutedWord: { + type: 'object', + description: 'A word that the account owner has muted.', + required: ['value', 'targets'], + properties: { + value: { + type: 'string', + description: 'The muted word itself.', + maxLength: 10000, + maxGraphemes: 1000, + }, + targets: { + type: 'array', + description: 'The intended targets of the muted word.', + items: { + type: 'ref', + ref: 'lex:app.bsky.actor.defs#mutedWordTarget', + }, + }, + }, + }, + mutedWordsPref: { + type: 'object', + required: ['items'], + properties: { + items: { + type: 'array', + items: { + type: 'ref', + ref: 'lex:app.bsky.actor.defs#mutedWord', + }, + description: 'A list of words the account owner has muted.', + }, + }, + }, + hiddenPostsPref: { + type: 'object', + required: ['items'], + properties: { + items: { + type: 'array', + items: { + type: 'string', + format: 'at-uri', + }, + description: + 'A list of URIs of posts the account owner has hidden.', + }, + }, + }, }, }, AppBskyActorGetPreferences: { @@ -6884,7 +6940,8 @@ export const schemaDict = { }, tags: { type: 'array', - description: 'Additional non-inline tags describing this post.', + description: + 'Additional hashtags, in addition to any included in post text and facets.', maxLength: 8, items: { 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 a802f70f44b..50afff62a0f 100644 --- a/packages/ozone/src/lexicon/types/app/bsky/actor/defs.ts +++ b/packages/ozone/src/lexicon/types/app/bsky/actor/defs.ts @@ -254,3 +254,62 @@ export function isInterestsPref(v: unknown): v is InterestsPref { export function validateInterestsPref(v: unknown): ValidationResult { return lexicons.validate('app.bsky.actor.defs#interestsPref', v) } + +export type MutedWordTarget = 'content' | 'tag' | (string & {}) + +/** A word that the account owner has muted. */ +export interface MutedWord { + /** The muted word itself. */ + value: string + /** The intended targets of the muted word. */ + targets: MutedWordTarget[] + [k: string]: unknown +} + +export function isMutedWord(v: unknown): v is MutedWord { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'app.bsky.actor.defs#mutedWord' + ) +} + +export function validateMutedWord(v: unknown): ValidationResult { + return lexicons.validate('app.bsky.actor.defs#mutedWord', v) +} + +export interface MutedWordsPref { + /** A list of words the account owner has muted. */ + items: MutedWord[] + [k: string]: unknown +} + +export function isMutedWordsPref(v: unknown): v is MutedWordsPref { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'app.bsky.actor.defs#mutedWordsPref' + ) +} + +export function validateMutedWordsPref(v: unknown): ValidationResult { + return lexicons.validate('app.bsky.actor.defs#mutedWordsPref', v) +} + +export interface HiddenPostsPref { + /** A list of URIs of posts the account owner has hidden. */ + items: string[] + [k: string]: unknown +} + +export function isHiddenPostsPref(v: unknown): v is HiddenPostsPref { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'app.bsky.actor.defs#hiddenPostsPref' + ) +} + +export function validateHiddenPostsPref(v: unknown): ValidationResult { + return lexicons.validate('app.bsky.actor.defs#hiddenPostsPref', v) +} diff --git a/packages/ozone/src/lexicon/types/app/bsky/feed/post.ts b/packages/ozone/src/lexicon/types/app/bsky/feed/post.ts index c30825e118a..881e3d199aa 100644 --- a/packages/ozone/src/lexicon/types/app/bsky/feed/post.ts +++ b/packages/ozone/src/lexicon/types/app/bsky/feed/post.ts @@ -32,7 +32,7 @@ export interface Record { labels?: | ComAtprotoLabelDefs.SelfLabels | { $type: string; [k: string]: unknown } - /** Additional non-inline tags describing this post. */ + /** Additional hashtags, in addition to any included in post text and facets. */ tags?: string[] /** Client-declared timestamp when this post was originally created. */ createdAt: string diff --git a/packages/pds/src/lexicon/lexicons.ts b/packages/pds/src/lexicon/lexicons.ts index 01a2f683d77..c8a8ebde670 100644 --- a/packages/pds/src/lexicon/lexicons.ts +++ b/packages/pds/src/lexicon/lexicons.ts @@ -4974,6 +4974,62 @@ export const schemaDict = { }, }, }, + mutedWordTarget: { + type: 'string', + knownValues: ['content', 'tag'], + maxLength: 640, + maxGraphemes: 64, + }, + mutedWord: { + type: 'object', + description: 'A word that the account owner has muted.', + required: ['value', 'targets'], + properties: { + value: { + type: 'string', + description: 'The muted word itself.', + maxLength: 10000, + maxGraphemes: 1000, + }, + targets: { + type: 'array', + description: 'The intended targets of the muted word.', + items: { + type: 'ref', + ref: 'lex:app.bsky.actor.defs#mutedWordTarget', + }, + }, + }, + }, + mutedWordsPref: { + type: 'object', + required: ['items'], + properties: { + items: { + type: 'array', + items: { + type: 'ref', + ref: 'lex:app.bsky.actor.defs#mutedWord', + }, + description: 'A list of words the account owner has muted.', + }, + }, + }, + hiddenPostsPref: { + type: 'object', + required: ['items'], + properties: { + items: { + type: 'array', + items: { + type: 'string', + format: 'at-uri', + }, + description: + 'A list of URIs of posts the account owner has hidden.', + }, + }, + }, }, }, AppBskyActorGetPreferences: { @@ -6884,7 +6940,8 @@ export const schemaDict = { }, tags: { type: 'array', - description: 'Additional non-inline tags describing this post.', + description: + 'Additional hashtags, in addition to any included in post text and facets.', maxLength: 8, items: { 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 a802f70f44b..50afff62a0f 100644 --- a/packages/pds/src/lexicon/types/app/bsky/actor/defs.ts +++ b/packages/pds/src/lexicon/types/app/bsky/actor/defs.ts @@ -254,3 +254,62 @@ export function isInterestsPref(v: unknown): v is InterestsPref { export function validateInterestsPref(v: unknown): ValidationResult { return lexicons.validate('app.bsky.actor.defs#interestsPref', v) } + +export type MutedWordTarget = 'content' | 'tag' | (string & {}) + +/** A word that the account owner has muted. */ +export interface MutedWord { + /** The muted word itself. */ + value: string + /** The intended targets of the muted word. */ + targets: MutedWordTarget[] + [k: string]: unknown +} + +export function isMutedWord(v: unknown): v is MutedWord { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'app.bsky.actor.defs#mutedWord' + ) +} + +export function validateMutedWord(v: unknown): ValidationResult { + return lexicons.validate('app.bsky.actor.defs#mutedWord', v) +} + +export interface MutedWordsPref { + /** A list of words the account owner has muted. */ + items: MutedWord[] + [k: string]: unknown +} + +export function isMutedWordsPref(v: unknown): v is MutedWordsPref { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'app.bsky.actor.defs#mutedWordsPref' + ) +} + +export function validateMutedWordsPref(v: unknown): ValidationResult { + return lexicons.validate('app.bsky.actor.defs#mutedWordsPref', v) +} + +export interface HiddenPostsPref { + /** A list of URIs of posts the account owner has hidden. */ + items: string[] + [k: string]: unknown +} + +export function isHiddenPostsPref(v: unknown): v is HiddenPostsPref { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'app.bsky.actor.defs#hiddenPostsPref' + ) +} + +export function validateHiddenPostsPref(v: unknown): ValidationResult { + return lexicons.validate('app.bsky.actor.defs#hiddenPostsPref', v) +} diff --git a/packages/pds/src/lexicon/types/app/bsky/feed/post.ts b/packages/pds/src/lexicon/types/app/bsky/feed/post.ts index c30825e118a..881e3d199aa 100644 --- a/packages/pds/src/lexicon/types/app/bsky/feed/post.ts +++ b/packages/pds/src/lexicon/types/app/bsky/feed/post.ts @@ -32,7 +32,7 @@ export interface Record { labels?: | ComAtprotoLabelDefs.SelfLabels | { $type: string; [k: string]: unknown } - /** Additional non-inline tags describing this post. */ + /** Additional hashtags, in addition to any included in post text and facets. */ tags?: string[] /** Client-declared timestamp when this post was originally created. */ createdAt: string