messagesHook.js 18 KB

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