起因

之前和女朋友聊天一直用的是QQ,然后有了火花、巨轮标识。自从上班以后,就很少进QQ了,导致火花和巨轮隔三岔五的就掉了,女朋友表示很生气,要求我维持好这些火花、巨轮标识,为了积极响应女朋友的要求,便有了这篇文章。

准备工作

github看了一圈qq机器人微信机器人的项目都有一堆,那就站在前人的肩膀上实现吧。感觉这种轻量型的项目比较适合go语言,那就go!
QQ机器人选型:go-cqhttp
微信机器人选型:openwechat

要干的事

要干的事
要干的事

这么一个流程实现了的话,女朋友QQ发的消息,我的微信小号会通知我,我回复女朋友消息时直接回复我的小号。如此一来,我用微信就可以处理QQ的消息,火花和巨轮标识就算是保住了。理论可以那就到了coding环节了。

show me code

qq.go

使用go-cqhttp运行websocket正向代理,然后qq.go中建立websocket连接监听qq消息,当接收到特定好友的消息时转发到微信(目前支持了文字消息和图片消息以及部分表情)

package main

import (
    "encoding/json"
    "fmt"
    "github.com/eatmoreapple/openwechat"
    "github.com/gorilla/websocket"
    "log"
    "net/http"
    "strings"
)

var QQ *websocket.Conn

type QQMsg struct {
    Action string                 `json:"action"`
    Params map[string]interface{} `json:"params"`
    Echo   string                 `json:"echo"`
}

func loginQQ() {
    header := http.Header{}
    //header.Set("Authorization", "Bearer ")
    ws, _, err := websocket.DefaultDialer.Dial("ws://localhost:8080", header)
    if err != nil {
        panic(err)
    }
    QQ = ws
    defer QQ.Close()

    channel := make(chan []byte)
    go func() {
        for {
            // 读取消息
            _, receive, err := QQ.ReadMessage()
            if err != nil {
                log.Fatal(err)
            }
            channel <- receive
        }
    }()

    for {
        select {
        case msg := <-channel:
            go dealMsg(msg)
        }
    }
}

type ReceiveMsg struct {
    PostType    string `json:"post_type"`
    MessageType string `json:"message_type"`
    Time        int    `json:"time"`
    SelfId      int    `json:"self_id"`
    SubType     string `json:"sub_type"`
    Sender      struct {
        Age      int    `json:"age"`
        Nickname string `json:"nickname"`
        Sex      string `json:"sex"`
        UserId   int64  `json:"user_id"`
    } `json:"sender"`
    MessageId  int    `json:"message_id"`
    UserId     int64  `json:"user_id"`
    TargetId   int    `json:"target_id"`
    Message    string `json:"message"`
    RawMessage string `json:"raw_message"`
    Font       int    `json:"font"`
}

func dealMsg(msg []byte) {
    receiveMsg := ReceiveMsg{}
    s := string(msg)
    if strings.Contains(s, "connect") {
        return
    }
    fmt.Println(s)
    err := json.Unmarshal(msg, &receiveMsg)
    if err != nil {
        fmt.Println("解析消息失败: ", err.Error())
        fmt.Println(err)
        return
    }
    // 如果收到特定qq号的消息,转发到微信
    if receiveMsg.UserId != 1784945498 {
        return
    }
    if W2Q {
        WeChatSendMsg(receiveMsg)
    }
}

func QQSendMsg(msg *openwechat.Message) {

    sendMsg := QQMsg{Action: "send_private_msg", Params: map[string]interface{}{
        "user_id": "1784945498",
        "message": msg.Content,
    }}
    err := QQ.WriteJSON(sendMsg)
    if err != nil {
        fmt.Println("发送消息失败: " + err.Error())
    }
}

wechat.go

登录微信个人账号,然后监听微信消息,当接收到特定好友的消息时,转发到qq(暂时只支持了处理文本消息)

package main

import (
    "bytes"
    "fmt"
    "github.com/eatmoreapple/openwechat"
    log "github.com/sirupsen/logrus"
    "io"
    "net/http"
    "os"
    "regexp"
    "strings"
)

var WeChat = &openwechat.Bot{}
var friend = &openwechat.Friend{}
var W2Q = true

func loginWechat() {
    WeChat = openwechat.DefaultBot(openwechat.Desktop) // 桌面模式
    // 注册消息处理函数
    WeChat.MessageHandler = func(msg *openwechat.Message) {
        if msg.IsText() && msg.Content == "ping" {
            msg.ReplyText("pong")
        }
    }
    // 注册登陆二维码回调
    WeChat.UUIDCallback = openwechat.PrintlnQrcodeUrl
    reloadStorage := openwechat.NewFileHotReloadStorage("storage.json")
    defer reloadStorage.Close()

    // 登陆
    if err := WeChat.HotLogin(reloadStorage, openwechat.NewRetryLoginOption()); err != nil {
        fmt.Println(err)
        return
    }

    // 获取登陆的用户
    self, err := WeChat.GetCurrentUser()
    if err != nil {
        fmt.Println(err)
        return
    }

    // 获取所有的好友
    friends, err := self.Friends()
    res := friends.Search(1, func(friend *openwechat.Friend) bool {
        return friend.ID() == "710964253"
    })
    if res.Count() > 0 {
        friend = res.First()
    }

    WeChat.MessageHandler = func(msg *openwechat.Message) {
        fmt.Println(msg.MsgType)
        sender, err := msg.Sender()
        if err != nil {
            panic(err)
        }
        // 收到大号的消息,转发到QQ
        if sender.ID() == "710964253" {
            content := msg.Content
            if content == "/start" {
                W2Q = true
            } else if content == "/stop" {
                W2Q = false
            } else if content == "/status" {
                msg.ReplyText(fmt.Sprint(W2Q))
            } else if W2Q {
                QQSendMsg(msg)
            }
        }
    }
    // 阻塞主goroutine, 直到发生异常或者用户主动退出
    WeChat.Block()

}
func downloadUrl(url string) *bytes.Reader {
    resp, err := http.Get(url)
    if err != nil {
        log.Error("下载图片失败", err)
    }
    defer resp.Body.Close()
    data, err := io.ReadAll(resp.Body)
    return bytes.NewReader(data)
}

func WeChatSendMsg(msg ReceiveMsg) {
    str := "\\[CQ:.+?\\]"
    reg := regexp.MustCompile(str)
    allMatch := reg.FindAllString(msg.Message, -1)
    for _, match := range allMatch {
        result := make(map[string]string)
        if strings.Contains(match, "CQ:face") {
            reg = regexp.MustCompile("id=(?P<id>\\d+)")
            result["CQ"] = "face"
        }
        if strings.Contains(match, "CQ:image") {
            reg = regexp.MustCompile("url=(?P<url>.+)\\]")
            result["CQ"] = "image"
        }
        match := reg.FindStringSubmatch(match)
        groupNames := reg.SubexpNames()
        for i, name := range groupNames {
            if i != 0 && name != "" {
                result[name] = match[i]
            }
        }
        if result["CQ"] == "image" {
            url := result["url"]
            reader := downloadUrl(url)
            _, err := friend.SendImage(reader)
            if err != nil {
                fmt.Println(err)
                log.Error(err)
            }
            log.Info(url)
        } else if result["CQ"] == "face" {
            id := result["id"]
            img, _ := os.Open("qq-face/" + id + ".png")
            defer img.Close()
            friend.SendImage(img)
        }
    }
    reg = regexp.MustCompile(str)
    text := reg.ReplaceAllString(msg.Message, "")
    if len(text) != 0 {
        friend.SendText(text)
    }
}

main.go

func main() {
    go loginQQ()
    loginWechat()
}

代码到这里就完成了,但是实际上仅靠上面这部分代码并不能正常跑起来,还需要下载go-cqhttp发布的成品登录qq,同时qq表情的使用图片的形式来发到微信的,所以还需要qq表情包的图片文件夹qq-face