diff --git a/components/freshchat/actions/fetch-conversation-details/fetch-conversation-details.mjs b/components/freshchat/actions/fetch-conversation-details/fetch-conversation-details.mjs new file mode 100644 index 0000000000000..9e8ad45ffd972 --- /dev/null +++ b/components/freshchat/actions/fetch-conversation-details/fetch-conversation-details.mjs @@ -0,0 +1,34 @@ +import freshchat from "../../freshchat.app.mjs"; + +export default { + key: "freshchat-fetch-conversation-details", + name: "Fetch Conversation Details", + description: "Fetches details for a specific conversation. [See the documentation](https://developers.freshchat.com/api/#retrieve_a_conversation)", + version: "0.0.1", + type: "action", + props: { + freshchat, + userId: { + propDefinition: [ + freshchat, + "userId", + ], + }, + conversationId: { + propDefinition: [ + freshchat, + "conversationId", + (c) => ({ + userId: c.userId, + }), + ], + }, + }, + async run({ $ }) { + const response = await this.freshchat.getConversation({ + conversationId: this.conversationId, + }); + $.export("$summary", `Fetched conversation details for conversation ${this.conversationId}`); + return response; + }, +}; diff --git a/components/freshchat/actions/list-agents/list-agents.mjs b/components/freshchat/actions/list-agents/list-agents.mjs new file mode 100644 index 0000000000000..663e5afd4dc99 --- /dev/null +++ b/components/freshchat/actions/list-agents/list-agents.mjs @@ -0,0 +1,69 @@ +import freshchat from "../../freshchat.app.mjs"; +import { ConfigurationError } from "@pipedream/platform"; + +export default { + key: "freshchat-list-agents", + name: "List Agents", + description: "Lists all agents in Freshchat. [See the documentation](https://developers.freshchat.com/api/#list_all_agents)", + version: "0.0.1", + type: "action", + props: { + freshchat, + groupId: { + propDefinition: [ + freshchat, + "groupId", + ], + optional: true, + }, + isDeactivated: { + type: "boolean", + label: "Is Deactivated", + description: "Limits the response to agent objects whose is_deactivated value matches the parameter value", + optional: true, + }, + availabilityStatus: { + type: "string", + label: "Availability Status", + description: "Limits the response to agent objects whose availability_status value matches the parameter value", + options: [ + "AVAILABLE", + "UNAVAILABLE", + ], + optional: true, + }, + maxResults: { + propDefinition: [ + freshchat, + "maxResults", + ], + }, + }, + async run({ $ }) { + try { + const response = await this.freshchat.getPaginatedResults({ + fn: this.freshchat.listAgents, + args: { + $, + params: { + groups: this.groupId, + is_deactivated: this.isDeactivated, + availability_status: this.availabilityStatus, + }, + }, + resourceKey: "agents", + max: this.maxResults, + }); + $.export("$summary", `Listed ${response.length} agent${response.length === 1 + ? "" + : "s"}`); + return response; + } catch (e) { + if (e.status === 404) { + $.export("$summary", "No agents found"); + } else { + throw new ConfigurationError(e); + } + } + }, +}; diff --git a/components/freshchat/actions/list-channels/list-channels.mjs b/components/freshchat/actions/list-channels/list-channels.mjs new file mode 100644 index 0000000000000..b00a16d1954fe --- /dev/null +++ b/components/freshchat/actions/list-channels/list-channels.mjs @@ -0,0 +1,41 @@ +import freshchat from "../../freshchat.app.mjs"; + +export default { + key: "freshchat-list-channels", + name: "List Channels", + description: "Lists all channels in Freshchat. [See the documentation](https://developers.freshchat.com/api/#channels-(topics))", + version: "0.0.1", + type: "action", + props: { + freshchat, + locale: { + type: "string", + label: "Locale", + description: "Limits the response to topics whose locale value matches the parameter value. Must be specified in the ISO-639 format. E.g. `en`", + optional: true, + }, + maxResults: { + propDefinition: [ + freshchat, + "maxResults", + ], + }, + }, + async run({ $ }) { + const channels = await this.freshchat.getPaginatedResults({ + fn: this.freshchat.listChannels, + resourceKey: "channels", + args: { + $, + params: { + locale: this.locale, + }, + }, + max: this.maxResults, + }); + $.export("$summary", `Listed ${channels.length} channel${channels.length === 1 + ? "" + : "s"}`); + return channels; + }, +}; diff --git a/components/freshchat/actions/send-message-in-chat/send-message-in-chat.mjs b/components/freshchat/actions/send-message-in-chat/send-message-in-chat.mjs new file mode 100644 index 0000000000000..40eb9cfb08e3e --- /dev/null +++ b/components/freshchat/actions/send-message-in-chat/send-message-in-chat.mjs @@ -0,0 +1,64 @@ +import freshchat from "../../freshchat.app.mjs"; + +export default { + key: "freshchat-send-message-in-chat", + name: "Send Message in Chat", + description: "Sends a message in a specific conversation. [See the documentation](https://developers.freshchat.com/api/#send_message_to_conversation)", + version: "0.0.1", + type: "action", + props: { + freshchat, + userId: { + propDefinition: [ + freshchat, + "userId", + ], + }, + conversationId: { + propDefinition: [ + freshchat, + "conversationId", + (c) => ({ + userId: c.userId, + }), + ], + }, + message: { + type: "string", + label: "Message", + description: "The content of the message to send", + }, + }, + async run({ $ }) { + const data = { + message_parts: [ + { + text: { + content: this.message, + }, + }, + ], + actor_type: "user", + actor_id: this.userId, + user_id: this.userId, + }; + try { + await this.freshchat.sendMessageInChat({ + $, + conversationId: this.conversationId, + data, + }); + } catch { + // Sends message, but always returns the error + /* { + "code": 400, + "status": "INVALID_VALUE", + "message": "com.demach.konotor.model.fragment.TextFragment cannot be cast to + com.demach.konotor.model.fragment.EmailFragment" + } + */ + } + $.export("$summary", `Sent message in conversation ${this.conversationId}`); + return data; + }, +}; diff --git a/components/freshchat/actions/update-conversation-status/update-conversation-status.mjs b/components/freshchat/actions/update-conversation-status/update-conversation-status.mjs new file mode 100644 index 0000000000000..dc483e46a6d83 --- /dev/null +++ b/components/freshchat/actions/update-conversation-status/update-conversation-status.mjs @@ -0,0 +1,63 @@ +import freshchat from "../../freshchat.app.mjs"; +import { ConfigurationError } from "@pipedream/platform"; + +export default { + key: "freshchat-update-conversation-status", + name: "Update Conversation Status", + description: "Updates the status of a specific conversation. [See the documentation](https://developers.freshchat.com/api/#update_a_conversation)", + version: "0.0.1", + type: "action", + props: { + freshchat, + userId: { + propDefinition: [ + freshchat, + "userId", + ], + }, + conversationId: { + propDefinition: [ + freshchat, + "conversationId", + (c) => ({ + userId: c.userId, + }), + ], + }, + status: { + type: "string", + label: "Status", + description: "Status of the conversation", + options: [ + "new", + "assigned", + "resolved", + "reopened", + ], + }, + agentId: { + propDefinition: [ + freshchat, + "agentId", + ], + description: "The ID of an agent to assign the conversation to. Required if status is `assigned`", + optional: true, + }, + }, + async run({ $ }) { + if (this.status === "assigned" && !this.agentId) { + throw new ConfigurationError("Agent ID is required when status is `assigned`"); + } + + const response = await this.freshchat.updateConversation({ + $, + conversationId: this.conversationId, + data: { + status: this.status, + assigned_agent_id: this.agentId, + }, + }); + $.export("$summary", `Updated conversation status to ${this.status}`); + return response; + }, +}; diff --git a/components/freshchat/freshchat.app.mjs b/components/freshchat/freshchat.app.mjs index 7cf882fe38d7f..56e4c682458ba 100644 --- a/components/freshchat/freshchat.app.mjs +++ b/components/freshchat/freshchat.app.mjs @@ -1,11 +1,242 @@ +import { axios } from "@pipedream/platform"; +const DEFAULT_LIMIT = 50; + export default { type: "app", app: "freshchat", - propDefinitions: {}, + propDefinitions: { + userId: { + type: "string", + label: "User ID", + description: "The ID of a user", + async options({ page }) { + const { users } = await this.listUsers({ + params: { + page: page + 1, + created_to: new Date().toISOString(), + }, + }); + return users?.map(({ + id: value, email, phone, + }) => ({ + label: email || phone, + value, + })) || []; + }, + }, + conversationId: { + type: "string", + label: "Conversation ID", + description: "The ID of a conversation", + async options({ + page, userId, + }) { + const { conversations } = await this.listConversations({ + userId, + params: { + page: page + 1, + }, + }); + return conversations?.map(({ id }) => id) || []; + }, + }, + groupId: { + type: "string", + label: "Group ID", + description: "The ID of a group", + async options({ page }) { + const { groups } = await this.listGroups({ + params: { + page: page + 1, + }, + }); + return groups?.map(({ + id: value, name: label, + }) => ({ + label, + value, + })) || []; + }, + }, + agentId: { + type: "string", + label: "Agent ID", + description: "The ID of an agent", + async options({ page }) { + const { agents } = await this.listAgents({ + params: { + page: page + 1, + }, + }); + return agents?.map(({ + id: value, email, phone, + }) => ({ + label: email || phone, + value, + })) || []; + }, + }, + actorId: { + type: "string", + label: "Actor ID", + description: "If a user sends the message, the value of this attribute is a valid `user.id`. If an agent sends the message, the value of this attribute is a valid `agent.id`. User must be a member of the conversation.", + async options({ + type, page, + }) { + page = page + 1; + let actors = []; + if (type === "user") { + const { users } = await this.listUsers({ + params: { + page, + created_to: new Date().toISOString(), + }, + }); + actors = users; + } + if (type === "agent") { + const { agents } = await this.listAgents({ + params: { + page, + }, + }); + actors = agents; + } + return actors.map(({ + id: value, email, phone, + }) => ({ + label: email || phone, + value, + })); + }, + }, + maxResults: { + type: "integer", + label: "Max Results", + description: "The maximum number of results to return", + default: 100, + optional: true, + }, + }, methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); + _baseUrl() { + return `https://${this.$auth.chat_url}`; + }, + _makeRequest({ + $ = this, path, ...opts + }) { + return axios($, { + url: `${this._baseUrl()}${path}`, + headers: { + "Authorization": `Bearer ${this.$auth.api_key}`, + "Accept": "application/json", + "Content-Type": "application/json", + }, + ...opts, + }); + }, + getConversation({ + conversationId, ...opts + }) { + return this._makeRequest({ + path: `/conversations/${conversationId}`, + ...opts, + }); + }, + listUsers(opts = {}) { + return this._makeRequest({ + path: "/users", + ...opts, + }); + }, + listConversations({ + userId, ...opts + }) { + return this._makeRequest({ + path: `/users/${userId}/conversations`, + ...opts, + }); + }, + listMessages({ + conversationId, ...opts + }) { + return this._makeRequest({ + path: `/conversations/${conversationId}/messages`, + ...opts, + }); + }, + listAgents(opts = {}) { + return this._makeRequest({ + path: "/agents", + ...opts, + }); + }, + listGroups(opts = {}) { + return this._makeRequest({ + path: "/groups", + ...opts, + }); + }, + listChannels(opts = {}) { + return this._makeRequest({ + path: "/channels", + ...opts, + }); + }, + sendMessageInChat({ + conversationId, ...opts + }) { + return this._makeRequest({ + method: "POST", + path: `/conversations/${conversationId}/messages`, + ...opts, + }); + }, + updateConversation({ + conversationId, ...opts + }) { + return this._makeRequest({ + method: "PUT", + path: `/conversations/${conversationId}`, + ...opts, + }); + }, + async *paginate({ + fn, args, resourceKey, max, + }) { + let hasMore, count = 0; + args = { + ...args, + params: { + ...args?.params, + page: 1, + items_per_page: DEFAULT_LIMIT, + }, + }; + do { + const response = await fn(args); + const items = response[resourceKey]; + if (items.length === 0) { + return; + } + for (const item of items) { + yield item; + if (max && ++count >= max) { + return; + } + } + hasMore = !response.pagination + ? false + : response.pagination.total_pages > args.params.page; + args.params.page++; + } while (hasMore); + }, + async getPaginatedResults(opts) { + const results = []; + for await (const item of this.paginate(opts)) { + results.push(item); + } + return results; }, }, -}; \ No newline at end of file +}; diff --git a/components/freshchat/package.json b/components/freshchat/package.json index 2e21265bcee07..16244b85697a0 100644 --- a/components/freshchat/package.json +++ b/components/freshchat/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/freshchat", - "version": "0.0.1", + "version": "0.1.0", "description": "Pipedream Freshchat Components", "main": "freshchat.app.mjs", "keywords": [ @@ -11,5 +11,8 @@ "author": "Pipedream (https://pipedream.com/)", "publishConfig": { "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.1.0" } -} \ No newline at end of file +} diff --git a/components/freshchat/sources/common/base.mjs b/components/freshchat/sources/common/base.mjs new file mode 100644 index 0000000000000..d35ddffbdeeea --- /dev/null +++ b/components/freshchat/sources/common/base.mjs @@ -0,0 +1,30 @@ +import freshchat from "../../freshchat.app.mjs"; +import { + DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, ConfigurationError, +} from "@pipedream/platform"; + +export default { + props: { + freshchat, + db: "$.service.db", + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + }, + methods: { + async processEvents() { + throw new ConfigurationError("processEvents must be implemented in the source"); + }, + }, + hooks: { + async deploy() { + await this.processEvents(25); + }, + }, + async run() { + await this.processEvents(); + }, +}; diff --git a/components/freshchat/sources/new-conversation-started/new-conversation-started.mjs b/components/freshchat/sources/new-conversation-started/new-conversation-started.mjs new file mode 100644 index 0000000000000..a6a66421de11c --- /dev/null +++ b/components/freshchat/sources/new-conversation-started/new-conversation-started.mjs @@ -0,0 +1,67 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "freshchat-new-conversation-started", + name: "New Conversation Started", + description: "Emit new event when a new conversation is started for a user. [See the documentation](https://developers.freshchat.com/api/#retrieve_all_conversation_for_a_user)", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + ...common.props, + userId: { + propDefinition: [ + common.props.freshchat, + "userId", + ], + }, + }, + methods: { + ...common.methods, + _getPreviousIds() { + return this.db.get("previousIds") || {}; + }, + _setPreviousIds(ids) { + this.db.set("previousIds", ids); + }, + generateMeta(conversation) { + return { + id: conversation.conversation_id, + summary: `New conversation started: ${conversation.conversation_id}`, + ts: Date.parse(conversation.created_time), + }; + }, + async processEvents(max) { + const previousIds = this._getPreviousIds(); + + const conversations = await this.freshchat.getPaginatedResults({ + fn: this.freshchat.listConversations, + args: { + userId: this.userId, + }, + resourceKey: "conversations", + }); + + let newConversations = conversations.filter((conversation) => !previousIds[conversation.id]); + + this._setPreviousIds(conversations.reduce((acc, conversation) => { + acc[conversation.id] = conversation.id; + return acc; + }, {})); + + if (max) { + newConversations = newConversations.slice(0, max); + } + + for (const conversation of newConversations) { + const conversationDetails = await this.freshchat.getConversation({ + conversationId: conversation.id, + }); + this.$emit(conversationDetails, this.generateMeta(conversationDetails)); + } + }, + }, + sampleEmit, +}; diff --git a/components/freshchat/sources/new-conversation-started/test-event.mjs b/components/freshchat/sources/new-conversation-started/test-event.mjs new file mode 100644 index 0000000000000..1818315611d79 --- /dev/null +++ b/components/freshchat/sources/new-conversation-started/test-event.mjs @@ -0,0 +1,16 @@ +export default { + "conversation_id": "7e673110-64fc-461f-bcec-cc8e9a47a568", + "conversation_internal_id": 1019183591791880, + "app_id": "d62390fd-87cd-4921-b52a-982245e0656c", + "status": "new", + "channel_id": "1aaf075f-9c7f-4a71-bf8f-a4e98f47d658", + "skill_id": 0, + "properties": { + "priority": "Low" + }, + "url": "https://pipedream-860999941381430793.myfreshworks.com/crm/messaging/a/1019183040122821/inbox/open/0/conversation/1019183591791880", + "org_contact_id": "1940890452607479808", + "created_time": "2025-07-03T21:31:59.088Z", + "updated_time": "2025-07-10T19:47:55.536Z", + "user_id": "d108c12a-7e7d-474c-94d2-be08551c8400" +} \ No newline at end of file diff --git a/components/freshchat/sources/new-message-received/new-message-received.mjs b/components/freshchat/sources/new-message-received/new-message-received.mjs new file mode 100644 index 0000000000000..ca2af5b2a3ab4 --- /dev/null +++ b/components/freshchat/sources/new-message-received/new-message-received.mjs @@ -0,0 +1,78 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "freshchat-new-message-received", + name: "New Message Received", + description: "Emit new event when a new message is received in a conversation. [See the documentation](https://developers.freshchat.com/api/#list_messages)", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + ...common.props, + userId: { + propDefinition: [ + common.props.freshchat, + "userId", + ], + }, + conversationId: { + propDefinition: [ + common.props.freshchat, + "conversationId", + (c) => ({ + userId: c.userId, + }), + ], + }, + }, + methods: { + ...common.methods, + _getLastCreatedTime() { + return this.db.get("lastCreatedTime"); + }, + _setLastCreatedTime(time) { + this.db.set("lastCreatedTime", time); + }, + generateMeta(message) { + return { + id: message.id, + summary: `New message received: ${message.id}`, + ts: Date.parse(message.created_time), + }; + }, + async processEvents(max) { + const lastCreatedTime = this._getLastCreatedTime(); + let maxCreatedTime = lastCreatedTime; + + const results = this.freshchat.paginate({ + fn: this.freshchat.listMessages, + args: { + conversationId: this.conversationId, + }, + resourceKey: "messages", + }); + + let messages = []; + for await (const message of results) { + if (!lastCreatedTime || Date.parse(message.created_time) > Date.parse(lastCreatedTime)) { + if (!maxCreatedTime || Date.parse(message.created_time) > Date.parse(maxCreatedTime)) { + maxCreatedTime = message.created_time; + } + messages.push(message); + } + } + this._setLastCreatedTime(maxCreatedTime); + + if (max) { + messages = messages.slice(0, max); + } + + for (const message of messages) { + this.$emit(message, this.generateMeta(message)); + } + }, + }, + sampleEmit, +}; diff --git a/components/freshchat/sources/new-message-received/test-event.mjs b/components/freshchat/sources/new-message-received/test-event.mjs new file mode 100644 index 0000000000000..fbc54ca811941 --- /dev/null +++ b/components/freshchat/sources/new-message-received/test-event.mjs @@ -0,0 +1,26 @@ +export default { + "bots_input": false, + "message_parts": [ + { + "text": { + "content": "hello world" + } + } + ], + "app_id": "d62390fd-87cd-4921-b52a-982245e0656c", + "actor_id": "d108c12a-7e7d-474c-94d2-be08551c8400", + "org_actor_id": "1940890452607479808", + "id": "7c6e8673-def2-4894-aef9-3daadda61542", + "channel_id": "1aaf075f-9c7f-4a71-bf8f-a4e98f47d658", + "conversation_id": "7e673110-64fc-461f-bcec-cc8e9a47a568", + "freshchat_conversation_id": "1019183591791880", + "freshchat_channel_id": "854617", + "interaction_id": "1019183591791880-1752164389742", + "message_type": "normal", + "actor_type": "user", + "created_time": "2025-07-10T19:47:55.442Z", + "user_id": "d108c12a-7e7d-474c-94d2-be08551c8400", + "restrictResponse": false, + "botsPrivateNote": false, + "isBotsInput": false +} \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b38fc416d13e0..ee1c70a12c948 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4978,7 +4978,11 @@ importers: components/freshbooks: {} - components/freshchat: {} + components/freshchat: + dependencies: + '@pipedream/platform': + specifier: ^3.1.0 + version: 3.1.0 components/freshdesk: dependencies: @@ -9840,8 +9844,7 @@ importers: components/paypro: {} - components/payrexx: - specifiers: {} + components/payrexx: {} components/paystack: dependencies: @@ -15877,14 +15880,6 @@ importers: specifier: ^6.0.0 version: 6.2.0 - modelcontextprotocol/node_modules2/@modelcontextprotocol/sdk/dist/cjs: {} - - modelcontextprotocol/node_modules2/@modelcontextprotocol/sdk/dist/esm: {} - - modelcontextprotocol/node_modules2/zod-to-json-schema/dist/cjs: {} - - modelcontextprotocol/node_modules2/zod-to-json-schema/dist/esm: {} - packages/ai: dependencies: '@pipedream/sdk':