diff --git a/components/wordpress_com/README.md b/components/wordpress_com/README.md index 46f51cadf9b2e..9d9e0003d9979 100644 --- a/components/wordpress_com/README.md +++ b/components/wordpress_com/README.md @@ -9,3 +9,33 @@ The Wordpress.com API empowers developers to extend and integrate their website' - **Comment Moderation Alerts**: Set up a Pipedream workflow that monitors Wordpress.com for new comments. When a comment is detected, analyze its content for specific keywords or sentiment using a service like Google's Natural Language API. If the comment requires attention (e.g., contains flagged words or negative sentiment), send an immediate alert to a Slack channel or via email to prompt review and moderation, keeping your community healthy and engaged. - **User Synchronization and Engagement**: Create a workflow that triggers when a new user registers on your Wordpress.com site. This workflow can add the user to a CRM like HubSpot or Salesforce, subscribe them to a Mailchimp email list, and even send a personalized welcome email via SendGrid. This ensures your user data is consistent across platforms and kickstarts the user engagement process from the moment they sign up. + + +# Available Event Sources + +Trigger workflows automatically when specific Wordpress.com events occur: + + New Post: Emit a new event when a post, page, or media attachment is published. + Required props: site ID or URL. Optional: post type (post, page, attachment). + + New Comment: Emit a new event when a comment is added to any post or page. + Required props: site ID or URL. Optional: post ID to filter comments. + + New Follower: Emit a new event when someone subscribes to the site's blog. + Required props: site ID or URL. + +Each source manages its own database cursor to ensure only new data is processed each time it runs — no duplicates, no missed updates. +Available Actions + +Perform direct operations on your Wordpress.com site: + + Create Post: Create a new post on the site. + Required props: site ID or URL, post title, post content. + Optional props: post status (draft, published), categories, and tags. + + Upload Media: Upload a media file (image, video, etc.) to the site's library. + Required props: site ID or URL, media file (binary or URL). + Optional props: media title and description. + + Delete Post: Delete an existing post from the site. + Required props: site ID or URL, post ID. \ No newline at end of file diff --git a/components/wordpress_com/actions/create-post/create-post.mjs b/components/wordpress_com/actions/create-post/create-post.mjs new file mode 100644 index 0000000000000..db81fd19ab850 --- /dev/null +++ b/components/wordpress_com/actions/create-post/create-post.mjs @@ -0,0 +1,86 @@ +import wordpress from "../../wordpress_com.app.mjs"; + +export default { + key: "wordpress_com-create-post", + name: "Create New Post", + description: "Creates a new post on a WordPress.com site. [See the documentation](https://developer.wordpress.com/docs/api/1.1/post/sites/%24site/posts/new/)", + version: "0.0.1", + type: "action", + props: { + wordpress, + site: { + propDefinition: [ + wordpress, + "siteId", + ], + }, + title: { + type: "string", + label: "Post Title", + description: "The title of the post", + }, + content: { + type: "string", + label: "Post Content", + description: "The content of the post (HTML or text)", + }, + status: { + type: "string", + label: "Status", + description: "The status of the post", + options: [ + "publish", + "draft", + "private", + "pending", + ], + default: "draft", + optional: true, + }, + type: { + type: "string", + label: "Post Type", + description: "The type of the post (post or page). For attachments, use the 'Upload Media' action.", + options: [ + { + label: "Post", + value: "post", + }, + { + label: "Page", + value: "page", + }, + ], + default: "post", + optional: true, + }, + }, + async run({ $ }) { + const warnings = []; + + const { + site, + wordpress, + ...fields + } = this; + + warnings.push(...wordpress.checkDomainOrId(site)); + + let response; + + try { + response = await wordpress.createWordpressPost({ + $, + site, + data: { + ...fields, + }, + }); + } catch (error) { + wordpress.throwCustomError("Could not create post", error, warnings); + }; + + $.export("$summary", `Post successfully created. ID = ${response?.ID}` + "\n- " + warnings.join("\n- ")); + }, +}; + diff --git a/components/wordpress_com/actions/delete-post/delete-post.mjs b/components/wordpress_com/actions/delete-post/delete-post.mjs new file mode 100644 index 0000000000000..367d4621cd170 --- /dev/null +++ b/components/wordpress_com/actions/delete-post/delete-post.mjs @@ -0,0 +1,54 @@ +import wordpress from "../../wordpress_com.app.mjs"; + +export default { + key: "wordpress_com-delete-post", + name: "Delete Post", + description: "Deletes a post. [See the documentation](https://developer.wordpress.com/docs/api/1.1/post/sites/%24site/posts/%24post_ID/delete/)", + version: "0.0.1", + type: "action", + props: { + wordpress, + site: { + propDefinition: [ + wordpress, + "siteId", + ], + }, + postId: { + propDefinition: [ + wordpress, + "postId", + (c) => ({ + site: c.site, + }), + ], + description: "The ID of the post you want to delete", + }, + }, + async run({ $ }) { + const warnings = []; + + const { + site, + wordpress, + postId, + } = this; + + warnings.push(...wordpress.checkDomainOrId(site)); + + let response; + + try { + response = await wordpress.deleteWordpressPost({ + $, + site, + postId, + }); + } catch (error) { + wordpress.throwCustomError("Could not delete post", error, warnings); + }; + + $.export("$summary", `Post ID = ${response?.ID} successfully deleted.` + "\n- " + warnings.join("\n- ")); + }, +}; + diff --git a/components/wordpress_com/actions/upload-media/upload-media.mjs b/components/wordpress_com/actions/upload-media/upload-media.mjs new file mode 100644 index 0000000000000..d368c709eaf77 --- /dev/null +++ b/components/wordpress_com/actions/upload-media/upload-media.mjs @@ -0,0 +1,87 @@ +import { prepareMediaUpload } from "../../common/utils.mjs"; +import wordpress from "../../wordpress_com.app.mjs"; + +export default { + key: "wordpress_com-upload-media", + name: "Upload Media", + description: "Uploads a media file from a URL to the specified WordPress.com site. [See the documentation](https://developer.wordpress.com/docs/api/1.1/post/sites/%24site/media/new/)", + version: "0.0.1", + type: "action", + props: { + wordpress, + site: { + propDefinition: [ + wordpress, + "siteId", + ], + }, + media: { + type: "any", + label: "Media URL", + description: "A direct media URL, or a FormData object with the file attached under the field name 'media[]'.", + }, + title: { + type: "string", + label: "Title", + description: "Title of the media", + optional: true, + }, + caption: { + type: "string", + label: "Caption", + description: "Optional caption text to associate with the uploaded media", + optional: true, + }, + description: { + type: "string", + label: "Description", + description: "A description of the uploaded media", + optional: true, + }, + }, + async run({ $ }) { + const warnings = []; + + const + { + wordpress, + site, + media, + ...fields + } = this; + + warnings.push(...wordpress.checkDomainOrId(site)); + + let form; + + // If not form data + if (wordpress.isFormData(media)) { + form = media; + + } else { + form = await prepareMediaUpload(media, fields, $); + } + + let response; + + try { + response = await wordpress.uploadWordpressMedia({ + $, + contentType: form.getHeaders()["content-type"], + site, + data: form, + }); + + const media = response.media[0]; + + $.export("$summary", `Media "${media.title}" uploaded successfully (ID: ${media.ID})` + "\n- " + warnings.join("\n- ")); + + console.log(response); + return response; + + } catch (error) { + wordpress.throwCustomError("Failed to upload media", error, warnings); + }; + }, +}; + diff --git a/components/wordpress_com/common/methods.mjs b/components/wordpress_com/common/methods.mjs new file mode 100644 index 0000000000000..be0ef60b373f1 --- /dev/null +++ b/components/wordpress_com/common/methods.mjs @@ -0,0 +1,226 @@ +export default { + + isString(input) { + return typeof input === "string"; + }, + + isNumber(input) { + return typeof input === "number"; + }, + + isEmptyString(input) { + if (this.isString(input)) { + if (input.trim() === "") return true; + }; + + return false; + }, + + isIdNumber(input) { + return Number.isInteger(input) && input > 0; + }, + + isObject(input) { + return ( + typeof input === "object" && + input !== null && + !Array.isArray(input) + ); + }, + + isArrayOfStrings(input) { + + if (!Array.isArray(input)) return false; + + for (let i = 0; i < input.length; i++) { + if (!this.isString(input[i])) + return false; + }; + + return true; + }, + + isFormData(input) { + return ( + typeof input === "object" && + input !== null && + typeof input.getHeaders === "function" && + typeof input.append === "function" + ); + }, + + /* ============================================================================================== + Return the trimmed string or input + as is if it's not a string +==================================================================================================*/ + trimIfString(input) { + return (typeof input === "string") + ? input.trim() + : input; + }, + + /* ============================================================================================= + Function tries to parse the input as JSON, + If it is not return the value as it was passed +//==============================================================================================*/ + parseIfJSONString(input) { + + if (typeof input === "string") { + try { + return JSON.parse(input); + } catch (error) { + // Parsing failed — return original input + return input; + } + } + + // If input is not a string, just return it as-is + return input; + }, + + /* ============================================================================================= + Validates a URL string: + - Rejects blank strings, spaces, tabs, and newlines + - Warns about suspicious or unusual characters + - Adds a warning if protocol is missing or malformed +================================================================================================ */ + checkIfUrlValid(input) { + + // Warning accumulator + let warnings = []; + + if (!this.isString(input)) { + warnings.push("URL is not a string"); + + }; + + if (this.isEmptyString(input)) { + warnings.push("URL is empty string"); + return warnings; + }; + + const trimmedInput = input.trim(); + + // Reject if spaces, tabs, or newlines are present + if ((/[ \t\n]/.test(trimmedInput))) { + warnings.push( "Url contains invalid characters like space, backslash etc., please check." + + this._reasonMsg(input)); + return warnings; + }; + + // Warn about suspicious characters + const simpleSuspiciousChars = /[\\[\]<>"^`]/g; + const invisibleChars = /\u200B|\u200C|\u200D|\u2060|\uFEFF|\u00A0/g; + + const dubiousMatches = + trimmedInput.match(simpleSuspiciousChars) || + trimmedInput.match(invisibleChars); + + if (dubiousMatches) { + warnings.push(" URL contains dubious or non-standard characters " + this._reasonMsg(input) ); + }; + + // urlObject for further use if the next check passes. + let urlObject; + // Tries to create a new URL object with the input string; + try { + urlObject = new URL(trimmedInput); // throws if invalid or has no protocol + + // Warn if user typed only one slash (e.g., https:/) + if (/^(https?):\/(?!\/)/.test(input)) { + + warnings.push(` It looks like you're missing one slash after "${urlObject.protocol}".` + + `Did you mean "${urlObject.protocol}//..."? ${this._reasonMsg(input)} `); + + }; + + } catch (err) { + // If the URL is invalid, try to create a new URL object with "http://" + // in case user forgot to add it; + try { + // If it works then there was no protocol in the input string; + urlObject = new URL("http://" + trimmedInput); + + warnings.push(" URL does not have http or https protocol \""); + + } catch (err) { + warnings.push(" URL contains potentionally unacceptable characters\"" + + this._reasonMsg(input)); + + }; + + }; + + return warnings; + + }, + + /* =========================================================================================== + Validates a Site Identifier string, which may be either: + - A numeric Site ID (e.g., "123456") + - A custom or subdomain (e.g., "mysite.example.com") +=============================================================================================== */ + + checkDomainOrId(input) { + + const warnings = []; + + // If it's an ID like number or string (e.g 12345 or "12345"); + // it's Valid. Return empty warnings array. + if (this.isIdNumber(Number(input))) return warnings; + + // If it's not a string. + if (!this.isString(input)) { + warnings.push("Provided value is not a domain or ID-like value (e.g., 1234 or '1234')."); + return warnings; + } + + const trimmed = input.trim(); + + // Now treat it as a domain and run checks: + if (/https?:\/\//.test(trimmed)) { + warnings.push("Domain contains protocol (http or https). Remove it." + + this._reasonMsg(input)); + } + + if (/[^a-zA-Z0-9.-]/.test(trimmed)) { + warnings.push("Domain. Only letters, numbers, dots, and dashes are allowed." + + this._reasonMsg(input)); + } + + if (!trimmed.includes(".")) { + warnings.push("Domain should contain at least one dot (e.g. example.com)." + + this._reasonMsg(input)); + } + + return warnings; + }, + + /* ============================================================================================= + Throws if axios request fails. + Determines whether an error originated from your own validation code or from the API request. + Useful for debugging and crafting more helpful error messages. +=================================================================================================*/ + throwCustomError(mainMessage, error, warnings) { + + const thrower = error?.response?.status + ? "API response" + : "Internal Code"; + + throw new Error(` ${mainMessage} ( ${thrower} error ) : ${error.message}. ` + "\n- " + + warnings.join("\n- ")); + }, + + /* ============================================================================================== + Appends a reason string to error messages for additional context. +=============================================================================================== */ + + _reasonMsg(reason) { + + return (reason && typeof reason === "string") + ? ` Reason: ${reason} ` + : ""; + }, + +}; + diff --git a/components/wordpress_com/common/utils.mjs b/components/wordpress_com/common/utils.mjs new file mode 100644 index 0000000000000..6bc6e199972c4 --- /dev/null +++ b/components/wordpress_com/common/utils.mjs @@ -0,0 +1,93 @@ +import { basename } from "path"; +import { get } from "https"; +import FormData from "form-data"; + +// Returns a new object containing only standard prop fields, removing any custom keys. +// Considered using JSON deep cloning, but opted for a manual approach to safely +// preserve functions or complex types in future data structures. +export function removeCustomPropFields(input) { + const blacklist = new Set([ + "extendedType", + "postBody", + ]); + const clean = {}; + + for (const key of Object.keys(input)) { + const prop = input[key]; + const cloned = {}; + + for (const field of Object.keys(prop)) { + if (!blacklist.has(field)) { + cloned[field] = prop[field]; + } + } + + clean[key] = cloned; + } + + return clean; +}; + +/** + * Prepares a multipart/form-data payload for uploading media to WordPress.com. + * Fetches the media file from a given URL, wraps it in a FormData object*/ + +export async function prepareMediaUpload(mediaUrl, fields = {}) { + + const { + title, caption, description, + } = fields; + + // Extract the filename from the URL (e.g., "mypicture.jpg") + const filename = basename(new URL(mediaUrl).pathname || "upload.jpg"); + + // Fetch the media as a stream and it's content type + const { + stream, contentType, + } = await fetchStreamWithHeaders(mediaUrl); + + // WordPress.com expects a multipart/form-data upload + const form = new FormData(); + + // Attach the media file to the form under the field name "media[]" + form.append("media[]", stream, { + filename, + contentType: contentType || "application/octet-stream", + }); + + // Attach optional metadata fields if provided + if (title) form.append("title", title); + if (caption) form.append("caption", caption); + if (description) form.append("description", description); + + return form; +}; + +/** + * Fetches a remote media file as a readable stream, including it's content type. + * Sends a basic GET request while mimicking a browser to avoid blocks from some servers.*/ +function fetchStreamWithHeaders(url) { + return new Promise((resolve, reject) => { + // Send a GET request with a fake browser User-Agent + get( + url, + { + headers: { + // Mimick a browser just in case . Some site refuse to give media to servers directly + "User-Agent": "Mozilla/5.0 (Node.js FormUploader)", + }, + }, + // Callback triggered when the response starts (event-driven) + (result) => { + if (result.statusCode !== 200) { + reject(new Error(`Failed to fetch media. Status: ${result.statusCode}`)); + } else { + resolve({ + stream: result, // The response body is a readable stream + contentType: result.headers["content-type"], // Extract MIME type + }); + } + }, + ).on("error", reject); // Call regect on error. + }); +} diff --git a/components/wordpress_com/package.json b/components/wordpress_com/package.json index 0d41d964ec38f..3615ac0edb227 100644 --- a/components/wordpress_com/package.json +++ b/components/wordpress_com/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/wordpress_com", - "version": "0.6.0", + "version": "0.7.0", "description": "Pipedream wordpress_com Components", "main": "wordpress_com.app.mjs", "keywords": [ @@ -13,6 +13,7 @@ "access": "public" }, "dependencies": { - "@pipedream/platform": "^3.0.0" + "@pipedream/platform": "^3.0.0", + "form-data": "^4.0.2" } } diff --git a/components/wordpress_com/sources/new-comment/new-comment.mjs b/components/wordpress_com/sources/new-comment/new-comment.mjs new file mode 100644 index 0000000000000..2cd1361d55d73 --- /dev/null +++ b/components/wordpress_com/sources/new-comment/new-comment.mjs @@ -0,0 +1,119 @@ +import wordpress from "../../wordpress_com.app.mjs"; + +export default { + key: "wordpress_com-new-comment", + name: "New Comment", + description: "Emit new event for each new comment added since the last run. If no new comments, emit nothing.", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + wordpress, + db: "$.service.db", + site: { + propDefinition: [ + wordpress, + "siteId", + ], + }, + postId: { + propDefinition: [ + wordpress, + "postId", + (c) => ({ + site: c.site, + }), + ], + description: "Enter a specific post ID to fetch comments for only that post. Leave empty to fetch all comments.", + optional: true, + }, + number: { + type: "integer", + label: "Maximum Comments to Fetch", + description: "The number of most recent comments to fetch each time the source runs", + default: 10, + optional: true, + min: 1, + max: 100, + }, + }, + async run({ $ }) { + const warnings = []; + + const { + wordpress, + db, + site, + postId, + number, + } = this; + + warnings.push(...wordpress.checkDomainOrId(site)); + + let response; + try { + response = await wordpress.getWordpressComments({ + $, + site, + postId, + number, + }); + + } catch (error) { + wordpress.throwCustomError("Failed to fetch comments from WordPress:", error, warnings); + } + + const comments = response.comments || []; + const lastCommentId = Number(await db.get("lastCommentId")); + + // First run: Initialize cursor + if (!lastCommentId) { + if (!comments.length) { + console.log("No comments found on first run. Source initialized with no cursor."); + return; + } + + const newest = comments[0]?.ID; + if (!newest) { + throw new Error("Failed to initialize: The latest comment does not have a valid ID."); + } + + await db.set("lastCommentId", newest); + console.log(`Initialized lastCommentId on first run with comment ID ${newest}.`); + return; + } + + let maxCommentIdTracker = lastCommentId; + const newComments = []; + + for (const comment of comments) { + if (Number(comment.ID) > lastCommentId) { + newComments.push(comment); + if (Number(comment.ID) > maxCommentIdTracker) { + maxCommentIdTracker = comment.ID; + } + } + } + + for (const comment of newComments.reverse()) { + this.$emit(comment, { + id: comment.ID, + summary: comment.author?.name || "Anonymous Comment", + ts: comment.date && +new Date(comment.date), + }); + } + + // Update last seen comment ID + if (newComments.length > 0) { + await db.set("lastCommentId", maxCommentIdTracker); + console.log(`Checked for new comments. Emitted ${newComments.length} comment(s).`); + } else { + console.log("No new comments found."); + } + + if (warnings.length > 0) { + console.log("Warnings:\n- " + warnings.join("\n- ")); + }; + }, +}; + diff --git a/components/wordpress_com/sources/new-follower/new-follower.mjs b/components/wordpress_com/sources/new-follower/new-follower.mjs new file mode 100644 index 0000000000000..841bbba2bb24d --- /dev/null +++ b/components/wordpress_com/sources/new-follower/new-follower.mjs @@ -0,0 +1,96 @@ +import wordpress from "../../wordpress_com.app.mjs"; + +export default { + key: "wordpress_com-new-follower", + name: "New Follower", + description: "Emit new event for each new follower that subscribes to the site.", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + wordpress, + db: "$.service.db", + site: { + propDefinition: [ + wordpress, + "siteId", + ], + }, + }, + async run({ $ }) { + const warnings = []; + + const { + wordpress, + db, + site, + } = this; + + warnings.push(...wordpress.checkDomainOrId(site)); + + let response; + try { + response = await wordpress.getWordpressFollowers({ + $, + site, + }); + + } catch (error) { + wordpress.throwCustomError("Failed to fetch followers from WordPress:", error, warnings); + } + + const followers = response.subscribers || []; + + const lastFollowerId = Number(await db.get("lastFollowerId")); + + // First run: Initialize cursor + if (!lastFollowerId) { + if (!followers.length) { + console.log("No followers found on first run. Source initialized with no cursor."); + return; + } + + const newest = followers[0]?.ID; + if (!newest) { + throw new Error("Failed to initialize: The latest follower does not have a valid ID."); + } + + await db.set("lastFollowerId", newest); + console.log(`Initialized lastFollowerId on first run with follower ID ${newest}.`); + return; + } + + let maxFollowerIdTracker = lastFollowerId; + const newFollowers = []; + + for (const follower of followers) { + if (Number(follower.ID) > lastFollowerId) { + newFollowers.push(follower); + if (Number(follower.ID) > maxFollowerIdTracker) { + maxFollowerIdTracker = follower.ID; + } + } + } + + for (const follower of newFollowers) { + this.$emit(follower, { + id: follower.ID, + summary: follower.label || follower.login || "Anonymous Follower", + ts: follower.date_subscribed && +new Date(follower.date_subscribed), + }); + } + + // Update last seen follower ID + if (newFollowers.length > 0) { + await db.set("lastFollowerId", maxFollowerIdTracker); + console.log(`Checked for new followers. Emitted ${newFollowers.length} follower(s).`); + } else { + console.log("No new followers found."); + } + + if (warnings.length > 0) { + console.log("Warnings:\n- " + warnings.join("\n- ")); + } + }, +}; + diff --git a/components/wordpress_com/sources/new-post/new-post.mjs b/components/wordpress_com/sources/new-post/new-post.mjs new file mode 100644 index 0000000000000..4a5de597ac794 --- /dev/null +++ b/components/wordpress_com/sources/new-post/new-post.mjs @@ -0,0 +1,130 @@ +import wordpress from "../../wordpress_com.app.mjs"; + +export default { + key: "wordpress_com-new-post", + name: "New Post", + description: "Emit new event for each new post published since the last run. If no new posts, emit nothing.", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + wordpress, + db: "$.service.db", + site: { + propDefinition: [ + wordpress, + "siteId", + ], + }, + type: { + type: "string", + label: "Post Type", + description: "Select the type of content to fetch", + options: [ + { + label: "Post", + value: "post", + }, + { + label: "Page", + value: "page", + }, + { + label: "Attachment", + value: "attachment", + }, + ], + default: "post", + }, + number: { + type: "integer", + label: "Maximum Posts to Fetch", + description: "The amount of most recent posts to fetch each time the source runs.", + default: 10, + optional: true, + min: 1, + max: 100, + }, + }, + async run({ $ }) { + const warnings = []; + + const { + wordpress, + db, + site, + type, + number, + } = this; + + warnings.push(...wordpress.checkDomainOrId(site)); + + let response; + try { + response = await wordpress.getWordpressPosts({ + $, + site, + type, + number, + }); + + } catch (error) { + wordpress.throwCustomError("Failed to fetch posts from WordPress:", error, warnings); + } + + const posts = (type === "attachment") + ? (response.media || []) + : (response.posts || []); + const lastPostId = Number(await db.get("lastPostId")); + + // First run: Initialize cursor + if (!lastPostId) { + if (!posts.length) { + console.log("No posts found on first run. Source initialized with no cursor."); + return; + } + + const newest = posts[0]?.ID; + if (!newest) { + throw new Error("Failed to initialize: The latest post does not have a valid ID."); + } + + await db.set("lastPostId", newest); + console.log(`Initialized lastPostId on first run with post ID ${newest}.`); + return; + } + + let maxPostIdTracker = lastPostId; + + const newPosts = []; + + for (const post of posts) { + if (Number(post.ID) > lastPostId) { + newPosts.push(post); + if (Number(post.ID) > maxPostIdTracker) { + maxPostIdTracker = post.ID; + } + } + } + + for (const post of newPosts.reverse()) { + this.$emit(post, { + id: post.ID, + summary: post.title, + ts: post.date && +new Date(post.date), + }); + } + + // Update last seen post ID + if (newPosts.length > 0) { + await db.set("lastPostId", maxPostIdTracker); + console.log(`Checked for new posts. Emitted ${newPosts.length} post(s).`); + } else { + console.log("No new posts found."); + } + + if (warnings.length > 0) { + console.log("Warnings:\n- " + warnings.join("\n- ")); + }; + }, +}; diff --git a/components/wordpress_com/wordpress_com.app.mjs b/components/wordpress_com/wordpress_com.app.mjs index d9f004c0453f5..fbfba5a065040 100644 --- a/components/wordpress_com/wordpress_com.app.mjs +++ b/components/wordpress_com/wordpress_com.app.mjs @@ -1,11 +1,159 @@ +import methods from "./common/methods.mjs"; +import { axios } from "@pipedream/platform"; + export default { type: "app", app: "wordpress_com", - propDefinitions: {}, + propDefinitions: { + siteId: { + type: "string", + label: "Site ID or Domain", + description: "Enter a site ID or domain (e.g. testsit38.wordpress.com). Do not include 'https://' or 'www'.", + async options() { + const { sites } = await this.listSites(); + return sites?.map(({ + ID: value, URL: label, + }) => ({ + label, + value, + })) || []; + }, + }, + postId: { + type: "string", + label: "Post ID", + description: "The ID of the post", + async options({ + site, page, + }) { + const { posts } = await this.getWordpressPosts({ + site, + params: { + page: page + 1, + order: "DESC", + order_by: "date", + }, + }); + return posts?.map(({ + ID: value, title: label, + }) => ({ + label, + value, + })) || []; + }, + }, + }, methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); + ...methods, + _baseUrl() { + return "https://public-api.wordpress.com/rest/v1.1"; + }, + _makeRequest({ + $ = this, + path, + contentType, + ...opts + }) { + return axios($, { + url: `${this._baseUrl()}${path}`, + headers: { + "Authorization": `Bearer ${this.$auth.oauth_access_token}`, + "Content-Type": (contentType) + ? contentType + : "application/json", + }, + ...opts, + }); + }, + createWordpressPost({ + site, + ...opts + }) { + return this._makeRequest({ + method: "POST", + path: `/sites/${site}/posts/new`, + ...opts, + }); + }, + uploadWordpressMedia({ + site, + contentType, + ...opts + }) { + return this._makeRequest({ + method: "POST", + path: `/sites/${site}/media/new`, + contentType, + ...opts, + }); + }, + deleteWordpressPost({ + site, + postId, + }) { + return this._makeRequest({ + method: "POST", // use POST instead of DELETE. Wordpress does not allow DELETE methods on free accounts. + path: `/sites/${site}/posts/${postId}/delete`, + }); + }, + getWordpressPosts({ + site, + number, + type, + ...opts + }) { + const path = (type === "attachment") + ? `/sites/${site}/media/` + : `/sites/${site}/posts/`; + + return this._makeRequest({ + method: "GET", + path, + params: { + order_by: "date", + order: "DESC", + type, + number, + }, + ...opts, + }); + }, + getWordpressComments({ + site, + postId, + number, + ...opts + }) { + const path = postId + ? `/sites/${site}/posts/${postId}/replies/` + : `/sites/${site}/comments/`; + + return this._makeRequest({ + method: "GET", + path, + params: { + order_by: "date", + order: "DESC", + number, + }, + ...opts, + }); + }, + getWordpressFollowers({ + site, + ...opts + }) { + return this._makeRequest({ + method: "GET", + path: `/sites/${site}/followers/`, + ...opts, + }); + }, + listSites(opts = {}) { + return this._makeRequest({ + path: "/me/sites", + ...opts, + }); }, }, }; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1a8d77d88c2c2..b88f18f694903 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2864,8 +2864,7 @@ importers: specifier: ^4.17.21 version: 4.17.21 - components/consulta_unica: - specifiers: {} + components/consulta_unica: {} components/contact_enhance: dependencies: @@ -14367,6 +14366,9 @@ importers: '@pipedream/platform': specifier: ^3.0.0 version: 3.0.3 + form-data: + specifier: ^4.0.2 + version: 4.0.2 components/wordpress_org: dependencies: @@ -23032,6 +23034,7 @@ packages: formidable@2.1.2: resolution: {integrity: sha512-CM3GuJ57US06mlpQ47YcunuUZ9jpm8Vx+P2CGt2j7HpgkKZO/DJYQ0Bobim8G6PFQmK5lOqOOdUXboU+h73A4g==} + deprecated: 'ACTION REQUIRED: SWITCH TO v3 - v1 and v2 are VULNERABLE! v1 is DEPRECATED FOR OVER 2 YEARS! Use formidable@latest or try formidable-mini for fresh projects' fp-ts@2.16.9: resolution: {integrity: sha512-+I2+FnVB+tVaxcYyQkHUq7ZdKScaBlX53A41mxQtpIccsfyv8PzdzP7fzp2AY832T4aoK6UZ5WRX/ebGd8uZuQ==} @@ -25781,6 +25784,7 @@ packages: node-domexception@1.0.0: resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} engines: {node: '>=10.5.0'} + deprecated: Use your platform's native DOMException instead node-fetch-h2@2.3.0: resolution: {integrity: sha512-ofRW94Ab0T4AOh5Fk8t0h8OBWrmjb0SSB20xh1H8YnPV9EJ+f5AMoYSUQ2zgJ4Iq2HAK0I2l5/Nequ8YzFS3Hg==}