diff --git a/components/leexi/actions/create-call/create-call.mjs b/components/leexi/actions/create-call/create-call.mjs new file mode 100644 index 0000000000000..e1c68e2119abc --- /dev/null +++ b/components/leexi/actions/create-call/create-call.mjs @@ -0,0 +1,98 @@ +import app from "../../leexi.app.mjs"; + +export default { + key: "leexi-create-call", + name: "Create Call", + description: "Create a new call in Leexi. [See the documentation](https://developer.leexi.ai/)", + version: "0.0.1", + type: "action", + props: { + app, + recordingS3Key: { + type: "string", + label: "Recording S3 Key", + description: "The S3 key returned by the presign_recording_url endpoint.", + }, + externalId: { + type: "string", + label: "External ID", + description: "The ID of the call in your system.", + }, + integrationUserExternalId: { + type: "string", + label: "Integration User External ID", + description: "The external ID of the user making the call on your platform.", + }, + integrationUserName: { + type: "string", + label: "Integration User Name", + description: "The name of the user making the call on your platform.", + }, + direction: { + type: "string", + label: "Call Direction", + description: "The direction of the call (inbound or outbound).", + options: [ + "inbound", + "outbound", + ], + }, + performedAt: { + type: "string", + label: "Performed At", + description: "The start time of the call, in ISO8601 format. Eg. `2024-01-09T17:05:09+01:00`", + }, + description: { + type: "string", + label: "Description", + description: "A description of the call.", + optional: true, + }, + rawPhoneNumber: { + type: "string", + label: "Phone Number", + description: "The phone number of the caller.", + optional: true, + }, + }, + methods: { + createCall(args = {}) { + return this.app.post({ + path: "/calls", + ...args, + }); + }, + }, + async run({ $ }) { + const { + createCall, + recordingS3Key, + externalId, + integrationUserExternalId, + integrationUserName, + direction, + performedAt, + description, + rawPhoneNumber, + } = this; + + const response = await createCall({ + $, + data: { + recording_s3_key: recordingS3Key, + external_id: externalId, + integration_user: { + external_id: integrationUserExternalId, + name: integrationUserName, + }, + direction, + performed_at: performedAt, + description, + raw_phone_number: rawPhoneNumber, + }, + }); + + $.export("$summary", "Successfully created a new call."); + return response; + }, +}; diff --git a/components/leexi/actions/create-presign-recording-url/create-presign-recording-url.mjs b/components/leexi/actions/create-presign-recording-url/create-presign-recording-url.mjs new file mode 100644 index 0000000000000..ba602c8bf3499 --- /dev/null +++ b/components/leexi/actions/create-presign-recording-url/create-presign-recording-url.mjs @@ -0,0 +1,78 @@ +import { readFileSync } from "fs"; +import app from "../../leexi.app.mjs"; + +export default { + key: "leexi-create-presign-recording-url", + name: "Create Presigned Recording URL", + description: "Creates a presigned URL for uploading a call recording. [See the documentation](https://developer.leexi.ai/)", + version: "0.0.1", + type: "action", + props: { + app, + extension: { + propDefinition: [ + app, + "extension", + ], + }, + filePath: { + type: "string", + label: "File Path", + description: "The path to the file to upload. Eg. `/tmp/recording.mp3`", + }, + }, + methods: { + createPresignedRecordingUrl(args = {}) { + return this.app.post({ + path: "/calls/presign_recording_url", + ...args, + }); + }, + uploadFile(args = {}) { + return this.app.put({ + ...args, + }); + }, + }, + async run({ $ }) { + const { + uploadFile, + createPresignedRecordingUrl, + extension, + filePath, + } = this; + + const response = await createPresignedRecordingUrl({ + $, + data: { + extension, + }, + }); + + if (!response.success) { + $.export("$error", "Failed to create a presigned URL for recording."); + return response; + } + + const { + data: { + headers, + url, + }, + } = response; + + const path = filePath?.startsWith("/tmp") + ? filePath + : `/tmp/${filePath}`; + + await uploadFile({ + $, + headers, + url, + data: readFileSync(path), + }); + + $.export("$summary", "Successfully created a presigned URL for recording"); + return response; + }, +}; diff --git a/components/leexi/actions/get-call/get-call.mjs b/components/leexi/actions/get-call/get-call.mjs new file mode 100644 index 0000000000000..3e400da5ddd09 --- /dev/null +++ b/components/leexi/actions/get-call/get-call.mjs @@ -0,0 +1,41 @@ +import app from "../../leexi.app.mjs"; + +export default { + key: "leexi-get-call", + name: "Get Call", + description: "Get details of a call by its ID. [See the documentation](https://developer.leexi.ai/)", + version: "0.0.1", + type: "action", + props: { + app, + callId: { + propDefinition: [ + app, + "callId", + ], + }, + }, + methods: { + getCall({ + callId, ...args + } = {}) { + return this.app._makeRequest({ + path: `/calls/${callId}`, + ...args, + }); + }, + }, + async run({ $ }) { + const { + getCall, + callId, + } = this; + + const response = await getCall({ + $, + callId, + }); + $.export("$summary", `Successfully retrieved details for call ID \`${response.data?.uuid}\`.`); + return response; + }, +}; diff --git a/components/leexi/common/constants.mjs b/components/leexi/common/constants.mjs new file mode 100644 index 0000000000000..4a282e6bceee9 --- /dev/null +++ b/components/leexi/common/constants.mjs @@ -0,0 +1,45 @@ +const BASE_URL = "https://public-api.leexi.ai"; +const VERSION_PATH = "/v1"; +const LAST_CREATED_AT = "lastCreatedAt"; +const DEFAULT_LIMIT = 100; +const DEFAULT_MAX = 300; + +const EXTENSION_OPTIONS = [ + ".mp4", + ".mkv", + ".avi", + ".webm", + ".mov", + ".wmv", + ".mpg", + ".mpeg", + ".mp3", + ".wav", + ".aac", + ".flac", + ".ogg", + ".m4a", + ".wma", + ".opus", + ".aiff", + ".alac", + ".amr", + ".ape", + ".dts", + ".ac3", + ".mid", + ".mp2", + ".mpc", + ".ra", + ".tta", + ".vox", +]; + +export default { + BASE_URL, + VERSION_PATH, + DEFAULT_LIMIT, + DEFAULT_MAX, + LAST_CREATED_AT, + EXTENSION_OPTIONS, +}; diff --git a/components/leexi/common/utils.mjs b/components/leexi/common/utils.mjs new file mode 100644 index 0000000000000..903b2593ed3c2 --- /dev/null +++ b/components/leexi/common/utils.mjs @@ -0,0 +1,11 @@ +async function iterate(iterations) { + const items = []; + for await (const item of iterations) { + items.push(item); + } + return items; +} + +export default { + iterate, +}; diff --git a/components/leexi/leexi.app.mjs b/components/leexi/leexi.app.mjs index 7f056f17c3017..8594bf98bf31e 100644 --- a/components/leexi/leexi.app.mjs +++ b/components/leexi/leexi.app.mjs @@ -1,11 +1,126 @@ +import { axios } from "@pipedream/platform"; +import constants from "./common/constants.mjs"; +import utils from "./common/utils.mjs"; + export default { type: "app", app: "leexi", - propDefinitions: {}, + propDefinitions: { + extension: { + type: "string", + label: "Recording File Extension", + description: "The file extension of the recording.", + options: constants.EXTENSION_OPTIONS, + }, + callId: { + type: "string", + label: "Call ID", + description: "The unique identifier of the call.", + async options({ page }) { + const { data } = await this.listCalls({ + params: { + page, + items: constants.DEFAULT_LIMIT, + }, + }); + return data.map(({ + uuid: value, direction, + }) => ({ + label: `${value} (${direction})`, + value, + })); + }, + }, + }, methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); + getUrl(path) { + return `${constants.BASE_URL}${constants.VERSION_PATH}${path}`; + }, + getAuth() { + const { + api_key_id: username, + api_key_secret: password, + } = this.$auth; + return { + username, + password, + }; + }, + _makeRequest({ + $ = this, url, path, ...args + } = {}) { + return axios($, { + url: url ?? this.getUrl(path), + ...(!url && { + auth: this.getAuth(), + }), + ...args, + }); + }, + post(args = {}) { + return this._makeRequest({ + method: "POST", + ...args, + }); + }, + put(args = {}) { + return this._makeRequest({ + method: "PUT", + ...args, + }); + }, + listCalls(args = {}) { + return this._makeRequest({ + debug: true, + path: "/calls", + ...args, + }); + }, + async *getIterations({ + resourcesFn, resourcesFnArgs, resourceName, + max = constants.DEFAULT_MAX, + }) { + let page = 1; + let resourcesCount = 0; + + while (true) { + const response = + await resourcesFn({ + ...resourcesFnArgs, + params: { + ...resourcesFnArgs?.params, + page, + items: constants.DEFAULT_LIMIT, + }, + }); + + const nextResources = resourceName && response[resourceName] || response; + + if (!nextResources?.length) { + console.log("No more resources found"); + return; + } + + for (const resource of nextResources) { + yield resource; + resourcesCount += 1; + + if (resourcesCount >= max) { + console.log("Reached max resources"); + return; + } + } + + if (nextResources.length < constants.DEFAULT_LIMIT) { + console.log("No next page found"); + return; + } + + page += 1; + } + }, + paginate(args = {}) { + return utils.iterate(this.getIterations(args)); }, }, -}; \ No newline at end of file +}; diff --git a/components/leexi/package.json b/components/leexi/package.json index cfbbd866c0b41..60695e94c299a 100644 --- a/components/leexi/package.json +++ b/components/leexi/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/leexi", - "version": "0.0.1", + "version": "0.1.0", "description": "Pipedream Leexi Components", "main": "leexi.app.mjs", "keywords": [ @@ -11,5 +11,8 @@ "author": "Pipedream (https://pipedream.com/)", "publishConfig": { "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^1.6.5" } -} \ No newline at end of file +} diff --git a/components/leexi/sources/common/polling.mjs b/components/leexi/sources/common/polling.mjs new file mode 100644 index 0000000000000..4a5e07ee18c78 --- /dev/null +++ b/components/leexi/sources/common/polling.mjs @@ -0,0 +1,80 @@ +import { + ConfigurationError, + DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, +} from "@pipedream/platform"; +import app from "../../leexi.app.mjs"; +import constants from "../../common/constants.mjs"; + +export default { + props: { + app, + db: "$.service.db", + timer: { + type: "$.interface.timer", + label: "Polling Schedule", + description: "How often to poll the API", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + }, + methods: { + setLastCreatedAt(value) { + this.db.set(constants.LAST_CREATED_AT, value); + }, + getLastCreatedAt() { + return this.db.get(constants.LAST_CREATED_AT); + }, + generateMeta() { + throw new ConfigurationError("generateMeta is not implemented"); + }, + getResourceName() { + throw new ConfigurationError("getResourceName is not implemented"); + }, + getResourcesFn() { + throw new ConfigurationError("getResourcesFn is not implemented"); + }, + getResourcesFnArgs() { + throw new ConfigurationError("getResourcesFnArgs is not implemented"); + }, + processResource(resource) { + const meta = this.generateMeta(resource); + this.$emit(resource, meta); + }, + async processResources(resources) { + const { + setLastCreatedAt, + processResource, + } = this; + + const [ + lastResource, + ] = resources; + + if (lastResource?.created_at) { + setLastCreatedAt(lastResource.created_at); + } + + Array.from(resources) + .reverse() + .forEach(processResource); + }, + }, + async run() { + const { + app, + getResourcesFn, + getResourcesFnArgs, + getResourceName, + processResources, + } = this; + + const resources = await app.paginate({ + resourcesFn: getResourcesFn(), + resourcesFnArgs: getResourcesFnArgs(), + resourceName: getResourceName(), + }); + + processResources(resources); + }, +}; diff --git a/components/leexi/sources/new-call-created/new-call-created.mjs b/components/leexi/sources/new-call-created/new-call-created.mjs new file mode 100644 index 0000000000000..64f10f28fab6b --- /dev/null +++ b/components/leexi/sources/new-call-created/new-call-created.mjs @@ -0,0 +1,34 @@ +import common from "../common/polling.mjs"; + +export default { + ...common, + key: "leexi-new-call-created", + name: "New Call Created", + description: "Emit new event when a new call is created. [See the documentation](https://developer.leexi.ai/)", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getResourceName() { + return "data"; + }, + getResourcesFn() { + return this.app.listCalls; + }, + getResourcesFnArgs() { + return { + params: { + from: this.getLastCreatedAt(), + }, + }; + }, + generateMeta(resource) { + return { + id: resource.uuid, + summary: `New Call: ${resource.direction}`, + ts: Date.parse(resource.created_at), + }; + }, + }, +}; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8811379252c97..1b1dd7d7460df 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4665,7 +4665,10 @@ importers: specifiers: {} components/leexi: - specifiers: {} + specifiers: + '@pipedream/platform': ^1.6.5 + dependencies: + '@pipedream/platform': 1.6.5 components/leiga: specifiers: