diff --git a/components/facebook_pages/.gitignore b/components/facebook_pages/.gitignore deleted file mode 100644 index ec761ccab7595..0000000000000 --- a/components/facebook_pages/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -*.js -*.mjs -dist \ No newline at end of file diff --git a/components/facebook_pages/app/facebook_pages.app.ts b/components/facebook_pages/app/facebook_pages.app.ts deleted file mode 100644 index 41c0716af3d4d..0000000000000 --- a/components/facebook_pages/app/facebook_pages.app.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { defineApp } from "@pipedream/types"; - -export default defineApp({ - type: "app", - app: "facebook_pages", - propDefinitions: {}, - methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); - }, - }, -}); \ No newline at end of file diff --git a/components/facebook_pages/facebook_pages.app.mjs b/components/facebook_pages/facebook_pages.app.mjs index d386d6998bdd6..05c4e13be8ff3 100644 --- a/components/facebook_pages/facebook_pages.app.mjs +++ b/components/facebook_pages/facebook_pages.app.mjs @@ -101,6 +101,11 @@ export default { description: "The maximum number of results to return", optional: true, }, + appId: { + type: "string", + label: "App ID", + description: "The Facebook App ID. You can find this in your Facebook App Dashboard.", + }, }, methods: { _baseUrl() { @@ -139,6 +144,12 @@ export default { const page = data.find(({ id }) => id == pageId); return page.access_token; }, + getAppAccessToken(args = {}) { + return this._makeRequest({ + path: "/oauth/access_token", + ...args, + }); + }, getPost({ pageId, postId, ...args }) { @@ -221,5 +232,43 @@ export default { ...args, }); }, + createSubscription({ + appId, ...args + } = {}) { + return this._makeRequest({ + path: `/${appId}/subscriptions`, + method: "POST", + ...args, + }); + }, + deleteSubscription({ + appId, ...args + } = {}) { + return this._makeRequest({ + path: `/${appId}/subscriptions`, + method: "DELETE", + ...args, + }); + }, + createPageSubscription({ + pageId, ...args + } = {}) { + return this._makeRequest({ + path: `/${pageId}/subscribed_apps`, + method: "POST", + pageId, + ...args, + }); + }, + deletePageSubscription({ + pageId, ...args + } = {}) { + return this._makeRequest({ + path: `/${pageId}/subscribed_apps`, + method: "DELETE", + pageId, + ...args, + }); + }, }, }; diff --git a/components/facebook_pages/package.json b/components/facebook_pages/package.json index 64bc3610db9f1..395f9fc781f28 100644 --- a/components/facebook_pages/package.json +++ b/components/facebook_pages/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/facebook_pages", - "version": "0.1.0", + "version": "0.2.0", "description": "Pipedream Facebook Pages Components", "main": "facebook_pages.app.mjs", "keywords": [ @@ -13,6 +13,6 @@ "access": "public" }, "dependencies": { - "@pipedream/platform": "^1.5.1" + "@pipedream/platform": "^3.1.0" } } diff --git a/components/facebook_pages/sources/README.md b/components/facebook_pages/sources/README.md new file mode 100644 index 0000000000000..284a8beb182c6 --- /dev/null +++ b/components/facebook_pages/sources/README.md @@ -0,0 +1,80 @@ +# Facebook Pages Webhook Sources + +This directory contains webhook sources for Facebook Pages events. These sources allow you to receive real-time notifications when various events occur on your Facebook Page. + +## Available Sources + +### 1. New Feed Activity (`new-feed-activity`) +Emit new event when there's any new activity in your Facebook Page's feed. This includes new posts, comments, reactions, and shares. + +### 2. New Post to Page (`new-post`) +Emit new event when a new post is made to your Facebook Page's feed. + +### 3. New Comment on Post (`new-comment`) +Emit new event when a new comment is added to a post on your Facebook Page. + +### 4. New Reaction on Post (`new-reaction`) +Emit new event when someone reacts to a post on your Facebook Page (likes, love, wow, etc.). + +### 5. New Share of Post (`new-share`) +Emit new event when someone shares a post from your Facebook Page. + +### 6. Page Updated (`page-updated`) +Emit new event when your Facebook Page information is updated (such as description, hours, location, etc.). + +### 7. New Message Received (`new-message`) +Emit new event when your Facebook Page receives a new message via Messenger. + +## Setup Instructions + +To use these webhook sources, you need to: + +1. **Create a Facebook App**: + - Go to [Facebook Developers](https://developers.facebook.com/) + - Create a new app or use an existing one + - Note your App ID + +2. **Configure Webhooks in Facebook**: + - In your Facebook App dashboard, go to Webhooks + - Subscribe to the "Page" object + - When deploying a source in Pipedream, you'll get a webhook URL and verify token + - Use these values in your Facebook webhook configuration + +3. **Set Required Permissions**: + - For feed webhooks: `pages_manage_metadata` and `pages_show_list` + - For message webhooks: `pages_messaging` + +4. **Subscribe Your Page**: + - The webhook source will automatically subscribe your selected page to receive events + - Make sure your page has not disabled the App platform in its settings + +## Common Properties + +All webhook sources share these properties: + +- **Facebook Pages App**: Your Facebook Pages connection +- **Page**: The Facebook Page to monitor for events +- **Verify Token**: A custom string for webhook verification (auto-generated but can be customized) + +## Event Data + +Each webhook event includes relevant data such as: +- Event type and timestamp +- User information (when available) +- Content (messages, post text, etc.) +- Related IDs (post_id, comment_id, etc.) + +## Troubleshooting + +1. **Webhook not receiving events**: + - Ensure your Facebook App has the correct permissions + - Verify the webhook is properly configured in Facebook + - Check that your page has the app installed + +2. **Verification failing**: + - Make sure the verify token matches exactly between Pipedream and Facebook + - The webhook URL must be publicly accessible (HTTPS) + +3. **Missing events**: + - Some events require specific permissions + - Check Facebook's webhook documentation for limitations \ No newline at end of file diff --git a/components/facebook_pages/sources/common/webhook.mjs b/components/facebook_pages/sources/common/webhook.mjs new file mode 100644 index 0000000000000..90667cbd32a93 --- /dev/null +++ b/components/facebook_pages/sources/common/webhook.mjs @@ -0,0 +1,112 @@ +import crypto from "crypto"; +import app from "../../facebook_pages.app.mjs"; + +export default { + props: { + app, + db: "$.service.db", + http: { + type: "$.interface.http", + customResponse: true, + }, + page: { + propDefinition: [ + app, + "page", + ], + }, + verifyToken: { + type: "string", + label: "Verify Token", + description: "A custom string you provide to Facebook for webhook verification. Facebook will send this token back when verifying your webhook endpoint.", + default: crypto.randomBytes(16).toString("hex"), + }, + }, + hooks: { + async activate() { + // Subscribe the page to the app with specified fields (page-level webhooks) + await this.app.createPageSubscription({ + pageId: this.page, + params: { + subscribed_fields: this.getFields().join(","), + }, + }); + + // Store page ID for deactivation + this._setPageId(this.page); + + console.log(`Webhook URL: ${this.http.endpoint}`); + console.log(`Verify Token: ${this.verifyToken}`); + console.log("Subscribed page-level webhooks for fields:", this.getFields()); + }, + async deactivate() { + const pageId = this._getPageId(); + if (pageId) { + await this.app.deletePageSubscription({ + pageId, + }); + } + }, + }, + methods: { + _getPageId() { + return this.db.get("pageId"); + }, + _setPageId(pageId) { + this.db.set("pageId", pageId); + }, + getFields() { + throw new Error("getFields is not implemented"); + }, + generateMeta() { + throw new Error("generateMeta is not implemented"); + }, + processEvent() { + throw new Error("processEvent is not implemented"); + }, + }, + async run({ + query, body, method, + }) { + // Handle webhook verification from Facebook + if (method === "GET") { + const mode = query["hub.mode"]; + const token = query["hub.verify_token"]; + const challenge = query["hub.challenge"]; + + if (mode === "subscribe" && token === this.verifyToken) { + console.log("Webhook verified"); + this.http.respond({ + status: 200, + body: challenge, + }); + return; + } else { + console.log("Webhook verification failed"); + this.http.respond({ + status: 403, + }); + return; + } + } + + // Handle webhook events + this.http.respond({ + status: 200, + }); + + if (body?.object === "page" && body?.entry) { + body.entry.forEach((entry) => { + if (entry.changes) { + entry.changes.forEach((change) => { + const eventData = this.processEvent(change); + if (eventData) { + const meta = this.generateMeta(eventData); + this.$emit(eventData, meta); + } + }); + } + }); + } + }, +}; diff --git a/components/facebook_pages/sources/new-comment/new-comment.mjs b/components/facebook_pages/sources/new-comment/new-comment.mjs new file mode 100644 index 0000000000000..42941ca857798 --- /dev/null +++ b/components/facebook_pages/sources/new-comment/new-comment.mjs @@ -0,0 +1,57 @@ +import common from "../common/webhook.mjs"; + +export default { + ...common, + key: "facebook_pages-new-comment", + name: "New Comment on Post", + description: "Emit new event when a new comment is added to a post on your Facebook Page.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getFields() { + return [ + "feed", + ]; + }, + generateMeta(data) { + const { + comment_id, parent_id, created_time, from, message, + } = data; + const ts = created_time + ? created_time * 1000 + : Date.now(); + const id = comment_id || `comment-${parent_id}-${ts}`; + + let summary = "New comment"; + if (from?.name) { + summary = `New comment by ${from.name}`; + } + if (message) { + const preview = message.substring(0, 50); + summary += `: ${preview}${message.length > 50 + ? "..." + : ""}`; + } + + return { + id, + summary, + ts, + }; + }, + processEvent(change) { + if (change.field === "feed" && change.value) { + const { + item, verb, + } = change.value; + // Only emit events for new comments + if (item === "comment" && verb === "add") { + return change.value; + } + } + return null; + }, + }, +}; diff --git a/components/facebook_pages/sources/new-feed-activity/new-feed-activity.mjs b/components/facebook_pages/sources/new-feed-activity/new-feed-activity.mjs new file mode 100644 index 0000000000000..221c4dce6fac5 --- /dev/null +++ b/components/facebook_pages/sources/new-feed-activity/new-feed-activity.mjs @@ -0,0 +1,48 @@ +import common from "../common/webhook.mjs"; + +export default { + ...common, + key: "facebook_pages-new-feed-activity", + name: "New Feed Activity", + description: "Emit new event when there's a new activity in your Facebook Page's feed. This includes new posts, comments, reactions, and shares.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getFields() { + return [ + "feed", + ]; + }, + generateMeta(data) { + const { + item, verb, post_id, created_time, + } = data; + const ts = created_time + ? created_time * 1000 + : Date.now(); + const id = post_id || `${item}-${verb}-${ts}`; + + let summary = `New ${verb} on ${item}`; + if (data.from?.name) { + summary = `${data.from.name} ${verb} ${item}`; + } + if (data.message) { + summary += `: ${data.message.substring(0, 50)}...`; + } + + return { + id, + summary, + ts, + }; + }, + processEvent(change) { + if (change.field === "feed") { + return change.value; + } + return null; + }, + }, +}; diff --git a/components/facebook_pages/sources/new-message/new-message.mjs b/components/facebook_pages/sources/new-message/new-message.mjs new file mode 100644 index 0000000000000..752b748a0cadc --- /dev/null +++ b/components/facebook_pages/sources/new-message/new-message.mjs @@ -0,0 +1,105 @@ +import common from "../common/webhook.mjs"; + +export default { + ...common, + key: "facebook_pages-new-message", + name: "New Message Received", + description: "Emit new event when your Facebook Page receives a new message via Messenger.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getFields() { + return [ + "messages", + ]; + }, + generateMeta(data) { + const { + sender, timestamp, message, + } = data; + const ts = timestamp || Date.now(); + const id = message?.mid || `message-${sender?.id}-${ts}`; + + let summary = "New message received"; + if (message?.text) { + summary = `New message: ${message.text.substring(0, 50)}...`; + } else if (message?.attachments?.length > 0) { + summary = `New message with ${message.attachments.length} attachment(s)`; + } + + return { + id, + summary, + ts, + }; + }, + processEvent(change) { + if (change.field === "messages" && change.value?.messaging) { + // Facebook sends messages in a messaging array + for (const messagingEvent of change.value.messaging) { + if (messagingEvent.message) { + return messagingEvent; + } + } + } + return null; + }, + }, + async run(event) { + const { + query, body, method, + } = event; + + // Handle webhook verification from Facebook + if (method === "GET") { + const mode = query["hub.mode"]; + const token = query["hub.verify_token"]; + const challenge = query["hub.challenge"]; + + if (mode === "subscribe" && token === this._getVerifyToken()) { + console.log("Webhook verified"); + this.http.respond({ + status: 200, + body: challenge, + }); + return; + } else { + console.log("Webhook verification failed"); + this.http.respond({ + status: 403, + }); + return; + } + } + + // Handle webhook events + this.http.respond({ + status: 200, + }); + + if (body?.object === "page" && body?.entry) { + for (const entry of body.entry) { + // Messages come in a different format than regular feed changes + if (entry.messaging) { + for (const messagingEvent of entry.messaging) { + if (messagingEvent.message) { + const meta = this.generateMeta(messagingEvent); + this.$emit(messagingEvent, meta); + } + } + } else if (entry.changes) { + // Fallback to the common handler for other message-related changes + for (const change of entry.changes) { + const eventData = this.processEvent(change); + if (eventData) { + const meta = this.generateMeta(eventData); + this.$emit(eventData, meta); + } + } + } + } + } + }, +}; diff --git a/components/facebook_pages/sources/new-post/new-post.mjs b/components/facebook_pages/sources/new-post/new-post.mjs new file mode 100644 index 0000000000000..03f9be229bd40 --- /dev/null +++ b/components/facebook_pages/sources/new-post/new-post.mjs @@ -0,0 +1,60 @@ +import common from "../common/webhook.mjs"; + +export default { + ...common, + key: "facebook_pages-new-post", + name: "New Post To Page", + description: "Emit new event when a new post is made to your Facebook Page's feed.", + version: "0.0.4", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getObject() { + return "page"; + }, + getFields() { + return [ + "feed", + ]; + }, + generateMeta(data) { + const { + post_id, created_time, from, message, + } = data; + const ts = created_time + ? created_time * 1000 + : Date.now(); + const id = post_id || `post-${ts}`; + + let summary = "New post"; + if (from?.name) { + summary = `New post by ${from.name}`; + } + if (message) { + const preview = message.substring(0, 50); + summary += `: ${preview}${message.length > 50 + ? "..." + : ""}`; + } + + return { + id, + summary, + ts, + }; + }, + processEvent(change) { + if (change.field === "feed" && change.value) { + const { + item, verb, + } = change.value; + // Only emit events for new posts + if (item === "post" && verb === "add") { + return change.value; + } + } + return null; + }, + }, +}; diff --git a/components/facebook_pages/sources/new-reaction/new-reaction.mjs b/components/facebook_pages/sources/new-reaction/new-reaction.mjs new file mode 100644 index 0000000000000..a6cc454f82bc8 --- /dev/null +++ b/components/facebook_pages/sources/new-reaction/new-reaction.mjs @@ -0,0 +1,54 @@ +import common from "../common/webhook.mjs"; + +export default { + ...common, + key: "facebook_pages-new-reaction", + name: "New Reaction on Post", + description: "Emit new event when someone reacts to a post on your Facebook Page (likes, love, wow, etc.).", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getFields() { + return [ + "feed", + ]; + }, + generateMeta(data) { + const { + post_id, reaction_type, from, created_time, + } = data; + const ts = created_time + ? created_time * 1000 + : Date.now(); + const id = `reaction-${post_id}-${from?.id || "unknown"}-${ts}`; + + let summary = "New reaction"; + if (reaction_type) { + summary = `New ${reaction_type} reaction`; + } + if (from?.name) { + summary += ` from ${from.name}`; + } + + return { + id, + summary, + ts, + }; + }, + processEvent(change) { + if (change.field === "feed" && change.value) { + const { + item, verb, + } = change.value; + // Only emit events for reactions + if (item === "reaction" && verb === "add") { + return change.value; + } + } + return null; + }, + }, +}; diff --git a/components/facebook_pages/sources/new-share/new-share.mjs b/components/facebook_pages/sources/new-share/new-share.mjs new file mode 100644 index 0000000000000..9e962dba69040 --- /dev/null +++ b/components/facebook_pages/sources/new-share/new-share.mjs @@ -0,0 +1,51 @@ +import common from "../common/webhook.mjs"; + +export default { + ...common, + key: "facebook_pages-new-share", + name: "New Share of Post", + description: "Emit new event when someone shares a post from your Facebook Page.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getFields() { + return [ + "feed", + ]; + }, + generateMeta(data) { + const { + post_id, share_id, from, created_time, + } = data; + const ts = created_time + ? created_time * 1000 + : Date.now(); + const id = share_id || `share-${post_id}-${ts}`; + + let summary = "New share"; + if (from?.name) { + summary = `${from.name} shared a post`; + } + + return { + id, + summary, + ts, + }; + }, + processEvent(change) { + if (change.field === "feed" && change.value) { + const { + item, verb, + } = change.value; + // Only emit events for shares + if (item === "share" && verb === "add") { + return change.value; + } + } + return null; + }, + }, +}; diff --git a/components/facebook_pages/sources/page-updated/page-updated.mjs b/components/facebook_pages/sources/page-updated/page-updated.mjs new file mode 100644 index 0000000000000..864006d370208 --- /dev/null +++ b/components/facebook_pages/sources/page-updated/page-updated.mjs @@ -0,0 +1,59 @@ +import common from "../common/webhook.mjs"; + +export default { + ...common, + key: "facebook_pages-page-updated", + name: "Page Updated", + description: "Emit new event when your Facebook Page information is updated (such as description, hours, location, etc.).", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getFields() { + return [ + "feed", + ]; + }, + generateMeta(data) { + const { + item, verb, created_time, + } = data; + const ts = created_time + ? created_time * 1000 + : Date.now(); + const id = `page-update-${ts}`; + + let summary = "Page updated"; + if (item === "status" && verb === "add") { + summary = "Page status updated"; + if (data.message) { + const preview = data.message.substring(0, 50); + summary += `: ${preview}${data.message.length > 50 + ? "..." + : ""}`; + } + } else if (verb === "edited") { + summary = `Page ${item} edited`; + } + + return { + id, + summary, + ts, + }; + }, + processEvent(change) { + if (change.field === "feed" && change.value) { + const { + item, verb, + } = change.value; + // Emit events for page updates (status updates, edits, etc.) + if ((item === "status" && verb === "add") || verb === "edited") { + return change.value; + } + } + return null; + }, + }, +}; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 232f64f1a103d..37b8e8ebc7f82 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4680,8 +4680,8 @@ importers: components/facebook_pages: dependencies: '@pipedream/platform': - specifier: ^1.5.1 - version: 1.6.6 + specifier: ^3.1.0 + version: 3.1.0 components/faceup: {} @@ -40189,8 +40189,6 @@ snapshots: '@putout/operator-filesystem': 5.0.0(putout@36.13.1(eslint@8.57.1)(typescript@5.6.3)) '@putout/operator-json': 2.2.0 putout: 36.13.1(eslint@8.57.1)(typescript@5.6.3) - transitivePeerDependencies: - - supports-color '@putout/operator-regexp@1.0.0(putout@36.13.1(eslint@8.57.1)(typescript@5.6.3))': dependencies: