| /* |
| * 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 |
| } |