From 8b64806ef5aa65747edf442f61c2cd26cc3229ad Mon Sep 17 00:00:00 2001 From: ttk Date: Wed, 10 Jan 2024 14:37:56 +0800 Subject: [PATCH 1/2] feat: ali sms --- README.md | 38 ++++++++++-------- conf/confTemplate.yaml | 6 +++ go.mod | 2 + go.sum | 4 ++ send/aliSms.go | 88 ++++++++++++++++++++++++++++++++++++++++++ send/dingdingBot.go | 2 +- send/send.go | 2 +- 7 files changed, 124 insertions(+), 18 deletions(-) create mode 100644 send/aliSms.go diff --git a/README.md b/README.md index bde2b2e..c26897f 100644 --- a/README.md +++ b/README.md @@ -38,19 +38,19 @@ docker run -d --name messenger -p 8888:8888 -v $(pwd)/conf:/messenger/conf --res 参数说明: -| 参数 | 是否必须 | 类型 | 说明 | -| :--------- | :------- | :------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| sender | 是 | string | sender名称:发送消息具体sender的名称,对应conf中的name | -| msgtype | 是 | string | 消息内容类型:每种消息发送方式支持多种消息内容类型,各发送方式支持的消息内容类型参考如下
email: text/plain text/html
wechatBot: [微信机器人](https://developer.work.weixin.qq.com/document/path/99110#%E6%B6%88%E6%81%AF%E7%B1%BB%E5%9E%8B%E5%8F%8A%E6%95%B0%E6%8D%AE%E6%A0%BC%E5%BC%8F)      wechatApp:[微信应用](https://developer.work.weixin.qq.com/document/path/90236#%E6%B6%88%E6%81%AF%E7%B1%BB%E5%9E%8B)
feishuBot:[飞书机器人](https://open.feishu.cn/document/client-docs/bot-v3/add-custom-bot#5a997364)      feishuApp:[飞书应用](https://open.feishu.cn/document/server-docs/im-v1/message-content-description/create_json#3c92befd)
dingdingBot:[钉钉机器人](https://open.dingtalk.com/document/orgapp/custom-robot-access#title-72m-8ag-pqw)    dingdingApp:[钉钉应用](https://open.dingtalk.com/document/orgapp/types-of-messages-sent-by-robots?spm) | -| content | 是 | string | 消息内容:邮件可直接填写内容字符串,其他消息内容本身具有结构,传入其JSON序列化之后的字符串,如微信应用的文本消息填写`{"content":"my content"}`序列化后字符串 | -| title | 否 | string | 消息标题:仅用于 email 类型 | -| tos | 否 | []string | 接收人列表:发送邮件、应用消息时需要填写 | -| ccs | 否 | []string | 抄送人列表:仅用于 email 类型 | -| extra | 否 | string | 额外参数:通常情况下您只需要关注消息内容类型和其内容发送人,但是当您需要传递一些额外参数时,比如微信应用开启重复检查和检查时间间隔,可以将extra设置为`{"enable_duplicate_check":1, "duplicate_check_interval": 1800}`序列化后字符串 | -| sync | 否 | bool | 同步发送:默认情况下,发送请求接受成功即返回200,消息会异步发送,若sync为true则会同步等待消息发送结果并返回 | -| simple | 否 | bool | 简单内容:默认情况下,消息内容是json字符串(参考content参数),对于简单的消息类型text和markdown可设置simple=true,此时content仅填写内容字符串本身即可,如`my content`。
支持的消息类型(msgtype)
text: wechatBot wechatApp feishuBot feishuApp dingdingBot dingdingApp
markdown: wechatBot wechatApp dingdingBot dingdingApp | -| ats | 否 | []string | @列表: 使用@all代表@所有人; 支持wechatBot(text, markdown(无法@all)), feishuBot(text), dingdingBot (text, markdown) | -| at_mobiles | 否 | []string | @列表: 同ats参数,但使用手机号而非user id; 支持wechatBot(text), dingdingBot(text, markdown) | +| 参数 | 是否必须 | 类型 | 说明 | +| :--------- | :------- | :------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| sender | 是 | string | sender名称:发送消息具体sender的名称,对应conf中的name | +| msgtype | 是 | string | 消息内容类型:每种消息发送方式支持多种消息内容类型,各发送方式支持的消息内容类型参考如下
wechatBot: [微信机器人](https://developer.work.weixin.qq.com/document/path/99110#%E6%B6%88%E6%81%AF%E7%B1%BB%E5%9E%8B%E5%8F%8A%E6%95%B0%E6%8D%AE%E6%A0%BC%E5%BC%8F)      wechatApp:[微信应用](https://developer.work.weixin.qq.com/document/path/90236#%E6%B6%88%E6%81%AF%E7%B1%BB%E5%9E%8B)
feishuBot:[飞书机器人](https://open.feishu.cn/document/client-docs/bot-v3/add-custom-bot#5a997364)      feishuApp:[飞书应用](https://open.feishu.cn/document/server-docs/im-v1/message-content-description/create_json#3c92befd)
dingdingBot:[钉钉机器人](https://open.dingtalk.com/document/orgapp/custom-robot-access#title-72m-8ag-pqw)    dingdingApp:[钉钉应用](https://open.dingtalk.com/document/orgapp/types-of-messages-sent-by-robots?spm)
email (邮件): text/plain text/html
aliSms (阿里云短信): sms
| +| content | 是 | string | 消息内容:IM(微信、飞书、钉钉)消息内容本身具有结构,传入其JSON序列化之后的字符串,如微信应用的文本消息填写`{"content":"my content"}`序列化后字符;邮件可直接填写内容字符串;阿里云短信填写模板变量JSON序列化后字符串如:{"name":"张三","number":"1390000****"} 序列化后字符串 | +| title | 否 | string | 消息标题:仅用于 email 类型 | +| tos | 否 | []string | 接收人列表:发送邮件、应用消息时需要填写 | +| ccs | 否 | []string | 抄送人列表:仅用于 email 类型 | +| extra | 否 | string | 额外参数:通常情况下您只需要关注消息内容类型和其内容发送人,但是当您需要传递一些额外参数时,比如微信应用开启重复检查和检查时间间隔,可以将extra设置为`{"enable_duplicate_check":1, "duplicate_check_interval": 1800}`序列化后字符串 | +| sync | 否 | bool | 同步发送:默认情况下,发送请求接受成功即返回200,消息会异步发送,若sync为true则会同步等待消息发送结果并返回 | +| simple | 否 | bool | 简单内容:默认情况下,消息内容是json字符串(参考content参数),对于简单的消息类型text和markdown可设置simple=true,此时content仅填写内容字符串本身即可,如`my content`。
支持的消息类型(msgtype)
text: wechatBot wechatApp feishuBot feishuApp dingdingBot dingdingApp
markdown: wechatBot wechatApp dingdingBot dingdingApp | +| ats | 否 | []string | @列表: 使用@all代表@所有人; 支持wechatBot(text, markdown(无法@all)), feishuBot(text), dingdingBot (text, markdown) | +| at_mobiles | 否 | []string | @列表: 同ats参数,但使用手机号而非user id; 支持wechatBot(text), dingdingBot(text, markdown) | 返回结果: ```json @@ -132,8 +132,8 @@ func main() { 参数说明: -| 参数(请求体) | 是否必须 | 类型 | 说明 | -| :------------- | :------- | :--- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| 参数(请求体) | 是否必须 | 类型 | 说明 | +| :------------- | :------- | :--- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | body | 是 | json | 请求body为您的sender配置,如`{"wechatBot": [{"name": "yourSenderName", "url": "https://xxx"}]}`
POST:同类型配置会被全部覆盖
PUT:同类型同名称的配置会被更新,新配置将被添加
DELETE:同类型同名称配置将被删除 | 返回结果: @@ -224,7 +224,7 @@ yaml配置文件定义了 - ip - token - sign签名 -3. senders 具体发送方式。senders支持动态增删,即再服务已经启动的情况下可以直接修改senders列表,服务会持续读取最新的改动。支持的发送方式类型 +3. senders 具体发送方式。senders支持动态增删,即在服务已经启动的情况下可以直接修改senders列表,服务会持续读取最新的改动。支持的发送方式类型 - email - wechatBot - wechatApp @@ -280,6 +280,12 @@ senders: # appKey: xxxx # appSecret: xxxx # robotCode: xxxx + aliSms: + # - name: yourSenderName8 + # accessKey: xxxx + # accessSecret: xxxx + # templateCode: SMS_123456789 + # signName: xxxx ``` ## 自定义发送 diff --git a/conf/confTemplate.yaml b/conf/confTemplate.yaml index 2ed2f29..68c5f28 100644 --- a/conf/confTemplate.yaml +++ b/conf/confTemplate.yaml @@ -44,3 +44,9 @@ senders: # appKey: xxxx # appSecret: xxxx # robotCode: xxxx + aliSms: + # - name: yourSenderName8 + # accessKey: xxxx + # accessSecret: xxxx + # templateCode: SMS_123456789 + # signName: xxxx diff --git a/go.mod b/go.mod index a91b94b..b496d50 100644 --- a/go.mod +++ b/go.mod @@ -53,6 +53,7 @@ require ( github.com/fsnotify/fsnotify v1.6.0 // indirect github.com/gin-gonic/gin v1.9.1 github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/moul/http2curl v1.0.0 github.com/pelletier/go-toml/v2 v2.0.8 // indirect github.com/samber/lo v1.38.1 github.com/spf13/cast v1.5.1 @@ -60,4 +61,5 @@ require ( golang.org/x/sys v0.8.0 // indirect golang.org/x/text v0.9.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect + moul.io/http2curl v1.0.0 ) diff --git a/go.sum b/go.sum index cc91e2a..cb47a96 100644 --- a/go.sum +++ b/go.sum @@ -243,6 +243,8 @@ github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lN github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/moul/http2curl v1.0.0 h1:dRMWoAtb+ePxMlLkrCbAqh4TlPHXvoGUSQ323/9Zahs= +github.com/moul/http2curl v1.0.0/go.mod h1:8UbvGypXm98wA/IqH45anm5Y2Z6ep6O31QGOAZ3H0fQ= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= @@ -522,5 +524,7 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +moul.io/http2curl v1.0.0 h1:6XwpyZOYsgZJrU8exnG87ncVkU1FVCcTRpwzOkTDUi8= +moul.io/http2curl v1.0.0/go.mod h1:f6cULg+e4Md/oW1cYmwW4IWQOVl2lGbmCNGOHvzX2kE= rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= diff --git a/send/aliSms.go b/send/aliSms.go new file mode 100644 index 0000000..5b6475c --- /dev/null +++ b/send/aliSms.go @@ -0,0 +1,88 @@ +package send + +import ( + "crypto/hmac" + "crypto/sha1" + "encoding/base64" + "fmt" + "math/rand" + "net/url" + "sort" + "strings" + "time" + + "github.com/samber/lo" + "github.com/spf13/cast" +) + +const ( + aliSmsUrl = "http://dysmsapi.aliyuncs.com" +) + +func init() { + registered["aliSms"] = func(conf map[string]string) sender { + return &aliSms{conf: conf} + } +} + +type aliSms struct { + conf map[string]string +} + +// send ali sms +// +// https://help.aliyun.com/zh/sms/developer-reference/api-dysmsapi-2017-05-25-sendsms +func (a *aliSms) send(msg *message) error { + body := map[string]string{ + "PhoneNumbers": strings.Join(msg.Tos, ","), + "SignName": a.conf["signName"], + "TemplateCode": a.conf["templateCode"], + "TemplateParam": msg.Content, + } + req := rc.R(). + SetHeader("Content-Type", "application/x-www-form-urlencoded"). + SetQueryParams(map[string]string{ + "Action": "SendSms", + "Version": "2017-05-25", + "Format": "JSON", + "AccessKeyId": a.conf["accessKey"], + "SignatureNonce": cast.ToString(rand.Int63()), + "Timestamp": time.Now().UTC().Format("2006-01-02T15:04:05Z"), + "SignatureMethod": "HMAC-SHA1", + "SignatureVersion": "1.0", + "AcceptLanguage": "zh-CN", + }). + SetFormData(body) + ks := lo.Keys(body) + for k, _ := range req.QueryParam { + ks = append(ks, k) + } + sort.Strings(ks) + encodeParams := make([]string, 0) + for _, k := range ks { + v, ok := body[k] + if !ok { + v = req.QueryParam.Get(k) + } + encodeParams = append(encodeParams, fmt.Sprintf("%s=%s", url.QueryEscape(k), url.QueryEscape(v))) + } + CanonicalizedQueryString := strings.Join(encodeParams, "&") + stringToSign := fmt.Sprintf("%s&%s&%s", + "POST", + url.QueryEscape("/"), + url.QueryEscape(CanonicalizedQueryString), + ) + hashed := hmac.New(sha1.New, []byte(a.conf["accessSecret"]+"&")) + hashed.Write([]byte(stringToSign)) + + signature := base64.StdEncoding.EncodeToString(hashed.Sum(nil)) + req.SetQueryParam("Signature", signature) + + resp, err := req.Post(aliSmsUrl) + + return handleErr("send to ali sms failed", err, resp, func(dt map[string]any) bool { return dt["Code"] == "OK" }) +} + +func (a *aliSms) getConf() map[string]string { + return a.conf +} diff --git a/send/dingdingBot.go b/send/dingdingBot.go index 69757d7..888ecd4 100644 --- a/send/dingdingBot.go +++ b/send/dingdingBot.go @@ -22,7 +22,7 @@ type dingdingBot struct { conf map[string]string } -// send dingtal bot message +// send dingtalk bot message // // https://open.dingtalk.com/document/orgapp/custom-bot-creation-and-installation func (d *dingdingBot) send(msg *message) error { diff --git a/send/send.go b/send/send.go index 0399422..f6a01b1 100644 --- a/send/send.go +++ b/send/send.go @@ -26,7 +26,7 @@ var ( msgCh = make(chan *message, 10000) confCh = make(chan struct{}, 1) name2sender = make(map[string]sender) - rc = resty.New() + rc = resty.NewWithClient(&http.Client{}) ) type sender interface { From 736b378b83c179a30ac70bde02dbef9280dff763 Mon Sep 17 00:00:00 2001 From: ttk Date: Wed, 10 Jan 2024 14:56:31 +0800 Subject: [PATCH 2/2] fix: go lint --- send/aliSms.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/send/aliSms.go b/send/aliSms.go index 5b6475c..0ba7ab6 100644 --- a/send/aliSms.go +++ b/send/aliSms.go @@ -54,7 +54,7 @@ func (a *aliSms) send(msg *message) error { }). SetFormData(body) ks := lo.Keys(body) - for k, _ := range req.QueryParam { + for k := range req.QueryParam { ks = append(ks, k) } sort.Strings(ks)