import { cloneDeep, isEmpty, olog } from "@/utils/utils"; import { v4 as uuid } from "uuid"; 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, to: msg.to, 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, to: msg.to, msgtype: 'text', msgcontent: { body: msg.text, preview_url: true, ...(msg.context ? { context: msg.context, message_origin: msg.message_origin.msgOrigin } : {}) }, }), 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, to: msg.to, msgtype: 'template', msgcontent: msg.template }), 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, actionId: msg.id, conversationid: msg.id.split('.')[0], type: 'text', title: msg.template_origin.components.header?.[0]?.text || '', text: autoLinkText(templateDataMapped?.body?.text || ''), // msg.template_origin.components.body?.[0]?.text || '', }; }, }, }; const whatsappMsgMapped = { 'whatsapp.inbound_message.received': { getMsg: (result) => { console.log('whatsapp.inbound_message.received', result); return isEmpty(result?.whatsappInboundMessage) ? null : { ...result.whatsappInboundMessage, conversationid: result.conversationid, messageorigin: result.messageorigin }; }, contentToRender: (contentObj) => { console.log('whatsapp.inbound_message.received to render', contentObj); // const contentObj = result?.whatsappInboundMessage || result; // debug: return parseRenderMessageItem(contentObj); }, contentToUpdate: () => null, }, 'whatsapp.message.updated': { getMsg: (result) => { console.log('getMsg', result); return isEmpty(result?.whatsappMessage) ? null : { ...result.whatsappMessage, conversationid: result.conversationid }; }, contentToRender: (contentObj) => { if ((contentObj?.status === 'failed' )) { 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} ❌` : '', }), }, }; export const msgStatusRenderMapped = { 'accepted': 'waiting', // 'sent', // 接口的发送请求 'sent': 'sent', 'delivered': 'received', 'read': 'read', 'failed': 'failed', }; 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, }), }, 'error': { // 发送消息的同步返回: 发送失败时 getMsg: (result) => result, contentToRender: () => null, contentToUpdate: (msgcontent) => ({ ...msgcontent, id: msgcontent.actionId, status: msgcontent?.status || 'failed', dateString: `发送失败 ${msgcontent.error.message} ❌`, conversationid: msgcontent.actionId.split('.')[0], }), }, }; export const whatsappMsgTypeMapped = { error: { type: (_m) => ({ type: 'system' }), data: (msg) => ({ id: msg.wamid, text: msg.errorCode ? msg.errorMessage : msg.text.body }), }, system: { type: 'system', data: (msg) => ({ id: msg.wamid, text: msg.system.body }), }, text: { type: 'text', data: (msg) => ({ id: msg.wamid, text: autoLinkText(msg.text.body), originText: msg.text.body, title: msg?.customerProfile?.name || '' }), renderForReply: (msg) => ({ id: msg.wamid, message: msg.text.body }), }, image: { type: 'photo', data: (msg) => ({ id: msg.wamid, text: msg.image.caption, data: { id: msg.wamid, uri: msg.image.link, width: '100%', 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: '100%', height: 200, alt: 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, data: { id: msg.wamid, videoURL: msg.video.link, status: { click: true, loading: 0, download: true, }, }, }), 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, data: { audioURL: msg.audio.link, }, }), 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})` }), renderForReply: (msg) => ({ id: msg.wamid, text: `[Message type unsupported](${msg.wamid})` }), }, reaction: { type: 'text', data: (msg) => ({ id: msg.wamid, text: msg.reaction?.emoji || '' }), }, document: { type: 'file', data: (msg) => ({ id: msg.wamid, title: msg.document?.filename || '', text: (msg.document?.filename || '') + `\n${msg.document?.caption || ''}`, 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: '[联系人]', }), }, 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`, 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}` }; }, }, }; /** * render received msg */ export const parseRenderMessageItem = (msg) => { console.log('parseRenderMessageItem', msg); const thisMsgType = Object.keys(whatsappMsgTypeMapped).includes(msg.type) ? msg.type : 'unsupported'; return { 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', from: msg.from, sender: msg.from, senderName: msg?.customerProfile?.name || msg.from, // title: msg.customerProfile.name, customer_name: msg?.customerProfile?.name || '', whatsapp_name: msg?.customerProfile?.name || '', whatsapp_phone_number: msg.from, whatsapp_msg_type: msg.type, statusCN: msgStatusRenderMappedCN[msg?.status || 'failed'], statusTitle: msgStatusRenderMappedCN[msg?.status || 'failed'], ...((isEmpty(msg.context) && isEmpty(msg.reaction)) || msg.context?.forwarded === true || isEmpty(msg.messageorigin) ? {} : { reply: { /** * reply: { message: msg.messageorigin, title: 'React from', titleColor: '#1ba784' } */ title: 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 ? '#a791ff' : "#128c7e", }, origin: msg.context, }), }; }; /** * 从数据库读取的记录 */ export const parseRenderMessageList = (messages, conversationid = null) => { return messages.map((msg, i) => { let msgContentString = ''; if (typeof msg.msgtext_AsJOSN === 'string') { // debug: json 缺少一部分 msgContentString = msg.msgtext_AsJOSN.charAt(msg.msgtext_AsJOSN.length - 1) !== '}' ? msg.msgtext_AsJOSN + '}}' : msg.msgtext_AsJOSN; } const msgContent = typeof msg.msgtext_AsJOSN === 'string' ? JSON.parse(msgContentString) : msg.msgtext_AsJOSN; msgContent.template = msg.msgtype === 'template' ? { ...msgContent.template, ...msg.template_AsJOSN } : {}; const msgType = Object.keys(whatsappMsgTypeMapped).includes(msgContent.type) ? msgContent.type : 'unsupported'; // const parseMethod = msgContent.bizType === 'whatsapp' ? cloneDeep(whatsappMsgTypeMapped) : {}; return { ...msg, msgOrigin: msgContent, ...(whatsappMsgTypeMapped?.[msgType]?.data(msgContent) || {}), type: msgContent.type, ...(typeof whatsappMsgTypeMapped[msgType].type === 'function' ? whatsappMsgTypeMapped[msgType].type(msg) : { type: whatsappMsgTypeMapped[msgType].type || 'text' }), date: msgContent?.sendTime || msg.msgtime || '', localDate: (msg.msgtime || '').replace('T', ' '), from: msgContent.from, sender: msgContent.from, senderName: msgContent?.customerProfile?.name || msgContent.from, ...(msg.msg_direction === 'outbound' ? { sender: 'me', senderName: 'me', status: msgStatusRenderMapped[msgContent?.status || 'failed'], dateString: msgStatusRenderMapped[msgContent?.status || 'failed'] === 'failed' ? `${(msg.msgtime || '').replace('T', ' ')} 发送失败 ${whatsappError?.[msgContent.errorCode] || msgContent.errorMessage} ❌` : '', statusCN: msgStatusRenderMappedCN[msgContent?.status || 'failed'], statusTitle: msgStatusRenderMappedCN[msgContent?.status || 'failed'], } : {}), ...((isEmpty(msg.messageorigin_AsJOSN) && isEmpty(msgContent.context)) // ...((isEmpty(msg.messageorigin_AsJOSN) || isEmpty(msgContent.context)) ? {} : { reply: { message: msg.messageorigin_AsJOSN?.text?.body || msg.messageorigin_AsJOSN?.text, title: msg.messageorigin_AsJOSN?.customerProfile?.name || msg.messageorigin_AsJOSN?.senderName || 'me', ...(typeof whatsappMsgTypeMapped[(msg.messageorigin_AsJOSN?.type || 'unsupported')]?.renderForReply === 'function' ? whatsappMsgTypeMapped[(msg.messageorigin_AsJOSN?.type || 'unsupported')].renderForReply(msg.messageorigin_AsJOSN) : {}), // titleColor: msg.messageorigin_AsJOSN?.customerProfile?.name ? '#a791ff' : "#128c7e", titleColor: msg.messageorigin_direction === 'inbound' ? '#a791ff' : "#128c7e", }, origin: msg.messageorigin_AsJOSN, }), // conversationid: conversationid, // title: msg.customerProfile.name, whatsapp_msg_type: msgContent.type, }; }); }; export const whatsappError = { '131026': '[131026] 消息无法投递(未注册/使用旧版/未同意政策).', '131047': '[131047] 会话超过24小时.', '131053': '[131053] 文件上传失败.', '131048': '[131048] 账户被风控.', // 消息发送太多, 达到垃圾数量限制 '131031': '[131031] 账户已锁定.', }; /** * 系统弹窗通知 */ export const handleNotification = (title, _options) => { 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(); // 关闭通知,适用于设置了手动关闭的通知 }; } };