{"version":3,"file":"inbound.mjs","names":[],"sources":["../../src/call-session/inbound.ts"],"sourcesContent":["import type WebPhone from \"../index.js\";\nimport callControlCommands from \"../rc-message/call-control-commands.js\";\nimport RcMessage from \"../rc-message/rc-message.js\";\nimport type InboundMessage from \"../sip-message/inbound.js\";\nimport type OutboundMessage from \"../sip-message/outbound/index.js\";\nimport RequestMessage from \"../sip-message/outbound/request.js\";\nimport ResponseMessage from \"../sip-message/outbound/response.js\";\nimport { branch, fakeDomain, uuid } from \"../utils.js\";\nimport CallSession from \"./index.js\";\n\nclass InboundCallSession extends CallSession {\n  public constructor(webPhone: WebPhone, inviteMessage: InboundMessage) {\n    super(webPhone);\n    this.sipMessage = inviteMessage;\n    this.localPeer = inviteMessage.headers.To;\n    this.remotePeer = inviteMessage.headers.From;\n    this.direction = \"inbound\";\n    this.state = \"ringing\";\n    this.emit(\"ringing\");\n  }\n\n  // for inbound calls from call queue, there might be p-rc-api-call-info header:\n  // p-rc-api-call-info: callAttributes=queue-call,reject;callerIdName=WIRELESS CALLER;displayInfo=queueName;displayInfoSub=callerIdName;queueName=Tyler's call queue\n  // when there is no such a header, the method returns undefined\n  public get rcApiCallInfo() {\n    if (!this.sipMessage.headers[\"p-rc-api-call-info\"]) {\n      return undefined;\n    }\n    return Object.fromEntries(\n      this.sipMessage.headers[\"p-rc-api-call-info\"]\n        .split(\";\")\n        .map((pair) => pair.trim())\n        .filter(Boolean)\n        .map((pair) => {\n          const [key, ...rest] = pair.split(\"=\");\n          return [key, rest.join(\"=\")]; // Handles '=' in value\n        }),\n    ) as { callerIdName?: string; queueName?: string };\n  }\n\n  public async confirmReceive() {\n    await this.sendRcMessage(callControlCommands.ClientReceiveConfirm);\n  }\n\n  public async toVoicemail() {\n    await this.sendRcMessage(callControlCommands.ClientVoicemail);\n    // wait for outbound reply to CANCEL\n    return new Promise<void>((resolve) => {\n      const handler = (outboundMessage: OutboundMessage) => {\n        if (\n          outboundMessage.headers[\"Call-Id\"] === this.callId &&\n          outboundMessage.headers.CSeq.endsWith(\" CANCEL\")\n        ) {\n          this.webPhone.sipClient.off(\"outboundMessage\", handler);\n          resolve();\n        }\n      };\n      this.webPhone.sipClient.on(\"outboundMessage\", handler);\n    });\n  }\n\n  public async decline() {\n    await this.sendRcMessage(callControlCommands.ClientReject);\n    // wait for outbound reply to CANCEL\n    return new Promise<void>((resolve) => {\n      const handler = (outboundMessage: OutboundMessage) => {\n        if (\n          outboundMessage.headers[\"Call-Id\"] === this.callId &&\n          outboundMessage.headers.CSeq.endsWith(\" CANCEL\")\n        ) {\n          this.webPhone.sipClient.off(\"outboundMessage\", handler);\n          resolve();\n        }\n      };\n      this.webPhone.sipClient.on(\"outboundMessage\", handler);\n    });\n  }\n\n  public async forward(target: string) {\n    await this.sendRcMessage(callControlCommands.ClientForward, {\n      FwdDly: \"0\",\n      Phn: target,\n      PhnTp: \"3\",\n    });\n    // wait for the final SIP message\n    return new Promise<void>((resolve) => {\n      const handler = (inboundMessage: InboundMessage) => {\n        if (inboundMessage.subject.startsWith(\"CANCEL sip:\")) {\n          this.webPhone.sipClient.off(\"inboundMessage\", handler);\n          resolve();\n        }\n      };\n      this.webPhone.sipClient.on(\"inboundMessage\", handler);\n    });\n  }\n\n  public async startReply() {\n    await this.sendRcMessage(callControlCommands.ClientStartReply);\n  }\n  public async reply(text: string): Promise<RcMessage> {\n    await this.sendRcMessage(callControlCommands.ClientReply, {\n      RepTp: \"0\",\n      Bdy: text,\n    });\n    return new Promise((resolve) => {\n      const sessionCloseHandler = async (inboundMessage: InboundMessage) => {\n        if (inboundMessage.subject.startsWith(\"MESSAGE sip:\")) {\n          const rcMessage = await RcMessage.fromXml(inboundMessage.body);\n          if (\n            rcMessage.headers.Cmd ===\n            callControlCommands.SessionClose.toString()\n          ) {\n            this.webPhone.sipClient.off(\"inboundMessage\", sessionCloseHandler);\n            resolve(rcMessage);\n            // no need to dispose session here, session will dispose unpon CANCEL or BYE\n          }\n        }\n      };\n      this.webPhone.sipClient.on(\"inboundMessage\", sessionCloseHandler);\n    });\n  }\n\n  public async answer() {\n    await this.init();\n\n    // most INVITE message will have a body with SDP offer.\n    if (this.sipMessage.body.length > 0) {\n      await this.rtcPeerConnection.setRemoteDescription({\n        type: \"offer\",\n        sdp: this.sipMessage.body,\n      });\n      const answer = await this.rtcPeerConnection.createAnswer();\n      await this.rtcPeerConnection.setLocalDescription(answer);\n      await this.waitForIceGatheringComplete();\n\n      const newMessage = new ResponseMessage(this.sipMessage, {\n        responseCode: 200,\n        headers: {\n          \"Content-Type\": \"application/sdp\",\n        },\n        body: this.rtcPeerConnection.localDescription!.sdp,\n      });\n      await this.webPhone.sipClient.reply(newMessage);\n    } else {\n      // some INVITE message has an empty body. For example, when you invoke RESTful API /pickup to answer a call from a call queue\n      const offer = await this.rtcPeerConnection.createOffer({\n        iceRestart: true,\n      });\n      await this.rtcPeerConnection.setLocalDescription(offer);\n      await this.waitForIceGatheringComplete();\n\n      const newMessage = new ResponseMessage(this.sipMessage, {\n        responseCode: 200,\n        headers: {\n          \"Content-Type\": \"application/sdp\",\n        },\n        body: this.rtcPeerConnection.localDescription!.sdp,\n      });\n      const ackMessage = await this.webPhone.sipClient.request(\n        newMessage as RequestMessage,\n      );\n      this.sipMessage = ackMessage;\n      this.rtcPeerConnection.setRemoteDescription({\n        type: \"answer\",\n        sdp: ackMessage.body,\n      });\n    }\n\n    this.state = \"answered\";\n    this.emit(\"answered\");\n\n    // wait for the final SIP message\n    return new Promise<void>((resolve) => {\n      const handler = async (inboundMessage: InboundMessage) => {\n        if (inboundMessage.subject.startsWith(\"MESSAGE sip:\")) {\n          const rcMessage = await RcMessage.fromXml(inboundMessage.body);\n          if (\n            rcMessage.headers.Cmd ===\n            callControlCommands.AlreadyProcessed.toString()\n          ) {\n            this.webPhone.sipClient.off(\"inboundMessage\", handler);\n            resolve();\n          }\n        }\n      };\n      this.webPhone.sipClient.on(\"inboundMessage\", handler);\n    });\n  }\n\n  protected async sendRcMessage(\n    cmd: number,\n    body:\n      | Record<string | number | symbol, never>\n      | {\n          RepTp: string;\n          Bdy: string;\n        }\n      | {\n          FwdDly: string;\n          Phn: string;\n          PhnTp: string;\n        } = {},\n  ) {\n    if (!this.sipMessage.headers[\"P-rc\"]) {\n      return;\n    }\n    const rcMessage = await RcMessage.fromXml(this.sipMessage.headers[\"P-rc\"]);\n    const newRcMessage = new RcMessage(\n      {\n        SID: rcMessage.headers.SID,\n        Req: rcMessage.headers.Req,\n        From: rcMessage.headers.To,\n        To: rcMessage.headers.From,\n        Cmd: cmd.toString(),\n      },\n      {\n        Cln: this.webPhone.sipInfo.authorizationId,\n        ...body,\n      },\n    );\n    const requestSipMessage = new RequestMessage(\n      `MESSAGE sip:${newRcMessage.headers.To} SIP/2.0`,\n      {\n        Via: `SIP/2.0/WSS ${fakeDomain};branch=${branch()}`,\n        To: `<sip:${newRcMessage.headers.To}>`,\n        From: `<sip:${this.webPhone.sipInfo.username}@${this.webPhone.sipInfo.domain}>;tag=${uuid()}`,\n        \"Call-Id\": this.callId,\n        \"Content-Type\": \"x-rc/agent\",\n      },\n      newRcMessage.toXml(),\n    );\n    await this.webPhone.sipClient.request(requestSipMessage);\n  }\n}\n\nexport default InboundCallSession;\n"],"mappings":"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"}