Skip to content

[Components] bluesky - new components #15087

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Dec 31, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions components/bluesky/actions/create-post/create-post.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import app from "../../bluesky.app.mjs";
import constants from "../../common/constants.mjs";

export default {
key: "bluesky-create-post",
name: "Create Post",
description: "Creates a new post on Bluesky. [See the documentation](https://docs.bsky.app/docs/api/com-atproto-repo-create-record).",
version: "0.0.1",
type: "action",
props: {
app,
text: {
type: "string",
label: "Text",
description: "The text content of the post.",
},
},
async run({ $ }) {
const {
app,
text,
} = this;

const response = await app.createRecord({
$,
data: {
collection: constants.RESOURCE_TYPE.POST,
record: {
["$type"]: constants.RESOURCE_TYPE.POST,
text,
createdAt: new Date().toISOString(),
},
},
});

$.export("$summary", `Successfully created a new post with uri \`${response.uri}\`.`);
return response;
},
};
61 changes: 61 additions & 0 deletions components/bluesky/actions/like-post/like-post.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import app from "../../bluesky.app.mjs";
import constants from "../../common/constants.mjs";

export default {
key: "bluesky-like-post",
name: "Like Post",
description: "Like a specific post on Bluesky. [See the documentation](https://docs.bsky.app/docs/api/com-atproto-repo-create-record).",
version: "0.0.1",
type: "action",
props: {
app,
postUrl: {
propDefinition: [
app,
"postUrl",
],
},
},
async run({ $ }) {
const {
app,
postUrl,
} = this;

const {
handle,
postId,
} = app.getHandleAndPostIdFromUrl(postUrl);

const {
uri,
cid,
} = await app.getRecord({
$,
params: {
repo: handle,
collection: constants.RESOURCE_TYPE.POST,
rkey: postId,
},
});

const response = await app.createRecord({
$,
data: {
collection: constants.RESOURCE_TYPE.LIKE,
record: {
["$type"]: constants.RESOURCE_TYPE.LIKE,
createdAt: new Date().toISOString(),
subject: {
uri,
cid,
py_type: "com.atproto.repo.strongRef",
},
},
},
});

$.export("$summary", "Successfully liked post.");
return response;
},
};
73 changes: 73 additions & 0 deletions components/bluesky/actions/retrieve-thread/retrieve-thread.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import app from "../../bluesky.app.mjs";

export default {
key: "bluesky-retrieve-thread",
name: "Retrieve Thread",
description: "Retrieve a full thread of posts. [See the documentation](https://docs.bsky.app/docs/api/app-bsky-feed-get-post-thread).",
version: "0.0.1",
type: "action",
props: {
app,
postUrl: {
propDefinition: [
app,
"postUrl",
],
},
depth: {
type: "integer",
label: "Depth",
description: "How many levels of reply depth should be included in response. Default is `6`.",
optional: true,
max: 100,
},
parentHeight: {
type: "integer",
label: "Parent Height",
description: "How many levels of parent (and grandparent, etc) post to include. Default is `80`.",
optional: true,
max: 100,
},
},
methods: {
getPostThread(args = {}) {
return this.app._makeRequest({
path: "/app.bsky.feed.getPostThread",
...args,
});
},
},
async run({ $ }) {
const {
app,
getPostThread,
postUrl,
depth,
parentHeight,
} = this;

const {
handle,
postId,
} = app.getHandleAndPostIdFromUrl(postUrl);

const { did } = await app.resolveHandle({
$,
params: {
handle,
},
});

const response = await getPostThread({
$,
params: {
uri: app.getPostUri(postId, did),
depth,
parentHeight,
},
});

$.export("$summary", "Successfully retrieved thread.");
return response;
},
};
171 changes: 167 additions & 4 deletions components/bluesky/bluesky.app.mjs
Original file line number Diff line number Diff line change
@@ -1,11 +1,174 @@
import { axios } from "@pipedream/platform";
import constants from "./common/constants.mjs";
import utils from "./common/utils.mjs";

export default {
type: "app",
app: "bluesky",
propDefinitions: {},
propDefinitions: {
postUrl: {
type: "string",
label: "Post URL",
description: "The URL will look like `https://bsky.app/profile/myhandle.bsky.social/post/3le7x3qgmaw23`.",
},
authorId: {
type: "string",
label: "Author ID",
description: "The ID of the author to track posts.",
},
accountId: {
type: "string",
label: "Account ID",
description: "The ID of the account to monitor for new followers.",
},
},
methods: {
// this.$auth contains connected account data
authKeys() {
console.log(Object.keys(this.$auth));
getHandleAndPostIdFromUrl(postUrl) {
const match = postUrl?.match(constants.HANDLE_AND_POST_ID_REGEX);
if (!match) {
throw new Error("Invalid post URL");
}
const {
handle,
postId,
} = match.groups;

return {
handle,
postId,
};
},
getPostUri(postId, did = this.getDID()) {
return `at://${did}/${constants.RESOURCE_TYPE.POST}/${postId}`;
},
getDID() {
return this.$auth.did;
},
Comment on lines +44 to +46
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add error handling for missing DID.

The method assumes this.$auth.did is always present. Consider adding validation.

   getDID() {
+    if (!this.$auth.did) {
+      throw new Error("DID not found in authentication context");
+    }
     return this.$auth.did;
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
getDID() {
return this.$auth.did;
},
getDID() {
if (!this.$auth.did) {
throw new Error("DID not found in authentication context");
}
return this.$auth.did;
},

getUrl(path) {
return `${constants.BASE_URL}${constants.VERSION_PATH}${path}`;
},
getHeaders(headers) {
return {
Authorization: `Bearer ${this.$auth.oauth_access_token}`,
...headers,
};
},
_makeRequest({
$ = this, path, headers, ...args
} = {}) {
return axios($, {
...args,
url: this.getUrl(path),
headers: this.getHeaders(headers),
});
},
post(args = {}) {
return this._makeRequest({
method: "POST",
...args,
});
},
createRecord(args = {}) {
return this.post({
path: "/com.atproto.repo.createRecord",
...args,
data: {
...args.data,
repo: this.getDID(),
},
});
},
getRecord(args = {}) {
return this._makeRequest({
path: "/com.atproto.repo.getRecord",
...args,
});
},
resolveHandle(args = {}) {
return this._makeRequest({
path: "/com.atproto.identity.resolveHandle",
...args,
});
},
getAuthorFeed(args = {}) {
return this._makeRequest({
path: "/app.bsky.feed.getAuthorFeed",
...args,
});
},
getTimeline(args = {}) {
return this._makeRequest({
path: "/app.bsky.feed.getTimeline",
...args,
});
},
getFollowers(args = {}) {
return this._makeRequest({
path: "/app.bsky.graph.getFollowers",
...args,
});
},
async *getIterations({
resourcesFn, resourcesFnArgs, resourceName,
lastDateAt, dateField,
max = constants.DEFAULT_MAX,
}) {
let cursor;
let resourcesCount = 0;
const firstRun = !lastDateAt;

while (true) {
const response = await resourcesFn({
...resourcesFnArgs,
params: {
...resourcesFnArgs?.params,
cursor,
limit: constants.DEFAULT_LIMIT,
},
});

const nextResources = utils.getNestedProperty(response, resourceName);

if (!nextResources?.length) {
console.log("No more resources found");
return;
}

for (const resource of nextResources) {
const isLastDateGreater = lastDateAt
&& Date.parse(lastDateAt) > Date.parse(utils.getNestedProperty(resource, dateField));

if (isLastDateGreater) {
console.log(`Last date is greater than the current resource date in ${dateField}`);
return;
}

if (!isLastDateGreater) {
yield resource;
resourcesCount += 1;
}

if (resourcesCount >= max) {
console.log("Reached max resources");
return;
}
}

if (firstRun) {
console.log("First run: only one request processed");
return;
}

if (nextResources.length < constants.DEFAULT_LIMIT) {
console.log("No next page found");
return;
}

cursor = response.cursor;
}
},
paginate(args = {}) {
return utils.iterate(this.getIterations(args));
},
},
};
41 changes: 41 additions & 0 deletions components/bluesky/common/constants.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
const BASE_URL = "https://bsky.social";
const VERSION_PATH = "/xrpc";

const INTERACTION_EVENT = {
REQUES_TLESS: "app.bsky.feed.defs#requestLess",
REQUEST_MORE: "app.bsky.feed.defs#requestMore",
CLICK_THROUGH_ITEM: "app.bsky.feed.defs#clickthroughItem",
CLICK_THROUGH_AUTHOR: "app.bsky.feed.defs#clickthroughAuthor",
CLICK_THROUGH_REPOSTER: "app.bsky.feed.defs#clickthroughReposter",
CLICK_THROUGH_EMBED: "app.bsky.feed.defs#clickthroughEmbed",
INTERACTION_SEEN: "app.bsky.feed.defs#interactionSeen",
INTERACTION_LIKE: "app.bsky.feed.defs#interactionLike",
INTERACTION_REPOST: "app.bsky.feed.defs#interactionRepost",
INTERACTION_REPLY: "app.bsky.feed.defs#interactionReply",
INTERACTION_QUOTE: "app.bsky.feed.defs#interactionQuote",
INTERACTION_SHARE: "app.bsky.feed.defs#interactionShare",
};

const RESOURCE_TYPE = {
POST: "app.bsky.feed.post",
LIKE: "app.bsky.feed.like",
};

const HANDLE_AND_POST_ID_REGEX = /(?:https?:\/\/)?(?:www\.)?(?:[^/]+)\/profile\/(?<handle>[^/]+)\/post\/(?<postId>[^/]+)/;

const DEFAULT_LIMIT = 3;
const DEFAULT_MAX = 600;
const IS_FIRST_RUN = "isFirstRun";
const LAST_DATE_AT = "lastDateAt";

export default {
BASE_URL,
VERSION_PATH,
INTERACTION_EVENT,
RESOURCE_TYPE,
HANDLE_AND_POST_ID_REGEX,
DEFAULT_LIMIT,
DEFAULT_MAX,
IS_FIRST_RUN,
LAST_DATE_AT,
};
Loading
Loading