diff --git a/components/loopmessage/README.md b/components/loopmessage/README.md index 7cbb4de007d06..a8413bdbc0da1 100644 --- a/components/loopmessage/README.md +++ b/components/loopmessage/README.md @@ -1,9 +1,11 @@ # Overview -The LoopMessage API offers you the ability to send, receive, and manage messages within your applications. Through Pipedream's integration, you can harness this API to automate communication processes, organize message flows, and even connect to various data sources or other APIs to create complex messaging workflows. With Pipedream, you can trigger actions based on events, schedule messages, and interact with users in real-time without managing servers or infrastructure. +LoopMessage is an omnichannel messaging solution that allows you to receive and send messages across multiple channels: iMessage/SMS/RCS/WhatsApp. Through Pipedream's integration, you can harness this platform to automate communication processes, organize message flows, and even connect to various data sources or other APIs to create complex messaging workflows. With Pipedream, you can trigger actions based on events, schedule messages, and interact with users in real-time without managing servers or infrastructure. # Example Use Cases +- **Building AI Assistants**: Create AI-powered chatbots that can respond to user queries, integrate with natural language processing (NLP) services, and provide personalized assistance. + - **Automated Customer Support Tickets**: Automatically create customer support tickets in your helpdesk software when a message is received via LoopMessage. Use this to streamline support queries and ensure no customer message is overlooked. - **Scheduled Notifications**: Send out scheduled notifications to users or groups based on certain triggers or timeframes. This could be used for reminders, promotional campaigns, or important updates, integrating with services like Google Calendar for event-driven alerts. diff --git a/components/loopmessage/actions/check-message-status/check-message-status.mjs b/components/loopmessage/actions/check-message-status/check-message-status.mjs new file mode 100644 index 0000000000000..5c60bd9c3adba --- /dev/null +++ b/components/loopmessage/actions/check-message-status/check-message-status.mjs @@ -0,0 +1,47 @@ +import app from "../../loopmessage.app.mjs"; + +export default { + key: "loopmessage-check-message-status", + name: "Check Message Status", + description: "Action to get the current outbound message status. Possible values: processing, failed, delivered.", + type: "action", + version: "0.0.4", + annotations: { + destructiveHint: false, + openWorldHint: true, + readOnlyHint: true, + }, + props: { + app, + messageId: { + type: "string", + label: "Message ID", + description: "Outbound message ID.", + }, + }, + methods: { + getSummary(response) { + return `Message status: ${response.status ?? "unknown"}`; + }, + }, + async run({ $: step }) { + try { + const response = await this.app.getMessageStatus(this.messageId, { + step, + }); + step.export("$summary", this.getSummary(response)); + + return response; + } catch (error) { + if (error.response?.status === 400) { + const message = + error.response.data?.message ?? + error.response.data?.error_code ?? + JSON.stringify(error.response.data); + + throw new Error(message); + } + throw error; + } + }, +}; diff --git a/components/loopmessage/actions/common/send-message.mjs b/components/loopmessage/actions/common/send-message.mjs deleted file mode 100644 index 0f60a293dbb0f..0000000000000 --- a/components/loopmessage/actions/common/send-message.mjs +++ /dev/null @@ -1,61 +0,0 @@ -import app from "../../loopmessage.app.mjs"; -import utils from "../../common/utils.mjs"; -import { ConfigurationError } from "@pipedream/platform"; - -export default { - props: { - app, - recipient: { - propDefinition: [ - app, - "recipient", - ], - }, - text: { - propDefinition: [ - app, - "text", - ], - }, - senderName: { - optional: true, - propDefinition: [ - app, - "senderName", - ], - }, - statusCallback: { - propDefinition: [ - app, - "statusCallback", - ], - }, - statusCallbackHeader: { - propDefinition: [ - app, - "statusCallbackHeader", - ], - }, - }, - methods: { - getSummary() { - throw new ConfigurationError("The `getSummary` method is not implemented."); - }, - }, - async run({ $: step }) { - const { - app, - getSummary, - ...data - } = this; - - const response = await app.sendMessage({ - step, - data: utils.keysToSnakeCase(data), - }); - - step.export("$summary", getSummary(response)); - - return response; - }, -}; diff --git a/components/loopmessage/actions/send-reaction/send-reaction.mjs b/components/loopmessage/actions/send-reaction/send-reaction.mjs index fbb6f0e41e214..65eeaa7e9b823 100644 --- a/components/loopmessage/actions/send-reaction/send-reaction.mjs +++ b/components/loopmessage/actions/send-reaction/send-reaction.mjs @@ -1,20 +1,26 @@ -import common from "../common/send-message.mjs"; import constants from "../../common/constants.mjs"; +import app from "../../loopmessage.app.mjs"; +import utils from "../../common/utils.mjs"; export default { - ...common, key: "loopmessage-send-reaction", name: "Send Reaction", - description: "Action to submit your request to the sending queue. When a request in the queue will be ready to send a reaction in iMessage, an attempt will be made to deliver it to the recipient. [See the documentation](https://docs.loopmessage.com/imessage-conversation-api/messaging/send-message#send-reaction)", + description: "Action to send a reaction in iMessage or RCS.", type: "action", - version: "0.0.3", + version: "0.0.4", annotations: { destructiveHint: false, openWorldHint: true, readOnlyHint: false, }, props: { - ...common.props, + app, + contact: { + propDefinition: [ + app, + "contact", + ], + }, messageId: { type: "string", label: "Message ID", @@ -23,14 +29,39 @@ export default { reaction: { type: "string", label: "Reaction", - description: "Reactions that starts with `-` mean *remove* it from the message. You can check the [Apple guide](https://support.apple.com/HT206894) about reactions and tapbacks.", + description: "Reactions that starts with `-` mean remove it from the message.", options: constants.REACTIONS, }, }, methods: { - ...common.methods, getSummary(response) { - return `Successfully sent a reaction to with ID \`${response.message_id}\``; + return `Request accepted. Message ID: \`${response.message_id}\``; }, }, + async run({ $: step }) { + const { + app, + ...data + } = this; + + try { + const response = await app.sendReaction({ + step, + data: utils.keysToSnakeCase(data), + }); + step.export("$summary", this.getSummary(response)); + + return response; + } catch (error) { + if (error.response?.status === 400) { + const message = + error.response.data?.message ?? + error.response.data?.error_code ?? + JSON.stringify(error.response.data); + + throw new Error(message); + } + throw error; + } + }, }; diff --git a/components/loopmessage/actions/send-text-message/send-text-message.mjs b/components/loopmessage/actions/send-text-message/send-text-message.mjs index 42ffc9c29b410..69584b774ae01 100644 --- a/components/loopmessage/actions/send-text-message/send-text-message.mjs +++ b/components/loopmessage/actions/send-text-message/send-text-message.mjs @@ -1,42 +1,107 @@ -import common from "../common/send-message.mjs"; +import app from "../../loopmessage.app.mjs"; +import utils from "../../common/utils.mjs"; export default { - ...common, key: "loopmessage-send-text-message", - name: "Send Text Message", - description: "Action to send a text in iMessage to an individual recipient. [See the documentation](https://docs.loopmessage.com/imessage-conversation-api/messaging/send-message#send-single-message)", + name: "Send Outbound Message", + description: "Action to send a message to an individual recipient.", type: "action", - version: "0.0.3", + version: "0.0.4", annotations: { destructiveHint: false, openWorldHint: true, readOnlyHint: false, }, props: { - ...common.props, - service: { + app, + contact: { propDefinition: [ - common.props.app, - "service", + app, + "contact", + ], + }, + text: { + propDefinition: [ + app, + "text", ], }, subject: { propDefinition: [ - common.props.app, + app, "subject", ], }, effect: { propDefinition: [ - common.props.app, + app, "effect", ], }, + sender: { + optional: true, + propDefinition: [ + app, + "sender", + ], + }, + attachments: { + type: "string[]", + label: "Attachments", + description: "Optional. An array of strings. The string must be a full URL of your image. URL should start with https://. HTTP links (without SSL) are not supported. This must be a publicly accessible file URL: we will not be able to reach any URLs that are hidden or that require authentication.", + optional: true, + }, + replyToId: { + optional: true, + propDefinition: [ + app, + "replyToId", + ], + }, + channel: { + optional: true, + propDefinition: [ + app, + "channel", + ], + }, + passthrough: { + optional: true, + propDefinition: [ + app, + "passthrough", + ], + }, }, methods: { - ...common.methods, getSummary(response) { - return `Successfully sent a text message with ID \`${response.message_id}\``; + return `Request accepted. Message ID: \`${response.message_id}\``; }, }, + async run({ $: step }) { + const { + app, + ...data + } = this; + + try { + const response = await app.sendMessage({ + step, + data: utils.keysToSnakeCase(data), + }); + step.export("$summary", this.getSummary(response)); + + return response; + } catch (error) { + if (error.response?.status === 400) { + const message = + error.response.data?.message ?? + error.response.data?.error_code ?? + JSON.stringify(error.response.data); + + throw new Error(message); + } + throw error; + } + }, }; diff --git a/components/loopmessage/actions/send-voice-message/send-voice-message.mjs b/components/loopmessage/actions/send-voice-message/send-voice-message.mjs new file mode 100644 index 0000000000000..e8137ce8b13b6 --- /dev/null +++ b/components/loopmessage/actions/send-voice-message/send-voice-message.mjs @@ -0,0 +1,75 @@ +import app from "../../loopmessage.app.mjs"; +import utils from "../../common/utils.mjs"; + +export default { + key: "loopmessage-send-voice-message", + name: "Send Outbound Voice Message", + description: "Send a voice memo. Supports only in: iMessage, RCS, WhatsApp.", + type: "action", + version: "0.0.1", + annotations: { + destructiveHint: false, + openWorldHint: true, + readOnlyHint: false, + }, + props: { + app, + contact: { + propDefinition: [ + app, + "contact", + ], + }, + sender: { + optional: true, + propDefinition: [ + app, + "sender", + ], + }, + mediaUrl: { + propDefinition: [ + app, + "mediaUrl", + ], + }, + passthrough: { + optional: true, + propDefinition: [ + app, + "passthrough", + ], + }, + }, + methods: { + getSummary(response) { + return `Request accepted. Message ID: \`${response.message_id}\``; + }, + }, + async run({ $: step }) { + const { + app, + ...data + } = this; + + try { + const response = await app.sendMessage({ + step, + data: utils.keysToSnakeCase(data), + }); + step.export("$summary", this.getSummary(response)); + + return response; + } catch (error) { + if (error.response?.status === 400) { + const message = + error.response.data?.message ?? + error.response.data?.error_code ?? + JSON.stringify(error.response.data); + + throw new Error(message); + } + throw error; + } + }, +}; diff --git a/components/loopmessage/actions/typing-indicator/typing-indicator.mjs b/components/loopmessage/actions/typing-indicator/typing-indicator.mjs new file mode 100644 index 0000000000000..f48b18966d209 --- /dev/null +++ b/components/loopmessage/actions/typing-indicator/typing-indicator.mjs @@ -0,0 +1,67 @@ +import app from "../../loopmessage.app.mjs"; +import utils from "../../common/utils.mjs"; + +export default { + key: "loopmessage-typing-indicator", + name: "Show Typing Indicator", + description: "Action to present a typing indicator or read status", + type: "action", + version: "0.0.1", + annotations: { + destructiveHint: false, + openWorldHint: true, + readOnlyHint: false, + }, + props: { + app, + messageId: { + propDefinition: [ + app, + "messageId", + ], + }, + typing: { + propDefinition: [ + app, + "typing", + ], + }, + read: { + propDefinition: [ + app, + "read", + ], + }, + }, + methods: { + getSummary() { + return "Request accepted."; + }, + }, + async run({ $: step }) { + const { + app, + ...data + } = this; + + try { + const response = await app.sendTyping({ + step, + data: utils.keysToSnakeCase(data), + }); + step.export("$summary", this.getSummary(response)); + + return response; + } catch (error) { + if (error.response?.status === 400) { + const message = + error.response.data?.message ?? + error.response.data?.error_code ?? + JSON.stringify(error.response.data); + + throw new Error(message); + } + throw error; + } + }, +}; diff --git a/components/loopmessage/common/constants.mjs b/components/loopmessage/common/constants.mjs index eac7689cec88d..e122d953d1f50 100644 --- a/components/loopmessage/common/constants.mjs +++ b/components/loopmessage/common/constants.mjs @@ -1,18 +1,14 @@ -const API_PLACEHOLDER = "{api}"; -const BASE_URL = `https://${API_PLACEHOLDER}.loopmessage.com`; +const BASE_URL = "https://pipedream-api.loopmessage.com"; const VERSION_PATH = "/api/v1"; const LAST_CREATED_AT = "lastCreatedAt"; const DEFAULT_MAX = 600; const AUTH_HEADER = "authHeader"; -const API = { - SERVER: "server", - LOOKUP: "lookup", -}; - const SERVICES = [ "imessage", "sms", + "whatsapp", + "rcs", ]; const EFFECTS = [ @@ -36,13 +32,13 @@ const REACTIONS = [ "like", "dislike", "laugh", - "exlaim", + "emphasize", "question", "-love", "-like", "-dislike", "-laugh", - "-exlaim", + "-emphasize", "-question", ]; @@ -55,6 +51,4 @@ export default { SERVICES, EFFECTS, REACTIONS, - API, - API_PLACEHOLDER, }; diff --git a/components/loopmessage/common/utils.mjs b/components/loopmessage/common/utils.mjs index fff9c6c3ca81b..a5cd7d0e01b56 100644 --- a/components/loopmessage/common/utils.mjs +++ b/components/loopmessage/common/utils.mjs @@ -1,11 +1,3 @@ -async function streamIterator(stream) { - const resources = []; - for await (const resource of stream) { - resources.push(resource); - } - return resources; -} - function toSnakeCase(str) { return str?.replace(/([A-Z])/g, "_$1").toLowerCase(); } @@ -22,6 +14,5 @@ function keysToSnakeCase(data = {}) { } export default { - streamIterator, keysToSnakeCase, }; diff --git a/components/loopmessage/loopmessage.app.mjs b/components/loopmessage/loopmessage.app.mjs index 8a809c4659bd2..81896f0936d8d 100644 --- a/components/loopmessage/loopmessage.app.mjs +++ b/components/loopmessage/loopmessage.app.mjs @@ -5,139 +5,110 @@ export default { type: "app", app: "loopmessage", propDefinitions: { - recipient: { + contact: { type: "string", - label: "Recipient", - description: "The recipient of the message. This can be a phone number or email address.", + label: "Contact", + description: "Recipient phone number or email address.", }, text: { type: "string", label: "Text", - description: "The text of the message.", + description: "Message text.", }, - senderName: { + subject: { type: "string", - label: "Sender Name", - description: "Your dedicated sender name. This parameter will be ignored if you send a request to a recipient who is added as a Sandbox contact. If you've connected a phone number, you'll need to keep passing your original sender name. DON'T use a phone number as a value for this parameter.", + label: "Subject", + description: "Optional. Message subject. A recipient will see this subject as a bold title before the text. For iMessage only.", + optional: true, }, - statusCallback: { + effect: { type: "string", - label: "Status Callback", - description: "The URL that will receive status updates for this message. Check the [Webhooks](https://docs.loopmessage.com/imessage-conversation-api/messaging/webhooks) section for details. Max length is 256 characters.", + label: "Effect", + description: "Optional. Add effect to your message. For iMessage only.", + options: constants.EFFECTS, optional: true, }, - statusCallbackHeader: { + sender: { type: "string", - label: "Status Callback Header", - description: "The custom Authorization header will be contained in the callback request. Max length is 256 characters.", + label: "Sender", + description: "Optional. Use a specific Sender Name for outbound message.", optional: true, + async options() { + const response = await this.makeRequest({ + method: "get", + path: "/integrations/pipedream/sender-name-list/", + }); + + return response.map((item) => ({ + label: item.label, + value: item.value, + })); + }, }, - service: { + replyToId: { type: "string", - label: "Service", - description: "You can choose wich service to use to deliver the message. Your sender name must have an active SMS feature. SMS does not support `subject`, `effect`, or `reply_to_id` parameters. `attachments` in SMS - only support pictures (MMS).", - options: constants.SERVICES, + label: "Reply To ID", + description: "Optional. Reply to a message with a specific ID", optional: true, }, - subject: { + channel: { type: "string", - label: "Subject", - description: "The subject of the message. A recipient will see this subject as a bold title before the message text.", + label: "Channel", optional: true, + description: "Optional. You can choose which service to use to deliver the message. By default, the required channel will be determined automatically.\nUse this parameter only in cases when you need to override the delivery channel for a specific request. DON'T use it as a default parameter for all requests.", + options: constants.SERVICES, }, - effect: { + mediaUrl: { type: "string", - label: "Effect", - description: "Add effect to your message. You can check the [Apple guide]() about `expressive messages`.", - options: constants.EFFECTS, - optional: true, + label: "Media URL", + description: "Voice/Media file URL. The string must be a full URL of your audio file. URL should start with https://..., http links (without SSL) are not supported. This must be a publicly accessible URL: we will not be able to reach any URLs that are hidden or that require authentication. Max length of each URL: 256 characters. Audio files of the following formats are supported: mp3, wav, m4a, caf, aac.", }, - contacts: { - type: "string[]", - label: "Contacts", - description: "An array of contacts to send the message to. Should contains phone numbers in international formats or email addresses. Example: `[\"+13231112233\", \"steve@mac.com\", \"1(787)111-22-33\"]`. Invalid recipients will be skipped.", + messageId: { + type: "string", + label: "Message ID", + description: "The ID of the message.", }, - region: { + passthrough: { type: "string", - label: "Region", - description: "Value in [ISO-2 country code](https://en.wikipedia.org/wiki/ISO_3166-2). For example: `US`, `GB`, `CA`, `AU` etc. This parameter should be passed only if you passed phone numbers without a country code.", + label: "Passthrough", + description: "Optional. A string of metadata you wish to store in this request. Max length: 1000 characters.", optional: true, }, - alertType: { - type: "string", - label: "Alert Type", - description: "The type of alert received via webhook.", - options: [ - { - label: "Message Scheduled", - value: "message_scheduled", - }, - { - label: "Conversation Initiated", - value: "conversation_inited", - }, - { - label: "Message Failed", - value: "message_failed", - }, - { - label: "Message Sent", - value: "message_sent", - }, - { - label: "Message Inbound", - value: "message_inbound", - }, - { - label: "Message Reaction", - value: "message_reaction", - }, - { - label: "Message Timeout", - value: "message_timeout", - }, - { - label: "Group Created", - value: "group_created", - }, - { - label: "Inbound Call", - value: "inbound_call", - }, - { - label: "Unknown Event", - value: "unknown", - }, - ], + typing: { + type: "integer", + label: "Typing", + description: "Typing duration in seconds.", + default: 3, + }, + read: { + type: "boolean", + label: "Read", + description: "Mark message as read.", + default: true, }, }, methods: { - getBaseUrl(api = constants.API.SERVER) { - const baseUrl = `${constants.BASE_URL}${constants.VERSION_PATH}`; - return baseUrl.replace(constants.API_PLACEHOLDER, api); + getBaseUrl() { + return `${constants.BASE_URL}${constants.VERSION_PATH}`; }, - getUrl(path, api) { - return `${this.getBaseUrl(api)}${path}`; + getUrl(path) { + return `${this.getBaseUrl()}${path}`; }, - getHeaders(headers) { + getHeaders(headers = {}) { return { "Content-Type": "application/json", - "Authorization": `Bearer ${this.$auth.authorization_key}`, - "Loop-Secret-Key": `${this.$auth.secret_api_key}`, + "Authorization": this.$auth.api_key, ...headers, }; }, makeRequest({ - step = this, path, headers, api, ...args + step = this, path, headers, ...args } = {}) { - - const config = { + return axios(step, { headers: this.getHeaders(headers), - url: this.getUrl(path, api), + url: this.getUrl(path), ...args, - }; - - return axios(step, config); + }); }, post(args = {}) { return this.makeRequest({ @@ -145,16 +116,74 @@ export default { ...args, }); }, + delete(args = {}) { + return this.makeRequest({ + method: "delete", + ...args, + }); + }, sendMessage(args = {}) { return this.post({ - path: "/message/send/", + path: "/integrations/pipedream/message/send/", + ...args, + }); + }, + updatePipedreamWebhook(webhookUrl, args = {}) { + return this.post({ + path: "/integrations/pipedream/update-pipedream-webhook/", + data: { + webhook_url: webhookUrl, + }, + ...args, + }).catch((error) => { + if (error.response?.status === 400) { + const data = error.response.data; + const message = + typeof data === "string" + ? data + : data?.message ?? data?.error ?? data?.detail ?? JSON.stringify(data); + throw new Error(message); + } + + throw error; + }); + }, + deactivatePipedreamWebhook(webhookUrl, args = {}) { + return this.delete({ + path: "/integrations/pipedream/update-pipedream-webhook/", + data: { + webhook_url: webhookUrl, + }, + ...args, + }).catch((error) => { + if (error.response?.status === 400) { + const data = error.response.data; + const message = + typeof data === "string" + ? data + : data?.message ?? data?.error ?? data?.detail ?? JSON.stringify(data); + throw new Error(message); + } + + throw error; + }); + }, + sendReaction(args = {}) { + return this.post({ + path: "/integrations/pipedream/reaction/", ...args, }); }, - singleLookup(args = {}) { - return this.app.post({ - api: constants.API.LOOKUP, - path: "/contact/lookup/", + sendTyping(args = {}) { + return this.post({ + path: "/integrations/pipedream/typing/", + ...args, + }); + }, + getMessageStatus(messageId, args = {}) { + return this.makeRequest({ + method: "get", + path: `/integrations/pipedream/message/status/${messageId}/`, ...args, }); }, diff --git a/components/loopmessage/package.json b/components/loopmessage/package.json index ff429bc383824..dcc48479dd136 100644 --- a/components/loopmessage/package.json +++ b/components/loopmessage/package.json @@ -1,11 +1,13 @@ { "name": "@pipedream/loopmessage", - "version": "0.2.0", + "version": "0.3.0", "description": "Pipedream LoopMessage Components", "main": "loopmessage.app.mjs", "keywords": [ "pipedream", - "loopmessage" + "loopmessage", + "imessage", + "blue" ], "homepage": "https://pipedream.com/apps/loopmessage", "author": "Pipedream (https://pipedream.com/)", diff --git a/components/loopmessage/sources/new-alert-received/new-alert-received.mjs b/components/loopmessage/sources/new-alert-received/new-alert-received.mjs deleted file mode 100644 index c93bc0337f722..0000000000000 --- a/components/loopmessage/sources/new-alert-received/new-alert-received.mjs +++ /dev/null @@ -1,35 +0,0 @@ -import app from "../../loopmessage.app.mjs"; - -export default { - key: "loopmessage-new-alert-received", - name: "New Alert Received (Instant)", - description: "Emit new event when an alert is received via webhook. [See the documentation](https://docs.loopmessage.com/imessage-conversation-api/messaging/webhooks)", - type: "source", - version: "0.0.1", - dedupe: "unique", - props: { - app, - db: "$.service.db", - http: { - type: "$.interface.http", - customResponse: true, - }, - alertType: { - propDefinition: [ - app, - "alertType", - ], - }, - }, - async run({ body }) { - this.$emit(body, { - id: body.webhook_id, - summary: `New Alert From ${body.sender_name}`, - ts: Date.parse(body.created_at), - }); - - this.http.respond({ - status: 200, - }); - }, -}; diff --git a/components/loopmessage/sources/new-inbound-message/new-inbound-message.mjs b/components/loopmessage/sources/new-inbound-message/new-inbound-message.mjs new file mode 100644 index 0000000000000..6cdaddd3662de --- /dev/null +++ b/components/loopmessage/sources/new-inbound-message/new-inbound-message.mjs @@ -0,0 +1,40 @@ +import app from "../../loopmessage.app.mjs"; + +export default { + key: "loopmessage-new-inbound-message", + name: "New Inbound Message (Instant)", + description: "Emit new event when an inbound message is received.", + type: "source", + version: "0.0.1", + dedupe: "unique", + props: { + app, + http: { + type: "$.interface.http", + customResponse: true, + }, + }, + hooks: { + async activate() { + await this.app.updatePipedreamWebhook(this.http.endpoint); + }, + async deactivate() { + await this.app.deactivatePipedreamWebhook(this.http.endpoint); + }, + }, + async run({ body }) { + this.$emit(body, { + id: body.webhook_id || body.message_id, + summary: body.contact + ? `New inbound message from ${body.contact}` + : "New inbound received", + ts: body.created_at + ? Date.parse(body.created_at) + : Date.now(), + }); + + this.http.respond({ + status: 200, + }); + }, +};