运行在docker里的微信机器人
- 作者:
- 淡白
- 创建时间:
- 2023-04-29 09:38:34
- Go docker 微信 机器人
摘要:该文章介绍了如何使用镜像 `danbai225/wechat-bot` 来创建一个微信机器人。文章提供了一个 Dockerfile 和 Docker Compose 文件,用于构建和运行该微信机器人。镜像包含了一个基于 iPad 协议的机器人,并提供了几个端口用于不同的功能: - 8080 端口用于访问 web-vnc 文件以通过 VNC 访问图像界面 - 5555 端口用于机器人的 WebSocket 连接 - 5556 端口提供了获取微信二维码和上传下载文件的 HTTP 接口 - 5900 端口用于 VNC 连接 文章还提供了一个 Go 客户端的示例代码,用于与机器人进行交互。可以使用该客户端发送文本、图片和文件消息,获取联系人列表和个人信息等。
因为有需要微信机器人,而基于ipad协议的机器人要一个月一月200。后面从网上找了些开源的资源,进行整合。实现了我们要的需求。
请直接使用镜像https://hub.docker.com/r/danbai225/wechat-bot
dockerFile
FROM chisbread/wechat-service:latest
COPY target/root/ /
RUN sudo rm -r /payloads
COPY root/ /
ADD qr-server /home/app/
RUN sudo chmod +x /home/app/qr-server
ADD proxy.sh /home/app/
RUN sudo chmod +x /home/app/proxy.sh
RUN sudo chown -R app:app /drive_c && cp -r /drive_c/* /home/app/.wine/drive_c/
RUN sudo rm -rf /WeChatSetup*
RUN mkdir /home/app/data
RUN sudo ln -s /home/app/data /home/app/.wine/dosdevices/c:/data
ENV HOOK_PROC_NAME=WeChat
ENV TARGET_AUTO_RESTART=yes
ENV TARGET_CMD=wechat-start
ENV INJ_CONDITION='[ "`ps -aux | grep funtool | grep -v grep`" != "" ] && exit 0'
ENTRYPOINT ["/inj-entrypoint.sh"]
docker-compose
version: "2.4"
services:
wechat-bot:
image: danbai225/wechat-bot:latest
container_name: wechat-bot
restart: always
ports:
- "8080:8080"
- "5555:5555"
- "5556:5556"
- "5900:5900"
extra_hosts:
- "dldir1.qq.com:127.0.0.1"
volumes:
- "./data:/home/app/data"
- "./wxFiles:/home/app/WeChat Files"
environment:
#- PROXY_IP=1.14.75.115 #如果设置则使用代理
- PROXY_PORT=7777
- PROXY_USER=user
- PROXY_PASS=pass
端口介绍
- 8080 是web接口里面有web-vnc的文件可以通过这个端口访问vnc
- 5555 机器人ws的端口
- 5556 获取微信qr码的http接口 包括上传、下载文件
- 5900 vnc端口
5555端口
js客户端:client-3.2.1.121.js go 客户端
package wechat_bot
import (
"bytes"
"crypto/md5"
"encoding/json"
"fmt"
"github.com/gofrs/uuid"
"github.com/lxzan/gws"
"io"
"mime/multipart"
"net/http"
"os"
"path/filepath"
"strings"
"sync"
"time"
)
const HeartBeat = 5005
const RecvTxtMsg = 1
const RecvPicMsg = 3
const UserList = 5000
const TxtMsg = 555
const PicMsg = 500
const AtMsg = 550
const PersonalDetail = 6550
const AttatchFile = 5003
const RecvFileMsg = 49
const PersonalInfo = 6500
const ChatroomMemberNick = 5020
func gid() string {
u, _ := uuid.NewV4()
return u.String()
}
func NewClient(ws, qrHttp string) (*Client, error) {
c := new(Client)
c.handler = handler{
Client: c,
}
socket, _, err := gws.NewClient(&c.handler, &gws.ClientOption{
Addr: ws,
})
c.socket = socket
c.addr = ws
c.qrAddr = qrHttp
c.contactListChan = make(chan []*Contact, 0)
c.infoChan = make(chan *Info, 0)
go socket.Listen()
return c, err
}
type Client struct {
handler handler
socket *gws.Conn
addr string
qrAddr string
onMsg func(msg []byte, Type int, reply *Reply) //type 1是文本 2是图片 3是文件
contactListChan chan []*Contact
infoChan chan *Info
lastTime int64 //最后心跳时间
lock sync.Mutex
}
type handler struct {
Client *Client
gws.BuiltinEventHandler
}
// Reply 快速回复
type Reply struct {
c *Client
wId string
wxId1 string
id string
nick string
}
func (r *Reply) GetMsgID() string {
return r.id
}
func (r *Reply) Msg(content string) error {
return r.c.SendTxt(content, r.wId)
}
func (r *Reply) GetNick() string {
if r.nick == "" {
r.nick, _ = r.c.GetNickFormRoom(r.wxId1, r.wId)
}
return r.nick
}
func (r *Reply) AtMsg(content string) error {
return r.c.SendAtMsg(content, r.wxId1, r.wId, r.GetNick())
}
func (r *Reply) PrivateChat(content string) error {
return r.c.SendTxt(content, r.wxId1)
}
func (r *Reply) PicMsg(path string) error {
return r.c.SendPicMsg(path, r.wId)
}
func (r *Reply) PrivatePicMsg(path string) error {
return r.c.SendPicMsg(path, r.wxId1)
}
func (r *Reply) File(path string) error {
return r.c.SendFile(path, r.wId)
}
func (r *Reply) PrivateFile(path string) error {
return r.c.SendFile(path, r.wxId1)
}
func (r *Reply) Bytes2Path(data []byte) (string, error) {
sum := md5.Sum(data)
md5str := fmt.Sprintf("%x", sum)
path := fmt.Sprintf("%s%c%s", os.TempDir(), os.PathSeparator, md5str)
err := os.WriteFile(path, data, 0666)
return path, err
}
// IsSendByFriend 来自私聊
func (r *Reply) IsSendByFriend() bool {
if strings.Contains(r.wId, "@chatroom") {
return false
} else {
return true
}
}
func (r *Reply) IsSendByGroup() bool {
if strings.Contains(r.wId, "@chatroom") {
return true
} else {
return false
}
}
// GetWxID 如果是群消息,返回群ID,如果是私聊消息,返回用户ID
func (r *Reply) GetWxID() string {
return r.wId
}
// GetPrivateWxID 返回用户ID
func (r *Reply) GetPrivateWxID() string {
if r.IsSendByGroup() {
return r.wxId1
} else {
return r.wId
}
}
type msg struct {
Id string `json:"id"`
Type int `json:"type"`
Wxid interface{} `json:"wxid"`
Roomid interface{} `json:"roomid"`
Content interface{} `json:"content"`
Nickname interface{} `json:"nickname"`
Ext interface{} `json:"ext"`
}
type ImgMsg struct {
Content string `json:"content"`
Detail string `json:"detail"`
Id1 string `json:"id1"`
Id2 string `json:"id2"`
Thumb string `json:"thumb"`
}
func ParsePictureMessage(msg []byte) *ImgMsg {
im := &ImgMsg{}
_ = json.Unmarshal(msg, im)
return im
}
type Dic struct {
name string //名称
firstIndex uint8 //第一个字节
lastIndex uint8 //第二个字节
}
var dicList = []Dic{{".jpg", 0xff, 0xd8},
{".png", 0x89, 0x50},
{".gif", 0x47, 0x49},
{"error", 0x00, 0x00}}
func parseData(data []byte) {
var addCode uint8
for _, dic := range dicList {
addCode = data[0] ^ dic.firstIndex
if data[1]^addCode == dic.lastIndex {
break
}
}
//对字节切片每个字节异或
for i, v := range data {
data[i] = v ^ addCode
}
}
func (im *ImgMsg) GetData(client *Client) ([]byte, error) {
time.Sleep(time.Second)
file, err := client.getFile(strings.ReplaceAll(im.Detail, "\\", "/"))
if err != nil {
return nil, err
}
parseData(file)
return file, nil
}
// SetOnWXmsg type 1是文本 2是图片 3是文件
func (c *Client) SetOnWXmsg(onMsg func(msg []byte, Type int, reply *Reply)) {
c.onMsg = onMsg
}
func (c *Client) LastHeartbeatTime() int64 {
return c.lastTime
}
func (c *handler) OnMessage(socket *gws.Conn, message *gws.Message) {
//fmt.Printf("recv: %s\n", message.Data.String())
m := &rMsg{}
_ = json.Unmarshal(message.Data.Bytes(), m)
switch m.Type {
case UserList:
contacts := make([]*Contact, 0)
marshal, _ := json.Marshal(m.Content)
_ = json.Unmarshal(marshal, &contacts)
c.Client.contactListChan <- contacts
case HeartBeat:
_ = c.Client.send(&msg{Id: gid(), Type: PicMsg, Wxid: "null", Roomid: "null", Content: "null", Nickname: "null", Ext: "null"})
c.Client.lastTime = time.Now().Unix()
case RecvTxtMsg:
if c.Client.onMsg != nil {
go c.Client.onMsg([]byte(m.Content.(string)), 1, &Reply{id: m.Id, c: c.Client, wId: m.WxID, wxId1: m.ID1})
}
case RecvPicMsg:
if c.Client.onMsg != nil {
marshal, _ := json.Marshal(m.Content)
go c.Client.onMsg(marshal, 2, &Reply{id: m.Id, c: c.Client, wId: m.WxID})
}
case RecvFileMsg:
if c.Client.onMsg != nil {
marshal, _ := json.Marshal(m.Content)
go c.Client.onMsg(marshal, 3, &Reply{id: m.Id, c: c.Client, wId: m.WxID})
}
case PersonalDetail:
marshal, _ := json.Marshal(m.Content)
info := &Info{}
_ = json.Unmarshal(marshal, &info)
c.Client.infoChan <- info
case PersonalInfo:
info := &Info{}
_ = json.Unmarshal([]byte(m.Content.(string)), &info)
c.Client.infoChan <- info
case ChatroomMemberNick:
info := &Info{}
_ = json.Unmarshal([]byte(m.Content.(string)), &info)
c.Client.infoChan <- info
}
}
func (c *Client) send(msg *msg) error {
marshal, err := json.Marshal(msg)
if err != nil {
return err
}
return c.socket.WriteString(string(marshal))
}
/*QR
* 获取扫描登录二维码
*/
func (c *Client) QR() ([]byte, error) {
resp, _ := http.Get(c.qrAddr + "/qr")
return io.ReadAll(resp.Body)
}
func (c *Client) upFile(data []byte, filename string) error {
// 创建multipart writer
body := &bytes.Buffer{}
writer := multipart.NewWriter(body)
// 创建文件数据部分
part, err := writer.CreateFormFile("file", filename)
if err != nil {
return err
}
_, err = part.Write(data)
if err != nil {
return err
}
// 写入multipart部分数据(文件名等)
if err = writer.WriteField("filename", filename); err != nil {
return err
}
if err = writer.Close(); err != nil {
return err
}
// 创建HTTP请求并设置multipart数据
req, err := http.NewRequest("POST", c.qrAddr+"/file", body)
if err != nil {
return err
}
req.Header.Set("Content-Type", writer.FormDataContentType())
// 发送HTTP请求
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
return nil
}
func (c *Client) getFile(path string) ([]byte, error) {
resp, err := http.Get(c.qrAddr + "/download?path=" + path)
if err != nil {
return nil, err
}
return io.ReadAll(resp.Body)
}
/*SendTxt
*发送文本消息
*content:消息内容
*to:接收者的wxid 个人消息为个人wxid 群消息为群wxid
*/
func (c *Client) SendTxt(content string, to string) error {
return c.send(&msg{
Id: "",
Type: TxtMsg,
Wxid: to,
Roomid: "null",
Content: content,
Nickname: "null",
Ext: "null",
})
}
/*
SendAtMsg
*发送@消息
*content:消息内容
*atWXid:被@的人的wxid
*to:群wxid
*nickname:被@的人的昵称
*/
func (c *Client) SendAtMsg(content string, atWXid, to, nickname string) error {
return c.send(&msg{
Id: gid(),
Type: AtMsg,
Wxid: atWXid,
Roomid: to,
Content: content,
Nickname: nickname,
Ext: "null",
})
}
/*
SendPicMsg
*发送图片信息
*content:消息内容
*atWXid:被@的人的wxid
*to:群wxid
*nickname:被@的人的昵称
*/
func (c *Client) SendPicMsg(path, wxid string) error {
file, err := os.ReadFile(path)
if err != nil {
return err
}
name := filepath.Base(path)
err = c.upFile(file, name)
if err != nil {
return err
}
return c.send(&msg{
Id: gid(),
Type: PicMsg,
Wxid: wxid,
Roomid: "null",
Content: "c:\\data\\" + name,
Nickname: "null",
Ext: "null",
})
}
/*
SendFile
*发送文件
*content:消息内容
*atWXid:被@的人的wxid
*to:群wxid
*nickname:被@的人的昵称
*/
func (c *Client) SendFile(path, wxid string) error {
file, err := os.ReadFile(path)
if err != nil {
return err
}
name := filepath.Base(path)
err = c.upFile(file, name)
if err != nil {
return err
}
return c.send(&msg{
Id: gid(),
Type: AttatchFile,
Wxid: wxid,
Roomid: "null",
Content: "c:\\data\\" + name,
Nickname: "null",
Ext: "null",
})
}
func (c *Client) GetContactList() ([]*Contact, error) {
c.lock.Lock()
defer c.lock.Unlock()
err := c.send(&msg{
Id: gid(),
Type: UserList,
Wxid: "null",
Roomid: "null",
Content: "null",
Nickname: "null",
Ext: "null",
})
if err != nil {
return nil, err
}
return <-c.contactListChan, nil
}
func (c *Client) GetPersonalDetail(wxid string) (*Info, error) {
c.lock.Lock()
defer c.lock.Unlock()
err := c.send(&msg{
Id: gid(),
Type: PersonalDetail,
Wxid: wxid,
Roomid: "null",
Content: "op:personal detail",
Nickname: "null",
Ext: "null",
})
if err != nil {
return nil, err
}
return <-c.infoChan, nil
}
func (c *Client) GetPersonal() (*Info, error) {
c.lock.Lock()
defer c.lock.Unlock()
err := c.send(&msg{
Id: gid(),
Type: PersonalInfo,
Wxid: "ROOT",
Roomid: "null",
Content: "op:personal info",
Nickname: "null",
Ext: "null",
})
if err != nil {
return nil, err
}
return <-c.infoChan, nil
}
func (c *Client) GetNickFormRoom(wxid, roomid string) (string, error) {
c.lock.Lock()
defer c.lock.Unlock()
err := c.send(&msg{
Id: gid(),
Type: ChatroomMemberNick,
Wxid: wxid,
Roomid: roomid,
Content: "null",
Nickname: "null",
Ext: "null",
})
if err != nil {
return "", err
}
i := <-c.infoChan
return i.Nick, nil
}
type rMsg struct {
Content interface{} `json:"content"`
Id string `json:"id"`
Receiver string `json:"receiver"`
WxID string `json:"wxid"`
ID1 string `json:"id1"`
Sender string `json:"sender"`
Srvid int `json:"srvid"`
Status string `json:"status"`
Time string `json:"time"`
Type int `json:"type"`
}
// Contact 联系人
type Contact struct {
Headimg string `json:"headimg"`
Name string `json:"name"`
Node int `json:"node"`
Remarks string `json:"remarks"`
Wxcode string `json:"wxcode"`
Wxid string `json:"wxid"`
}
// Info 该结构体多个返回结果在公用不保证所有字段都有,自行判断
type Info struct {
BigHeadimg string `json:"big_headimg"`
Cover string `json:"cover"`
LittleHeadimg string `json:"little_headimg"`
Signature string `json:"signature"`
WxCode string `json:"wx_code"`
WxHeadImage string `json:"wx_head_image"`
WxId string `json:"wx_id"`
WxName string `json:"wx_name"`
Nick string `json:"nick"`
}
5556端口
接口:
/qr
GET 无参数 返回QR码(刚开始可能未加载完成所以返回的可能不是qr码需要多请求几次看看能不能解析出二维码)
/file
POST FORM表单 参数 file 上传文件 路径保存在/home/app/data
/download
GET 参数 path 下载文件 起始路径是/home/app/WeChat Files
也就是微信存储目录
参考 wechat-bot 参考 wechat-service