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