├── .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 --------------------------------------------------------------------------------