diff --git a/README.md b/README.md index c566cd5..a6acf98 100644 --- a/README.md +++ b/README.md @@ -37,30 +37,36 @@ - [Server 酱](https://sct.ftqq.com/) - [自定义 Bark 服务](https://github.com/Finb/Bark) - [自定义 Webhook 服务](./examples/webhook) +- [pushplus](https://pushplus.plus/) +- [蓝信](https://developer.lanxin.cn/official/article?id=646ecae03d4e4adb7039c0e4&module=development-help&article_id=646f193b3d4e4adb7039c21c) ### 使用 Docker Docker 方式推荐使用环境变量来配置服务参数 -| 环境变量名 | 说明 | 默认值 | -|-------------------------|-------------------------------------------------------------------------|-----------------------------------------| -| `DB_CONN` | 数据库链接字符串,详情见 [数据库连接](#数据库连接) | `sqlite3://vuln_v3.sqlite3` | -| `DINGDING_ACCESS_TOKEN` | 钉钉机器人 url 的 `access_token` 部分 | | -| `DINGDING_SECRET` | 钉钉机器人的加签值 (仅支持加签方式) | | -| `LARK_ACCESS_TOKEN` | 飞书机器人 url 的 `/open-apis/bot/v2/hook/` 后的部分, 也支持直接指定完整的 url 来访问私有部署的飞书 | | -| `LARK_SECRET` | 飞书机器人的加签值 (仅支持加签方式) | | -| `WECHATWORK_KEY ` | 微信机器人 url 的 `key` 部分 | | -| `SERVERCHAN_KEY ` | Server酱的 `SCKEY` | | -| `WEBHOOK_URL` | 自定义 webhook 服务的完整 url | | -| `BARK_URL` | Bark 服务的完整 url, 路径需要包含 DeviceKey | | -| `TELEGRAM_BOT_TOKEN` | Telegram Bot Token | | -| `TELEGRAM_CHAT_IDS` | Telegram Bot 需要发送给的 chat 列表,使用 `,` 分割 | | +| 环境变量名 | 说明 | 默认值 | +| ----------------------- | ------------------------------------------------------------ | --------------------------------------- | +| `DB_CONN` | 数据库链接字符串,详情见 [数据库连接](#数据库连接) | `sqlite3://vuln_v3.sqlite3` | +| `DINGDING_ACCESS_TOKEN` | 钉钉机器人 url 的 `access_token` 部分 | | +| `DINGDING_SECRET` | 钉钉机器人的加签值 (仅支持加签方式) | | +| `LARK_ACCESS_TOKEN` | 飞书机器人 url 的 `/open-apis/bot/v2/hook/` 后的部分, 也支持直接指定完整的 url 来访问私有部署的飞书 | | +| `LARK_SECRET` | 飞书机器人的加签值 (仅支持加签方式) | | +| `WECHATWORK_KEY ` | 微信机器人 url 的 `key` 部分 | | +| `SERVERCHAN_KEY ` | Server酱的 `SCKEY` | | +| `WEBHOOK_URL` | 自定义 webhook 服务的完整 url | | +| `BARK_URL` | Bark 服务的完整 url, 路径需要包含 DeviceKey | | +| `PUSHPLUS_KEY` | PushPlus的token | | +| `LANXIN_DOMAIN` | 蓝信webhook机器人的域名 | | +| `LANXIN_TOKEN` | 蓝信webhook机器人的hook token | | +| `LANXIN_SECRET` | 蓝信webhook机器人的签名 | | +| `TELEGRAM_BOT_TOKEN` | Telegram Bot Token | | +| `TELEGRAM_CHAT_IDS` | Telegram Bot 需要发送给的 chat 列表,使用 `,` 分割 | | | `SOURCES` | 启用哪些漏洞信息源,逗号分隔, 可选 `avd`, `ti`, `oscs`, `seebug`,`threatbook`,`struts2` | `avd,ti,oscs,threatbook,seebug,struts2` | -| `INTERVAL` | 检查周期,支持秒 `60s`, 分钟 `10m`, 小时 `1h`, 最低 `1m` | `30m` | -| `ENABLE_CVE_FILTER` | 启用 CVE 过滤,开启后多个数据源的统一 CVE 将只推送一次 | `true` | -| `NO_FILTER` | 禁用上述推送过滤策略,所有新发现的漏洞都会被推送 | `false` | -| `NO_START_MESSAGE` | 禁用服务启动的提示信息 | `false` | -| `HTTPS_PROXY` | 给所有请求配置代理, 支持 `socks5://xxxx` 或者 `http(s)://xxkx` | | +| `INTERVAL` | 检查周期,支持秒 `60s`, 分钟 `10m`, 小时 `1h`, 最低 `1m` | `30m` | +| `ENABLE_CVE_FILTER` | 启用 CVE 过滤,开启后多个数据源的统一 CVE 将只推送一次 | `true` | +| `NO_FILTER` | 禁用上述推送过滤策略,所有新发现的漏洞都会被推送 | `false` | +| `NO_START_MESSAGE` | 禁用服务启动的提示信息 | `false` | +| `HTTPS_PROXY` | 给所有请求配置代理, 支持 `socks5://xxxx` 或者 `http(s)://xxkx` | | 比如使用钉钉机器人 @@ -104,6 +110,30 @@ docker run --restart always -d \ +
使用PushPlus + +```bash +docker run --restart always -d \ + -e PUSHPLUS_KEY=xxx \ + -e INTERVAL=30m \ + zemal/watchvuln:latest +``` + +
+ +
使用蓝信Webhook机器人 + +```bash +docker run --restart always -d \ + -e LANXIN_DOMAIN=xxx \ + -e LANXIN_TOKEN=xxx \ + -e LANXIN_SECRET=xxx \ + -e INTERVAL=30m \ + zemal/watchvuln:latest +``` + +
+
使用Telegram 机器人 ```bash @@ -184,8 +214,12 @@ GLOBAL OPTIONS: --bark-url value, --bark value your bark server url, ex: http://127.0.0.1:1111/DeviceKey --dingding-access-token value, --dt value webhook access token of dingding bot --dingding-sign-secret value, --ds value sign secret of dingding bot - --lark-access-token value, --lt value webhook access token of lark + --lanxin-domain value, --lxd value your lanxin server url, ex: https://apigw-example.domain + --lanxin-hook-token value, --lxt value lanxin hook token + --lanxin-sign-secret value, --lxs value sign secret of lanxin + --lark-access-token value, --lt value webhook access token/url of lark --lark-sign-secret value, --ls value sign secret of lark + --pushplus-key value, --pk value send key for push plus --serverchan-key value, --sk value send key for server chan --telegram-bot-token value, --tgtk value telegram bot token, ex: 123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11 --telegram-chat-ids value, --tgids value chat ids want to send on telegram, ex: 123456,4312341,123123 @@ -220,7 +254,6 @@ $ ./watchvuln --dt DINGDING_ACCESS_TOKEN --ds DINGDING_SECRET -i 30m ```bash $ ./watchvuln --lt LARK_ACCESS_TOKEN --ls LARK_SECRET -i 30m - ```
@@ -241,6 +274,22 @@ $ ./watchvuln --sk xxxx -i 30m +
使用PushPlus + +``` +$ ./watchvuln --pk xxxx -i 30m +``` + +
+ +
使用蓝信Webhook机器人 + +``` +$ ./watchvuln --lxd xxxx --lxt xxx --lxs xxx -i 30m +``` + +
+
使用Telegram 机器人 ``` diff --git a/go.mod b/go.mod index 85c8040..41bc794 100644 --- a/go.mod +++ b/go.mod @@ -22,7 +22,7 @@ require ( github.com/stretchr/testify v1.8.4 github.com/urfave/cli/v2 v2.26.0 github.com/vimsucks/wxwork-bot-go v0.0.0-20221213061339-fcbcd88ede1c - golang.org/x/net v0.21.0 + golang.org/x/net v0.22.0 golang.org/x/sync v0.5.0 modernc.org/sqlite v1.28.0 ) @@ -40,6 +40,7 @@ require ( github.com/dlclark/regexp2 v1.10.0 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/go-openapi/inflect v0.19.0 // indirect + github.com/go-resty/resty/v2 v2.12.0 // indirect github.com/go-sourcemap/sourcemap v2.1.3+incompatible // indirect github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect github.com/golang/protobuf v1.5.3 // indirect diff --git a/go.sum b/go.sum index 6ae3b26..b077796 100644 --- a/go.sum +++ b/go.sum @@ -48,6 +48,8 @@ github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+m github.com/go-logr/logr v1.3.0 h1:2y3SDp0ZXuc6/cjLSZ+Q3ir+QB9T/iG5yYRXqsagWSY= github.com/go-openapi/inflect v0.19.0 h1:9jCH9scKIbHeV9m12SmPilScz6krDxKRasNNSNPXu/4= github.com/go-openapi/inflect v0.19.0/go.mod h1:lHpZVlpIQqLyKwJ4N+YSc9hchQy/i12fJykb83CRBH4= +github.com/go-resty/resty/v2 v2.12.0 h1:rsVL8P90LFvkUYq/V5BTVe203WfRIU4gvcf+yfzJzGA= +github.com/go-resty/resty/v2 v2.12.0/go.mod h1:o0yGPrkS3lOe1+eFajk6kBW8ScXzwU3hD69/gt2yB/0= github.com/go-sourcemap/sourcemap v2.1.3+incompatible h1:W1iEw64niKVGogNgBN3ePyLFfuisuzeidWPMPWmECqU= github.com/go-sourcemap/sourcemap v2.1.3+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg= github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI= @@ -175,6 +177,7 @@ go.uber.org/mock v0.3.0 h1:3mUxI1No2/60yUYax92Pt8eNOEecx2D3lcXZh2NEZJo= go.uber.org/mock v0.3.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= golang.org/x/exp v0.0.0-20231214170342-aacd6d4b4611 h1:qCEDpW1G+vcj3Y7Fy52pEM1AWm3abj8WimGYejI3SC4= @@ -191,8 +194,11 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc= +golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= golang.org/x/oauth2 v0.8.0 h1:6dkIjl3j3LtZ/O3sTgZTMsLKSftL/B8Zgq4huOIIUu8= golang.org/x/oauth2 v0.8.0/go.mod h1:yr7u4HXZRm1R1kBWqr/xKNqewf0plRYoB7sla+BCIXE= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -211,12 +217,17 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= +golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= @@ -227,6 +238,7 @@ golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= diff --git a/main.go b/main.go index 80503bf..71dcc9d 100644 --- a/main.go +++ b/main.go @@ -77,12 +77,36 @@ func main() { Usage: "send key for server chan", Category: "[\x00Push Options]", }, + &cli.StringFlag{ + Name: "pushplus-key", + Aliases: []string{"pk"}, + Usage: "send key for push plus", + Category: "[\x00Push Options]", + }, &cli.StringFlag{ Name: "webhook-url", Aliases: []string{"webhook"}, Usage: "your webhook server url, ex: http://127.0.0.1:1111/webhook", Category: "[\x00Push Options]", }, + &cli.StringFlag{ + Name: "lanxin-domain", + Aliases: []string{"lxd"}, + Usage: "your lanxin server url, ex: https://apigw-example.domain", + Category: "[\x00Push Options]", + }, + &cli.StringFlag{ + Name: "lanxin-hook-token", + Aliases: []string{"lxt"}, + Usage: "lanxin hook token", + Category: "[\x00Push Options]", + }, + &cli.StringFlag{ + Name: "lanxin-sign-secret", + Aliases: []string{"lxs"}, + Usage: "sign secret of lanxin", + Category: "[\x00Push Options]", + }, &cli.StringFlag{ Name: "bark-url", Aliases: []string{"bark"}, @@ -265,10 +289,14 @@ func initPusher(c *cli.Context) (push.TextPusher, push.RawPusher, error) { dingSecret := c.String("dingding-sign-secret") wxWorkKey := c.String("wechatwork-key") webhook := c.String("webhook-url") + lanxinDomain := c.String("lanxin-domain") + lanxinToken := c.String("lanxin-hook-token") + lanxinSecret := c.String("lanxin-sign-secret") bark := c.String("bark-url") larkToken := c.String("lark-access-token") larkSecret := c.String("lark-sign-secret") serverChanKey := c.String("serverchan-key") + pushPlusKey := c.String("pushplus-key") telegramBotTokey := c.String("telegram-bot-token") telegramChatIDs := c.String("telegram-chat-ids") @@ -284,6 +312,15 @@ func initPusher(c *cli.Context) (push.TextPusher, push.RawPusher, error) { if os.Getenv("WEBHOOK_URL") != "" { webhook = os.Getenv("WEBHOOK_URL") } + if os.Getenv("LANXIN_DOMAIN") != "" { + lanxinDomain = os.Getenv("LANXIN_DOMAIN") + } + if os.Getenv("LANXIN_TOKEN") != "" { + lanxinToken = os.Getenv("LANXIN_TOKEN") + } + if os.Getenv("LANXIN_SECRET") != "" { + lanxinSecret = os.Getenv("LANXIN_SECRET") + } if os.Getenv("BARK_URL") != "" { bark = os.Getenv("BARK_URL") } @@ -296,6 +333,9 @@ func initPusher(c *cli.Context) (push.TextPusher, push.RawPusher, error) { if os.Getenv("SERVERCHAN_KEY") != "" { serverChanKey = os.Getenv("SERVERCHAN_KEY") } + if os.Getenv("PUSHPLUS_KEY") != "" { + pushPlusKey = os.Getenv("PUSHPLUS_KEY") + } if os.Getenv("TELEGRAM_BOT_TOKEN") != "" { telegramBotTokey = os.Getenv("TELEGRAM_BOT_TOKEN") } @@ -317,6 +357,9 @@ func initPusher(c *cli.Context) (push.TextPusher, push.RawPusher, error) { if webhook != "" { rawPusher = append(rawPusher, push.NewWebhook(webhook)) } + if lanxinDomain != "" && lanxinToken != "" && lanxinSecret != "" { + textPusher = append(textPusher, push.NewLanxin(lanxinDomain, lanxinToken, lanxinSecret)) + } if bark != "" { deviceKeys := strings.Split(bark, "/") deviceKey := deviceKeys[len(deviceKeys)-1] @@ -326,6 +369,9 @@ func initPusher(c *cli.Context) (push.TextPusher, push.RawPusher, error) { if serverChanKey != "" { textPusher = append(textPusher, push.NewServerChan(serverChanKey)) } + if pushPlusKey != "" { + textPusher = append(textPusher, push.NewPushPlus(pushPlusKey)) + } if telegramBotTokey != "" && telegramChatIDs != "" { tgPusher, err := push.NewTelegram(telegramBotTokey, telegramChatIDs) if err != nil { diff --git a/push/lanxin.go b/push/lanxin.go new file mode 100644 index 0000000..46cc33c --- /dev/null +++ b/push/lanxin.go @@ -0,0 +1,148 @@ +package push + +import ( + "bytes" + "crypto/hmac" + "crypto/sha256" + "encoding/base64" + "encoding/json" + "fmt" + "github.com/kataras/golog" + "github.com/pkg/errors" + "io" + "net/http" + "strconv" + "time" +) + +var _ = TextPusher(&LanXin{}) + +type LanXin struct { + domain string + token string + secret string + log *golog.Logger +} + +func NewLanxin(domain string, token string, secret string) TextPusher { + return &LanXin{ + domain: domain, + token: token, + secret: secret, + log: golog.Child("[pusher-lanxin]"), + } +} + +type MessageData struct { + Text struct { + Content string `json:"content"` + } `json:"text"` +} + +type LanXinMessage struct { + Sign string `json:"sign"` + Timestamp string `json:"timestamp"` + MsgType string `json:"msgType"` + MsgData MessageData `json:"msgData"` +} + +type LanXinResponse struct { + ErrCode int `json:"errCode"` + ErrMsg string `json:"errMsg"` + Data struct { + MsgID string `json:"msgId"` + } `json:"data"` +} + +func GenSign(secret string, timestamp int64) string { + stringToSign := fmt.Sprintf("%v", timestamp) + "@" + secret + fmt.Println(stringToSign) + + h := hmac.New(sha256.New, []byte(stringToSign)) + signature := base64.StdEncoding.EncodeToString(h.Sum(nil)) + + fmt.Println(signature) + return signature +} + +// func (m *LanXin) PushRaw(r *RawMessage) error { +// m.log.Infof("sending lanxin data %s, %v", r.Type, r.Content) +// resp, err := m.Send(r.Content.(string)) +// if err != nil { +// return err +// } +// m.log.Infof("raw response from server: %s", resp.Data.MsgID) +// return nil +// } + +func (m *LanXin) PushMarkdown(title, content string) error { + m.log.Infof("sending markdown %s", title) + _, err := m.Send(content) + if err != nil { + return errors.Wrap(err, "lanxin") + } + + return nil +} + +func (m *LanXin) PushText(s string) error { + m.log.Infof("sending text %s", s) + _, err := m.Send(s) + if err != nil { + return errors.Wrap(err, "lanxin") + } + + return nil +} + +func (m *LanXin) Send(content string) (response *LanXinResponse, error error) { + res := &LanXinResponse{} + + if len(m.domain) == 0 { + return res, errors.New("invalid domain") + } + + if len(m.token) == 0 { + return res, errors.New("invalid token") + } + + if len(m.secret) == 0 { + return res, errors.New("invalid secret") + } + + url := m.domain + "/v1/bot/hook/messages/create?hook_token=" + m.token + now := time.Now().Unix() // 获取当前时间戳 + msg := LanXinMessage{ + Sign: GenSign(m.secret, now), + Timestamp: strconv.FormatInt(now, 10), + MsgType: "text", + MsgData: MessageData{ + Text: struct { + Content string `json:"content"` + }{ + Content: content, + }, + }, + } + + payload, _ := json.Marshal(msg) + req, _ := http.NewRequest("POST", url, bytes.NewBuffer(payload)) + req.Header.Set("Content-Type", "application/json") + + client := &http.Client{} + resp, _ := client.Do(req) + + data, err := io.ReadAll(resp.Body) + if err != nil { + return nil, errors.Wrap(err, "read body") + } + defer resp.Body.Close() + err = json.Unmarshal(data, res) + if err != nil { + return res, errors.New("json格式好数据失败") + } + if res.ErrCode == 0 { + return res, nil + } + return res, errors.New(res.ErrMsg) +} diff --git a/push/lanxin_test.go b/push/lanxin_test.go new file mode 100644 index 0000000..dc3ae16 --- /dev/null +++ b/push/lanxin_test.go @@ -0,0 +1,28 @@ +package push + +import ( + "fmt" + "testing" + + "github.com/kataras/golog" + "github.com/stretchr/testify/assert" +) + +var lanxin = newLanxin() + +func newLanxin() *LanXin { + return &LanXin{ + // 下面要填入能用的domain、token和secret + domain: "", + token: "", + secret: "", + log: golog.Child("[pusher-lanxin]"), + } +} + +func TestLanxinSendText(t *testing.T) { + result, err := lanxin.Send("6666") + fmt.Println(result) + assert.Nil(t, err) + assert.Contains(t, result.ErrMsg, "OK") +} diff --git a/push/pushplus.go b/push/pushplus.go new file mode 100644 index 0000000..cf0884c --- /dev/null +++ b/push/pushplus.go @@ -0,0 +1,93 @@ +package push + +import ( + "encoding/json" + "fmt" + "github.com/go-resty/resty/v2" + "github.com/kataras/golog" + "github.com/pkg/errors" +) + +type PushPlusMessage struct { + Token string `json:"token"` + Title string `json:"title" describe:"消息标题"` + Content string `json:"content" describe:"具体消息内容,根据不同template支持不同格式"` + Template string `json:"template" describe:"发送消息模板"` +} + +type PushPlusResponse struct { + Code int `json:"code"` + Msg string `json:"msg"` + Data string `json:"data"` +} + +const URI = "https://www.pushplus.plus/send" + +var _ = TextPusher(&PushPlus{}) + +type PushPlus struct { + token string + log *golog.Logger +} + +func NewPushPlus(token string) TextPusher { + return &PushPlus{ + token: token, + log: golog.Child("[pusher-push-plus]"), + } +} + +func (r *PushPlus) Send(message PushPlusMessage) (response *PushPlusResponse, error error) { + res := &PushPlusResponse{} + message.Token = r.token + + if len(message.Token) == 0 { + return res, errors.New("invalid token") + } + + result, err := resty.New().R().SetBody(message).SetHeader("Content-Type", "application/json").Post(URI) + + if err != nil { + return res, errors.New(fmt.Sprintf("请求失败:%s", err.Error())) + } + err = json.Unmarshal(result.Body(), res) + if err != nil { + return res, errors.New("json 格式化数据失败") + } + + if res.Code == 200 { + return res, nil + } + + return res, errors.New(res.Msg) +} + +func (d *PushPlus) PushText(s string) error { + d.log.Infof("sending text %s", s) + message := PushPlusMessage{ + Title: "", + Content: s, + Template: "txt", + } + + _, err := d.Send(message) + if err != nil { + return errors.Wrap(err, "push-plus") + } + return nil +} + +func (d *PushPlus) PushMarkdown(title, content string) error { + d.log.Infof("sending markdown %s", title) + message := PushPlusMessage{ + Title: title, + Content: content, + Template: "markdown", + } + + _, err := d.Send(message) + if err != nil { + return errors.Wrap(err, "push-plus") + } + return nil +} diff --git a/push/pushplus_test.go b/push/pushplus_test.go new file mode 100644 index 0000000..35d6ee0 --- /dev/null +++ b/push/pushplus_test.go @@ -0,0 +1,41 @@ +package push + +import ( + "testing" + + "github.com/kataras/golog" + "github.com/stretchr/testify/assert" +) + +var pushplus = newPushPlus() + +func newPushPlus() *PushPlus { + return &PushPlus{ + token: "", // 这里输入可用的key + log: golog.Child("[pusher-push-plus]"), + } +} + +func TestPushPlusSendTxt(t *testing.T) { + message := PushPlusMessage{ + Title: "test1", + Content: `

纯文本内容

`, + Template: "txt", + } + + result, err1 := pushplus.Send(message) + assert.Nil(t, err1) + assert.Contains(t, result.Msg, "请求成功") +} + +func TestPushPlusSendMarkdown(t *testing.T) { + message := PushPlusMessage{ + Title: "test", + Content: "# 内容", + Template: "markdown", + } + + result, err := pushplus.Send(message) + assert.Nil(t, err) + assert.Contains(t, result.Msg, "请求成功") +}