├── avatar └── .keep ├── comm ├── web │ ├── http_test.go │ └── http.go ├── ticker │ ├── ticker.go │ ├── love.go │ ├── encourage.go │ ├── master_ticker.go │ ├── hcc_fans_ticker.go │ ├── ticker_test.go │ └── schedule-notice.go ├── global │ └── global.go ├── adcode │ ├── adcode_test.go │ └── adcode.go ├── image │ ├── image_test.go │ └── image.go ├── tian │ ├── const.go │ ├── model.go │ ├── tian_test.go │ ├── tian2.go │ └── get_message.go ├── weather │ ├── model.go │ ├── weather_test.go │ └── weather.go ├── redis │ ├── redis_test.go │ └── redis.go ├── qweather │ ├── qweather_test.go │ └── qweather.go ├── funcs │ ├── funcs.go │ ├── funcs_test.go │ └── date.go ├── msg │ ├── msg_test.go │ └── msg.go └── conf │ └── conf.go ├── reword.png ├── demonstration.jpg ├── test.go ├── .gitignore ├── go.mod ├── config └── dev.yaml ├── README.md ├── main.go └── go.sum /avatar/.keep: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /comm/web/http_test.go: -------------------------------------------------------------------------------- 1 | package web 2 | -------------------------------------------------------------------------------- /reword.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaobinqt/go-wxbot/HEAD/reword.png -------------------------------------------------------------------------------- /demonstration.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaobinqt/go-wxbot/HEAD/demonstration.jpg -------------------------------------------------------------------------------- /comm/ticker/ticker.go: -------------------------------------------------------------------------------- 1 | package ticker 2 | 3 | func Ticker() { 4 | go LoveTicker() 5 | //go FansTicker() 6 | go BubeiGroupTicker() 7 | go EncourageTicker() 8 | go MasterTicker() 9 | go ScheduleNoticeTicker() 10 | go KeepLive() 11 | } 12 | -------------------------------------------------------------------------------- /test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "go-wxbot/openwechat/comm/global" 7 | ) 8 | 9 | func Test() { 10 | groups, _ := global.WxSelf.Groups(true) 11 | for _, each := range groups { 12 | fmt.Println(each.NickName, each.UserName) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | .vscode 3 | .DS_Store 4 | .idea 5 | 6 | 7 | openwechat/config/prod.yaml 8 | openwechat/avatar/test.png 9 | openwechat/comm/funcs/cc.txt 10 | openwechat/avatar/*.png 11 | openwechat/avatar/*.jpeg 12 | openwechat/avatar/*.gif 13 | wxbot.exe 14 | prod.yaml 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /comm/global/global.go: -------------------------------------------------------------------------------- 1 | package global 2 | 3 | import ( 4 | "github.com/eatmoreapple/openwechat" 5 | "go-wxbot/openwechat/comm/conf" 6 | ) 7 | 8 | var ( 9 | Conf *conf.Conf 10 | WxSelf *openwechat.Self 11 | WxFriends openwechat.Friends // 可能有缓存 12 | WxGroups openwechat.Groups // 可能有缓存 13 | ) 14 | -------------------------------------------------------------------------------- /comm/adcode/adcode_test.go: -------------------------------------------------------------------------------- 1 | package adcode 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | func TestLoadAdcodeInfo(t *testing.T) { 9 | loadAdcodeInfo() 10 | } 11 | 12 | func TestGetAdcodeByCityName(t *testing.T) { 13 | fmt.Println("山亭区 adcode = ", GetAdcodeByCityName("山亭区")) 14 | fmt.Println("泾县 adcode = ", GetAdcodeByCityName("泾县")) 15 | fmt.Println("鱼台 adcode = ", GetAdcodeByCityName("鱼台")) 16 | } 17 | -------------------------------------------------------------------------------- /comm/image/image_test.go: -------------------------------------------------------------------------------- 1 | package image 2 | 3 | import "testing" 4 | 5 | func TestGetImage(t *testing.T) { 6 | imgURL, err := GetImage() 7 | if err != nil { 8 | t.Log(err) 9 | return 10 | } 11 | 12 | t.Log(imgURL) 13 | } 14 | 15 | func TestSendEncourageImg(t *testing.T) { 16 | path, err := SaveEncourageImg("https://img.qianxiaoduan.com/image/wallpaper/5126.jpg") 17 | if err != nil { 18 | t.Log(err) 19 | return 20 | } 21 | t.Log(path) 22 | } 23 | -------------------------------------------------------------------------------- /comm/tian/const.go: -------------------------------------------------------------------------------- 1 | package tian 2 | 3 | import "errors" 4 | 5 | const ( 6 | C_dujitang = "dujitang" // 毒鸡汤 7 | C_mingyan = "mingyan" // 名人名言 8 | C_godreply = "godreply" // 神回复 9 | C_wanan = "wanan" // 晚安心语 10 | C_saylove = "saylove" // 土味情话 11 | C_caipu = "caipu" // 菜谱 12 | C_englishSentence = "ensentence" // 英语一句话 13 | C_lizhiguyan = "lzmy" // 励志古言 14 | ) 15 | 16 | var ( 17 | ErrNotfoundCaiPu = errors.New("not found cookbook") 18 | ) 19 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module go-wxbot/openwechat 2 | 3 | go 1.16 4 | 5 | require ( 6 | github.com/Lofanmi/chinese-calendar-golang v0.0.0-20211214151323-ef5cb443e55e 7 | github.com/eatmoreapple/openwechat v1.4.5-0.20230911013309-68d6f5444502 8 | github.com/go-redis/redis/v8 v8.11.5 9 | github.com/json-iterator/go v1.1.12 10 | github.com/pkg/errors v0.9.1 11 | github.com/satori/go.uuid v1.2.0 12 | github.com/sirupsen/logrus v1.8.1 13 | github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e 14 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b 15 | ) 16 | 17 | //replace github.com/eatmoreapple/openwechat v1.1.11 => D:\go\src\openwechat 18 | -------------------------------------------------------------------------------- /config/dev.yaml: -------------------------------------------------------------------------------- 1 | app: 2 | env: dev 3 | 4 | keys: 5 | bot_name: 卫小兵 # 机器人名称 6 | christmas_hat_url: http://canvas.lihuanting.com 7 | weather_key: xxx # 高德天气 api key 8 | tianapi_key: xxxx # 天行 api key 9 | tianapi_key1: xxx # 天行 api key 10 | qweather_key: xxx # 和风天气 api key 11 | honey_love: xxxxx 12 | lover_ch_name: xxx 13 | master_account: xxx 14 | houchangcun_fans: 后厂村吴彦祖粉丝团 # 粉丝群 15 | banzhuan_group: xxx # 老翔头搬砖群 16 | bubei_group: 不背打卡小分队,14 # 不背单词打卡群 17 | bubei_start_date: 2100-07-15 # 不背单词打卡开始日期,一个周期是 14 天 18 | wu_zhuang_shi_members: xxx,xxx 19 | remind_msg: xxxx 20 | 21 | redis: 22 | ip: 192.xxx.14.xx 23 | port: 6379 24 | passwd: xxxxx 25 | -------------------------------------------------------------------------------- /comm/weather/model.go: -------------------------------------------------------------------------------- 1 | package weather 2 | 3 | type WeatherInfo struct { 4 | Status string `json:"status"` 5 | Count string `json:"count"` 6 | Info string `json:"info"` 7 | Infocode string `json:"infocode"` 8 | Lives []Live `json:"lives"` 9 | } 10 | 11 | type Live struct { 12 | Province string `json:"province"` 13 | City string `json:"city"` 14 | Adcode string `json:"adcode"` 15 | Weather string `json:"weather"` 16 | Temperature string `json:"temperature"` 17 | Winddirection string `json:"winddirection"` 18 | Windpower string `json:"windpower"` 19 | Humidity string `json:"humidity"` 20 | Reporttime string `json:"reporttime"` 21 | } 22 | -------------------------------------------------------------------------------- /comm/weather/weather_test.go: -------------------------------------------------------------------------------- 1 | package weather 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "testing" 7 | 8 | "github.com/json-iterator/go/extra" 9 | "github.com/sirupsen/logrus" 10 | conf2 "go-wxbot/openwechat/comm/conf" 11 | ) 12 | 13 | func initAction(t *testing.T) (conf *conf2.Conf) { 14 | extra.RegisterFuzzyDecoders() 15 | logrus.SetLevel(logrus.DebugLevel) 16 | var ( 17 | err error 18 | ) 19 | conf, err = conf2.GetConf("../../config/prod.yaml") 20 | if err != nil { 21 | t.Logf("get conf err:%s ", err.Error()) 22 | return 23 | } 24 | 25 | return conf 26 | } 27 | 28 | func TestGetWeatherInfo(t *testing.T) { 29 | conf := initAction(t) 30 | os.Chdir("../../") 31 | format, err := GetFormatWeatherMessage(conf, "泾县") 32 | fmt.Println(format, err) 33 | } 34 | -------------------------------------------------------------------------------- /comm/tian/model.go: -------------------------------------------------------------------------------- 1 | package tian 2 | 3 | type Info1 struct { 4 | Code int `json:"code"` 5 | Msg string `json:"msg"` 6 | Newslist []struct { 7 | Title string `json:"title"` 8 | Content string `json:"content"` 9 | Id int `json:"id"` 10 | TypeId int `json:"type_id"` 11 | TypeName string `json:"type_name"` 12 | CpName string `json:"cp_name"` 13 | Zuofa string `json:"zuofa"` 14 | Texing string `json:"texing"` 15 | Tishi string `json:"tishi"` 16 | Tiaoliao string `json:"tiaoliao"` 17 | Yuanliao string `json:"yuanliao"` 18 | En string `json:"en"` 19 | Zh string `json:"zh"` 20 | Saying string `json:"saying"` 21 | Transl string `json:"transl"` 22 | Source string `json:"source"` 23 | } `json:"newslist"` 24 | } 25 | -------------------------------------------------------------------------------- /comm/redis/redis_test.go: -------------------------------------------------------------------------------- 1 | package redis 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "testing" 7 | 8 | "github.com/json-iterator/go/extra" 9 | "github.com/sirupsen/logrus" 10 | conf2 "go-wxbot/openwechat/comm/conf" 11 | "go-wxbot/openwechat/comm/global" 12 | ) 13 | 14 | func initAction(t *testing.T) { 15 | extra.RegisterFuzzyDecoders() 16 | logrus.SetLevel(logrus.DebugLevel) 17 | var ( 18 | err error 19 | ) 20 | conf, err := conf2.GetConf("../../config/prod.yaml") 21 | if err != nil { 22 | t.Logf("get conf err:%s ", err.Error()) 23 | return 24 | } 25 | 26 | global.Conf = conf 27 | } 28 | 29 | func TestGetRedis(t *testing.T) { 30 | initAction(t) 31 | 32 | client := GetRedis() 33 | err := client.Set(context.Background(), "test", 1111, 0).Err() 34 | fmt.Println("err ===", err) 35 | } 36 | -------------------------------------------------------------------------------- /comm/redis/redis.go: -------------------------------------------------------------------------------- 1 | package redis 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/go-redis/redis/v8" 7 | "github.com/sirupsen/logrus" 8 | "go-wxbot/openwechat/comm/global" 9 | ) 10 | 11 | var ( 12 | gClient *redis.Client 13 | ) 14 | 15 | // RedisInit . 16 | func RedisInit() { 17 | gClient = redis.NewClient(&redis.Options{ 18 | Addr: getAddr(), 19 | Password: global.Conf.RedisConf.Passwd, 20 | }) 21 | 22 | return 23 | } 24 | 25 | func getAddr() string { 26 | return fmt.Sprintf("%s:%s", global.Conf.RedisConf.IP, global.Conf.RedisConf.Port) 27 | } 28 | 29 | // GetRedis 单例连接池 30 | func GetRedis() (c *redis.Client) { 31 | if gClient != nil { 32 | return gClient 33 | } 34 | 35 | return redis.NewClient(&redis.Options{ 36 | Addr: getAddr(), 37 | Password: global.Conf.RedisConf.Passwd, 38 | }) 39 | } 40 | 41 | func CloseRedis() { 42 | err := gClient.Close() 43 | if err != nil { 44 | logrus.Errorf("CloseRedis redis err:%s ", err.Error()) 45 | } 46 | return 47 | } 48 | -------------------------------------------------------------------------------- /comm/qweather/qweather_test.go: -------------------------------------------------------------------------------- 1 | package qweather 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/json-iterator/go/extra" 7 | "github.com/sirupsen/logrus" 8 | conf2 "go-wxbot/openwechat/comm/conf" 9 | "go-wxbot/openwechat/comm/global" 10 | ) 11 | 12 | func initAction(t *testing.T) (conf *conf2.Conf) { 13 | extra.RegisterFuzzyDecoders() 14 | logrus.SetLevel(logrus.DebugLevel) 15 | var ( 16 | err error 17 | ) 18 | conf, err = conf2.GetConf("../../config/prod.yaml") 19 | if err != nil { 20 | t.Logf("get conf err:%s ", err.Error()) 21 | return 22 | } 23 | 24 | global.Conf = conf 25 | 26 | return conf 27 | } 28 | 29 | func TestGetLocationID(t *testing.T) { 30 | initAction(t) 31 | id, err := GetLocationID("北京") 32 | if err != nil { 33 | t.Error(err) 34 | } 35 | t.Log(id) 36 | } 37 | 38 | func TestGetQWeatherDetail(t *testing.T) { 39 | initAction(t) 40 | detail, err := GetQWeatherDetail("101010100", "北京") 41 | if err != nil { 42 | t.Error(err) 43 | } 44 | t.Log(detail) 45 | } 46 | -------------------------------------------------------------------------------- /comm/funcs/funcs.go: -------------------------------------------------------------------------------- 1 | package funcs 2 | 3 | import ( 4 | "encoding/base64" 5 | "io/ioutil" 6 | "net/http" 7 | "os" 8 | 9 | "github.com/pkg/errors" 10 | "github.com/sirupsen/logrus" 11 | ) 12 | 13 | func Wd() string { 14 | wd, _ := os.Getwd() 15 | return wd 16 | } 17 | 18 | func toBase64(b []byte) string { 19 | return base64.StdEncoding.EncodeToString(b) 20 | } 21 | 22 | func Img2base64(path string) (resultBase64 string, err error) { 23 | bytes, err := ioutil.ReadFile(path) 24 | if err != nil { 25 | err = errors.Wrapf(err, "Img2base64 ReadFile err") 26 | logrus.Error(err.Error()) 27 | return "", err 28 | } 29 | 30 | var base64Encoding string 31 | 32 | mimeType := http.DetectContentType(bytes) 33 | 34 | switch mimeType { 35 | case "image/jpeg": 36 | base64Encoding += "data:image/jpeg;base64," 37 | case "image/png": 38 | base64Encoding += "data:image/png;base64," 39 | } 40 | 41 | // Append the base64 encoded output 42 | base64Encoding += toBase64(bytes) 43 | 44 | return base64Encoding, nil 45 | } 46 | -------------------------------------------------------------------------------- /comm/tian/tian_test.go: -------------------------------------------------------------------------------- 1 | package tian 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/json-iterator/go/extra" 8 | "github.com/sirupsen/logrus" 9 | conf2 "go-wxbot/openwechat/comm/conf" 10 | "go-wxbot/openwechat/comm/global" 11 | ) 12 | 13 | func initAction(t *testing.T) (conf *conf2.Conf) { 14 | extra.RegisterFuzzyDecoders() 15 | logrus.SetLevel(logrus.DebugLevel) 16 | var ( 17 | err error 18 | ) 19 | conf, err = conf2.GetConf("../../config/prod.yaml") 20 | if err != nil { 21 | t.Logf("get conf err:%s ", err.Error()) 22 | return 23 | } 24 | 25 | global.Conf = conf 26 | 27 | return conf 28 | } 29 | 30 | func TestGetMessage(t *testing.T) { 31 | _ = initAction(t) 32 | ret, err := GetMessage(C_godreply) 33 | fmt.Println(ret, err) 34 | ret, err = GetMessage(C_mingyan) 35 | fmt.Println(ret, err) 36 | ret, err = GetMessage(C_caipu, "红烧肉") 37 | fmt.Println(ret, err) 38 | ret, err = GetMessage(C_caipu, "西红柿鸡蛋汤") 39 | fmt.Println(ret, err) 40 | ret, err = GetMessage(C_englishSentence) 41 | fmt.Println(ret, err) 42 | } 43 | 44 | func TestGetMessageV1(t *testing.T) { 45 | _ = initAction(t) 46 | ret, err := GetMessageV1(C_lizhiguyan) 47 | fmt.Println(ret, err) 48 | } 49 | -------------------------------------------------------------------------------- /comm/msg/msg_test.go: -------------------------------------------------------------------------------- 1 | package msg 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "testing" 7 | 8 | "github.com/json-iterator/go/extra" 9 | "github.com/sirupsen/logrus" 10 | conf2 "go-wxbot/openwechat/comm/conf" 11 | "go-wxbot/openwechat/comm/funcs" 12 | "go-wxbot/openwechat/comm/global" 13 | ) 14 | 15 | func initAction(t *testing.T) (conf *conf2.Conf) { 16 | extra.RegisterFuzzyDecoders() 17 | logrus.SetLevel(logrus.DebugLevel) 18 | var ( 19 | err error 20 | ) 21 | conf, err = conf2.GetConf("../../config/prod.yaml") 22 | if err != nil { 23 | t.Logf("get conf err:%s ", err.Error()) 24 | return 25 | } 26 | 27 | global.Conf = conf 28 | 29 | return conf 30 | } 31 | 32 | func Test_1(t *testing.T) { 33 | initAction(t) 34 | os.Chdir("../../") 35 | base64, err := funcs.Img2base64("D:\\go\\src\\go-wxbot\\avatar\\test.png") 36 | if err != nil { 37 | fmt.Println(err.Error()) 38 | return 39 | } 40 | 41 | hatBase64, err := AvatarAddChristmasHat(base64) 42 | if err != nil { 43 | fmt.Println(err.Error()) 44 | return 45 | } 46 | 47 | filename, err := SaveImageToDisk("ddd", hatBase64) 48 | if err != nil { 49 | fmt.Println(err.Error()) 50 | return 51 | } 52 | 53 | fmt.Println("success ===================", filename) 54 | } 55 | -------------------------------------------------------------------------------- /comm/adcode/adcode.go: -------------------------------------------------------------------------------- 1 | package adcode 2 | 3 | import ( 4 | "bufio" 5 | "io" 6 | "os" 7 | "strings" 8 | 9 | "github.com/pkg/errors" 10 | "github.com/sirupsen/logrus" 11 | ) 12 | 13 | var ( 14 | adcodeMap = make(map[string]string) // map[adcode]LocInfo 15 | ) 16 | 17 | func loadAdcodeInfo() { 18 | // 读取文件 19 | f, err := os.Open("comm/adcode/adcode.txt") 20 | if err != nil { 21 | err = errors.Wrapf(err, "open adcode.txt err") 22 | logrus.Error(err.Error()) 23 | return 24 | } 25 | defer f.Close() 26 | 27 | rd := bufio.NewReader(f) 28 | for { 29 | line, err := rd.ReadString('\n') //以'\n'为结束符读入一行 30 | line = strings.TrimRight(line, "\n") 31 | if err != nil || io.EOF == err { 32 | break 33 | } 34 | adcodeArr := strings.Split(line, ",") 35 | if len(adcodeArr) < 9 { 36 | continue 37 | } 38 | 39 | adcodeMap[adcodeArr[1]] = adcodeArr[0] 40 | if adcodeArr[8] != "" { 41 | adcodeMap[adcodeArr[8]] = adcodeArr[0] 42 | } else { 43 | adcodeMap[adcodeArr[7]] = adcodeArr[0] 44 | } 45 | } 46 | 47 | return 48 | } 49 | 50 | // GetAdcodeByCityName 通过城市获取 adcode, 支持到区县和简称,比如 山亭区/山亭 51 | func GetAdcodeByCityName(cityName string) (adcode string) { 52 | if len(adcodeMap) == 0 { 53 | loadAdcodeInfo() 54 | } 55 | 56 | return adcodeMap[cityName] 57 | } 58 | -------------------------------------------------------------------------------- /comm/funcs/funcs_test.go: -------------------------------------------------------------------------------- 1 | package funcs 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | "time" 7 | 8 | "github.com/json-iterator/go/extra" 9 | "github.com/sirupsen/logrus" 10 | conf2 "go-wxbot/openwechat/comm/conf" 11 | "go-wxbot/openwechat/comm/global" 12 | ) 13 | 14 | func initAction(t *testing.T) (conf *conf2.Conf) { 15 | extra.RegisterFuzzyDecoders() 16 | logrus.SetLevel(logrus.DebugLevel) 17 | var ( 18 | err error 19 | ) 20 | conf, err = conf2.GetConf("../../config/prod.yaml") 21 | if err != nil { 22 | t.Logf("get conf err:%s ", err.Error()) 23 | return 24 | } 25 | 26 | global.Conf = conf 27 | 28 | return conf 29 | } 30 | 31 | func TestImg2base64(t *testing.T) { 32 | ret, err := Img2base64("D:\\go\\src\\go-wxbot\\avatar\\test.png") 33 | if err != nil { 34 | fmt.Println(err.Error()) 35 | return 36 | } 37 | 38 | fmt.Println(ret) 39 | } 40 | 41 | func TestGetDiffDays(t *testing.T) { 42 | 43 | x := GetDiffDaysSolar(getCurrentDate(), "08-22") 44 | fmt.Println(x) 45 | } 46 | 47 | func TestGetqiXi(t *testing.T) { 48 | x := getLunar2SolarDate(int64(time.Now().Year()), 7, 10) 49 | fmt.Println("xxxxx1111", x) 50 | xx := GetDiffDaysLunar(getCurrentDate(), x, 7, 10) 51 | fmt.Println(xx) 52 | } 53 | 54 | func TestImportDateFormatMsg(t *testing.T) { 55 | initAction(t) 56 | x := ImportDateFormatMsg() 57 | t.Log(x) 58 | } 59 | 60 | func TestRemainingDays(t *testing.T) { 61 | t.Log(RemainingDays()) 62 | } 63 | -------------------------------------------------------------------------------- /comm/tian/tian2.go: -------------------------------------------------------------------------------- 1 | package tian 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "time" 7 | 8 | jsoniter "github.com/json-iterator/go" 9 | "github.com/pkg/errors" 10 | "github.com/sirupsen/logrus" 11 | "go-wxbot/openwechat/comm/global" 12 | "go-wxbot/openwechat/comm/web" 13 | ) 14 | 15 | // 不是 vip 只能多注册几个账号,哈哈哈 16 | 17 | func GetMessageV1(stype string, word ...string) (message string, err error) { 18 | var ( 19 | surl, respBody string 20 | statusCode int 21 | info Info1 22 | ) 23 | 24 | surl = fmt.Sprintf("http://api.tianapi.com/%s/index?key=%s", stype, global.Conf.Keys.TianapiKey1) 25 | 26 | respBody, statusCode, err = web.HTTP(surl, http.MethodGet, map[string]string{}, 30*time.Second, "") 27 | if err != nil { 28 | err = errors.Wrapf(err, "GetMessage http err") 29 | logrus.Error(err.Error()) 30 | return "", err 31 | } 32 | 33 | if statusCode != http.StatusOK { 34 | err = fmt.Errorf("GetMessage http statusCode not 200 is %d", statusCode) 35 | logrus.Error(err.Error()) 36 | return "", err 37 | } 38 | 39 | err = jsoniter.Unmarshal([]byte(respBody), &info) 40 | if err != nil { 41 | err = errors.Wrapf(err, "GetMessage http Unmarshal err") 42 | logrus.Error(err.Error()) 43 | return "", err 44 | } 45 | 46 | if stype == C_lizhiguyan { 47 | if len(info.Newslist) > 0 && info.Newslist[0].Saying != "" { 48 | message = fmt.Sprintf("%s【翻译:%s】。", info.Newslist[0].Saying, info.Newslist[0].Transl) 49 | return message, nil 50 | } 51 | } 52 | 53 | err = fmt.Errorf("GetMessage http content empty") 54 | logrus.Error(err.Error()) 55 | return "", err 56 | } 57 | -------------------------------------------------------------------------------- /comm/web/http.go: -------------------------------------------------------------------------------- 1 | package web 2 | 3 | import ( 4 | "context" 5 | "crypto/tls" 6 | "io/ioutil" 7 | "net/http" 8 | "strings" 9 | "time" 10 | 11 | "github.com/pkg/errors" 12 | "github.com/sirupsen/logrus" 13 | ) 14 | 15 | // HTTP . 16 | func HTTP(reqURL, method string, header map[string]string, 17 | timeout time.Duration, body string) (respBody string, statusCode int, err error) { 18 | timeBegin := time.Now() 19 | 20 | defer func() { 21 | logrus.Infof("url:%s, cost:%d ms, body:%s", reqURL, time.Since(timeBegin).Milliseconds(), body) 22 | }() 23 | 24 | logrus.Infof("req:%s", reqURL) 25 | 26 | newReq, err := http.NewRequest(method, reqURL, strings.NewReader(body)) 27 | if err != nil { 28 | err = errors.Wrapf(err, "NewRequest error:%s", reqURL) 29 | return "", http.StatusInternalServerError, err 30 | } 31 | 32 | if header != nil { 33 | for k, v := range header { 34 | if strings.EqualFold(k, "host") { 35 | newReq.Host = v 36 | } 37 | newReq.Header.Set(k, v) 38 | } 39 | } 40 | 41 | ctx, cancel := context.WithTimeout(context.Background(), timeout) 42 | defer cancel() 43 | newReq = newReq.WithContext(ctx) 44 | logrus.Tracef("newReq:%+v", newReq) 45 | 46 | // 忽略对证书的校验 47 | tr := &http.Transport{ 48 | DisableKeepAlives: true, 49 | TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, 50 | } 51 | 52 | newResp, err := (&http.Client{ 53 | Transport: tr, 54 | }).Do(newReq) 55 | if err != nil { 56 | err = errors.Wrapf(err, "request error:%s", reqURL) 57 | return "", http.StatusInternalServerError, err 58 | } 59 | defer newResp.Body.Close() 60 | 61 | newBody, err := ioutil.ReadAll(newResp.Body) 62 | return string(newBody), newResp.StatusCode, err 63 | } 64 | -------------------------------------------------------------------------------- /comm/weather/weather.go: -------------------------------------------------------------------------------- 1 | package weather 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "time" 7 | 8 | jsoniter "github.com/json-iterator/go" 9 | "github.com/pkg/errors" 10 | "github.com/sirupsen/logrus" 11 | adcode2 "go-wxbot/openwechat/comm/adcode" 12 | "go-wxbot/openwechat/comm/global" 13 | "go-wxbot/openwechat/comm/web" 14 | ) 15 | 16 | // https://restapi.amap.com/v3/weather/weatherInfo?city=110101&key=<用户key> 17 | func GetWeatherInfo(cityName string) (info WeatherInfo, err error) { 18 | var ( 19 | surl, 20 | respBody, adcode string 21 | statusCode int 22 | ) 23 | 24 | adcode = adcode2.GetAdcodeByCityName(cityName) 25 | if adcode == "" { 26 | return info, fmt.Errorf("get adcode empty,cityName: %s", cityName) 27 | } 28 | 29 | surl = fmt.Sprintf("https://restapi.amap.com/v3/weather/weatherInfo?city=%s&key=%s", 30 | adcode, global.Conf.Keys.WeatherKey) 31 | 32 | respBody, statusCode, err = web.HTTP(surl, http.MethodGet, map[string]string{}, 30*time.Second, "") 33 | if err != nil { 34 | err = errors.Wrapf(err, "GetWeatherInfo http err") 35 | logrus.Error(err.Error()) 36 | return info, err 37 | } 38 | 39 | if statusCode != http.StatusOK { 40 | return info, fmt.Errorf("GetWeatherInfo http statusCode not 200 is %d", statusCode) 41 | } 42 | 43 | err = jsoniter.Unmarshal([]byte(respBody), &info) 44 | if err != nil { 45 | err = errors.Wrapf(err, "GetWeatherInfo Unmarshal err") 46 | return info, err 47 | } 48 | 49 | if info.Info != "OK" { 50 | err = fmt.Errorf("GetWeatherInfo resp info not OK is %s", info.Info) 51 | return info, err 52 | } 53 | 54 | return info, nil 55 | } 56 | 57 | func GetFormatWeatherMessage(cityName string) (format string, err error) { 58 | var ( 59 | info WeatherInfo 60 | ) 61 | info, err = GetWeatherInfo(cityName) 62 | if err != nil { 63 | return "", err 64 | } 65 | 66 | format = fmt.Sprintf(` 67 | %s%s今日天气 %s,温度 %s 摄氏度,空气湿度 %s。 68 | `, 69 | info.Lives[0].Province, 70 | info.Lives[0].City, 71 | info.Lives[0].Weather, 72 | info.Lives[0].Temperature, 73 | info.Lives[0].Humidity, 74 | ) 75 | 76 | return format, nil 77 | } 78 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Go 微信机器人 2 | 3 | ⚠️本项目只是作者的一个玩具,大部分功能只为个人定制,并不通用。 4 | 5 | ## 功能演示 6 | 7 | [//]: # ([查看 gif 演示](https://cdn.xiaobinqt.cn/%E6%BC%94%E7%A4%BA.gif)) 8 | 9 |
10 | 11 | ## 部署说明 12 | 13 | clone 项目到本地,然后进入项目目录,将 `config/dev.yaml` 文件改成 `config/prod.yaml`, yaml 配置文件需要配置下,可以去对应的网站获取 apiKey。 14 | 15 | 执行如下命令: 16 | 17 | 18 | ```shell 19 | go mod tidy # 下载依赖 20 | 21 | go build -v -o wxbot # 编译 22 | 23 | nohup ./wxbot > core.log & # 后台运行, 可以查看日志 core.log 24 | ``` 25 | 26 | `less core.log` 可以查看日志,日志里有二维码,可以扫码登录。 27 | 28 | ## 功能列表 29 | 30 | ### 定时给女朋友推消息 31 | 32 | 每天早上 9:30 给女朋友推送一条早安消息,每天晚上 23:00 给女朋友推送一条晚安消息。好吧,我要被女朋友锤了:cry:。 33 | 34 | ### 自定义事件提醒 35 | 36 | 生活中的很多事情都是通过微信提醒的,比如快递消息等。这个自定义事件消息,可以通过固定规则,让机器人定时给我们发消息提醒我们某事。 37 | 38 | 比如`+s15:32,消息内容,3,60`就会定时「今天 15:31 提醒我「消息内容」,提醒 3 次每次间隔 60s」 39 | 40 | 又如`+st20221227 15:35,,消息内容,3,60`会定时「20221227日 15:35 提醒我「消息内容」,提醒 3 次每次间隔 60s」。具体可以参看功能演示部分。 41 | 42 | ### 定时给群推送消息 43 | 44 | **现在只能通过群名获取群信息**,每天定时给群推送上班打卡等消息,比如每天提醒吃饭。 45 | 46 | ### 根据关键字回复 47 | 48 | 基于 [天行](https://www.tianapi.com/) api 和 [和风天气](https://console.qweather.com/#/console?lang=zh) 查询接口开发。 49 | 50 | 比如在群里发送【泾县天气】机器人会回复泾县今日的天气情况。 51 | 52 | 现在支持的关键字查询如下: 53 | 54 | ``` 55 | 天气查询,如:泾县天气。 56 | 菜谱查询,如: 红烧肉菜谱,红烧肉做法。 57 | 输入【打赏】打赏卫小兵。 58 | 输入【程序员鼓励师】收到程序员鼓励师的回复。 59 | 输入【毒鸡汤】关键字回复毒鸡汤。 60 | 输入【事件提醒】获取设置事件提醒的格式。 61 | 输入【圣诞帽】关键字回复简单处理后的圣诞帽头像,个别用户获取不到头像信息。 62 | 输入【英语一句话】关键字回复一句学习英语。 63 | ``` 64 | 65 | ## 联系方式 66 | 67 |  68 | 69 | ## 感谢 70 | 71 | 基于 [openwechat](https://github.com/eatmoreapple/openwechat) 开发,感谢作者。 72 | 73 | 74 | -------------------------------------------------------------------------------- /comm/conf/conf.go: -------------------------------------------------------------------------------- 1 | package conf 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | 7 | "github.com/pkg/errors" 8 | "github.com/sirupsen/logrus" 9 | "gopkg.in/yaml.v3" 10 | ) 11 | 12 | // Conf . 13 | type Conf struct { 14 | App App `json:"app" yaml:"app"` 15 | Keys Keys `json:"keys" yaml:"keys"` 16 | RedisConf RedisConf `json:"redis" yaml:"redis"` 17 | } 18 | 19 | type RedisConf struct { 20 | IP string `json:"ip" yaml:"ip"` 21 | Port string `json:"port" yaml:"port"` 22 | Passwd string `json:"passwd" yaml:"passwd"` 23 | } 24 | 25 | // App . 26 | type App struct { 27 | Env string `json:"env" yaml:"env"` 28 | } 29 | 30 | type Keys struct { 31 | ChristmasHatURL string `json:"christmas_hat_url" yaml:"christmas_hat_url"` 32 | BotName string `json:"bot_name" yaml:"bot_name"` 33 | WeatherKey string `json:"weather_key" yaml:"weather_key"` 34 | TianapiKey string `json:"tianapi_key" yaml:"tianapi_key"` 35 | TianapiKey1 string `json:"tianapi_key1" yaml:"tianapi_key1"` 36 | HoneyLove string `json:"honey_love" yaml:"honey_love"` 37 | LoverChName string `json:"lover_ch_name" yaml:"lover_ch_name"` 38 | MasterAccount string `json:"master_account" yaml:"master_account"` 39 | HouchangcunFans string `json:"houchangcun_fans" yaml:"houchangcun_fans"` 40 | BanzhuanGroup string `json:"banzhuan_group" yaml:"banzhuan_group"` 41 | BubeiGroup string `json:"bubei_group" yaml:"bubei_group"` 42 | QweatherKey string `json:"qweather_key" yaml:"qweather_key"` 43 | BubeiStartDate string `json:"bubei_start_date" yaml:"bubei_start_date"` 44 | WuZhuangShiMembers string `json:"wu_zhuang_shi_members" yaml:"wu_zhuang_shi_members"` 45 | RemindMsg string `json:"remind_msg" yaml:"remind_msg"` 46 | } 47 | 48 | // GetConf . 49 | func GetConf(cfg string) (conf *Conf, err error) { 50 | var ( 51 | yamlFile = make([]byte, 0) 52 | ) 53 | 54 | filepath := fmt.Sprintf("%s", cfg) 55 | logrus.Infof("filepath: %s", filepath) 56 | yamlFile, err = ioutil.ReadFile(filepath) 57 | if err != nil { 58 | err = errors.Wrapf(err, "ReadFile error") 59 | logrus.Errorf(err.Error()) 60 | return conf, err 61 | } 62 | 63 | err = yaml.Unmarshal(yamlFile, &conf) 64 | if err != nil { 65 | err = errors.Wrapf(err, "yaml.Unmarshal error") 66 | logrus.Errorf(err.Error()) 67 | return conf, err 68 | } 69 | 70 | return conf, nil 71 | } 72 | -------------------------------------------------------------------------------- /comm/ticker/love.go: -------------------------------------------------------------------------------- 1 | package ticker 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "github.com/pkg/errors" 8 | "github.com/sirupsen/logrus" 9 | "go-wxbot/openwechat/comm/funcs" 10 | "go-wxbot/openwechat/comm/global" 11 | "go-wxbot/openwechat/comm/tian" 12 | ) 13 | 14 | func SendMessageToLover(prefix, stype string) { 15 | var ( 16 | err error 17 | message string 18 | ) 19 | message, err = tian.GetMessage(stype) 20 | if err != nil { 21 | err = errors.Wrapf(err, "SendMessageToLover get message err") 22 | logrus.Error(err.Error()) 23 | return 24 | } 25 | 26 | message = fmt.Sprintf("%s%s", prefix, message) 27 | err = global.WxFriends.SearchByRemarkName(1, global.Conf.Keys.HoneyLove).SendText(message) 28 | if err != nil { 29 | err = errors.Wrapf(err, "SendMessageToLover err") 30 | logrus.Error(err.Error()) 31 | } 32 | } 33 | 34 | func LoveTicker() { 35 | for { 36 | select { 37 | case t := <-time.After(1 * time.Minute): 38 | nowTime := t.Format("15:04") 39 | if nowTime == "09:30" { 40 | SendMessageToLover("亲爱的,早上好!爱你每一天!\n新的一天从一句土味情话开始:", tian.C_saylove) 41 | } 42 | 43 | if nowTime == "10:00" { 44 | lz, err := tian.GetMessageV1(tian.C_lizhiguyan) 45 | message := "" 46 | if err != nil { 47 | message = fmt.Sprintf("今年还剩 %d 天。\n\n盛年不重来,一日难再晨。及时当勉励,岁月不待人。", funcs.RemainingDays()) 48 | } else { 49 | message = fmt.Sprintf("今年还剩 %d 天。\n\n%s", funcs.RemainingDays(), lz) 50 | } 51 | 52 | err = global.WxFriends. 53 | SearchByRemarkName(1, global.Conf.Keys.HoneyLove). 54 | SendText(message) 55 | if err != nil { 56 | err = errors.Wrapf(err, "SendMessageToHoneyLove err") 57 | logrus.Error(err.Error()) 58 | } 59 | } 60 | 61 | if nowTime == "23:00" { 62 | SendMessageToLover("亲爱的,11 点了,该洗漱睡觉了!\n临睡之际送你一句土味情话:", tian.C_saylove) 63 | } 64 | 65 | if t.Weekday() >= 1 && t.Weekday() <= 5 { 66 | if nowTime == "09:55" { 67 | global.WxFriends. 68 | SearchByRemarkName(1, global.Conf.Keys.HoneyLove). 69 | SendText("虽然我们都不爱上班,但是还是不要忘记上报打卡。") 70 | } 71 | 72 | if nowTime == "20:00" { 73 | global.WxFriends. 74 | SearchByRemarkName(1, global.Conf.Keys.HoneyLove). 75 | SendText("八点了,该下班了,记得打卡。") 76 | } 77 | 78 | if nowTime == "21:00" { 79 | global.WxFriends. 80 | SearchByRemarkName(1, global.Conf.Keys.HoneyLove). 81 | SendText("九点了,要是还没下班,真的要准备下班了,记得打卡。") 82 | } 83 | } 84 | 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | 7 | "github.com/eatmoreapple/openwechat" 8 | "github.com/json-iterator/go/extra" 9 | "github.com/pkg/errors" 10 | "github.com/sirupsen/logrus" 11 | qrcode "github.com/skip2/go-qrcode" 12 | conf2 "go-wxbot/openwechat/comm/conf" 13 | "go-wxbot/openwechat/comm/global" 14 | msg2 "go-wxbot/openwechat/comm/msg" 15 | "go-wxbot/openwechat/comm/ticker" 16 | ) 17 | 18 | var ( 19 | cfgPath = flag.String("c", "config/prod.yaml", "*.yaml config path") 20 | err error 21 | ) 22 | 23 | func ConsoleQrCode(uuid string) { 24 | q, _ := qrcode.New("https://login.weixin.qq.com/l/"+uuid, qrcode.Low) 25 | fmt.Println(q.ToString(true)) 26 | } 27 | 28 | func main() { 29 | extra.RegisterFuzzyDecoders() 30 | flag.Parse() 31 | logrus.SetLevel(logrus.DebugLevel) 32 | logrus.Debugf("config: %s", *cfgPath) 33 | 34 | // 加载配置文件 35 | global.Conf, err = conf2.GetConf(*cfgPath) 36 | if err != nil { 37 | logrus.Fatalf(err.Error()) 38 | } 39 | 40 | bot := openwechat.DefaultBot(openwechat.Desktop) 41 | //bot := openwechat.DefaultBot(openwechat.Normal) // 桌面模式,上面登录不上的可以尝试切换这种模式 42 | 43 | bot.SyncCheckCallback = nil // 关闭心跳 44 | 45 | // 注册消息处理函数 46 | bot.MessageHandler = func(msg *openwechat.Message) { 47 | if msg.IsText() && msg.Content == "ping" { 48 | _, err = msg.ReplyText("pong") 49 | if err != nil { 50 | err = errors.Wrapf(err, "ping msg replyText err") 51 | logrus.Error(err.Error()) 52 | } 53 | } 54 | 55 | // 处理消息 56 | msg2.HandleMsg(msg) 57 | } 58 | 59 | // 注册登陆二维码回调 60 | //bot.UUIDCallback = openwechat.PrintlnQrcodeUrl 61 | bot.UUIDCallback = ConsoleQrCode 62 | 63 | // 登陆 64 | if err = bot.Login(); err != nil { 65 | logrus.Fatalf("bot.Login err %s", err.Error()) 66 | } 67 | 68 | // 获取登陆的用户 69 | global.WxSelf, err = bot.GetCurrentUser() 70 | if err != nil { 71 | logrus.Fatalf("GetCurrentUser err: %s ", err.Error()) 72 | } 73 | 74 | // 获取所有的好友 75 | global.WxFriends, err = global.WxSelf.Friends(true) 76 | if err != nil { 77 | logrus.Fatalf("wx self get friends err: %s ", err.Error()) 78 | } 79 | 80 | // 获取所有的群组 81 | global.WxGroups, err = global.WxSelf.Groups(true) 82 | if err != nil { 83 | logrus.Fatalf("wx self get groups err: %s ", err.Error()) 84 | } 85 | 86 | ticker.Ticker() 87 | 88 | //Test() 89 | 90 | // 阻塞主goroutine, 直到发生异常或者用户主动退出 91 | err = bot.Block() 92 | if err != nil { 93 | err = errors.Wrapf(err, "bot.Block() clash") 94 | logrus.Error(err.Error()) 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /comm/tian/get_message.go: -------------------------------------------------------------------------------- 1 | package tian 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "time" 7 | 8 | jsoniter "github.com/json-iterator/go" 9 | "github.com/pkg/errors" 10 | "github.com/sirupsen/logrus" 11 | "go-wxbot/openwechat/comm/global" 12 | "go-wxbot/openwechat/comm/web" 13 | ) 14 | 15 | func GetMessage(stype string, word ...string) (message string, err error) { 16 | var ( 17 | surl, respBody string 18 | statusCode int 19 | info Info1 20 | ) 21 | 22 | if stype == C_caipu { 23 | surl = fmt.Sprintf("http://api.tianapi.com/%s/index?key=%s&word=%s", 24 | stype, global.Conf.Keys.TianapiKey, word[0]) 25 | } else { 26 | surl = fmt.Sprintf("http://api.tianapi.com/%s/index?key=%s", stype, global.Conf.Keys.TianapiKey) 27 | } 28 | 29 | respBody, statusCode, err = web.HTTP(surl, http.MethodGet, map[string]string{}, 30*time.Second, "") 30 | if err != nil { 31 | err = errors.Wrapf(err, "GetMessage http err") 32 | logrus.Error(err.Error()) 33 | return "", err 34 | } 35 | 36 | if statusCode != http.StatusOK { 37 | err = fmt.Errorf("GetMessage http statusCode not 200 is %d", statusCode) 38 | logrus.Error(err.Error()) 39 | return "", err 40 | } 41 | 42 | err = jsoniter.Unmarshal([]byte(respBody), &info) 43 | if err != nil { 44 | err = errors.Wrapf(err, "GetMessage http Unmarshal err") 45 | logrus.Error(err.Error()) 46 | return "", err 47 | } 48 | 49 | if stype == C_godreply { // 神回复比较特殊一点 50 | if len(info.Newslist) > 0 && info.Newslist[0].Content != "" && info.Newslist[0].Title != "" { 51 | message = fmt.Sprintf(` 52 | %s 53 | %s 54 | `, 55 | info.Newslist[0].Title, 56 | info.Newslist[0].Content, 57 | ) 58 | return message, nil 59 | } 60 | } else if stype == C_caipu { // 菜谱 61 | if len(info.Newslist) > 0 && info.Newslist[0].Zuofa != "" && info.Newslist[0].Yuanliao != "" { 62 | message = fmt.Sprintf(` 63 | 原料 :%s 64 | %s做法 :%s 65 | `, 66 | info.Newslist[0].Yuanliao, 67 | "\n", 68 | info.Newslist[0].Zuofa, 69 | ) 70 | return message, nil 71 | } 72 | return "", ErrNotfoundCaiPu 73 | } else if stype == C_englishSentence { // 英语一句话 74 | if len(info.Newslist) > 0 && info.Newslist[0].Zh != "" && info.Newslist[0].En != "" { 75 | message = fmt.Sprintf(` 76 | %s 77 | %s 78 | `, 79 | info.Newslist[0].En, 80 | info.Newslist[0].Zh, 81 | ) 82 | return message, nil 83 | } 84 | } else { 85 | if len(info.Newslist) > 0 && info.Newslist[0].Content != "" { 86 | return info.Newslist[0].Content, nil 87 | } 88 | } 89 | 90 | err = fmt.Errorf("GetMessage http content empty") 91 | logrus.Error(err.Error()) 92 | return "", err 93 | } 94 | -------------------------------------------------------------------------------- /comm/ticker/encourage.go: -------------------------------------------------------------------------------- 1 | package ticker 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strings" 7 | "time" 8 | 9 | "github.com/eatmoreapple/openwechat" 10 | "github.com/pkg/errors" 11 | "github.com/sirupsen/logrus" 12 | "go-wxbot/openwechat/comm/global" 13 | "go-wxbot/openwechat/comm/image" 14 | ) 15 | 16 | // 程序员鼓励师 17 | 18 | func EncourageTicker() { 19 | for { 20 | select { 21 | case t := <-time.After(1 * time.Minute): 22 | nowTime := t.Format("15:04") 23 | if nowTime != "11:55" { 24 | continue 25 | } 26 | 27 | var ( 28 | err error 29 | message, 30 | imgURL, imgPath string 31 | groups openwechat.Groups 32 | ) 33 | 34 | message = fmt.Sprintf("BUG 虽好,但不要贪多哦!程序员鼓励师提醒,该吃午饭了~") 35 | 36 | imgURL, err = image.GetImage() 37 | if err != nil { 38 | err = errors.Wrapf(err, "Encourage get image err") 39 | logrus.Error(err.Error()) 40 | continue 41 | } 42 | 43 | imgPath, err = image.SaveEncourageImg(imgURL) 44 | if err != nil { 45 | err = errors.Wrapf(err, "Encourage save image err") 46 | logrus.Error(err.Error()) 47 | continue 48 | } 49 | 50 | imgt1, err := os.Open(imgPath) 51 | if err != nil { 52 | err = errors.Wrapf(err, "open img file err") 53 | logrus.Error(err.Error()) 54 | continue 55 | } 56 | defer imgt1.Close() 57 | 58 | imgt2, err := os.Open(imgPath) 59 | if err != nil { 60 | err = errors.Wrapf(err, "open img file err") 61 | logrus.Error(err.Error()) 62 | continue 63 | } 64 | defer imgt2.Close() 65 | 66 | groups, err = global.WxSelf.Groups(true) 67 | if err != nil { 68 | err = errors.Wrapf(err, "SendMessageToFans get groups err") 69 | logrus.Error(err.Error()) 70 | continue 71 | } 72 | 73 | // 后场村粉丝群 74 | //groups.SearchByNickName(1, global.Conf.Keys.HouchangcunFans).SendText(message) 75 | //groups.SearchByNickName(1, global.Conf.Keys.HouchangcunFans).SendImage(imgt1) 76 | 77 | // 五壮士群 78 | for _, each := range groups { 79 | members, err := each.Members() 80 | if err != nil { 81 | err = errors.Wrapf(err, "SendMessageToFans get members err") 82 | logrus.Error(err.Error()) 83 | continue 84 | } 85 | 86 | // 不能通过群备注来获取群,真是恶心 87 | var Is = false 88 | for _, member := range members { 89 | if strings.Contains(global.Conf.Keys.WuZhuangShiMembers, member.NickName) { 90 | Is = true 91 | break 92 | } 93 | } 94 | 95 | if Is { 96 | each.SendText(message) 97 | each.SendImage(imgt2) 98 | } 99 | } 100 | 101 | os.Remove(imgPath) 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /comm/ticker/master_ticker.go: -------------------------------------------------------------------------------- 1 | package ticker 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "github.com/pkg/errors" 8 | "github.com/sirupsen/logrus" 9 | "go-wxbot/openwechat/comm/funcs" 10 | "go-wxbot/openwechat/comm/global" 11 | "go-wxbot/openwechat/comm/tian" 12 | ) 13 | 14 | // 每天提醒自己一些事 15 | func MasterTicker() { 16 | for { 17 | select { 18 | case t := <-time.After(1 * time.Minute): 19 | nowTime := t.Format("15:04") 20 | 21 | yasiEnd, _ := time.ParseInLocation("2006-01-02", "2023-10-07", time.Local) 22 | yasiRemaindays := int(yasiEnd.Sub(t).Hours() / 24) 23 | 24 | if nowTime == "10:00" { 25 | lz, err := tian.GetMessageV1(tian.C_lizhiguyan) 26 | message := "" 27 | if err != nil { 28 | message = fmt.Sprintf("盛年不重来,一日难再晨。及时当勉励,岁月不待人。\n今年还剩 %d 天。", funcs.RemainingDays()) 29 | } else { 30 | message = fmt.Sprintf("今年还剩 %d 天。\n\n%s", funcs.RemainingDays(), lz) 31 | } 32 | 33 | err = global.WxFriends. 34 | SearchByRemarkName(1, global.Conf.Keys.MasterAccount). 35 | SendText(message) 36 | if err != nil { 37 | err = errors.Wrapf(err, "SendMessageToMasterAccout err") 38 | logrus.Error(err.Error()) 39 | } 40 | 41 | _ = global.WxFriends. 42 | SearchByRemarkName(1, global.Conf.Keys.MasterAccount). 43 | SendText(fmt.Sprintf(`离雅思过期时间还有 %d 天,兄弟,留给你的时间不多了!`, yasiRemaindays)) 44 | } 45 | 46 | if nowTime == "22:00" { 47 | message := "记得背单词兄弟,别一天天的想偷懒!" 48 | err := global.WxFriends. 49 | SearchByRemarkName(1, global.Conf.Keys.MasterAccount). 50 | SendText(message) 51 | if err != nil { 52 | err = errors.Wrapf(err, "SendMessageToMasterAccout err") 53 | logrus.Error(err.Error()) 54 | } 55 | } 56 | 57 | if nowTime == "23:00" { 58 | message := "休息一下,整理一下今天的账单吧!记日记的时间也到了,不要忘记了哦!" 59 | err := global.WxFriends. 60 | SearchByRemarkName(1, global.Conf.Keys.MasterAccount). 61 | SendText(message) 62 | if err != nil { 63 | err = errors.Wrapf(err, "SendMessageToMasterAccout err") 64 | logrus.Error(err.Error()) 65 | } 66 | 67 | _ = global.WxFriends. 68 | SearchByRemarkName(1, global.Conf.Keys.MasterAccount). 69 | SendText(fmt.Sprintf(`离雅思过期时间还有 %d 天,兄弟,留给你的时间不多了!`, yasiRemaindays)) 70 | } 71 | 72 | if nowTime == "23:30" { 73 | message := funcs.ImportDateFormatMsg() 74 | logrus.Infof("send remind msg: %s", message) 75 | err := global.WxFriends. 76 | SearchByRemarkName(1, global.Conf.Keys.MasterAccount). 77 | SendText(message) 78 | if err != nil { 79 | err = errors.Wrapf(err, "SendMessageToMasterAccout err") 80 | logrus.Error(err.Error()) 81 | } 82 | } 83 | 84 | } 85 | } 86 | } 87 | 88 | func KeepLive() { 89 | for { 90 | select { 91 | case <-time.After(10 * time.Minute): 92 | global.WxSelf.FileHelper().SendText(fmt.Sprintf(`保活: %s`, 93 | time.Now().Format("2006-01-02 15:04:05"))) 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /comm/funcs/date.go: -------------------------------------------------------------------------------- 1 | package funcs 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "github.com/Lofanmi/chinese-calendar-golang/calendar" 8 | "go-wxbot/openwechat/comm/global" 9 | ) 10 | 11 | func ImportDateFormatMsg() (msg string) { 12 | yuanDan := "01-01" // 元旦 13 | valentineDay := "02-14" // 情人节 14 | anniversary := "09-17" // 纪念日 15 | //qiXiDay := "七月初七" // 七夕,七月初七 16 | day520 := "05-20" 17 | //springFestival := "正月初一" // 春节 18 | //loverBirthday := "七月初十" 19 | 20 | //logrus.Debugf(yuanDan, valentineDay, anniversary, qiXiDay, day520, springFestival, loverBirthday) 21 | 22 | msg = fmt.Sprintf("%s。\n距离元旦【01-01】还有 %d 天\n距离春节还有 %d 天\n距离【02-14】情人节还有 %d 天\n距离【520】还有 %d 天\n距离七夕,七月初七还有 %d 天\n"+ 23 | "距离纪念日还有 %d 天\n距离%s的生日还有 %d 天", 24 | global.Conf.Keys.RemindMsg, 25 | GetDiffDaysSolar(getCurrentDate(), yuanDan), 26 | GetDiffDaysLunar(getCurrentDate(), getLunar2SolarDate(int64(time.Now().Year()), 1, 0), 1, 1), 27 | GetDiffDaysSolar(getCurrentDate(), valentineDay), 28 | GetDiffDaysSolar(getCurrentDate(), day520), 29 | GetDiffDaysLunar(getCurrentDate(), getLunar2SolarDate(int64(time.Now().Year()), 7, 7), 7, 7), 30 | GetDiffDaysSolar(getCurrentDate(), anniversary), global.Conf.Keys.LoverChName, 31 | GetDiffDaysLunar(getCurrentDate(), getLunar2SolarDate(int64(time.Now().Year()), 7, 10), 7, 10), 32 | ) 33 | 34 | return msg 35 | } 36 | 37 | const DefaultDateFormat = "2006-01-02" 38 | 39 | func getYearDay(year int, date string) string { 40 | return fmt.Sprintf(`%d-%s`, year, date) 41 | } 42 | 43 | func getCurrentDate() string { 44 | return time.Now().Format("01-02") 45 | } 46 | 47 | // 获取两个时间相差的天数 48 | func GetDiffDaysSolar(curDate, futureDate string) (dd int) { 49 | curD, _ := time.ParseInLocation(DefaultDateFormat, getYearDay(time.Now().Year(), curDate), time.Local) 50 | furD, _ := time.ParseInLocation(DefaultDateFormat, getYearDay(time.Now().Year(), futureDate), time.Local) 51 | 52 | // 如果是负数说明已经过去了,加一年再计算 53 | dd = int(furD.Sub(curD).Hours() / 24) 54 | if dd > 0 { 55 | return dd 56 | } 57 | 58 | furD, _ = time.ParseInLocation(DefaultDateFormat, 59 | getYearDay(time.Now().AddDate(1, 0, 0).Year(), futureDate), time.Local) 60 | 61 | return int(furD.Sub(curD).Hours() / 24) 62 | } 63 | 64 | func GetDiffDaysLunar(curDate, futureDate string, lunarMonth, lunarDay int64) (dd int) { 65 | curD, _ := time.ParseInLocation(DefaultDateFormat, getYearDay(time.Now().Year(), curDate), time.Local) 66 | furD, _ := time.ParseInLocation(DefaultDateFormat, getYearDay(time.Now().Year(), futureDate), time.Local) 67 | 68 | // 如果是负数说明已经过去了,加一年再计算 69 | //fmt.Println(curD.String(), furD.String(), int(furD.Sub(curD).Hours()/24)) 70 | dd = int(furD.Sub(curD).Hours() / 24) 71 | if dd > 0 { 72 | return dd 73 | } 74 | 75 | futureDate = getLunar2SolarDate(int64(time.Now().AddDate(1, 0, 0).Year()), 76 | lunarMonth, lunarDay) 77 | furD, _ = time.ParseInLocation(DefaultDateFormat, 78 | getYearDay(time.Now().AddDate(1, 0, 0).Year(), futureDate), time.Local) 79 | 80 | return int(furD.Sub(curD).Hours() / 24) 81 | } 82 | 83 | // TODO 这里可能不准,需要计算闰月 84 | func getLunar2SolarDate(year, month, day int64) string { 85 | c := calendar.ByLunar(year, month, day, 0, 0, 0, false) 86 | return fmt.Sprintf("%02d-%02d", c.Solar.GetMonth(), c.Solar.GetDay()) 87 | } 88 | 89 | func RemainingDays() int { 90 | curD, _ := time.ParseInLocation(DefaultDateFormat, time.Now().Format("2006-01-02"), time.Local) 91 | furD, _ := time.ParseInLocation(DefaultDateFormat, fmt.Sprintf("%d-12-31", time.Now().Year()), time.Local) 92 | 93 | return int(furD.Sub(curD).Hours() / 24) 94 | } 95 | -------------------------------------------------------------------------------- /comm/image/image.go: -------------------------------------------------------------------------------- 1 | package image 2 | 3 | import ( 4 | "crypto/tls" 5 | "fmt" 6 | "io" 7 | "math/rand" 8 | "net/http" 9 | "os" 10 | "path" 11 | "time" 12 | 13 | jsoniter "github.com/json-iterator/go" 14 | "github.com/pkg/errors" 15 | "github.com/sirupsen/logrus" 16 | "go-wxbot/openwechat/comm/web" 17 | ) 18 | 19 | const ImageSourceURL = "https://imgegg.qianxiaoduan.com/wallpaper?offset=%d&limit=3&type=1" 20 | const ImageURL = "https://img.qianxiaoduan.com/" 21 | 22 | func GetRandomOffset() int { 23 | rand.Seed(time.Now().UnixNano()) 24 | return rand.Intn(10000) 25 | } 26 | 27 | type ImgSourceResp struct { 28 | Success bool `json:"success"` 29 | Code int `json:"code"` 30 | Message string `json:"message"` 31 | Result struct { 32 | Count int `json:"count"` 33 | Rows []struct { 34 | Id string `json:"id"` 35 | Thumbnail string `json:"thumbnail"` 36 | Url string `json:"url"` 37 | Hot int `json:"hot"` 38 | Width int `json:"width"` 39 | Height int `json:"height"` 40 | Name interface{} `json:"name"` 41 | Type string `json:"type"` 42 | Scale string `json:"scale"` 43 | Tag string `json:"tag"` 44 | CreatedAt string `json:"createdAt"` 45 | UpdatedAt string `json:"updatedAt"` 46 | } `json:"rows"` 47 | } `json:"result"` 48 | } 49 | 50 | func GetImage() (imgURL string, err error) { 51 | var ( 52 | surl, respBody string 53 | statusCode int 54 | imgSourceResp ImgSourceResp 55 | ) 56 | surl = fmt.Sprintf(ImageSourceURL, GetRandomOffset()) 57 | respBody, statusCode, err = web.HTTP(surl, http.MethodGet, map[string]string{}, 30*time.Second, "") 58 | if err != nil { 59 | err = errors.Wrapf(err, "GetImage request error:%s", surl) 60 | logrus.Error(err.Error()) 61 | return "", err 62 | } 63 | 64 | if statusCode != http.StatusOK { 65 | err = fmt.Errorf("GetImage request error:%s, statusCode:%d", surl, statusCode) 66 | logrus.Error(err.Error()) 67 | return "", err 68 | } 69 | 70 | err = jsoniter.Unmarshal([]byte(respBody), &imgSourceResp) 71 | if err != nil { 72 | err = errors.Wrapf(err, "GetImage unmarshal error:%s", surl) 73 | logrus.Error(err.Error()) 74 | return "", err 75 | } 76 | 77 | if len(imgSourceResp.Result.Rows) == 0 { 78 | err = fmt.Errorf("GetImage Result.Rows len is 0 error:%s, ", surl) 79 | logrus.Error(err.Error()) 80 | return "", err 81 | } 82 | 83 | return fmt.Sprintf("%s%s", ImageURL, imgSourceResp.Result.Rows[0].Url), nil 84 | } 85 | 86 | func SaveEncourageImg(imgURL string) (savePath string, err error) { 87 | savePath = fmt.Sprintf("%s/%d%s", os.TempDir(), time.Now().UnixNano(), path.Ext(imgURL)) 88 | logrus.Debugf("SendEncourageImg savePath: %s", savePath) 89 | 90 | request, err := http.NewRequest(http.MethodGet, imgURL, nil) 91 | if err != nil { 92 | err = errors.Wrapf(err, "SendEncourageImg newRequest err") 93 | logrus.Error(err.Error()) 94 | return "", err 95 | } 96 | 97 | newResp, err := (&http.Client{ 98 | Transport: &http.Transport{ 99 | DisableKeepAlives: true, 100 | TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, 101 | }, 102 | }).Do(request) 103 | if err != nil { 104 | err = errors.Wrapf(err, "SendEncourageImg client do err") 105 | logrus.Error(err.Error()) 106 | return "", err 107 | } 108 | defer newResp.Body.Close() 109 | 110 | out, err := os.Create(savePath) 111 | if err != nil { 112 | err = errors.Wrapf(err, "SendEncourageImg os.Create err") 113 | logrus.Error(err.Error()) 114 | return "", err 115 | } 116 | 117 | defer out.Close() 118 | 119 | io.Copy(out, newResp.Body) 120 | 121 | return savePath, nil 122 | } 123 | -------------------------------------------------------------------------------- /comm/ticker/hcc_fans_ticker.go: -------------------------------------------------------------------------------- 1 | package ticker 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | "strings" 7 | "time" 8 | 9 | "github.com/eatmoreapple/openwechat" 10 | "github.com/pkg/errors" 11 | "github.com/sirupsen/logrus" 12 | "go-wxbot/openwechat/comm/global" 13 | "go-wxbot/openwechat/comm/tian" 14 | ) 15 | 16 | // 后厂村吴彦祖粉丝团 17 | 18 | func SendMessageToFans(prefix, stype string) { 19 | var ( 20 | err error 21 | message string 22 | groups openwechat.Groups 23 | ) 24 | message, err = tian.GetMessage(stype) 25 | if err != nil { 26 | err = errors.Wrapf(err, "SendMessageToFans get message err") 27 | logrus.Error(err.Error()) 28 | return 29 | } 30 | 31 | message = fmt.Sprintf("%s%s", prefix, message) 32 | 33 | groups, err = global.WxSelf.Groups(true) 34 | if err != nil { 35 | err = errors.Wrapf(err, "SendMessageToFans get groups err") 36 | logrus.Error(err.Error()) 37 | return 38 | } 39 | 40 | err = groups.SearchByNickName(1, global.Conf.Keys.HouchangcunFans).SendText(message) 41 | if err != nil { 42 | err = errors.Wrapf(err, "SendMessageToFans to groups err, group nickname: %s", 43 | global.Conf.Keys.HouchangcunFans) 44 | logrus.Error(err.Error()) 45 | } 46 | 47 | err = groups.SearchByNickName(1, global.Conf.Keys.BanzhuanGroup).SendText(message) 48 | if err != nil { 49 | err = errors.Wrapf(err, "SendMessageToFans to groups err, group nickname: %s", 50 | global.Conf.Keys.HouchangcunFans) 51 | logrus.Error(err.Error()) 52 | } 53 | } 54 | 55 | func FansTicker() { 56 | for { 57 | select { 58 | case t := <-time.After(1 * time.Minute): 59 | nowTime := t.Format("15:04") 60 | if nowTime == "09:30" { 61 | pp := "" 62 | if t.Weekday() >= 1 && t.Weekday() <= 5 { 63 | pp = fmt.Sprintf(`星期%s快乐,不要忘记上班签到哦~`, weekdayCn(int(t.Weekday()))) 64 | prefix := fmt.Sprintf("%s\n新的一天从一碗毒鸡汤开始:", pp) 65 | SendMessageToFans(prefix, tian.C_dujitang) 66 | } 67 | 68 | //else { // 关闭周末提醒,毕竟大家要睡觉,哈哈哈 69 | // pp = fmt.Sprintf(`星期%s快乐,如果今天你得了福报要加班的话,不要忘记签到哦~`, 70 | // weekdayCn(int(t.Weekday()))) 71 | //} 72 | } 73 | } 74 | } 75 | } 76 | 77 | // 不背单词打卡群 78 | func BubeiGroupTicker() { 79 | // 计算时间是是否在打开时间段内 80 | startDate, err := time.ParseInLocation("2006-01-02", global.Conf.Keys.BubeiStartDate, time.Local) 81 | if err != nil { 82 | err = errors.Wrapf(err, "BubeiGroupTicker parse start date err") 83 | logrus.Error(err.Error()) 84 | return 85 | } 86 | 87 | cfArr := strings.Split(global.Conf.Keys.BubeiGroup, ",") 88 | bubeiGroupName := cfArr[0] 89 | days := 14 90 | if len(cfArr) > 1 { 91 | days, _ = strconv.Atoi(cfArr[1]) 92 | } 93 | if days == 0 { 94 | days = 14 95 | } 96 | endDate := startDate.AddDate(0, 0, days) 97 | logrus.Debugf("BubeiGroupTicker false or true,after:[%t],before:[%t],days:[%d],bubeiGroupName:[%s]", 98 | time.Now().After(startDate), time.Now().Before(endDate), days, bubeiGroupName) 99 | 100 | for { 101 | select { 102 | case t := <-time.After(1 * time.Minute): 103 | if time.Now().After(startDate) && time.Now().Before(endDate) { 104 | nowTime := t.Format("15:04") 105 | if nowTime == "22:30" { 106 | // 获取群列表 107 | groups, err := global.WxSelf.Groups(true) 108 | if err != nil { 109 | err = errors.Wrapf(err, "BubeiGroupTicker get groups err") 110 | logrus.Error(err.Error()) 111 | continue 112 | } 113 | // 搜索群 114 | for _, group := range groups { 115 | if group.NickName == bubeiGroupName { 116 | group.SendText("22:30 了,没打卡的小伙伴,赶紧去打卡吧!") 117 | } 118 | } 119 | } 120 | } 121 | } 122 | } 123 | } 124 | 125 | func weekdayCn(i int) string { 126 | var m = map[int]string{ 127 | 0: "日", 128 | 1: "一", 129 | 2: "二", 130 | 3: "三", 131 | 4: "四", 132 | 5: "五", 133 | 6: "六", 134 | } 135 | 136 | return m[i] 137 | } 138 | -------------------------------------------------------------------------------- /comm/ticker/ticker_test.go: -------------------------------------------------------------------------------- 1 | package ticker 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | "time" 7 | 8 | "github.com/go-redis/redis/v8" 9 | "github.com/json-iterator/go/extra" 10 | "github.com/sirupsen/logrus" 11 | conf2 "go-wxbot/openwechat/comm/conf" 12 | "go-wxbot/openwechat/comm/global" 13 | ) 14 | 15 | func initAction(t *testing.T) { 16 | extra.RegisterFuzzyDecoders() 17 | logrus.SetLevel(logrus.DebugLevel) 18 | var ( 19 | err error 20 | ) 21 | conf, err := conf2.GetConf("../../config/prod.yaml") 22 | if err != nil { 23 | t.Logf("get conf err:%s ", err.Error()) 24 | return 25 | } 26 | 27 | global.Conf = conf 28 | } 29 | 30 | func TestSendLoveMessage(t *testing.T) { 31 | var done = make(chan struct{}) 32 | go LoveTicker() 33 | 34 | fmt.Println("done...") 35 | <-done 36 | } 37 | 38 | func TestNoticeMessage(t *testing.T) { 39 | count, interval, startTimestamp, message, err := parseNoticeMessage("+st20221227 15:35,消息内容") 40 | fmt.Println(count, interval, startTimestamp, message, err) 41 | fmt.Println("-------------------------------------------------------") 42 | count, interval, startTimestamp, message, err = parseNoticeMessage("+st20221227 15:35,消息内容,3,15") 43 | fmt.Println(count, interval, startTimestamp, message, err) 44 | fmt.Println("-------------------------------------------------------") 45 | count, interval, startTimestamp, message, err = parseNoticeMessage("+s15:35,消息内容") 46 | fmt.Println(count, interval, startTimestamp, message, err) 47 | fmt.Println("-------------------------------------------------------") 48 | count, interval, startTimestamp, message, err = parseNoticeMessage("+s15:35,消息内容,1") 49 | fmt.Println(count, interval, startTimestamp, message, err) 50 | fmt.Println("-------------------------------------------------------") 51 | count, interval, startTimestamp, message, err = parseNoticeMessage("+s15:35,消息内容,2,60") 52 | fmt.Println(count, interval, startTimestamp, message, err) 53 | fmt.Println("-------------------------------------------------------") 54 | count, interval, startTimestamp, message, err = parseNoticeMessage("+s16:11,记得喝水,2,60") 55 | fmt.Println(count, interval, startTimestamp, message, err) 56 | fmt.Println("-------------------------------------------------------") 57 | } 58 | 59 | func TestParseTime(t *testing.T) { 60 | count, interval, startTimestamp, message, err := parseNoticeMessage("+st20221227 15:35,消息内容,3,15") 61 | fmt.Println(count, interval, startTimestamp, message, err) 62 | if err != nil { 63 | fmt.Println("失败11111", err.Error()) 64 | return 65 | } 66 | printMember(formatMember(count, interval, startTimestamp, message, "1111")) 67 | 68 | fmt.Println("---------------------------------------------------------------------") 69 | count, interval, startTimestamp, message, err = parseNoticeMessage("+s15:32,消息内容2222,3,15") 70 | fmt.Println(count, interval, startTimestamp, message, err) 71 | if err != nil { 72 | fmt.Println("失败222222", err.Error()) 73 | return 74 | } 75 | printMember(formatMember(count, interval, startTimestamp, message, "1111")) 76 | 77 | fmt.Println("---------------------------------------------------------------------") 78 | count, interval, startTimestamp, message, err = parseNoticeMessage("+s15:32,消息内容3333333,1,45") 79 | fmt.Println(count, interval, startTimestamp, message, err) 80 | if err != nil { 81 | fmt.Println("失败33333", err.Error()) 82 | return 83 | } 84 | printMember(formatMember(count, interval, startTimestamp, message, "1111")) 85 | } 86 | 87 | func printMember(members []*redis.Z) { 88 | for _, each := range members { 89 | fmt.Println(each.Member, each.Score) 90 | } 91 | } 92 | 93 | func TestZSetRedis(t *testing.T) { 94 | initAction(t) 95 | err := set("+s19:32,记得买辣椒,2,60", "697611681") 96 | fmt.Println(err) 97 | } 98 | 99 | func TestZGetRedis(t *testing.T) { 100 | initAction(t) 101 | msg, err := get(time.Now().AddDate(0, 0, 15).Unix()) 102 | fmt.Println(msg, err) 103 | } 104 | 105 | func TestZDelRedis(t *testing.T) { 106 | initAction(t) 107 | del() 108 | } 109 | -------------------------------------------------------------------------------- /comm/qweather/qweather.go: -------------------------------------------------------------------------------- 1 | package qweather 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "net/url" 7 | "strings" 8 | "time" 9 | 10 | jsoniter "github.com/json-iterator/go" 11 | "github.com/pkg/errors" 12 | "github.com/sirupsen/logrus" 13 | "go-wxbot/openwechat/comm/global" 14 | "go-wxbot/openwechat/comm/web" 15 | ) 16 | 17 | const QWeatherHOST = "https://geoapi.qweather.com" 18 | 19 | type QWeatherResp struct { 20 | Code string `json:"code"` 21 | Location []struct { 22 | Name string `json:"name"` 23 | Id string `json:"id"` 24 | Lat string `json:"lat"` 25 | Lon string `json:"lon"` 26 | Adm2 string `json:"adm2"` 27 | Adm1 string `json:"adm1"` 28 | Country string `json:"country"` 29 | Tz string `json:"tz"` 30 | UtcOffset string `json:"utcOffset"` 31 | IsDst string `json:"isDst"` 32 | Type string `json:"type"` 33 | Rank string `json:"rank"` 34 | FxLink string `json:"fxLink"` 35 | } `json:"location"` 36 | Refer struct { 37 | Sources []string `json:"sources"` 38 | License []string `json:"license"` 39 | } `json:"refer"` 40 | } 41 | 42 | func GetLocationID(cityName string) (id string, err error) { 43 | var ( 44 | respBody, surl string 45 | statusCode int 46 | params = url.Values{} 47 | qweatherResp QWeatherResp 48 | ) 49 | 50 | params.Set("location", cityName) 51 | params.Set("key", global.Conf.Keys.QweatherKey) 52 | params.Set("range", "cn") 53 | //params.Set("adm", cityName) 54 | 55 | surl = fmt.Sprintf("%s/v2/city/lookup?%s", QWeatherHOST, params.Encode()) 56 | respBody, statusCode, err = web.HTTP(surl, http.MethodGet, map[string]string{}, 10*time.Second, "") 57 | 58 | if err != nil { 59 | err = errors.Wrapf(err, "GetLocationID HTTP error") 60 | logrus.Error(err.Error()) 61 | return "", err 62 | } 63 | 64 | if statusCode != http.StatusOK { 65 | err = fmt.Errorf("GetLocationID HTTP status code error: %d", statusCode) 66 | logrus.Error(err.Error()) 67 | return "", err 68 | } 69 | 70 | err = jsoniter.Unmarshal([]byte(respBody), &qweatherResp) 71 | if err != nil { 72 | err = errors.Wrapf(err, "GetLocationID jsoniter.Unmarshal error") 73 | logrus.Error(err.Error()) 74 | return "", err 75 | } 76 | 77 | if qweatherResp.Code != "200" { 78 | err = fmt.Errorf("GetLocationID qweatherResp.Code error: %s", qweatherResp.Code) 79 | logrus.Error(err.Error()) 80 | return "", err 81 | } 82 | 83 | if len(qweatherResp.Location) == 0 { 84 | err = fmt.Errorf("GetLocationID qweatherResp.Location empyt") 85 | logrus.Error(err.Error()) 86 | return "", err 87 | } 88 | 89 | // 匹配 90 | for _, v := range qweatherResp.Location { 91 | if v.Name == cityName { 92 | return v.Id, nil 93 | } 94 | if v.Adm2 == cityName { 95 | return v.Id, nil 96 | } 97 | if strings.Contains(v.Adm1, cityName) { 98 | return v.Id, nil 99 | } 100 | } 101 | 102 | return "", fmt.Errorf("GetLocationID not found") 103 | } 104 | 105 | type QWeatherDetailResp struct { 106 | Code string `json:"code"` 107 | UpdateTime string `json:"updateTime"` 108 | FxLink string `json:"fxLink"` 109 | Now struct { 110 | ObsTime string `json:"obsTime"` 111 | Temp string `json:"temp"` 112 | FeelsLike string `json:"feelsLike"` 113 | Icon string `json:"icon"` 114 | Text string `json:"text"` 115 | Wind360 string `json:"wind360"` 116 | WindDir string `json:"windDir"` 117 | WindScale string `json:"windScale"` 118 | WindSpeed string `json:"windSpeed"` 119 | Humidity string `json:"humidity"` 120 | Precip string `json:"precip"` 121 | Pressure string `json:"pressure"` 122 | Vis string `json:"vis"` 123 | Cloud string `json:"cloud"` 124 | Dew string `json:"dew"` 125 | } `json:"now"` 126 | Refer struct { 127 | Sources []string `json:"sources"` 128 | License []string `json:"license"` 129 | } `json:"refer"` 130 | } 131 | 132 | func GetQWeatherDetail(cityID, cityName string) (detail string, err error) { 133 | var ( 134 | respBody, surl string 135 | statusCode int 136 | params = url.Values{} 137 | qweatherDetailResp QWeatherDetailResp 138 | ) 139 | 140 | params.Set("location", cityID) 141 | params.Set("key", global.Conf.Keys.QweatherKey) 142 | 143 | surl = fmt.Sprintf("https://devapi.qweather.com/v7/weather/now?%s", params.Encode()) 144 | respBody, statusCode, err = web.HTTP(surl, http.MethodGet, map[string]string{}, 10*time.Second, "") 145 | 146 | if err != nil { 147 | err = errors.Wrapf(err, "GetQWeatherDetail HTTP error") 148 | logrus.Error(err.Error()) 149 | return "", err 150 | } 151 | 152 | if statusCode != http.StatusOK { 153 | err = fmt.Errorf("GetQWeatherDetail HTTP status code error: %d", statusCode) 154 | logrus.Error(err.Error()) 155 | return "", err 156 | } 157 | 158 | err = jsoniter.Unmarshal([]byte(respBody), &qweatherDetailResp) 159 | if err != nil { 160 | err = errors.Wrapf(err, "GetQWeatherDetail jsoniter.Unmarshal error") 161 | logrus.Error(err.Error()) 162 | return "", err 163 | } 164 | 165 | if qweatherDetailResp.Code != "200" { 166 | err = fmt.Errorf("GetQWeatherDetail qweatherResp.Code error: %s", qweatherDetailResp.Code) 167 | logrus.Error(err.Error()) 168 | return "", err 169 | } 170 | 171 | detail = fmt.Sprintf(`%s今天天气,温度 %s 度,%s,%s %s 级,相对湿度 %s`, cityName, 172 | qweatherDetailResp.Now.Temp, qweatherDetailResp.Now.Text, 173 | qweatherDetailResp.Now.WindDir, qweatherDetailResp.Now.WindScale, 174 | qweatherDetailResp.Now.Humidity, 175 | ) 176 | 177 | return detail, nil 178 | } 179 | -------------------------------------------------------------------------------- /comm/ticker/schedule-notice.go: -------------------------------------------------------------------------------- 1 | package ticker 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "strconv" 7 | "strings" 8 | "time" 9 | 10 | "github.com/eatmoreapple/openwechat" 11 | redis2 "github.com/go-redis/redis/v8" 12 | "github.com/pkg/errors" 13 | uuid "github.com/satori/go.uuid" 14 | "github.com/sirupsen/logrus" 15 | "go-wxbot/openwechat/comm/global" 16 | "go-wxbot/openwechat/comm/redis" 17 | ) 18 | 19 | /** 20 | 提醒消息 21 | 格式1:+s15:32,消息内容,3,60 // 今天 15:31 提醒我「消息内容」,提醒 3 次每次间隔 60s 22 | 格式2: +st20221227 15:35,,消息内容,3,60 // 20221227日 15:35 提醒我「消息内容」,提醒 3 次每次间隔 60s 23 | 24 | 25 | */ 26 | 27 | var ( 28 | ctx = context.Background() 29 | ) 30 | 31 | const ( 32 | messageType1 = "+s" 33 | messageType2 = "+st" 34 | memberKey = "weixin:schedule.notice" 35 | ) 36 | 37 | func IsScheduleNotice(message string) bool { 38 | if strings.HasPrefix(message, messageType1) || strings.HasPrefix(message, messageType2) { 39 | return true 40 | } 41 | return false 42 | } 43 | 44 | func parseNoticeMessage(tf string) (count, interval int, startTimestamp int64, message string, err error) { 45 | if strings.HasPrefix(tf, messageType1) == false && strings.HasPrefix(tf, messageType2) == false { // 格式不正确 46 | return 0, 0, 0, "", fmt.Errorf("提醒事件格式错误,可以输入「事件提醒」关键字获取帮助。") 47 | } 48 | 49 | var ( 50 | msgType string 51 | ) 52 | 53 | if tf[0:2] == messageType1 && tf[0:3] != messageType2 { 54 | tf = tf[2:] 55 | msgType = messageType1 56 | } else if tf[0:3] == messageType2 { 57 | tf = tf[3:] 58 | msgType = messageType2 59 | } 60 | 61 | tf = strings.ReplaceAll(tf, ",", ",") 62 | tf = strings.ReplaceAll(tf, ":", ":") 63 | tfArr := strings.Split(tf, ",") 64 | if len(tfArr) < 2 { // 格式不正确 65 | return 0, 0, 0, "", fmt.Errorf("格式错误,可以输入「事件提醒」关键字获取帮助.") 66 | } 67 | 68 | stime, message, counts := tfArr[0], tfArr[1], "1" 69 | intervals := "" // 间隔 70 | if len(tfArr) >= 3 { 71 | counts = tfArr[2] 72 | } 73 | if len(tfArr) > 3 { 74 | intervals = tfArr[3] 75 | } 76 | 77 | interval, _ = strconv.Atoi(intervals) 78 | count, _ = strconv.Atoi(counts) 79 | 80 | if message == "" { 81 | return 0, 0, 0, "", fmt.Errorf("提醒消息不能为空") 82 | } 83 | 84 | if count <= 0 { 85 | return 0, 0, 0, "", fmt.Errorf("提醒次数最小为 1") 86 | } 87 | 88 | if count >= 1 && interval < 60 { 89 | count, interval = 1, 0 90 | } 91 | 92 | if msgType == messageType1 { 93 | t, err := time.ParseInLocation("2006-01-02 15:04", 94 | fmt.Sprintf("%s %s", time.Now().Format("2006-01-02"), stime), time.Local) 95 | if err != nil { 96 | return 0, 0, 0, "", fmt.Errorf("时间格式错误:%s,可以输入「事件提醒」关键字获取帮助。", tf) 97 | } 98 | startTimestamp = t.Unix() 99 | } else { 100 | t, err := time.ParseInLocation("20060102 15:04", stime, time.Local) 101 | if err != nil { 102 | return 0, 0, 0, "", fmt.Errorf("时间格式错误:%s,可以输入「事件提醒」关键字获取帮助。", tf) 103 | } 104 | startTimestamp = t.Unix() 105 | } 106 | 107 | if startTimestamp < time.Now().Unix()-60 { 108 | return 0, 0, 0, "", fmt.Errorf("时间错误,提醒时间至少要大与当前时间一分钟") 109 | } 110 | 111 | return count, interval, startTimestamp, message, nil 112 | } 113 | 114 | func formatMember(count, interval int, startTimestamp int64, message, userID string) []*redis2.Z { 115 | var ( 116 | members = make([]*redis2.Z, 0) 117 | ) 118 | 119 | for i := 0; i < count; i++ { 120 | tmp := startTimestamp + int64(i*interval) 121 | members = append(members, &redis2.Z{ 122 | Score: float64(tmp), 123 | Member: fmt.Sprintf("%s.placeholder.%s.placeholder.%s", message, userID, uuid.NewV4().String()), 124 | }) 125 | } 126 | 127 | return members 128 | } 129 | 130 | func set(tf, userID string) (replyMsg string) { 131 | var ( 132 | count, interval int 133 | startTimestamp int64 134 | message string 135 | members = make([]*redis2.Z, 0) 136 | err error 137 | ) 138 | 139 | count, interval, startTimestamp, message, err = parseNoticeMessage(tf) 140 | if err != nil { 141 | return err.Error() 142 | } 143 | 144 | members = formatMember(count, interval, startTimestamp, message, userID) 145 | 146 | redisClient := redis.GetRedis() 147 | if redisClient == nil { 148 | err = fmt.Errorf("get redis client err") 149 | logrus.Error(err.Error()) 150 | return err.Error() 151 | } 152 | 153 | err = redisClient.ZAdd(ctx, memberKey, members...).Err() 154 | if err != nil { 155 | err = errors.Wrapf(err, "redis zadd err") 156 | return err.Error() 157 | } 158 | 159 | replyMsg = fmt.Sprintf("提醒事件添加成功,会在 %s 提醒:%s", 160 | time.Unix(startTimestamp, 0).Format("2006-01-02 15:04:05"), message) 161 | if count > 1 && interval > 0 { 162 | replyMsg = fmt.Sprintf("%s。一共提醒 %d 次,每次间隔 %d 秒。", replyMsg, count, interval) 163 | } 164 | 165 | return replyMsg 166 | } 167 | 168 | func get(timestamp int64) (msg []string, err error) { 169 | redisClient := redis.GetRedis() 170 | if redisClient == nil { 171 | err = fmt.Errorf("get redis client err") 172 | logrus.Error(err.Error()) 173 | return nil, err 174 | } 175 | 176 | // zrangebyscore weixin:schedule.notice -inf (1672300344 177 | zRangeByScore := redisClient.ZRangeByScore(ctx, memberKey, &redis2.ZRangeBy{ 178 | Min: "-inf", 179 | Max: fmt.Sprintf("(%d", timestamp), 180 | }) 181 | if zRangeByScore.Err() != nil { 182 | err = errors.Wrapf(err, "get ZRangeByScore err") 183 | logrus.Error(err.Error()) 184 | return nil, err 185 | } 186 | 187 | return zRangeByScore.Val(), nil 188 | } 189 | 190 | func del() { 191 | var ( 192 | err error 193 | ) 194 | redisClient := redis.GetRedis() 195 | if redisClient == nil { 196 | err = fmt.Errorf("get redis client err") 197 | logrus.Error(err.Error()) 198 | return 199 | } 200 | 201 | zRemRangeByScore := redisClient.ZRemRangeByScore(ctx, memberKey, 202 | "0", fmt.Sprintf("(%d", time.Now().Unix())) 203 | if zRemRangeByScore.Err() != nil { 204 | err = errors.Wrapf(err, "schedule notice del err") 205 | logrus.Error(err.Error()) 206 | } 207 | 208 | return 209 | } 210 | 211 | func AddScheduleNotice(msg, userID string) (replyMsg string) { 212 | return set(msg, userID) 213 | } 214 | 215 | type Msg struct { 216 | Message string 217 | UID string 218 | } 219 | 220 | func ScheduleNoticeTicker() { 221 | var ( 222 | msg = make([]string, 0) 223 | err error 224 | doing bool 225 | ) 226 | for { 227 | select { 228 | case t := <-time.After(20 * time.Second): 229 | if doing { 230 | continue 231 | } 232 | msg, err = get(t.Unix()) 233 | if err != nil { 234 | doing = false 235 | continue 236 | } 237 | 238 | if len(msg) == 0 { 239 | doing = false 240 | continue 241 | } 242 | 243 | msgf := make([]Msg, 0) 244 | for _, each := range msg { 245 | tmpArr := strings.Split(each, ".placeholder.") 246 | if len(tmpArr) < 2 { 247 | continue 248 | } 249 | msgf = append(msgf, Msg{ 250 | Message: tmpArr[0], 251 | UID: tmpArr[1], 252 | }) 253 | } 254 | if len(msgf) == 0 { 255 | doing = false 256 | continue 257 | } 258 | 259 | // 先删除再发消息,发送时有网络请求会慢 260 | del() 261 | go sendNotice(msgf) 262 | 263 | doing = false 264 | } 265 | } 266 | } 267 | 268 | func sendNotice(msgf []Msg) { 269 | var ( 270 | err error 271 | idMap = make(map[string]string, 0) 272 | gMsgMap = make(map[string]*openwechat.Group) // TODO 暂时不考虑同一个群消息重复问题 273 | fMsgMap = make(map[string]*openwechat.Friend) 274 | ) 275 | 276 | for _, each := range msgf { 277 | idMap[each.UID] = each.Message 278 | } 279 | 280 | g, err := global.WxSelf.Groups(true) 281 | if err == nil { 282 | for _, each := range g { 283 | if message, ok := idMap[each.ID()]; ok { 284 | gMsgMap[message] = each 285 | } 286 | } 287 | } 288 | 289 | f, err := global.WxSelf.Friends(true) 290 | if err == nil { 291 | for _, each := range f { 292 | if message, ok := idMap[each.ID()]; ok { 293 | fMsgMap[message] = each 294 | } 295 | } 296 | } 297 | 298 | if len(fMsgMap) > 0 { 299 | for message, each := range fMsgMap { 300 | _, err = global.WxSelf.SendTextToFriend(each, message) 301 | if err != nil { 302 | err = errors.Wrapf(err, "sendNotice SendTextToFriend err") 303 | logrus.Error(err.Error()) 304 | } 305 | } 306 | } 307 | 308 | if len(gMsgMap) > 0 { 309 | for message, each := range gMsgMap { 310 | _, err = global.WxSelf.SendTextToGroup(each, message) 311 | if err != nil { 312 | err = errors.Wrapf(err, "sendNotice SendTextToGroup err") 313 | logrus.Error(err.Error()) 314 | } 315 | } 316 | } 317 | 318 | } 319 | -------------------------------------------------------------------------------- /comm/msg/msg.go: -------------------------------------------------------------------------------- 1 | package msg 2 | 3 | import ( 4 | "bytes" 5 | "encoding/base64" 6 | "fmt" 7 | "image/gif" 8 | "image/jpeg" 9 | "image/png" 10 | "log" 11 | "net/http" 12 | "net/url" 13 | "os" 14 | "strings" 15 | "time" 16 | 17 | "github.com/eatmoreapple/openwechat" 18 | jsoniter "github.com/json-iterator/go" 19 | "github.com/pkg/errors" 20 | "github.com/sirupsen/logrus" 21 | "go-wxbot/openwechat/comm/funcs" 22 | "go-wxbot/openwechat/comm/global" 23 | "go-wxbot/openwechat/comm/image" 24 | "go-wxbot/openwechat/comm/qweather" 25 | "go-wxbot/openwechat/comm/tian" 26 | "go-wxbot/openwechat/comm/ticker" 27 | "go-wxbot/openwechat/comm/web" 28 | ) 29 | 30 | func HandleMsg(msg *openwechat.Message) { 31 | if msg.IsSendBySelf() { // 自己的消息不处理 32 | return 33 | } 34 | 35 | var ( 36 | contentText = "" 37 | err error 38 | sender *openwechat.User 39 | ) 40 | 41 | sender, err = msg.Sender() 42 | if err != nil { 43 | err = errors.Wrapf(err, "%s获取发送人信息失败", global.Conf.Keys.BotName) 44 | msg.ReplyText(err.Error()) 45 | return 46 | } 47 | 48 | if msg.IsText() { // 处理文本消息 49 | contentText = trimMsgContent(msg.Content) 50 | if contentText != "打赏" && contentText != "圣诞帽" && contentText != "程序员鼓励师" { 51 | reply := contextTextBypass(contentText, sender.ID()) 52 | reply = strings.TrimLeft(reply, "\n") 53 | reply = strings.TrimRight(reply, "\n") 54 | _, err = msg.ReplyText(reply) 55 | if err != nil { 56 | err = errors.Wrapf(err, "reply text msg err,contentText: %s", contentText) 57 | logrus.Error(err.Error()) 58 | } 59 | return 60 | } 61 | 62 | handleTextReplyBypass(msg, contentText) 63 | } 64 | } 65 | 66 | func handleTextReplyBypass(msg *openwechat.Message, txt string) { 67 | if txt == "打赏" { 68 | img, err := os.Open("reword.png") 69 | defer img.Close() 70 | if err != nil { 71 | err = errors.Wrapf(err, "reword open file err") 72 | logrus.Error(err.Error()) 73 | _, err = msg.ReplyText("学雷锋,视钱财如粪土,不用打赏。") 74 | handleErr(err) 75 | return 76 | } 77 | 78 | _, err = msg.ReplyImage(img) 79 | handleErr(err) 80 | return 81 | } 82 | 83 | if txt == "圣诞帽" { 84 | handleChristmasHatMsg(msg) 85 | return 86 | } 87 | 88 | if txt == "程序员鼓励师" { 89 | Encourage(msg) 90 | return 91 | } 92 | 93 | } 94 | 95 | func Encourage(msg *openwechat.Message) { 96 | imgURL, err := image.GetImage() 97 | if err != nil { 98 | msg.ReplyText("鼓励师今天不在家,不要摸鱼,赶紧干活~") 99 | return 100 | } 101 | 102 | savePath, err := image.SaveEncourageImg(imgURL) 103 | if err != nil { 104 | msg.ReplyText("鼓励师今天不在家,BUG 虽好,但不要贪多哦~") 105 | return 106 | } 107 | defer os.Remove(savePath) 108 | 109 | img, err := os.Open(savePath) 110 | if err != nil { 111 | msg.ReplyText("鼓励师今天不在家,么么哒~") 112 | return 113 | } 114 | defer img.Close() 115 | 116 | msg.ReplyImage(img) 117 | } 118 | 119 | func handleChristmasHatMsg(msg *openwechat.Message) { 120 | var ( 121 | sender *openwechat.User 122 | err error 123 | avatarPath, 124 | avatarBase64, base64Hat, base64SaveName string 125 | ) 126 | 127 | // 保存用户的头像 128 | sender, err = msg.SenderInGroup() 129 | if err != nil { 130 | err = errors.Wrapf(err, "SenderInGroup err") 131 | logrus.Error(err.Error()) 132 | msg.ReplyText(fmt.Sprintf("%s处理不过来了,过会儿再来生成圣诞帽吧!", global.Conf.Keys.BotName)) 133 | return 134 | } 135 | 136 | avatarPath = avatarSavePath(sender.NickName) 137 | logrus.Debugf("avatarPath: %s ", avatarPath) 138 | err = sender.SaveAvatar(avatarPath) 139 | if err != nil { 140 | err = errors.Wrapf(err, "SaveAvatar err:%s", sender.NickName) 141 | logrus.Error(err.Error()) 142 | msg.ReplyText(fmt.Sprintf("%s获取%s的头像失败了,你的头像可能是被小马哥加密了哦!!", 143 | global.Conf.Keys.BotName, sender.NickName)) 144 | return 145 | } 146 | 147 | // 头像转成 base64 148 | avatarBase64, err = funcs.Img2base64(avatarPath) 149 | if err != nil { 150 | err = errors.Wrapf(err, "Img2base64 err:%s,path: %s", sender.NickName, avatarPath) 151 | logrus.Error(err.Error()) 152 | msg.ReplyText(fmt.Sprintf("%s处理不过来了,过会儿再来生成圣诞帽吧!", global.Conf.Keys.BotName)) 153 | return 154 | } 155 | 156 | logrus.Debugf("Img2base64 avatarBase64: %s \n", avatarBase64) 157 | 158 | // 调用接口把 base64 头像加个圣诞帽 159 | base64Hat, err = AvatarAddChristmasHat(avatarBase64) 160 | if err != nil { 161 | msg.ReplyText(fmt.Sprintf("%s处理不过来了,过会儿再来生成圣诞帽吧!", global.Conf.Keys.BotName)) 162 | return 163 | } 164 | 165 | // 将返回的图片存到本地 166 | base64SaveName = fmt.Sprintf("%s_base64", url.QueryEscape(sender.NickName)) 167 | filename, err := SaveImageToDisk(base64SaveName, base64Hat) 168 | if err != nil { 169 | err = errors.Wrapf(err, "saveImageToDisk err:%s,path: %s", sender.NickName, avatarPath) 170 | logrus.Error(err.Error()) 171 | msg.ReplyText(fmt.Sprintf("%s处理不过来了,过会儿再来生成圣诞帽吧!", global.Conf.Keys.BotName)) 172 | return 173 | } 174 | 175 | // 发送加了圣诞帽的头像 176 | avatarBase64Path := fmt.Sprintf("%s/avatar/%s", funcs.Wd(), filename) 177 | img, err := os.Open(avatarBase64Path) 178 | if err != nil { 179 | err = errors.Wrapf(err, "avatarBase64Path open err:%s,path: %s", sender.NickName, avatarPath) 180 | logrus.Error(err.Error()) 181 | msg.ReplyText(fmt.Sprintf("%s处理不过来了,过会儿再来生成圣诞帽吧!", global.Conf.Keys.BotName)) 182 | return 183 | } 184 | defer img.Close() 185 | 186 | _, err = msg.ReplyImage(img) 187 | if err != nil { 188 | err = errors.Wrapf(err, "ReplyImage err") 189 | logrus.Error(err.Error()) 190 | } 191 | } 192 | 193 | type ChristmasHatReq struct { 194 | Base64 string `json:"base64"` 195 | } 196 | 197 | type AvatarResp struct { 198 | Success bool `json:"success"` 199 | Data string `json:"data"` 200 | Msg string `json:"msg"` 201 | } 202 | 203 | func AvatarAddChristmasHat(avatarBase64 string) (hatBase64 string, err error) { 204 | var ( 205 | req ChristmasHatReq 206 | surl string 207 | reqBytes = make([]byte, 0) 208 | respBody string 209 | statusCode int 210 | hatRet AvatarResp 211 | ) 212 | req.Base64 = avatarBase64 213 | surl = fmt.Sprintf("%s", global.Conf.Keys.ChristmasHatURL) 214 | reqBytes, _ = jsoniter.Marshal(req) 215 | 216 | respBody, statusCode, err = web.HTTP(surl, http.MethodGet, map[string]string{ 217 | "Content-Type": "application/json; charset=utf-8", 218 | }, 30*time.Second, string(reqBytes)) 219 | if err != nil { 220 | err = errors.Wrapf(err, "avatarAddChristmasHat http err") 221 | logrus.Error(err.Error()) 222 | return "", err 223 | } 224 | 225 | if statusCode != http.StatusOK { 226 | return "", fmt.Errorf("avatarAddChristmasHat statusCode not 200 is %d", statusCode) 227 | } 228 | 229 | err = jsoniter.Unmarshal([]byte(respBody), &hatRet) 230 | if err != nil { 231 | err = errors.Wrapf(err, "avatarAddChristmasHat Unmarshal err") 232 | logrus.Error(err.Error()) 233 | return "", err 234 | } 235 | 236 | if hatRet.Success == false { 237 | return "", fmt.Errorf("avatarAddChristmasHat success not true is %t", hatRet.Success) 238 | } 239 | 240 | return hatRet.Data, nil 241 | } 242 | 243 | func avatarSavePath(nickname string) (path string) { 244 | return fmt.Sprintf("%s/avatar/%s.png", funcs.Wd(), url.QueryEscape(nickname)) 245 | } 246 | 247 | func handleErr(err error, grep ...string) { 248 | prefix := "" 249 | if len(grep) > 0 { 250 | for _, each := range grep { 251 | prefix = fmt.Sprintf("%s %s", prefix, each) 252 | } 253 | } 254 | if err != nil { 255 | logrus.Errorf("%s [%s]", prefix, err.Error()) 256 | } 257 | } 258 | 259 | func trimMsgContent(content string) string { 260 | content = strings.TrimLeft(content, " ") 261 | content = strings.TrimRight(content, " ") 262 | return content 263 | } 264 | 265 | func SaveImageToDisk(saveName, data string) (filename string, err error) { 266 | idx := strings.Index(data, ";base64,") 267 | if idx < 0 { 268 | return "", fmt.Errorf("InvalidImage") 269 | } 270 | ImageType := data[11:idx] 271 | log.Println(ImageType) 272 | 273 | unbased, err := base64.StdEncoding.DecodeString(data[idx+8:]) 274 | if err != nil { 275 | return "", fmt.Errorf("Cannot decode b64") 276 | } 277 | r := bytes.NewReader(unbased) 278 | switch ImageType { 279 | case "png": 280 | im, err := png.Decode(r) 281 | if err != nil { 282 | return "", fmt.Errorf("Bad png") 283 | } 284 | 285 | filename = fmt.Sprintf("%s.png", saveName) 286 | f, err := os.OpenFile(fmt.Sprintf("%s/avatar/%s", funcs.Wd(), filename), os.O_WRONLY|os.O_CREATE, 0777) 287 | if err != nil { 288 | return "", fmt.Errorf("Cannot open file") 289 | } 290 | 291 | png.Encode(f, im) 292 | case "jpeg": 293 | im, err := jpeg.Decode(r) 294 | if err != nil { 295 | return "", fmt.Errorf("Bad jpeg") 296 | } 297 | 298 | filename = fmt.Sprintf("%s.jpeg", saveName) 299 | f, err := os.OpenFile(fmt.Sprintf("%s/avatar/%s", funcs.Wd(), filename), os.O_WRONLY|os.O_CREATE, 0777) 300 | if err != nil { 301 | return "", fmt.Errorf("Cannot open file") 302 | } 303 | 304 | jpeg.Encode(f, im, nil) 305 | case "gif": 306 | im, err := gif.Decode(r) 307 | if err != nil { 308 | return "", fmt.Errorf("Bad gif") 309 | } 310 | 311 | filename = fmt.Sprintf("%s.gif", saveName) 312 | f, err := os.OpenFile(fmt.Sprintf("%s/avatar/%s", funcs.Wd(), filename), os.O_WRONLY|os.O_CREATE, 0777) 313 | if err != nil { 314 | return "", fmt.Errorf("Cannot open file") 315 | } 316 | 317 | gif.Encode(f, im, nil) 318 | } 319 | 320 | return filename, nil 321 | } 322 | 323 | func contextTextBypass(txt, userID string) (retMsg string) { 324 | var ( 325 | err error 326 | ) 327 | if txt == "菜单" { 328 | return ` 329 | 天气查询,如:泾县天气。 330 | 菜谱查询,如: 红烧肉菜谱,红烧肉做法。 331 | 输入【打赏】打赏卫小兵。 332 | 输入【程序员鼓励师】收到程序员鼓励师的回复。 333 | 输入【事件提醒】获取设置事件提醒的格式。 334 | 输入【毒鸡汤】关键字回复毒鸡汤。 335 | 输入【圣诞帽】关键字回复简单处理后的圣诞帽头像,个别用户获取不到头像信息。 336 | 输入【英语一句话】关键字回复一句学习英语。 337 | ` 338 | } 339 | 340 | if txt == "天气" { 341 | return "支持天气查询,如: 泾县天气。" 342 | } 343 | 344 | if txt == "菜谱" || txt == "做法" { 345 | return "支持菜谱查询,如: 红烧肉菜谱,红烧肉做法。" 346 | } 347 | 348 | // 天气处理 349 | if strings.HasSuffix(txt, "天气") { 350 | return handleWeatherMsg(txt) 351 | } 352 | 353 | // 毒鸡汤处理 354 | if txt == "毒鸡汤" { 355 | retMsg, err = tian.GetMessage(tian.C_dujitang) 356 | if err != nil { 357 | logrus.Error(err.Error()) 358 | return "" 359 | } 360 | return retMsg 361 | } 362 | 363 | // 菜谱处理 364 | if strings.HasSuffix(txt, "菜谱") || strings.HasSuffix(txt, "做法") { 365 | return handleCookbookMsg(txt) 366 | } 367 | 368 | // 英语一句话 369 | if txt == "英语一句话" { 370 | retMsg, err = tian.GetMessage(tian.C_englishSentence) 371 | if err != nil { 372 | logrus.Error(err.Error()) 373 | return "" 374 | } 375 | return retMsg 376 | } 377 | 378 | // 事件提醒 379 | if txt == "事件提醒" { 380 | return ` 381 | 格式0:+s15:32,消息内容 382 | 格式0说明:今天 15:32 提醒我「消息内容」 383 | 384 | 格式1:+s15:32,消息内容,3,60 385 | 格式1说明:今天 15:32 提醒我「消息内容」,提醒 3 次每次间隔 60s 386 | 387 | 格式2: +st20221227 15:35,消息内容 388 | 格式2说明:20221227 日 15:35 提醒我「消息内容」。注意此格式的日期和时间中间的空格不能丢 389 | 390 | 格式3: +st20221227 15:35,消息内容,3,60 391 | 格式3说明:20221227 日 15:35 提醒我「消息内容」,提醒 3 次每次间隔 60s。注意此格式的日期和时间中间的空格不能丢 392 | ` 393 | } 394 | 395 | // todo 其他的一些 396 | if ticker.IsScheduleNotice(txt) { 397 | return ticker.AddScheduleNotice(txt, userID) 398 | } 399 | 400 | return "" 401 | } 402 | 403 | func reword() (img *os.File, err error) { 404 | img, err = os.Open("reword.png") 405 | defer img.Close() 406 | return img, err 407 | } 408 | 409 | func handleCookbookMsg(txt string) (cookbook string) { 410 | var ( 411 | err error 412 | ) 413 | originTxt := txt 414 | 415 | txt = strings.ReplaceAll(txt, "做法", "") 416 | txt = strings.ReplaceAll(txt, "菜谱", "") 417 | cookbook, err = tian.GetMessage(tian.C_caipu, txt) 418 | if err != nil && err != tian.ErrNotfoundCaiPu { 419 | logrus.Error(err.Error()) 420 | return "" 421 | } 422 | 423 | if err == tian.ErrNotfoundCaiPu { 424 | return fmt.Sprintf("暂未找到%s,请重新输入关键字查询", originTxt) 425 | } 426 | 427 | return cookbook 428 | } 429 | 430 | func handleWeatherMsg(txt string) (formatWeatherMsg string) { 431 | var ( 432 | err error 433 | cityID string 434 | ) 435 | originTxt := txt 436 | txt = strings.ReplaceAll(txt, "天气", "") 437 | cityID, err = qweather.GetLocationID(txt) 438 | if err != nil { 439 | err = errors.Wrapf(err, "handleWeatherMsg GetFormatWeatherMessage err") 440 | logrus.Error(err.Error()) 441 | return fmt.Sprintf("(1)未查询到%s,请检查城市输入是否正确,当前只支持到区/县一级的城市查询,如:泾县天气,新市区天气。", originTxt) 442 | } 443 | 444 | formatWeatherMsg, err = qweather.GetQWeatherDetail(cityID, txt) 445 | if err != nil { 446 | err = errors.Wrapf(err, "handleWeatherMsg GetFormatWeatherMessage err") 447 | logrus.Error(err.Error()) 448 | return fmt.Sprintf("(2)未查询到%s,请检查城市输入是否正确,当前只支持到区/县一级的城市查询,如:泾县天气,新市区天气。", originTxt) 449 | } 450 | 451 | return formatWeatherMsg 452 | } 453 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/Lofanmi/chinese-calendar-golang v0.0.0-20211214151323-ef5cb443e55e h1:hWFKGrEqJI14SqwK7GShkaTV1NtQzMZFLFasITmH/LI= 2 | github.com/Lofanmi/chinese-calendar-golang v0.0.0-20211214151323-ef5cb443e55e/go.mod h1:nG6VxnU5//MJzjwFAYQzFcrVdm+3RGD8NwO9riziV8E= 3 | github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE= 4 | github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 5 | github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE= 6 | github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= 7 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8= 8 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= 9 | github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8= 10 | github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= 11 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 12 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 13 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 14 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= 15 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= 16 | github.com/eatmoreapple/openwechat v1.4.5-0.20230911013309-68d6f5444502 h1:KbLMdjk5eyy3rV7ZVBwyC3KGw05YaMoeySRopvK89Tg= 17 | github.com/eatmoreapple/openwechat v1.4.5-0.20230911013309-68d6f5444502/go.mod h1:h4m2N8m0XsUKlm7UR8BUGkV89GNuKHCnlGV3J8n9Mpw= 18 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 19 | github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= 20 | github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= 21 | github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI= 22 | github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo= 23 | github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 h1:p104kn46Q8WdvHunIJ9dAyjPVtrBPhSr3KT2yUst43I= 24 | github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= 25 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 26 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 27 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 28 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 29 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 30 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 31 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 32 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 33 | github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= 34 | github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= 35 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 36 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 37 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 38 | github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= 39 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 40 | github.com/google/gofuzz v1.0.0 h1:A8PeW59pxE9IoFRqBp37U+mSNaQoZ46F1f0f863XSXw= 41 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 42 | github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 h1:yAJXTCF9TqKcTiHJAE8dj7HMvPfh66eeA2JYW7eFpSE= 43 | github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= 44 | github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= 45 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 46 | github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639 h1:mV02weKRL81bEnm8A0HT1/CAelMQDBuQIfLw8n+d6xI= 47 | github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= 48 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 49 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 50 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= 51 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 52 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 53 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 54 | github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= 55 | github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= 56 | github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= 57 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 58 | github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= 59 | github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= 60 | github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= 61 | github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= 62 | github.com/onsi/ginkgo/v2 v2.0.0 h1:CcuG/HvWNkkaqCUpJifQY8z7qEMBJya6aLPx6ftGyjQ= 63 | github.com/onsi/ginkgo/v2 v2.0.0/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c= 64 | github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= 65 | github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= 66 | github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= 67 | github.com/onsi/gomega v1.18.1 h1:M1GfJqGRrBrrGGsbxzV5dqM2U2ApXefZCQpkukxYRLE= 68 | github.com/onsi/gomega v1.18.1/go.mod h1:0q+aL8jAiMXy9hbwj2mr5GziHiwhAIQpFmmtT5hitRs= 69 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 70 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 71 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 72 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 73 | github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww= 74 | github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= 75 | github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= 76 | github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= 77 | github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0= 78 | github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M= 79 | github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4= 80 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 81 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 82 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 83 | github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= 84 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 85 | github.com/yuin/goldmark v1.2.1 h1:ruQGxdhGHe7FWOJPT0mKs5+pD2Xs1Bm/kdGlHO04FmM= 86 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 87 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 88 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 89 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI= 90 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 91 | golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4= 92 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 93 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 94 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 95 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 96 | golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 97 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 98 | golang.org/x/net v0.0.0-20210428140749-89ef3d95e781 h1:DzZ89McO9/gWPsQXS/FVKAlG02ZjaQ6AlZRBimEYOd0= 99 | golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= 100 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 101 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 102 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9 h1:SQFwaSi55rU7vdNs9Yr0Z324VNlrF+0wMqRXT4St8ck= 103 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 104 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 105 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 106 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 107 | golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 108 | golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 109 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 110 | golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 111 | golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 112 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 113 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 114 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 115 | golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 116 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 117 | golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e h1:fLOSk5Q00efkSvAm+4xcoXD+RRmLmmulPn5I3Y9F2EM= 118 | golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 119 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E= 120 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 121 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 122 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 123 | golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= 124 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 125 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 126 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 127 | golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e h1:4nW4NLDYnU28ojHaHO8OVxFHk/aQ33U01a9cjED+pzE= 128 | golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 129 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 130 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 131 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 132 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= 133 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 134 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 135 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 136 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 137 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 138 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 139 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 140 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 141 | google.golang.org/protobuf v1.26.0 h1:bxAC2xTBsZGibn2RTntX0oH50xLsqy1OxA9tTL3p/lk= 142 | google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 143 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 144 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 145 | gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= 146 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 147 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= 148 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 149 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 150 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 151 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 152 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 153 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 154 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= 155 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 156 | --------------------------------------------------------------------------------