@atproto/api 0.1.2 → 0.2.0

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 (205) hide show
  1. package/README.md +179 -25
  2. package/build.js +10 -1
  3. package/dist/agent.d.ts +11 -6
  4. package/dist/bsky-agent.d.ts +43 -0
  5. package/dist/client/index.d.ts +127 -215
  6. package/dist/client/lexicons.d.ts +1496 -1347
  7. package/dist/client/types/app/bsky/actor/defs.d.ts +47 -0
  8. package/dist/client/types/app/bsky/actor/getProfile.d.ts +2 -2
  9. package/dist/client/types/app/bsky/actor/getProfiles.d.ts +2 -2
  10. package/dist/client/types/app/bsky/actor/getSuggestions.d.ts +2 -2
  11. package/dist/client/types/app/bsky/actor/profile.d.ts +4 -60
  12. package/dist/client/types/app/bsky/actor/searchActors.d.ts +22 -0
  13. package/dist/client/types/app/bsky/actor/searchActorsTypeahead.d.ts +20 -0
  14. package/dist/client/types/app/bsky/actor/updateProfile.d.ts +4 -11
  15. package/dist/client/types/app/bsky/embed/external.d.ts +9 -13
  16. package/dist/client/types/app/bsky/embed/images.d.ts +9 -13
  17. package/dist/client/types/app/bsky/embed/record.d.ts +41 -0
  18. package/dist/client/types/app/bsky/embed/recordWithMedia.d.ts +24 -0
  19. package/dist/client/types/app/bsky/feed/defs.d.ts +77 -0
  20. package/dist/client/types/app/bsky/feed/getAuthorFeed.d.ts +4 -4
  21. package/dist/client/types/app/bsky/feed/getLikes.d.ts +34 -0
  22. package/dist/client/types/app/bsky/feed/getPostThread.d.ts +2 -24
  23. package/dist/client/types/app/bsky/feed/getRepostedBy.d.ts +3 -3
  24. package/dist/client/types/app/bsky/feed/getTimeline.d.ts +3 -3
  25. package/dist/client/types/app/bsky/feed/like.d.ts +9 -0
  26. package/dist/client/types/app/bsky/feed/post.d.ts +5 -29
  27. package/dist/client/types/app/bsky/graph/follow.d.ts +1 -2
  28. package/dist/client/types/app/bsky/graph/getFollowers.d.ts +5 -5
  29. package/dist/client/types/app/bsky/graph/getFollows.d.ts +5 -5
  30. package/dist/client/types/app/bsky/graph/getMutes.d.ts +3 -3
  31. package/dist/client/types/app/bsky/graph/muteActor.d.ts +17 -0
  32. package/dist/client/types/app/bsky/graph/unmuteActor.d.ts +17 -0
  33. package/dist/client/types/app/bsky/notification/getUnreadCount.d.ts +17 -0
  34. package/dist/client/types/app/bsky/notification/listNotifications.d.ts +35 -0
  35. package/dist/client/types/app/bsky/richtext/facet.d.ts +30 -0
  36. package/dist/client/types/app/bsky/unspecced/getPopular.d.ts +21 -0
  37. package/dist/client/types/com/atproto/admin/defs.d.ts +181 -0
  38. package/dist/client/types/com/atproto/admin/getModerationAction.d.ts +2 -2
  39. package/dist/client/types/com/atproto/admin/getModerationActions.d.ts +3 -3
  40. package/dist/client/types/com/atproto/admin/getModerationReport.d.ts +2 -2
  41. package/dist/client/types/com/atproto/admin/getModerationReports.d.ts +3 -3
  42. package/dist/client/types/com/atproto/admin/getRecord.d.ts +2 -2
  43. package/dist/client/types/com/atproto/admin/getRepo.d.ts +2 -2
  44. package/dist/client/types/com/atproto/admin/resolveModerationReports.d.ts +2 -2
  45. package/dist/client/types/com/atproto/admin/reverseModerationAction.d.ts +2 -2
  46. package/dist/client/types/com/atproto/admin/searchRepos.d.ts +3 -3
  47. package/dist/client/types/com/atproto/admin/takeModerationAction.d.ts +5 -6
  48. package/dist/client/types/com/atproto/identity/resolveHandle.d.ts +18 -0
  49. package/dist/client/types/com/atproto/identity/updateHandle.d.ts +17 -0
  50. package/dist/client/types/com/atproto/moderation/createReport.d.ts +38 -0
  51. package/dist/client/types/com/atproto/moderation/defs.d.ts +3 -0
  52. package/dist/client/types/com/atproto/repo/applyWrites.d.ts +47 -0
  53. package/dist/client/types/com/atproto/repo/createRecord.d.ts +7 -2
  54. package/dist/client/types/com/atproto/repo/deleteRecord.d.ts +7 -2
  55. package/dist/client/types/com/atproto/repo/describeRepo.d.ts +22 -0
  56. package/dist/client/types/com/atproto/repo/getRecord.d.ts +1 -1
  57. package/dist/client/types/com/atproto/repo/listRecords.d.ts +3 -3
  58. package/dist/client/types/com/atproto/repo/putRecord.d.ts +7 -2
  59. package/dist/client/types/com/atproto/repo/uploadBlob.d.ts +20 -0
  60. package/dist/client/types/com/atproto/server/createAccount.d.ts +44 -0
  61. package/dist/client/types/com/atproto/server/createInviteCode.d.ts +22 -0
  62. package/dist/client/types/com/atproto/server/createSession.d.ts +29 -0
  63. package/dist/client/types/com/atproto/server/deleteAccount.d.ts +25 -0
  64. package/dist/client/types/com/atproto/server/deleteSession.d.ts +13 -0
  65. package/dist/client/types/com/atproto/server/describeServer.d.ts +27 -0
  66. package/dist/client/types/com/atproto/server/getSession.d.ts +18 -0
  67. package/dist/client/types/com/atproto/server/refreshSession.d.ts +24 -0
  68. package/dist/client/types/com/atproto/server/requestAccountDelete.d.ts +13 -0
  69. package/dist/client/types/com/atproto/server/requestPasswordReset.d.ts +17 -0
  70. package/dist/client/types/com/atproto/server/resetPassword.d.ts +24 -0
  71. package/dist/client/types/com/atproto/sync/getBlob.d.ts +15 -0
  72. package/dist/client/types/com/atproto/sync/getBlocks.d.ts +15 -0
  73. package/dist/client/types/com/atproto/sync/listBlobs.d.ts +20 -0
  74. package/dist/client/types/com/atproto/sync/notifyOfUpdate.d.ts +13 -0
  75. package/dist/client/types/com/atproto/sync/requestCrawl.d.ts +13 -0
  76. package/dist/client/types/com/atproto/sync/subscribeAllRepos.d.ts +6 -17
  77. package/dist/client/types/com/atproto/sync/subscribeRepos.d.ts +58 -0
  78. package/dist/helpers/bsky.d.ts +20 -0
  79. package/dist/index.d.ts +6 -0
  80. package/dist/index.js +8222 -3608
  81. package/dist/index.js.map +4 -4
  82. package/dist/mixins/bsky.d.ts +23 -0
  83. package/dist/rich-text/detection.d.ts +4 -0
  84. package/dist/rich-text/rich-text.d.ts +39 -0
  85. package/dist/rich-text/sanitization.d.ts +4 -0
  86. package/dist/rich-text/sanitize.d.ts +4 -0
  87. package/dist/rich-text/unicode.d.ts +11 -0
  88. package/dist/types.d.ts +2 -2
  89. package/docs/rn-fetch-handler.ts +88 -0
  90. package/package.json +4 -1
  91. package/src/agent.ts +51 -15
  92. package/src/bsky-agent.ts +228 -0
  93. package/src/client/index.ts +371 -581
  94. package/src/client/lexicons.ts +1920 -1746
  95. package/src/client/types/app/bsky/actor/defs.ts +97 -0
  96. package/src/client/types/app/bsky/actor/getProfile.ts +4 -3
  97. package/src/client/types/app/bsky/actor/getProfiles.ts +4 -3
  98. package/src/client/types/app/bsky/actor/getSuggestions.ts +4 -3
  99. package/src/client/types/app/bsky/actor/profile.ts +5 -95
  100. package/src/client/types/app/bsky/actor/{searchTypeahead.ts → searchActors.ts} +6 -3
  101. package/src/client/types/app/bsky/actor/{search.ts → searchActorsTypeahead.ts} +4 -5
  102. package/src/client/types/app/bsky/embed/external.ts +14 -13
  103. package/src/client/types/app/bsky/embed/images.ts +14 -15
  104. package/src/client/types/app/bsky/embed/record.ts +90 -0
  105. package/src/client/types/app/bsky/embed/recordWithMedia.ts +53 -0
  106. package/src/client/types/app/bsky/feed/defs.ts +156 -0
  107. package/src/client/types/app/bsky/feed/getAuthorFeed.ts +6 -5
  108. package/src/client/types/app/bsky/feed/{getVotes.ts → getLikes.ts} +11 -12
  109. package/src/client/types/app/bsky/feed/getPostThread.ts +5 -48
  110. package/src/client/types/app/bsky/feed/getRepostedBy.ts +5 -4
  111. package/src/client/types/app/bsky/feed/getTimeline.ts +5 -4
  112. package/src/client/types/app/bsky/feed/{vote.ts → like.ts} +4 -4
  113. package/src/client/types/app/bsky/feed/post.ts +12 -51
  114. package/src/client/types/app/bsky/feed/repost.ts +2 -1
  115. package/src/client/types/app/bsky/graph/follow.ts +3 -3
  116. package/src/client/types/app/bsky/graph/getFollowers.ts +7 -6
  117. package/src/client/types/app/bsky/graph/getFollows.ts +7 -6
  118. package/src/client/types/app/bsky/graph/getMutes.ts +5 -4
  119. package/src/client/types/app/bsky/graph/{mute.ts → muteActor.ts} +3 -2
  120. package/src/client/types/app/bsky/graph/{unmute.ts → unmuteActor.ts} +3 -2
  121. package/src/client/types/app/bsky/notification/{getCount.ts → getUnreadCount.ts} +2 -1
  122. package/src/client/types/app/bsky/notification/{list.ts → listNotifications.ts} +13 -9
  123. package/src/client/types/app/bsky/notification/updateSeen.ts +2 -1
  124. package/src/client/types/app/bsky/richtext/facet.ts +81 -0
  125. package/src/client/types/app/bsky/unspecced/getPopular.ts +38 -0
  126. package/src/client/types/com/atproto/admin/defs.ts +366 -0
  127. package/src/client/types/com/atproto/admin/getModerationAction.ts +4 -3
  128. package/src/client/types/com/atproto/admin/getModerationActions.ts +5 -4
  129. package/src/client/types/com/atproto/admin/getModerationReport.ts +4 -3
  130. package/src/client/types/com/atproto/admin/getModerationReports.ts +5 -4
  131. package/src/client/types/com/atproto/admin/getRecord.ts +4 -3
  132. package/src/client/types/com/atproto/admin/getRepo.ts +4 -3
  133. package/src/client/types/com/atproto/admin/resolveModerationReports.ts +4 -3
  134. package/src/client/types/com/atproto/admin/reverseModerationAction.ts +4 -3
  135. package/src/client/types/com/atproto/admin/searchRepos.ts +5 -4
  136. package/src/client/types/com/atproto/admin/takeModerationAction.ts +10 -10
  137. package/src/client/types/com/atproto/{handle/resolve.ts → identity/resolveHandle.ts} +2 -1
  138. package/src/client/types/com/atproto/{handle/update.ts → identity/updateHandle.ts} +2 -1
  139. package/src/client/types/com/atproto/{report/create.ts → moderation/createReport.ts} +10 -10
  140. package/src/client/types/com/atproto/moderation/defs.ts +17 -0
  141. package/src/client/types/com/atproto/repo/{batchWrite.ts → applyWrites.ts} +21 -12
  142. package/src/client/types/com/atproto/repo/createRecord.ts +15 -3
  143. package/src/client/types/com/atproto/repo/deleteRecord.ts +15 -3
  144. package/src/client/types/com/atproto/repo/{describe.ts → describeRepo.ts} +3 -2
  145. package/src/client/types/com/atproto/repo/getRecord.ts +4 -3
  146. package/src/client/types/com/atproto/repo/listRecords.ts +7 -6
  147. package/src/client/types/com/atproto/repo/putRecord.ts +18 -6
  148. package/src/client/types/com/atproto/repo/strongRef.ts +2 -1
  149. package/src/client/types/com/atproto/{blob/upload.ts → repo/uploadBlob.ts} +3 -2
  150. package/src/client/types/com/atproto/{account/create.ts → server/createAccount.ts} +9 -1
  151. package/src/client/types/com/atproto/{account → server}/createInviteCode.ts +2 -1
  152. package/src/client/types/com/atproto/{session/create.ts → server/createSession.ts} +2 -1
  153. package/src/client/types/com/atproto/{account/delete.ts → server/deleteAccount.ts} +2 -1
  154. package/src/client/types/com/atproto/{account/requestDelete.ts → server/deleteSession.ts} +2 -1
  155. package/src/client/types/com/atproto/server/{getAccountsConfig.ts → describeServer.ts} +4 -3
  156. package/src/client/types/com/atproto/{session/get.ts → server/getSession.ts} +2 -1
  157. package/src/client/types/com/atproto/{session/refresh.ts → server/refreshSession.ts} +2 -1
  158. package/src/client/types/com/atproto/{session/delete.ts → server/requestAccountDelete.ts} +2 -1
  159. package/src/client/types/com/atproto/{account → server}/requestPasswordReset.ts +2 -1
  160. package/src/client/types/com/atproto/{account → server}/resetPassword.ts +2 -1
  161. package/src/client/types/com/atproto/sync/getBlob.ts +33 -0
  162. package/src/client/types/com/atproto/sync/getBlocks.ts +32 -0
  163. package/src/client/types/com/atproto/sync/getCheckout.ts +2 -1
  164. package/src/client/types/com/atproto/sync/getCommitPath.ts +2 -1
  165. package/src/client/types/com/atproto/sync/getHead.ts +2 -1
  166. package/src/client/types/com/atproto/sync/getRecord.ts +2 -1
  167. package/src/client/types/com/atproto/sync/getRepo.ts +2 -1
  168. package/src/client/types/com/atproto/sync/listBlobs.ts +40 -0
  169. package/src/client/types/com/atproto/sync/notifyOfUpdate.ts +30 -0
  170. package/src/client/types/com/atproto/{account/get.ts → sync/requestCrawl.ts} +6 -2
  171. package/src/client/types/com/atproto/sync/subscribeRepos.ts +131 -0
  172. package/src/index.ts +12 -0
  173. package/src/rich-text/detection.ts +83 -0
  174. package/src/rich-text/rich-text.ts +401 -0
  175. package/src/rich-text/sanitization.ts +40 -0
  176. package/src/rich-text/unicode.ts +47 -0
  177. package/src/types.ts +2 -2
  178. package/tests/agent.test.ts +10 -6
  179. package/tests/bsky-agent.test.ts +140 -0
  180. package/tests/errors.test.ts +4 -4
  181. package/tests/rich-text-detection.test.ts +229 -0
  182. package/tests/rich-text-sanitization.test.ts +211 -0
  183. package/tests/rich-text.test.ts +661 -0
  184. package/tsconfig.build.tsbuildinfo +1 -1
  185. package/src/client/types/app/bsky/actor/ref.ts +0 -64
  186. package/src/client/types/app/bsky/actor/updateProfile.ts +0 -71
  187. package/src/client/types/app/bsky/feed/feedViewPost.ts +0 -64
  188. package/src/client/types/app/bsky/feed/setVote.ts +0 -40
  189. package/src/client/types/app/bsky/graph/assertCreator.ts +0 -9
  190. package/src/client/types/app/bsky/graph/assertMember.ts +0 -9
  191. package/src/client/types/app/bsky/graph/assertion.ts +0 -27
  192. package/src/client/types/app/bsky/graph/confirmation.ts +0 -28
  193. package/src/client/types/app/bsky/system/actorUser.ts +0 -9
  194. package/src/client/types/app/bsky/system/declRef.ts +0 -26
  195. package/src/client/types/app/bsky/system/declaration.ts +0 -24
  196. package/src/client/types/com/atproto/admin/blob.ts +0 -84
  197. package/src/client/types/com/atproto/admin/moderationAction.ts +0 -118
  198. package/src/client/types/com/atproto/admin/moderationReport.ts +0 -64
  199. package/src/client/types/com/atproto/admin/record.ts +0 -92
  200. package/src/client/types/com/atproto/admin/repo.ts +0 -103
  201. package/src/client/types/com/atproto/repo/recordRef.ts +0 -25
  202. package/src/client/types/com/atproto/repo/repoRef.ts +0 -24
  203. package/src/client/types/com/atproto/report/reasonType.ts +0 -16
  204. package/src/client/types/com/atproto/report/subject.ts +0 -66
  205. package/src/client/types/com/atproto/sync/subscribeAllRepos.ts +0 -48
@@ -0,0 +1,401 @@
1
+ /*
2
+ = Rich Text Manipulation
3
+
4
+ When we sanitize rich text, we have to update the entity indices as the
5
+ text is modified. This can be modeled as inserts() and deletes() of the
6
+ rich text string. The possible scenarios are outlined below, along with
7
+ their expected behaviors.
8
+
9
+ NOTE: Slices are start inclusive, end exclusive
10
+
11
+ == richTextInsert()
12
+
13
+ Target string:
14
+
15
+ 0 1 2 3 4 5 6 7 8 910 // string indices
16
+ h e l l o w o r l d // string value
17
+ ^-------^ // target slice {start: 2, end: 7}
18
+
19
+ Scenarios:
20
+
21
+ A: ^ // insert "test" at 0
22
+ B: ^ // insert "test" at 4
23
+ C: ^ // insert "test" at 8
24
+
25
+ A = before -> move both by num added
26
+ B = inner -> move end by num added
27
+ C = after -> noop
28
+
29
+ Results:
30
+
31
+ A: 0 1 2 3 4 5 6 7 8 910 // string indices
32
+ t e s t h e l l o w // string value
33
+ ^-------^ // target slice {start: 6, end: 11}
34
+
35
+ B: 0 1 2 3 4 5 6 7 8 910 // string indices
36
+ h e l l t e s t o w // string value
37
+ ^---------------^ // target slice {start: 2, end: 11}
38
+
39
+ C: 0 1 2 3 4 5 6 7 8 910 // string indices
40
+ h e l l o w o t e s // string value
41
+ ^-------^ // target slice {start: 2, end: 7}
42
+
43
+ == richTextDelete()
44
+
45
+ Target string:
46
+
47
+ 0 1 2 3 4 5 6 7 8 910 // string indices
48
+ h e l l o w o r l d // string value
49
+ ^-------^ // target slice {start: 2, end: 7}
50
+
51
+ Scenarios:
52
+
53
+ A: ^---------------^ // remove slice {start: 0, end: 9}
54
+ B: ^-----^ // remove slice {start: 7, end: 11}
55
+ C: ^-----------^ // remove slice {start: 4, end: 11}
56
+ D: ^-^ // remove slice {start: 3, end: 5}
57
+ E: ^-----^ // remove slice {start: 1, end: 5}
58
+ F: ^-^ // remove slice {start: 0, end: 2}
59
+
60
+ A = entirely outer -> delete slice
61
+ B = entirely after -> noop
62
+ C = partially after -> move end to remove-start
63
+ D = entirely inner -> move end by num removed
64
+ E = partially before -> move start to remove-start index, move end by num removed
65
+ F = entirely before -> move both by num removed
66
+
67
+ Results:
68
+
69
+ A: 0 1 2 3 4 5 6 7 8 910 // string indices
70
+ l d // string value
71
+ // target slice (deleted)
72
+
73
+ B: 0 1 2 3 4 5 6 7 8 910 // string indices
74
+ h e l l o w // string value
75
+ ^-------^ // target slice {start: 2, end: 7}
76
+
77
+ C: 0 1 2 3 4 5 6 7 8 910 // string indices
78
+ h e l l // string value
79
+ ^-^ // target slice {start: 2, end: 4}
80
+
81
+ D: 0 1 2 3 4 5 6 7 8 910 // string indices
82
+ h e l w o r l d // string value
83
+ ^---^ // target slice {start: 2, end: 5}
84
+
85
+ E: 0 1 2 3 4 5 6 7 8 910 // string indices
86
+ h w o r l d // string value
87
+ ^-^ // target slice {start: 1, end: 3}
88
+
89
+ F: 0 1 2 3 4 5 6 7 8 910 // string indices
90
+ l l o w o r l d // string value
91
+ ^-------^ // target slice {start: 0, end: 5}
92
+ */
93
+
94
+ import { AtpAgent } from '../agent'
95
+ import { AppBskyFeedPost, AppBskyRichtextFacet } from '../client'
96
+ import { UnicodeString } from './unicode'
97
+ import { sanitizeRichText } from './sanitization'
98
+ import { detectFacets } from './detection'
99
+
100
+ export type Facet = AppBskyRichtextFacet.Main
101
+ export type FacetLink = AppBskyRichtextFacet.Link
102
+ export type FacetMention = AppBskyRichtextFacet.Mention
103
+ export type Entity = AppBskyFeedPost.Entity
104
+
105
+ export interface RichTextProps {
106
+ text: string
107
+ facets?: Facet[]
108
+ /**
109
+ * @deprecated Use facets instead
110
+ */
111
+ entities?: Entity[]
112
+ }
113
+
114
+ export interface RichTextOpts {
115
+ cleanNewlines?: boolean
116
+ }
117
+
118
+ export class RichTextSegment {
119
+ constructor(public text: string, public facet?: Facet) {}
120
+
121
+ get link(): FacetLink | undefined {
122
+ const link = this.facet?.features.find(AppBskyRichtextFacet.isLink)
123
+ if (AppBskyRichtextFacet.isLink(link)) {
124
+ return link
125
+ }
126
+ return undefined
127
+ }
128
+
129
+ isLink() {
130
+ return !!this.link
131
+ }
132
+
133
+ get mention(): FacetMention | undefined {
134
+ const mention = this.facet?.features.find(AppBskyRichtextFacet.isMention)
135
+ if (AppBskyRichtextFacet.isMention(mention)) {
136
+ return mention
137
+ }
138
+ return undefined
139
+ }
140
+
141
+ isMention() {
142
+ return !!this.mention
143
+ }
144
+ }
145
+
146
+ export class RichText {
147
+ unicodeText: UnicodeString
148
+ facets?: Facet[]
149
+
150
+ constructor(props: RichTextProps, opts?: RichTextOpts) {
151
+ this.unicodeText = new UnicodeString(props.text)
152
+ this.facets = props.facets
153
+ if (!this.facets?.length && props.entities?.length) {
154
+ this.facets = entitiesToFacets(this.unicodeText, props.entities)
155
+ }
156
+ if (this.facets) {
157
+ this.facets.sort(facetSort)
158
+ }
159
+ if (opts?.cleanNewlines) {
160
+ sanitizeRichText(this, { cleanNewlines: true }).copyInto(this)
161
+ }
162
+ }
163
+
164
+ get text() {
165
+ return this.unicodeText.toString()
166
+ }
167
+
168
+ get length() {
169
+ return this.unicodeText.length
170
+ }
171
+
172
+ get graphemeLength() {
173
+ return this.unicodeText.graphemeLength
174
+ }
175
+
176
+ clone() {
177
+ return new RichText({
178
+ text: this.unicodeText.utf16,
179
+ facets: cloneDeep(this.facets),
180
+ })
181
+ }
182
+
183
+ copyInto(target: RichText) {
184
+ target.unicodeText = this.unicodeText
185
+ target.facets = cloneDeep(this.facets)
186
+ }
187
+
188
+ *segments(): Generator<RichTextSegment, void, void> {
189
+ const facets = this.facets || []
190
+ if (!facets.length) {
191
+ yield new RichTextSegment(this.unicodeText.utf16)
192
+ return
193
+ }
194
+
195
+ let textCursor = 0
196
+ let facetCursor = 0
197
+ do {
198
+ const currFacet = facets[facetCursor]
199
+ if (textCursor < currFacet.index.byteStart) {
200
+ yield new RichTextSegment(
201
+ this.unicodeText.slice(textCursor, currFacet.index.byteStart),
202
+ )
203
+ } else if (textCursor > currFacet.index.byteStart) {
204
+ facetCursor++
205
+ continue
206
+ }
207
+ if (currFacet.index.byteStart < currFacet.index.byteEnd) {
208
+ const subtext = this.unicodeText.slice(
209
+ currFacet.index.byteStart,
210
+ currFacet.index.byteEnd,
211
+ )
212
+ if (!subtext.trim()) {
213
+ // dont empty string entities
214
+ yield new RichTextSegment(subtext)
215
+ } else {
216
+ yield new RichTextSegment(subtext, currFacet)
217
+ }
218
+ }
219
+ textCursor = currFacet.index.byteEnd
220
+ facetCursor++
221
+ } while (facetCursor < facets.length)
222
+ if (textCursor < this.unicodeText.length) {
223
+ yield new RichTextSegment(
224
+ this.unicodeText.slice(textCursor, this.unicodeText.length),
225
+ )
226
+ }
227
+ }
228
+
229
+ insert(insertIndex: number, insertText: string) {
230
+ this.unicodeText = new UnicodeString(
231
+ this.unicodeText.slice(0, insertIndex) +
232
+ insertText +
233
+ this.unicodeText.slice(insertIndex),
234
+ )
235
+
236
+ if (!this.facets?.length) {
237
+ return this
238
+ }
239
+
240
+ const numCharsAdded = insertText.length
241
+ for (const ent of this.facets) {
242
+ // see comment at top of file for labels of each scenario
243
+ // scenario A (before)
244
+ if (insertIndex <= ent.index.byteStart) {
245
+ // move both by num added
246
+ ent.index.byteStart += numCharsAdded
247
+ ent.index.byteEnd += numCharsAdded
248
+ }
249
+ // scenario B (inner)
250
+ else if (
251
+ insertIndex >= ent.index.byteStart &&
252
+ insertIndex < ent.index.byteEnd
253
+ ) {
254
+ // move end by num added
255
+ ent.index.byteEnd += numCharsAdded
256
+ }
257
+ // scenario C (after)
258
+ // noop
259
+ }
260
+ return this
261
+ }
262
+
263
+ delete(removeStartIndex: number, removeEndIndex: number) {
264
+ this.unicodeText = new UnicodeString(
265
+ this.unicodeText.slice(0, removeStartIndex) +
266
+ this.unicodeText.slice(removeEndIndex),
267
+ )
268
+
269
+ if (!this.facets?.length) {
270
+ return this
271
+ }
272
+
273
+ const numCharsRemoved = removeEndIndex - removeStartIndex
274
+ for (const ent of this.facets) {
275
+ // see comment at top of file for labels of each scenario
276
+ // scenario A (entirely outer)
277
+ if (
278
+ removeStartIndex <= ent.index.byteStart &&
279
+ removeEndIndex >= ent.index.byteEnd
280
+ ) {
281
+ // delete slice (will get removed in final pass)
282
+ ent.index.byteStart = 0
283
+ ent.index.byteEnd = 0
284
+ }
285
+ // scenario B (entirely after)
286
+ else if (removeStartIndex > ent.index.byteEnd) {
287
+ // noop
288
+ }
289
+ // scenario C (partially after)
290
+ else if (
291
+ removeStartIndex > ent.index.byteStart &&
292
+ removeStartIndex <= ent.index.byteEnd &&
293
+ removeEndIndex > ent.index.byteEnd
294
+ ) {
295
+ // move end to remove start
296
+ ent.index.byteEnd = removeStartIndex
297
+ }
298
+ // scenario D (entirely inner)
299
+ else if (
300
+ removeStartIndex >= ent.index.byteStart &&
301
+ removeEndIndex <= ent.index.byteEnd
302
+ ) {
303
+ // move end by num removed
304
+ ent.index.byteEnd -= numCharsRemoved
305
+ }
306
+ // scenario E (partially before)
307
+ else if (
308
+ removeStartIndex < ent.index.byteStart &&
309
+ removeEndIndex >= ent.index.byteStart &&
310
+ removeEndIndex <= ent.index.byteEnd
311
+ ) {
312
+ // move start to remove-start index, move end by num removed
313
+ ent.index.byteStart = removeStartIndex
314
+ ent.index.byteEnd -= numCharsRemoved
315
+ }
316
+ // scenario F (entirely before)
317
+ else if (removeEndIndex < ent.index.byteStart) {
318
+ // move both by num removed
319
+ ent.index.byteStart -= numCharsRemoved
320
+ ent.index.byteEnd -= numCharsRemoved
321
+ }
322
+ }
323
+
324
+ // filter out any facets that were made irrelevant
325
+ this.facets = this.facets.filter(
326
+ (ent) => ent.index.byteStart < ent.index.byteEnd,
327
+ )
328
+ return this
329
+ }
330
+
331
+ /**
332
+ * Detects facets such as links and mentions
333
+ * Note: Overwrites the existing facets with auto-detected facets
334
+ */
335
+ async detectFacets(agent: AtpAgent) {
336
+ this.facets = detectFacets(this.unicodeText)
337
+ if (this.facets) {
338
+ for (const facet of this.facets) {
339
+ for (const feature of facet.features) {
340
+ if (AppBskyRichtextFacet.isMention(feature)) {
341
+ const did = await agent
342
+ .resolveHandle({ handle: feature.did })
343
+ .catch((_) => undefined)
344
+ .then((res) => res?.data.did)
345
+ feature.did = did || ''
346
+ }
347
+ }
348
+ }
349
+ this.facets.sort(facetSort)
350
+ }
351
+ }
352
+
353
+ /**
354
+ * Detects facets such as links and mentions but does not resolve them
355
+ * Will produce invalid facets! For instance, mentions will not have their DIDs set.
356
+ * Note: Overwrites the existing facets with auto-detected facets
357
+ */
358
+ detectFacetsWithoutResolution() {
359
+ this.facets = detectFacets(this.unicodeText)
360
+ if (this.facets) {
361
+ this.facets.sort(facetSort)
362
+ }
363
+ }
364
+ }
365
+
366
+ const facetSort = (a, b) => a.index.byteStart - b.index.byteStart
367
+
368
+ function entitiesToFacets(text: UnicodeString, entities: Entity[]): Facet[] {
369
+ const facets: Facet[] = []
370
+ for (const ent of entities) {
371
+ if (ent.type === 'link') {
372
+ facets.push({
373
+ $type: 'app.bsky.richtext.facet',
374
+ index: {
375
+ byteStart: text.utf16IndexToUtf8Index(ent.index.start),
376
+ byteEnd: text.utf16IndexToUtf8Index(ent.index.end),
377
+ },
378
+ features: [{ $type: 'app.bsky.richtext.facet#link', uri: ent.value }],
379
+ })
380
+ } else if (ent.type === 'mention') {
381
+ facets.push({
382
+ $type: 'app.bsky.richtext.facet',
383
+ index: {
384
+ byteStart: text.utf16IndexToUtf8Index(ent.index.start),
385
+ byteEnd: text.utf16IndexToUtf8Index(ent.index.end),
386
+ },
387
+ features: [
388
+ { $type: 'app.bsky.richtext.facet#mention', did: ent.value },
389
+ ],
390
+ })
391
+ }
392
+ }
393
+ return facets
394
+ }
395
+
396
+ function cloneDeep<T>(v: T): T {
397
+ if (typeof v === 'undefined') {
398
+ return v
399
+ }
400
+ return JSON.parse(JSON.stringify(v))
401
+ }
@@ -0,0 +1,40 @@
1
+ import { RichText } from './rich-text'
2
+ import { UnicodeString } from './unicode'
3
+
4
+ const EXCESS_SPACE_RE = /[\r\n]([\u00AD\u2060\u200D\u200C\u200B\s]*[\r\n]){2,}/
5
+ const REPLACEMENT_STR = '\n\n'
6
+
7
+ export function sanitizeRichText(
8
+ richText: RichText,
9
+ opts: { cleanNewlines?: boolean },
10
+ ) {
11
+ if (opts.cleanNewlines) {
12
+ richText = clean(richText, EXCESS_SPACE_RE, REPLACEMENT_STR)
13
+ }
14
+ return richText
15
+ }
16
+
17
+ function clean(
18
+ richText: RichText,
19
+ targetRegexp: RegExp,
20
+ replacementString: string,
21
+ ): RichText {
22
+ richText = richText.clone()
23
+
24
+ let match = richText.unicodeText.utf16.match(targetRegexp)
25
+ while (match && typeof match.index !== 'undefined') {
26
+ const oldText = richText.unicodeText
27
+ const removeStartIndex = richText.unicodeText.utf16IndexToUtf8Index(
28
+ match.index,
29
+ )
30
+ const removeEndIndex = removeStartIndex + new UnicodeString(match[0]).length
31
+ richText.delete(removeStartIndex, removeEndIndex)
32
+ if (richText.unicodeText.utf16 === oldText.utf16) {
33
+ break // sanity check
34
+ }
35
+ richText.insert(removeStartIndex, replacementString)
36
+ match = richText.unicodeText.utf16.match(targetRegexp)
37
+ }
38
+
39
+ return richText
40
+ }
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Javascript uses utf16-encoded strings while most environments and specs
3
+ * have standardized around utf8 (including JSON).
4
+ *
5
+ * After some lengthy debated we decided that richtext facets need to use
6
+ * utf8 indices. This means we need tools to convert indices between utf8
7
+ * and utf16, and that's precisely what this library handles.
8
+ */
9
+
10
+ import { graphemeLen } from '@atproto/common-web'
11
+
12
+ const encoder = new TextEncoder()
13
+ const decoder = new TextDecoder()
14
+
15
+ export class UnicodeString {
16
+ utf16: string
17
+ utf8: Uint8Array
18
+ private _graphemeLen?: number | undefined
19
+
20
+ constructor(utf16: string) {
21
+ this.utf16 = utf16
22
+ this.utf8 = encoder.encode(utf16)
23
+ }
24
+
25
+ get length() {
26
+ return this.utf8.byteLength
27
+ }
28
+
29
+ get graphemeLength() {
30
+ if (!this._graphemeLen) {
31
+ this._graphemeLen = graphemeLen(this.utf16)
32
+ }
33
+ return this._graphemeLen
34
+ }
35
+
36
+ slice(start?: number, end?: number): string {
37
+ return decoder.decode(this.utf8.slice(start, end))
38
+ }
39
+
40
+ utf16IndexToUtf8Index(i: number) {
41
+ return encoder.encode(this.utf16.slice(0, i)).byteLength
42
+ }
43
+
44
+ toString() {
45
+ return this.utf16
46
+ }
47
+ }
package/src/types.ts CHANGED
@@ -56,7 +56,7 @@ export interface AtpAgentFetchHandlerResponse {
56
56
  headers: Record<string, string>
57
57
  body: any
58
58
  }
59
- export type AptAgentFetchHandler = (
59
+ export type AtpAgentFetchHandler = (
60
60
  httpUri: string,
61
61
  httpMethod: string,
62
62
  httpHeaders: AtpAgentFetchHeaders,
@@ -67,5 +67,5 @@ export type AptAgentFetchHandler = (
67
67
  * AtpAgent global config opts
68
68
  */
69
69
  export interface AtpAgentGlobalOpts {
70
- fetch: AptAgentFetchHandler
70
+ fetch: AtpAgentFetchHandler
71
71
  }
@@ -17,7 +17,7 @@ describe('agent', () => {
17
17
 
18
18
  beforeAll(async () => {
19
19
  server = await runTestServer({
20
- dbPostgresSchema: 'session',
20
+ dbPostgresSchema: 'api_agent',
21
21
  })
22
22
  close = server.close
23
23
  })
@@ -48,7 +48,9 @@ describe('agent', () => {
48
48
  expect(agent.session?.handle).toEqual(res.data.handle)
49
49
  expect(agent.session?.did).toEqual(res.data.did)
50
50
 
51
- const { data: sessionInfo } = await agent.api.com.atproto.session.get({})
51
+ const { data: sessionInfo } = await agent.api.com.atproto.server.getSession(
52
+ {},
53
+ )
52
54
  expect(sessionInfo).toEqual({
53
55
  did: res.data.did,
54
56
  handle: res.data.handle,
@@ -88,7 +90,8 @@ describe('agent', () => {
88
90
  expect(agent2.session?.handle).toEqual(res1.data.handle)
89
91
  expect(agent2.session?.did).toEqual(res1.data.did)
90
92
 
91
- const { data: sessionInfo } = await agent2.api.com.atproto.session.get({})
93
+ const { data: sessionInfo } =
94
+ await agent2.api.com.atproto.server.getSession({})
92
95
  expect(sessionInfo).toEqual({
93
96
  did: res1.data.did,
94
97
  handle: res1.data.handle,
@@ -128,7 +131,8 @@ describe('agent', () => {
128
131
  expect(agent2.session?.handle).toEqual(res1.data.handle)
129
132
  expect(agent2.session?.did).toEqual(res1.data.did)
130
133
 
131
- const { data: sessionInfo } = await agent2.api.com.atproto.session.get({})
134
+ const { data: sessionInfo } =
135
+ await agent2.api.com.atproto.server.getSession({})
132
136
  expect(sessionInfo).toEqual({
133
137
  did: res1.data.did,
134
138
  handle: res1.data.handle,
@@ -248,7 +252,7 @@ describe('agent', () => {
248
252
  body: { error: 'ExpiredToken' },
249
253
  }
250
254
  }
251
- if (httpUri.includes('com.atproto.session.refresh')) {
255
+ if (httpUri.includes('com.atproto.server.refreshSession')) {
252
256
  refreshCalls++
253
257
  }
254
258
  return defaultFetchHandler(httpUri, httpMethod, httpHeaders, httpReqBody)
@@ -358,7 +362,7 @@ describe('agent', () => {
358
362
  body: { error: 'ExpiredToken' },
359
363
  }
360
364
  }
361
- if (httpUri.includes('com.atproto.session.refresh')) {
365
+ if (httpUri.includes('com.atproto.server.refreshSession')) {
362
366
  return {
363
367
  status: 500,
364
368
  headers: {},
@@ -0,0 +1,140 @@
1
+ import {
2
+ CloseFn,
3
+ runTestServer,
4
+ TestServerInfo,
5
+ } from '@atproto/pds/tests/_util'
6
+ import { BskyAgent, ComAtprotoRepoPutRecord, AppBskyActorProfile } from '..'
7
+
8
+ describe('agent', () => {
9
+ let server: TestServerInfo
10
+ let close: CloseFn
11
+
12
+ beforeAll(async () => {
13
+ server = await runTestServer({
14
+ dbPostgresSchema: 'bsky_agent',
15
+ })
16
+ close = server.close
17
+ })
18
+
19
+ afterAll(async () => {
20
+ await close()
21
+ })
22
+
23
+ it('upsertProfile correctly creates and updates profiles.', async () => {
24
+ const agent = new BskyAgent({ service: server.url })
25
+
26
+ await agent.createAccount({
27
+ handle: 'user1.test',
28
+ email: 'user1@test.com',
29
+ password: 'password',
30
+ })
31
+
32
+ const profile1 = await agent.getProfile({ actor: agent.session?.did || '' })
33
+ expect(profile1.data.displayName).toBeFalsy()
34
+
35
+ await agent.upsertProfile((existing) => {
36
+ expect(existing).toBeFalsy()
37
+ return {
38
+ displayName: 'Bob',
39
+ }
40
+ })
41
+
42
+ const profile2 = await agent.getProfile({ actor: agent.session?.did || '' })
43
+ expect(profile2.data.displayName).toBe('Bob')
44
+
45
+ await agent.upsertProfile((existing) => {
46
+ expect(existing).toBeTruthy()
47
+ return {
48
+ displayName: existing?.displayName?.toUpperCase(),
49
+ }
50
+ })
51
+
52
+ const profile3 = await agent.getProfile({ actor: agent.session?.did || '' })
53
+ expect(profile3.data.displayName).toBe('BOB')
54
+ })
55
+
56
+ it('upsertProfile correctly handles CAS failures.', async () => {
57
+ const agent = new BskyAgent({ service: server.url })
58
+
59
+ await agent.createAccount({
60
+ handle: 'user2.test',
61
+ email: 'user2@test.com',
62
+ password: 'password',
63
+ })
64
+
65
+ const profile1 = await agent.getProfile({ actor: agent.session?.did || '' })
66
+ expect(profile1.data.displayName).toBeFalsy()
67
+
68
+ let hasConflicted = false
69
+ let ranTwice = false
70
+ await agent.upsertProfile(async (existing) => {
71
+ if (!hasConflicted) {
72
+ await agent.com.atproto.repo.putRecord({
73
+ repo: agent.session?.did || '',
74
+ collection: 'app.bsky.actor.profile',
75
+ rkey: 'self',
76
+ record: {
77
+ $type: 'app.bsky.actor.profile',
78
+ displayName: String(Math.random()),
79
+ },
80
+ })
81
+ hasConflicted = true
82
+ } else {
83
+ ranTwice = true
84
+ }
85
+ return {
86
+ displayName: 'Bob',
87
+ }
88
+ })
89
+ expect(ranTwice).toBe(true)
90
+
91
+ const profile2 = await agent.getProfile({ actor: agent.session?.did || '' })
92
+ expect(profile2.data.displayName).toBe('Bob')
93
+ })
94
+
95
+ it('upsertProfile wont endlessly retry CAS failures.', async () => {
96
+ const agent = new BskyAgent({ service: server.url })
97
+
98
+ await agent.createAccount({
99
+ handle: 'user3.test',
100
+ email: 'user3@test.com',
101
+ password: 'password',
102
+ })
103
+
104
+ const profile1 = await agent.getProfile({ actor: agent.session?.did || '' })
105
+ expect(profile1.data.displayName).toBeFalsy()
106
+
107
+ const p = agent.upsertProfile(async (existing) => {
108
+ await agent.com.atproto.repo.putRecord({
109
+ repo: agent.session?.did || '',
110
+ collection: 'app.bsky.actor.profile',
111
+ rkey: 'self',
112
+ record: {
113
+ $type: 'app.bsky.actor.profile',
114
+ displayName: String(Math.random()),
115
+ },
116
+ })
117
+ return {
118
+ displayName: 'Bob',
119
+ }
120
+ })
121
+ await expect(p).rejects.toThrow(ComAtprotoRepoPutRecord.InvalidSwapError)
122
+ })
123
+
124
+ it('upsertProfile validates the record.', async () => {
125
+ const agent = new BskyAgent({ service: server.url })
126
+
127
+ await agent.createAccount({
128
+ handle: 'user4.test',
129
+ email: 'user4@test.com',
130
+ password: 'password',
131
+ })
132
+
133
+ const p = agent.upsertProfile((existing) => {
134
+ return {
135
+ displayName: { string: 'Bob' },
136
+ } as unknown as AppBskyActorProfile.Record
137
+ })
138
+ await expect(p).rejects.toThrow('Record/displayName must be a string')
139
+ })
140
+ })