Untitled

mail@pastecode.io avatar
unknown
plain_text
15 days ago
13 kB
7
Indexable
Never
"use node"
import {
    TemplateConfig,
    TwilioService,
    TwilioSubChannel,
    vTwilioSubChannel,
} from "../../../omni/models/types"
import { SubChannel, TLCType } from "../../../tlc/types"
import { TwilioAttachment } from "../../../twilio/schema"
import { internal } from "../../../_generated/api"
import { Doc, Id } from "../../../_generated/dataModel"
import {
    ActionCtx,
    internalAction,
    internalQuery,
    MutationCtx,
    QueryCtx,
} from "../../../_generated/server"
import { ConversationDataTwilio } from "../../conversationMap/types"
import { ChannelNodeTwilio } from "../channelNodeChannels/twilio"
import { ChannelNodeService } from "../../channelNode/public"
import { ContactNodeService } from "../../contactNode/public"
import { TwilioMessageType } from "../../../twilio/types"
import moment from "moment"
import { v4 as uuidv4 } from "uuid"
import { v } from "convex/values"
import { ChannelNode } from "../../channelNode/schema"
import { ConversationMapService } from "../../conversationMap/public"
import { TwilioClient } from "../../../omni/services/channels/twilioClient"
import { decrypt } from "../../../actions/utils/crypto"
import { createTwilioService } from "../../../omni/services/twilioServiceFactory"

// ┌────────────────┐
// │   TLC          │
// │    ┌───────────┤
// │    │ SubChannel│
// │    │           │
// └────┴───────────┘

// This class is used for complex UI interactions with twilio
// the twilio service is wrapped by the contact integration here
// thus we use contactNodeService and channelNodeService
export class TwilioActionsService {
    // Sending from channelNode
    static async sendMessageByConversationSid(
        ctx: ActionCtx,
        twilioService: TwilioService,
        channelNodeId: Id<"channelNode">,
        conversationSid: string,
        messageString: string,
        attachments?: TwilioAttachment[]
    ) {
        await ChannelNodeTwilio.sendMessage({
            ctx,
            twilioService,
            channelNodeId,
            conversationSid,
            messageString,
            attachments,
        })
    }

    static async sendTemplateByConversationSid(
        ctx: ActionCtx,
        twilioService: TwilioService,
        channelNodeId: Id<"channelNode">,
        conversationSid: string,
        templateConfig: TemplateConfig
    ) {
        await ChannelNodeTwilio.sendTemplate({
            ctx,
            twilioService,
            channelNodeId,
            conversationSid,
            templateConfig,
        })
    }

    static async sendMessageByChannelNodeId({
        ctx,
        workspaceId,
        twilioService,
        channelNodeId,
        provider,
        recipient,
        messageString,
        attachments,
    }: {
        ctx: ActionCtx
        workspaceId: Id<"workspaces">
        twilioService: TwilioService
        channelNodeId: Id<"channelNode">
        provider: string
        recipient: string
        messageString: string
        attachments?: TwilioAttachment[]
    }) {
        const finalConversationSid =
            await this.getOrCreateConversationSid(
                ctx,
                workspaceId,
                twilioService,
                channelNodeId,
                provider,
                recipient
            )

        // We should probably schedule this out
        await this.sendMessageByConversationSid(
            ctx,
            twilioService,
            channelNodeId,
            finalConversationSid,
            messageString,
            attachments
        )
    }

    static async sendTemplateByChannelNodeId(
        ctx: ActionCtx,
        workspaceId: Id<"workspaces">,
        twilioService: TwilioService,
        channelNodeId: Id<"channelNode">,
        provider: string,
        recipient: string,
        templateConfig: TemplateConfig
    ) {
        const finalConversationSid =
            await this.getOrCreateConversationSid(
                ctx,
                workspaceId,
                twilioService,
                channelNodeId,
                provider,
                recipient
            )

        // We should probably schedule this out
        await this.sendTemplateByConversationSid(
            ctx,
            twilioService,
            channelNodeId,
            finalConversationSid,
            templateConfig
        )
    }

    private static async getOrCreateConversationSid(
        ctx: ActionCtx,
        workspaceId: Id<"workspaces">,
        twilioService: TwilioService,
        channelNodeId: Id<"channelNode">,
        provider: string,
        recipient: string
    ): Promise<string> {
        const conversationMap = await ctx.runQuery(
            internal.contacts.conversationMap.public
                .getByChannelNodeIdAndProvider,
            { workspaceId, channelNodeId, provider }
        )

        if (conversationMap) {
            return (
                conversationMap.conversationData as ConversationDataTwilio
            ).conversationId
        } else {
            const conversation =
                await ChannelNodeTwilio.createConversation({
                    ctx,
                    twilioService,
                    workspaceId,
                    recipients: [recipient],
                    channelNodeId,
                })
            return conversation.conversationSid
        }
    }

    // Receiving messages -----------------------------------------------------

    static async receiveKnownMessage(
        ctx: MutationCtx,
        workspaceId: Id<"workspaces">,
        type: TwilioSubChannel,
        conversationSid: string,
        payload: TwilioMessageType
    ) {
        const existingConversationMap =
            await ConversationMapService.getByTypeAndConversationId(
                ctx,
                workspaceId,
                type,
                conversationSid
            )

        if (!existingConversationMap) {
            throw new Error(
                `Conversation map not found;; type: ${type}, conversationSid: ${conversationSid}`
            )
        }

        const existingChannelNode = await ctx.db.get(
            existingConversationMap.channelNodeId
        )

        if (!existingChannelNode) {
            throw new Error(
                `Channel node not found;; channelNodeId: ${existingConversationMap.channelNodeId}`
            )
        }

        await ChannelNodeTwilio.addAndPropagateUMData({
            ctx,
            channelNode: existingChannelNode,
            payload,
        })
    }

    static async receivedUnknownMessage(
        ctx: MutationCtx,
        workspaceId: Id<"workspaces">,
        type: TwilioSubChannel,
        from: string,
        recipients: string[], // This is a string of recipients if it is a group
        provider: string,
        body: string,
        dateCreated: string
    ) {
        const combinedRecipientsString = recipients.join(",")

        let existingChannelNode =
            await ChannelNodeService.getByTypeAndValue(
                ctx,
                workspaceId,
                type,
                combinedRecipientsString
            )

        if (existingChannelNode) {
            const existingConversationMap =
                await ConversationMapService.getByChannelNodeIdAndProvider(
                    ctx,
                    workspaceId,
                    existingChannelNode._id,
                    provider
                )

            if (existingConversationMap) return
        } else {
            const contactId =
                await ContactNodeService.createUnmappedContact(
                    ctx,
                    workspaceId
                )

            const channelNodeId = await ChannelNodeService.create(
                ctx,
                workspaceId,
                contactId,
                {
                    type,
                    value: combinedRecipientsString,
                }
            )

            // This always exists
            existingChannelNode = (await ctx.db.get(channelNodeId))!
        }

        await ctx.scheduler.runAfter(
            0,
            internal.contacts.services.channels.twilio
                .createConversationAddMessage,
            {
                workspaceId,
                recipients,
                messageString: body,
                channelNode: existingChannelNode,
                from,
                dateCreated,
            }
        )

        // await this.ctx.scheduler.runAfter(
        //     0,
        //     internal.actions.ai.tools.summarizer
        //         .runSummarizationAndSentimentFlow,
        //     {
        //         workspaceId: this.workspaceId,
        //         conversationId: conversationSid,
        //         subChannel: type,
        //     }
        // )
    }
}

export const createConversationAddMessage = internalAction({
    args: {
        workspaceId: v.id("workspaces"),
        recipients: v.array(v.string()),
        messageString: v.string(),
        channelNode: v.object(ChannelNode.withSystemFields),
        from: v.string(),
        dateCreated: v.string(),
    },
    handler: async (
        ctx,
        {
            workspaceId,
            recipients,
            messageString,
            channelNode,
            from,
            dateCreated,
        }
    ) => {
        const twilioService = {} as TwilioService

        const { conversationSid, participantBindings } =
            await ChannelNodeTwilio.createConversation({
                ctx,
                twilioService,
                workspaceId,
                recipients,
                channelNodeId: channelNode._id,
            })

        const messageSender = participantBindings.find(
            (participantBinding) => {
                return participantBinding.address === from
            }
        )

        if (!messageSender) {
            throw new Error(
                `ParticipantBinding not found;; participantBindings: ${participantBindings}`
            )
        }

        await ctx.runMutation(
            internal.contacts.services.channelNodeChannels.twilio
                .addAndPropagateUMData,
            {
                channelNode,
                payload: {
                    workspaceId,
                    messageSid: uuidv4(),
                    participantSid: messageSender.participantSid,
                    conversationSid,
                    body: messageString,
                    date: moment(dateCreated).utc().toISOString(),
                    isIncoming: true,
                },
            }
        )
    },
})

// Refactor this code and SMS/WA channels code to become static
export async function getTwilioService(
    ctx: ActionCtx,
    workspaceId: Id<"workspaces">,
    channelType: TwilioSubChannel
) {
    const twilioSubaccountAndProviderPhone = await ctx.runQuery(
        internal.contacts.services.channels.twilioFactory
            .getTwilioSubaccountAndProviderPhone,
        {
            workspaceId,
            channelType,
        }
    )

    const { providerPhone, subaccount } =
        twilioSubaccountAndProviderPhone

    const twilioClient = new TwilioClient(
        subaccount.accountSid,
        decrypt(subaccount.encryptedAuthToken)
    )

    return createTwilioService({
        ctx,
        workspaceId,
        channelType,
        twilioClient,
        providerPhone,
    })
}

// export const sendMessageByConversationSidInternalAction =
//     internalAction({
//         args: {
//             channelNodeId: v.id("channelNode"),
//             conversationSid: v.string(),
//             messageString: v.string(),
//             attachments: v.optional(v.array(vTwilioAttachment)),
//         },
//         handler: async (
//             ctx,
//             {
//                 channelNodeId,
//                 conversationSid,
//                 messageString,
//                 attachments,
//             }
//         ): Promise<void> => {
//             const twilioService = {} as TwilioService
//
//             await TwilioActionsService.sendMessageByConversationSid(
//                 ctx,
//                 twilioService,
//                 channelNodeId,
//                 conversationSid,
//                 messageString,
//                 attachments
//             )
//         },
//     })
//
// export const sendTemplateByConversationSidInternalAction =
//     internalAction({
//         args: {
//             channelNodeId: v.id("channelNode"),
//             conversationSid: v.string(),
//             templateConfig: v.any(),
//         },
//         handler: async (
//             ctx,
//             { channelNodeId, conversationSid, templateConfig }
//         ): Promise<void> => {
//             const twilioService = {} as TwilioService
//
//             await TwilioActionsService.sendTemplateByConversationSid(
//                 ctx,
//                 twilioService,
//                 channelNodeId,
//                 conversationSid,
//                 templateConfig
//             )
//         },
//     })
Leave a Comment