├── .gitignore ├── .idea ├── misc.xml ├── vcs.xml ├── .gitignore ├── sqldialects.xml ├── modules.xml └── wechat-remind-bot.iml ├── app.ini ├── go.mod ├── README.md ├── bcmd ├── help.go ├── ding.go ├── myinfo.go ├── remindme.go ├── money.go ├── closecheckin.go ├── notremind.go ├── checkin.go ├── opencheckin.go └── base.go ├── main.go ├── vars └── vars.go ├── bcron ├── eat_remind.go └── remind.go ├── models ├── checkin.go ├── notremind.go ├── room.go └── models.go ├── remind.sql ├── startup └── vars.go └── go.sum /.gitignore: -------------------------------------------------------------------------------- 1 | remind 2 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Datasource local storage ignored files 5 | /dataSources/ 6 | /dataSources.local.xml 7 | # Editor-based HTTP Client requests 8 | /httpRequests/ 9 | -------------------------------------------------------------------------------- /app.ini: -------------------------------------------------------------------------------- 1 | [app] 2 | CronSpec = * 7-23 * * * 3 | #需要提醒吃饭并分享小程序的群列表 4 | #EatRemindRoomIds = 5 | #EatRemindCronSpec = 30 11,18 * * * 6 | 7 | [database] 8 | User = root 9 | Password =root 10 | Host = 127.0.0.1:3306 11 | Name = remind 12 | -------------------------------------------------------------------------------- /.idea/sqldialects.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/wechat-remind-bot.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/dchaofei/wechat-remind-bot 2 | 3 | go 1.14 4 | 5 | require ( 6 | github.com/go-sql-driver/mysql v1.5.0 7 | github.com/jinzhu/gorm v1.9.14 8 | github.com/robfig/cron/v3 v3.0.1 9 | github.com/smartystreets/goconvey v1.6.4 // indirect 10 | github.com/wechaty/go-wechaty v0.3.0 11 | gopkg.in/ini.v1 v1.57.0 12 | ) 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 一个微信提醒机器人、可以用于群聊提醒、签到、打卡等 2 | 3 | # 如何使用 4 | 1. 打开 mysql,执行 `remind.sql` 文件里的 sql 5 | 2. 配置 app.ini 里的数据库连接 6 | 3. 运行程序 `WECHATY_PUPPET_SERVICE_TOKEN=xxx go run main.go` 7 | 4. 把机器人拉到群里,回复 `#开启打卡`, 机器人会按照 app.ini 的 `CronSpec` 设置的时间间隔定时提醒群里未打卡的成员。 8 | 9 | # 支持命令 10 | - $以后不要提醒我 11 | - $关闭打卡 12 | - $帮助 13 | - $开启打卡 14 | - $提醒我 15 | - $打卡 16 | - $外卖红包 17 | - ...... 18 | -------------------------------------------------------------------------------- /bcmd/help.go: -------------------------------------------------------------------------------- 1 | package bcmd 2 | 3 | import "github.com/wechaty/go-wechaty/wechaty/user" 4 | 5 | const HelpCmdName = "$帮助" 6 | 7 | func init() { 8 | registerHandle(HelpCmdName, new(help)) 9 | } 10 | 11 | type help struct{} 12 | 13 | func (h *help) Handle(message *user.Message) { 14 | s := "" 15 | for _, name := range GetHandlerNames() { 16 | s += name + "\n" 17 | } 18 | s = "支持的命令:\n\n" + s 19 | message.Say(s) 20 | } 21 | -------------------------------------------------------------------------------- /bcmd/ding.go: -------------------------------------------------------------------------------- 1 | package bcmd 2 | 3 | import ( 4 | "github.com/wechaty/go-wechaty/wechaty/user" 5 | "log" 6 | ) 7 | 8 | const DingCmdName = "$ding" 9 | 10 | func init() { 11 | registerHandle(DingCmdName, new(ding)) 12 | } 13 | 14 | type ding struct{} 15 | 16 | func (d *ding) Handle(message *user.Message) { 17 | _, err := message.Say("dong") 18 | if err != nil { 19 | log.Println("ding handler exception:", err) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/dchaofei/wechat-remind-bot/models" 5 | "github.com/dchaofei/wechat-remind-bot/startup" 6 | "log" 7 | "os" 8 | "os/signal" 9 | "syscall" 10 | ) 11 | 12 | func main() { 13 | startup.SetupVars() 14 | models.Setup() 15 | var quitSig = make(chan os.Signal) 16 | signal.Notify(quitSig, os.Interrupt, syscall.SIGTERM) 17 | select { 18 | case <-quitSig: 19 | log.Fatal("exit.by.signal") 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /vars/vars.go: -------------------------------------------------------------------------------- 1 | package vars 2 | 3 | import ( 4 | "github.com/robfig/cron/v3" 5 | "github.com/wechaty/go-wechaty/wechaty" 6 | ) 7 | 8 | type App struct { 9 | CronSpec string 10 | EatRemindRoomIds []string 11 | EatRemindCronSpec string 12 | } 13 | 14 | type Database struct { 15 | User string 16 | Password string 17 | Host string 18 | Name string 19 | } 20 | 21 | var ( 22 | AppSetting = &App{} 23 | DataBaseSetting = &Database{} 24 | Bot *wechaty.Wechaty 25 | CronInstance *cron.Cron 26 | ) 27 | -------------------------------------------------------------------------------- /bcmd/myinfo.go: -------------------------------------------------------------------------------- 1 | package bcmd 2 | 3 | import ( 4 | "fmt" 5 | "github.com/wechaty/go-wechaty/wechaty/user" 6 | ) 7 | 8 | const MyInfoCmdName = "$我的信息" 9 | 10 | func init() { 11 | registerHandle(MyInfoCmdName, new(myInfo)) 12 | } 13 | 14 | type myInfo struct{} 15 | 16 | func (m *myInfo) Handle(message *user.Message) { 17 | room := message.Room() 18 | if room == nil { 19 | message.Say("该功能仅支持群聊") 20 | return 21 | } 22 | from := message.From() 23 | name := from.Name() 24 | id := from.ID() 25 | room.Say(fmt.Sprintf("\nwx_id:%s\n昵称:%s", id, name), from) 26 | } 27 | -------------------------------------------------------------------------------- /bcmd/remindme.go: -------------------------------------------------------------------------------- 1 | package bcmd 2 | 3 | import ( 4 | "github.com/dchaofei/wechat-remind-bot/models" 5 | "github.com/wechaty/go-wechaty/wechaty/user" 6 | ) 7 | 8 | const RemindMeCmdName = "$提醒我" 9 | 10 | func init() { 11 | registerHandle(RemindMeCmdName, new(remindMe)) 12 | } 13 | 14 | type remindMe struct{} 15 | 16 | func (n *remindMe) Handle(message *user.Message) { 17 | room := message.Room() 18 | if room == nil { 19 | message.Say("该功能仅支持群聊") 20 | return 21 | } 22 | roomModel, err := models.GetRoom(room.ID()) 23 | if err != nil { 24 | message.Say(err.Error()) 25 | return 26 | } 27 | if roomModel == nil { 28 | return 29 | } 30 | from := message.From() 31 | err = models.DeleteBy(roomModel.ID, from.ID()) 32 | if err != nil { 33 | room.Say("操作失败: "+err.Error(), from) 34 | return 35 | } 36 | room.Say("操作成功", from) 37 | } 38 | -------------------------------------------------------------------------------- /bcron/eat_remind.go: -------------------------------------------------------------------------------- 1 | package bcron 2 | 3 | import ( 4 | "fmt" 5 | "github.com/dchaofei/wechat-remind-bot/bcmd" 6 | "github.com/dchaofei/wechat-remind-bot/vars" 7 | "log" 8 | ) 9 | 10 | func EatRemind() { 11 | for _, roomId := range vars.AppSetting.EatRemindRoomIds { 12 | go func(roomId string) { 13 | defer func() { 14 | if err := recover(); err != nil { 15 | log.Printf("发送 eatRemind {%s} panic: %v", roomId, err) 16 | } 17 | }() 18 | fmt.Println("执行中") 19 | room := vars.Bot.Room().Load(roomId) 20 | _, err := room.Say("兄弟姐们儿,点外卖的抓紧时间啦! #小程序:外卖券领取plus\n\n仅在午饭晚饭的时候会有此提醒") 21 | if err != nil { 22 | log.Printf("发送消息失败: {%s} err: %s", roomId, err) 23 | return 24 | } 25 | if _, err := room.Say(bcmd.MiniProgram); err != nil { 26 | log.Printf("发送小程序失败: {%s} err: %s", roomId, err) 27 | } 28 | }(roomId) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /models/checkin.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "github.com/jinzhu/gorm" 5 | "time" 6 | ) 7 | 8 | type Checkin struct { 9 | ID int `gorm:"primary_key" json:"id"` 10 | WxID string `json:"wx_id"` 11 | RoomID int64 `json:"room_id"` 12 | Date string `json:"date"` 13 | CreatedOn *time.Time `json:"created_on"` 14 | } 15 | 16 | func ExistCheckinBy(wxID, roomID, date interface{}) (bool, error) { 17 | var checkin Checkin 18 | err := db.Select("id").Where("wx_id = ? AND room_id = ? AND date = ?", wxID, roomID, date).First(&checkin).Error 19 | if err != nil && err != gorm.ErrRecordNotFound { 20 | return false, err 21 | } 22 | 23 | if checkin.ID > 0 { 24 | return true, nil 25 | } 26 | return false, nil 27 | } 28 | 29 | func GetAlreadyCheckinWxIdsBy(roomID, date interface{}) ([]string, error) { 30 | var ids []string 31 | return ids, db.Model(&Checkin{}).Where("room_id = ? and date = ?", roomID, date).Pluck("wx_id", &ids).Error 32 | } 33 | 34 | func AddCheckIn(checkin *Checkin) error { 35 | return db.Create(checkin).Error 36 | } 37 | -------------------------------------------------------------------------------- /bcmd/money.go: -------------------------------------------------------------------------------- 1 | package bcmd 2 | 3 | import ( 4 | "github.com/wechaty/go-wechaty/wechaty-puppet/schemas" 5 | "github.com/wechaty/go-wechaty/wechaty/user" 6 | "log" 7 | ) 8 | 9 | const MoneyCmdName = "$外卖红包" 10 | 11 | func init() { 12 | m :=new(money) 13 | registerHandle(MoneyCmdName, m, 98) 14 | registerHandle("$红包", m, 99) 15 | } 16 | 17 | type money struct{} 18 | 19 | var MiniProgram = user.NewMiniProgram(&schemas.MiniProgramPayload{ 20 | Appid: "wx68b30d5e22041892", 21 | Description: "", 22 | PagePath: "pages/index/index.html", 23 | ThumbUrl: "3062020100045630540201000204996066a702032f802902042049110e02045fbf9ad3042f6175706170706d73675f613033383638643431626632356535635f313630363339323533313431395f3836343431340204010800030201000405004c56f900", 24 | Title: "这里的外卖最便宜,因为有券", 25 | Username: "gh_915306feedc7@app", 26 | ThumbKey: "4c3c5c93a3ff093ce9d8f740767801ff", 27 | }) 28 | 29 | func (h *money) Handle(message *user.Message) { 30 | _, err := message.Say(MiniProgram) 31 | if err != nil { 32 | log.Println("money handler exception:", err) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /bcmd/closecheckin.go: -------------------------------------------------------------------------------- 1 | package bcmd 2 | 3 | import ( 4 | "github.com/dchaofei/wechat-remind-bot/models" 5 | "github.com/wechaty/go-wechaty/wechaty/user" 6 | ) 7 | 8 | const CloseCheckInCmdName = "$关闭打卡" 9 | 10 | func init() { 11 | registerHandle(CloseCheckInCmdName, new(closeCheckIn)) 12 | } 13 | 14 | type closeCheckIn struct{} 15 | 16 | func (o *closeCheckIn) Handle(message *user.Message) { 17 | room := message.Room() 18 | if room == nil { 19 | message.Say("该功能仅支持群聊") 20 | return 21 | } 22 | roomModel, err := models.GetRoom(room.ID()) 23 | if err != nil { 24 | message.Say(err.Error()) 25 | return 26 | } 27 | if roomModel == nil { 28 | return 29 | } 30 | from := message.From() 31 | if from.ID() != roomModel.AdminWxID { 32 | room.Say("只有当前群的 bot 管理员才能操作此功能", from) 33 | return 34 | } 35 | if roomModel.Status != models.OpenCheckinStatus { 36 | room.Say("打卡已关闭,请不要重复关闭", from) 37 | return 38 | } 39 | err = models.UpdateRoomStatus(roomModel.ID, models.CloseCheckinStatus) 40 | if err != nil { 41 | room.Say("关闭打卡失败: "+err.Error(), from) 42 | return 43 | } 44 | room.Say("打卡已关闭", from) 45 | } 46 | -------------------------------------------------------------------------------- /bcmd/notremind.go: -------------------------------------------------------------------------------- 1 | package bcmd 2 | 3 | import ( 4 | "github.com/dchaofei/wechat-remind-bot/models" 5 | "github.com/wechaty/go-wechaty/wechaty/user" 6 | ) 7 | 8 | const NotRemindCmdName = "$以后不要提醒我" 9 | 10 | func init() { 11 | registerHandle(NotRemindCmdName, new(notRemind)) 12 | } 13 | 14 | type notRemind struct{} 15 | 16 | func (n *notRemind) Handle(message *user.Message) { 17 | room := message.Room() 18 | if room == nil { 19 | message.Say("该功能仅支持群聊") 20 | return 21 | } 22 | roomModel, err := models.GetRoom(room.ID()) 23 | if err != nil { 24 | message.Say(err.Error()) 25 | return 26 | } 27 | if roomModel == nil { 28 | return 29 | } 30 | from := message.From() 31 | exist, err := models.ExistNotRemindBy(roomModel.ID, from.ID()) 32 | if err != nil { 33 | room.Say(err.Error(), from) 34 | return 35 | } 36 | if exist { 37 | return 38 | } 39 | err = models.AddNotRemind(&models.NotRemind{ 40 | WxID: from.ID(), 41 | RoomID: roomModel.ID, 42 | }) 43 | if err != nil { 44 | room.Say("操作失败,请稍后重试", from) 45 | return 46 | } 47 | room.Say("以后将不在提醒你,如果需要继续提醒,请对我说:#提醒我", from) 48 | } 49 | -------------------------------------------------------------------------------- /models/notremind.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "github.com/jinzhu/gorm" 5 | "time" 6 | ) 7 | 8 | type NotRemind struct { 9 | ID int `gorm:"primary_key" json:"id"` 10 | WxID string `json:"wx_id"` 11 | RoomID int64 `json:"room_id"` 12 | CreatedOn *time.Time `json:"created_on"` 13 | } 14 | 15 | func GetNotRemindWxIDsBy(roomID interface{}) ([]string, error) { 16 | var ids []string 17 | return ids, db.Model(&NotRemind{}).Where("room_id = ?", roomID).Pluck("wx_id", &ids).Error 18 | } 19 | 20 | func AddNotRemind(remind *NotRemind) error { 21 | return db.Create(remind).Error 22 | } 23 | 24 | func ExistNotRemindBy(roomID, wxID interface{}) (bool, error) { 25 | var notRemind NotRemind 26 | err := db.Select("id").Where("wx_id = ? AND room_id = ?", wxID, roomID).First(¬Remind).Error 27 | if err != nil && err != gorm.ErrRecordNotFound { 28 | return false, err 29 | } 30 | if notRemind.ID > 0 { 31 | return true, nil 32 | } 33 | return false, nil 34 | } 35 | 36 | func DeleteBy(roomID, wxID interface{}) error { 37 | return db.Where("wx_id = ? AND room_id = ?", wxID, roomID).Delete(&NotRemind{}).Error 38 | } 39 | -------------------------------------------------------------------------------- /models/room.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "github.com/jinzhu/gorm" 5 | "time" 6 | ) 7 | 8 | type RoomStatus uint8 9 | 10 | const OpenCheckinStatus RoomStatus = 1 11 | const CloseCheckinStatus RoomStatus = 0 12 | 13 | type Room struct { 14 | ID int64 `gorm:"primary_key" json:"id"` 15 | WxRoomID string `json:"wx_room_id"` 16 | AdminWxID string `json:"admin_wx_id"` 17 | Status RoomStatus `json:"status"` 18 | CreatedOn *time.Time `json:"created_on"` 19 | ModifiedOn *time.Time `json:"modified_on"` 20 | } 21 | 22 | func GetRoom(wxRoomID string) (*Room, error) { 23 | var room Room 24 | err := db.Where("wx_room_id = ?", wxRoomID).First(&room).Error 25 | if err == gorm.ErrRecordNotFound { 26 | return nil, nil 27 | } 28 | if err != nil { 29 | return nil, err 30 | } 31 | return &room, nil 32 | } 33 | 34 | func GetOpenStatusRooms() ([]*Room, error) { 35 | var rooms []*Room 36 | return rooms, db.Where("status = ?", OpenCheckinStatus).Find(&rooms).Error 37 | } 38 | 39 | func AddRoom(room *Room) error { 40 | return db.Create(room).Error 41 | } 42 | 43 | func UpdateRoomStatus(roomID int64, status RoomStatus) error { 44 | return db.Model(&Room{}).Where("id = ?", roomID).Update("status", status).Error 45 | } 46 | -------------------------------------------------------------------------------- /remind.sql: -------------------------------------------------------------------------------- 1 | create database `remind` character set utf8mb4 collate utf8mb4_general_ci; 2 | 3 | create table `room` ( 4 | `id` int primary key auto_increment, 5 | `wx_room_id` varchar(50) not null comment '微信群聊id', 6 | `admin_wx_id` varchar(50) not null comment '管理员微信id', 7 | `status` tinyint not null default 0 comment '是否开启签到 0关闭,1开启', 8 | `created_on` timestamp not null default current_timestamp, 9 | `modified_on` timestamp not null default current_timestamp on update current_timestamp, 10 | unique key (`wx_room_id`) 11 | ) ENGINE=innoDB DEFAULT CHARSET=utf8mb4 COMMENT='微信群表'; 12 | 13 | create table `checkin` ( 14 | `id` int primary key auto_increment, 15 | `room_id` int not null comment '微信群聊主键id', 16 | `wx_id` varchar(50) not null comment '微信id', 17 | `date` date not null comment '签到日期', 18 | `created_on` timestamp not null default current_timestamp, 19 | unique (`room_id`,`wx_id`,`date`) 20 | ) ENGINE=innoDB DEFAULT CHARSET=utf8mb4 COMMENT='签到表'; 21 | 22 | create table `not_remind` ( 23 | `id` int primary key auto_increment, 24 | `room_id` int not null comment '微信群聊主键id', 25 | `wx_id` varchar(50) not null comment '微信id', 26 | `created_on` timestamp not null default current_timestamp, 27 | unique key (`room_id`, `wx_id`) 28 | ) ENGINE=innoDB DEFAULT CHARSET=utf8mb4 COMMENT='不提醒表'; 29 | -------------------------------------------------------------------------------- /bcmd/checkin.go: -------------------------------------------------------------------------------- 1 | package bcmd 2 | 3 | import ( 4 | "github.com/dchaofei/wechat-remind-bot/models" 5 | "github.com/wechaty/go-wechaty/wechaty/user" 6 | "time" 7 | ) 8 | 9 | const CheckInCmdName = "$打卡" 10 | 11 | func init() { 12 | registerHandle(CheckInCmdName, new(checkIn)) 13 | } 14 | 15 | type checkIn struct{} 16 | 17 | func (s *checkIn) Handle(message *user.Message) { 18 | room := message.Room() 19 | if room == nil { 20 | message.Say("该功能仅支持群聊") 21 | return 22 | } 23 | roomModel, err := models.GetRoom(room.ID()) 24 | if err != nil { 25 | message.Say(err.Error()) 26 | return 27 | } 28 | if roomModel == nil { 29 | return 30 | } 31 | from := message.From() 32 | if roomModel.Status != models.OpenCheckinStatus { 33 | room.Say("打卡功能未开启", from) 34 | return 35 | } 36 | date := time.Now().Format("2006-01-02") 37 | exist, err := models.ExistCheckinBy(from.ID(), roomModel.ID, date) 38 | if err != nil { 39 | message.Say(err.Error()) 40 | return 41 | } 42 | if exist { 43 | room.Say("今天已经打卡,请不要重复打卡", from) 44 | return 45 | } 46 | if err := models.AddCheckIn(&models.Checkin{ 47 | WxID: from.ID(), 48 | RoomID: roomModel.ID, 49 | Date: date, 50 | }); err != nil { 51 | room.Say(err.Error(), from) 52 | return 53 | } 54 | room.Say("打卡成功,今天将不再提醒你"+date, from) 55 | } 56 | -------------------------------------------------------------------------------- /bcmd/opencheckin.go: -------------------------------------------------------------------------------- 1 | package bcmd 2 | 3 | import ( 4 | "github.com/dchaofei/wechat-remind-bot/models" 5 | "github.com/wechaty/go-wechaty/wechaty/user" 6 | ) 7 | 8 | const OpenCheckInCmdName = "$开启打卡" 9 | 10 | func init() { 11 | registerHandle(OpenCheckInCmdName, new(openCheckIn)) 12 | } 13 | 14 | type openCheckIn struct{} 15 | 16 | func (o *openCheckIn) Handle(message *user.Message) { 17 | room := message.Room() 18 | if room == nil { 19 | message.Say("该功能仅支持群聊") 20 | return 21 | } 22 | roomModel, err := models.GetRoom(room.ID()) 23 | if err != nil { 24 | message.Say(err.Error()) 25 | return 26 | } 27 | if roomModel == nil { 28 | if err := o.create(message); err != nil { 29 | message.Say(err.Error()) 30 | return 31 | } 32 | message.Say("开启打卡成功") 33 | return 34 | } 35 | from := message.From() 36 | if from.ID() != roomModel.AdminWxID { 37 | room.Say("只有当前群的 bot 管理员才能操作此功能", from) 38 | return 39 | } 40 | if roomModel.Status != models.CloseCheckinStatus { 41 | room.Say("打卡已开启,请不要重复开启", from) 42 | return 43 | } 44 | err = models.UpdateRoomStatus(roomModel.ID, models.OpenCheckinStatus) 45 | if err != nil { 46 | room.Say("开启打卡失败: "+err.Error(), from) 47 | return 48 | } 49 | room.Say("打卡已开启", from) 50 | } 51 | 52 | func (o *openCheckIn) create(message *user.Message) error { 53 | roomModel := &models.Room{ 54 | ID: 0, 55 | WxRoomID: message.Room().ID(), 56 | AdminWxID: message.From().ID(), 57 | Status: models.OpenCheckinStatus, 58 | } 59 | return models.AddRoom(roomModel) 60 | } 61 | -------------------------------------------------------------------------------- /bcmd/base.go: -------------------------------------------------------------------------------- 1 | package bcmd 2 | 3 | import ( 4 | "github.com/wechaty/go-wechaty/wechaty/user" 5 | "sort" 6 | ) 7 | 8 | type Handler interface { 9 | Handle(message *user.Message) 10 | } 11 | 12 | type handlerName struct { 13 | name string 14 | sort int 15 | } 16 | 17 | type handlerNames []handlerName 18 | 19 | func (h handlerNames) Len() int { 20 | return len(h) 21 | } 22 | 23 | func (h handlerNames) Less(i, j int) bool { 24 | return h[i].sort < h[j].sort 25 | } 26 | 27 | func (h handlerNames) Swap(i, j int) { 28 | h[i], h[j] = h[j], h[i] 29 | } 30 | 31 | var handlers = map[string]Handler{} 32 | 33 | var sortedHandlerNames handlerNames 34 | 35 | var outputNames []string 36 | 37 | func registerHandle(name string, handler Handler, sortI ...int) { 38 | if _, ok := handlers[name]; ok { 39 | panic(name + " handler 已经存在") 40 | } 41 | handlers[name] = handler 42 | sortField := 0 43 | if len(sortI) > 0 { 44 | sortField = sortI[0] 45 | } 46 | sortedHandlerNames = append(sortedHandlerNames, handlerName{ 47 | name: name, 48 | sort: sortField, 49 | }) 50 | sort.Sort(sortedHandlerNames) 51 | setHandlerNames() 52 | } 53 | 54 | func GetHandler(name string) Handler { 55 | return handlers[name] 56 | } 57 | 58 | func setHandlerNames() { 59 | var names []string 60 | var unSortNames []string 61 | for _, v := range sortedHandlerNames { 62 | if v.sort == 0 { 63 | unSortNames = append(unSortNames, v.name) 64 | continue 65 | } 66 | names = append(names, v.name) 67 | } 68 | sort.Strings(unSortNames) 69 | 70 | outputNames = append(unSortNames, names...) 71 | } 72 | 73 | func GetHandlerNames() []string { 74 | return outputNames 75 | } 76 | -------------------------------------------------------------------------------- /models/models.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "fmt" 5 | "github.com/dchaofei/wechat-remind-bot/vars" 6 | _ "github.com/go-sql-driver/mysql" 7 | "github.com/jinzhu/gorm" 8 | "time" 9 | ) 10 | 11 | var db *gorm.DB 12 | 13 | func Setup() { 14 | var ( 15 | err error 16 | user, password, host, dbname string 17 | ) 18 | user = vars.DataBaseSetting.User 19 | password = vars.DataBaseSetting.Password 20 | host = vars.DataBaseSetting.Host 21 | dbname = vars.DataBaseSetting.Name 22 | 23 | db, err = gorm.Open("mysql", 24 | fmt.Sprintf("%s:%s@tcp(%s)/%s?charset=utf8&parseTime=True&loc=Local", 25 | user, 26 | password, 27 | host, 28 | dbname, 29 | )) 30 | if err != nil { 31 | panic(err) 32 | } 33 | db.SingularTable(true) 34 | db.Callback().Create().Replace("gorm:update_time_stamp", updateTimeStampForCreateCallback) 35 | db.Callback().Update().Replace("gorm:update_time_stamp", updateTimeStampForUpdateCallback) 36 | db.LogMode(true) 37 | 38 | db.DB().SetMaxIdleConns(10) 39 | db.DB().SetMaxOpenConns(100) 40 | } 41 | 42 | // updateTimeStampForCreateCallback will set `CreatedOn`, `ModifiedOn` when creating 43 | func updateTimeStampForCreateCallback(scope *gorm.Scope) { 44 | if !scope.HasError() { 45 | nowTime := time.Now() 46 | if createTimeField, ok := scope.FieldByName("CreatedOn"); ok { 47 | if createTimeField.IsBlank { 48 | createTimeField.Set(nowTime) 49 | } 50 | } 51 | 52 | if modifyTimeField, ok := scope.FieldByName("ModifiedOn"); ok { 53 | if modifyTimeField.IsBlank { 54 | modifyTimeField.Set(nowTime) 55 | } 56 | } 57 | } 58 | } 59 | 60 | // updateTimeStampForUpdateCallback will set `ModifiedOn` when updating 61 | func updateTimeStampForUpdateCallback(scope *gorm.Scope) { 62 | if _, ok := scope.Get("gorm:update_column"); !ok { 63 | scope.SetColumn("ModifiedOn", time.Now()) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /startup/vars.go: -------------------------------------------------------------------------------- 1 | package startup 2 | 3 | import ( 4 | "fmt" 5 | "github.com/dchaofei/wechat-remind-bot/bcmd" 6 | "github.com/dchaofei/wechat-remind-bot/bcron" 7 | "github.com/dchaofei/wechat-remind-bot/vars" 8 | "github.com/robfig/cron/v3" 9 | "github.com/wechaty/go-wechaty/wechaty" 10 | "github.com/wechaty/go-wechaty/wechaty-puppet/schemas" 11 | "github.com/wechaty/go-wechaty/wechaty/user" 12 | "gopkg.in/ini.v1" 13 | "log" 14 | "strings" 15 | ) 16 | 17 | func SetupVars() { 18 | loadIni() 19 | vars.Bot = getBot() 20 | vars.CronInstance = getCron() 21 | } 22 | 23 | func getBot() *wechaty.Wechaty { 24 | bot := wechaty.NewWechaty() 25 | bot.OnScan(func(context *wechaty.Context, qrCode string, status schemas.ScanStatus, data string) { 26 | log.Printf("Scan QR Code to login: %v\nhttps://api.qrserver.com/v1/create-qr-code/?data=%s\n", status, qrCode) 27 | }).OnLogin(func(context *wechaty.Context, user *user.ContactSelf) { 28 | log.Printf("%s logined\n", user.Name()) 29 | }).OnLogout(func(context *wechaty.Context, user *user.ContactSelf, reason string) { 30 | log.Printf("%s logout, reason: %s\n", user.Name(), reason) 31 | }).OnMessage(func(context *wechaty.Context, message *user.Message) { 32 | str := message.String() 33 | if message.Room() != nil { 34 | str = fmt.Sprintf("roomID: %s ; %s", message.Room().ID(), str) 35 | } 36 | log.Println(str) 37 | h := bcmd.GetHandler(strings.Replace(message.Text(), "$", "$", 1)) 38 | if h != nil { 39 | h.Handle(message) 40 | } 41 | }).OnStart(func(context *wechaty.Context) { 42 | log.Println("started") 43 | }) 44 | 45 | var err = bot.Start() 46 | if err != nil { 47 | log.Fatalf("getBot Start: %v", err) 48 | } 49 | return bot 50 | } 51 | 52 | func getCron() *cron.Cron { 53 | c := cron.New() 54 | if _, err := c.AddFunc(vars.AppSetting.CronSpec, bcron.Remind); err != nil { 55 | log.Fatalf("getCrom c.AddFunc: %v", err) 56 | } 57 | if vars.AppSetting.EatRemindCronSpec != "" { 58 | if _, err := c.AddFunc(vars.AppSetting.EatRemindCronSpec, bcron.EatRemind); err != nil { 59 | log.Fatalf("getCrom c.AddFunc: %v", err) 60 | } 61 | } 62 | c.Start() 63 | return c 64 | } 65 | 66 | var cfg *ini.File 67 | 68 | func loadIni() { 69 | var err error 70 | cfg, err = ini.Load("app.ini") 71 | if err != nil { 72 | log.Fatalf("loadIni, fail to parse 'app.ini': %v", err) 73 | } 74 | 75 | mapTo("app", vars.AppSetting) 76 | mapTo("database", vars.DataBaseSetting) 77 | 78 | vars.AppSetting.EatRemindRoomIds = cfg.Section("app").Key("EatRemindRoomIds").Strings(",") 79 | } 80 | 81 | // mapTo map section 82 | func mapTo(section string, v interface{}) { 83 | err := cfg.Section(section).MapTo(v) 84 | if err != nil { 85 | log.Fatalf("Cfg.MapTo %s err: %v", section, err) 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /bcron/remind.go: -------------------------------------------------------------------------------- 1 | package bcron 2 | 3 | import ( 4 | "github.com/dchaofei/wechat-remind-bot/models" 5 | "github.com/dchaofei/wechat-remind-bot/vars" 6 | "github.com/wechaty/go-wechaty/wechaty-puppet/helper" 7 | "github.com/wechaty/go-wechaty/wechaty-puppet/schemas" 8 | _interface "github.com/wechaty/go-wechaty/wechaty/interface" 9 | "log" 10 | "math" 11 | "time" 12 | ) 13 | 14 | func Remind() { 15 | rooms, err := models.GetOpenStatusRooms() 16 | if err != nil { 17 | log.Println("Remind models.GetOpenStatusRooms() err: ", err) 18 | return 19 | } 20 | async := helper.NewAsync(0) 21 | for _, room := range rooms { 22 | room := room 23 | async.AddTask(func() (interface{}, error) { 24 | remind(room) 25 | return nil, nil 26 | }) 27 | } 28 | async.Result() 29 | } 30 | 31 | func remind(roomModel *models.Room) { 32 | bot := vars.Bot 33 | room := bot.Room().Find(&schemas.RoomQueryFilter{ 34 | Id: roomModel.WxRoomID, 35 | }) 36 | if room == nil { 37 | log.Println("remind 没有找到 room: ", roomModel.WxRoomID) 38 | return 39 | } 40 | 41 | // 防止新成员没有进来 42 | if err := room.Sync(); err != nil { 43 | log.Println("room.Sync err: ", err.Error()) 44 | } 45 | 46 | notRemindWxIds, err := models.GetNotRemindWxIDsBy(roomModel.ID) 47 | if err != nil { 48 | log.Println("remind GetNotRemindWxIDsBy err: ", err.Error()) 49 | return 50 | } 51 | 52 | alreadyCheckinIds, err := models.GetAlreadyCheckinWxIdsBy(roomModel.ID, time.Now().Format("2006-01-02")) 53 | if err != nil { 54 | log.Println("remind GetAlreadyCheckinWxIdsBy err: ", err.Error()) 55 | return 56 | } 57 | notRemindWxIds = append(notRemindWxIds, alreadyCheckinIds...) 58 | 59 | contacts, err := room.MemberAll(nil) 60 | if err != nil { 61 | log.Println("remind MemberAll err: ", err.Error()) 62 | return 63 | } 64 | 65 | var remindContacts []_interface.IContact 66 | for _, contact := range contacts { 67 | if inArray(contact.ID(), notRemindWxIds) || contact.Self() { 68 | continue 69 | } 70 | remindContacts = append(remindContacts, contact) 71 | // 防止联系人昵称变更,导致 @ 失败 72 | err := contact.Sync() 73 | if err != nil { 74 | log.Println("contact.Sync err: ", err.Error()) 75 | } 76 | } 77 | 78 | length := len(remindContacts) 79 | 80 | if length == 0 { 81 | return 82 | } 83 | 84 | // 分批@,每次只@100人 85 | start := 0 86 | max := 100 87 | for { 88 | if start >= length { 89 | return 90 | } 91 | min := math.Min(float64(start+max), float64(length)) 92 | room.Say("\n\n不要忘记打卡哦!!!\n\n\n如果今天不想收到提醒请回复:$打卡\n了解更多命令回复:$帮助\n点外卖回复:$红包", remindContacts[start:int(min)]...) 93 | start+=max 94 | } 95 | } 96 | 97 | func inArray(s string, ss []string) bool { 98 | for _, v := range ss { 99 | if s == v { 100 | return true 101 | } 102 | } 103 | return false 104 | } 105 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 3 | github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc= 4 | github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y= 5 | github.com/bitly/go-simplejson v0.5.0 h1:6IH+V8/tVMab511d5bn4M7EwGXZf9Hj6i2xSwkNEM+Y= 6 | github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA= 7 | github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY= 8 | github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= 9 | github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= 10 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 11 | github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= 12 | github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= 13 | github.com/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd h1:83Wprp6ROGeiHFAP8WJdI2RoxALQYgdllERc3N5N2DM= 14 | github.com/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= 15 | github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 16 | github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 17 | github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= 18 | github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= 19 | github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5 h1:Yzb9+7DPaBjB8zlTR87/ElzFsnQfuHnVUVqpZZIcV5Y= 20 | github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0= 21 | github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs= 22 | github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= 23 | github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe h1:lXe2qZdvpiX5WZkZR4hgp4KJVfY3nMkvmwbVkpv1rVY= 24 | github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= 25 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 26 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 27 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 28 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 29 | github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 30 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 31 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 32 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 33 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 34 | github.com/golang/protobuf v1.4.0 h1:oOuy+ugB+P/kBdUnG5QaMXSIyJ1q38wWSojYCb3z5VQ= 35 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 36 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 37 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 38 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 39 | github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4= 40 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 41 | github.com/google/uuid v1.1.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 42 | github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= 43 | github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 44 | github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= 45 | github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= 46 | github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc= 47 | github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= 48 | github.com/jinzhu/gorm v1.9.14 h1:Kg3ShyTPcM6nzVo148fRrcMO6MNKuqtOUwnzqMgVniM= 49 | github.com/jinzhu/gorm v1.9.14/go.mod h1:G3LB3wezTOWM2ITLzPxEXgSkOXAntiLHS7UdBefADcs= 50 | github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= 51 | github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= 52 | github.com/jinzhu/now v1.0.1 h1:HjfetcXq097iXP0uoPCdnM4Efp5/9MsM0/M+XOTeR3M= 53 | github.com/jinzhu/now v1.0.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= 54 | github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= 55 | github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= 56 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 57 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 58 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 59 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 60 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 61 | github.com/lib/pq v1.1.1 h1:sJZmqHoEaY7f+NPP8pgLB/WxulyR3fewgCM2qaSlBb4= 62 | github.com/lib/pq v1.1.1/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= 63 | github.com/lucsky/cuid v1.0.2 h1:z4XlExeoderxoPj2/dxKOyPxe9RCOu7yNq9/XWxIUMQ= 64 | github.com/lucsky/cuid v1.0.2/go.mod h1:QaaJqckboimOmhRSJXSx/+IT+VTfxfPGSo/6mfgUfmE= 65 | github.com/maruel/rs v0.0.0-20150922171536-2c81c4312fe4 h1:u9jwvcKbQpghIXgNl/EOL8hzhAFXh4ePrEP493W3tNA= 66 | github.com/maruel/rs v0.0.0-20150922171536-2c81c4312fe4/go.mod h1:kcRFpEzolcEklV6rD7W95mG49/sbdX/PlFmd7ni3RvA= 67 | github.com/mattn/go-sqlite3 v1.14.0 h1:mLyGNKR8+Vv9CAU7PphKa2hkEqxxhn8i32J6FPj1/QA= 68 | github.com/mattn/go-sqlite3 v1.14.0/go.mod h1:JIl7NbARA7phWnGvh0LKTyg7S9BA+6gx71ShQilpsus= 69 | github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95 h1:+OLn68pqasWca0z5ryit9KGfp3sUsW4Lqg32iRMJyzs= 70 | github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJG+0mI8eUu6xqkFDYS2kb2saOteoSB3cE= 71 | github.com/otiai10/marmoset v0.4.0 h1:Hg59lQI7qQowBEdsAJ/+VDTEospTBzXX/A1Gsw4mlvA= 72 | github.com/otiai10/marmoset v0.4.0/go.mod h1:t2q6dXWZ9YcFdRREDApX4bCmfQnL3isJ2dgl8ychlXg= 73 | github.com/otiai10/mint v1.3.0 h1:Ady6MKVezQwHBkGzLFbrsywyp09Ah7rkmfjV3Bcr5uc= 74 | github.com/otiai10/mint v1.3.0/go.mod h1:F5AjcsTsWUqX+Na9fpHb52P8pcRX2CI6A3ctIT91xUo= 75 | github.com/otiai10/opengraph v1.1.1 h1:zaHbzhegXGqxVpiI7xlQQ0vKBWvHJbagnUjDC40sFtQ= 76 | github.com/otiai10/opengraph v1.1.1/go.mod h1:ZMbPcfiSRSsg3+yrWZCXrgYL6kEK4KpH4GG1iyIvEXs= 77 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 78 | github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 79 | github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= 80 | github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= 81 | github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 82 | github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= 83 | github.com/skip2/go-qrcode v0.0.0-20191027152451-9434209cb086 h1:RYiqpb2ii2Z6J4x0wxK46kvPBbFuZcdhS+CIztmYgZs= 84 | github.com/skip2/go-qrcode v0.0.0-20191027152451-9434209cb086/go.mod h1:PLPIyL7ikehBD1OAjmKKiOEhbvWyHGaNDjquXMcYABo= 85 | github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= 86 | github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= 87 | github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= 88 | github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= 89 | github.com/tuotoo/qrcode v0.0.0-20190222102259-ac9c44189bf2 h1:BWVtt2VBY+lmVDu9MGKqLGKl04B+iRHcrW1Ptyi/8tg= 90 | github.com/tuotoo/qrcode v0.0.0-20190222102259-ac9c44189bf2/go.mod h1:lPnW9HVS0vJdeYyQtOvIvlXgZPNhUAhwz+z5r8AJk0Y= 91 | github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= 92 | github.com/wechaty/go-grpc v0.18.12 h1:uHGKugN0/7d0vnrJ0CSwyfULwOA0Z1lf72wVowHILqg= 93 | github.com/wechaty/go-grpc v0.18.12/go.mod h1:gtyrRa9Ts5KGzQh61CIytAYyE9HmVna6yFRamKN4udk= 94 | github.com/wechaty/go-wechaty v0.3.0 h1:P9JoNYIgSqX/FO2bKMgGEbd8VX1/M35hjeA6Td/h2Kg= 95 | github.com/wechaty/go-wechaty v0.3.0/go.mod h1:hfpNlGqZmuHQWxK9GL1mFImh8j2CXi83/YorrKQ0KLE= 96 | github.com/willf/bitset v1.1.10 h1:NotGKqX0KwQ72NUzqrjZq5ipPNDQex9lo3WpaS8L2sc= 97 | github.com/willf/bitset v1.1.10/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4= 98 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 99 | golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 100 | golang.org/x/crypto v0.0.0-20191205180655-e7c4368fe9dd h1:GGJVjV8waZKRHrgwvtH66z9ZGVurTD1MT0n1Bb+q4aM= 101 | golang.org/x/crypto v0.0.0-20191205180655-e7c4368fe9dd/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 102 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 103 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 104 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 105 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 106 | golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 107 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 108 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 109 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 110 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 111 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 112 | golang.org/x/net v0.0.0-20190926025831-c00fd9afed17/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 113 | golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 114 | golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e h1:3G+cUijn7XD+S4eJFddp53Pv7+slrESplyjG25HgL+k= 115 | golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 116 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 117 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 118 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 119 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 120 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 121 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 122 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 123 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 124 | golang.org/x/sys v0.0.0-20200413165638-669c56c373c4 h1:opSr2sbRXk5X5/givKrrKj9HXxFpW2sdCiP8MJSKLQY= 125 | golang.org/x/sys v0.0.0-20200413165638-669c56c373c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 126 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 127 | golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= 128 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 129 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 130 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 131 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 132 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 133 | golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 134 | golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 135 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= 136 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 137 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 138 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 139 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 140 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 141 | google.golang.org/genproto v0.0.0-20200416231807-8751e049a2a0 h1:N5O9PpTbQrkvH0IQ1q+mmGyg8Gt6iKcu6b6+gmz3jnA= 142 | google.golang.org/genproto v0.0.0-20200416231807-8751e049a2a0/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 143 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 144 | google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= 145 | google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= 146 | google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 147 | google.golang.org/grpc v1.28.1 h1:C1QC6KzgSiLyBabDi87BbjaGreoRgGUF5nOyvfrAZ1k= 148 | google.golang.org/grpc v1.28.1/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= 149 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 150 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 151 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 152 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 153 | google.golang.org/protobuf v1.21.0 h1:qdOKuR/EIArgaWNjetjgTzgVTAZ+S/WXVrq9HW9zimw= 154 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 155 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 156 | gopkg.in/ini.v1 v1.57.0 h1:9unxIsFcTt4I55uWluz+UmL95q4kdJ0buvQ1ZIqVQww= 157 | gopkg.in/ini.v1 v1.57.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= 158 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 159 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 160 | honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 161 | --------------------------------------------------------------------------------