@agnocon/piece-bluesky 0.1.6
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.MIT-AP +24 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +32 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/actions/create-post.d.ts +21 -0
- package/dist/lib/actions/create-post.d.ts.map +1 -0
- package/dist/lib/actions/create-post.js +379 -0
- package/dist/lib/actions/create-post.js.map +1 -0
- package/dist/lib/actions/find-post.d.ts +8 -0
- package/dist/lib/actions/find-post.d.ts.map +1 -0
- package/dist/lib/actions/find-post.js +77 -0
- package/dist/lib/actions/find-post.js.map +1 -0
- package/dist/lib/actions/find-thread.d.ts +10 -0
- package/dist/lib/actions/find-thread.d.ts.map +1 -0
- package/dist/lib/actions/find-thread.js +117 -0
- package/dist/lib/actions/find-thread.js.map +1 -0
- package/dist/lib/actions/like-post.d.ts +14 -0
- package/dist/lib/actions/like-post.d.ts.map +1 -0
- package/dist/lib/actions/like-post.js +129 -0
- package/dist/lib/actions/like-post.js.map +1 -0
- package/dist/lib/actions/repost.d.ts +14 -0
- package/dist/lib/actions/repost.d.ts.map +1 -0
- package/dist/lib/actions/repost.js +132 -0
- package/dist/lib/actions/repost.js.map +1 -0
- package/dist/lib/common/auth.d.ts +11 -0
- package/dist/lib/common/auth.d.ts.map +1 -0
- package/dist/lib/common/auth.js +75 -0
- package/dist/lib/common/auth.js.map +1 -0
- package/dist/lib/common/client.d.ts +8 -0
- package/dist/lib/common/client.d.ts.map +1 -0
- package/dist/lib/common/client.js +47 -0
- package/dist/lib/common/client.js.map +1 -0
- package/dist/lib/common/props.d.ts +45 -0
- package/dist/lib/common/props.d.ts.map +1 -0
- package/dist/lib/common/props.js +360 -0
- package/dist/lib/common/props.js.map +1 -0
- package/dist/lib/triggers/new-follower-on-account.d.ts +19 -0
- package/dist/lib/triggers/new-follower-on-account.d.ts.map +1 -0
- package/dist/lib/triggers/new-follower-on-account.js +116 -0
- package/dist/lib/triggers/new-follower-on-account.js.map +1 -0
- package/dist/lib/triggers/new-post.d.ts +75 -0
- package/dist/lib/triggers/new-post.d.ts.map +1 -0
- package/dist/lib/triggers/new-post.js +214 -0
- package/dist/lib/triggers/new-post.js.map +1 -0
- package/dist/lib/triggers/new-posts-by-author.d.ts +59 -0
- package/dist/lib/triggers/new-posts-by-author.d.ts.map +1 -0
- package/dist/lib/triggers/new-posts-by-author.js +243 -0
- package/dist/lib/triggers/new-posts-by-author.js.map +1 -0
- package/dist/lib/triggers/new-timeline-posts.d.ts +19 -0
- package/dist/lib/triggers/new-timeline-posts.d.ts.map +1 -0
- package/dist/lib/triggers/new-timeline-posts.js +143 -0
- package/dist/lib/triggers/new-timeline-posts.js.map +1 -0
- package/package.json +48 -0
- package/src/index.ts +25 -0
- package/src/lib/actions/create-post.ts +444 -0
- package/src/lib/actions/find-post.ts +76 -0
- package/src/lib/actions/find-thread.ts +121 -0
- package/src/lib/actions/like-post.ts +128 -0
- package/src/lib/actions/repost.ts +132 -0
- package/src/lib/common/auth.ts +74 -0
- package/src/lib/common/client.ts +42 -0
- package/src/lib/common/props.ts +392 -0
- package/src/lib/triggers/new-follower-on-account.ts +112 -0
- package/src/lib/triggers/new-post.ts +225 -0
- package/src/lib/triggers/new-posts-by-author.ts +252 -0
- package/src/lib/triggers/new-timeline-posts.ts +141 -0
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { createAction, Property } from '@agnocon/pieces-framework';
|
|
2
|
+
import { blueskyAuth } from '../common/auth';
|
|
3
|
+
import { createBlueskyAgent } from '../common/client';
|
|
4
|
+
import { postUrlProperty, extractPostInfoFromUrl } from '../common/props';
|
|
5
|
+
|
|
6
|
+
export const findPost = createAction({
|
|
7
|
+
auth: blueskyAuth,
|
|
8
|
+
name: 'findPost',
|
|
9
|
+
displayName: 'Find Post',
|
|
10
|
+
description: 'Get detailed information about a specific post',
|
|
11
|
+
audience: 'both',
|
|
12
|
+
aiMetadata: {
|
|
13
|
+
description: 'Retrieves a single Bluesky post with its content, author, and engagement counts (likes, reposts, replies, quotes), given a bsky.app post URL or AT-URI. Use to look up the current state or metadata of a known post. Read-only and idempotent.',
|
|
14
|
+
idempotent: true,
|
|
15
|
+
},
|
|
16
|
+
props: {
|
|
17
|
+
postUrl: postUrlProperty,
|
|
18
|
+
},
|
|
19
|
+
async run({ auth, propsValue }) {
|
|
20
|
+
const { postUrl } = propsValue;
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
const agent = await createBlueskyAgent(auth.props);
|
|
24
|
+
|
|
25
|
+
const postInfo = extractPostInfoFromUrl(postUrl);
|
|
26
|
+
let atUri = postInfo.uri;
|
|
27
|
+
|
|
28
|
+
if (!atUri && postInfo.handle && postInfo.postId) {
|
|
29
|
+
const didDoc = await agent.resolveHandle({ handle: postInfo.handle });
|
|
30
|
+
|
|
31
|
+
if (didDoc.data?.did) {
|
|
32
|
+
atUri = `at://${didDoc.data.did}/app.bsky.feed.post/${postInfo.postId}`;
|
|
33
|
+
} else {
|
|
34
|
+
throw new Error(`Could not resolve handle: ${postInfo.handle}`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (!atUri) {
|
|
39
|
+
throw new Error('Could not parse the post URL. Please make sure you are using a valid Bluesky post URL like: https://bsky.app/profile/username.bsky.social/post/xxx');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (!atUri.startsWith('at://')) {
|
|
43
|
+
throw new Error('Invalid URI format. Must be an AT-URI starting with "at://" or a valid Bluesky URL');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const response = await agent.getPosts({ uris: [atUri] });
|
|
47
|
+
|
|
48
|
+
if (!response.data?.posts || response.data.posts.length === 0) {
|
|
49
|
+
throw new Error('Post not found or not accessible');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const post = response.data.posts[0];
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
success: true,
|
|
56
|
+
uri: atUri,
|
|
57
|
+
cid: post.cid,
|
|
58
|
+
record: post.record,
|
|
59
|
+
author: post.author,
|
|
60
|
+
indexedAt: post.indexedAt,
|
|
61
|
+
replyCount: post.replyCount || 0,
|
|
62
|
+
repostCount: post.repostCount || 0,
|
|
63
|
+
likeCount: post.likeCount || 0,
|
|
64
|
+
quoteCount: post.quoteCount || 0,
|
|
65
|
+
embed: post.embed,
|
|
66
|
+
labels: post.labels,
|
|
67
|
+
threadgate: post.threadgate,
|
|
68
|
+
viewer: post.viewer,
|
|
69
|
+
retrievedAt: new Date().toISOString(),
|
|
70
|
+
};
|
|
71
|
+
} catch (error) {
|
|
72
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
73
|
+
throw new Error(`Failed to find post: ${errorMessage}`);
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
});
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { createAction, Property } from '@agnocon/pieces-framework';
|
|
2
|
+
import { blueskyAuth } from '../common/auth';
|
|
3
|
+
import { createBlueskyAgent } from '../common/client';
|
|
4
|
+
import { postUrlProperty, threadDepthDropdown, parentHeightDropdown, extractPostInfoFromUrl } from '../common/props';
|
|
5
|
+
|
|
6
|
+
export const findThread = createAction({
|
|
7
|
+
auth: blueskyAuth,
|
|
8
|
+
name: 'findThread',
|
|
9
|
+
displayName: 'Find Thread',
|
|
10
|
+
description: 'Get a full conversation thread with replies',
|
|
11
|
+
audience: 'both',
|
|
12
|
+
aiMetadata: {
|
|
13
|
+
description: 'Retrieves the full conversation thread around a Bluesky post — parent posts and nested replies — given a bsky.app post URL or AT-URI, with configurable reply depth and parent height (each 0-1000). Use to read an entire discussion rather than a single post. Read-only and idempotent.',
|
|
14
|
+
idempotent: true,
|
|
15
|
+
},
|
|
16
|
+
props: {
|
|
17
|
+
postUrl: postUrlProperty,
|
|
18
|
+
depth: threadDepthDropdown,
|
|
19
|
+
parentHeight: parentHeightDropdown,
|
|
20
|
+
},
|
|
21
|
+
async run({ auth, propsValue }) {
|
|
22
|
+
const { postUrl, depth = 6, parentHeight = 80 } = propsValue;
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
const agent = await createBlueskyAgent(auth.props);
|
|
26
|
+
|
|
27
|
+
const postInfo = extractPostInfoFromUrl(postUrl);
|
|
28
|
+
let atUri = postInfo.uri;
|
|
29
|
+
|
|
30
|
+
if (!atUri && postInfo.handle && postInfo.postId) {
|
|
31
|
+
const didDoc = await agent.resolveHandle({ handle: postInfo.handle });
|
|
32
|
+
|
|
33
|
+
if (didDoc.data?.did) {
|
|
34
|
+
atUri = `at://${didDoc.data.did}/app.bsky.feed.post/${postInfo.postId}`;
|
|
35
|
+
} else {
|
|
36
|
+
throw new Error(`Could not resolve handle: ${postInfo.handle}`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (!atUri) {
|
|
41
|
+
throw new Error('Could not parse the post URL. Please make sure you are using a valid Bluesky post URL like: https://bsky.app/profile/username.bsky.social/post/xxx');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (!atUri.startsWith('at://')) {
|
|
45
|
+
throw new Error('Invalid URI format. Must be an AT-URI starting with "at://" or a valid Bluesky URL');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const depthNum = parseInt(depth.toString(), 10);
|
|
49
|
+
const parentHeightNum = parseInt(parentHeight.toString(), 10);
|
|
50
|
+
|
|
51
|
+
if (isNaN(depthNum) || depthNum < 0 || depthNum > 1000) {
|
|
52
|
+
throw new Error('Depth must be a number between 0 and 1000');
|
|
53
|
+
}
|
|
54
|
+
if (isNaN(parentHeightNum) || parentHeightNum < 0 || parentHeightNum > 1000) {
|
|
55
|
+
throw new Error('Parent height must be a number between 0 and 1000');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const response = await agent.getPostThread({
|
|
59
|
+
uri: atUri,
|
|
60
|
+
depth: depthNum,
|
|
61
|
+
parentHeight: parentHeightNum,
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
const stats = {
|
|
65
|
+
totalPosts: 0,
|
|
66
|
+
parentPosts: 0,
|
|
67
|
+
replyPosts: 0,
|
|
68
|
+
notFoundPosts: 0,
|
|
69
|
+
blockedPosts: 0,
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const countPosts = (threadPost: any): void => {
|
|
73
|
+
if (!threadPost) return;
|
|
74
|
+
|
|
75
|
+
if (threadPost.$type === 'app.bsky.feed.defs#notFoundPost') {
|
|
76
|
+
stats.notFoundPosts++;
|
|
77
|
+
} else if (threadPost.$type === 'app.bsky.feed.defs#blockedPost') {
|
|
78
|
+
stats.blockedPosts++;
|
|
79
|
+
} else if (threadPost.post) {
|
|
80
|
+
stats.totalPosts++;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (threadPost.parent) {
|
|
84
|
+
stats.parentPosts++;
|
|
85
|
+
countPosts(threadPost.parent);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (threadPost.replies && Array.isArray(threadPost.replies)) {
|
|
89
|
+
stats.replyPosts += threadPost.replies.length;
|
|
90
|
+
threadPost.replies.forEach((reply: any) => countPosts(reply));
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
if (response.data.thread) {
|
|
95
|
+
countPosts(response.data.thread);
|
|
96
|
+
if (
|
|
97
|
+
response.data.thread.$type === 'app.bsky.feed.defs#threadViewPost' &&
|
|
98
|
+
'post' in response.data.thread &&
|
|
99
|
+
response.data.thread.post
|
|
100
|
+
) {
|
|
101
|
+
stats.totalPosts = 1;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
success: true,
|
|
107
|
+
thread: response.data.thread,
|
|
108
|
+
requestedUri: atUri,
|
|
109
|
+
parameters: {
|
|
110
|
+
depth: depthNum,
|
|
111
|
+
parentHeight: parentHeightNum,
|
|
112
|
+
},
|
|
113
|
+
statistics: stats,
|
|
114
|
+
retrievedAt: new Date().toISOString(),
|
|
115
|
+
};
|
|
116
|
+
} catch (error) {
|
|
117
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
118
|
+
throw new Error(`Failed to find thread: ${errorMessage}`);
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
});
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { createAction, Property } from '@agnocon/pieces-framework';
|
|
2
|
+
import { blueskyAuth, BlueSkyAuthType } from '../common/auth';
|
|
3
|
+
import { createBlueskyAgent } from '../common/client';
|
|
4
|
+
import { parseBlueskyUrl } from '../common/props';
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
export const likePost = createAction({
|
|
8
|
+
auth: blueskyAuth,
|
|
9
|
+
name: 'likePost',
|
|
10
|
+
displayName: 'Like Post',
|
|
11
|
+
description: 'Like a post on Bluesky',
|
|
12
|
+
audience: 'both',
|
|
13
|
+
aiMetadata: {
|
|
14
|
+
description: 'Adds a like from the authenticated account to a specific Bluesky post, identified either by an AT-URI / bsky.app post URL or by selecting from the recent timeline. Use to register approval of a post. Not idempotent — each call creates a separate like record.',
|
|
15
|
+
idempotent: false,
|
|
16
|
+
},
|
|
17
|
+
props: {
|
|
18
|
+
selectionMethod: Property.StaticDropdown({
|
|
19
|
+
displayName: 'Select Method',
|
|
20
|
+
description: 'How to choose the post',
|
|
21
|
+
required: true,
|
|
22
|
+
defaultValue: 'timeline',
|
|
23
|
+
options: {
|
|
24
|
+
options: [
|
|
25
|
+
{ label: 'From my timeline', value: 'timeline' },
|
|
26
|
+
{ label: 'Enter URL manually', value: 'manual' },
|
|
27
|
+
],
|
|
28
|
+
},
|
|
29
|
+
}),
|
|
30
|
+
|
|
31
|
+
postSelection: Property.Dropdown({
|
|
32
|
+
auth: blueskyAuth,
|
|
33
|
+
displayName: 'Select Post',
|
|
34
|
+
description: 'Choose from your recent timeline posts (only when "From my timeline" is selected above)',
|
|
35
|
+
required: false,
|
|
36
|
+
refreshers: ['auth'],
|
|
37
|
+
options: async ({ auth }) => {
|
|
38
|
+
try {
|
|
39
|
+
if (!auth) return { options: [] };
|
|
40
|
+
const agent = await createBlueskyAgent(auth.props);
|
|
41
|
+
const timeline = await agent.getTimeline({ limit: 50 });
|
|
42
|
+
|
|
43
|
+
return {
|
|
44
|
+
options: timeline.data.feed.map(item => ({
|
|
45
|
+
label: `@${item.post.author.handle}: ${item.post.record['text'] ? String(item.post.record['text']).substring(0, 80) : 'Media post'}${String(item.post.record['text'] || '').length > 80 ? '...' : ''} (${new Date(item.post.indexedAt).toLocaleDateString()})`,
|
|
46
|
+
value: item.post.uri
|
|
47
|
+
}))
|
|
48
|
+
};
|
|
49
|
+
} catch (error) {
|
|
50
|
+
return {
|
|
51
|
+
options: [{ label: 'Error loading posts', value: '' }]
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}),
|
|
56
|
+
|
|
57
|
+
postUrl: Property.ShortText({
|
|
58
|
+
displayName: 'Post URL',
|
|
59
|
+
description: 'Paste the Bluesky post URL',
|
|
60
|
+
required: false,
|
|
61
|
+
}),
|
|
62
|
+
},
|
|
63
|
+
async run({ auth, propsValue }) {
|
|
64
|
+
const { selectionMethod, postSelection, postUrl } = propsValue;
|
|
65
|
+
|
|
66
|
+
let postUri: string;
|
|
67
|
+
|
|
68
|
+
if (selectionMethod === 'timeline') {
|
|
69
|
+
if (!postSelection) {
|
|
70
|
+
throw new Error('Please select a post from your timeline dropdown');
|
|
71
|
+
}
|
|
72
|
+
postUri = postSelection as string;
|
|
73
|
+
} else if (selectionMethod === 'manual') {
|
|
74
|
+
if (!postUrl || !postUrl.trim()) {
|
|
75
|
+
throw new Error('Post URL is required when using manual entry method');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
const agent = await createBlueskyAgent(auth.props);
|
|
80
|
+
postUri = await parseBlueskyUrl(postUrl.trim(), agent);
|
|
81
|
+
} catch (error) {
|
|
82
|
+
throw new Error(`Invalid post URL: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
83
|
+
}
|
|
84
|
+
} else {
|
|
85
|
+
throw new Error('Please select a post selection method');
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
try {
|
|
89
|
+
const agent = await createBlueskyAgent(auth.props);
|
|
90
|
+
|
|
91
|
+
const postsResponse = await agent.getPosts({ uris: [postUri] });
|
|
92
|
+
|
|
93
|
+
if (!postsResponse.data.posts || postsResponse.data.posts.length === 0) {
|
|
94
|
+
throw new Error('Post not found');
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const post = postsResponse.data.posts[0];
|
|
98
|
+
|
|
99
|
+
const response = await agent.like(postUri, post.cid);
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
success: true,
|
|
103
|
+
likeUri: response.uri,
|
|
104
|
+
likeCid: response.cid,
|
|
105
|
+
postUri: postUri,
|
|
106
|
+
postCid: post.cid,
|
|
107
|
+
postAuthor: post.author.handle,
|
|
108
|
+
postText: post.record['text'] ? String(post.record['text']).substring(0, 100) + (String(post.record['text']).length > 100 ? '...' : '') : 'No text available',
|
|
109
|
+
selectionMethod: selectionMethod,
|
|
110
|
+
likedAt: new Date().toISOString(),
|
|
111
|
+
};
|
|
112
|
+
} catch (error) {
|
|
113
|
+
if (error instanceof Error) {
|
|
114
|
+
if (error.message.includes('Post not found')) {
|
|
115
|
+
throw new Error('Post not found. Please check the URL and try again.');
|
|
116
|
+
}
|
|
117
|
+
if (error.message.includes('Invalid post URL format')) {
|
|
118
|
+
throw new Error('Invalid post URL format. Please use a valid Bluesky post URL or AT-URI.');
|
|
119
|
+
}
|
|
120
|
+
if (error.message.includes('Authentication')) {
|
|
121
|
+
throw new Error('Authentication failed. Please check your credentials.');
|
|
122
|
+
}
|
|
123
|
+
throw new Error(`Failed to like post: ${error.message}`);
|
|
124
|
+
}
|
|
125
|
+
throw new Error('Failed to like post: Unknown error occurred');
|
|
126
|
+
}
|
|
127
|
+
},
|
|
128
|
+
});
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { createAction, Property } from '@agnocon/pieces-framework';
|
|
2
|
+
import { blueskyAuth, BlueSkyAuthType } from '../common/auth';
|
|
3
|
+
import { createBlueskyAgent } from '../common/client';
|
|
4
|
+
import { parseBlueskyUrl } from '../common/props';
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
export const repostPost = createAction({
|
|
9
|
+
auth: blueskyAuth,
|
|
10
|
+
name: 'repostPost',
|
|
11
|
+
displayName: 'Repost Post',
|
|
12
|
+
description: 'Share someone else\'s post to your timeline',
|
|
13
|
+
audience: 'both',
|
|
14
|
+
aiMetadata: {
|
|
15
|
+
description: 'Reposts an existing Bluesky post to the authenticated account\'s timeline, identified by an AT-URI / bsky.app post URL or selected from the recent timeline. Use to amplify another user\'s post. Not idempotent — each call creates a separate repost record.',
|
|
16
|
+
idempotent: false,
|
|
17
|
+
},
|
|
18
|
+
props: {
|
|
19
|
+
selectionMethod: Property.StaticDropdown({
|
|
20
|
+
displayName: 'Select Method',
|
|
21
|
+
description: 'How to choose the post',
|
|
22
|
+
required: true,
|
|
23
|
+
defaultValue: 'timeline',
|
|
24
|
+
options: {
|
|
25
|
+
options: [
|
|
26
|
+
{ label: 'From my timeline', value: 'timeline' },
|
|
27
|
+
{ label: 'Enter URL manually', value: 'manual' },
|
|
28
|
+
],
|
|
29
|
+
},
|
|
30
|
+
}),
|
|
31
|
+
|
|
32
|
+
postSelection: Property.Dropdown({
|
|
33
|
+
auth: blueskyAuth,
|
|
34
|
+
displayName: 'Select Post',
|
|
35
|
+
description: 'Choose from your recent timeline posts (only when "From my timeline" is selected above)',
|
|
36
|
+
required: false,
|
|
37
|
+
refreshers: ['auth'],
|
|
38
|
+
options: async ({ auth }) => {
|
|
39
|
+
try {
|
|
40
|
+
if (!auth) return { options: [] };
|
|
41
|
+
const agent = await createBlueskyAgent(auth.props);
|
|
42
|
+
const timeline = await agent.getTimeline({ limit: 50 });
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
options: timeline.data.feed.map(item => ({
|
|
46
|
+
label: `@${item.post.author.handle}: ${item.post.record['text'] ? String(item.post.record['text']).substring(0, 80) : 'Media post'}${String(item.post.record['text'] || '').length > 80 ? '...' : ''} (${new Date(item.post.indexedAt).toLocaleDateString()})`,
|
|
47
|
+
value: item.post.uri
|
|
48
|
+
}))
|
|
49
|
+
};
|
|
50
|
+
} catch (error) {
|
|
51
|
+
return {
|
|
52
|
+
options: [{ label: 'Error loading posts', value: '' }]
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}),
|
|
57
|
+
|
|
58
|
+
postUrl: Property.ShortText({
|
|
59
|
+
displayName: 'Post URL',
|
|
60
|
+
description: 'Paste the Bluesky post URL',
|
|
61
|
+
required: false,
|
|
62
|
+
}),
|
|
63
|
+
},
|
|
64
|
+
async run({ auth, propsValue }) {
|
|
65
|
+
const { selectionMethod, postSelection, postUrl } = propsValue;
|
|
66
|
+
|
|
67
|
+
let postUri: string;
|
|
68
|
+
|
|
69
|
+
if (selectionMethod === 'timeline') {
|
|
70
|
+
if (!postSelection) {
|
|
71
|
+
throw new Error('Please select a post from your timeline dropdown');
|
|
72
|
+
}
|
|
73
|
+
postUri = postSelection as string;
|
|
74
|
+
} else if (selectionMethod === 'manual') {
|
|
75
|
+
if (!postUrl || !postUrl.trim()) {
|
|
76
|
+
throw new Error('Post URL is required when using manual entry method');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
const agent = await createBlueskyAgent(auth.props);
|
|
81
|
+
postUri = await parseBlueskyUrl(postUrl.trim(), agent);
|
|
82
|
+
} catch (error) {
|
|
83
|
+
throw new Error(`Invalid post URL: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
84
|
+
}
|
|
85
|
+
} else {
|
|
86
|
+
throw new Error('Please select a post selection method');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
const agent = await createBlueskyAgent(auth.props);
|
|
91
|
+
|
|
92
|
+
const postsResponse = await agent.getPosts({ uris: [postUri] });
|
|
93
|
+
|
|
94
|
+
if (!postsResponse.data.posts || postsResponse.data.posts.length === 0) {
|
|
95
|
+
throw new Error('Post not found');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const post = postsResponse.data.posts[0];
|
|
99
|
+
|
|
100
|
+
const response = await agent.repost(postUri, post.cid);
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
success: true,
|
|
104
|
+
repostUri: response.uri,
|
|
105
|
+
repostCid: response.cid,
|
|
106
|
+
originalPost: {
|
|
107
|
+
uri: postUri,
|
|
108
|
+
cid: post.cid,
|
|
109
|
+
author: post.author.handle,
|
|
110
|
+
text: post.record['text'] ? String(post.record['text']).substring(0, 100) + (String(post.record['text']).length > 100 ? '...' : '') : 'No text available',
|
|
111
|
+
createdAt: post.record['createdAt'] || post.indexedAt,
|
|
112
|
+
},
|
|
113
|
+
selectionMethod: selectionMethod,
|
|
114
|
+
repostedAt: new Date().toISOString(),
|
|
115
|
+
};
|
|
116
|
+
} catch (error) {
|
|
117
|
+
if (error instanceof Error) {
|
|
118
|
+
if (error.message.includes('Post not found')) {
|
|
119
|
+
throw new Error('Post not found. Please check the URL and try again.');
|
|
120
|
+
}
|
|
121
|
+
if (error.message.includes('Invalid post URL format')) {
|
|
122
|
+
throw new Error('Invalid post URL format. Please use a valid Bluesky post URL or AT-URI.');
|
|
123
|
+
}
|
|
124
|
+
if (error.message.includes('Authentication')) {
|
|
125
|
+
throw new Error('Authentication failed. Please check your credentials.');
|
|
126
|
+
}
|
|
127
|
+
throw new Error(`Failed to repost: ${error.message}`);
|
|
128
|
+
}
|
|
129
|
+
throw new Error('Failed to repost: Unknown error occurred');
|
|
130
|
+
}
|
|
131
|
+
},
|
|
132
|
+
});
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { PieceAuth, Property } from '@agnocon/pieces-framework';
|
|
2
|
+
import { AtpAgent } from '@atproto/api';
|
|
3
|
+
|
|
4
|
+
export interface BlueSkyAuthType {
|
|
5
|
+
pdsHost?: string;
|
|
6
|
+
identifier: string;
|
|
7
|
+
password: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const description = `
|
|
11
|
+
To authenticate with Bluesky:
|
|
12
|
+
|
|
13
|
+
1. **PDS Host**: The Personal Data Server host. Leave empty to use the default Bluesky network (https://bsky.social).
|
|
14
|
+
2. **Identifier**: Your Bluesky handle (e.g., yourhandle.bsky.social) or email address.
|
|
15
|
+
3. **Password**: Your Bluesky account password or app password.
|
|
16
|
+
|
|
17
|
+
For enhanced security, consider using an app password from your Bluesky account settings.
|
|
18
|
+
`;
|
|
19
|
+
|
|
20
|
+
export const blueskyAuth = PieceAuth.CustomAuth({
|
|
21
|
+
description: description,
|
|
22
|
+
required: true,
|
|
23
|
+
props: {
|
|
24
|
+
pdsHost: Property.ShortText({
|
|
25
|
+
displayName: 'PDS Host',
|
|
26
|
+
description: 'The Personal Data Server host. Leave empty for default Bluesky network (https://bsky.social)',
|
|
27
|
+
required: false,
|
|
28
|
+
defaultValue: 'https://bsky.social',
|
|
29
|
+
}),
|
|
30
|
+
identifier: Property.ShortText({
|
|
31
|
+
displayName: 'Identifier',
|
|
32
|
+
description: 'Your Bluesky handle or email address',
|
|
33
|
+
required: true,
|
|
34
|
+
}),
|
|
35
|
+
password: PieceAuth.SecretText({
|
|
36
|
+
displayName: 'Password',
|
|
37
|
+
description: 'Your Bluesky account password or app password',
|
|
38
|
+
required: true,
|
|
39
|
+
}),
|
|
40
|
+
},
|
|
41
|
+
validate: async ({ auth }) => {
|
|
42
|
+
try {
|
|
43
|
+
const agent = new AtpAgent({
|
|
44
|
+
service: auth.pdsHost || 'https://bsky.social',
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
await agent.login({
|
|
48
|
+
identifier: auth.identifier,
|
|
49
|
+
password: auth.password,
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
valid: true,
|
|
54
|
+
};
|
|
55
|
+
} catch (error: any) {
|
|
56
|
+
if (error.message?.includes('Invalid identifier or password')) {
|
|
57
|
+
return {
|
|
58
|
+
valid: false,
|
|
59
|
+
error: 'Invalid credentials. Please check your identifier and password.',
|
|
60
|
+
};
|
|
61
|
+
} else if (error.message?.includes('Invalid request')) {
|
|
62
|
+
return {
|
|
63
|
+
valid: false,
|
|
64
|
+
error: 'Invalid request. Please check your identifier format.',
|
|
65
|
+
};
|
|
66
|
+
} else {
|
|
67
|
+
return {
|
|
68
|
+
valid: false,
|
|
69
|
+
error: `Authentication failed: ${error.message || 'Unknown error'}`,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
});
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { AtpAgent } from '@atproto/api';
|
|
2
|
+
import { BlueSkyAuthType } from './auth';
|
|
3
|
+
|
|
4
|
+
const agentCache = new Map<string, { agent: AtpAgent; expires: number }>();
|
|
5
|
+
|
|
6
|
+
export async function createBlueskyAgent(auth: BlueSkyAuthType): Promise<AtpAgent> {
|
|
7
|
+
const cacheKey = `${auth.identifier}:${auth.pdsHost || 'https://bsky.social'}`;
|
|
8
|
+
const cached = agentCache.get(cacheKey);
|
|
9
|
+
|
|
10
|
+
if (cached && Date.now() < cached.expires) {
|
|
11
|
+
return cached.agent;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
try {
|
|
15
|
+
const agent = new AtpAgent({
|
|
16
|
+
service: auth.pdsHost || 'https://bsky.social',
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
await agent.login({
|
|
20
|
+
identifier: auth.identifier,
|
|
21
|
+
password: auth.password,
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
agentCache.set(cacheKey, {
|
|
25
|
+
agent,
|
|
26
|
+
expires: Date.now() + 50 * 60 * 1000,
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
return agent;
|
|
30
|
+
} catch (error: any) {
|
|
31
|
+
agentCache.delete(cacheKey);
|
|
32
|
+
throw new Error(`Failed to create Bluesky agent: ${error.message || 'Unknown error'}`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export async function getCurrentSession(auth: BlueSkyAuthType) {
|
|
37
|
+
const agent = await createBlueskyAgent(auth);
|
|
38
|
+
return {
|
|
39
|
+
did: agent.session?.did,
|
|
40
|
+
handle: agent.session?.handle,
|
|
41
|
+
};
|
|
42
|
+
}
|