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  }