Skip to content

Commit

Permalink
Eric/tag sanitization (#2247)
Browse files Browse the repository at this point in the history
* Don't remove hash from muted words

* Split out crud actions, only sanitize on inserts

* Add changeset

* Handle hash emoji in mute words

* Add sanitization for invalid chars

* Remove console

* Add util

* Clean up changesets

* Format

* Wow forgot to commit change
  • Loading branch information
estrattonbailey authored Feb 29, 2024
1 parent 1b0b4f9 commit 2a0ceb8
Show file tree
Hide file tree
Showing 9 changed files with 265 additions and 81 deletions.
5 changes: 5 additions & 0 deletions .changeset/rotten-actors-dance.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@atproto/api': patch
---

Fix double sanitization bug when editing muted words.
5 changes: 5 additions & 0 deletions .changeset/silly-carrots-repair.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@atproto/api': patch
---

More sanitization of muted words, including newlines and leading/trailing whitespace
5 changes: 5 additions & 0 deletions .changeset/small-dragons-cry.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@atproto/api': patch
---

Add `sanitizeMutedWordValue` util
5 changes: 5 additions & 0 deletions .changeset/sour-gorillas-unite.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@atproto/api': patch
---

Handle hash emoji in mute words
171 changes: 97 additions & 74 deletions packages/api/src/bsky-agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
BskyThreadViewPreference,
BskyInterestsPreference,
} from './types'
import { sanitizeMutedWordValue } from './util'

const FEED_VIEW_PREF_DEFAULTS = {
hideReplies: false,
Expand Down Expand Up @@ -565,16 +566,108 @@ export class BskyAgent extends AtpAgent {
})
}

async upsertMutedWords(mutedWords: AppBskyActorDefs.MutedWord[]) {
await updateMutedWords(this, mutedWords, 'upsert')
async upsertMutedWords(newMutedWords: AppBskyActorDefs.MutedWord[]) {
await updatePreferences(this, (prefs: AppBskyActorDefs.Preferences) => {
let mutedWordsPref = prefs.findLast(
(pref) =>
AppBskyActorDefs.isMutedWordsPref(pref) &&
AppBskyActorDefs.validateMutedWordsPref(pref).success,
)

if (mutedWordsPref && AppBskyActorDefs.isMutedWordsPref(mutedWordsPref)) {
for (const updatedWord of newMutedWords) {
let foundMatch = false
const sanitizedUpdatedValue = sanitizeMutedWordValue(
updatedWord.value,
)

// was trimmed down to an empty string e.g. single `#`
if (!sanitizedUpdatedValue) continue

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,
})
}
}
} else {
// if the pref doesn't exist, create it
mutedWordsPref = {
items: newMutedWords.map((w) => ({
...w,
value: sanitizeMutedWordValue(w.value),
})),
}
}

return prefs
.filter((p) => !AppBskyActorDefs.isMutedWordsPref(p))
.concat([
{ ...mutedWordsPref, $type: 'app.bsky.actor.defs#mutedWordsPref' },
])
})
}

async updateMutedWord(mutedWord: AppBskyActorDefs.MutedWord) {
await updateMutedWords(this, [mutedWord], 'update')
await updatePreferences(this, (prefs: AppBskyActorDefs.Preferences) => {
let mutedWordsPref = prefs.findLast(

Check warning on line 624 in packages/api/src/bsky-agent.ts

View workflow job for this annotation

GitHub Actions / Build & Publish

'mutedWordsPref' is never reassigned. Use 'const' instead
(pref) =>
AppBskyActorDefs.isMutedWordsPref(pref) &&
AppBskyActorDefs.validateMutedWordsPref(pref).success,
)

if (mutedWordsPref && AppBskyActorDefs.isMutedWordsPref(mutedWordsPref)) {
for (const existingItem of mutedWordsPref.items) {
if (existingItem.value === mutedWord.value) {
existingItem.targets = mutedWord.targets
break
}
}
}

return prefs
.filter((p) => !AppBskyActorDefs.isMutedWordsPref(p))
.concat([
{ ...mutedWordsPref, $type: 'app.bsky.actor.defs#mutedWordsPref' },
])
})
}

async removeMutedWord(mutedWord: AppBskyActorDefs.MutedWord) {
await updateMutedWords(this, [mutedWord], 'remove')
await updatePreferences(this, (prefs: AppBskyActorDefs.Preferences) => {
let mutedWordsPref = prefs.findLast(

Check warning on line 649 in packages/api/src/bsky-agent.ts

View workflow job for this annotation

GitHub Actions / Build & Publish

'mutedWordsPref' is never reassigned. Use 'const' instead
(pref) =>
AppBskyActorDefs.isMutedWordsPref(pref) &&
AppBskyActorDefs.validateMutedWordsPref(pref).success,
)

if (mutedWordsPref && AppBskyActorDefs.isMutedWordsPref(mutedWordsPref)) {
for (let i = 0; i < mutedWordsPref.items.length; i++) {
const existing = mutedWordsPref.items[i]
if (existing.value === mutedWord.value) {
mutedWordsPref.items.splice(i, 1)
break
}
}
}

return prefs
.filter((p) => !AppBskyActorDefs.isMutedWordsPref(p))
.concat([
{ ...mutedWordsPref, $type: 'app.bsky.actor.defs#mutedWordsPref' },
])
})
}

async hidePost(postUri: string) {
Expand Down Expand Up @@ -646,76 +739,6 @@ 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 word of mutedWords) {
let foundMatch = false

for (const existingItem of mutedWordsPref.items) {
if (existingItem.value === sanitizeMutedWord(word).value) {
existingItem.targets =
action === 'upsert'
? Array.from(
new Set([...existingItem.targets, ...word.targets]),
)
: word.targets
foundMatch = true
break
}
}

if (action === 'upsert' && !foundMatch) {
mutedWordsPref.items.push(sanitizeMutedWord(word))
}
}
} 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,
Expand Down
1 change: 1 addition & 0 deletions packages/api/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export {
} from '@atproto/lexicon'
export { parseLanguage } from '@atproto/common-web'
export * from './types'
export * from './util'
export * from './client'
export * from './agent'
export * from './rich-text/rich-text'
Expand Down
6 changes: 6 additions & 0 deletions packages/api/src/util.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export function sanitizeMutedWordValue(value: string) {
return value
.trim()
.replace(/^#(?!\ufe0f)/, '')
.replace(/[\r\n\u00AD\u2060\u200D\u200C\u200B]+/, '')

Check warning on line 5 in packages/api/src/util.ts

View workflow job for this annotation

GitHub Actions / Build & Publish

Unexpected joined character sequence in character class
}
Loading

0 comments on commit 2a0ceb8

Please sign in to comment.