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) + } +}