/
githubmirror
/
client
Обзор
Документация
Войти
/
githubmirror
/
client
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
master
go/chat/server.go
4 172 строки
142 KB
zoom-ua
mark decryption failed errors as non-retryable; atomic writes for EncryptedDB (#29344)
22 июн 2026, 21:32
Не верифицирован
22 июн 2026, 21:32
a334649
Код
Авторство
О чём код?
package chat import ( "bytes" "context" "encoding/hex" "encoding/json" "errors" "fmt" "io" "os" "regexp" "slices" "sort" "strings" "sync" "time" "github.com/keybase/client/go/chat/attachments" "github.com/keybase/client/go/chat/globals" "github.com/keybase/client/go/chat/search" "github.com/keybase/client/go/chat/storage" "github.com/keybase/client/go/chat/types" "github.com/keybase/client/go/chat/unfurl" "github.com/keybase/client/go/chat/utils" "github.com/keybase/client/go/libkb" "github.com/keybase/client/go/protocol/chat1" "github.com/keybase/client/go/protocol/gregor1" "github.com/keybase/client/go/protocol/keybase1" "github.com/keybase/client/go/teambot" "github.com/keybase/client/go/teams" "github.com/keybase/client/go/teams/opensearch" "github.com/keybase/pipeliner" "golang.org/x/sync/errgroup" "golang.org/x/sync/semaphore" ) type UISource interface { GetChatUI(sessionID int) libkb.ChatUI GetStreamUICli() *keybase1.StreamUiClient } type Server struct { globals.Contextified utils.DebugLabeler serverConn types.ServerConnection uiSource UISource boxer *Boxer identNotifier types.IdentifyNotifier searchMu sync.Mutex searchInboxMu sync.Mutex loadGalleryMu sync.Mutex searchCancelFn context.CancelFunc searchInboxCancelFn context.CancelFunc loadGalleryCancelFn context.CancelFunc fileAttachmentDownloadConfigurationMu sync.RWMutex fileAttachmentDownloadCacheDir string fileAttachmentDownloadDownloadDir string // Only for testing rc chat1.RemoteInterface mockChatUI libkb.ChatUI } var _ chat1.LocalInterface = (*Server)(nil) func NewServer(g *globals.Context, serverConn types.ServerConnection, uiSource UISource) *Server { return &Server{ Contextified: globals.NewContextified(g), DebugLabeler: utils.NewDebugLabeler(g.ExternalG(), "Server", false), serverConn: serverConn, uiSource: uiSource, boxer: NewBoxer(g), identNotifier: NewCachingIdentifyNotifier(g), fileAttachmentDownloadCacheDir: g.GetEnv().GetCacheDir(), fileAttachmentDownloadDownloadDir: g.GetEnv().GetDownloadsDir(), } } func (h *Server) getChatUI(sessionID int) libkb.ChatUI { if h.mockChatUI != nil { return h.mockChatUI } return h.uiSource.GetChatUI(sessionID) } func (h *Server) getStreamUICli() *keybase1.StreamUiClient { return h.uiSource.GetStreamUICli() } func (h *Server) shouldSquashError(err error) bool { // these are not offline errors, but we never want the JS to receive them and potentially // display a black bar switch terr := err.(type) { case storage.AbortedError: return true case TransientUnboxingError: return h.shouldSquashError(terr.Inner()) } switch err { case utils.ErrConvLockTabDeadlock, context.Canceled: return true } return false } func (h *Server) squashSquashableErrors(err error) error { if h.shouldSquashError(err) { return nil } return err } func (h *Server) handleOfflineError(ctx context.Context, err error, res chat1.OfflinableResult, ) error { if err == nil { return nil } if h.shouldSquashError(err) { return nil } errKind := IsOfflineError(err) h.Debug(ctx, "handleOfflineError: errType: %T", err) if errKind != OfflineErrorKindOnline { h.Debug(ctx, "handleOfflineError: setting offline: err: %s", err) res.SetOffline() switch errKind { case OfflineErrorKindOfflineReconnect: // Reconnect Gregor if we think we are offline (and told to reconnect) h.Debug(ctx, "handleOfflineError: reconnecting to gregor") if _, err := h.serverConn.Reconnect(ctx); err != nil { h.Debug(ctx, "handleOfflineError: error reconnecting: %s", err) } else { h.Debug(ctx, "handleOfflineError: success reconnecting") } default: // Nothing to do for other errors. } return nil } return err } func (h *Server) setResultRateLimit(ctx context.Context, res types.RateLimitedResult) { res.SetRateLimits(globals.CtxRateLimits(ctx)) } func (h *Server) suspendBgConvLoads(ctx context.Context) func() { return utils.SuspendComponents(ctx, h.G(), []types.Suspendable{ h.G().ConvLoader, h.G().Indexer, }) } func (h *Server) suspendInboxSource(ctx context.Context) func() { return utils.SuspendComponent(ctx, h.G(), h.G().InboxSource) } func (h *Server) RequestInboxLayout(ctx context.Context, reselectMode chat1.InboxLayoutReselectMode) (err error) { ctx = globals.ChatCtx(ctx, h.G(), keybase1.TLFIdentifyBehavior_CHAT_GUI, nil, nil) defer h.Trace(ctx, &err, "RequestInboxLayout")() h.G().UIInboxLoader.UpdateLayout(ctx, reselectMode, "UI request") return nil } func (h *Server) RequestInboxUnbox(ctx context.Context, convIDs []chat1.ConversationID) (err error) { ctx = globals.ChatCtx(ctx, h.G(), keybase1.TLFIdentifyBehavior_CHAT_GUI, nil, nil) ctx = globals.CtxAddLocalizerCancelable(ctx) reqID := libkb.RandStringB64(3) defer h.Trace(ctx, &err, "RequestInboxUnbox")() defer h.PerfTrace(ctx, &err, "RequestInboxUnbox")() h.Debug(ctx, "RequestInboxUnbox[%s]: begin convs: %d", reqID, len(convIDs)) defer func() { h.Debug(ctx, "RequestInboxUnbox[%s]: return err: %v", reqID, err) }() for _, convID := range convIDs { h.GetPerfLog().CDebugf(ctx, "RequestInboxUnbox: queuing unbox for: %s", convID) h.Debug(ctx, "RequestInboxUnbox: queuing unbox for: %s", convID) } if err := h.G().UIInboxLoader.UpdateConvs(ctx, convIDs); err != nil { h.Debug(ctx, "RequestInboxUnbox: failed to update convs: %s", err) } return nil } func (h *Server) RequestInboxSmallIncrease(ctx context.Context) (err error) { ctx = globals.ChatCtx(ctx, h.G(), keybase1.TLFIdentifyBehavior_CHAT_GUI, nil, nil) defer h.Trace(ctx, &err, "RequestInboxSmallIncrease")() h.G().UIInboxLoader.UpdateLayoutFromSmallIncrease(ctx) return nil } func (h *Server) RequestInboxSmallReset(ctx context.Context) (err error) { ctx = globals.ChatCtx(ctx, h.G(), keybase1.TLFIdentifyBehavior_CHAT_GUI, nil, nil) defer h.Trace(ctx, &err, "RequestInboxSmallReset")() h.G().UIInboxLoader.UpdateLayoutFromSmallReset(ctx) return nil } func (h *Server) GetInboxNonblockLocal(ctx context.Context, arg chat1.GetInboxNonblockLocalArg) (res chat1.NonblockFetchRes, err error) { var breaks []keybase1.TLFIdentifyFailure ctx = globals.ChatCtx(ctx, h.G(), arg.IdentifyBehavior, &breaks, h.identNotifier) ctx = globals.CtxAddLocalizerCancelable(ctx) defer h.Trace(ctx, &err, "GetInboxNonblockLocal")() defer h.PerfTrace(ctx, &err, "GetInboxNonblockLocal")() defer func() { h.setResultRateLimit(ctx, &res) }() defer func() { err = h.handleOfflineError(ctx, err, &res) }() defer func() { if res.Offline { h.Debug(ctx, "GetInboxNonblockLocal: result obtained offline") } }() defer h.suspendBgConvLoads(ctx)() if err := h.G().UIInboxLoader.LoadNonblock(ctx, arg.Query, arg.MaxUnbox, arg.SkipUnverified); err != nil { return res, err } res.Offline = h.G().InboxSource.IsOffline(ctx) res.IdentifyFailures = breaks return res, nil } func (h *Server) MarkAsReadLocal(ctx context.Context, arg chat1.MarkAsReadLocalArg) (res chat1.MarkAsReadLocalRes, err error) { var identBreaks []keybase1.TLFIdentifyFailure ctx = globals.ChatCtx(ctx, h.G(), keybase1.TLFIdentifyBehavior_CHAT_GUI, &identBreaks, h.identNotifier) defer h.Trace(ctx, &err, "MarkAsReadLocal(%s, %v)", arg.ConversationID, arg.MsgID)() defer func() { h.setResultRateLimit(ctx, &res) }() defer func() { err = h.handleOfflineError(ctx, err, &res) }() uid, err := utils.AssertLoggedInUID(ctx, h.G()) if err != nil { h.Debug(ctx, "MarkAsRead: not logged in: %s", err) return chat1.MarkAsReadLocalRes{}, nil } if err = h.G().InboxSource.MarkAsRead(ctx, arg.ConversationID, uid, arg.MsgID, arg.ForceUnread); err != nil { switch err { case utils.ErrGetUnverifiedConvNotFound, utils.ErrGetVerifiedConvNotFound: // if we couldn't find the conv, then just act like it worked default: return res, err } } return chat1.MarkAsReadLocalRes{ Offline: h.G().InboxSource.IsOffline(ctx), }, nil } func (h *Server) MarkTLFAsReadLocal(ctx context.Context, arg chat1.MarkTLFAsReadLocalArg) (res chat1.MarkTLFAsReadLocalRes, err error) { var identBreaks []keybase1.TLFIdentifyFailure ctx = globals.ChatCtx(ctx, h.G(), keybase1.TLFIdentifyBehavior_CHAT_GUI, &identBreaks, nil) defer h.Trace(ctx, &err, "MarkTLFAsRead")() defer func() { h.setResultRateLimit(ctx, &res) }() defer func() { err = h.handleOfflineError(ctx, err, &res) }() uid, err := utils.AssertLoggedInUID(ctx, h.G()) if err != nil { return res, err } convs, err := h.G().TeamChannelSource.GetChannelsFull(ctx, uid, arg.TlfID, chat1.TopicType_CHAT) if err != nil { return res, err } epick := libkb.FirstErrorPicker{} for _, conv := range convs { _, err = h.MarkAsReadLocal(ctx, chat1.MarkAsReadLocalArg{ ConversationID: conv.GetConvID(), MsgID: &conv.ReaderInfo.MaxMsgid, }) epick.Push(err) } return chat1.MarkTLFAsReadLocalRes{ Offline: h.G().InboxSource.IsOffline(ctx), }, epick.Error() } // GetInboxAndUnboxLocal implements keybase.chatLocal.getInboxAndUnboxLocal protocol. func (h *Server) GetInboxAndUnboxLocal(ctx context.Context, arg chat1.GetInboxAndUnboxLocalArg) (res chat1.GetInboxAndUnboxLocalRes, err error) { var identBreaks []keybase1.TLFIdentifyFailure ctx = globals.ChatCtx(ctx, h.G(), arg.IdentifyBehavior, &identBreaks, h.identNotifier) if arg.Query != nil && arg.Query.TopicType != nil && *arg.Query.TopicType != chat1.TopicType_CHAT { // make this cancelable for things like KBFS file edit convs ctx = globals.CtxAddLocalizerCancelable(ctx) } defer h.Trace(ctx, &err, "GetInboxAndUnboxLocal")() defer func() { h.setResultRateLimit(ctx, &res) }() defer func() { err = h.handleOfflineError(ctx, err, &res) }() uid, err := utils.AssertLoggedInUID(ctx, h.G()) if err != nil { return res, err } // Ignore these requests on mobile if h.G().IsMobileAppType() && arg.Query != nil && arg.Query.TopicType != nil && *arg.Query.TopicType == chat1.TopicType_KBFSFILEEDIT { return chat1.GetInboxAndUnboxLocalRes{ IdentifyFailures: identBreaks, }, nil } // Read inbox from the source ib, _, err := h.G().InboxSource.Read(ctx, uid, types.ConversationLocalizerBlocking, types.InboxSourceDataSourceAll, nil, arg.Query) switch err.(type) { case nil: case UnknownTLFNameError: h.Debug(ctx, "GetInboxAndUnboxLocal: got unknown TLF name error, returning blank results") ib.Convs = nil default: return res, err } return chat1.GetInboxAndUnboxLocalRes{ Conversations: ib.Convs, Offline: h.G().InboxSource.IsOffline(ctx), IdentifyFailures: identBreaks, }, nil } func (h *Server) GetInboxAndUnboxUILocal(ctx context.Context, arg chat1.GetInboxAndUnboxUILocalArg) (res chat1.GetInboxAndUnboxUILocalRes, err error) { var identBreaks []keybase1.TLFIdentifyFailure ctx = globals.ChatCtx(ctx, h.G(), arg.IdentifyBehavior, &identBreaks, h.identNotifier) defer h.Trace(ctx, &err, "GetInboxAndUnboxUILocal")() defer func() { h.setResultRateLimit(ctx, &res) }() defer func() { err = h.handleOfflineError(ctx, err, &res) }() uid, err := utils.AssertLoggedInUID(ctx, h.G()) if err != nil { return res, err } // Read inbox from the source ib, _, err := h.G().InboxSource.Read(ctx, uid, types.ConversationLocalizerBlocking, types.InboxSourceDataSourceAll, nil, arg.Query) switch err.(type) { case nil: case UnknownTLFNameError: h.Debug(ctx, "GetInboxAndUnboxUILocal: got unknown TLF name error, returning blank results") ib.Convs = nil default: return res, err } return chat1.GetInboxAndUnboxUILocalRes{ Conversations: utils.PresentConversationLocals(ctx, h.G(), uid, ib.Convs, utils.PresentParticipantsModeInclude), IdentifyFailures: identBreaks, }, nil } // GetThreadLocal implements keybase.chatLocal.getThreadLocal protocol. func (h *Server) GetThreadLocal(ctx context.Context, arg chat1.GetThreadLocalArg) (res chat1.GetThreadLocalRes, err error) { var identBreaks []keybase1.TLFIdentifyFailure ctx = globals.ChatCtx(ctx, h.G(), arg.IdentifyBehavior, &identBreaks, h.identNotifier) defer h.Trace(ctx, &err, "GetThreadLocal")() defer func() { h.setResultRateLimit(ctx, &res) }() defer func() { err = h.handleOfflineError(ctx, err, &res) }() uid, err := utils.AssertLoggedInUID(ctx, h.G()) if err != nil { return chat1.GetThreadLocalRes{}, err } thread, err := h.G().UIThreadLoader.Load(ctx, uid, arg.ConversationID, arg.Reason, nil, arg.Query, arg.Pagination) if err != nil { return chat1.GetThreadLocalRes{}, err } return chat1.GetThreadLocalRes{ Thread: thread, IdentifyFailures: identBreaks, }, nil } func (h *Server) GetUnreadline(ctx context.Context, arg chat1.GetUnreadlineArg) (res chat1.UnreadlineRes, err error) { var identBreaks []keybase1.TLFIdentifyFailure ctx = globals.ChatCtx(ctx, h.G(), arg.IdentifyBehavior, &identBreaks, h.identNotifier) defer h.Trace(ctx, &err, "GetUnreadline: convID: %v, readMsgID: %v", arg.ConvID, arg.ReadMsgID)() defer func() { h.setResultRateLimit(ctx, &res) }() defer func() { err = h.handleOfflineError(ctx, err, &res) }() uid, err := utils.AssertLoggedInUID(ctx, h.G()) if err != nil { return res, err } res.UnreadlineID, err = h.G().ConvSource.GetUnreadline(ctx, arg.ConvID, uid, arg.ReadMsgID) if err != nil { h.Debug(ctx, "GetUnreadline: unable to run UnreadMsgID: %v", err) return res, err } return res, nil } func (h *Server) GetThreadNonblock(ctx context.Context, arg chat1.GetThreadNonblockArg) (res chat1.NonblockFetchRes, err error) { var identBreaks []keybase1.TLFIdentifyFailure ctx = globals.ChatCtx(ctx, h.G(), arg.IdentifyBehavior, &identBreaks, h.identNotifier) reqID := libkb.RandStringB64(3) defer h.Trace(ctx, &err, "GetThreadNonblock(%s,%v,%v)", arg.ConversationID, arg.CbMode, arg.Reason)() defer h.PerfTrace(ctx, &err, "GetThreadNonblock(%s,%v,%v)", arg.ConversationID, arg.CbMode, arg.Reason)() defer func() { h.setResultRateLimit(ctx, &res) }() defer func() { err = h.handleOfflineError(ctx, err, &res) }() defer func() { h.Debug(ctx, "GetThreadNonblock[%s]: return convID: %s err: %v", reqID, arg.ConversationID, err) }() defer h.suspendBgConvLoads(ctx)() h.Debug(ctx, "GetThreadNonblock[%s]: suspend inbox source begin convID: %s", reqID, arg.ConversationID) resumeInboxSource := h.suspendInboxSource(ctx) h.Debug(ctx, "GetThreadNonblock[%s]: suspend inbox source done convID: %s", reqID, arg.ConversationID) defer func() { h.Debug(ctx, "GetThreadNonblock[%s]: resume inbox source begin convID: %s", reqID, arg.ConversationID) resumeInboxSource() h.Debug(ctx, "GetThreadNonblock[%s]: resume inbox source done convID: %s", reqID, arg.ConversationID) }() h.Debug(ctx, "GetThreadNonblock[%s]: begin convID: %s sessionID: %d", reqID, arg.ConversationID, arg.SessionID) uid, err := utils.AssertLoggedInUID(ctx, h.G()) if err != nil { return chat1.NonblockFetchRes{}, err } chatUI := h.getChatUI(arg.SessionID) return res, h.G().UIThreadLoader.LoadNonblock(ctx, chatUI, uid, arg.ConversationID, arg.Reason, arg.Pgmode, arg.CbMode, arg.KnownRemotes, arg.Query, arg.Pagination) } func (h *Server) NewConversationsLocal(ctx context.Context, arg chat1.NewConversationsLocalArg) (res chat1.NewConversationsLocalRes, err error) { var identBreaks []keybase1.TLFIdentifyFailure ctx = globals.ChatCtx(ctx, h.G(), arg.IdentifyBehavior, &identBreaks, h.identNotifier) defer h.Trace(ctx, &err, "NewConversationsLocal(len=%d)", len(arg.NewConversationLocalArguments))() defer func() { h.setResultRateLimit(ctx, &res) }() _, err = utils.AssertLoggedInUID(ctx, h.G()) if err != nil { return chat1.NewConversationsLocalRes{}, err } var errs []error for _, convArg := range arg.NewConversationLocalArguments { var result chat1.NewConversationsLocalResult newConvRes, err := h.NewConversationLocal(ctx, chat1.NewConversationLocalArg{ TlfName: convArg.TlfName, TopicType: convArg.TopicType, TlfVisibility: convArg.TlfVisibility, TopicName: convArg.TopicName, MembersType: convArg.MembersType, IdentifyBehavior: arg.IdentifyBehavior, }) if err != nil { e := err.Error() result.Err = &e errs = append(errs, err) } else { result.Result = new(chat1.NewConversationLocalRes) result.Result.Conv = newConvRes.Conv result.Result.UiConv = newConvRes.UiConv } res.Results = append(res.Results, result) } res.IdentifyFailures = identBreaks return res, libkb.CombineErrors(errs...) } // NewConversationLocal implements keybase.chatLocal.newConversationLocal protocol. // Create a new conversation. Or in the case of CHAT, create-or-get a conversation. func (h *Server) NewConversationLocal(ctx context.Context, arg chat1.NewConversationLocalArg) (res chat1.NewConversationLocalRes, err error) { var identBreaks []keybase1.TLFIdentifyFailure ctx = globals.ChatCtx(ctx, h.G(), arg.IdentifyBehavior, &identBreaks, h.identNotifier) defer h.Trace(ctx, &err, "NewConversationLocal(%s|%v)", arg.TlfName, arg.MembersType)() defer func() { h.setResultRateLimit(ctx, &res) }() uid, err := utils.AssertLoggedInUID(ctx, h.G()) if err != nil { return chat1.NewConversationLocalRes{}, err } conv, created, err := NewConversation(ctx, h.G(), uid, arg.TlfName, arg.TopicName, arg.TopicType, arg.MembersType, arg.TlfVisibility, nil, h.remoteClient, NewConvFindExistingNormal) if err != nil { h.Debug(ctx, "NewConversationLocal: failed to make conv: %s", err) return res, err } res.Conv = conv res.UiConv = utils.PresentConversationLocal(ctx, h.G(), uid, conv, utils.PresentParticipantsModeInclude) res.IdentifyFailures = identBreaks // If we are making a new channel in a team, send a system message to // indicate this. if created && arg.MembersType == chat1.ConversationMembersType_TEAM && arg.TopicType == chat1.TopicType_CHAT && arg.TopicName != nil && *arg.TopicName != globals.DefaultTeamTopic { subBody := chat1.NewMessageSystemWithNewchannel(chat1.MessageSystemNewChannel{ Creator: h.G().Env.GetUsername().String(), NameAtCreation: *arg.TopicName, ConvID: conv.GetConvID(), }) body := chat1.NewMessageBodyWithSystem(subBody) err = h.G().ChatHelper.SendMsgByName(ctx, conv.Info.TlfName, &globals.DefaultTeamTopic, arg.MembersType, keybase1.TLFIdentifyBehavior_CHAT_CLI, body, chat1.MessageType_SYSTEM) if err != nil { h.Debug(ctx, "NewConversationLocal: unable to post new channel system message: %v", err) } } // If we have this conv hidden, then let's bring it back before returning if !utils.GetConversationStatusBehavior(conv.Info.Status).ShowInInbox { h.Debug(ctx, "NewConversationLocal: new conversation not shown, unhiding: %s", conv.GetConvID()) if err := h.G().InboxSource.RemoteSetConversationStatus(ctx, uid, conv.GetConvID(), chat1.ConversationStatus_UNFILED); err != nil { h.Debug(ctx, "NewConversationLocal: unable to unhide conv: %s", err) return res, err } } return res, nil } func (h *Server) limitConvResults(ctx context.Context, uid gregor1.UID, allConvs []types.RemoteConversation, num int, ) ([]chat1.ConversationLocal, error) { var convs []types.RemoteConversation sort.Sort(utils.RemoteConvByMtime(allConvs)) if len(allConvs) <= num { convs = allConvs } else { convs = allConvs[:num] } locals, _, err := h.G().InboxSource.Localize(ctx, uid, convs, types.ConversationLocalizerBlocking) if err != nil { return nil, err } return locals, nil } func (h *Server) GetInboxSummaryForCLILocal(ctx context.Context, arg chat1.GetInboxSummaryForCLILocalQuery) (res chat1.GetInboxSummaryForCLILocalRes, err error) { var identBreaks []keybase1.TLFIdentifyFailure ctx = globals.ChatCtx(ctx, h.G(), keybase1.TLFIdentifyBehavior_CHAT_CLI, &identBreaks, h.identNotifier) defer h.Trace(ctx, &err, "GetInboxSummaryForCLILocal")() defer func() { h.setResultRateLimit(ctx, &res) }() defer func() { err = h.handleOfflineError(ctx, err, &res) }() uid, err := utils.AssertLoggedInUID(ctx, h.G()) if err != nil { return chat1.GetInboxSummaryForCLILocalRes{}, err } var after time.Time if len(arg.After) > 0 { after, err = utils.ParseTimeFromRFC3339OrDurationFromPast(h.G(), arg.After) if err != nil { return res, fmt.Errorf("parsing time or duration (%s) error: %s", arg.After, err) } } var before time.Time if len(arg.Before) > 0 { before, err = utils.ParseTimeFromRFC3339OrDurationFromPast(h.G(), arg.Before) if err != nil { return res, fmt.Errorf("parsing time or duration (%s) error: %s", arg.Before, err) } } var queryBase chat1.GetInboxQuery queryBase.ComputeActiveList = true queryBase.OneChatTypePerTLF = new(bool) *queryBase.OneChatTypePerTLF = true if !after.IsZero() { gafter := gregor1.ToTime(after) queryBase.After = &gafter } if !before.IsZero() { gbefore := gregor1.ToTime(before) queryBase.Before = &gbefore } if arg.TopicType != chat1.TopicType_NONE { queryBase.TopicType = &arg.TopicType } if arg.Visibility != keybase1.TLFVisibility_ANY { queryBase.TlfVisibility = &arg.Visibility } queryBase.Status = arg.Status queryBase.ConvIDs = arg.ConvIDs var gires chat1.GetInboxAndUnboxLocalRes if arg.UnreadFirst { if arg.UnreadFirstLimit.AtMost <= 0 { arg.UnreadFirstLimit.AtMost = int(^uint(0) >> 1) // maximum int } if arg.UnreadFirstLimit.AtMost < arg.UnreadFirstLimit.AtLeast { arg.UnreadFirstLimit.AtMost = arg.UnreadFirstLimit.AtLeast } query := queryBase query.UnreadOnly, query.ReadOnly = true, false ib, err := h.G().InboxSource.ReadUnverified(ctx, uid, types.InboxSourceDataSourceAll, &query) if err != nil { return chat1.GetInboxSummaryForCLILocalRes{}, err } res.Conversations, err = h.limitConvResults(ctx, uid, ib.ConvsUnverified, arg.UnreadFirstLimit.AtMost) if err != nil { return chat1.GetInboxSummaryForCLILocalRes{}, err } more := utils.Collar( arg.UnreadFirstLimit.AtLeast-len(res.Conversations), arg.UnreadFirstLimit.NumRead, arg.UnreadFirstLimit.AtMost-len(res.Conversations), ) if more > 0 { query := queryBase query.UnreadOnly, query.ReadOnly = false, true ib, err := h.G().InboxSource.ReadUnverified(ctx, uid, types.InboxSourceDataSourceAll, &query) if err != nil { return chat1.GetInboxSummaryForCLILocalRes{}, err } moreConvs, err := h.limitConvResults(ctx, uid, ib.ConvsUnverified, more) if err != nil { return chat1.GetInboxSummaryForCLILocalRes{}, err } res.Conversations = append(res.Conversations, moreConvs...) } } else { if arg.ActivitySortedLimit <= 0 { arg.ActivitySortedLimit = int(^uint(0) >> 1) // maximum int } query := queryBase query.UnreadOnly, query.ReadOnly = false, false ib, err := h.G().InboxSource.ReadUnverified(ctx, uid, types.InboxSourceDataSourceAll, &query) if err != nil { return chat1.GetInboxSummaryForCLILocalRes{}, err } res.Conversations, err = h.limitConvResults(ctx, uid, ib.ConvsUnverified, arg.ActivitySortedLimit) if err != nil { return chat1.GetInboxSummaryForCLILocalRes{}, err } } res.Offline = gires.Offline return res, nil } func (h *Server) GetConversationForCLILocal(ctx context.Context, arg chat1.GetConversationForCLILocalQuery) (res chat1.GetConversationForCLILocalRes, err error) { var identBreaks []keybase1.TLFIdentifyFailure ctx = globals.ChatCtx(ctx, h.G(), keybase1.TLFIdentifyBehavior_CHAT_CLI, &identBreaks, h.identNotifier) defer h.Trace(ctx, &err, "GetConversationForCLILocal")() defer func() { h.setResultRateLimit(ctx, &res) }() defer func() { err = h.handleOfflineError(ctx, err, &res) }() if _, err = utils.AssertLoggedInUID(ctx, h.G()); err != nil { return res, err } if arg.Limit.AtMost <= 0 { arg.Limit.AtMost = int(^uint(0) >> 1) // maximum int } if arg.Limit.AtMost < arg.Limit.AtLeast { arg.Limit.AtMost = arg.Limit.AtLeast } convLocal := arg.Conv var since time.Time if arg.Since != nil { since, err = utils.ParseTimeFromRFC3339OrDurationFromPast(h.G(), *arg.Since) if err != nil { return res, fmt.Errorf("parsing time or duration (%s) error: %s", *arg.Since, since) } } query := chat1.GetThreadQuery{ MarkAsRead: arg.MarkAsRead, MessageTypes: arg.MessageTypes, } if !since.IsZero() { gsince := gregor1.ToTime(since) query.After = &gsince } tv, err := h.GetThreadLocal(ctx, chat1.GetThreadLocalArg{ ConversationID: convLocal.Info.Id, Query: &query, IdentifyBehavior: keybase1.TLFIdentifyBehavior_CHAT_CLI, }) if err != nil { return chat1.GetConversationForCLILocalRes{}, err } // apply message count limits var messages []chat1.MessageUnboxed for _, m := range tv.Thread.Messages { messages = append(messages, m) arg.Limit.AtMost-- arg.Limit.AtLeast-- if m.GetMessageID() <= convLocal.ReaderInfo.ReadMsgid { arg.Limit.NumRead-- } if arg.Limit.AtMost <= 0 || (arg.Limit.NumRead <= 0 && arg.Limit.AtLeast <= 0) { break } } return chat1.GetConversationForCLILocalRes{ Conversation: convLocal, Messages: messages, Offline: tv.Offline, }, nil } func (h *Server) GetMessagesLocal(ctx context.Context, arg chat1.GetMessagesLocalArg) (res chat1.GetMessagesLocalRes, err error) { var identBreaks []keybase1.TLFIdentifyFailure ctx = globals.ChatCtx(ctx, h.G(), arg.IdentifyBehavior, &identBreaks, h.identNotifier) defer h.Trace(ctx, &err, "GetMessagesLocal")() defer func() { h.setResultRateLimit(ctx, &res) }() defer func() { err = h.handleOfflineError(ctx, err, &res) }() uid, err := utils.AssertLoggedInUID(ctx, h.G()) if err != nil { return res, err } reason := chat1.GetThreadReason_GENERAL messages, err := h.G().ChatHelper.GetMessages(ctx, uid, arg.ConversationID, arg.MessageIDs, !arg.DisableResolveSupersedes, &reason) if err != nil { return res, err } return chat1.GetMessagesLocalRes{ Messages: messages, IdentifyFailures: identBreaks, }, nil } func (h *Server) GetNextAttachmentMessageLocal(ctx context.Context, arg chat1.GetNextAttachmentMessageLocalArg, ) (res chat1.GetNextAttachmentMessageLocalRes, err error) { var identBreaks []keybase1.TLFIdentifyFailure ctx = globals.ChatCtx(ctx, h.G(), arg.IdentifyBehavior, &identBreaks, h.identNotifier) defer h.Trace(ctx, &err, "GetNextAttachmentMessageLocal(%s,%d,%v)", arg.ConvID, arg.MessageID, arg.BackInTime)() defer func() { h.setResultRateLimit(ctx, &res) }() defer func() { err = h.handleOfflineError(ctx, err, &res) }() uid, err := utils.AssertLoggedInUID(ctx, h.G()) if err != nil { return res, err } gallery := attachments.NewGallery(h.G()) unboxed, _, err := gallery.NextMessage(ctx, uid, arg.ConvID, arg.MessageID, attachments.NextMessageOptions{ BackInTime: arg.BackInTime, AssetTypes: arg.AssetTypes, }, ) if err != nil { return res, err } var resMsg *chat1.UIMessage if unboxed != nil { resMsg = new(chat1.UIMessage) *resMsg = utils.PresentMessageUnboxed(ctx, h.G(), *unboxed, uid, arg.ConvID) } return chat1.GetNextAttachmentMessageLocalRes{ Message: resMsg, IdentifyFailures: identBreaks, }, nil } func (h *Server) SetConversationStatusLocal(ctx context.Context, arg chat1.SetConversationStatusLocalArg) (res chat1.SetConversationStatusLocalRes, err error) { var identBreaks []keybase1.TLFIdentifyFailure ctx = globals.ChatCtx(ctx, h.G(), arg.IdentifyBehavior, &identBreaks, h.identNotifier) defer h.Trace(ctx, &err, "SetConversationStatusLocal: %v", arg.Status)() defer func() { h.setResultRateLimit(ctx, &res) }() uid, err := utils.AssertLoggedInUID(ctx, h.G()) if err != nil { return res, err } err = h.G().InboxSource.RemoteSetConversationStatus(ctx, uid, arg.ConversationID, arg.Status) return chat1.SetConversationStatusLocalRes{ IdentifyFailures: identBreaks, }, err } // PostLocal implements keybase.chatLocal.postLocal protocol. func (h *Server) PostLocal(ctx context.Context, arg chat1.PostLocalArg) (res chat1.PostLocalRes, err error) { var identBreaks []keybase1.TLFIdentifyFailure ctx = globals.ChatCtx(ctx, h.G(), arg.IdentifyBehavior, &identBreaks, h.identNotifier) defer h.Trace(ctx, &err, "PostLocal")() defer func() { h.setResultRateLimit(ctx, &res) }() uid, err := utils.AssertLoggedInUID(ctx, h.G()) if err != nil { return res, err } // Check for any slash command hits for an execute if handled, err := h.G().CommandsSource.AttemptBuiltinCommand(ctx, uid, arg.ConversationID, arg.Msg.ClientHeader.TlfName, arg.Msg.MessageBody, arg.ReplyTo); handled { h.Debug(ctx, "PostLocal: handled slash command with error: %s", err) return res, nil } // Run Stellar UI on any payments in the body if !arg.SkipInChatPayments { if arg.Msg.MessageBody, err = h.runStellarSendUI(ctx, arg.SessionID, uid, arg.ConversationID, arg.Msg.MessageBody, arg.ReplyTo); err != nil { return res, err } } var prepareOpts chat1.SenderPrepareOptions prepareOpts.ReplyTo = arg.ReplyTo sender := NewBlockingSender(h.G(), h.boxer, h.remoteClient) _, msgBoxed, err := sender.Send(ctx, arg.ConversationID, arg.Msg, 0, nil, nil, &prepareOpts) if err != nil { h.Debug(ctx, "PostLocal: unable to send message: %s", err.Error()) return res, err } return chat1.PostLocalRes{ MessageID: msgBoxed.GetMessageID(), IdentifyFailures: identBreaks, }, nil } func (h *Server) PostDeleteNonblock(ctx context.Context, arg chat1.PostDeleteNonblockArg) (chat1.PostLocalNonblockRes, error) { var parg chat1.PostLocalNonblockArg parg.ClientPrev = arg.ClientPrev parg.ConversationID = arg.ConversationID parg.IdentifyBehavior = arg.IdentifyBehavior parg.OutboxID = arg.OutboxID parg.Msg.ClientHeader.MessageType = chat1.MessageType_DELETE parg.Msg.ClientHeader.Supersedes = arg.Supersedes parg.Msg.ClientHeader.TlfName = arg.TlfName parg.Msg.ClientHeader.TlfPublic = arg.TlfPublic return h.PostLocalNonblock(ctx, parg) } func (h *Server) PostEditNonblock(ctx context.Context, arg chat1.PostEditNonblockArg) (res chat1.PostLocalNonblockRes, err error) { var identBreaks []keybase1.TLFIdentifyFailure ctx = globals.ChatCtx(ctx, h.G(), arg.IdentifyBehavior, &identBreaks, h.identNotifier) defer h.Trace(ctx, &err, "PostEditNonblock")() var parg chat1.PostLocalNonblockArg var supersedes chat1.MessageID if arg.Target.MessageID != nil && *arg.Target.MessageID > 0 { supersedes = *arg.Target.MessageID } if supersedes.IsNil() && arg.Target.OutboxID == nil { return res, errors.New("must specify a messageID or outboxID for edit") } parg.ClientPrev = arg.ClientPrev parg.ConversationID = arg.ConversationID parg.IdentifyBehavior = arg.IdentifyBehavior parg.OutboxID = arg.OutboxID parg.Msg.ClientHeader.MessageType = chat1.MessageType_EDIT parg.Msg.ClientHeader.Supersedes = supersedes parg.Msg.ClientHeader.TlfName = arg.TlfName parg.Msg.ClientHeader.TlfPublic = arg.TlfPublic parg.Msg.MessageBody = chat1.NewMessageBodyWithEdit(chat1.MessageEdit{ MessageID: supersedes, Body: arg.Body, }) if supersedes.IsNil() { h.Debug(ctx, "PostEditNonblock: setting supersedes outboxID: %s", arg.Target.OutboxID) parg.Msg.SupersedesOutboxID = arg.Target.OutboxID } return h.PostLocalNonblock(ctx, parg) } func (h *Server) runStellarSendUI(ctx context.Context, sessionID int, uid gregor1.UID, convID chat1.ConversationID, msgBody chat1.MessageBody, replyTo *chat1.MessageID, ) (res chat1.MessageBody, err error) { defer h.Trace(ctx, &err, "runStellarSendUI")() ui := h.getChatUI(sessionID) bodyTyp, err := msgBody.MessageType() if err != nil || bodyTyp != chat1.MessageType_TEXT { return msgBody, nil } body := msgBody.Text().Body parsedPayments := h.G().StellarSender.ParsePayments(ctx, uid, convID, body, replyTo) if len(parsedPayments) == 0 { h.Debug(ctx, "runStellarSendUI: no payments") return msgBody, nil } h.Debug(ctx, "runStellarSendUI: payments found, showing confirm screen") if err := ui.ChatStellarShowConfirm(ctx); err != nil { return res, err } defer func() { _ = ui.ChatStellarDone(ctx, err != nil) }() uiSummary, toSend, err := h.G().StellarSender.DescribePayments(ctx, uid, convID, parsedPayments) if err != nil { if err := libkb.ExportErrorAsStatus(h.G().GlobalContext, err); err != nil { _, _ = ui.ChatStellarDataError(ctx, *err) } else { h.Debug(ctx, "error exported to nothing") // should never happen } return res, err } h.Debug(ctx, "runStellarSendUI: payments described, telling UI") accepted, err := ui.ChatStellarDataConfirm(ctx, uiSummary) if err != nil { return res, err } if !accepted { return res, errors.New("Payment message declined") } h.Debug(ctx, "runStellarSendUI: message confirmed, sending payments") payments, err := h.G().StellarSender.SendPayments(ctx, convID, toSend) if err != nil { // Send regardless here h.Debug(ctx, "runStellarSendUI: failed to send payments, but continuing on: %s", err) return msgBody, nil } newBody := msgBody.Text().DeepCopy() newBody.Body = body newBody.Payments = payments return chat1.NewMessageBodyWithText(newBody), nil } var quickReactionPattern = regexp.MustCompile(`(?:^\+:)([^\s]+)(?::)$`) func (h *Server) isQuickReaction(ctx context.Context, uid gregor1.UID, convID chat1.ConversationID, body string, ) (reaction string, msgID chat1.MessageID, ok bool) { body = strings.TrimSpace(body) if !strings.HasPrefix(body, "+:") || !strings.HasSuffix(body, ":") { return "", 0, false } hits := quickReactionPattern.FindStringSubmatch(body) if len(hits) < 2 { return "", 0, false } tryStock := func() (string, bool) { if h.G().EmojiSource.IsStockEmoji(hits[1]) { return ":" + hits[1] + ":", true } return "", false } tryCustom := func() (string, bool) { emojis, err := h.G().EmojiSource.Harvest(ctx, body, uid, convID, types.EmojiHarvestModeFast) if err != nil { h.Debug(ctx, "isQuickReaction: failed to harvest: %s", err) return "", false } if len(emojis) != 1 { return "", false } return ":" + emojis[0].Alias + ":", true } var hit *string if reaction, ok = tryStock(); ok { hit = new(string) *hit = reaction } if hit == nil { if reaction, ok = tryCustom(); ok { hit = new(string) *hit = reaction } } if hit == nil { return "", 0, false } conv, err := utils.GetUnverifiedConv(ctx, h.G(), uid, convID, types.InboxSourceDataSourceLocalOnly) if err != nil { h.Debug(ctx, "isQuickReaction: failed to get conv: %s", err) return "", 0, false } return *hit, conv.MaxVisibleMsgID(), true } func (h *Server) PostTextNonblock(ctx context.Context, arg chat1.PostTextNonblockArg) (res chat1.PostLocalNonblockRes, err error) { ctx = globals.ChatCtx(ctx, h.G(), arg.IdentifyBehavior, nil, h.identNotifier) defer h.Trace(ctx, &err, "PostTextNonblock")() uid, err := utils.AssertLoggedInUID(ctx, h.G()) if err != nil { return res, err } reaction, msgID, ok := h.isQuickReaction(ctx, uid, arg.ConversationID, arg.Body) if ok { h.Debug(ctx, "PostTextNonblock: detected quick reaction") return h.PostReactionNonblock(ctx, chat1.PostReactionNonblockArg{ ConversationID: arg.ConversationID, TlfName: arg.TlfName, TlfPublic: arg.TlfPublic, Supersedes: msgID, Body: reaction, OutboxID: arg.OutboxID, ClientPrev: arg.ClientPrev, IdentifyBehavior: arg.IdentifyBehavior, }) } var parg chat1.PostLocalNonblockArg parg.SessionID = arg.SessionID parg.ClientPrev = arg.ClientPrev parg.ConversationID = arg.ConversationID parg.IdentifyBehavior = arg.IdentifyBehavior parg.OutboxID = arg.OutboxID parg.ReplyTo = arg.ReplyTo parg.Msg.ClientHeader.MessageType = chat1.MessageType_TEXT parg.Msg.ClientHeader.TlfName = arg.TlfName parg.Msg.ClientHeader.TlfPublic = arg.TlfPublic parg.Msg.MessageBody = chat1.NewMessageBodyWithText(chat1.MessageText{ Body: arg.Body, }) if arg.EphemeralLifetime != nil { parg.Msg.ClientHeader.EphemeralMetadata = &chat1.MsgEphemeralMetadata{ Lifetime: *arg.EphemeralLifetime, } } return h.PostLocalNonblock(ctx, parg) } func (h *Server) PostReactionNonblock(ctx context.Context, arg chat1.PostReactionNonblockArg) (res chat1.PostLocalNonblockRes, err error) { var parg chat1.PostLocalNonblockArg parg.ClientPrev = arg.ClientPrev parg.ConversationID = arg.ConversationID parg.IdentifyBehavior = arg.IdentifyBehavior parg.OutboxID = arg.OutboxID parg.Msg.ClientHeader.MessageType = chat1.MessageType_REACTION parg.Msg.ClientHeader.Supersedes = arg.Supersedes parg.Msg.ClientHeader.TlfName = arg.TlfName parg.Msg.ClientHeader.TlfPublic = arg.TlfPublic parg.Msg.MessageBody = chat1.NewMessageBodyWithReaction(chat1.MessageReaction{ MessageID: arg.Supersedes, Body: arg.Body, }) return h.PostLocalNonblock(ctx, parg) } func (h *Server) PostHeadlineNonblock(ctx context.Context, arg chat1.PostHeadlineNonblockArg) (chat1.PostLocalNonblockRes, error) { var parg chat1.PostLocalNonblockArg parg.ClientPrev = arg.ClientPrev parg.ConversationID = arg.ConversationID parg.IdentifyBehavior = arg.IdentifyBehavior parg.OutboxID = arg.OutboxID parg.Msg.ClientHeader.MessageType = chat1.MessageType_HEADLINE parg.Msg.ClientHeader.TlfName = arg.TlfName parg.Msg.ClientHeader.TlfPublic = arg.TlfPublic parg.Msg.MessageBody = chat1.NewMessageBodyWithHeadline(chat1.MessageHeadline{ Headline: arg.Headline, }) return h.PostLocalNonblock(ctx, parg) } func (h *Server) PostHeadline(ctx context.Context, arg chat1.PostHeadlineArg) (chat1.PostLocalRes, error) { var parg chat1.PostLocalArg parg.ConversationID = arg.ConversationID parg.IdentifyBehavior = arg.IdentifyBehavior parg.Msg.ClientHeader.MessageType = chat1.MessageType_HEADLINE parg.Msg.ClientHeader.TlfName = arg.TlfName parg.Msg.ClientHeader.TlfPublic = arg.TlfPublic parg.Msg.MessageBody = chat1.NewMessageBodyWithHeadline(chat1.MessageHeadline{ Headline: arg.Headline, }) return h.PostLocal(ctx, parg) } func (h *Server) PostMetadataNonblock(ctx context.Context, arg chat1.PostMetadataNonblockArg) (chat1.PostLocalNonblockRes, error) { var parg chat1.PostLocalNonblockArg parg.ClientPrev = arg.ClientPrev parg.ConversationID = arg.ConversationID parg.IdentifyBehavior = arg.IdentifyBehavior parg.OutboxID = arg.OutboxID parg.Msg.ClientHeader.MessageType = chat1.MessageType_METADATA parg.Msg.ClientHeader.TlfName = arg.TlfName parg.Msg.ClientHeader.TlfPublic = arg.TlfPublic parg.Msg.MessageBody = chat1.NewMessageBodyWithMetadata(chat1.MessageConversationMetadata{ ConversationTitle: arg.ChannelName, }) return h.PostLocalNonblock(ctx, parg) } func (h *Server) PostMetadata(ctx context.Context, arg chat1.PostMetadataArg) (chat1.PostLocalRes, error) { var parg chat1.PostLocalArg parg.ConversationID = arg.ConversationID parg.IdentifyBehavior = arg.IdentifyBehavior parg.Msg.ClientHeader.MessageType = chat1.MessageType_METADATA parg.Msg.ClientHeader.TlfName = arg.TlfName parg.Msg.ClientHeader.TlfPublic = arg.TlfPublic parg.Msg.MessageBody = chat1.NewMessageBodyWithMetadata(chat1.MessageConversationMetadata{ ConversationTitle: arg.ChannelName, }) return h.PostLocal(ctx, parg) } func (h *Server) PostDeleteHistoryUpto(ctx context.Context, arg chat1.PostDeleteHistoryUptoArg) (res chat1.PostLocalRes, err error) { ctx = globals.ChatCtx(ctx, h.G(), arg.IdentifyBehavior, nil, h.identNotifier) defer h.Trace(ctx, &err, "PostDeleteHistoryUpto")() delh := chat1.MessageDeleteHistory{Upto: arg.Upto} var parg chat1.PostLocalArg parg.ConversationID = arg.ConversationID parg.IdentifyBehavior = arg.IdentifyBehavior parg.Msg.ClientHeader.MessageType = chat1.MessageType_DELETEHISTORY parg.Msg.ClientHeader.TlfName = arg.TlfName parg.Msg.ClientHeader.TlfPublic = arg.TlfPublic parg.Msg.ClientHeader.DeleteHistory = &delh parg.Msg.MessageBody = chat1.NewMessageBodyWithDeletehistory(delh) h.Debug(ctx, "PostDeleteHistoryUpto: deleting upto msgid:%v", delh.Upto) return h.PostLocal(ctx, parg) } func (h *Server) PostDeleteHistoryThrough(ctx context.Context, arg chat1.PostDeleteHistoryThroughArg) (res chat1.PostLocalRes, err error) { ctx = globals.ChatCtx(ctx, h.G(), arg.IdentifyBehavior, nil, h.identNotifier) defer h.Trace(ctx, &err, "PostDeleteHistoryThrough")() return h.PostDeleteHistoryUpto(ctx, chat1.PostDeleteHistoryUptoArg{ ConversationID: arg.ConversationID, TlfName: arg.TlfName, TlfPublic: arg.TlfPublic, IdentifyBehavior: arg.IdentifyBehavior, Upto: arg.Through + 1, }) } func (h *Server) PostDeleteHistoryByAge(ctx context.Context, arg chat1.PostDeleteHistoryByAgeArg) (res chat1.PostLocalRes, err error) { ctx = globals.ChatCtx(ctx, h.G(), arg.IdentifyBehavior, nil, h.identNotifier) defer h.Trace(ctx, &err, "PostDeleteHistoryByAge")() gmRes, err := h.remoteClient().GetMessageBefore(ctx, chat1.GetMessageBeforeArg{ ConvID: arg.ConversationID, Age: arg.Age, }) if err != nil { return res, err } upto := gmRes.MsgID + 1 h.Debug(ctx, "PostDeleteHistoryByAge: deleting upto msgid:%v (age:%v)", upto, arg.Age) return h.PostDeleteHistoryUpto(ctx, chat1.PostDeleteHistoryUptoArg{ ConversationID: arg.ConversationID, TlfName: arg.TlfName, TlfPublic: arg.TlfPublic, IdentifyBehavior: arg.IdentifyBehavior, Upto: upto, }) } func (h *Server) GenerateOutboxID(ctx context.Context) (res chat1.OutboxID, err error) { ctx = globals.ChatCtx(ctx, h.G(), keybase1.TLFIdentifyBehavior_CHAT_SKIP, nil, h.identNotifier) defer h.Trace(ctx, &err, "GenerateOutboxID")() return storage.NewOutboxID() } func (h *Server) PostLocalNonblock(ctx context.Context, arg chat1.PostLocalNonblockArg) (res chat1.PostLocalNonblockRes, err error) { var identBreaks []keybase1.TLFIdentifyFailure ctx = globals.ChatCtx(ctx, h.G(), arg.IdentifyBehavior, &identBreaks, h.identNotifier) defer h.Trace(ctx, &err, "PostLocalNonblock")() defer h.suspendBgConvLoads(ctx)() defer func() { h.setResultRateLimit(ctx, &res) }() uid, err := utils.AssertLoggedInUID(ctx, h.G()) if err != nil { return res, err } // Clear draft if err := h.G().InboxSource.Draft(ctx, uid, arg.ConversationID, nil); err != nil { h.Debug(ctx, "PostLocalNonblock: failed to clear draft: %s", err) } // Check for any slash command hits for an execute if handled, err := h.G().CommandsSource.AttemptBuiltinCommand(ctx, uid, arg.ConversationID, arg.Msg.ClientHeader.TlfName, arg.Msg.MessageBody, arg.ReplyTo); handled { h.Debug(ctx, "PostLocalNonblock: handled slash command with error: %s", err) return res, nil } if !arg.SkipInChatPayments { // Determine if the messages contains any Stellar payments, and execute // them if so if arg.Msg.MessageBody, err = h.runStellarSendUI(ctx, arg.SessionID, uid, arg.ConversationID, arg.Msg.MessageBody, arg.ReplyTo); err != nil { return res, err } } // Create non block sender var prepareOpts chat1.SenderPrepareOptions sender := NewBlockingSender(h.G(), h.boxer, h.remoteClient) nonblockSender := NewNonblockingSender(h.G(), sender) prepareOpts.ReplyTo = arg.ReplyTo if arg.Msg.ClientHeader.Conv.TopicType == chat1.TopicType_NONE { arg.Msg.ClientHeader.Conv.TopicType = chat1.TopicType_CHAT } obid, _, err := nonblockSender.Send(ctx, arg.ConversationID, arg.Msg, arg.ClientPrev, arg.OutboxID, nil, &prepareOpts) if err != nil { return res, fmt.Errorf("PostLocalNonblock: unable to send message: err: %s", err.Error()) } h.Debug(ctx, "PostLocalNonblock: using outboxID: %s", obid) return chat1.PostLocalNonblockRes{ OutboxID: obid, IdentifyFailures: identBreaks, }, nil } // MakePreview implements chat1.LocalInterface.MakePreview. func (h *Server) MakePreview(ctx context.Context, arg chat1.MakePreviewArg) (res chat1.MakePreviewRes, err error) { defer h.Trace(ctx, &err, "MakePreview")() return attachments.NewSender(h.G()).MakePreview(ctx, arg.Filename, arg.OutboxID) } func (h *Server) MakeAudioPreview(ctx context.Context, arg chat1.MakeAudioPreviewArg) (res chat1.MakePreviewRes, err error) { defer h.Trace(ctx, &err, "MakeAudioPreview")() return attachments.NewSender(h.G()).MakeAudioPreview(ctx, arg.Amps, arg.Duration) } func (h *Server) GetUploadTempFile(ctx context.Context, arg chat1.GetUploadTempFileArg) (res string, err error) { defer h.Trace(ctx, &err, "GetUploadTempFile")() return h.G().AttachmentUploader.GetUploadTempFile(ctx, arg.OutboxID, arg.Filename) } func (h *Server) MakeUploadTempFile(ctx context.Context, arg chat1.MakeUploadTempFileArg) (res string, err error) { defer h.Trace(ctx, &err, "MakeUploadTempFile")() if res, err = h.G().AttachmentUploader.GetUploadTempFile(ctx, arg.OutboxID, arg.Filename); err != nil { return res, err } // Write via a temp file + rename so an interrupted write can't leave a // partially written upload payload behind. return res, libkb.NewFile(res, arg.Data, 0o600).Save(h.G().ExternalG().Log) } func (h *Server) CancelUploadTempFile(ctx context.Context, outboxID chat1.OutboxID) (err error) { defer h.Trace(ctx, &err, "CancelUploadTempFile: %s", outboxID)() return h.G().AttachmentUploader.CancelUploadTempFile(ctx, outboxID) } func (h *Server) PostFileAttachmentLocalNonblock(ctx context.Context, arg chat1.PostFileAttachmentLocalNonblockArg, ) (res chat1.PostLocalNonblockRes, err error) { var identBreaks []keybase1.TLFIdentifyFailure ctx = globals.ChatCtx(ctx, h.G(), arg.Arg.IdentifyBehavior, &identBreaks, h.identNotifier) defer h.Trace(ctx, &err, "PostFileAttachmentLocalNonblock")() defer h.suspendBgConvLoads(ctx)() defer func() { h.setResultRateLimit(ctx, &res) }() uid, err := utils.AssertLoggedInUID(ctx, h.G()) if err != nil { return res, err } // Create non block sender sender := NewNonblockingSender(h.G(), NewBlockingSender(h.G(), h.boxer, h.remoteClient)) outboxID, _, err := attachments.NewSender(h.G()).PostFileAttachmentMessage(ctx, sender, arg.Arg.ConversationID, arg.Arg.TlfName, arg.Arg.Visibility, arg.Arg.OutboxID, arg.Arg.Filename, arg.Arg.Title, arg.Arg.Metadata, arg.ClientPrev, arg.Arg.EphemeralLifetime, arg.Arg.CallerPreview) if err != nil { return res, err } _, err = h.G().AttachmentUploader.Register(ctx, uid, arg.Arg.ConversationID, outboxID, arg.Arg.Title, arg.Arg.Filename, nil, arg.Arg.CallerPreview) if err != nil { return res, err } return chat1.PostLocalNonblockRes{ OutboxID: outboxID, IdentifyFailures: identBreaks, }, nil } // PostFileAttachmentLocal implements chat1.LocalInterface.PostFileAttachmentLocal. func (h *Server) PostFileAttachmentLocal(ctx context.Context, arg chat1.PostFileAttachmentLocalArg) (res chat1.PostLocalRes, err error) { var identBreaks []keybase1.TLFIdentifyFailure ctx = globals.ChatCtx(ctx, h.G(), arg.Arg.IdentifyBehavior, &identBreaks, h.identNotifier) defer h.Trace(ctx, &err, "PostFileAttachmentLocal")() defer h.suspendBgConvLoads(ctx)() defer func() { h.setResultRateLimit(ctx, &res) }() uid, err := utils.AssertLoggedInUID(ctx, h.G()) if err != nil { return res, err } // Get base of message we are going to send sender := NewBlockingSender(h.G(), h.boxer, h.remoteClient) _, msgID, err := attachments.NewSender(h.G()).PostFileAttachment(ctx, sender, uid, arg.Arg.ConversationID, arg.Arg.TlfName, arg.Arg.Visibility, arg.Arg.OutboxID, arg.Arg.Filename, arg.Arg.Title, arg.Arg.Metadata, 0, arg.Arg.EphemeralLifetime, arg.Arg.CallerPreview) if err != nil { return res, err } if msgID == nil { return res, errors.New("no message ID returned from post") } return chat1.PostLocalRes{ MessageID: *msgID, IdentifyFailures: identBreaks, }, nil } // DownloadAttachmentLocal implements chat1.LocalInterface.DownloadAttachmentLocal. func (h *Server) DownloadAttachmentLocal(ctx context.Context, arg chat1.DownloadAttachmentLocalArg) (res chat1.DownloadAttachmentLocalRes, err error) { var identBreaks []keybase1.TLFIdentifyFailure ctx = globals.ChatCtx(ctx, h.G(), arg.IdentifyBehavior, &identBreaks, h.identNotifier) defer h.Trace(ctx, &err, "DownloadAttachmentLocal")() defer func() { h.setResultRateLimit(ctx, &res) }() uid, err := utils.AssertLoggedInUID(ctx, h.G()) if err != nil { return res, err } if arg.MessageID == 0 { return res, errors.New("invalid message ID provided") } darg := downloadAttachmentArg{ SessionID: arg.SessionID, ConversationID: arg.ConversationID, MessageID: arg.MessageID, Preview: arg.Preview, IdentifyBehavior: arg.IdentifyBehavior, } cli := h.getStreamUICli() darg.Sink = libkb.NewRemoteStreamBuffered(arg.Sink, cli, arg.SessionID) return h.downloadAttachmentLocal(ctx, uid, darg) } func (h *Server) getFileAttachmentDownloadDirs() (cacheDir, downloadDir string) { h.fileAttachmentDownloadConfigurationMu.RLock() defer h.fileAttachmentDownloadConfigurationMu.RUnlock() return h.fileAttachmentDownloadCacheDir, h.fileAttachmentDownloadDownloadDir } func (h *Server) ConfigureFileAttachmentDownloadLocal(ctx context.Context, arg chat1.ConfigureFileAttachmentDownloadLocalArg) (err error) { h.fileAttachmentDownloadConfigurationMu.Lock() defer h.fileAttachmentDownloadConfigurationMu.Unlock() h.fileAttachmentDownloadCacheDir, h.fileAttachmentDownloadDownloadDir = arg.CacheDirOverride, arg.DownloadDirOverride return nil } // DownloadFileAttachmentLocal implements chat1.LocalInterface.DownloadFileAttachmentLocal. func (h *Server) DownloadFileAttachmentLocal(ctx context.Context, arg chat1.DownloadFileAttachmentLocalArg) (res chat1.DownloadFileAttachmentLocalRes, err error) { var identBreaks []keybase1.TLFIdentifyFailure ctx = globals.ChatCtx(ctx, h.G(), arg.IdentifyBehavior, &identBreaks, h.identNotifier) defer h.Trace(ctx, &err, "DownloadFileAttachmentLocal")() defer func() { h.setResultRateLimit(ctx, &res) }() uid, err := utils.AssertLoggedInUID(ctx, h.G()) if err != nil { return res, err } if arg.MessageID == 0 { return res, errors.New("invalid message ID provided") } darg := downloadAttachmentArg{ SessionID: arg.SessionID, ConversationID: arg.ConversationID, MessageID: arg.MessageID, Preview: arg.Preview, IdentifyBehavior: arg.IdentifyBehavior, } cacheDir, downloadDir := h.getFileAttachmentDownloadDirs() downloadParentdir, useArbitraryName := downloadDir, false if arg.DownloadToCache { downloadParentdir, useArbitraryName = cacheDir, true } filePath, sink, err := attachments.SinkFromFilename(ctx, h.G(), uid, arg.ConversationID, arg.MessageID, downloadParentdir, useArbitraryName) if err != nil { return res, err } defer func() { // In the event of any error delete the file if it's empty. if err != nil { h.Debug(ctx, "DownloadFileAttachmentLocal: deleteFileIfEmpty: %v", deleteFileIfEmpty(filePath)) } }() if err := attachments.Quarantine(ctx, filePath); err != nil { h.Debug(ctx, "DownloadFileAttachmentLocal: failed to quarantine download: %s", err) return res, err } darg.Sink = sink ires, err := h.downloadAttachmentLocal(ctx, uid, darg) if err != nil { return res, err } return chat1.DownloadFileAttachmentLocalRes{ FilePath: filePath, IdentifyFailures: ires.IdentifyFailures, }, nil } func deleteFileIfEmpty(filename string) (err error) { f, err := os.Stat(filename) if err != nil { return err } if f.Size() == 0 { return os.Remove(filename) } return nil } type downloadAttachmentArg struct { SessionID int ConversationID chat1.ConversationID MessageID chat1.MessageID Sink io.WriteCloser Preview bool IdentifyBehavior keybase1.TLFIdentifyBehavior } func (h *Server) downloadAttachmentLocal(ctx context.Context, uid gregor1.UID, arg downloadAttachmentArg) (res chat1.DownloadAttachmentLocalRes, err error) { var identBreaks []keybase1.TLFIdentifyFailure ctx = globals.ChatCtx(ctx, h.G(), arg.IdentifyBehavior, &identBreaks, h.identNotifier) progress := func(bytesComplete, bytesTotal int64) { h.G().NotifyRouter.HandleChatAttachmentDownloadProgress(ctx, keybase1.UID(uid.String()), arg.ConversationID, arg.MessageID, bytesComplete, bytesTotal) } h.Debug(ctx, "downloadAttachmentLocal: fetching asset from attachment message: convID: %s messageID: %d", arg.ConversationID, arg.MessageID) err = attachments.Download(ctx, h.G(), uid, arg.ConversationID, arg.MessageID, arg.Sink, arg.Preview, progress, h.remoteClient) if err != nil { return res, err } h.G().NotifyRouter.HandleChatAttachmentDownloadComplete(ctx, keybase1.UID(uid.String()), arg.ConversationID, arg.MessageID) return chat1.DownloadAttachmentLocalRes{ IdentifyFailures: identBreaks, }, nil } func (h *Server) presentUIItem(ctx context.Context, uid gregor1.UID, conv *chat1.ConversationLocal, partMode utils.PresentParticipantsMode, ) (res *chat1.InboxUIItem) { if conv != nil { pc := utils.PresentConversationLocal(ctx, h.G(), uid, *conv, partMode) res = &pc } return res } func (h *Server) CancelPost(ctx context.Context, outboxID chat1.OutboxID) (err error) { ctx = globals.ChatCtx(ctx, h.G(), keybase1.TLFIdentifyBehavior_CHAT_SKIP, nil, h.identNotifier) defer h.Trace(ctx, &err, "CancelPost(%s)", outboxID)() uid, err := utils.AssertLoggedInUID(ctx, h.G()) if err != nil { return err } outbox := storage.NewOutbox(h.G(), uid) obr, err := outbox.RemoveMessage(ctx, outboxID) if err != nil { return err } // Alert the attachment uploader as well, in case this outboxID corresponds to an attachment upload if err := h.G().AttachmentUploader.Cancel(ctx, outboxID); err != nil { return err } convLocal, err := h.G().InboxSource.IncrementLocalConvVersion(ctx, uid, obr.ConvID) if err != nil { h.Debug(ctx, "CancelPost: failed to get IncrementLocalConvVersion") } act := chat1.NewChatActivityWithFailedMessage(chat1.FailedMessageInfo{ OutboxRecords: []chat1.OutboxRecord{obr}, Conv: h.presentUIItem(ctx, uid, convLocal, utils.PresentParticipantsModeSkip), }) h.G().ActivityNotifier.Activity(context.Background(), uid, chat1.TopicType_NONE, &act, chat1.ChatActivitySource_LOCAL) return h.G().Badger.Send(ctx) } func (h *Server) RetryPost(ctx context.Context, arg chat1.RetryPostArg) (err error) { ctx = globals.ChatCtx(ctx, h.G(), keybase1.TLFIdentifyBehavior_CHAT_SKIP, nil, h.identNotifier) defer h.Trace(ctx, &err, "RetryPost: obr: %v", arg.OutboxID)() uid, err := utils.AssertLoggedInUID(ctx, h.G()) if err != nil { return err } // Mark as retry in the outbox outbox := storage.NewOutbox(h.G(), uid) obr, err := outbox.RetryMessage(ctx, arg.OutboxID, arg.IdentifyBehavior) if err != nil { return err } else if obr == nil { return nil } if obr.IsAttachment() { if _, err := h.G().AttachmentUploader.Retry(ctx, obr.OutboxID); err != nil { h.Debug(ctx, "RetryPost: failed to retry attachment upload: %s", err) } } // Force the send loop to try again h.G().MessageDeliverer.ForceDeliverLoop(ctx) return nil } // remoteClient returns a client connection to gregord. func (h *Server) remoteClient() chat1.RemoteInterface { if h.rc != nil { return h.rc } return h.serverConn.GetClient() } func (h *Server) setTestRemoteClient(ri chat1.RemoteInterface) { h.rc = ri } func (h *Server) FindGeneralConvFromTeamID(ctx context.Context, teamID keybase1.TeamID) (res chat1.InboxUIItem, err error) { ctx = globals.ChatCtx(ctx, h.G(), keybase1.TLFIdentifyBehavior_CHAT_GUI, nil, nil) defer h.Trace(ctx, &err, "FindGeneralConvFromTeamID")() uid, err := utils.AssertLoggedInUID(ctx, h.G()) if err != nil { return res, err } tlfID, err := chat1.TeamIDToTLFID(teamID) if err != nil { return res, err } vis := keybase1.TLFVisibility_PRIVATE topicName := globals.DefaultTeamTopic topicType := chat1.TopicType_CHAT query := &chat1.GetInboxLocalQuery{ Name: &chat1.NameQuery{ TlfID: &tlfID, MembersType: chat1.ConversationMembersType_TEAM, }, TlfVisibility: &vis, TopicName: &topicName, TopicType: &topicType, } ib, _, err := h.G().InboxSource.Read(ctx, uid, types.ConversationLocalizerBlocking, types.InboxSourceDataSourceAll, nil, query) if err != nil { return res, err } if len(ib.Convs) != 1 { return res, libkb.NotFoundError{} } return utils.PresentConversationLocal(ctx, h.G(), uid, ib.Convs[0], utils.PresentParticipantsModeSkip), nil } func (h *Server) FindConversationsLocal(ctx context.Context, arg chat1.FindConversationsLocalArg, ) (res chat1.FindConversationsLocalRes, err error) { var identBreaks []keybase1.TLFIdentifyFailure ctx = globals.ChatCtx(ctx, h.G(), arg.IdentifyBehavior, &identBreaks, h.identNotifier) defer h.Trace(ctx, &err, "FindConversationsLocal")() defer func() { h.setResultRateLimit(ctx, &res) }() defer func() { err = h.handleOfflineError(ctx, err, &res) }() uid, err := utils.AssertLoggedInUID(ctx, h.G()) if err != nil { return res, err } res.Conversations, err = FindConversations(ctx, h.G(), h.DebugLabeler, types.InboxSourceDataSourceAll, h.remoteClient, uid, arg.TlfName, arg.TopicType, arg.MembersType, arg.Visibility, arg.TopicName, arg.OneChatPerTLF) if err != nil { return res, err } res.UiConversations = utils.PresentConversationLocals(ctx, h.G(), uid, res.Conversations, utils.PresentParticipantsModeInclude) return res, nil } func (h *Server) UpdateUnsentText(ctx context.Context, arg chat1.UpdateUnsentTextArg) (err error) { ctx = globals.ChatCtx(ctx, h.G(), keybase1.TLFIdentifyBehavior_CHAT_GUI, nil, h.identNotifier) defer h.Trace(ctx, &err, "UpdateUnsentText convID: %s", arg.ConversationID)() uid, err := utils.AssertLoggedInUID(ctx, h.G()) if err != nil { h.Debug(ctx, "UpdateUnsentText: not logged in: %s", err) return nil } // Save draft var draftText *string if len(arg.Text) > 0 { draftText = &arg.Text } if err := h.G().InboxSource.Draft(ctx, uid, arg.ConversationID, draftText); err != nil { h.Debug(ctx, "UpdateUnsentText: failed to save draft: %s", err) } // Attempt to prefetch any unfurls in the background that are in the message text go h.G().Unfurler.Prefetch(globals.BackgroundChatCtx(ctx, h.G()), uid, arg.ConversationID, arg.Text) // Preview any slash commands in the text go h.G().CommandsSource.PreviewBuiltinCommand(globals.BackgroundChatCtx(ctx, h.G()), uid, arg.ConversationID, arg.TlfName, arg.Text) return nil } func (h *Server) UpdateTyping(ctx context.Context, arg chat1.UpdateTypingArg) (err error) { var identBreaks []keybase1.TLFIdentifyFailure ctx = globals.ChatCtx(ctx, h.G(), keybase1.TLFIdentifyBehavior_CHAT_GUI, &identBreaks, h.identNotifier) defer h.Trace(ctx, &err, "UpdateTyping convID: %s", arg.ConversationID)() uid, err := utils.AssertLoggedInUID(ctx, h.G()) if err != nil { h.Debug(ctx, "UpdateTyping: not logged in: %s", err) return nil } // Just bail out if we are offline if !h.G().Syncer.IsConnected(ctx) { return nil } deviceID := make([]byte, libkb.DeviceIDLen) if err := h.G().Env.GetDeviceID().ToBytes(deviceID); err != nil { h.Debug(ctx, "UpdateTyping: failed to get device: %s", err) return nil } if err := h.remoteClient().UpdateTypingRemote(ctx, chat1.UpdateTypingRemoteArg{ Uid: uid, DeviceID: deviceID, ConvID: arg.ConversationID, Typing: arg.Typing, }); err != nil { h.Debug(ctx, "UpdateTyping: failed to hit the server: %s", err.Error()) } return nil } func (h *Server) JoinConversationByIDLocal(ctx context.Context, convID chat1.ConversationID) (res chat1.JoinLeaveConversationLocalRes, err error) { var identBreaks []keybase1.TLFIdentifyFailure ctx = globals.ChatCtx(ctx, h.G(), keybase1.TLFIdentifyBehavior_CHAT_GUI, &identBreaks, h.identNotifier) defer h.Trace(ctx, &err, "JoinConversationByIDLocal: convID: %s", convID)() defer func() { err = h.handleOfflineError(ctx, err, &res) }() defer func() { h.setResultRateLimit(ctx, &res) }() defer func() { if res.Offline { h.Debug(ctx, "JoinConversationByIDLocal: result obtained offline") } }() uid, err := utils.AssertLoggedInUID(ctx, h.G()) if err != nil { return res, err } err = JoinConversation(ctx, h.G(), h.DebugLabeler, h.remoteClient, uid, convID) if err != nil { return res, err } res.Offline = h.G().InboxSource.IsOffline(ctx) return res, nil } func (h *Server) JoinConversationLocal(ctx context.Context, arg chat1.JoinConversationLocalArg) (res chat1.JoinLeaveConversationLocalRes, err error) { var identBreaks []keybase1.TLFIdentifyFailure ctx = globals.ChatCtx(ctx, h.G(), keybase1.TLFIdentifyBehavior_CHAT_GUI, &identBreaks, h.identNotifier) defer h.Trace(ctx, &err, "JoinConversation(%s)", arg.TopicName)() defer func() { err = h.handleOfflineError(ctx, err, &res) }() defer func() { h.setResultRateLimit(ctx, &res) }() defer func() { if res.Offline { h.Debug(ctx, "JoinConversationLocal: result obtained offline") } }() uid, err := utils.AssertLoggedInUID(ctx, h.G()) if err != nil { return res, err } if err = JoinConversationByName(ctx, h.G(), h.DebugLabeler, h.remoteClient, uid, arg.TlfName, arg.TopicName, arg.TopicType, arg.Visibility); err != nil { return res, err } res.Offline = h.G().InboxSource.IsOffline(ctx) return res, nil } func (h *Server) LeaveConversationLocal(ctx context.Context, convID chat1.ConversationID) (res chat1.JoinLeaveConversationLocalRes, err error) { var identBreaks []keybase1.TLFIdentifyFailure ctx = globals.ChatCtx(ctx, h.G(), keybase1.TLFIdentifyBehavior_CHAT_GUI, &identBreaks, h.identNotifier) defer h.Trace(ctx, &err, "LeaveConversation(%s)", convID)() defer func() { err = h.handleOfflineError(ctx, err, &res) }() defer func() { h.setResultRateLimit(ctx, &res) }() defer func() { if res.Offline { h.Debug(ctx, "LeaveConversationLocal: result obtained offline") } }() uid, err := utils.AssertLoggedInUID(ctx, h.G()) if err != nil { return res, err } err = LeaveConversation(ctx, h.G(), h.DebugLabeler, h.remoteClient, uid, convID) if err != nil { return res, err } res.Offline = h.G().InboxSource.IsOffline(ctx) return res, nil } func (h *Server) PreviewConversationByIDLocal(ctx context.Context, convID chat1.ConversationID) (res chat1.PreviewConversationLocalRes, err error) { var identBreaks []keybase1.TLFIdentifyFailure ctx = globals.ChatCtx(ctx, h.G(), keybase1.TLFIdentifyBehavior_CHAT_GUI, &identBreaks, h.identNotifier) defer h.Trace(ctx, &err, "PreviewConversationByIDLocal: convID: %s", convID)() defer func() { err = h.handleOfflineError(ctx, err, &res) }() defer func() { h.setResultRateLimit(ctx, &res) }() defer func() { if res.Offline { h.Debug(ctx, "PreviewConversationByIDLocal: result obtained offline") } }() uid, err := utils.AssertLoggedInUID(ctx, h.G()) if err != nil { return res, err } conv, err := PreviewConversation(ctx, h.G(), h.DebugLabeler, h.remoteClient, uid, convID) if err != nil { return res, err } res.Conv = utils.PresentConversationLocal(ctx, h.G(), uid, conv, utils.PresentParticipantsModeInclude) res.Offline = h.G().InboxSource.IsOffline(ctx) return res, nil } func (h *Server) DeleteConversationLocal(ctx context.Context, arg chat1.DeleteConversationLocalArg) (res chat1.DeleteConversationLocalRes, err error) { var identBreaks []keybase1.TLFIdentifyFailure ctx = globals.ChatCtx(ctx, h.G(), keybase1.TLFIdentifyBehavior_CHAT_GUI, &identBreaks, h.identNotifier) defer h.Trace(ctx, &err, "DeleteConversation(%s)", arg.ConvID)() defer func() { err = h.handleOfflineError(ctx, err, &res) }() defer func() { h.setResultRateLimit(ctx, &res) }() defer func() { if res.Offline { h.Debug(ctx, "DeleteConversationLocal: result obtained offline") } }() uid, err := utils.AssertLoggedInUID(ctx, h.G()) if err != nil { return res, err } ui := h.getChatUI(arg.SessionID) confirmed := arg.Confirmed if !confirmed { confirmed, err = ui.ChatConfirmChannelDelete(ctx, chat1.ChatConfirmChannelDeleteArg{ SessionID: arg.SessionID, Channel: arg.ChannelName, }) if err != nil { return res, err } } if !confirmed { return res, errors.New("channel delete unconfirmed") } if err := h.G().InboxSource.RemoteDeleteConversation(ctx, uid, arg.ConvID); err != nil { return res, err } res.Offline = h.G().InboxSource.IsOffline(ctx) return res, nil } func (h *Server) RemoveFromConversationLocal(ctx context.Context, arg chat1.RemoveFromConversationLocalArg) (res chat1.RemoveFromConversationLocalRes, err error) { var identBreaks []keybase1.TLFIdentifyFailure ctx = globals.ChatCtx(ctx, h.G(), keybase1.TLFIdentifyBehavior_CHAT_GUI, &identBreaks, h.identNotifier) defer h.Trace(ctx, &err, "RemoveFromConversation(%s)", arg.ConvID)() defer func() { h.setResultRateLimit(ctx, &res) }() if _, err = utils.AssertLoggedInUID(ctx, h.G()); err != nil { return res, err } err = RemoveFromConversation(ctx, h.G(), h.DebugLabeler, h.remoteClient, arg.ConvID, arg.Usernames) if err != nil { return res, err } return res, nil } func (h *Server) GetTLFConversationsLocal(ctx context.Context, arg chat1.GetTLFConversationsLocalArg) (res chat1.GetTLFConversationsLocalRes, err error) { var identBreaks []keybase1.TLFIdentifyFailure ctx = globals.ChatCtx(ctx, h.G(), keybase1.TLFIdentifyBehavior_CHAT_GUI, &identBreaks, h.identNotifier) defer h.Trace(ctx, &err, "GetTLFConversationsLocal(%s)", arg.TlfName)() defer func() { err = h.handleOfflineError(ctx, err, &res) }() defer func() { h.setResultRateLimit(ctx, &res) }() defer func() { if res.Offline { h.Debug(ctx, "GetTLFConversationsLocal: result obtained offline") } }() uid, err := utils.AssertLoggedInUID(ctx, h.G()) if err != nil { return res, err } // Fetch the TLF ID from specified name nameInfo, err := CreateNameInfoSource(ctx, h.G(), arg.MembersType).LookupID(ctx, arg.TlfName, false) if err != nil { h.Debug(ctx, "GetTLFConversationsLocal: failed to get TLFID from name: %v", err) return res, err } var convs []chat1.ConversationLocal convs, err = h.G().TeamChannelSource.GetChannelsFull(ctx, uid, nameInfo.ID, arg.TopicType) if err != nil { return res, err } res.Convs = utils.PresentConversationLocals(ctx, h.G(), uid, convs, utils.PresentParticipantsModeInclude) res.Offline = h.G().InboxSource.IsOffline(ctx) return res, nil } func (h *Server) GetChannelMembershipsLocal(ctx context.Context, arg chat1.GetChannelMembershipsLocalArg) (res chat1.GetChannelMembershipsLocalRes, err error) { var identBreaks []keybase1.TLFIdentifyFailure ctx = globals.ChatCtx(ctx, h.G(), keybase1.TLFIdentifyBehavior_CHAT_GUI, &identBreaks, h.identNotifier) defer func() { err = h.handleOfflineError(ctx, err, &res) }() defer func() { h.setResultRateLimit(ctx, &res) }() defer func() { if res.Offline { h.Debug(ctx, "GetTLFConversationsLocal: result obtained offline") } }() myUID, err := utils.AssertLoggedInUID(ctx, h.G()) if err != nil { return chat1.GetChannelMembershipsLocalRes{}, err } chatTopicType := chat1.TopicType_CHAT tlfID := chat1.TLFID(arg.TeamID.ToBytes()) // fetch all conversations in the supplied team inbox, err := h.G().InboxSource.ReadUnverified(ctx, myUID, types.InboxSourceDataSourceAll, &chat1.GetInboxQuery{ TlfID: &tlfID, MembersTypes: []chat1.ConversationMembersType{chat1.ConversationMembersType_TEAM}, TopicType: &chatTopicType, }) if err != nil { return res, err } // find a list of conversations that the provided uid is a member of var memberConvs []types.RemoteConversation for _, conv := range inbox.ConvsUnverified { uids, err := h.G().ParticipantsSource.Get(ctx, myUID, conv.GetConvID(), types.InboxSourceDataSourceAll) if err != nil { return res, err } for _, uid := range uids { if bytes.Equal(uid, arg.Uid) { memberConvs = append(memberConvs, conv) break } } } // localize those conversations so we can get the topic name convsLocal, _, err := h.G().InboxSource.Localize(ctx, myUID, memberConvs, types.ConversationLocalizerBlocking) for _, conv := range convsLocal { res.Channels = append(res.Channels, chat1.ChannelNameMention{ ConvID: conv.GetConvID(), TopicName: conv.GetTopicName(), }) } return res, nil } func (h *Server) SetAppNotificationSettingsLocal(ctx context.Context, arg chat1.SetAppNotificationSettingsLocalArg, ) (res chat1.SetAppNotificationSettingsLocalRes, err error) { var identBreaks []keybase1.TLFIdentifyFailure ctx = globals.ChatCtx(ctx, h.G(), keybase1.TLFIdentifyBehavior_CHAT_GUI, &identBreaks, h.identNotifier) defer h.Trace(ctx, &err, "SetAppNotificationSettings(%s)", arg.ConvID)() defer func() { err = h.handleOfflineError(ctx, err, &res) }() defer func() { h.setResultRateLimit(ctx, &res) }() defer func() { if res.Offline { h.Debug(ctx, "SetAppNotificationSettingsLocal: result obtained offline") } }() if _, err = utils.AssertLoggedInUID(ctx, h.G()); err != nil { return res, err } var nsettings chat1.ConversationNotificationInfo nsettings.ChannelWide = arg.ChannelWide nsettings.Settings = make(map[keybase1.DeviceType]map[chat1.NotificationKind]bool) nsettings.Settings[keybase1.DeviceType_MOBILE] = make(map[chat1.NotificationKind]bool) nsettings.Settings[keybase1.DeviceType_DESKTOP] = make(map[chat1.NotificationKind]bool) for _, setting := range arg.Settings { nsettings.Settings[setting.DeviceType][setting.Kind] = setting.Enabled } _, err = h.remoteClient().SetAppNotificationSettings(ctx, chat1.SetAppNotificationSettingsArg{ ConvID: arg.ConvID, Settings: nsettings, }) if err != nil { h.Debug(ctx, "SetAppNotificationSettings: failed to post to remote: %s", err.Error()) return res, err } res.Offline = h.G().InboxSource.IsOffline(ctx) return res, nil } func (h *Server) UnboxMobilePushNotification(ctx context.Context, arg chat1.UnboxMobilePushNotificationArg) (res string, err error) { var identBreaks []keybase1.TLFIdentifyFailure ctx = globals.ChatCtx(ctx, h.G(), keybase1.TLFIdentifyBehavior_CHAT_GUI, &identBreaks, h.identNotifier) defer h.Trace(ctx, &err, "UnboxMobilePushNotification(%s)", arg.ConvID)() uid, err := utils.AssertLoggedInUID(ctx, h.G()) if err != nil { return "", err } bConvID, err := hex.DecodeString(arg.ConvID) if err != nil { h.Debug(ctx, "UnboxMobilePushNotification: invalid convID: %s msg: %s", arg.ConvID, err.Error()) return "", err } convID := chat1.ConversationID(bConvID) mp := NewMobilePush(h.G()) msg, err := mp.UnboxPushNotification(ctx, uid, convID, arg.MembersType, arg.Payload) if err != nil { return "", err } if _, err := utils.GetVerifiedConv(ctx, h.G(), uid, convID, types.InboxSourceDataSourceAll); err != nil { return "", err } if arg.ShouldAck { mp.AckNotificationSuccess(ctx, arg.PushIDs) } if msg.IsValid() { body := msg.Valid().MessageBody bodyTyp, err := body.MessageType() if err != nil { return "", err } if bodyTyp == chat1.MessageType_TEXT { return body.Text().Body, nil } } return res, nil } func (h *Server) SetGlobalAppNotificationSettingsLocal(ctx context.Context, strSettings map[string]bool, ) (err error) { ctx = globals.ChatCtx(ctx, h.G(), keybase1.TLFIdentifyBehavior_CHAT_GUI, nil, h.identNotifier) defer h.Trace(ctx, &err, "SetGlobalAppNotificationSettings")() if _, err = utils.AssertLoggedInUID(ctx, h.G()); err != nil { return err } return setGlobalAppNotificationSettings(ctx, h.G(), h.remoteClient, strSettings) } func (h *Server) GetGlobalAppNotificationSettingsLocal(ctx context.Context) (res chat1.GlobalAppNotificationSettings, err error) { ctx = globals.ChatCtx(ctx, h.G(), keybase1.TLFIdentifyBehavior_CHAT_GUI, nil, h.identNotifier) defer h.Trace(ctx, &err, "GetGlobalAppNotificationSettings")() if _, err = utils.AssertLoggedInUID(ctx, h.G()); err != nil { return res, err } return getGlobalAppNotificationSettings(ctx, h.G(), h.remoteClient) } func (h *Server) AddTeamMemberAfterReset(ctx context.Context, arg chat1.AddTeamMemberAfterResetArg, ) (err error) { ctx = globals.ChatCtx(ctx, h.G(), keybase1.TLFIdentifyBehavior_CHAT_GUI, nil, h.identNotifier) defer h.Trace(ctx, &err, "AddTeamMemberAfterReset")() uid, err := utils.AssertLoggedInUID(ctx, h.G()) if err != nil { return err } // Lookup conversation to get team ID iboxRes, _, err := h.G().InboxSource.Read(ctx, uid, types.ConversationLocalizerBlocking, types.InboxSourceDataSourceAll, nil, &chat1.GetInboxLocalQuery{ ConvIDs: []chat1.ConversationID{arg.ConvID}, }) if err != nil { return err } if len(iboxRes.Convs) != 1 { return errors.New("failed to find conversation to add reset user back into") } var teamID keybase1.TeamID conv := iboxRes.Convs[0] switch conv.Info.MembersType { case chat1.ConversationMembersType_IMPTEAMUPGRADE: team, err := NewTeamLoader(h.G().ExternalG()).loadTeam(ctx, conv.Info.Triple.Tlfid, conv.Info.TlfName, conv.Info.MembersType, conv.Info.Visibility == keybase1.TLFVisibility_PUBLIC, nil) if err != nil { return err } teamID = team.ID case chat1.ConversationMembersType_IMPTEAMNATIVE, chat1.ConversationMembersType_TEAM: teamID = keybase1.TeamID(conv.Info.Triple.Tlfid.String()) default: return fmt.Errorf("unable to add member back to non team conversation: %v", conv.Info.MembersType) } return teams.ReAddMemberAfterReset(ctx, h.G().ExternalG(), teamID, arg.Username) } func (h *Server) GetAllResetConvMembers(ctx context.Context) (res chat1.GetAllResetConvMembersRes, err error) { ctx = globals.ChatCtx(ctx, h.G(), keybase1.TLFIdentifyBehavior_CHAT_GUI, nil, h.identNotifier) defer h.Trace(ctx, &err, "GetAllResetConvMembers")() defer func() { h.setResultRateLimit(ctx, &res) }() if _, err := utils.AssertLoggedInUID(ctx, h.G()); err != nil { return res, err } resetConvs, err := h.remoteClient().GetResetConversations(ctx) if err != nil { return res, err } for _, resetMember := range resetConvs.ResetConvs { username, err := h.G().GetUPAKLoader().LookupUsername(ctx, keybase1.UID(resetMember.Uid.String())) if err != nil { return res, err } res.Members = append(res.Members, chat1.ResetConvMember{ Uid: resetMember.Uid, Conv: resetMember.ConvID, Username: username.String(), }) } return res, nil } func (h *Server) SetConvRetentionLocal(ctx context.Context, arg chat1.SetConvRetentionLocalArg) (err error) { ctx = globals.ChatCtx(ctx, h.G(), keybase1.TLFIdentifyBehavior_CHAT_GUI, nil, h.identNotifier) defer h.Trace(ctx, &err, "SetConvRetentionLocal(%v, %v)", arg.ConvID, arg.Policy.Summary())() uid, err := utils.AssertLoggedInUID(ctx, h.G()) if err != nil { return err } // short circuit if the policy is unchanged. policy := arg.Policy conv, err := utils.GetVerifiedConv(ctx, h.G(), uid, arg.ConvID, types.InboxSourceDataSourceAll) if err != nil { return err } if convRetention := conv.ConvRetention; convRetention != nil && policy.Eq(*convRetention) { h.Debug(ctx, "retention policy unchanged, skipping update") return nil } if _, err = h.remoteClient().SetConvRetention(ctx, chat1.SetConvRetentionArg{ ConvID: arg.ConvID, Policy: policy, }); err != nil { return err } // Post a SYSTEM message to conversation about the change. If we're // inheriting the team policy, fetch that for the message. isInherit := false typ, err := policy.Typ() if err != nil { return err } switch typ { case chat1.RetentionPolicyType_INHERIT: isInherit = true default: // Nothing to do for other policy types. } if isInherit { teamRetention := conv.TeamRetention if teamRetention == nil { policy = chat1.RetentionPolicy{} } else { policy = *teamRetention } } username := h.G().Env.GetUsername() subBody := chat1.NewMessageSystemWithChangeretention(chat1.MessageSystemChangeRetention{ User: username.String(), IsTeam: false, IsInherit: isInherit, MembersType: conv.GetMembersType(), Policy: policy, }) body := chat1.NewMessageBodyWithSystem(subBody) return h.G().ChatHelper.SendMsgByID(ctx, arg.ConvID, conv.Info.TlfName, body, chat1.MessageType_SYSTEM, conv.Info.Visibility) } func (h *Server) SetTeamRetentionLocal(ctx context.Context, arg chat1.SetTeamRetentionLocalArg) (err error) { ctx = globals.ChatCtx(ctx, h.G(), keybase1.TLFIdentifyBehavior_CHAT_GUI, nil, h.identNotifier) defer h.Trace(ctx, &err, "SetTeamRetentionLocal(%v, %v)", arg.TeamID, arg.Policy.Summary())() if _, err = utils.AssertLoggedInUID(ctx, h.G()); err != nil { return err } teamRetention, err := h.GetTeamRetentionLocal(ctx, arg.TeamID) if err != nil { return err } policy := arg.Policy if teamRetention != nil && policy.Eq(*teamRetention) { h.Debug(ctx, "retention policy unchanged, skipping update") return nil } if _, err = h.remoteClient().SetTeamRetention(ctx, chat1.SetTeamRetentionArg{ TeamID: arg.TeamID, Policy: policy, }); err != nil { return err } // Post a SYSTEM message to the #general channel about the change. tlfID, err := chat1.MakeTLFID(arg.TeamID.String()) if err != nil { return err } username := h.G().Env.GetUsername() subBody := chat1.NewMessageSystemWithChangeretention(chat1.MessageSystemChangeRetention{ User: username.String(), IsTeam: true, IsInherit: false, MembersType: chat1.ConversationMembersType_TEAM, Policy: arg.Policy, }) body := chat1.NewMessageBodyWithSystem(subBody) info, err := CreateNameInfoSource(ctx, h.G(), chat1.ConversationMembersType_TEAM).LookupName(ctx, tlfID, false, "") if err != nil { return err } return h.G().ChatHelper.SendMsgByName(ctx, info.CanonicalName, &globals.DefaultTeamTopic, chat1.ConversationMembersType_TEAM, keybase1.TLFIdentifyBehavior_CHAT_CLI, body, chat1.MessageType_SYSTEM) } func (h *Server) GetTeamRetentionLocal(ctx context.Context, teamID keybase1.TeamID) (res *chat1.RetentionPolicy, err error) { ctx = globals.ChatCtx(ctx, h.G(), keybase1.TLFIdentifyBehavior_CHAT_GUI, nil, h.identNotifier) defer h.Trace(ctx, &err, "GetTeamRetentionLocal(%s)", teamID)() uid, err := utils.AssertLoggedInUID(ctx, h.G()) if err != nil { return res, err } tlfID, err := chat1.MakeTLFID(teamID.String()) if err != nil { return res, err } ib, err := h.G().InboxSource.ReadUnverified(ctx, uid, types.InboxSourceDataSourceAll, &chat1.GetInboxQuery{ TlfID: &tlfID, }) if err != nil { return res, err } if len(ib.ConvsUnverified) == 0 { return res, errors.New("no conversations found") } return ib.ConvsUnverified[0].Conv.TeamRetention, nil } func (h *Server) SetConvMinWriterRoleLocal(ctx context.Context, arg chat1.SetConvMinWriterRoleLocalArg) (err error) { defer h.Trace(ctx, &err, "SetConvMinWriterRole(%v, %v)", arg.ConvID, arg.Role)() _, err = h.remoteClient().SetConvMinWriterRole(ctx, chat1.SetConvMinWriterRoleArg(arg)) return err } func (h *Server) UpgradeKBFSConversationToImpteam(ctx context.Context, convID chat1.ConversationID) (err error) { ctx = globals.ChatCtx(ctx, h.G(), keybase1.TLFIdentifyBehavior_CHAT_GUI, nil, h.identNotifier) defer h.Trace(ctx, &err, "UpgradeKBFSConversationToImpteam(%s)", convID)() uid, err := utils.AssertLoggedInUID(ctx, h.G()) if err != nil { return err } ibox, _, err := h.G().InboxSource.Read(ctx, uid, types.ConversationLocalizerBlocking, types.InboxSourceDataSourceAll, nil, &chat1.GetInboxLocalQuery{ ConvIDs: []chat1.ConversationID{convID}, }) if err != nil { return err } if len(ibox.Convs) == 0 { return errors.New("no conversation found") } conv := ibox.Convs[0] if conv.GetMembersType() != chat1.ConversationMembersType_KBFS { return fmt.Errorf("cannot upgrade %v conversation", conv.GetMembersType()) } tlfID := conv.Info.Triple.Tlfid tlfName := conv.Info.TlfName public := conv.Info.Visibility == keybase1.TLFVisibility_PUBLIC return h.G().ChatHelper.UpgradeKBFSToImpteam(ctx, tlfName, tlfID, public) } func (h *Server) cancelActiveSearchLocked() { if h.searchCancelFn != nil { h.searchCancelFn() h.searchCancelFn = nil } } func (h *Server) getSearchContext(ctx context.Context) context.Context { // enforce a single search happening at a time h.searchMu.Lock() h.cancelActiveSearchLocked() ctx, h.searchCancelFn = context.WithCancel(ctx) h.searchMu.Unlock() return ctx } func (h *Server) CancelActiveSearch(ctx context.Context) (err error) { defer h.Trace(ctx, &err, "CancelActiveSearch")() h.searchMu.Lock() h.cancelActiveSearchLocked() h.searchMu.Unlock() return nil } func (h *Server) getSearchRegexp(query string, opts chat1.SearchOpts) (re *regexp.Regexp, err error) { if opts.IsRegex { re, err = regexp.Compile(query) } else { // String queries are set case insensitive re, err = utils.GetQueryRe(query) } if err != nil { return nil, err } return re, nil } func (h *Server) SearchRegexp(ctx context.Context, arg chat1.SearchRegexpArg) (res chat1.SearchRegexpRes, err error) { var identBreaks []keybase1.TLFIdentifyFailure ctx = globals.ChatCtx(ctx, h.G(), arg.IdentifyBehavior, &identBreaks, h.identNotifier) defer h.Trace(ctx, &err, "SearchRegexp")() defer func() { err = h.handleOfflineError(ctx, err, &res) }() defer func() { h.setResultRateLimit(ctx, &res) }() uid, err := utils.AssertLoggedInUID(ctx, h.G()) if err != nil { return res, err } ctx = h.getSearchContext(ctx) query, opts := search.UpgradeSearchOptsFromQuery(arg.Query, arg.Opts, h.G().GetEnv().GetUsername().String()) re, err := h.getSearchRegexp(query, opts) if err != nil { return res, err } chatUI := h.getChatUI(arg.SessionID) uiCh := make(chan chat1.ChatSearchHit, 10) ch := make(chan struct{}) go func() { for searchHit := range uiCh { select { case <-ctx.Done(): return default: _ = chatUI.ChatSearchHit(ctx, chat1.ChatSearchHitArg{ SearchHit: searchHit, }) } } close(ch) }() hits, _, err := h.G().RegexpSearcher.Search(ctx, uid, arg.ConvID, re, uiCh, opts) if err != nil { return res, err } <-ch _ = chatUI.ChatSearchDone(ctx, chat1.ChatSearchDoneArg{ NumHits: len(hits), }) return chat1.SearchRegexpRes{ Hits: hits, IdentifyFailures: identBreaks, }, nil } func (h *Server) cancelActiveInboxSearchLocked() { if h.searchInboxCancelFn != nil { h.searchInboxCancelFn() h.searchInboxCancelFn = nil } } func (h *Server) getInboxSearchContext(ctx context.Context) context.Context { // enforce a single search happening at a time h.searchInboxMu.Lock() h.cancelActiveInboxSearchLocked() ctx, h.searchInboxCancelFn = context.WithCancel(ctx) h.searchInboxMu.Unlock() return ctx } func (h *Server) CancelActiveInboxSearch(ctx context.Context) (err error) { defer h.Trace(ctx, &err, "CancelActiveInboxSearch")() h.searchInboxMu.Lock() h.cancelActiveInboxSearchLocked() h.searchInboxMu.Unlock() return nil } func (h *Server) delegateInboxSearch(ctx context.Context, uid gregor1.UID, query, origQuery string, opts chat1.SearchOpts, ui libkb.ChatUI, ) (res chat1.ChatSearchInboxResults, err error) { defer h.Trace(ctx, &err, "delegateInboxSearch")() convs, err := h.G().Indexer.SearchableConvs(ctx, opts.ConvID) if err != nil { return res, err } re, err := h.getSearchRegexp(query, opts) if err != nil { return res, err } select { case <-ctx.Done(): return res, ctx.Err() default: _ = ui.ChatSearchConvHits(ctx, chat1.UIChatSearchConvHits{}) } var numHits, numConvs int for index, conv := range convs { uiCh := make(chan chat1.ChatSearchHit, 10) ch := make(chan struct{}) go func() { for searchHit := range uiCh { select { case <-ctx.Done(): return default: _ = ui.ChatSearchHit(ctx, chat1.ChatSearchHitArg{ SearchHit: searchHit, }) } } close(ch) }() hits, _, err := h.G().RegexpSearcher.Search(ctx, uid, conv.GetConvID(), re, uiCh, opts) if err != nil { h.Debug(ctx, "delegateInboxSearch: failed to search conv: %s", err) continue } <-ch if len(hits) == 0 { continue } numHits += len(hits) numConvs++ inboxHit := chat1.ChatSearchInboxHit{ ConvID: conv.GetConvID(), TeamType: conv.GetTeamType(), ConvName: utils.GetRemoteConvDisplayName(conv), Query: origQuery, Time: hits[0].HitMessage.Valid().Ctime, Hits: hits, } select { case <-ctx.Done(): return res, ctx.Err() default: _ = ui.ChatSearchInboxHit(ctx, chat1.ChatSearchInboxHitArg{ SearchHit: inboxHit, }) } res.Hits = append(res.Hits, inboxHit) if opts.MaxConvsHit > 0 && len(res.Hits) > opts.MaxConvsHit { break } if opts.MaxConvsSearched > 0 && index > opts.MaxConvsSearched { break } } select { case <-ctx.Done(): return res, ctx.Err() default: _ = ui.ChatSearchInboxDone(ctx, chat1.ChatSearchInboxDoneArg{ Res: chat1.ChatSearchInboxDone{ NumHits: numHits, NumConvs: numConvs, Delegated: true, }, }) } return res, nil } func (h *Server) SearchInbox(ctx context.Context, arg chat1.SearchInboxArg) (res chat1.SearchInboxRes, err error) { var identBreaks []keybase1.TLFIdentifyFailure ctx = globals.ChatCtx(ctx, h.G(), arg.IdentifyBehavior, &identBreaks, h.identNotifier) defer h.Trace(ctx, &err, "SearchInbox")() defer func() { err = h.handleOfflineError(ctx, err, &res) }() defer func() { h.setResultRateLimit(ctx, &res) }() defer h.suspendBgConvLoads(ctx)() uid, err := utils.AssertLoggedInUID(ctx, h.G()) if err != nil { return res, err } chatUI := h.getChatUI(arg.SessionID) select { case <-ctx.Done(): return res, ctx.Err() default: _ = chatUI.ChatSearchInboxStart(ctx) } username := h.G().GetEnv().GetUsernameForUID(keybase1.UID(uid.String())).String() query, opts := search.UpgradeSearchOptsFromQuery(arg.Query, arg.Opts, username) doSearch := !arg.NamesOnly && len(query) > 0 forceDelegate := false if arg.Opts.ConvID != nil { fullyIndexed, err := h.G().Indexer.FullyIndexed(ctx, *arg.Opts.ConvID) if err != nil { h.Debug(ctx, "SearchInbox: failed to check fully indexed, delegating... err: %s", err) forceDelegate = true } else { forceDelegate = !fullyIndexed } if len(query) < search.MinTokenLength { forceDelegate = true } if forceDelegate { h.Debug(ctx, "SearchInbox: force delegating since not indexed") } ctx = h.getSearchContext(ctx) } else { ctx = h.getInboxSearchContext(ctx) } if doSearch && (opts.IsRegex || forceDelegate) { inboxRes, err := h.delegateInboxSearch(ctx, uid, query, arg.Query, opts, chatUI) if err != nil { return res, err } res.Res = &inboxRes res.IdentifyFailures = identBreaks return res, nil } eg := errgroup.Group{} // stream hits back to client UI hitUICh := make(chan chat1.ChatSearchInboxHit, 10) var numHits int eg.Go(func() error { if !doSearch { return nil } for searchHit := range hitUICh { numHits += len(searchHit.Hits) select { case <-ctx.Done(): return nil default: _ = chatUI.ChatSearchInboxHit(ctx, chat1.ChatSearchInboxHitArg{ SearchHit: searchHit, }) } } return nil }) // stream index status back to client UI indexUICh := make(chan chat1.ChatSearchIndexStatus, 10) eg.Go(func() error { if !doSearch { return nil } for status := range indexUICh { select { case <-ctx.Done(): return nil default: _ = chatUI.ChatSearchIndexStatus(ctx, chat1.ChatSearchIndexStatusArg{ Status: status, }) } } return nil }) // send up conversation name matches eg.Go(func() error { if opts.MaxNameConvs == 0 { return nil } convHits, err := h.G().InboxSource.Search(ctx, uid, query, opts.MaxNameConvs, types.InboxSourceSearchEmptyModeUnread) if err != nil { h.Debug(ctx, "SearchInbox: failed to get conv hits: %s", err) return nil } select { case <-ctx.Done(): return ctx.Err() default: _ = chatUI.ChatSearchConvHits(ctx, chat1.UIChatSearchConvHits{ Hits: utils.PresentRemoteConversationsAsSearchHits(convHits, username), UnreadMatches: len(query) == 0, }) } return nil }) // send up team name matches g := h.G().ExternalG() mctx := libkb.NewMetaContext(ctx, g) eg.Go(func() error { if opts.MaxTeams == 0 { return nil } hits, err := opensearch.Local(mctx, query, opts.MaxTeams) if err != nil { h.Debug(ctx, "SearchInbox: failed to get team hits: %s", err) return nil } select { case <-ctx.Done(): return nil default: _ = chatUI.ChatSearchTeamHits(ctx, chat1.UIChatSearchTeamHits{ Hits: hits, SuggestedMatches: len(query) == 0, }) } return nil }) eg.Go(func() error { hits, err := teambot.NewFeaturedBotLoader(g).SearchLocal( mctx, keybase1.SearchLocalArg{ Query: query, Limit: opts.MaxBots, SkipCache: opts.SkipBotCache, }) if err != nil { h.Debug(ctx, "SearchInbox: failed to get bot hits: %s", err) return nil } select { case <-ctx.Done(): return nil default: _ = chatUI.ChatSearchBotHits(ctx, chat1.UIChatSearchBotHits{ Hits: hits.Bots, SuggestedMatches: len(query) == 0, }) } return nil }) var searchRes *chat1.ChatSearchInboxResults if doSearch { select { case <-time.After(50 * time.Millisecond): case <-ctx.Done(): return } if searchRes, err = h.G().Indexer.Search(ctx, query, arg.Query, opts, hitUICh, indexUICh); err != nil { return res, err } } if err := eg.Wait(); err != nil { h.Debug(ctx, "unable to wait for search: %v") } var doneRes chat1.ChatSearchInboxDone if searchRes != nil { doneRes = chat1.ChatSearchInboxDone{ NumHits: numHits, NumConvs: len(searchRes.Hits), PercentIndexed: searchRes.PercentIndexed, } } select { case <-ctx.Done(): return res, ctx.Err() default: _ = chatUI.ChatSearchInboxDone(ctx, chat1.ChatSearchInboxDoneArg{ Res: doneRes, }) } return chat1.SearchInboxRes{ Res: searchRes, IdentifyFailures: identBreaks, }, nil } func (h *Server) ProfileChatSearch(ctx context.Context, identifyBehavior keybase1.TLFIdentifyBehavior) ( res map[chat1.ConvIDStr]chat1.ProfileSearchConvStats, err error, ) { var identBreaks []keybase1.TLFIdentifyFailure ctx = globals.ChatCtx(ctx, h.G(), identifyBehavior, &identBreaks, h.identNotifier) defer h.Trace(ctx, &err, "ProfileChatSearch")() _, err = utils.AssertLoggedInUID(ctx, h.G()) if err != nil { return nil, err } res, err = h.G().Indexer.IndexInbox(ctx) if err != nil { return nil, err } b, err := json.Marshal(res) if err != nil { return nil, err } h.Debug(ctx, "%s\n", string(b)) return res, err } func (h *Server) GetStaticConfig(ctx context.Context) (res chat1.StaticConfig, err error) { defer h.Trace(ctx, &err, "GetStaticConfig")() return chat1.StaticConfig{ DeletableByDeleteHistory: chat1.DeletableMessageTypesByDeleteHistory(), BuiltinCommands: h.G().CommandsSource.GetBuiltins(ctx), }, nil } func (h *Server) ResolveUnfurlPrompt(ctx context.Context, arg chat1.ResolveUnfurlPromptArg) (err error) { var identBreaks []keybase1.TLFIdentifyFailure defer func() { // squash all errors coming out of here err = nil }() ctx = globals.ChatCtx(ctx, h.G(), arg.IdentifyBehavior, &identBreaks, h.identNotifier) defer h.Trace(ctx, &err, "ResolveUnfurlPrompt")() uid, err := utils.AssertLoggedInUID(ctx, h.G()) if err != nil { return err } fetchAndUnfurl := func() error { msgs, err := h.G().ConvSource.GetMessages(ctx, arg.ConvID, uid, []chat1.MessageID{arg.MsgID}, nil, nil, true) if err != nil { return err } if len(msgs) != 1 { return errors.New("message not found") } h.G().Unfurler.UnfurlAndSend(ctx, uid, arg.ConvID, msgs[0]) return nil } atyp, err := arg.Result.ActionType() if err != nil { return err } switch atyp { case chat1.UnfurlPromptAction_NOTNOW: // do nothing case chat1.UnfurlPromptAction_ACCEPT: if err = h.G().Unfurler.WhitelistAdd(ctx, uid, arg.Result.Accept()); err != nil { return fmt.Errorf("failed to add to whitelist, doing nothing: %s", err) } if err = fetchAndUnfurl(); err != nil { return fmt.Errorf("failed to fetch and unfurl: %s", err) } case chat1.UnfurlPromptAction_ONETIME: h.G().Unfurler.WhitelistAddExemption(ctx, uid, unfurl.NewSingleMessageWhitelistExemption(arg.ConvID, arg.MsgID, arg.Result.Onetime())) if err = fetchAndUnfurl(); err != nil { return fmt.Errorf("failed to fetch and unfurl: %s", err) } case chat1.UnfurlPromptAction_NEVER: if err = h.G().Unfurler.SetMode(ctx, uid, chat1.UnfurlMode_NEVER); err != nil { return fmt.Errorf("failed to set mode to never: %s", err) } case chat1.UnfurlPromptAction_ALWAYS: if err = h.G().Unfurler.SetMode(ctx, uid, chat1.UnfurlMode_ALWAYS); err != nil { return fmt.Errorf("failed to set mode to always: %s", err) } if err = fetchAndUnfurl(); err != nil { return fmt.Errorf("failed to fetch and unfurl: %s", err) } } return nil } func (h *Server) GetUnfurlSettings(ctx context.Context) (res chat1.UnfurlSettingsDisplay, err error) { ctx = globals.ChatCtx(ctx, h.G(), keybase1.TLFIdentifyBehavior_CHAT_GUI, nil, h.identNotifier) defer h.Trace(ctx, &err, "GetUnfurlSettings")() uid, err := utils.AssertLoggedInUID(ctx, h.G()) if err != nil { return res, err } settings, err := h.G().Unfurler.GetSettings(ctx, uid) if err != nil { return res, err } res.Mode = settings.Mode for w := range settings.Whitelist { res.Whitelist = append(res.Whitelist, w) } slices.Sort(res.Whitelist) return res, nil } func (h *Server) SaveUnfurlSettings(ctx context.Context, arg chat1.SaveUnfurlSettingsArg) (err error) { ctx = globals.ChatCtx(ctx, h.G(), keybase1.TLFIdentifyBehavior_CHAT_GUI, nil, h.identNotifier) defer h.Trace(ctx, &err, "SaveUnfurlSettings")() uid, err := utils.AssertLoggedInUID(ctx, h.G()) if err != nil { return err } wm := make(map[string]bool) for _, w := range arg.Whitelist { wm[w] = true } return h.G().Unfurler.SetSettings(ctx, uid, chat1.UnfurlSettings{ Mode: arg.Mode, Whitelist: wm, }) } func (h *Server) ToggleMessageCollapse(ctx context.Context, arg chat1.ToggleMessageCollapseArg) (err error) { ctx = globals.ChatCtx(ctx, h.G(), keybase1.TLFIdentifyBehavior_CHAT_GUI, nil, h.identNotifier) defer h.Trace(ctx, &err, "ToggleMessageCollapse convID=%s msgID=%d collapsed=%v", arg.ConvID, arg.MsgID, arg.Collapse)() uid, err := utils.AssertLoggedInUID(ctx, h.G()) if err != nil { return err } if err := utils.NewCollapses(h.G()).ToggleSingle(ctx, uid, arg.ConvID, arg.MsgID, arg.Collapse); err != nil { return err } msg, err := h.G().ConvSource.GetMessage(ctx, arg.ConvID, uid, arg.MsgID, nil, nil, true) if err != nil { h.Debug(ctx, "ToggleMessageCollapse: failed to get message: %s", err) return nil } if !msg.IsValid() { h.Debug(ctx, "ToggleMessageCollapse: invalid message") return nil } if msg.Valid().MessageBody.IsType(chat1.MessageType_UNFURL) { unfurledMsg, err := h.G().ConvSource.GetMessage(ctx, arg.ConvID, uid, msg.Valid().MessageBody.Unfurl().MessageID, nil, nil, true) if err != nil { h.Debug(ctx, "ToggleMessageCollapse: failed to get unfurl base message: %s", err) return nil } // give a notification about the unfurled message notif := chat1.MessagesUpdated{ ConvID: arg.ConvID, Updates: []chat1.UIMessage{utils.PresentMessageUnboxed(ctx, h.G(), unfurledMsg, uid, arg.ConvID)}, } act := chat1.NewChatActivityWithMessagesUpdated(notif) h.G().ActivityNotifier.Activity(ctx, uid, chat1.TopicType_CHAT, &act, chat1.ChatActivitySource_LOCAL) } else if msg.Valid().MessageBody.IsType(chat1.MessageType_ATTACHMENT) { notif := chat1.MessagesUpdated{ ConvID: arg.ConvID, Updates: []chat1.UIMessage{utils.PresentMessageUnboxed(ctx, h.G(), msg, uid, arg.ConvID)}, } act := chat1.NewChatActivityWithMessagesUpdated(notif) h.G().ActivityNotifier.Activity(ctx, uid, chat1.TopicType_CHAT, &act, chat1.ChatActivitySource_LOCAL) } return nil } func (h *Server) BulkAddToConv(ctx context.Context, arg chat1.BulkAddToConvArg) (err error) { ctx = globals.ChatCtx(ctx, h.G(), keybase1.TLFIdentifyBehavior_CHAT_GUI, nil, h.identNotifier) defer h.Trace(ctx, &err, "BulkAddToConv: convID: %v, numUsers: %v", arg.ConvID, len(arg.Usernames))() uid, err := utils.AssertLoggedInUID(ctx, h.G()) if err != nil { return err } return h.G().ChatHelper.BulkAddToConv(ctx, uid, arg.ConvID, arg.Usernames) } func (h *Server) BulkAddToManyConvs(ctx context.Context, arg chat1.BulkAddToManyConvsArg) (err error) { for _, convID := range arg.Conversations { if err = h.BulkAddToConv(ctx, chat1.BulkAddToConvArg{ ConvID: convID, Usernames: arg.Usernames, }); err != nil { return err } } return nil } func (h *Server) PutReacjiSkinTone(ctx context.Context, skinTone keybase1.ReacjiSkinTone) (res keybase1.UserReacjis, err error) { ctx = globals.ChatCtx(ctx, h.G(), keybase1.TLFIdentifyBehavior_CHAT_GUI, nil, h.identNotifier) defer h.Trace(ctx, &err, "PutReacjiSkinTone")() uid, err := utils.AssertLoggedInUID(ctx, h.G()) if err != nil { return res, err } store := storage.NewReacjiStore(h.G()) err = store.PutSkinTone(ctx, uid, skinTone) if err != nil { return res, err } res = store.UserReacjis(ctx, uid) return res, nil } func (h *Server) ResolveMaybeMention(ctx context.Context, mention chat1.MaybeMention) (err error) { ctx = globals.ChatCtx(ctx, h.G(), keybase1.TLFIdentifyBehavior_CHAT_GUI, nil, h.identNotifier) defer h.Trace(ctx, &err, "ResolveMaybeMention")() uid, err := utils.AssertLoggedInUID(ctx, h.G()) if err != nil { return err } // Try to load as user if mention.Channel == "" { nn := libkb.NewNormalizedUsername(mention.Name) if _, err = h.G().GetUPAKLoader().LookupUID(ctx, nn); err != nil { h.Debug(ctx, "ResolveMaybeMention: not a user") } else { _ = h.getChatUI(0).ChatMaybeMentionUpdate(ctx, mention.Name, mention.Channel, chat1.NewUIMaybeMentionInfoWithUser()) return nil } } // Try to load as team return h.G().TeamMentionLoader.LoadTeamMention(ctx, uid, mention, nil, true) } func (h *Server) getLoadGalleryContext(ctx context.Context) context.Context { // enforce a single search happening at a time h.loadGalleryMu.Lock() if h.loadGalleryCancelFn != nil { h.loadGalleryCancelFn() h.loadGalleryCancelFn = nil } ctx, h.loadGalleryCancelFn = context.WithCancel(ctx) h.loadGalleryMu.Unlock() return ctx } func (h *Server) LoadGallery(ctx context.Context, arg chat1.LoadGalleryArg) (res chat1.LoadGalleryRes, err error) { ctx = globals.ChatCtx(ctx, h.G(), keybase1.TLFIdentifyBehavior_CHAT_GUI, nil, h.identNotifier) defer h.Trace(ctx, &err, "LoadGallery")() defer func() { err = h.squashSquashableErrors(err) }() defer func() { h.setResultRateLimit(ctx, &res) }() defer h.suspendBgConvLoads(ctx)() uid, err := utils.AssertLoggedInUID(ctx, h.G()) if err != nil { return res, err } ctx = h.getLoadGalleryContext(ctx) chatUI := h.getChatUI(arg.SessionID) convID := arg.ConvID var opts attachments.NextMessageOptions opts.BackInTime = true switch arg.Typ { case chat1.GalleryItemTyp_MEDIA: opts.MessageType = chat1.MessageType_ATTACHMENT opts.AssetTypes = []chat1.AssetMetadataType{ chat1.AssetMetadataType_IMAGE, chat1.AssetMetadataType_VIDEO, } case chat1.GalleryItemTyp_LINK: opts.MessageType = chat1.MessageType_TEXT opts.FilterLinks = true case chat1.GalleryItemTyp_DOC: opts.MessageType = chat1.MessageType_ATTACHMENT opts.AssetTypes = []chat1.AssetMetadataType{chat1.AssetMetadataType_NONE} default: return res, errors.New("invalid gallery type") } var msgID chat1.MessageID if arg.FromMsgID != nil { msgID = *arg.FromMsgID } else { conv, err := utils.GetUnverifiedConv(ctx, h.G(), uid, convID, types.InboxSourceDataSourceAll) if err != nil { return res, err } msgID = conv.Conv.ReaderInfo.MaxMsgid + 1 } hitCh := make(chan chat1.UIMessage) go func(ctx context.Context) { for msg := range hitCh { _ = chatUI.ChatLoadGalleryHit(ctx, msg) } }(ctx) gallery := attachments.NewGallery(h.G()) msgs, last, err := gallery.NextMessages(ctx, uid, convID, msgID, arg.Num, opts, hitCh) if err != nil { return res, err } return chat1.LoadGalleryRes{ Last: last, Messages: utils.PresentMessagesUnboxed(ctx, h.G(), msgs, uid, convID), }, nil } func (h *Server) LoadFlip(ctx context.Context, arg chat1.LoadFlipArg) (res chat1.LoadFlipRes, err error) { var identBreaks []keybase1.TLFIdentifyFailure ctx = globals.ChatCtx(ctx, h.G(), keybase1.TLFIdentifyBehavior_CHAT_GUI, &identBreaks, h.identNotifier) defer h.Trace(ctx, &err, "LoadFlip")() defer func() { h.setResultRateLimit(ctx, &res) }() uid, err := utils.AssertLoggedInUID(ctx, h.G()) if err != nil { return res, err } statusCh, errCh := h.G().CoinFlipManager.LoadFlip(ctx, uid, arg.HostConvID, arg.HostMsgID, arg.FlipConvID, arg.GameID) select { case status := <-statusCh: res.Status = status case err = <-errCh: return res, err } res.IdentifyFailures = identBreaks return res, nil } func (h *Server) LocationUpdate(ctx context.Context, coord chat1.Coordinate) (err error) { var identBreaks []keybase1.TLFIdentifyFailure ctx = globals.ChatCtx(ctx, h.G(), keybase1.TLFIdentifyBehavior_CHAT_GUI, &identBreaks, h.identNotifier) defer h.Trace(ctx, &err, "LocationUpdate")() _, err = utils.AssertLoggedInUID(ctx, h.G()) if err != nil { return err } h.G().LiveLocationTracker.LocationUpdate(ctx, coord) return nil } func (h *Server) AdvertiseBotCommandsLocal(ctx context.Context, arg chat1.AdvertiseBotCommandsLocalArg) (res chat1.AdvertiseBotCommandsLocalRes, err error) { var identBreaks []keybase1.TLFIdentifyFailure ctx = globals.ChatCtx(ctx, h.G(), keybase1.TLFIdentifyBehavior_CHAT_GUI, &identBreaks, h.identNotifier) defer h.Trace(ctx, &err, "AdvertiseBotCommandsLocal")() defer func() { h.setResultRateLimit(ctx, &res) }() _, err = utils.AssertLoggedInUID(ctx, h.G()) if err != nil { return res, err } if err := h.G().BotCommandManager.Advertise(ctx, arg.Alias, arg.Advertisements); err != nil { return res, err } return res, nil } func (h *Server) ClearBotCommandsLocal(ctx context.Context, filter *chat1.ClearBotCommandsFilter) (res chat1.ClearBotCommandsLocalRes, err error) { var identBreaks []keybase1.TLFIdentifyFailure ctx = globals.ChatCtx(ctx, h.G(), keybase1.TLFIdentifyBehavior_CHAT_GUI, &identBreaks, h.identNotifier) defer h.Trace(ctx, &err, "ClearBotCommandsLocal")() defer func() { h.setResultRateLimit(ctx, &res) }() _, err = utils.AssertLoggedInUID(ctx, h.G()) if err != nil { return res, err } if err := h.G().BotCommandManager.Clear(ctx, filter); err != nil { return res, err } return res, nil } func (h *Server) ListPublicBotCommandsLocal(ctx context.Context, username string) (res chat1.ListBotCommandsLocalRes, err error) { var identBreaks []keybase1.TLFIdentifyFailure ctx = globals.ChatCtx(ctx, h.G(), keybase1.TLFIdentifyBehavior_CHAT_GUI, &identBreaks, h.identNotifier) defer h.Trace(ctx, &err, "ListPublicBotCommandsLocal")() defer func() { h.setResultRateLimit(ctx, &res) }() _, err = utils.AssertLoggedInUID(ctx, h.G()) if err != nil { return res, err } convID, err := h.G().BotCommandManager.PublicCommandsConv(ctx, username) if err != nil { if _, ok := err.(UnknownTLFNameError); ok { h.Debug(ctx, "ListPublicBotCommandsLocal: unknown conv name") return res, nil } return res, err } if convID == nil { return res, nil } return h.ListBotCommandsLocal(ctx, *convID) } func (h *Server) ListBotCommandsLocal(ctx context.Context, convID chat1.ConversationID) (res chat1.ListBotCommandsLocalRes, err error) { var identBreaks []keybase1.TLFIdentifyFailure ctx = globals.ChatCtx(ctx, h.G(), keybase1.TLFIdentifyBehavior_CHAT_GUI, &identBreaks, h.identNotifier) defer h.Trace(ctx, &err, "ListBotCommandsLocal")() defer func() { h.setResultRateLimit(ctx, &res) }() _, err = utils.AssertLoggedInUID(ctx, h.G()) if err != nil { return res, err } completeCh, err := h.G().BotCommandManager.UpdateCommands(ctx, convID, nil) if err != nil { return res, err } if err := <-completeCh; err != nil { h.Debug(ctx, "ListBotCommandsLocal: failed to update commands, list might be stale: %s", err) } lres, _, err := h.G().BotCommandManager.ListCommands(ctx, convID) if err != nil { return res, err } res.Commands = lres return res, nil } func (h *Server) GetMutualTeamsLocal(ctx context.Context, usernames []string) (res chat1.GetMutualTeamsLocalRes, err error) { var identBreaks []keybase1.TLFIdentifyFailure ctx = globals.ChatCtx(ctx, h.G(), keybase1.TLFIdentifyBehavior_CHAT_GUI, &identBreaks, h.identNotifier) defer h.Trace(ctx, &err, "GetMutualTeamsLocal")() defer func() { h.setResultRateLimit(ctx, &res) }() defer func() { err = h.handleOfflineError(ctx, err, &res) }() uid, err := utils.AssertLoggedInUID(ctx, h.G()) if err != nil { return res, err } providedUIDs := make([]keybase1.UID, 0, len(usernames)) for _, username := range usernames { providedUIDs = append(providedUIDs, libkb.GetUIDByUsername(h.G().GlobalContext, username)) } // get all the default channels for all the teams you're in chatTopic := chat1.TopicType_CHAT inbox, err := h.G().InboxSource.ReadUnverified(ctx, uid, types.InboxSourceDataSourceLocalOnly, &chat1.GetInboxQuery{ MembersTypes: []chat1.ConversationMembersType{chat1.ConversationMembersType_TEAM}, TopicName: &globals.DefaultTeamTopic, TopicType: &chatTopic, AllowUnseenQuery: true, }) if err != nil { return res, err } type matchedTeam struct { mtime gregor1.Time team chat1.SharedTeam } // loop through convs var matchedTeams []matchedTeam var resLock sync.Mutex pipeliner := pipeliner.NewPipeliner(4) for _, conv := range inbox.ConvsUnverified { if conv.GetMembersType() != chat1.ConversationMembersType_TEAM { continue } if err := pipeliner.WaitForRoom(ctx); err != nil { return res, err } go func(conv types.RemoteConversation) { var err error userPresent := make(map[keybase1.UID]bool) for _, uid := range providedUIDs { userPresent[keybase1.UID(uid.String())] = false } members, err := h.G().ParticipantsSource.Get(ctx, uid, conv.GetConvID(), types.InboxSourceDataSourceAll) if err != nil { pipeliner.CompleteOne(err) return } for _, uid := range members { if _, exists := userPresent[keybase1.UID(uid.String())]; exists { // if we see a user in a team that we're looking for, mark that in the userPresent map userPresent[keybase1.UID(uid.String())] = true } } allOK := true for _, inTeam := range userPresent { if !inTeam { allOK = false break } } if allOK { resLock.Lock() matchedTeams = append(matchedTeams, matchedTeam{ mtime: conv.GetMtime(), team: chat1.SharedTeam{ TeamID: keybase1.TeamID(conv.Conv.Metadata.IdTriple.Tlfid.String()), Name: utils.GetRemoteConvTLFName(conv), }, }) resLock.Unlock() } pipeliner.CompleteOne(nil) }(conv) } err = pipeliner.Flush(ctx) sort.Slice(matchedTeams, func(i, j int) bool { return matchedTeams[i].mtime > matchedTeams[j].mtime }) res.Teams = make([]chat1.SharedTeam, 0, len(matchedTeams)) for _, matched := range matchedTeams { res.Teams = append(res.Teams, matched.team) } return res, err } func (h *Server) PinMessage(ctx context.Context, arg chat1.PinMessageArg) (res chat1.PinMessageRes, err error) { var identBreaks []keybase1.TLFIdentifyFailure ctx = globals.ChatCtx(ctx, h.G(), keybase1.TLFIdentifyBehavior_CHAT_GUI, &identBreaks, h.identNotifier) defer h.Trace(ctx, &err, "PinMessage")() defer func() { h.setResultRateLimit(ctx, &res) }() uid, err := utils.AssertLoggedInUID(ctx, h.G()) if err != nil { return res, err } msg, err := h.G().ConvSource.GetMessage(ctx, arg.ConvID, uid, arg.MsgID, nil, nil, true) if err != nil { return res, err } if !msg.IsValid() { return res, fmt.Errorf("unable to pin message, message invalid") } bod := msg.Valid().MessageBody typ, err := bod.MessageType() if err != nil { return res, err } switch typ { case chat1.MessageType_TEXT, chat1.MessageType_ATTACHMENT: default: return res, fmt.Errorf("Unable to pin messageof type %v, expected %v or %v", typ, chat1.MessageType_TEXT, chat1.MessageType_ATTACHMENT) } if _, err := h.PostLocalNonblock(ctx, chat1.PostLocalNonblockArg{ ConversationID: arg.ConvID, Msg: chat1.MessagePlaintext{ ClientHeader: chat1.MessageClientHeader{ MessageType: chat1.MessageType_PIN, }, MessageBody: chat1.NewMessageBodyWithPin(chat1.MessagePin{ MsgID: arg.MsgID, }), }, }); err != nil { return res, err } return res, nil } func (h *Server) UnpinMessage(ctx context.Context, convID chat1.ConversationID) (res chat1.PinMessageRes, err error) { var identBreaks []keybase1.TLFIdentifyFailure ctx = globals.ChatCtx(ctx, h.G(), keybase1.TLFIdentifyBehavior_CHAT_GUI, &identBreaks, h.identNotifier) defer h.Trace(ctx, &err, "UnpinMessage")() defer func() { h.setResultRateLimit(ctx, &res) }() uid, err := utils.AssertLoggedInUID(ctx, h.G()) if err != nil { return res, err } conv, err := utils.GetVerifiedConv(ctx, h.G(), uid, convID, types.InboxSourceDataSourceAll) if err != nil { return res, err } pin, err := conv.GetMaxMessage(chat1.MessageType_PIN) if err != nil { return res, err } if _, err := h.PostLocal(ctx, chat1.PostLocalArg{ ConversationID: convID, Msg: chat1.MessagePlaintext{ ClientHeader: chat1.MessageClientHeader{ MessageType: chat1.MessageType_DELETE, Supersedes: pin.GetMessageID(), }, }, }); err != nil { return res, err } return res, nil } func (h *Server) IgnorePinnedMessage(ctx context.Context, convID chat1.ConversationID) (err error) { var identBreaks []keybase1.TLFIdentifyFailure ctx = globals.ChatCtx(ctx, h.G(), keybase1.TLFIdentifyBehavior_CHAT_GUI, &identBreaks, h.identNotifier) defer h.Trace(ctx, &err, "IgnorePinnedMessage")() uid, err := utils.AssertLoggedInUID(ctx, h.G()) if err != nil { return err } conv, err := utils.GetVerifiedConv(ctx, h.G(), uid, convID, types.InboxSourceDataSourceAll) if err != nil { return err } pin, err := conv.GetMaxMessage(chat1.MessageType_PIN) if err != nil { return err } if err := storage.NewPinIgnore(h.G(), uid).Ignore(ctx, convID, pin.GetMessageID()); err != nil { return err } h.G().InboxSource.NotifyUpdate(ctx, uid, convID) return nil } func (h *Server) validateBotRole(ctx context.Context, role keybase1.TeamRole) error { switch role { case keybase1.TeamRole_BOT, keybase1.TeamRole_RESTRICTEDBOT: return nil default: return fmt.Errorf("Only %v and %v are valid roles. Found %v", keybase1.TeamRole_BOT, keybase1.TeamRole_RESTRICTEDBOT, role) } } func (h *Server) teamIDFromTLFName(ctx context.Context, membersType chat1.ConversationMembersType, tlfName string, isPublic bool, ) (res keybase1.TeamID, err error) { switch membersType { case chat1.ConversationMembersType_KBFS: return res, errors.New("unable to find a team for KBFS conv") case chat1.ConversationMembersType_TEAM, chat1.ConversationMembersType_IMPTEAMNATIVE, chat1.ConversationMembersType_IMPTEAMUPGRADE: nameInfo, err := CreateNameInfoSource(ctx, h.G(), membersType).LookupID(ctx, tlfName, isPublic) if err != nil { return "", err } if membersType == chat1.ConversationMembersType_IMPTEAMUPGRADE { team, err := NewTeamLoader(h.G().ExternalG()).loadTeam(ctx, nameInfo.ID, tlfName, membersType, isPublic, nil) if err != nil { return res, err } return team.ID, nil } return keybase1.TeamIDFromString(nameInfo.ID.String()) } return res, fmt.Errorf("unknown members type: %v", membersType) } func (h *Server) fixupTeamErrorWithTLFName(ctx context.Context, username, tlfName string, err error) error { switch err.(type) { case nil: return nil case libkb.ExistsError: h.Debug(ctx, "fixupTeamErrorWithTLFName: %v", err) return libkb.ExistsError{Msg: fmt.Sprintf( "user %q is already a member of team %q", username, tlfName)} case libkb.NotFoundError: h.Debug(ctx, "fixupTeamErrorWithTLFName: %v", err) return libkb.NotFoundError{Msg: fmt.Sprintf( "user %q is not a member of team %q", username, tlfName)} default: return err } } func (h *Server) teamIDFromConvID(ctx context.Context, uid gregor1.UID, convID chat1.ConversationID) (id keybase1.TeamID, conv chat1.ConversationLocal, err error) { if conv, err = utils.GetVerifiedConv(ctx, h.G(), uid, convID, types.InboxSourceDataSourceAll); err != nil { return id, conv, err } team, err := NewTeamLoader(h.G().ExternalG()).loadTeam(ctx, conv.Info.Triple.Tlfid, conv.Info.TlfName, conv.GetMembersType(), conv.IsPublic(), nil) if err != nil { return id, conv, err } return team.ID, conv, nil } func (h *Server) AddBotMember(ctx context.Context, arg chat1.AddBotMemberArg) (err error) { var identBreaks []keybase1.TLFIdentifyFailure ctx = globals.ChatCtx(ctx, h.G(), keybase1.TLFIdentifyBehavior_CHAT_GUI, &identBreaks, h.identNotifier) defer h.Trace(ctx, &err, "AddBotMember")() uid, err := utils.AssertLoggedInUID(ctx, h.G()) if err != nil { return err } if err := h.validateBotRole(ctx, arg.Role); err != nil { return err } teamID, conv, err := h.teamIDFromConvID(ctx, uid, arg.ConvID) if err != nil { return err } defer func() { err = h.fixupTeamErrorWithTLFName(ctx, arg.Username, conv.Info.TlfName, err) }() _, err = teams.AddMemberByID(ctx, h.G().ExternalG(), teamID, arg.Username, arg.Role, arg.BotSettings, nil /* emailInviteMsg */) if err != nil { return err } err = teams.SendTeamChatWelcomeMessage(ctx, h.G().ExternalG(), teamID, conv.Info.TlfName, arg.Username, conv.GetMembersType(), arg.Role) if err != nil { return err } return nil } func (h *Server) EditBotMember(ctx context.Context, arg chat1.EditBotMemberArg) (err error) { var identBreaks []keybase1.TLFIdentifyFailure ctx = globals.ChatCtx(ctx, h.G(), keybase1.TLFIdentifyBehavior_CHAT_GUI, &identBreaks, h.identNotifier) defer h.Trace(ctx, &err, "EditBotMember")() uid, err := utils.AssertLoggedInUID(ctx, h.G()) if err != nil { return err } if err := h.validateBotRole(ctx, arg.Role); err != nil { return err } teamID, _, err := h.teamIDFromConvID(ctx, uid, arg.ConvID) if err != nil { return err } return teams.EditMemberByID(ctx, h.G().ExternalG(), teamID, arg.Username, arg.Role, arg.BotSettings) } func (h *Server) RemoveBotMember(ctx context.Context, arg chat1.RemoveBotMemberArg) (err error) { var identBreaks []keybase1.TLFIdentifyFailure ctx = globals.ChatCtx(ctx, h.G(), keybase1.TLFIdentifyBehavior_CHAT_GUI, &identBreaks, h.identNotifier) defer h.Trace(ctx, &err, "RemoveBotMember")() uid, err := utils.AssertLoggedInUID(ctx, h.G()) if err != nil { return err } teamID, conv, err := h.teamIDFromConvID(ctx, uid, arg.ConvID) if err != nil { return err } defer func() { err = h.fixupTeamErrorWithTLFName(ctx, arg.Username, conv.Info.TlfName, err) }() return teams.RemoveMemberByID(ctx, h.G().ExternalG(), teamID, arg.Username) } func (h *Server) SetBotMemberSettings(ctx context.Context, arg chat1.SetBotMemberSettingsArg) (err error) { var identBreaks []keybase1.TLFIdentifyFailure ctx = globals.ChatCtx(ctx, h.G(), keybase1.TLFIdentifyBehavior_CHAT_GUI, &identBreaks, h.identNotifier) defer h.Trace(ctx, &err, "SetBotMemberSettings")() uid, err := utils.AssertLoggedInUID(ctx, h.G()) if err != nil { return err } teamID, conv, err := h.teamIDFromConvID(ctx, uid, arg.ConvID) if err != nil { return err } defer func() { err = h.fixupTeamErrorWithTLFName(ctx, arg.Username, conv.Info.TlfName, err) }() return teams.SetBotSettingsByID(ctx, h.G().ExternalG(), teamID, arg.Username, arg.BotSettings) } func (h *Server) GetBotMemberSettings(ctx context.Context, arg chat1.GetBotMemberSettingsArg) (res keybase1.TeamBotSettings, err error) { var identBreaks []keybase1.TLFIdentifyFailure ctx = globals.ChatCtx(ctx, h.G(), keybase1.TLFIdentifyBehavior_CHAT_GUI, &identBreaks, h.identNotifier) defer h.Trace(ctx, &err, "GetBotMemberSettings")() uid, err := utils.AssertLoggedInUID(ctx, h.G()) if err != nil { return res, err } teamID, conv, err := h.teamIDFromConvID(ctx, uid, arg.ConvID) if err != nil { return res, err } defer func() { err = h.fixupTeamErrorWithTLFName(ctx, arg.Username, conv.Info.TlfName, err) }() return teams.GetBotSettingsByID(ctx, h.G().ExternalG(), teamID, arg.Username) } func (h *Server) GetTeamRoleInConversation(ctx context.Context, arg chat1.GetTeamRoleInConversationArg) (res keybase1.TeamRole, err error) { ctx = globals.ChatCtx(ctx, h.G(), keybase1.TLFIdentifyBehavior_CHAT_GUI, nil, nil) defer h.Trace(ctx, &err, "GetTeamRoleInConversation")() uid, err := utils.AssertLoggedInUID(ctx, h.G()) if err != nil { return res, err } teamID, _, err := h.teamIDFromConvID(ctx, uid, arg.ConvID) if err != nil { return res, err } return teams.MemberRoleFromID(ctx, h.G().ExternalG(), teamID, arg.Username) } func (h *Server) SimpleSearchInboxConvNames(ctx context.Context, query string) (res []chat1.SimpleSearchInboxConvNamesHit, err error) { ctx = globals.ChatCtx(ctx, h.G(), keybase1.TLFIdentifyBehavior_CHAT_GUI, nil, nil) defer h.Trace(ctx, &err, "SimpleSearchInboxConvNames")() uid, err := utils.AssertLoggedInUID(ctx, h.G()) if err != nil { return res, err } username := h.G().GetEnv().GetUsername().String() allConvs, err := h.G().InboxSource.Search(ctx, uid, query, 100, types.InboxSourceSearchEmptyModeAllBySendCtime) if err != nil { return res, err } for _, conv := range allConvs { switch conv.GetTeamType() { case chat1.TeamType_NONE: searchable := utils.SearchableRemoteConversationName(conv, username) res = append(res, chat1.SimpleSearchInboxConvNamesHit{ Name: searchable, ConvID: conv.GetConvID(), Parts: strings.Split(searchable, ","), TlfName: utils.GetRemoteConvTLFName(conv), }) case chat1.TeamType_SIMPLE, chat1.TeamType_COMPLEX: res = append(res, chat1.SimpleSearchInboxConvNamesHit{ Name: utils.SearchableRemoteConversationName(conv, username), ConvID: conv.GetConvID(), IsTeam: true, TlfName: utils.GetRemoteConvTLFName(conv), }) } } return res, nil } func (h *Server) AddBotConvSearch(ctx context.Context, term string) (res []chat1.ConvSearchHit, err error) { ctx = globals.ChatCtx(ctx, h.G(), keybase1.TLFIdentifyBehavior_CHAT_GUI, nil, nil) defer h.Trace(ctx, &err, "AddBotConvSearch")() uid, err := utils.AssertLoggedInUID(ctx, h.G()) if err != nil { return res, err } username := h.G().GetEnv().GetUsername().String() allConvs, err := h.G().InboxSource.Search(ctx, uid, term, 100, types.InboxSourceSearchEmptyModeAll) if err != nil { return res, err } res = make([]chat1.ConvSearchHit, 0, len(allConvs)) for _, conv := range allConvs { switch conv.GetTeamType() { case chat1.TeamType_NONE: searchable := utils.SearchableRemoteConversationName(conv, username) res = append(res, chat1.ConvSearchHit{ Name: searchable, ConvID: conv.GetConvID(), Parts: strings.Split(searchable, ","), }) case chat1.TeamType_SIMPLE: res = append(res, chat1.ConvSearchHit{ Name: utils.SearchableRemoteConversationName(conv, username), ConvID: conv.GetConvID(), IsTeam: true, }) case chat1.TeamType_COMPLEX: if conv.Conv.Metadata.IsDefaultConv { res = append(res, chat1.ConvSearchHit{ Name: utils.GetRemoteConvTLFName(conv), ConvID: conv.GetConvID(), IsTeam: true, }) } } } return res, nil } func (h *Server) TeamIDFromTLFName(ctx context.Context, arg chat1.TeamIDFromTLFNameArg) (res keybase1.TeamID, err error) { var identBreaks []keybase1.TLFIdentifyFailure ctx = globals.ChatCtx(ctx, h.G(), keybase1.TLFIdentifyBehavior_CHAT_GUI, &identBreaks, h.identNotifier) defer h.Trace(ctx, &err, "TeamIDFromTLFName")() if _, err = utils.AssertLoggedInUID(ctx, h.G()); err != nil { return res, err } return h.teamIDFromTLFName(ctx, arg.MembersType, arg.TlfName, arg.TlfPublic) } func (h *Server) DismissJourneycard(ctx context.Context, arg chat1.DismissJourneycardArg) (err error) { ctx = globals.ChatCtx(ctx, h.G(), keybase1.TLFIdentifyBehavior_CHAT_GUI, nil, h.identNotifier) defer h.Trace(ctx, &err, "DismissJourneycard")() uid, err := utils.AssertLoggedInUID(ctx, h.G()) if err != nil { return err } inbox, err := h.G().InboxSource.ReadUnverified(ctx, uid, types.InboxSourceDataSourceLocalOnly, &chat1.GetInboxQuery{ ConvID: &arg.ConvID, SkipBgLoads: true, AllowUnseenQuery: true, }) if err != nil { return err } switch len(inbox.ConvsUnverified) { case 0: return fmt.Errorf("could not find conversation") case 1: if inbox.ConvsUnverified[0].LocalMetadata == nil { return fmt.Errorf("no local metadata") } teamID, err := keybase1.TeamIDFromString(inbox.ConvsUnverified[0].Conv.Metadata.IdTriple.Tlfid.String()) if err != nil { return err } h.G().JourneyCardManager.Dismiss(ctx, uid, teamID, arg.ConvID, arg.CardType) return nil default: return fmt.Errorf("got %v conversations but expected 1", len(inbox.ConvsUnverified)) } } const welcomeMessageName = "__welcome_message" // welcomeMessageMaxLen is duplicated at // shared/teams/edit-welcome-message/index.tsx:welcomeMessageMaxLen; keep the // values in sync! const welcomeMessageMaxLen = 400 func getWelcomeMessageConv(ctx context.Context, g *globals.Context, uid gregor1.UID, teamID keybase1.TeamID) (res types.RemoteConversation, err error) { onePerTlf := true tlfID := chat1.TLFID(teamID.ToBytes()) topicType := chat1.TopicType_CHAT inbox, err := g.InboxSource.ReadUnverified(ctx, uid, types.InboxSourceDataSourceLocalOnly, &chat1.GetInboxQuery{ MembersTypes: []chat1.ConversationMembersType{chat1.ConversationMembersType_TEAM}, TlfID: &tlfID, TopicType: &topicType, AllowUnseenQuery: true, OneChatTypePerTLF: &onePerTlf, }) if err != nil { return res, err } if len(inbox.ConvsUnverified) == 0 { return res, libkb.NotFoundError{} } return inbox.ConvsUnverified[0], nil } func getWelcomeMessage(ctx context.Context, g *globals.Context, ri func() chat1.RemoteInterface, uid gregor1.UID, teamID keybase1.TeamID) (message chat1.WelcomeMessageDisplay, err error) { conv, err := getWelcomeMessageConv(ctx, g, uid, teamID) if err != nil { return message, err } message = chat1.WelcomeMessageDisplay{Set: false} s := NewConvDevConversationBackedStorage(g, chat1.TopicType_DEV, true /* adminOnly */, ri) found, _, err := s.Get(ctx, uid, conv.GetConvID(), welcomeMessageName, &message, false) if !found { return message, nil } switch err.(type) { case nil: case *DevStorageAdminOnlyError: return message, nil default: return message, err } if len(message.Raw) > welcomeMessageMaxLen { return message, nil } message.Display = utils.PresentDecoratedTextNoMentions(ctx, message.Raw) return message, nil } func setWelcomeMessage(ctx context.Context, g *globals.Context, ri func() chat1.RemoteInterface, uid gregor1.UID, teamID keybase1.TeamID, message chat1.WelcomeMessage) (err error) { if len(message.Raw) > welcomeMessageMaxLen { return fmt.Errorf("welcome message must be at most %d characters; was %d", welcomeMessageMaxLen, len(message.Raw)) } conv, err := getWelcomeMessageConv(ctx, g, uid, teamID) if err != nil { return err } s := NewConvDevConversationBackedStorage(g, chat1.TopicType_DEV, true /* adminOnly */, ri) return s.Put(ctx, uid, conv.GetConvID(), welcomeMessageName, message) } func (h *Server) SetWelcomeMessage(ctx context.Context, arg chat1.SetWelcomeMessageArg) (err error) { var identBreaks []keybase1.TLFIdentifyFailure ctx = globals.ChatCtx(ctx, h.G(), keybase1.TLFIdentifyBehavior_CHAT_GUI, &identBreaks, h.identNotifier) defer h.Trace(ctx, &err, "SetWelcomeMessage")() uid, err := utils.AssertLoggedInUID(ctx, h.G()) if err != nil { return err } return setWelcomeMessage(ctx, h.G(), h.remoteClient, uid, arg.TeamID, arg.Message) } func (h *Server) GetWelcomeMessage(ctx context.Context, teamID keybase1.TeamID) (res chat1.WelcomeMessageDisplay, err error) { var identBreaks []keybase1.TLFIdentifyFailure ctx = globals.ChatCtx(ctx, h.G(), keybase1.TLFIdentifyBehavior_CHAT_GUI, &identBreaks, h.identNotifier) defer h.Trace(ctx, &err, "GetWelcomeMessage")() uid, err := utils.AssertLoggedInUID(ctx, h.G()) if err != nil { return res, err } return getWelcomeMessage(ctx, h.G(), h.remoteClient, uid, teamID) } func (h *Server) GetDefaultTeamChannelsLocal(ctx context.Context, teamID keybase1.TeamID) (res chat1.GetDefaultTeamChannelsLocalRes, err error) { ctx = globals.ChatCtx(ctx, h.G(), keybase1.TLFIdentifyBehavior_CHAT_GUI, nil, h.identNotifier) defer h.Trace(ctx, &err, "GetDefaultTeamChannelsLocal %v", teamID)() defer func() { h.setResultRateLimit(ctx, &res) }() uid, err := utils.AssertLoggedInUID(ctx, h.G()) if err != nil { return res, err } resp, err := h.remoteClient().GetDefaultTeamChannels(ctx, teamID) if err != nil { return res, err } if len(resp.Convs) == 0 { return res, nil } topicType := chat1.TopicType_CHAT query := &chat1.GetInboxLocalQuery{ ConvIDs: resp.Convs, TopicType: &topicType, } ib, _, err := h.G().InboxSource.Read(ctx, uid, types.ConversationLocalizerBlocking, types.InboxSourceDataSourceAll, nil, query) if err != nil { return res, err } res.Convs = utils.PresentConversationLocals(ctx, h.G(), uid, ib.Convs, utils.PresentParticipantsModeSkip) return res, nil } func (h *Server) SetDefaultTeamChannelsLocal(ctx context.Context, arg chat1.SetDefaultTeamChannelsLocalArg) (res chat1.SetDefaultTeamChannelsLocalRes, err error) { ctx = globals.ChatCtx(ctx, h.G(), keybase1.TLFIdentifyBehavior_CHAT_GUI, nil, h.identNotifier) defer h.Trace(ctx, &err, "SetDefaultTeamChannelsLocal: %v", arg.TeamID)() defer func() { h.setResultRateLimit(ctx, &res) }() _, err = utils.AssertLoggedInUID(ctx, h.G()) if err != nil { return res, err } convs := make([]chat1.ConversationID, 0, len(arg.Convs)) for _, conv := range arg.Convs { convID, err := chat1.MakeConvID(conv.String()) if err != nil { return res, err } convs = append(convs, convID) } _, err = h.remoteClient().SetDefaultTeamChannels(ctx, chat1.SetDefaultTeamChannelsArg{ TeamID: arg.TeamID, Convs: convs, }) return res, err } func (h *Server) GetLastActiveForTLF(ctx context.Context, tlfIDStr chat1.TLFIDStr) (res chat1.LastActiveStatus, err error) { ctx = globals.ChatCtx(ctx, h.G(), keybase1.TLFIdentifyBehavior_CHAT_GUI, nil, h.identNotifier) defer h.Trace(ctx, &err, "GetLastActiveForTLF")() uid, err := utils.AssertLoggedInUID(ctx, h.G()) if err != nil { return res, err } tlfID, err := chat1.MakeTLFID(tlfIDStr.String()) if err != nil { return res, err } mtime, err := h.G().TeamChannelSource.GetLastActiveForTLF(ctx, uid, tlfID, chat1.TopicType_CHAT) if err != nil { return res, err } return utils.ToLastActiveStatus(mtime), nil } func (h *Server) GetLastActiveForTeams(ctx context.Context) (res chat1.LastActiveStatusAll, err error) { ctx = globals.ChatCtx(ctx, h.G(), keybase1.TLFIdentifyBehavior_CHAT_GUI, nil, h.identNotifier) defer h.Trace(ctx, &err, "GetLastActiveForTeams")() uid, err := utils.AssertLoggedInUID(ctx, h.G()) if err != nil { return res, err } activity, err := h.G().TeamChannelSource.GetLastActiveForTeams(ctx, uid, chat1.TopicType_CHAT) if err != nil { return res, err } res.Teams = make(map[chat1.TLFIDStr]chat1.LastActiveStatus) res.Channels = make(map[chat1.ConvIDStr]chat1.LastActiveStatus) for tlfID, mtime := range activity.Teams { res.Teams[tlfID] = utils.ToLastActiveStatus(mtime) } for convID, mtime := range activity.Channels { res.Channels[convID] = utils.ToLastActiveStatus(mtime) } return res, nil } func (h *Server) GetRecentJoinsLocal(ctx context.Context, convID chat1.ConversationID) (numJoins int, err error) { ctx = globals.ChatCtx(ctx, h.G(), keybase1.TLFIdentifyBehavior_CHAT_GUI, nil, h.identNotifier) defer h.Trace(ctx, &err, "GetRecentJoinsLocal")() _, err = utils.AssertLoggedInUID(ctx, h.G()) if err != nil { return 0, err } return h.G().TeamChannelSource.GetRecentJoins(ctx, convID, h.remoteClient()) } func (h *Server) RefreshParticipants(ctx context.Context, convID chat1.ConversationID) (err error) { ctx = globals.ChatCtx(ctx, h.G(), keybase1.TLFIdentifyBehavior_CHAT_GUI, nil, h.identNotifier) defer h.Trace(ctx, &err, "RefreshParticipants")() uid, err := utils.AssertLoggedInUID(ctx, h.G()) if err != nil { return err } h.G().ParticipantsSource.GetWithNotifyNonblock(ctx, uid, convID, types.InboxSourceDataSourceAll) return nil } func (h *Server) GetParticipants(ctx context.Context, convID chat1.ConversationID) (participants []chat1.ConversationLocalParticipant, err error) { ctx = globals.ChatCtx(ctx, h.G(), keybase1.TLFIdentifyBehavior_CHAT_CLI, nil, h.identNotifier) defer h.Trace(ctx, &err, "GetParticipants")() uid, err := utils.AssertLoggedInUID(ctx, h.G()) if err != nil { return nil, err } uids, err := h.G().ParticipantsSource.Get(ctx, uid, convID, types.InboxSourceDataSourceAll) if err != nil { return nil, err } participants, err = h.G().ParticipantsSource.GetParticipantsFromUids(ctx, uids) if err != nil { return nil, err } return participants, nil } func (h *Server) GetLastActiveAtLocal(ctx context.Context, arg chat1.GetLastActiveAtLocalArg) (lastActiveAt gregor1.Time, err error) { ctx = globals.ChatCtx(ctx, h.G(), keybase1.TLFIdentifyBehavior_CHAT_GUI, nil, h.identNotifier) defer h.Trace(ctx, &err, "GetLastActiveAtLocal")() _, err = utils.AssertLoggedInUID(ctx, h.G()) if err != nil { return 0, err } uid := gregor1.UID(libkb.UsernameToUID(arg.Username).ToBytes()) return h.G().TeamChannelSource.GetLastActiveAt(ctx, arg.TeamID, uid, h.remoteClient()) } func (h *Server) GetLastActiveAtMultiLocal(ctx context.Context, arg chat1.GetLastActiveAtMultiLocalArg) (res map[keybase1.TeamID]gregor1.Time, err error) { ctx = globals.ChatCtx(ctx, h.G(), keybase1.TLFIdentifyBehavior_CHAT_GUI, nil, h.identNotifier) defer h.Trace(ctx, &err, "GetLastActiveAtMultiLocal")() _, err = utils.AssertLoggedInUID(ctx, h.G()) if err != nil { return nil, err } uid := gregor1.UID(libkb.UsernameToUID(arg.Username).ToBytes()) res = map[keybase1.TeamID]gregor1.Time{} resMu := sync.Mutex{} sem := semaphore.NewWeighted(10) eg, subctx := errgroup.WithContext(ctx) for _, teamID := range arg.TeamIDs { if err := sem.Acquire(subctx, 1); err != nil { return nil, err } teamID := teamID eg.Go(func() error { defer sem.Release(1) lastActive, err := h.G().TeamChannelSource.GetLastActiveAt(subctx, teamID, uid, h.remoteClient()) if err != nil { return err } resMu.Lock() res[teamID] = lastActive resMu.Unlock() return nil }) } if err := eg.Wait(); err != nil { return nil, err } return res, nil } func (h *Server) getEmojiError(err error) *chat1.EmojiError { if err == nil { return nil } if verr, ok := err.(*EmojiValidationError); ok { return verr.Export() } return &chat1.EmojiError{ Clidisplay: err.Error(), Uidisplay: "unknown error", } } func (h *Server) AddEmoji(ctx context.Context, arg chat1.AddEmojiArg) (res chat1.AddEmojiRes, err error) { ctx = globals.ChatCtx(ctx, h.G(), keybase1.TLFIdentifyBehavior_CHAT_GUI, nil, h.identNotifier) defer h.Trace(ctx, &err, "AddEmoji")() defer func() { h.setResultRateLimit(ctx, &res) }() defer func() { res.Error = h.getEmojiError(err) err = nil }() uid, err := utils.AssertLoggedInUID(ctx, h.G()) if err != nil { return res, err } if _, err := h.G().EmojiSource.Add(ctx, uid, arg.ConvID, arg.Alias, arg.Filename, arg.AllowOverwrite); err != nil { return res, err } return res, nil } func (h *Server) AddEmojis(ctx context.Context, arg chat1.AddEmojisArg) (res chat1.AddEmojisRes, err error) { ctx = globals.ChatCtx(ctx, h.G(), keybase1.TLFIdentifyBehavior_CHAT_GUI, nil, h.identNotifier) defer h.Trace(ctx, &err, "AddEmojis")() defer func() { h.setResultRateLimit(ctx, &res) }() uid, err := utils.AssertLoggedInUID(ctx, h.G()) if err != nil { return res, err } if len(arg.Aliases) != len(arg.Filenames) { return chat1.AddEmojisRes{}, errors.New("aliases and filenames have different length") } res.FailedFilenames = make(map[string]chat1.EmojiError) res.SuccessFilenames = make([]string, 0, len(arg.Aliases)) for i := range arg.Aliases { _, err := h.G().EmojiSource.Add(ctx, uid, arg.ConvID, arg.Aliases[i], arg.Filenames[i], arg.AllowOverwrite[i]) if err != nil { res.FailedFilenames[arg.Filenames[i]] = *h.getEmojiError(err) } else { res.SuccessFilenames = append(res.SuccessFilenames, arg.Filenames[i]) } } return res, nil } func (h *Server) AddEmojiAlias(ctx context.Context, arg chat1.AddEmojiAliasArg) (res chat1.AddEmojiAliasRes, err error) { ctx = globals.ChatCtx(ctx, h.G(), keybase1.TLFIdentifyBehavior_CHAT_GUI, nil, h.identNotifier) defer h.Trace(ctx, &err, "AddEmojiAlias")() defer func() { h.setResultRateLimit(ctx, &res) }() defer func() { res.Error = h.getEmojiError(err) err = nil }() uid, err := utils.AssertLoggedInUID(ctx, h.G()) if err != nil { return res, err } if _, err := h.G().EmojiSource.AddAlias(ctx, uid, arg.ConvID, arg.NewAlias, arg.ExistingAlias); err != nil { return res, err } return res, nil } func (h *Server) RemoveEmoji(ctx context.Context, arg chat1.RemoveEmojiArg) (res chat1.RemoveEmojiRes, err error) { ctx = globals.ChatCtx(ctx, h.G(), keybase1.TLFIdentifyBehavior_CHAT_GUI, nil, h.identNotifier) defer h.Trace(ctx, &err, "RemoveEmoji")() defer func() { h.setResultRateLimit(ctx, &res) }() uid, err := utils.AssertLoggedInUID(ctx, h.G()) if err != nil { return res, err } if err := h.G().EmojiSource.Remove(ctx, uid, arg.ConvID, arg.Alias); err != nil { return res, err } return res, nil } func (h *Server) UserEmojis(ctx context.Context, arg chat1.UserEmojisArg) (res chat1.UserEmojiRes, err error) { ctx = globals.ChatCtx(ctx, h.G(), keybase1.TLFIdentifyBehavior_CHAT_GUI, nil, h.identNotifier) defer h.Trace(ctx, &err, "UserEmojis")() defer func() { h.setResultRateLimit(ctx, &res) }() uid, err := utils.AssertLoggedInUID(ctx, h.G()) if err != nil { return res, err } res.Emojis, err = h.G().EmojiSource.Get(ctx, uid, arg.ConvID, arg.Opts) return res, err } func (h *Server) ToggleEmojiAnimations(ctx context.Context, enabled bool) (err error) { ctx = globals.ChatCtx(ctx, h.G(), keybase1.TLFIdentifyBehavior_CHAT_GUI, nil, h.identNotifier) defer h.Trace(ctx, &err, "ToggleEmojiAnimations")() uid, err := utils.AssertLoggedInUID(ctx, h.G()) if err != nil { return err } return h.G().EmojiSource.ToggleAnimations(ctx, uid, enabled) } func (h *Server) ForwardMessage(ctx context.Context, arg chat1.ForwardMessageArg) (res chat1.PostLocalRes, err error) { var identBreaks []keybase1.TLFIdentifyFailure ctx = globals.ChatCtx(ctx, h.G(), arg.IdentifyBehavior, &identBreaks, h.identNotifier) defer h.Trace(ctx, &err, "ForwardMessage")() defer func() { h.setResultRateLimit(ctx, &res) }() uid, err := utils.AssertLoggedInUID(ctx, h.G()) if err != nil { return res, err } reason := chat1.GetThreadReason_FORWARDMSG msg, err := h.G().ConvSource.GetMessage(ctx, arg.SrcConvID, uid, arg.MsgID, &reason, nil, true) if err != nil { return res, err } else if !msg.IsValid() { return res, fmt.Errorf("unable to forward message, source is invalid") } dstConv, err := utils.GetVerifiedConv(ctx, h.G(), uid, arg.DstConvID, types.InboxSourceDataSourceAll) if err != nil { return res, err } mvalid := msg.Valid() switch mvalid.ClientHeader.MessageType { case chat1.MessageType_ATTACHMENT: // download from the original source mbod := msg.Valid().MessageBody.Attachment() sink, _, err := h.G().AttachmentUploader.GetUploadTempSink(ctx, mbod.Object.Filename) if err != nil { return res, err } _, err = h.downloadAttachmentLocal(ctx, uid, downloadAttachmentArg{ SessionID: arg.SessionID, ConversationID: arg.SrcConvID, MessageID: arg.MsgID, IdentifyBehavior: arg.IdentifyBehavior, Sink: sink, }) if err != nil { return res, err } var ephemeralLifetime *gregor1.DurationSec if md := mvalid.EphemeralMetadata(); md != nil { ephemeralLifetime = &md.Lifetime } return h.PostFileAttachmentLocal(ctx, chat1.PostFileAttachmentLocalArg{ SessionID: arg.SessionID, Arg: chat1.PostFileAttachmentArg{ ConversationID: arg.DstConvID, TlfName: dstConv.Info.TlfName, Visibility: dstConv.Info.Visibility, Filename: sink.Name(), Title: arg.Title, Metadata: mbod.Metadata, IdentifyBehavior: arg.IdentifyBehavior, EphemeralLifetime: ephemeralLifetime, }, }) default: return h.PostLocal(ctx, chat1.PostLocalArg{ SessionID: arg.SessionID, ConversationID: arg.DstConvID, Msg: chat1.MessagePlaintext{ ClientHeader: chat1.MessageClientHeader{ Conv: dstConv.Info.Triple, TlfName: dstConv.Info.TlfName, TlfPublic: dstConv.Info.Visibility == keybase1.TLFVisibility_PUBLIC, MessageType: mvalid.ClientHeader.MessageType, EphemeralMetadata: mvalid.EphemeralMetadata(), }, MessageBody: mvalid.MessageBody.DeepCopy(), }, IdentifyBehavior: arg.IdentifyBehavior, SkipInChatPayments: true, }) } } func (h *Server) ForwardMessageNonblock(ctx context.Context, arg chat1.ForwardMessageNonblockArg) (res chat1.PostLocalNonblockRes, err error) { var identBreaks []keybase1.TLFIdentifyFailure ctx = globals.ChatCtx(ctx, h.G(), arg.IdentifyBehavior, &identBreaks, h.identNotifier) defer h.Trace(ctx, &err, "ForwardMessageNonblock")() defer h.suspendBgConvLoads(ctx)() defer func() { h.setResultRateLimit(ctx, &res) }() uid, err := utils.AssertLoggedInUID(ctx, h.G()) if err != nil { return res, err } reason := chat1.GetThreadReason_FORWARDMSG msg, err := h.G().ConvSource.GetMessage(ctx, arg.SrcConvID, uid, arg.MsgID, &reason, nil, true) if err != nil { return res, err } else if !msg.IsValid() { return res, fmt.Errorf("unable to forward message, source is invalid") } dstConv, err := utils.GetVerifiedConv(ctx, h.G(), uid, arg.DstConvID, types.InboxSourceDataSourceAll) if err != nil { return res, err } mvalid := msg.Valid() switch mvalid.ClientHeader.MessageType { case chat1.MessageType_ATTACHMENT: mbod := msg.Valid().MessageBody.Attachment() sink, _, err := h.G().AttachmentUploader.GetUploadTempSink(ctx, mbod.Object.Filename) if err != nil { return res, err } _, err = h.downloadAttachmentLocal(ctx, uid, downloadAttachmentArg{ SessionID: arg.SessionID, ConversationID: arg.SrcConvID, MessageID: arg.MsgID, IdentifyBehavior: arg.IdentifyBehavior, Sink: sink, }) if err != nil { return res, err } var ephemeralLifetime *gregor1.DurationSec if md := mvalid.EphemeralMetadata(); md != nil { ephemeralLifetime = &md.Lifetime } return h.PostFileAttachmentLocalNonblock(ctx, chat1.PostFileAttachmentLocalNonblockArg{ SessionID: arg.SessionID, Arg: chat1.PostFileAttachmentArg{ ConversationID: arg.DstConvID, TlfName: dstConv.Info.TlfName, Visibility: dstConv.Info.Visibility, Filename: sink.Name(), Title: arg.Title, Metadata: mbod.Metadata, IdentifyBehavior: arg.IdentifyBehavior, EphemeralLifetime: ephemeralLifetime, }, }) default: return h.PostLocalNonblock(ctx, chat1.PostLocalNonblockArg{ SessionID: arg.SessionID, ConversationID: arg.DstConvID, Msg: chat1.MessagePlaintext{ ClientHeader: chat1.MessageClientHeader{ Conv: dstConv.Info.Triple, TlfName: dstConv.Info.TlfName, TlfPublic: dstConv.Info.Visibility == keybase1.TLFVisibility_PUBLIC, MessageType: mvalid.ClientHeader.MessageType, EphemeralMetadata: mvalid.EphemeralMetadata(), }, MessageBody: mvalid.MessageBody.DeepCopy(), }, IdentifyBehavior: arg.IdentifyBehavior, SkipInChatPayments: true, }) } } func (h *Server) ForwardMessageConvSearch(ctx context.Context, term string) (res []chat1.ConvSearchHit, err error) { ctx = globals.ChatCtx(ctx, h.G(), keybase1.TLFIdentifyBehavior_CHAT_GUI, nil, nil) defer h.Trace(ctx, &err, "ForwardMessageConvSearch")() uid, err := utils.AssertLoggedInUID(ctx, h.G()) if err != nil { return res, err } username := h.G().GetEnv().GetUsername().String() allConvs, err := h.G().InboxSource.Search(ctx, uid, term, 100, types.InboxSourceSearchEmptyModeAllBySendCtime) if err != nil { return res, err } res = make([]chat1.ConvSearchHit, 0, len(allConvs)) for _, conv := range allConvs { if conv.CannotWrite() { continue } switch conv.GetTeamType() { case chat1.TeamType_NONE: searchable := utils.SearchableRemoteConversationName(conv, username) res = append(res, chat1.ConvSearchHit{ Name: searchable, ConvID: conv.GetConvID(), Parts: strings.Split(searchable, ","), }) case chat1.TeamType_SIMPLE, chat1.TeamType_COMPLEX: res = append(res, chat1.ConvSearchHit{ Name: utils.SearchableRemoteConversationName(conv, username), ConvID: conv.GetConvID(), IsTeam: true, }) } } return res, nil } func (h *Server) TrackGiphySelect(ctx context.Context, arg chat1.TrackGiphySelectArg) (res chat1.TrackGiphySelectRes, err error) { var identBreaks []keybase1.TLFIdentifyFailure ctx = globals.ChatCtx(ctx, h.G(), keybase1.TLFIdentifyBehavior_CHAT_GUI, &identBreaks, h.identNotifier) // Never log user content. defer h.Trace(ctx, &err, "TrackGiphySelect")() uid, err := utils.AssertLoggedInUID(ctx, h.G()) if err != nil { h.Debug(ctx, "TrackGiphySelect: not logged in: %s", err) return chat1.TrackGiphySelectRes{}, nil } err = storage.NewGiphyStore(h.G()).Put(ctx, uid, arg.Result) return chat1.TrackGiphySelectRes{}, err } func (h *Server) ArchiveChat(ctx context.Context, arg chat1.ArchiveChatJobRequest) (res chat1.ArchiveChatRes, err error) { var identBreaks []keybase1.TLFIdentifyFailure ctx = globals.ChatCtx(ctx, h.G(), arg.IdentifyBehavior, &identBreaks, h.identNotifier) defer h.Trace(ctx, &err, "ArchiveChat")() uid, err := utils.AssertLoggedInUID(ctx, h.G()) if err != nil { h.Debug(ctx, "ArchiveChat: not logged in: %s", err) return chat1.ArchiveChatRes{}, nil } outpath, err := NewChatArchiver(h.G(), uid, h.remoteClient).ArchiveChat(ctx, arg) if err != nil { return chat1.ArchiveChatRes{}, err } return chat1.ArchiveChatRes{ IdentifyFailures: identBreaks, OutputPath: outpath, }, err } func (h *Server) ArchiveChatList(ctx context.Context, identifyBehavior keybase1.TLFIdentifyBehavior) (res chat1.ArchiveChatListRes, err error) { var identBreaks []keybase1.TLFIdentifyFailure ctx = globals.ChatCtx(ctx, h.G(), identifyBehavior, &identBreaks, h.identNotifier) defer h.Trace(ctx, &err, "ArchiveChatList")() _, err = utils.AssertLoggedInUID(ctx, h.G()) if err != nil { h.Debug(ctx, "ArchiveChatList: not logged in: %s", err) return chat1.ArchiveChatListRes{}, nil } return h.G().ArchiveRegistry.List(ctx) } func (h *Server) ArchiveChatDelete(ctx context.Context, arg chat1.ArchiveChatDeleteArg) (err error) { var identBreaks []keybase1.TLFIdentifyFailure ctx = globals.ChatCtx(ctx, h.G(), arg.IdentifyBehavior, &identBreaks, h.identNotifier) defer h.Trace(ctx, &err, "ArchiveChatDelete")() _, err = utils.AssertLoggedInUID(ctx, h.G()) if err != nil { h.Debug(ctx, "ArchiveChatDelete: not logged in: %s", err) return nil } return h.G().ArchiveRegistry.Delete(ctx, arg.JobID, arg.DeleteOutputPath) } func (h *Server) ArchiveChatPause(ctx context.Context, arg chat1.ArchiveChatPauseArg) (err error) { var identBreaks []keybase1.TLFIdentifyFailure ctx = globals.ChatCtx(ctx, h.G(), arg.IdentifyBehavior, &identBreaks, h.identNotifier) defer h.Trace(ctx, &err, "ArchiveChatPause")() _, err = utils.AssertLoggedInUID(ctx, h.G()) if err != nil { h.Debug(ctx, "ArchiveChatPause: not logged in: %s", err) return nil } return h.G().ArchiveRegistry.Pause(ctx, arg.JobID) } func (h *Server) ArchiveChatResume(ctx context.Context, arg chat1.ArchiveChatResumeArg) (err error) { var identBreaks []keybase1.TLFIdentifyFailure ctx = globals.ChatCtx(ctx, h.G(), arg.IdentifyBehavior, &identBreaks, h.identNotifier) defer h.Trace(ctx, &err, "ArchiveChatResume")() _, err = utils.AssertLoggedInUID(ctx, h.G()) if err != nil { h.Debug(ctx, "ArchiveChatResume: not logged in: %s", err) return nil } return h.G().ArchiveRegistry.Resume(ctx, arg.JobID) }