whatsapp 群组消息监控/ 群组功能
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

413 lines
14 KiB

7 months ago
7 months ago
  1. /**
  2. * pm2 管理启动
  3. * 启动: pm2 start server
  4. * 停止pm2 stop server
  5. *
  6. **/
  7. const wppconnect = require('@wppconnect-team/wppconnect');
  8. const bodyParser = require("body-parser");
  9. const express = require("express");
  10. const {upload_files, saveToLocal} = require("./upload_files");
  11. const utils = require("./utils");
  12. const {push_data, create_kafka_client} = require("./kafka_utils");
  13. const topic_message = "whatsapp_test1";
  14. // const topic_status = "whatsapp_test2";
  15. const user_iphone = '8615201683893@c.us'; // TODO:需要修改当前登陆账号手机 用于判断被踢的是不是自己
  16. // 封装部分日志打印内容 新增日期显示
  17. {
  18. const newLog = function () {
  19. process.stdout.write(`${utils.timestampToTime(new Date() / 1000)} `)
  20. arguments.callee.oLog.apply(this, arguments);
  21. };
  22. const newError = function () {
  23. process.stdout.write(`${utils.timestampToTime(new Date() / 1000)} `)
  24. arguments.callee.oError.apply(this, arguments);
  25. };
  26. newLog.oLog = console.log;
  27. newError.oError = console.error;
  28. console.log = newLog;
  29. console.error = newError;
  30. }
  31. // 初始化express服务
  32. const app = express();
  33. const PORT = 3000;
  34. app.use(bodyParser.json());
  35. app.use(bodyParser.urlencoded({extended: true}));
  36. // 反复登陆可能是网络问题
  37. wppconnect.create({
  38. session: 'olive',
  39. statusFind: (statusSession, session) => {
  40. console.log('Status Session: ', statusSession); //return isLogged || notLogged || browserClose || qrReadSuccess || qrReadFail || autocloseCalled || desconnectedMobile || deleteToken
  41. console.log('Session name: ', session);
  42. },
  43. headless: "new",
  44. disableWelcome:true,
  45. browserArgs: [
  46. '--disable-setuid-sandbox',
  47. '--no-sandbox',
  48. '--safebrowsing-disable-auto-update',
  49. '--disable-features=LeakyPeeker'
  50. ],
  51. puppeteerOptions: {
  52. args: ["--no-sandbox", '--disable-setuid-sandbox'],
  53. }
  54. }).then((client) => start(client)).catch((error) => console.log(error));
  55. async function _download_file(client, message_id, mine, file_name){
  56. let response = undefined;
  57. try{
  58. var base64 = await client.downloadMedia(message_id);
  59. response = await upload_files(base64, mine, file_name);
  60. }catch(error){
  61. console.error("[onMessage] 下载异常: ", error);
  62. response = "protocolTimeout";
  63. }
  64. return response;
  65. }
  66. /**
  67. *
  68. * 下载文件流程
  69. **/
  70. async function download_file(client, message_id, message_type, mine){
  71. console.log("下载文件流程: ", message_type, mine);
  72. let response = {media_data: undefined, local_data: undefined};
  73. switch(message_type){
  74. case "image":
  75. var file_name = "image.jpg";
  76. response = await _download_file(client, message_id, mine, file_name); // 新增下载方法
  77. break;
  78. case "video":
  79. var file_name = "video.mp4";
  80. response = await _download_file(client,message_id, mine, file_name); // 新增下载方法
  81. break;
  82. case "ptt": // opus
  83. var file_name = "audio.opus";
  84. response = await _download_file(client,message_id, mine, file_name); // 新增下载方法
  85. break;
  86. case "sticker": // webp
  87. var file_name = "sticker.webp";
  88. response = await _download_file(client,message_id, mine, file_name); // 新增下载方法
  89. break;
  90. case "audio":
  91. var file_name = mine.replace("/", ".");
  92. response = await _download_file(client,message_id, mine, file_name); // 新增下载方法
  93. break;
  94. case "document":
  95. var file_name = mine.replace("/", ".");
  96. response = await _download_file(client,message_id, mine, file_name); // 新增下载方法
  97. break;
  98. }
  99. // if(response === "error"){
  100. // let result = saveToLocal(base64, mine);
  101. // console.error("上传失败: 保存到本地 ", result);
  102. // return {media_data: response, local_data: result}
  103. // }
  104. return response;
  105. }
  106. /**
  107. * 服务启动入口
  108. **/
  109. function start(client) {
  110. // 获得kafka对象的生产者
  111. const producer = create_kafka_client();
  112. // 监控消息部分
  113. // ----------------------------------------------------------
  114. client.onMessage(async (message) => {
  115. console.log("[onMessage] 原始数据:", message);
  116. // 拦截消息类型
  117. let chats_type = ["chat", "image", "video", "sticker", "ptt", "document", "audio"]
  118. // 拦截群组信息类型
  119. let other_type = ["remove"]
  120. console.log("[onMessage] 原始数据类型:", message.type);
  121. if(message.isGroupMsg === true && chats_type.indexOf(message.type) > -1){
  122. let data = { // subtype
  123. group_id: message.chatId, // 没有群组名称
  124. sender_name: message.sender.pushname,
  125. sender_id: message.sender.id,
  126. message_id :message.id,
  127. reply_to_msg_id: message.quotedMsgId, // 这个不一定有这个字段 表示为回复内容
  128. message_text: undefined,
  129. media: undefined,
  130. datetime: utils.timestampToTime(message.timestamp), // 修改时间戳为 yymmdd hhmmss
  131. type: message.type,
  132. mimetype: message.mimetype,
  133. subtype: message.subtype
  134. }
  135. // 下载资源
  136. let {media_data, local_data} = await download_file(client, message.id, message.type, message.mimetype);
  137. data["media"] = media_data;
  138. data["local_data"] = local_data; // 失败的话下载到本地
  139. // 是否有资源 影响对应的字段
  140. if(media_data){ // 有资源文件的
  141. data["message_text"] = message.caption;
  142. if(media_data === "protocolTimeout"){ // 如果是异常状态则修改为undefined
  143. data["media"] = undefined;
  144. console.log("下载失败 舍弃本条数据: ", data);
  145. return;
  146. }
  147. }else{
  148. data["message_text"] = message.content;
  149. }
  150. if(message.subtype === "url"){ // chat下的url 表示为引用有外部链接
  151. // message.thumbnail // 缩略图 base64
  152. media_data = await upload_files(message.thumbnail, "image/jpeg", "thubm.jpeg");
  153. console.log("[onMessage] 下载相关链接缩略图")
  154. data["media"] = media_data;
  155. data["link_title"] = message.title;
  156. data["description"] = message.description;
  157. }
  158. console.log("[onMessage] 封装数据:", data);
  159. try{ // 推送 消息
  160. await push_data(producer, data, topic_message);
  161. }catch(error){
  162. console.log("[onMessage] 推送异常: ", error);
  163. }
  164. }else if(message.type === "gp2" && other_type.indexOf(message.subtype) > -1 && message.recipients.indexOf(user_iphone) > -1){
  165. // 被踢出群 且还得是自己 别人被踢也能监听到
  166. let data = {
  167. group_id: message.chatId,
  168. sender_name: message.sender.pushname,
  169. sender_id: message.sender.id,
  170. subtype: message.subtype
  171. }
  172. let group_data = await client.getContact(message.chatId);
  173. console.log("[onMessage] 被踢群 ", group_data)
  174. data["group_title"] = group_data["name"];
  175. try{ // 推送 消息
  176. await push_data(producer, data, topic_message);
  177. }catch(error){
  178. console.error("[onMessage] 推送异常: ", error.message);
  179. }
  180. console.log("[onMessage] kick out of group", data);
  181. }
  182. });
  183. // 接口部分
  184. // ----------------------------------------------------------
  185. // 初始化接口 获取所有群组信息
  186. app.get('/whatsapp/index', async (req, res) => {
  187. console.log("[index] 获取所有接口信息");
  188. let result = {code: 200, data: undefined}; // template
  189. try{
  190. let message = await client.listChats({onlyGroups: true}); // 获取的是聊天框里的所有聊天 即使被踢了
  191. result["data"] = message
  192. }catch(error){
  193. console.log(error);
  194. result = {code: 500, data: error.message}
  195. }
  196. res.send(result);
  197. });
  198. // http://127.0.0.1:3000/whatsapp/join_group?link=https://chat.whatsapp.com/G1pxOV1USEAGmlvhzYGS8k
  199. // 加群接口
  200. app.get("/whatsapp/join_group", async (req, res) => {
  201. // 被踢的群就没有办法再加、已经加过的群返回群id {"id": ""}
  202. // TODO: 需要区分不同失败的异常 收集
  203. // 1. 需要认证的群,返回:
  204. // 2. 提交认证后,返回: already-exists
  205. // 3. 提交认证后且入群,返回 conflict
  206. let result = {code: 200, data: undefined}; // template
  207. let join_link = req.query.link;
  208. console.log("[join_group] 需要添加的群链接为:", join_link);
  209. try{
  210. let data = await client.joinGroup(join_link);
  211. result["data"] = data;
  212. }catch(error){
  213. console.log(error);
  214. // 测试error
  215. result = {code: 500, data: error.message}
  216. if(error.message === "conflict"){ // 针对加完群需要批准的 过滤异常
  217. let data = await client.getGroupInfoFromInviteLink(join_link);
  218. console.log("[join_group] 通过邀请码反查数据:", data);
  219. result["code"] = 203
  220. result["data"] = {id: data.id};
  221. }
  222. }
  223. console.log("[join_group] result:", result);
  224. res.send(result);
  225. });
  226. // 获取联系人信息
  227. app.get("/whatsapp/get_profile", async (req, res) => {
  228. let result = {code: 200, data: undefined}; // template
  229. try{
  230. let chat_id = req.query.userId; // 593968824284@c.us
  231. console.log("[get_profile] 需要获得用户id:", chat_id);
  232. let data = await client.getProfilePicFromServer(chat_id);
  233. result["data"] = data;
  234. }catch(error){
  235. console.log(error);
  236. result = {code: 500, data: error.message}
  237. }
  238. console.log("[get_profile] result:", result);
  239. res.send(result);
  240. });
  241. // 检查在线状态
  242. app.get("/whatsapp/check_online", async (req, res) => {
  243. let result = {code: 200, data: undefined}; // template
  244. try{
  245. console.log("[check_online] 检查在线状态");
  246. let data = await client.isOnline();
  247. result["data"] = data;
  248. }catch(error){
  249. console.log(error);
  250. result = {code: 500, data: error.message}
  251. }
  252. console.log("[check_online] result:", result);
  253. res.send(result);
  254. });
  255. // 获得某个聊天的详情介绍 包括头像图片 这个获取群组信息的最全 包含群成员******
  256. app.get('/whatsapp/get_chat', async (req, res) => {
  257. let result = {code: 200, data: undefined}; // template
  258. try{
  259. let chat_id = req.query.chatId; // 593968824284@c.us
  260. console.log("[get_chat] 需要获得内容的chatId:", chat_id);
  261. let data = await client.getChatById(chat_id);
  262. result["data"] = data;
  263. }catch(error){
  264. console.log(error);
  265. result = {code: 500, data: error.message}
  266. }
  267. console.log("[get_chat] result:", result);
  268. res.send(result);
  269. });
  270. // 获得某个群组
  271. app.get('/whatsapp/get_contact', async (req, res) => {
  272. let result = {code: 200, data: undefined}; // template
  273. try{
  274. let chat_id = req.query.contactId; // 593968824284@c.us
  275. let invoke_id = req.query.inviteCode;
  276. console.log("[get_contact] 需要获得群组内容:", chat_id, invoke_id);
  277. let data = await client.getContact(chat_id);
  278. console.log("[get_contact] data: ", data)
  279. // console.log(data.getProfilePicFromServer())
  280. result["data"] = data;
  281. if(invoke_id){ // 如果存在邀请码则拼接其他数据
  282. let data1 = await client.getGroupInfoFromInviteLink(invoke_id);
  283. data1.profilePicThumbObj = data.profilePicThumbObj; // 合并字段
  284. result["data"] = data1;
  285. }
  286. }catch(error){
  287. console.log(error);
  288. result = {code: 500, data: error.message}
  289. }
  290. console.log("[get_contact] result:", result);
  291. res.send(result);
  292. });
  293. // 邀请码反查群信息
  294. app.get('/whatsapp/invoke', async (req, res) => {
  295. let result = {code: 200, data: undefined}; // template
  296. try{
  297. let chat_id = req.query.inviteCode; // 593968824284@c.us
  298. console.log("[invoke] 需要获得群组内容:", chat_id);
  299. let data = await client.getGroupInfoFromInviteLink(chat_id);
  300. result["data"] = data;
  301. }catch(error){
  302. console.log(error);
  303. result = {code: 500, data: error.message}
  304. }
  305. console.log("[invoke] result:", result);
  306. res.send(result);
  307. });
  308. app.post('/whatsapp/send', async (req, res) => {
  309. console.log("enter send!");
  310. let result = {code: 200, data: undefined}; // template
  311. try{
  312. let body = req.body; // 获取消息内容以及类型 120363335479943723@g.us
  313. let message_type = body["message_type"];
  314. let content = body["content"]
  315. let chat_id = body["chat_id"];
  316. var data = ""
  317. if(message_type === "text"){ // 文本
  318. data = await client.sendText(chat_id, content); // 指定目标群组 + 内容
  319. }else if(message_type === "file"){ // 文件 content 内容支持 本地文件/ data:text/plain;base64
  320. data = await client.sendFile(chat_id, content); // 指定目标群组 + 内容
  321. }else if(message_type === "image"){ // 图片 content 内容支持 本地文件/link
  322. data = await client.sendImage(chat_id, content);
  323. }else{
  324. data = "";
  325. }
  326. console.log("[invoke] data => ", data);
  327. console.log("[invoke] 发送内容的群组id为:", chat_id, content);
  328. result["data"] = "发送成功"; // 由于没有发送成功的回执消息
  329. }catch(error){
  330. console.log(error);
  331. result = {code: 500, data: error.message}
  332. }
  333. console.log("[invoke] result:", result);
  334. res.send(result);
  335. });
  336. // 开起服务
  337. app.listen(PORT, '0.0.0.0', () => console.log("开启服务,端口3000", new Date().toString()));
  338. }