@atcute/bluesky-threading 1.0.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.
package/LICENSE ADDED
@@ -0,0 +1,17 @@
1
+ Permission is hereby granted, free of charge, to any person obtaining a copy
2
+ of this software and associated documentation files (the "Software"), to deal
3
+ in the Software without restriction, including without limitation the rights
4
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
5
+ copies of the Software, and to permit persons to whom the Software is
6
+ furnished to do so, subject to the following conditions:
7
+
8
+ The above copyright notice and this permission notice shall be included in all
9
+ copies or substantial portions of the Software.
10
+
11
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
12
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
13
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
14
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
15
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
16
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
17
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,38 @@
1
+ # @atcute/bluesky-threading
2
+
3
+ create Bluesky threads containing multiple posts with one write.
4
+
5
+ ```ts
6
+ import { XRPC } from '@atcute/client';
7
+ import { AtpAuth } from '@atcute/client/middlewares/auth';
8
+
9
+ import RichTextBuilder from '@atcute/bluesky-richtext-builder';
10
+ import { publishThread } from '@atcute/bluesky-threading';
11
+
12
+ const rpc = new XRPC({ service: 'https://bsky.social' });
13
+ const auth = new AtpAuth(rpc);
14
+
15
+ await auth.login({ identifier: '...', password: '...' });
16
+
17
+ await publishThread(rpc, {
18
+ author: 'did:plc:ia76kvnndjutgedggx2ibrem',
19
+ languages: ['en'],
20
+ posts: [
21
+ {
22
+ content: new RichTextBuilder()
23
+ .addText('Hello, please visit my website! ')
24
+ .addLink('example.com', 'https://example.com'),
25
+ },
26
+ {
27
+ content: {
28
+ text: `Here's the second post!`,
29
+ },
30
+ },
31
+ {
32
+ content: {
33
+ text: `Third post for good measure.`,
34
+ },
35
+ },
36
+ ],
37
+ });
38
+ ```
package/dist/cbor.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ export declare function serializeRecordCid(record: {
2
+ $type: string;
3
+ }): Promise<string>;
package/dist/cbor.js ADDED
@@ -0,0 +1,12 @@
1
+ import { encode } from '@atcute/cbor';
2
+ import { create, format } from '@atcute/cid';
3
+ // Sanity-check by requiring a $type here, this is because the records are
4
+ // expected to be encoded with it, even though the PDS accepts record writes
5
+ // without the field.
6
+ export async function serializeRecordCid(record) {
7
+ const bytes = encode(record);
8
+ const cid = await create(0x71, bytes);
9
+ const serialized = format(cid);
10
+ return serialized;
11
+ }
12
+ //# sourceMappingURL=cbor.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cbor.js","sourceRoot":"","sources":["../lib/cbor.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AACtC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAE7C,0EAA0E;AAC1E,4EAA4E;AAC5E,qBAAqB;AACrB,MAAM,CAAC,KAAK,UAAU,kBAAkB,CAAC,MAAyB;IACjE,MAAM,KAAK,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC;IAE7B,MAAM,GAAG,GAAG,MAAM,MAAM,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;IACtC,MAAM,UAAU,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC;IAE/B,OAAO,UAAU,CAAC;AACnB,CAAC"}
@@ -0,0 +1,18 @@
1
+ import '@atcute/bluesky/lexicons';
2
+ import { type XRPC } from '@atcute/client';
3
+ import type { Brand, ComAtprotoRepoApplyWrites } from '@atcute/client/lexicons';
4
+ import type { ComposedThread } from './types.js';
5
+ export type * from './types.js';
6
+ /**
7
+ * Create post records and publish them
8
+ * @param rpc An authenticated Bluesky RPC client
9
+ * @param thread Composed thread
10
+ * @returns An array of post records that were published
11
+ */
12
+ export declare function publishThread(rpc: XRPC, thread: Omit<ComposedThread, 'rpc'>): Promise<Brand.Union<ComAtprotoRepoApplyWrites.Create>[]>;
13
+ /**
14
+ * Create post records without publishing, allows you to do it yourself.
15
+ * @param thread Composed thread
16
+ * @returns An array of post records
17
+ */
18
+ export declare function createThread(thread: ComposedThread): Promise<Brand.Union<ComAtprotoRepoApplyWrites.Create>[]>;
package/dist/index.js ADDED
@@ -0,0 +1,306 @@
1
+ import '@atcute/bluesky/lexicons';
2
+ import { XRPCError } from '@atcute/client';
3
+ import * as TID from '@atcute/tid';
4
+ import { serializeRecordCid } from './cbor.js';
5
+ import { getNow } from './time.js';
6
+ /**
7
+ * Create post records and publish them
8
+ * @param rpc An authenticated Bluesky RPC client
9
+ * @param thread Composed thread
10
+ * @returns An array of post records that were published
11
+ */
12
+ export async function publishThread(rpc, thread) {
13
+ const records = await createThread({ ...thread, rpc });
14
+ await rpc.call('com.atproto.repo.applyWrites', {
15
+ signal: thread.signal,
16
+ data: {
17
+ repo: thread.author,
18
+ writes: records,
19
+ },
20
+ });
21
+ return records;
22
+ }
23
+ /**
24
+ * Create post records without publishing, allows you to do it yourself.
25
+ * @param thread Composed thread
26
+ * @returns An array of post records
27
+ */
28
+ export async function createThread(thread) {
29
+ const rpc = thread.rpc;
30
+ const signal = thread.signal;
31
+ const did = thread.author;
32
+ const posts = thread.posts;
33
+ const threadgate = thread.gate;
34
+ const languages = thread.languages;
35
+ const writes = [];
36
+ const now = thread.createdAt !== undefined ? new Date(thread.createdAt) : new Date(getNow());
37
+ assert(!Number.isNaN(now.getTime()), `provided createdAt value is invalid`);
38
+ let reply;
39
+ let rkey;
40
+ if (thread.reply) {
41
+ let post = thread.reply;
42
+ if (typeof post === 'string') {
43
+ // AT-URI being passed
44
+ assertXrpc(rpc, `ComposedThread.reply`);
45
+ post = await getPost(post);
46
+ }
47
+ let root;
48
+ let ref;
49
+ if ('record' in post) {
50
+ // AppBskyFeedDefs.PostView being passed
51
+ root = post.record.reply?.root;
52
+ ref = { uri: post.uri, cid: post.cid };
53
+ }
54
+ else if ('value' in post) {
55
+ // AppBskyEmbedRecord.ViewRecord being passed
56
+ root = post.value.reply?.root;
57
+ ref = { uri: post.uri, cid: post.cid };
58
+ }
59
+ else {
60
+ assert(false, `Unexpected end of code`);
61
+ }
62
+ reply = {
63
+ root: root ? { uri: root.uri, cid: root.cid } : ref,
64
+ parent: ref,
65
+ };
66
+ }
67
+ assert(!reply || !threadgate, `threadgate and reply are mutually exclusive`);
68
+ for (let idx = 0, len = posts.length; idx < len; idx++) {
69
+ // Get the record key for this post
70
+ rkey = TID.createRaw(now.getTime(), Math.floor(Math.random() * 1023));
71
+ const post = posts[idx];
72
+ const uri = `at://${did}/app.bsky.feed.post/${rkey}`;
73
+ // Resolve embeds
74
+ let embed;
75
+ if (post.embed !== undefined) {
76
+ embed = await resolveEmbed(post.embed);
77
+ }
78
+ // Get the self-labels
79
+ const labels = getEmbedLabels(post.embed);
80
+ let selfLabels;
81
+ if (labels?.length) {
82
+ selfLabels = {
83
+ $type: 'com.atproto.label.defs#selfLabels',
84
+ values: labels.map((val) => ({ val })),
85
+ };
86
+ }
87
+ // Now form the record
88
+ const content = post.content;
89
+ const record = {
90
+ // IMPORTANT: $type has to exist, CID is calculated with the `$type` field
91
+ // present and will produce the wrong CID if you omit it.
92
+ // `createRecord` and `applyWrites` currently lets you omit this and it'll
93
+ // add it for you, but we want to avoid that here.
94
+ $type: 'app.bsky.feed.post',
95
+ createdAt: now.toISOString(),
96
+ text: content.text,
97
+ facets: content.facets,
98
+ reply: reply,
99
+ embed: embed,
100
+ langs: post.languages ?? languages,
101
+ labels: selfLabels,
102
+ };
103
+ writes.push({
104
+ $type: 'com.atproto.repo.applyWrites#create',
105
+ collection: 'app.bsky.feed.post',
106
+ rkey: rkey,
107
+ value: record,
108
+ });
109
+ // If this is the first post, and we have a threadgate set, create one now.
110
+ if (idx === 0 && threadgate) {
111
+ const threadgateRecord = {
112
+ createdAt: now.toISOString(),
113
+ post: uri,
114
+ allow: resolveThreadgate(threadgate),
115
+ };
116
+ writes.push({
117
+ $type: 'com.atproto.repo.applyWrites#create',
118
+ collection: 'app.bsky.feed.threadgate',
119
+ rkey: rkey,
120
+ value: threadgateRecord,
121
+ });
122
+ }
123
+ if (idx !== len - 1) {
124
+ // Retrieve the next reply reference
125
+ const serialized = await serializeRecordCid(record);
126
+ const ref = {
127
+ cid: serialized,
128
+ uri: uri,
129
+ };
130
+ reply = {
131
+ root: reply ? reply.root : ref,
132
+ parent: ref,
133
+ };
134
+ // Posts are not guaranteed to be shown in the correct order if they are
135
+ // all posted with the same timestamp.
136
+ now.setMilliseconds(now.getMilliseconds() + 1);
137
+ }
138
+ }
139
+ return writes;
140
+ async function resolveEmbed(root) {
141
+ const type = root.type;
142
+ if (type === 'recordWithMedia') {
143
+ return {
144
+ $type: 'app.bsky.embed.recordWithMedia',
145
+ record: await resolveRecordEmbed(root.record),
146
+ media: await resolveMediaEmbed(root.media),
147
+ };
148
+ }
149
+ else if (type === 'image' || type === 'external') {
150
+ return await resolveMediaEmbed(root);
151
+ }
152
+ else {
153
+ return await resolveRecordEmbed(root);
154
+ }
155
+ async function resolveMediaEmbed(embed) {
156
+ const type = embed.type;
157
+ if (type === 'external') {
158
+ const rawThumb = embed.thumbnail;
159
+ let thumb;
160
+ if (rawThumb !== undefined) {
161
+ if (rawThumb instanceof Blob) {
162
+ assertXrpc(rpc, `PostExternalEmbed.thumbnail`);
163
+ thumb = await uploadBlob(rawThumb);
164
+ }
165
+ else {
166
+ thumb = rawThumb;
167
+ }
168
+ }
169
+ return {
170
+ $type: 'app.bsky.embed.external',
171
+ external: {
172
+ uri: embed.uri,
173
+ title: embed.title,
174
+ description: embed.description ?? '',
175
+ thumb: thumb,
176
+ },
177
+ };
178
+ }
179
+ if (type === 'image') {
180
+ const images = [];
181
+ for (const image of embed.images) {
182
+ const aspectRatio = image.aspectRatio;
183
+ const rawBlob = image.blob;
184
+ let blob;
185
+ if (rawBlob instanceof Blob) {
186
+ assertXrpc(rpc, `PostImageEmbed.images[].blob`);
187
+ blob = await uploadBlob(rawBlob);
188
+ }
189
+ else {
190
+ blob = rawBlob;
191
+ }
192
+ images.push({
193
+ image: blob,
194
+ alt: image.alt ?? '',
195
+ aspectRatio: aspectRatio ? { width: aspectRatio.width, height: aspectRatio.height } : undefined,
196
+ });
197
+ }
198
+ return {
199
+ $type: 'app.bsky.embed.images',
200
+ images: images,
201
+ };
202
+ }
203
+ assert(false, `Unexpected end of code`);
204
+ }
205
+ async function resolveRecordEmbed(embed) {
206
+ const uri = embed.uri;
207
+ let cid = embed.cid;
208
+ if (cid === undefined) {
209
+ const type = embed.type;
210
+ if (type === 'quote') {
211
+ assertXrpc(rpc, 'PostQuoteEmbed');
212
+ const post = await getPost(uri);
213
+ cid = post.cid;
214
+ }
215
+ else if (type === 'feed') {
216
+ assertXrpc(rpc, 'PostFeedEmbed');
217
+ const { data } = await rpc.get('app.bsky.feed.getFeedGenerator', {
218
+ signal: signal,
219
+ params: { feed: uri },
220
+ });
221
+ cid = data.view.cid;
222
+ }
223
+ else if (type === 'list') {
224
+ assertXrpc(rpc, 'PostListEmbed');
225
+ const { data } = await rpc.get('app.bsky.graph.getList', {
226
+ signal: signal,
227
+ params: { list: uri, limit: 1 },
228
+ });
229
+ cid = data.list.cid;
230
+ }
231
+ else if (type === 'starterpack') {
232
+ assertXrpc(rpc, 'PostStarterpackEmbed');
233
+ const { data } = await rpc.get('app.bsky.graph.getStarterPack', {
234
+ signal: signal,
235
+ params: { starterPack: uri },
236
+ });
237
+ cid = data.starterPack.cid;
238
+ }
239
+ else {
240
+ assert(false, `Unexpected end of code`);
241
+ }
242
+ }
243
+ return {
244
+ $type: 'app.bsky.embed.record',
245
+ record: {
246
+ uri: uri,
247
+ cid: cid,
248
+ },
249
+ };
250
+ }
251
+ }
252
+ async function uploadBlob(blob) {
253
+ // `rpc` intentionally non-null asserted.
254
+ const { data } = await rpc.call('com.atproto.repo.uploadBlob', {
255
+ signal: signal,
256
+ data: blob,
257
+ });
258
+ return data.blob;
259
+ }
260
+ async function getPost(uri) {
261
+ // `rpc` intentionally non-null asserted.
262
+ const { data } = await rpc.get('app.bsky.feed.getPosts', {
263
+ signal: signal,
264
+ params: {
265
+ uris: [uri],
266
+ },
267
+ });
268
+ const post = data.posts[0];
269
+ if (!post) {
270
+ throw new XRPCError(400, { kind: 'NotFound', message: `Post not found: ${uri}` });
271
+ }
272
+ return post;
273
+ }
274
+ }
275
+ function resolveThreadgate(gate) {
276
+ const rules = [];
277
+ if (gate.follows) {
278
+ rules.push({ $type: 'app.bsky.feed.threadgate#followingRule' });
279
+ }
280
+ if (gate.mentions) {
281
+ rules.push({ $type: 'app.bsky.feed.threadgate#mentionRule' });
282
+ }
283
+ for (const listUri of gate.listUris ?? []) {
284
+ rules.push({ $type: 'app.bsky.feed.threadgate#listRule', list: listUri });
285
+ }
286
+ return rules;
287
+ }
288
+ function getEmbedLabels(embed) {
289
+ if (embed !== undefined) {
290
+ const type = embed.type;
291
+ if (type === 'image' || type === 'external') {
292
+ return embed.labels;
293
+ }
294
+ }
295
+ }
296
+ function assert(condition, message) {
297
+ if (!condition) {
298
+ throw new Error(message);
299
+ }
300
+ }
301
+ function assertXrpc(rpc, thing) {
302
+ if (rpc === undefined) {
303
+ throw new Error(`${thing} requires supplying RPC instance`);
304
+ }
305
+ }
306
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../lib/index.ts"],"names":[],"mappings":"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"}
package/dist/time.d.ts ADDED
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Return the current time, make sure that each call never returns same value
3
+ * so that posts sent at the same time from different accounts don't end up
4
+ * colliding with each other potentially causing them to not be shown.
5
+ */
6
+ export declare function getNow(): number;
package/dist/time.js ADDED
@@ -0,0 +1,15 @@
1
+ let lastTimestamp = 0;
2
+ /**
3
+ * Return the current time, make sure that each call never returns same value
4
+ * so that posts sent at the same time from different accounts don't end up
5
+ * colliding with each other potentially causing them to not be shown.
6
+ */
7
+ export function getNow() {
8
+ let timestamp = Math.max(Date.now(), lastTimestamp);
9
+ if (timestamp === lastTimestamp) {
10
+ // 30 ms apart seems reasonable, the expectation is <=25 posts per thread.
11
+ timestamp += 30;
12
+ }
13
+ return (lastTimestamp = timestamp);
14
+ }
15
+ //# sourceMappingURL=time.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"time.js","sourceRoot":"","sources":["../lib/time.ts"],"names":[],"mappings":"AAAA,IAAI,aAAa,GAAW,CAAC,CAAC;AAE9B;;;;GAIG;AACH,MAAM,UAAU,MAAM;IACrB,IAAI,SAAS,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,EAAE,EAAE,aAAa,CAAC,CAAC;IACpD,IAAI,SAAS,KAAK,aAAa,EAAE,CAAC;QACjC,0EAA0E;QAC1E,SAAS,IAAI,EAAE,CAAC;IACjB,CAAC;IAED,OAAO,CAAC,aAAa,GAAG,SAAS,CAAC,CAAC;AACpC,CAAC"}
@@ -0,0 +1,165 @@
1
+ import type { XRPC } from '@atcute/client';
2
+ import type { AppBskyEmbedRecord, AppBskyFeedDefs, AppBskyRichtextFacet, At } from '@atcute/client/lexicons';
3
+ /** An embed that links to an external page */
4
+ export interface PostExternalEmbed {
5
+ type: 'external';
6
+ /** Link to the page */
7
+ uri: string;
8
+ /** Page title */
9
+ title: string;
10
+ /** Page description */
11
+ description?: string;
12
+ /**
13
+ * Page thumbnail, accepts either a Web Blob instance or a blob returned from
14
+ * the `com.atproto.repo.uploadBlob` procedure. Supplying the former requires
15
+ * you to also supply an authenticated RPC instance for it to be able to make
16
+ * procedure calls.
17
+ */
18
+ thumbnail?: Blob | At.Blob;
19
+ /** Labels to describe this external embed */
20
+ labels?: string[];
21
+ }
22
+ /** An image within the image embed */
23
+ export interface ComposedImage {
24
+ /**
25
+ * The image data, accepts either a Web Blob instance or a blob returned from
26
+ * the `com.atproto.repo.uploadBlob` procedure. Supplying the former requires
27
+ * you to also supply an authenticated RPC instance for it to be able to make
28
+ * procedure calls.
29
+ */
30
+ blob: Blob | At.Blob;
31
+ /**
32
+ * Alternative text for this image, helps describe images for low-vision users
33
+ * and provide context for everyone.
34
+ */
35
+ alt?: string;
36
+ /**
37
+ * Aspect ratio of the image, supplying this is recommended as clients makes
38
+ * use of it to properly display images.
39
+ */
40
+ aspectRatio?: {
41
+ /** Height of the image */
42
+ height: number;
43
+ /** Width of the image */
44
+ width: number;
45
+ };
46
+ }
47
+ /** An embed that displays images */
48
+ export interface PostImageEmbed {
49
+ type: 'image';
50
+ /** An array of images */
51
+ images: ComposedImage[];
52
+ /** Labels to describe this image embed */
53
+ labels?: string[];
54
+ }
55
+ /** Union type of media embeds */
56
+ export type PostMediaEmbed = PostExternalEmbed | PostImageEmbed;
57
+ /** An embed that displays a feed card */
58
+ export interface PostFeedEmbed {
59
+ type: 'feed';
60
+ /** AT-URI of the feed */
61
+ uri: At.Uri;
62
+ /**
63
+ * CID of the feed, if not supplied, requires you to also supply an RPC
64
+ * instance for it to be able to make query calls.
65
+ */
66
+ cid?: string;
67
+ }
68
+ /** An embed that displays a user/moderation list card */
69
+ export interface PostListEmbed {
70
+ type: 'list';
71
+ /** AT-URI of the list */
72
+ uri: string;
73
+ /**
74
+ * CID of the list, if not supplied, requires you to also supply an RPC
75
+ * instance for it to be able to make query calls.
76
+ */
77
+ cid?: string;
78
+ }
79
+ /** An embed that displays a post card (quote post/repost with quote) */
80
+ export interface PostQuoteEmbed {
81
+ type: 'quote';
82
+ /** AT-URI of the post */
83
+ uri: string;
84
+ /**
85
+ * CID of the post, if not supplied, requires you to also supply an RPC
86
+ * instance for it to be able to make query calls.
87
+ */
88
+ cid?: string;
89
+ }
90
+ /** An embed that displays a starter pack card */
91
+ export interface PostStarterpackEmbed {
92
+ type: 'starterpack';
93
+ /** AT-URI of the post */
94
+ uri: string;
95
+ /**
96
+ * CID of the starter pack, if not supplied, requires you to also supply an
97
+ * RPC instance for it to be able to make query calls.
98
+ */
99
+ cid?: string;
100
+ }
101
+ /** Union type of "record" embeds */
102
+ export type PostRecordEmbed = PostFeedEmbed | PostListEmbed | PostQuoteEmbed | PostStarterpackEmbed;
103
+ /** An embed that displays a media and a "record" embed */
104
+ export interface PostRecordWithMediaEmbed {
105
+ type: 'recordWithMedia';
106
+ /** The "record" embed */
107
+ record: PostRecordEmbed;
108
+ /** The media embed */
109
+ media: PostMediaEmbed;
110
+ }
111
+ /** Union type of embeds that can be assigned to a post */
112
+ export type PostEmbed = PostMediaEmbed | PostRecordEmbed | PostRecordWithMediaEmbed;
113
+ /** The post being composed */
114
+ export interface ComposedPost {
115
+ /** The language that this post is in */
116
+ languages?: string[];
117
+ /** The content of the post */
118
+ content: {
119
+ /** Post text */
120
+ text: string;
121
+ /** Decorations applied to the text */
122
+ facets?: AppBskyRichtextFacet.Main[];
123
+ };
124
+ /** Embed assigned to this post */
125
+ embed?: PostEmbed;
126
+ }
127
+ /** Reply gating options, leave this empty to deny everyone from replying */
128
+ export interface ComposedThreadgate {
129
+ /** Allow replies from users you follow */
130
+ follows?: boolean;
131
+ /** Allow replies from users mentioned in the post */
132
+ mentions?: boolean;
133
+ /** Allow replies from users that are in these user lists */
134
+ listUris?: At.Uri[];
135
+ }
136
+ /** Base interface for the thread being composed */
137
+ export interface ComposedThread {
138
+ /** An RPC instance, necessary for some options that takes action on your behalf */
139
+ rpc?: XRPC;
140
+ /** Abort signal */
141
+ signal?: AbortSignal;
142
+ /** Author of the thread */
143
+ author: At.DID;
144
+ /**
145
+ * The "creation time" for this thread,
146
+ * if not supplied, the current time is used
147
+ */
148
+ createdAt?: string | number | Date;
149
+ /**
150
+ * The post it should reply to, accepts either an AT-URI of the post, a view
151
+ * of the post, or an embed view of the post. Supplying an AT-URI requires you
152
+ * to also supply an PRC instance for it to be able to make query calls.
153
+ */
154
+ reply?: string | AppBskyFeedDefs.PostView | AppBskyEmbedRecord.ViewRecord;
155
+ /**
156
+ * Thread gating to apply on this thread, this option can't be set if this is
157
+ * a reply, especially to another user's thread. Leave this undefined to allow
158
+ * everyone to reply to the thread, supply an empty object to deny everyone.
159
+ */
160
+ gate?: ComposedThreadgate;
161
+ /** The language that all the posts are in, can be overridden per-post */
162
+ languages?: string[];
163
+ /** An array of posts */
164
+ posts: ComposedPost[];
165
+ }
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../lib/types.ts"],"names":[],"mappings":""}
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "type": "module",
3
+ "name": "@atcute/bluesky-threading",
4
+ "version": "1.0.0",
5
+ "description": "create Bluesky threads containing multiple posts with one write",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "url": "https://codeberg.org/mary-ext/atcute"
9
+ },
10
+ "files": [
11
+ "dist/"
12
+ ],
13
+ "exports": {
14
+ ".": "./dist/index.js"
15
+ },
16
+ "sideEffects": false,
17
+ "peerDependencies": {
18
+ "@atcute/bluesky": "^1.0.0",
19
+ "@atcute/client": "^1.0.0"
20
+ },
21
+ "dependencies": {
22
+ "@atcute/cbor": "^1.0.0",
23
+ "@atcute/tid": "^1.0.0",
24
+ "@atcute/cid": "^1.0.0"
25
+ },
26
+ "devDependencies": {
27
+ "@types/bun": "^1.1.6",
28
+ "@atcute/bluesky-richtext-builder": "^1.0.0",
29
+ "@atcute/bluesky": "^1.0.0",
30
+ "@atcute/client": "^1.0.0"
31
+ },
32
+ "scripts": {
33
+ "build": "tsc --project tsconfig.build.json",
34
+ "test": "bun test --coverage",
35
+ "prepublish": "rm -rf dist; pnpm run build"
36
+ }
37
+ }