diff --git a/components/drift/actions/create-contact/create-contact.mjs b/components/drift/actions/create-contact/create-contact.mjs new file mode 100644 index 0000000000000..377210c995d6c --- /dev/null +++ b/components/drift/actions/create-contact/create-contact.mjs @@ -0,0 +1,81 @@ +import drift from "../../drift.app.mjs"; +import { removeNullEntries } from "../../common/utils.mjs"; + +export default { + key: "drift-create-contact", + name: "Create Contact", + description: "Creates a contact in Drift. [See the documentation](https://devdocs.drift.com/docs/creating-a-contact).", + version: "0.0.1", + type: "action", + props: { + drift, + email: { + type: "string", + label: "Email", + description: "The contact's email address", + }, + name: { + type: "string", + label: "Name", + description: "The contact's full name", + optional: true, + }, + phone: { + type: "string", + label: "Phone", + description: "The contact's phone number", + optional: true, + }, + source: { + type: "string", + label: "Lead Source", + description: "The value of the 'lead_create_source' custom attribute to match (case-sensitive).", + optional: true, + }, + customAttributes: { + type: "object", + label: "Custom Attributes", + description: "Additional custom attributes to store on the contact", + optional: true, + }, + }, + + async run({ $ }) { + + const { + drift, email, name, phone, source, + } = this; + + const customAttributes = drift.parseIfJSONString(this.customAttributes); + + const attributes = removeNullEntries({ + email, + name, + phone, + source, + ...customAttributes, + }); + + const existingContact = await drift.getContactByEmail({ + $, + params: { + email, + }, + }); + + if (existingContact && existingContact.data.length > 0) { + throw new Error (`Contact ${email} already exists`); + }; + + const response = await drift.createContact({ + $, + data: { + attributes, + }, + }); + + $.export("$summary", `Contact "${email}" has been created successfully.`); + return response; + }, +}; + diff --git a/components/drift/actions/delete-contact/delete-contact.mjs b/components/drift/actions/delete-contact/delete-contact.mjs new file mode 100644 index 0000000000000..12573957279e5 --- /dev/null +++ b/components/drift/actions/delete-contact/delete-contact.mjs @@ -0,0 +1,41 @@ +import drift from "../../drift.app.mjs"; + +export default { + key: "drift-delete-contact", + name: "Delete Contact", + description: "Deletes a contact in Drift by ID or email. [See the documentation](https://devdocs.drift.com/docs/removing-a-contact).", + version: "0.0.1", + type: "action", + props: { + drift, + emailOrId: { + type: "string", + label: "Email or Id", + description: "The contact's email address or ID", + }, + }, + + async run({ $ }) { + + const { + drift, emailOrId, + } = this; + + let contact = await drift.getContactByEmailOrId($, emailOrId); + contact = contact.data[0] || contact.data; + + const contactId = contact.id; + const contactEmail = contact.attributes.email; + + const response = await drift.deleteContactById({ + $, + contactId, + }); + + $.export("$summary", `Contact "${contactEmail}" ID "${contactId}" + has been deleted successfully.`); + + return response; + }, +}; + diff --git a/components/drift/actions/get-contact/get-contact.mjs b/components/drift/actions/get-contact/get-contact.mjs new file mode 100644 index 0000000000000..c05adf8dfe32a --- /dev/null +++ b/components/drift/actions/get-contact/get-contact.mjs @@ -0,0 +1,37 @@ +import drift from "../../drift.app.mjs"; + +export default { + key: "drift-get-contact", + name: "Get Contact", + description: "Retrieves a contact in Drift by ID or email. [See the documentation](https://devdocs.drift.com/docs/retrieving-contact)", + version: "0.0.1", + type: "action", + props: { + drift, + emailOrId: { + type: "string", + label: "Email or Id", + description: "The contact's email address or ID", + }, + }, + + async run({ $ }) { + + const { + drift, emailOrId, + } = this; + + const response = await drift.getContactByEmailOrId($, emailOrId); + + const contact = response.data[0] || response.data; + + if (!contact) { + throw new Error("Failed to get contact"); + }; + + $.export("$summary", `Contact ${contact.attributes.email} ID "${contact.id}"` + + " has been fetched successfully."); + + return contact; + }, +}; diff --git a/components/drift/actions/update-contact/update-contact.mjs b/components/drift/actions/update-contact/update-contact.mjs new file mode 100644 index 0000000000000..3c3c4bbc75b3d --- /dev/null +++ b/components/drift/actions/update-contact/update-contact.mjs @@ -0,0 +1,85 @@ +import drift from "../../drift.app.mjs"; +import { removeNullEntries } from "../../common/utils.mjs"; + +export default { + key: "drift-update-contact", + name: "Update Contact", + description: "Updates a contact in Drift using ID or email. Only changed attributes will be updated. [See Drift API documentation](https://devdocs.drift.com/docs/updating-a-contact)", + version: "0.0.1", + type: "action", + props: { + drift, + emailOrId: { + type: "string", + label: "Email or ID", + description: "The contact’s email address or numeric ID.", + }, + email: { + type: "string", + label: "Email", + description: "The contact’s email address", + optional: true, + }, + name: { + type: "string", + label: "Name", + description: "The contact’s name.", + optional: true, + }, + phone: { + type: "string", + label: "Phone", + description: "The contact’s phone number.", + optional: true, + }, + source: { + type: "string", + label: "Lead Source", + description: "The value of the 'lead_create_source' custom attribute to match (case-sensitive).", + optional: true, + }, + customAttributes: { + type: "object", + label: "Custom Attributes", + description: "Any custom attributes to update (e.g. company, job title, etc).", + optional: true, + }, + }, + + async run({ $ }) { + const { + drift, name, email, phone, source, emailOrId, + } = this; + + const customAttributes = drift.parseIfJSONString(this.customAttributes); + + const attributes = removeNullEntries({ + name, + phone, + email, + source, + ...customAttributes, + }); + + if (!Object.keys(attributes).length) { + throw new Error("No attributes provided to update."); + }; + + let contact = await drift.getContactByEmailOrId($, emailOrId); + + const contactId = contact.data[0]?.id || contact.data.id; + + const response = await drift.updateContact({ + $, + contactId, + data: { + attributes, + }, + }); + + $.export("$summary", `Contact ID "${contactId}" has been updated successfully.`); + + return response; + }, +}; + diff --git a/components/drift/common/utils.mjs b/components/drift/common/utils.mjs new file mode 100644 index 0000000000000..83f1ee34c1c70 --- /dev/null +++ b/components/drift/common/utils.mjs @@ -0,0 +1,48 @@ +const removeNullEntries = (obj) => + obj && Object.entries(obj).reduce((acc, [ + key, + value, + ]) => { + const isNumber = typeof value === "number"; + const isBoolean = typeof value === "boolean"; + const isNotEmpyString = typeof value === "string" && value.trim() !== ""; + const isNotEmptyArray = Array.isArray(value) && value.length; + const isNotEmptyObject = + typeof value === "object" && + value !== null && + !Array.isArray(value) && + Object.keys(value).length !== 0; + isNotEmptyObject && (value = removeNullEntries(value)); + return ((value || value === false) && + (isNotEmpyString || isNotEmptyArray || isNotEmptyObject || isBoolean || isNumber)) + ? { + ...acc, + [key]: value, + } + : acc; + }, {}); + +function doesContextMatch(inputContext, fetchedContext) { + + if (typeof inputContext !== "object" || inputContext === null || Array.isArray(inputContext)) { + throw new Error ("Message context is not an object"); + }; + + for (const key of Object.keys(inputContext)) { + if (!(key in fetchedContext)) { + console.log(`Invalid context field "${key}", emission skipped` ); + return false; + } + if (fetchedContext[key] !== inputContext[key]) { + console.log(`Context values of "${key}" do not match, emission skipped` ); + return false; + } + } + return true; +}; + +export { + removeNullEntries, + doesContextMatch, +}; + diff --git a/components/drift/drift.app.mjs b/components/drift/drift.app.mjs index 154985335be3c..ffd6ead4c3190 100644 --- a/components/drift/drift.app.mjs +++ b/components/drift/drift.app.mjs @@ -1,11 +1,167 @@ +import { axios } from "@pipedream/platform"; + export default { type: "app", app: "drift", propDefinitions: {}, methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); + _baseUrl() { + return "https://driftapi.com"; + }, + + _makeRequest({ + $ = this, + path, + method = "GET", + contentType, + ...opts + }) { + + return axios($, { + method, + url: `${this._baseUrl()}${path}`, + headers: { + "Authorization": `Bearer ${this.$auth?.oauth_access_token}`, + "Content-Type": contentType || "application/json", + }, + ...opts, + }); + }, + + getNextPage($, url) { + return axios($, { + method: "GET", + url, + headers: { + "Authorization": `Bearer ${this.$auth?.oauth_access_token}`, + }, + }); + }, + + getContactByEmail(opts) { + return this._makeRequest({ + path: "/contacts", + ...opts, + }); + }, + + createContact(opts) { + return this._makeRequest({ + method: "POST", + path: "/contacts", + ...opts, + }); + }, + + updateContact({ + contactId, ...opts + }) { + return this._makeRequest({ + method: "PATCH", + path: `/contacts/${contactId}`, + ...opts, + }); + }, + + getContactById({ + contactId, ...opts + }) { + return this._makeRequest({ + method: "GET", + path: `/contacts/${contactId}`, + ...opts, + }); + }, + + deleteContactById({ + contactId, ...opts + }) { + return this._makeRequest({ + method: "DELETE", + path: `/contacts/${contactId}`, + ...opts, + }); + }, + + async getContactByEmailOrId($, emailOrId) { + + let response; + + if (this.isIdNumber(Number(emailOrId))) { + + const contactId = Number(emailOrId); + + try { + response = await this.getContactById({ + $, + contactId, + }); + } catch (error) { + if (error.status === 404) { + throw new Error(`No contact found with ID: ${contactId}`); + } else { + throw error; + } + } + + } else { + const email = emailOrId; + response = await this.getContactByEmail({ + $, + params: { + email, + }, + }); + if (!response?.data?.length) { + throw new Error(`No contact found with email: ${email}`); + }; + }; + + return response; + }, + + getNewestConversations(arr, lastKnown) { + const firtsNew = arr.indexOf(lastKnown); + if (firtsNew === -1) throw new Error("Id not found"); + const newest = arr.slice(0, firtsNew); + return newest.reverse(); + }, + async getMessagesByConvId($, conversationId) { + + const messages = []; + let next; + + do { + const result = await this._makeRequest({ + $, + path: `/conversations/${conversationId}/messages${next + ? `?next=${next}` + : ""}`, + }); + + messages.push(...result.data.messages); + next = result?.pagination?.next; + + } while (next); + + return messages; + }, + + parseIfJSONString(input) { + + if (typeof input === "string") { + try { + return JSON.parse(input); + } catch (error) { + // Parsing failed — return original input + return input; + } + } + + return input; + }, + isIdNumber(input) { + return Number.isInteger(input) && input > 0; }, }, }; diff --git a/components/drift/package.json b/components/drift/package.json index d3b526a5e573f..94028402e9cfe 100644 --- a/components/drift/package.json +++ b/components/drift/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/drift", - "version": "0.6.0", + "version": "0.7.0", "description": "Pipedream drift Components", "main": "drift.app.mjs", "keywords": [ diff --git a/components/drift/sources/new-conversation/new-conversation.mjs b/components/drift/sources/new-conversation/new-conversation.mjs new file mode 100644 index 0000000000000..682c005945a9b --- /dev/null +++ b/components/drift/sources/new-conversation/new-conversation.mjs @@ -0,0 +1,88 @@ +import drift from "../../drift.app.mjs"; +import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; + +export default { + key: "drift-new-conversation", + name: "New Conversation", + description: "Emit new when a new conversation is started in Drift. [See the documentations](https://devdocs.drift.com/docs/retrieve-a-conversation)", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + drift, + db: "$.service.db", + timer: { + type: "$.interface.timer", + label: "Polling Interval", + description: "How often to poll Drift for new conversations.", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + }, + + async run({ $ }) { + const { + db, drift, + } = this; + + const conversations = []; + + const result = await drift._makeRequest({ + $, + path: "/conversations/list?limit=100&statusId=1", + }); + + if (!result.data.length) { + console.log("No conversations found."); + return; + }; + + const lastConversation = await db.get("lastConversation"); + + if (!lastConversation) { + await db.set("lastConversation", result.data[0].id); + console.log(`Initialized with ID ${result.data[0].id}.`); + return; + }; + + let isEnough = result.data.some((obj) => obj.id === lastConversation); + + conversations.push(...result.data); + + let nextUrl = result.links?.next; + + while (!isEnough && nextUrl) { + const next = await drift.getNextPage($, nextUrl); + isEnough = next.data.some((obj) => obj.id === lastConversation); + conversations.push(...next.data); + nextUrl = next.links?.next; + }; + + conversations.sort((a, b) => a.id - b.id); + + const lastConvIndex = conversations.findIndex((obj) => obj.id === lastConversation); + + if (lastConvIndex === -1) { + throw new Error ("lastConversation not found in fetched data. Skipping emit."); + }; + + if (lastConvIndex + 1 === conversations.length) { + console.log("No new conversations found"); + return; + }; + + for (let i = lastConvIndex + 1; i < conversations.length; i++) { + + this.$emit(conversations[i], { + id: conversations[i].id, + summary: `New conversation with ID ${conversations[i].contactId}`, + ts: conversations[i].createdAt, + }); + + }; + + const lastConvId = conversations[conversations.length - 1].id; + await db.set("lastConversation", lastConvId); + }, +}; diff --git a/components/drift/sources/new-message/new-message.mjs b/components/drift/sources/new-message/new-message.mjs new file mode 100644 index 0000000000000..fe48caa707e61 --- /dev/null +++ b/components/drift/sources/new-message/new-message.mjs @@ -0,0 +1,90 @@ +import drift from "../../drift.app.mjs"; +import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; +import { doesContextMatch } from "../../common/utils.mjs"; + +export default { + key: "drift-new-message", + name: "New Message in Conversation", + description: "Emit new event when a message is received in a specific Drift conversation. [See the documentations](https://devdocs.drift.com/docs/retrieve-a-conversations-messages)", + version: "0.0.1", + type: "source", + dedupe: "unique", + + props: { + drift, + db: "$.service.db", + conversationId: { + type: "integer", + label: "Conversation ID", + description: "Enter the ID of the conversation", + }, + messageContext: { + type: "object", + label: "Message Context", + description: "Enter message context [See the documentation](https://devdocs.drift.com/docs/message-model).", + optional: true, + }, + timer: { + type: "$.interface.timer", + label: "Polling Interval", + description: "How often to poll Drift for new messages.", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + }, + + async run({ $ }) { + const { + drift, + db, + conversationId, + } = this; + + const messageContext = drift.parseIfJSONString(this.messageContext); + + console.log(messageContext); + const messages = await drift.getMessagesByConvId($, conversationId); + + if (!messages?.length) { + console.log("No messages found."); + return; + }; + + let lastMessageId = await db.get("lastMessage"); + + const lastFetchedMsgId = messages[messages.length - 1].id; + + if (!lastMessageId) { + await db.set("lastMessage", lastFetchedMsgId); + console.log(`Initialized with ID ${lastFetchedMsgId}.`); + return; + }; + + if (lastMessageId === lastFetchedMsgId) { + console.log("No new messages found"); + return; + }; + + const lastMessageIndex = messages.findIndex((obj) => obj.id === lastMessageId); + + if (lastMessageIndex === -1) { + console.log("Last message ID not found."); + return; + }; + + for (let i = lastMessageIndex + 1; i < messages.length; i++) { + if (messageContext) { + if (!doesContextMatch(messageContext, messages[i].context)) continue; + } + this.$emit(messages[i], { + id: messages[i].id, + summary: `New message with ID ${messages[i].id}`, + ts: messages[i].createdAt, + }); + }; + + await db.set("lastMessage", lastFetchedMsgId); + + }, +}; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 08f58741e85d2..deb3c3dd1573c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4085,8 +4085,7 @@ importers: specifier: ^1.3.0 version: 1.6.6 - components/emailverify_io: - specifiers: {} + components/emailverify_io: {} components/emelia: {} @@ -34740,6 +34739,8 @@ 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: