Skip to content

Commit 40dff13

Browse files
authored
[Components] bluesky - new components (#15087)
1 parent 306d2d8 commit 40dff13

File tree

15 files changed

+947
-14
lines changed

15 files changed

+947
-14
lines changed
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import app from "../../bluesky.app.mjs";
2+
import constants from "../../common/constants.mjs";
3+
4+
export default {
5+
key: "bluesky-create-post",
6+
name: "Create Post",
7+
description: "Creates a new post on Bluesky. [See the documentation](https://docs.bsky.app/docs/api/com-atproto-repo-create-record).",
8+
version: "0.0.1",
9+
type: "action",
10+
props: {
11+
app,
12+
text: {
13+
type: "string",
14+
label: "Text",
15+
description: "The text content of the post.",
16+
},
17+
},
18+
async run({ $ }) {
19+
const {
20+
app,
21+
text,
22+
} = this;
23+
24+
const response = await app.createRecord({
25+
$,
26+
data: {
27+
collection: constants.RESOURCE_TYPE.POST,
28+
record: {
29+
["$type"]: constants.RESOURCE_TYPE.POST,
30+
text,
31+
createdAt: new Date().toISOString(),
32+
},
33+
},
34+
});
35+
36+
$.export("$summary", `Successfully created a new post with uri \`${response.uri}\`.`);
37+
return response;
38+
},
39+
};
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import app from "../../bluesky.app.mjs";
2+
import constants from "../../common/constants.mjs";
3+
4+
export default {
5+
key: "bluesky-like-post",
6+
name: "Like Post",
7+
description: "Like a specific post on Bluesky. [See the documentation](https://docs.bsky.app/docs/api/com-atproto-repo-create-record).",
8+
version: "0.0.1",
9+
type: "action",
10+
props: {
11+
app,
12+
postUrl: {
13+
propDefinition: [
14+
app,
15+
"postUrl",
16+
],
17+
},
18+
},
19+
async run({ $ }) {
20+
const {
21+
app,
22+
postUrl,
23+
} = this;
24+
25+
const {
26+
handle,
27+
postId,
28+
} = app.getHandleAndPostIdFromUrl(postUrl);
29+
30+
const {
31+
uri,
32+
cid,
33+
} = await app.getRecord({
34+
$,
35+
params: {
36+
repo: handle,
37+
collection: constants.RESOURCE_TYPE.POST,
38+
rkey: postId,
39+
},
40+
});
41+
42+
const response = await app.createRecord({
43+
$,
44+
data: {
45+
collection: constants.RESOURCE_TYPE.LIKE,
46+
record: {
47+
["$type"]: constants.RESOURCE_TYPE.LIKE,
48+
createdAt: new Date().toISOString(),
49+
subject: {
50+
uri,
51+
cid,
52+
py_type: "com.atproto.repo.strongRef",
53+
},
54+
},
55+
},
56+
});
57+
58+
$.export("$summary", "Successfully liked post.");
59+
return response;
60+
},
61+
};
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import app from "../../bluesky.app.mjs";
2+
3+
export default {
4+
key: "bluesky-retrieve-thread",
5+
name: "Retrieve Thread",
6+
description: "Retrieve a full thread of posts. [See the documentation](https://docs.bsky.app/docs/api/app-bsky-feed-get-post-thread).",
7+
version: "0.0.1",
8+
type: "action",
9+
props: {
10+
app,
11+
postUrl: {
12+
propDefinition: [
13+
app,
14+
"postUrl",
15+
],
16+
},
17+
depth: {
18+
type: "integer",
19+
label: "Depth",
20+
description: "How many levels of reply depth should be included in response. Default is `6`.",
21+
optional: true,
22+
max: 100,
23+
},
24+
parentHeight: {
25+
type: "integer",
26+
label: "Parent Height",
27+
description: "How many levels of parent (and grandparent, etc) post to include. Default is `80`.",
28+
optional: true,
29+
max: 100,
30+
},
31+
},
32+
methods: {
33+
getPostThread(args = {}) {
34+
return this.app._makeRequest({
35+
path: "/app.bsky.feed.getPostThread",
36+
...args,
37+
});
38+
},
39+
},
40+
async run({ $ }) {
41+
const {
42+
app,
43+
getPostThread,
44+
postUrl,
45+
depth,
46+
parentHeight,
47+
} = this;
48+
49+
const {
50+
handle,
51+
postId,
52+
} = app.getHandleAndPostIdFromUrl(postUrl);
53+
54+
const { did } = await app.resolveHandle({
55+
$,
56+
params: {
57+
handle,
58+
},
59+
});
60+
61+
const response = await getPostThread({
62+
$,
63+
params: {
64+
uri: app.getPostUri(postId, did),
65+
depth,
66+
parentHeight,
67+
},
68+
});
69+
70+
$.export("$summary", "Successfully retrieved thread.");
71+
return response;
72+
},
73+
};

components/bluesky/bluesky.app.mjs

Lines changed: 167 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,174 @@
1+
import { axios } from "@pipedream/platform";
2+
import constants from "./common/constants.mjs";
3+
import utils from "./common/utils.mjs";
4+
15
export default {
26
type: "app",
37
app: "bluesky",
4-
propDefinitions: {},
8+
propDefinitions: {
9+
postUrl: {
10+
type: "string",
11+
label: "Post URL",
12+
description: "The URL will look like `https://bsky.app/profile/myhandle.bsky.social/post/3le7x3qgmaw23`.",
13+
},
14+
authorId: {
15+
type: "string",
16+
label: "Author ID",
17+
description: "The ID of the author to track posts.",
18+
},
19+
accountId: {
20+
type: "string",
21+
label: "Account ID",
22+
description: "The ID of the account to monitor for new followers.",
23+
},
24+
},
525
methods: {
6-
// this.$auth contains connected account data
7-
authKeys() {
8-
console.log(Object.keys(this.$auth));
26+
getHandleAndPostIdFromUrl(postUrl) {
27+
const match = postUrl?.match(constants.HANDLE_AND_POST_ID_REGEX);
28+
if (!match) {
29+
throw new Error("Invalid post URL");
30+
}
31+
const {
32+
handle,
33+
postId,
34+
} = match.groups;
35+
36+
return {
37+
handle,
38+
postId,
39+
};
40+
},
41+
getPostUri(postId, did = this.getDID()) {
42+
return `at://${did}/${constants.RESOURCE_TYPE.POST}/${postId}`;
43+
},
44+
getDID() {
45+
return this.$auth.did;
46+
},
47+
getUrl(path) {
48+
return `${constants.BASE_URL}${constants.VERSION_PATH}${path}`;
49+
},
50+
getHeaders(headers) {
51+
return {
52+
Authorization: `Bearer ${this.$auth.oauth_access_token}`,
53+
...headers,
54+
};
55+
},
56+
_makeRequest({
57+
$ = this, path, headers, ...args
58+
} = {}) {
59+
return axios($, {
60+
...args,
61+
url: this.getUrl(path),
62+
headers: this.getHeaders(headers),
63+
});
64+
},
65+
post(args = {}) {
66+
return this._makeRequest({
67+
method: "POST",
68+
...args,
69+
});
70+
},
71+
createRecord(args = {}) {
72+
return this.post({
73+
path: "/com.atproto.repo.createRecord",
74+
...args,
75+
data: {
76+
...args.data,
77+
repo: this.getDID(),
78+
},
79+
});
80+
},
81+
getRecord(args = {}) {
82+
return this._makeRequest({
83+
path: "/com.atproto.repo.getRecord",
84+
...args,
85+
});
86+
},
87+
resolveHandle(args = {}) {
88+
return this._makeRequest({
89+
path: "/com.atproto.identity.resolveHandle",
90+
...args,
91+
});
92+
},
93+
getAuthorFeed(args = {}) {
94+
return this._makeRequest({
95+
path: "/app.bsky.feed.getAuthorFeed",
96+
...args,
97+
});
98+
},
99+
getTimeline(args = {}) {
100+
return this._makeRequest({
101+
path: "/app.bsky.feed.getTimeline",
102+
...args,
103+
});
104+
},
105+
getFollowers(args = {}) {
106+
return this._makeRequest({
107+
path: "/app.bsky.graph.getFollowers",
108+
...args,
109+
});
110+
},
111+
async *getIterations({
112+
resourcesFn, resourcesFnArgs, resourceName,
113+
lastDateAt, dateField,
114+
max = constants.DEFAULT_MAX,
115+
}) {
116+
let cursor;
117+
let resourcesCount = 0;
118+
const firstRun = !lastDateAt;
119+
120+
while (true) {
121+
const response = await resourcesFn({
122+
...resourcesFnArgs,
123+
params: {
124+
...resourcesFnArgs?.params,
125+
cursor,
126+
limit: constants.DEFAULT_LIMIT,
127+
},
128+
});
129+
130+
const nextResources = utils.getNestedProperty(response, resourceName);
131+
132+
if (!nextResources?.length) {
133+
console.log("No more resources found");
134+
return;
135+
}
136+
137+
for (const resource of nextResources) {
138+
const isLastDateGreater = lastDateAt
139+
&& Date.parse(lastDateAt) > Date.parse(utils.getNestedProperty(resource, dateField));
140+
141+
if (isLastDateGreater) {
142+
console.log(`Last date is greater than the current resource date in ${dateField}`);
143+
return;
144+
}
145+
146+
if (!isLastDateGreater) {
147+
yield resource;
148+
resourcesCount += 1;
149+
}
150+
151+
if (resourcesCount >= max) {
152+
console.log("Reached max resources");
153+
return;
154+
}
155+
}
156+
157+
if (firstRun) {
158+
console.log("First run: only one request processed");
159+
return;
160+
}
161+
162+
if (nextResources.length < constants.DEFAULT_LIMIT) {
163+
console.log("No next page found");
164+
return;
165+
}
166+
167+
cursor = response.cursor;
168+
}
169+
},
170+
paginate(args = {}) {
171+
return utils.iterate(this.getIterations(args));
9172
},
10173
},
11174
};
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
const BASE_URL = "https://bsky.social";
2+
const VERSION_PATH = "/xrpc";
3+
4+
const INTERACTION_EVENT = {
5+
REQUES_TLESS: "app.bsky.feed.defs#requestLess",
6+
REQUEST_MORE: "app.bsky.feed.defs#requestMore",
7+
CLICK_THROUGH_ITEM: "app.bsky.feed.defs#clickthroughItem",
8+
CLICK_THROUGH_AUTHOR: "app.bsky.feed.defs#clickthroughAuthor",
9+
CLICK_THROUGH_REPOSTER: "app.bsky.feed.defs#clickthroughReposter",
10+
CLICK_THROUGH_EMBED: "app.bsky.feed.defs#clickthroughEmbed",
11+
INTERACTION_SEEN: "app.bsky.feed.defs#interactionSeen",
12+
INTERACTION_LIKE: "app.bsky.feed.defs#interactionLike",
13+
INTERACTION_REPOST: "app.bsky.feed.defs#interactionRepost",
14+
INTERACTION_REPLY: "app.bsky.feed.defs#interactionReply",
15+
INTERACTION_QUOTE: "app.bsky.feed.defs#interactionQuote",
16+
INTERACTION_SHARE: "app.bsky.feed.defs#interactionShare",
17+
};
18+
19+
const RESOURCE_TYPE = {
20+
POST: "app.bsky.feed.post",
21+
LIKE: "app.bsky.feed.like",
22+
};
23+
24+
const HANDLE_AND_POST_ID_REGEX = /(?:https?:\/\/)?(?:www\.)?(?:[^/]+)\/profile\/(?<handle>[^/]+)\/post\/(?<postId>[^/]+)/;
25+
26+
const DEFAULT_LIMIT = 3;
27+
const DEFAULT_MAX = 600;
28+
const IS_FIRST_RUN = "isFirstRun";
29+
const LAST_DATE_AT = "lastDateAt";
30+
31+
export default {
32+
BASE_URL,
33+
VERSION_PATH,
34+
INTERACTION_EVENT,
35+
RESOURCE_TYPE,
36+
HANDLE_AND_POST_ID_REGEX,
37+
DEFAULT_LIMIT,
38+
DEFAULT_MAX,
39+
IS_FIRST_RUN,
40+
LAST_DATE_AT,
41+
};

0 commit comments

Comments
 (0)