Skip to content

Commit 9be9921

Browse files
committed
🔀 Merge with upstream
2 parents be01cf0 + c7e6ef0 commit 9be9921

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

68 files changed

+922
-189
lines changed

.changeset/cuddly-adults-beg.md

-5
This file was deleted.

.changeset/lovely-dogs-run.md

-5
This file was deleted.

.github/workflows/build-and-push-ozone-aws.yaml

+1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ on:
33
push:
44
branches:
55
- main
6+
- ozone-cdn-invalidation
67
env:
78
REGISTRY: ${{ secrets.AWS_ECR_REGISTRY_USEAST2_PACKAGES_REGISTRY }}
89
USERNAME: ${{ secrets.AWS_ECR_REGISTRY_USEAST2_PACKAGES_USERNAME }}

lexicons/com/atproto/admin/defs.json

+4
Original file line numberDiff line numberDiff line change
@@ -596,6 +596,10 @@
596596
"type": "string",
597597
"description": "The subject line of the email sent to the user."
598598
},
599+
"content": {
600+
"type": "string",
601+
"description": "The content of the email sent to the user."
602+
},
599603
"comment": {
600604
"type": "string",
601605
"description": "Additional comment about the outgoing comm."

packages/api/CHANGELOG.md

+37
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,42 @@
11
# @atproto/api
22

3+
## 0.10.4
4+
5+
### Patch Changes
6+
7+
- [#2260](https://github.com/bluesky-social/atproto/pull/2260) [`6ec885992`](https://github.com/bluesky-social/atproto/commit/6ec8859929a16f9725319cc398b716acf913b01f) Thanks [@estrattonbailey](https://github.com/estrattonbailey)! - Export regex from rich text detection
8+
9+
- [#2260](https://github.com/bluesky-social/atproto/pull/2260) [`6ec885992`](https://github.com/bluesky-social/atproto/commit/6ec8859929a16f9725319cc398b716acf913b01f) Thanks [@estrattonbailey](https://github.com/estrattonbailey)! - Disallow rare unicode whitespace characters from tags
10+
11+
- [#2260](https://github.com/bluesky-social/atproto/pull/2260) [`6ec885992`](https://github.com/bluesky-social/atproto/commit/6ec8859929a16f9725319cc398b716acf913b01f) Thanks [@estrattonbailey](https://github.com/estrattonbailey)! - Allow tags to lead with numbers
12+
13+
## 0.10.3
14+
15+
### Patch Changes
16+
17+
- [#2247](https://github.com/bluesky-social/atproto/pull/2247) [`2a0ceb818`](https://github.com/bluesky-social/atproto/commit/2a0ceb8180faa17de8061d4fa6c361b57a2005ed) Thanks [@estrattonbailey](https://github.com/estrattonbailey)! - Fix double sanitization bug when editing muted words.
18+
19+
- [#2247](https://github.com/bluesky-social/atproto/pull/2247) [`2a0ceb818`](https://github.com/bluesky-social/atproto/commit/2a0ceb8180faa17de8061d4fa6c361b57a2005ed) Thanks [@estrattonbailey](https://github.com/estrattonbailey)! - More sanitization of muted words, including newlines and leading/trailing whitespace
20+
21+
- [#2247](https://github.com/bluesky-social/atproto/pull/2247) [`2a0ceb818`](https://github.com/bluesky-social/atproto/commit/2a0ceb8180faa17de8061d4fa6c361b57a2005ed) Thanks [@estrattonbailey](https://github.com/estrattonbailey)! - Add `sanitizeMutedWordValue` util
22+
23+
- [#2247](https://github.com/bluesky-social/atproto/pull/2247) [`2a0ceb818`](https://github.com/bluesky-social/atproto/commit/2a0ceb8180faa17de8061d4fa6c361b57a2005ed) Thanks [@estrattonbailey](https://github.com/estrattonbailey)! - Handle hash emoji in mute words
24+
25+
## 0.10.2
26+
27+
### Patch Changes
28+
29+
- [#2245](https://github.com/bluesky-social/atproto/pull/2245) [`61b3d2525`](https://github.com/bluesky-social/atproto/commit/61b3d25253353db2da1336004f94e7dc5adb0410) Thanks [@mary-ext](https://github.com/mary-ext)! - Prevent hashtag emoji from being parsed as a tag
30+
31+
- [#2218](https://github.com/bluesky-social/atproto/pull/2218) [`43531905c`](https://github.com/bluesky-social/atproto/commit/43531905ce1aec6d36d9be5943782811ecca6e6d) Thanks [@estrattonbailey](https://github.com/estrattonbailey)! - Fix mute word upsert logic by ensuring we're comparing sanitized word values
32+
33+
- [#2245](https://github.com/bluesky-social/atproto/pull/2245) [`61b3d2525`](https://github.com/bluesky-social/atproto/commit/61b3d25253353db2da1336004f94e7dc5adb0410) Thanks [@mary-ext](https://github.com/mary-ext)! - Properly calculate length of tag
34+
35+
- Updated dependencies [[`0c815b964`](https://github.com/bluesky-social/atproto/commit/0c815b964c030aa0f277c40bf9786f130dc320f4)]:
36+
- @atproto/syntax@0.2.0
37+
- @atproto/lexicon@0.3.2
38+
- @atproto/xrpc@0.4.2
39+
340
## 0.10.1
441

542
### Patch Changes

packages/api/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@atproto/api",
3-
"version": "0.10.1",
3+
"version": "0.10.4",
44
"license": "MIT",
55
"description": "Client library for atproto and Bluesky",
66
"keywords": [

packages/api/src/bsky-agent.ts

+97-74
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
BskyThreadViewPreference,
1414
BskyInterestsPreference,
1515
} from './types'
16+
import { sanitizeMutedWordValue } from './util'
1617

1718
const FEED_VIEW_PREF_DEFAULTS = {
1819
hideReplies: false,
@@ -565,16 +566,108 @@ export class BskyAgent extends AtpAgent {
565566
})
566567
}
567568

568-
async upsertMutedWords(mutedWords: AppBskyActorDefs.MutedWord[]) {
569-
await updateMutedWords(this, mutedWords, 'upsert')
569+
async upsertMutedWords(newMutedWords: AppBskyActorDefs.MutedWord[]) {
570+
await updatePreferences(this, (prefs: AppBskyActorDefs.Preferences) => {
571+
let mutedWordsPref = prefs.findLast(
572+
(pref) =>
573+
AppBskyActorDefs.isMutedWordsPref(pref) &&
574+
AppBskyActorDefs.validateMutedWordsPref(pref).success,
575+
)
576+
577+
if (mutedWordsPref && AppBskyActorDefs.isMutedWordsPref(mutedWordsPref)) {
578+
for (const updatedWord of newMutedWords) {
579+
let foundMatch = false
580+
const sanitizedUpdatedValue = sanitizeMutedWordValue(
581+
updatedWord.value,
582+
)
583+
584+
// was trimmed down to an empty string e.g. single `#`
585+
if (!sanitizedUpdatedValue) continue
586+
587+
for (const existingItem of mutedWordsPref.items) {
588+
if (existingItem.value === sanitizedUpdatedValue) {
589+
existingItem.targets = Array.from(
590+
new Set([...existingItem.targets, ...updatedWord.targets]),
591+
)
592+
foundMatch = true
593+
break
594+
}
595+
}
596+
597+
if (!foundMatch) {
598+
mutedWordsPref.items.push({
599+
...updatedWord,
600+
value: sanitizedUpdatedValue,
601+
})
602+
}
603+
}
604+
} else {
605+
// if the pref doesn't exist, create it
606+
mutedWordsPref = {
607+
items: newMutedWords.map((w) => ({
608+
...w,
609+
value: sanitizeMutedWordValue(w.value),
610+
})),
611+
}
612+
}
613+
614+
return prefs
615+
.filter((p) => !AppBskyActorDefs.isMutedWordsPref(p))
616+
.concat([
617+
{ ...mutedWordsPref, $type: 'app.bsky.actor.defs#mutedWordsPref' },
618+
])
619+
})
570620
}
571621

572622
async updateMutedWord(mutedWord: AppBskyActorDefs.MutedWord) {
573-
await updateMutedWords(this, [mutedWord], 'update')
623+
await updatePreferences(this, (prefs: AppBskyActorDefs.Preferences) => {
624+
let mutedWordsPref = prefs.findLast(
625+
(pref) =>
626+
AppBskyActorDefs.isMutedWordsPref(pref) &&
627+
AppBskyActorDefs.validateMutedWordsPref(pref).success,
628+
)
629+
630+
if (mutedWordsPref && AppBskyActorDefs.isMutedWordsPref(mutedWordsPref)) {
631+
for (const existingItem of mutedWordsPref.items) {
632+
if (existingItem.value === mutedWord.value) {
633+
existingItem.targets = mutedWord.targets
634+
break
635+
}
636+
}
637+
}
638+
639+
return prefs
640+
.filter((p) => !AppBskyActorDefs.isMutedWordsPref(p))
641+
.concat([
642+
{ ...mutedWordsPref, $type: 'app.bsky.actor.defs#mutedWordsPref' },
643+
])
644+
})
574645
}
575646

576647
async removeMutedWord(mutedWord: AppBskyActorDefs.MutedWord) {
577-
await updateMutedWords(this, [mutedWord], 'remove')
648+
await updatePreferences(this, (prefs: AppBskyActorDefs.Preferences) => {
649+
let mutedWordsPref = prefs.findLast(
650+
(pref) =>
651+
AppBskyActorDefs.isMutedWordsPref(pref) &&
652+
AppBskyActorDefs.validateMutedWordsPref(pref).success,
653+
)
654+
655+
if (mutedWordsPref && AppBskyActorDefs.isMutedWordsPref(mutedWordsPref)) {
656+
for (let i = 0; i < mutedWordsPref.items.length; i++) {
657+
const existing = mutedWordsPref.items[i]
658+
if (existing.value === mutedWord.value) {
659+
mutedWordsPref.items.splice(i, 1)
660+
break
661+
}
662+
}
663+
}
664+
665+
return prefs
666+
.filter((p) => !AppBskyActorDefs.isMutedWordsPref(p))
667+
.concat([
668+
{ ...mutedWordsPref, $type: 'app.bsky.actor.defs#mutedWordsPref' },
669+
])
670+
})
578671
}
579672

580673
async hidePost(postUri: string) {
@@ -646,76 +739,6 @@ async function updateFeedPreferences(
646739
return res
647740
}
648741

649-
/**
650-
* A helper specifically for updating muted words preferences
651-
*/
652-
async function updateMutedWords(
653-
agent: BskyAgent,
654-
mutedWords: AppBskyActorDefs.MutedWord[],
655-
action: 'upsert' | 'update' | 'remove',
656-
) {
657-
const sanitizeMutedWord = (word: AppBskyActorDefs.MutedWord) => ({
658-
value: word.value.replace(/^#/, ''),
659-
targets: word.targets,
660-
})
661-
662-
await updatePreferences(agent, (prefs: AppBskyActorDefs.Preferences) => {
663-
let mutedWordsPref = prefs.findLast(
664-
(pref) =>
665-
AppBskyActorDefs.isMutedWordsPref(pref) &&
666-
AppBskyActorDefs.validateMutedWordsPref(pref).success,
667-
)
668-
669-
if (mutedWordsPref && AppBskyActorDefs.isMutedWordsPref(mutedWordsPref)) {
670-
if (action === 'upsert' || action === 'update') {
671-
for (const word of mutedWords) {
672-
let foundMatch = false
673-
674-
for (const existingItem of mutedWordsPref.items) {
675-
if (existingItem.value === sanitizeMutedWord(word).value) {
676-
existingItem.targets =
677-
action === 'upsert'
678-
? Array.from(
679-
new Set([...existingItem.targets, ...word.targets]),
680-
)
681-
: word.targets
682-
foundMatch = true
683-
break
684-
}
685-
}
686-
687-
if (action === 'upsert' && !foundMatch) {
688-
mutedWordsPref.items.push(sanitizeMutedWord(word))
689-
}
690-
}
691-
} else if (action === 'remove') {
692-
for (const word of mutedWords) {
693-
for (let i = 0; i < mutedWordsPref.items.length; i++) {
694-
const existing = mutedWordsPref.items[i]
695-
if (existing.value === sanitizeMutedWord(word).value) {
696-
mutedWordsPref.items.splice(i, 1)
697-
break
698-
}
699-
}
700-
}
701-
}
702-
} else {
703-
// if the pref doesn't exist, create it
704-
if (action === 'upsert') {
705-
mutedWordsPref = {
706-
items: mutedWords.map(sanitizeMutedWord),
707-
}
708-
}
709-
}
710-
711-
return prefs
712-
.filter((p) => !AppBskyActorDefs.isMutedWordsPref(p))
713-
.concat([
714-
{ ...mutedWordsPref, $type: 'app.bsky.actor.defs#mutedWordsPref' },
715-
])
716-
})
717-
}
718-
719742
async function updateHiddenPost(
720743
agent: BskyAgent,
721744
postUri: string,

packages/api/src/client/lexicons.ts

+4
Original file line numberDiff line numberDiff line change
@@ -905,6 +905,10 @@ export const schemaDict = {
905905
type: 'string',
906906
description: 'The subject line of the email sent to the user.',
907907
},
908+
content: {
909+
type: 'string',
910+
description: 'The content of the email sent to the user.',
911+
},
908912
comment: {
909913
type: 'string',
910914
description: 'Additional comment about the outgoing comm.',

packages/api/src/client/types/com/atproto/admin/defs.ts

+2
Original file line numberDiff line numberDiff line change
@@ -709,6 +709,8 @@ export function validateModEventUnmute(v: unknown): ValidationResult {
709709
export interface ModEventEmail {
710710
/** The subject line of the email sent to the user. */
711711
subjectLine: string
712+
/** The content of the email sent to the user. */
713+
content?: string
712714
/** Additional comment about the outgoing comm. */
713715
comment?: string
714716
[k: string]: unknown

packages/api/src/index.ts

+2
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,13 @@ export {
88
} from '@atproto/lexicon'
99
export { parseLanguage } from '@atproto/common-web'
1010
export * from './types'
11+
export * from './util'
1112
export * from './client'
1213
export * from './agent'
1314
export * from './rich-text/rich-text'
1415
export * from './rich-text/sanitization'
1516
export * from './rich-text/unicode'
17+
export * from './rich-text/util'
1618
export * from './moderation'
1719
export * from './moderation/types'
1820
export { LABELS } from './moderation/const/labels'

packages/api/src/rich-text/detection.ts

+18-12
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
import TLDs from 'tlds'
22
import { AppBskyRichtextFacet } from '../client'
33
import { UnicodeString } from './unicode'
4+
import {
5+
URL_REGEX,
6+
MENTION_REGEX,
7+
TAG_REGEX,
8+
TRAILING_PUNCTUATION_REGEX,
9+
} from './util'
410

511
export type Facet = AppBskyRichtextFacet.Main
612

@@ -9,7 +15,7 @@ export function detectFacets(text: UnicodeString): Facet[] | undefined {
915
const facets: Facet[] = []
1016
{
1117
// mentions
12-
const re = /(^|\s|\()(@)([a-zA-Z0-9.-]+)(\b)/g
18+
const re = MENTION_REGEX
1319
while ((match = re.exec(text.utf16))) {
1420
if (!isValidDomain(match[3]) && !match[3].endsWith('.test')) {
1521
continue // probably not a handle
@@ -33,8 +39,7 @@ export function detectFacets(text: UnicodeString): Facet[] | undefined {
3339
}
3440
{
3541
// links
36-
const re =
37-
/(^|\s|\()((https?:\/\/[\S]+)|((?<domain>[a-z][a-z0-9]*(\.[a-z0-9]+)+)[\S]*))/gim
42+
const re = URL_REGEX
3843
while ((match = re.exec(text.utf16))) {
3944
let uri = match[2]
4045
if (!uri.startsWith('http')) {
@@ -70,27 +75,28 @@ export function detectFacets(text: UnicodeString): Facet[] | undefined {
7075
}
7176
}
7277
{
73-
const re = /(?:^|\s)(#[^\d\s]\S*)(?=\s)?/g
78+
const re = TAG_REGEX
7479
while ((match = re.exec(text.utf16))) {
75-
let [tag] = match
76-
const hasLeadingSpace = /^\s/.test(tag)
80+
let [, leading, tag] = match
7781

78-
tag = tag.trim().replace(/\p{P}+$/gu, '') // strip ending punctuation
82+
if (!tag) continue
7983

80-
// inclusive of #, max of 64 chars
81-
if (tag.length > 66) continue
84+
// strip ending punctuation and any spaces
85+
tag = tag.trim().replace(TRAILING_PUNCTUATION_REGEX, '')
8286

83-
const index = match.index + (hasLeadingSpace ? 1 : 0)
87+
if (tag.length === 0 || tag.length > 64) continue
88+
89+
const index = match.index + leading.length
8490

8591
facets.push({
8692
index: {
8793
byteStart: text.utf16IndexToUtf8Index(index),
88-
byteEnd: text.utf16IndexToUtf8Index(index + tag.length), // inclusive of last char
94+
byteEnd: text.utf16IndexToUtf8Index(index + 1 + tag.length),
8995
},
9096
features: [
9197
{
9298
$type: 'app.bsky.richtext.facet#tag',
93-
tag: tag.replace(/^#/, ''),
99+
tag: tag,
94100
},
95101
],
96102
})

packages/api/src/rich-text/util.ts

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
export const MENTION_REGEX = /(^|\s|\()(@)([a-zA-Z0-9.-]+)(\b)/g
2+
export const URL_REGEX =
3+
/(^|\s|\()((https?:\/\/[\S]+)|((?<domain>[a-z][a-z0-9]*(\.[a-z0-9]+)+)[\S]*))/gim
4+
export const TRAILING_PUNCTUATION_REGEX = /\p{P}+$/gu
5+
6+
/**
7+
* `\ufe0f` emoji modifier
8+
* `\u00AD\u2060\u200A\u200B\u200C\u200D\u20e2` zero-width spaces (likely incomplete)
9+
*/
10+
export const TAG_REGEX =
11+
/(^|\s)[#]((?!\ufe0f)[^\s\u00AD\u2060\u200A\u200B\u200C\u200D\u20e2]*[^\d\s\p{P}\u00AD\u2060\u200A\u200B\u200C\u200D\u20e2]+[^\s\u00AD\u2060\u200A\u200B\u200C\u200D\u20e2]*)?/gu

0 commit comments

Comments
 (0)