├── .gitignore
├── .goreleaser.yml
├── README.md
├── config.yaml
├── config
└── config.go
├── go.mod
├── go.sum
├── main.go
├── models
├── base.go
├── group.go
├── msg.go
└── user.go
├── pkg
├── define
│ └── typeDefine.go
├── httpClient
│ └── http.go
└── utils
│ ├── reg.go
│ └── wirteFile.go
├── wcbot
├── api.go
├── cron.go
├── imageHandle.go
├── imageHandle_test.go
├── middleware.go
├── testHandle_test.go
├── textHandle.go
├── utils.go
└── wcbot.go
└── wxqr
└── wxqr.png
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea
2 | logs
3 |
4 | wxqr
--------------------------------------------------------------------------------
/.goreleaser.yml:
--------------------------------------------------------------------------------
1 | # This is an example goreleaser.yaml file with some sane defaults.
2 | # Make sure to check the documentation at http://goreleaser.com
3 | before:
4 | hooks:
5 | # you may remove this if you don't use vgo
6 | - go mod tidy
7 | # you may remove this if you don't need go generate
8 | - go generate ./...
9 | builds:
10 | - env:
11 | - CGO_ENABLED=0
12 | archives:
13 | - replacements:
14 | darwin: Darwin
15 | linux: Linux
16 | windows: Windows
17 | 386: i386
18 | amd64: x86_64
19 | checksum:
20 | name_template: 'checksums.txt'
21 | snapshot:
22 | name_template: "{{ .Tag }}-next"
23 | changelog:
24 | sort: asc
25 | filters:
26 | exclude:
27 | - '^docs:'
28 | - '^test:'
29 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | wxBot4g 是基于go的微信机器人
2 |
3 | ## 技术
4 | - gin(http框架)
5 | - cron(定时任务)
6 | - etree(解析xml)
7 | - viper(配置文件读取)
8 | - logrus(日志框架)
9 | - go-qrcode(登陆二维码生成)
10 |
11 | ## 目前支持的消息类型
12 | ### 好友消息
13 | - [x] 文本
14 | - [x] 图片
15 | - [x] 地理位置
16 | - [x] 个人名片
17 | - [x] 语音
18 | - [x] 小视频
19 | - [ ] 动画
20 |
21 | ### 群消息
22 | - [x] 文本
23 | - [x] 图片
24 | - [x] 地理位置
25 | - [x] 个人名片
26 | - [x] 语音
27 | - [ ] 动画
28 |
29 | ### TODO功能
30 | - [x] 提供restful api,发送消息到指定好友/群
31 | - [ ] 文件/图片上传阿里云oss
32 | - [ ] 监听指定群报警
33 | - [ ] 聊天记录中文分析,情感分析
34 |
35 | ## 使用例子
36 |
37 | 24行代码就实现微信机器人的监听消息功能
38 |
39 | ```
40 | package main
41 |
42 | import (
43 | "wxBot4g/models"
44 | "wxBot4g/pkg/define"
45 | "wxBot4g/wcbot"
46 |
47 | "github.com/sirupsen/logrus"
48 | )
49 |
50 | func HandleMsg(msg models.RealRecvMsg) {
51 | logrus.Debug("MsgType: ", msg.MsgType, " ", " MsgTypeId: ", msg.MsgTypeId)
52 | logrus.Info(
53 | "消息类型:", define.MsgIdString(msg.MsgType), " ",
54 | "数据类型:", define.MsgTypeIdString(msg.MsgTypeId), " ",
55 | "发送人:", msg.SendMsgUSer.Name, " ",
56 | "内容:", msg.Content.Data)
57 | }
58 |
59 | func main() {
60 | bot := wcbot.New(HandleMsg)
61 | bot.Debug = true
62 | bot.Run()
63 | }
64 | ```
65 |
66 | ## 消息类型和数据类型
67 |
68 | ### MsgType(消息类型)
69 |
70 | | 数据类型编号 | 数据类型 | 说明 |
71 | | ------------ | ---------- | -------------------- |
72 | | 0 | Init | 初始化消息,内部数据 |
73 | | 1 | Self | 自己发送的消息 |
74 | | 2 | FileHelper | 文件消息 |
75 | | 3 | Group | 群消息 |
76 | | 4 | Contact | 联系人消息 |
77 | | 5 | Public | 公众号消息 |
78 | | 6 | Special | 特殊账号消息 |
79 | | 51 | 获取wxid | 获取wxid消息 |
80 | | 99 | Unknown | 未知账号消息 |
81 |
82 | ### MsgTypeId(数据类型)
83 |
84 | | 数据类型编号 | 数据类型 | 说明 |
85 | | ------------ | --------- | ------------------------------------------------------------ |
86 | | 0 | Text | 文本消息的具体内容 |
87 | | 1 | Location | 地理位置 |
88 | | 3 | Image | 图片数据的url,HTTP POST请求此url可以得到jpg文件格式的数据 |
89 | | 4 | Voice | 语音数据的url,HTTP POST请求此url可以得到mp3文件格式的数据 |
90 | | 5 | Recommend | 包含 nickname (昵称), alias (别名),province (省份),city (城市), gender (性别)字段 |
91 | | 6 | Animation | 动画url, HTTP POST请求此url可以得到gif文件格式的数据 |
92 | | 7 | Share | 字典,包含 type (类型),title (标题),desc (描述),url (链接),from (源网站)字段 |
93 | | 8 | Video | 视频,未支持 |
94 | | 9 | VideoCall | 视频电话,未支持 |
95 | | 10 | Redraw | 撤回消息 |
96 | | 11 | Empty | 内容,未支持 |
97 | | 99 | Unknown | 未支持 |
98 |
99 | ## 功能api
100 |
101 | ### 发送文本消息(好友/群)
102 |
103 | ```
104 | http://127.0.0.1:7788/v1/msg/text?to=测试群&word=你好, 测试一下&appKey=khr1244o1oh
105 | ```
106 |
107 | ### 发送图片消息(好友/群)
108 |
109 | 请参考`wxBot4g/wcbot/imageHandle_test.go`
110 |
111 | v1.1
112 |
113 | - 增加终端二维码扫码登录
114 | - 增加api,发送文本、图片消息到指定群
115 | - 增加单元测试
--------------------------------------------------------------------------------
/config.yaml:
--------------------------------------------------------------------------------
1 | runmode: debug # 开发模式, debug, release, test
2 | addr: 7788 # HTTP绑定端口
3 | name: wxBot4g # Server的名字
4 | appKey: khr9348yo1oh
5 | retryTimes: 10
6 |
7 | wxbot4g:
8 | wxqrDir: ./wxqr/
9 | heartbeatURL: /v1/health/heartbeat
10 | imageDir2: F:/go_home/project/wxBot4g/imageDir/
11 | imageDir: ./imageDir/
--------------------------------------------------------------------------------
/config/config.go:
--------------------------------------------------------------------------------
1 | package config
2 |
3 | import (
4 | "os"
5 | "strings"
6 |
7 | "github.com/fsnotify/fsnotify"
8 |
9 | "github.com/sirupsen/logrus"
10 |
11 | "github.com/spf13/viper"
12 | )
13 |
14 | type AppConfig struct {
15 | ServerConf *ServerConfig
16 | WxBot4gConf *WxBot4gConfig
17 | }
18 |
19 | type ServerConfig struct {
20 | Mode string `json:"mode"`
21 | Port int `json:"port"`
22 | AppKey string `json:"appKey"`
23 | RetryTimes int `json:"retryTimes"`
24 | }
25 |
26 | type WxBot4gConfig struct {
27 | WxQrDir string `json:"wxqrDir"`
28 | HeartbeatURL string `json:"heartbeatURL"`
29 | ImageDir string `json:"imageDir"`
30 | }
31 |
32 | var (
33 | Config AppConfig
34 | )
35 |
36 | func init() {
37 | if err := initConfig(); err != nil {
38 | panic(err)
39 | }
40 | initLog()
41 | watchConfig()
42 |
43 | Config = AppConfig{
44 | ServerConf: &ServerConfig{
45 | Mode: viper.GetString("runmode"),
46 | Port: viper.GetInt("addr"),
47 | AppKey: viper.GetString("appKey"),
48 | RetryTimes: viper.GetInt("retryTimes"),
49 | },
50 | WxBot4gConf: &WxBot4gConfig{
51 | WxQrDir: viper.GetString("wxbot4g.wxqrDir"),
52 | HeartbeatURL: viper.GetString("wxbot4g.heartbeatURL"),
53 | ImageDir: viper.GetString("wxbot4g.imageDir"),
54 | },
55 | }
56 | }
57 |
58 | func initConfig() error {
59 | viper.AddConfigPath(".")
60 | viper.SetConfigName("config")
61 |
62 | viper.SetConfigType("yaml")
63 | viper.AutomaticEnv()
64 | viper.SetEnvPrefix("wxBot4g")
65 | replacer := strings.NewReplacer(".", "_")
66 | viper.SetEnvKeyReplacer(replacer)
67 | if err := viper.ReadInConfig(); err != nil {
68 | return err
69 | }
70 |
71 | return nil
72 | }
73 |
74 | func watchConfig() {
75 | viper.WatchConfig()
76 | viper.OnConfigChange(func(e fsnotify.Event) {
77 | logrus.Info("Config file changed: %s", e.Name)
78 | })
79 | }
80 |
81 | func initLog() {
82 | logrus.SetFormatter(&logrus.JSONFormatter{})
83 | logrus.SetOutput(os.Stdout)
84 | logrus.SetLevel(logrus.TraceLevel)
85 | logrus.SetReportCaller(true)
86 | }
87 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module wxBot4g
2 |
3 | go 1.12
4 |
5 | require (
6 | github.com/546669204/golang-http-do v0.0.0-20180806031056-2047ec1931b3 // indirect
7 | github.com/axgle/mahonia v0.0.0-20180208002826-3358181d7394 // indirect
8 | github.com/beevik/etree v1.1.0
9 | github.com/fsnotify/fsnotify v1.4.7
10 | github.com/gin-gonic/gin v1.4.0
11 | github.com/mdp/qrterminal v1.0.1
12 | github.com/robfig/cron v1.2.0
13 | github.com/sirupsen/logrus v1.4.2
14 | github.com/skip2/go-qrcode v0.0.0-20190110000554-dc11ecdae0a9
15 | github.com/spf13/viper v1.4.0
16 | github.com/tuotoo/qrcode v0.0.0-20190222102259-ac9c44189bf2
17 | github.com/willf/bitset v1.1.10 // indirect
18 | gopkg.in/h2non/filetype.v1 v1.0.5
19 | )
20 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
2 | github.com/546669204/golang-http-do v0.0.0-20180806031056-2047ec1931b3 h1:0YjeP3ltblCQYdX+35Awp/9fdKE2ezPwjfyBnverLtc=
3 | github.com/546669204/golang-http-do v0.0.0-20180806031056-2047ec1931b3/go.mod h1:FQuxv+yhrfc59V8OZZ2g9MJNONURGOEHefLqJD0i4Mc=
4 | github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
5 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
6 | github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
7 | github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
8 | github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
9 | github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
10 | github.com/axgle/mahonia v0.0.0-20180208002826-3358181d7394 h1:OYA+5W64v3OgClL+IrOD63t4i/RW7RqrAVl9LTZ9UqQ=
11 | github.com/axgle/mahonia v0.0.0-20180208002826-3358181d7394/go.mod h1:Q8n74mJTIgjX4RBBcHnJ05h//6/k6foqmgE45jTQtxg=
12 | github.com/beevik/etree v1.1.0 h1:T0xke/WvNtMoCqgzPhkX2r4rjY3GDZFi+FjpRZY2Jbs=
13 | github.com/beevik/etree v1.1.0/go.mod h1:r8Aw8JqVegEf0w2fDnATrX9VpkMcyFeM0FhwO62wh+A=
14 | github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
15 | github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
16 | github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
17 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
18 | github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
19 | github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
20 | github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
21 | github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
22 | github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
23 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
24 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
25 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
26 | github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
27 | github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
28 | github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
29 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
30 | github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
31 | github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3 h1:t8FVkw33L+wilf2QiWkw0UV77qRpcH/JHPKGpKa2E8g=
32 | github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s=
33 | github.com/gin-gonic/gin v1.4.0 h1:3tMoCCfM7ppqsR0ptz/wi1impNpT7/9wQtMZ8lr1mCQ=
34 | github.com/gin-gonic/gin v1.4.0/go.mod h1:OW2EZn3DO8Ln9oIKOvM++LBO+5UPHJJDH72/q/3rZdM=
35 | github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
36 | github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
37 | github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
38 | github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
39 | github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
40 | github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
41 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
42 | github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
43 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
44 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
45 | github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg=
46 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
47 | github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
48 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
49 | github.com/google/uuid v1.1.0 h1:Jf4mxPC/ziBnoPIdpQdPJ9OeiomAUHLvxmPRSPH9m4s=
50 | github.com/google/uuid v1.1.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
51 | github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
52 | github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
53 | github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
54 | github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
55 | github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
56 | github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
57 | github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
58 | github.com/json-iterator/go v1.1.6 h1:MrUvLMLTMxbqFJ9kzlvat/rYZqZnW3u4wkLzWTaFwKs=
59 | github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
60 | github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
61 | github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
62 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
63 | github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk=
64 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
65 | github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
66 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
67 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
68 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
69 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
70 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
71 | github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY=
72 | github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
73 | github.com/maruel/rs v0.0.0-20150922171536-2c81c4312fe4 h1:u9jwvcKbQpghIXgNl/EOL8hzhAFXh4ePrEP493W3tNA=
74 | github.com/maruel/rs v0.0.0-20150922171536-2c81c4312fe4/go.mod h1:kcRFpEzolcEklV6rD7W95mG49/sbdX/PlFmd7ni3RvA=
75 | github.com/mattn/go-isatty v0.0.7 h1:UvyT9uN+3r7yLEYSlJsbQGdsaB/a0DlgWP3pql6iwOc=
76 | github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
77 | github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
78 | github.com/mdp/qrterminal v1.0.1 h1:07+fzVDlPuBlXS8tB0ktTAyf+Lp1j2+2zK3fBOL5b7c=
79 | github.com/mdp/qrterminal v1.0.1/go.mod h1:Z33WhxQe9B6CdW37HaVqcRKzP+kByF3q/qLxOGe12xQ=
80 | github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
81 | github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
82 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
83 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
84 | github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI=
85 | github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
86 | github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
87 | github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
88 | github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc=
89 | github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
90 | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
91 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
92 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
93 | github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
94 | github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
95 | github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
96 | github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
97 | github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
98 | github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
99 | github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
100 | github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
101 | github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
102 | github.com/robfig/cron v1.2.0 h1:ZjScXvvxeQ63Dbyxy76Fj3AT3Ut0aKsyd2/tl3DTMuQ=
103 | github.com/robfig/cron v1.2.0/go.mod h1:JGuDeoQd7Z6yL4zQhZ3OPEVHB7fL6Ka6skscFHfmt2k=
104 | github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
105 | github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
106 | github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
107 | github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
108 | github.com/skip2/go-qrcode v0.0.0-20190110000554-dc11ecdae0a9 h1:lpEzuenPuO1XNTeikEmvqYFcU37GVLl8SRNblzyvGBE=
109 | github.com/skip2/go-qrcode v0.0.0-20190110000554-dc11ecdae0a9/go.mod h1:PLPIyL7ikehBD1OAjmKKiOEhbvWyHGaNDjquXMcYABo=
110 | github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
111 | github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
112 | github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI=
113 | github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
114 | github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8=
115 | github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
116 | github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk=
117 | github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
118 | github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=
119 | github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
120 | github.com/spf13/viper v1.4.0 h1:yXHLWeravcrgGyFSyCgdYpXQ9dR9c/WED3pg1RhxqEU=
121 | github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE=
122 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
123 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
124 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
125 | github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
126 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
127 | github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
128 | github.com/tuotoo/qrcode v0.0.0-20190222102259-ac9c44189bf2 h1:BWVtt2VBY+lmVDu9MGKqLGKl04B+iRHcrW1Ptyi/8tg=
129 | github.com/tuotoo/qrcode v0.0.0-20190222102259-ac9c44189bf2/go.mod h1:lPnW9HVS0vJdeYyQtOvIvlXgZPNhUAhwz+z5r8AJk0Y=
130 | github.com/ugorji/go v1.1.4 h1:j4s+tAvLfL3bZyefP2SEWmhBzmuIlH/eqNuPdFPgngw=
131 | github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
132 | github.com/willf/bitset v1.1.10 h1:NotGKqX0KwQ72NUzqrjZq5ipPNDQex9lo3WpaS8L2sc=
133 | github.com/willf/bitset v1.1.10/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4=
134 | github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
135 | github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
136 | go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
137 | go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
138 | go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
139 | go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
140 | golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
141 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
142 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
143 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
144 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
145 | golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
146 | golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
147 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
148 | golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
149 | golang.org/x/net v0.0.0-20190522155817-f3200d17e092 h1:4QSRKanuywn15aTZvI/mIDEgPQpswuFndXpOj3rKEco=
150 | golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
151 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
152 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
153 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
154 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
155 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
156 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
157 | golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
158 | golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
159 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
160 | golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
161 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894 h1:Cz4ceDQGXuKRnVBDTS23GTn/pU5OE2C0WrNTOYK1Uuc=
162 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
163 | golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
164 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
165 | golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
166 | golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
167 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
168 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
169 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
170 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
171 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
172 | google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
173 | gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
174 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
175 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
176 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
177 | gopkg.in/go-playground/assert.v1 v1.2.1 h1:xoYuJVE7KT85PYWrN730RguIQO0ePzVRfFMXadIrXTM=
178 | gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE=
179 | gopkg.in/go-playground/validator.v8 v8.18.2 h1:lFB4DoMU6B626w8ny76MV7VX6W2VHct2GVOI3xgiMrQ=
180 | gopkg.in/go-playground/validator.v8 v8.18.2/go.mod h1:RX2a/7Ha8BgOhfk7j780h4/u/RRjR0eouCJSH80/M2Y=
181 | gopkg.in/h2non/filetype.v1 v1.0.5 h1:CC1jjJjoEhNVbMhXYalmGBhOBK2V70Q1N850wt/98/Y=
182 | gopkg.in/h2non/filetype.v1 v1.0.5/go.mod h1:M0yem4rwSX5lLVrkEuRRp2/NinFMD5vgJ4DlAhZcfNo=
183 | gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
184 | gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
185 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
186 | gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
187 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
188 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
189 | rsc.io/qr v0.2.0 h1:6vBLea5/NRMVTz8V66gipeLycZMl/+UlFmk8DvqQ6WY=
190 | rsc.io/qr v0.2.0/go.mod h1:IF+uZjkb9fqyeF/4tlBoynqmQxUoPfWEKh921coOuXs=
191 |
--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "wxBot4g/models"
5 | "wxBot4g/pkg/define"
6 | "wxBot4g/wcbot"
7 |
8 | "github.com/sirupsen/logrus"
9 | )
10 |
11 | var (
12 | Bot *wcbot.WcBot
13 | )
14 |
15 | type WeChatBot struct {
16 | }
17 |
18 | func (w *WeChatBot)HandleMessage(msg models.RealRecvMsg) {
19 | //过滤不支持消息99
20 | if msg.MsgType == 99 || msg.MsgTypeId == 99 {
21 | return
22 | }
23 |
24 | //获取unknown的username
25 | contentUser := msg.Content.User.Name
26 | if msg.Content.User.Name == "unknown" {
27 | contentUser = Bot.GetGroupUserName(msg.Content.User.Uid)
28 | }
29 |
30 | logrus.Debug(
31 | "消息类型:", define.MsgIdString(msg.MsgTypeId), " ",
32 | "数据类型:", define.MsgTypeIdString(msg.Content.Type), " ",
33 | "发送者:", msg.FromUserName, " ",
34 | "发送人:", msg.SendMsgUSer.Name, " ",
35 | "发送内容人:", contentUser, " ",
36 | "内容:", msg.Content.Data)
37 | }
38 |
39 | func main() {
40 | Bot = wcbot.New()
41 | Bot.Debug = true
42 | //Bot.QrCodeInTerminal() //默认在 wxqr 目录生成二维码,调用此函数,在终端打印二维码
43 |
44 | Bot.AddHandler(&WeChatBot{})
45 |
46 | Bot.Run()
47 | }
48 |
--------------------------------------------------------------------------------
/models/base.go:
--------------------------------------------------------------------------------
1 | package models
2 |
3 | import "fmt"
4 |
5 | // 同步数据和keys
6 | type SyncKeysJsonData struct {
7 | Count int `json:"Count"`
8 | SyncKeys []SyncKey `json:"List"`
9 | }
10 |
11 | type SyncKey struct {
12 | Key int64 `json:"Key"`
13 | Val int64 `json:"Val"`
14 | }
15 |
16 | func (sks SyncKeysJsonData) ToString() string {
17 | resultStr := ""
18 |
19 | for i := 0; i < sks.Count; i++ {
20 | resultStr = resultStr + fmt.Sprintf("%d_%d|", sks.SyncKeys[i].Key, sks.SyncKeys[i].Val)
21 | }
22 |
23 | return resultStr[:len(resultStr)-1]
24 | }
25 |
26 | // RecvMsgs 微信消息对象列表
27 | type RecvMsgs struct {
28 | MsgCount int `json:"AddMsgCount"`
29 | MsgList []RecvMsg `json:"AddMsgList"`
30 | SyncKeys SyncKeysJsonData `json:"SyncKey"`
31 | ModContactCount int `json:"ModContactCount"`
32 | ModContactList []interface{} `json:"ModContactList"`
33 | }
34 |
35 | // RecvMsg 微信消息对象
36 | type RecvMsg struct {
37 | MsgId string `json:"MsgId"`
38 | FromUserName string `json:"FromUserName"`
39 | ToUserName string `json:"ToUserName"`
40 | MsgType int `json:"MsgType"`
41 | Content string `json:"Content"`
42 | CreateTime int64 `json:"CreateTime"`
43 | RecommendInfo interface{} `json:"RecommendInfo"`
44 | FileName string `json:"FileName"`
45 | AppMsgType int `json:"AppMsgType"`
46 | StatusNotifyCode int `json:"StatusNotifyCode"`
47 | StatusNotifyUserName string `json:"StatusNotifyUserName"`
48 | Url string `json:"Url"`
49 | }
50 |
51 | // RecvMsg 微信消息对象
52 | type RealRecvMsg struct {
53 | MsgTypeId int `json:"MsgTypeId"`
54 | MsgId string `json:"MsgId"`
55 | FromUserName string `json:"FromUserName"`
56 | ToUserName string `json:"ToUserName"`
57 | MsgType int `json:"MsgType"` //消息类型
58 | Content Content `json:"Content"`
59 | CreateTime int64 `json:"CreateTime"`
60 | SendMsgUSer MsgUser `json:"SendMsgUSer"`
61 | }
62 |
--------------------------------------------------------------------------------
/models/group.go:
--------------------------------------------------------------------------------
1 | package models
2 |
3 | type GroupMember struct {
4 | DisplayName string `json:"display_name"`
5 | Nickname string `json:"nickname"`
6 | RemarkName string `json:"remark_name"`
7 | }
8 |
--------------------------------------------------------------------------------
/models/msg.go:
--------------------------------------------------------------------------------
1 | package models
2 |
3 | type Content struct {
4 | Type int `json:"type"`
5 | Data string `json:"data"`
6 | Detail Detail `json:"detail,omitempty"`
7 | Desc string `json:"desc,omitempty"`
8 | User ContentUser `json:"user"`
9 | Img []byte `json:"img,omitempty"`
10 | Voice []byte `json:"voice,omitempty"`
11 | Other interface{} `json:"other,omitempty"`
12 | }
13 |
14 | type Detail struct {
15 | Type string `json:"type"`
16 | Value string `json:"value"`
17 | }
18 |
--------------------------------------------------------------------------------
/models/user.go:
--------------------------------------------------------------------------------
1 | package models
2 |
3 | /**
4 | User结构
5 | {
6 | "Uin":0,
7 | "UserName":"@d662309d027cb60f57991e409c4dd59a02cc718df7ab45fe9448c94e90f0581e",
8 | "NickName":"小二",
9 | "HeadImgUrl":"/cgi-bin/mmwebwx-bin/webwxgeticon?seq=693951199u0026username=@d662309d026skey=@crypt_7914bf7f_7772e6fd5ceb2c04c8",
10 | "ContactFlag":42343,
11 | "MemberCount":0,
12 | "MemberList":[
13 |
14 | ],
15 | "RemarkName":"行行行423",
16 | "HideInputBarFlag":0,
17 | "Sex":2,
18 | "Signature":"有亲友可待",
19 | "VerifyFlag":0,
20 | "OwnerUin":0,
21 | "PYInitial":"YE",
22 | "PYQuanPin":"darwr",
23 | "RemarkPYInitial":"QXMY",
24 | "RemarkPYQuanPin":"4654465",
25 | "StarFriend":0,
26 | "AppAccountFlag":0,
27 | "Statues":0,
28 | "AttrStatus":242173,
29 | "Province":"广东",
30 | "City":"广州",
31 | "Alias":"",
32 | "SnsFlag":49,
33 | "UniFriend":0,
34 | "DisplayName":"",
35 | "ChatRoomId":0,
36 | "KeyWord":"",
37 | "EncryChatRoomId":"",
38 | "IsOwner":0
39 | }
40 | */
41 | // ContactList 微信获取所有联系人列表
42 | type ContactList struct {
43 | Seq int `json:"Seq"`
44 | MemberCount int `json:"MemberCount"`
45 | MemberList []User `json:"MemberList"`
46 | }
47 |
48 | // GroupList 微信获取所有群列表
49 | type GroupList struct {
50 | Count int `json:"Count"`
51 | ContactList []GroupUser `json:"ContactList"`
52 | }
53 |
54 | type GroupUser struct {
55 | Uin int `json:"Uin"`
56 | UserName string `json:"UserName"` //群唯一标识(每次登陆变化)
57 | NickName string `json:"NickName"`
58 | MemberCount int `json:"MemberCount"`
59 | MemberList []User `json:"MemberList"`
60 | }
61 |
62 | // User 微信通用User结构
63 | type User struct {
64 | Uin int64 `json:"Uin"`
65 | UserName string `json:"UserName"`
66 | NickName string `json:"NickName"`
67 | DisplayName string `json:"DisplayName"`
68 | RemarkName string `json:"RemarkName"`
69 | Sex int8 `json:"Sex"`
70 | Province string `json:"Province"`
71 | City string `json:"City"`
72 | VerifyFlag int `json:"VerifyFlag"`
73 | Signature string `json:"Signature"` //个性签名
74 | EncryChatRoomId string `json:"EncryChatRoomId"` //群id
75 | }
76 |
77 | //accountInfo通信录
78 | type AccountInfo struct {
79 | Type string `json:"type"`
80 | User User `json:"user"`
81 | Group interface{} `json:"group"`
82 | }
83 |
84 | // MsgUser 消息User结构
85 | type MsgUser struct {
86 | ID string `json:"id"`
87 | Name string `json:"name"`
88 | }
89 |
90 | // ContentUser 消息User结构
91 | type ContentUser struct {
92 | Uid string `json:"uid,omitempty"`
93 | Name string `json:"name,omitempty"`
94 | }
95 |
--------------------------------------------------------------------------------
/pkg/define/typeDefine.go:
--------------------------------------------------------------------------------
1 | package define
2 |
3 | var (
4 | MsgIdList = make(map[int]string) //消息类型
5 | MsgTypeIdList = make(map[int]string) //消息数据类型
6 | )
7 |
8 | func init() {
9 | MsgIdList = map[int]string{
10 | 0: "init data", //初始化消息,内部数据
11 | 1: "Self", //自己发送的消息
12 | 2: "FileHelper", //文件消息
13 | 3: "Group", //群消息
14 | 4: "Contact", //联系人消息
15 | 5: "Public", //公众号消息
16 | 6: "Special", //特殊账号消息
17 | 51: "pull wxid", //获取wxid消息
18 | 99: "Unknown", // 未知账号消息}
19 | }
20 | MsgTypeIdList = map[int]string{
21 | 0: "Text",
22 | 1: "Location",
23 | 3: "Image",
24 | 4: "Voice",
25 | 5: "Recommend",
26 | 6: "Animation",
27 | 7: "Share",
28 | 8: "Video",
29 | 9: "VideoCall",
30 | 10: "Redraw",
31 | 11: "Empty",
32 | 99: "Unknown",
33 | }
34 | }
35 |
36 | func MsgIdString(msgId int) string {
37 | if typeName, ok := MsgIdList[msgId]; ok {
38 | return typeName
39 | } else {
40 | return "Unknown"
41 | }
42 | }
43 |
44 | func MsgTypeIdString(msgTypeId int) string {
45 | if typeName, ok := MsgTypeIdList[msgTypeId]; ok {
46 | return typeName
47 | } else {
48 | return "Unknown"
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/pkg/httpClient/http.go:
--------------------------------------------------------------------------------
1 | package httpClient
2 |
3 | import (
4 | "encoding/json"
5 | "io/ioutil"
6 | "net/http"
7 | "net/url"
8 | "strings"
9 |
10 | "github.com/sirupsen/logrus"
11 | )
12 |
13 | type Client struct {
14 | cookies []*http.Cookie
15 | headers map[string]string
16 | client http.Client
17 | }
18 |
19 | func New(headers map[string]string) *Client {
20 | httpClient := new(Client)
21 | httpClient.cookies = make([]*http.Cookie, 0)
22 | httpClient.headers = make(map[string]string)
23 | for k, v := range headers {
24 | httpClient.headers[k] = v
25 | }
26 |
27 | return httpClient
28 | }
29 |
30 | func (h *Client) Post(url string, body interface{}) ([]byte, error) {
31 | var (
32 | err error
33 | req *http.Request
34 | )
35 | if body != nil {
36 | jBody, err := json.Marshal(body)
37 | if err != nil {
38 | logrus.Error(err.Error())
39 | return nil, err
40 | }
41 | req, err = http.NewRequest("POST", url, strings.NewReader(string(jBody)))
42 | } else {
43 | req, err = http.NewRequest("POST", url, nil)
44 | }
45 |
46 | if err != nil {
47 | logrus.Error(err.Error())
48 | return nil, err
49 | }
50 |
51 | if len(h.cookies) > 0 {
52 | for _, cookie := range h.cookies {
53 | req.AddCookie(cookie)
54 | }
55 | }
56 | if h.headers != nil {
57 | for k, v := range h.headers {
58 | req.Header.Add(k, v)
59 | }
60 | }
61 |
62 | resp, err := h.client.Do(req)
63 | if err != nil {
64 | logrus.Error(err.Error())
65 | return nil, err
66 | }
67 | defer resp.Body.Close()
68 |
69 | out, err := ioutil.ReadAll(resp.Body)
70 | if err != nil {
71 | logrus.Error(err.Error())
72 | return nil, err
73 | }
74 |
75 | return out, nil
76 | }
77 |
78 | func (h *Client) PostString(url string, body string) ([]byte, error) {
79 | var (
80 | err error
81 | req *http.Request
82 | )
83 |
84 | req, err = http.NewRequest("POST", url, strings.NewReader(body))
85 | if err != nil {
86 | logrus.Error(err.Error())
87 | return nil, err
88 | }
89 |
90 | if len(h.cookies) > 0 {
91 | for _, cookie := range h.cookies {
92 | req.AddCookie(cookie)
93 | }
94 | }
95 | if h.headers != nil {
96 | for k, v := range h.headers {
97 | req.Header.Add(k, v)
98 | }
99 | }
100 |
101 | resp, err := h.client.Do(req)
102 | if err != nil {
103 | logrus.Error(err.Error())
104 | return nil, err
105 | }
106 | defer resp.Body.Close()
107 |
108 | out, err := ioutil.ReadAll(resp.Body)
109 | if err != nil {
110 | logrus.Error(err.Error())
111 | return nil, err
112 | }
113 |
114 | return out, nil
115 | }
116 |
117 | func (h *Client) PostMedia(url string, body []byte) ([]byte, error) {
118 | var (
119 | err error
120 | req *http.Request
121 | )
122 | if body != nil {
123 | req, err = http.NewRequest("POST", url, strings.NewReader(string(body)))
124 | } else {
125 | req, err = http.NewRequest("POST", url, nil)
126 | }
127 |
128 | if err != nil {
129 | logrus.Error(err.Error())
130 | return nil, err
131 | }
132 |
133 | if len(h.cookies) > 0 {
134 | for _, cookie := range h.cookies {
135 | req.AddCookie(cookie)
136 | }
137 | }
138 | if h.headers != nil {
139 | for k, v := range h.headers {
140 | req.Header.Add(k, v)
141 | }
142 | }
143 |
144 | resp, err := h.client.Do(req)
145 | if err != nil {
146 | logrus.Error(err.Error())
147 | return nil, err
148 | }
149 | defer resp.Body.Close()
150 |
151 | out, err := ioutil.ReadAll(resp.Body)
152 | if err != nil {
153 | logrus.Error(err.Error())
154 | return nil, err
155 | }
156 |
157 | return out, nil
158 | }
159 |
160 | func (h *Client) Get(url string, params url.Values) ([]byte, error) {
161 | if params != nil && len(params) > 0 {
162 | url = url + params.Encode()
163 | }
164 |
165 | req, err := http.NewRequest("GET", url, nil)
166 | if err != nil {
167 | logrus.Error(err.Error())
168 | return nil, err
169 | }
170 |
171 | if len(h.cookies) > 0 {
172 | for _, cookie := range h.cookies {
173 | req.AddCookie(cookie)
174 | }
175 | }
176 | if h.headers != nil {
177 | for k, v := range h.headers {
178 | req.Header.Add(k, v)
179 | }
180 | }
181 |
182 | resp, err := h.client.Do(req)
183 | if err != nil {
184 | logrus.Error(err.Error())
185 | return nil, err
186 | }
187 | defer resp.Body.Close()
188 |
189 | if len(h.cookies) == 0 {
190 | h.SetCookie(resp.Cookies())
191 | }
192 |
193 | out, err := ioutil.ReadAll(resp.Body)
194 | if err != nil {
195 | logrus.Error(err.Error())
196 | return nil, err
197 | }
198 | return out, nil
199 | }
200 |
201 | func (h *Client) SetCookie(cookie []*http.Cookie) {
202 | h.cookies = cookie
203 | }
204 |
205 | func (h *Client) GetCookie() []*http.Cookie {
206 | return h.cookies
207 | }
208 |
209 | func (h *Client) GetCookieByName(name string) *http.Cookie {
210 | for _, cookie := range h.cookies {
211 | if cookie.Name == name {
212 | return cookie
213 | }
214 | }
215 | return nil
216 | }
217 |
218 | func (h *Client) SetHeader(header map[string]string) {
219 | if header != nil {
220 | for key, value := range header {
221 | h.headers[key] = value
222 | }
223 | }
224 | }
225 |
226 | func (h *Client) GetHeader() map[string]string {
227 | return h.headers
228 | }
229 |
230 | func (h *Client) DelHeader(header map[string]string) {
231 | if header != nil {
232 | for key, _ := range header {
233 | delete(h.headers, key)
234 | }
235 | }
236 | }
237 |
--------------------------------------------------------------------------------
/pkg/utils/reg.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import "regexp"
4 |
5 | func RegexpMatchStr(regx string, data string) [][]string {
6 | reg := regexp.MustCompile(regx)
7 | pm := reg.FindAllStringSubmatch(data, -1)
8 | return pm
9 | }
10 |
--------------------------------------------------------------------------------
/pkg/utils/wirteFile.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import (
4 | "encoding/json"
5 | "os"
6 |
7 | "github.com/sirupsen/logrus"
8 | )
9 |
10 | func WriteFile(filepath string, data interface{}) error {
11 | f, err := os.OpenFile(filepath, os.O_RDWR|os.O_CREATE, 777)
12 | if err != nil {
13 | logrus.Error(err)
14 | return err
15 | }
16 | defer f.Close()
17 |
18 | switch data.(type) {
19 | case []byte:
20 | if _, err = f.Write(data.([]byte)); err != nil {
21 | logrus.Error(err)
22 | return err
23 | }
24 | default:
25 | jData, err := json.Marshal(data)
26 | if err != nil {
27 | logrus.Error(err)
28 | return err
29 | }
30 | if _, err = f.Write(jData); err != nil {
31 | logrus.Error(err)
32 | return err
33 | }
34 | }
35 |
36 | return nil
37 | }
38 |
--------------------------------------------------------------------------------
/wcbot/api.go:
--------------------------------------------------------------------------------
1 | package wcbot
2 |
3 | import (
4 | "bufio"
5 | "bytes"
6 | "crypto/md5"
7 | "encoding/hex"
8 | "encoding/json"
9 | "errors"
10 | "fmt"
11 | "io"
12 | "io/ioutil"
13 | "math/rand"
14 | "mime/multipart"
15 | "os"
16 | "strconv"
17 | "strings"
18 | "time"
19 |
20 | "gopkg.in/h2non/filetype.v1/types"
21 |
22 | "github.com/sirupsen/logrus"
23 |
24 | "gopkg.in/h2non/filetype.v1"
25 | )
26 |
27 | func (wc *WcBot) SendMsgByUid(word, dst string) bool {
28 | urlStr := wc.baseUri + fmt.Sprintf("/webwxsendmsg?pass_ticket=%s", wc.passTicket)
29 |
30 | msgId := strconv.Itoa(int(time.Now().UnixNano()/int64(time.Millisecond))) +
31 | strings.Replace(fmt.Sprintf("%f", rand.Float64()), ".", "", -1)
32 |
33 | body := struct {
34 | BaseRequest interface{} `json:"BaseRequest"`
35 | Msg interface{} `json:"Msg"`
36 | }{
37 | BaseRequest: wc.baseRequest,
38 | Msg: map[string]interface{}{
39 | "Type": 1,
40 | "Content": word,
41 | "FromUserName": wc.myAccount["UserName"],
42 | "ToUserName": dst,
43 | "LocalID": msgId,
44 | "ClientMsgId": msgId,
45 | },
46 | }
47 |
48 | data, err := wc.httpClient.Post(urlStr, body)
49 | if err != nil {
50 | logrus.Error(err.Error())
51 | return false
52 | }
53 |
54 | mData := struct {
55 | Ret int `json:"Ret"`
56 | }{}
57 | if err := json.Unmarshal(data, &mData); err != nil {
58 | logrus.Error(err.Error())
59 | return false
60 | }
61 |
62 | ret := mData.Ret == 0
63 |
64 | return ret
65 | }
66 |
67 | func (wc *WcBot) SendMsg(name, word string, isFile bool) bool {
68 | uId := wc.GetUserId(name)
69 | if uId != "" {
70 | if isFile {
71 | f, err := os.Open(word)
72 | if err != nil {
73 | logrus.Error(err)
74 | return false
75 | }
76 | defer f.Close()
77 | rd := bufio.NewReader(f)
78 | result := true
79 | for {
80 | line, err := rd.ReadString('\n')
81 | if err != nil || io.EOF == err {
82 | break
83 | }
84 | logrus.Debug("-> " + name + ": " + line)
85 | result = wc.SendMsgByUid(line, uId)
86 | if !result {
87 | logrus.Error("send msg by uid error")
88 | }
89 |
90 | time.Sleep(time.Second)
91 | }
92 | return result
93 | } else {
94 | if wc.SendMsgByUid(word, uId) {
95 | return true
96 | } else {
97 | return false
98 | }
99 | }
100 | } else {
101 | logrus.Error("user is not exist")
102 | return false
103 | }
104 | }
105 |
106 | func (wc *WcBot) SendMediaMsgByUid(filepath, to string) error {
107 | info, err := os.Stat(filepath)
108 | if err != nil {
109 | logrus.Error(err)
110 | return err
111 | }
112 |
113 | buf, err := ioutil.ReadFile(filepath)
114 | if err != nil {
115 | logrus.Error(err)
116 | return err
117 | }
118 |
119 | kind, err := filetype.Get(buf)
120 | if err != nil {
121 | logrus.Error(err)
122 | return err
123 | }
124 |
125 | media, err := wc.UploadMedia(buf, kind, info, to)
126 | if err != nil {
127 | logrus.Error(err)
128 | return err
129 | }
130 |
131 | var msg = make(map[string]interface{})
132 | msg["FromUserName"] = wc.myAccount["UserName"].(string)
133 | msg["ToUserName"] = to
134 | msg["LocalID"] = fmt.Sprintf("%d", time.Now().Unix())
135 | msg["ClientMsgId"] = msg["LocalID"]
136 |
137 | if filetype.IsImage(buf) {
138 | if strings.HasSuffix(kind.MIME.Value, `gif`) {
139 | msg["Type"] = 47
140 | msg["MediaId"] = media
141 | msg["EmojiFlag"] = 2
142 | if err := wc.sendMsgEmoticon(msg); err != nil {
143 | return err
144 | }
145 | } else {
146 | msg["Type"] = 3
147 | msg["MediaId"] = media
148 | if err := wc.sendMsgImage(msg); err != nil {
149 | return err
150 | }
151 | }
152 | } else {
153 | info, _ := os.Stat(filepath)
154 | if filetype.IsVideo(buf) {
155 | msg["Type"] = 43
156 | msg["MediaId"] = media
157 | if err := wc.sendMsgVideo(msg); err != nil {
158 | return err
159 | }
160 | } else {
161 | msg["Type"] = 6
162 | msg[`Content`] = fmt.Sprintf(`%s610%s%s`, info.Name(), media, kind.Extension)
163 | if err := wc.sendMsgFile(msg); err != nil {
164 | return err
165 | }
166 | }
167 | }
168 |
169 | return err
170 | }
171 |
172 | func (wc *WcBot) SendMedia(imagePath, toName string) error {
173 | to := wc.GetUserId(toName)
174 | if to != "" {
175 | if err := wc.SendMediaMsgByUid(imagePath, to); err == nil {
176 | return nil
177 | } else {
178 | return err
179 | }
180 | } else {
181 | logrus.Error("user is not exist")
182 | return errors.New("user is not exist")
183 | }
184 | }
185 |
186 | func (wc *WcBot) UploadMedia(buf []byte, kind types.Type, info os.FileInfo, to string) (string, error) {
187 | head := buf[:261]
188 |
189 | var mediaType string
190 | if filetype.IsImage(head) {
191 | mediaType = `pic`
192 | } else if filetype.IsVideo(head) {
193 | mediaType = `video`
194 | } else {
195 | mediaType = `doc`
196 | }
197 |
198 | fields := map[string]string{
199 | `id`: `WU_FILE_` + fmt.Sprintf("%d", wc.fileIndex),
200 | `name`: info.Name(),
201 | `type`: kind.MIME.Value,
202 | `lastModifiedDate`: info.ModTime().UTC().String(),
203 | `size`: fmt.Sprintf("%d", info.Size()),
204 | `mediatype`: mediaType,
205 | `pass_ticket`: wc.passTicket,
206 | `webwx_data_ticket`: wc.httpClient.GetCookieByName("webwx_data_ticket").Value,
207 | }
208 | md5Ctx := md5.New()
209 | md5Ctx.Write(buf)
210 | cipherStr := md5Ctx.Sum(nil)
211 | media, err := json.Marshal(&map[string]interface{}{
212 | `BaseRequest`: wc.baseRequest,
213 | `ClientMediaId`: fmt.Sprintf("%d", time.Now().Unix()),
214 | `TotalLen`: fmt.Sprintf("%d", info.Size()),
215 | `StartPos`: 0,
216 | `DataLen`: fmt.Sprintf("%d", info.Size()),
217 | `MediaType`: 4,
218 | `UploadType`: 2,
219 | `ToUserName`: to,
220 | `FromUserName`: wc.myAccount["UserName"].(string),
221 | `FileMd5`: hex.EncodeToString(cipherStr),
222 | })
223 |
224 | if err != nil {
225 | logrus.Error(err)
226 | return "", err
227 | }
228 |
229 | body := &bytes.Buffer{}
230 | writer := multipart.NewWriter(body)
231 |
232 | fw, err := writer.CreateFormFile(`filename`, info.Name())
233 | if err != nil {
234 | logrus.Error(err)
235 | return "", err
236 | }
237 | _, err = fw.Write(buf)
238 | if err != nil {
239 | logrus.Error(err)
240 | return "", err
241 | }
242 |
243 | for k, v := range fields {
244 | err = writer.WriteField(k, v)
245 | }
246 | err = writer.WriteField(`uploadmediarequest`, string(media))
247 |
248 | if err != nil {
249 | logrus.Error(err)
250 | return "", err
251 | }
252 |
253 | writer.Close()
254 | data, _ := ioutil.ReadAll(body)
255 |
256 | header := make(map[string]string)
257 | header["Content-Type"] = writer.FormDataContentType()
258 | wc.httpClient.SetHeader(header)
259 |
260 | strUrl := `https://file.wx.qq.com/cgi-bin/mmwebwx-bin/webwxuploadmedia?f=json`
261 | resp, err := wc.httpClient.PostMedia(strUrl, data)
262 | if err != nil {
263 | logrus.Error(err)
264 | return "", err
265 | }
266 | wc.httpClient.DelHeader(header)
267 |
268 | wc.fileIndex++
269 |
270 | mData := make(map[string]interface{})
271 | err = json.Unmarshal(resp, &mData)
272 | if err != nil {
273 | logrus.Error(err)
274 | return "", err
275 | }
276 |
277 | return mData["MediaId"].(string), nil
278 | }
279 |
280 | func (wc *WcBot) sendMsgImage(con map[string]interface{}) error {
281 | strUrl := fmt.Sprintf("https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxsendmsgimg?fun=async&f=json&pass_ticket=%s",
282 | wc.passTicket)
283 | jCon, err := json.Marshal(con)
284 | if err != nil {
285 | logrus.Error(err)
286 | return err
287 | }
288 | body := fmt.Sprintf(`{"BaseRequest":{"Uin":%s,"Sid":"%s","Skey":"%s","DeviceID":"%s"},"Msg":%s,"Scene":0}`,
289 | wc.uin, wc.sid, wc.sKey, wc.deviceId, jCon)
290 |
291 | header := make(map[string]string)
292 | header["Content-Type"] = `application/json;charset=UTF-8`
293 | wc.httpClient.SetHeader(header)
294 |
295 | _, err = wc.httpClient.PostString(strUrl, body)
296 | if err != nil {
297 | logrus.Error(err)
298 | return err
299 | }
300 | wc.httpClient.DelHeader(header)
301 | return nil
302 | }
303 |
304 | func (wc *WcBot) sendMsgEmoticon(con map[string]interface{}) error {
305 | strUrl := fmt.Sprintf("https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxsendemoticon?fun=sys&f=json&pass_ticket=%s", wc.passTicket)
306 | jCon, err := json.Marshal(con)
307 | if err != nil {
308 | return err
309 | }
310 | body := fmt.Sprintf(`{"BaseRequest":{"Uin":%s,"Sid":"%s","Skey":"%s","DeviceID":"%s"},"Msg":%s,"Scene":0}`,
311 | wc.uin, wc.sid, wc.sKey, wc.deviceId, jCon)
312 |
313 | header := make(map[string]string)
314 | header["Content-Type"] = `application/json;charset=UTF-8`
315 | wc.httpClient.SetHeader(header)
316 |
317 | _, err = wc.httpClient.PostString(strUrl, body)
318 | if err != nil {
319 | logrus.Error(err)
320 | return err
321 | }
322 | wc.httpClient.DelHeader(header)
323 | return nil
324 | }
325 |
326 | func (wc *WcBot) sendMsgVideo(con map[string]interface{}) error {
327 | strUrl := fmt.Sprintf("https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxsendvideomsg?fun=async&f=json&pass_ticket=%s", wc.passTicket)
328 | jCon, err := json.Marshal(con)
329 | if err != nil {
330 | return err
331 | }
332 | body := fmt.Sprintf(`{"BaseRequest":{"Uin":%s,"Sid":"%s","Skey":"%s","DeviceID":"%s"},"Msg":%s,"Scene":0}`,
333 | wc.uin, wc.sid, wc.sKey, wc.deviceId, jCon)
334 |
335 | header := make(map[string]string)
336 | header["Content-Type"] = `application/json;charset=UTF-8`
337 | wc.httpClient.SetHeader(header)
338 |
339 | _, err = wc.httpClient.PostString(strUrl, body)
340 | if err != nil {
341 | logrus.Error(err)
342 | return err
343 | }
344 | wc.httpClient.DelHeader(header)
345 | return nil
346 | }
347 |
348 | func (wc *WcBot) sendMsgFile(con map[string]interface{}) error {
349 | strUrl := fmt.Sprintf("https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxsendappmsg?fun=async&f=json&pass_ticket=%s", wc.passTicket)
350 | jCon, err := json.Marshal(con)
351 | if err != nil {
352 | return err
353 | }
354 | body := fmt.Sprintf(`{"BaseRequest":{"Uin":%s,"Sid":"%s","Skey":"%s","DeviceID":"%s"},"Msg":%s,"Scene":0}`,
355 | wc.uin, wc.sid, wc.sKey, wc.deviceId, jCon)
356 |
357 | header := make(map[string]string)
358 | header["Content-Type"] = `application/json;charset=UTF-8`
359 | wc.httpClient.SetHeader(header)
360 |
361 | _, err = wc.httpClient.PostString(strUrl, body)
362 | if err != nil {
363 | logrus.Error(err)
364 | return err
365 | }
366 | wc.httpClient.DelHeader(header)
367 | return nil
368 | }
369 |
--------------------------------------------------------------------------------
/wcbot/cron.go:
--------------------------------------------------------------------------------
1 | package wcbot
2 |
3 | //import "C"
4 | import (
5 | "fmt"
6 | "wxBot4g/config"
7 | "net/http"
8 | "time"
9 |
10 | "github.com/sirupsen/logrus"
11 |
12 | "github.com/robfig/cron"
13 | )
14 |
15 | func InitHeartbeatCron() {
16 | c := cron.New()
17 | err := c.AddFunc("@every 180s", heartbeat)
18 | if err != nil {
19 | logrus.Error(err)
20 | return
21 | }
22 |
23 | c.Start()
24 | return
25 | }
26 |
27 | func heartbeat() {
28 | retryTimes := 0
29 | logrus.Debug(time.Now())
30 | RETRY:
31 | urlStr := fmt.Sprintf("http://127.0.0.1:%d/v1/health/heartbeat?word=keepalive&appKey=%s",
32 | config.Config.ServerConf.Port, config.Config.ServerConf.AppKey)
33 |
34 | if _, err := http.Get(urlStr); err != nil {
35 | logrus.Error("wechat bot is die, now retry to send keepalive")
36 | if config.Config.ServerConf.RetryTimes > 0 && retryTimes < config.Config.ServerConf.RetryTimes {
37 | retryTimes++
38 | time.Sleep(time.Second)
39 | goto RETRY
40 | } else {
41 | logrus.Error("wechat bot is die, over send keepalive")
42 | //TODO 警报通知管理官,机器人挂了。钉钉/微信/企业微信/邮件
43 | }
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/wcbot/imageHandle.go:
--------------------------------------------------------------------------------
1 | package wcbot
2 |
3 | import (
4 | "net/http"
5 | "os"
6 | "path"
7 | "wxBot4g/config"
8 |
9 | "github.com/sirupsen/logrus"
10 |
11 | "github.com/gin-gonic/gin"
12 | )
13 |
14 | /*
15 | to: 目的好友/群
16 | image: 通过上传接口
17 | */
18 | func ImageHandle(c *gin.Context) {
19 | //消息处理
20 | if err := handleImageMsg(c); err != nil {
21 | c.Status(http.StatusBadRequest)
22 | _, _ = c.Writer.Write(nil)
23 | }
24 |
25 | c.Status(http.StatusOK)
26 | _, _ = c.Writer.Write(nil)
27 | }
28 |
29 | func handleImageMsg(c *gin.Context) error {
30 | to := c.Query("to")
31 |
32 | file, err := c.FormFile("file")
33 | if err != nil {
34 | logrus.Error(err)
35 | return err
36 | }
37 |
38 | filename := path.Base(file.Filename)
39 | filename = config.Config.WxBot4gConf.ImageDir + filename
40 | err = c.SaveUploadedFile(file, filename)
41 | if err != nil {
42 | logrus.Error(err)
43 | return err
44 | }
45 |
46 | if _, err := os.Stat(filename); err == nil {
47 | if to == "" {
48 | if err := WechatBot.SendMediaMsgByUid(filename, "filehelper"); err != nil {
49 | logrus.Error(err)
50 | return err
51 | }
52 | } else {
53 | if err := WechatBot.SendMedia(filename, to); err != nil {
54 | logrus.Error(err)
55 | return err
56 | }
57 | }
58 | if err = os.Remove(filename); err != nil {
59 | logrus.Error(err)
60 | return err
61 | }
62 | return nil
63 | } else {
64 | logrus.Error(err)
65 | return err
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/wcbot/imageHandle_test.go:
--------------------------------------------------------------------------------
1 | package wcbot
2 |
3 | import (
4 | "bytes"
5 | "fmt"
6 | "io"
7 | "io/ioutil"
8 | "mime/multipart"
9 | "net/http"
10 | "os"
11 | "testing"
12 | )
13 |
14 | func postFile(targetUrl, filename string) (*http.Response, error) {
15 | bodyBuf := bytes.NewBufferString("")
16 | bodyWriter := multipart.NewWriter(bodyBuf)
17 |
18 | _, err := bodyWriter.CreateFormFile("file", filename)
19 | if err != nil {
20 | fmt.Println("error writing to buffer")
21 | return nil, err
22 | }
23 |
24 | fh, err := os.Open(filename)
25 | if err != nil {
26 | fmt.Println("error opening file")
27 | return nil, err
28 | }
29 | boundary := bodyWriter.Boundary()
30 | closeBuf := bytes.NewBufferString(fmt.Sprintf("\r\n--%s--\r\n", boundary))
31 |
32 | requestReader := io.MultiReader(bodyBuf, fh, closeBuf)
33 | fi, err := fh.Stat()
34 | if err != nil {
35 | fmt.Printf("Error Stating file: %s", filename)
36 | return nil, err
37 | }
38 | req, err := http.NewRequest("POST", targetUrl, requestReader)
39 | if err != nil {
40 | return nil, err
41 | }
42 |
43 | req.Header.Add("Content-Type", "multipart/form-data; boundary="+boundary)
44 | req.ContentLength = fi.Size() + int64(bodyBuf.Len()) + int64(closeBuf.Len())
45 |
46 | return http.DefaultClient.Do(req)
47 | }
48 |
49 | func TestImageHandle(t *testing.T) {
50 | resp, err := postFile(`http://127.0.0.1:7788/v1/msg/image?appKey=khr9348yo1oh`, `C:/Users/Administrator/Pictures/2.jpg`)
51 | if err != nil {
52 | return
53 | }
54 | defer resp.Body.Close()
55 |
56 | body, err := ioutil.ReadAll(resp.Body)
57 | if err != nil {
58 | fmt.Println(" post err=", err)
59 | }
60 | fmt.Println(string(body))
61 | }
62 |
--------------------------------------------------------------------------------
/wcbot/middleware.go:
--------------------------------------------------------------------------------
1 | package wcbot
2 |
3 | import (
4 | "net/http"
5 | "wxBot4g/config"
6 |
7 | "github.com/sirupsen/logrus"
8 |
9 | "github.com/gin-gonic/gin"
10 | )
11 |
12 | func Auth() gin.HandlerFunc {
13 | return func(c *gin.Context) {
14 | logrus.Debug(c.Request.URL)
15 | if c.Request.URL.Path == "/favicon.ico" {
16 | c.Abort()
17 | return
18 | }
19 |
20 | appKey := c.Query("appKey")
21 | if appKey != config.Config.ServerConf.AppKey {
22 | c.Status(http.StatusBadRequest)
23 | _, _ = c.Writer.Write(nil)
24 | c.Abort()
25 | return
26 | }
27 |
28 | c.Next()
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/wcbot/testHandle_test.go:
--------------------------------------------------------------------------------
1 | package wcbot
2 |
3 | import (
4 | "net/http"
5 | "testing"
6 | )
7 |
8 | func TestTextHandle(t *testing.T) {
9 | urlStr := "http://127.0.0.1:7788/v1/msg/text?to=测试&word=那就等下&appKey=khr9348yo1oh"
10 | _, err := http.Get(urlStr)
11 | if err != nil {
12 | t.Error(err.Error())
13 | return
14 | }
15 | t.Log("send text msg ok")
16 | }
17 |
--------------------------------------------------------------------------------
/wcbot/textHandle.go:
--------------------------------------------------------------------------------
1 | package wcbot
2 |
3 | import (
4 | "errors"
5 | "net/http"
6 |
7 | "github.com/gin-gonic/gin"
8 | "github.com/sirupsen/logrus"
9 | )
10 |
11 | func TextHandle(c *gin.Context) {
12 | //消息处理
13 | if err := handleTextMsg(c); err != nil {
14 | c.Status(http.StatusBadRequest)
15 | _, _ = c.Writer.Write(nil)
16 | }
17 |
18 | c.Status(http.StatusOK)
19 | _, _ = c.Writer.Write(nil)
20 | }
21 |
22 | func handleTextMsg(c *gin.Context) error {
23 | to := c.Query("to")
24 | word := c.Query("word")
25 |
26 | if to == "" && word == "" {
27 | logrus.Error("param error")
28 | return errors.New("param error")
29 | }
30 |
31 | if to == "" {
32 | if ok := WechatBot.SendMsgByUid(word, "filehelper"); !ok {
33 | logrus.Error("send msg error")
34 | return errors.New("send msg error")
35 | }
36 | } else {
37 | if ok := WechatBot.SendMsg(to, word, false); !ok {
38 | logrus.Error("send msg error")
39 | return errors.New("send msg error")
40 | }
41 | }
42 | return nil
43 | }
44 |
--------------------------------------------------------------------------------
/wcbot/utils.go:
--------------------------------------------------------------------------------
1 | package wcbot
2 |
3 | import (
4 | "fmt"
5 | "regexp"
6 | "strings"
7 | "wxBot4g/models"
8 |
9 | "github.com/sirupsen/logrus"
10 | )
11 |
12 | func (wc *WcBot) isContact(uid string) bool {
13 | for _, contact := range wc.contactList {
14 | if uid == contact.UserName {
15 | return true
16 | }
17 | }
18 | return false
19 | }
20 |
21 | func (wc *WcBot) isPublic(uid string) bool {
22 | for _, contact := range wc.publicList {
23 | if uid == contact.UserName {
24 | return true
25 | }
26 | }
27 | return false
28 | }
29 |
30 | func (wc *WcBot) isSpecial(uid string) bool {
31 | for _, contact := range wc.specialList {
32 | if uid == contact.UserName {
33 | return true
34 | }
35 | }
36 | return false
37 | }
38 |
39 | func (wc *WcBot) getContactInfo(uid string) models.AccountInfo {
40 | if _, ok := wc.accountInfo["normal_member"][uid]; ok {
41 | return wc.accountInfo["normal_member"][uid]
42 | }
43 | return models.AccountInfo{}
44 | }
45 |
46 | func (wc *WcBot) getGroupMemberInfo(uid string) models.AccountInfo {
47 | if _, ok := wc.accountInfo["group_member"][uid]; ok {
48 | return wc.accountInfo["group_member"][uid]
49 | }
50 | return models.AccountInfo{}
51 | }
52 |
53 | func (wc *WcBot) getContactName(uid string) *models.GroupMember {
54 | info := wc.getContactInfo(uid)
55 |
56 | //info = info["info"].(map[string]interface{})
57 |
58 | var groupMember models.GroupMember
59 | if info.User.RemarkName != "" {
60 | groupMember.RemarkName = info.User.RemarkName
61 | }
62 |
63 | if info.User.DisplayName != "" {
64 | groupMember.DisplayName = info.User.DisplayName
65 | }
66 |
67 | if info.User.NickName != "" {
68 | groupMember.Nickname = info.User.NickName
69 | }
70 | return &groupMember
71 | }
72 |
73 | func (wc *WcBot) getContactPreferName(name *models.GroupMember) string {
74 | if name == nil {
75 | return ""
76 | }
77 | if name.RemarkName != "" {
78 | return name.RemarkName
79 | }
80 |
81 | if name.DisplayName != "" {
82 | return name.DisplayName
83 | }
84 |
85 | if name.Nickname != "" {
86 | return name.Nickname
87 | }
88 | return ""
89 | }
90 |
91 | func (wc *WcBot) getGroupMemberPreferName(name *models.GroupMember) string {
92 | if name == nil {
93 | return ""
94 | }
95 | if name.RemarkName != "" {
96 | return name.RemarkName
97 | }
98 |
99 | if name.DisplayName != "" {
100 | return name.DisplayName
101 | }
102 |
103 | if name.Nickname != "" {
104 | return name.Nickname
105 | }
106 | return ""
107 | }
108 |
109 | /**
110 | getGroupMemberName 获取群聊中指定成员的名称信息
111 |
112 | param gid: 群id
113 | param uid: 群聊成员id
114 | return: 名称信息,类似 {"display_name": "test_user", "nickname": "test", "remark_name": "for_test" }
115 | */
116 | func (wc *WcBot) getGroupMemberName(gid, uid string) *models.GroupMember {
117 | groups, ok := wc.groupMembers[gid]
118 | if !ok {
119 | return nil
120 | }
121 | for _, group := range groups {
122 | if group.UserName == uid {
123 | groupMember := new(models.GroupMember)
124 | if group.RemarkName != "" {
125 | groupMember.RemarkName = group.RemarkName
126 | }
127 |
128 | if group.DisplayName != "" {
129 | groupMember.DisplayName = group.DisplayName
130 | }
131 |
132 | if group.NickName != "" {
133 | groupMember.Nickname = group.NickName
134 | }
135 |
136 | return groupMember
137 | }
138 | }
139 | return nil
140 | }
141 |
142 | func (wc *WcBot) GetGroupUserName(uId string) string {
143 | if uId == "" {
144 | return "unknown"
145 | }
146 | if err := wc.batchGetGroupMembers(); err != nil {
147 | logrus.Error(err)
148 | return "unknown"
149 | }
150 | for _, groupUsers := range wc.groupMembers {
151 | for _, user := range groupUsers {
152 | if uId == user.UserName {
153 | if uId == user.DisplayName {
154 | return user.DisplayName
155 | } else if uId == user.NickName {
156 | return user.NickName
157 | } else {
158 | return "unknown"
159 | }
160 | }
161 | }
162 | }
163 | return "unknown"
164 | }
165 |
166 | func (wc *WcBot) searchContent(key, content, fmat string) string {
167 | return "unknown"
168 | }
169 |
170 | func (wc *WcBot) procAtInfo(msg string) (string, string, []models.Detail) {
171 | if msg == "" {
172 | return "", "", nil
173 | }
174 |
175 | segs := strings.Split(msg, `\u2005`)
176 | strMsgAll := ""
177 | strMsg := ""
178 | infos := make([]models.Detail, 0)
179 | if len(segs) > 1 {
180 | for i := 0; i < len(segs)-1; i++ {
181 | segs[i] += `\u2005`
182 | reg := regexp.MustCompile(`@.*\u2005`)
183 | pmm := reg.FindAllStringSubmatch(segs[i], -1)
184 | if pmm[0] != nil {
185 | pm := ""
186 | for key, value := range pmm[0] {
187 | if key >= 2 {
188 | pm = pm + value
189 | }
190 | }
191 | name := pm
192 | str := strings.Replace(segs[i], pm, "", -1)
193 | strMsgAll = strMsgAll + str + "@" + name + " "
194 | strMsg += str
195 | if str != "" {
196 | infos = append(infos, models.Detail{Type: "str", Value: str})
197 | }
198 | infos = append(infos, models.Detail{Type: "at", Value: str})
199 | } else {
200 | infos = append(infos, models.Detail{Type: "str", Value: segs[i]})
201 | strMsgAll += segs[i]
202 | strMsg += segs[i]
203 | }
204 | }
205 | strMsgAll = strMsgAll + segs[len(segs)-1]
206 | strMsg += segs[len(segs)-1]
207 | infos = append(infos, models.Detail{Type: "str", Value: segs[(len(segs) - 1)]})
208 | } else {
209 | infos = append(infos, models.Detail{Type: "str", Value: segs[(len(segs) - 1)]})
210 | strMsgAll = msg
211 | strMsg = msg
212 | }
213 | return strings.ReplaceAll(strMsgAll, "\u2005", ""), strings.ReplaceAll(strMsg, "\u2005", ""), infos
214 | }
215 | func (wc *WcBot) getMsgImgUrl(msgid string) string {
216 | return wc.baseUri + fmt.Sprintf(`/webwxgetmsgimg?MsgID=%s&skey=%s`, msgid, wc.sKey)
217 | }
218 |
219 | func (wc *WcBot) getVoiceUrl(msgid string) string {
220 | return wc.baseUri + fmt.Sprintf(`/webwxgetvoice?msgid=%s&skey=%s`, msgid, wc.sKey)
221 |
222 | }
223 |
224 | func (wc *WcBot) getVideoUrl(msgid string) string {
225 | return wc.baseUri + fmt.Sprintf(`/webwxgetvideo?msgid=%s&skey=%s`, msgid, wc.sKey)
226 | }
227 |
228 | func (wc *WcBot) sendMsgImgAliyun(msgid, uin string) string {
229 | return ""
230 | }
231 |
--------------------------------------------------------------------------------
/wcbot/wcbot.go:
--------------------------------------------------------------------------------
1 | package wcbot
2 |
3 | import (
4 | "bytes"
5 | "encoding/hex"
6 | "encoding/json"
7 | "errors"
8 | "fmt"
9 | "html"
10 | "math/rand"
11 | "net/http"
12 | "net/url"
13 | "os"
14 | "path"
15 | "strconv"
16 | "strings"
17 | "time"
18 | "wxBot4g/config"
19 | "wxBot4g/models"
20 | "wxBot4g/pkg/httpClient"
21 | "wxBot4g/pkg/utils"
22 |
23 | "github.com/mdp/qrterminal"
24 |
25 | "github.com/gin-gonic/gin"
26 |
27 | _ "wxBot4g/config"
28 |
29 | "github.com/beevik/etree"
30 | "github.com/sirupsen/logrus"
31 | "github.com/skip2/go-qrcode"
32 | qrcodetl "github.com/tuotoo/qrcode"
33 | )
34 |
35 | type Handler interface {
36 | HandleMessage(models.RealRecvMsg)
37 | }
38 |
39 | type WcBot struct {
40 | Debug bool
41 | QrCodeTerminal bool
42 | uuid string
43 | baseUri string
44 | baseHost string
45 | redirectUri string
46 | uin string
47 | sid string
48 | sKey string
49 | passTicket string
50 | deviceId string
51 | Cookies []*http.Cookie
52 |
53 | baseRequest map[string]interface{}
54 | syncKeyStr string
55 | syncKey interface{}
56 | syncHost string
57 | status string
58 | batchCount int //一次拉取50个联系人的信息
59 | fullUserNameList []string //直接获取不到通讯录时,获取的username列表
60 | wxIdList []string //获取到的wxid的列表
61 | cursor int //拉取联系人信息的游标
62 | isBigContact bool //通讯录人数过多,无法直接获取
63 | tempPwd string
64 | httpClient *httpClient.Client
65 | conf map[string]interface{}
66 | myAccount map[string]interface{}
67 | chatSet string //当前登录用户
68 | memberList []models.User //所有相关账号: 联系人, 公众号, 群组, 特殊账号
69 | groupMembers map[string][]models.User //所有群组的成员, {'group_id1': [member1, member2, ...], ...}
70 | accountInfo map[string]map[string]models.AccountInfo //所有账户, {'group_member':{'id':{'type':'group_member', 'info':{}}, ...}, 'normal_member':{'id':{}, ...}}
71 | contactList []models.User // 联系人列表
72 | publicList []models.User // 公众账号列表
73 | groupList []models.User // 群聊列表
74 | specialList []models.User // 特殊账号列表
75 | encryChatRoomIdList map[string]string // 存储群聊的EncryChatRoomId,获取群内成员头像时需要用到
76 | groupIdName map[string]interface{}
77 | fileIndex int
78 | send2oss bool
79 | ossUrl string
80 | handler Handler
81 | }
82 |
83 | var (
84 | UNKONWN = "unkonwn"
85 | SUCCESS = "200"
86 | SCANED = "201"
87 | TIMEOUT = "408"
88 | ERRSYSTEM = "500"
89 | )
90 |
91 | var (
92 | WechatBot *WcBot
93 | )
94 |
95 | func New() *WcBot {
96 | wcBot := new(WcBot)
97 | wcBot.Debug = true
98 | wcBot.QrCodeTerminal = false
99 | wcBot.uuid = ""
100 | wcBot.baseUri = ""
101 | wcBot.baseHost = ""
102 | wcBot.redirectUri = ""
103 | wcBot.uin = ""
104 | wcBot.sid = ""
105 | wcBot.sKey = ""
106 | wcBot.passTicket = ""
107 | wcBot.deviceId = ""
108 | wcBot.Cookies = make([]*http.Cookie, 0)
109 |
110 | wcBot.baseRequest = make(map[string]interface{})
111 | wcBot.syncKeyStr = ""
112 | wcBot.syncHost = ""
113 | wcBot.status = "wait4login"
114 | wcBot.batchCount = 50
115 | wcBot.fullUserNameList = make([]string, 0)
116 | wcBot.wxIdList = make([]string, 0)
117 | wcBot.cursor = 0
118 | wcBot.isBigContact = false
119 | wcBot.tempPwd = config.Config.WxBot4gConf.WxQrDir
120 | wcBot.httpClient = httpClient.New(map[string]string{"User-Agent": "Mozilla/5.0 (X11; Linux i686; U;) Gecko/20070322 Kazehakase/0.4.5"})
121 | wcBot.conf = make(map[string]interface{})
122 |
123 | wcBot.chatSet = ""
124 | wcBot.myAccount = make(map[string]interface{})
125 | wcBot.memberList = make([]models.User, 0)
126 | wcBot.groupMembers = make(map[string][]models.User)
127 |
128 | wcBot.accountInfo = make(map[string]map[string]models.AccountInfo)
129 | wcBot.accountInfo["normal_member"] = make(map[string]models.AccountInfo)
130 | wcBot.accountInfo["group_member"] = make(map[string]models.AccountInfo)
131 |
132 | wcBot.contactList = make([]models.User, 0)
133 | wcBot.publicList = make([]models.User, 0)
134 | wcBot.groupList = make([]models.User, 0)
135 | wcBot.specialList = make([]models.User, 0)
136 | wcBot.encryChatRoomIdList = make(map[string]string)
137 | wcBot.groupIdName = make(map[string]interface{})
138 | wcBot.fileIndex = 0
139 | wcBot.send2oss = false
140 | wcBot.ossUrl = ""
141 | WechatBot = wcBot
142 |
143 | if _, err := os.Stat(wcBot.tempPwd); err != nil {
144 | if !os.IsExist(err) {
145 | if err := os.Mkdir(wcBot.tempPwd, os.ModePerm); err != nil {
146 | logrus.Error(err)
147 | return nil
148 | }
149 | }
150 | }
151 |
152 | return wcBot
153 | }
154 |
155 | func (wc *WcBot) QrCodeInTerminal() {
156 | wc.QrCodeTerminal = true
157 | }
158 |
159 | func initHttpServer() {
160 | g := gin.New()
161 | gin.SetMode(config.Config.ServerConf.Mode)
162 |
163 | g.Use(gin.Recovery())
164 | g.NoRoute(func(c *gin.Context) {
165 | c.String(http.StatusNotFound, "The incorrect API route")
166 | })
167 |
168 | g.GET(config.Config.WxBot4gConf.HeartbeatURL, TextHandle)
169 | v1 := g.Group("/v1/msg")
170 | {
171 | v1.GET("/text", TextHandle)
172 | v1.POST("/image", ImageHandle)
173 | }
174 |
175 | go InitHeartbeatCron()
176 |
177 | logrus.Error(http.ListenAndServe(":"+strconv.Itoa(config.Config.ServerConf.Port), g).Error())
178 | }
179 |
180 | func (wc *WcBot) Run() {
181 | if err := wc.getUuid(); err != nil {
182 | logrus.Error(err.Error())
183 | return
184 | }
185 |
186 | if err := wc.genQrCode(path.Join(wc.tempPwd, "wxqr.png")); err != nil {
187 | logrus.Error(err.Error())
188 | return
189 | }
190 |
191 | if code := wc.wait4login(); code != SUCCESS {
192 | logrus.Error("web wechat login failed, failed code=", code)
193 | wc.status = "loginout"
194 | return
195 | }
196 |
197 | if ok := wc.login(); ok {
198 | logrus.Info("succeed: web wechat login")
199 | } else {
200 | logrus.Error("failed: web wechat login")
201 | wc.status = "loginout"
202 | return
203 | }
204 |
205 | if ok := wc.init(); ok {
206 | logrus.Info("succeed: web wechat init")
207 | } else {
208 | logrus.Info("failed: web wechat init")
209 | }
210 |
211 | if ok := wc.statusNotify(); ok {
212 | logrus.Info("succeed: web wechat status notify")
213 | } else {
214 | logrus.Info("failed: web wechat status notify")
215 | }
216 |
217 | if ok := wc.GetContact(false, ""); ok == "unknown" {
218 | logrus.Info(fmt.Sprintf("Get %d contacts", len(wc.contactList)))
219 | logrus.Info("succeed: start to process messages")
220 | }
221 |
222 | //监听 api 服务
223 | go initHttpServer()
224 |
225 | wc.procMsgLoop()
226 |
227 | wc.status = "loginout"
228 | }
229 |
230 | func (wc *WcBot) getUuid() error {
231 | urlStr := "https://login.weixin.qq.com/jslogin?"
232 | params := url.Values{
233 | "appid": []string{"wx782c26e4c19acffb"},
234 | "fun": []string{"new"},
235 | "lang": []string{"zh_CN"},
236 | "_": []string{strconv.Itoa(int(time.Now().Unix())*1000 + rand.Intn(1000))},
237 | }
238 | data, err := wc.httpClient.Get(urlStr, params)
239 | if err != nil {
240 | logrus.Error(err.Error())
241 | return err
242 | }
243 |
244 | regx := `window.QRLogin.code = (\d+); window.QRLogin.uuid = "(\S+?)"`
245 | pm := utils.RegexpMatchStr(regx, string(data))
246 | if pm != nil && pm[0] != nil && len(pm[0]) >= 3 {
247 | code := pm[0][1]
248 | wc.uuid = pm[0][2]
249 | if code == SUCCESS {
250 | return nil
251 | } else {
252 | return errors.New(fmt.Sprintf("error code is : %s", code))
253 | }
254 | }
255 | return errors.New("regexp code uuid error")
256 | }
257 |
258 | func (wc *WcBot) genQrCode(filePath string) error {
259 | //wc.show_image(filePath)
260 | if wc.QrCodeTerminal {
261 | urlStr := "https://login.weixin.qq.com/qrcode/" + wc.uuid
262 | data, err := wc.httpClient.Get(urlStr, nil)
263 | if err != nil {
264 | logrus.Error(err)
265 | return err
266 | }
267 | M, err := qrcodetl.Decode(bytes.NewReader(data))
268 | if err != nil {
269 | logrus.Error(err)
270 | return err
271 | }
272 | qrterminal.GenerateHalfBlock(M.Content, qrterminal.M, os.Stdout)
273 | } else {
274 | urlStr := "https://login.weixin.qq.com/l/" + wc.uuid
275 | if err := qrcode.WriteFile(urlStr, qrcode.High, 256, filePath); err != nil {
276 | logrus.Error(err)
277 | return err
278 | }
279 | }
280 |
281 | logrus.Info("please use WeChat to scan the QR code")
282 | return nil
283 | }
284 |
285 | func (wc *WcBot) wait4login() string {
286 | /**
287 | http comet:
288 | tip=1, 等待用户扫描二维码,
289 | 201: scaned
290 | 408: timeout
291 | tip=0, 等待用户确认登录,
292 | 200: confirmed
293 | */
294 | var (
295 | tip = 1
296 | tryLaterSecs = 1
297 | maxRetryTime = 10
298 | code = UNKONWN
299 | loginUrl = "https://login.weixin.qq.com/cgi-bin/mmwebwx-bin/login?tip=%d&uuid=%s&_=%s"
300 | )
301 | for retryTime := maxRetryTime; retryTime > 0; retryTime-- {
302 | urlStr := fmt.Sprintf(loginUrl, tip, wc.uuid, strconv.Itoa(int(time.Now().Unix())))
303 |
304 | code, data, err := wc.doRequest(urlStr)
305 |
306 | if err != nil {
307 | logrus.Error(err.Error())
308 | return ERRSYSTEM
309 | }
310 |
311 | switch code {
312 | case SCANED:
313 | logrus.Info("please confirm to login")
314 | tip = 0
315 | case TIMEOUT:
316 | logrus.Warnf(" WeChat login timeout. retry in %d secs later", tryLaterSecs)
317 | tip = 1
318 | retryTime--
319 | time.Sleep(time.Duration(tryLaterSecs))
320 | case SUCCESS:
321 | regx := `window.redirect_uri="(\S+?)";`
322 | param := utils.RegexpMatchStr(regx, string(data))
323 | if len(param) < 1 || len(param[0]) < 2 {
324 | err = errors.New("param less 1 param or param[0] less 2")
325 | return ERRSYSTEM
326 | }
327 | wc.redirectUri = param[0][1] + `&fun=new&version=v2`
328 | wc.baseUri = wc.redirectUri[:strings.LastIndex(wc.redirectUri, "/")]
329 | tempHost := wc.baseUri[8:]
330 | wc.baseHost = tempHost[:strings.Index(tempHost, "/")]
331 | return code
332 | default:
333 | logrus.Warnf("WeChat login exception return_code=%s. retry in %d secs later", code, tryLaterSecs)
334 | tip = 1
335 | retryTime--
336 | time.Sleep(time.Duration(tryLaterSecs))
337 | }
338 | }
339 | return code
340 | }
341 |
342 | func (wc *WcBot) login() bool {
343 | if len(wc.redirectUri) < 4 {
344 | logrus.Error("Login failed due to network problem, please try again")
345 | return false
346 | }
347 |
348 | data, err := wc.httpClient.Get(wc.redirectUri, nil)
349 | if err != nil {
350 | logrus.Error(err.Error())
351 | return false
352 | }
353 |
354 | doc := etree.NewDocument()
355 | if err := doc.ReadFromString(string(data)); err != nil {
356 | panic(err)
357 | }
358 |
359 | root := doc.SelectElement("error")
360 | if root == nil {
361 | return false
362 | }
363 |
364 | wc.sKey = root.SelectElement("skey").Text()
365 | wc.sid = root.SelectElement("wxsid").Text()
366 | wc.uin = root.SelectElement("wxuin").Text()
367 | wc.passTicket = root.SelectElement("pass_ticket").Text()
368 |
369 | if wc.sKey == "" || wc.sid == "" || wc.uin == "" || wc.passTicket == "" {
370 | return false
371 | }
372 |
373 | wc.baseRequest["Uin"] = wc.uin
374 | wc.baseRequest["Sid"] = wc.sid
375 | wc.baseRequest["Skey"] = wc.sKey
376 | wc.baseRequest["DeviceID"] = wc.deviceId
377 |
378 | wc.Cookies = wc.httpClient.GetCookie()
379 |
380 | return true
381 |
382 | }
383 |
384 | func (wc *WcBot) init() bool {
385 | var (
386 | wxMsgs = models.RecvMsgs{}
387 | )
388 |
389 | urlStr := wc.baseUri + fmt.Sprintf("/webwxinit?r=%d&lang=en_US&pass_ticket=%s", int(time.Now().Unix()), wc.passTicket)
390 |
391 | body := struct {
392 | BaseRequest interface{} `json:"BaseRequest"`
393 | }{
394 | BaseRequest: wc.baseRequest,
395 | }
396 |
397 | data, err := wc.httpClient.Post(urlStr, body)
398 | if err != nil {
399 | logrus.Error(err.Error())
400 | return false
401 | }
402 |
403 | mData := make(map[string]interface{})
404 | if err := json.Unmarshal(data, &mData); err != nil {
405 | logrus.Error(err.Error())
406 | return false
407 | }
408 |
409 | err = json.Unmarshal(data, &wxMsgs)
410 | if err != nil {
411 | logrus.Error(err.Error())
412 | return false
413 | }
414 |
415 | for _, item := range mData["ContactList"].([]interface{}) {
416 | if mItem, ok := item.(map[string]interface{}); ok {
417 | if mItem["UserName"].(string)[0:2] == "@@" {
418 | wc.groupIdName[mItem["UserName"].(string)] = mItem["NickName"].(string)
419 | }
420 | }
421 | }
422 |
423 | wc.syncKey = wxMsgs.SyncKeys
424 | wc.syncKeyStr = wxMsgs.SyncKeys.ToString()
425 |
426 | wc.myAccount = mData["User"].(map[string]interface{})
427 | wc.chatSet = mData["ChatSet"].(string)
428 |
429 | mmData := struct {
430 | Ret int `json:"Ret"`
431 | }{}
432 | if err := json.Unmarshal(data, &mmData); err != nil {
433 | logrus.Error(err.Error())
434 | return false
435 | }
436 |
437 | ret := mmData.Ret == 0
438 | return ret
439 | }
440 |
441 | func (wc *WcBot) statusNotify() bool {
442 | urlStr := wc.baseUri + fmt.Sprintf("/webwxstatusnotify?lang=zh_CN&pass_ticket=%s", wc.passTicket)
443 |
444 | wc.baseRequest["Uin"], _ = strconv.Atoi(wc.baseRequest["Uin"].(string))
445 |
446 | body := struct {
447 | BaseRequest interface{} `json:"BaseRequest"`
448 | Code int `json:"Code"`
449 | FromUserName string `json:"FromUserName"`
450 | ToUserName string `json:"ToUserName"`
451 | ClientMsgId int `json:"ClientMsgId"`
452 | }{
453 | BaseRequest: wc.baseRequest,
454 | Code: 3,
455 | FromUserName: wc.myAccount["UserName"].(string),
456 | ToUserName: wc.myAccount["UserName"].(string),
457 | ClientMsgId: int(time.Now().Unix()),
458 | }
459 |
460 | data, err := wc.httpClient.Post(urlStr, body)
461 | if err != nil {
462 | logrus.Error(err.Error())
463 | return false
464 | }
465 |
466 | mData := make(map[string]interface{})
467 | if err := json.Unmarshal(data, &mData); err != nil {
468 | logrus.Error(err.Error())
469 | return false
470 | }
471 |
472 | mmData := struct {
473 | Ret int `json:"Ret"`
474 | }{}
475 | if err := json.Unmarshal(data, &mmData); err != nil {
476 | logrus.Error(err.Error())
477 | return false
478 | }
479 |
480 | ret := mmData.Ret == 0
481 | return ret
482 | }
483 |
484 | func (wc *WcBot) GetContact(isUnknow bool, uId string) string {
485 | contactMap := make(map[string]models.User, 0)
486 | urlStr := wc.baseUri + fmt.Sprintf("/webwxgetcontact?lang=zh_CN&seq=%s&pass_ticket=%s&skey=%s&r=%s",
487 | "0", wc.passTicket, wc.sKey, strconv.Itoa(int(time.Now().Unix())))
488 |
489 | //如果通讯录联系人过多,这里会直接获取失败
490 | data, err := wc.httpClient.Post(urlStr, nil)
491 | if err != nil {
492 | logrus.Error(err.Error())
493 | return ""
494 | }
495 |
496 | var contactList models.ContactList
497 | err = json.Unmarshal(data, &contactList)
498 | if err != nil {
499 | logrus.Error(err)
500 | return ""
501 | }
502 |
503 | for i := 0; i < contactList.MemberCount; i++ {
504 | contactMap[contactList.MemberList[i].UserName] = contactList.MemberList[i]
505 | }
506 |
507 | for contactList.Seq != 0 {
508 | logrus.Info(fmt.Sprintf("Geting contacts. Get %d contacts for now", contactList.MemberCount))
509 |
510 | urlStr := wc.baseUri + fmt.Sprintf("/webwxgetcontact?seq=%s&pass_ticket=%s&skey=%s&r=%d",
511 | strconv.Itoa(contactList.Seq), wc.passTicket, wc.sKey, int(time.Now().Unix()))
512 | data, err := wc.httpClient.Post(urlStr, nil)
513 | if err != nil {
514 | logrus.Error(err.Error())
515 | return ""
516 | }
517 |
518 | var contactList models.ContactList
519 | err = json.Unmarshal(data, &contactList)
520 | if err != nil {
521 | logrus.Error(err)
522 | return ""
523 | }
524 |
525 | for i := 0; i < contactList.MemberCount; i++ {
526 | contactMap[contactList.MemberList[i].UserName] = contactList.MemberList[i]
527 | }
528 | }
529 |
530 | wc.memberList = append(wc.memberList, contactList.MemberList...)
531 |
532 | specialUsers := map[string]bool{
533 | "newsapp": true, "fmessage": true, "filehelper": true, "weibo": true, "qqmail": true,
534 | "qmessage": true, "qqsync": true, "floatbottle": true,
535 | "lbsapp": true, "medianote": true, "qqfriend": true, "readerapp": true,
536 | "blogapp": true, "facebookapp:true": true, "masssendapp": true, "meishiapp": true,
537 | "feedsapp": true, "voip:true": true, "blogappweixin": true, "weixin": true, "brandsessionholder": true,
538 | "weixinreminder": true, "officialaccounts": true, "wxid_novlwrv3lqwv11": true,
539 | "gh_22b87fa7cb3c": true, "wxitil": true, "userexperience_alarm": true, "notification_messages": true,
540 | }
541 |
542 | if len(wc.memberList) <= 0 {
543 | return ""
544 | }
545 |
546 | for _, user := range wc.memberList {
547 | if user.VerifyFlag&8 != 0 {
548 | // 公众号
549 | wc.publicList = append(wc.publicList, user)
550 | wc.accountInfo["normal_member"][user.UserName] = models.AccountInfo{Type: "public", User: user}
551 | } else if _, ok := specialUsers[user.UserName]; ok {
552 | // 特殊账户
553 | wc.accountInfo["normal_member"][user.UserName] = models.AccountInfo{Type: "special", User: user}
554 | } else if strings.Contains(user.UserName, "@@") {
555 | // 群聊
556 | wc.groupList = append(wc.groupList, user)
557 | wc.accountInfo["normal_member"][user.UserName] = models.AccountInfo{Type: "group", User: user}
558 | } else if user.UserName == wc.myAccount["UserName"].(string) {
559 | // 自己
560 | wc.accountInfo["normal_member"][user.UserName] = models.AccountInfo{Type: "self", User: user}
561 | } else {
562 | wc.contactList = append(wc.contactList, user)
563 | wc.accountInfo["normal_member"][user.UserName] = models.AccountInfo{Type: "contact", User: user}
564 | }
565 | }
566 |
567 | if err := wc.batchGetGroupMembers(); err != nil {
568 | logrus.Error(err)
569 | return ""
570 | }
571 |
572 | if wc.Debug {
573 | if err = utils.WriteFile(wc.tempPwd+"groupList.json", wc.groupList); err != nil {
574 | logrus.Error(err)
575 | return ""
576 | }
577 |
578 | if err = utils.WriteFile(wc.tempPwd+"accountInfo.json", wc.accountInfo); err != nil {
579 | logrus.Error(err)
580 | return ""
581 | }
582 | }
583 |
584 | for _, groups := range wc.groupMembers {
585 | for _, group := range groups {
586 | if _, ok := wc.accountInfo["normal_member"][group.UserName]; !ok {
587 | wc.accountInfo["group_member"][group.UserName] = models.AccountInfo{Type: "contact", User: group, Group: group}
588 |
589 | //暂时不在此获取昵称,请调用GetGroupUserName
590 | //if isUnknow && uId != "" {
591 | // if uId == group.UserName {
592 | // return group.UserName
593 | // } else if uId == group.DisplayName {
594 | // return group.DisplayName
595 | // } else if uId == group.NickName {
596 | // return group.NickName
597 | // } else {
598 | // return "unknown"
599 | // }
600 | //}
601 | }
602 | }
603 | }
604 |
605 | return "unknown"
606 | }
607 |
608 | func (wc *WcBot) procMsgLoop() {
609 | wc.testSyncCheck()
610 | wc.status = "loginsuccess" //WxbotManage使用
611 | for {
612 | retCode, selector, err := wc.syncCheck()
613 | logrus.Debug(retCode, " ", selector)
614 | if err != nil {
615 | logrus.Error(err)
616 | }
617 | switch retCode {
618 | case "1100":
619 | //从微信客户端上登出
620 | case "1101":
621 | //从其它设备上登了网页微信
622 | case "0":
623 | //msg="微信正常"
624 | switch selector {
625 | case "2":
626 | //有新消息
627 | if r, err := wc.sync(); err == nil {
628 | wc.handleMsg(r)
629 | } else {
630 | logrus.Error(err)
631 | }
632 | case "3":
633 | //未知
634 | if r, err := wc.sync(); err == nil {
635 | wc.handleMsg(r)
636 | }
637 | case "4":
638 | //通讯录更新
639 | if r, err := wc.sync(); err == nil {
640 | wc.handleMsg(r)
641 | }
642 | case "6":
643 | //可能是红包
644 | if r, err := wc.sync(); err == nil {
645 | wc.handleMsg(r)
646 | }
647 | case "7":
648 | //在手机上操作了微信
649 | if r, err := wc.sync(); err == nil {
650 | wc.handleMsg(r)
651 | }
652 | case "0":
653 | //无事件
654 | }
655 | default:
656 | logrus.Errorf("sync_check, retcode:%s selector:%s", retCode, selector)
657 | }
658 | wc.Schedule()
659 |
660 | time.Sleep(time.Second)
661 | }
662 | }
663 |
664 | func (wc *WcBot) Schedule() {
665 | /**
666 | 做任务型事情的函数,如果需要,可以在子类中覆盖此函数
667 | 此函数在处理消息的间隙被调用,请不要长时间阻塞此函数
668 | */
669 | }
670 |
671 | func (wc *WcBot) doRequest(url string) (code string, data []byte, err error) {
672 | data, err = wc.httpClient.Get(url, nil)
673 | if err != nil {
674 | logrus.Error(err.Error())
675 | return
676 | }
677 | regx := `window.code=(\d+);`
678 | codes := utils.RegexpMatchStr(regx, string(data))
679 | if len(codes) < 1 || len(codes[0]) < 2 {
680 | err = errors.New("codes less 1 param or codes[0] less 2")
681 | return
682 | }
683 | code = codes[0][1]
684 | return
685 | }
686 |
687 | /**
688 | {
689 | "BaseResponse":{
690 | "Ret":0,
691 | "ErrMsg":""
692 | },
693 | "Count":10,
694 | "ContactList":[
695 | {
696 | "Uin":0,
697 | "UserName":"@@40bccd2526c469d875a325076c1afefc35b1f0a677aa6f266a019ff8d4cd1aae",
698 | "NickName":"吃货群",
699 | "HeadImgUrl":"/cgi-bin/mmwebwx-bin/webwxgetheadimg?seq=657825175&username=@@40bccd2526c469d875a325076c1afefc35b1f0a677aa6f266a019ff8d4cd1aae&skey=",
700 | "ContactFlag":3,
701 | "MemberCount":6,
702 | "MemberList":[
703 | {
704 | "Uin":0,
705 | "UserName":"@2c301cc8ad2d753b22cac512b13de1be",
706 | "NickName":"阿花 ",
707 | "AttrStatus":33784319,
708 | "PYInitial":"",
709 | "PYQuanPin":"",
710 | "RemarkPYInitial":"",
711 | "RemarkPYQuanPin":"",
712 | "MemberStatus":0,
713 | "DisplayName":"",
714 | "KeyWord":"blu"
715 | },
716 | {
717 | "Uin":0,
718 | "UserName":"@a42ee05b2f48f05ad8e5caff36c72972",
719 | "NickName":"子杰",
720 | "AttrStatus":242279,
721 | "PYInitial":"",
722 | "PYQuanPin":"",
723 | "RemarkPYInitial":"",
724 | "RemarkPYQuanPin":"",
725 | "MemberStatus":0,
726 | "DisplayName":"",
727 | "KeyWord":"jzz"
728 | },
729 | //...
730 | }
731 | //...
732 | ]
733 | }
734 | */
735 | func (wc *WcBot) batchGetGroupMembers() error {
736 | urlStr := wc.baseUri + fmt.Sprintf("/webwxbatchgetcontact?type=ex&r=%s&pass_ticket=%s",
737 | strconv.Itoa(int(time.Now().Unix())), wc.passTicket)
738 |
739 | body := struct {
740 | BaseRequest interface{} `json:"BaseRequest"`
741 | Count interface{} `json:"Count"`
742 | List []interface{} `json:"List"`
743 | }{
744 | BaseRequest: wc.baseRequest,
745 | Count: len(wc.groupList),
746 | }
747 |
748 | for _, group := range wc.groupList {
749 | body.List = append(body.List, struct {
750 | UserName string `json:"UserName"`
751 | EncryChatRoomId string `json:"EncryChatRoomId"`
752 | }{
753 | group.UserName,
754 | "",
755 | })
756 | }
757 |
758 | data, err := wc.httpClient.Post(urlStr, body)
759 | if err != nil {
760 | logrus.Error(err.Error())
761 | return err
762 | }
763 |
764 | var groupList models.GroupList
765 | err = json.Unmarshal(data, &groupList)
766 | if err != nil {
767 | logrus.Error(err)
768 | return err
769 | }
770 |
771 | groupMembers := make(map[string][]models.User)
772 | encryChatRoomId := make(map[string]string)
773 |
774 | if wc.Debug {
775 | if err = utils.WriteFile(wc.tempPwd+"batchGetGroupMembers.json", data); err != nil {
776 | logrus.Error(err)
777 | return err
778 | }
779 | }
780 |
781 | for _, group := range groupList.ContactList {
782 | gid := group.UserName
783 | for _, member := range group.MemberList {
784 | groupMembers[gid] = append(groupMembers[gid], member)
785 | encryChatRoomId[gid] = member.EncryChatRoomId
786 | }
787 | }
788 |
789 | wc.groupMembers = groupMembers
790 | wc.encryChatRoomIdList = encryChatRoomId
791 |
792 | if wc.Debug {
793 | if err = utils.WriteFile(wc.tempPwd+"groupMembers.json", wc.groupMembers); err != nil {
794 | logrus.Error(err)
795 | return err
796 | }
797 | }
798 |
799 | return nil
800 | }
801 |
802 | func (wc *WcBot) testSyncCheck() bool {
803 | //host1 := []string{"webpush.", "webpush2."}
804 | host1 := []string{"webpush."}
805 | host2 := []string{"wx.qq.com", wc.baseHost}
806 |
807 | for _, h1 := range host1 {
808 | for _, h2 := range host2 {
809 | wc.syncHost = h1 + h2
810 | retCode, _, err := wc.syncCheck()
811 | if err != nil {
812 | retCode = "-1"
813 | }
814 | if retCode == "0" {
815 | return true
816 | }
817 | }
818 | }
819 | return false
820 | }
821 |
822 | func (wc *WcBot) syncCheck() (string, string, error) {
823 | tt := time.Now().UnixNano() / 1000000
824 | params := url.Values{
825 | "r": []string{strconv.Itoa(int(tt))},
826 | "sid": []string{wc.sid},
827 | "uin": []string{wc.uin},
828 | "skey": []string{wc.sKey},
829 | "deviceid": []string{wc.deviceId},
830 | "synckey": []string{wc.syncKeyStr},
831 | "_": []string{strconv.Itoa(int(tt))},
832 | }
833 |
834 | urlStr := "https://" + wc.syncHost + "/cgi-bin/mmwebwx-bin/synccheck?"
835 |
836 | wc.httpClient.SetCookie(wc.Cookies)
837 |
838 | data, err := wc.httpClient.Get(urlStr, params)
839 | if err != nil {
840 | logrus.Error(err.Error())
841 | return "-1", "-1", err
842 | }
843 |
844 | regx := `window.synccheck=\{retcode:"(\d+)",selector:"(\d+)"\}`
845 | pm := utils.RegexpMatchStr(regx, string(data))
846 | if pm != nil && pm[0] != nil && len(pm[0]) >= 3 {
847 | retCode := pm[0][1]
848 | selector := pm[0][2]
849 |
850 | return retCode, selector, nil
851 | }
852 | return "-1", "-1", errors.New("regexp error")
853 | }
854 |
855 | func (wc *WcBot) sync() (models.RecvMsgs, error) {
856 | var (
857 | wxMsges = models.RecvMsgs{}
858 | )
859 | urlStr := wc.baseUri + fmt.Sprintf("/webwxsync?sid=%s&skey=%s&lang=en_US&pass_ticket=%s",
860 | wc.sid, wc.sKey, wc.passTicket)
861 |
862 | body := struct {
863 | BaseRequest interface{} `json:"BaseRequest"`
864 | SyncKey interface{} `json:"SyncKey"`
865 | RR int `json:"rr"`
866 | }{
867 | BaseRequest: wc.baseRequest,
868 | SyncKey: wc.syncKey,
869 | RR: int(time.Now().UnixNano()),
870 | }
871 |
872 | wc.httpClient.SetCookie(wc.Cookies)
873 |
874 | data, err := wc.httpClient.Post(urlStr, body)
875 | if err != nil {
876 | logrus.Error(err.Error())
877 | return wxMsges, err
878 | }
879 |
880 | err = json.Unmarshal(data, &wxMsges)
881 | if err != nil {
882 | return wxMsges, err
883 | }
884 |
885 | wc.syncKey = wxMsges.SyncKeys
886 | wc.syncKeyStr = wxMsges.SyncKeys.ToString()
887 |
888 | if wxMsges.ModContactCount == 1 && len(wxMsges.ModContactList) > 0 {
889 | groupName := wxMsges.ModContactList[0].(map[string]interface{})["NickName"].(string)
890 | if groupName != "" {
891 | groupId := wxMsges.ModContactList[0].(map[string]interface{})["UserName"].(string)
892 | wc.groupIdName[groupId] = groupName
893 | }
894 | }
895 |
896 | return wxMsges, nil
897 | }
898 |
899 | func (wc *WcBot) GetUserId(name string) string {
900 | if name == "" {
901 | return ""
902 | }
903 |
904 | for _, contact := range wc.contactList {
905 | if contact.RemarkName != "" && name == contact.RemarkName {
906 | return contact.UserName
907 | }
908 |
909 | if contact.DisplayName != "" && name == contact.DisplayName {
910 | return contact.UserName
911 | }
912 |
913 | if contact.NickName != "" && name == contact.NickName {
914 | return contact.UserName
915 | }
916 | }
917 |
918 | for _, group := range wc.groupList {
919 | if group.RemarkName != "" && name == group.RemarkName {
920 | return group.UserName
921 | }
922 |
923 | if group.DisplayName != "" && name == group.DisplayName {
924 | return group.UserName
925 | }
926 |
927 | if group.NickName != "" && name == group.NickName {
928 | return group.UserName
929 | }
930 | }
931 |
932 | for gid, gName := range wc.groupIdName {
933 | if gName == name {
934 | return gid
935 | }
936 | }
937 |
938 | return ""
939 | }
940 |
941 | /**
942 | content_type_id:
943 | 0 -> Text
944 | 1 -> Location
945 | 3 -> Image
946 | 4 -> Voice
947 | 5 -> Recommend
948 | 6 -> Animation
949 | 7 -> Share
950 | 8 -> Video
951 | 9 -> VideoCall
952 | 10 -> Redraw
953 | 11 -> Empty
954 | 99 -> Unknown
955 | msg_type_id: 消息类型id
956 | msg: 消息结构体
957 | return: 解析的消息
958 | */
959 | func (wc *WcBot) extractMsgContent(msgTypeId int, msg models.RecvMsg) models.Content {
960 | mType := msg.MsgType
961 | content := html.UnescapeString(msg.Content)
962 | msgId := msg.MsgId
963 |
964 | var msgContent models.Content
965 | if msgTypeId == 0 {
966 | msgContent.Type = 11
967 | msgContent.Data = ""
968 | return msgContent
969 | } else if msgTypeId == 2 {
970 | //File Helper
971 | msgContent.Type = 0
972 | msgContent.Data = strings.Replace(content, `
`, "\n", -1)
973 | return msgContent
974 | } else if msgTypeId == 3 {
975 | //群聊
976 | sp := strings.Index(content, `
`)
977 | uId := content[:sp]
978 | content = content[sp:]
979 | content = strings.Replace(content, `
`, "", -1)
980 | uId = uId[:(len(uId) - 1)]
981 | name := wc.getContactPreferName(wc.getContactName(uId))
982 | if name == "" {
983 | name = wc.getGroupMemberPreferName(wc.getGroupMemberName(msg.FromUserName, uId))
984 | }
985 | if name == "" {
986 | name = "unknown"
987 | }
988 | msgContent.User = models.ContentUser{Uid: uId, Name: name}
989 | } else {
990 | // Self, Contact, Special, Public, Unknown
991 | //pass
992 | }
993 |
994 | msgPrefix := ""
995 | if msgContent.User.Name != "" {
996 | msgPrefix = msgContent.User.Name
997 | }
998 |
999 | if mType == 1 {
1000 | if strings.Contains(content, `http: //weixin.qq.com/cgi-bin/redirectforward?args=`) {
1001 | data, err := wc.httpClient.Get(content, nil)
1002 | if err != nil {
1003 | logrus.Error(err)
1004 | }
1005 | pos := wc.searchContent("title", string(data), "xml")
1006 | msgContent.Type = 1
1007 | msgContent.Data = pos
1008 | msgContent.Detail = models.Detail{Type: "str", Value: string(data)}
1009 | } else {
1010 | msgContent.Type = 0
1011 | if msgTypeId == 3 || (msgTypeId == 1 && msg.ToUserName[:2] == "@@") {
1012 | msgContent.Data, msgContent.Desc, msgContent.Other = wc.procAtInfo(content)
1013 | } else {
1014 | msgContent.Data = content
1015 | }
1016 | }
1017 | } else if mType == 3 {
1018 | //发送图片
1019 | msgContent.Type = 3
1020 | msgContent.Data = wc.getMsgImgUrl(msgId)
1021 | data, err := wc.httpClient.Get(msgContent.Data, nil)
1022 | if err != nil {
1023 | logrus.Error(err)
1024 | }
1025 |
1026 | maxEnLen := hex.EncodedLen(len(data)) // 最大编码长度
1027 | dst1 := make([]byte, maxEnLen)
1028 | hex.Encode(dst1, data)
1029 |
1030 | msgContent.Img = make([]byte, 0)
1031 | msgContent.Img = append(msgContent.Img, dst1...)
1032 |
1033 | //TODO 发送照片到阿里云oss
1034 | if wc.send2oss {
1035 | wc.ossUrl = wc.sendMsgImgAliyun(msgId, wc.uin)
1036 | }
1037 | } else if mType == 34 {
1038 | //发送语音
1039 | msgContent.Type = 4
1040 | msgContent.Data = wc.getVoiceUrl(msgId)
1041 |
1042 | data, err := wc.httpClient.Get(msgContent.Data, nil)
1043 | if err != nil {
1044 | logrus.Error(err)
1045 | }
1046 |
1047 | maxEnLen := hex.EncodedLen(len(data)) // 最大编码长度
1048 | dst1 := make([]byte, maxEnLen)
1049 | hex.Encode(dst1, data)
1050 |
1051 | msgContent.Img = make([]byte, 0)
1052 | msgContent.Voice = append(msgContent.Img, dst1...)
1053 | } else if mType == 37 {
1054 | // TODO 添加好友
1055 | msgContent.Type = 37
1056 | msgContent.Other = msg.RecommendInfo
1057 | } else if mType == 42 {
1058 | msgContent.Type = 5
1059 | info := msg.RecommendInfo
1060 |
1061 | allSex := map[int]interface{}{0: "unknown", 1: "male", 2: "female"}
1062 |
1063 | msgContent.Other = map[string]interface{}{
1064 | "nickname": info.(map[string]interface{})["NickName"],
1065 | "alias": info.(map[string]interface{})["Alias"],
1066 | "province": info.(map[string]interface{})["Province"],
1067 | "city": info.(map[string]interface{})["City"],
1068 | "gender": allSex[info.(map[string]interface{})["Sex"].(int)]}
1069 | } else if mType == 47 {
1070 | msgContent.Type = 6
1071 | msgContent.Data = wc.searchContent("cdnurl", content, "attr")
1072 | if wc.Debug {
1073 | logrus.Infof("%s[Animation] %s", msgPrefix, msgContent.Data)
1074 | }
1075 | } else if mType == 49 {
1076 | var appMsgType string
1077 | msgContent.Type = 7
1078 | if msg.AppMsgType == 3 {
1079 | appMsgType = "music"
1080 | } else if msg.AppMsgType == 5 {
1081 | appMsgType = "link"
1082 | } else if msg.AppMsgType == 7 {
1083 | appMsgType = "weibo"
1084 | } else {
1085 | appMsgType = "unknown"
1086 | }
1087 | msgContent.Other = map[string]interface{}{
1088 | "type": appMsgType,
1089 | "title": msg.FileName,
1090 | "desc": wc.searchContent("des", content, "xml"),
1091 | "url": msg.Url,
1092 | "from": wc.searchContent("appname", content, "xml"),
1093 | "content": msg.Content, //有的公众号会发一次性3 4条链接一个大图,如果只url那只能获取第一条,content里面有所有的链接
1094 | }
1095 | } else if mType == 62 {
1096 | msgContent.Type = 8
1097 | msgContent.Data = content
1098 | if wc.Debug {
1099 | logrus.Infof("%s[Video] Please check on mobiles", msgPrefix)
1100 | }
1101 | } else if mType == 53 {
1102 | msgContent.Type = 9
1103 | msgContent.Data = content
1104 | if wc.Debug {
1105 | logrus.Infof("%s[Video Call]", msgPrefix)
1106 | }
1107 | } else if mType == 10002 {
1108 | msgContent.Type = 10
1109 | msgContent.Data = content
1110 | if wc.Debug {
1111 | logrus.Infof("%s[Redraw]", msgPrefix)
1112 | }
1113 | } else if mType == 10000 {
1114 | msgContent.Type = 12
1115 | msgContent.Data = msg.Content
1116 | if wc.Debug {
1117 | logrus.Info("[Unknown]")
1118 | }
1119 | } else if mType == 43 {
1120 | msgContent.Type = 13
1121 | msgContent.Data = wc.getVideoUrl(msgId)
1122 | if wc.Debug {
1123 | logrus.Infof("%s[video] %s", msgPrefix, msgContent.Data)
1124 | }
1125 | } else {
1126 | msgContent.Type = 99
1127 | msgContent.Data = content
1128 | if wc.Debug {
1129 | logrus.Warnf("[Unknown] msg content type:%d", 99)
1130 | }
1131 | }
1132 |
1133 | return msgContent
1134 | }
1135 |
1136 | func (wc *WcBot) AddHandler(handler Handler) {
1137 | wc.handler = handler
1138 | }
1139 |
1140 | /**
1141 | 处理原始微信消息的内部函数
1142 | msg_type_id:
1143 | 0 -> Init //初始化消息,内部数据
1144 | 1 -> Self //自己发送的消息
1145 | 2 -> FileHelper //文件消息
1146 | 3 -> Group //群消息
1147 | 4 -> Contact //联系人消息
1148 | 5 -> Public //公众号消息
1149 | 6 -> Special //特殊账号消息
1150 | 51 -> 获取wxid //获取wxid消息
1151 | 99 -> Unknown // 未知账号消息
1152 | */
1153 | func (wc *WcBot) handleMsg(data models.RecvMsgs) {
1154 | //wc.handleMsgAll(data)
1155 | for _, msg := range data.MsgList {
1156 | msgUser := models.MsgUser{
1157 | ID: msg.FromUserName,
1158 | Name: UNKONWN,
1159 | }
1160 |
1161 | msgTypeId := 0
1162 |
1163 | if msg.MsgType == 51 && msg.StatusNotifyCode == 4 {
1164 | //系统消息
1165 | msgTypeId = 0
1166 | msgUser.Name = "system"
1167 | //获取所有联系人的username 和 wxid,但是会收到3次这个消息,只取第一次
1168 | if wc.isBigContact && len(wc.fullUserNameList) == 0 {
1169 | wc.fullUserNameList = strings.Split(msg.StatusNotifyUserName, ",")
1170 | //wc.wxid_list = re.search(r"username>(.*?)</username", msg.Content).group(1).split(",")
1171 | }
1172 | } else if msg.MsgType == 37 {
1173 | //好友消息
1174 | msgTypeId = 37
1175 | msgUser.Name = wc.getContactPreferName(wc.getContactName(msgUser.ID))
1176 | } else if msg.FromUserName == msg.ToUserName {
1177 | //发给自己
1178 | } else if msg.ToUserName == "filehelper" {
1179 | //文件助手
1180 | msgTypeId = 2
1181 | msgUser.Name = "file_helper"
1182 | } else if msg.FromUserName[:2] == "@@" {
1183 | //群消息
1184 | msgTypeId = 3
1185 | msgUser.Name = wc.getContactPreferName(wc.getContactName(msgUser.ID))
1186 | } else if wc.isContact(msg.FromUserName) {
1187 | //Contact
1188 | msgTypeId = 4
1189 | msgUser.Name = wc.getContactPreferName(wc.getContactName(msgUser.ID))
1190 | } else if wc.isPublic(msg.FromUserName) {
1191 | //Public
1192 | msgTypeId = 5
1193 | msgUser.Name = wc.getContactPreferName(wc.getContactName(msgUser.ID))
1194 | } else if wc.isSpecial(msg.FromUserName) {
1195 | //Special
1196 | msgTypeId = 6
1197 | msgUser.Name = wc.getContactPreferName(wc.getContactName(msgUser.ID))
1198 | } else {
1199 | msgTypeId = 99
1200 | msgUser.Name = UNKONWN
1201 | }
1202 |
1203 | content := wc.extractMsgContent(msgTypeId, msg)
1204 | realMsg := models.RealRecvMsg{
1205 | MsgTypeId: msgTypeId,
1206 | MsgId: msg.MsgId,
1207 | FromUserName: msg.FromUserName,
1208 | ToUserName: msg.ToUserName,
1209 | MsgType: msg.MsgType,
1210 | Content: content,
1211 | CreateTime: msg.CreateTime,
1212 | SendMsgUSer: msgUser,
1213 | }
1214 | go wc.handler.HandleMessage(realMsg)
1215 | }
1216 | }
1217 |
--------------------------------------------------------------------------------
/wxqr/wxqr.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/liangjfblue/wxBot4g/57fb365c5ac705325efa6f9a2edce082889c7a99/wxqr/wxqr.png
--------------------------------------------------------------------------------