import { cloneDeep, isEmpty, olog, fixTo2Decimals, pick, objectMapper } from "@/utils/commons"; import dayjs from "dayjs"; import { v4 as uuid } from "uuid"; function removeFirstPlus(str) { if (str.startsWith('+')) { return str.slice(1); } return str; } export const WABAccounts = [ { "id": "217973041403372", "phoneNumber": "+8618174165365", "wabaId": "190290134156880", "verifiedName": "Global Highlights Multilanguage", "qualityRating": "GREEN", "messagingLimit": "TIER_1K", "isOfficialBusinessAccount": false, "codeVerificationStatus": "EXPIRED", "status": "CONNECTED", "displayPhoneNumber": "+86 181 7416 5365", "nameStatus": "APPROVED", "newNameStatus": "NONE" }, { "id": "160079783860667", "phoneNumber": "+8617607730395", "wabaId": "190290134156880", "verifiedName": "Global Highlights", "qualityRating": "GREEN", "messagingLimit": "TIER_1K", "isOfficialBusinessAccount": false, "codeVerificationStatus": "EXPIRED", "status": "CONNECTED", "displayPhoneNumber": "+86 176 0773 0395", "nameStatus": "APPROVED", "newNameStatus": "NONE", "decision": "DEFERRED", "requestedVerifiedName": "Global Highlights", "rejectionReason": "NONE" } ]; export const WABAccountsMapped = WABAccounts.reduce((a, c) => ({ ...a, [removeFirstPlus(c.phoneNumber)]: c, [c.phoneNumber]: c }), {}) export const replaceTemplateString = (str, replacements) => { let result = str; let keys = str.match(/{{(.*?)}}/g).map(key => key.replace(/{{|}}/g, '')); for (let i = 0; i < keys.length; i++) { let replaceValue = replacements[i]; let template = new RegExp(`{{${keys[i]}}}`, 'g'); result = result.replace(template, replaceValue); } return result; } /** * @deprecated 在渲染时处理 */ export const autoLinkText = (text) => { return text; // let regex = /(https?:\/\/[^\s]+)/g; // let newText = text.replace(regex, '$1'); // return newText; } /** * // +8618777396951 lyj { "to": "+8613317835586", // qqs "msgtype": "text", "msgcontent": "{\"body\":\"txtmsgtest\"}" } */ const mediaMsg = { contentToSend: (msg) => ({ action: 'message', actionId: msg.id, renderId: msg.id, externalId: msg.externalId, to: msg.to, from: msg.from, msgcontent: { [msg.type]: { link: msg.data.dataUri, ...(msg.text ? { caption: msg.text } : {}), ...(msg.type === 'document' ? { filename: msg.name } : {}) }, ...(msg.context ? { context: msg.context, message_origin: msg.message_origin.msgOrigin } : {}), }, }), contentToRender: (msg) => ({ ...msg, actionId: msg.id, conversationid: msg.id.split('.')[0], data: { ...msg.data, status: { download: msg.data?.loading ? false : true, click: true, loading: msg.data.loading } }, ...(msg.context ? { reply: { id: msg.message_origin.id, message: msg.message_origin.text, title: msg.message_origin.senderName || 'Reference', titleColor: msg.message_origin?.senderName !== 'me' ? '#a791ff' : '#128c7e', }, } : {}), }), }; export const sentMsgTypeMapped = { text: { type: 'text', contentToSend: (msg) => ({ action: 'message', actionId: msg.id, renderId: msg.id, externalId: msg.externalId, to: msg.to, from: msg.from, msgtype: 'text', msgcontent: { body: msg.text, preview_url: true, ...(msg.context ? { context: msg.context, message_origin: msg.message_origin?.msgOrigin || msg.message_origin } : {}) }, }), contentToRender: (msg) => ({ ...msg, whatsapp_msg_type: 'text', actionId: msg.id, conversationid: msg.id.split('.')[0], originText: msg.text, text: autoLinkText(msg.text), ...(msg.context ? { reply: { id: msg.message_origin.id, message: msg.message_origin.text, title: msg.message_origin.senderName || 'Reference', titleColor: msg.message_origin?.senderName !== 'me' ? '#a791ff' : '#128c7e', photoURL: msg.message_origin?.data?.uri || '', }, } : {}), }), }, photo: { type: 'image', contentToSend: (msg) => ({ ...mediaMsg.contentToSend({ ...msg, type: 'image' }), msgtype: 'image', }), contentToRender: (msg) => ({ ...msg, ...mediaMsg.contentToRender(msg), whatsapp_msg_type: 'image', }), }, sticker: { type: 'sticker', contentToSend: (msg) => ({ ...mediaMsg.contentToSend({ ...msg, type: 'sticker' }), msgtype: 'sticker', }), contentToRender: (msg) => ({ ...msg, ...mediaMsg.contentToRender(msg), whatsapp_msg_type: 'sticker', type: 'photo', }), }, video: { type: 'video', contentToSend: (msg) => ({ ...mediaMsg.contentToSend(msg), msgtype: 'video', }), contentToRender: (msg) => ({ ...msg, ...mediaMsg.contentToRender(msg), whatsapp_msg_type: 'video', }), }, document: { type: 'document', contentToSend: (msg) => ({ ...mediaMsg.contentToSend(msg), msgtype: 'document', }), contentToRender: (msg) => ({ ...msg, ...mediaMsg.contentToRender(msg), text: (msg?.name || '') + `\n${msg?.text || ''}`, title: msg?.name || '', originText: msg?.name || '', whatsapp_msg_type: 'document', type: 'file', }), }, whatsappTemplate: { contentToSend: (msg) => ({ action: 'message', actionId: msg.id, renderId: msg.id, externalId: msg.externalId, to: msg.to, from: msg.from, msgtype: 'template', msgcontent: { ...msg.template, components: [ ...msg.template.components.filter((com) => !['footer', 'buttons'].includes(com.type.toLowerCase())), ...(msg.template.components.filter((com) => 'buttons' === com.type.toLowerCase()).length > 0 ? msg.template.components .filter((com) => 'buttons' === com.type.toLowerCase())[0] // .buttons.filter((btns) => ! ['phone_number', 'url'].includes( btns.type.toLowerCase())) .buttons.filter((btns) => !isEmpty(btns.example)) // 静态按钮不发 .map((btn, btnI) => ({ type: 'button', sub_type: btn.type.toLowerCase(), index: btnI, // parameters: [{ text: 'lq1FTtA8', type: 'text' }] })) : []), ], }, }), contentToRender: (msg) => { const templateDataMapped = msg.template?.components ? msg.template.components.reduce((r, v) => ({ ...r, [v.type]: v }), {}) : null; // const templateParam = (templateDataMapped?.body?.parameters || []).map(e => e.text); // const fillTemplate = templateParam.length ? replaceTemplateString(msg.template_origin.components.body?.[0]?.text || '', templateParam) : (msg.template_origin.components.body?.[0]?.text || ''); // const footer = msg.template_origin.components?.footer?.[0]?.text || ''; return { ...msg, template: { ...msg.template, components: [ ...msg.template.components.filter((com) => !['footer', 'buttons'].includes(com.type.toLowerCase())), ...(msg.template.components.filter((com) => 'buttons' === com.type.toLowerCase()).length > 0 ? msg.template.components .filter((com) => 'buttons' === com.type.toLowerCase())[0] // .buttons.filter((btns) => ! ['phone_number', 'url'].includes( btns.type.toLowerCase())) .buttons.filter((btns) => !isEmpty(btns.example)) // 静态按钮不发 .map((btn, btnI) => ({ type: 'button', sub_type: btn.type.toLowerCase(), index: btnI, // parameters: [{ text: 'lq1FTtA8', type: 'text' }] })) : []), ], }, actionId: msg.id, conversationid: msg.id.split('.')[0], type: 'text', title: msg.template.name, // || msg.template_origin.components.header?.[0]?.text || '', text: autoLinkText(templateDataMapped?.body?.text || ''), // msg.template_origin.components.body?.[0]?.text || '', whatsapp_msg_type: 'template', }; }, }, email: { type: 'email', // contentToSend: (msg) => ({ // action: 'message', // actionId: msg.id, // renderId: msg.id, // to: msg.to, // msgtype: 'email', // msgcontent: { body: msg.text, preview_url: true, }, // }), contentToRender: (msg) => ({ ...msg, whatsapp_msg_type: '', actionId: msg.id, conversationid: msg.conversationid, originText: msg.text, text: (msg.text), msgtext: { ...msg, }, msgOrigin: { ...msg, }, }), }, }; const whatsappMsgMapped = { 'whatsapp.inbound_message.received': { getMsg: (result) => { // console.log('whatsapp.inbound_message.received', result); const data1 = pick(result, ['conversationid', 'opi_sn', 'coli_sn', 'coli_id']) return isEmpty(result?.whatsappInboundMessage) ? null : { ...result.whatsappInboundMessage, ...data1, messageorigin: result.messageorigin, msg_source: 'WABA', msg_direction: 'inbound' } }, contentToRender: (contentObj) => { // console.log('whatsapp.inbound_message.received to render', contentObj); return parseRenderMessageItem(contentObj) }, contentToUpdate: () => null, }, 'whatsapp.message.updated': { getMsg: (result) => { // console.log('getMsg', result); return isEmpty(result?.whatsappMessage) ? null : { ...result.whatsappMessage, conversationid: result.conversationid, messageorigin: result.messageorigin, msg_source: 'WABA', msg_direction: 'outbound' } }, contentToRender: (contentObj) => { if (contentObj?.status === 'failed' && ['130472', 'BAD_REQUEST'].includes(contentObj.errorCode)) { contentObj = { ...contentObj, type: 'error', text: { body: `❌ ${whatsappError?.[contentObj.errorCode] || contentObj.errorMessage}` }, // contentObj.errorMessage // Message failed to send. id: contentObj.id, wamid: contentObj.id, } return parseRenderMessageItem(contentObj) } // * 仅更新消息状态, 没有输出 return null }, contentToUpdate: (msgcontent) => ({ ...msgcontent, ...parseRenderMessageItem(msgcontent), id: msgcontent.wamid, status: msgStatusRenderMapped[msgcontent?.status || 'failed'], sender: 'me', dateString: msgcontent.status === 'failed' ? `发送失败 ${whatsappError?.[msgcontent.errorCode] || msgcontent.errorMessage || ''} ❌` : '', }), }, 'wai.message.received': { getMsg: (result) => { const data1 = pick(result, ['conversationid', 'opi_sn', 'coli_sn', 'coli_id']) return isEmpty(result?.waiMessage) ? null : { ...result.waiMessage, ...data1, messageorigin: result.messageorigin, msg_source: 'wai', ...objectMapper(result.waiMessage, { direction: { key: 'msg_direction' } }) } }, contentToRender: (contentObj) => { return parseRenderMessageItem(contentObj) }, contentToUpdate: () => null, }, 'wai.message.updated': { getMsg: (result) => { return isEmpty(result?.waiMessage) ? null : { ...result.waiMessage, conversationid: result.conversationid, messageorigin: result.messageorigin, msg_source: 'wai', ...objectMapper(result.waiMessage, { direction: { key: 'msg_direction' } }), } }, contentToRender: (contentObj) => { if (contentObj?.status === 'failed') { contentObj = { ...contentObj, type: 'error', text: { body: `❌` }, // contentObj.errorMessage // Message failed to send. id: contentObj.id, wamid: contentObj.id, } return parseRenderMessageItem(contentObj) } // * 仅更新消息状态, 没有输出 return null }, contentToUpdate: (msgcontent) => ({ ...msgcontent, ...parseRenderMessageItem(msgcontent), id: msgcontent.wamid, status: msgcontent.msg_direction === 'outbound' ? msgStatusRenderMapped[msgcontent?.status || 'failed'] : '', sender: msgcontent.msg_direction === 'outbound' ? 'me' : msgcontent?.customerProfile?.name || '', dateString: msgcontent.status === 'failed' ? `发送失败 ❌` : '', }), }, 'wai.creds.update': { getMsg: (result) => { return isEmpty(result?.waiMessage) ? {} : { ...result.waiMessage, conversationid: result.conversationid, msg_source: 'wai', } }, contentToRender: (contentObj) => null, contentToUpdate: (msgcontent) => null, contentToNotify: (contentObj) => { return { ...contentObj, status: contentObj?.status || '', key: contentObj.to || '', content: `WhatsApp号码: ${contentObj.to}`, title: (contentObj.status === 'offline') ? `WhatsApp 断开连接` : '', type: (contentObj.status === 'offline') ? 'warning' : 'info', }; }, }, } const emailMsgMapped = { 'email.inbound.received': { getMsg: (result) => { // console.log('email.inbound.received', result); const data1 = pick(result, ['conversationid', 'opi_sn', 'coli_sn', 'coli_id']); return isEmpty(result?.emailMessage) ? null : { ...result.emailMessage, ...data1, msg_source: 'email', msg_direction: 'inbound' }; }, contentToRender: (contentObj) => { // console.log('email.inbound.received to render', contentObj); return parseRenderMessageItem(contentObj); }, contentToUpdate: (msgcontent) => null, }, 'email.updated': { getMsg: (result) => { // console.log('email.updated', result); const { emailMessage } = result; const data1 = pick(result, ['conversationid', 'opi_sn', 'coli_sn', 'coli_id']); return isEmpty(result?.emailMessage) ? null : { ...emailMessage, ...data1, msg_source: 'email', msg_direction: 'outbound' }; }, contentToRender: (contentObj) => null, contentToUpdate: (msgcontent) => ({ ...msgcontent, ...parseRenderMessageItem({...msgcontent, }), id: msgcontent.id, status: msgStatusRenderMapped[(msgcontent?.status || 'failed')], sender: 'me', dateString: msgcontent.status==='failed' ? `发送失败 ❌` : '', }), }, 'email.action.received': { getMsg: (result) => { return isEmpty(result?.emailMessage) ? null : { ...result.emailMessage, id: result.id }; }, contentToRender: (contentObj) => null, contentToUpdate: (msgcontent) => null, contentToNotify: (contentObj) => { return { ...contentObj, status: contentObj?.status || 'failed', key: contentObj.email || contentObj.from || '', content: `${contentObj.email || contentObj.from || '未知邮箱'} ${contentObj?.error?.message || ''}`, title: (contentObj.status === 'failed') ? `接收邮件失败` : '', type: (contentObj.status === 'failed') ? 'warning' : 'info', }; }, } } export const msgStatusRenderMapped = { 'accepted': 'waiting', // 'sent', // 接口的发送请求 'sent': 'sent', 'delivered': 'received', 'read': 'read', 'failed': 'failed', 'send': 'sent', }; export const msgStatusRenderMappedCN = { 'accepted': '[发送ing]', 'sent': '[已发送]', 'delivered': '[已送达]', 'read': '[已读]', 'failed': '❗', }; export const receivedMsgTypeMapped = { ...cloneDeep(whatsappMsgMapped), 'message': { // 发送消息的同步记录 status: 'accepted' getMsg: (result) => ({ ...result, conversationid: result.actionId.split('.')[0] }), contentToRender: () => null, contentToUpdate: (msgcontent) => ({ ...msgcontent, actionId: msgcontent.actionId, id: msgcontent.wamid, status: msgStatusRenderMapped[(msgcontent?.status || 'failed')], conversationid: msgcontent.actionId.split('.')[0], // msgcontent.conversation.sn, date: msgcontent.createTime, sender: 'me', }), }, 'error': { // 发送消息的同步返回: 发送失败时 getMsg: (result) => result, contentToRender: () => null, contentToUpdate: (msgcontent) => { let apiErrorCode, apiErrorMsg = ''; const waCode = msgcontent.error.message.match(/\(#(\d+)\)/); let waError = whatsappError?.[waCode?.[1]] || msgcontent.error.message; apiErrorMsg = whatsappError[msgcontent.error.code]; if (msgcontent.error.message.includes('Invalid E.146 phone number')) { waError = whatsappError.INVALID_PHONE_NUMBER; apiErrorMsg = ''; } return { ...msgcontent, id: msgcontent.actionId, status: msgcontent?.status || 'failed', dateString: `发送失败 ${waError} \n[${msgcontent.error.code}] ${apiErrorMsg} ❌`, conversationid: msgcontent.actionId.split('.')[0], }; }, }, ...cloneDeep(emailMsgMapped), }; /** * 消息类型处理, 合并各渠道类型 * * WABA: error, system, text, image, video, audio, sticker, contact, location, document, template, interactive, order, list, button, reaction, * * Email: email */ export const whatsappMsgTypeMapped = { error: { type: (_m) => ({ type: 'system' }), data: (msg) => ({ id: msg.wamid, text: msg.errorCode ? msg.errorMessage : msg.text.body }), renderForReply: (msg) => ({ id: msg.wamid, message: msg.errorCode ? msg.errorMessage : msg.text.body }), }, system: { type: 'system', data: (msg) => ({ id: msg.wamid, text: msg.system?.body }), renderForReply: (msg) => ({ id: msg.wamid, message: msg?.text?.body || msg?.text }), }, text: { type: 'text', data: (msg) => ({ id: msg.wamid, text: autoLinkText(msg?.text?.body), originText: msg?.text?.body, title: msg?.customerProfile?.name || '', }), // msg?.from || renderForReply: (msg) => ({ id: msg.wamid, message: msg?.text?.body || msg?.text }), }, image: { type: 'photo', data: (msg) => ({ id: msg.wamid, text: msg.image?.caption, onPhotoError: ({ currentTarget }) => { currentTarget.onerror = null; currentTarget.src="https://hiana-crm.oss-accelerate.aliyuncs.com/WAMedia/afe412d4-3acf-4e79-a623-048aeb4d696a.png"; }, data: { id: msg.wamid, uri: msg.image?.link, width: 'auto', height: 200, alt: msg.image?.caption || '', status: { click: true, loading: 0, download: true, }, }, originText: msg.image?.caption || '', }), renderForReply: (msg) => ({ id: msg.wamid, photoURL: msg.image?.link, width: 'auto', height: 200, alt: msg.image?.caption || '', message: msg.image?.caption || '[图片]', }), }, sticker: { type: 'photo', data: (msg) => ({ id: msg.wamid, data: { id: msg.wamid, uri: msg.sticker?.link, width: '100%', height: 120, alt: '', status: { click: true, loading: 0, download: true, }, }, }), renderForReply: (msg) => ({ id: msg.wamid, photoURL: msg.sticker.link, width: '100%', height: 200, alt: '', message: '[表情]', }), }, video: { type: 'video', data: (msg) => ({ id: msg.wamid, text: msg.video.caption, data: { id: msg.wamid, videoURL: msg.video.link, status: { click: true, loading: 0, download: true, }, height: 200, width: '100%', }, }), renderForReply: (msg) => ({ id: msg.wamid, videoURL: msg.video.link, photoURL: msg.video.link, message: msg.video?.caption || '[视频]', width: 200, height: 200, alt: '', }), }, audio: { type: 'audio', data: (msg) => ({ id: msg.wamid, audioProps: { preload: 'auto' }, data: { audioURL: msg.audio.link, audioType: msg.audio?.mime_type || 'audio/ogg', controlsList: 'nofullscreen', }, }), renderForReply: (msg) => ({ id: msg.wamid, message: '[语音]', }), }, // unsupported: { type: 'system', data: (msg) => ({ text: 'Message type is currently not supported.' }) }, unsupported: { type: 'text', data: (msg) => ({ id: msg.wamid, text: `[暂不支持此消息类型](${msg.wamid})`, dateString: `${dayjs(msg.sendTime).format('MM-DD HH:mm')} [ WhatsApp未提供消息内容 ] 可能是客人删除消息/会话, \n可询问客人截图/详细内容 或 忽略 📌` }), renderForReply: (msg) => ({ id: msg?.wamid || msg.id, message: `[Message type unsupported](${msg.wamid})` }), }, unresolvable: { type: 'text', data: (msg) => ({ id: msg.wamid, text: `[无法解析](${msg.wamid})`, }), renderForReply: (msg) => ({ id: msg?.wamid || msg.id, message: `[无法解析](${msg.wamid})` }), }, reaction: { type: 'text', data: (msg) => ({ id: msg.wamid, text: msg.reaction?.emoji || '' }), renderForReply: (msg) => ({ id: msg.wamid, message: msg.reaction?.emoji || '' }), }, document: { type: 'file', data: (msg) => ({ id: msg.wamid, title: msg.document?.filename || '', text: msg.document?.caption || msg.document?.filename || '', data: { uri: msg.document?.link, status: { click: false, download: true, loading: 0 } }, originText: msg.document?.caption || msg.document?.filename || '', }), renderForReply: (msg) => ({ id: msg.wamid, message: msg.document?.caption || msg.document?.filename || '[文件]', }), }, contacts: { type: 'meetingLink', data: (msg) => ({ id: msg.wamid, meetingID: msg.wamid, title: (msg.contacts || []).length === 1 ? `联系人` : `${(msg.contacts || []).length} 位联系人`, text: (msg.contacts || []).map((ele) => `${(ele?.org?.company || '') +' '+ele.name.formatted_name}: ${ele.phones[0].wa_id || ele.phones[0].phone}`).join('\n'), data: (msg.contacts || []).map((ele) => ({ id: ele.phones[0]?.wa_id || ele.phones[0].phone, wa_id: ele.phones[0]?.wa_id || '', phone: ele.phones[0].phone, name: (ele?.org?.company || '') +' '+ele.name.formatted_name })), waBtn: (msg.contacts || []).some(ele => ele.phones.some(p => p.wa_id)), }), renderForReply: (msg) => ({ id: msg.wamid, message: '[联系人] ' + msg.contacts?.[0].name.formatted_name + '...', }), }, location: { type: 'location', data: (msg) => ({ id: msg.wamid, title: `位置信息 ${msg.location?.name || ''} 已转高德地图, ↓点击打开`, text: msg.location?.address, // 地址 // src: `https://uri.amap.com/marker?position=${msg.location?.longitude},${msg.location?.latitude}&callnative=1`, src: 'https://cdn.pixabay.com/photo/2016/03/22/04/23/map-1272165_1280.png', href: `https://uri.amap.com/marker?position=${msg.location?.longitude},${msg.location?.latitude}&callnative=1`, data: { longitude: msg.location?.longitude, latitude: msg.location?.latitude, }, originText: msg.location?.address || '', }), renderForReply: (msg) => ({ id: msg.wamid, message: '[位置]', }), }, template: { type: 'text', data: (msg) => { const templateDataMapped = msg.template?.components ? msg.template.components.reduce((r, v) => ({ ...r, [v.type]: v }), {}) : {}; return { id: msg.wamid, text: autoLinkText(templateDataMapped?.body?.text || `......${(templateDataMapped?.body?.parameters || []).map(pv => pv?.text || '').join('......')}......`), title: '模板消息', }; // msg.template.name }, renderForReply: (msg) => { const templateDataMapped = msg.template?.components ? msg.template.components.reduce((r, v) => ({ ...r, [v.type]: v }), {}) : null; return { id: msg.wamid, message: templateDataMapped?.body?.text || templateDataMapped?.body?.parameters?.[0]?.text || '', title: '模板消息', }; // `${msg.template.name}` }, }, email: { type: 'email', data: (msg) => ({ id: msg.id || msg.uid, subject: msg.subject, }), renderForReply: (msg) => { const _msg = { ...msg, ...msg.email }; return { id: _msg.id, message: `[邮件] ${_msg.subject}`, }; }, }, }; /** * render received msg */ export const parseRenderMessageItem = (msg) => { // console.log('parseRenderMessageItem', msg); const thisMsgType = Object.keys(whatsappMsgTypeMapped).includes(msg.type) ? msg.type : 'unsupported'; return { ...msg, opi_sn: msg.opi_sn || '', msgOrigin: msg, date: msg?.sendTime || msg?.createTime || '', ...(whatsappMsgTypeMapped?.[thisMsgType]?.data(msg) || {}), conversationid: msg.conversationid, ...(typeof whatsappMsgTypeMapped[thisMsgType].type === 'function' ? whatsappMsgTypeMapped[thisMsgType].type(msg) : { type: whatsappMsgTypeMapped[thisMsgType].type || 'text' }), // type: whatsappMsgTypeMapped?.[thisMsgType]?.type || 'text', localDate: (msg?.sendTime || msg?.createTime || '').replace('T', ' '), dateString: dayjs(msg?.sendTime || msg.createTime).format('MM-DD HH:mm'), from: msg.from, sender: msg.from, senderName: msg.msg_direction === 'outbound' ? 'me' : msg?.customerProfile?.name || msg?.fromName || msg?.from || '', customer_name: msg?.customerProfile?.name || '', whatsapp_name: msg?.customerProfile?.name || '', whatsapp_phone_number: isEmpty(msg?.customerProfile) ? msg.to : msg.from, // whatsapp_msg_type: msg.msg_source==='WABA' ? msg.type : '', // whatsapp_msg_type: (msg.msg_source || 'WABA') === 'WABA' ? msg.type : '', // 1.0接口没有msg_source statusCN: msgStatusRenderMappedCN[msg?.status || 'failed'], statusTitle: msgStatusRenderMappedCN[msg?.status || 'failed'], replyButton: !['waiting', 'failed'].includes(msg?.status || '') , ...((isEmpty(msg.context) && isEmpty(msg.reaction)) || msg.context?.forwarded === true // || isEmpty(msg.messageorigin) ? {} : { reply: { /** * reply: { message: msg.messageorigin, title: 'React from', titleColor: '#1ba784' } */ title: isEmpty(msg.messageorigin) ? '...' : msg.messageorigin?.customerProfile?.name || 'me', ...(typeof whatsappMsgTypeMapped[(msg?.messageorigin?.type || 'unsupported')]?.renderForReply === 'function' ? whatsappMsgTypeMapped[(msg?.messageorigin?.type || 'unsupported')].renderForReply(msg.messageorigin) : {}), titleColor: msg.messageorigin?.customerProfile?.name && msg.messageorigin?.sender !== 'me' ? '#a791ff' : "#128c7e", id: msg.context?.id || msg.reaction?.message_id, }, origin: msg.context, }), msg_source: msg?.msg_source || msg.type, ...((msg.msg_source) === 'WABA' ? { whatsapp_msg_type: msg.type, waba: msg.msg_direction === 'outbound' ? msg.from : msg.to, wabaName: WABAccountsMapped[msg.msg_direction === 'outbound' ? msg.from : msg.to]?.verifiedName, } : { whatsapp_msg_type: '', waba: '', wabaName: '', }), }; }; /** * 从数据库读取的记录 */ export const parseRenderMessageList = (messages) => { return messages.map((msg, i) => { let msgContentString = ''; const msgtext = msg?.msgtext ?? msg?.msgtext_AsJOSN; const messageorigin = msg?.messageorigin ?? msg?.messageorigin_AsJOSN; const template = msg?.template_AsJOSN ?? msg?.template_AsJOSN_AsJOSN; if (typeof msgtext === 'string') { // debug: json 缺少一部分 msgContentString = msgtext.charAt(msgtext.length - 1) !== '}' ? msgtext + '}}' : msgtext; // if (msg.msgtext.charAt(msg.msgtext.length - 1) === '"') { // msgContentString = msg.msgtext + '}}'; // } else { // msgContentString = msg.msgtext + '"}'; // } } const msgContent = typeof msgtext === 'string' ? JSON.parse(msgContentString) : (msgtext || {}); const msgType = isEmpty(msgContent) ? msg.msgtype : (Object.keys(whatsappMsgTypeMapped).includes(msgContent.type) ? msgContent.type : 'unresolvable') msgContent.template = msg.msgtype === 'template' ? { ...msgContent.template, ...template } : {}; // const parseMethod = msgContent.bizType === 'whatsapp' ? cloneDeep(whatsappMsgTypeMapped) : {}; let waCode, waError = ''; if ((msgContent?.status || 'failed') === 'failed' && msgContent.errorMessage && msg.msg_direction === 'outbound') { (waCode = msgContent.errorMessage.match(/\(#(\d+)\)/)); (waError = (whatsappError?.[waCode?.[1]] || whatsappError?.[msgContent.errorCode] || msgContent.errorMessage)); if (!isEmpty(msgContent.whatsappApiError)) { waError = whatsappError?.[msgContent.whatsappApiError.code] || msgContent.whatsappApiError.message; // waError += `\n[${msgContent.errorCode}] ${whatsappError?.[msgContent.errorCode] || msgContent.errorMessage}`; } if (msgContent.errorMessage.includes('Invalid E.146 phone number')) { waError = whatsappError.INVALID_PHONE_NUMBER; } } const msgTypeData = whatsappMsgTypeMapped?.[msgType]?.data(msgContent) || {}; return { ...msg, msgOrigin: { ...msgContent, ...msgContent.email }, // id: msg.id || msgContent.wamid || msgContent.id, ...msgTypeData, id: msgTypeData?.id || msg.sn, type: msgContent.type, ...(typeof whatsappMsgTypeMapped[msgType].type === 'function' ? whatsappMsgTypeMapped[msgType].type(msg) : { type: whatsappMsgTypeMapped[msgType].type || 'text' }), date: msg.msgtime, // msgContent?.sendTime || msg.msgtime || '', dateText: dayjs(msg.msgtime).format('MM-DD HH:mm'), dateString: dayjs(msg.msgtime).format('MM-DD HH:mm'), localDate: (msg.msgtime || '').replace('T', ' '), from: msgContent.from, sender: msgContent.from, senderName: msgContent?.customerProfile?.name || msgContent.from || 'me', replyButton: !['waiting', 'failed'].includes(msgContent?.status || '') , // 用forwarded表示Resend, 与Reply互斥 forwarded: msg.msg_direction === 'outbound' && msg.msg_source === 'email' && ['email'].includes(msgContent.type) && (msgContent?.status || 'failed') === 'failed', ...(msg.msg_direction === 'outbound' ? { sender: 'me', senderName: 'me', status: msgStatusRenderMapped[msgContent?.status || 'failed'], dateString: msgStatusRenderMapped[msgContent?.status || 'failed'] === 'failed' ? `${(msg.msgtime || '').replace('T', ' ')} 发送失败 ${waError} ❌` : '', statusCN: msgStatusRenderMappedCN[msgContent?.status || 'failed'], statusTitle: msgStatusRenderMappedCN[msgContent?.status || 'failed'], id: (msgContent?.status || 'failed') === 'failed' ? (msgContent.actionId || msgContent.id) : (msgTypeData.id || msg.id || msg.sn), actionId: msgContent.actionId, replyButton: !['waiting', 'failed'].includes(msgContent?.status || 'failed') , } : {}), ...(isEmpty(messageorigin) && (isEmpty(msgContent.context) || msgContent.context?.forwarded === true) ? // ...((isEmpty(msg.context) && isEmpty(msg.reaction)) || msg.context?.forwarded === true || isEmpty(messageorigin) // ...((isEmpty(messageorigin) || isEmpty(msgContent.context)) {} : { reply: { message: messageorigin?.text?.body || messageorigin?.text, title: messageorigin?.customerProfile?.name || messageorigin?.senderName || 'me', ...(typeof whatsappMsgTypeMapped[messageorigin?.type || 'unsupported']?.renderForReply === 'function' ? whatsappMsgTypeMapped[messageorigin?.type || 'unsupported'].renderForReply(messageorigin) : {}), // titleColor: messageorigin?.customerProfile?.name ? '#a791ff' : '#128c7e', titleColor: msg.messageorigin_direction === 'inbound' ? '#a791ff' : '#128c7e', id: msgContent.context?.id || msgContent.context?.message_id || msgContent.reaction?.message_id || messageorigin?.wamid, }, origin: messageorigin, }), // conversationid: conversationid, // title: msg.customerProfile.name, // whatsapp_msg_type: (msg.msg_source || 'WABA') === 'WABA' ? msgContent.type : '', // 1.0接口没有msg_source whatsapp_msg_type: '', waba: '', wabaName: '', ...((msg.msg_source) === 'WABA' ? { whatsapp_msg_type: msgContent.type, waba: msg.msg_direction === 'outbound' ? msgContent.from : msgContent.to, wabaName: WABAccountsMapped[msg.msg_direction === 'outbound' ? msgContent.from : msgContent.to]?.verifiedName, } : {}), ...((msg.msg_source) === 'wai' ? { whatsapp_msg_type: msgContent.type, wabaName: '个人号', } : {}), } }); }; export const whatsappError = { 'BAD_REQUEST': ' ', 'PARAM_INVALID': '参数错误, 请联系技术组', 'INTERNAL_SERVER_ERROR': '无法连接WhatsApp.\n请稍后重试', '2': '[2] 无法连接WhatsApp.\n请稍后重试', // (#2) Service temporarily unavailable 'INVALID_PHONE_NUMBER': '无效号码, 请修正号码后重新从订单进入会话', '100': '参数错误, 请联系技术组', 'FORBIDDEN': '[FORBIDDEN] ', '4': '[4] 无法连接WhatsApp.\n请稍后重试', // (#4) Application request limit reached '131026': '[131026] 消息无法投递(未同意WhatsApp 的隐私政策).\n请使用邮件联系', '131047': '[131047] 会话未激活. \n请使用模板消息💬发送', '131053': '[131053] 文件上传失败.', '131048': '[131048] 账户被风控.', // 消息发送太多, 达到垃圾数量限制 '131049': '[131049] 号码触发风控. \n请暂停发送营销消息, 使用跟进模板\n或引导客户主动发起会话.', // 消息发送太多, 营销限制 '131031': '[131031] 账户已锁定.', '130472': '[130472] 此号码不接收商业号消息\n请使用邮件联系 或 引导客户主动发起会话.', }; export const whatsappSupportFileTypes = { sticker: { types: ['image/webp'], size: 1024 * 100 }, photo: { types: ['image/jpeg', 'image/png'], size: 1024 * 1024 * 5 }, video: { types: ['video/mp4', 'video/3gp'], size: 1024 * 1024 * 16 }, document: { types: [ 'text/plain', 'application/pdf', 'application/vnd.ms-powerpoint', 'application/msword', 'application/vnd.ms-excel', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', 'application/vnd.openxmlformats-officedocument.presentationml.presentation', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', ], size: 1024 * 1024 * 100, }, audio: { types: ['audio/aac', 'audio/mp4', 'audio/mpeg', 'audio/amr', 'audio/ogg'], size: 1024 * 1024 * 16 }, }; /** * 系统弹窗通知 */ export const handleNotification = (title, _options) => { if (!("Notification" in window)) { // alert("This browser does not support desktop notification"); return false; } var notification; const options = { // requireInteraction: true, // 设置手动关闭 // tag: 'global-sales-notification', // 通知ID,同类通知建议设置相同ID,避免通知过多遮挡桌面 // icon: '/favicon.ico', // 通知图标 ..._options, }; // 检查用户是否同意接受通知 if (Notification.permission === 'granted') { notification = new Notification(title, options); } else if (Notification.permission !== 'denied') { Notification.requestPermission().then(function (permission) { if (permission === 'granted') { notification = new Notification(title, options); } }); } else { // 用户拒绝通知权限 } // 通知弹窗被点击后的回调 if (typeof notification !== 'undefined') { notification.onclick = () => { // window.parent.parent.focus(); window.focus(); // 显示当前标签页 notification.close(); // 关闭通知,适用于设置了手动关闭的通知 }; } }; /** * WhatsApp 国际号码 * * - Make sure to remove any leading 0s or special calling codes. * - All phone numbers in Argentina (country code "54") should have a "9" between the country code and area code. The prefix "15" must be removed so the final number will have 13 digits total: +54 9 XXX XXX XXXX * - Phone numbers in Mexico (country code "52") need to have "1" after "+52", even if they're Nextel numbers. * - 巴西号码: 13位的话删除国家地区码后面的`9` * - UK: 号码部分10位, 前缀`7`补全 */ export const phoneNumberToWAID = (input) => { // Remove any non-digit characters const cleanedInput = (input.replace(/[^\d]/g, '')) .replace(/^0+/, '') // Remove leading zeros ; // Check if the number starts with a valid country code const countryCode = cleanedInput.slice(0, 2); const isArgentina = countryCode === '54'; const isMexico = countryCode === '52'; const isBrazil = countryCode === '55'; const isUK = countryCode === '44'; // Remove leading 0s and special calling codes let formattedNumber = cleanedInput.replace(/^0+/, ''); if (isArgentina) { // Remove the prefix "15" if present // formattedNumber = formattedNumber.replace(/^15/, ''); formattedNumber = formattedNumber.replace(/^54 ?15/, '54'); // Remove leading '0' after country code formattedNumber = formattedNumber.replace(/^54 ?0/, '54'); // Insert "9" between the country code and area code formattedNumber = `54 9 ${formattedNumber.slice(2)}`; } else if (isMexico) { // Remove leading '0' after country code // formattedNumber = formattedNumber.replace(/^52 ?0/, '52'); // Insert "1" after the country code // formattedNumber = `52 1 ${formattedNumber.slice(2)}`; } else if (isBrazil) { if (cleanedInput.length > 12) { formattedNumber = cleanedInput.slice(0, 4) + cleanedInput.slice(-8); } } else if (isUK) { if (cleanedInput.length < 12) { formattedNumber = `44 7 ${cleanedInput.slice(2)}`; } } // Remove any non-digit characters formattedNumber = formattedNumber.replace(/[^\d]/g, ''); return formattedNumber; } export const uploadProgressSimulate = () => fixTo2Decimals(Math.random() * (0.8 - 0.2) + 0.2);