之前又出過gin-frame的文章,今天就利用gin-frame做一個釘釘智慧提醒功能吧。
需求
工作生活中,瑣事太多,就容易忘,希望有這麼個智慧提醒功能,能利用釘釘準時推送提醒我!
比如:
每週五18:00提醒我發週報
每週三14:00提醒我發車買奶茶
10分鐘提醒我去會議室開會
2020-08-20提醒我紀念日到了
…諸如此類
釘釘接入
釘釘有機器人功能,之前已有應用到專案告警提醒等功能中,效果不錯,另外Outgoing機制也已經開放使用,我們就可以利用它進行對話了。
按照文件在群裡新建機器人即可。我開啟的是webhook自定義機器人,outgoing提送地址就是專案接收資訊地址,比如:http://cron.13sai.com/dingdingPost。
解析內容
釘釘文件的outgoing說明不全,或者是藏在哪裡我沒找到,可以使用@機器人接收資訊列印看一下。
{
"conversationId":"xxx",
"atUsers":[
{
"dingtalkId":"xxx"
}],
"chatbotUserId":"xxx",
"msgId":"xxx",
"senderNick":"sai0556",
"isAdmin":false,
"sessionWebhookExpiredTime":1594978626787,
"createAt":1594973226742,
"conversationType":"2",
"senderId":"xxx",
"conversationTitle":"智慧備忘錄",
"isInAtList":true,
"sessionWebhook":"xxx",
"text":{
"content":" hello gin-frame"
},
"msgtype":"text"
}
//關注senderId傳送人id,text傳送內容,senderNick傳送人暱稱即可
定義一個struct,接收訊息
type DingDingMsgContent struct {
SenderNick string `json:"senderNick"`
SenderId string `json:"senderId"`
Text struct {
Content string `json:"content"`
} `json:"text"`
}
func DingDing(c *gin.Context) {
data, _ := ioutil.ReadAll(c.Request.Body)
form := DingDingMsgContent{}
err := json.Unmarshal([]byte(data), &form)
// err := c.ShouldBindJSON(&form)
if err != nil {
fmt.Println(err)
return
}
....
}
傳送釘釘訊息
func SendDD(msg string) {
// 列印出來看看是個啥
fmt.Println(msg)
tips := make(map[string]interface{})
content := make(map[string]interface{})
tips["msgtype"] = "text"
// @ 是用來提醒群裡對應的人
arr := strings.Split(msg, "@")
// [提醒]是機器人關鍵字,個人建議設定機器人限制ip或使用token,比較靠譜
content["content"] = fmt.Sprintf("%s[提醒]", arr[0])
tips["text"] = content
if len(arr) > 1 {
mobile := make([]string, 0)
at := make(map[string]interface{})
mobile = append(mobile, arr[1])
at["atMobiles"] = mobile
tips["at"] = at
}
bytesData, err := json.Marshal(tips)
if err != nil {
fmt.Println(err.Error() )
return
}
reader := bytes.NewReader(bytesData)
url := viper.GetString("dingding_url")
request, err := http.NewRequest("POST", url, reader)
if err != nil {
return
}
request.Header.Set("Content-Type", "application/json;charset=UTF-8")
client := http.Client{}
_, err = client.Do(request)
if err != nil {
fmt.Println(err.Error())
return
}
// 返回可自行處理,可重試,偷個懶不處理了
}
關鍵字
// util/common.go
// 就列了一些常見的,可自行擴充套件
func UpdateKeywords() {
redis := model.RedisClient.Pipeline()
key := KeyWords
redis.HSet(model.Ctx, key, "分鐘後", "1|60")
redis.HSet(model.Ctx, key, "時後", "1|3600")
redis.HSet(model.Ctx, key, "天后", "1|86400")
redis.HSet(model.Ctx, key, "每天", "-1|1")
redis.HSet(model.Ctx, key, "每週一", "2|0")
redis.HSet(model.Ctx, key, "每週二", "2|1")
redis.HSet(model.Ctx, key, "每週三", "2|2")
redis.HSet(model.Ctx, key, "每週四", "2|3")
redis.HSet(model.Ctx, key, "每週五", "2|4")
redis.HSet(model.Ctx, key, "每週六", "2|5")
redis.HSet(model.Ctx, key, "每週日", "2|6")
redis.HSet(model.Ctx, key, "週一", "3|0")
redis.HSet(model.Ctx, key, "週二", "3|1")
redis.HSet(model.Ctx, key, "週三", "3|2")
redis.HSet(model.Ctx, key, "週四", "3|3")
redis.HSet(model.Ctx, key, "週五", "3|4")
redis.HSet(model.Ctx, key, "週六", "3|5")
redis.HSet(model.Ctx, key, "週日", "3|6")
redis.HSet(model.Ctx, key, "下週一", "3|7")
redis.HSet(model.Ctx, key, "下週二", "3|8")
redis.HSet(model.Ctx, key, "下週三", "3|9")
redis.HSet(model.Ctx, key, "下週四", "3|10")
redis.HSet(model.Ctx, key, "下週五", "3|11")
redis.HSet(model.Ctx, key, "下週六", "3|12")
redis.HSet(model.Ctx, key, "下週日", "3|13")
redis.HSet(model.Ctx, key, "下星期一", "3|7")
redis.HSet(model.Ctx, key, "下星期二", "3|8")
redis.HSet(model.Ctx, key, "下星期三", "3|9")
redis.HSet(model.Ctx, key, "下星期四", "3|10")
redis.HSet(model.Ctx, key, "下星期五", "3|11")
redis.HSet(model.Ctx, key, "下星期六", "3|12")
redis.HSet(model.Ctx, key, "下星期日", "3|13")
redis.HSet(model.Ctx, key, "今天", "4|0")
redis.HSet(model.Ctx, key, "明天", "4|1")
redis.HSet(model.Ctx, key, "後天", "4|2")
redis.HSet(model.Ctx, key, "取消", "0|0")
redis.Exec(model.Ctx)
}
解析內容
func parseContent(form DingDingMsgContent) (err error) {
str := form.Text.Content
redis := model.RedisClient
// 要先繫結喲,不然無法@到對應的人
index := strings.Index(str, "手機")
if index > -1 {
reg := regexp.MustCompile("1[0-9]{10}")
res := reg.FindAllString(str, 1)
if len(res) < 1 || res[0] == "" {
err = errors.New("手機格式不正確")
return
}
redis.HSet(model.Ctx, util.KeyDingDingID, form.SenderId, res[0])
err = errors.New("繫結成功")
return
}
hExist := redis.HExists(model.Ctx, util.KeyDingDingID, form.SenderId)
if !hExist.Val() {
err = errors.New("繫結手機號才能精確提醒哦,傳送--手機 13456567878--@我即可")
return
}
index = strings.Index(util.StrSub(str, 0, 4), "取消")
if index > -1 {
reg := regexp.MustCompile("[a-z0-9]{32}")
res := reg.FindAllString(str, 1)
if len(res) < 1 {
err = errors.New("任務id不正確")
return
}
if er := util.CancelQueue(res[0], form.SenderId); er != nil {
err = er
return
}
err = errors.New("取消成功")
return
}
return tips(form)
}
// 提醒內容
func tips(form DingDingMsgContent) (err error) {
rd := model.RedisClient
str := form.Text.Content
mobile := rd.HGet(model.Ctx, util.KeyDingDingID, form.SenderId).Val()
key := util.KeyWords
list, _ := rd.HGetAll(model.Ctx, key).Result()
now := time.Now().Unix()
tipsType := 1
k := ""
v := ""
fmt.Println("str", str)
index := 0
for key, value := range list {
index = util.UnicodeIndex(str, key)
if index > -1 && util.StrLen(key) > util.StrLen(k) {
fmt.Println("index", index, str, key, value)
k = key
v = value
}
}
msg := ""
var score int64
if k != "" {
kLen := util.StrLen(k)
msg = util.StrSub(str, index+kLen)
val := strings.Split(v, "|")
unit := val[1]
units,_ := strconv.Atoi(unit)
switch val[0] {
// 多少時間後
case "1":
reg := regexp.MustCompile("[0-9]{1,2}")
res := reg.FindAllString(str, 1)
minute, _ := strconv.Atoi(res[0])
score = now + int64(units*minute)
// 每週
case "2":
reg := regexp.MustCompile("[0-9]{1,2}")
res := reg.FindAllString(util.StrSub(msg, 0, 7), -1)
hour := 9
minute := 0
if len(res) > 0 {
hour, _ = strconv.Atoi(res[0])
}
if len(res) > 1 {
minute, _ = strconv.Atoi(res[1])
}
now = util.GetWeekTS(int64(units))
score = now + int64(60*minute + 3600*hour)
tipsType = 2
// 下週
case "3":
reg := regexp.MustCompile("[0-9]{1,2}")
res := reg.FindAllString(util.StrSub(msg, 0, 7), -1)
hour := 9
minute := 0
if len(res) > 0 {
hour, _ = strconv.Atoi(res[0])
}
if len(res) > 1 {
minute, _ = strconv.Atoi(res[1])
}
now = util.TodayTS()
score = now + int64(60*minute + 3600*hour + units*86400)
case "4":
reg := regexp.MustCompile("[0-9]{1,2}")
res := reg.FindAllString(util.StrSub(msg, 0, 7), -1)
hour := 9
minute := 0
if len(res) > 0 {
hour, _ = strconv.Atoi(res[0])
}
if len(res) > 1 {
minute, _ = strconv.Atoi(res[1])
}
now = util.GetWeekTS(int64(units))
score = now + int64(60*minute + 3600*hour)
case "-1":
reg := regexp.MustCompile("[0-9]{1,10}")
res := reg.FindAllString(util.StrSub(msg, 0, 7), -1)
fmt.Println("res", res)
hour := 9
minute := 0
if len(res) > 0 {
hour, _ = strconv.Atoi(res[0])
}
if len(res) > 1 {
minute, _ = strconv.Atoi(res[1])
}
now = util.TodayTS() + 86400
score = now + int64(60*minute + 3600*hour)
fmt.Println(now, score, minute, hour)
tipsType = 3
default:
}
} else {
reg := regexp.MustCompile("(([0-9]{4})[-|/|年])?([0-9]{1,2})[-|/|月]([0-9]{1,2})日?")
pi := reg.FindAllStringSubmatch(str, -1)
if (len(pi) > 0 ) {
date := pi[0]
if date[2] == "" {
date[2] = "2020"
}
location, _ := time.LoadLocation("Asia/Shanghai")
tm2, _ := time.ParseInLocation("2006/01/02", fmt.Sprintf("%s/%s/%s", date[2], date[3], date[4]), location)
score = util.GetZeroTime(tm2).Unix()
msg = reg.ReplaceAllString(str, "")
fmt.Println(msg)
} else {
msg = str
score = util.TodayTS()
}
reg = regexp.MustCompile("[0-9]{1,10}")
res := reg.FindAllString(util.StrSub(msg, 0, 7), -1)
fmt.Println("res", res)
hour := 9
minute := 0
if len(res) >= 1 {
hour, _ = strconv.Atoi(res[0])
fmt.Println("hour", hour, minute)
}
if len(res) > 1 {
minute, _ = strconv.Atoi(res[1])
}
score += int64(60*minute + 3600*hour)
}
if msg == "" {
err = errors.New("你說啥")
return
}
index = util.UnicodeIndex(msg, "提醒我")
if index < 0 {
err = errors.New("大哥,要我提醒你幹啥呢?請傳送--下週一13點提醒我寫作業")
return
}
msg = util.StrSub(msg, index+3)
fmt.Println(msg, mobile)
msg = util.StrCombine(msg, "@", mobile)
fmt.Println(score, msg, tipsType, err)
if err != nil {
util.SendDD(err.Error())
return
}
member := util.StrCombine(strconv.Itoa(tipsType), msg)
rd.ZAdd(model.Ctx, util.KeyCrontab, &redis.Z{
Score: float64(score),
Member: member,
})
uniqueKey := util.Md5(member)
rd.HSet(model.Ctx, util.StrCombine(util.KeyUserCron, form.SenderId), uniqueKey, member)
util.SendDD(fmt.Sprintf("設定成功(取消請回復:取消任務%s)--%s提醒您%s", uniqueKey, time.Unix(score, 0).Format("2006/01/02 15:04:05"), msg))
return
}
定時傳送
上面的程式碼能看出來,使用的是redis的有序集合,我們每分鐘去取過期的集合內容就ok了。
func Cron() {
c := cron.New()
spec := "*/10 * * * * ?"
c.AddJob(spec, Queue{})
c.Start()
}
type Queue struct {
}
func (q Queue) Run() {
now := time.Now().Unix()
rd := model.RedisClient
op := &redis.ZRangeBy{
Min: "0",
Max: strconv.FormatInt(now, 10),
}
ret, err := rd.ZRangeByScoreWithScores(model.Ctx, KeyCrontab, op).Result()
if err != nil {
fmt.Printf("zrangebyscore failed, err:%v\n", err)
return
}
for _, z := range ret {
fmt.Println(z.Member.(string), z.Score)
QueueDo(z.Member.(string), z.Score)
}
}
func QueueDo(msg string, score float64) {
msgType := msg[0:1]
SendDD(msg[1:])
rd := model.RedisClient
rd.ZRem(model.Ctx, KeyCrontab, msg)
switch msgType {
case "2":
rd.ZAdd(model.Ctx, KeyCrontab, &redis.Z{
Score: score + 7*86400,
Member: msg,
})
case "3":
rd.ZAdd(model.Ctx, KeyCrontab, &redis.Z{
Score: score + 86400,
Member: msg,
})
default:
rd.ZRem(model.Ctx, KeyCrontab, msg)
}
}
func CancelQueue(uniqueKey string, SenderId string) (err error) {
rd := model.RedisClient
member := rd.HGet(model.Ctx, StrCombine(KeyUserCron, SenderId), uniqueKey).Val()
if member == "" {
fmt.Println(StrCombine(KeyUserCron, SenderId), uniqueKey)
err = errors.New("沒有此任務")
return
}
fmt.Println(member, "member")
rd.ZRem(model.Ctx, KeyCrontab, member)
rd.HDel(model.Ctx, StrCombine(KeyUserCron, SenderId), uniqueKey)
err = errors.New("取消成功")
return
}
啟動任務:
util.UpdateKeywords()
util.Cron()
程式碼中有用到很多函式,不理解的可下載程式碼檢視。
這只是個基礎的提醒功能,有bug,也有需要改進的點,歡迎討論交流。
本作品採用《CC 協議》,轉載必須註明作者和本文連結
收藏前不妨點個贊試試!!!
分享開發知識,歡迎交流。qq交流群:965666112,公眾號:愛好歷史的程式設計師。
點選直達個人部落格