@atproto/api 0.6.16 → 0.6.18
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.
- package/CHANGELOG.md +12 -0
- package/dist/client/lexicons.d.ts +22 -0
- package/dist/client/types/app/bsky/feed/post.d.ts +1 -0
- package/dist/client/types/app/bsky/richtext/facet.d.ts +7 -1
- package/dist/index.js +66 -2
- package/dist/index.js.map +2 -2
- package/dist/rich-text/rich-text.d.ts +3 -0
- package/package.json +2 -2
- package/src/client/lexicons.ts +23 -0
- package/src/client/types/app/bsky/feed/post.ts +2 -0
- package/src/client/types/app/bsky/richtext/facet.ts +17 -1
- package/src/rich-text/detection.ts +27 -0
- package/src/rich-text/rich-text.ts +13 -0
- package/tests/agent.test.ts +15 -4
- package/tests/bsky-agent.test.ts +26 -13
- package/tests/rich-text-detection.test.ts +104 -0
|
@@ -4,6 +4,7 @@ import { UnicodeString } from './unicode';
|
|
|
4
4
|
export declare type Facet = AppBskyRichtextFacet.Main;
|
|
5
5
|
export declare type FacetLink = AppBskyRichtextFacet.Link;
|
|
6
6
|
export declare type FacetMention = AppBskyRichtextFacet.Mention;
|
|
7
|
+
export declare type FacetTag = AppBskyRichtextFacet.Tag;
|
|
7
8
|
export declare type Entity = AppBskyFeedPost.Entity;
|
|
8
9
|
export interface RichTextProps {
|
|
9
10
|
text: string;
|
|
@@ -21,6 +22,8 @@ export declare class RichTextSegment {
|
|
|
21
22
|
isLink(): boolean;
|
|
22
23
|
get mention(): FacetMention | undefined;
|
|
23
24
|
isMention(): boolean;
|
|
25
|
+
get tag(): FacetTag | undefined;
|
|
26
|
+
isTag(): boolean;
|
|
24
27
|
}
|
|
25
28
|
export declare class RichText {
|
|
26
29
|
unicodeText: UnicodeString;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@atproto/api",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.18",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "Client library for atproto and Bluesky",
|
|
6
6
|
"keywords": [
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"devDependencies": {
|
|
28
28
|
"common-tags": "^1.8.2",
|
|
29
29
|
"@atproto/lex-cli": "^0.2.1",
|
|
30
|
-
"@atproto/pds": "^0.1.
|
|
30
|
+
"@atproto/pds": "^0.1.18"
|
|
31
31
|
},
|
|
32
32
|
"scripts": {
|
|
33
33
|
"codegen": "pnpm docgen && node ./scripts/generate-code.mjs && lex gen-api ./src/client ../../lexicons/com/atproto/*/* ../../lexicons/app/bsky/*/*",
|
package/src/client/lexicons.ts
CHANGED
|
@@ -5622,6 +5622,16 @@ export const schemaDict = {
|
|
|
5622
5622
|
type: 'union',
|
|
5623
5623
|
refs: ['lex:com.atproto.label.defs#selfLabels'],
|
|
5624
5624
|
},
|
|
5625
|
+
tags: {
|
|
5626
|
+
type: 'array',
|
|
5627
|
+
maxLength: 8,
|
|
5628
|
+
items: {
|
|
5629
|
+
type: 'string',
|
|
5630
|
+
maxLength: 640,
|
|
5631
|
+
maxGraphemes: 64,
|
|
5632
|
+
},
|
|
5633
|
+
description: 'Additional non-inline tags describing this post.',
|
|
5634
|
+
},
|
|
5625
5635
|
createdAt: {
|
|
5626
5636
|
type: 'string',
|
|
5627
5637
|
format: 'datetime',
|
|
@@ -6761,6 +6771,7 @@ export const schemaDict = {
|
|
|
6761
6771
|
refs: [
|
|
6762
6772
|
'lex:app.bsky.richtext.facet#mention',
|
|
6763
6773
|
'lex:app.bsky.richtext.facet#link',
|
|
6774
|
+
'lex:app.bsky.richtext.facet#tag',
|
|
6764
6775
|
],
|
|
6765
6776
|
},
|
|
6766
6777
|
},
|
|
@@ -6788,6 +6799,18 @@ export const schemaDict = {
|
|
|
6788
6799
|
},
|
|
6789
6800
|
},
|
|
6790
6801
|
},
|
|
6802
|
+
tag: {
|
|
6803
|
+
type: 'object',
|
|
6804
|
+
description: 'A hashtag.',
|
|
6805
|
+
required: ['tag'],
|
|
6806
|
+
properties: {
|
|
6807
|
+
tag: {
|
|
6808
|
+
type: 'string',
|
|
6809
|
+
maxLength: 640,
|
|
6810
|
+
maxGraphemes: 64,
|
|
6811
|
+
},
|
|
6812
|
+
},
|
|
6813
|
+
},
|
|
6791
6814
|
byteSlice: {
|
|
6792
6815
|
type: 'object',
|
|
6793
6816
|
description:
|
|
@@ -8,7 +8,7 @@ import { CID } from 'multiformats/cid'
|
|
|
8
8
|
|
|
9
9
|
export interface Main {
|
|
10
10
|
index: ByteSlice
|
|
11
|
-
features: (Mention | Link | { $type: string; [k: string]: unknown })[]
|
|
11
|
+
features: (Mention | Link | Tag | { $type: string; [k: string]: unknown })[]
|
|
12
12
|
[k: string]: unknown
|
|
13
13
|
}
|
|
14
14
|
|
|
@@ -61,6 +61,22 @@ export function validateLink(v: unknown): ValidationResult {
|
|
|
61
61
|
return lexicons.validate('app.bsky.richtext.facet#link', v)
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
+
/** A hashtag. */
|
|
65
|
+
export interface Tag {
|
|
66
|
+
tag: string
|
|
67
|
+
[k: string]: unknown
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function isTag(v: unknown): v is Tag {
|
|
71
|
+
return (
|
|
72
|
+
isObj(v) && hasProp(v, '$type') && v.$type === 'app.bsky.richtext.facet#tag'
|
|
73
|
+
)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function validateTag(v: unknown): ValidationResult {
|
|
77
|
+
return lexicons.validate('app.bsky.richtext.facet#tag', v)
|
|
78
|
+
}
|
|
79
|
+
|
|
64
80
|
/** A text segment. Start is inclusive, end is exclusive. Indices are for utf8-encoded strings. */
|
|
65
81
|
export interface ByteSlice {
|
|
66
82
|
byteStart: number
|
|
@@ -69,6 +69,33 @@ export function detectFacets(text: UnicodeString): Facet[] | undefined {
|
|
|
69
69
|
})
|
|
70
70
|
}
|
|
71
71
|
}
|
|
72
|
+
{
|
|
73
|
+
const re = /(?:^|\s)(#[^\d\s]\S*)(?=\s)?/g
|
|
74
|
+
while ((match = re.exec(text.utf16))) {
|
|
75
|
+
let [tag] = match
|
|
76
|
+
const hasLeadingSpace = /^\s/.test(tag)
|
|
77
|
+
|
|
78
|
+
tag = tag.trim().replace(/\p{P}+$/gu, '') // strip ending punctuation
|
|
79
|
+
|
|
80
|
+
// inclusive of #, max of 64 chars
|
|
81
|
+
if (tag.length > 66) continue
|
|
82
|
+
|
|
83
|
+
const index = match.index + (hasLeadingSpace ? 1 : 0)
|
|
84
|
+
|
|
85
|
+
facets.push({
|
|
86
|
+
index: {
|
|
87
|
+
byteStart: text.utf16IndexToUtf8Index(index),
|
|
88
|
+
byteEnd: text.utf16IndexToUtf8Index(index + tag.length), // inclusive of last char
|
|
89
|
+
},
|
|
90
|
+
features: [
|
|
91
|
+
{
|
|
92
|
+
$type: 'app.bsky.richtext.facet#tag',
|
|
93
|
+
tag,
|
|
94
|
+
},
|
|
95
|
+
],
|
|
96
|
+
})
|
|
97
|
+
}
|
|
98
|
+
}
|
|
72
99
|
return facets.length > 0 ? facets : undefined
|
|
73
100
|
}
|
|
74
101
|
|
|
@@ -100,6 +100,7 @@ import { detectFacets } from './detection'
|
|
|
100
100
|
export type Facet = AppBskyRichtextFacet.Main
|
|
101
101
|
export type FacetLink = AppBskyRichtextFacet.Link
|
|
102
102
|
export type FacetMention = AppBskyRichtextFacet.Mention
|
|
103
|
+
export type FacetTag = AppBskyRichtextFacet.Tag
|
|
103
104
|
export type Entity = AppBskyFeedPost.Entity
|
|
104
105
|
|
|
105
106
|
export interface RichTextProps {
|
|
@@ -141,6 +142,18 @@ export class RichTextSegment {
|
|
|
141
142
|
isMention() {
|
|
142
143
|
return !!this.mention
|
|
143
144
|
}
|
|
145
|
+
|
|
146
|
+
get tag(): FacetTag | undefined {
|
|
147
|
+
const tag = this.facet?.features.find(AppBskyRichtextFacet.isTag)
|
|
148
|
+
if (AppBskyRichtextFacet.isTag(tag)) {
|
|
149
|
+
return tag
|
|
150
|
+
}
|
|
151
|
+
return undefined
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
isTag() {
|
|
155
|
+
return !!this.tag
|
|
156
|
+
}
|
|
144
157
|
}
|
|
145
158
|
|
|
146
159
|
export class RichText {
|
package/tests/agent.test.ts
CHANGED
|
@@ -197,7 +197,7 @@ describe('agent', () => {
|
|
|
197
197
|
|
|
198
198
|
// put the agent through the auth flow
|
|
199
199
|
AtpAgent.configure({ fetch: tokenExpiredFetchHandler })
|
|
200
|
-
const res1 = await agent
|
|
200
|
+
const res1 = await createPost(agent)
|
|
201
201
|
AtpAgent.configure({ fetch: defaultFetchHandler })
|
|
202
202
|
|
|
203
203
|
expect(res1.success).toEqual(true)
|
|
@@ -267,9 +267,9 @@ describe('agent', () => {
|
|
|
267
267
|
// put the agent through the auth flow
|
|
268
268
|
AtpAgent.configure({ fetch: tokenExpiredFetchHandler })
|
|
269
269
|
const [res1, res2, res3] = await Promise.all([
|
|
270
|
-
agent
|
|
271
|
-
agent
|
|
272
|
-
agent
|
|
270
|
+
createPost(agent),
|
|
271
|
+
createPost(agent),
|
|
272
|
+
createPost(agent),
|
|
273
273
|
])
|
|
274
274
|
AtpAgent.configure({ fetch: defaultFetchHandler })
|
|
275
275
|
|
|
@@ -462,3 +462,14 @@ describe('agent', () => {
|
|
|
462
462
|
})
|
|
463
463
|
})
|
|
464
464
|
})
|
|
465
|
+
|
|
466
|
+
const createPost = async (agent: AtpAgent) => {
|
|
467
|
+
return agent.api.com.atproto.repo.createRecord({
|
|
468
|
+
repo: agent.session?.did ?? '',
|
|
469
|
+
collection: 'app.bsky.feed.post',
|
|
470
|
+
record: {
|
|
471
|
+
text: 'hello there',
|
|
472
|
+
createdAt: new Date().toISOString(),
|
|
473
|
+
},
|
|
474
|
+
})
|
|
475
|
+
}
|
package/tests/bsky-agent.test.ts
CHANGED
|
@@ -20,6 +20,20 @@ describe('agent', () => {
|
|
|
20
20
|
await close()
|
|
21
21
|
})
|
|
22
22
|
|
|
23
|
+
const getProfileDisplayName = async (
|
|
24
|
+
agent: BskyAgent,
|
|
25
|
+
): Promise<string | undefined> => {
|
|
26
|
+
try {
|
|
27
|
+
const res = await agent.api.app.bsky.actor.profile.get({
|
|
28
|
+
repo: agent.session?.did || '',
|
|
29
|
+
rkey: 'self',
|
|
30
|
+
})
|
|
31
|
+
return res.value.displayName ?? ''
|
|
32
|
+
} catch (err) {
|
|
33
|
+
return undefined
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
23
37
|
it('upsertProfile correctly creates and updates profiles.', async () => {
|
|
24
38
|
const agent = new BskyAgent({ service: server.url })
|
|
25
39
|
|
|
@@ -28,9 +42,8 @@ describe('agent', () => {
|
|
|
28
42
|
email: 'user1@test.com',
|
|
29
43
|
password: 'password',
|
|
30
44
|
})
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
expect(profile1.data.displayName).toBeFalsy()
|
|
45
|
+
const displayName1 = await getProfileDisplayName(agent)
|
|
46
|
+
expect(displayName1).toBeFalsy()
|
|
34
47
|
|
|
35
48
|
await agent.upsertProfile((existing) => {
|
|
36
49
|
expect(existing).toBeFalsy()
|
|
@@ -39,8 +52,8 @@ describe('agent', () => {
|
|
|
39
52
|
}
|
|
40
53
|
})
|
|
41
54
|
|
|
42
|
-
const
|
|
43
|
-
expect(
|
|
55
|
+
const displayName2 = await getProfileDisplayName(agent)
|
|
56
|
+
expect(displayName2).toBe('Bob')
|
|
44
57
|
|
|
45
58
|
await agent.upsertProfile((existing) => {
|
|
46
59
|
expect(existing).toBeTruthy()
|
|
@@ -49,8 +62,8 @@ describe('agent', () => {
|
|
|
49
62
|
}
|
|
50
63
|
})
|
|
51
64
|
|
|
52
|
-
const
|
|
53
|
-
expect(
|
|
65
|
+
const displayName3 = await getProfileDisplayName(agent)
|
|
66
|
+
expect(displayName3).toBe('BOB')
|
|
54
67
|
})
|
|
55
68
|
|
|
56
69
|
it('upsertProfile correctly handles CAS failures.', async () => {
|
|
@@ -62,8 +75,8 @@ describe('agent', () => {
|
|
|
62
75
|
password: 'password',
|
|
63
76
|
})
|
|
64
77
|
|
|
65
|
-
const
|
|
66
|
-
expect(
|
|
78
|
+
const displayName1 = await getProfileDisplayName(agent)
|
|
79
|
+
expect(displayName1).toBeFalsy()
|
|
67
80
|
|
|
68
81
|
let hasConflicted = false
|
|
69
82
|
let ranTwice = false
|
|
@@ -88,8 +101,8 @@ describe('agent', () => {
|
|
|
88
101
|
})
|
|
89
102
|
expect(ranTwice).toBe(true)
|
|
90
103
|
|
|
91
|
-
const
|
|
92
|
-
expect(
|
|
104
|
+
const displayName2 = await getProfileDisplayName(agent)
|
|
105
|
+
expect(displayName2).toBe('Bob')
|
|
93
106
|
})
|
|
94
107
|
|
|
95
108
|
it('upsertProfile wont endlessly retry CAS failures.', async () => {
|
|
@@ -101,8 +114,8 @@ describe('agent', () => {
|
|
|
101
114
|
password: 'password',
|
|
102
115
|
})
|
|
103
116
|
|
|
104
|
-
const
|
|
105
|
-
expect(
|
|
117
|
+
const displayName1 = await getProfileDisplayName(agent)
|
|
118
|
+
expect(displayName1).toBeFalsy()
|
|
106
119
|
|
|
107
120
|
const p = agent.upsertProfile(async (_existing) => {
|
|
108
121
|
await agent.com.atproto.repo.putRecord({
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { AtpAgent, RichText, RichTextSegment } from '../src'
|
|
2
|
+
import { isTag } from '../src/client/types/app/bsky/richtext/facet'
|
|
2
3
|
|
|
3
4
|
describe('detectFacets', () => {
|
|
4
5
|
const agent = new AtpAgent({ service: 'http://localhost' })
|
|
@@ -208,6 +209,109 @@ describe('detectFacets', () => {
|
|
|
208
209
|
expect(Array.from(rt.segments(), segmentToOutput)).toEqual(outputs[i])
|
|
209
210
|
}
|
|
210
211
|
})
|
|
212
|
+
|
|
213
|
+
it('correctly detects tags inline', async () => {
|
|
214
|
+
const inputs: [
|
|
215
|
+
string,
|
|
216
|
+
string[],
|
|
217
|
+
{ byteStart: number; byteEnd: number }[],
|
|
218
|
+
][] = [
|
|
219
|
+
['#a', ['#a'], [{ byteStart: 0, byteEnd: 2 }]],
|
|
220
|
+
[
|
|
221
|
+
'#a #b',
|
|
222
|
+
['#a', '#b'],
|
|
223
|
+
[
|
|
224
|
+
{ byteStart: 0, byteEnd: 2 },
|
|
225
|
+
{ byteStart: 3, byteEnd: 5 },
|
|
226
|
+
],
|
|
227
|
+
],
|
|
228
|
+
['#1', [], []],
|
|
229
|
+
['#tag', ['#tag'], [{ byteStart: 0, byteEnd: 4 }]],
|
|
230
|
+
['body #tag', ['#tag'], [{ byteStart: 5, byteEnd: 9 }]],
|
|
231
|
+
['#tag body', ['#tag'], [{ byteStart: 0, byteEnd: 4 }]],
|
|
232
|
+
['body #tag body', ['#tag'], [{ byteStart: 5, byteEnd: 9 }]],
|
|
233
|
+
['body #1', [], []],
|
|
234
|
+
['body #a1', ['#a1'], [{ byteStart: 5, byteEnd: 8 }]],
|
|
235
|
+
['#', [], []],
|
|
236
|
+
['text #', [], []],
|
|
237
|
+
['text # text', [], []],
|
|
238
|
+
[
|
|
239
|
+
'body #thisisa64characterstring_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
|
|
240
|
+
['#thisisa64characterstring_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'],
|
|
241
|
+
[{ byteStart: 5, byteEnd: 71 }],
|
|
242
|
+
],
|
|
243
|
+
[
|
|
244
|
+
'body #thisisa65characterstring_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaab',
|
|
245
|
+
[],
|
|
246
|
+
[],
|
|
247
|
+
],
|
|
248
|
+
[
|
|
249
|
+
'its a #double#rainbow',
|
|
250
|
+
['#double#rainbow'],
|
|
251
|
+
[{ byteStart: 6, byteEnd: 21 }],
|
|
252
|
+
],
|
|
253
|
+
['##hashash', ['##hashash'], [{ byteStart: 0, byteEnd: 9 }]],
|
|
254
|
+
['some #n0n3s@n5e!', ['#n0n3s@n5e'], [{ byteStart: 5, byteEnd: 15 }]],
|
|
255
|
+
[
|
|
256
|
+
'works #with,punctuation',
|
|
257
|
+
['#with,punctuation'],
|
|
258
|
+
[{ byteStart: 6, byteEnd: 23 }],
|
|
259
|
+
],
|
|
260
|
+
[
|
|
261
|
+
'strips trailing #punctuation, #like. #this!',
|
|
262
|
+
['#punctuation', '#like', '#this'],
|
|
263
|
+
[
|
|
264
|
+
{ byteStart: 16, byteEnd: 28 },
|
|
265
|
+
{ byteStart: 30, byteEnd: 35 },
|
|
266
|
+
{ byteStart: 37, byteEnd: 42 },
|
|
267
|
+
],
|
|
268
|
+
],
|
|
269
|
+
[
|
|
270
|
+
'strips #multi_trailing___...',
|
|
271
|
+
['#multi_trailing'],
|
|
272
|
+
[{ byteStart: 7, byteEnd: 22 }],
|
|
273
|
+
],
|
|
274
|
+
[
|
|
275
|
+
'works with #🦋 emoji, and #butter🦋fly',
|
|
276
|
+
['#🦋', '#butter🦋fly'],
|
|
277
|
+
[
|
|
278
|
+
{ byteStart: 11, byteEnd: 16 },
|
|
279
|
+
{ byteStart: 28, byteEnd: 42 },
|
|
280
|
+
],
|
|
281
|
+
],
|
|
282
|
+
[
|
|
283
|
+
'#same #same #but #diff',
|
|
284
|
+
['#same', '#same', '#but', '#diff'],
|
|
285
|
+
[
|
|
286
|
+
{ byteStart: 0, byteEnd: 5 },
|
|
287
|
+
{ byteStart: 6, byteEnd: 11 },
|
|
288
|
+
{ byteStart: 12, byteEnd: 16 },
|
|
289
|
+
{ byteStart: 17, byteEnd: 22 },
|
|
290
|
+
],
|
|
291
|
+
],
|
|
292
|
+
]
|
|
293
|
+
|
|
294
|
+
for (const [input, tags, indices] of inputs) {
|
|
295
|
+
const rt = new RichText({ text: input })
|
|
296
|
+
await rt.detectFacets(agent)
|
|
297
|
+
|
|
298
|
+
let detectedTags: string[] = []
|
|
299
|
+
let detectedIndices: { byteStart: number; byteEnd: number }[] = []
|
|
300
|
+
|
|
301
|
+
for (const { facet } of rt.segments()) {
|
|
302
|
+
if (!facet) continue
|
|
303
|
+
for (const feature of facet.features) {
|
|
304
|
+
if (isTag(feature)) {
|
|
305
|
+
detectedTags.push(feature.tag)
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
detectedIndices.push(facet.index)
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
expect(detectedTags).toEqual(tags)
|
|
312
|
+
expect(detectedIndices).toEqual(indices)
|
|
313
|
+
}
|
|
314
|
+
})
|
|
211
315
|
})
|
|
212
316
|
|
|
213
317
|
function segmentToOutput(segment: RichTextSegment): string[] {
|