diff --git a/CODEOWNERS b/CODEOWNERS index 0f4e3df646e..16142f986f6 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1,19 +1,20 @@ # Global * @botpress/engineering -# Scripts -/scripts @franklevasseur - # Client, SDK, CLI /packages @botpress/engineering # Bots -/packages/bots @franklevasseur +/packages/bots @botpress/engineering # Plugins /plugins @botpress/engineering +/plugins/hitl @pascal-botpress @franklevasseur @botpress/engineering +/plugins/file-synchronizer @pascal-botpress @botpress/engineering # Integrations +/integrations @botpress/engineering + ## Utils /integrations/browser @allardy @botpress/engineering /integrations/charts @allardy @botpress/engineering @@ -28,37 +29,42 @@ /integrations/openai @AbrahamLopez10 @botpress/engineering ## Others -/integrations/airtable @pascal-botpress @botpress/engineering -/integrations/asana @SebastienPoitras @botpress/engineering -/integrations/bigcommerce-sync @matthewbotpress @botpress/engineering +/integrations/airtable @JustinBordage @botpress/engineering +/integrations/asana @JustinBordage @botpress/engineering +/integrations/bigcommerce-sync @Nathaniel-Girard @botpress/engineering /integrations/chat @franklevasseur @botpress/engineering -/integrations/clickup @franklevasseur @botpress/engineering +/integrations/clickup @JustinBordage @botpress/engineering +/integrations/confluence @Nathaniel-Girard @botpress/engineering /integrations/dropbox @pascal-botpress @botpress/engineering -/integrations/freshchat @SebastienPoitras @botpress/engineering -/integrations/github @pascal-botpress @botpress/engineering +/integrations/email @Nathaniel-Girard @botpress/engineering +/integrations/freshchat @davidvitora @botpress/engineering +/integrations/github @JustinBordage @pascal-botpress @botpress/engineering /integrations/gmail @pascal-botpress @botpress/engineering /integrations/googlecalendar @pascal-botpress @botpress/engineering /integrations/googledrive @pascal-botpress @botpress/engineering /integrations/gsheets @pascal-botpress @botpress/engineering /integrations/instagram @SebastienPoitras @botpress/engineering -/integrations/intercom @SebastienPoitras @botpress/engineering -/integrations/line @SebastienPoitras @botpress/engineering +/integrations/intercom @Nathaniel-Girard @botpress/engineering +/integrations/line @Nathaniel-Girard @botpress/engineering /integrations/linear @franklevasseur @botpress/engineering -/integrations/mailchimp @SebastienPoitras @botpress/engineering +/integrations/mailchimp @JustinBordage @botpress/engineering /integrations/messenger @SebastienPoitras @botpress/engineering +/integrations/monday @JustinBordage @botpress/engineering /integrations/notion @pascal-botpress @botpress/engineering +/integrations/resend @JustinBordage @botpress/engineering +/integrations/sendgrid @JustinBordage @botpress/engineering /integrations/slack @pascal-botpress @botpress/engineering -/integrations/stripe @SebastienPoitras @botpress/engineering +/integrations/stripe @Nathaniel-Girard @botpress/engineering /integrations/sunco @SebastienPoitras @botpress/engineering -/integrations/teams @franklevasseur @botpress/engineering -/integrations/telegram @franklevasseur @botpress/engineering -/integrations/todoist @SebastienPoitras @botpress/engineering -/integrations/trello @pascal-botpress @botpress/engineering +/integrations/teams @JustinBordage @davidvitora @botpress/engineering +/integrations/telegram @franklevasseur @JustinBordage @botpress/engineering +/integrations/todoist @JustinBordage @botpress/engineering +/integrations/trello @JustinBordage @botpress/engineering /integrations/twilio @SebastienPoitras @botpress/engineering -/integrations/viber @SebastienPoitras @botpress/engineering -/integrations/vonage @SebastienPoitras @botpress/engineering +/integrations/viber @Nathaniel-Girard @botpress/engineering +/integrations/vonage @Nathaniel-Girard @botpress/engineering /integrations/webhook @franklevasseur @botpress/engineering /integrations/whatsapp @SebastienPoitras @botpress/engineering -/integrations/zapier @SebastienPoitras @botpress/engineering -/integrations/zendesk @franklevasseur @botpress/engineering -/integrations/zoho @matthewbotpress @botpress/engineering \ No newline at end of file +/integrations/zapier @Nathaniel-Girard @botpress/engineering +/integrations/zendesk @pascal-botpress @franklevasseur @botpress/engineering +/integrations/zoho @Nathaniel-Girard @botpress/engineering diff --git a/bots/hit-looper/bot.definition.ts b/bots/hit-looper/bot.definition.ts index 929190bbd42..93e8527fc6e 100644 --- a/bots/hit-looper/bot.definition.ts +++ b/bots/hit-looper/bot.definition.ts @@ -5,7 +5,7 @@ import chat from './bp_modules/chat' import hitl from './bp_modules/hitl' import zendesk from './bp_modules/zendesk' -const zendeskHitl = zendesk.definition.interfaces['hitl'] +const zendeskHitl = zendesk.definition.interfaces['hitl'] export default new sdk.BotDefinition({ configuration: { diff --git a/bots/hit-looper/src/index.ts b/bots/hit-looper/src/index.ts index 6fc86a3e50e..067543d3d78 100644 --- a/bots/hit-looper/src/index.ts +++ b/bots/hit-looper/src/index.ts @@ -40,6 +40,9 @@ bot.on.message('*', async (props) => { const { conversation: upstreamConversation, user: upstreamUser } = props + const _randFrom = (...values: TValueType[]): TValueType => + values[Math.floor(Math.random() * values.length)]! + if (props.message.type === 'text' && props.message.payload.text.trim() === '/start_hitl') { await props.client.updateUser({ id: upstreamUser.id, @@ -55,6 +58,7 @@ bot.on.message('*', async (props) => { input: { title: `Hitl request ${Date.now()}`, description: 'I have a problem', + hitlSession: { priority: _randFrom('low', 'high', 'urgent') }, conversationId: upstreamConversation.id, userId: upstreamUser.id, }, diff --git a/integrations/email/hub.md b/integrations/email/hub.md new file mode 100644 index 00000000000..13cd5a983ed --- /dev/null +++ b/integrations/email/hub.md @@ -0,0 +1,18 @@ +# Email integration + +## Description + +This integration provides Internet Messaging Access Protocol (IMAP) and Simple Messaging Transport Protocol (SMTP) actions to read and send email messages. +The integration **does not** support HTML content yet. + +## Getting started + +### Configuration + +The configuration contains three required fields. Here is an example of config + +```yml +user: yourEmailAccount@gmail.com +password: yourAccountPassword +host: imap.gmail.com #for gmail +``` diff --git a/integrations/email/icon.svg b/integrations/email/icon.svg new file mode 100644 index 00000000000..2716815c17c --- /dev/null +++ b/integrations/email/icon.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/integrations/email/integration.definition.ts b/integrations/email/integration.definition.ts new file mode 100644 index 00000000000..c20e9085c88 --- /dev/null +++ b/integrations/email/integration.definition.ts @@ -0,0 +1,126 @@ +import { z, IntegrationDefinition, messages } from '@botpress/sdk' + +const emailSchema = z.object({ + id: z.string(), + subject: z.string(), + inReplyTo: z.string().optional(), + date: z.string().optional().describe('ISO datetime'), + sender: z.string(), + firstMessageId: z.string().optional(), +}) + +export default new IntegrationDefinition({ + name: 'email', + version: '0.0.1', + readme: 'hub.md', + icon: 'icon.svg', + configuration: { + schema: z + .object({ + user: z + .string() + .describe('The email account you want to use to receive and send messages. Example: example@gmail.com'), + password: z.string().describe('The password to the email account.'), + imapHost: z.string().describe('The imap server you want to connect to. Example: imap.gmail.com'), + smtpHost: z.string().describe('The smtp server you want to connect to. Example: smtp.gmail.com'), + }) + .required(), + }, + states: { + lastSyncTimestamp: { + type: 'integration', + schema: z.object({ + lastSyncTimestamp: z.string().datetime(), + }), + }, + syncLock: { + type: 'integration', + schema: z.object({ currentlySyncing: z.boolean().default(false) }), + }, + }, + actions: { + listEmails: { + title: 'List emails', + description: 'List all emails in the inbox', + input: { + schema: z.object({ + nextToken: z.string().optional().describe('The page number in the inbox. Starts at 0').optional(), + }), + }, + output: { + schema: z.object({ + messages: z.array(emailSchema), + nextToken: z.string().optional(), + }), + }, + }, + getEmail: { + title: 'Get emails', + description: 'Get the email with specified id from the inbox', + input: { + schema: z.object({ + id: z.string(), + }), + }, + output: { + schema: emailSchema.extend({ + body: z.string().optional(), + }), + }, + }, + syncEmails: { + title: 'Sync Emails', + description: 'Sends unseen emails as new messages. Call periodically to allow your bot to receive new emails.', + input: { schema: z.object({}) }, + output: { + schema: z.object({}), + }, + }, + sendEmail: { + title: 'Send Email', + description: 'Send an email using SMTP', + input: { + schema: z.object({ + to: z.string().describe('The email address of the recipient'), + subject: z.string().optional().describe('The subject of the outgoing email'), + text: z.string().optional().describe('The text contained in the body of the email'), + inReplyTo: z.string().optional().describe('The id of the email you want to reply to'), + replyTo: z + .string() + .optional() + .describe( + 'The email address to which replies should be sent. This allows recipients to reply to a different address than the sender' + ), + }), + }, + output: { + schema: z.object({}), + }, + }, + }, + channels: { + default: { + message: { tags: { id: { title: 'Email id', description: 'The email id ' } } }, + messages: { text: messages.defaults.text }, + conversation: { + tags: { + firstMessageId: { + title: 'First Message Id', + description: 'The id (from the IMAP server) of the first incoming message of the conversation', + }, + subject: { title: 'Thread Subject', description: 'Subject for the conversation' }, + to: { title: 'Recipient', description: 'Recipient email address for the conversation' }, + latestEmail: { + title: 'Email id', + description: 'The id of the latest email received (to put in the In-Reply-To header)', + }, + }, + }, + }, + }, + user: { + tags: { + email: { title: 'User email', description: 'Required' }, + }, + }, +}) diff --git a/integrations/email/package.json b/integrations/email/package.json new file mode 100644 index 00000000000..69ac528429b --- /dev/null +++ b/integrations/email/package.json @@ -0,0 +1,19 @@ +{ + "name": "@botpresshub/email", + "scripts": { + "check:type": "tsc --noEmit", + "build": "bp build", + "test": "vitest" + }, + "private": true, + "dependencies": { + "@botpress/client": "workspace:*", + "@botpress/sdk": "workspace:*", + "imap": "^0.8.17", + "nodemailer": "^6.7.2" + }, + "devDependencies": { + "@types/imap": "^0.8.42", + "@types/nodemailer": "^6.4.4" + } +} diff --git a/integrations/email/src/actions.ts b/integrations/email/src/actions.ts new file mode 100644 index 00000000000..744b6b3def5 --- /dev/null +++ b/integrations/email/src/actions.ts @@ -0,0 +1,131 @@ +import * as sdk from '@botpress/sdk' +import * as imap from './imap' +import * as locking from './locking' +import * as smtp from './smtp' +import * as bp from '.botpress' + +const DEFAULT_START_PAGE = 0 +const ELEMENTS_PER_PAGE = 50 + +export const sendEmail: bp.IntegrationProps['actions']['sendEmail'] = async (props) => { + return await smtp.sendNodemailerMail(props.ctx.configuration, props.input, props.logger) +} + +export const listEmails: bp.IntegrationProps['actions']['listEmails'] = async (props) => { + const page = parseInt(props.input.nextToken ?? DEFAULT_START_PAGE.toString()) + if (page < 0) { + throw new sdk.RuntimeError('The nextToken value cannot be negative') + } + const perPage = ELEMENTS_PER_PAGE + const messages = await imap.getMessages( + { page, perPage }, + { + ctx: props.ctx, + logger: props.logger, + }, + { bodyNeeded: false } + ) + return messages +} + +export const getEmail: bp.IntegrationProps['actions']['getEmail'] = async (props) => { + const email = await imap.getMessageById(props.input.id, props) + if (!email) throw new sdk.RuntimeError('Could not find an email with corresponding id.') + + props.logger.info(`Retrieved email with id ${props.input.id}`) + return email +} + +export const syncEmails: bp.IntegrationProps['actions']['syncEmails'] = async (props) => { + props.logger.forBot().info(`Starting sync in the inbox at [${new Date().toISOString()}]`) + await _syncEmails(props, { enableNewMessageNotification: true }) + props.logger.forBot().info(`Finished sync in the inbox at [${new Date().toISOString()}]`) + + return {} +} + +const _syncEmails = async ( + props: { ctx: bp.Context; client: bp.Client; logger: bp.Logger }, + options: { enableNewMessageNotification: boolean } +) => { + const lock = new locking.LockHandler({ client: props.client, ctx: props.ctx }) + + const currentlySyncing = await lock.readLock() + if (currentlySyncing) throw new sdk.RuntimeError('The bot is still syncing the messages. Try again later.') + await lock.setLock(true) + + const { + state: { payload: lastSyncTimestamp }, + } = await props.client.getState({ + name: 'lastSyncTimestamp', + id: props.ctx.integrationId, + type: 'integration', + }) + + const allMessages = await imap.getMessages( + { page: DEFAULT_START_PAGE, perPage: ELEMENTS_PER_PAGE }, + { + ctx: props.ctx, + logger: props.logger, + }, + { bodyNeeded: options.enableNewMessageNotification } + ) + + for (const message of allMessages.messages) { + if (message.sender === props.ctx.configuration.user) continue + + const messageAlreadySeen = + message.date && lastSyncTimestamp && new Date(message.date) <= new Date(lastSyncTimestamp.lastSyncTimestamp) + if (messageAlreadySeen) continue + + if (options.enableNewMessageNotification) { + props.logger.forBot().info(`Detecting a new email from '${message.sender}': ${message.subject}`) + await _notifyNewMessage(props, message) + } + } + + await props.client.setState({ + name: 'lastSyncTimestamp', + id: props.ctx.integrationId, + type: 'integration', + payload: { + lastSyncTimestamp: new Date().toISOString(), + }, + }) + + await lock.setLock(false) + + return {} +} + +const _notifyNewMessage = async (props: { client: bp.Client; logger: bp.Logger }, message: imap.Email) => { + const { user } = await props.client.getOrCreateUser({ + tags: { email: message.sender }, + }) + + const firstMessageId = message.firstMessageId || message.id + + const { conversation } = await props.client.getOrCreateConversation({ + channel: 'default', + tags: { + firstMessageId, + subject: message.subject, + to: user.tags.email, + latestEmail: message.id, + }, + discriminateByTags: ['firstMessageId'], + }) + props.logger + .forBot() + .info( + `Retrieved or created conversation with id '${conversation.tags.firstMessageId}' and subject '${conversation.tags.subject}'.` + ) + + await props.client.createMessage({ + conversationId: conversation.id, + userId: user.id, + payload: { text: message.body ?? '' }, + tags: { id: message.id }, + type: 'text', + }) +} diff --git a/integrations/email/src/channels.ts b/integrations/email/src/channels.ts new file mode 100644 index 00000000000..aeb1a849a72 --- /dev/null +++ b/integrations/email/src/channels.ts @@ -0,0 +1,23 @@ +import { RuntimeError } from '@botpress/sdk' +import * as smtp from './smtp' +import * as bp from '.botpress' + +export const defaultChannel = { + messages: { + text: async (props) => { + if (!props.conversation.tags.to) throw new RuntimeError("Tried sending an email without a 'to' header") + + await smtp.sendNodemailerMail( + props.ctx.configuration, + { + to: props.conversation.tags.to, + subject: 'Sent from botpress email integration', + text: props.payload.text, + inReplyTo: props.conversation.tags.latestEmail, + replyTo: props.ctx.configuration.user, + }, + props.logger + ) + }, + }, +} satisfies bp.IntegrationProps['channels']['default'] diff --git a/integrations/email/src/imap.ts b/integrations/email/src/imap.ts new file mode 100644 index 00000000000..8f787271fa3 --- /dev/null +++ b/integrations/email/src/imap.ts @@ -0,0 +1,249 @@ +import * as sdk from '@botpress/sdk' +import Imap from 'imap' +import * as paging from './paging' +import * as bp from '.botpress' + +type HeaderData = { + id: string + subject: string + inReplyTo: string | undefined + date: string | undefined + sender: string + firstMessageId: string | undefined +} + +const INBOX_ERROR_MESSAGE = 'An error occured while opening the inbox' + +export type Email = bp.actions.listEmails.output.Output['messages'][0] & { body?: string } +export type EmailResponse = { messages: Array } & { nextToken?: string } + +const _connectToImap = async (props: { + ctx: bp.Context + logger: bp.Logger +}): Promise<{ imap: Imap; box: Imap.Box }> => { + const imap: Imap = new Imap(_getConfig(props.ctx.configuration)) + + await new Promise((resolve, reject) => { + imap.once('ready', resolve) + imap.once('error', (err: Error) => { + reject(new sdk.RuntimeError('An error occured while connecting to the inbox', err)) + }) + imap.connect() + }) + + return { + imap, + box: await new Promise((resolve, reject) => { + imap.openBox('INBOX', true, (err, box) => { + if (err) { + reject(new sdk.RuntimeError(INBOX_ERROR_MESSAGE, err)) + } else { + resolve(box) + } + }) + }), + } +} + +export const getMessages = async function ( + range: { page: number; perPage: number }, + props: { ctx: bp.Context; logger: bp.Logger }, + options?: { bodyNeeded: boolean } +): Promise { + let messages: EmailResponse + let imap: Imap | undefined = undefined + let box: Imap.Box | undefined = undefined + + try { + ;({ imap, box } = await _connectToImap(props)) + if (!imap || !box) throw new sdk.RuntimeError(INBOX_ERROR_MESSAGE) + + const imapBodies = ['HEADER'] + if (options?.bodyNeeded || !options) { + imapBodies.push('TEXT') + } + + const { firstElementIndex, lastElementIndex } = paging.pageToSpan({ + page: range.page, + perPage: range.perPage, + totalElements: box.messages.total, + }) + + const imapRange = `${firstElementIndex}:${lastElementIndex}` + const f: Imap.ImapFetch = imap.seq.fetch(imapRange, { + bodies: imapBodies, + struct: true, + }) + const nextToken = paging.getNextToken({ page: range.page, firstElementIndex })?.toString() + + messages = { messages: await _handleFetch(imap, f, imapBodies.length), nextToken } + } catch (thrown: unknown) { + if (imap?.state !== 'disconnected') { + imap?.end() + } + + const err = thrown instanceof Error ? thrown : new Error(String(thrown)) + throw new sdk.RuntimeError( + 'An error occured while opening the inbox or fetching messages. Verify the integration configuration parameters.', + err + ) + } + + props.logger.forBot().info(`Read ${messages.messages.length} messages from the inbox`) + return messages +} + +const _handleFetch = function (imap: Imap, f: Imap.ImapFetch, totalParts: number): Promise> { + const messages: Array = [] + return new Promise((resolve, reject) => { + f.on('message', (msg: Imap.ImapMessage) => { + let headerData: HeaderData + let body: string + + msg.on('body', (stream: NodeJS.ReadableStream, info) => { + let buffer = '' + stream.on('data', function (chunk) { + buffer += chunk.toString('utf8') + }) + stream.once('end', function () { + if (info.which === 'HEADER') { + headerData = _parseHeader(buffer) + } else if (info.which === 'TEXT') { + body = buffer + } + }) + }) + + let partsProcessed = 0 + + msg.on('body', (stream: NodeJS.ReadableStream) => { + stream.once('end', () => { + partsProcessed++ + if (partsProcessed === totalParts) { + // All parts for this message have been processed + messages.push({ + ...headerData, + body, + }) + } + }) + }) + }) + + f.once('error', (err) => { + reject(new sdk.RuntimeError('An error occured while fetching messages', err)) + }) + + f.once('end', function () { + imap.end() + resolve(messages) + }) + }) +} + +export const getMessageById = async function ( + messageId: string, + props: { ctx: bp.Context; logger: bp.Logger }, + options?: { bodyNeeded: boolean } +): Promise { + let imap: Imap | undefined = undefined + + try { + ;({ imap } = await _connectToImap(props)) + if (!imap) throw new sdk.RuntimeError('') + + const imapBodies = ['HEADER'] + if (options?.bodyNeeded || !options) { + imapBodies.push('TEXT') + } + + // Search for the message by message-id + const searchCriteria = [['HEADER', 'MESSAGE-ID', messageId]] + const uids: number[] = await new Promise((resolve, reject) => { + if (!imap) throw new sdk.RuntimeError(INBOX_ERROR_MESSAGE) + imap.search(searchCriteria, (err, results) => { + if (err) { + reject(new sdk.RuntimeError('An error occured while searching for the message', err)) + } else { + resolve(results) + } + }) + }) + + if (!uids || uids.length === 0) { + imap.end() + return undefined + } + + const f: Imap.ImapFetch = imap.fetch(uids, { + bodies: imapBodies, + struct: true, + }) + + const messages = await _handleFetch(imap, f, imapBodies.length) + return messages[0] + } catch (thrown: unknown) { + if (imap?.state !== 'disconnected') { + imap?.end() + } + const err = thrown instanceof Error ? thrown : new Error(String(thrown)) + throw new sdk.RuntimeError('An error occured while searching for the message by message-id.', err) + } +} + +const _getConfig = function (config: bp.configuration.Configuration) { + return { + user: config.user, + password: config.password, + host: config.imapHost, + port: 993, + tls: true, + tlsOptions: { rejectUnauthorized: false }, + } +} + +function _getStringBetweenAngles(input: string): string | undefined { + const start = input.indexOf('<') + const end = input.indexOf('>') + if (start !== -1 && end !== -1 && end > start) { + return input.substring(start, end + 1) + } + return undefined +} + +const _parseHeader = (buffer: string): HeaderData => { + const headerBuffer = buffer + + let subject = '' + let sender = '' + let id = '' + let inReplyTo: string | undefined + let firstMessageId: string | undefined + let date: string | undefined + + try { + const parsedHeader = Imap.parseHeader(headerBuffer) + subject = (parsedHeader.subject || ['']).join(' ') + sender = (parsedHeader.from || ['']).join(' ') + if (sender.includes('<') && sender.includes('>')) { + sender = sender.substring(sender.indexOf('<') + 1, sender.lastIndexOf('>')) + } + + inReplyTo = parsedHeader['in-reply-to']?.[0] + if (!parsedHeader['message-id']?.[0]) { + throw new sdk.RuntimeError('Email message is missing a message-id (uid)') + } + id = parsedHeader['message-id']?.[0] + if (parsedHeader.date && parsedHeader.date.length > 0) { + date = parsedHeader.date[0] + } + const references = parsedHeader['references']?.[0] + if (references) { + firstMessageId = _getStringBetweenAngles(references) + } + } catch (e) { + console.error('Error parsing header:', e) + } + + return { date, firstMessageId, id, inReplyTo, sender, subject } +} diff --git a/integrations/email/src/index.ts b/integrations/email/src/index.ts new file mode 100644 index 00000000000..50070482383 --- /dev/null +++ b/integrations/email/src/index.ts @@ -0,0 +1,19 @@ +import * as actions from './actions' +import * as channels from './channels' +import * as setup from './setup' +import * as bp from '.botpress' + +export default new bp.Integration({ + register: setup.register, + unregister: setup.unregister, + actions: { + listEmails: actions.listEmails, + getEmail: actions.getEmail, + syncEmails: actions.syncEmails, + sendEmail: actions.sendEmail, + }, + channels: { + default: channels.defaultChannel, + }, + handler: async () => {}, +}) diff --git a/integrations/email/src/locking.ts b/integrations/email/src/locking.ts new file mode 100644 index 00000000000..f9ece54234b --- /dev/null +++ b/integrations/email/src/locking.ts @@ -0,0 +1,25 @@ +import * as bp from '.botpress' + +export class LockHandler { + public constructor(private readonly _props: { client: bp.Client; ctx: bp.Context }) {} + + public async setLock(value: boolean): Promise { + await this._props.client.getOrSetState({ + name: 'syncLock', + id: this._props.ctx.integrationId, + type: 'integration', + payload: { + currentlySyncing: value, + }, + }) + } + + public async readLock(): Promise { + const syncLock = await this._props.client.getState({ + name: 'syncLock', + id: this._props.ctx.integrationId, + type: 'integration', + }) + return syncLock.state.payload.currentlySyncing ?? false + } +} diff --git a/integrations/email/src/paging.test.ts b/integrations/email/src/paging.test.ts new file mode 100644 index 00000000000..114fd9e94bf --- /dev/null +++ b/integrations/email/src/paging.test.ts @@ -0,0 +1,43 @@ +import { expect, test } from 'vitest' +import { pageToSpan, Span, getNextToken, NextTokenProps } from './paging' +import * as sdk from '@botpress/sdk' + +test('pageToSpan with with zero messages throws', () => { + expect(() => pageToSpan({ page: 0, perPage: 50, totalElements: 0 })).toThrow(sdk.RuntimeError) +}) + +test('pageToSpan with single element returns single element', () => { + expect(pageToSpan({ page: 0, perPage: 50, totalElements: 1 })).toEqual({ + firstElementIndex: 1, + lastElementIndex: 1, + } satisfies Span) +}) + +test('pageToSpan with partial page returns all elements', () => { + expect(pageToSpan({ page: 0, perPage: 50, totalElements: 13 })).toEqual({ + firstElementIndex: 1, + lastElementIndex: 13, + } satisfies Span) +}) + +test('pageToSpan with full page returns first page', () => { + expect(pageToSpan({ page: 0, perPage: 50, totalElements: 300 })).toEqual({ + firstElementIndex: 251, + lastElementIndex: 300, + } satisfies Span) +}) + +test('pageToSpan with multiple pages on next page returns next page', () => { + expect(pageToSpan({ page: 1, perPage: 50, totalElements: 300 })).toEqual({ + firstElementIndex: 201, + lastElementIndex: 250, + } satisfies Span) +}) + +test('given a page greater than one, getNextToken returns next token', () => { + expect(getNextToken({ page: 0, firstElementIndex: 10 })).toEqual(1) +}) + +test('given a page equal to one, getNextToken returns undefined', () => { + expect(getNextToken({ page: 0, firstElementIndex: 1 })).toEqual(undefined) +}) diff --git a/integrations/email/src/paging.ts b/integrations/email/src/paging.ts new file mode 100644 index 00000000000..30958372c81 --- /dev/null +++ b/integrations/email/src/paging.ts @@ -0,0 +1,32 @@ +import * as sdk from '@botpress/sdk' + +export type PageToSpanProps = { + page: number + perPage: number + totalElements: number +} + +export type NextTokenProps = { + page: number + firstElementIndex: number +} + +export type Span = { + firstElementIndex: number + lastElementIndex: number +} + +export const pageToSpan = (props: PageToSpanProps): Span => { + if (props.totalElements <= 0) { + throw new sdk.RuntimeError('Could not read the inbox: the number of messages in the inbox is 0') + } + const lastElementIndex = Math.max(1, props.totalElements - props.page * props.perPage) + const firstElementIndex = Math.max(1, lastElementIndex - props.perPage + 1) + + return { firstElementIndex, lastElementIndex } +} + +export const getNextToken = (props: NextTokenProps): number | undefined => { + if (props.firstElementIndex === 1) return undefined + return props.page + 1 +} diff --git a/integrations/email/src/setup.ts b/integrations/email/src/setup.ts new file mode 100644 index 00000000000..17f48763ea2 --- /dev/null +++ b/integrations/email/src/setup.ts @@ -0,0 +1,27 @@ +import * as sdk from '@botpress/sdk' +import { getMessages } from './imap' +import * as locking from './locking' +import * as bp from '.botpress' + +export const register: bp.IntegrationProps['register'] = async (props) => { + const lock = new locking.LockHandler({ client: props.client, ctx: props.ctx }) + await lock.setLock(false) + + await props.client.setState({ + name: 'lastSyncTimestamp', + id: props.ctx.integrationId, + type: 'integration', + payload: { lastSyncTimestamp: new Date().toISOString() }, + }) + + try { + await getMessages({ page: 0, perPage: 1 }, props) + } catch (thrown: unknown) { + const err = thrown instanceof Error ? thrown : new Error(`${thrown}`) + throw new sdk.RuntimeError('An error occured when registering the integration. Verify your configuration.', err) + } +} + +export const unregister: bp.IntegrationProps['unregister'] = async () => { + // nothing to unregister +} diff --git a/integrations/email/src/smtp.ts b/integrations/email/src/smtp.ts new file mode 100644 index 00000000000..37f287a03a0 --- /dev/null +++ b/integrations/email/src/smtp.ts @@ -0,0 +1,24 @@ +import nodemailer from 'nodemailer' +import * as bp from '.botpress' + +export const sendNodemailerMail = async ( + config: bp.Context['configuration'], + props: bp.actions.sendEmail.input.Input, + logger: bp.Logger +) => { + const transporter = nodemailer.createTransport({ + host: config.smtpHost, + auth: { + user: config.user, + pass: config.password, + }, + }) + + await transporter.sendMail({ + from: config.user, + ...props, + references: props.inReplyTo, + }) + logger.forBot().info(`Sent email with subject '${props.subject}' via SMTP`) + return {} +} diff --git a/integrations/email/tsconfig.json b/integrations/email/tsconfig.json new file mode 100644 index 00000000000..d974db67237 --- /dev/null +++ b/integrations/email/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "baseUrl": ".", + "outDir": "dist", + "experimentalDecorators": true, + "emitDecoratorMetadata": true + }, + "include": [".botpress/**/*", "src/**/*", "integration.definition.ts", "definitions/**/*"] +} diff --git a/integrations/freshchat/integration.definition.ts b/integrations/freshchat/integration.definition.ts index 11af0f5cea8..8ce357bb8a9 100644 --- a/integrations/freshchat/integration.definition.ts +++ b/integrations/freshchat/integration.definition.ts @@ -1,12 +1,12 @@ -import { IntegrationDefinition } from '@botpress/sdk' +import * as sdk from '@botpress/sdk' import hitl from './bp_modules/hitl' import { INTEGRATION_NAME } from './src/const' import { events, configuration, channels, states, user } from './src/definitions' -export default new IntegrationDefinition({ +export default new sdk.IntegrationDefinition({ name: INTEGRATION_NAME, title: 'Freshchat (Beta)', - version: '1.2.0', + version: '1.3.0', icon: 'icon.svg', description: 'This integration allows your bot to use Freshchat as a HITL Provider', readme: 'hub.md', @@ -15,8 +15,21 @@ export default new IntegrationDefinition({ channels, events, user, -}).extend(hitl, () => ({ - entities: {}, + entities: { + hitlConversation: { + title: 'HITL Conversation', + description: 'A support request', + schema: sdk.z.object({ + priority: sdk.z + .enum(['Low', 'Medium', 'High', 'Urgent']) + .title('Conversation Priority') + .describe('Priority of the conversation. Leave empty for default priority.') + .optional(), + }), + }, + }, +}).extend(hitl, (self) => ({ + entities: { hitlSession: self.entities.hitlConversation }, channels: { hitl: { title: 'Freshchat', diff --git a/integrations/freshchat/src/actions/hitl.ts b/integrations/freshchat/src/actions/hitl.ts index 1644a346600..9192bcaee8c 100644 --- a/integrations/freshchat/src/actions/hitl.ts +++ b/integrations/freshchat/src/actions/hitl.ts @@ -74,6 +74,7 @@ export const startHitl: bp.IntegrationProps['actions']['startHitl'] = async ({ c userId: user.tags.id as string, messages, channelId, + priority: input.hitlSession?.priority, }) const { conversation } = await client.getOrCreateConversation({ diff --git a/integrations/freshchat/src/client.ts b/integrations/freshchat/src/client.ts index c82230b535d..6feae50f6a4 100644 --- a/integrations/freshchat/src/client.ts +++ b/integrations/freshchat/src/client.ts @@ -36,6 +36,7 @@ class FreshchatClient { userId: string messages: FreshchatMessage[] channelId: string + priority?: 'Low' | 'Medium' | 'High' | 'Urgent' }): Promise<{ conversation_id: string; channel_id: string }> { const { data } = await this._client.post('/conversations', { channel_id: args.channelId, @@ -45,6 +46,9 @@ class FreshchatClient { id: args.userId, }, ], + properties: { + ...(args.priority ? { priority: args.priority } : {}), + }, }) return data } diff --git a/integrations/zendesk/integration.definition.ts b/integrations/zendesk/integration.definition.ts index 3354bc099a5..255da1f0fa6 100644 --- a/integrations/zendesk/integration.definition.ts +++ b/integrations/zendesk/integration.definition.ts @@ -1,13 +1,13 @@ /* bplint-disable */ -import { IntegrationDefinition } from '@botpress/sdk' +import * as sdk from '@botpress/sdk' import { sentry as sentryHelpers } from '@botpress/sdk-addons' import hitl from './bp_modules/hitl' import { actions, events, configuration, channels, states, user } from './src/definitions' -export default new IntegrationDefinition({ +export default new sdk.IntegrationDefinition({ name: 'zendesk', title: 'Zendesk', - version: '2.5.0', + version: '2.6.0', icon: 'icon.svg', description: 'Optimize your support workflow. Trigger workflows from ticket updates as well as manage tickets, access conversations, and engage with customers.', @@ -19,8 +19,21 @@ export default new IntegrationDefinition({ actions, events, secrets: sentryHelpers.COMMON_SECRET_NAMES, -}).extend(hitl, () => ({ - entities: {}, + entities: { + hitlTicket: { + schema: sdk.z.object({ + priority: sdk.z + .enum(['low', 'normal', 'high', 'urgent']) + .title('Ticket Priority') + .describe('Priority of the ticket. Leave empty for default priority.') + .optional(), + }), + }, + }, +}).extend(hitl, (self) => ({ + entities: { + hitlSession: self.entities.hitlTicket, + }, channels: { hitl: { title: 'Zendesk Ticket', diff --git a/integrations/zendesk/src/actions/hitl.ts b/integrations/zendesk/src/actions/hitl.ts index 37dfb2f0f3e..c7ff381526f 100644 --- a/integrations/zendesk/src/actions/hitl.ts +++ b/integrations/zendesk/src/actions/hitl.ts @@ -15,9 +15,14 @@ export const startHitl: bp.IntegrationProps['actions']['startHitl'] = async (pro throw new sdk.RuntimeError(`User ${user.id} not linked in Zendesk`) } - const ticket = await zendeskClient.createTicket(input.title ?? 'Untitled Ticket', await _buildTicketBody(props), { - id: zendeskAuthorId, - }) + const ticket = await zendeskClient.createTicket( + input.title ?? 'Untitled Ticket', + await _buildTicketBody(props), + { + id: zendeskAuthorId, + }, + { priority: input.hitlSession?.priority } + ) const zendeskTicketId = `${ticket.id}` const { conversation } = await client.getOrCreateConversation({ diff --git a/interfaces/hitl/interface.definition.ts b/interfaces/hitl/interface.definition.ts index 84a8b238f8b..fc0f0389f1e 100644 --- a/interfaces/hitl/interface.definition.ts +++ b/interfaces/hitl/interface.definition.ts @@ -32,8 +32,14 @@ const messageSchema = sdk.z.union(messagePayloadSchemas as Tuple + schema: (entities) => sdk.z.object({ // Also known as downstreamUserId: userId: sdk.z.string().title('User ID').describe('ID of the Botpress user representing the end user'), @@ -122,6 +128,11 @@ export default new sdk.InterfaceDefinition({ ) .optional(), + hitlSession: entities.hitlSession + .optional() + .title('Extra configuration') + .describe('Configuration of the HITL session'), + // All messages sent prior to HITL session creation: messageHistory: sdk.z .array(messageSchema) diff --git a/plugins/hitl/plugin.definition.ts b/plugins/hitl/plugin.definition.ts index d6ba9781d86..3c414f7d658 100644 --- a/plugins/hitl/plugin.definition.ts +++ b/plugins/hitl/plugin.definition.ts @@ -85,7 +85,7 @@ const PLUGIN_CONFIG_SCHEMA = sdk.z.object({ export default new sdk.PluginDefinition({ name: 'hitl', - version: '0.12.0', + version: '0.13.0', title: 'Human In The Loop', description: 'Seamlessly transfer conversations to human agents', icon: 'icon.svg', @@ -98,37 +98,42 @@ export default new sdk.PluginDefinition({ title: 'Start HITL', description: 'Starts the HITL mode', input: { - schema: sdk.z - .object({ - title: sdk.z.string().title('Ticket Title').describe('Title of the HITL ticket'), - description: sdk.z - .string() - .title('Ticket Description') - .optional() - .describe('Description of the HITL ticket'), - userId: sdk.z - .string() - .title('User ID') - .describe('ID of the user that starts the HITL mode') - .placeholder('{{ event.userId }}'), - userEmail: sdk.z - .string() - .title('User Email') - .optional() - .describe( - 'Email of the user that starts the HITL mode. If this value is unset, the agent will try to use the email provided by the channel.' - ), - conversationId: sdk.z - .string() - .title('Conversation ID') // this is the upstream conversation - .describe('ID of the conversation on which to start the HITL mode') - .placeholder('{{ event.conversationId }}'), - configurationOverrides: PLUGIN_CONFIG_SCHEMA.partial() - .optional() - .title('Configuration Overrides') - .describe('Use this to override the global configuration for this specific HITL session'), - }) - .passthrough(), + schema: ({ entities }) => + sdk.z + .object({ + title: sdk.z.string().title('Ticket Title').describe('Title of the HITL ticket'), + description: sdk.z + .string() + .title('Ticket Description') + .optional() + .describe('Description of the HITL ticket'), + hitlSession: entities.hitl.hitlSession + .optional() + .title('Extra configuration') + .describe('Configuration of the HITL session'), + userId: sdk.z + .string() + .title('User ID') + .describe('ID of the user that starts the HITL mode') + .placeholder('{{ event.userId }}'), + userEmail: sdk.z + .string() + .title('User Email') + .optional() + .describe( + 'Email of the user that starts the HITL mode. If this value is unset, the agent will try to use the email provided by the channel.' + ), + conversationId: sdk.z + .string() + .title('Conversation ID') // this is the upstream conversation + .describe('ID of the conversation on which to start the HITL mode') + .placeholder('{{ event.conversationId }}'), + configurationOverrides: PLUGIN_CONFIG_SCHEMA.partial() + .optional() + .title('Configuration Overrides') + .describe('Use this to override the global configuration for this specific HITL session'), + }) + .passthrough(), }, output: { schema: sdk.z.object({}) }, }, diff --git a/plugins/hitl/src/actions/start-hitl.ts b/plugins/hitl/src/actions/start-hitl.ts index 5f9f9213731..5020765256b 100644 --- a/plugins/hitl/src/actions/start-hitl.ts +++ b/plugins/hitl/src/actions/start-hitl.ts @@ -115,7 +115,9 @@ const _createDownstreamConversation = async ( ): Promise => { // Call startHitl in the hitl integration (zendesk, etc.): const { conversationId: downstreamConversationId } = await props.actions.hitl.startHitl({ - ...input, // the Studio might pass additional fields here, so we spread the input to ensure everything is forwarded to the integration + title: input.title, + description: input.description, + hitlSession: input.hitlSession, userId: downstreamUserId, messageHistory, }) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5549e1187cc..72c9c01bca3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -640,6 +640,28 @@ importers: specifier: ^2.39.1 version: 2.39.1 + integrations/email: + dependencies: + '@botpress/client': + specifier: workspace:* + version: link:../../packages/client + '@botpress/sdk': + specifier: workspace:* + version: link:../../packages/sdk + imap: + specifier: ^0.8.17 + version: 0.8.17 + nodemailer: + specifier: ^6.7.2 + version: 6.9.3 + devDependencies: + '@types/imap': + specifier: ^0.8.42 + version: 0.8.42 + '@types/nodemailer': + specifier: ^6.4.4 + version: 6.4.8 + integrations/fireworks-ai: dependencies: '@botpress/client': @@ -1352,7 +1374,7 @@ importers: version: link:../../packages/sdk-addons sunshine-conversations-client: specifier: ^9.12.0 - version: 9.14.0(@babel/core@7.27.1) + version: 9.14.0(@babel/core@7.28.0) devDependencies: '@botpress/common': specifier: workspace:* @@ -1678,7 +1700,7 @@ importers: version: 29.5.0(@types/node@22.16.4)(ts-node@10.9.2) ts-jest: specifier: ^29.1.0 - version: 29.1.0(@babel/core@7.27.1)(jest@29.5.0)(typescript@5.8.3) + version: 29.1.0(@babel/core@7.28.0)(jest@29.5.0)(typescript@5.8.3) integrations/zendesk: dependencies: @@ -3861,14 +3883,14 @@ packages: uuid: 8.3.2 dev: false - /@babel/cli@7.22.5(@babel/core@7.27.1): + /@babel/cli@7.22.5(@babel/core@7.28.0): resolution: {integrity: sha512-N5d7MjzwsQ2wppwjhrsicVDhJSqF9labEP/swYiHhio4Ca2XjEehpgPmerjnLQl7BPE59BLud0PTWGYwqFl/cQ==} engines: {node: '>=6.9.0'} hasBin: true peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.27.1 + '@babel/core': 7.28.0 '@jridgewell/trace-mapping': 0.3.25 commander: 4.1.1 convert-source-map: 1.9.0 @@ -3901,8 +3923,8 @@ packages: resolution: {integrity: sha512-oH5UPLMWR3L2wEFLnFJ1TZXqHufiTKAiLfqw5zkhS4dKXLJ10yVztfil/twG8EDTA4F/tvVNw9nOl4ZMslB8rQ==} engines: {node: '>=6.9.0'} - /@babel/compat-data@7.27.2: - resolution: {integrity: sha512-TUtMJYRPyUb/9aU8f3K0mjmjf6M9N5Woshn2CS6nqJSeJtTtQcpLUXjGt9vbF8ZGff0El99sWkLgzwW3VXnxZQ==} + /@babel/compat-data@7.28.0: + resolution: {integrity: sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw==} engines: {node: '>=6.9.0'} /@babel/core@7.26.9: @@ -3927,20 +3949,20 @@ packages: transitivePeerDependencies: - supports-color - /@babel/core@7.27.1: - resolution: {integrity: sha512-IaaGWsQqfsQWVLqMn9OB92MNN7zukfVA4s7KKAI0KfrrDsZ0yhi5uV4baBuLuN7n3vsZpwP8asPPcVwApxvjBQ==} + /@babel/core@7.28.0: + resolution: {integrity: sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ==} engines: {node: '>=6.9.0'} dependencies: '@ampproject/remapping': 2.3.0 '@babel/code-frame': 7.27.1 - '@babel/generator': 7.27.1 + '@babel/generator': 7.28.0 '@babel/helper-compilation-targets': 7.27.2 - '@babel/helper-module-transforms': 7.27.1(@babel/core@7.27.1) - '@babel/helpers': 7.27.1 - '@babel/parser': 7.27.2 + '@babel/helper-module-transforms': 7.27.3(@babel/core@7.28.0) + '@babel/helpers': 7.27.6 + '@babel/parser': 7.28.0 '@babel/template': 7.27.2 - '@babel/traverse': 7.27.1 - '@babel/types': 7.27.1 + '@babel/traverse': 7.28.0 + '@babel/types': 7.28.1 convert-source-map: 2.0.0 debug: 4.4.1 gensync: 1.0.0-beta.2 @@ -3969,6 +3991,16 @@ packages: '@jridgewell/trace-mapping': 0.3.25 jsesc: 3.1.0 + /@babel/generator@7.28.0: + resolution: {integrity: sha512-lJjzvrbEeWrhB4P3QBsH7tey117PjLZnDbLiQEKjQ/fNJTjuq4HSqgFA+UNSwZT8D7dxxbnuSBMsa1lrWzKlQg==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/parser': 7.28.0 + '@babel/types': 7.28.1 + '@jridgewell/gen-mapping': 0.3.12 + '@jridgewell/trace-mapping': 0.3.29 + jsesc: 3.1.0 + /@babel/helper-annotate-as-pure@7.27.1: resolution: {integrity: sha512-WnuuDILl9oOBbKnb4L+DyODx7iC47XfzmNCpTttFsSp6hTG7XZxu60+4IO+2/hPfcGOoKbFiwoI/+zwARbNQow==} engines: {node: '>=6.9.0'} @@ -3990,9 +4022,9 @@ packages: resolution: {integrity: sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==} engines: {node: '>=6.9.0'} dependencies: - '@babel/compat-data': 7.27.2 + '@babel/compat-data': 7.28.0 '@babel/helper-validator-option': 7.27.1 - browserslist: 4.24.5 + browserslist: 4.25.1 lru-cache: 5.1.1 semver: 6.3.1 @@ -4014,6 +4046,10 @@ packages: - supports-color dev: false + /@babel/helper-globals@7.28.0: + resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==} + engines: {node: '>=6.9.0'} + /@babel/helper-member-expression-to-functions@7.27.1: resolution: {integrity: sha512-E5chM8eWjTp/aNoVpcbfM7mLxu9XGLWYise2eBKGQomAk/Mb4XoxyqXTZbuTohbsl8EKqdlMhnDI2CCLfcs9wA==} engines: {node: '>=6.9.0'} @@ -4069,16 +4105,16 @@ packages: - supports-color dev: false - /@babel/helper-module-transforms@7.27.1(@babel/core@7.27.1): - resolution: {integrity: sha512-9yHn519/8KvTU5BjTVEEeIM3w9/2yXNKoD82JifINImhpKkARMJKPP59kLo+BafpdN5zgNeIcS4jsGDmd3l58g==} + /@babel/helper-module-transforms@7.27.3(@babel/core@7.28.0): + resolution: {integrity: sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 dependencies: - '@babel/core': 7.27.1 + '@babel/core': 7.28.0 '@babel/helper-module-imports': 7.27.1 '@babel/helper-validator-identifier': 7.27.1 - '@babel/traverse': 7.27.1 + '@babel/traverse': 7.28.0 transitivePeerDependencies: - supports-color @@ -4148,12 +4184,12 @@ packages: '@babel/template': 7.26.9 '@babel/types': 7.26.9 - /@babel/helpers@7.27.1: - resolution: {integrity: sha512-FCvFTm0sWV8Fxhpp2McP5/W53GPllQ9QeQ7SiqGWjMf/LVG07lFa5+pgK05IRhVwtvafT22KF+ZSnM9I545CvQ==} + /@babel/helpers@7.27.6: + resolution: {integrity: sha512-muE8Tt8M22638HU31A3CgfSUciwz1fhATfoVai05aPXGor//CdWDCbnlY1yvBPo07njuVOCNGCSp/GTt12lIug==} engines: {node: '>=6.9.0'} dependencies: '@babel/template': 7.27.2 - '@babel/types': 7.27.1 + '@babel/types': 7.28.1 /@babel/parser@7.26.9: resolution: {integrity: sha512-81NWa1njQblgZbQHxWHpxxCzNsa3ZwvFqpUg7P+NNUU6f3UU2jBEg4OlF/J6rl8+PQGh1q6/zWScd001YwcA5A==} @@ -4169,6 +4205,13 @@ packages: dependencies: '@babel/types': 7.27.1 + /@babel/parser@7.28.0: + resolution: {integrity: sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g==} + engines: {node: '>=6.0.0'} + hasBin: true + dependencies: + '@babel/types': 7.28.1 + /@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.26.9): resolution: {integrity: sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==} peerDependencies: @@ -4412,6 +4455,20 @@ packages: transitivePeerDependencies: - supports-color + /@babel/traverse@7.28.0: + resolution: {integrity: sha512-mGe7UK5wWyh0bKRfupsUchrQGqvDbZDbKJw+kcRGSmdHVYrv+ltd0pnpDTVpiTqnaBru9iEvA8pz8W46v0Amwg==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/generator': 7.28.0 + '@babel/helper-globals': 7.28.0 + '@babel/parser': 7.28.0 + '@babel/template': 7.27.2 + '@babel/types': 7.28.1 + debug: 4.4.1 + transitivePeerDependencies: + - supports-color + /@babel/types@7.26.9: resolution: {integrity: sha512-Y3IR1cRnOxOCDvMmNiym7XpXQ93iGDDPHx+Zj+NM+rg0fBaShfQLkg+hKPaZCEvg5N/LeCo4+Rj/i3FuJsIQaw==} engines: {node: '>=6.9.0'} @@ -4426,6 +4483,13 @@ packages: '@babel/helper-string-parser': 7.27.1 '@babel/helper-validator-identifier': 7.27.1 + /@babel/types@7.28.1: + resolution: {integrity: sha512-x0LvFTekgSX+83TI28Y9wYPUfzrnl2aT5+5QLnO6v7mSJYtEEevuDRN0F0uSHRk1G1IWZC43o00Y0xDDrpBGPQ==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.27.1 + /@bcherny/json-schema-ref-parser@10.0.5-fork: resolution: {integrity: sha512-E/jKbPoca1tfUPj3iSbitDZTGnq6FUFjkH6L8U2oDwSuwK1WhnnVtCG7oFOTg/DDnyoXbQYUiUiGOibHqaGVnw==} engines: {node: '>= 16'} @@ -5858,6 +5922,12 @@ packages: chalk: 4.1.2 dev: true + /@jridgewell/gen-mapping@0.3.12: + resolution: {integrity: sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==} + dependencies: + '@jridgewell/sourcemap-codec': 1.5.4 + '@jridgewell/trace-mapping': 0.3.29 + /@jridgewell/gen-mapping@0.3.8: resolution: {integrity: sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==} engines: {node: '>=6.0.0'} @@ -5877,12 +5947,21 @@ packages: /@jridgewell/sourcemap-codec@1.5.0: resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} + /@jridgewell/sourcemap-codec@1.5.4: + resolution: {integrity: sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==} + /@jridgewell/trace-mapping@0.3.25: resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} dependencies: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.0 + /@jridgewell/trace-mapping@0.3.29: + resolution: {integrity: sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==} + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.4 + /@jridgewell/trace-mapping@0.3.9: resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} dependencies: @@ -8308,6 +8387,12 @@ packages: '@types/unist': 3.0.3 dev: false + /@types/imap@0.8.42: + resolution: {integrity: sha512-FusePG9Cp2GYN6OLow9xBCkjznFkAR7WCz0Fm+j1p/ER6C8V8P71DtjpSmwrZsS7zekCeqdTPHEk9N5OgPwcsg==} + dependencies: + '@types/node': 22.16.4 + dev: true + /@types/is-stream@1.1.0: resolution: {integrity: sha512-jkZatu4QVbR60mpIzjINmtS1ZF4a/FqdTUTBeQDVOQ2PYyidtwFKr0B5G6ERukKwliq+7mIXvxyppwzG5EgRYg==} dependencies: @@ -9808,15 +9893,15 @@ packages: node-releases: 2.0.19 update-browserslist-db: 1.1.2(browserslist@4.24.4) - /browserslist@4.24.5: - resolution: {integrity: sha512-FDToo4Wo82hIdgc1CQ+NQD0hEhmpPjrZ3hiUgwgOG6IuTdlpr8jdjyG24P6cNP1yJpTLzS5OcGgSw0xmDU1/Tw==} + /browserslist@4.25.1: + resolution: {integrity: sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true dependencies: - caniuse-lite: 1.0.30001718 - electron-to-chromium: 1.5.155 + caniuse-lite: 1.0.30001727 + electron-to-chromium: 1.5.188 node-releases: 2.0.19 - update-browserslist-db: 1.1.3(browserslist@4.24.5) + update-browserslist-db: 1.1.3(browserslist@4.25.1) /bs-logger@0.2.6: resolution: {integrity: sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==} @@ -9967,8 +10052,8 @@ packages: /caniuse-lite@1.0.30001700: resolution: {integrity: sha512-2S6XIXwaE7K7erT8dY+kLQcpa5ms63XlRkMkReXjle+kf6c5g38vyMl+Z5y8dSxOFDhcFe+nxnn261PLxBSQsQ==} - /caniuse-lite@1.0.30001718: - resolution: {integrity: sha512-AflseV1ahcSunK53NfEs9gFWgOEmzr0f+kaMFA4xiLZlr9Hzt7HxcSpIFcnNCUkz6R6dWKa54rUz3HUmI3nVcw==} + /caniuse-lite@1.0.30001727: + resolution: {integrity: sha512-pB68nIHmbN6L/4C6MH1DokyR3bYqFwjaSs/sWDHGj4CTcFtQUQMuJftVwWkXq7mNWOybD3KhUv3oWHoGxgP14Q==} /capital-case@1.0.4: resolution: {integrity: sha512-ds37W8CytHgwnhGGTi88pcPyR15qoNkOpYwmMMfnWqqWgESapLqvDx6huFjQ5vqWSn2Z06173XNA7LtMOeUh1A==} @@ -10789,8 +10874,8 @@ packages: /electron-to-chromium@1.5.104: resolution: {integrity: sha512-Us9M2L4cO/zMBqVkJtnj353nQhMju9slHm62NprKTmdF3HH8wYOtNvDFq/JB2+ZRoGLzdvYDiATlMHs98XBM1g==} - /electron-to-chromium@1.5.155: - resolution: {integrity: sha512-ps5KcGGmwL8VaeJlvlDlu4fORQpv3+GIcF5I3f9tUKUlJ/wsysh6HU8P5L1XWRYeXfA0oJd4PyM8ds8zTFf6Ng==} + /electron-to-chromium@1.5.188: + resolution: {integrity: sha512-pfEx5CBFAocOKNrc+i5fSvhDaI1Vr9R9aT5uX1IzM3hhdL6k649wfuUcdUd9EZnmbE1xdfA51CwqQ61CO3Xl3g==} /emittery@0.13.1: resolution: {integrity: sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==} @@ -12703,6 +12788,14 @@ packages: engines: {node: '>= 4'} dev: true + /imap@0.8.17: + resolution: {integrity: sha512-6jPudBfTHLOdA/+IqCyCfxrozq75HxwVzqn0UDaweiD0t5qN0UUrU4JpGSd6NR233L5MRZ0Ioq/bZjZIJjDQtw==} + engines: {node: '>=0.8.0'} + dependencies: + readable-stream: 1.1.14 + utf7: 1.0.0 + dev: false + /immer@9.0.21: resolution: {integrity: sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==} dev: false @@ -13141,6 +13234,10 @@ packages: is-docker: 2.2.1 dev: false + /isarray@0.0.1: + resolution: {integrity: sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==} + dev: false + /isarray@1.0.0: resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} @@ -15475,8 +15572,8 @@ packages: resolution: {integrity: sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==} dev: false - /pg-cloudflare@1.2.5: - resolution: {integrity: sha512-OOX22Vt0vOSRrdoUPKJ8Wi2OpE/o/h9T8X1s4qSkCedbNah9ei2W2765be8iMVxQUsvgT7zIAT2eIa9fs5+vtg==} + /pg-cloudflare@1.2.7: + resolution: {integrity: sha512-YgCtzMH0ptvZJslLM1ffsY4EuGaU0cx4XSdXLRFae8bPP4dS5xL1tNB3k2o/N64cHJpwU7dxKli/nZ2lUa5fLg==} requiresBuild: true dev: true optional: true @@ -15528,7 +15625,7 @@ packages: pg-types: 2.2.0 pgpass: 1.0.5 optionalDependencies: - pg-cloudflare: 1.2.5 + pg-cloudflare: 1.2.7 dev: true /pgpass@1.0.5: @@ -15887,6 +15984,15 @@ packages: loose-envify: 1.4.0 dev: false + /readable-stream@1.1.14: + resolution: {integrity: sha512-+MeVjFf4L44XUkhM1eYbD8fyEsxcV81pqMSR5gblfcLCHfZvbrqy4/qYHE+/R5HoBUT11WV5O08Cr1n3YXkWVQ==} + dependencies: + core-util-is: 1.0.2 + inherits: 2.0.4 + isarray: 0.0.1 + string_decoder: 0.10.31 + dev: false + /readable-stream@2.3.8: resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} dependencies: @@ -16814,6 +16920,10 @@ packages: define-properties: 1.2.1 es-object-atoms: 1.0.0 + /string_decoder@0.10.31: + resolution: {integrity: sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==} + dev: false + /string_decoder@1.1.1: resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} dependencies: @@ -16908,10 +17018,10 @@ packages: ts-interface-checker: 0.1.13 dev: true - /sunshine-conversations-client@9.14.0(@babel/core@7.27.1): + /sunshine-conversations-client@9.14.0(@babel/core@7.28.0): resolution: {integrity: sha512-4fFoQEvOG7W7DEicVQ5bImblbUan5HPPhSs1Zt+cRXUuiL4wjrWWBWnSQ1REPGAA9Dw2Wcrgj9dgaeHB4AHdrA==} dependencies: - '@babel/cli': 7.22.5(@babel/core@7.27.1) + '@babel/cli': 7.22.5(@babel/core@7.28.0) superagent: 5.3.1 transitivePeerDependencies: - '@babel/core' @@ -17258,7 +17368,7 @@ packages: tslib: 1.14.1 dev: false - /ts-jest@29.1.0(@babel/core@7.27.1)(jest@29.5.0)(typescript@5.8.3): + /ts-jest@29.1.0(@babel/core@7.28.0)(jest@29.5.0)(typescript@5.8.3): resolution: {integrity: sha512-ZhNr7Z4PcYa+JjMl62ir+zPiNJfXJN6E8hSLnaUKhOgqcn8vb3e537cpkd0FuAfRK3sR1LSqM1MOhliXNgOFPA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} hasBin: true @@ -17279,7 +17389,7 @@ packages: esbuild: optional: true dependencies: - '@babel/core': 7.27.1 + '@babel/core': 7.28.0 bs-logger: 0.2.6 fast-json-stable-stringify: 2.1.0 jest: 29.5.0(@types/node@22.16.4)(ts-node@10.9.2) @@ -17874,13 +17984,13 @@ packages: escalade: 3.2.0 picocolors: 1.1.1 - /update-browserslist-db@1.1.3(browserslist@4.24.5): + /update-browserslist-db@1.1.3(browserslist@4.25.1): resolution: {integrity: sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==} hasBin: true peerDependencies: browserslist: '>= 4.21.0' dependencies: - browserslist: 4.24.5 + browserslist: 4.25.1 escalade: 3.2.0 picocolors: 1.1.1 @@ -17921,6 +18031,10 @@ packages: resolution: {integrity: sha512-XdVKMF4SJ0nP/O7XIPB0JwAEuT9lDIYnNsK8yGVe43y0AWoKeJNdv3ZNWh7ksJ6KqQFjOO6ox/VEitLnaVNufw==} dev: false + /utf7@1.0.0: + resolution: {integrity: sha512-vMfAo9fTQeZG7W8PEOWa9g++rkHxYqBBrb22OWY+OMUnSFFtQURySIIJ2LstpF5JI6SzsGQNaglin2aLwA7pTA==} + dev: false + /util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}