搭建whatsapp机器人

在今天的数字时代,智能机器人和自动化任务的需求不断增长。此次分享将深入探讨如何使用 whatsapp-web.js 框架来实现与 WhatsApp Web 客户端的互动,从而创建一个功能强大的 WhatsApp 智能机器人。 还会揭示技术实现原理,媒体数据解密方法,以及如何接入 ChatGPT,使机器人更具智能化。

先来看下半成品机器人的效果

智能回复

手机远程发送锁屏指令

智能回复本地知识库媒体资源

背景:WhatsApp 个人管理是目前主站正在做的一个新的产品方向。许多外贸人员联系海外客户的一个重要渠道就是在 WhatsApp 进行沟通、获客营销。

WhatsApp 有超过 20 亿人使用,平台为了防止滥发消息做了很多严格限制,甚至面临封号问题;由于 GFW 政策,国内的使用体验并不好,所以 WhatsApp 个人管理应用需要实现在本地搭建一个通讯平台来帮助外贸人员和海外客户正常沟通,同时在原有的 WhatsApp 功能基础上扩展功能,包括对联系人进行分组、添加到 CRM、陌生人进行发信。(简单的说就是搭建一个 WhatsApp 第三方客户端)

技术调研:

whatsapp-web.js

用于与 WhatsApp Web 客户端进行交互的Node.js库。它允许开发者通过编程方式与 WhatsApp Web 客户端进行互动,以执行各种操作,如发送消息、接收消息、获取联系人信息等。

@wppconnect/wa-js

WPPConnect 是 JavaScript 社区开发的开源项目,旨在将 WhatsApp Web 的功能导出到 Node,可用于支持任何交互的创建,例如客户服务、媒体发送、基于短语人工的智能识别还有很多其他的事情。

动手搭建开发环境

npm init -y
npm install whatsapp-web.js

创建启动文件

const { Client, LocalAuth } = require("whatsapp-web.js");

const client = new Client({
  
  puppeteer: {
    headless: false,
  },
  
  authStrategy: new LocalAuth({ clientId: "bot" }),
});

client.on("qr", (qr) => {
  
  console.log("QR RECEIVED", qr);
});

client.on("ready", () => {
  console.log("Client is ready!");
});

client.on("message", async (message) => {
  if (message.body === "你好") {
    
    const reply = "你好!";
    client.sendMessage(message.from, reply);
  }
});

client.initialize();

新增快捷回复指令

client.on("message", async (message) => {
  const command = message.body;

  
  if (command === "打开浏览器") {
    executeSystemCommand('open -a "Google Chrome"');
  } else if (command === "打开计算器") {
    executeSystemCommand('open -a "Calculator"');
  }
  
  if (command === "你好") {
    
    const reply = "你好!";
    client.sendMessage(message.from, reply);
  }
});

function executeSystemCommand(command) {
  exec(command, (error, stdout, stderr) => {
    if (error) {
      console.error(`Error executing command: ${error}`);
      return;
    }
    console.log(`Command output: ${stdout}`);
  });
}

技术实现原理:

node 启动 puppeteer 浏览器加载 web.whatsapp.com 页面,注入 JS 脚本(window.WWebJS、window.mR、window.Store)来增强用户获取页面信息(获取联系人列表、聊天列表)与页面进行交互(发消息、发送文件、标记已读/未读)

node 端监听消息回调事件进行处理,执行 Page.evaluate() 方法操作页面 WWebJS.sendMessage、WWebJS.sendDeleteChat 等函数

上面图中 WWebJS 是如何实现的?它是如何做到调用 whatsapp 打包后的代码模块内的函数?

Webpack 模块加载原理及执行分析

moduleRaid: 用于从 Webpack 嵌入网站的 webpackJsonp 函数中获取模块和模块构造函数。它还提供了搜索返回模块的功能

它利用了 webpack 的模块加载机制,通过查找页面 window.webpackChunkwhatsapp_web_client 对象获取 __webpack_modules__ (webpack 维护的所有模块结合),通过遍历模块将特定模块方法赋值到 WWebJS 对象上。

WhatsApp 中的媒体数据解密和下载:

whatsapp 的官网宣传是“安全私密地收发消息”,做到了端到端加密。而且对媒体资源进行了加密处理,即使通过接口截获到完整消息,下载后的媒体资源也是 enc 格式(Tips:enc 文件包含受保护的数据)

Message 媒体文件返回的是图片缩略图(base64),分辨率很低,如果想要获取图片的原始文件需要进行解密。

deprecatedMms3Url 为图片的真实地址(受到媒体保护的 enc 文件格式)

encFilehash 为文件密钥

  1. b64ToBuffer 函数将 base64 编码的字符串转换为 Uint8Array,这是一个用于表示二进制数据的 JavaScript 类型。它首先修复 base64 字符串中的 URL 编码问题,然后将其解码为二进制数据。

  2. hkdf 函数执行 HKDF 导出密钥派生操作。它导入一个原始密钥,然后使用 HKDF 派生一个新密钥。HKDF 是一种密钥派生函数,用于从给定的输入密钥派生出一个或多个新密钥,通常用于加密和解密操作。

  3. getMediaKeys 函数基于媒体类型和给定的媒体密钥,使用 hkdf 函数派生出与媒体类型相关的密钥。它使用 MEDIA_HKDF_KEY_MAPPING 来查找媒体类型与 HKDF 密钥名称之间的关系。

  4. decryptMedia 函数是导出的主要函数,用于解密加密的媒体数据。它使用 getMediaKeys 获取媒体密钥,然后使用该密钥解密传入的二进制数据。解密过程包括从密钥中提取初始化向量(IV),创建解密密钥,并执行解密操作。

const MEDIA_HKDF_KEY_MAPPING = {
  audio: "Audio",
  document: "Document",
  gif: "Video",
  image: "Image",
  ppic: "",
  product: "Image",
  ptt: "Audio",
  sticker: "Image",
  video: "Video",
  "thumbnail-document": "Document Thumbnail",
  "thumbnail-image": "Image Thumbnail",
  "thumbnail-video": "Video Thumbnail",
  "thumbnail-link": "Link Thumbnail",
  "md-msg-hist": "History",
  "md-app-state": "App State",
  "product-catalog-image": "",
  "payment-bg-image": "Payment Background",
};

const AES_CHUNK_SIZE = 16;
const toSmallestChunkSize = (num) => {
  return Math.floor(num / AES_CHUNK_SIZE) * AES_CHUNK_SIZE;
};

function b64ToBuffer(b64) {
  b64 = b64.replace(/_/g, "/").replace(/-/g, "+");
  b64 += "===".slice((b64.length + 3) % 4);
  const b = atob(b64)
    .split("")
    .map((s) => s.charCodeAt(0));
  return new Uint8Array(b);
}

async function hkdf(key, info, length = 112 * 8) {
  const bufferKey = b64ToBuffer(key);
  const baseKey = await crypto.subtle.importKey(
    "raw",
    bufferKey,
    "HKDF",
    false,
    ["deriveKey"]
  );

  const deriveKey = await crypto.subtle.deriveKey(
    {
      name: "HKDF",
      info: new TextEncoder().encode(info),
      hash: "SHA-256",
      salt: new Uint8Array(0),
    },
    baseKey,
    { name: "HMAC", hash: "SHA-256", length },
    true,
    ["sign"]
  );
  return crypto.subtle.exportKey("raw", deriveKey);
}

function getMediaKeys(mediaKey, mediaType) {
  const type =
    MEDIA_HKDF_KEY_MAPPING[mediaType] ||
    mediaType.slice(0, 1).toUpperCase() + mediaType.slice(1);
  return hkdf(mediaKey, `WhatsApp ${type} Keys`);
}

async function decryptMedia(buffer, mediaKey, mediaType) {
  const hkdfKey = await getMediaKeys(mediaKey, mediaType);
  const iv = hkdfKey.slice(0, 16);
  const cipherKey = hkdfKey.slice(16, 48);
  const decryptLegnth = toSmallestChunkSize(buffer.byteLength);

  const key = await crypto.subtle.importKey(
    "raw",
    cipherKey,
    {
      name: "AES-CBC",
      length: 256,
    },
    false,
    ["decrypt"]
  );

  return crypto.subtle.decrypt(
    { name: "AES-CBC", iv, length: 256 },
    key,
    buffer.slice(0, decryptLegnth)
  );
}
const DEFAULT_MEDIA_HOST = "https://media-hkg4-2.cdn.whatsapp.net";

const url = `${DEFAULT_MEDIA_HOST}${mediaInfo.directPath}&hash=${mediaInfo.encFilehash}`;
fetch(url)
  .then((res) => res.arrayBuffer())
  .then((buff) => decryptMedia(buff, mediaInfo.mediaKey, mediaInfo.type))
  .then((buffer) => {
      
    const blob = new Blob([buffer], { type: mediaInfo.mimetype });
  });

如何在 Node.js 中接入 chatGPT,让机器人变得更智能?

智能对话的难点主要在于语义分析,随着 chatGPT 的基础设施日趋完善,web 应用接入 openAI SDK 变得不是难事,下面我来展示 node.js 应用如何接入 chatGPT

  1. 创建OpenAI帐户,在开发者平台注册账号后申请密钥(每个账号有 5 美元的免费额度),并保存在环境变量中
export OPENAI_API_KEY='your-api-key-here'
  1. 安装 openai 包
npm install openai
  1. 编写Node.js代码
const OpenAI = require('openai');
const openai = new OpenAI();
async function chatWithGPT(prompt) {
  try {
    const completion = await openai.chat.completions.create({
      model: 'gpt-3.5-turbo',
      messages: [
        { role: 'system', content: 'You are a helpful assistant.' },
        { role: 'user', content: prompt },
      ],
    });

    console.log(completion.choices[0]);
    return completion.choices[0];
  } catch (error) {
    console.error('Error:', error);
  }
}

const userPrompt = 'Translate the following English text to Chinese: "Hello, how are you?"';
chatWithGPT(userPrompt);

同理,也可以接入 Dall-E 模型,智能生图

优点:

  • 没有网络限制,随时随地可以调用 chatGPT;

  • 可以共享使用,不受单一账号限制

  • 通过前置特定的 prompt,打造个性化的助手

如何提高工作效率?

在 Mac 上使用快捷指令,能够利用自动化流程和应用间的协作,突破应用本身的限制,带来更高效的工作流程。

  • 自动发早会日报,避免漏发消息

  • 不在工位,远程锁屏

  • 对于不需要复杂页面的 web 应用可以考虑使用 Language UI 来实现

更早的文章

React 的快与慢

在 React 出现之前,web 的性能瓶颈是 js 和 DOM 交互这块,当时很多优化大多集中在如何减少不必要的 DOM 操作,这些优化都是点状的优化,缺乏一个大而全的优化体系,直到 React 出现。众所周知 React 使用 Virtual DOM 来描述 UI 界面,开发者只需要操作改变 Virtual DOM。React 通过对比前后两个 Virtual DOM 的差异(diff 的过程),精准的拿到要发生变化的 UI 描述,这样就避免了不必要的 DOM 操作,减少了页面重绘的次...…

继续阅读