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