import type { RcVDialInNumberObj } from '@ringcentral-integration/commons/interfaces/Rcv.model'; import { format } from '@ringcentral-integration/utils'; import { formatMeetingId } from './formatMeetingId'; import i18n from './i18n'; import type { CommonBrand, DialInSectionParams, FormatToHtmlOptions, ParcelledLink, RcmMainParams, RcvMainParams, TplResult, } from './index.interface'; function formatSmartphones( dialInNumber: string | RcVDialInNumberObj[], pinNumber: string, showMeetingPasswordPSTN: boolean, meetingPasswordPSTN: string, ) { if (!dialInNumber || dialInNumber.length === 0) { return ''; } if (typeof dialInNumber === 'string') { return `${dialInNumber},,${pinNumber}#${ showMeetingPasswordPSTN ? `,,${meetingPasswordPSTN}#` : '' }`; } return dialInNumber .map((obj) => { const passwordField = showMeetingPasswordPSTN ? `,,${meetingPasswordPSTN}#` : ''; const locationField = obj?.country?.name && obj.location ? `${obj.country.name} (${obj.location})` : obj?.country?.name || ''; return `${obj.phoneNumber},,${pinNumber}#${passwordField} ${locationField}`; }) .join('\n\t'); } function formatDialInNumber(dialInNumber: string | RcVDialInNumberObj[]) { if (!dialInNumber || dialInNumber.length === 0) { return ''; } if (typeof dialInNumber === 'string') { return dialInNumber; } return dialInNumber .map((obj) => { const locationField = obj?.country?.name && obj.location ? `${obj.country.name} (${obj.location})` : obj?.country?.name || ''; return `${obj.phoneNumber} ${locationField}`; }) .join('\n\t'); } function getPasswordTpl( meetingPassword: string, currentLocale: string, ): string { const passwordLiteral = i18n.getString('password', currentLocale); return meetingPassword ? `${passwordLiteral}: ${meetingPassword}` : ''; } /** * replace all text link into anchor link * Should match: http://www.example.com * Should not match: http://www.example.com * Then replace it into http://www.example.com * @param input */ function replaceTextLinksToAnchors(input: string): string { /** * [^<>\]]+ means should match any characters except < or > or ] * (?!\s*<\/a>) means url should not be followed by either "" or " " * (?!"[^>]*>) means url should not followed by "> * further explanation: origin string: http://www.example.com should not match * (?=[\s!,?\]]|$) means url can be followed by punctuations or whitespace or nothing */ // https://stackoverflow.com/questions/19060460/url-replace-with-anchor-not-replacing-existing-anchors const pattern = /(?:(?:ht|f)tps?:\/\/|www)[^<>\]]+(?!\s*<\/a>)(?!"[^>]*>)(?=[\s!,?\]<]|$)/gim; return input.replace(pattern, ($0: string): string => { return `${$0}`; }); } const htmlNewLine = '
'; const htmlIndentation = ' '; const htmlTabIndentation: string = htmlIndentation.repeat(4); function formatTextToHtml( plantText: string, options: FormatToHtmlOptions = {}, ): string { const { links = [], uselessSentences = [], searchLinks = false, newLine = htmlNewLine, indentation = htmlIndentation, tabIndentation = htmlTabIndentation, } = options; let htmlContent = plantText .replace(/\r\n|\n|\r/g, '\n') // formalize newline .split('\n') // split with formalized newline .map((line) => { return line .replace(/\t/g, tabIndentation) // replace all Tab with 4 indentations .replace(/^\s*/, ($0) => indentation.repeat($0.length)); // replace leading white spaces with indentations }) .join(newLine); uselessSentences.forEach((sentence) => { if (sentence) { htmlContent = htmlContent.replace(sentence, ''); } }); links.forEach((link) => { if (link) { const isPlantLink = typeof link === 'string'; const uri = isPlantLink ? link : link.uri; const text = isPlantLink ? link : link.text; if (uri) { htmlContent = htmlContent.replace( uri, `${text || uri}`, ); } } }); if (searchLinks) { htmlContent = replaceTextLinksToAnchors(htmlContent); } return htmlContent; } /** * Dial-in password: ${passwordPstn} */ function getRcvPstnPasswordTpl( meetingPasswordPSTN: string, currentLocale: string, ): string { const passwordPstnLiteral = i18n.getString('passwordPstn', currentLocale); return `${passwordPstnLiteral} ${meetingPasswordPSTN}`; } function getBaseRcmTpl( { meeting, serviceInfo, extensionInfo, invitationInfo }: RcmMainParams, brand: CommonBrand, currentLocale: string, addNoModifyAlert = false, ): TplResult { const accountName = extensionInfo.name; const meetingId = meeting.id; const joinUri = meeting.links.joinUri; const password = meeting.password; const mobileDialingNumberTpl = serviceInfo.mobileDialingNumberTpl; const phoneDialingNumberTpl = serviceInfo.phoneDialingNumberTpl; const passwordTpl = getPasswordTpl(password, currentLocale); const teleconference = brand.brandConfig.teleconference?.toString() ?? ''; const prefix = addNoModifyAlert ? `${i18n.getString('doNotModify', currentLocale)}\n` : ''; let formattedMsg = invitationInfo?.invitation; if (!formattedMsg) { formattedMsg = format( i18n.getString('inviteMeetingContent', currentLocale), { accountName, brandName: brand.name, joinUri, passwordTpl, mobileDialingNumberTpl, phoneDialingNumberTpl, meetingId: formatMeetingId(meetingId), teleconference, }, ); } return { formattedMsg: `${prefix}${formattedMsg}`, links: { joinUri, teleconference, }, }; } function getRcmEventTpl( mainInfo: RcmMainParams, brand: CommonBrand, currentLocale: string, addNoModifyAlert = false, ): string { const tplResult = getBaseRcmTpl( mainInfo, brand, currentLocale, addNoModifyAlert, ); return tplResult.formattedMsg; } function getRcmHtmlEventTpl( mainInfo: RcmMainParams, brand: CommonBrand, currentLocale: string, addNoModifyAlert = false, ): string { const tplResult = getBaseRcmTpl( mainInfo, brand, currentLocale, addNoModifyAlert, ); return formatTextToHtml(tplResult.formattedMsg, { links: [tplResult.links.joinUri, tplResult.links.teleconference], }); } /* Outcome example: One tap to join audio only from a smartphone: +16504191505,,977988816#,,3893596796# United States (San Mateo, CA) Or dial: +1 (650) 4191505 United States (San Mateo, CA) Access Code / Meeting ID: 977988816 Dial-in password: 3893596796 */ function formatDialInSection({ dialInNumber, isMeetingSecret, meetingPasswordPSTN, shortId, currentLocale, }: DialInSectionParams) { /* TODO: after get the translation, remove rcvInviteMeetingContentDial * rcvInviteMeetingContentCountryDial is the correct one */ const dialingString = i18n.getString( typeof dialInNumber === 'string' ? 'rcvInviteMeetingContentDial' : 'rcvInviteMeetingContentCountryDial', currentLocale, ); const showMeetingPasswordPSTN = !!(isMeetingSecret && meetingPasswordPSTN); const dialingInfo = format(dialingString, { smartphones: formatSmartphones( dialInNumber, shortId, showMeetingPasswordPSTN, meetingPasswordPSTN, ), dialNumber: formatDialInNumber(dialInNumber), pinNumber: formatMeetingId(shortId), }); const pstnPasswordTpl = !showMeetingPasswordPSTN ? '' : getRcvPstnPasswordTpl(meetingPasswordPSTN, currentLocale); return `${dialingInfo}${pstnPasswordTpl}`; } // RCINT-22191 hotfix // The feature need us to extract just the dial-in and teleconference section // Although the logic is OK for common usage, the requirement is specific // There is no any evidence that the other projects will need this // Therefore, this is reserved for the calendar-update-tool project function getRcvDialInInfo({ rcvTeleconference, ...args }: DialInSectionParams & { rcvTeleconference: string }) { const hasDialInNumber = args?.dialInNumber?.length > 0; const dialInSection = hasDialInNumber ? formatDialInSection(args) : ''; const teleconferenceInfo = format( i18n.getString('rcvTeleconference', args.currentLocale), { teleconference: rcvTeleconference, }, ); return `${dialInSection}${teleconferenceInfo}`; } function getBaseRcvTpl( { meeting, extensionInfo, dialInNumber, invitationInfo }: RcvMainParams, brand: CommonBrand, currentLocale: string, enableRcvConnector = false, enableE2EE = false, addNoModifyAlert = false, ): TplResult { const prefix = addNoModifyAlert ? `${i18n.getString('doNotModify', currentLocale)}\n` : ''; const joinUri = meeting.joinUri; const teleconference = brand.rcvTeleconference; if (invitationInfo) { return { formattedMsg: `${prefix}${invitationInfo}`, links: { joinUri, // @ts-expect-error TS(2322): Type 'string | undefined' is not assignable to typ... Remove this comment to see the full error message teleconference, }, }; } const accountName = extensionInfo.name; const { meetingPassword, meetingPasswordPSTN, isMeetingSecret, e2ee } = meeting; const meetingContent: Array = []; const showMeetingPasswordPSTN = !!(isMeetingSecret && meetingPasswordPSTN); if (enableE2EE && e2ee) { meetingContent.push( i18n.getString('rcvE2EEInviteMeetingContent', currentLocale), ); const formattedMsg = format(meetingContent.join(''), { accountName, brandName: brand.name, rcvProductName: brand.brandConfig.rcvProductName, joinUri, e2EESupportLinkText: format( i18n.getString('e2EESupportLinkText', currentLocale), { brandName: brand.name, }, ), rcvE2EESupportUrl: brand.rcvE2EESupportUrl, }); return { formattedMsg: `${prefix}${formattedMsg}`, links: { joinUri, // @ts-expect-error TS(2322): Type 'string | undefined' is not assignable to typ... Remove this comment to see the full error message teleconference, }, }; } meetingContent.push(brand.brandConfig.rcvInviteMeetingContent as string); if (dialInNumber && dialInNumber.length > 0) { /* TODO: after get the translation, remove rcvInviteMeetingContentDial * rcvInviteMeetingContentCountryDial is the correct one */ meetingContent.push( i18n.getString( typeof dialInNumber === 'string' ? 'rcvInviteMeetingContentDial' : 'rcvInviteMeetingContentCountryDial', currentLocale, ), ); if (showMeetingPasswordPSTN) { meetingContent.push( getRcvPstnPasswordTpl(meetingPasswordPSTN, currentLocale), ); } } meetingContent.push(`${i18n.getString('rcvTeleconference', currentLocale)}`); const passwordTpl = isMeetingSecret ? getPasswordTpl(meetingPassword, currentLocale) : ''; if (enableRcvConnector) { meetingContent.push(`${i18n.getString('rcvSipHeader', currentLocale)}`); const rcvSipContent = isMeetingSecret ? 'rcvSipContentWithPwd' : 'rcvSipContentNoPwd'; meetingContent.push(`${i18n.getString(rcvSipContent, currentLocale)}`); } const shortId = meeting.shortId; const formattedMsg = format(meetingContent.join(''), { accountName, brandName: brand.brandConfig.rcvBrandName ?? brand.name, joinUri, passwordTpl, meetingPasswordPSTN, meetingId: shortId, pinNumber: formatMeetingId(shortId), teleconference, dialNumber: formatDialInNumber(dialInNumber), smartphones: formatSmartphones( dialInNumber, shortId, showMeetingPasswordPSTN, meetingPasswordPSTN, ), rcvProductName: brand.brandConfig.rcvProductName, }); return { formattedMsg: `${prefix}${formattedMsg}`, links: { joinUri, // @ts-expect-error TS(2322): Type 'string | undefined' is not assignable to typ... Remove this comment to see the full error message teleconference, }, }; } function getRcvEventTpl( mainInfo: RcvMainParams, brand: CommonBrand, currentLocale: string, enableRcvConnector = false, enableE2EE = false, addNoModifyAlert = false, ): string { const tplResult = getBaseRcvTpl( mainInfo, brand, currentLocale, enableRcvConnector, enableE2EE, addNoModifyAlert, ); return tplResult.formattedMsg; } function getRcvHtmlEventTpl( mainInfo: RcvMainParams, brand: CommonBrand, currentLocale: string, enableRcvConnector = false, enableE2EE = false, ): string { const tplResult = getBaseRcvTpl( mainInfo, brand, currentLocale, enableRcvConnector, enableE2EE, ); const links: [string, string, ParcelledLink] = [ tplResult.links.joinUri, tplResult.links.teleconference, { // @ts-expect-error TS(2322): Type 'string | undefined' is not assignable to typ... Remove this comment to see the full error message uri: brand.rcvE2EESupportUrl, text: format(i18n.getString('e2EESupportLinkText', currentLocale), { brandName: brand.name, }), }, ]; return formatTextToHtml(tplResult.formattedMsg, { uselessSentences: [ `${format(i18n.getString('e2EESupportLinkText', currentLocale), { brandName: brand.name, })}
`, ], links, }); } function getMeetingId( meetingUri: string, rcvUriRegExp: RegExp, rcmUriRegExp: RegExp, ): string { if (meetingUri) { const regs = [rcmUriRegExp, rcvUriRegExp]; for (let i = 0; i < regs.length; i += 1) { const matches = regs[i].exec(meetingUri); if (matches && matches.length > 0) { const match0 = matches[0]; const link = ( match0.indexOf('?') > -1 ? matches[0].substring(0, matches[0].indexOf('?')) : match0 ).split('/'); const id = link[link.length - 1]; return id; } } } // @ts-expect-error TS(2322): Type 'null' is not assignable to type 'string'. return null; } function stripMeetingLinks( text: string, rcvUriRegExp: RegExp, rcmUriRegExp: RegExp, ): string { let result = text; [rcmUriRegExp, rcvUriRegExp].forEach((reg) => { while (reg.test(result)) { result = result.replace(reg, ''); } }); return result; } function meetingLinkContains( rcvUriRegExp: RegExp, rcmUriRegExp: RegExp, text?: string, ): { hasRCM: boolean; hasRCV: boolean } { return { hasRCM: rcmUriRegExp.test(text ?? ''), hasRCV: rcvUriRegExp.test(text ?? ''), }; } export { formatMeetingId, formatTextToHtml, getBaseRcvTpl, getMeetingId, getRcmEventTpl, getRcmHtmlEventTpl, getRcvDialInInfo, getRcvEventTpl, getRcvHtmlEventTpl, htmlIndentation, htmlNewLine, htmlTabIndentation, meetingLinkContains, replaceTextLinksToAnchors, stripMeetingLinks, };