diff --git a/notify/manager.go b/notify/manager.go
index 95d5756..a235d98 100644
--- a/notify/manager.go
+++ b/notify/manager.go
@@ -36,6 +36,7 @@ func NewManager(senders ...Sender) *Manager {
return manager
}
+// Manager 通知管理器,可用于将通知同时发送至多个渠道
type Manager struct {
senders []Sender
notifyChannel chan Notify
@@ -47,6 +48,7 @@ func (slf *Manager) PushNotify(notify Notify) {
slf.notifyChannel <- notify
}
+// Release 释放通知管理器
func (slf *Manager) Release() {
slf.closeChannel <- struct{}{}
}
diff --git a/notify/notifies/feishu.go b/notify/notifies/feishu.go
index 7aac1b6..c86c85a 100644
--- a/notify/notifies/feishu.go
+++ b/notify/notifies/feishu.go
@@ -2,34 +2,22 @@ package notifies
import "encoding/json"
-const (
- FeiShuMsgTypeText = "text" // 文本
- FeiShuMsgTypeRichText = "post" // 富文本
- FeiShuMsgTypeImage = "image" // 图片
- FeiShuMsgTypeInteractive = "interactive" // 消息卡片
- FeiShuMsgTypeShareChat = "share_chat" // 分享群名片
- FeiShuMsgTypeShareUser = "share_user" // 分享个人名片
- FeiShuMsgTypeAudio = "audio" // 音频
- FeiShuMsgTypeMedia = "media" // 视频
- FeiShuMsgTypeFile = "file" // 文件
- FeiShuMsgTypeSticker = "sticker" // 表情包
-
-)
-
-func NewFeiShu(msgType, receiveId, content string) *FeiShu {
- return &FeiShu{
- ReceiveId: receiveId,
- Content: content,
- MsgType: msgType,
+// NewFeiShu 创建飞书通知消息
+func NewFeiShu(message FeiShuMessage) *FeiShu {
+ feishu := &FeiShu{
+ Content: map[string]any{},
}
+ message(feishu)
+ return feishu
}
+// FeiShu 飞书通知消息
type FeiShu struct {
- ReceiveId string `json:"receive_id"`
- Content string `json:"content"`
- MsgType string `json:"msg_type"`
+ Content any `json:"content"`
+ MsgType string `json:"msg_type"`
}
+// Format 格式化通知内容
func (slf *FeiShu) Format() (string, error) {
data, err := json.Marshal(slf)
if err != nil {
diff --git a/notify/notifies/feishu_messages.go b/notify/notifies/feishu_messages.go
new file mode 100644
index 0000000..e659534
--- /dev/null
+++ b/notify/notifies/feishu_messages.go
@@ -0,0 +1,169 @@
+package notifies
+
+const (
+ FeiShuMsgTypeText = "text" // 文本
+ FeiShuMsgTypeRichText = "post" // 富文本
+ FeiShuMsgTypeImage = "image" // 图片
+ FeiShuMsgTypeInteractive = "interactive" // 消息卡片
+ FeiShuMsgTypeShareChat = "share_chat" // 分享群名片
+ FeiShuMsgTypeShareUser = "share_user" // 分享个人名片
+ FeiShuMsgTypeAudio = "audio" // 音频
+ FeiShuMsgTypeMedia = "media" // 视频
+ FeiShuMsgTypeFile = "file" // 文件
+ FeiShuMsgTypeSticker = "sticker" // 表情包
+)
+
+const (
+ FeiShuStyleBold = "bold" // 加粗
+ FeiShuStyleUnderline = "underline" // 下划线
+ FeiShuStyleLineThrough = "lineThrough" // 删除线
+ FeiShuStyleItalic = "italic" // 斜体
+)
+
+type FeiShuMessage func(feishu *FeiShu)
+
+// FeiShuMessageWithText 飞书文本消息
+// - 支持通过换行符进行消息换行
+// - 支持通过 名字 进行@用户
+// - 支持通过 所有人 进行@所有人(必须满足所在群开启@所有人功能。)
+//
+// 支持加粗、斜体、下划线、删除线四种样式,可嵌套使用:
+// - 加粗: 文本示例
+// - 斜体: 文本示例
+// - 下划线 : 文本示例
+// - 删除线: 文本示例
+//
+// 超链接使用说明
+// - 超链接的使用格式为[文本](链接), 如[Feishu Open Platform](https://open.feishu.cn) 。
+// - 请确保链接是合法的,否则会以原始内容发送消息。
+func FeiShuMessageWithText(text string) FeiShuMessage {
+ return func(feishu *FeiShu) {
+ feishu.Content = struct {
+ Text string `json:"text"`
+ }{text}
+ feishu.MsgType = FeiShuMsgTypeText
+ }
+}
+
+// FeiShuMessageWithRichText 飞书富文本消息
+func FeiShuMessageWithRichText(richText *FeiShuRichText) FeiShuMessage {
+ return func(feishu *FeiShu) {
+ feishu.Content = struct {
+ Post any `json:"post,omitempty"`
+ }{richText.content}
+ feishu.MsgType = FeiShuMsgTypeRichText
+ }
+}
+
+// FeiShuMessageWithImage 飞书图片消息
+// - imageKey 可通过上传图片接口获取
+func FeiShuMessageWithImage(imageKey string) FeiShuMessage {
+ return func(feishu *FeiShu) {
+ feishu.Content = struct {
+ ImageKey string `json:"imageKey"`
+ }{imageKey}
+ feishu.MsgType = FeiShuMsgTypeImage
+ }
+}
+
+// FeiShuMessageWithInteractive 飞书卡片消息
+// - json 表示卡片的 json 数据或者消息模板的 json 数据
+func FeiShuMessageWithInteractive(json string) FeiShuMessage {
+ return func(feishu *FeiShu) {
+ feishu.Content = json
+ feishu.MsgType = FeiShuMsgTypeInteractive
+ }
+}
+
+// FeiShuMessageWithShareChat 飞书分享群名片
+// - chatId 群ID获取方式请参见群ID说明
+//
+// 群ID说明:https://open.feishu.cn/document/server-docs/group/chat/chat-id-description
+func FeiShuMessageWithShareChat(chatId string) FeiShuMessage {
+ return func(feishu *FeiShu) {
+ feishu.Content = struct {
+ ChatID string `json:"chat_id"`
+ }{chatId}
+ feishu.MsgType = FeiShuMsgTypeShareChat
+ }
+}
+
+// FeiShuMessageWithShareUser 飞书分享个人名片
+// - userId 表示用户的 OpenID 获取方式请参见了解更多:如何获取 Open ID
+//
+// 如何获取 Open ID:https://open.feishu.cn/document/faq/trouble-shooting/how-to-obtain-openid
+func FeiShuMessageWithShareUser(userId string) FeiShuMessage {
+ return func(feishu *FeiShu) {
+ feishu.Content = struct {
+ UserID string `json:"user_id"`
+ }{userId}
+ feishu.MsgType = FeiShuMsgTypeShareUser
+ }
+}
+
+// FeiShuMessageWithAudio 飞书语音消息
+// - fileKey 语音文件Key,可通过上传文件接口获取
+//
+// 上传文件:https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/reference/im-v1/file/create
+func FeiShuMessageWithAudio(fileKey string) FeiShuMessage {
+ return func(feishu *FeiShu) {
+ feishu.Content = struct {
+ FileKey string `json:"file_key"`
+ }{fileKey}
+ feishu.MsgType = FeiShuMsgTypeAudio
+ }
+}
+
+// FeiShuMessageWithMedia 飞书视频消息
+// - fileKey 视频文件Key,可通过上传文件接口获取
+//
+// 上传文件:https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/reference/im-v1/file/create
+func FeiShuMessageWithMedia(fileKey string) FeiShuMessage {
+ return func(feishu *FeiShu) {
+ feishu.Content = struct {
+ FileKey string `json:"file_key"`
+ }{fileKey}
+ feishu.MsgType = FeiShuMsgTypeMedia
+ }
+}
+
+// FeiShuMessageWithMediaAndCover 飞书带封面的视频消息
+// - fileKey 视频文件Key,可通过上传文件接口获取
+// - imageKey 图片文件Key,可通过上传文件接口获取
+//
+// 上传文件:https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/reference/im-v1/file/create
+func FeiShuMessageWithMediaAndCover(fileKey, imageKey string) FeiShuMessage {
+ return func(feishu *FeiShu) {
+ feishu.Content = struct {
+ FileKey string `json:"file_key"`
+ ImageKey string `json:"image_key"`
+ }{fileKey, imageKey}
+ feishu.MsgType = FeiShuMsgTypeMedia
+ }
+}
+
+// FeiShuMessageWithFile 飞书文件消息
+// - fileKey 文件Key,可通过上传文件接口获取
+//
+// 上传文件:https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/reference/im-v1/file/create
+func FeiShuMessageWithFile(fileKey string) FeiShuMessage {
+ return func(feishu *FeiShu) {
+ feishu.Content = struct {
+ FileKey string `json:"file_key"`
+ }{fileKey}
+ feishu.MsgType = FeiShuMsgTypeFile
+ }
+}
+
+// FeiShuMessageWithSticker 飞书表情包消息
+// - fileKey 表情包文件Key,目前仅支持发送机器人收到的表情包,可通过接收消息事件的推送获取表情包 file_key。
+//
+// 接收消息事件:https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/reference/im-v1/message/events/receive
+func FeiShuMessageWithSticker(fileKey string) FeiShuMessage {
+ return func(feishu *FeiShu) {
+ feishu.Content = struct {
+ FileKey string `json:"file_key"`
+ }{fileKey}
+ feishu.MsgType = FeiShuMsgTypeSticker
+ }
+}
diff --git a/notify/notifies/feishu_rich_text.go b/notify/notifies/feishu_rich_text.go
new file mode 100644
index 0000000..59d5c3c
--- /dev/null
+++ b/notify/notifies/feishu_rich_text.go
@@ -0,0 +1,147 @@
+package notifies
+
+// NewFeiShuRichText 创建一个飞书富文本
+func NewFeiShuRichText() *FeiShuRichText {
+ return &FeiShuRichText{
+ content: map[string]*FeiShuRichTextContent{},
+ }
+}
+
+// FeiShuRichText 飞书富文本结构
+type FeiShuRichText struct {
+ content map[string]*FeiShuRichTextContent
+}
+
+// Create 创建一个特定语言和标题的富文本内容
+func (slf *FeiShuRichText) Create(lang, title string) *FeiShuRichTextContent {
+ content := &FeiShuRichTextContent{
+ richText: slf,
+ Title: title,
+ Content: make([][]map[string]any, 1),
+ }
+ slf.content[lang] = content
+ return content
+}
+
+// FeiShuRichTextContent 飞书富文本内容体
+type FeiShuRichTextContent struct {
+ richText *FeiShuRichText
+ Title string `json:"title,omitempty"`
+ Content [][]map[string]any `json:"content,omitempty"`
+}
+
+// AddText 添加文本
+func (slf *FeiShuRichTextContent) AddText(text string, styles ...string) *FeiShuRichTextContent {
+ content := map[string]any{
+ "tag": "text",
+ "text": text,
+ "style": styles,
+ }
+ slf.Content[0] = append(slf.Content[0], content)
+ return slf
+}
+
+// AddUnescapeText 添加 unescape 解码的文本
+func (slf *FeiShuRichTextContent) AddUnescapeText(text string, styles ...string) *FeiShuRichTextContent {
+ content := map[string]any{
+ "tag": "text",
+ "text": text,
+ "un_escape": true,
+ "style": styles,
+ }
+ slf.Content[0] = append(slf.Content[0], content)
+ return slf
+}
+
+// AddLink 添加超链接文本
+// - 请确保链接地址的合法性,否则消息会发送失败
+func (slf *FeiShuRichTextContent) AddLink(text, href string, styles ...string) *FeiShuRichTextContent {
+ content := map[string]any{
+ "tag": "a",
+ "text": text,
+ "href": href,
+ "style": styles,
+ }
+ slf.Content[0] = append(slf.Content[0], content)
+ return slf
+}
+
+// AddAt 添加@的用户
+// - @单个用户时,userId 字段必须是有效值
+// - @所有人填"all"。
+func (slf *FeiShuRichTextContent) AddAt(userId string, styles ...string) *FeiShuRichTextContent {
+ content := map[string]any{
+ "tag": "at",
+ "user_id": userId,
+ "style": styles,
+ }
+ slf.Content[0] = append(slf.Content[0], content)
+ return slf
+}
+
+// AddAtWithUsername 添加包含用户名的@用户
+// - @单个用户时,userId 字段必须是有效值
+// - @所有人填"all"。
+func (slf *FeiShuRichTextContent) AddAtWithUsername(userId, username string, styles ...string) *FeiShuRichTextContent {
+ content := map[string]any{
+ "tag": "at",
+ "user_id": userId,
+ "user_name": username,
+ "style": styles,
+ }
+ slf.Content[0] = append(slf.Content[0], content)
+ return slf
+}
+
+// AddImg 添加图片
+// - imageKey 表示图片的唯一标识,可通过上传图片接口获取
+func (slf *FeiShuRichTextContent) AddImg(imageKey string) *FeiShuRichTextContent {
+ content := map[string]any{
+ "tag": "img",
+ "image_key": imageKey,
+ }
+ slf.Content[0] = append(slf.Content[0], content)
+ return slf
+}
+
+// AddMedia 添加视频
+// - fileKey 表示视频文件的唯一标识,可通过上传文件接口获取
+func (slf *FeiShuRichTextContent) AddMedia(fileKey string) *FeiShuRichTextContent {
+ content := map[string]any{
+ "tag": "media",
+ "file_key": fileKey,
+ }
+ slf.Content[0] = append(slf.Content[0], content)
+ return slf
+}
+
+// AddMediaWithCover 添加包含封面的视频
+// - fileKey 表示视频文件的唯一标识,可通过上传文件接口获取
+// - imageKey 表示图片的唯一标识,可通过上传图片接口获取
+func (slf *FeiShuRichTextContent) AddMediaWithCover(fileKey, imageKey string) *FeiShuRichTextContent {
+ content := map[string]any{
+ "tag": "media",
+ "file_key": fileKey,
+ "image_key": imageKey,
+ }
+ slf.Content[0] = append(slf.Content[0], content)
+ return slf
+}
+
+// AddEmotion 添加表情
+// - emojiType 表示表情类型,部分可选值请参见表情文案。
+//
+// 表情文案:https://open.feishu.cn/document/server-docs/im-v1/message-reaction/emojis-introduce
+func (slf *FeiShuRichTextContent) AddEmotion(emojiType string) *FeiShuRichTextContent {
+ content := map[string]any{
+ "tag": "emotion",
+ "emoji_type": emojiType,
+ }
+ slf.Content[0] = append(slf.Content[0], content)
+ return slf
+}
+
+// Ok 确认完成,将返回 FeiShuRichText 可继续创建多语言富文本
+func (slf *FeiShuRichTextContent) Ok() *FeiShuRichText {
+ return slf.richText
+}
diff --git a/notify/sender.go b/notify/sender.go
index f78a678..9baad90 100644
--- a/notify/sender.go
+++ b/notify/sender.go
@@ -1,5 +1,7 @@
package notify
+// Sender 通知发送器接口声明
type Sender interface {
+ // Push 推送通知
Push(notify Notify) error
}
diff --git a/notify/senders/feishu.go b/notify/senders/feishu.go
index 2ff6aac..77810b3 100644
--- a/notify/senders/feishu.go
+++ b/notify/senders/feishu.go
@@ -1,40 +1,54 @@
package senders
import (
+ "encoding/json"
"fmt"
"github.com/go-resty/resty/v2"
"github.com/kercylan98/minotaur/notify"
"net/http"
)
-func NewFeiShu(webhook, receiveId string) *FeiShu {
+// NewFeiShu 根据特定的 webhook 地址创建飞书发送器
+func NewFeiShu(webhook string) *FeiShu {
return &FeiShu{
- client: resty.New(),
- webhook: webhook,
- receiveId: receiveId,
+ client: resty.New(),
+ webhook: webhook,
}
}
+// FeiShu 飞书发送器
type FeiShu struct {
- client *resty.Client
- webhook string
- receiveId string
+ client *resty.Client
+ webhook string
}
+// Push 推送通知
func (slf *FeiShu) Push(notify notify.Notify) error {
content, err := notify.Format()
if err != nil {
return err
}
resp, err := slf.client.R().
- SetHeader("Content-Type", "application/json").
+ SetHeader("Content-Type", "application/json; charset=utf-8").
SetBody(content).
Post(slf.webhook)
if err != nil {
return err
}
if resp.StatusCode() != http.StatusOK {
- return fmt.Errorf("FeiShu notify reader push failed, err: %s", resp.String())
+ return fmt.Errorf("FeiShu notify push failed, err: %s", resp.String())
+ }
+
+ var respStruct = struct {
+ Code int `json:"code"`
+ Msg string `json:"msg"`
+ Data any `json:"data"`
+ }{}
+ if err := json.Unmarshal(resp.Body(), &respStruct); err != nil {
+ return fmt.Errorf("FeiShu notify response unmarshal failed, err: %s", err)
+ }
+ if respStruct.Code != 0 {
+ return fmt.Errorf("FeiShu notify push failed, err: [%d] %s", respStruct.Code, respStruct.Msg)
}
return err
}
diff --git a/notify/senders/feishu_test.go b/notify/senders/feishu_test.go
new file mode 100644
index 0000000..7272c17
--- /dev/null
+++ b/notify/senders/feishu_test.go
@@ -0,0 +1,15 @@
+package senders
+
+import (
+ "github.com/kercylan98/minotaur/notify/notifies"
+ "testing"
+)
+
+func TestFeiShu_Push(t *testing.T) {
+ fs := NewFeiShu("https://open.feishu.cn/open-apis/bot/v2/hook/d886f30f-814c-47b1-aeb0-b508da0f7f22")
+
+ rt := notifies.NewFeiShu(notifies.FeiShuMessageWithRichText(notifies.NewFeiShuRichText().Create("zh_cn", "标题咯").AddText("哈哈哈").Ok()))
+ if err := fs.Push(rt); err != nil {
+ panic(err)
+ }
+}