notification-lark/notification.go (268 lines of code) (raw):
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package lark
import (
"context"
"embed"
"encoding/json"
"fmt"
"strings"
lark_i18n "github.com/apache/answer-plugins/notification-lark/i18n"
"github.com/apache/answer-plugins/util"
"github.com/apache/answer/plugin"
"github.com/segmentfault/pacman/i18n"
"github.com/segmentfault/pacman/log"
lark "github.com/larksuite/oapi-sdk-go/v3"
"github.com/larksuite/oapi-sdk-go/v3/event/dispatcher"
larkApplication "github.com/larksuite/oapi-sdk-go/v3/service/application/v6"
larkIM "github.com/larksuite/oapi-sdk-go/v3/service/im/v1"
larkWebSocket "github.com/larksuite/oapi-sdk-go/v3/ws"
)
//go:embed info.yaml
var Info embed.FS
type LarkClient struct {
ws *larkWebSocket.Client
http *lark.Client
}
type Notification struct {
info *util.Info
config *NotificationConfig
client *LarkClient
userConfigCache *UserConfigCache
}
const (
LarkBindAccountMenuEventKey = "10001"
NotificationTypeInteractive = "interactive"
MsgTypeText = "text"
ReceiveIdTypeOpenId = "open_id"
)
var (
TagColor = []string{"neutral", "blue", "turquoise", "lime", "orange", "violet", "indigo", "wathet", "green", "yellow", "red", "purple", "carmine"}
)
func init() {
plugin.Register(&Notification{
userConfigCache: NewUserConfigCache(),
})
}
func (n Notification) Info() plugin.Info {
if n.info == nil {
info := &util.Info{}
info.GetInfo(Info)
n.info = info
}
return plugin.Info{
Name: plugin.MakeTranslator(lark_i18n.InfoName),
SlugName: n.info.SlugName,
Description: plugin.MakeTranslator(lark_i18n.InfoDescription),
Author: n.info.Author,
Version: n.info.Version,
Link: n.info.Link,
}
}
func renderTag(tags []string) string {
var builder strings.Builder
for _, tag := range tags {
idx := RandomInt(0, int64(len(TagColor)))
builder.WriteString(fmt.Sprintf(`<text_tag color='%s'> %s </text_tag>`, TagColor[idx], tag))
}
return builder.String()
}
func renderNotification(msg plugin.NotificationMessage) string {
lang := i18n.Language(msg.ReceiverLang)
switch msg.Type {
case plugin.NotificationUpdateQuestion:
return plugin.TranslateWithData(lang, lark_i18n.TplUpdateQuestion, msg)
case plugin.NotificationAnswerTheQuestion:
return plugin.TranslateWithData(lang, lark_i18n.TplAnswerTheQuestion, msg)
case plugin.NotificationUpdateAnswer:
return plugin.TranslateWithData(lang, lark_i18n.TplUpdateAnswer, msg)
case plugin.NotificationAcceptAnswer:
return plugin.TranslateWithData(lang, lark_i18n.TplAcceptAnswer, msg)
case plugin.NotificationCommentQuestion:
return plugin.TranslateWithData(lang, lark_i18n.TplCommentQuestion, msg)
case plugin.NotificationCommentAnswer:
return plugin.TranslateWithData(lang, lark_i18n.TplCommentAnswer, msg)
case plugin.NotificationReplyToYou:
return plugin.TranslateWithData(lang, lark_i18n.TplReplyToYou, msg)
case plugin.NotificationMentionYou:
return plugin.TranslateWithData(lang, lark_i18n.TplMentionYou, msg)
case plugin.NotificationInvitedYouToAnswer:
return plugin.TranslateWithData(lang, lark_i18n.TplInvitedYouToAnswer, msg)
case plugin.NotificationNewQuestion, plugin.NotificationNewQuestionFollowedTag:
msg.QuestionTags = renderTag(strings.Split(msg.QuestionTags, ","))
return plugin.TranslateWithData(lang, lark_i18n.TplNewQuestion, msg)
}
return ""
}
func makeCardMsg(args plugin.NotificationMessage) Card {
action := &Action{
Tag: "action",
Actions: []*Button{
{
Width: "fill",
Text: &Text{
Tag: "plain_text",
Content: "查看详情",
Icon: &Icon{
Tag: "standard_icon",
Token: "link-copy_outlined",
},
},
Behaviors: []Behavior{
{
Type: "open_url",
DefaultURL: args.QuestionUrl,
},
},
},
},
}
columnSet := func(content string) ColumnSet {
return ColumnSet{
Show: &Show{
Tag: "column_set",
FlexMode: "flex_mode",
Columns: []Column{
{
Elements: []Element{
{
PlainText: &PlainText{
Tag: "div",
Text: &Text{
Tag: "lark_md",
Content: content,
},
},
},
},
},
},
},
Action: action,
}
}
card := Card{
Config: &Config{
WidthMode: "compact",
UseCustomTranslation: PtrBool(true),
EnableForward: PtrBool(false),
},
Header: &Header{
Title: &Text{
Tag: "plain_text",
I18n: &I18n{
ZhCn: "新通知",
EnUs: "New Notification",
},
},
UdIcon: &Icon{
Tag: "icon",
Token: "bell_outlined",
Color: "blue",
},
Template: ThemeGreen,
},
I18nElements: &I18nElements{
ZhCn: []ColumnSet{},
EnUs: []ColumnSet{},
},
}
args.ReceiverLang = string(i18n.LanguageChinese)
card.I18nElements.ZhCn = append(card.I18nElements.ZhCn, columnSet(renderNotification(args)))
args.ReceiverLang = string(i18n.LanguageEnglish)
card.I18nElements.EnUs = append(card.I18nElements.EnUs, columnSet(renderNotification(args)))
return card
}
// GetNewQuestionSubscribers returns the subscribers of the new question notification
func (n *Notification) GetNewQuestionSubscribers() (userIDs []string) {
for userID, conf := range n.userConfigCache.userConfigMapping {
if conf.AllNewQuestions {
userIDs = append(userIDs, userID)
}
}
return userIDs
}
// Notify sends a notification to the user
func (n *Notification) Notify(msg plugin.NotificationMessage) {
ctx := context.TODO()
log.Debugf("Attempting to send notification to user %s: %+v", msg.ReceiverUserID, msg)
// get user config
userConfig, err := n.getUserConfig(msg.ReceiverUserID)
if err != nil {
log.Errorf("get user config failed: %v", err)
return
}
if userConfig == nil {
log.Debugf("user %s has no config", msg.ReceiverUserID)
return
}
if userConfig.OpenId == "" {
log.Debugf("user %s not set the open id", msg.ReceiverUserID)
return
}
// check if the notification is enabled
switch msg.Type {
case plugin.NotificationNewQuestion:
if !userConfig.AllNewQuestions {
log.Debugf("user %s not config the new question", msg.ReceiverUserID)
return
}
case plugin.NotificationNewQuestionFollowedTag:
if !userConfig.NewQuestionsForFollowingTags {
log.Debugf("user %s not config the new question followed tag", msg.ReceiverUserID)
return
}
default:
if !userConfig.InboxNotifications {
log.Debugf("user %s not config the inbox notification", msg.ReceiverUserID)
return
}
}
log.Debugf("user %s config the notification", msg.ReceiverUserID)
cardMsg := makeCardMsg(msg)
notificationMsg, err := json.Marshal(cardMsg)
if err != nil {
log.Errorf("marshal notification message failed: %v", err)
return
}
log.Debugf("card message: %s", notificationMsg)
if len(notificationMsg) == 0 {
log.Debugf("this type of notification will be drop, the type is %s", msg.Type)
return
}
req := larkIM.NewCreateMessageReqBuilder().
ReceiveIdType(ReceiveIdTypeOpenId).
Body(larkIM.NewCreateMessageReqBodyBuilder().
ReceiveId(userConfig.OpenId).
MsgType(NotificationTypeInteractive).
Content(string(notificationMsg)).
Build()).
Build()
resp, err := n.client.http.Im.Message.Create(ctx, req)
if err != nil || !resp.Success() {
log.Errorf("Failed to send message to user %s: %v", userConfig.OpenId, err)
}
}
// LarkWsEventMenuClick is the event handler for the menu click event
func (n *Notification) LarkWsEventMenuClick(ctx context.Context, event *larkApplication.P2BotMenuV6) error {
switch *event.Event.EventKey {
case LarkBindAccountMenuEventKey:
contentData, _ := json.Marshal(map[string]interface{}{
"text": *event.Event.Operator.OperatorId.OpenId,
})
req := larkIM.NewCreateMessageReqBuilder().
ReceiveIdType(ReceiveIdTypeOpenId).
Body(larkIM.NewCreateMessageReqBodyBuilder().
ReceiveId(*event.Event.Operator.OperatorId.OpenId).
MsgType(MsgTypeText).
Content(string(contentData)).
Build()).
Build()
resp, err := n.client.http.Im.Message.Create(context.Background(), req)
if err != nil || !resp.Success() {
fmt.Printf("Failed to send message: %v\n", err)
return nil
}
}
return nil
}
func (n *Notification) LarkWsEventHub() *dispatcher.EventDispatcher {
return dispatcher.NewEventDispatcher(n.config.VerificationToken, n.config.EventEncryptKey).
OnP2BotMenuV6(n.LarkWsEventMenuClick)
}
func (n *LarkClient) Start() error {
// TODO: wait feishu sdk fix the cancel not work issue
// https://github.com/larksuite/oapi-sdk-go/issues/141
// ctx, cancel := context.WithCancel(context.TODO())
n.ws.Start(context.TODO())
return nil
}