当前位置:   article > 正文

简易命令行控制的「微信机器人」及群聊信息过滤实现

微信机器人根据命令创建记录

生活中学校通知信息都会借助微信群聊发布,并且时常会要求“收到请回复”。这就导致了少数重要信息与大量无关信息混杂在一起,屏蔽与不屏蔽都不是好办法。遂实现了命令行控制的「微信机器人」,并在此基础上完成了群聊信息过滤功能。

1. 开发过程

  1. 微信官方并没有相关的API。已知的企业微信机器人聊天平台有局限性,并不能灵活的完成我所期望的功能以及实现未来可能的拓展。
  2. 找了很多开源的wxbot,但他们都是基于Web版微信,笔者的微信账号并不能登陆成功。
  3. 后来找到了Wechaty,发现它完美解决了与微信交互的问题,并且封装了各式各样的接口,有详细的API文档。于是参与了开源激励计划,获取了免费甚至长期有效的iPad Puppet的Token。

2. 核心代码

完整代码请访问LazyBot Public Repo

利用Wechaty与微信交互

查阅了Wechaty的API文档,并且学习了介绍视频之后,笔者首先完成了一个入口程序,以方便移植与拓展。

  1. // ./index.js
  2. import tokenJSON from './token.json'
  3. import infoJSON from './package.json'
  4. console.log(`Running LazyBot ${infoJSON.version}...`)
  5. console.log(`Trying to detect 'token' from './token.json'`)
  6. if (!tokenJSON.token) {
  7. console.log(`Unable to acquire 'token' field from './token.json'. If you don't possess token of Wechaty, turn to https://github.com/juzibot/Welcome/wiki/Everything-about-Wechaty for more information.`)
  8. return
  9. }
  10. console.log(`Detect 'token' from './token.json': ${tokenJSON.token}`)
  11. import main from './main.js'
  12. main(tokenJSON.token, './bot-settings.json')

其中token被保存在./token.json中,bot的设定被保存在./bot-settings.json中。

随后,笔者仿照视频搭建了一个Wechaty的基本框架。

  1. // ./main.js
  2. module.exports = function(token, botSettingFile) {
  3. // Import Wechaty
  4. // ...
  5. // Import Settings
  6. // ...
  7. import botSettings from './bot-settings.json';
  8. import schedule from 'node-schedule';
  9. // Import Command System
  10. import commandUnits from './commands.js';
  11. import utils from './utils.js';
  12. // Construct Puppet
  13. const puppet = new PuppetPadplus({ token });
  14. const name = "LazyBot";
  15. const bot = new Wechaty({ puppet, name });
  16. // Initialize Writing Settings
  17. // Writing Settings of Bot to local file every 10 min.
  18. // ...
  19. // Initialize Bot Settings
  20. // ...
  21. // Begin Bot
  22. bot
  23. .on('scan', function (qrcode, status) {
  24. if (status === ScanStatus.Waiting) {
  25. QrcodeTerminal.generate(qrcode);
  26. }
  27. })
  28. .on('login', function (user) {
  29. console.log(`${user.name()} Login`);
  30. })
  31. .on('logout', function (user) {
  32. console.log(`${user.name()} Logout`);
  33. })
  34. .on('room-invite', async roomInvitation => {
  35. // ...
  36. })
  37. .on('room-leave', async (room, leaverList, remover) => {
  38. // ...
  39. })
  40. .on('friendship', async (friendship) => {
  41. // ...
  42. })
  43. .on('message', async function(message){
  44. // ...
  45. });
  46. bot.start();
  47. }

其中,bot的配置被保存在了botSettings当中,并且每隔10 min就被保存到本地./bot-settings.json中。

解析命令

首先,为了区别普通的消息与命令,笔者规定任何以.开头的消息文本都被视为命令。但是保险起见,笔者要求在群聊当中, 需要先开启 LazyBot,然后才会触发解析。

  1. // ./utils.js
  2. /**
  3. * Test whether message is a command
  4. * @param {string} message
  5. * @returns {boolean}
  6. */
  7. function isCommand(message) {
  8. return message[0] === '.';
  9. }
  10. // ./main.js -> 'message' event function
  11. if (message.room()) {
  12. const id = message.room().id;
  13. // ...
  14. // Initialize settings of group
  15. if (!botSettings["groups"][id]) botSettings["groups"][id] = { "switch": false, "monitors": {} };
  16. // ...
  17. if (utils.isCommand(text)) {
  18. // Enable or Disable LazyBot
  19. if (text === ".enable-lazybot") { botSettings["groups"][id]["switch"] = true; return;}
  20. else if (text === ".disable-lazybot") { botSettings["groups"][id]["switch"] = false;return; }
  21. // ... Parse Command if ("switch" === true) and Do Something
  22. }
  23. }

然后,是解析的实现。命令以空格作为分隔符,但是考虑到有时候空格可能会作为参数的一部分,笔者采用了被包裹在配对的"'之间的空格将不再被视作为分隔符的解决方案。解析完毕后,会生成一个命令对象。

  1. // ./utils.js
  2. /**
  3. * Parse message as command
  4. * ' ', which is not between '' or "", is perceived as separator
  5. * subCommand beginning with '-' is perceived as non-boolean flag
  6. * subCommand beginning with '--' is perceived as boolean flag
  7. * @param {string} message
  8. */
  9. function parseCommand (message) {
  10. // ... function for checking boolean flag
  11. // ... function for checking non-boolean flag
  12. const ret = {
  13. mainCommand:"",
  14. flags:{},
  15. args:[],
  16. err: ""
  17. };
  18. const _commands = message.split(' ');
  19. const commands = [];
  20. // After Splitting message by ' ', merge items like ["a, b"], and delete quotes.
  21. // e.g. ".abc 'abc e'" => [".abc", "'abc", "e'"] => [".abc", "abc e"]
  22. // ...
  23. // Parse Commands
  24. // ...
  25. return ret;
  26. }

其中,以--开头的参数被认为是boolean flag,而以-开头的参数被认为是non-boolean flag,并且其后所接的参数被认为属于这个flag

至此,命令行的输入与解析就实现完成。之后就是相应功能的实现。为了能够一般化命令行指令,笔者定义了一个处理中心,接收解析好的命令,并且 找到合适的实例去执行。

  1. // ./commands.js
  2. /**
  3. * Command ".help" is reserved for listing all possible commands with their descriptions.
  4. */
  5. class CommandUnits{
  6. RegisterCommand(command, description) {
  7. this.commands[command.mainCommand] = command;
  8. this.descriptions[command.mainCommand] = description;
  9. }
  10. RegisterRegexCommand(regex, command, display, description) {
  11. this.regexCommands.push({
  12. regex, command, display, description
  13. });
  14. }
  15. CallCommand(parsedCommands, message, botSettings){
  16. if (parsedCommands.mainCommand === ".help") {
  17. // Display All Possible Commands
  18. // ...
  19. return ret;
  20. }
  21. // Test for complete Match first
  22. if (this.commands[parsedCommands.mainCommand]) return this.commands[parsedCommands.mainCommand].Call(parsedCommands, message, botSettings);
  23. for (const regexCommand of this.regexCommands) {
  24. if (regexCommand.regex.test(parsedCommands.mainCommand)) return regexCommand.command.Call(parsedCommands, message, botSettings);
  25. }
  26. return `Unrecognized Command: ${parsedCommands.mainCommand}.`;
  27. }
  28. Ready(){
  29. // Sort Regex Command to Display more beautifully
  30. // ...
  31. }
  32. constructor() {
  33. this.commands = {};
  34. this.descriptions = {};
  35. this.regexCommands = [];
  36. }
  37. }

其中,CommandUnits接收正常的Command和正则形式的Regex Command,所有的Command在使用前都需要显式的注册到CommandUnits中。 并且,CommandUnits在确定接受完所有的注册后,需要显式的调用Ready去做一些初始化工作。

随后,就是Command的具体实现。同样,笔者定义了一个类来完成封装。

  1. // ./commands.js
  2. class Command{
  3. _parse(commands) {
  4. const parsedCommands = {
  5. mainCommand: commands.mainCommand,
  6. err: "",
  7. args: [],
  8. flags:{}
  9. };
  10. // Parse parsed Commands to suit needs of this specific instance
  11. // ...
  12. return parsedCommands;
  13. }
  14. Call(commands, message, botSettings) {
  15. const parsedCommands = this._parse(commands);
  16. if (parsedCommands.err !== "") return parsedCommands.err;
  17. if (parsedCommands.flags["help"] || parsedCommands.flags["h"] !== undefined) return this.help();
  18. return this.caller(parsedCommands, message , botSettings);
  19. }
  20. help() {
  21. // Return Help Information for this instance
  22. // ...
  23. }
  24. /**
  25. *
  26. * @param {string} mainCommand
  27. * @param {Array<{flag: string, description: string}>} booleanFlags
  28. * @param {Array<{flag: string, description: string}>} nonBooleanFlags
  29. * @param {(commands, message, botSettings) => string} caller
  30. */
  31. constructor(mainCommand, booleanFlags, nonBooleanFlags, caller) {
  32. this.mainCommand = mainCommand;
  33. this.booleanFlags = { flags: [], descriptions:{}};
  34. this.nonBooleanFlags = { flags: [], descriptions:{} };
  35. for (const flag of booleanFlags) {
  36. this.booleanFlags.flags.push(flag["flag"]);
  37. this.booleanFlags.descriptions[flag["flag"]] = flag["description"] || "";
  38. }
  39. // Add `--help` command
  40. // ...
  41. for (const flag of nonBooleanFlags) {
  42. this.nonBooleanFlags.flags.push(flag["flag"]);
  43. this.nonBooleanFlags.descriptions[flag["flag"]] = flag["description"] || "";
  44. }
  45. // Add `-h` command
  46. // ...
  47. this.caller = caller;
  48. }
  49. }

其中,flags被以Array<Object>的形式注册到Command中,相应的处理函数也被注册到其中。笔者设计Command类时,要求为Command和每个 flag提供帮助文本以自动生成帮助信息

下面是两个简单的使用的例子。

  1. // 让 Bot 识别 .hi 指令,对注册过的用户回复“欢迎”的消息。
  2. commandUnits.RegisterCommand(new Command(".hi",[],[],
  3. function (commands, message, botSettings) {
  4. if (!botSettings["users"][message.from().id]) {
  5. console.log(`Invalid .hi command from unregistered user ${message.from().id}`);
  6. return registerPrompt;
  7. }
  8. return `Hello, ${message.from().name()}! What a nice day!`;
  9. }), "Say Hi to Bot");

  1. // 让 Bot 识别 .register 指令, 以接受用户的注册。对注册过的用户,提示已经注册过,除非显式加入`--force`参数。
  2. commandUnits.RegisterCommand(new Command(".register",
  3. [
  4. {
  5. flag:"force",
  6. description:"Force to register account. In this case, the original account will be deleted, if have registered."
  7. }
  8. ], [],
  9. function (commands, message, botSettings) {
  10. if (message.room()) return "";
  11. const id = message.from().id;
  12. if (botSettings["users"][id] && !commands.flags["force"]) {
  13. console.log("Account",id,"Tried to register again");
  14. return `Have registered Account ${id}`;
  15. }
  16. botSettings["users"][id] = {};
  17. console.log("Account Registered",id);
  18. return `Hello, ${message.from().name()}!`;
  19. }), "Register Account");

实现群聊信息过滤

在准备好所有工具过后,就要实现群聊信息的过滤了。笔者采用的是黑名单白名单方案,各个方案接受指定用户的检测或者是基于正则表达式的文本检测。 保险起见,笔者要求用户显式的开启过滤功能,并且为黑名单白名单分别设置了开关。

  1. // ./command.js
  2. commandUnits.RegisterCommand(new Command(".monitor",
  3. [
  4. {flag: "off", description: "Turn Off The Monitor"},
  5. {flag: "whitelist", description: "Toggle the WhiteList"},
  6. {flag: "blacklist", description: "Toggle the BlackList"}
  7. ],
  8. [],
  9. async function (commands, message, botSettings) {
  10. // Check whether `.monitor` is valid in current environment
  11. // ...
  12. const id = message.room().id;
  13. const userId = message.from().id;
  14. if (!botSettings["groups"][id]["monitors"][userId]) {
  15. // Initialize User Settings in Monitors
  16. // ...
  17. }
  18. // Parse Options
  19. // ...
  20. return "";
  21. }), "Monitor Group Chat");
  1. commandUnits.RegisterCommand(new Command(".whitelist",
  2. [
  3. {flag:"delete", description: "Whether to delete rules instead of adding"},
  4. {flag:"show", description: "Display WhiteList"},
  5. ],
  6. [
  7. {flag:"u", description: "Add/Delete User to/from White List"},
  8. {flag:"m", description: "Add/Delete Message Filter (Regex) to/from White List"}
  9. ],
  10. async function (commands, message, botSettings) {
  11. // Check whether `.whitelist` is valid in current environment
  12. // ...
  13. const topic = await message.room().topic();
  14. const id = message.room().id;
  15. const userId = message.from().id;
  16. if (commands.flags["show"]) {
  17. // Send Setting Information of White List to User
  18. // ...
  19. return "";
  20. }
  21. if (commands.flags["u"]) {
  22. // Get User's Id from its name and Add / Delete it to / from settings if appropriate.
  23. // ...
  24. return "";
  25. }
  26. if (commands.flags["m"]) {
  27. // Add / Delete filtering rule to / from settings if appropriate.
  28. // ...
  29. return "";
  30. }
  31. }), "Manipulate WhiteList of Message Filter of Group Chat");

其中,.blacklist同理。

然后,完善main.js中的相应事件,LazyBot就初步搭建成功。

示例

3. 已知问题

  1. 暂时无法完成连续性指令。
  2. 设置被缓存在本地文件当中,写入数据库会更好。

4. 优势

  1. 实现了泛式的命令行控制,易于拓展功能。
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/我家自动化/article/detail/993881?site
推荐阅读
相关标签
  

闽ICP备14008679号