@atproto/api 0.20.22 → 0.20.25

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/dist/client/lexicons.d.ts +28 -12
  3. package/dist/client/lexicons.d.ts.map +1 -1
  4. package/dist/client/lexicons.js +14 -6
  5. package/dist/client/lexicons.js.map +1 -1
  6. package/dist/client/types/app/bsky/ageassurance/defs.d.ts +2 -0
  7. package/dist/client/types/app/bsky/ageassurance/defs.d.ts.map +1 -1
  8. package/dist/client/types/app/bsky/ageassurance/defs.js.map +1 -1
  9. package/package.json +22 -16
  10. package/definitions/labels.json +0 -170
  11. package/docs/moderation.md +0 -260
  12. package/jest.config.cjs +0 -23
  13. package/jest.d.ts +0 -20
  14. package/jest.setup.ts +0 -97
  15. package/scripts/code/labels.mjs +0 -77
  16. package/scripts/generate-code.mjs +0 -4
  17. package/src/age-assurance.test.ts +0 -218
  18. package/src/age-assurance.ts +0 -137
  19. package/src/agent.ts +0 -1645
  20. package/src/atp-agent.ts +0 -570
  21. package/src/bsky-agent.ts +0 -14
  22. package/src/const.ts +0 -1
  23. package/src/index.ts +0 -44
  24. package/src/mocker.ts +0 -220
  25. package/src/moderation/decision.ts +0 -389
  26. package/src/moderation/index.ts +0 -69
  27. package/src/moderation/mutewords.ts +0 -170
  28. package/src/moderation/subjects/account.ts +0 -44
  29. package/src/moderation/subjects/feed-generator.ts +0 -24
  30. package/src/moderation/subjects/notification.ts +0 -25
  31. package/src/moderation/subjects/post.ts +0 -433
  32. package/src/moderation/subjects/profile.ts +0 -26
  33. package/src/moderation/subjects/status.ts +0 -27
  34. package/src/moderation/subjects/user-list.ts +0 -48
  35. package/src/moderation/types.ts +0 -191
  36. package/src/moderation/ui.ts +0 -21
  37. package/src/moderation/util.ts +0 -111
  38. package/src/predicate.ts +0 -52
  39. package/src/rich-text/detection.ts +0 -146
  40. package/src/rich-text/rich-text.ts +0 -418
  41. package/src/rich-text/sanitization.ts +0 -42
  42. package/src/rich-text/unicode.ts +0 -47
  43. package/src/rich-text/util.ts +0 -15
  44. package/src/session-manager.ts +0 -5
  45. package/src/types.ts +0 -165
  46. package/src/util.ts +0 -96
  47. package/tests/atp-agent.test.ts +0 -3863
  48. package/tests/dispatcher.test.ts +0 -527
  49. package/tests/errors.test.ts +0 -29
  50. package/tests/moderation-behaviors.test.ts +0 -951
  51. package/tests/moderation-custom-labels.test.ts +0 -334
  52. package/tests/moderation-mutewords.test.ts +0 -1299
  53. package/tests/moderation-prefs.test.ts +0 -402
  54. package/tests/moderation-quoteposts.test.ts +0 -277
  55. package/tests/moderation.test.ts +0 -739
  56. package/tests/rich-text-detection.test.ts +0 -456
  57. package/tests/rich-text-sanitization.test.ts +0 -216
  58. package/tests/rich-text.test.ts +0 -705
  59. package/tests/util/echo-server.ts +0 -37
  60. package/tests/util/moderation-behavior.ts +0 -268
  61. package/tsconfig.build.json +0 -9
  62. package/tsconfig.build.tsbuildinfo +0 -1
  63. package/tsconfig.json +0 -7
  64. package/tsconfig.tests.json +0 -10
@@ -1,170 +0,0 @@
1
- import { AppBskyActorDefs, AppBskyRichtextFacet } from '../client/index.js'
2
-
3
- const REGEX = {
4
- LEADING_TRAILING_PUNCTUATION: /(?:^\p{P}+|\p{P}+$)/gu,
5
- ESCAPE: /[[\]{}()*+?.\\^$|\s]/g,
6
- SEPARATORS: /[/\-–—()[\]_]+/g,
7
- WORD_BOUNDARY: /[\s\n\t\r\f\v]+?/g,
8
- }
9
-
10
- /**
11
- * List of 2-letter lang codes for languages that either don't use spaces, or
12
- * don't use spaces in a way conducive to word-based filtering.
13
- *
14
- * For these, we use a simple `String.includes` to check for a match.
15
- */
16
- const LANGUAGE_EXCEPTIONS = [
17
- 'ja', // Japanese
18
- 'zh', // Chinese
19
- 'ko', // Korean
20
- 'th', // Thai
21
- 'vi', // Vietnamese
22
- ]
23
-
24
- export type MuteWordMatch = {
25
- /**
26
- * The `AppBskyActorDefs.MutedWord` that matched.
27
- */
28
- word: AppBskyActorDefs.MutedWord
29
- /**
30
- * The string that matched the muted word.
31
- */
32
- predicate: string
33
- }
34
-
35
- export type Params = {
36
- mutedWords: AppBskyActorDefs.MutedWord[]
37
- text: string
38
- facets?: AppBskyRichtextFacet.Main[]
39
- outlineTags?: string[]
40
- languages?: string[]
41
- actor?: AppBskyActorDefs.ProfileView | AppBskyActorDefs.ProfileViewBasic
42
- }
43
-
44
- /**
45
- * Checks if the given text matches any of the muted words, returning an array
46
- * of matches. If no matches are found, returns `undefined`.
47
- */
48
- export function matchMuteWords({
49
- mutedWords,
50
- text,
51
- facets,
52
- outlineTags,
53
- languages,
54
- actor,
55
- }: Params): MuteWordMatch[] | undefined {
56
- const exception = LANGUAGE_EXCEPTIONS.includes(languages?.[0] || '')
57
- const tags = ([] as string[])
58
- .concat(outlineTags || [])
59
- .concat(
60
- (facets || []).flatMap((facet) =>
61
- facet.features.filter(AppBskyRichtextFacet.isTag).map((tag) => tag.tag),
62
- ),
63
- )
64
- .map((t) => t.toLowerCase())
65
-
66
- const matches: MuteWordMatch[] = []
67
-
68
- outer: for (const muteWord of mutedWords) {
69
- const mutedWord = muteWord.value.toLowerCase()
70
- const postText = text.toLowerCase()
71
-
72
- // expired, ignore
73
- if (muteWord.expiresAt && muteWord.expiresAt < new Date().toISOString())
74
- continue
75
-
76
- if (
77
- muteWord.actorTarget === 'exclude-following' &&
78
- Boolean(actor?.viewer?.following)
79
- )
80
- continue
81
-
82
- // `content` applies to tags as well
83
- if (tags.includes(mutedWord)) {
84
- matches.push({ word: muteWord, predicate: muteWord.value })
85
- continue
86
- }
87
- // rest of the checks are for `content` only
88
- if (!muteWord.targets.includes('content')) continue
89
- // single character or other exception, has to use includes
90
- if ((mutedWord.length === 1 || exception) && postText.includes(mutedWord)) {
91
- matches.push({ word: muteWord, predicate: muteWord.value })
92
- continue
93
- }
94
- // too long
95
- if (mutedWord.length > postText.length) continue
96
- // exact match
97
- if (mutedWord === postText) {
98
- matches.push({ word: muteWord, predicate: muteWord.value })
99
- continue
100
- }
101
- // any muted phrase with space or punctuation
102
- if (/(?:\s|\p{P})+?/u.test(mutedWord) && postText.includes(mutedWord)) {
103
- matches.push({ word: muteWord, predicate: muteWord.value })
104
- continue
105
- }
106
-
107
- // check individual character groups
108
- const words = postText.split(REGEX.WORD_BOUNDARY)
109
- for (const word of words) {
110
- if (word === mutedWord) {
111
- matches.push({ word: muteWord, predicate: word })
112
- continue outer
113
- }
114
-
115
- // compare word without leading/trailing punctuation, but allow internal
116
- // punctuation (such as `s@ssy`)
117
- const wordTrimmedPunctuation = word.replace(
118
- REGEX.LEADING_TRAILING_PUNCTUATION,
119
- '',
120
- )
121
-
122
- if (mutedWord === wordTrimmedPunctuation) {
123
- matches.push({ word: muteWord, predicate: word })
124
- continue outer
125
- }
126
-
127
- if (mutedWord.length > wordTrimmedPunctuation.length) continue
128
-
129
- if (/\p{P}+/u.test(wordTrimmedPunctuation)) {
130
- /**
131
- * Exit case for any punctuation within the predicate that we _do_
132
- * allow e.g. `and/or` should not match `Andor`.
133
- */
134
- if (/[/]+/.test(wordTrimmedPunctuation)) {
135
- continue outer
136
- }
137
-
138
- const spacedWord = wordTrimmedPunctuation.replace(/\p{P}+/gu, ' ')
139
- if (spacedWord === mutedWord) {
140
- matches.push({ word: muteWord, predicate: word })
141
- continue outer
142
- }
143
-
144
- const contiguousWord = spacedWord.replace(/\s/gu, '')
145
- if (contiguousWord === mutedWord) {
146
- matches.push({ word: muteWord, predicate: word })
147
- continue outer
148
- }
149
-
150
- const wordParts = wordTrimmedPunctuation.split(/\p{P}+/u)
151
- for (const wordPart of wordParts) {
152
- if (wordPart === mutedWord) {
153
- matches.push({ word: muteWord, predicate: word })
154
- continue outer
155
- }
156
- }
157
- }
158
- }
159
- }
160
-
161
- return matches.length ? matches : undefined
162
- }
163
-
164
- /**
165
- * Checks if the given text matches any of the muted words, returning a boolean
166
- * if any matches are found.
167
- */
168
- export function hasMutedWord(params: Params) {
169
- return !!matchMuteWords(params)
170
- }
@@ -1,44 +0,0 @@
1
- import { ModerationDecision } from '../decision.js'
2
- import { Label, ModerationOpts, ModerationSubjectProfile } from '../types.js'
3
-
4
- export function decideAccount(
5
- subject: ModerationSubjectProfile,
6
- opts: ModerationOpts,
7
- ): ModerationDecision {
8
- const acc = new ModerationDecision()
9
-
10
- acc.setDid(subject.did)
11
- acc.setIsMe(subject.did === opts.userDid)
12
- if (subject.viewer?.muted) {
13
- if (subject.viewer?.mutedByList) {
14
- acc.addMutedByList(subject.viewer?.mutedByList)
15
- } else {
16
- acc.addMuted(subject.viewer?.muted)
17
- }
18
- }
19
- if (subject.viewer?.blocking) {
20
- if (subject.viewer?.blockingByList) {
21
- acc.addBlockingByList(subject.viewer?.blockingByList)
22
- } else {
23
- acc.addBlocking(subject.viewer?.blocking)
24
- }
25
- }
26
- acc.addBlockedBy(subject.viewer?.blockedBy)
27
-
28
- for (const label of filterAccountLabels(subject.labels)) {
29
- acc.addLabel('account', label, opts)
30
- }
31
-
32
- return acc
33
- }
34
-
35
- export function filterAccountLabels(labels?: Label[]): Label[] {
36
- if (!labels) {
37
- return []
38
- }
39
- return labels.filter(
40
- (label) =>
41
- !label.uri.endsWith('/app.bsky.actor.profile/self') ||
42
- label.val === '!no-unauthenticated',
43
- )
44
- }
@@ -1,24 +0,0 @@
1
- import { ModerationDecision } from '../decision.js'
2
- import { ModerationOpts, ModerationSubjectFeedGenerator } from '../types.js'
3
- import { decideAccount } from './account.js'
4
- import { decideProfile } from './profile.js'
5
-
6
- export function decideFeedGenerator(
7
- subject: ModerationSubjectFeedGenerator,
8
- opts: ModerationOpts,
9
- ): ModerationDecision {
10
- const acc = new ModerationDecision()
11
-
12
- acc.setDid(subject.creator.did)
13
- acc.setIsMe(subject.creator.did === opts.userDid)
14
- if (subject.labels?.length) {
15
- for (const label of subject.labels) {
16
- acc.addLabel('content', label, opts)
17
- }
18
- }
19
- return ModerationDecision.merge(
20
- acc,
21
- decideAccount(subject.creator, opts),
22
- decideProfile(subject.creator, opts),
23
- )
24
- }
@@ -1,25 +0,0 @@
1
- import { ModerationDecision } from '../decision.js'
2
- import { ModerationOpts, ModerationSubjectNotification } from '../types.js'
3
- import { decideAccount } from './account.js'
4
- import { decideProfile } from './profile.js'
5
-
6
- export function decideNotification(
7
- subject: ModerationSubjectNotification,
8
- opts: ModerationOpts,
9
- ): ModerationDecision {
10
- const acc = new ModerationDecision()
11
-
12
- acc.setDid(subject.author.did)
13
- acc.setIsMe(subject.author.did === opts.userDid)
14
- if (subject.labels?.length) {
15
- for (const label of subject.labels) {
16
- acc.addLabel('content', label, opts)
17
- }
18
- }
19
-
20
- return ModerationDecision.merge(
21
- acc,
22
- decideAccount(subject.author, opts),
23
- decideProfile(subject.author, opts),
24
- )
25
- }
@@ -1,433 +0,0 @@
1
- import {
2
- AppBskyActorDefs,
3
- AppBskyEmbedExternal,
4
- AppBskyEmbedGallery,
5
- AppBskyEmbedImages,
6
- AppBskyEmbedRecord,
7
- AppBskyEmbedRecordWithMedia,
8
- AppBskyFeedPost,
9
- } from '../../client/index.js'
10
- import { $Typed } from '../../client/util.js'
11
- import { ModerationDecision } from '../decision.js'
12
- import { MuteWordMatch, matchMuteWords } from '../mutewords.js'
13
- import { ModerationOpts, ModerationSubjectPost } from '../types.js'
14
- import { decideAccount } from './account.js'
15
- import { decideProfile } from './profile.js'
16
-
17
- export function decidePost(
18
- subject: ModerationSubjectPost,
19
- opts: ModerationOpts,
20
- ): ModerationDecision {
21
- return ModerationDecision.merge(
22
- decideSubject(subject, opts),
23
- decideEmbed(subject.embed, opts)?.downgrade(),
24
- decideAccount(subject.author, opts),
25
- decideProfile(subject.author, opts),
26
- )
27
- }
28
-
29
- function decideSubject(
30
- subject: ModerationSubjectPost,
31
- opts: ModerationOpts,
32
- ): ModerationDecision {
33
- const acc = new ModerationDecision()
34
-
35
- acc.setDid(subject.author.did)
36
- acc.setIsMe(subject.author.did === opts.userDid)
37
- if (subject.labels?.length) {
38
- for (const label of subject.labels) {
39
- acc.addLabel('content', label, opts)
40
- }
41
- }
42
- acc.addHidden(checkHiddenPost(subject, opts.prefs.hiddenPosts))
43
- if (!acc.isMe) {
44
- acc.addMutedWord(matchAllMuteWords(subject, opts.prefs.mutedWords))
45
- }
46
-
47
- return acc
48
- }
49
-
50
- function decideEmbed(
51
- embed:
52
- | undefined
53
- | $Typed<AppBskyEmbedRecord.View>
54
- | $Typed<AppBskyEmbedRecordWithMedia.View>
55
- | { $type: string },
56
- opts: ModerationOpts,
57
- ) {
58
- if (embed) {
59
- if (
60
- (AppBskyEmbedRecord.isView(embed) ||
61
- AppBskyEmbedRecordWithMedia.isView(embed)) &&
62
- AppBskyEmbedRecord.isViewRecord(embed.record)
63
- ) {
64
- // quote post
65
- return decideQuotedPost(embed.record, opts)
66
- } else if (
67
- AppBskyEmbedRecordWithMedia.isView(embed) &&
68
- AppBskyEmbedRecord.isViewRecord(embed.record.record)
69
- ) {
70
- // quoted post with media
71
- return decideQuotedPost(embed.record.record, opts)
72
- } else if (
73
- (AppBskyEmbedRecord.isView(embed) ||
74
- AppBskyEmbedRecordWithMedia.isView(embed)) &&
75
- AppBskyEmbedRecord.isViewBlocked(embed.record)
76
- ) {
77
- // blocked quote post
78
- return decideBlockedQuotedPost(embed.record, opts)
79
- } else if (
80
- AppBskyEmbedRecordWithMedia.isView(embed) &&
81
- AppBskyEmbedRecord.isViewBlocked(embed.record.record)
82
- ) {
83
- // blocked quoted post with media
84
- return decideBlockedQuotedPost(embed.record.record, opts)
85
- }
86
- }
87
-
88
- return undefined
89
- }
90
-
91
- function decideQuotedPost(
92
- subject: AppBskyEmbedRecord.ViewRecord,
93
- opts: ModerationOpts,
94
- ) {
95
- const acc = new ModerationDecision()
96
- acc.setDid(subject.author.did)
97
- acc.setIsMe(subject.author.did === opts.userDid)
98
- if (subject.labels?.length) {
99
- for (const label of subject.labels) {
100
- acc.addLabel('content', label, opts)
101
- }
102
- }
103
- return ModerationDecision.merge(
104
- acc,
105
- decideAccount(subject.author, opts),
106
- decideProfile(subject.author, opts),
107
- )
108
- }
109
-
110
- function decideBlockedQuotedPost(
111
- subject: AppBskyEmbedRecord.ViewBlocked,
112
- opts: ModerationOpts,
113
- ) {
114
- const acc = new ModerationDecision()
115
- acc.setDid(subject.author.did)
116
- acc.setIsMe(subject.author.did === opts.userDid)
117
- if (subject.author.viewer?.muted) {
118
- if (subject.author.viewer?.mutedByList) {
119
- acc.addMutedByList(subject.author.viewer?.mutedByList)
120
- } else {
121
- acc.addMuted(subject.author.viewer?.muted)
122
- }
123
- }
124
- if (subject.author.viewer?.blocking) {
125
- if (subject.author.viewer?.blockingByList) {
126
- acc.addBlockingByList(subject.author.viewer?.blockingByList)
127
- } else {
128
- acc.addBlocking(subject.author.viewer?.blocking)
129
- }
130
- }
131
- acc.addBlockedBy(subject.author.viewer?.blockedBy)
132
- return acc
133
- }
134
-
135
- function checkHiddenPost(
136
- subject: ModerationSubjectPost,
137
- hiddenPosts: string[] | undefined,
138
- ) {
139
- if (!hiddenPosts?.length) {
140
- return false
141
- }
142
- if (hiddenPosts.includes(subject.uri)) {
143
- return true
144
- }
145
- if (subject.embed) {
146
- if (
147
- AppBskyEmbedRecord.isView(subject.embed) &&
148
- AppBskyEmbedRecord.isViewRecord(subject.embed.record) &&
149
- hiddenPosts.includes(subject.embed.record.uri)
150
- ) {
151
- return true
152
- }
153
- if (
154
- AppBskyEmbedRecordWithMedia.isView(subject.embed) &&
155
- AppBskyEmbedRecord.isViewRecord(subject.embed.record.record) &&
156
- hiddenPosts.includes(subject.embed.record.record.uri)
157
- ) {
158
- return true
159
- }
160
- }
161
- return false
162
- }
163
-
164
- function matchAllMuteWords(
165
- subject: ModerationSubjectPost,
166
- mutedWords: AppBskyActorDefs.MutedWord[] | undefined,
167
- ): MuteWordMatch[] | undefined {
168
- if (!mutedWords?.length) {
169
- return
170
- }
171
-
172
- const postAuthor = subject.author
173
-
174
- if (AppBskyFeedPost.isRecord(subject.record)) {
175
- const post = subject.record as AppBskyFeedPost.Record
176
-
177
- const matches = matchMuteWords({
178
- mutedWords,
179
- text: post.text,
180
- facets: post.facets,
181
- outlineTags: post.tags,
182
- languages: post.langs,
183
- actor: postAuthor,
184
- })
185
- // post text
186
- if (matches) {
187
- return matches
188
- }
189
-
190
- if (post.embed && AppBskyEmbedImages.isMain(post.embed)) {
191
- // post images
192
- for (const image of post.embed.images) {
193
- const matches = matchMuteWords({
194
- mutedWords,
195
- text: image.alt,
196
- languages: post.langs,
197
- actor: postAuthor,
198
- })
199
- if (matches) {
200
- return matches
201
- }
202
- }
203
- }
204
-
205
- if (post.embed && AppBskyEmbedGallery.isMain(post.embed)) {
206
- // post gallery items
207
- for (const item of post.embed.items) {
208
- if (AppBskyEmbedGallery.isImage(item)) {
209
- const matches = matchMuteWords({
210
- mutedWords,
211
- text: item.alt,
212
- languages: post.langs,
213
- actor: postAuthor,
214
- })
215
- if (matches) {
216
- return matches
217
- }
218
- }
219
- }
220
- }
221
- }
222
-
223
- const { embed } = subject
224
- if (embed) {
225
- // quote post
226
- if (
227
- (AppBskyEmbedRecord.isView(embed) ||
228
- AppBskyEmbedRecordWithMedia.isView(embed)) &&
229
- AppBskyEmbedRecord.isViewRecord(embed.record)
230
- ) {
231
- if (AppBskyFeedPost.isRecord(embed.record.value)) {
232
- const embeddedPost = embed.record.value as AppBskyFeedPost.Record
233
- const embedAuthor = embed.record.author
234
- const matches = matchMuteWords({
235
- mutedWords,
236
- text: embeddedPost.text,
237
- facets: embeddedPost.facets,
238
- outlineTags: embeddedPost.tags,
239
- languages: embeddedPost.langs,
240
- actor: embedAuthor,
241
- })
242
-
243
- // quoted post text
244
- if (matches) {
245
- return matches
246
- }
247
-
248
- // quoted post's images
249
- if (AppBskyEmbedImages.isMain(embeddedPost.embed)) {
250
- for (const image of embeddedPost.embed.images) {
251
- const matches = matchMuteWords({
252
- mutedWords,
253
- text: image.alt,
254
- languages: embeddedPost.langs,
255
- actor: embedAuthor,
256
- })
257
- if (matches) {
258
- return matches
259
- }
260
- }
261
- }
262
-
263
- // quoted post's gallery
264
- if (AppBskyEmbedGallery.isMain(embeddedPost.embed)) {
265
- for (const item of embeddedPost.embed.items) {
266
- if (AppBskyEmbedGallery.isImage(item)) {
267
- const matches = matchMuteWords({
268
- mutedWords,
269
- text: item.alt,
270
- languages: embeddedPost.langs,
271
- actor: embedAuthor,
272
- })
273
- if (matches) {
274
- return matches
275
- }
276
- }
277
- }
278
- }
279
-
280
- // quoted post's link card
281
- if (AppBskyEmbedExternal.isMain(embeddedPost.embed)) {
282
- const { external } = embeddedPost.embed
283
- const matches = matchMuteWords({
284
- mutedWords,
285
- text: external.title + ' ' + external.description,
286
- languages: [],
287
- actor: embedAuthor,
288
- })
289
- if (matches) {
290
- return matches
291
- }
292
- }
293
-
294
- if (AppBskyEmbedRecordWithMedia.isMain(embeddedPost.embed)) {
295
- // quoted post's link card when it did a quote + media
296
- if (AppBskyEmbedExternal.isMain(embeddedPost.embed.media)) {
297
- const { external } = embeddedPost.embed.media
298
- const matches = matchMuteWords({
299
- mutedWords,
300
- text: external.title + ' ' + external.description,
301
- languages: [],
302
- actor: embedAuthor,
303
- })
304
- if (matches) {
305
- return matches
306
- }
307
- }
308
-
309
- // quoted post's images when it did a quote + media
310
- if (AppBskyEmbedImages.isMain(embeddedPost.embed.media)) {
311
- for (const image of embeddedPost.embed.media.images) {
312
- const matches = matchMuteWords({
313
- mutedWords,
314
- text: image.alt,
315
- languages: AppBskyFeedPost.isRecord(embeddedPost.record)
316
- ? embeddedPost.langs
317
- : [],
318
- actor: embedAuthor,
319
- })
320
- if (matches) {
321
- return matches
322
- }
323
- }
324
- }
325
-
326
- // quoted post's gallery when it did a quote + media
327
- if (AppBskyEmbedGallery.isMain(embeddedPost.embed.media)) {
328
- for (const item of embeddedPost.embed.media.items) {
329
- if (AppBskyEmbedGallery.isImage(item)) {
330
- const matches = matchMuteWords({
331
- mutedWords,
332
- text: item.alt,
333
- languages: AppBskyFeedPost.isRecord(embeddedPost.record)
334
- ? embeddedPost.langs
335
- : [],
336
- actor: embedAuthor,
337
- })
338
- if (matches) {
339
- return matches
340
- }
341
- }
342
- }
343
- }
344
- }
345
- }
346
- }
347
- // link card
348
- else if (AppBskyEmbedExternal.isView(embed)) {
349
- const { external } = embed
350
- const matches = matchMuteWords({
351
- mutedWords,
352
- text: external.title + ' ' + external.description,
353
- languages: [],
354
- actor: postAuthor,
355
- })
356
- if (matches) {
357
- return matches
358
- }
359
- }
360
- // quote post with media
361
- else if (
362
- AppBskyEmbedRecordWithMedia.isView(embed) &&
363
- AppBskyEmbedRecord.isViewRecord(embed.record.record)
364
- ) {
365
- const embedAuthor = embed.record.record.author
366
-
367
- // quoted post text
368
- if (AppBskyFeedPost.isRecord(embed.record.record.value)) {
369
- const post = embed.record.record.value as AppBskyFeedPost.Record
370
- const matches = matchMuteWords({
371
- mutedWords,
372
- text: post.text,
373
- facets: post.facets,
374
- outlineTags: post.tags,
375
- languages: post.langs,
376
- actor: embedAuthor,
377
- })
378
- if (matches) {
379
- return matches
380
- }
381
- }
382
-
383
- // quoted post images
384
- if (AppBskyEmbedImages.isView(embed.media)) {
385
- for (const image of embed.media.images) {
386
- const matches = matchMuteWords({
387
- mutedWords,
388
- text: image.alt,
389
- languages: AppBskyFeedPost.isRecord(subject.record)
390
- ? (subject.record as AppBskyFeedPost.Record).langs
391
- : [],
392
- actor: embedAuthor,
393
- })
394
- if (matches) {
395
- return matches
396
- }
397
- }
398
- }
399
-
400
- // quoted post gallery
401
- if (AppBskyEmbedGallery.isView(embed.media)) {
402
- for (const item of embed.media.items) {
403
- if (AppBskyEmbedGallery.isViewImage(item)) {
404
- const matches = matchMuteWords({
405
- mutedWords,
406
- text: item.alt,
407
- languages: AppBskyFeedPost.isRecord(subject.record)
408
- ? (subject.record as AppBskyFeedPost.Record).langs
409
- : [],
410
- actor: embedAuthor,
411
- })
412
- if (matches) {
413
- return matches
414
- }
415
- }
416
- }
417
- }
418
-
419
- if (AppBskyEmbedExternal.isView(embed.media)) {
420
- const { external } = embed.media
421
- const matches = matchMuteWords({
422
- mutedWords,
423
- text: external.title + ' ' + external.description,
424
- languages: [],
425
- actor: embedAuthor,
426
- })
427
- if (matches) {
428
- return matches
429
- }
430
- }
431
- }
432
- }
433
- }