轮椅之Q群词云机器人.md

在tg群里看到一个每日词云的bot,心血来潮在qq上实现了下

类似这种↓

过程很顺利,词云生成(word_cloud),中文分词(jieba),群消息上报和发送(go-cqhttp)都有好用的轮子

选用Deno搭建服务器浅尝了下类型体操


首先是群消息的处理,为了绕过CQ码的解析,在go-cqhttp的配置中使用array作为消息类型

go-cqhttp中的消息类型

我们期望的消息格式长这样

{
  post_type: "message" | "message_sent", // 他人发送或自身发送消息
  message_type: "group",                 // 群聊消息
  sub_type: "normal",                    // 普通消息
  group_id: xxxxxxxxxx,                  // 群号
  sender: {
    card: "xxxx" | "",                   // 群昵称,优先采用
    nickname: "xxxx"                     // 昵称,群昵称为空时采用
  },
  message: [
    {
      type: "text",                      // 只处理文本内容
      data: "xxxxxx",                    // 消息文本
    },
    // ...
  ]
}

go-cqhttp中的群消息格式

上报请求的处理

const handler = async (request: Request) => {
  const json = await request.json();

  if (msgPredicate(json)) {
    console.log(JSON.stringify(json));
    const group_id: number = json.group_id;
    const nickname: string = json.sender.card != "" // 处理优先级
      ? json.sender.card
      : json.sender.nickname;
    const user_rank = context[group_id].user_rank;
    const messages = context[group_id].messages;

    if (nickname in user_rank) user_rank[nickname]++;
    else user_rank[nickname] = 1;

    json.message.forEach(
      (element: { type: string; data: { text: string } }) => {
        console.log(JSON.stringify(element));
        if (element.type == "text") messages.push(element.data.text);
      },
    );
  }
  return new Response(null, { status: 204 }); // 204表示不进行任何快速反应
};

其中msgPredicate的具体定义如下,基本就是对上边期望数据的筛选

const msgPredicate = (
  json: {
    post_type: string;
    message_type?: string;
    sub_type?: string;
    group_id?: number;
  },
) =>
  (json.post_type == "message" || json.post_type == "message_sent") &&
  json.message_type == "group" &&
  json.sub_type == "normal" && config.groups.includes(json.group_id ?? 0);

对于定时生成词云使用了deno_cron,解析配置中的cron表达式并调用处理函数

cron(config.cron, () => { /* ... */ });

接着拼接同一个群聊所有消息,启动子进程生成图片

const all_msg = ctx.messages.join("\n");

// proc = exec("python3 ./word_cloud.py")
// proc.stdin: redirected
// all_msg: write to stdin

word_cloud.py调用jieba.cut分词,统计词频后使用WordCloud.generate_from_frequencies生成图片并to_file./result.png

这里使用base64通过CQ码发送图片,请求长这样

// POST /send_group_msg
{
  group_id: xxxx,
  message: "[CQ:image,file=base64://...]"
}

图片转b64用了Deno的标准库

import { encode } from "https://deno.land/[email protected]/encoding/base64.ts";

function get_image_cqcode(path: string) {
  const base64 = encode(Deno.readFileSync(path));
  return `[CQ:image,file=base64://${base64}]`;
}

发言排行处理

function get_description(ctx: { user_rank: { [nickname: string]: number } }) {
  console.log(JSON.stringify(ctx));
  const entries = Object.entries(ctx.user_rank);
  const people_count = entries.length;
  const msg_count = entries.map(([_, count]) => count).reduce(
    (prev, cur) => prev + cur,
    0,
  );
  entries.sort(([_, count_a], [__, count_b]) => count_b - count_a);
  const rank = entries.length <= 10 ? entries : entries.slice(0, 10);
  return `本群 ${people_count} 位朋友共产生 ${msg_count} 条发言\n活跃用户排行榜\n${
    rank.map(([name, count]) => `${name} 贡献: ${count}`).join("\n")
  }`;
}

请求格式

// POST /send_group_msg
{
  group_id: xxxx,
  message: "xxxx",
  auto_escape: true // 防止奇奇怪怪的昵称被解析成CQ码
}

项目地址:CrackTC/qwordcloud

冲着能用写的,代码不好看诶嘿~