diff --git a/components/azure_storage/azure_storage.app.mjs b/components/azure_storage/azure_storage.app.mjs index 31a2e6f221cff..3e43fbd8b2755 100644 --- a/components/azure_storage/azure_storage.app.mjs +++ b/components/azure_storage/azure_storage.app.mjs @@ -8,4 +8,4 @@ export default { console.log(Object.keys(this.$auth)); }, }, -}; \ No newline at end of file +}; diff --git a/components/elevio/elevio.app.mjs b/components/elevio/elevio.app.mjs index 8d46bd8b141cb..896b2d48698b1 100644 --- a/components/elevio/elevio.app.mjs +++ b/components/elevio/elevio.app.mjs @@ -8,4 +8,4 @@ export default { console.log(Object.keys(this.$auth)); }, }, -}; \ No newline at end of file +}; diff --git a/components/homerun/homerun.app.mjs b/components/homerun/homerun.app.mjs index f4e285ea899ef..a04fd2ae8ebc4 100644 --- a/components/homerun/homerun.app.mjs +++ b/components/homerun/homerun.app.mjs @@ -8,4 +8,4 @@ export default { console.log(Object.keys(this.$auth)); }, }, -}; \ No newline at end of file +}; diff --git a/components/ragie/ragie.app.mjs b/components/ragie/ragie.app.mjs index 6822491f8c965..763d081ee89fe 100644 --- a/components/ragie/ragie.app.mjs +++ b/components/ragie/ragie.app.mjs @@ -8,4 +8,4 @@ export default { console.log(Object.keys(this.$auth)); }, }, -}; \ No newline at end of file +}; diff --git a/components/refiner/refiner.app.mjs b/components/refiner/refiner.app.mjs index 22f2ae3e1ec16..459526d9fd6b1 100644 --- a/components/refiner/refiner.app.mjs +++ b/components/refiner/refiner.app.mjs @@ -8,4 +8,4 @@ export default { console.log(Object.keys(this.$auth)); }, }, -}; \ No newline at end of file +}; diff --git a/components/richpanel/actions/add-ticket-message/add-ticket-message.mjs b/components/richpanel/actions/add-ticket-message/add-ticket-message.mjs new file mode 100644 index 0000000000000..6bd4f210b0906 --- /dev/null +++ b/components/richpanel/actions/add-ticket-message/add-ticket-message.mjs @@ -0,0 +1,46 @@ +import richpanel from "../../richpanel.app.mjs"; + +export default { + key: "richpanel-add-ticket-message", + name: "Add Ticket Message", + description: "Adds a message to an existing ticket. [See the documentation](https://developer.richpanel.com/reference/update-a-conversation)", + version: "0.0.1", + type: "action", + props: { + richpanel, + conversationId: { + propDefinition: [ + richpanel, + "conversationId", + ], + }, + commentBody: { + propDefinition: [ + richpanel, + "commentBody", + ], + }, + commentSenderType: { + propDefinition: [ + richpanel, + "commentSenderType", + ], + }, + }, + async run({ $ }) { + const response = await this.richpanel.updateTicket({ + $, + conversationId: this.conversationId, + data: { + ticket: { + comment: { + body: this.commentBody, + sender_type: this.commentSenderType, + }, + }, + }, + }); + $.export("$summary", `Added message to ticket ${this.conversationId} successfully`); + return response; + }, +}; diff --git a/components/richpanel/actions/create-ticket/create-ticket.mjs b/components/richpanel/actions/create-ticket/create-ticket.mjs new file mode 100644 index 0000000000000..2f4e7b8518036 --- /dev/null +++ b/components/richpanel/actions/create-ticket/create-ticket.mjs @@ -0,0 +1,206 @@ +import { ConfigurationError } from "@pipedream/platform"; +import { VIA_CHANNEL_OPTIONS } from "../../common/constants.mjs"; +import { parseObject } from "../../common/utils.mjs"; +import richpanel from "../../richpanel.app.mjs"; + +export default { + key: "richpanel-create-ticket", + name: "Create Ticket", + description: "Creates a new support ticket in Richpanel. [See the documentation](https://developer.richpanel.com/reference/create-conversation).", + version: "0.0.1", + type: "action", + props: { + richpanel, + id: { + propDefinition: [ + richpanel, + "createId", + ], + optional: true, + }, + status: { + propDefinition: [ + richpanel, + "status", + ], + }, + subject: { + type: "string", + label: "Subject", + description: "The subject of the ticket.", + optional: true, + }, + commentBody: { + propDefinition: [ + richpanel, + "commentBody", + ], + }, + priority: { + type: "string", + label: "Priority", + description: "The priority of the ticket.", + options: [ + "HIGH", + "LOW", + ], + optional: true, + }, + commentSenderType: { + propDefinition: [ + richpanel, + "commentSenderType", + ], + }, + viaChannel: { + type: "string", + label: "Via Channel", + description: "The channel via which the ticket is created.", + reloadProps: true, + options: VIA_CHANNEL_OPTIONS, + }, + viaSourceFromAddress: { + type: "string", + label: "Via Source From Address", + description: "The email address of the source from.", + hidden: true, + }, + viaSourceFromName: { + type: "string", + label: "Via Source From Name", + description: "The name of the source from.", + hidden: true, + }, + viaSourceToAddress: { + type: "string", + label: "Via Source To Address", + description: "The email address of the source to.", + hidden: true, + }, + viaSourceToName: { + type: "string", + label: "Via Source To Name", + description: "The name of the source to.", + hidden: true, + }, + viaSourceFromNumber: { + type: "string", + label: "Via Source From Number", + description: "The phone number of the source from.", + hidden: true, + }, + viaSourceToNumber: { + type: "string", + label: "Via Source To Number", + description: "The phone number of the source to.", + hidden: true, + }, + viaSourceFrom: { + type: "object", + label: "Via Source From", + description: "The object source from which the ticket was created. **Examples: {\"address\": \"abc@email.com\"} or {\"id\": \"+16692668044\"}. It depends on the selected channel**.", + hidden: true, + }, + viaSourceTo: { + type: "object", + label: "Via Source To", + description: "The object source to which the ticket was created. **Examples: {\"address\": \"abc@email.com\"} or {\"id\": \"+16692668044\"}. It depends on the selected channel**.", + hidden: true, + }, + tags: { + propDefinition: [ + richpanel, + "tags", + ], + optional: true, + }, + }, + async additionalProps(props) { + switch (this.viaChannel) { + case "email" : + props.viaSourceFromAddress.hidden = false; + props.viaSourceFromName.hidden = false; + props.viaSourceToAddress.hidden = false; + props.viaSourceToName.hidden = false; + props.viaSourceFrom.hidden = true; + props.viaSourceTo.hidden = true; + props.viaSourceFromNumber.hidden = true; + props.viaSourceToNumber.hidden = true; + break; + case "aircall" : + props.viaSourceFromNumber.hidden = false; + props.viaSourceToNumber.hidden = false; + props.viaSourceFrom.hidden = true; + props.viaSourceTo.hidden = true; + props.viaSourceFromAddress.hidden = true; + props.viaSourceFromName.hidden = true; + props.viaSourceToAddress.hidden = true; + props.viaSourceToName.hidden = true; + break; + default: + props.viaSourceFrom.hidden = false; + props.viaSourceTo.hidden = false; + props.viaSourceFromAddress.hidden = true; + props.viaSourceFromName.hidden = true; + props.viaSourceToAddress.hidden = true; + props.viaSourceToName.hidden = true; + props.viaSourceFromNumber.hidden = true; + props.viaSourceToNumber.hidden = true; + } + return {}; + }, + async run({ $ }) { + try { + const source = {}; + switch (this.viaChannel) { + case "email" : + source.from = { + address: this.viaSourceFromAddress, + name: this.viaSourceFromName, + }; + source.to = { + address: this.viaSourceToAddress, + name: this.viaSourceToName, + }; + break; + case "aircall" : + source.from = { + id: this.viaSourceFromNumber, + }; + source.to = { + id: this.viaSourceToNumber, + }; + break; + default: + source.from = parseObject(this.viaSourceFrom); + source.to = parseObject(this.viaSourceTo); + } + + const response = await this.richpanel.createTicket({ + $, + data: { + ticket: { + id: this.id, + status: this.status, + subject: this.subject, + comment: { + body: this.commentBody, + sender_type: this.commentSenderType, + }, + priority: this.priority, + via: { + channel: this.viaChannel, + source, + }, + tags: parseObject(this.tags), + }, + }, + }); + + $.export("$summary", `Created ticket ${response.ticket.id}`); + return response; + } catch ({ response }) { + throw new ConfigurationError(response?.data?.error?.message); + } + }, +}; diff --git a/components/richpanel/actions/update-ticket-status/update-ticket-status.mjs b/components/richpanel/actions/update-ticket-status/update-ticket-status.mjs new file mode 100644 index 0000000000000..00a7637941308 --- /dev/null +++ b/components/richpanel/actions/update-ticket-status/update-ticket-status.mjs @@ -0,0 +1,37 @@ +import richpanel from "../../richpanel.app.mjs"; + +export default { + key: "richpanel-update-ticket-status", + name: "Update Ticket Status", + description: "Updates the status of an existing ticket in Richpanel. [See the documentation](https://developer.richpanel.com/reference/update-a-conversation).", + version: "0.0.1", + type: "action", + props: { + richpanel, + conversationId: { + propDefinition: [ + richpanel, + "conversationId", + ], + }, + status: { + propDefinition: [ + richpanel, + "status", + ], + }, + }, + async run({ $ }) { + const response = await this.richpanel.updateTicket({ + $, + conversationId: this.conversationId, + data: { + ticket: { + status: this.status, + }, + }, + }); + $.export("$summary", `Updated ticket ${this.conversationId} to status ${this.status}`); + return response; + }, +}; diff --git a/components/richpanel/common/constants.mjs b/components/richpanel/common/constants.mjs new file mode 100644 index 0000000000000..27f1dac482253 --- /dev/null +++ b/components/richpanel/common/constants.mjs @@ -0,0 +1,52 @@ +export const STATUS_OPTIONS = [ + { + label: "Open", + value: "OPEN", + }, + { + label: "Closed", + value: "CLOSED", + }, +]; + +export const COMMENT_SENDER_TYPE_OPTIONS = [ + { + label: "Customer", + value: "customer", + }, + { + label: "Operator", + value: "operator", + }, +]; + +export const VIA_CHANNEL_OPTIONS = [ + { + label: "Email", + value: "email", + }, + { + label: "Messenger", + value: "messenger", + }, + { + label: "Facebook Message", + value: "facebook_message", + }, + { + label: "Instagram", + value: "instagram", + }, + { + label: "Aircall", + value: "aircall", + }, + { + label: "Phone", + value: "phone", + }, + { + label: "WhatsApp", + value: "whatsapp", + }, +]; diff --git a/components/richpanel/common/utils.mjs b/components/richpanel/common/utils.mjs new file mode 100644 index 0000000000000..e7e1c8305652b --- /dev/null +++ b/components/richpanel/common/utils.mjs @@ -0,0 +1,26 @@ +export const parseObject = (obj) => { + if (!obj) return undefined; + + if (Array.isArray(obj)) { + return obj.map((item) => { + if (typeof item === "string") { + try { + return JSON.parse(item); + } catch (e) { + return item; + } + } + return item; + }); + } + if (typeof obj === "string") { + try { + return JSON.parse(obj); + } catch (e) { + return obj; + } + } + return obj; +}; + +export const camelToSnakeCase = (str) => str.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`); diff --git a/components/richpanel/package.json b/components/richpanel/package.json index ac5f1dd576c7f..01290215ca3d2 100644 --- a/components/richpanel/package.json +++ b/components/richpanel/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/richpanel", - "version": "0.0.1", + "version": "0.1.0", "description": "Pipedream Richpanel Components", "main": "richpanel.app.mjs", "keywords": [ @@ -11,5 +11,8 @@ "author": "Pipedream (https://pipedream.com/)", "publishConfig": { "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3" } -} \ No newline at end of file +} diff --git a/components/richpanel/richpanel.app.mjs b/components/richpanel/richpanel.app.mjs index 8b09379770ea3..82811996dc383 100644 --- a/components/richpanel/richpanel.app.mjs +++ b/components/richpanel/richpanel.app.mjs @@ -1,11 +1,148 @@ +import { axios } from "@pipedream/platform"; +import { + COMMENT_SENDER_TYPE_OPTIONS, + STATUS_OPTIONS, +} from "./common/constants.mjs"; + export default { type: "app", app: "richpanel", - propDefinitions: {}, + propDefinitions: { + createId: { + type: "string", + label: "Ticket ID", + description: "The ID of the ticket to create", + }, + status: { + type: "string", + label: "Status", + description: "The status of the ticket", + options: STATUS_OPTIONS, + }, + commentBody: { + type: "string", + label: "Comment Body", + description: "The body of the comment for the ticket", + }, + commentSenderType: { + type: "string", + label: "Comment Sender Type", + description: "The sender type of the comment", + options: COMMENT_SENDER_TYPE_OPTIONS, + }, + tags: { + type: "string[]", + label: "Tags", + description: "Tags associated with the new ticket.", + async options() { + const { tag } = await this.listTags(); + + return tag.map(({ + id: value, name: label, + }) => ({ + label, + value, + })); + }, + }, + conversationId: { + type: "string", + label: "Ticket ID", + description: "ID of the ticket to update", + async options({ page }) { + const { ticket } = await this.listTickets({ + params: { + page: page + 1, + }, + }); + + console.log("ticket: ", ticket); + + return ticket.map(({ + id: value, subject: label, + }) => ({ + label, + value, + })); + }, + }, + }, methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); + _baseUrl() { + return "https://api.richpanel.com/v1"; + }, + _headers() { + return { + "x-richpanel-key": this.$auth.api_key, + "Content-Type": "application/json", + }; + }, + _makeRequest({ + $ = this, path, ...opts + }) { + return axios($, { + url: this._baseUrl() + path, + headers: this._headers(), + ...opts, + }); + }, + createTicket(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/tickets", + ...opts, + }); + }, + listTags() { + return this._makeRequest({ + path: "/tags", + }); + }, + listTickets(opts = {}) { + return this._makeRequest({ + path: "/tickets", + ...opts, + }); + }, + updateTicket({ + conversationId, ...opts + }) { + return this._makeRequest({ + method: "PUT", + path: `/tickets/${conversationId}`, + ...opts, + }); + }, + + async *paginate({ + fn, params = {}, maxResults = null, ...opts + }) { + let hasMore = false; + let count = 0; + let page = 0; + + do { + params.page = ++page; + const { ticket } = await fn({ + params, + ...opts, + }); + + if (!ticket) { + return; + } + + for (const d of ticket) { + yield d; + + if (maxResults && ++count === maxResults) { + return count; + } + } + + hasMore = ticket && ticket.length; + + } while (hasMore); }, }, }; diff --git a/components/richpanel/sources/common/base.mjs b/components/richpanel/sources/common/base.mjs new file mode 100644 index 0000000000000..4a04b05f489a2 --- /dev/null +++ b/components/richpanel/sources/common/base.mjs @@ -0,0 +1,72 @@ +import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; +import { camelToSnakeCase } from "../../common/utils.mjs"; +import richpanel from "../../richpanel.app.mjs"; + +export default { + props: { + richpanel, + db: "$.service.db", + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + }, + methods: { + _getLastDate() { + return this.db.get("lastDate") || "1970-01-01"; + }, + _setLastDate(lastDate) { + this.db.set("lastDate", lastDate); + }, + async prepareData(data) { + const response = []; + for await (const item of data) { + response.push(item); + } + return response; + }, + getParams() { + return {}; + }, + async emitEvent(maxResults = false) { + const lastDate = this._getLastDate(); + const dateField = this.getDateField(); + + const response = this.richpanel.paginate({ + fn: this.richpanel.listTickets, + maxResults, + params: { + ...this.getParams(), + startDate: lastDate, + sortKey: dateField, + sortOrder: "DESC", + }, + }); + + let responseArray = await this.prepareData(response, lastDate); + + if (responseArray.length) { + this._setLastDate(responseArray[0][camelToSnakeCase(dateField)]); + } + + for (const item of responseArray.reverse()) { + const dateVal = item[camelToSnakeCase(dateField)]; + this.$emit(item, { + id: `${item.id}-${dateVal}`, + summary: this.getSummary(item), + ts: Date.parse(dateVal), + }); + } + }, + }, + hooks: { + async deploy() { + await this.emitEvent(25); + }, + }, + async run() { + await this.emitEvent(); + }, +}; diff --git a/components/richpanel/sources/new-message/new-message.mjs b/components/richpanel/sources/new-message/new-message.mjs new file mode 100644 index 0000000000000..92d6a9161f680 --- /dev/null +++ b/components/richpanel/sources/new-message/new-message.mjs @@ -0,0 +1,37 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "richpanel-new-message", + name: "New Message in Ticket", + description: "Emit new event when a new message is sent on an existing or new ticket.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getDateField() { + return "updatedAt"; + }, + getSummary(item) { + return `New message on ticket ${item.id}`; + }, + async prepareData(data, lastDate) { + const response = []; + for await (const item of data) { + for (const message of item.comments) { + if (Date.parse(message.created_at) > Date.parse(lastDate)) { + response.push({ + ...message, + updated_at: message.created_at, + ticket_id: item.id, + }); + } + } + } + return response; + }, + }, + sampleEmit, +}; diff --git a/components/richpanel/sources/new-message/test-event.mjs b/components/richpanel/sources/new-message/test-event.mjs new file mode 100644 index 0000000000000..899cbd81e9a61 --- /dev/null +++ b/components/richpanel/sources/new-message/test-event.mjs @@ -0,0 +1,16 @@ +export default { + "attachments": [], + "author_id": "0bb92ea8-1ea0-4f85-8d7c-90f183b2636c", + "created_at": "2021-04-14T16:22:51.000Z", + "updated_at": "2021-04-14T16:22:51.000Z", + "body": "made by Anushka Goyal", + "id": "453347427_idontknow436_186779", + "metadata": {}, + "plain_body": "made by Anushka Goyal", + "type": "VoiceComment", + "via": { + "channel": "aircall" + }, + "isOperator": true, + "ticket_id": "453347427_idontknow436", +} \ No newline at end of file diff --git a/components/richpanel/sources/new-ticket/new-ticket.mjs b/components/richpanel/sources/new-ticket/new-ticket.mjs new file mode 100644 index 0000000000000..4f353840ef2b1 --- /dev/null +++ b/components/richpanel/sources/new-ticket/new-ticket.mjs @@ -0,0 +1,22 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "richpanel-new-ticket", + name: "New Support Ticket Created", + description: "Emit new event when a support ticket is created.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getDateField() { + return "createdAt"; + }, + getSummary(item) { + return `New Ticket: ${item.subject}`; + }, + }, + sampleEmit, +}; diff --git a/components/richpanel/sources/new-ticket/test-event.mjs b/components/richpanel/sources/new-ticket/test-event.mjs new file mode 100644 index 0000000000000..4c6f009ff8d81 --- /dev/null +++ b/components/richpanel/sources/new-ticket/test-event.mjs @@ -0,0 +1,30 @@ +export default { + "id": "453347427_idontknow436_186779", + "created_at": "2021-04-14T16:22:51.000Z", + "updated_at": "2021-04-14T16:22:51.000Z", + "assignee_id": "0bb92ea8-1ea0-4f85-8d7c-90f183b2636c", + "organization_id": "idontknow436", + "priority": "LOW", + "recipient": "186779", + "status": "OPEN", + "subject": "Outgoing call to +1 844-902-3197", + "via": {}, + "comments": [ + { + "attachments": [], + "author_id": "0bb92ea8-1ea0-4f85-8d7c-90f183b2636c", + "created_at": "2021-04-14T16:22:51.000Z", + "body": "made by Anushka Goyal", + "id": "453347427_idontknow436_186779", + "metadata": {}, + "plain_body": "made by Anushka Goyal", + "type": "VoiceComment", + "via": { + "channel": "aircall" + }, + "isOperator": true + } + ], + "url": "https://app.richpanel.com/453347427_idontknow436_186779", + "tags": [] +} \ No newline at end of file diff --git a/components/typefully/typefully.app.mjs b/components/typefully/typefully.app.mjs index 8d5b67d9a762a..3a90372e0eece 100644 --- a/components/typefully/typefully.app.mjs +++ b/components/typefully/typefully.app.mjs @@ -8,4 +8,4 @@ export default { console.log(Object.keys(this.$auth)); }, }, -}; \ No newline at end of file +}; diff --git a/components/what_are_those/what_are_those.app.mjs b/components/what_are_those/what_are_those.app.mjs index 2a92abacf67a2..4d3e2915f2fd2 100644 --- a/components/what_are_those/what_are_those.app.mjs +++ b/components/what_are_those/what_are_those.app.mjs @@ -8,4 +8,4 @@ export default { console.log(Object.keys(this.$auth)); }, }, -}; \ No newline at end of file +}; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3af469732522d..2950e440f4bf4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1005,8 +1005,7 @@ importers: specifier: ^9.0.1 version: 9.0.1 - components/azure_storage: - specifiers: {} + components/azure_storage: {} components/backblaze: {} @@ -3156,8 +3155,7 @@ importers: specifier: ^4.0.0 version: 4.0.1 - components/elevio: - specifiers: {} + components/elevio: {} components/elmah_io: dependencies: @@ -4820,8 +4818,7 @@ importers: specifier: ^1.6.0 version: 1.6.6 - components/homerun: - specifiers: {} + components/homerun: {} components/hookdeck: dependencies: @@ -8316,8 +8313,7 @@ importers: specifier: ^1.1.1 version: 1.6.6 - components/ragie: - specifiers: {} + components/ragie: {} components/railsr: {} @@ -8510,8 +8506,7 @@ importers: specifier: ^1.3.0 version: 1.6.6 - components/refiner: - specifiers: {} + components/refiner: {} components/reflect: dependencies: @@ -8730,7 +8725,11 @@ importers: specifier: ^1.5.1 version: 1.6.6 - components/richpanel: {} + components/richpanel: + dependencies: + '@pipedream/platform': + specifier: ^3.0.3 + version: 3.0.3 components/ringcentral: dependencies: @@ -11046,8 +11045,7 @@ importers: specifier: ^6.2.13 version: 6.2.13 - components/typefully: - specifiers: {} + components/typefully: {} components/typless: {} @@ -11572,8 +11570,7 @@ importers: components/weworkbook: {} - components/what_are_those: - specifiers: {} + components/what_are_those: {} components/whatconverts: {}