messagesHook.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607
  1. import { useWebRTCStore } from "@/stores/modules/webrtcStore";
  2. import * as MsgType from "@/common/constant/msgType";
  3. import * as Constant from "@/common/constant/Constant";
  4. import { setupNotifications, soundVoice } from "@/utils/notifications.js";
  5. import { useWebSocketStore } from "@/stores/modules/webSocketStore";
  6. import { useWalletStore } from "@/stores/modules/walletStore.js";
  7. const IM_PATH = import.meta.env.VITE_IM_PATH_FIlE;
  8. // 格式化扩展消息提取
  9. const formatMessageExt = (message, ext) => {
  10. const isExt = ext && typeof ext == 'object';
  11. if(isExt){
  12. return {
  13. ...message,
  14. content:ext.content,
  15. msgId: ext.msgId,
  16. // fromUuids: ext.fromUuid,
  17. cc : ext.cc, // @群成员 0:全部
  18. id : ext.id, // 真实消息id
  19. quote : ext.quote, //引用的消息id
  20. isTemp : ext.isTemp, //消息阅后即焚
  21. sessionId: ext.sessionId, //会话ID
  22. sessionUuid: ext.sessionUuid, //会话UUID
  23. }
  24. }
  25. return message
  26. }
  27. // 格式化头像地址
  28. const formatAvatarUrl = (url) => url && /^https?:\/\//.test(url) ? url: (IM_PATH + (url || ''))
  29. // 消息通知
  30. const notifications = (state)=>{
  31. // 检测是否开启免打扰
  32. if (!state.toUserInfo.slience) {
  33. setupNotifications(1)
  34. }
  35. }
  36. /**
  37. * 发送消息处理
  38. * @param {{}} message
  39. * @param {useWebSocketStore} wsStore
  40. */
  41. export const setMessageHook = (message, wsStore) => {
  42. const walletStore = useWalletStore();
  43. console.log('发送消息',message)
  44. // 尝试解析扩展消息JOSON
  45. let msg;
  46. try{
  47. msg = JSON.parse(message.content);
  48. if (msg && typeof msg !== 'object') {
  49. msg = ''
  50. }
  51. }catch(e){}
  52. // 处理好友系统消息
  53. wsStore.funApprovalFriend(message)
  54. // 过滤发送的系统消息
  55. if (MsgType.MSG_SYSTEM_GROUP.includes(message.messageType)) {
  56. return;
  57. }
  58. // 处理个人消息撤回
  59. if (message.messageType === MsgType.MESSAGE_REVOKE) {
  60. // console.log('消息撤回',message)
  61. // 更新消息
  62. if (msg && msg.isTemp) {
  63. wsStore.modifyMessageId({isTemp: true}, msg, message.to)
  64. } else {
  65. wsStore.deleteMessage(message, msg.id, message.to);
  66. }
  67. // 更新会话列表
  68. if(msg.isTemp){
  69. wsStore.updateSessionNewMessage({...message, ...msg, content:'[消息已销毁]'}, message.to);
  70. } else {
  71. wsStore.pushMessage({
  72. ...message,
  73. align: 'right',
  74. fromAvatar: message.avatar,
  75. toUuid: message.to,
  76. fromUuid: message.from,
  77. content: `${message.from == walletStore.account ? '你' : (message.fromUsername || '对方')}撤回了一条消息`,
  78. }, message.to);
  79. }
  80. }
  81. // 语音和视频挂断/拒接消息处理
  82. if (Constant.MSG_AUDIO_GROUP.includes(message.contentType)) {
  83. let sender = wsStore.toUserAudioInfo.sender || {
  84. uuid: walletStore.account,
  85. nickname: walletStore.username,
  86. avatar: walletStore.avatar,
  87. }
  88. wsStore.pushMessage({
  89. ...message,
  90. align: sender.uuid == message.from ? 'right' : 'left',
  91. fromAvatar: message.avatar,
  92. toUuid: message.to,
  93. fromUuid: message.from,
  94. content: message.contentType === Constant.REJECT_AUDIO_ONLINE || message.contentType === Constant.REJECT_VIDEO_ONLINE ? '[对方拒绝]' : '[通话结束]',
  95. sender,
  96. }, message.to);
  97. }
  98. // 文本消息
  99. if (message.contentType == MsgType.MSG_TYPE.TEXT) {
  100. msg.isTemp=false;
  101. wsStore.pushMessage(formatMessageExt({
  102. ...message,
  103. align: 'right',
  104. fromAvatar: message.avatar,
  105. toUuid:message.to,
  106. fromUuid:message.from,
  107. }, msg), message.to);
  108. }
  109. // 音频消息
  110. if (message.contentType === MsgType.MSG_TYPE.AUDIO) {
  111. msg.isTemp = false;
  112. const blob = new Blob([message.file], { type: message.fileSuffix });
  113. const url = URL.createObjectURL(blob);
  114. wsStore.pushMessage(formatMessageExt({
  115. ...message,
  116. toUuid: message.to,
  117. fromUuid: message.from,
  118. align: 'right',
  119. fromAvatar: message.avatar,
  120. localUrl: url,
  121. }, msg), message.to);
  122. }
  123. // 图片
  124. if (message.contentType === MsgType.MSG_TYPE.IMAGE) {
  125. const blob = new Blob([message.file], { type: message.fileSuffix });
  126. const url = URL.createObjectURL(blob);
  127. wsStore.pushMessage(formatMessageExt({
  128. ...message,
  129. toUuid: message.to,
  130. fromUuid: message.from,
  131. align: 'right',
  132. fromAvatar: message.avatar,
  133. localUrl: url,
  134. }, msg), message.to);
  135. }
  136. // 视频消息
  137. if (message.contentType === MsgType.MSG_TYPE.VIDEO) {
  138. const blob = new Blob([message.file], { type: message.fileSuffix });
  139. const url = URL.createObjectURL(blob);
  140. wsStore.pushMessage(formatMessageExt({
  141. ...message,
  142. toUuid: message.to,
  143. fromUuid: message.from,
  144. align: 'right',
  145. fromAvatar: message.avatar,
  146. localUrl: url,
  147. }, msg), message.to);
  148. }
  149. };
  150. /**
  151. * 接收到消息处理
  152. * @param {{}} payload
  153. * @param {useWebSocketStore} wsStore
  154. * @returns
  155. */
  156. export const handleMessageHook = async (payload, wsStore) => {
  157. // console.log('接收消息', payload);
  158. const walletStore = useWalletStore();
  159. const message = {...payload};
  160. // 过滤欢迎消息和心跳
  161. if (message.type === MsgType.MESSAGE_HEAT_BEAT || message.from == 'System'){
  162. return;
  163. }
  164. console.log('接收消息', message);
  165. // if (!message.contentType && !message.messageType && !message.type) return;
  166. // 尝试解析扩展消息JOSON
  167. let msg;
  168. try{
  169. msg = JSON.parse(message.content);
  170. if (msg && typeof msg !== 'object') {
  171. msg = ''
  172. }
  173. }catch(e){}
  174. // 处理消息回执, 更新消息id
  175. if (message.messageType === MsgType.MESSAGE_RECEIPT) {
  176. // 检测是否在群内或是好友
  177. wsStore.modifyMessageId(message, { id: msg.id, uniqueId: msg.uniqueId, msgId: msg.msgId }, message.from)
  178. return;
  179. }
  180. // 消息撤回/阅后即焚(个人和群)
  181. if (message.messageType === MsgType.MESSAGE_REVOKE || message.messageType === MsgType.MESSAGE_REVOKE_GROUP) {
  182. // 消息送达回执发送
  183. wsStore.systemReceiptMessage(message, msg)
  184. // 更新消息
  185. if (msg && msg.isTemp) {
  186. wsStore.modifyMessageId({isTemp: true}, msg,message.from)
  187. } else {
  188. wsStore.deleteMessage(message, msg.id);
  189. }
  190. // 更新会话列表
  191. if (msg.isTemp) {
  192. wsStore.updateSessionNewMessage({...message, ...msg, content: '[消息已销毁]'}, message.from);
  193. }
  194. else {
  195. wsStore.pushMessage({
  196. ...message,
  197. toUuid: wsStore.toUserInfo.type == 'user' ? message.to : message.from,
  198. fromUuid: wsStore.toUserInfo.type == 'user' ? message.from : message.to,
  199. // fromUuids: msg?.fromUuid,
  200. align: 'left',
  201. content: `${(msg.fromUsername || '对方')}撤回了一条消息`,
  202. fromAvatar: message.avatar,
  203. });
  204. }
  205. }
  206. // 接收好友系统消息
  207. if(MsgType.MSG_TYPE_FRIEND.includes(message.messageType)){
  208. // 消息送达回执发送
  209. wsStore.systemReceiptMessage(message, msg)
  210. // 好友系统功能消息
  211. wsStore.listenFriendSystem(message);
  212. return;
  213. }
  214. // 接收群系统消息
  215. if (MsgType.MSG_TYPE_GROUP.includes(message.messageType)) {
  216. // 消息送达回执发送
  217. wsStore.systemReceiptMessage(message, msg)
  218. //群系统功能消息
  219. wsStore.listenGroupSystemMessage(message, msg);
  220. // 群系统消息转换
  221. let newContent = msg?msg.content:message.content;
  222. let contentType = MsgType.MSG_TYPE.NOTICE;
  223. // 处理群通知
  224. if (message.messageType === MsgType.MESSAGE_NOTICE_GROUP) {
  225. newContent = `管理员修改了群${msg.notice?'公告':'名称'}`;
  226. // 更新群通知
  227. if(msg.name){
  228. message.nickname = msg.name;
  229. }
  230. if(msg.notice){
  231. message.notice = msg.notice;
  232. }
  233. }
  234. // 如果是@群成员
  235. if(message.messageType == MsgType.MESSAGE_CC_GROUP){
  236. contentType = MsgType.MSG_TYPE.TEXT
  237. }
  238. // 如果是修改群个人昵称直接返回
  239. if (message.messageType == MsgType.MESSAGE_NICKNAME_GROUP) return;
  240. wsStore.pushMessage({
  241. ...message,
  242. align: 'left',
  243. content: newContent,
  244. contentType,
  245. messageType: MsgType.MESSAGE_TYPE_GROUP,
  246. fromAvatar: message.avatar,
  247. toUuid: wsStore.toUserInfo.type == 'user' ? message.to : message.from,
  248. fromUuid: wsStore.toUserInfo.type == 'user' ? message.from : message.to,
  249. // fromUuids: msg?.fromUuid,
  250. });
  251. notifications(wsStore);
  252. return;
  253. }
  254. // 语音和视频挂断/拒接消息处理
  255. if (Constant.MSG_AUDIO_GROUP.includes(message.contentType)) {
  256. // 消息送达回执发送
  257. wsStore.systemReceiptMessage(message, msg)
  258. let sender = wsStore.toUserAudioInfo.sender || {
  259. uuid: walletStore.account,
  260. nickname: walletStore.username,
  261. avatar: walletStore.avatar,
  262. }
  263. // 语音消息
  264. wsStore.pushMessage({
  265. ...message,
  266. align: sender.uuid != message.from?'right':'left',
  267. toUuid: wsStore.toUserAudioInfo.type == 'user' ? message.to : message.from,
  268. fromUuid: wsStore.toUserInfo.type == 'user' ? message.from : msg.fromUuid,
  269. // fromUuids: msg?.fromUuid,
  270. content: message.contentType === Constant.REJECT_AUDIO_ONLINE || message.contentType === Constant.REJECT_VIDEO_ONLINE ? '[对方拒绝]' : '[通话结束]',
  271. sender,
  272. fromAvatar: message.avatar,
  273. });
  274. }
  275. // 文本消息
  276. if (message.contentType === MsgType.MSG_TYPE.TEXT) {
  277. // 消息送达回执发送
  278. wsStore.systemReceiptMessage(message, msg)
  279. wsStore.pushMessage(formatMessageExt({
  280. ...message,
  281. toUuid: wsStore.toUserInfo.type == 'user' ? message.to : message.from,
  282. fromUuid: wsStore.toUserInfo.type == 'user' ? message.from : msg.fromUuid,
  283. // fromUuids: msg?.fromUuid,
  284. align: 'left',
  285. fromAvatar: message.avatar,
  286. }, msg));
  287. notifications(wsStore);
  288. return
  289. }
  290. // 音频消息
  291. if (message.contentType === MsgType.MSG_TYPE.AUDIO) {
  292. // 消息送达回执发送
  293. wsStore.systemReceiptMessage(message, msg)
  294. const audioBlob = new Blob([message.file], {
  295. type: `audio/${message.fileSuffix}`,
  296. });
  297. // 生成可播放的 ObjectURL
  298. const audioUrl = URL.createObjectURL(audioBlob);
  299. wsStore.pushMessage(formatMessageExt({
  300. ...message,
  301. file: audioUrl,
  302. toUuid: wsStore.toUserInfo.type == 'user' ? message.to : message.from,
  303. fromUuid: wsStore.toUserInfo.type == 'user' ? message.from : msg.fromUuid,
  304. // fromUuids: msg?.fromUuid,
  305. align: 'left',
  306. fromAvatar: message.avatar,
  307. }, msg));
  308. notifications(wsStore);
  309. return
  310. }
  311. // 图片
  312. if (message.contentType === MsgType.MSG_TYPE.IMAGE) {
  313. // 消息送达回执发送
  314. wsStore.systemReceiptMessage(message, msg)
  315. const blob = new Blob([message.file], { type: message.fileSuffix });
  316. const url = URL.createObjectURL(blob);
  317. wsStore.pushMessage(formatMessageExt({
  318. ...message,
  319. file: url,
  320. toUuid: wsStore.toUserInfo.type == 'user' ? message.to : message.from,
  321. fromUuid: wsStore.toUserInfo.type == 'user' ? message.from : msg.fromUuid,
  322. // fromUuids: msg?.fromUuid,
  323. align: 'left',
  324. fromAvatar: message.avatar,
  325. }, msg));
  326. notifications(wsStore);
  327. return
  328. }
  329. // 视频
  330. if (message.contentType === MsgType.MSG_TYPE.VIDEO) {
  331. // 消息送达回执发送
  332. wsStore.systemReceiptMessage(message, msg)
  333. const blob = new Blob([message.file], { type: message.fileSuffix });
  334. const url = URL.createObjectURL(blob);
  335. wsStore.pushMessage(formatMessageExt({
  336. ...message,
  337. file: url,
  338. toUuid: wsStore.toUserInfo.type == 'user' ? message.to : message.from,
  339. fromUuid: wsStore.toUserInfo.type == 'user' ? message.from : msg.fromUuid,
  340. // fromUuids: msg?.fromUuid,
  341. align: 'left',
  342. fromAvatar: message.avatar,
  343. }, msg));
  344. notifications(wsStore);
  345. return
  346. }
  347. // 音频在线:被呼叫
  348. if (
  349. message.contentType === Constant.DIAL_AUDIO_ONLINE ||
  350. message.contentType === Constant.DIAL_VIDEO_ONLINE
  351. ) {
  352. console.log("收到播号")
  353. // 接收请求者信息,更新当前会话
  354. wsStore.toUserAudioInfo = {
  355. uuid: message.from,
  356. type: "user",
  357. // 接收到来自谁的拨号请求信息
  358. toUuid: message.from,
  359. toAvatar: message.avatar,
  360. toUsername: message.fromUsername,
  361. // 自己的信息
  362. fromUuid: message.to,
  363. fromAvatar: walletStore.avatar,
  364. fromUsername: wsStore.toUserInfo.nickname,
  365. // 发送者信息
  366. sender: {
  367. uuid: message.from,
  368. avatar: message.avatar,
  369. nickname: message.fromUsername,
  370. },
  371. };
  372. // wsStore.toUserInfo = {
  373. // ...wsStore.toUserInfo,
  374. // uuid: message.from,
  375. // type: "user",
  376. // unReadNum: 1,
  377. // sender: {
  378. // uuid: message.from,
  379. // avatar: message.avatar,
  380. // nickname: message.fromUsername,
  381. // },
  382. // };
  383. // 调起通话界面
  384. const rtcStore = useWebRTCStore();
  385. rtcStore.streamType = message.contentType == Constant.DIAL_AUDIO_ONLINE ? 'audio' :'video'
  386. rtcStore.isCaller = false
  387. rtcStore.imSate = {
  388. ...rtcStore.imSate,
  389. videoCallModal: true,
  390. callName: message.fromUsername,
  391. fromUserUuid: message.from,
  392. callAvatar: message.avatar
  393. };
  394. // 播放铃声
  395. soundVoice.play()
  396. return
  397. }
  398. // 音频在线:挂断
  399. if (
  400. message.contentType === Constant.CANCELL_AUDIO_ONLINE ||
  401. message.contentType === Constant.CANCELL_VIDEO_ONLINE
  402. ) {
  403. console.log("音频在线:挂断")
  404. const rtcStore = useWebRTCStore();
  405. // 关闭通话界面
  406. rtcStore.imSate.videoCallModal = false;
  407. // 停止铃声
  408. soundVoice.stop()
  409. // 清理通话p2p配置
  410. rtcStore.cleanup()
  411. return;
  412. }
  413. // 音频在线:拒接
  414. if (
  415. message.contentType === Constant.REJECT_AUDIO_ONLINE ||
  416. message.contentType === Constant.REJECT_VIDEO_ONLINE
  417. ) {
  418. console.log("音频在线:拒接")
  419. // 停止铃声
  420. soundVoice.stop()
  421. const rtcStore = useWebRTCStore();
  422. // 关闭通话界面
  423. rtcStore.imSate.videoCallModal = false;
  424. // 清理通话p2p配置
  425. rtcStore.cleanup()
  426. return;
  427. }
  428. // 音频在线:接听
  429. if (
  430. message.contentType === Constant.ACCEPT_VIDEO_ONLINE ||
  431. message.contentType === Constant.ACCEPT_AUDIO_ONLINE
  432. ) {
  433. console.log("音频在线:接听", message.contentType)
  434. // 停止铃声
  435. soundVoice.stop()
  436. const video = message.contentType == Constant.ACCEPT_VIDEO_ONLINE
  437. // 初始化通话p2p配置
  438. startAudioOnline(video, wsStore);
  439. return;
  440. }
  441. // 音频通话交换信息
  442. if (message.type === Constant.MESSAGE_TRANS_TYPE) {
  443. if (
  444. message.contentType >= Constant.DIAL_MEDIA_START &&
  445. message.contentType <= Constant.DIAL_MEDIA_END
  446. ) {
  447. console.log("音频通话")
  448. // 媒体通话处理
  449. // dealMediaCall(message);
  450. return;
  451. }
  452. // 停止铃声
  453. soundVoice.stop()
  454. const rtcStore = useWebRTCStore();
  455. const { type, sdp, iceCandidate } = JSON.parse(message.content);
  456. // 接收answer:设置对端sdp
  457. if (type === "answer") {
  458. console.log('关联远程RTC')
  459. // 关联远程RTC标识到本地RTC
  460. const answerSdp = new RTCSessionDescription({ type, sdp });
  461. await rtcStore.setRemoteDescription(answerSdp);
  462. }
  463. // 处理 ICE 候选(统一处理offer_ice和answer_ice)
  464. if (type.endsWith("_ice")) {
  465. console.log("添加ICE", type, iceCandidate)
  466. const candidate = new RTCIceCandidate(iceCandidate);
  467. await rtcStore
  468. .addIceCandidate(candidate)
  469. .catch((error) => console.error("添加ICE候选失败:", error));
  470. // return;
  471. }
  472. // 响应对端offer
  473. if (type === "offer") {
  474. // 检查媒体权限是否开启
  475. // let preview = null;
  476. console.log("被呼叫者收到offer")
  477. // 被呼叫收到offer, 需要创建RTC, 并开启音视频数据流
  478. // console.log("message.contentType=", message.contentType)
  479. let video = false;
  480. // 视频电话
  481. if (message.contentType === Constant.VIDEO_ONLINE) {
  482. // preview = document.getElementById("localVideoReceiver");
  483. video = true;
  484. // // 屏幕共享
  485. }
  486. // 创建RTC
  487. if (!rtcStore.peerConnection) {
  488. rtcStore.initConnection(false, video); // false表示是Answer方
  489. }
  490. // 音频电话
  491. if (message.contentType === Constant.AUDIO_ONLINE) {
  492. // preview = document.getElementById("audioPhone");
  493. // 音频电话
  494. }
  495. // 获取音视频流数据
  496. // navigator.mediaDevices
  497. // .getUserMedia({ audio: true, video: video })
  498. rtcStore.getUserMedia({ audio: true, video: video })
  499. .then(async (stream) => {
  500. console.log('关联数据流和远程RTC')
  501. // 关联数据流到本地RTC
  502. rtcStore.addLocalStream(stream);
  503. // 关联远程RTC标识到本地RTC
  504. const offer = new RTCSessionDescription({ type, sdp });
  505. return await rtcStore.setRemoteDescription(offer);
  506. })
  507. // 创建回应offer
  508. .then(() => rtcStore.createAnswer())
  509. .then((answer) => {
  510. console.log('发送answer给呼叫者', answer)
  511. // 发送给被呼叫者(拨打者)
  512. wsStore.sendMessage({
  513. content: JSON.stringify(answer),
  514. type: Constant.MESSAGE_TRANS_TYPE,
  515. messageType: message.contentType,
  516. fromUsername: wsStore.toUserAudioInfo.fromUsername,
  517. avatar: wsStore.toUserAudioInfo.fromAvatar,
  518. to: wsStore.toUserAudioInfo.toUuid,
  519. from: wsStore.toUserAudioInfo.fromUuid,
  520. });
  521. })
  522. .catch((error) => {
  523. console.error("回应呼叫失败:", error);
  524. });
  525. }
  526. }
  527. };
  528. // 消息回调
  529. const messageCallback = (contentType, state)=>{
  530. state.sendMessage({
  531. contentType, // 消息内容类型
  532. content: "callback", //
  533. type: Constant.MESSAGE_TRANS_TYPE,
  534. });
  535. }
  536. // 创建呼叫:开启语音电话
  537. const startAudioOnline = (video, state) => {
  538. const rtcStore = useWebRTCStore();
  539. const wsStore = useWebSocketStore();
  540. // 初始化webrtc连接
  541. rtcStore.initConnection(true, video);
  542. // 获取音视频数据流
  543. // navigator.mediaDevices
  544. // .getUserMedia({ audio: true, video: video })
  545. rtcStore.getUserMedia({ audio: true, video: video })
  546. .then((stream) => {
  547. console.log('关联数据流和创建offer')
  548. // 关联数据流到本地RTC
  549. rtcStore.addLocalStream(stream);
  550. // 创建RTC offer
  551. return rtcStore.createOffer();
  552. })
  553. .then((offer) => {
  554. console.log('caller', offer)
  555. // 发送offer给被呼叫者(接听者)
  556. state.sendMessage({
  557. contentType: video? Constant.VIDEO_ONLINE: Constant.AUDIO_ONLINE, // 消息内容类型
  558. content: JSON.stringify(offer),
  559. type: Constant.MESSAGE_TRANS_TYPE,
  560. fromUsername: wsStore.toUserAudioInfo.fromUsername,
  561. avatar: wsStore.toUserAudioInfo.fromAvatar,
  562. to: wsStore.toUserAudioInfo.toUuid,
  563. from: wsStore.toUserAudioInfo.fromUuid,
  564. });
  565. })
  566. .catch((error) => {
  567. console.error("发起呼叫失败:", error);
  568. });
  569. };