github.com/keybase/client/go@v0.0.0-20241007131713-f10651d043c8/bind/notifications.go (about) 1 package keybase 2 3 import ( 4 "errors" 5 "fmt" 6 "regexp" 7 "runtime" 8 "time" 9 10 "github.com/keybase/client/go/chat" 11 "github.com/keybase/client/go/chat/globals" 12 "github.com/keybase/client/go/chat/storage" 13 "github.com/keybase/client/go/chat/types" 14 "github.com/keybase/client/go/chat/utils" 15 "github.com/keybase/client/go/libkb" 16 "github.com/keybase/client/go/protocol/chat1" 17 "github.com/keybase/client/go/protocol/gregor1" 18 "github.com/keybase/client/go/protocol/keybase1" 19 "github.com/kyokomi/emoji" 20 context "golang.org/x/net/context" 21 ) 22 23 type Person struct { 24 KeybaseUsername string 25 KeybaseAvatar string 26 IsBot bool 27 } 28 29 type Message struct { 30 ID int 31 Kind string // "Text" | "Reaction" 32 Plaintext string 33 ServerMessage string // This is the server's suggested display message for the notification 34 From *Person 35 At int64 36 } 37 38 type ChatNotification struct { 39 Message *Message 40 ConvID string 41 TeamName string 42 TopicName string 43 TlfName string 44 // e.g. "keybase#general, CoolTeam, Susannah,Jake" 45 ConversationName string 46 IsGroupConversation bool 47 IsPlaintext bool 48 SoundName string 49 BadgeCount int 50 } 51 52 func HandlePostTextReply(strConvID, tlfName string, intMessageID int, body string) (err error) { 53 ctx := context.Background() 54 defer kbCtx.CTrace(ctx, "HandlePostTextReply", &err)() 55 defer func() { err = flattenError(err) }() 56 outboxID, err := storage.NewOutboxID() 57 if err != nil { 58 return err 59 } 60 convID, err := chat1.MakeConvID(strConvID) 61 if err != nil { 62 return err 63 } 64 _, err = kbCtx.ChatHelper.SendTextByIDNonblock(context.Background(), convID, tlfName, body, &outboxID, nil) 65 66 kbCtx.Log.CDebugf(ctx, "Marking as read from QuickReply: convID: %s", strConvID) 67 gc := globals.NewContext(kbCtx, kbChatCtx) 68 uid, err := utils.AssertLoggedInUID(ctx, gc) 69 if err != nil { 70 return err 71 } 72 73 msgID := chat1.MessageID(intMessageID) 74 if err = kbChatCtx.InboxSource.MarkAsRead(context.Background(), convID, uid, &msgID, false /* forceUnread */); err != nil { 75 kbCtx.Log.CDebugf(ctx, "Failed to mark as read from QuickReply: convID: %s. Err: %s", strConvID, err) 76 // We don't want to fail this method call just because we couldn't mark it as aread 77 err = nil 78 } 79 80 return nil 81 } 82 83 var spoileRegexp = regexp.MustCompile(`!>(.*?)<!`) 84 85 func HandleBackgroundNotification(strConvID, body, serverMessageBody, sender string, intMembersType int, 86 displayPlaintext bool, intMessageID int, pushID string, badgeCount, unixTime int, soundName string, 87 pusher PushNotifier, showIfStale bool) (err error) { 88 if err := waitForInit(10 * time.Second); err != nil { 89 return err 90 } 91 gc := globals.NewContext(kbCtx, kbChatCtx) 92 ctx := globals.ChatCtx(context.Background(), gc, 93 keybase1.TLFIdentifyBehavior_CHAT_GUI, nil, chat.NewCachingIdentifyNotifier(gc)) 94 95 defer kbCtx.CTrace(ctx, fmt.Sprintf("HandleBackgroundNotification(%s,%s,%v,%d,%d,%s,%d,%d)", 96 strConvID, sender, displayPlaintext, intMembersType, intMessageID, pushID, badgeCount, unixTime), &err)() 97 defer func() { err = flattenError(err) }() 98 99 // Unbox 100 if !kbCtx.ActiveDevice.HaveKeys() { 101 return libkb.LoginRequiredError{} 102 } 103 mp := chat.NewMobilePush(gc) 104 uid := gregor1.UID(kbCtx.Env.GetUID().ToBytes()) 105 convID, err := chat1.MakeConvID(strConvID) 106 if err != nil { 107 kbCtx.Log.CDebugf(ctx, "HandleBackgroundNotification: invalid convID: %s msg: %s", strConvID, err) 108 return err 109 } 110 membersType := chat1.ConversationMembersType(intMembersType) 111 conv, err := utils.GetVerifiedConv(ctx, gc, uid, convID, types.InboxSourceDataSourceLocalOnly) 112 if err != nil { 113 kbCtx.Log.CDebugf(ctx, "Failed to get conversation info", err) 114 return err 115 } 116 117 chatNotification := ChatNotification{ 118 IsPlaintext: displayPlaintext, 119 Message: &Message{ 120 ID: intMessageID, 121 ServerMessage: serverMessageBody, 122 From: &Person{}, 123 At: int64(unixTime) * 1000, 124 }, 125 ConvID: strConvID, 126 TopicName: conv.Info.TopicName, 127 TlfName: conv.Info.TlfName, 128 IsGroupConversation: len(conv.Info.Participants) > 2, 129 ConversationName: utils.FormatConversationName(conv.Info, string(kbCtx.Env.GetUsername())), 130 SoundName: soundName, 131 BadgeCount: badgeCount, 132 } 133 134 msgUnboxed, err := mp.UnboxPushNotification(ctx, uid, convID, membersType, body) 135 if err == nil && msgUnboxed.IsValid() { 136 chatNotification.Message.From.IsBot = msgUnboxed.SenderIsBot() 137 username := msgUnboxed.Valid().SenderUsername 138 chatNotification.Message.From.KeybaseUsername = username 139 140 if displayPlaintext && !msgUnboxed.Valid().IsEphemeral() { 141 // We show avatars on Android 142 if runtime.GOOS == "android" { 143 avatar, err := kbSvc.GetUserAvatar(username) 144 145 if err != nil { 146 kbCtx.Log.CDebugf(ctx, "Push Notif: Err in getting user avatar %v", err) 147 } else { 148 chatNotification.Message.From.KeybaseAvatar = avatar 149 } 150 } 151 152 switch msgUnboxed.GetMessageType() { 153 case chat1.MessageType_TEXT: 154 chatNotification.Message.Kind = "Text" 155 chatNotification.Message.Plaintext = spoileRegexp.ReplaceAllString(msgUnboxed.Valid().MessageBody.Text().Body, "•••") 156 case chat1.MessageType_REACTION: 157 chatNotification.Message.Kind = "Reaction" 158 reaction, err := utils.GetReaction(msgUnboxed) 159 if err != nil { 160 return err 161 } 162 chatNotification.Message.Plaintext = 163 emoji.Sprintf("Reacted to your message with %v", reaction) 164 default: 165 kbCtx.Log.CDebugf(ctx, "unboxNotification: Unknown message type: %v", 166 msgUnboxed.GetMessageType()) 167 return errors.New("invalid message type for plaintext") 168 } 169 } 170 } else { 171 kbCtx.Log.CDebugf(ctx, "unboxNotification: failed to unbox: %s", err) 172 chatNotification.Message.From.KeybaseUsername = sender 173 // just bail out of here at this point since we won't be displaying anything useful, 174 // and we don't want to accidentally ack the plaintext notification when we didn't really 175 // display it. 176 if len(serverMessageBody) == 0 { 177 return errors.New("Unbox failed; nothing to display") 178 } 179 } 180 181 age := time.Since(time.Unix(int64(unixTime), 0)) 182 183 // On iOS we don't want to show stale notifications. Nonsilent notifications 184 // can come later and cause duplicate notifications. On Android, both silent 185 // and non-silent notifications go through this function; and Java checks if we 186 // have already seen a notification. We don't need this stale logic. 187 if !showIfStale && age >= 2*time.Minute { 188 kbCtx.Log.CDebugf(ctx, "HandleBackgroundNotification: stale notification: %v", age) 189 return errors.New("stale notification") 190 } 191 192 // only display and ack this notification if we actually have something to display 193 if pusher != nil && (len(chatNotification.Message.Plaintext) > 0 || len(chatNotification.Message.ServerMessage) > 0) { 194 pusher.DisplayChatNotification(&chatNotification) 195 if len(pushID) > 0 { 196 mp.AckNotificationSuccess(ctx, []string{pushID}) 197 } 198 } 199 return nil 200 }