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 }