因为有需要微信机器人,而基于ipad协议的机器人要一个月一月200。后面从网上找了些开源的资源,进行整合。实现了我们要的需求。
请直接使用镜像https://hub.docker.com/r/danbai225/wechat-bot
FROM chisbread/wechat-service:latestCOPY target/root/ /RUN sudo rm -r /payloadsCOPY root/ /ADD qr-server /home/app/RUN sudo chmod +x /home/app/qr-serverADD proxy.sh /home/app/RUN sudo chmod +x /home/app/proxy.shRUN 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/dataRUN sudo ln -s /home/app/data /home/app/.wine/dosdevices/c:/dataENV HOOK_PROC_NAME=WeChatENV TARGET_AUTO_RESTART=yesENV TARGET_CMD=wechat-startENV INJ_CONDITION='[ "`ps -aux | grep funtool | grep -v grep`" != "" ] && exit 0'ENTRYPOINT ["/inj-entrypoint.sh"]
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
js客户端:client-3.2.1.121.js
go 客户端
package wechat_botimport ("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 = 5005const RecvTxtMsg = 1const RecvPicMsg = 3const UserList = 5000const TxtMsg = 555const PicMsg = 500const AtMsg = 550const PersonalDetail = 6550const AttatchFile = 5003const RecvFileMsg = 49const PersonalInfo = 6500const ChatroomMemberNick = 5020func 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 = socketc.addr = wsc.qrAddr = qrHttpc.contactListChan = make(chan []*Contact, 0)c.infoChan = make(chan *Info, 0)go socket.Listen()return c, err}type Client struct {handler handlersocket *gws.Connaddr stringqrAddr stringonMsg func(msg []byte, Type int, reply *Reply) //type 1是文本 2是图片 3是文件contactListChan chan []*ContactinfoChan chan *InfolastTime int64 //最后心跳时间lock sync.Mutex}type handler struct {Client *Clientgws.BuiltinEventHandler}// Reply 快速回复type Reply struct {c *ClientwId stringwxId1 stringid stringnick 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,如果是私聊消息,返回用户IDfunc (r *Reply) GetWxID() string {return r.wId}// GetPrivateWxID 返回用户IDfunc (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 uint8for _, dic := range dicList {addCode = data[0] ^ dic.firstIndexif 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 <- contactscase 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 <- infocase PersonalInfo:info := &Info{}_ = json.Unmarshal([]byte(m.Content.(string)), &info)c.Client.infoChan <- infocase 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 writerbody := &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.infoChanreturn 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"`}
接口:/qr
GET 无参数 返回QR码(刚开始可能未加载完成所以返回的可能不是qr码需要多请求几次看看能不能解析出二维码)/file
POST FORM表单 参数 file 上传文件 路径保存在/home/app/data
/download
GET 参数 path 下载文件 起始路径是/home/app/WeChat Files
也就是微信存储目录
参考 wechat-bot
参考 wechat-service