/messenger

一个简单轻量的消息发送服务

Primary LanguageGoApache License 2.0Apache-2.0

Messenger

Apache License 2.0 go>=1.18 API

messenger是一个简单轻量的消息发送服务,支持邮件、微信、飞书、钉钉,同时支持配置动态修改,使用简单灵活

安装

源码

cd messenger
sh build.sh
cp conf/confTemplate.yaml conf/conf.yaml # edit your config
./messenger

docker

cd messenger
cp conf/confTemplate.yaml conf/conf.yaml # edit your config
docker build --tag messenger .
docker run -d --name messenger -p 8888:8888 -v $(pwd)/conf:/messenger/conf --restart=always messenger 

API

您也可以通过访问 http://127.0.0.1:8888/swagger/index.html 查看swagger api文档

发送消息

请求方式:POST

请求地址:http://127.0.0.1:8888/v1/message

参数说明:

参数 是否必须 类型 说明
sender string sender名称:发送消息具体sender的名称,对应conf中的name
msgtype string 消息内容类型:每种消息发送方式支持多种消息内容类型,各发送方式支持的消息内容类型参考如下
wechatBot: 微信机器人      wechatApp:微信应用
feishuBot:飞书机器人      feishuApp:飞书应用
dingdingBot:钉钉机器人    dingdingApp:钉钉应用
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)

返回结果:

// 正常 httpStatusCode==200
{
  "msg": "ok"
}

// 异常 httpStatusCode!=200
{
  "msg": "xxxx"
}

请求示例:

curl

curl  -X POST \
  'http://localhost:8888/v1/message' \
  --header 'Content-Type: application/json' \
  --data-raw '{
  "sender": "yourSenderName",
  "msgtype": "text",
  "content": "{\"content\":\"一行文本内容\"}"
}'

python

import json
import requests

reqUrl = "http://localhost:8888/v1/message"

response = requests.post(reqUrl, json={
    "sender": "yourSenderName",
    "msgtype": "text",
    "content": json.dumps({
        "content": "一行文本内容1"
    })
})

print(response.status_code)

golang

package main

import (
	"fmt"

	"github.com/go-resty/resty/v2"
)

func main() {
	reqUrl := "http://localhost:8888/v1/message"

	content, _ := json.Marshal(map[string]any{
		"content": "一行文本内容2",
	})
	resp, err := resty.New().R().
		SetBody(map[string]any{
			"sender":  "yourSenderName",
			"msgtype": "text",
			"content": string(content),
		}).Post(reqUrl)

	fmt.Println(err, resp.StatusCode())
}

更新配置

请求方式:POST PUT DELETE

请求地址:http://127.0.0.1:8888/v1/senders

参数说明:

参数(请求体) 是否必须 类型 说明
body json 请求body为您的sender配置,如{"wechatBot": [{"name": "yourSenderName", "url": "https://xxx"}]}
POST:同类型配置会被全部覆盖
PUT:同类型同名称的配置会被更新,新配置将被添加
DELETE:同类型同名称配置将被删除

返回结果:

// 正常 httpStatusCode==200
{
  "msg": "ok"
}

// 异常 httpStatusCode!=200
{
  "msg": "xxxx"
}

查询用户ID

请求方式:POST

请求地址:http://127.0.0.1:8888/v1/uid/getbyphone

参数说明:

参数 是否必须 类型 说明
sender string 查询用户id时使用的sender名称
phone string 手机号

返回结果:

// 正常 httpStatusCode==200
{
  "uid": "xxxxxxxxxxxxx",
  "msg": "ok"
}

// 异常 httpStatusCode!=200
{
  "msg": "xxxx"
}

查询消息历史

请求方式:GET

请求地址:http://127.0.0.1:8888/v1/histories

参数说明:

参数 是否必须 类型 说明
page_index int 分页序号,从1开始
page_size int 分页每页数量
start int64 起始时间unix时间戳
end int64 结束时间unix时间戳
sender string 发送方式名称
content string 消息内容

返回结果:

// 正常 httpStatusCode==200
{
  "count": 1,
  "list": [
    {
      "CreatedAt": 1705911411,
      "Err": "",
      "Id": 1,
      "Message": "json string of message",
      "ReceivedAt": 1705911410,
      "Req": "curl command of request",
      "Resp": "json string of response body and http code",
      "Status": true
    }
  ],
  "msg": "ok"
}

// 异常 httpStatusCode!=200
{
  "msg": "xxxx"
}

鉴权

当配置文件中开启auths鉴权配置后,请求需要加入鉴权信息,目前支持三种鉴权方式.

IP

发送请求的客户端ip需匹配pattern

token

发送请求中需要添加请求头 X-Token = token in your yaml config

sign

签名鉴权需要添加请求头 X-TS = 当前unix秒数时间戳 X-Nonce = 随机内容 X-Sign = 根据签名算法生成的签名

签名算法步骤为

  1. 将ts和nonce信息加入body中 body["ts"] = X-TS, body["nonce"] = X-Nonce
  2. 将body中的键值对按键排序后拼接
  3. 使用配置文件中的secret计算sha256的值,并将结果进行base64编码,如 secret=666时,步骤二中结果为
  4. 设置请求头中的 X-Sign = 步骤三结果
// golang 签名示例
secret := "666"
body := map[string]any{
"sender":  "wechatBot",
"msgtype": "text",
"content": "{\"content\":\"一行文本内容\"}",
}
body["ts"] = "1695005697" // cast.ToString(time.Now().Unix())
body["nonce"] = "123"
keys := lo.Keys(body)
sort.Strings(keys)

// content{"content":"一行文本内容"}msgtypetextnonce123senderwechatBotts1695005697
kvStr := strings.Join(lo.Map(keys, func(k string, _ int) string { return fmt.Sprintf("%s%s", k, body[k]) }), "")

mac := hmac.New(sha256.New, []byte(secret))
_, _ = mac.Write([]byte(kvStr))

// nAW4/1vz8EjdJEVXqTevmX7yBOzQtUti1Z2TIgAxogc=
sign := base64.StdEncoding.EncodeToString(mac.Sum(nil))

配置说明

yaml配置文件定义了

  1. app 服务配置ip的、端口
  2. auths 鉴权方式。多种鉴权方式同时配置时,按配置先后进行检查,满足任意一种方式即通过鉴权。支持的鉴权方式为
    • ip
    • token
    • sign签名
  3. senders 具体发送方式。senders支持动态增删,即在服务已经启动的情况下可以直接修改senders列表,服务会持续读取最新的改动。支持的发送方式类型
    • email
    • wechatBot
    • wechatApp
    • feishuBot
    • feishuApp
    • dingdingBot
    • dingdingApp
    • aliSms
app:
  ip:
  port: 8888

auths:
  # - type: ip
  #   pattern: 192.168.*.*

  # - type: token
  #   token: your token

  # - type: sign
  #   secret: your secret

senders:
  email:
    # - name: yourSenderName1
    #   host: mail.xxx.com
    #   port: 25
    #   account: test@xxx.com
    #   password: #无密码时留空即可
    #   tls: "false"
  wechatBot:
    # - name: yourSenderName2
    #   url: https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=xxxxx
  wechatApp:
    # - name: yourSenderName3
    #   corpid: xxxx
    #   agentid: xxxx
    #   corpsecret: xxxx
  feishuBot:
    # - name: yourSenderName4
    #   url: https://open.feishu.cn/open-apis/bot/v2/hook/xxxxxx
  feishuApp:
    # - name: yourSenderName5
    #   app_id: cli_xxxx
    #   app_secret: xxxx
  dingdingBot:
    # - name: yourSenderName6
    #   url: https://oapi.dingding.com/robot/send?access_token=xxxx
    #   token: xxxx #仅加密方式为加签时填写
  dingdingApp:
    # - name: yourSenderName7
    #   appKey: xxxx
    #   appSecret: xxxx
    #   robotCode: xxxx
  aliSms:
    # - name: yourSenderName8
    #   accessKey: xxxx
    #   accessSecret: xxxx
    #   templateCode: SMS_123456789
    #   signName: xxxx

自定义发送

通常情况下,以上7中方式能满足大部分需求,但是如果你想要定制自己的sender,可以按如下步骤进行开发

  1. 在send目录下创建你的sender文件,如mysender.go
  2. 定义mysender结构体并实现sender接口
    type sender interface {
        send(*message) error
        getConf() map[string]string
    }
  3. 新增init方法将你的sender注册到后台goroutine中,registered的key mysender即为你的sender的类型,可以在配置文件中使用。通常建议将文件名、结构体名、类型名保持一直
    func init() {
        registered["mysender"] = func(conf config) sender {
            return &wechatBot{conf: conf}
        }
    }

Web

http://127.0.0.1:8888/web