diff --git a/src/logger.ts b/src/logger.ts index 287e587..fb41972 100644 --- a/src/logger.ts +++ b/src/logger.ts @@ -33,10 +33,10 @@ export interface Logger { * Log at `'fatal'` level the given msg. If the first argument is an object, all its properties will be included in the JSON line. * If more args follows `msg`, these will be used to format `msg` using `util.format`. * - * @typeParam T: the interface of the object being serialized. Default is object. - * @param obj: object to be serialized - * @param msg: the log message to write - * @param ...args: format string values when `msg` is a format string + * @typeParam T - the interface of the object being serialized. Default is object. + * @param obj - object to be serialized + * @param msg - the log message to write + * @param args - format string values when `msg` is a format string */ fatal: LogFn @@ -44,10 +44,10 @@ export interface Logger { * Log at `'error'` level the given msg. If the first argument is an object, all its properties will be included in the JSON line. * If more args follows `msg`, these will be used to format `msg` using `util.format`. * - * @typeParam T: the interface of the object being serialized. Default is object. - * @param obj: object to be serialized - * @param msg: the log message to write - * @param ...args: format string values when `msg` is a format string + * @typeParam T - the interface of the object being serialized. Default is object. + * @param obj - object to be serialized + * @param msg - the log message to write + * @param args - format string values when `msg` is a format string */ error: LogFn @@ -55,10 +55,10 @@ export interface Logger { * Log at `'warn'` level the given msg. If the first argument is an object, all its properties will be included in the JSON line. * If more args follows `msg`, these will be used to format `msg` using `util.format`. * - * @typeParam T: the interface of the object being serialized. Default is object. - * @param obj: object to be serialized - * @param msg: the log message to write - * @param ...args: format string values when `msg` is a format string + * @typeParam T - the interface of the object being serialized. Default is object. + * @param obj - object to be serialized + * @param msg - the log message to write + * @param args - format string values when `msg` is a format string */ warn: LogFn @@ -66,10 +66,10 @@ export interface Logger { * Log at `'info'` level the given msg. If the first argument is an object, all its properties will be included in the JSON line. * If more args follows `msg`, these will be used to format `msg` using `util.format`. * - * @typeParam T: the interface of the object being serialized. Default is object. - * @param obj: object to be serialized - * @param msg: the log message to write - * @param ...args: format string values when `msg` is a format string + * @typeParam T - the interface of the object being serialized. Default is object. + * @param obj - object to be serialized + * @param msg - the log message to write + * @param args - format string values when `msg` is a format string */ info: LogFn @@ -77,10 +77,10 @@ export interface Logger { * Log at `'debug'` level the given msg. If the first argument is an object, all its properties will be included in the JSON line. * If more args follows `msg`, these will be used to format `msg` using `util.format`. * - * @typeParam T: the interface of the object being serialized. Default is object. - * @param obj: object to be serialized - * @param msg: the log message to write - * @param ...args: format string values when `msg` is a format string + * @typeParam T - the interface of the object being serialized. Default is object. + * @param obj - object to be serialized + * @param msg - the log message to write + * @param args - format string values when `msg` is a format string */ debug: LogFn @@ -88,10 +88,10 @@ export interface Logger { * Log at `'trace'` level the given msg. If the first argument is an object, all its properties will be included in the JSON line. * If more args follows `msg`, these will be used to format `msg` using `util.format`. * - * @typeParam T: the interface of the object being serialized. Default is object. - * @param obj: object to be serialized - * @param msg: the log message to write - * @param ...args: format string values when `msg` is a format string + * @typeParam T - the interface of the object being serialized. Default is object. + * @param obj - object to be serialized + * @param msg - the log message to write + * @param args - format string values when `msg` is a format string */ trace: LogFn diff --git a/src/services/novu.ts b/src/services/novu.ts index 06f1ec3..79d7a65 100644 --- a/src/services/novu.ts +++ b/src/services/novu.ts @@ -1,23 +1,70 @@ import defaultKy from 'ky' +/** + * Base URL endpoint for the Novu API. + */ export const NOVU_API_BASE_URL = 'https://api.novu.co/v1' export type NovuSubscriber = { + /** + * Unique identifier for the subscriber. This can be any value that is meaningful to your application such as a user ID stored in your database or a unique email address. + */ subscriberId: string + + /** + * Email address of the subscriber. + */ email?: string + + /** + * First name of the subscriber. + */ firstName?: string + + /** + * Last name of the subscriber. + */ lastName?: string + + /** + * Phone number of the subscriber. + */ phone?: string } +/** + * Response from the Novu API when triggering an event. + */ export type NovuTriggerEventResponse = { + /** + * Data about the triggered event. + */ data: { + /** + * Whether the trigger was acknowledged or not. + */ acknowledged?: boolean + + /** + * Status for trigger. + */ status?: string + + /** + * Transaction id for trigger. + */ transactionId?: string + + /** + * In case of an error, this field will contain the error message. + */ + error?: Array } } +/** + * Options for triggering an event in Novu. + */ export type NovuTriggerOptions = { /** * Name of the event to trigger. This should match the name of an existing notification template in Novu. @@ -30,17 +77,33 @@ export type NovuTriggerOptions = { payload: Record /** - * List of subscribers to send the notification to + * List of subscribers to send the notification to. Each subscriber must at least have a unique `subscriberId` to identify them in Novu and, if not already known to Novu, an `email` address or `phone` number depending on the notification template being used. */ to: NovuSubscriber[] } +/** + * Client for interacting with the Novu API. + */ export class NovuClient { + /** + * Instance of ky for making requests to the Novu API. + */ api: typeof defaultKy + /** + * API key to use for authenticating requests to the Novu API. + */ apiKey: string + + /** + * Base URL endpoint for the Novu API. + */ apiBaseUrl: string + /** + * Novu API client constructor. + */ constructor({ apiKey = process.env.NOVU_API_KEY, apiBaseUrl = NOVU_API_BASE_URL, diff --git a/src/services/twilio-conversation.ts b/src/services/twilio-conversation.ts index 7bd0dd7..259da19 100644 --- a/src/services/twilio-conversation.ts +++ b/src/services/twilio-conversation.ts @@ -94,6 +94,70 @@ export interface TwilioConversationMessages { } } +/** + * Participant Conversation Resource. + * + * This interface represents a participant in a conversation, along with the conversation details. + */ +interface ParticipantConversation { + /** The unique ID of the Account responsible for this conversation. */ + account_sid: string + + /** The unique ID of the Conversation Service this conversation belongs to. */ + chat_service_sid: string + + /** The unique ID of the Participant. */ + participant_sid: string + + /** The unique string that identifies the conversation participant as Conversation User. */ + participant_user_sid: string + + /** + * A unique string identifier for the conversation participant as Conversation User. + * This parameter is non-null if (and only if) the participant is using the Conversations SDK to communicate. + */ + participant_identity: string + + /** + * Information about how this participant exchanges messages with the conversation. + * A JSON parameter consisting of type and address fields of the participant. + */ + participant_messaging_binding: object + + /** The unique ID of the Conversation this Participant belongs to. */ + conversation_sid: string + + /** An application-defined string that uniquely identifies the Conversation resource. */ + conversation_unique_name: string + + /** The human-readable name of this conversation, limited to 256 characters. */ + conversation_friendly_name: string + + /** + * An optional string metadata field you can use to store any data you wish. + * The string value must contain structurally valid JSON if specified. + */ + conversation_attributes: string + + /** The date that this conversation was created, given in ISO 8601 format. */ + conversation_date_created: string + + /** The date that this conversation was last updated, given in ISO 8601 format. */ + conversation_date_updated: string + + /** Identity of the creator of this Conversation. */ + conversation_created_by: string + + /** The current state of this User Conversation. One of inactive, active or closed. */ + conversation_state: 'inactive' | 'active' | 'closed' + + /** Timer date values representing state update for this conversation. */ + conversation_timers: object + + /** Contains absolute URLs to access the participant and conversation of this conversation. */ + links: { participant: string; conversation: string } +} + export type TwilioSendAndWaitOptions = { /** * The recipient's phone number in E.164 format (e.g. +14565551234). @@ -204,6 +268,31 @@ export class TwilioConversationClient { return this.api.delete(`Conversations/${conversationSid}`) } + /** + * Removes a participant from a conversation. + */ + async removeParticipant({ + conversationSid, + participantSid + }: { + conversationSid: string + participantSid: string + }) { + return this.api.delete( + `Conversations/${conversationSid}/Participants/${participantSid}` + ) + } + + /** + * Fetches all conversations a participant as identified by their phone number is a part of. + */ + async findParticipantConversations(participantPhoneNumber: string) { + const encodedPhoneNumber = encodeURIComponent(participantPhoneNumber) + return this.api + .get(`ParticipantConversations?Address=${encodedPhoneNumber}`) + .json<{ conversations: ParticipantConversation[] }>() + } + /** * Creates a new conversation. */ @@ -333,7 +422,22 @@ export class TwilioConversationClient { ) const { sid: conversationSid } = await this.createConversation(name) - await this.addParticipant({ conversationSid, recipientPhoneNumber }) + + // Find and remove participant from conversation they are currently in, if any: + const { conversations } = await this.findParticipantConversations( + recipientPhoneNumber + ) + for (const conversation of conversations) { + await this.removeParticipant({ + conversationSid: conversation.conversation_sid, + participantSid: conversation.participant_sid + }) + } + + const { sid: participantSid } = await this.addParticipant({ + conversationSid, + recipientPhoneNumber + }) await this.sendTextWithChunking({ conversationSid, text }) const start = Date.now() @@ -341,7 +445,7 @@ export class TwilioConversationClient { do { if (aborted) { - await this.deleteConversation(conversationSid) + await this.removeParticipant({ conversationSid, participantSid }) const reason = stopSignal?.reason || 'Aborted waiting for reply' if (reason instanceof Error) { @@ -360,7 +464,7 @@ export class TwilioConversationClient { const candidate = candidates[candidates.length - 1] if (validate(candidate)) { - await this.deleteConversation(conversationSid) + await this.removeParticipant({ conversationSid, participantSid }) return candidate } @@ -377,7 +481,7 @@ export class TwilioConversationClient { await sleep(intervalMs) } while (Date.now() - start < timeoutMs) - await this.deleteConversation(conversationSid) + await this.removeParticipant({ conversationSid, participantSid }) throw new Error('Twilio timeout waiting for reply') } }