github.com/keybase/client/go@v0.0.0-20240520164431-4f512a4c85a3/chat/server_test.go (about)

     1  // Copyright 2016 Keybase, Inc. All rights reserved. Use of
     2  // this source code is governed by the included BSD license.
     3  
     4  package chat
     5  
     6  import (
     7  	"bytes"
     8  	"crypto/rand"
     9  	"encoding/binary"
    10  	"encoding/hex"
    11  	"encoding/json"
    12  	"errors"
    13  	"fmt"
    14  	"net/http"
    15  	"os"
    16  	"sort"
    17  	"strconv"
    18  	"strings"
    19  	"testing"
    20  	"time"
    21  
    22  	"github.com/davecgh/go-spew/spew"
    23  	"github.com/keybase/client/go/chat/bots"
    24  	"github.com/keybase/client/go/kbhttp/manager"
    25  
    26  	"golang.org/x/net/context"
    27  
    28  	"encoding/base64"
    29  
    30  	"github.com/keybase/client/go/chat/commands"
    31  	"github.com/keybase/client/go/chat/globals"
    32  	"github.com/keybase/client/go/chat/msgchecker"
    33  	"github.com/keybase/client/go/chat/search"
    34  	"github.com/keybase/client/go/chat/storage"
    35  	"github.com/keybase/client/go/chat/types"
    36  	"github.com/keybase/client/go/chat/utils"
    37  	"github.com/keybase/client/go/chat/wallet"
    38  	"github.com/keybase/client/go/gregor"
    39  	grutils "github.com/keybase/client/go/gregor/utils"
    40  	"github.com/keybase/client/go/kbtest"
    41  	"github.com/keybase/client/go/libkb"
    42  	"github.com/keybase/client/go/logger"
    43  	"github.com/keybase/client/go/protocol/chat1"
    44  	"github.com/keybase/client/go/protocol/gregor1"
    45  	"github.com/keybase/client/go/protocol/keybase1"
    46  	"github.com/keybase/client/go/protocol/stellar1"
    47  	"github.com/keybase/client/go/teams"
    48  	"github.com/keybase/clockwork"
    49  	"github.com/keybase/go-codec/codec"
    50  	"github.com/keybase/go-framed-msgpack-rpc/rpc"
    51  	"github.com/stretchr/testify/require"
    52  )
    53  
    54  type gregorTestConnection struct {
    55  	globals.Contextified
    56  	utils.DebugLabeler
    57  
    58  	cli          rpc.GenericClient
    59  	uid          gregor1.UID
    60  	sessionToken string
    61  }
    62  
    63  var _ rpc.ConnectionHandler = (*gregorTestConnection)(nil)
    64  
    65  func newGregorTestConnection(g *globals.Context, uid gregor1.UID, sessionToken string) *gregorTestConnection {
    66  	return &gregorTestConnection{
    67  		Contextified: globals.NewContextified(g),
    68  		DebugLabeler: utils.NewDebugLabeler(g.ExternalG(), "gregorTestConnection", false),
    69  		uid:          uid,
    70  		sessionToken: sessionToken,
    71  	}
    72  }
    73  
    74  func (g *gregorTestConnection) Connect(ctx context.Context) (err error) {
    75  	defer g.Trace(ctx, &err, "Connect")()
    76  	uri, err := rpc.ParseFMPURI(g.G().Env.GetGregorURI())
    77  	if err != nil {
    78  		return err
    79  	}
    80  	opts := rpc.ConnectionOpts{
    81  		TagsFunc:      logger.LogTagsFromContextRPC,
    82  		WrapErrorFunc: libkb.MakeWrapError(g.G().ExternalG()),
    83  	}
    84  	trans := rpc.NewConnectionTransport(uri, libkb.NewRPCLogFactory(g.G().ExternalG()),
    85  		g.G().ExternalG().RemoteNetworkInstrumenterStorage,
    86  		libkb.MakeWrapError(g.G().ExternalG()), rpc.DefaultMaxFrameLength)
    87  	conn := rpc.NewConnectionWithTransport(g, trans,
    88  		libkb.NewContextifiedErrorUnwrapper(g.G().ExternalG()),
    89  		logger.LogOutputWithDepthAdder{Logger: g.G().Log}, opts)
    90  	g.cli = conn.GetClient()
    91  	return nil
    92  }
    93  
    94  func (g *gregorTestConnection) GetClient() chat1.RemoteInterface {
    95  	return chat1.RemoteClient{Cli: g.cli}
    96  }
    97  
    98  func (g *gregorTestConnection) Reconnect(ctx context.Context) (bool, error) {
    99  	return false, nil
   100  }
   101  
   102  func (g *gregorTestConnection) OnConnect(ctx context.Context, conn *rpc.Connection,
   103  	cli rpc.GenericClient, srv *rpc.Server) error {
   104  	g.Debug(ctx, "logged in: authenticating")
   105  	ac := gregor1.AuthClient{Cli: cli}
   106  	auth, err := ac.AuthenticateSessionToken(ctx, gregor1.SessionToken(g.sessionToken))
   107  	if err != nil {
   108  		g.Debug(ctx, "auth error: %s", err)
   109  		return err
   110  	}
   111  	if !auth.Uid.Eq(g.uid) {
   112  		return fmt.Errorf("wrong uid authed: auth: %s uid: %s", auth.Uid, g.uid)
   113  	}
   114  
   115  	return srv.Register(gregor1.OutgoingProtocol(g))
   116  }
   117  
   118  func (g *gregorTestConnection) BroadcastMessage(ctx context.Context, m gregor1.Message) error {
   119  	if obm := m.ToOutOfBandMessage(); obm != nil {
   120  		_, err := g.G().PushHandler.HandleOobm(ctx, obm)
   121  		return err
   122  	}
   123  	if ibm := m.ToInBandMessage(); ibm != nil {
   124  		if creation := ibm.ToStateUpdateMessage().Creation(); creation != nil {
   125  			switch creation.Category().String() {
   126  			case "team.sbs":
   127  				var msg keybase1.TeamSBSMsg
   128  				if err := json.Unmarshal(creation.Body().Bytes(), &msg); err != nil {
   129  					g.G().Log.CDebugf(ctx, "error unmarshaling team.sbs item: %s", err)
   130  					return err
   131  				}
   132  				err := teams.HandleSBSRequest(ctx, g.G().ExternalG(), msg)
   133  				if err != nil {
   134  					return err
   135  				}
   136  			case "team.change":
   137  				var msg []keybase1.TeamChangeRow
   138  				if err := json.Unmarshal(creation.Body().Bytes(), &msg); err != nil {
   139  					g.G().Log.CDebugf(ctx, "error unmarshaling team.change items: %s", err)
   140  					return err
   141  				}
   142  				err := teams.HandleChangeNotification(ctx, g.G().ExternalG(), msg, keybase1.TeamChangeSet{})
   143  				if err != nil {
   144  					return err
   145  				}
   146  			}
   147  		}
   148  	}
   149  	return nil
   150  }
   151  
   152  func (g *gregorTestConnection) State(ctx context.Context) (gregor.State, error) {
   153  	return gregor1.IncomingClient{Cli: g.cli}.State(ctx, gregor1.StateArg{
   154  		Uid: g.uid,
   155  	})
   156  }
   157  
   158  func (g *gregorTestConnection) UpdateCategory(ctx context.Context, cat string, body []byte,
   159  	dtime gregor1.TimeOrOffset) (gregor1.MsgID, error) {
   160  	msg, err := grutils.TemplateMessage(g.uid)
   161  	if err != nil {
   162  		return nil, err
   163  	}
   164  	msgID := msg.Ibm_.StateUpdate_.Md_.MsgID_
   165  	msg.Ibm_.StateUpdate_.Creation_ = &gregor1.Item{
   166  		Category_: gregor1.Category(cat),
   167  		Body_:     gregor1.Body(body),
   168  		Dtime_:    dtime,
   169  	}
   170  	msg.Ibm_.StateUpdate_.Dismissal_ = &gregor1.Dismissal{
   171  		Ranges_: []gregor1.MsgRange{
   172  			{
   173  				Category_:   gregor1.Category(cat),
   174  				SkipMsgIDs_: []gregor1.MsgID{msgID},
   175  			}},
   176  	}
   177  	return msgID, gregor1.IncomingClient{Cli: g.cli}.ConsumeMessage(ctx, msg)
   178  }
   179  
   180  func (g *gregorTestConnection) DismissItem(ctx context.Context, cli gregor1.IncomingInterface, id gregor.MsgID) error {
   181  	msg, err := grutils.FormMessageForDismissItem(ctx, g.uid, id)
   182  	if err != nil {
   183  		return err
   184  	}
   185  	return gregor1.IncomingClient{Cli: g.cli}.ConsumeMessage(ctx, msg.(gregor1.Message))
   186  }
   187  
   188  func (g *gregorTestConnection) DismissCategory(ctx context.Context, cat gregor1.Category) error {
   189  	msg, err := grutils.FormMessageForDismissCategory(ctx, g.uid, cat)
   190  	if err != nil {
   191  		return err
   192  	}
   193  	return gregor1.IncomingClient{Cli: g.cli}.ConsumeMessage(ctx, msg.(gregor1.Message))
   194  }
   195  
   196  func (g *gregorTestConnection) InjectItem(ctx context.Context, cat string, body []byte,
   197  	dtime gregor1.TimeOrOffset) (gregor1.MsgID, error) {
   198  	msg, err := grutils.FormMessageForInjectItem(ctx, g.uid, cat, body, dtime)
   199  	if err != nil {
   200  		return nil, err
   201  	}
   202  	retMsgID := gregor1.MsgID(msg.ToInBandMessage().Metadata().MsgID().Bytes())
   203  	return retMsgID, gregor1.IncomingClient{Cli: g.cli}.ConsumeMessage(ctx, msg.(gregor1.Message))
   204  }
   205  
   206  func (g *gregorTestConnection) LocalDismissItem(ctx context.Context, id gregor.MsgID) error {
   207  	return nil
   208  }
   209  
   210  func (g *gregorTestConnection) OnConnectError(err error, reconnectThrottleDuration time.Duration) {
   211  }
   212  
   213  func (g *gregorTestConnection) OnDoCommandError(err error, nextTime time.Duration) {
   214  }
   215  
   216  func (g *gregorTestConnection) OnDisconnected(ctx context.Context, status rpc.DisconnectStatus) {
   217  }
   218  
   219  func (g *gregorTestConnection) ShouldRetry(name string, err error) bool {
   220  	return false
   221  }
   222  
   223  func (g *gregorTestConnection) ShouldRetryOnConnect(err error) bool {
   224  	return false
   225  }
   226  
   227  func (g *gregorTestConnection) HandlerName() string {
   228  	return "gregorTestConnection"
   229  }
   230  
   231  func newTestContext(tc *kbtest.ChatTestContext) context.Context {
   232  	if tc.ChatG.CtxFactory == nil {
   233  		g := globals.NewContext(tc.G, tc.ChatG)
   234  		g.CtxFactory = NewCtxFactory(g)
   235  	}
   236  	return globals.ChatCtx(context.Background(), tc.Context(), keybase1.TLFIdentifyBehavior_CHAT_CLI,
   237  		nil, NewCachingIdentifyNotifier(tc.Context()))
   238  }
   239  
   240  func newTestContextWithTlfMock(tc *kbtest.ChatTestContext, tlfMock types.NameInfoSource) context.Context {
   241  	ctx := newTestContext(tc)
   242  	return globals.CtxAddOverrideNameInfoSource(ctx, tlfMock)
   243  }
   244  
   245  type testUISource struct {
   246  }
   247  
   248  func (t testUISource) GetChatUI(sessionID int) libkb.ChatUI {
   249  	return nil
   250  }
   251  
   252  func (t testUISource) GetStreamUICli() *keybase1.StreamUiClient {
   253  	return &keybase1.StreamUiClient{Cli: nil}
   254  }
   255  
   256  // Create a team with me as the owner
   257  func createTeam(tc libkb.TestContext) string {
   258  	b, err := libkb.RandBytes(4)
   259  	require.NoError(tc.T, err)
   260  
   261  	name := fmt.Sprintf("TeAm%v", hex.EncodeToString(b))
   262  	_, err = teams.CreateRootTeam(context.TODO(), tc.G, name, keybase1.TeamSettings{})
   263  	require.NoError(tc.T, err)
   264  	return name
   265  }
   266  
   267  // Create a team with me as the owner and writers as writers.
   268  // Writers must not include me.
   269  func createTeamWithWriters(tc libkb.TestContext, writers []*kbtest.FakeUser) string {
   270  	name := createTeam(tc)
   271  	for _, u := range writers {
   272  		err := teams.SetRoleWriter(context.TODO(), tc.G, name, u.Username)
   273  		require.NoError(tc.T, err, "team set role")
   274  	}
   275  	return name
   276  }
   277  
   278  type byUsername []*kbtest.FakeUser
   279  
   280  func (b byUsername) Len() int      { return len(b) }
   281  func (b byUsername) Swap(i, j int) { b[i], b[j] = b[j], b[i] }
   282  func (b byUsername) Less(i, j int) bool {
   283  	return strings.Compare(b[i].Username, b[j].Username) < 0
   284  }
   285  
   286  func teamKey(users []*kbtest.FakeUser) (res string) {
   287  	ucopy := make([]*kbtest.FakeUser, len(users))
   288  	copy(ucopy, users)
   289  	sort.Sort(byUsername(ucopy))
   290  	for _, u := range ucopy {
   291  		res += u.Username
   292  	}
   293  	return res
   294  }
   295  
   296  var useRemoteMock = true
   297  
   298  func runWithMemberTypes(t *testing.T, f func(membersType chat1.ConversationMembersType)) {
   299  	useRemoteMock = false
   300  	defer func() { useRemoteMock = true }()
   301  	t.Logf("Team Stage Begin")
   302  	start := time.Now()
   303  	f(chat1.ConversationMembersType_TEAM)
   304  	t.Logf("Team Stage End: %v", time.Since(start))
   305  
   306  	t.Logf("Implicit Team Stage Begin")
   307  	os.Setenv("KEYBASE_FEATURES", "admin")
   308  	defer os.Setenv("KEYBASE_FEATURES", "")
   309  	start = time.Now()
   310  	f(chat1.ConversationMembersType_IMPTEAMNATIVE)
   311  	t.Logf("Implicit Team Stage End: %v", time.Since(start))
   312  }
   313  
   314  func runWithEphemeral(t *testing.T, mt chat1.ConversationMembersType, f func(ephemeralLifetime *gregor1.DurationSec)) {
   315  	switch mt {
   316  	case chat1.ConversationMembersType_TEAM, chat1.ConversationMembersType_IMPTEAMUPGRADE, chat1.ConversationMembersType_IMPTEAMNATIVE:
   317  		f(nil)
   318  		lifetime := gregor1.DurationSec(24 * 60 * 60 * 6)
   319  		f(&lifetime)
   320  	default:
   321  		f(nil)
   322  	}
   323  }
   324  
   325  func runWithRetentionPolicyTypes(t *testing.T, f func(policy chat1.RetentionPolicy, ephemeralLifetime *gregor1.DurationSec)) {
   326  	age := gregor1.DurationSec(1)
   327  	t.Logf("using EXPIRE retention policy")
   328  	f(chat1.NewRetentionPolicyWithExpire(chat1.RpExpire{Age: age}), nil)
   329  	t.Logf("using EPHEMERAL retention policy")
   330  	f(chat1.NewRetentionPolicyWithEphemeral(chat1.RpEphemeral{Age: age}), &age)
   331  }
   332  
   333  type chatTestUserContext struct {
   334  	startCtx context.Context
   335  	u        *kbtest.FakeUser
   336  	h        *Server
   337  	ri       chat1.RemoteInterface
   338  	m        libkb.MetaContext
   339  }
   340  
   341  func (tuc *chatTestUserContext) user() *kbtest.FakeUser {
   342  	return tuc.u
   343  }
   344  
   345  func (tuc *chatTestUserContext) chatLocalHandler() chat1.LocalInterface {
   346  	return tuc.h
   347  }
   348  
   349  type chatTestContext struct {
   350  	world *kbtest.ChatMockWorld
   351  
   352  	userContextCache map[string]*chatTestUserContext
   353  	teamCache        map[string]string
   354  }
   355  
   356  func makeChatTestContext(t *testing.T, name string, numUsers int) *chatTestContext {
   357  	ctc := &chatTestContext{}
   358  	ctc.world = NewChatMockWorld(t, name, numUsers)
   359  	ctc.userContextCache = make(map[string]*chatTestUserContext)
   360  	ctc.teamCache = make(map[string]string)
   361  	return ctc
   362  }
   363  
   364  func (c *chatTestContext) advanceFakeClock(d time.Duration) {
   365  	c.world.Fc.Advance(d)
   366  }
   367  
   368  func (c *chatTestContext) as(t *testing.T, user *kbtest.FakeUser) *chatTestUserContext {
   369  	var ctx context.Context
   370  	require.NotNil(t, user)
   371  
   372  	if tuc, ok := c.userContextCache[user.Username]; ok {
   373  		return tuc
   374  	}
   375  
   376  	tc, ok := c.world.Tcs[user.Username]
   377  	require.True(t, ok)
   378  	g := globals.NewContext(tc.G, tc.ChatG)
   379  	h := NewServer(g, nil, testUISource{})
   380  	uid := gregor1.UID(user.User.GetUID().ToBytes())
   381  
   382  	var tlf *kbtest.TlfMock
   383  	var ri chat1.RemoteInterface
   384  	var serverConn types.ServerConnection
   385  	if useRemoteMock {
   386  		mockRemote := kbtest.NewChatRemoteMock(c.world)
   387  		mockRemote.SetCurrentUser(user.User.GetUID().ToBytes())
   388  		tlf = kbtest.NewTlfMock(c.world)
   389  		ri = mockRemote
   390  		serverConn = kbtest.NewChatRemoteMockServerConnection(mockRemote)
   391  		ctx = newTestContextWithTlfMock(tc, tlf)
   392  	} else {
   393  		ctx = newTestContext(tc)
   394  		nist, err := tc.G.ActiveDevice.NIST(context.TODO())
   395  		require.NoError(t, err)
   396  		sessionToken := nist.Token().String()
   397  		gh := newGregorTestConnection(tc.Context(), uid, sessionToken)
   398  		g.GregorState = gh
   399  		require.NoError(t, gh.Connect(ctx))
   400  		ri = gh.GetClient()
   401  		serverConn = gh
   402  	}
   403  
   404  	h.boxer = NewBoxer(g)
   405  
   406  	chatStorage := storage.New(g, nil)
   407  	chatStorage.SetClock(c.world.Fc)
   408  	g.CtxFactory = NewCtxFactory(g)
   409  	g.ConvSource = NewHybridConversationSource(g, h.boxer, chatStorage,
   410  		func() chat1.RemoteInterface { return ri })
   411  	chatStorage.SetAssetDeleter(g.ConvSource)
   412  	g.InboxSource = NewHybridInboxSource(g,
   413  		func() chat1.RemoteInterface { return ri })
   414  	g.InboxSource.Start(context.TODO(), uid)
   415  	g.InboxSource.Connected(context.TODO())
   416  	g.ServerCacheVersions = storage.NewServerVersions(g)
   417  	chatSyncer := NewSyncer(g)
   418  	g.Syncer = chatSyncer
   419  	g.ConnectivityMonitor = &libkb.NullConnectivityMonitor{}
   420  	searcher := search.NewRegexpSearcher(g)
   421  	// Force small pages during tests to ensure we fetch context from new pages
   422  	searcher.SetPageSize(2)
   423  	g.RegexpSearcher = searcher
   424  	indexer := search.NewIndexer(g)
   425  	ictx := globals.CtxAddIdentifyMode(context.Background(), keybase1.TLFIdentifyBehavior_CHAT_SKIP, nil)
   426  	indexer.SetPageSize(2)
   427  	indexer.SetStartSyncDelay(0)
   428  	indexer.Start(ictx, uid)
   429  	g.Indexer = indexer
   430  
   431  	h.setTestRemoteClient(ri)
   432  
   433  	tc.G.SetService()
   434  	baseSender := NewBlockingSender(g, h.boxer, func() chat1.RemoteInterface { return ri })
   435  	deliverer := NewDeliverer(g, baseSender, serverConn)
   436  	deliverer.SetClock(c.world.Fc)
   437  	if useRemoteMock {
   438  		deliverer.setTestingNameInfoSource(tlf)
   439  	}
   440  	g.MessageDeliverer = deliverer
   441  	g.MessageDeliverer.Start(context.TODO(), uid)
   442  	g.MessageDeliverer.Connected(context.TODO())
   443  
   444  	retrier := NewFetchRetrier(g)
   445  	retrier.SetClock(c.world.Fc)
   446  	g.FetchRetrier = retrier
   447  	g.FetchRetrier.Connected(context.TODO())
   448  	g.FetchRetrier.Start(context.TODO(), uid)
   449  
   450  	g.ConvLoader = NewBackgroundConvLoader(g)
   451  	g.EphemeralPurger = types.DummyEphemeralPurger{}
   452  	g.CommandsSource = commands.NewSource(g)
   453  
   454  	pushHandler := NewPushHandler(g)
   455  	pushHandler.Start(context.TODO(), nil)
   456  	g.PushHandler = pushHandler
   457  	g.TeamChannelSource = NewTeamChannelSource(g)
   458  	g.AttachmentURLSrv = types.DummyAttachmentHTTPSrv{}
   459  	g.ActivityNotifier = NewNotifyRouterActivityRouter(g)
   460  	g.AttachmentUploader = types.DummyAttachmentUploader{}
   461  	g.Unfurler = types.DummyUnfurler{}
   462  	g.StellarLoader = types.DummyStellarLoader{}
   463  	g.StellarSender = types.DummyStellarSender{}
   464  	g.TeamMentionLoader = types.DummyTeamMentionLoader{}
   465  	g.CoinFlipManager = NewFlipManager(g, func() chat1.RemoteInterface { return ri })
   466  	g.CoinFlipManager.Start(context.TODO(), uid)
   467  	g.JourneyCardManager = NewJourneyCardManager(g, func() chat1.RemoteInterface { return ri })
   468  	g.BotCommandManager = bots.NewCachingBotCommandManager(g, func() chat1.RemoteInterface { return ri },
   469  		CreateNameInfoSource)
   470  	g.BotCommandManager.Start(context.TODO(), uid)
   471  	g.UIInboxLoader = types.DummyUIInboxLoader{}
   472  	g.UIThreadLoader = NewUIThreadLoader(g, func() chat1.RemoteInterface { return ri })
   473  	g.ParticipantsSource = NewCachingParticipantSource(g, func() chat1.RemoteInterface { return ri })
   474  	g.EmojiSource = NewDevConvEmojiSource(g, func() chat1.RemoteInterface { return ri })
   475  	g.EphemeralTracker = NewEphemeralTracker(g)
   476  	g.EphemeralTracker.Start(context.TODO(), uid)
   477  
   478  	tc.G.ChatHelper = NewHelper(g, func() chat1.RemoteInterface { return ri })
   479  
   480  	tuc := &chatTestUserContext{
   481  		h:        h,
   482  		u:        user,
   483  		startCtx: ctx,
   484  		ri:       ri,
   485  		m:        libkb.NewMetaContext(ctx, tc.G),
   486  	}
   487  	c.userContextCache[user.Username] = tuc
   488  	return tuc
   489  }
   490  
   491  func (c *chatTestContext) cleanup() {
   492  	c.world.Cleanup()
   493  }
   494  
   495  func (c *chatTestContext) users() (users []*kbtest.FakeUser) {
   496  	for _, u := range c.world.Users {
   497  		users = append(users, u)
   498  	}
   499  	return users
   500  }
   501  
   502  func mustCreatePublicConversationForTest(t *testing.T, ctc *chatTestContext, creator *kbtest.FakeUser,
   503  	topicType chat1.TopicType, membersType chat1.ConversationMembersType, others ...*kbtest.FakeUser) (created chat1.ConversationInfoLocal) {
   504  	created = mustCreateConversationForTestNoAdvanceClock(t, ctc, creator, topicType,
   505  		nil, keybase1.TLFVisibility_PUBLIC, membersType, others...)
   506  	ctc.advanceFakeClock(time.Second)
   507  	return created
   508  }
   509  
   510  func mustCreateConversationForTest(t *testing.T, ctc *chatTestContext, creator *kbtest.FakeUser,
   511  	topicType chat1.TopicType, membersType chat1.ConversationMembersType, others ...*kbtest.FakeUser) (created chat1.ConversationInfoLocal) {
   512  	created = mustCreateConversationForTestNoAdvanceClock(t, ctc, creator, topicType,
   513  		nil, keybase1.TLFVisibility_PRIVATE, membersType, others...)
   514  	ctc.advanceFakeClock(time.Second)
   515  	return created
   516  }
   517  
   518  func mustCreateChannelForTest(t *testing.T, ctc *chatTestContext, creator *kbtest.FakeUser,
   519  	topicType chat1.TopicType, topicName *string, membersType chat1.ConversationMembersType,
   520  	others ...*kbtest.FakeUser) (created chat1.ConversationInfoLocal) {
   521  	created = mustCreateConversationForTestNoAdvanceClock(t, ctc, creator, topicType,
   522  		topicName, keybase1.TLFVisibility_PRIVATE, membersType, others...)
   523  	ctc.advanceFakeClock(time.Second)
   524  	return created
   525  }
   526  
   527  func mustCreateConversationForTestNoAdvanceClock(t *testing.T, ctc *chatTestContext,
   528  	creator *kbtest.FakeUser, topicType chat1.TopicType, topicName *string, visibility keybase1.TLFVisibility,
   529  	membersType chat1.ConversationMembersType, others ...*kbtest.FakeUser) (created chat1.ConversationInfoLocal) {
   530  	var err error
   531  
   532  	t.Logf("mustCreateConversationForTestNoAdvanceClock")
   533  	t.Logf("creator: %v", creator.Username)
   534  	for _, o := range others {
   535  		t.Logf("other: %v", o.Username)
   536  	}
   537  
   538  	// Create conversation name based on list of users
   539  	var name string
   540  	switch membersType {
   541  	case chat1.ConversationMembersType_KBFS, chat1.ConversationMembersType_IMPTEAMNATIVE,
   542  		chat1.ConversationMembersType_IMPTEAMUPGRADE:
   543  		var memberStr []string
   544  		for _, other := range others {
   545  			memberStr = append(memberStr, other.Username)
   546  		}
   547  		memberStr = append(memberStr, creator.Username)
   548  		name = strings.Join(memberStr, ",")
   549  	case chat1.ConversationMembersType_TEAM:
   550  		tc := ctc.world.Tcs[creator.Username]
   551  		users := others
   552  		users = append(users, creator)
   553  		key := teamKey(users)
   554  		if tn, ok := ctc.teamCache[key]; !ok {
   555  			name = createTeamWithWriters(tc.TestContext, others)
   556  			ctc.teamCache[key] = name
   557  		} else {
   558  			name = tn
   559  		}
   560  	default:
   561  		t.Fatalf("unhandled membersType: %v", membersType)
   562  	}
   563  
   564  	tc := ctc.as(t, creator)
   565  	ncres, err := tc.chatLocalHandler().NewConversationLocal(tc.startCtx,
   566  		chat1.NewConversationLocalArg{
   567  			TlfName:          name,
   568  			TopicType:        topicType,
   569  			TopicName:        topicName,
   570  			TlfVisibility:    visibility,
   571  			MembersType:      membersType,
   572  			IdentifyBehavior: keybase1.TLFIdentifyBehavior_CHAT_CLI,
   573  		})
   574  	require.NoError(t, err)
   575  
   576  	// Set initial active list
   577  	conv := ctc.world.GetConversationByID(ncres.Conv.GetConvID())
   578  	if conv != nil {
   579  		conv.Metadata.ActiveList = append(conv.Metadata.ActiveList, creator.GetUID().ToBytes())
   580  		for _, o := range others {
   581  			conv.Metadata.ActiveList = append(conv.Metadata.ActiveList, o.GetUID().ToBytes())
   582  		}
   583  	}
   584  
   585  	return ncres.Conv.Info
   586  }
   587  
   588  func postLocalEphemeralForTest(t *testing.T, ctc *chatTestContext, asUser *kbtest.FakeUser, conv chat1.ConversationInfoLocal, msg chat1.MessageBody, ephemeralLifetime *gregor1.DurationSec) (chat1.PostLocalRes, error) {
   589  	defer ctc.advanceFakeClock(time.Second)
   590  	mt, err := msg.MessageType()
   591  	require.NoError(t, err)
   592  	tc := ctc.as(t, asUser)
   593  	var ephemeralMetadata *chat1.MsgEphemeralMetadata
   594  	if ephemeralLifetime != nil {
   595  		ephemeralMetadata = &chat1.MsgEphemeralMetadata{
   596  			Lifetime: *ephemeralLifetime,
   597  		}
   598  	}
   599  	return tc.chatLocalHandler().PostLocal(tc.startCtx, chat1.PostLocalArg{
   600  		ConversationID: conv.Id,
   601  		Msg: chat1.MessagePlaintext{
   602  			ClientHeader: chat1.MessageClientHeader{
   603  				Conv:              conv.Triple,
   604  				MessageType:       mt,
   605  				TlfName:           conv.TlfName,
   606  				EphemeralMetadata: ephemeralMetadata,
   607  			},
   608  			MessageBody: msg,
   609  		},
   610  		IdentifyBehavior: keybase1.TLFIdentifyBehavior_CHAT_CLI,
   611  	})
   612  }
   613  
   614  func mustPostLocalEphemeralForTest(t *testing.T, ctc *chatTestContext,
   615  	asUser *kbtest.FakeUser, conv chat1.ConversationInfoLocal, msg chat1.MessageBody, ephemeralLifetime *gregor1.DurationSec) chat1.MessageID {
   616  	res, err := postLocalEphemeralForTest(t, ctc, asUser, conv, msg, ephemeralLifetime)
   617  	require.NoError(t, err)
   618  	ctc.advanceFakeClock(time.Second)
   619  	return res.MessageID
   620  }
   621  
   622  func postLocalForTestNoAdvanceClock(t *testing.T, ctc *chatTestContext, asUser *kbtest.FakeUser, conv chat1.ConversationInfoLocal, msg chat1.MessageBody) (chat1.PostLocalRes, error) {
   623  	mt, err := msg.MessageType()
   624  	require.NoError(t, err)
   625  	tc := ctc.as(t, asUser)
   626  	return tc.chatLocalHandler().PostLocal(tc.startCtx, chat1.PostLocalArg{
   627  		ConversationID: conv.Id,
   628  		Msg: chat1.MessagePlaintext{
   629  			ClientHeader: chat1.MessageClientHeader{
   630  				Conv:        conv.Triple,
   631  				MessageType: mt,
   632  				TlfName:     conv.TlfName,
   633  			},
   634  			MessageBody: msg,
   635  		},
   636  		IdentifyBehavior: keybase1.TLFIdentifyBehavior_CHAT_CLI,
   637  	})
   638  }
   639  
   640  func postLocalForTest(t *testing.T, ctc *chatTestContext,
   641  	asUser *kbtest.FakeUser, conv chat1.ConversationInfoLocal, msg chat1.MessageBody) (chat1.PostLocalRes, error) {
   642  	defer ctc.advanceFakeClock(time.Second)
   643  	return postLocalForTestNoAdvanceClock(t, ctc, asUser, conv, msg)
   644  }
   645  
   646  func mustPostLocalForTestNoAdvanceClock(t *testing.T, ctc *chatTestContext,
   647  	asUser *kbtest.FakeUser, conv chat1.ConversationInfoLocal, msg chat1.MessageBody) chat1.MessageID {
   648  	x, err := postLocalForTestNoAdvanceClock(t, ctc, asUser, conv, msg)
   649  	require.NoError(t, err)
   650  	return x.MessageID
   651  }
   652  
   653  func mustPostLocalForTest(t *testing.T, ctc *chatTestContext,
   654  	asUser *kbtest.FakeUser, conv chat1.ConversationInfoLocal, msg chat1.MessageBody) chat1.MessageID {
   655  	msgID := mustPostLocalForTestNoAdvanceClock(t, ctc, asUser, conv, msg)
   656  	ctc.advanceFakeClock(time.Second)
   657  	return msgID
   658  }
   659  
   660  func mustSetConvRetentionLocal(t *testing.T, ctc *chatTestContext, asUser *kbtest.FakeUser,
   661  	convID chat1.ConversationID, policy chat1.RetentionPolicy) {
   662  	tc := ctc.as(t, asUser)
   663  	err := tc.chatLocalHandler().SetConvRetentionLocal(tc.startCtx, chat1.SetConvRetentionLocalArg{
   664  		ConvID: convID,
   665  		Policy: policy,
   666  	})
   667  	require.NoError(t, err)
   668  }
   669  
   670  func mustSetTeamRetentionLocal(t *testing.T, ctc *chatTestContext, asUser *kbtest.FakeUser,
   671  	teamID keybase1.TeamID, policy chat1.RetentionPolicy) {
   672  	tc := ctc.as(t, asUser)
   673  	err := tc.chatLocalHandler().SetTeamRetentionLocal(tc.startCtx, chat1.SetTeamRetentionLocalArg{
   674  		TeamID: teamID,
   675  		Policy: policy,
   676  	})
   677  	require.NoError(t, err)
   678  }
   679  
   680  func mustSetConvRetention(t *testing.T, ctc *chatTestContext, asUser *kbtest.FakeUser,
   681  	convID chat1.ConversationID, policy chat1.RetentionPolicy, sweepChannel uint64) {
   682  	tc := ctc.as(t, asUser)
   683  	// Use the remote version instead of the local version in order to have access to sweepChannel.
   684  	_, err := tc.ri.SetConvRetention(tc.startCtx, chat1.SetConvRetentionArg{
   685  		ConvID:       convID,
   686  		Policy:       policy,
   687  		SweepChannel: sweepChannel,
   688  	})
   689  	require.NoError(t, err)
   690  }
   691  
   692  func mustSetTeamRetention(t *testing.T, ctc *chatTestContext, asUser *kbtest.FakeUser,
   693  	teamID keybase1.TeamID, policy chat1.RetentionPolicy, sweepChannel uint64) {
   694  	tc := ctc.as(t, asUser)
   695  	// Use the remote version instead of the local version in order to have access to sweepChannel.
   696  	_, err := tc.ri.SetTeamRetention(tc.startCtx, chat1.SetTeamRetentionArg{
   697  		TeamID:       teamID,
   698  		Policy:       policy,
   699  		SweepChannel: sweepChannel,
   700  	})
   701  	require.NoError(t, err)
   702  }
   703  
   704  func mustJoinConversationByID(t *testing.T, ctc *chatTestContext, asUser *kbtest.FakeUser, convID chat1.ConversationID) {
   705  	tc := ctc.as(t, asUser)
   706  	_, err := tc.chatLocalHandler().JoinConversationByIDLocal(tc.startCtx, convID)
   707  	require.NoError(t, err)
   708  }
   709  
   710  // Make chatsweeperd run a particular conversation until messages are deleted.
   711  // The RPC does not need to run as a particular user, that's just an easy way to get a remote client.
   712  // Consumes expunge notifications from `listener` and returns the latest one.
   713  // `upto` is optional (0 means any)
   714  func sweepPollForDeletion(t *testing.T, ctc *chatTestContext, asUser *kbtest.FakeUser, listener *serverChatListener, convID chat1.ConversationID, uptoWant chat1.MessageID) chat1.ExpungeInfo {
   715  	t.Logf("sweepPollForDeletion(convID: %v, uptoWant: %v", convID, uptoWant)
   716  	tc := ctc.as(t, asUser)
   717  	maxTime := 5 * time.Second
   718  	afterCh := time.After(maxTime)
   719  	var foundTaskCount int
   720  	var upto chat1.MessageID
   721  	for i := 0; ; i++ {
   722  		ctx := globals.ChatCtx(context.Background(), tc.h.G(), keybase1.TLFIdentifyBehavior_CLI, nil, nil)
   723  		trace, _ := globals.CtxTrace(ctx)
   724  		t.Logf("+ RetentionSweepConv(%v) (uptoWant %v) [chat-trace=%v]", convID.String(), uptoWant, trace)
   725  		res, err := tc.ri.RetentionSweepConv(ctx, convID)
   726  		t.Logf("- RetentionSweepConv res: %+v", res)
   727  		require.NoError(t, err)
   728  		if res.FoundTask {
   729  			foundTaskCount++
   730  		}
   731  		if res.DeletedMessages {
   732  			var expungeInfo chat1.ExpungeInfo
   733  			for {
   734  				expungeInfo = consumeExpunge(t, listener)
   735  				if expungeInfo.Expunge == res.Expunge {
   736  					break
   737  				}
   738  				t.Logf("sweepPollForDeletion %+v != %+v, trying consumeExpunge again",
   739  					expungeInfo.Expunge, res.Expunge)
   740  			}
   741  			require.Equal(t, convID, expungeInfo.ConvID, "accidentally consumed expunge info for other conv")
   742  			upto = res.Expunge.Upto
   743  			if upto >= uptoWant {
   744  				return expungeInfo
   745  			}
   746  			t.Logf("sweepPollForDeletion ignoring expungeInfo: %+v (uptoWant:%v)", expungeInfo.Expunge, uptoWant)
   747  		}
   748  		time.Sleep(10 * time.Millisecond)
   749  		select {
   750  		case <-afterCh:
   751  			require.FailNow(t, fmt.Sprintf("no messages deleted after %v runs, %v hit, upto %v, %v",
   752  				i, foundTaskCount, upto, maxTime))
   753  		default:
   754  		}
   755  	}
   756  }
   757  
   758  // Sweep a conv and assert that no deletion occurred
   759  func sweepNoDeletion(t *testing.T, ctc *chatTestContext, asUser *kbtest.FakeUser, convID chat1.ConversationID) {
   760  	t.Logf("sweepNoDeletion(convID: %v)", convID)
   761  	tc := ctc.as(t, asUser)
   762  	res, err := tc.ri.RetentionSweepConv(tc.startCtx, convID)
   763  	require.NoError(t, err)
   764  	require.False(t, res.DeletedMessages, "messages deleted")
   765  }
   766  
   767  func TestChatSrvNewConversationLocal(t *testing.T) {
   768  	runWithMemberTypes(t, func(mt chat1.ConversationMembersType) {
   769  		ctc := makeChatTestContext(t, "NewConversationLocal", 2)
   770  		defer ctc.cleanup()
   771  		users := ctc.users()
   772  		created := mustCreateConversationForTest(t, ctc, users[0], chat1.TopicType_CHAT, mt,
   773  			ctc.as(t, users[1]).user())
   774  		tc := ctc.world.Tcs[users[0].Username]
   775  		ctx := ctc.as(t, users[0]).startCtx
   776  		uid := users[0].User.GetUID().ToBytes()
   777  		conv, err := utils.GetUnverifiedConv(ctx, tc.Context(), uid, created.Id,
   778  			types.InboxSourceDataSourceRemoteOnly)
   779  		require.NoError(t, err)
   780  		require.NotZero(t, len(conv.Conv.MaxMsgSummaries))
   781  		switch mt {
   782  		case chat1.ConversationMembersType_KBFS, chat1.ConversationMembersType_IMPTEAMNATIVE:
   783  			refName := string(kbtest.CanonicalTlfNameForTest(
   784  				ctc.as(t, users[0]).user().Username + "," + ctc.as(t, users[1]).user().Username),
   785  			)
   786  			require.Equal(t, refName, conv.Conv.MaxMsgSummaries[0].TlfName)
   787  		case chat1.ConversationMembersType_TEAM:
   788  			teamName := ctc.teamCache[teamKey(ctc.users())]
   789  			require.Equal(t, strings.ToLower(teamName), conv.Conv.MaxMsgSummaries[0].TlfName)
   790  		}
   791  	})
   792  }
   793  
   794  func TestChatSrvNewChatConversationLocalTwice(t *testing.T) {
   795  	runWithMemberTypes(t, func(mt chat1.ConversationMembersType) {
   796  		ctc := makeChatTestContext(t, "NewConversationLocalTwice", 2)
   797  		defer ctc.cleanup()
   798  		users := ctc.users()
   799  
   800  		c1 := mustCreateConversationForTest(t, ctc, users[0], chat1.TopicType_CHAT,
   801  			mt, ctc.as(t, users[1]).user())
   802  		c2 := mustCreateConversationForTest(t, ctc, users[0], chat1.TopicType_CHAT,
   803  			mt, ctc.as(t, users[1]).user())
   804  
   805  		t.Logf("c1: %v c2: %v", c1, c2)
   806  		require.True(t, c2.Id.Eq(c1.Id))
   807  	})
   808  }
   809  
   810  func TestChatNewDevConversationLocalTwice(t *testing.T) {
   811  	runWithMemberTypes(t, func(mt chat1.ConversationMembersType) {
   812  		ctc := makeChatTestContext(t, "NewDevConversationLocalTwice", 2)
   813  		defer ctc.cleanup()
   814  		users := ctc.users()
   815  
   816  		mustCreateConversationForTest(t, ctc, users[0], chat1.TopicType_DEV,
   817  			mt, ctc.as(t, users[1]).user())
   818  		mustCreateConversationForTest(t, ctc, users[0], chat1.TopicType_DEV,
   819  			mt, ctc.as(t, users[1]).user())
   820  	})
   821  }
   822  
   823  func TestChatSrvNewConversationMultiTeam(t *testing.T) {
   824  	runWithMemberTypes(t, func(mt chat1.ConversationMembersType) {
   825  		ctc := makeChatTestContext(t, "NewConversationLocalTeams", 2)
   826  		defer ctc.cleanup()
   827  		users := ctc.users()
   828  
   829  		conv := mustCreateConversationForTest(t, ctc, users[0], chat1.TopicType_CHAT,
   830  			mt, ctc.as(t, users[1]).user())
   831  
   832  		tc := ctc.as(t, users[0])
   833  		topicName := "MIKETIME"
   834  		arg := chat1.NewConversationLocalArg{
   835  			TlfName:          conv.TlfName,
   836  			TopicName:        &topicName,
   837  			TopicType:        chat1.TopicType_CHAT,
   838  			TlfVisibility:    keybase1.TLFVisibility_PRIVATE,
   839  			MembersType:      mt,
   840  			IdentifyBehavior: keybase1.TLFIdentifyBehavior_CHAT_CLI,
   841  		}
   842  		ncres, err := tc.chatLocalHandler().NewConversationLocal(tc.startCtx, arg)
   843  		require.NoError(t, err)
   844  		switch mt {
   845  		case chat1.ConversationMembersType_TEAM:
   846  			require.Equal(t, topicName, ncres.Conv.Info.TopicName)
   847  			require.NotEqual(t, conv.Id, ncres.Conv.GetConvID())
   848  		default:
   849  			require.Equal(t, conv.Id, ncres.Conv.GetConvID())
   850  		}
   851  		// recreate as the second user
   852  		tc2 := ctc.as(t, users[1])
   853  		ncres2, err := tc2.chatLocalHandler().NewConversationLocal(tc2.startCtx, arg)
   854  		require.NoError(t, err)
   855  		switch mt {
   856  		case chat1.ConversationMembersType_TEAM:
   857  			require.Equal(t, topicName, ncres.Conv.Info.TopicName)
   858  			require.Equal(t, ncres.Conv.GetConvID(), ncres2.Conv.GetConvID())
   859  		default:
   860  			require.Equal(t, conv.Id, ncres.Conv.GetConvID())
   861  		}
   862  
   863  		// Try some invalid names
   864  		topicName = "#mike"
   865  		_, err = tc.chatLocalHandler().NewConversationLocal(tc.startCtx, arg)
   866  		require.Error(t, err)
   867  		topicName = "/mike"
   868  		_, err = tc.chatLocalHandler().NewConversationLocal(tc.startCtx, arg)
   869  		require.Error(t, err)
   870  		topicName = "mi.ke"
   871  		_, err = tc.chatLocalHandler().NewConversationLocal(tc.startCtx, arg)
   872  		require.Error(t, err)
   873  		arg.TopicName = nil
   874  		ncres, err = tc.chatLocalHandler().NewConversationLocal(tc.startCtx, arg)
   875  		require.NoError(t, err)
   876  		switch mt {
   877  		case chat1.ConversationMembersType_TEAM:
   878  			require.Equal(t, globals.DefaultTeamTopic, ncres.Conv.Info.TopicName)
   879  		default:
   880  		}
   881  		arg.TopicName = &topicName
   882  		topicName = "dskjdskdjskdjskdjskdjskdjskdjskjdskjdskdskdjksdjks"
   883  		_, err = tc.chatLocalHandler().NewConversationLocal(tc.startCtx, arg)
   884  		require.Error(t, err)
   885  	})
   886  }
   887  
   888  func TestChatSrvGetInboxAndUnboxLocal(t *testing.T) {
   889  	runWithMemberTypes(t, func(mt chat1.ConversationMembersType) {
   890  		ctc := makeChatTestContext(t, "ResolveConversationLocal", 2)
   891  		defer ctc.cleanup()
   892  		users := ctc.users()
   893  
   894  		ctx := ctc.as(t, users[0]).startCtx
   895  		created := mustCreateConversationForTest(t, ctc, users[0], chat1.TopicType_CHAT,
   896  			mt, ctc.as(t, users[1]).user())
   897  
   898  		gilres, err := ctc.as(t, users[0]).chatLocalHandler().GetInboxAndUnboxLocal(ctx, chat1.GetInboxAndUnboxLocalArg{
   899  			Query: &chat1.GetInboxLocalQuery{
   900  				ConvIDs: []chat1.ConversationID{created.Id},
   901  			},
   902  			IdentifyBehavior: keybase1.TLFIdentifyBehavior_CHAT_CLI,
   903  		})
   904  		if err != nil {
   905  			t.Fatalf("GetInboxAndUnboxLocal error: %v", err)
   906  		}
   907  		conversations := gilres.Conversations
   908  		if len(conversations) != 1 {
   909  			t.Fatalf("unexpected response from GetInboxAndUnboxLocal. expected 1 items, got %d\n", len(conversations))
   910  		}
   911  
   912  		tc := ctc.world.Tcs[users[0].Username]
   913  		uid := users[0].User.GetUID().ToBytes()
   914  
   915  		conv, err := utils.GetUnverifiedConv(ctx, tc.Context(), uid, created.Id,
   916  			types.InboxSourceDataSourceRemoteOnly)
   917  		require.NoError(t, err)
   918  		if conversations[0].Info.TlfName != conv.Conv.MaxMsgSummaries[0].TlfName {
   919  			t.Fatalf("unexpected TlfName in response from GetInboxAndUnboxLocal. %s != %s (mt = %v)", conversations[0].Info.TlfName, conv.Conv.MaxMsgSummaries[0].TlfName, mt)
   920  		}
   921  		if !conversations[0].Info.Id.Eq(created.Id) {
   922  			t.Fatalf("unexpected Id in response from GetInboxAndUnboxLocal. %s != %s\n", conversations[0].Info.Id, created.Id)
   923  		}
   924  		if conversations[0].Info.Triple.TopicType != chat1.TopicType_CHAT {
   925  			t.Fatalf("unexpected topicType in response from GetInboxAndUnboxLocal. %s != %s\n", conversations[0].Info.Triple.TopicType, chat1.TopicType_CHAT)
   926  		}
   927  	})
   928  }
   929  func TestChatSrvGetInboxNonblockLocalMetadata(t *testing.T) {
   930  	runWithMemberTypes(t, func(mt chat1.ConversationMembersType) {
   931  		ctc := makeChatTestContext(t, "GetInboxNonblockLocalLocalMetadata", 6)
   932  		defer ctc.cleanup()
   933  		users := ctc.users()
   934  
   935  		numconvs := 5
   936  		ui := kbtest.NewChatUI()
   937  		ctc.as(t, users[0]).h.mockChatUI = ui
   938  		tc := ctc.world.Tcs[users[0].Username]
   939  		ctx := ctc.as(t, users[0]).startCtx
   940  		uid := gregor1.UID(users[0].GetUID().ToBytes())
   941  		tc.G.UIRouter = kbtest.NewMockUIRouter(ui)
   942  		tc.ChatG.UIInboxLoader = NewUIInboxLoader(tc.Context())
   943  		tc.ChatG.UIInboxLoader.Start(ctx, uid)
   944  		defer func() { <-tc.ChatG.UIInboxLoader.Stop(ctx) }()
   945  
   946  		var firstConv chat1.ConversationInfoLocal
   947  		switch mt {
   948  		case chat1.ConversationMembersType_TEAM:
   949  			firstConv = mustCreateConversationForTest(t, ctc, users[0], chat1.TopicType_CHAT, mt, users[1:]...)
   950  		default:
   951  		}
   952  
   953  		// Create a bunch of blank convos
   954  		oldUILoader := tc.ChatG.UIInboxLoader
   955  		tc.ChatG.UIInboxLoader = types.DummyUIInboxLoader{}
   956  		convs := make(map[chat1.ConvIDStr]bool)
   957  		for i := 0; i < numconvs; i++ {
   958  			var created chat1.ConversationInfoLocal
   959  			switch mt {
   960  			case chat1.ConversationMembersType_TEAM:
   961  				topicName := fmt.Sprintf("%d", i+1)
   962  				ncres, err := ctc.as(t, users[0]).chatLocalHandler().NewConversationLocal(ctx,
   963  					chat1.NewConversationLocalArg{
   964  						TlfName:          firstConv.TlfName,
   965  						TopicName:        &topicName,
   966  						TopicType:        chat1.TopicType_CHAT,
   967  						TlfVisibility:    keybase1.TLFVisibility_PRIVATE,
   968  						MembersType:      chat1.ConversationMembersType_TEAM,
   969  						IdentifyBehavior: keybase1.TLFIdentifyBehavior_CHAT_CLI,
   970  					})
   971  				require.NoError(t, err)
   972  				created = ncres.Conv.Info
   973  			default:
   974  				created = mustCreateConversationForTest(t, ctc, users[0], chat1.TopicType_CHAT,
   975  					mt, ctc.as(t, users[i+1]).user())
   976  			}
   977  			t.Logf("created: %s", created.Id)
   978  			convs[created.Id.ConvIDStr()] = true
   979  
   980  			mustPostLocalForTest(t, ctc, users[i+1], created,
   981  				chat1.NewMessageBodyWithText(chat1.MessageText{
   982  					Body: fmt.Sprintf("%d", i+1),
   983  				}))
   984  			time.Sleep(100 * time.Millisecond)
   985  		}
   986  
   987  		tc.ChatG.UIInboxLoader = oldUILoader
   988  		_, err := ctc.as(t, users[0]).chatLocalHandler().GetInboxNonblockLocal(ctx,
   989  			chat1.GetInboxNonblockLocalArg{
   990  				IdentifyBehavior: keybase1.TLFIdentifyBehavior_CHAT_CLI,
   991  			},
   992  		)
   993  		require.NoError(t, err)
   994  
   995  		// Account for initial team convo
   996  		switch mt {
   997  		case chat1.ConversationMembersType_TEAM:
   998  			numconvs++
   999  		default:
  1000  		}
  1001  
  1002  		select {
  1003  		case ibox := <-ui.InboxCb:
  1004  			require.NotNil(t, ibox.InboxRes, "nil inbox")
  1005  			require.Equal(t, numconvs, len(ibox.InboxRes.Items))
  1006  			for _, conv := range ibox.InboxRes.Items {
  1007  				require.Nil(t, conv.LocalMetadata)
  1008  			}
  1009  		case <-time.After(20 * time.Second):
  1010  			require.Fail(t, "no inbox received")
  1011  		}
  1012  		// Get all convos
  1013  		for i := 0; i < numconvs; i++ {
  1014  			select {
  1015  			case conv := <-ui.InboxCb:
  1016  				require.NotNil(t, conv.ConvRes, "no conv")
  1017  				delete(convs, conv.ConvID.ConvIDStr())
  1018  			case <-time.After(20 * time.Second):
  1019  				require.Fail(t, "no conv received")
  1020  			}
  1021  		}
  1022  		require.Equal(t, 0, len(convs), "didn't get all convs")
  1023  
  1024  		_, err = ctc.as(t, users[0]).chatLocalHandler().GetInboxNonblockLocal(ctx,
  1025  			chat1.GetInboxNonblockLocalArg{
  1026  				IdentifyBehavior: keybase1.TLFIdentifyBehavior_CHAT_CLI,
  1027  			},
  1028  		)
  1029  		require.NoError(t, err)
  1030  
  1031  		select {
  1032  		case ibox := <-ui.InboxCb:
  1033  			require.NotNil(t, ibox.InboxRes, "nil inbox")
  1034  			require.Equal(t, numconvs, len(ibox.InboxRes.Items))
  1035  			sort.Slice(ibox.InboxRes.Items, func(i, j int) bool {
  1036  				return ibox.InboxRes.Items[i].Time.After(ibox.InboxRes.Items[j].Time)
  1037  			})
  1038  			for index, conv := range ibox.InboxRes.Items {
  1039  				snippet := "<nil>"
  1040  				if conv.LocalMetadata != nil {
  1041  					snippet = conv.LocalMetadata.Snippet
  1042  				}
  1043  				t.Logf("metadata snippet: index: %d snippet: %s time: %v", index, snippet,
  1044  					conv.Time)
  1045  			}
  1046  			index := 0
  1047  			for _, conv := range ibox.InboxRes.Items {
  1048  				require.NotNil(t, conv.LocalMetadata)
  1049  				switch mt {
  1050  				case chat1.ConversationMembersType_TEAM:
  1051  					if conv.ConvID == firstConv.Id.ConvIDStr() {
  1052  						continue
  1053  					}
  1054  					if strings.Contains(conv.LocalMetadata.Snippet, "created a new channel") {
  1055  						continue
  1056  					}
  1057  					require.Equal(t, fmt.Sprintf("%d", numconvs-index-1), conv.LocalMetadata.ChannelName)
  1058  					require.Equal(t,
  1059  						fmt.Sprintf("%s: %d", users[numconvs-index-1].Username, numconvs-index-1),
  1060  						conv.LocalMetadata.Snippet)
  1061  					require.Zero(t, len(conv.LocalMetadata.WriterNames))
  1062  				default:
  1063  					require.Equal(t, fmt.Sprintf("%d", numconvs-index), conv.LocalMetadata.Snippet)
  1064  					require.Equal(t, 2, len(conv.LocalMetadata.WriterNames))
  1065  				}
  1066  				index++
  1067  			}
  1068  		case <-time.After(20 * time.Second):
  1069  			require.Fail(t, "no inbox received")
  1070  		}
  1071  		// Get all convos
  1072  		for i := 0; i < numconvs; i++ {
  1073  			select {
  1074  			case conv := <-ui.InboxCb:
  1075  				require.NotNil(t, conv.ConvRes, "no conv")
  1076  				delete(convs, conv.ConvID.ConvIDStr())
  1077  			case <-time.After(20 * time.Second):
  1078  				require.Fail(t, "no conv received")
  1079  			}
  1080  		}
  1081  		require.Equal(t, 0, len(convs), "didnt get all convs")
  1082  	})
  1083  }
  1084  
  1085  func TestChatSrvGetInboxNonblock(t *testing.T) {
  1086  	runWithMemberTypes(t, func(mt chat1.ConversationMembersType) {
  1087  		ctc := makeChatTestContext(t, "GetInboxNonblockLocal", 6)
  1088  		defer ctc.cleanup()
  1089  		users := ctc.users()
  1090  
  1091  		numconvs := 5
  1092  		ui := kbtest.NewChatUI()
  1093  		ctc.as(t, users[0]).h.mockChatUI = ui
  1094  		tc := ctc.world.Tcs[users[0].Username]
  1095  		tc.G.UIRouter = kbtest.NewMockUIRouter(ui)
  1096  		ctx := ctc.as(t, users[0]).startCtx
  1097  		uid := gregor1.UID(users[0].GetUID().ToBytes())
  1098  		tc.G.UIRouter = kbtest.NewMockUIRouter(ui)
  1099  		tc.ChatG.UIInboxLoader = NewUIInboxLoader(tc.Context())
  1100  		tc.ChatG.UIInboxLoader.Start(ctx, uid)
  1101  		defer func() { <-tc.ChatG.UIInboxLoader.Stop(ctx) }()
  1102  
  1103  		// Create a bunch of blank convos
  1104  		convs := make(map[chat1.ConvIDStr]bool)
  1105  		for i := 0; i < numconvs; i++ {
  1106  			created := mustCreateConversationForTest(t, ctc, users[0], chat1.TopicType_CHAT,
  1107  				mt, ctc.as(t, users[i+1]).user())
  1108  			convs[created.Id.ConvIDStr()] = true
  1109  		}
  1110  
  1111  		t.Logf("blank convos test")
  1112  		// Get inbox (should be blank)
  1113  		_, err := ctc.as(t, users[0]).chatLocalHandler().GetInboxNonblockLocal(ctx,
  1114  			chat1.GetInboxNonblockLocalArg{
  1115  				IdentifyBehavior: keybase1.TLFIdentifyBehavior_CHAT_CLI,
  1116  			},
  1117  		)
  1118  		require.NoError(t, err)
  1119  		select {
  1120  		case ibox := <-ui.InboxCb:
  1121  			require.NotNil(t, ibox.InboxRes, "nil inbox")
  1122  			switch mt {
  1123  			case chat1.ConversationMembersType_TEAM:
  1124  				require.Equal(t, numconvs, len(ibox.InboxRes.Items))
  1125  			default:
  1126  				require.Zero(t, len(ibox.InboxRes.Items), "wrong size inbox")
  1127  			}
  1128  		case <-time.After(20 * time.Second):
  1129  			require.Fail(t, "no inbox received")
  1130  		}
  1131  		// Get all convos
  1132  		for i := 0; i < numconvs; i++ {
  1133  			select {
  1134  			case conv := <-ui.InboxCb:
  1135  				require.NotNil(t, conv.ConvRes, "no conv")
  1136  				delete(convs, conv.ConvID.ConvIDStr())
  1137  			case <-time.After(20 * time.Second):
  1138  				require.Fail(t, "no conv received")
  1139  			}
  1140  		}
  1141  		require.Equal(t, 0, len(convs), "didnt get all convs")
  1142  
  1143  		// Send a bunch of messages
  1144  		t.Logf("messages in convos test")
  1145  		convs = make(map[chat1.ConvIDStr]bool)
  1146  		for i := 0; i < numconvs; i++ {
  1147  			conv := mustCreateConversationForTest(t, ctc, users[0], chat1.TopicType_CHAT,
  1148  				mt, ctc.as(t, users[i+1]).user())
  1149  			convs[conv.Id.ConvIDStr()] = true
  1150  
  1151  			_, err := ctc.as(t, users[0]).chatLocalHandler().PostLocal(ctx, chat1.PostLocalArg{
  1152  				ConversationID: conv.Id,
  1153  				Msg: chat1.MessagePlaintext{
  1154  					ClientHeader: chat1.MessageClientHeader{
  1155  						Conv:        conv.Triple,
  1156  						MessageType: chat1.MessageType_TEXT,
  1157  						TlfName:     conv.TlfName,
  1158  					},
  1159  					MessageBody: chat1.NewMessageBodyWithText(chat1.MessageText{
  1160  						Body: "HI",
  1161  					}),
  1162  				},
  1163  				IdentifyBehavior: keybase1.TLFIdentifyBehavior_CHAT_CLI,
  1164  			})
  1165  			require.NoError(t, err)
  1166  		}
  1167  
  1168  		// Get inbox (should be blank)
  1169  		_, err = ctc.as(t, users[0]).chatLocalHandler().GetInboxNonblockLocal(ctx,
  1170  			chat1.GetInboxNonblockLocalArg{
  1171  				IdentifyBehavior: keybase1.TLFIdentifyBehavior_CHAT_CLI,
  1172  			},
  1173  		)
  1174  		require.NoError(t, err)
  1175  		select {
  1176  		case ibox := <-ui.InboxCb:
  1177  			require.NotNil(t, ibox.InboxRes, "nil inbox")
  1178  			require.Equal(t, len(convs), len(ibox.InboxRes.Items), "wrong size inbox")
  1179  		case <-time.After(20 * time.Second):
  1180  			require.Fail(t, "no inbox received")
  1181  		}
  1182  		// Get all convos
  1183  		for i := 0; i < numconvs; i++ {
  1184  			select {
  1185  			case conv := <-ui.InboxCb:
  1186  				require.NotNil(t, conv.ConvRes, "no conv")
  1187  				delete(convs, conv.ConvID.ConvIDStr())
  1188  			case <-time.After(20 * time.Second):
  1189  				require.Fail(t, "no conv received")
  1190  			}
  1191  		}
  1192  		require.Equal(t, 0, len(convs), "didnt get all convs")
  1193  
  1194  		// Make sure there is nothing left
  1195  		select {
  1196  		case <-ui.InboxCb:
  1197  			require.Fail(t, "should have drained channel")
  1198  		default:
  1199  		}
  1200  	})
  1201  }
  1202  
  1203  func TestChatSrvGetInboxAndUnboxLocalTlfName(t *testing.T) {
  1204  	runWithMemberTypes(t, func(mt chat1.ConversationMembersType) {
  1205  		ctc := makeChatTestContext(t, "ResolveConversationLocal", 2)
  1206  		defer ctc.cleanup()
  1207  		users := ctc.users()
  1208  
  1209  		created := mustCreateConversationForTest(t, ctc, users[0], chat1.TopicType_CHAT,
  1210  			mt, ctc.as(t, users[1]).user())
  1211  
  1212  		var name string
  1213  		switch mt {
  1214  		case chat1.ConversationMembersType_KBFS, chat1.ConversationMembersType_IMPTEAMNATIVE,
  1215  			chat1.ConversationMembersType_IMPTEAMUPGRADE:
  1216  			name = ctc.as(t, users[1]).user().Username + "," + ctc.as(t, users[0]).user().Username // not canonical
  1217  		case chat1.ConversationMembersType_TEAM:
  1218  			name = ctc.teamCache[teamKey(ctc.users())]
  1219  		}
  1220  
  1221  		visibility := keybase1.TLFVisibility_PRIVATE
  1222  		ctx := ctc.as(t, users[0]).startCtx
  1223  		gilres, err := ctc.as(t, users[0]).chatLocalHandler().GetInboxAndUnboxLocal(ctx, chat1.GetInboxAndUnboxLocalArg{
  1224  			Query: &chat1.GetInboxLocalQuery{
  1225  				Name: &chat1.NameQuery{
  1226  					Name:        name,
  1227  					MembersType: mt,
  1228  				},
  1229  				TlfVisibility: &visibility,
  1230  			},
  1231  			IdentifyBehavior: keybase1.TLFIdentifyBehavior_CHAT_CLI,
  1232  		})
  1233  		require.NoError(t, err)
  1234  		conversations := gilres.Conversations
  1235  		require.Equal(t, 1, len(conversations))
  1236  		tc := ctc.world.Tcs[users[0].Username]
  1237  		uid := users[0].User.GetUID().ToBytes()
  1238  		conv, err := utils.GetUnverifiedConv(ctx, tc.Context(), uid, created.Id,
  1239  			types.InboxSourceDataSourceRemoteOnly)
  1240  		require.NoError(t, err)
  1241  		require.Equal(t, conversations[0].Info.TlfName, conv.Conv.MaxMsgSummaries[0].TlfName)
  1242  		require.Equal(t, conversations[0].Info.Id, created.Id)
  1243  		require.Equal(t, chat1.TopicType_CHAT, conversations[0].Info.Triple.TopicType)
  1244  	})
  1245  }
  1246  
  1247  func TestChatSrvPostLocal(t *testing.T) {
  1248  	runWithMemberTypes(t, func(mt chat1.ConversationMembersType) {
  1249  		ctc := makeChatTestContext(t, "PostLocal", 2)
  1250  		defer ctc.cleanup()
  1251  		users := ctc.users()
  1252  
  1253  		created := mustCreateConversationForTest(t, ctc, users[0], chat1.TopicType_CHAT,
  1254  			mt, ctc.as(t, users[1]).user())
  1255  
  1256  		// un-canonicalize TLF name
  1257  		t.Logf("TLF name: %s", created.TlfName)
  1258  		parts := strings.Split(created.TlfName, ",")
  1259  		sort.Sort(sort.Reverse(sort.StringSlice(parts)))
  1260  		created.TlfName = strings.Join(parts, ",")
  1261  
  1262  		mustPostLocalForTest(t, ctc, users[0], created, chat1.NewMessageBodyWithText(chat1.MessageText{Body: "hello!"}))
  1263  
  1264  		// we just posted this message, so should be the first one.
  1265  		uid := users[0].User.GetUID().ToBytes()
  1266  		tc := ctc.world.Tcs[users[0].Username]
  1267  		ctx := ctc.as(t, users[0]).startCtx
  1268  		tv, err := tc.Context().ConvSource.Pull(ctx, created.Id, uid, chat1.GetThreadReason_GENERAL, nil,
  1269  			nil, nil)
  1270  		require.NoError(t, err)
  1271  		t.Logf("nmsg: %v", len(tv.Messages))
  1272  		require.NotZero(t, len(tv.Messages))
  1273  		msg := tv.Messages[0]
  1274  
  1275  		if mt == chat1.ConversationMembersType_KBFS {
  1276  			require.NotEqual(t, created.TlfName, msg.Valid().ClientHeader.TlfName)
  1277  		}
  1278  		require.NotZero(t, len(msg.Valid().ClientHeader.Sender.Bytes()))
  1279  		require.NotZero(t, len(msg.Valid().ClientHeader.SenderDevice.Bytes()))
  1280  
  1281  		t.Logf("try headline specific RPC interface")
  1282  		res, err := ctc.as(t, users[0]).chatLocalHandler().PostHeadline(ctx, chat1.PostHeadlineArg{
  1283  			ConversationID:   created.Id,
  1284  			TlfName:          created.TlfName,
  1285  			TlfPublic:        false,
  1286  			Headline:         "HI",
  1287  			IdentifyBehavior: keybase1.TLFIdentifyBehavior_CHAT_CLI,
  1288  		})
  1289  		require.NoError(t, err)
  1290  		t.Logf("headline -> msgid:%v", res.MessageID)
  1291  		tv, err = tc.Context().ConvSource.Pull(ctx, created.Id, uid, chat1.GetThreadReason_GENERAL, nil,
  1292  			nil, nil)
  1293  		require.NoError(t, err)
  1294  		t.Logf("nmsg: %v", len(tv.Messages))
  1295  		require.NotZero(t, len(tv.Messages))
  1296  		msg = tv.Messages[0]
  1297  		require.Equal(t, chat1.MessageType_HEADLINE, msg.GetMessageType())
  1298  
  1299  		t.Logf("try delete-history RPC interface")
  1300  		_, err = ctc.as(t, users[0]).chatLocalHandler().PostDeleteHistoryByAge(ctx, chat1.PostDeleteHistoryByAgeArg{
  1301  			ConversationID:   created.Id,
  1302  			TlfName:          created.TlfName,
  1303  			TlfPublic:        false,
  1304  			IdentifyBehavior: keybase1.TLFIdentifyBehavior_CHAT_CLI,
  1305  			Age:              0,
  1306  		})
  1307  		require.NoError(t, err)
  1308  		tv, err = tc.Context().ConvSource.Pull(ctx, created.Id, uid, chat1.GetThreadReason_GENERAL, nil,
  1309  			nil, nil)
  1310  		require.NoError(t, err)
  1311  		t.Logf("nmsg: %v", len(tv.Messages))
  1312  		// Teams don't use the remote mock. So PostDeleteHistoryByAge won't
  1313  		// have gotten a good answer from GetMessageBefore.
  1314  		if useRemoteMock {
  1315  			t.Logf("check that the deletable messages are gone")
  1316  			for _, m := range tv.Messages {
  1317  				require.False(t, chat1.IsDeletableByDeleteHistory(m.GetMessageType()),
  1318  					"deletable message found: %v %v", m.GetMessageID(), m.GetMessageType())
  1319  			}
  1320  		}
  1321  	})
  1322  }
  1323  
  1324  func TestChatSrvPostLocalAtMention(t *testing.T) {
  1325  	runWithMemberTypes(t, func(mt chat1.ConversationMembersType) {
  1326  		ctc := makeChatTestContext(t, "PostLocal", 2)
  1327  		defer ctc.cleanup()
  1328  		users := ctc.users()
  1329  
  1330  		switch mt {
  1331  		case chat1.ConversationMembersType_KBFS, chat1.ConversationMembersType_IMPTEAMNATIVE,
  1332  			chat1.ConversationMembersType_IMPTEAMUPGRADE:
  1333  			return
  1334  		}
  1335  
  1336  		listener := newServerChatListener()
  1337  		ctc.as(t, users[1]).h.G().NotifyRouter.AddListener(listener)
  1338  		ctx := ctc.as(t, users[0]).startCtx
  1339  
  1340  		created := mustCreateConversationForTest(t, ctc, users[0], chat1.TopicType_CHAT,
  1341  			mt, ctc.as(t, users[1]).user())
  1342  
  1343  		text := fmt.Sprintf("@%s", users[1].Username)
  1344  		mustPostLocalForTest(t, ctc, users[0], created,
  1345  			chat1.NewMessageBodyWithText(chat1.MessageText{Body: text}))
  1346  
  1347  		select {
  1348  		case info := <-listener.newMessageRemote:
  1349  			require.True(t, info.Message.IsValid())
  1350  			require.Equal(t, chat1.MessageType_TEXT, info.Message.GetMessageType())
  1351  			require.Equal(t, 1, len(info.Message.Valid().AtMentions))
  1352  			require.Equal(t, users[1].Username, info.Message.Valid().AtMentions[0])
  1353  			require.True(t, info.DisplayDesktopNotification)
  1354  			require.NotEqual(t, "", info.DesktopNotificationSnippet)
  1355  		case <-time.After(20 * time.Second):
  1356  			require.Fail(t, "no new message")
  1357  		}
  1358  
  1359  		// Test that edits work
  1360  		postRes, err := ctc.as(t, users[0]).chatLocalHandler().PostLocal(ctx, chat1.PostLocalArg{
  1361  			ConversationID: created.Id,
  1362  			Msg: chat1.MessagePlaintext{
  1363  				ClientHeader: chat1.MessageClientHeader{
  1364  					Conv:        created.Triple,
  1365  					MessageType: chat1.MessageType_TEXT,
  1366  					TlfName:     created.TlfName,
  1367  				},
  1368  				MessageBody: chat1.NewMessageBodyWithText(chat1.MessageText{
  1369  					Body: "HI",
  1370  				}),
  1371  			},
  1372  			IdentifyBehavior: keybase1.TLFIdentifyBehavior_CHAT_CLI,
  1373  		})
  1374  		require.NoError(t, err)
  1375  		consumeNewMsgRemote(t, listener, chat1.MessageType_TEXT)
  1376  		_, err = ctc.as(t, users[0]).chatLocalHandler().PostLocal(ctx, chat1.PostLocalArg{
  1377  			ConversationID: created.Id,
  1378  			Msg: chat1.MessagePlaintext{
  1379  				ClientHeader: chat1.MessageClientHeader{
  1380  					Conv:        created.Triple,
  1381  					MessageType: chat1.MessageType_EDIT,
  1382  					TlfName:     created.TlfName,
  1383  					Supersedes:  postRes.MessageID,
  1384  				},
  1385  				MessageBody: chat1.NewMessageBodyWithEdit(chat1.MessageEdit{
  1386  					MessageID: postRes.MessageID,
  1387  					Body:      fmt.Sprintf("@%s", users[1].Username),
  1388  				}),
  1389  			},
  1390  		})
  1391  		require.NoError(t, err)
  1392  		select {
  1393  		case info := <-listener.newMessageRemote:
  1394  			require.True(t, info.Message.IsValid())
  1395  			require.Equal(t, chat1.MessageType_EDIT, info.Message.GetMessageType())
  1396  			require.Equal(t, 1, len(info.Message.Valid().AtMentions))
  1397  			require.Equal(t, users[1].Username, info.Message.Valid().AtMentions[0])
  1398  		case <-time.After(20 * time.Second):
  1399  			require.Fail(t, "no new message")
  1400  		}
  1401  		threadRes, err := ctc.as(t, users[1]).chatLocalHandler().GetThreadLocal(ctx, chat1.GetThreadLocalArg{
  1402  			ConversationID: created.Id,
  1403  			Query: &chat1.GetThreadQuery{
  1404  				MessageTypes: []chat1.MessageType{chat1.MessageType_TEXT},
  1405  			},
  1406  		})
  1407  		filterOutJourneycards(&threadRes.Thread)
  1408  		require.NoError(t, err)
  1409  		require.Equal(t, 2, len(threadRes.Thread.Messages))
  1410  		require.True(t, threadRes.Thread.Messages[0].IsValid())
  1411  		require.Equal(t, 1, len(threadRes.Thread.Messages[0].Valid().AtMentionUsernames))
  1412  		require.Equal(t, users[1].Username, threadRes.Thread.Messages[0].Valid().AtMentionUsernames[0])
  1413  
  1414  		// Make sure @channel works
  1415  		mustPostLocalForTest(t, ctc, users[0], created,
  1416  			chat1.NewMessageBodyWithText(chat1.MessageText{Body: "@channel"}))
  1417  		select {
  1418  		case info := <-listener.newMessageRemote:
  1419  			require.True(t, info.Message.IsValid())
  1420  			require.Equal(t, chat1.MessageType_TEXT, info.Message.GetMessageType())
  1421  			require.Zero(t, len(info.Message.Valid().AtMentions))
  1422  			require.Equal(t, chat1.ChannelMention_ALL, info.Message.Valid().ChannelMention)
  1423  			require.True(t, info.DisplayDesktopNotification)
  1424  			require.NotEqual(t, "", info.DesktopNotificationSnippet)
  1425  		case <-time.After(20 * time.Second):
  1426  			require.Fail(t, "no new message")
  1427  		}
  1428  
  1429  		// Test that system messages do the right thing
  1430  		subBody := chat1.NewMessageSystemWithAddedtoteam(chat1.MessageSystemAddedToTeam{
  1431  			Addee: users[1].Username,
  1432  		})
  1433  		mustPostLocalForTest(t, ctc, users[0], created, chat1.NewMessageBodyWithSystem(subBody))
  1434  		select {
  1435  		case info := <-listener.newMessageRemote:
  1436  			require.True(t, info.Message.IsValid())
  1437  			require.Equal(t, chat1.MessageType_SYSTEM, info.Message.GetMessageType())
  1438  			require.Equal(t, 1, len(info.Message.Valid().AtMentions))
  1439  			require.Equal(t, users[1].Username, info.Message.Valid().AtMentions[0])
  1440  			require.Equal(t, chat1.ChannelMention_NONE, info.Message.Valid().ChannelMention)
  1441  			require.True(t, info.DisplayDesktopNotification)
  1442  			require.NotEqual(t, "", info.DesktopNotificationSnippet)
  1443  		case <-time.After(20 * time.Second):
  1444  			require.Fail(t, "no new message")
  1445  		}
  1446  
  1447  		// Test that flip messages do the right thing
  1448  		mustPostLocalForTest(t, ctc, users[0], created, chat1.NewMessageBodyWithFlip(chat1.MessageFlip{
  1449  			Text: fmt.Sprintf("/flip @%s", users[1].Username),
  1450  		}))
  1451  		select {
  1452  		case info := <-listener.newMessageRemote:
  1453  			require.True(t, info.Message.IsValid())
  1454  			require.Equal(t, chat1.MessageType_FLIP, info.Message.GetMessageType())
  1455  			require.Equal(t, 1, len(info.Message.Valid().AtMentions))
  1456  			require.Equal(t, users[1].Username, info.Message.Valid().AtMentions[0])
  1457  			require.Equal(t, chat1.ChannelMention_NONE, info.Message.Valid().ChannelMention)
  1458  			require.True(t, info.DisplayDesktopNotification)
  1459  			require.NotEqual(t, "", info.DesktopNotificationSnippet)
  1460  		case <-time.After(20 * time.Second):
  1461  			require.Fail(t, "no new message")
  1462  		}
  1463  	})
  1464  }
  1465  
  1466  func TestChatSrvPostLocalLengthLimit(t *testing.T) {
  1467  	runWithMemberTypes(t, func(mt chat1.ConversationMembersType) {
  1468  		ctc := makeChatTestContext(t, "PostLocal", 2)
  1469  		defer ctc.cleanup()
  1470  		users := ctc.users()
  1471  
  1472  		var created chat1.ConversationInfoLocal
  1473  		var dev chat1.ConversationInfoLocal
  1474  		switch mt {
  1475  		case chat1.ConversationMembersType_TEAM:
  1476  			firstConv := mustCreateConversationForTest(t, ctc, users[0], chat1.TopicType_CHAT,
  1477  				mt, ctc.as(t, users[1]).user())
  1478  			topicName := "MIKE"
  1479  			ncres, err := ctc.as(t, users[0]).chatLocalHandler().NewConversationLocal(context.TODO(),
  1480  				chat1.NewConversationLocalArg{
  1481  					TlfName:       firstConv.TlfName,
  1482  					TopicName:     &topicName,
  1483  					TopicType:     chat1.TopicType_CHAT,
  1484  					TlfVisibility: keybase1.TLFVisibility_PRIVATE,
  1485  					MembersType:   chat1.ConversationMembersType_TEAM,
  1486  				})
  1487  			require.NoError(t, err)
  1488  			created = ncres.Conv.Info
  1489  		default:
  1490  			created = mustCreateConversationForTest(t, ctc, users[0], chat1.TopicType_CHAT,
  1491  				mt, ctc.as(t, users[1]).user())
  1492  		}
  1493  		dev = mustCreateConversationForTest(t, ctc, users[0], chat1.TopicType_DEV,
  1494  			mt, ctc.as(t, users[1]).user())
  1495  
  1496  		// text msg
  1497  		maxTextBody := strings.Repeat(".", msgchecker.TextMessageMaxLength)
  1498  		_, err := postLocalForTest(t, ctc, users[0], created, chat1.NewMessageBodyWithText(chat1.MessageText{Body: maxTextBody}))
  1499  		require.NoError(t, err)
  1500  		_, err = postLocalForTest(t, ctc, users[0], created, chat1.NewMessageBodyWithText(chat1.MessageText{Body: maxTextBody + "!"}))
  1501  		require.Error(t, err)
  1502  
  1503  		// dev text
  1504  		maxDevTextBody := strings.Repeat(".", msgchecker.DevTextMessageMaxLength)
  1505  		_, err = postLocalForTest(t, ctc, users[0], dev,
  1506  			chat1.NewMessageBodyWithText(chat1.MessageText{Body: maxDevTextBody}))
  1507  		require.NoError(t, err)
  1508  		_, err = postLocalForTest(t, ctc, users[0], dev,
  1509  			chat1.NewMessageBodyWithText(chat1.MessageText{Body: maxDevTextBody + "!"}))
  1510  		require.Error(t, err)
  1511  
  1512  		// headline
  1513  		maxHeadlineBody := strings.Repeat(".", msgchecker.HeadlineMaxLength)
  1514  		_, err = postLocalForTest(t, ctc, users[0], created, chat1.NewMessageBodyWithHeadline(chat1.MessageHeadline{Headline: maxHeadlineBody}))
  1515  		require.NoError(t, err)
  1516  		_, err = postLocalForTest(t, ctc, users[0], created, chat1.NewMessageBodyWithHeadline(chat1.MessageHeadline{Headline: maxHeadlineBody + "!"}))
  1517  		require.Error(t, err)
  1518  
  1519  		// topic
  1520  		maxTopicBody := strings.Repeat("a", msgchecker.TopicMaxLength)
  1521  		_, err = postLocalForTest(t, ctc, users[0], created, chat1.NewMessageBodyWithMetadata(chat1.MessageConversationMetadata{ConversationTitle: maxTopicBody}))
  1522  		require.NoError(t, err)
  1523  		_, err = postLocalForTest(t, ctc, users[0], created, chat1.NewMessageBodyWithMetadata(chat1.MessageConversationMetadata{ConversationTitle: maxTopicBody + "a"}))
  1524  		require.Error(t, err)
  1525  		_, err = postLocalForTest(t, ctc, users[0], created, chat1.NewMessageBodyWithMetadata(chat1.MessageConversationMetadata{ConversationTitle: "#mike"}))
  1526  		require.Error(t, err)
  1527  		_, err = postLocalForTest(t, ctc, users[0], created, chat1.NewMessageBodyWithMetadata(chat1.MessageConversationMetadata{ConversationTitle: "mii.ke"}))
  1528  		require.Error(t, err)
  1529  
  1530  		// request payment
  1531  		maxPaymentNote := strings.Repeat(".", msgchecker.RequestPaymentTextMaxLength)
  1532  		_, err = postLocalForTest(t, ctc, users[0], created, chat1.NewMessageBodyWithRequestpayment(
  1533  			chat1.MessageRequestPayment{
  1534  				RequestID: stellar1.KeybaseRequestID("dummy id"),
  1535  				Note:      maxPaymentNote,
  1536  			}))
  1537  		require.NoError(t, err)
  1538  		_, err = postLocalForTest(t, ctc, users[0], created, chat1.NewMessageBodyWithRequestpayment(
  1539  			chat1.MessageRequestPayment{
  1540  				RequestID: stellar1.KeybaseRequestID("dummy id"),
  1541  				Note:      maxPaymentNote + "!",
  1542  			}))
  1543  		require.Error(t, err)
  1544  	})
  1545  }
  1546  
  1547  func TestChatSrvGetThreadLocal(t *testing.T) {
  1548  	runWithMemberTypes(t, func(mt chat1.ConversationMembersType) {
  1549  		ctc := makeChatTestContext(t, "GetThreadLocal", 2)
  1550  		defer ctc.cleanup()
  1551  		users := ctc.users()
  1552  
  1553  		created := mustCreateConversationForTest(t, ctc, users[0], chat1.TopicType_CHAT,
  1554  			mt, ctc.as(t, users[1]).user())
  1555  		msgID1, err := postLocalForTest(t, ctc, users[0], created,
  1556  			chat1.NewMessageBodyWithText(chat1.MessageText{Body: "hello!"}))
  1557  		require.NoError(t, err)
  1558  		t.Logf("msgID1: %d", msgID1.MessageID)
  1559  
  1560  		ctx := ctc.as(t, users[0]).startCtx
  1561  		tvres, err := ctc.as(t, users[0]).chatLocalHandler().GetThreadLocal(ctx, chat1.GetThreadLocalArg{
  1562  			ConversationID: created.Id,
  1563  			Query: &chat1.GetThreadQuery{
  1564  				MessageTypes: []chat1.MessageType{chat1.MessageType_TEXT},
  1565  			},
  1566  		})
  1567  		require.NoError(t, err)
  1568  
  1569  		tv := tvres.Thread
  1570  		expectedMessages := 1
  1571  		require.Len(t, tv.Messages, expectedMessages,
  1572  			"unexpected response from GetThreadLocal . number of messages")
  1573  		require.Equal(t, "hello!", tv.Messages[0].Valid().MessageBody.Text().Body)
  1574  
  1575  		// Test message ID control
  1576  		plres, err := postLocalForTest(t, ctc, users[0], created,
  1577  			chat1.NewMessageBodyWithText(chat1.MessageText{Body: "hello!"}))
  1578  		require.NoError(t, err)
  1579  		t.Logf("msgID2: %d", plres.MessageID)
  1580  		msgID3, err := postLocalForTest(t, ctc, users[0], created,
  1581  			chat1.NewMessageBodyWithText(chat1.MessageText{Body: "hello!"}))
  1582  		require.NoError(t, err)
  1583  		t.Logf("msgID3: %d", msgID3.MessageID)
  1584  		tvres, err = ctc.as(t, users[0]).chatLocalHandler().GetThreadLocal(ctx, chat1.GetThreadLocalArg{
  1585  			ConversationID: created.Id,
  1586  			Query: &chat1.GetThreadQuery{
  1587  				MessageTypes: []chat1.MessageType{chat1.MessageType_TEXT},
  1588  				MessageIDControl: &chat1.MessageIDControl{
  1589  					Pivot: &plres.MessageID,
  1590  					Mode:  chat1.MessageIDControlMode_NEWERMESSAGES,
  1591  					Num:   1,
  1592  				},
  1593  			},
  1594  		})
  1595  		require.NoError(t, err)
  1596  		require.Equal(t, 1, len(tvres.Thread.Messages))
  1597  		require.Equal(t, msgID3.MessageID, tvres.Thread.Messages[0].GetMessageID())
  1598  		tvres, err = ctc.as(t, users[0]).chatLocalHandler().GetThreadLocal(ctx, chat1.GetThreadLocalArg{
  1599  			ConversationID: created.Id,
  1600  			Query: &chat1.GetThreadQuery{
  1601  				MessageTypes: []chat1.MessageType{chat1.MessageType_TEXT},
  1602  				MessageIDControl: &chat1.MessageIDControl{
  1603  					Pivot: &plres.MessageID,
  1604  					Mode:  chat1.MessageIDControlMode_OLDERMESSAGES,
  1605  					Num:   1,
  1606  				},
  1607  			},
  1608  		})
  1609  		require.NoError(t, err)
  1610  		require.Equal(t, 1, len(tvres.Thread.Messages))
  1611  		require.Equal(t, msgID1.MessageID, tvres.Thread.Messages[0].GetMessageID())
  1612  
  1613  		tvres, err = ctc.as(t, users[0]).chatLocalHandler().GetThreadLocal(ctx, chat1.GetThreadLocalArg{
  1614  			ConversationID: created.Id,
  1615  			Query: &chat1.GetThreadQuery{
  1616  				MessageTypes: []chat1.MessageType{chat1.MessageType_TEXT},
  1617  				MessageIDControl: &chat1.MessageIDControl{
  1618  					Num: 2,
  1619  				},
  1620  			},
  1621  		})
  1622  		require.NoError(t, err)
  1623  		require.Equal(t, 2, len(tvres.Thread.Messages))
  1624  		require.Equal(t, msgID3.MessageID, tvres.Thread.Messages[0].GetMessageID())
  1625  		require.Equal(t, plres.MessageID, tvres.Thread.Messages[1].GetMessageID())
  1626  	})
  1627  }
  1628  
  1629  func TestChatSrvGetThreadLocalMarkAsRead(t *testing.T) {
  1630  	runWithMemberTypes(t, func(mt chat1.ConversationMembersType) {
  1631  		// TODO: investigate LocalDb in TestContext and make it behave the same way
  1632  		// as in real context / docker tests. This test should fail without the fix
  1633  		// in ConvSource for marking is read, but does not currently.
  1634  		ctc := makeChatTestContext(t, "GetThreadLocalMarkAsRead", 2)
  1635  		defer ctc.cleanup()
  1636  		users := ctc.users()
  1637  
  1638  		withUser1 := mustCreateConversationForTest(t, ctc, users[0], chat1.TopicType_CHAT,
  1639  			mt, ctc.as(t, users[1]).user())
  1640  
  1641  		mustPostLocalForTest(t, ctc, users[0], withUser1, chat1.NewMessageBodyWithText(chat1.MessageText{Body: "hello0"}))
  1642  		mustPostLocalForTest(t, ctc, users[1], withUser1, chat1.NewMessageBodyWithText(chat1.MessageText{Body: "hello1"}))
  1643  		mustPostLocalForTest(t, ctc, users[0], withUser1, chat1.NewMessageBodyWithText(chat1.MessageText{Body: "hello2"}))
  1644  
  1645  		ctx := ctc.as(t, users[0]).startCtx
  1646  		res, err := ctc.as(t, users[0]).chatLocalHandler().GetInboxSummaryForCLILocal(ctx, chat1.GetInboxSummaryForCLILocalQuery{
  1647  			TopicType: chat1.TopicType_CHAT,
  1648  		})
  1649  		require.NoError(t, err)
  1650  		require.Equal(t, 1, len(res.Conversations))
  1651  		require.Equal(t, res.Conversations[0].Info.Id.String(), withUser1.Id.String())
  1652  		var found bool
  1653  		for _, m := range res.Conversations[0].MaxMessages {
  1654  			if m.GetMessageType() == chat1.MessageType_TEXT {
  1655  				require.NotEqual(t, res.Conversations[0].ReaderInfo.ReadMsgid, m.GetMessageID())
  1656  				found = true
  1657  				break
  1658  			}
  1659  		}
  1660  		require.True(t, found)
  1661  
  1662  		// Do a get thread local without requesting marking as read first. This
  1663  		// should cause HybridConversationSource to cache the thread. Then we do
  1664  		// another call requesting marking as read before checking if the thread is
  1665  		// marked as read. This is to ensure that when the query requests for a
  1666  		// mark-as-read, and the thread gets a cache hit, the
  1667  		// HybridConversationSource should not just return the thread, but also send
  1668  		// a MarkAsRead RPC to remote. (Currently this is done in
  1669  		// HybridConversationSource.Pull)
  1670  		//
  1671  		// TODO: This doesn't make sense! In integration tests, this isn't necessary
  1672  		// since a Pull() is called during PostLocal (when populating the Prev
  1673  		// pointers).  However it seems in this test, it doesn't do so. This first
  1674  		// GetThreadLocal always gets a cache miss, resulting a remote call. If
  1675  		// PostLocal had worked like integration, this shouldn't be necessary. We
  1676  		// should find out where the problem is and fix it! Although after that fix,
  1677  		// this should probably still stay here just in case.
  1678  		_, err = ctc.as(t, users[0]).chatLocalHandler().GetThreadLocal(ctx, chat1.GetThreadLocalArg{
  1679  			ConversationID: withUser1.Id,
  1680  			Query: &chat1.GetThreadQuery{
  1681  				MarkAsRead: false,
  1682  			},
  1683  		})
  1684  		require.NoError(t, err)
  1685  		tv, err := ctc.as(t, users[0]).chatLocalHandler().GetThreadLocal(ctx, chat1.GetThreadLocalArg{
  1686  			ConversationID: withUser1.Id,
  1687  			Query: &chat1.GetThreadQuery{
  1688  				MarkAsRead: true,
  1689  			},
  1690  		})
  1691  		require.NoError(t, err)
  1692  
  1693  		expectedMessages := 4 // 3 messages and 1 TLF
  1694  		require.Len(t, tv.Thread.Messages, expectedMessages,
  1695  			"unexpected response from GetThreadLocal . number of messages")
  1696  
  1697  		res, err = ctc.as(t, users[0]).chatLocalHandler().GetInboxSummaryForCLILocal(ctx, chat1.GetInboxSummaryForCLILocalQuery{
  1698  			TopicType: chat1.TopicType_CHAT,
  1699  		})
  1700  		require.NoError(t, err)
  1701  		require.Equal(t, 1, len(res.Conversations))
  1702  		found = false
  1703  		for _, m := range res.Conversations[0].MaxMessages {
  1704  			if m.GetMessageType() == chat1.MessageType_TEXT {
  1705  				require.Equal(t, res.Conversations[0].ReaderInfo.ReadMsgid,
  1706  					m.GetMessageID())
  1707  				found = true
  1708  				break
  1709  			}
  1710  		}
  1711  		require.True(t, found)
  1712  	})
  1713  }
  1714  
  1715  type messageSabotagerRemote struct {
  1716  	chat1.RemoteInterface
  1717  }
  1718  
  1719  func (m messageSabotagerRemote) GetThreadRemote(ctx context.Context, arg chat1.GetThreadRemoteArg) (chat1.GetThreadRemoteRes, error) {
  1720  	res, err := m.RemoteInterface.GetThreadRemote(ctx, arg)
  1721  	if err != nil {
  1722  		return res, err
  1723  	}
  1724  	if len(res.Thread.Messages) > 0 {
  1725  		res.Thread.Messages[0].BodyCiphertext.E[0] += 50
  1726  	}
  1727  	return res, nil
  1728  }
  1729  
  1730  func TestChatSrvGracefulUnboxing(t *testing.T) {
  1731  	runWithMemberTypes(t, func(mt chat1.ConversationMembersType) {
  1732  		ctc := makeChatTestContext(t, "GracefulUnboxing", 2)
  1733  		defer ctc.cleanup()
  1734  		users := ctc.users()
  1735  
  1736  		listener := newServerChatListener()
  1737  		ctc.as(t, users[0]).h.G().NotifyRouter.AddListener(listener)
  1738  
  1739  		created := mustCreateConversationForTest(t, ctc, users[0], chat1.TopicType_CHAT,
  1740  			mt, ctc.as(t, users[1]).user())
  1741  		mustPostLocalForTest(t, ctc, users[0], created, chat1.NewMessageBodyWithText(chat1.MessageText{Body: "innocent hello"}))
  1742  		mustPostLocalForTest(t, ctc, users[0], created, chat1.NewMessageBodyWithText(chat1.MessageText{Body: "evil hello"}))
  1743  
  1744  		consumeNewMsgRemote(t, listener, chat1.MessageType_TEXT)
  1745  		consumeNewMsgRemote(t, listener, chat1.MessageType_TEXT)
  1746  
  1747  		// make evil hello evil
  1748  		tc := ctc.world.Tcs[users[0].Username]
  1749  		uid := users[0].User.GetUID().ToBytes()
  1750  		require.NoError(t, tc.Context().ConvSource.Clear(context.TODO(), created.Id, uid, nil))
  1751  
  1752  		ri := ctc.as(t, users[0]).ri
  1753  		sabRemote := messageSabotagerRemote{RemoteInterface: ri}
  1754  		tc.Context().UIThreadLoader.(*UIThreadLoader).SetRemoteInterface(func() chat1.RemoteInterface {
  1755  			return sabRemote
  1756  		})
  1757  		ctx := ctc.as(t, users[0]).startCtx
  1758  		tv, err := ctc.as(t, users[0]).chatLocalHandler().GetThreadLocal(ctx, chat1.GetThreadLocalArg{
  1759  			ConversationID: created.Id,
  1760  		})
  1761  		tc.Context().UIThreadLoader.(*UIThreadLoader).SetRemoteInterface(func() chat1.RemoteInterface {
  1762  			return ri
  1763  		})
  1764  		if err != nil {
  1765  			t.Fatalf("GetThreadLocal error: %v", err)
  1766  		}
  1767  
  1768  		require.Len(t, tv.Thread.Messages, 3,
  1769  			"unexpected response from GetThreadLocal . number of messages")
  1770  
  1771  		if tv.Thread.Messages[0].IsValid() || len(tv.Thread.Messages[0].Error().ErrMsg) == 0 {
  1772  			t.Fatalf("unexpected response from GetThreadLocal. expected an error message from bad msg, got %#+v\n", tv.Thread.Messages[0])
  1773  		}
  1774  		if !tv.Thread.Messages[1].IsValid() || tv.Thread.Messages[1].Valid().MessageBody.Text().Body != "innocent hello" {
  1775  			t.Fatalf("unexpected response from GetThreadLocal. expected 'innocent hello' got %#+v\n", tv.Thread.Messages[1].Valid())
  1776  		}
  1777  	})
  1778  }
  1779  
  1780  func TestChatSrvGetInboxSummaryForCLILocal(t *testing.T) {
  1781  	runWithMemberTypes(t, func(mt chat1.ConversationMembersType) {
  1782  		ctc := makeChatTestContext(t, "GetInboxSummaryForCLILocal", 4)
  1783  		defer ctc.cleanup()
  1784  		users := ctc.users()
  1785  
  1786  		withUser1 := mustCreateConversationForTest(t, ctc, users[0], chat1.TopicType_CHAT,
  1787  			mt, ctc.as(t, users[1]).user())
  1788  		mustPostLocalForTest(t, ctc, users[0], withUser1, chat1.NewMessageBodyWithText(chat1.MessageText{Body: "hello0"}))
  1789  		mustPostLocalForTest(t, ctc, users[1], withUser1, chat1.NewMessageBodyWithText(chat1.MessageText{Body: "hello1"}))
  1790  
  1791  		withUser2 := mustCreateConversationForTest(t, ctc, users[0], chat1.TopicType_CHAT,
  1792  			mt, ctc.as(t, users[2]).user())
  1793  		mustPostLocalForTest(t, ctc, users[0], withUser2, chat1.NewMessageBodyWithText(chat1.MessageText{Body: fmt.Sprintf("Dude I just said hello to %s!", ctc.as(t, users[2]).user().Username)}))
  1794  
  1795  		withUser3 := mustCreateConversationForTest(t, ctc, users[0], chat1.TopicType_CHAT,
  1796  			mt, ctc.as(t, users[3]).user())
  1797  		mustPostLocalForTest(t, ctc, users[0], withUser3, chat1.NewMessageBodyWithText(chat1.MessageText{Body: "O_O"}))
  1798  
  1799  		withUser12 := mustCreateConversationForTest(t, ctc, users[0], chat1.TopicType_CHAT,
  1800  			mt, ctc.as(t, users[1]).user(), ctc.as(t, users[2]).user())
  1801  		mustPostLocalForTest(t, ctc, users[0], withUser12, chat1.NewMessageBodyWithText(chat1.MessageText{Body: "O_O"}))
  1802  
  1803  		withUser123 := mustCreateConversationForTest(t, ctc, users[0], chat1.TopicType_CHAT,
  1804  			mt, ctc.as(t, users[1]).user(), ctc.as(t, users[2]).user(), ctc.as(t, users[3]).user())
  1805  		mustPostLocalForTest(t, ctc, users[0], withUser123, chat1.NewMessageBodyWithText(chat1.MessageText{Body: "O_O"}))
  1806  
  1807  		ctx := ctc.as(t, users[0]).startCtx
  1808  		res, err := ctc.as(t, users[0]).chatLocalHandler().GetInboxSummaryForCLILocal(ctx, chat1.GetInboxSummaryForCLILocalQuery{
  1809  			After:     "1d",
  1810  			TopicType: chat1.TopicType_CHAT,
  1811  		})
  1812  		if err != nil {
  1813  			t.Fatalf("GetInboxSummaryForCLILocal error: %v", err)
  1814  		}
  1815  		if len(res.Conversations) != 5 {
  1816  			t.Fatalf("unexpected response from GetInboxSummaryForCLILocal . expected 3 items, got %d\n", len(res.Conversations))
  1817  		}
  1818  		if !res.Conversations[0].Info.Id.Eq(withUser123.Id) {
  1819  			t.Fatalf("unexpected response from GetInboxSummaryForCLILocal; newest updated conversation is not the first in response.\n")
  1820  		}
  1821  		// TODO: fix this when merging master back in... (what?)
  1822  		expectedMessages := 2
  1823  		require.Len(t, res.Conversations[0].MaxMessages, expectedMessages,
  1824  			"unexpected response from GetInboxSummaryForCLILocal . number of messages in the first conversation")
  1825  
  1826  		res, err = ctc.as(t, users[0]).chatLocalHandler().GetInboxSummaryForCLILocal(ctx, chat1.GetInboxSummaryForCLILocalQuery{
  1827  			ActivitySortedLimit: 2,
  1828  			TopicType:           chat1.TopicType_CHAT,
  1829  		})
  1830  		if err != nil {
  1831  			t.Fatalf("GetInboxSummaryForCLILocal error: %v", err)
  1832  		}
  1833  		if len(res.Conversations) != 2 {
  1834  			t.Fatalf("unexpected response from GetInboxSummaryForCLILocal . expected 2 items, got %d\n", len(res.Conversations))
  1835  		}
  1836  
  1837  		res, err = ctc.as(t, users[0]).chatLocalHandler().GetInboxSummaryForCLILocal(ctx, chat1.GetInboxSummaryForCLILocalQuery{
  1838  			ActivitySortedLimit: 2,
  1839  			TopicType:           chat1.TopicType_CHAT,
  1840  		})
  1841  		if err != nil {
  1842  			t.Fatalf("GetInboxSummaryForCLILocal error: %v", err)
  1843  		}
  1844  		if len(res.Conversations) != 2 {
  1845  			t.Fatalf("unexpected response from GetInboxSummaryForCLILocal . expected 2 items, got %d\n", len(res.Conversations))
  1846  		}
  1847  
  1848  		res, err = ctc.as(t, users[0]).chatLocalHandler().GetInboxSummaryForCLILocal(ctx,
  1849  			chat1.GetInboxSummaryForCLILocalQuery{
  1850  				UnreadFirst: true,
  1851  				UnreadFirstLimit: chat1.UnreadFirstNumLimit{
  1852  					AtLeast: 0,
  1853  					AtMost:  1000,
  1854  					NumRead: 1,
  1855  				},
  1856  				TopicType: chat1.TopicType_CHAT,
  1857  			})
  1858  		if err != nil {
  1859  			t.Fatalf("GetInboxSummaryForCLILocal error: %v", err)
  1860  		}
  1861  		if len(res.Conversations) != 2 {
  1862  			t.Fatalf("unexpected response from GetInboxSummaryForCLILocal . expected 2 items, got %d\n", len(res.Conversations))
  1863  		}
  1864  		if !res.Conversations[0].Info.Id.Eq(withUser1.Id) {
  1865  			t.Fatalf("unexpected response from GetInboxSummaryForCLILocal; unread conversation is not the first in response.\n")
  1866  		}
  1867  
  1868  		res, err = ctc.as(t, users[0]).chatLocalHandler().GetInboxSummaryForCLILocal(ctx, chat1.GetInboxSummaryForCLILocalQuery{
  1869  			UnreadFirst: true,
  1870  			UnreadFirstLimit: chat1.UnreadFirstNumLimit{
  1871  				AtLeast: 0,
  1872  				AtMost:  2,
  1873  				NumRead: 5,
  1874  			},
  1875  			TopicType: chat1.TopicType_CHAT,
  1876  		})
  1877  		if err != nil {
  1878  			t.Fatalf("GetInboxSummaryForCLILocal error: %v", err)
  1879  		}
  1880  		if len(res.Conversations) != 2 {
  1881  			t.Fatalf("unexpected response from GetInboxSummaryForCLILocal . expected 1 item, got %d\n", len(res.Conversations))
  1882  		}
  1883  
  1884  		res, err = ctc.as(t, users[0]).chatLocalHandler().GetInboxSummaryForCLILocal(ctx, chat1.GetInboxSummaryForCLILocalQuery{
  1885  			UnreadFirst: true,
  1886  			UnreadFirstLimit: chat1.UnreadFirstNumLimit{
  1887  				AtLeast: 3,
  1888  				AtMost:  100,
  1889  				NumRead: 0,
  1890  			},
  1891  			TopicType: chat1.TopicType_CHAT,
  1892  		})
  1893  		if err != nil {
  1894  			t.Fatalf("GetInboxSummaryForCLILocal error: %v", err)
  1895  		}
  1896  		if len(res.Conversations) != 3 {
  1897  			t.Fatalf("unexpected response from GetInboxSummaryForCLILocal . expected 1 item, got %d\n", len(res.Conversations))
  1898  		}
  1899  	})
  1900  }
  1901  
  1902  func TestChatSrvGetMessagesLocal(t *testing.T) {
  1903  	runWithMemberTypes(t, func(mt chat1.ConversationMembersType) {
  1904  		ctc := makeChatTestContext(t, "GetMessagesLocal", 2)
  1905  		defer ctc.cleanup()
  1906  		users := ctc.users()
  1907  
  1908  		created := mustCreateConversationForTest(t, ctc, users[0], chat1.TopicType_CHAT,
  1909  			mt, ctc.as(t, users[1]).user())
  1910  		mustPostLocalForTest(t, ctc, users[0], created, chat1.NewMessageBodyWithText(chat1.MessageText{Body: "Sometimes you eat the bar"}))
  1911  		mustPostLocalForTest(t, ctc, users[1], created, chat1.NewMessageBodyWithText(chat1.MessageText{Body: "and sometimes"}))
  1912  		mustPostLocalForTest(t, ctc, users[0], created, chat1.NewMessageBodyWithText(chat1.MessageText{Body: "the bar eats you."}))
  1913  
  1914  		// GetMessagesLocal currently seems to return messages descending ID order.
  1915  		// It would probably be good if this changed to return either in req order or ascending.
  1916  		getIDs := []chat1.MessageID{3, 2, 1}
  1917  
  1918  		ctx := ctc.as(t, users[0]).startCtx
  1919  		res, err := ctc.as(t, users[0]).chatLocalHandler().GetMessagesLocal(ctx, chat1.GetMessagesLocalArg{
  1920  			ConversationID: created.Id,
  1921  			MessageIDs:     getIDs,
  1922  		})
  1923  		if err != nil {
  1924  			t.Fatalf("GetMessagesLocal error: %v", err)
  1925  		}
  1926  		for i, msg := range res.Messages {
  1927  			if !msg.IsValid() {
  1928  				t.Fatalf("Missing message: %v", getIDs[i])
  1929  			}
  1930  			msgID := msg.GetMessageID()
  1931  			if msgID != getIDs[i] {
  1932  				t.Fatalf("Wrong message ID: got %v but expected %v", msgID, getIDs[i])
  1933  			}
  1934  		}
  1935  		if len(res.Messages) != len(getIDs) {
  1936  			t.Fatalf("GetMessagesLocal got %v items but expected %v", len(res.Messages), len(getIDs))
  1937  		}
  1938  	})
  1939  }
  1940  
  1941  func extractOutbox(t *testing.T, msgs []chat1.MessageUnboxed) []chat1.MessageUnboxed {
  1942  	var routbox []chat1.MessageUnboxed
  1943  	for _, msg := range msgs {
  1944  		typ, err := msg.State()
  1945  		require.NoError(t, err)
  1946  		if typ == chat1.MessageUnboxedState_OUTBOX {
  1947  			routbox = append(routbox, msg)
  1948  		}
  1949  	}
  1950  	return routbox
  1951  }
  1952  
  1953  func TestChatSrvGetOutbox(t *testing.T) {
  1954  	runWithMemberTypes(t, func(mt chat1.ConversationMembersType) {
  1955  		ctc := makeChatTestContext(t, "GetOutbox", 3)
  1956  		defer ctc.cleanup()
  1957  		users := ctc.users()
  1958  
  1959  		var err error
  1960  		created := mustCreateConversationForTest(t, ctc, users[0], chat1.TopicType_CHAT,
  1961  			mt, ctc.as(t, users[1]).user())
  1962  		created2 := mustCreateConversationForTest(t, ctc, users[0], chat1.TopicType_CHAT,
  1963  			mt, ctc.as(t, users[2]).user())
  1964  
  1965  		u := users[0]
  1966  		h := ctc.as(t, users[0]).h
  1967  		ctx := ctc.as(t, users[0]).startCtx
  1968  		tc := ctc.world.Tcs[ctc.as(t, users[0]).user().Username]
  1969  		outbox := storage.NewOutbox(tc.Context(), users[0].User.GetUID().ToBytes())
  1970  
  1971  		obr, err := outbox.PushMessage(ctx, created.Id, chat1.MessagePlaintext{
  1972  			MessageBody: chat1.NewMessageBodyWithText(chat1.MessageText{}),
  1973  			ClientHeader: chat1.MessageClientHeader{
  1974  				Sender:    u.User.GetUID().ToBytes(),
  1975  				TlfName:   u.Username,
  1976  				TlfPublic: false,
  1977  				OutboxInfo: &chat1.OutboxInfo{
  1978  					Prev: 10,
  1979  				},
  1980  			},
  1981  		}, nil, nil, nil, keybase1.TLFIdentifyBehavior_CHAT_CLI)
  1982  		require.NoError(t, err)
  1983  
  1984  		// only badgeable messages are added to the thread
  1985  		_, err = outbox.PushMessage(ctx, created.Id, chat1.MessagePlaintext{
  1986  			ClientHeader: chat1.MessageClientHeader{
  1987  				Sender:    u.User.GetUID().ToBytes(),
  1988  				TlfName:   u.Username,
  1989  				TlfPublic: false,
  1990  				OutboxInfo: &chat1.OutboxInfo{
  1991  					Prev: 10,
  1992  				},
  1993  			},
  1994  		}, nil, nil, nil, keybase1.TLFIdentifyBehavior_CHAT_CLI)
  1995  		require.NoError(t, err)
  1996  
  1997  		thread, err := h.GetThreadLocal(ctx, chat1.GetThreadLocalArg{
  1998  			ConversationID: created.Id,
  1999  		})
  2000  		require.NoError(t, err)
  2001  
  2002  		routbox := extractOutbox(t, thread.Thread.Messages)
  2003  		require.Equal(t, 1, len(routbox), "wrong size outbox")
  2004  		require.Equal(t, obr.OutboxID, routbox[0].Outbox().OutboxID, "wrong outbox ID")
  2005  
  2006  		thread, err = h.GetThreadLocal(ctx, chat1.GetThreadLocalArg{
  2007  			ConversationID: created2.Id,
  2008  		})
  2009  		require.NoError(t, err)
  2010  		routbox = extractOutbox(t, thread.Thread.Messages)
  2011  		require.Equal(t, 0, len(routbox), "non empty outbox")
  2012  	})
  2013  }
  2014  
  2015  func TestChatSrvGap(t *testing.T) {
  2016  	runWithMemberTypes(t, func(mt chat1.ConversationMembersType) {
  2017  		ctc := makeChatTestContext(t, "GetOutbox", 2)
  2018  		defer ctc.cleanup()
  2019  		users := ctc.users()
  2020  
  2021  		var err error
  2022  		created := mustCreateConversationForTest(t, ctc, users[0], chat1.TopicType_CHAT,
  2023  			mt, ctc.as(t, users[1]).user())
  2024  
  2025  		res, err := postLocalForTest(t, ctc, users[0], created, chat1.NewMessageBodyWithText(chat1.MessageText{Body: "Sometimes you eat the bar"}))
  2026  		require.NoError(t, err)
  2027  
  2028  		u := users[0]
  2029  		h := ctc.as(t, users[0]).h
  2030  		ctx := ctc.as(t, users[0]).startCtx
  2031  		tc := ctc.world.Tcs[ctc.as(t, users[0]).user().Username]
  2032  		msgID := res.MessageID
  2033  		mres, err := h.remoteClient().GetMessagesRemote(ctx, chat1.GetMessagesRemoteArg{
  2034  			ConversationID: created.Id,
  2035  			MessageIDs:     []chat1.MessageID{msgID},
  2036  		})
  2037  		require.NoError(t, err)
  2038  
  2039  		require.Len(t, mres.Msgs, 1, "number of messages")
  2040  
  2041  		ooMsg := mres.Msgs[0]
  2042  		ooMsg.ServerHeader.MessageID = 5
  2043  
  2044  		payload := chat1.NewMessagePayload{
  2045  			Action:  types.ActionNewMessage,
  2046  			ConvID:  created.Id,
  2047  			Message: ooMsg,
  2048  		}
  2049  
  2050  		listener := newServerChatListener()
  2051  		tc.G.NotifyRouter.AddListener(listener)
  2052  
  2053  		mh := codec.MsgpackHandle{WriteExt: true}
  2054  		var data []byte
  2055  		enc := codec.NewEncoderBytes(&data, &mh)
  2056  		require.NoError(t, enc.Encode(payload))
  2057  		ph := NewPushHandler(tc.Context())
  2058  		ph.Start(context.TODO(), nil)
  2059  		defer func() { <-ph.Stop(context.TODO()) }()
  2060  		require.NoError(t, ph.Activity(ctx, &gregor1.OutOfBandMessage{
  2061  			Uid_:    u.User.GetUID().ToBytes(),
  2062  			System_: gregor1.System(types.PushActivity),
  2063  			Body_:   data,
  2064  		}))
  2065  
  2066  		updates := consumeNewThreadsStale(t, listener)
  2067  		require.Equal(t, 1, len(updates))
  2068  		require.Equal(t, created.Id, updates[0].ConvID, "wrong cid")
  2069  		require.Equal(t, chat1.StaleUpdateType_CLEAR, updates[0].UpdateType)
  2070  
  2071  		ooMsg.ServerHeader.MessageID = 6
  2072  		payload = chat1.NewMessagePayload{
  2073  			Action:  types.ActionNewMessage,
  2074  			ConvID:  created.Id,
  2075  			Message: ooMsg,
  2076  		}
  2077  		enc = codec.NewEncoderBytes(&data, &mh)
  2078  		require.NoError(t, enc.Encode(payload))
  2079  		require.NoError(t, ph.Activity(ctx, &gregor1.OutOfBandMessage{
  2080  			Uid_:    u.User.GetUID().ToBytes(),
  2081  			System_: gregor1.System(types.PushActivity),
  2082  			Body_:   data,
  2083  		}))
  2084  
  2085  		select {
  2086  		case <-listener.threadsStale:
  2087  			require.Fail(t, "should not get stale event here")
  2088  		default:
  2089  		}
  2090  	})
  2091  }
  2092  
  2093  type resolveRes struct {
  2094  	convID chat1.ConversationID
  2095  	info   chat1.ConversationResolveInfo
  2096  }
  2097  
  2098  type serverChatListener struct {
  2099  	libkb.NoopNotifyListener
  2100  
  2101  	// ChatActivity channels
  2102  	newMessageLocal         chan chat1.IncomingMessage
  2103  	newMessageRemote        chan chat1.IncomingMessage
  2104  	newConversation         chan chat1.NewConversationInfo
  2105  	membersUpdate           chan chat1.MembersUpdateInfo
  2106  	appNotificationSettings chan chat1.SetAppNotificationSettingsInfo
  2107  	teamType                chan chat1.TeamTypeInfo
  2108  	expunge                 chan chat1.ExpungeInfo
  2109  	ephemeralPurge          chan chat1.EphemeralPurgeNotifInfo
  2110  	reactionUpdate          chan chat1.ReactionUpdateNotif
  2111  	messagesUpdated         chan chat1.MessagesUpdated
  2112  	readMessage             chan chat1.ReadMessageInfo
  2113  
  2114  	threadsStale     chan []chat1.ConversationStaleUpdate
  2115  	convUpdate       chan chat1.ConversationID
  2116  	inboxStale       chan struct{}
  2117  	joinedConv       chan *chat1.InboxUIItem
  2118  	leftConv         chan chat1.ConversationID
  2119  	resetConv        chan chat1.ConversationID
  2120  	identifyUpdate   chan keybase1.CanonicalTLFNameAndIDWithBreaks
  2121  	inboxSynced      chan chat1.ChatSyncResult
  2122  	setConvRetention chan chat1.ConversationID
  2123  	setTeamRetention chan keybase1.TeamID
  2124  	setConvSettings  chan chat1.ConversationID
  2125  	kbfsUpgrade      chan chat1.ConversationID
  2126  	resolveConv      chan resolveRes
  2127  	subteamRename    chan []chat1.ConversationID
  2128  	unfurlPrompt     chan chat1.MessageID
  2129  	welcomeMessage   chan keybase1.TeamID
  2130  	setStatus        chan chat1.SetStatusInfo
  2131  	teamChangedByID  chan keybase1.TeamChangedByIDArg
  2132  }
  2133  
  2134  var _ libkb.NotifyListener = (*serverChatListener)(nil)
  2135  
  2136  func (n *serverChatListener) ChatIdentifyUpdate(update keybase1.CanonicalTLFNameAndIDWithBreaks) {
  2137  	n.identifyUpdate <- update
  2138  }
  2139  func (n *serverChatListener) ChatInboxStale(uid keybase1.UID) {
  2140  	n.inboxStale <- struct{}{}
  2141  }
  2142  func (n *serverChatListener) ChatConvUpdate(uid keybase1.UID, convID chat1.ConversationID) {
  2143  	n.convUpdate <- convID
  2144  }
  2145  func (n *serverChatListener) ChatThreadsStale(uid keybase1.UID, cids []chat1.ConversationStaleUpdate) {
  2146  	n.threadsStale <- cids
  2147  }
  2148  func (n *serverChatListener) ChatInboxSynced(uid keybase1.UID, topicType chat1.TopicType,
  2149  	syncRes chat1.ChatSyncResult) {
  2150  	switch topicType {
  2151  	case chat1.TopicType_CHAT, chat1.TopicType_NONE:
  2152  		n.inboxSynced <- syncRes
  2153  	}
  2154  }
  2155  func (n *serverChatListener) NewChatActivity(uid keybase1.UID, activity chat1.ChatActivity,
  2156  	source chat1.ChatActivitySource) {
  2157  	typ, _ := activity.ActivityType()
  2158  	switch typ {
  2159  	case chat1.ChatActivityType_INCOMING_MESSAGE:
  2160  		switch source {
  2161  		case chat1.ChatActivitySource_LOCAL:
  2162  			n.newMessageLocal <- activity.IncomingMessage()
  2163  		case chat1.ChatActivitySource_REMOTE:
  2164  			n.newMessageRemote <- activity.IncomingMessage()
  2165  		}
  2166  	case chat1.ChatActivityType_READ_MESSAGE:
  2167  		n.readMessage <- activity.ReadMessage()
  2168  	case chat1.ChatActivityType_NEW_CONVERSATION:
  2169  		n.newConversation <- activity.NewConversation()
  2170  	case chat1.ChatActivityType_MEMBERS_UPDATE:
  2171  		n.membersUpdate <- activity.MembersUpdate()
  2172  	case chat1.ChatActivityType_SET_APP_NOTIFICATION_SETTINGS:
  2173  		n.appNotificationSettings <- activity.SetAppNotificationSettings()
  2174  	case chat1.ChatActivityType_TEAMTYPE:
  2175  		n.teamType <- activity.Teamtype()
  2176  	case chat1.ChatActivityType_EXPUNGE:
  2177  		n.expunge <- activity.Expunge()
  2178  	case chat1.ChatActivityType_EPHEMERAL_PURGE:
  2179  		n.ephemeralPurge <- activity.EphemeralPurge()
  2180  	case chat1.ChatActivityType_REACTION_UPDATE:
  2181  		n.reactionUpdate <- activity.ReactionUpdate()
  2182  	case chat1.ChatActivityType_MESSAGES_UPDATED:
  2183  		n.messagesUpdated <- activity.MessagesUpdated()
  2184  	case chat1.ChatActivityType_SET_STATUS:
  2185  		n.setStatus <- activity.SetStatus()
  2186  	}
  2187  }
  2188  func (n *serverChatListener) ChatJoinedConversation(uid keybase1.UID, convID chat1.ConversationID,
  2189  	conv *chat1.InboxUIItem) {
  2190  	n.joinedConv <- conv
  2191  }
  2192  func (n *serverChatListener) ChatLeftConversation(uid keybase1.UID, convID chat1.ConversationID) {
  2193  	n.leftConv <- convID
  2194  }
  2195  func (n *serverChatListener) ChatResetConversation(uid keybase1.UID, convID chat1.ConversationID) {
  2196  	n.resetConv <- convID
  2197  }
  2198  func (n *serverChatListener) ChatTLFResolve(uid keybase1.UID, convID chat1.ConversationID,
  2199  	info chat1.ConversationResolveInfo) {
  2200  	n.resolveConv <- resolveRes{
  2201  		convID: convID,
  2202  		info:   info,
  2203  	}
  2204  }
  2205  func (n *serverChatListener) ChatSetConvRetention(uid keybase1.UID, convID chat1.ConversationID) {
  2206  	n.setConvRetention <- convID
  2207  }
  2208  func (n *serverChatListener) ChatSetTeamRetention(uid keybase1.UID, teamID keybase1.TeamID) {
  2209  	n.setTeamRetention <- teamID
  2210  }
  2211  func (n *serverChatListener) ChatSetConvSettings(uid keybase1.UID, convID chat1.ConversationID) {
  2212  	n.setConvSettings <- convID
  2213  }
  2214  func (n *serverChatListener) ChatKBFSToImpteamUpgrade(uid keybase1.UID, convID chat1.ConversationID) {
  2215  	n.kbfsUpgrade <- convID
  2216  }
  2217  func (n *serverChatListener) ChatSubteamRename(uid keybase1.UID, convIDs []chat1.ConversationID) {
  2218  	n.subteamRename <- convIDs
  2219  }
  2220  func (n *serverChatListener) ChatPromptUnfurl(uid keybase1.UID, convID chat1.ConversationID,
  2221  	msgID chat1.MessageID, domain string) {
  2222  	n.unfurlPrompt <- msgID
  2223  }
  2224  func (n *serverChatListener) ChatWelcomeMessageLoaded(teamID keybase1.TeamID,
  2225  	_ chat1.WelcomeMessageDisplay) {
  2226  	n.welcomeMessage <- teamID
  2227  }
  2228  func (n *serverChatListener) TeamChangedByID(teamID keybase1.TeamID, latestSeqno keybase1.Seqno, implicitTeam bool,
  2229  	changes keybase1.TeamChangeSet, latestHiddenSeqno keybase1.Seqno, source keybase1.TeamChangedSource) {
  2230  
  2231  	n.teamChangedByID <- keybase1.TeamChangedByIDArg{
  2232  		TeamID:            teamID,
  2233  		LatestSeqno:       latestSeqno,
  2234  		ImplicitTeam:      implicitTeam,
  2235  		Changes:           changes,
  2236  		LatestHiddenSeqno: latestHiddenSeqno,
  2237  	}
  2238  }
  2239  func newServerChatListener() *serverChatListener {
  2240  	buf := 100
  2241  	return &serverChatListener{
  2242  		newMessageLocal:         make(chan chat1.IncomingMessage, buf),
  2243  		newMessageRemote:        make(chan chat1.IncomingMessage, buf),
  2244  		newConversation:         make(chan chat1.NewConversationInfo, buf),
  2245  		readMessage:             make(chan chat1.ReadMessageInfo, buf),
  2246  		membersUpdate:           make(chan chat1.MembersUpdateInfo, buf),
  2247  		appNotificationSettings: make(chan chat1.SetAppNotificationSettingsInfo, buf),
  2248  		teamType:                make(chan chat1.TeamTypeInfo, buf),
  2249  		expunge:                 make(chan chat1.ExpungeInfo, buf),
  2250  		ephemeralPurge:          make(chan chat1.EphemeralPurgeNotifInfo, buf),
  2251  		reactionUpdate:          make(chan chat1.ReactionUpdateNotif, buf),
  2252  		messagesUpdated:         make(chan chat1.MessagesUpdated, buf),
  2253  
  2254  		threadsStale:     make(chan []chat1.ConversationStaleUpdate, buf),
  2255  		convUpdate:       make(chan chat1.ConversationID, buf),
  2256  		inboxStale:       make(chan struct{}, buf),
  2257  		joinedConv:       make(chan *chat1.InboxUIItem, buf),
  2258  		leftConv:         make(chan chat1.ConversationID, buf),
  2259  		resetConv:        make(chan chat1.ConversationID, buf),
  2260  		identifyUpdate:   make(chan keybase1.CanonicalTLFNameAndIDWithBreaks, buf),
  2261  		inboxSynced:      make(chan chat1.ChatSyncResult, buf),
  2262  		setConvRetention: make(chan chat1.ConversationID, buf),
  2263  		setTeamRetention: make(chan keybase1.TeamID, buf),
  2264  		setConvSettings:  make(chan chat1.ConversationID, buf),
  2265  		kbfsUpgrade:      make(chan chat1.ConversationID, buf),
  2266  		resolveConv:      make(chan resolveRes, buf),
  2267  		subteamRename:    make(chan []chat1.ConversationID, buf),
  2268  		unfurlPrompt:     make(chan chat1.MessageID, buf),
  2269  		setStatus:        make(chan chat1.SetStatusInfo, buf),
  2270  		teamChangedByID:  make(chan keybase1.TeamChangedByIDArg, buf),
  2271  	}
  2272  }
  2273  
  2274  func TestChatSrvPostLocalNonblock(t *testing.T) {
  2275  	runWithMemberTypes(t, func(mt chat1.ConversationMembersType) {
  2276  		runWithEphemeral(t, mt, func(ephemeralLifetime *gregor1.DurationSec) {
  2277  			ctc := makeChatTestContext(t, "PostLocalNonblock", 3)
  2278  			defer ctc.cleanup()
  2279  			users := ctc.users()
  2280  
  2281  			ui := kbtest.NewChatUI()
  2282  			ctc.as(t, users[0]).h.mockChatUI = ui
  2283  			tc := ctc.as(t, users[0])
  2284  			listener := newServerChatListener()
  2285  			ctc.as(t, users[0]).h.G().NotifyRouter.AddListener(listener)
  2286  			ctc.world.Tcs[users[0].Username].ChatG.Syncer.(*Syncer).isConnected = true
  2287  			if ephemeralLifetime != nil {
  2288  				tc.m.G().GetUPAKLoader().ClearMemory()
  2289  			}
  2290  
  2291  			assertEphemeral := func(ephemeralLifetime *gregor1.DurationSec, unboxed chat1.UIMessage) {
  2292  				valid := unboxed.Valid()
  2293  				require.False(t, valid.IsEphemeralExpired)
  2294  				require.Nil(t, valid.ExplodedBy)
  2295  				require.False(t, valid.MessageBody.IsNil())
  2296  				if ephemeralLifetime == nil {
  2297  					require.False(t, valid.IsEphemeral)
  2298  					require.EqualValues(t, valid.Etime, 0)
  2299  				} else {
  2300  					require.True(t, valid.IsEphemeral)
  2301  					lifetime := ephemeralLifetime.ToDuration()
  2302  					require.True(t, time.Now().Add(lifetime).Sub(valid.Etime.Time()) <= lifetime)
  2303  				}
  2304  			}
  2305  
  2306  			assertNotEphemeral := func(ephemeralLifetime *gregor1.DurationSec, unboxed chat1.UIMessage) {
  2307  				valid := unboxed.Valid()
  2308  				require.False(t, valid.IsEphemeralExpired)
  2309  				require.False(t, valid.IsEphemeral)
  2310  				require.EqualValues(t, valid.Etime, 0)
  2311  				require.Nil(t, valid.ExplodedBy)
  2312  				require.False(t, valid.MessageBody.IsNil())
  2313  			}
  2314  
  2315  			assertReactionUpdate := func(convID chat1.ConversationID, targetMsgID chat1.MessageID, reactionMap chat1.ReactionMap) {
  2316  				info := consumeReactionUpdate(t, listener)
  2317  				require.Equal(t, convID, info.ConvID)
  2318  				require.Len(t, info.ReactionUpdates, 1)
  2319  				reactionUpdate := info.ReactionUpdates[0]
  2320  				require.Equal(t, targetMsgID, reactionUpdate.TargetMsgID)
  2321  				for _, reactions := range reactionUpdate.Reactions.Reactions {
  2322  					for k, r := range reactions.Users {
  2323  						require.NotZero(t, r.Ctime)
  2324  						r.Ctime = 0
  2325  						reactions.Users[k] = r
  2326  					}
  2327  				}
  2328  				var refMap chat1.UIReactionMap
  2329  				refMap.Reactions = make(map[string]chat1.UIReactionDesc)
  2330  				for emoji, users := range reactionMap.Reactions {
  2331  					refMap.Reactions[emoji] = chat1.UIReactionDesc{
  2332  						Users: make(map[string]chat1.Reaction),
  2333  					}
  2334  					for username, reaction := range users {
  2335  						refMap.Reactions[emoji].Users[username] = reaction
  2336  					}
  2337  				}
  2338  				require.Equal(t, refMap, reactionUpdate.Reactions)
  2339  			}
  2340  
  2341  			var err error
  2342  			var created, fwd chat1.ConversationInfoLocal
  2343  			switch mt {
  2344  			case chat1.ConversationMembersType_TEAM:
  2345  				first := mustCreateConversationForTest(t, ctc, users[0], chat1.TopicType_CHAT,
  2346  					mt, ctc.as(t, users[1]).user())
  2347  				consumeNewConversation(t, listener, first.Id)
  2348  				topicName := "mike"
  2349  				ncres, err := ctc.as(t, users[0]).chatLocalHandler().NewConversationLocal(tc.startCtx,
  2350  					chat1.NewConversationLocalArg{
  2351  						TlfName:       first.TlfName,
  2352  						TopicName:     &topicName,
  2353  						TopicType:     chat1.TopicType_CHAT,
  2354  						TlfVisibility: keybase1.TLFVisibility_PRIVATE,
  2355  						MembersType:   chat1.ConversationMembersType_TEAM,
  2356  					})
  2357  				require.NoError(t, err)
  2358  				created = ncres.Conv.Info
  2359  				consumeNewConversation(t, listener, created.Id)
  2360  				consumeNewMsgLocal(t, listener, chat1.MessageType_JOIN)
  2361  				consumeNewMsgRemote(t, listener, chat1.MessageType_JOIN)
  2362  				consumeNewPendingMsg(t, listener)
  2363  				consumeNewMsgLocal(t, listener, chat1.MessageType_SYSTEM)
  2364  				consumeNewMsgRemote(t, listener, chat1.MessageType_SYSTEM)
  2365  				consumeNewMsgLocal(t, listener, chat1.MessageType_SYSTEM)
  2366  				consumeNewMsgRemote(t, listener, chat1.MessageType_SYSTEM)
  2367  
  2368  				topicName = "fwd"
  2369  				ncres, err = ctc.as(t, users[0]).chatLocalHandler().NewConversationLocal(tc.startCtx,
  2370  					chat1.NewConversationLocalArg{
  2371  						TlfName:       first.TlfName,
  2372  						TopicName:     &topicName,
  2373  						TopicType:     chat1.TopicType_CHAT,
  2374  						TlfVisibility: keybase1.TLFVisibility_PRIVATE,
  2375  						MembersType:   chat1.ConversationMembersType_TEAM,
  2376  					})
  2377  				require.NoError(t, err)
  2378  				fwd = ncres.Conv.Info
  2379  				consumeNewConversation(t, listener, fwd.Id)
  2380  				consumeNewMsgLocal(t, listener, chat1.MessageType_JOIN)
  2381  				consumeNewMsgRemote(t, listener, chat1.MessageType_JOIN)
  2382  				consumeNewMsgLocal(t, listener, chat1.MessageType_SYSTEM)
  2383  				consumeNewMsgRemote(t, listener, chat1.MessageType_SYSTEM)
  2384  			default:
  2385  				created = mustCreateConversationForTest(t, ctc, users[0], chat1.TopicType_CHAT,
  2386  					mt, ctc.as(t, users[1]).user())
  2387  				fwd = mustCreateConversationForTest(t, ctc, users[0], chat1.TopicType_CHAT,
  2388  					mt, ctc.as(t, users[2]).user())
  2389  			}
  2390  
  2391  			t.Logf("send a text message")
  2392  			arg := chat1.PostTextNonblockArg{
  2393  				ConversationID:    created.Id,
  2394  				TlfName:           created.TlfName,
  2395  				TlfPublic:         created.Visibility == keybase1.TLFVisibility_PUBLIC,
  2396  				Body:              "hi",
  2397  				IdentifyBehavior:  keybase1.TLFIdentifyBehavior_CHAT_CLI,
  2398  				EphemeralLifetime: ephemeralLifetime,
  2399  			}
  2400  			res, err := ctc.as(t, users[0]).chatLocalHandler().PostTextNonblock(tc.startCtx, arg)
  2401  			require.NoError(t, err)
  2402  			consumeNewPendingMsg(t, listener)
  2403  			var unboxed chat1.UIMessage
  2404  			select {
  2405  			case info := <-listener.newMessageRemote:
  2406  				unboxed = info.Message
  2407  				require.True(t, unboxed.IsValid(), "invalid message")
  2408  				require.NotNil(t, unboxed.Valid().OutboxID, "no outbox ID")
  2409  				require.Equal(t, chat1.MessageType_TEXT, unboxed.GetMessageType(), "invalid type")
  2410  				require.Equal(t, res.OutboxID.String(), *unboxed.Valid().OutboxID, "mismatch outbox ID")
  2411  				assertEphemeral(ephemeralLifetime, unboxed)
  2412  			case <-time.After(20 * time.Second):
  2413  				require.Fail(t, "no event received")
  2414  			}
  2415  			consumeNewMsgLocal(t, listener, chat1.MessageType_TEXT)
  2416  
  2417  			t.Logf("fwd a text message")
  2418  			arg1 := chat1.ForwardMessageNonblockArg{
  2419  				SrcConvID: created.Id,
  2420  				DstConvID: fwd.Id,
  2421  				MsgID:     unboxed.GetMessageID(),
  2422  			}
  2423  			res, err = ctc.as(t, users[0]).chatLocalHandler().ForwardMessageNonblock(tc.startCtx, arg1)
  2424  			require.NoError(t, err)
  2425  			consumeNewPendingMsg(t, listener)
  2426  			var fwdedMsg chat1.UIMessage
  2427  			select {
  2428  			case info := <-listener.newMessageRemote:
  2429  				fwdedMsg = info.Message
  2430  				require.True(t, fwdedMsg.IsValid(), "invalid message")
  2431  				require.NotNil(t, fwdedMsg.Valid().OutboxID, "no outbox ID")
  2432  				require.Equal(t, chat1.MessageType_TEXT, fwdedMsg.GetMessageType(), "invalid type")
  2433  				require.Equal(t, res.OutboxID.String(), *fwdedMsg.Valid().OutboxID, "mismatch outbox ID")
  2434  				assertEphemeral(ephemeralLifetime, fwdedMsg)
  2435  			case <-time.After(20 * time.Second):
  2436  				require.Fail(t, "no event received")
  2437  			}
  2438  			consumeNewMsgLocal(t, listener, chat1.MessageType_TEXT)
  2439  
  2440  			t.Logf("post text message with prefetched outbox ID")
  2441  			genOutboxID, err := ctc.as(t, users[0]).chatLocalHandler().GenerateOutboxID(context.TODO())
  2442  			require.NoError(t, err)
  2443  			arg = chat1.PostTextNonblockArg{
  2444  				ConversationID:    created.Id,
  2445  				TlfName:           created.TlfName,
  2446  				TlfPublic:         created.Visibility == keybase1.TLFVisibility_PUBLIC,
  2447  				Body:              "hi",
  2448  				OutboxID:          &genOutboxID,
  2449  				IdentifyBehavior:  keybase1.TLFIdentifyBehavior_CHAT_CLI,
  2450  				EphemeralLifetime: ephemeralLifetime,
  2451  			}
  2452  			res, err = ctc.as(t, users[0]).chatLocalHandler().PostTextNonblock(tc.startCtx, arg)
  2453  			require.NoError(t, err)
  2454  			consumeNewMsgLocal(t, listener, chat1.MessageType_TEXT) // pending message
  2455  
  2456  			select {
  2457  			case info := <-listener.newMessageRemote:
  2458  				unboxed = info.Message
  2459  				require.True(t, unboxed.IsValid(), "invalid message")
  2460  				require.NotNil(t, unboxed.Valid().OutboxID, "no outbox ID")
  2461  				require.Equal(t, genOutboxID.String(), *unboxed.Valid().OutboxID, "mismatch outbox ID")
  2462  				require.Equal(t, res.OutboxID.String(), *unboxed.Valid().OutboxID, "mismatch outbox ID")
  2463  				require.Equal(t, chat1.MessageType_TEXT, unboxed.GetMessageType(), "invalid type")
  2464  				assertEphemeral(ephemeralLifetime, unboxed)
  2465  			case <-time.After(20 * time.Second):
  2466  				require.Fail(t, "no event received")
  2467  			}
  2468  			consumeNewMsgLocal(t, listener, chat1.MessageType_TEXT)
  2469  
  2470  			textUnboxed := unboxed
  2471  
  2472  			t.Logf("react to the message")
  2473  			// An ephemeralLifetime is added if we are reacting to an ephemeral message
  2474  			reactionKey := ":+1:"
  2475  			rarg := chat1.PostReactionNonblockArg{
  2476  				ConversationID:   created.Id,
  2477  				TlfName:          created.TlfName,
  2478  				TlfPublic:        created.Visibility == keybase1.TLFVisibility_PUBLIC,
  2479  				Supersedes:       textUnboxed.GetMessageID(),
  2480  				Body:             reactionKey,
  2481  				IdentifyBehavior: keybase1.TLFIdentifyBehavior_CHAT_CLI,
  2482  			}
  2483  			res, err = ctc.as(t, users[0]).chatLocalHandler().PostReactionNonblock(tc.startCtx, rarg)
  2484  			require.NoError(t, err)
  2485  			consumeNewPendingMsg(t, listener)
  2486  			select {
  2487  			case info := <-listener.newMessageRemote:
  2488  				unboxed = info.Message
  2489  				require.True(t, unboxed.IsValid(), "invalid message")
  2490  				require.NotNil(t, unboxed.Valid().OutboxID, "no outbox ID")
  2491  				require.Equal(t, res.OutboxID.String(), *unboxed.Valid().OutboxID, "mismatch outbox ID")
  2492  				require.Equal(t, chat1.MessageType_REACTION, unboxed.GetMessageType(), "invalid type")
  2493  				assertEphemeral(ephemeralLifetime, unboxed)
  2494  			case <-time.After(20 * time.Second):
  2495  				require.Fail(t, "no event received")
  2496  			}
  2497  			consumeNewMsgLocal(t, listener, chat1.MessageType_REACTION)
  2498  			reactionUnboxed := unboxed
  2499  			expectedReactionMap := chat1.ReactionMap{
  2500  				Reactions: map[string]map[string]chat1.Reaction{
  2501  					":+1:": {
  2502  						users[0].Username: {
  2503  							ReactionMsgID: reactionUnboxed.GetMessageID(),
  2504  						},
  2505  					},
  2506  				},
  2507  			}
  2508  			assertReactionUpdate(created.Id, textUnboxed.GetMessageID(), expectedReactionMap)
  2509  
  2510  			t.Logf("edit the message")
  2511  			// An ephemeralLifetime is added if we are editing an ephemeral message
  2512  			targetMsgID := textUnboxed.GetMessageID()
  2513  			earg := chat1.PostEditNonblockArg{
  2514  				ConversationID: created.Id,
  2515  				TlfName:        created.TlfName,
  2516  				TlfPublic:      created.Visibility == keybase1.TLFVisibility_PUBLIC,
  2517  				Target: chat1.EditTarget{
  2518  					MessageID: &targetMsgID,
  2519  				},
  2520  				Body:             "hi2",
  2521  				IdentifyBehavior: keybase1.TLFIdentifyBehavior_CHAT_CLI,
  2522  			}
  2523  			res, err = ctc.as(t, users[0]).chatLocalHandler().PostEditNonblock(tc.startCtx, earg)
  2524  			require.NoError(t, err)
  2525  			consumeNewPendingMsg(t, listener)
  2526  			select {
  2527  			case info := <-listener.newMessageRemote:
  2528  				unboxed = info.Message
  2529  				require.True(t, unboxed.IsValid(), "invalid message")
  2530  				require.NotNil(t, unboxed.Valid().OutboxID, "no outbox ID")
  2531  				require.Equal(t, res.OutboxID.String(), *unboxed.Valid().OutboxID, "mismatch outbox ID")
  2532  				require.Equal(t, chat1.MessageType_EDIT, unboxed.GetMessageType(), "invalid type")
  2533  				assertEphemeral(ephemeralLifetime, unboxed)
  2534  			case <-time.After(20 * time.Second):
  2535  				require.Fail(t, "no event received")
  2536  			}
  2537  			consumeNewMsgLocal(t, listener, chat1.MessageType_EDIT)
  2538  
  2539  			// Repost a reaction and ensure it is deleted
  2540  			t.Logf("repost reaction = delete reaction")
  2541  			res, err = ctc.as(t, users[0]).chatLocalHandler().PostReactionNonblock(tc.startCtx, rarg)
  2542  			require.NoError(t, err)
  2543  			consumeNewPendingMsg(t, listener)
  2544  			select {
  2545  			case info := <-listener.newMessageRemote:
  2546  				unboxed = info.Message
  2547  				require.True(t, unboxed.IsValid(), "invalid message")
  2548  				require.NotNil(t, unboxed.Valid().OutboxID, "no outbox ID")
  2549  				require.Equal(t, res.OutboxID.String(), *unboxed.Valid().OutboxID, "mismatch outbox ID")
  2550  				require.Equal(t, chat1.MessageType_DELETE, unboxed.GetMessageType(), "invalid type")
  2551  				assertNotEphemeral(ephemeralLifetime, unboxed)
  2552  			case <-time.After(20 * time.Second):
  2553  				require.Fail(t, "no event received")
  2554  			}
  2555  			consumeNewMsgLocal(t, listener, chat1.MessageType_DELETE)
  2556  			assertReactionUpdate(created.Id, textUnboxed.GetMessageID(), chat1.ReactionMap{})
  2557  
  2558  			t.Logf("delete the message")
  2559  			darg := chat1.PostDeleteNonblockArg{
  2560  				ConversationID:   created.Id,
  2561  				TlfName:          created.TlfName,
  2562  				TlfPublic:        created.Visibility == keybase1.TLFVisibility_PUBLIC,
  2563  				Supersedes:       textUnboxed.GetMessageID(),
  2564  				IdentifyBehavior: keybase1.TLFIdentifyBehavior_CHAT_CLI,
  2565  			}
  2566  			res, err = ctc.as(t, users[0]).chatLocalHandler().PostDeleteNonblock(tc.startCtx, darg)
  2567  			require.NoError(t, err)
  2568  			consumeNewPendingMsg(t, listener)
  2569  			select {
  2570  			case info := <-listener.newMessageRemote:
  2571  				unboxed = info.Message
  2572  				require.True(t, unboxed.IsValid(), "invalid message")
  2573  				require.NotNil(t, unboxed.Valid().OutboxID, "no outbox ID")
  2574  				require.Equal(t, res.OutboxID.String(), *unboxed.Valid().OutboxID, "mismatch outbox ID")
  2575  				require.Equal(t, chat1.MessageType_DELETE, unboxed.GetMessageType(), "invalid type")
  2576  				assertNotEphemeral(ephemeralLifetime, unboxed)
  2577  			case <-time.After(20 * time.Second):
  2578  				require.Fail(t, "no event received")
  2579  			}
  2580  			consumeNewMsgLocal(t, listener, chat1.MessageType_DELETE)
  2581  
  2582  			t.Logf("post headline")
  2583  			headline := "SILENCE!"
  2584  			harg := chat1.PostHeadlineNonblockArg{
  2585  				ConversationID:   created.Id,
  2586  				TlfName:          created.TlfName,
  2587  				TlfPublic:        created.Visibility == keybase1.TLFVisibility_PUBLIC,
  2588  				IdentifyBehavior: keybase1.TLFIdentifyBehavior_CHAT_CLI,
  2589  				Headline:         headline,
  2590  			}
  2591  			res, err = ctc.as(t, users[0]).chatLocalHandler().PostHeadlineNonblock(tc.startCtx, harg)
  2592  			require.NoError(t, err)
  2593  			consumeNewPendingMsg(t, listener)
  2594  			select {
  2595  			case info := <-listener.newMessageRemote:
  2596  				unboxed = info.Message
  2597  				require.True(t, unboxed.IsValid(), "invalid message")
  2598  				require.NotNil(t, unboxed.Valid().OutboxID, "no outbox ID")
  2599  				require.Equal(t, res.OutboxID.String(), *unboxed.Valid().OutboxID, "mismatch outbox ID")
  2600  				require.Equal(t, chat1.MessageType_HEADLINE, unboxed.GetMessageType(), "invalid type")
  2601  				switch mt {
  2602  				case chat1.ConversationMembersType_TEAM:
  2603  					require.Equal(t, headline, unboxed.Valid().MessageBody.Headline().Headline)
  2604  				default:
  2605  					// Nothing to do for other member types.
  2606  				}
  2607  				assertNotEphemeral(ephemeralLifetime, unboxed)
  2608  			case <-time.After(20 * time.Second):
  2609  				require.Fail(t, "no event received")
  2610  			}
  2611  			consumeNewMsgLocal(t, listener, chat1.MessageType_HEADLINE)
  2612  
  2613  			t.Logf("change name")
  2614  			topicName := "NEWNAME"
  2615  			marg := chat1.PostMetadataNonblockArg{
  2616  				ConversationID:   created.Id,
  2617  				TlfName:          created.TlfName,
  2618  				TlfPublic:        created.Visibility == keybase1.TLFVisibility_PUBLIC,
  2619  				IdentifyBehavior: keybase1.TLFIdentifyBehavior_CHAT_CLI,
  2620  				ChannelName:      topicName,
  2621  			}
  2622  			res, err = ctc.as(t, users[0]).chatLocalHandler().PostMetadataNonblock(tc.startCtx, marg)
  2623  			require.NoError(t, err)
  2624  			consumeNewPendingMsg(t, listener)
  2625  			select {
  2626  			case info := <-listener.newMessageRemote:
  2627  				unboxed = info.Message
  2628  				require.True(t, unboxed.IsValid(), "invalid message")
  2629  				require.NotNil(t, unboxed.Valid().OutboxID, "no outbox ID")
  2630  				require.Equal(t, res.OutboxID.String(), *unboxed.Valid().OutboxID, "mismatch outbox ID")
  2631  				require.Equal(t, chat1.MessageType_METADATA, unboxed.GetMessageType(), "invalid type")
  2632  				switch mt {
  2633  				case chat1.ConversationMembersType_TEAM:
  2634  					require.Equal(t, topicName, unboxed.Valid().MessageBody.Metadata().ConversationTitle)
  2635  				default:
  2636  					// Nothing to do for other member types.
  2637  				}
  2638  				assertNotEphemeral(ephemeralLifetime, unboxed)
  2639  			case <-time.After(20 * time.Second):
  2640  				require.Fail(t, "no event received")
  2641  			}
  2642  			consumeNewMsgLocal(t, listener, chat1.MessageType_METADATA)
  2643  		})
  2644  	})
  2645  }
  2646  
  2647  func filterOutboxMessages(tv chat1.ThreadView) (res []chat1.MessageUnboxed) {
  2648  	for _, m := range tv.Messages {
  2649  		if !m.IsOutbox() {
  2650  			res = append(res, m)
  2651  		}
  2652  	}
  2653  	return res
  2654  }
  2655  
  2656  func TestChatSrvPostEditNonblock(t *testing.T) {
  2657  	runWithMemberTypes(t, func(mt chat1.ConversationMembersType) {
  2658  		switch mt {
  2659  		case chat1.ConversationMembersType_KBFS:
  2660  			return
  2661  		default:
  2662  			// Fall through for other member types.
  2663  		}
  2664  		ctc := makeChatTestContext(t, "TestChatSrvPostEditNonblock", 1)
  2665  		defer ctc.cleanup()
  2666  		users := ctc.users()
  2667  
  2668  		listener := newServerChatListener()
  2669  		ctc.as(t, users[0]).h.G().NotifyRouter.AddListener(listener)
  2670  		tc := ctc.world.Tcs[users[0].Username]
  2671  		tc.ChatG.Syncer.(*Syncer).isConnected = true
  2672  		ctx := ctc.as(t, users[0]).startCtx
  2673  		conv := mustCreateConversationForTest(t, ctc, users[0], chat1.TopicType_CHAT, mt)
  2674  		t.Logf("test convID: %x", conv.Id.DbShortForm())
  2675  		checkMessage := func(intended string, num int) {
  2676  			res, err := ctc.as(t, users[0]).chatLocalHandler().GetThreadLocal(ctx, chat1.GetThreadLocalArg{
  2677  				ConversationID: conv.Id,
  2678  				Query: &chat1.GetThreadQuery{
  2679  					MessageTypes: []chat1.MessageType{chat1.MessageType_TEXT},
  2680  				},
  2681  			})
  2682  			require.NoError(t, err)
  2683  			thread := filterOutboxMessages(res.Thread)
  2684  			require.Equal(t, num, len(thread))
  2685  			require.True(t, thread[0].IsValid())
  2686  			require.Equal(t, intended, thread[0].Valid().MessageBody.Text().Body)
  2687  		}
  2688  
  2689  		outboxID, err := storage.NewOutboxID()
  2690  		require.NoError(t, err)
  2691  		postRes, err := ctc.as(t, users[0]).chatLocalHandler().PostLocal(ctx, chat1.PostLocalArg{
  2692  			ConversationID: conv.Id,
  2693  			Msg: chat1.MessagePlaintext{
  2694  				ClientHeader: chat1.MessageClientHeader{
  2695  					Conv:        conv.Triple,
  2696  					MessageType: chat1.MessageType_TEXT,
  2697  					TlfName:     conv.TlfName,
  2698  					OutboxID:    &outboxID,
  2699  				},
  2700  				MessageBody: chat1.NewMessageBodyWithText(chat1.MessageText{
  2701  					Body: "hi",
  2702  				}),
  2703  			},
  2704  		})
  2705  		require.NoError(t, err)
  2706  		consumeNewMsgLocal(t, listener, chat1.MessageType_TEXT)
  2707  		consumeNewMsgRemote(t, listener, chat1.MessageType_TEXT)
  2708  		_, err = ctc.as(t, users[0]).chatLocalHandler().PostEditNonblock(ctx, chat1.PostEditNonblockArg{
  2709  			ConversationID: conv.Id,
  2710  			TlfName:        conv.TlfName,
  2711  			Target: chat1.EditTarget{
  2712  				MessageID: &postRes.MessageID,
  2713  			},
  2714  			Body: "hi!",
  2715  		})
  2716  		require.NoError(t, err)
  2717  		consumeNewMsgLocal(t, listener, chat1.MessageType_EDIT)
  2718  		consumeNewMsgRemote(t, listener, chat1.MessageType_EDIT)
  2719  		checkMessage("hi!", 1)
  2720  
  2721  		_, err = ctc.as(t, users[0]).chatLocalHandler().PostEditNonblock(ctx, chat1.PostEditNonblockArg{
  2722  			ConversationID: conv.Id,
  2723  			TlfName:        conv.TlfName,
  2724  			Target: chat1.EditTarget{
  2725  				OutboxID: &outboxID,
  2726  			},
  2727  			Body: "hi!!",
  2728  		})
  2729  		require.NoError(t, err)
  2730  		consumeNewMsgLocal(t, listener, chat1.MessageType_EDIT)
  2731  		consumeNewMsgRemote(t, listener, chat1.MessageType_EDIT)
  2732  		checkMessage("hi!!", 1)
  2733  	})
  2734  }
  2735  
  2736  func TestChatSrvFindConversations(t *testing.T) {
  2737  	runWithMemberTypes(t, func(mt chat1.ConversationMembersType) {
  2738  		switch mt {
  2739  		case chat1.ConversationMembersType_TEAM, chat1.ConversationMembersType_KBFS:
  2740  			return
  2741  		}
  2742  
  2743  		ctc := makeChatTestContext(t, "FindConversations", 3)
  2744  		defer ctc.cleanup()
  2745  		users := ctc.users()
  2746  
  2747  		t.Logf("basic test")
  2748  		created := mustCreatePublicConversationForTest(t, ctc, users[2], chat1.TopicType_CHAT,
  2749  			mt, users[1])
  2750  		t.Logf("created public conversation: %+v", created)
  2751  		if useRemoteMock {
  2752  			convRemote := ctc.world.GetConversationByID(created.Id)
  2753  			require.NotNil(t, convRemote)
  2754  			convRemote.Metadata.Visibility = keybase1.TLFVisibility_PUBLIC
  2755  			convRemote.Metadata.ActiveList =
  2756  				[]gregor1.UID{users[2].User.GetUID().ToBytes(), users[1].User.GetUID().ToBytes()}
  2757  		}
  2758  
  2759  		ctx := ctc.as(t, users[0]).startCtx
  2760  		ctx2 := ctc.as(t, users[2]).startCtx
  2761  		res, err := ctc.as(t, users[0]).chatLocalHandler().FindConversationsLocal(ctx,
  2762  			chat1.FindConversationsLocalArg{
  2763  				TlfName:          created.TlfName,
  2764  				MembersType:      mt,
  2765  				Visibility:       keybase1.TLFVisibility_PUBLIC,
  2766  				TopicType:        chat1.TopicType_CHAT,
  2767  				IdentifyBehavior: keybase1.TLFIdentifyBehavior_CHAT_CLI,
  2768  			})
  2769  		require.NoError(t, err)
  2770  		require.Equal(t, 1, len(res.Conversations), "no conv found for %v", mt)
  2771  		require.Equal(t, created.Id, res.Conversations[0].GetConvID(), "wrong conv")
  2772  
  2773  		t.Logf("simple post")
  2774  		_, err = ctc.as(t, users[2]).chatLocalHandler().PostLocal(ctx2, chat1.PostLocalArg{
  2775  			ConversationID:   created.Id,
  2776  			IdentifyBehavior: keybase1.TLFIdentifyBehavior_CHAT_CLI,
  2777  			Msg: chat1.MessagePlaintext{
  2778  				ClientHeader: chat1.MessageClientHeader{
  2779  					Conv:        created.Triple,
  2780  					MessageType: chat1.MessageType_TEXT,
  2781  					TlfName:     created.TlfName,
  2782  					TlfPublic:   true,
  2783  				},
  2784  				MessageBody: chat1.NewMessageBodyWithText(chat1.MessageText{
  2785  					Body: "PUBLIC",
  2786  				}),
  2787  			},
  2788  		})
  2789  		require.NoError(t, err)
  2790  
  2791  		t.Logf("read from conversation")
  2792  		tres, err := ctc.as(t, users[0]).chatLocalHandler().GetThreadLocal(ctx, chat1.GetThreadLocalArg{
  2793  			ConversationID:   res.Conversations[0].GetConvID(),
  2794  			IdentifyBehavior: keybase1.TLFIdentifyBehavior_CHAT_CLI,
  2795  			Query: &chat1.GetThreadQuery{
  2796  				MessageTypes: []chat1.MessageType{chat1.MessageType_TEXT},
  2797  			},
  2798  		})
  2799  		require.NoError(t, err)
  2800  		require.Equal(t, 1, len(tres.Thread.Messages), "wrong length")
  2801  
  2802  		t.Logf("test topic name")
  2803  		_, err = ctc.as(t, users[2]).chatLocalHandler().PostLocal(ctx2, chat1.PostLocalArg{
  2804  			ConversationID:   created.Id,
  2805  			IdentifyBehavior: keybase1.TLFIdentifyBehavior_CHAT_CLI,
  2806  			Msg: chat1.MessagePlaintext{
  2807  				ClientHeader: chat1.MessageClientHeader{
  2808  					Conv:        created.Triple,
  2809  					MessageType: chat1.MessageType_METADATA,
  2810  					TlfName:     created.TlfName,
  2811  					TlfPublic:   true,
  2812  				},
  2813  				MessageBody: chat1.NewMessageBodyWithMetadata(chat1.MessageConversationMetadata{
  2814  					ConversationTitle: "MIKE",
  2815  				}),
  2816  			},
  2817  		})
  2818  		require.NoError(t, err)
  2819  
  2820  		res, err = ctc.as(t, users[0]).chatLocalHandler().FindConversationsLocal(ctx,
  2821  			chat1.FindConversationsLocalArg{
  2822  				TlfName:          created.TlfName,
  2823  				MembersType:      mt,
  2824  				Visibility:       keybase1.TLFVisibility_PUBLIC,
  2825  				TopicType:        chat1.TopicType_CHAT,
  2826  				IdentifyBehavior: keybase1.TLFIdentifyBehavior_CHAT_CLI,
  2827  			})
  2828  		require.NoError(t, err)
  2829  		require.Equal(t, 0, len(res.Conversations), "conv found")
  2830  
  2831  		res, err = ctc.as(t, users[0]).chatLocalHandler().FindConversationsLocal(ctx,
  2832  			chat1.FindConversationsLocalArg{
  2833  				TlfName:          created.TlfName,
  2834  				MembersType:      mt,
  2835  				Visibility:       keybase1.TLFVisibility_PUBLIC,
  2836  				TopicType:        chat1.TopicType_CHAT,
  2837  				IdentifyBehavior: keybase1.TLFIdentifyBehavior_CHAT_CLI,
  2838  				TopicName:        "MIKE",
  2839  			})
  2840  		require.NoError(t, err)
  2841  		require.Equal(t, 1, len(res.Conversations), "conv found")
  2842  		require.Equal(t, created.Id, res.Conversations[0].GetConvID(), "wrong conv")
  2843  	})
  2844  }
  2845  
  2846  func TestChatSrvFindConversationsWithSBS(t *testing.T) {
  2847  	runWithMemberTypes(t, func(mt chat1.ConversationMembersType) {
  2848  		switch mt {
  2849  		case chat1.ConversationMembersType_TEAM, chat1.ConversationMembersType_KBFS:
  2850  			return
  2851  		}
  2852  
  2853  		ctc := makeChatTestContext(t, "FindConversations", 2)
  2854  		defer ctc.cleanup()
  2855  		users := ctc.users()
  2856  
  2857  		// Create a conversation between both users. Attempt to send to
  2858  		// `user1,user2@rooter` and make sure we resolve and find the
  2859  		// conversation correctly.
  2860  		created := mustCreateConversationForTest(t, ctc, users[0], chat1.TopicType_CHAT,
  2861  			mt, users[1])
  2862  		tc1 := ctc.world.Tcs[users[1].Username]
  2863  		sbsName := strings.Join([]string{users[0].Username, fmt.Sprintf("%s@rooter", users[1].Username)}, ",")
  2864  
  2865  		ctx := ctc.as(t, users[0]).startCtx
  2866  		// Fail since we haven't proved rooter yet
  2867  		res, err := ctc.as(t, users[0]).chatLocalHandler().FindConversationsLocal(ctx,
  2868  			chat1.FindConversationsLocalArg{
  2869  				TlfName:          sbsName,
  2870  				MembersType:      mt,
  2871  				TopicType:        chat1.TopicType_CHAT,
  2872  				IdentifyBehavior: keybase1.TLFIdentifyBehavior_CHAT_CLI,
  2873  			})
  2874  		require.NoError(t, err)
  2875  		require.Zero(t, len(res.Conversations))
  2876  
  2877  		proveRooter(t, tc1.Context().ExternalG(), users[1])
  2878  		res, err = ctc.as(t, users[0]).chatLocalHandler().FindConversationsLocal(ctx,
  2879  			chat1.FindConversationsLocalArg{
  2880  				TlfName:          sbsName,
  2881  				MembersType:      mt,
  2882  				TopicType:        chat1.TopicType_CHAT,
  2883  				IdentifyBehavior: keybase1.TLFIdentifyBehavior_CHAT_CLI,
  2884  			})
  2885  		require.NoError(t, err)
  2886  		require.Equal(t, 1, len(res.Conversations), "no conv found")
  2887  		require.Equal(t, created.Id, res.Conversations[0].GetConvID(), "wrong conv")
  2888  	})
  2889  }
  2890  
  2891  func receiveThreadResult(t *testing.T, cb chan kbtest.NonblockThreadResult) (res *chat1.UIMessages) {
  2892  	var tres kbtest.NonblockThreadResult
  2893  	select {
  2894  	case tres = <-cb:
  2895  		res = tres.Thread
  2896  	case <-time.After(20 * time.Second):
  2897  		require.Fail(t, "no thread received")
  2898  	}
  2899  	if !tres.Full {
  2900  		select {
  2901  		case tres = <-cb:
  2902  			require.True(t, tres.Full)
  2903  			res = tres.Thread
  2904  		case <-time.After(20 * time.Second):
  2905  			require.Fail(t, "no thread received")
  2906  		}
  2907  	}
  2908  	return res
  2909  }
  2910  
  2911  func TestChatSrvGetThreadNonblockServerPage(t *testing.T) {
  2912  	runWithMemberTypes(t, func(mt chat1.ConversationMembersType) {
  2913  		ctc := makeChatTestContext(t, "TestChatSrvGetThreadNonblockIncremental", 1)
  2914  		defer ctc.cleanup()
  2915  		users := ctc.users()
  2916  
  2917  		ui := kbtest.NewChatUI()
  2918  		ctc.as(t, users[0]).h.mockChatUI = ui
  2919  
  2920  		query := chat1.GetThreadQuery{
  2921  			MessageTypes: []chat1.MessageType{chat1.MessageType_TEXT},
  2922  		}
  2923  		ctx := ctc.as(t, users[0]).startCtx
  2924  		conv := mustCreateConversationForTest(t, ctc, users[0], chat1.TopicType_CHAT, mt)
  2925  
  2926  		t.Logf("send a bunch of messages")
  2927  		numMsgs := 5
  2928  		msg := chat1.NewMessageBodyWithText(chat1.MessageText{Body: "hi"})
  2929  		for i := 0; i < numMsgs; i++ {
  2930  			mustPostLocalForTest(t, ctc, users[0], conv, msg)
  2931  		}
  2932  
  2933  		// Basic
  2934  		delay := 10 * time.Minute
  2935  		clock := clockwork.NewFakeClock()
  2936  		tc := ctc.world.Tcs[users[0].Username]
  2937  		ri := ctc.as(t, users[0]).ri
  2938  		uiThreadLoader := NewUIThreadLoader(tc.Context(), func() chat1.RemoteInterface { return ri })
  2939  		uiThreadLoader.clock = clock
  2940  		uiThreadLoader.cachedThreadDelay = nil
  2941  		uiThreadLoader.remoteThreadDelay = &delay
  2942  		uiThreadLoader.validatedDelay = 0
  2943  		tc.ChatG.UIThreadLoader = uiThreadLoader
  2944  		cb := make(chan struct{})
  2945  		p := utils.PresentPagination(&chat1.Pagination{
  2946  			Num: 1,
  2947  		})
  2948  		go func() {
  2949  			_, err := ctc.as(t, users[0]).chatLocalHandler().GetThreadNonblock(ctx,
  2950  				chat1.GetThreadNonblockArg{
  2951  					ConversationID:   conv.Id,
  2952  					IdentifyBehavior: keybase1.TLFIdentifyBehavior_CHAT_CLI,
  2953  					Query:            &query,
  2954  					Pagination:       p,
  2955  					Pgmode:           chat1.GetThreadNonblockPgMode_SERVER,
  2956  				},
  2957  			)
  2958  			require.NoError(t, err)
  2959  			close(cb)
  2960  		}()
  2961  		clock.Advance(50 * time.Millisecond)
  2962  		select {
  2963  		case res := <-ui.ThreadCb:
  2964  			require.False(t, res.Full)
  2965  			require.Equal(t, 1, len(res.Thread.Messages))
  2966  			require.NotNil(t, res.Thread.Pagination)
  2967  			require.False(t, res.Thread.Pagination.Last)
  2968  		case <-time.After(20 * time.Second):
  2969  			require.Fail(t, "no thread cb")
  2970  		}
  2971  		recvRemote := func() bool {
  2972  			for i := 0; i < 5; i++ {
  2973  				clock.Advance(20 * time.Minute)
  2974  				select {
  2975  				case res := <-ui.ThreadCb:
  2976  					require.True(t, res.Full)
  2977  					require.Equal(t, 1, len(res.Thread.Messages))
  2978  					require.Equal(t, chat1.MessageID(6), res.Thread.Messages[0].GetMessageID())
  2979  					p = res.Thread.Pagination
  2980  					require.NotNil(t, p)
  2981  					require.False(t, p.Last)
  2982  					return true
  2983  				case <-time.After(2 * time.Second):
  2984  					require.Fail(t, "no thread cb")
  2985  				}
  2986  			}
  2987  			return false
  2988  		}
  2989  		require.True(t, recvRemote())
  2990  		select {
  2991  		case <-cb:
  2992  		case <-time.After(20 * time.Second):
  2993  			require.Fail(t, "GetThread never finished")
  2994  		}
  2995  
  2996  		cb = make(chan struct{})
  2997  		p.Num = 1
  2998  		p.Next = "deadbeef"
  2999  		go func() {
  3000  			_, err := ctc.as(t, users[0]).chatLocalHandler().GetThreadNonblock(ctx,
  3001  				chat1.GetThreadNonblockArg{
  3002  					ConversationID:   conv.Id,
  3003  					IdentifyBehavior: keybase1.TLFIdentifyBehavior_CHAT_CLI,
  3004  					Query:            &query,
  3005  					Pagination:       p,
  3006  					Pgmode:           chat1.GetThreadNonblockPgMode_SERVER,
  3007  				},
  3008  			)
  3009  			require.NoError(t, err)
  3010  			close(cb)
  3011  		}()
  3012  		clock.Advance(50 * time.Millisecond)
  3013  		select {
  3014  		case res := <-ui.ThreadCb:
  3015  			require.False(t, res.Full)
  3016  			require.Equal(t, 1, len(res.Thread.Messages))
  3017  			require.NotNil(t, res.Thread.Pagination)
  3018  			require.False(t, res.Thread.Pagination.Last)
  3019  		case <-time.After(20 * time.Second):
  3020  			require.Fail(t, "no thread cb")
  3021  		}
  3022  		recvRemote = func() bool {
  3023  			for i := 0; i < 5; i++ {
  3024  				clock.Advance(20 * time.Minute)
  3025  				select {
  3026  				case res := <-ui.ThreadCb:
  3027  					require.True(t, res.Full)
  3028  					require.Equal(t, 1, len(res.Thread.Messages))
  3029  					require.Equal(t, chat1.MessageID(5), res.Thread.Messages[0].GetMessageID())
  3030  					p = res.Thread.Pagination
  3031  					require.NotNil(t, p)
  3032  					require.False(t, p.Last)
  3033  					return true
  3034  				case <-time.After(2 * time.Second):
  3035  					t.Logf("no thread cb")
  3036  				}
  3037  			}
  3038  			return false
  3039  		}
  3040  		require.True(t, recvRemote())
  3041  		select {
  3042  		case <-cb:
  3043  		case <-time.After(20 * time.Second):
  3044  			require.Fail(t, "GetThread never finished")
  3045  		}
  3046  
  3047  		for i := 0; i < 5; i++ {
  3048  			p.Num = 50
  3049  			cb = make(chan struct{})
  3050  			go func() {
  3051  				_, err := ctc.as(t, users[0]).chatLocalHandler().GetThreadNonblock(ctx,
  3052  					chat1.GetThreadNonblockArg{
  3053  						ConversationID:   conv.Id,
  3054  						IdentifyBehavior: keybase1.TLFIdentifyBehavior_CHAT_CLI,
  3055  						Query:            &query,
  3056  						Pagination:       p,
  3057  						Pgmode:           chat1.GetThreadNonblockPgMode_SERVER,
  3058  					},
  3059  				)
  3060  				require.NoError(t, err)
  3061  				close(cb)
  3062  			}()
  3063  			clock.Advance(50 * time.Millisecond)
  3064  			if i == 0 {
  3065  				select {
  3066  				case res := <-ui.ThreadCb:
  3067  					require.False(t, res.Full)
  3068  					require.Equal(t, 3, len(res.Thread.Messages))
  3069  					require.NotNil(t, res.Thread.Pagination)
  3070  					require.True(t, res.Thread.Pagination.Last)
  3071  				case <-time.After(20 * time.Second):
  3072  					require.Fail(t, "no thread cb")
  3073  				}
  3074  			} else {
  3075  				select {
  3076  				case <-ui.ThreadCb:
  3077  					require.Fail(t, "no callback expected")
  3078  				default:
  3079  				}
  3080  			}
  3081  			recvRemote = func() bool {
  3082  				for j := 0; j < 5; j++ {
  3083  					clock.Advance(20 * time.Minute)
  3084  					if i == 0 {
  3085  						select {
  3086  						case res := <-ui.ThreadCb:
  3087  							require.True(t, res.Full)
  3088  							require.Equal(t, 3, len(res.Thread.Messages))
  3089  							require.Equal(t, chat1.MessageID(4), res.Thread.Messages[0].GetMessageID())
  3090  							require.NotNil(t, res.Thread.Pagination.Last)
  3091  							require.True(t, res.Thread.Pagination.Last)
  3092  							return true
  3093  						case <-time.After(2 * time.Second):
  3094  							t.Logf("no thread cb")
  3095  						}
  3096  					} else {
  3097  						select {
  3098  						case <-ui.ThreadCb:
  3099  							require.Fail(t, "no callback expected")
  3100  						default:
  3101  							return true
  3102  						}
  3103  					}
  3104  				}
  3105  				return false
  3106  			}
  3107  			require.True(t, recvRemote())
  3108  			select {
  3109  			case <-cb:
  3110  			case <-time.After(20 * time.Second):
  3111  				require.Fail(t, "GetThread never finished")
  3112  			}
  3113  		}
  3114  	})
  3115  }
  3116  
  3117  func TestChatSrvGetThreadNonblockIncremental(t *testing.T) {
  3118  	runWithMemberTypes(t, func(mt chat1.ConversationMembersType) {
  3119  		ctc := makeChatTestContext(t, "TestChatSrvGetThreadNonblockIncremental", 1)
  3120  		defer ctc.cleanup()
  3121  		users := ctc.users()
  3122  
  3123  		ui := kbtest.NewChatUI()
  3124  		ctc.as(t, users[0]).h.mockChatUI = ui
  3125  
  3126  		query := chat1.GetThreadQuery{
  3127  			MessageTypes: []chat1.MessageType{chat1.MessageType_TEXT},
  3128  		}
  3129  		ctx := ctc.as(t, users[0]).startCtx
  3130  		conv := mustCreateConversationForTest(t, ctc, users[0], chat1.TopicType_CHAT, mt)
  3131  
  3132  		t.Logf("send a bunch of messages")
  3133  		numMsgs := 20
  3134  		msg := chat1.NewMessageBodyWithText(chat1.MessageText{Body: "hi"})
  3135  		for i := 0; i < numMsgs; i++ {
  3136  			mustPostLocalForTest(t, ctc, users[0], conv, msg)
  3137  		}
  3138  
  3139  		// Basic
  3140  		delay := 10 * time.Minute
  3141  		clock := clockwork.NewFakeClock()
  3142  		tc := ctc.world.Tcs[users[0].Username]
  3143  		ri := ctc.as(t, users[0]).ri
  3144  		uiThreadLoader := NewUIThreadLoader(tc.Context(), func() chat1.RemoteInterface { return ri })
  3145  		uiThreadLoader.clock = clock
  3146  		uiThreadLoader.cachedThreadDelay = nil
  3147  		uiThreadLoader.remoteThreadDelay = &delay
  3148  		uiThreadLoader.validatedDelay = 0
  3149  		tc.ChatG.UIThreadLoader = uiThreadLoader
  3150  		cb := make(chan struct{})
  3151  		go func() {
  3152  			_, err := ctc.as(t, users[0]).chatLocalHandler().GetThreadNonblock(ctx,
  3153  				chat1.GetThreadNonblockArg{
  3154  					ConversationID:   conv.Id,
  3155  					IdentifyBehavior: keybase1.TLFIdentifyBehavior_CHAT_CLI,
  3156  					Query:            &query,
  3157  				},
  3158  			)
  3159  			require.NoError(t, err)
  3160  			close(cb)
  3161  		}()
  3162  		clock.Advance(50 * time.Millisecond)
  3163  		select {
  3164  		case res := <-ui.ThreadCb:
  3165  			require.False(t, res.Full)
  3166  			require.Equal(t, numMsgs, len(res.Thread.Messages))
  3167  		case <-time.After(20 * time.Second):
  3168  			require.Fail(t, "no thread cb")
  3169  		}
  3170  		clock.Advance(20 * time.Minute)
  3171  		select {
  3172  		case res := <-ui.ThreadCb:
  3173  			require.True(t, res.Full)
  3174  			require.Equal(t, numMsgs, len(res.Thread.Messages))
  3175  		case <-time.After(20 * time.Second):
  3176  			require.Fail(t, "no thread cb")
  3177  		}
  3178  		select {
  3179  		case <-cb:
  3180  		case <-time.After(20 * time.Second):
  3181  			require.Fail(t, "GetThread never finished")
  3182  		}
  3183  
  3184  		// Incremental
  3185  		cb = make(chan struct{})
  3186  		go func() {
  3187  			_, err := ctc.as(t, users[0]).chatLocalHandler().GetThreadNonblock(ctx,
  3188  				chat1.GetThreadNonblockArg{
  3189  					ConversationID:   conv.Id,
  3190  					IdentifyBehavior: keybase1.TLFIdentifyBehavior_CHAT_CLI,
  3191  					Query:            &query,
  3192  					CbMode:           chat1.GetThreadNonblockCbMode_INCREMENTAL,
  3193  				},
  3194  			)
  3195  			require.NoError(t, err)
  3196  			close(cb)
  3197  		}()
  3198  		clock.Advance(50 * time.Millisecond)
  3199  		select {
  3200  		case res := <-ui.ThreadCb:
  3201  			require.False(t, res.Full)
  3202  			require.Equal(t, numMsgs, len(res.Thread.Messages))
  3203  		case <-time.After(20 * time.Second):
  3204  			require.Fail(t, "no thread cb")
  3205  		}
  3206  		mustPostLocalForTest(t, ctc, users[0], conv, msg)
  3207  		clock.Advance(20 * time.Minute)
  3208  		select {
  3209  		case res := <-ui.ThreadCb:
  3210  			require.True(t, res.Full)
  3211  			require.Equal(t, 1, len(res.Thread.Messages))
  3212  			require.True(t, res.Thread.Pagination.Last)
  3213  		case <-time.After(20 * time.Second):
  3214  			require.Fail(t, "no thread cb")
  3215  		}
  3216  		select {
  3217  		case <-cb:
  3218  		case <-time.After(20 * time.Second):
  3219  			require.Fail(t, "GetThread never finished")
  3220  		}
  3221  
  3222  	})
  3223  }
  3224  
  3225  func msgState(t *testing.T, msg chat1.UIMessage) chat1.MessageUnboxedState {
  3226  	state, err := msg.State()
  3227  	require.NoError(t, err)
  3228  	return state
  3229  }
  3230  
  3231  func confirmIsPlaceholder(t *testing.T, msgID chat1.MessageID, msg chat1.UIMessage, hidden bool) {
  3232  	require.Equal(t, msgID, msg.GetMessageID())
  3233  	require.Equal(t, chat1.MessageUnboxedState_PLACEHOLDER, msgState(t, msg))
  3234  	require.Equal(t, hidden, msg.Placeholder().Hidden)
  3235  }
  3236  
  3237  func confirmIsText(t *testing.T, msgID chat1.MessageID, msg chat1.UIMessage, text string) {
  3238  	require.Equal(t, msgID, msg.GetMessageID())
  3239  	require.Equal(t, chat1.MessageUnboxedState_VALID, msgState(t, msg))
  3240  	require.Equal(t, chat1.MessageType_TEXT, msg.GetMessageType())
  3241  	require.Equal(t, text, msg.Valid().MessageBody.Text().Body)
  3242  }
  3243  
  3244  func TestChatSrvGetThreadNonblockSupersedes(t *testing.T) {
  3245  	runWithMemberTypes(t, func(mt chat1.ConversationMembersType) {
  3246  		ctc := makeChatTestContext(t, "GetThreadNonblockSupersedes", 1)
  3247  		defer ctc.cleanup()
  3248  		users := ctc.users()
  3249  
  3250  		uid := gregor1.UID(users[0].GetUID().ToBytes())
  3251  		ui := kbtest.NewChatUI()
  3252  		ctc.as(t, users[0]).h.mockChatUI = ui
  3253  		ctx := ctc.as(t, users[0]).startCtx
  3254  		<-ctc.as(t, users[0]).h.G().ConvLoader.Stop(ctx)
  3255  		listener := newServerChatListener()
  3256  		ctc.as(t, users[0]).h.G().NotifyRouter.AddListener(listener)
  3257  
  3258  		conv := mustCreateConversationForTest(t, ctc, users[0], chat1.TopicType_CHAT, mt)
  3259  		cs := ctc.world.Tcs[users[0].Username].ChatG.ConvSource
  3260  		msg := chat1.NewMessageBodyWithText(chat1.MessageText{Body: "hi"})
  3261  		msgID1 := mustPostLocalForTest(t, ctc, users[0], conv, msg)
  3262  		consumeNewMsgRemote(t, listener, chat1.MessageType_TEXT)
  3263  		msgRes, err := ctc.as(t, users[0]).chatLocalHandler().GetMessagesLocal(ctx, chat1.GetMessagesLocalArg{
  3264  			ConversationID:           conv.Id,
  3265  			MessageIDs:               []chat1.MessageID{msgID1},
  3266  			DisableResolveSupersedes: true,
  3267  		})
  3268  		require.NoError(t, err)
  3269  		require.Equal(t, 1, len(msgRes.Messages))
  3270  		msg1 := msgRes.Messages[0]
  3271  		editMsgID1 := mustEditMsg(ctx, t, ctc, users[0], conv, msgID1)
  3272  		consumeNewMsgRemote(t, listener, chat1.MessageType_EDIT)
  3273  
  3274  		msgIDs := []chat1.MessageID{editMsgID1, msgID1, 1}
  3275  		require.NoError(t, cs.Clear(context.TODO(), conv.Id, uid, nil))
  3276  		tc := ctc.world.Tcs[users[0].Username]
  3277  		rconv, err := utils.GetUnverifiedConv(ctx, tc.Context(), uid, conv.Id,
  3278  			types.InboxSourceDataSourceAll)
  3279  		require.NoError(t, err)
  3280  		err = cs.PushUnboxed(ctx, rconv, uid, []chat1.MessageUnboxed{msg1})
  3281  		require.NoError(t, err)
  3282  
  3283  		delay := 10 * time.Minute
  3284  		clock := clockwork.NewFakeClock()
  3285  		ri := ctc.as(t, users[0]).ri
  3286  		uiThreadLoader := NewUIThreadLoader(tc.Context(), func() chat1.RemoteInterface { return ri })
  3287  		uiThreadLoader.clock = clock
  3288  		uiThreadLoader.cachedThreadDelay = nil
  3289  		uiThreadLoader.remoteThreadDelay = &delay
  3290  		uiThreadLoader.validatedDelay = 0
  3291  		tc.ChatG.UIThreadLoader = uiThreadLoader
  3292  		cb := make(chan struct{})
  3293  		query := chat1.GetThreadQuery{
  3294  			MessageTypes: []chat1.MessageType{chat1.MessageType_TEXT},
  3295  		}
  3296  		go func() {
  3297  			_, err := ctc.as(t, users[0]).chatLocalHandler().GetThreadNonblock(ctx,
  3298  				chat1.GetThreadNonblockArg{
  3299  					ConversationID: conv.Id,
  3300  					Query:          &query,
  3301  					CbMode:         chat1.GetThreadNonblockCbMode_INCREMENTAL,
  3302  				},
  3303  			)
  3304  			require.NoError(t, err)
  3305  			close(cb)
  3306  		}()
  3307  		clock.Advance(50 * time.Millisecond)
  3308  		select {
  3309  		case res := <-ui.ThreadCb:
  3310  			require.False(t, res.Full)
  3311  			require.Equal(t, len(msgIDs), len(res.Thread.Messages))
  3312  			// Not unread
  3313  			require.Equal(t, msgIDs, utils.PluckUIMessageIDs(res.Thread.Messages))
  3314  			confirmIsText(t, msgID1, res.Thread.Messages[1], "hi")
  3315  			require.False(t, res.Thread.Messages[1].Valid().Superseded)
  3316  			confirmIsPlaceholder(t, editMsgID1, res.Thread.Messages[0], false)
  3317  			confirmIsPlaceholder(t, 1, res.Thread.Messages[2], false)
  3318  		case <-time.After(20 * time.Second):
  3319  			require.Fail(t, "no thread cb")
  3320  		}
  3321  		clock.Advance(20 * time.Minute)
  3322  		select {
  3323  		case res := <-ui.ThreadCb:
  3324  			require.True(t, res.Full)
  3325  			require.Equal(t, len(msgIDs), len(res.Thread.Messages))
  3326  			// Not unread
  3327  			confirmIsPlaceholder(t, editMsgID1, res.Thread.Messages[0], true)
  3328  			confirmIsText(t, msgID1, res.Thread.Messages[1], "edited")
  3329  			confirmIsPlaceholder(t, 1, res.Thread.Messages[2], true)
  3330  		case <-time.After(20 * time.Second):
  3331  			require.Fail(t, "no thread cb")
  3332  		}
  3333  		select {
  3334  		case <-cb:
  3335  		case <-time.After(20 * time.Second):
  3336  			require.Fail(t, "GetThread never finished")
  3337  		}
  3338  
  3339  		deleteMsgID := mustDeleteMsg(ctx, t, ctc, users[0], conv, msgID1)
  3340  		consumeNewMsgRemote(t, listener, chat1.MessageType_DELETE)
  3341  		msgIDs = []chat1.MessageID{deleteMsgID, editMsgID1, msgID1, 1}
  3342  		require.NoError(t, cs.Clear(context.TODO(), conv.Id, uid, nil))
  3343  
  3344  		err = cs.PushUnboxed(ctx, rconv, uid, []chat1.MessageUnboxed{msg1})
  3345  		require.NoError(t, err)
  3346  		cb = make(chan struct{})
  3347  		go func() {
  3348  			_, err := ctc.as(t, users[0]).chatLocalHandler().GetThreadNonblock(ctx,
  3349  				chat1.GetThreadNonblockArg{
  3350  					ConversationID: conv.Id,
  3351  					Query:          &query,
  3352  					CbMode:         chat1.GetThreadNonblockCbMode_INCREMENTAL,
  3353  				},
  3354  			)
  3355  			require.NoError(t, err)
  3356  			close(cb)
  3357  		}()
  3358  		clock.Advance(50 * time.Millisecond)
  3359  		select {
  3360  		case res := <-ui.ThreadCb:
  3361  			require.False(t, res.Full)
  3362  			require.Equal(t, len(msgIDs), len(res.Thread.Messages))
  3363  			// Not unread
  3364  			require.Equal(t, msgIDs, utils.PluckUIMessageIDs(res.Thread.Messages))
  3365  			confirmIsPlaceholder(t, deleteMsgID, res.Thread.Messages[0], false)
  3366  			confirmIsPlaceholder(t, editMsgID1, res.Thread.Messages[1], false)
  3367  			confirmIsText(t, msgID1, res.Thread.Messages[2], "hi")
  3368  			require.False(t, res.Thread.Messages[2].Valid().Superseded)
  3369  			confirmIsPlaceholder(t, 1, res.Thread.Messages[3], false)
  3370  		case <-time.After(20 * time.Second):
  3371  			require.Fail(t, "no thread cb")
  3372  		}
  3373  		clock.Advance(20 * time.Minute)
  3374  		select {
  3375  		case res := <-ui.ThreadCb:
  3376  			require.True(t, res.Full)
  3377  			require.Equal(t, len(msgIDs), len(res.Thread.Messages))
  3378  			// Not unread
  3379  			confirmIsPlaceholder(t, deleteMsgID, res.Thread.Messages[0], true)
  3380  			confirmIsPlaceholder(t, editMsgID1, res.Thread.Messages[1], true)
  3381  			confirmIsPlaceholder(t, msgID1, res.Thread.Messages[2], true)
  3382  			confirmIsPlaceholder(t, 1, res.Thread.Messages[3], true)
  3383  		case <-time.After(20 * time.Second):
  3384  			require.Fail(t, "no thread cb")
  3385  		}
  3386  		select {
  3387  		case <-cb:
  3388  		case <-time.After(20 * time.Second):
  3389  			require.Fail(t, "GetThread never finished")
  3390  		}
  3391  	})
  3392  }
  3393  
  3394  func TestChatSrvGetUnreadLine(t *testing.T) {
  3395  	runWithMemberTypes(t, func(mt chat1.ConversationMembersType) {
  3396  		ctc := makeChatTestContext(t, "GetUnreadLine", 2)
  3397  		defer ctc.cleanup()
  3398  		users := ctc.users()
  3399  
  3400  		ui := kbtest.NewChatUI()
  3401  		ctc.as(t, users[0]).h.mockChatUI = ui
  3402  		ctc.as(t, users[1]).h.mockChatUI = ui
  3403  		ctx1 := ctc.as(t, users[0]).startCtx
  3404  		ctx2 := ctc.as(t, users[1]).startCtx
  3405  		<-ctc.as(t, users[0]).h.G().ConvLoader.Stop(ctx1)
  3406  		<-ctc.as(t, users[1]).h.G().ConvLoader.Stop(ctx2)
  3407  		listener1 := newServerChatListener()
  3408  		ctc.as(t, users[0]).h.G().NotifyRouter.AddListener(listener1)
  3409  		listener2 := newServerChatListener()
  3410  		ctc.as(t, users[1]).h.G().NotifyRouter.AddListener(listener2)
  3411  		g1 := ctc.world.Tcs[users[0].Username].ChatG
  3412  		g2 := ctc.world.Tcs[users[1].Username].ChatG
  3413  
  3414  		conv := mustCreateConversationForTest(t, ctc, users[0], chat1.TopicType_CHAT, mt, users[1])
  3415  		msg := chat1.NewMessageBodyWithText(chat1.MessageText{Body: "hi"})
  3416  		msgID1 := mustPostLocalForTest(t, ctc, users[0], conv, msg)
  3417  		consumeNewMsgRemote(t, listener1, chat1.MessageType_TEXT)
  3418  		consumeNewMsgRemote(t, listener2, chat1.MessageType_TEXT)
  3419  
  3420  		mustEditMsg(ctx1, t, ctc, users[0], conv, msgID1)
  3421  		consumeNewMsgRemote(t, listener1, chat1.MessageType_EDIT)
  3422  		consumeNewMsgRemote(t, listener2, chat1.MessageType_EDIT)
  3423  
  3424  		assertUnreadline := func(ctx context.Context, g *globals.ChatContext, user *kbtest.FakeUser,
  3425  			readMsgID, unreadLineID chat1.MessageID) {
  3426  			for i := 0; i < 1; i++ {
  3427  				if i == 0 {
  3428  					require.NoError(t, g.ConvSource.Clear(ctx, conv.Id, user.GetUID().ToBytes(), nil))
  3429  				}
  3430  				res, err := ctc.as(t, user).chatLocalHandler().GetUnreadline(ctx,
  3431  					chat1.GetUnreadlineArg{
  3432  						ConvID:    conv.Id,
  3433  						ReadMsgID: readMsgID,
  3434  					})
  3435  				require.NoError(t, err)
  3436  				if unreadLineID == 0 {
  3437  					require.Nil(t, res.UnreadlineID)
  3438  				} else {
  3439  					require.NotNil(t, res.UnreadlineID)
  3440  					require.Equal(t, unreadLineID, *res.UnreadlineID, "unread line mismatch")
  3441  				}
  3442  			}
  3443  		}
  3444  
  3445  		// user2 will have an unread id of the TEXT message even after the edit
  3446  		msgID0 := chat1.MessageID(1)
  3447  		assertUnreadline(ctx1, g1, users[0], msgID0, msgID1)
  3448  		assertUnreadline(ctx1, g1, users[0], msgID1, 0)
  3449  		assertUnreadline(ctx2, g2, users[1], msgID0, msgID1)
  3450  		assertUnreadline(ctx2, g2, users[1], msgID1, 0)
  3451  
  3452  		// subsequent TEXT post leaves unreadline unchanged.
  3453  		msg = chat1.NewMessageBodyWithText(chat1.MessageText{Body: "hi"})
  3454  		msgID2 := mustPostLocalForTest(t, ctc, users[0], conv, msg)
  3455  		consumeNewMsgRemote(t, listener1, chat1.MessageType_TEXT)
  3456  		consumeNewMsgRemote(t, listener2, chat1.MessageType_TEXT)
  3457  		assertUnreadline(ctx2, g2, users[1], msgID0, msgID1)
  3458  		// If user2 has read to msgID1, msgID2 is the next candidate
  3459  		assertUnreadline(ctx2, g2, users[1], msgID1, msgID2)
  3460  
  3461  		_, err := ctc.as(t, users[1]).chatLocalHandler().MarkAsReadLocal(ctx1,
  3462  			chat1.MarkAsReadLocalArg{
  3463  				ConversationID: conv.Id,
  3464  				MsgID:          &msgID0,
  3465  				ForceUnread:    true,
  3466  			})
  3467  		require.NoError(t, err)
  3468  		assertUnreadline(ctx2, g2, users[1], msgID0, msgID1)
  3469  
  3470  		// reaction does not affect things
  3471  		mustReactToMsg(ctx1, t, ctc, users[0], conv, msgID2, ":+1:")
  3472  		consumeNewMsgRemote(t, listener1, chat1.MessageType_REACTION)
  3473  		consumeNewMsgRemote(t, listener2, chat1.MessageType_REACTION)
  3474  		assertUnreadline(ctx2, g2, users[1], 1, msgID1)
  3475  
  3476  		// user2 will bump unreadLineID to msgID which the next visible msg
  3477  		mustDeleteMsg(ctx1, t, ctc, users[0], conv, msgID1)
  3478  		consumeNewMsgRemote(t, listener1, chat1.MessageType_DELETE)
  3479  		consumeNewMsgRemote(t, listener2, chat1.MessageType_DELETE)
  3480  		assertUnreadline(ctx2, g2, users[1], 1, msgID2)
  3481  
  3482  		// user2 will have no unread id since the only visible message was now deleted
  3483  		msgID3 := mustDeleteMsg(ctx1, t, ctc, users[0], conv, msgID2)
  3484  		consumeNewMsgRemote(t, listener1, chat1.MessageType_DELETE)
  3485  		consumeNewMsgRemote(t, listener2, chat1.MessageType_DELETE)
  3486  		assertUnreadline(ctx2, g2, users[1], 1, 0)
  3487  
  3488  		// if we are fully read, there is no line and we don't go to the server
  3489  		g1.ConvSource.SetRemoteInterface(func() chat1.RemoteInterface {
  3490  			return chat1.RemoteClient{Cli: errorClient{}}
  3491  		})
  3492  		assertUnreadline(ctx1, g1, users[0], msgID3, 0)
  3493  		g2.ConvSource.SetRemoteInterface(func() chat1.RemoteInterface {
  3494  			return chat1.RemoteClient{Cli: errorClient{}}
  3495  		})
  3496  		assertUnreadline(ctx2, g2, users[1], msgID3, 0)
  3497  	})
  3498  }
  3499  
  3500  func mustDeleteHistory(ctx context.Context, t *testing.T, ctc *chatTestContext, user *kbtest.FakeUser,
  3501  	conv chat1.ConversationInfoLocal, upto chat1.MessageID) chat1.MessageID {
  3502  	delH := chat1.MessageDeleteHistory{
  3503  		Upto: upto,
  3504  	}
  3505  	postRes, err := ctc.as(t, user).chatLocalHandler().PostLocal(ctx, chat1.PostLocalArg{
  3506  		ConversationID: conv.Id,
  3507  		Msg: chat1.MessagePlaintext{
  3508  			ClientHeader: chat1.MessageClientHeader{
  3509  				Conv:          conv.Triple,
  3510  				MessageType:   chat1.MessageType_DELETEHISTORY,
  3511  				TlfName:       conv.TlfName,
  3512  				DeleteHistory: &delH,
  3513  			},
  3514  			MessageBody: chat1.NewMessageBodyWithDeletehistory(delH),
  3515  		},
  3516  	})
  3517  	require.NoError(t, err)
  3518  	return postRes.MessageID
  3519  }
  3520  
  3521  func mustDeleteMsg(ctx context.Context, t *testing.T, ctc *chatTestContext, user *kbtest.FakeUser,
  3522  	conv chat1.ConversationInfoLocal, msgID chat1.MessageID) chat1.MessageID {
  3523  	postRes, err := ctc.as(t, user).chatLocalHandler().PostLocal(ctx, chat1.PostLocalArg{
  3524  		ConversationID: conv.Id,
  3525  		Msg: chat1.MessagePlaintext{
  3526  			ClientHeader: chat1.MessageClientHeader{
  3527  				Conv:        conv.Triple,
  3528  				MessageType: chat1.MessageType_DELETE,
  3529  				TlfName:     conv.TlfName,
  3530  				Supersedes:  msgID,
  3531  			},
  3532  		},
  3533  	})
  3534  	require.NoError(t, err)
  3535  	return postRes.MessageID
  3536  }
  3537  
  3538  func mustEditMsg(ctx context.Context, t *testing.T, ctc *chatTestContext, user *kbtest.FakeUser,
  3539  	conv chat1.ConversationInfoLocal, msgID chat1.MessageID) chat1.MessageID {
  3540  	postRes, err := ctc.as(t, user).chatLocalHandler().PostLocal(ctx, chat1.PostLocalArg{
  3541  		ConversationID: conv.Id,
  3542  		Msg: chat1.MessagePlaintext{
  3543  			ClientHeader: chat1.MessageClientHeader{
  3544  				Conv:        conv.Triple,
  3545  				MessageType: chat1.MessageType_EDIT,
  3546  				TlfName:     conv.TlfName,
  3547  				Supersedes:  msgID,
  3548  			},
  3549  			MessageBody: chat1.NewMessageBodyWithEdit(chat1.MessageEdit{
  3550  				MessageID: msgID,
  3551  				Body:      "edited",
  3552  			}),
  3553  		},
  3554  	})
  3555  	require.NoError(t, err)
  3556  	return postRes.MessageID
  3557  }
  3558  
  3559  func mustReactToMsg(ctx context.Context, t *testing.T, ctc *chatTestContext, user *kbtest.FakeUser,
  3560  	conv chat1.ConversationInfoLocal, msgID chat1.MessageID, reaction string) chat1.MessageID {
  3561  	postRes, err := ctc.as(t, user).chatLocalHandler().PostLocal(ctx, chat1.PostLocalArg{
  3562  		ConversationID: conv.Id,
  3563  		Msg: chat1.MessagePlaintext{
  3564  			ClientHeader: chat1.MessageClientHeader{
  3565  				Conv:        conv.Triple,
  3566  				MessageType: chat1.MessageType_REACTION,
  3567  				TlfName:     conv.TlfName,
  3568  				Supersedes:  msgID,
  3569  			},
  3570  			MessageBody: chat1.NewMessageBodyWithReaction(chat1.MessageReaction{
  3571  				MessageID: msgID,
  3572  				Body:      reaction,
  3573  			}),
  3574  		},
  3575  	})
  3576  	require.NoError(t, err)
  3577  	return postRes.MessageID
  3578  }
  3579  
  3580  func TestChatSrvGetThreadNonblockPlaceholders(t *testing.T) {
  3581  	runWithMemberTypes(t, func(mt chat1.ConversationMembersType) {
  3582  		ctc := makeChatTestContext(t, "GetThreadNonblockPlaceholders", 1)
  3583  		defer ctc.cleanup()
  3584  		users := ctc.users()
  3585  
  3586  		uid := gregor1.UID(users[0].GetUID().ToBytes())
  3587  		ui := kbtest.NewChatUI()
  3588  		ctc.as(t, users[0]).h.mockChatUI = ui
  3589  		ctx := ctc.as(t, users[0]).startCtx
  3590  		<-ctc.as(t, users[0]).h.G().ConvLoader.Stop(ctx)
  3591  		listener := newServerChatListener()
  3592  		ctc.as(t, users[0]).h.G().NotifyRouter.AddListener(listener)
  3593  
  3594  		conv := mustCreateConversationForTest(t, ctc, users[0], chat1.TopicType_CHAT, mt)
  3595  		cs := ctc.world.Tcs[users[0].Username].ChatG.ConvSource
  3596  		msg := chat1.NewMessageBodyWithText(chat1.MessageText{Body: "hi"})
  3597  		msgID1 := mustPostLocalForTest(t, ctc, users[0], conv, msg)
  3598  		consumeNewMsgRemote(t, listener, chat1.MessageType_TEXT)
  3599  		editMsgID1 := mustEditMsg(ctx, t, ctc, users[0], conv, msgID1)
  3600  		consumeNewMsgRemote(t, listener, chat1.MessageType_EDIT)
  3601  		msgID2 := mustPostLocalForTest(t, ctc, users[0], conv, msg)
  3602  		consumeNewMsgRemote(t, listener, chat1.MessageType_TEXT)
  3603  		editMsgID2 := mustEditMsg(ctx, t, ctc, users[0], conv, msgID2)
  3604  		consumeNewMsgRemote(t, listener, chat1.MessageType_EDIT)
  3605  		msgID3 := mustPostLocalForTest(t, ctc, users[0], conv, msg)
  3606  		consumeNewMsgRemote(t, listener, chat1.MessageType_TEXT)
  3607  		msgRes, err := ctc.as(t, users[0]).chatLocalHandler().GetMessagesLocal(ctx, chat1.GetMessagesLocalArg{
  3608  			ConversationID: conv.Id,
  3609  			MessageIDs:     []chat1.MessageID{msgID3},
  3610  		})
  3611  		require.NoError(t, err)
  3612  		require.Equal(t, 1, len(msgRes.Messages))
  3613  		msg3 := msgRes.Messages[0]
  3614  		msgIDs := []chat1.MessageID{msgID3, editMsgID2, msgID2, editMsgID1, msgID1, 1}
  3615  
  3616  		require.NoError(t, cs.Clear(context.TODO(), conv.Id, uid, nil))
  3617  
  3618  		tc := ctc.world.Tcs[users[0].Username]
  3619  		rconv, err := utils.GetUnverifiedConv(ctx, tc.Context(), uid, conv.Id,
  3620  			types.InboxSourceDataSourceAll)
  3621  		require.NoError(t, err)
  3622  		err = cs.PushUnboxed(ctx, rconv, uid, []chat1.MessageUnboxed{msg3})
  3623  		require.NoError(t, err)
  3624  
  3625  		delay := 10 * time.Minute
  3626  		clock := clockwork.NewFakeClock()
  3627  		ri := ctc.as(t, users[0]).ri
  3628  		uiThreadLoader := NewUIThreadLoader(tc.Context(), func() chat1.RemoteInterface { return ri })
  3629  		uiThreadLoader.clock = clock
  3630  		uiThreadLoader.cachedThreadDelay = nil
  3631  		uiThreadLoader.remoteThreadDelay = &delay
  3632  		uiThreadLoader.validatedDelay = 0
  3633  		tc.ChatG.UIThreadLoader = uiThreadLoader
  3634  		cb := make(chan struct{})
  3635  		query := chat1.GetThreadQuery{
  3636  			MessageTypes: []chat1.MessageType{chat1.MessageType_TEXT},
  3637  		}
  3638  		go func() {
  3639  			_, err := ctc.as(t, users[0]).chatLocalHandler().GetThreadNonblock(ctx,
  3640  				chat1.GetThreadNonblockArg{
  3641  					ConversationID: conv.Id,
  3642  					Query:          &query,
  3643  					CbMode:         chat1.GetThreadNonblockCbMode_INCREMENTAL,
  3644  				},
  3645  			)
  3646  			require.NoError(t, err)
  3647  			close(cb)
  3648  		}()
  3649  		select {
  3650  		case res := <-ui.ThreadCb:
  3651  			require.False(t, res.Full)
  3652  			require.Equal(t, len(msgIDs), len(res.Thread.Messages))
  3653  			require.Equal(t, msgIDs, utils.PluckUIMessageIDs(res.Thread.Messages))
  3654  			confirmIsText(t, msgID3, res.Thread.Messages[0], "hi")
  3655  			confirmIsPlaceholder(t, editMsgID2, res.Thread.Messages[1], false)
  3656  			confirmIsPlaceholder(t, msgID2, res.Thread.Messages[2], false)
  3657  			confirmIsPlaceholder(t, editMsgID1, res.Thread.Messages[3], false)
  3658  			confirmIsPlaceholder(t, msgID1, res.Thread.Messages[4], false)
  3659  			confirmIsPlaceholder(t, 1, res.Thread.Messages[5], false)
  3660  		case <-time.After(20 * time.Second):
  3661  			require.Fail(t, "no thread cb")
  3662  		}
  3663  		recvRemote := func() bool {
  3664  			for i := 0; i < 5; i++ {
  3665  				clock.Advance(20 * time.Minute)
  3666  				select {
  3667  				case res := <-ui.ThreadCb:
  3668  					require.True(t, res.Full)
  3669  					require.Equal(t, len(msgIDs)-1, len(res.Thread.Messages))
  3670  					confirmIsPlaceholder(t, editMsgID2, res.Thread.Messages[0], true)
  3671  					confirmIsText(t, msgID2, res.Thread.Messages[1], "edited")
  3672  					confirmIsPlaceholder(t, editMsgID1, res.Thread.Messages[2], true)
  3673  					confirmIsText(t, msgID1, res.Thread.Messages[3], "edited")
  3674  					confirmIsPlaceholder(t, 1, res.Thread.Messages[4], true)
  3675  					return true
  3676  				case <-time.After(2 * time.Second):
  3677  					t.Logf("no cb received")
  3678  				}
  3679  			}
  3680  			return false
  3681  		}
  3682  		require.True(t, recvRemote())
  3683  		select {
  3684  		case <-cb:
  3685  		case <-time.After(20 * time.Second):
  3686  			require.Fail(t, "GetThread never finished")
  3687  		}
  3688  	})
  3689  }
  3690  
  3691  func TestChatSrvGetThreadNonblockPlaceholderFirst(t *testing.T) {
  3692  	runWithMemberTypes(t, func(mt chat1.ConversationMembersType) {
  3693  		ctc := makeChatTestContext(t, "GetThreadNonblockPlaceholdersFirst", 1)
  3694  		defer ctc.cleanup()
  3695  		users := ctc.users()
  3696  
  3697  		uid := gregor1.UID(users[0].GetUID().ToBytes())
  3698  		ui := kbtest.NewChatUI()
  3699  		ctc.as(t, users[0]).h.mockChatUI = ui
  3700  		ctx := ctc.as(t, users[0]).startCtx
  3701  		<-ctc.as(t, users[0]).h.G().ConvLoader.Stop(ctx)
  3702  		listener := newServerChatListener()
  3703  		ctc.as(t, users[0]).h.G().NotifyRouter.AddListener(listener)
  3704  
  3705  		conv := mustCreateConversationForTest(t, ctc, users[0], chat1.TopicType_CHAT, mt)
  3706  		tc := ctc.world.Tcs[users[0].Username]
  3707  		cs := tc.ChatG.ConvSource
  3708  		msg := chat1.NewMessageBodyWithText(chat1.MessageText{Body: "hi"})
  3709  		msgID1 := mustPostLocalForTest(t, ctc, users[0], conv, msg)
  3710  		consumeNewMsgRemote(t, listener, chat1.MessageType_TEXT)
  3711  		msgID2 := mustPostLocalForTest(t, ctc, users[0], conv, msg)
  3712  		consumeNewMsgRemote(t, listener, chat1.MessageType_TEXT)
  3713  		msgRes, err := ctc.as(t, users[0]).chatLocalHandler().GetMessagesLocal(ctx, chat1.GetMessagesLocalArg{
  3714  			ConversationID: conv.Id,
  3715  			MessageIDs:     []chat1.MessageID{msgID1},
  3716  		})
  3717  		require.NoError(t, err)
  3718  		require.Equal(t, 1, len(msgRes.Messages))
  3719  		msg1 := msgRes.Messages[0]
  3720  		msgIDs := []chat1.MessageID{msgID2, msgID1, 1}
  3721  
  3722  		require.NoError(t, cs.Clear(context.TODO(), conv.Id, uid, nil))
  3723  		rconv, err := utils.GetUnverifiedConv(ctx, tc.Context(), uid, conv.Id,
  3724  			types.InboxSourceDataSourceAll)
  3725  		require.NoError(t, err)
  3726  		err = cs.PushUnboxed(ctx, rconv, uid, []chat1.MessageUnboxed{msg1})
  3727  		require.NoError(t, err)
  3728  
  3729  		delay := 10 * time.Minute
  3730  		clock := clockwork.NewFakeClock()
  3731  		ri := ctc.as(t, users[0]).ri
  3732  		uiThreadLoader := NewUIThreadLoader(tc.Context(), func() chat1.RemoteInterface { return ri })
  3733  		uiThreadLoader.clock = clock
  3734  		uiThreadLoader.cachedThreadDelay = nil
  3735  		uiThreadLoader.remoteThreadDelay = &delay
  3736  		uiThreadLoader.validatedDelay = 0
  3737  		tc.ChatG.UIThreadLoader = uiThreadLoader
  3738  		cb := make(chan struct{})
  3739  		query := chat1.GetThreadQuery{
  3740  			MessageTypes: []chat1.MessageType{chat1.MessageType_TEXT},
  3741  		}
  3742  		go func() {
  3743  			_, err := ctc.as(t, users[0]).chatLocalHandler().GetThreadNonblock(ctx,
  3744  				chat1.GetThreadNonblockArg{
  3745  					ConversationID: conv.Id,
  3746  					Query:          &query,
  3747  					CbMode:         chat1.GetThreadNonblockCbMode_INCREMENTAL,
  3748  				},
  3749  			)
  3750  			require.NoError(t, err)
  3751  			close(cb)
  3752  		}()
  3753  		clock.Advance(50 * time.Millisecond)
  3754  		select {
  3755  		case res := <-ui.ThreadCb:
  3756  			require.False(t, res.Full)
  3757  			require.Equal(t, len(msgIDs), len(res.Thread.Messages))
  3758  			require.Equal(t, msgIDs, utils.PluckUIMessageIDs(res.Thread.Messages))
  3759  			confirmIsPlaceholder(t, msgID2, res.Thread.Messages[0], false)
  3760  			confirmIsText(t, msgID1, res.Thread.Messages[1], "hi")
  3761  			confirmIsPlaceholder(t, 1, res.Thread.Messages[2], false)
  3762  		case <-time.After(20 * time.Second):
  3763  			require.Fail(t, "no thread cb")
  3764  		}
  3765  		clock.Advance(20 * time.Minute)
  3766  		select {
  3767  		case res := <-ui.ThreadCb:
  3768  			require.True(t, res.Full)
  3769  			require.Equal(t, len(msgIDs)-1, len(res.Thread.Messages))
  3770  			confirmIsText(t, msgID2, res.Thread.Messages[0], "hi")
  3771  			confirmIsPlaceholder(t, 1, res.Thread.Messages[1], true)
  3772  		case <-time.After(20 * time.Second):
  3773  			require.Fail(t, "no thread cb")
  3774  		}
  3775  		select {
  3776  		case <-cb:
  3777  		case <-time.After(20 * time.Second):
  3778  			require.Fail(t, "GetThread never finished")
  3779  		}
  3780  	})
  3781  }
  3782  
  3783  func TestChatSrvGetThreadNonblockOldPages(t *testing.T) {
  3784  	runWithMemberTypes(t, func(mt chat1.ConversationMembersType) {
  3785  		if mt == chat1.ConversationMembersType_KBFS {
  3786  			return
  3787  		}
  3788  		ctc := makeChatTestContext(t, "GetThreadNonblock", 1)
  3789  		defer ctc.cleanup()
  3790  		users := ctc.users()
  3791  
  3792  		uid := gregor1.UID(users[0].GetUID().ToBytes())
  3793  		ui := kbtest.NewChatUI()
  3794  		ctc.as(t, users[0]).h.mockChatUI = ui
  3795  		ctx := ctc.as(t, users[0]).startCtx
  3796  		bgConvLoads := make(chan chat1.ConversationID, 10)
  3797  		ctc.as(t, users[0]).h.G().ConvLoader.Start(ctx, uid)
  3798  		ctc.as(t, users[0]).h.G().ConvLoader.(*BackgroundConvLoader).loads = bgConvLoads
  3799  
  3800  		t.Logf("send a bunch of messages")
  3801  		conv := mustCreateConversationForTest(t, ctc, users[0], chat1.TopicType_CHAT, mt)
  3802  		select {
  3803  		case <-bgConvLoads:
  3804  		case <-time.After(20 * time.Second):
  3805  			require.Fail(t, "no bkg load")
  3806  		}
  3807  		numMsgs := 20
  3808  		msg := chat1.NewMessageBodyWithText(chat1.MessageText{Body: "hi"})
  3809  		for i := 0; i < numMsgs; i++ {
  3810  			mustPostLocalForTest(t, ctc, users[0], conv, msg)
  3811  		}
  3812  		select {
  3813  		case <-bgConvLoads:
  3814  			require.Fail(t, "no more bg loads")
  3815  		default:
  3816  		}
  3817  		_, err := ctc.as(t, users[0]).chatLocalHandler().GetThreadNonblock(ctx,
  3818  			chat1.GetThreadNonblockArg{
  3819  				ConversationID:   conv.Id,
  3820  				IdentifyBehavior: keybase1.TLFIdentifyBehavior_CHAT_CLI,
  3821  				Pagination:       utils.PresentPagination(&chat1.Pagination{Num: 1}),
  3822  			},
  3823  		)
  3824  		require.NoError(t, err)
  3825  		res := receiveThreadResult(t, ui.ThreadCb)
  3826  		require.Equal(t, 1, len(res.Messages))
  3827  		select {
  3828  		case <-bgConvLoads:
  3829  		case <-time.After(20 * time.Second):
  3830  			require.Fail(t, "no bkg load")
  3831  		}
  3832  		select {
  3833  		case <-bgConvLoads:
  3834  			require.Fail(t, "no more bg loads")
  3835  		default:
  3836  		}
  3837  	})
  3838  }
  3839  
  3840  func TestChatSrvGetThreadNonblock(t *testing.T) {
  3841  	runWithMemberTypes(t, func(mt chat1.ConversationMembersType) {
  3842  		ctc := makeChatTestContext(t, "GetThreadNonblock", 1)
  3843  		defer ctc.cleanup()
  3844  		users := ctc.users()
  3845  
  3846  		ui := kbtest.NewChatUI()
  3847  		ctc.as(t, users[0]).h.mockChatUI = ui
  3848  
  3849  		t.Logf("test empty thread")
  3850  		query := chat1.GetThreadQuery{
  3851  			MessageTypes: []chat1.MessageType{chat1.MessageType_TEXT},
  3852  		}
  3853  		ctx := ctc.as(t, users[0]).startCtx
  3854  		conv := mustCreateConversationForTest(t, ctc, users[0], chat1.TopicType_CHAT, mt)
  3855  		_, err := ctc.as(t, users[0]).chatLocalHandler().GetThreadNonblock(ctx,
  3856  			chat1.GetThreadNonblockArg{
  3857  				ConversationID:   conv.Id,
  3858  				IdentifyBehavior: keybase1.TLFIdentifyBehavior_CHAT_CLI,
  3859  				Query:            &query,
  3860  			},
  3861  		)
  3862  		require.NoError(t, err)
  3863  		res := receiveThreadResult(t, ui.ThreadCb)
  3864  		require.Zero(t, len(res.Messages))
  3865  
  3866  		t.Logf("send a bunch of messages")
  3867  		numMsgs := 20
  3868  		msg := chat1.NewMessageBodyWithText(chat1.MessageText{Body: "hi"})
  3869  		for i := 0; i < numMsgs; i++ {
  3870  			mustPostLocalForTest(t, ctc, users[0], conv, msg)
  3871  		}
  3872  
  3873  		t.Logf("read back full thread")
  3874  		_, err = ctc.as(t, users[0]).chatLocalHandler().GetThreadNonblock(ctx,
  3875  			chat1.GetThreadNonblockArg{
  3876  				ConversationID:   conv.Id,
  3877  				IdentifyBehavior: keybase1.TLFIdentifyBehavior_CHAT_CLI,
  3878  				Query:            &query,
  3879  			},
  3880  		)
  3881  		require.NoError(t, err)
  3882  		res = receiveThreadResult(t, ui.ThreadCb)
  3883  		require.Equal(t, numMsgs, len(res.Messages))
  3884  
  3885  		t.Logf("read back with a delay on the local pull")
  3886  
  3887  		delay := 10 * time.Minute
  3888  		clock := clockwork.NewFakeClock()
  3889  		tc := ctc.world.Tcs[users[0].Username]
  3890  		ri := ctc.as(t, users[0]).ri
  3891  		uiThreadLoader := NewUIThreadLoader(tc.Context(), func() chat1.RemoteInterface { return ri })
  3892  		uiThreadLoader.clock = clock
  3893  		uiThreadLoader.cachedThreadDelay = &delay
  3894  		tc.ChatG.UIThreadLoader = uiThreadLoader
  3895  		_, err = ctc.as(t, users[0]).chatLocalHandler().GetThreadNonblock(ctx,
  3896  			chat1.GetThreadNonblockArg{
  3897  				ConversationID:   conv.Id,
  3898  				IdentifyBehavior: keybase1.TLFIdentifyBehavior_CHAT_CLI,
  3899  				Query:            &query,
  3900  			},
  3901  		)
  3902  		require.NoError(t, err)
  3903  		res = receiveThreadResult(t, ui.ThreadCb)
  3904  		require.Equal(t, numMsgs, len(res.Messages))
  3905  		clock.Advance(20 * time.Minute)
  3906  		select {
  3907  		case <-ui.ThreadCb:
  3908  			require.Fail(t, "no cb expected")
  3909  		default:
  3910  		}
  3911  	})
  3912  }
  3913  
  3914  func TestChatSrvGetThreadNonblockError(t *testing.T) {
  3915  	runWithMemberTypes(t, func(mt chat1.ConversationMembersType) {
  3916  		ctc := makeChatTestContext(t, "GetThreadNonblock", 1)
  3917  		defer ctc.cleanup()
  3918  		users := ctc.users()
  3919  
  3920  		listener := newServerChatListener()
  3921  		ctc.as(t, users[0]).h.G().NotifyRouter.AddListener(listener)
  3922  
  3923  		uid := users[0].User.GetUID().ToBytes()
  3924  		ui := kbtest.NewChatUI()
  3925  		ctc.as(t, users[0]).h.mockChatUI = ui
  3926  
  3927  		query := chat1.GetThreadQuery{
  3928  			MessageTypes: []chat1.MessageType{chat1.MessageType_TEXT},
  3929  		}
  3930  		conv := mustCreateConversationForTest(t, ctc, users[0], chat1.TopicType_CHAT, mt)
  3931  		numMsgs := 20
  3932  		msg := chat1.NewMessageBodyWithText(chat1.MessageText{Body: "hi"})
  3933  		for i := 0; i < numMsgs; i++ {
  3934  			mustPostLocalForTest(t, ctc, users[0], conv, msg)
  3935  		}
  3936  		require.NoError(t,
  3937  			ctc.world.Tcs[users[0].Username].ChatG.ConvSource.Clear(context.TODO(), conv.Id, uid, nil))
  3938  		g := ctc.world.Tcs[users[0].Username].ChatG
  3939  		ri := ctc.as(t, users[0]).ri
  3940  		g.UIThreadLoader.(*UIThreadLoader).SetRemoteInterface(func() chat1.RemoteInterface {
  3941  			return chat1.RemoteClient{Cli: errorClient{}}
  3942  		})
  3943  
  3944  		ctx := ctc.as(t, users[0]).startCtx
  3945  		_, err := ctc.as(t, users[0]).chatLocalHandler().GetThreadNonblock(ctx,
  3946  			chat1.GetThreadNonblockArg{
  3947  				ConversationID:   conv.Id,
  3948  				IdentifyBehavior: keybase1.TLFIdentifyBehavior_CHAT_CLI,
  3949  				Query:            &query,
  3950  			},
  3951  		)
  3952  		require.Error(t, err)
  3953  
  3954  		// Advance clock and look for stale
  3955  		g.UIThreadLoader.(*UIThreadLoader).SetRemoteInterface(func() chat1.RemoteInterface { return ri })
  3956  		ctc.world.Fc.Advance(time.Hour)
  3957  
  3958  		updates := consumeNewThreadsStale(t, listener)
  3959  		require.Equal(t, 1, len(updates))
  3960  		require.Equal(t, chat1.StaleUpdateType_NEWACTIVITY, updates[0].UpdateType)
  3961  	})
  3962  }
  3963  
  3964  var errGetInboxNonblockFailingUI = errors.New("get outta here")
  3965  
  3966  type getInboxNonblockFailingUI struct {
  3967  	*kbtest.ChatUI
  3968  	failUnverified, failVerified bool
  3969  }
  3970  
  3971  func (u *getInboxNonblockFailingUI) ChatInboxUnverified(ctx context.Context,
  3972  	arg chat1.ChatInboxUnverifiedArg) error {
  3973  	if u.failUnverified {
  3974  		return errGetInboxNonblockFailingUI
  3975  	}
  3976  	return u.ChatUI.ChatInboxUnverified(ctx, arg)
  3977  }
  3978  
  3979  func (u *getInboxNonblockFailingUI) ChatInboxConversation(ctx context.Context,
  3980  	arg chat1.ChatInboxConversationArg) error {
  3981  	if u.failVerified {
  3982  		return errGetInboxNonblockFailingUI
  3983  	}
  3984  	return u.ChatUI.ChatInboxConversation(ctx, arg)
  3985  }
  3986  
  3987  func TestChatSrvGetInboxNonblockChatUIError(t *testing.T) {
  3988  	useRemoteMock = false
  3989  	defer func() { useRemoteMock = true }()
  3990  	ctc := makeChatTestContext(t, "TestChatSrvGetInboxNonblockChatUIError", 2)
  3991  	defer ctc.cleanup()
  3992  
  3993  	timeout := 2 * time.Second
  3994  	users := ctc.users()
  3995  	tc := ctc.world.Tcs[users[0].Username]
  3996  	ctx := ctc.as(t, users[0]).startCtx
  3997  	tui := kbtest.NewChatUI()
  3998  	ui := &getInboxNonblockFailingUI{ChatUI: tui, failUnverified: true, failVerified: true}
  3999  	ctc.as(t, users[0]).h.mockChatUI = ui
  4000  	tc.G.UIRouter = kbtest.NewMockUIRouter(ui)
  4001  	uid := gregor1.UID(users[0].GetUID().ToBytes())
  4002  	tc.ChatG.UIInboxLoader = NewUIInboxLoader(tc.Context())
  4003  	tc.ChatG.UIInboxLoader.Start(ctx, uid)
  4004  	defer func() { <-tc.ChatG.UIInboxLoader.Stop(ctx) }()
  4005  
  4006  	listener0 := newServerChatListener()
  4007  	ctc.as(t, users[0]).h.G().NotifyRouter.AddListener(listener0)
  4008  
  4009  	conv := mustCreateConversationForTest(t, ctc, users[0], chat1.TopicType_CHAT,
  4010  		chat1.ConversationMembersType_IMPTEAMNATIVE)
  4011  	mustPostLocalForTest(t, ctc, users[0], conv, chat1.NewMessageBodyWithText(chat1.MessageText{
  4012  		Body: "HIIHIHIHI",
  4013  	}))
  4014  	_, err := ctc.as(t, users[0]).chatLocalHandler().GetInboxNonblockLocal(ctx,
  4015  		chat1.GetInboxNonblockLocalArg{
  4016  			Query: &chat1.GetInboxLocalQuery{
  4017  				ConvIDs: []chat1.ConversationID{conv.Id},
  4018  			},
  4019  		})
  4020  	require.NoError(t, err)
  4021  	waitForThreadStale := func() {
  4022  		for i := 0; i < 5; i++ {
  4023  			tc.Context().FetchRetrier.Force(ctx)
  4024  			select {
  4025  			case <-listener0.threadsStale:
  4026  				return
  4027  			case <-time.After(timeout):
  4028  				t.Logf("no threads stale yet, trying again")
  4029  			}
  4030  		}
  4031  		require.Fail(t, "no threads stale")
  4032  	}
  4033  	waitForThreadStale()
  4034  
  4035  	_, err = ctc.as(t, users[0]).chatLocalHandler().GetInboxNonblockLocal(ctx,
  4036  		chat1.GetInboxNonblockLocalArg{})
  4037  	require.NoError(t, err)
  4038  	tc.Context().FetchRetrier.Force(ctx)
  4039  	select {
  4040  	case <-listener0.inboxStale:
  4041  	case <-time.After(timeout):
  4042  		require.Fail(t, "no inbox stale")
  4043  	}
  4044  
  4045  	ui.failUnverified = false
  4046  	_, err = ctc.as(t, users[0]).chatLocalHandler().GetInboxNonblockLocal(ctx,
  4047  		chat1.GetInboxNonblockLocalArg{
  4048  			Query: &chat1.GetInboxLocalQuery{
  4049  				ConvIDs: []chat1.ConversationID{conv.Id},
  4050  			},
  4051  			IdentifyBehavior: keybase1.TLFIdentifyBehavior_CHAT_CLI,
  4052  		})
  4053  	require.NoError(t, err)
  4054  	select {
  4055  	case <-ui.InboxCb:
  4056  	case <-time.After(timeout):
  4057  		require.Fail(t, "no untrusted inbox")
  4058  	}
  4059  	tc.Context().FetchRetrier.Force(ctx)
  4060  	select {
  4061  	case upds := <-listener0.threadsStale:
  4062  		require.Equal(t, 1, len(upds))
  4063  		require.Equal(t, conv.Id, upds[0].ConvID)
  4064  	case <-time.After(timeout):
  4065  		require.Fail(t, "no conv stale")
  4066  	}
  4067  }
  4068  
  4069  func TestChatSrvGetInboxNonblockError(t *testing.T) {
  4070  	runWithMemberTypes(t, func(mt chat1.ConversationMembersType) {
  4071  		ctc := makeChatTestContext(t, "GetInboxNonblockLocal", 1)
  4072  		defer ctc.cleanup()
  4073  		users := ctc.users()
  4074  
  4075  		listener := newServerChatListener()
  4076  		ctc.as(t, users[0]).h.G().NotifyRouter.AddListener(listener)
  4077  
  4078  		timeout := 2 * time.Second
  4079  		ctx := ctc.as(t, users[0]).startCtx
  4080  		uid := users[0].User.GetUID().ToBytes()
  4081  		ui := kbtest.NewChatUI()
  4082  		ctc.as(t, users[0]).h.mockChatUI = ui
  4083  		tc := ctc.world.Tcs[users[0].Username]
  4084  		tc.G.UIRouter = kbtest.NewMockUIRouter(ui)
  4085  		<-ctc.as(t, users[0]).h.G().ConvLoader.Stop(ctx)
  4086  		listener0 := newServerChatListener()
  4087  		ctc.as(t, users[0]).h.G().NotifyRouter.AddListener(listener0)
  4088  		tc.ChatG.UIInboxLoader = NewUIInboxLoader(tc.Context())
  4089  		tc.ChatG.UIInboxLoader.Start(ctx, uid)
  4090  		defer func() { <-tc.ChatG.UIInboxLoader.Stop(ctx) }()
  4091  
  4092  		conv := mustCreateConversationForTest(t, ctc, users[0], chat1.TopicType_CHAT, mt)
  4093  		numMsgs := 20
  4094  		msg := chat1.NewMessageBodyWithText(chat1.MessageText{Body: "hi"})
  4095  		for i := 0; i < numMsgs; i++ {
  4096  			mustPostLocalForTest(t, ctc, users[0], conv, msg)
  4097  			consumeNewMsgRemote(t, listener0, chat1.MessageType_TEXT)
  4098  		}
  4099  		g := ctc.world.Tcs[users[0].Username].Context()
  4100  		g.ConvSource.SetRemoteInterface(func() chat1.RemoteInterface {
  4101  			return chat1.RemoteClient{Cli: errorClient{}}
  4102  		})
  4103  		require.NoError(t,
  4104  			ctc.world.Tcs[users[0].Username].ChatG.ConvSource.Clear(context.TODO(), conv.Id, uid, nil))
  4105  		ri := ctc.as(t, users[0]).ri
  4106  
  4107  		_, err := ctc.as(t, users[0]).chatLocalHandler().GetInboxNonblockLocal(ctx,
  4108  			chat1.GetInboxNonblockLocalArg{
  4109  				Query: &chat1.GetInboxLocalQuery{
  4110  					ConvIDs: []chat1.ConversationID{conv.Id},
  4111  				},
  4112  				IdentifyBehavior: keybase1.TLFIdentifyBehavior_CHAT_CLI,
  4113  			})
  4114  		require.NoError(t, err)
  4115  
  4116  		// Eat untrusted CB
  4117  		select {
  4118  		case <-ui.InboxCb:
  4119  		case <-time.After(20 * time.Second):
  4120  			require.Fail(t, "no untrusted inbox")
  4121  		}
  4122  
  4123  		select {
  4124  		case nbres := <-ui.InboxCb:
  4125  			require.Error(t, nbres.Err)
  4126  		case <-time.After(20 * time.Second):
  4127  			require.Fail(t, "no inbox load event")
  4128  		}
  4129  
  4130  		// Advance clock and look for stale
  4131  		g.ConvSource.SetRemoteInterface(func() chat1.RemoteInterface {
  4132  			return ri
  4133  		})
  4134  		waitForThreadsStale := func() {
  4135  			for i := 0; i < 5; i++ {
  4136  				tc.Context().FetchRetrier.Force(ctx)
  4137  				select {
  4138  				case updates := <-listener0.threadsStale:
  4139  					require.Equal(t, 1, len(updates))
  4140  					require.Equal(t, chat1.StaleUpdateType_NEWACTIVITY, updates[0].UpdateType)
  4141  					return
  4142  				case <-time.After(timeout):
  4143  					t.Logf("no threads stale yet, trying again")
  4144  				}
  4145  			}
  4146  		}
  4147  		waitForThreadsStale()
  4148  
  4149  		t.Logf("testing untrusted inbox load failure")
  4150  		ttype := chat1.TopicType_CHAT
  4151  		require.NoError(t, storage.NewInbox(g).Clear(context.TODO(), uid))
  4152  		g.InboxSource.SetRemoteInterface(func() chat1.RemoteInterface {
  4153  			return chat1.RemoteClient{Cli: errorClient{}}
  4154  		})
  4155  		query := &chat1.GetInboxLocalQuery{
  4156  			TopicType: &ttype,
  4157  		}
  4158  		_, err = ctc.as(t, users[0]).chatLocalHandler().GetInboxNonblockLocal(ctx,
  4159  			chat1.GetInboxNonblockLocalArg{
  4160  				Query:            query,
  4161  				IdentifyBehavior: keybase1.TLFIdentifyBehavior_CHAT_CLI,
  4162  			})
  4163  		require.Error(t, err)
  4164  		g.InboxSource.SetRemoteInterface(func() chat1.RemoteInterface {
  4165  			return ri
  4166  		})
  4167  		ctc.world.Fc.Advance(time.Hour)
  4168  		select {
  4169  		case <-listener.inboxStale:
  4170  		case <-time.After(20 * time.Second):
  4171  			require.Fail(t, "no threads stale message received")
  4172  		}
  4173  
  4174  		rquery, _, err := g.InboxSource.GetInboxQueryLocalToRemote(context.TODO(), query)
  4175  		require.NoError(t, err)
  4176  		_, lconvs, err := storage.NewInbox(g).Read(context.TODO(), uid, rquery)
  4177  		require.NoError(t, err)
  4178  		require.Equal(t, 1, len(lconvs))
  4179  		require.Equal(t, lconvs[0].GetConvID(), conv.Id)
  4180  	})
  4181  }
  4182  
  4183  func TestChatSrvMakePreview(t *testing.T) {
  4184  	ctc := makeChatTestContext(t, "MakePreview", 1)
  4185  	defer ctc.cleanup()
  4186  	user := ctc.users()[0]
  4187  
  4188  	// make a preview of a jpg
  4189  	outboxID, err := storage.NewOutboxID()
  4190  	require.NoError(t, err)
  4191  	arg := chat1.MakePreviewArg{
  4192  		Filename: "testdata/ship.jpg",
  4193  		OutboxID: outboxID,
  4194  	}
  4195  	ri := ctc.as(t, user).ri
  4196  	tc := ctc.world.Tcs[user.Username]
  4197  	tc.ChatG.AttachmentURLSrv = NewAttachmentHTTPSrv(tc.Context(),
  4198  		manager.NewSrv(tc.Context().ExternalG()),
  4199  		types.DummyAttachmentFetcher{},
  4200  		func() chat1.RemoteInterface { return ri })
  4201  	res, err := ctc.as(t, user).chatLocalHandler().MakePreview(context.TODO(), arg)
  4202  	require.NoError(t, err)
  4203  	require.NotNil(t, res.Location)
  4204  	typ, err := res.Location.Ltyp()
  4205  	require.NoError(t, err)
  4206  	require.Equal(t, chat1.PreviewLocationTyp_URL, typ)
  4207  	resp, err := http.Get(res.Location.Url())
  4208  	require.NoError(t, err)
  4209  	require.Equal(t, 200, resp.StatusCode)
  4210  	require.NotNil(t, res.Metadata)
  4211  	require.Equal(t, "image/jpeg", res.MimeType)
  4212  	img := res.Metadata.Image()
  4213  	require.Equal(t, 640, img.Width)
  4214  	require.Equal(t, 480, img.Height)
  4215  
  4216  	// MakePreview(pdf) shouldn't generate a preview file, but should return mimetype
  4217  	outboxID, err = storage.NewOutboxID()
  4218  	require.NoError(t, err)
  4219  	arg = chat1.MakePreviewArg{
  4220  		Filename: "testdata/weather.pdf",
  4221  		OutboxID: outboxID,
  4222  	}
  4223  	res, err = ctc.as(t, user).chatLocalHandler().MakePreview(context.TODO(), arg)
  4224  	require.NoError(t, err)
  4225  	require.Nil(t, res.Location)
  4226  	require.Nil(t, res.Metadata)
  4227  	require.Equal(t, "application/pdf", res.MimeType)
  4228  }
  4229  
  4230  func inMessageTypes(x chat1.MessageType, ys []chat1.MessageType) bool {
  4231  	for _, y := range ys {
  4232  		if x == y {
  4233  			return true
  4234  		}
  4235  	}
  4236  	return false
  4237  }
  4238  
  4239  func consumeNewPendingMsg(t *testing.T, listener *serverChatListener) {
  4240  	select {
  4241  	case msg := <-listener.newMessageLocal:
  4242  		require.True(t, msg.Message.IsOutbox())
  4243  	case <-time.After(20 * time.Second):
  4244  		require.Fail(t, "failed to get new pending message notification")
  4245  	}
  4246  }
  4247  
  4248  func consumeNewMsgLocal(t *testing.T, listener *serverChatListener, typ chat1.MessageType) chat1.UIMessage {
  4249  	return consumeNewMsgWhileIgnoring(t, listener, typ, nil, chat1.ChatActivitySource_LOCAL)
  4250  }
  4251  
  4252  func consumeNewMsgRemote(t *testing.T, listener *serverChatListener, typ chat1.MessageType) chat1.UIMessage {
  4253  	return consumeNewMsgWhileIgnoring(t, listener, typ, nil, chat1.ChatActivitySource_REMOTE)
  4254  }
  4255  
  4256  func consumeNewMsgWhileIgnoring(t *testing.T, listener *serverChatListener, typ chat1.MessageType,
  4257  	ignoreTypes []chat1.MessageType, source chat1.ChatActivitySource) chat1.UIMessage {
  4258  	require.False(t, inMessageTypes(typ, ignoreTypes), "can't ignore the hunted")
  4259  	timeoutCh := time.After(20 * time.Second)
  4260  	var newMsgCh chan chat1.IncomingMessage
  4261  	switch source {
  4262  	case chat1.ChatActivitySource_LOCAL:
  4263  		newMsgCh = listener.newMessageLocal
  4264  	case chat1.ChatActivitySource_REMOTE:
  4265  		newMsgCh = listener.newMessageRemote
  4266  	}
  4267  	for {
  4268  		select {
  4269  		case msg := <-newMsgCh:
  4270  			rtyp := msg.Message.GetMessageType()
  4271  			ignore := inMessageTypes(rtyp, ignoreTypes)
  4272  			ignoredStr := ""
  4273  			if ignore {
  4274  				ignoredStr = " (ignored)"
  4275  			}
  4276  			t.Logf("consumed newMessage(%v): %v%v", source, msg.Message.GetMessageType(), ignoredStr)
  4277  			if !ignore {
  4278  				require.Equal(t, typ, msg.Message.GetMessageType())
  4279  				return msg.Message
  4280  			}
  4281  		case <-timeoutCh:
  4282  			require.Fail(t, fmt.Sprintf("failed to get newMessage %v notification: %v", source, typ))
  4283  			return chat1.UIMessage{}
  4284  		}
  4285  	}
  4286  }
  4287  
  4288  func consumeNewThreadsStale(t *testing.T, listener *serverChatListener) []chat1.ConversationStaleUpdate {
  4289  	select {
  4290  	case updates := <-listener.threadsStale:
  4291  		return updates
  4292  	case <-time.After(20 * time.Second):
  4293  		require.Fail(t, "no threads stale message received")
  4294  	}
  4295  	return nil
  4296  }
  4297  
  4298  func assertNoNewConversation(t *testing.T, listener *serverChatListener) {
  4299  	select {
  4300  	case <-listener.newConversation:
  4301  		require.Fail(t, "unexpected newConversation")
  4302  	default:
  4303  	}
  4304  }
  4305  
  4306  func consumeNewConversation(t *testing.T, listener *serverChatListener, convID chat1.ConversationID) {
  4307  	select {
  4308  	case convInfo := <-listener.newConversation:
  4309  		require.Equal(t, convID, convInfo.ConvID)
  4310  		require.NotNil(t, convInfo.Conv)
  4311  	case <-time.After(20 * time.Second):
  4312  		require.Fail(t, "failed to get new conversation notification")
  4313  	}
  4314  }
  4315  
  4316  func consumeTeamType(t *testing.T, listener *serverChatListener) {
  4317  	select {
  4318  	case <-listener.teamType:
  4319  	case <-time.After(20 * time.Second):
  4320  		require.Fail(t, "failed to get team type notification")
  4321  	}
  4322  }
  4323  
  4324  func consumeMembersUpdate(t *testing.T, listener *serverChatListener) {
  4325  	select {
  4326  	case <-listener.membersUpdate:
  4327  	case <-time.After(20 * time.Second):
  4328  		require.Fail(t, "failed to get members update notification")
  4329  	}
  4330  }
  4331  
  4332  func consumeJoinConv(t *testing.T, listener *serverChatListener) {
  4333  	select {
  4334  	case <-listener.joinedConv:
  4335  	case <-time.After(20 * time.Second):
  4336  		require.Fail(t, "failed to get join conv notification")
  4337  	}
  4338  }
  4339  
  4340  func consumeLeaveConv(t *testing.T, listener *serverChatListener) {
  4341  	select {
  4342  	case <-listener.leftConv:
  4343  	case <-time.After(20 * time.Second):
  4344  		require.Fail(t, "failed to get leave conv notification")
  4345  	}
  4346  }
  4347  
  4348  func consumeSetConvRetention(t *testing.T, listener *serverChatListener) chat1.ConversationID {
  4349  	select {
  4350  	case x := <-listener.setConvRetention:
  4351  		return x
  4352  	case <-time.After(20 * time.Second):
  4353  		require.Fail(t, "failed to get setConvRetention notification")
  4354  		return chat1.ConversationID{}
  4355  	}
  4356  }
  4357  
  4358  func consumeSetTeamRetention(t *testing.T, listener *serverChatListener) (res keybase1.TeamID) {
  4359  	select {
  4360  	case x := <-listener.setTeamRetention:
  4361  		return x
  4362  	case <-time.After(20 * time.Second):
  4363  		require.Fail(t, "failed to get setTeamRetention notification")
  4364  		return res
  4365  	}
  4366  }
  4367  
  4368  func consumeSetConvSettings(t *testing.T, listener *serverChatListener) chat1.ConversationID {
  4369  	select {
  4370  	case x := <-listener.setConvSettings:
  4371  		return x
  4372  	case <-time.After(20 * time.Second):
  4373  		require.Fail(t, "failed to get setConvSettings notification")
  4374  		return chat1.ConversationID{}
  4375  	}
  4376  }
  4377  
  4378  func consumeSubteamRename(t *testing.T, listener *serverChatListener) []chat1.ConversationID {
  4379  	select {
  4380  	case x := <-listener.subteamRename:
  4381  		return x
  4382  	case <-time.After(20 * time.Second):
  4383  		require.Fail(t, "failed to get subteamRename notification")
  4384  		return nil
  4385  	}
  4386  }
  4387  
  4388  func consumeExpunge(t *testing.T, listener *serverChatListener) chat1.ExpungeInfo {
  4389  	select {
  4390  	case x := <-listener.expunge:
  4391  		return x
  4392  	case <-time.After(20 * time.Second):
  4393  		require.Fail(t, "failed to get expunge notification")
  4394  		return chat1.ExpungeInfo{}
  4395  	}
  4396  }
  4397  
  4398  func consumeReactionUpdate(t *testing.T, listener *serverChatListener) chat1.ReactionUpdateNotif {
  4399  	select {
  4400  	case x := <-listener.reactionUpdate:
  4401  		return x
  4402  	case <-time.After(20 * time.Second):
  4403  		require.Fail(t, "failed to get reactionUpdate notification")
  4404  		return chat1.ReactionUpdateNotif{}
  4405  	}
  4406  }
  4407  
  4408  func TestChatSrvTeamChannels(t *testing.T) {
  4409  	runWithMemberTypes(t, func(mt chat1.ConversationMembersType) {
  4410  		ctc := makeChatTestContext(t, "TestChatTeamChannels", 3)
  4411  		defer ctc.cleanup()
  4412  		users := ctc.users()
  4413  
  4414  		// Only run this test for teams
  4415  		switch mt {
  4416  		case chat1.ConversationMembersType_TEAM:
  4417  		default:
  4418  			return
  4419  		}
  4420  
  4421  		ctx := ctc.as(t, users[0]).startCtx
  4422  		ctx1 := ctc.as(t, users[1]).startCtx
  4423  		ctx2 := ctc.as(t, users[2]).startCtx
  4424  
  4425  		listener0 := newServerChatListener()
  4426  		ctc.as(t, users[0]).h.G().NotifyRouter.AddListener(listener0)
  4427  		ctc.world.Tcs[users[0].Username].ChatG.Syncer.(*Syncer).isConnected = true
  4428  
  4429  		listener1 := newServerChatListener()
  4430  		ctc.as(t, users[1]).h.G().NotifyRouter.AddListener(listener1)
  4431  		ctc.world.Tcs[users[1].Username].ChatG.Syncer.(*Syncer).isConnected = true
  4432  
  4433  		listener2 := newServerChatListener()
  4434  		ctc.as(t, users[2]).h.G().NotifyRouter.AddListener(listener2)
  4435  		ctc.world.Tcs[users[2].Username].ChatG.Syncer.(*Syncer).isConnected = true
  4436  
  4437  		conv := mustCreateConversationForTest(t, ctc, users[0], chat1.TopicType_CHAT,
  4438  			mt, ctc.as(t, users[1]).user(), ctc.as(t, users[2]).user())
  4439  		t.Logf("first conv: %s", conv.Id)
  4440  		consumeNewConversation(t, listener0, conv.Id)
  4441  		consumeNewConversation(t, listener1, conv.Id)
  4442  		consumeNewConversation(t, listener2, conv.Id)
  4443  
  4444  		t.Logf("create a conversation, and join user 1 into by sending a message")
  4445  		topicName := "zjoinonsend"
  4446  		ncres, err := ctc.as(t, users[0]).chatLocalHandler().NewConversationLocal(ctx,
  4447  			chat1.NewConversationLocalArg{
  4448  				TlfName:       conv.TlfName,
  4449  				TopicName:     &topicName,
  4450  				TopicType:     chat1.TopicType_CHAT,
  4451  				TlfVisibility: keybase1.TLFVisibility_PRIVATE,
  4452  				MembersType:   chat1.ConversationMembersType_TEAM,
  4453  			})
  4454  		require.NoError(t, err)
  4455  		consumeNewConversation(t, listener0, ncres.Conv.GetConvID())
  4456  		assertNoNewConversation(t, listener1)
  4457  		assertNoNewConversation(t, listener2)
  4458  		consumeNewMsgRemote(t, listener0, chat1.MessageType_JOIN)
  4459  		consumeNewMsgRemote(t, listener0, chat1.MessageType_SYSTEM)
  4460  		consumeNewMsgRemote(t, listener0, chat1.MessageType_SYSTEM)
  4461  		consumeNewMsgRemote(t, listener1, chat1.MessageType_SYSTEM)
  4462  		consumeNewMsgRemote(t, listener1, chat1.MessageType_SYSTEM)
  4463  		consumeNewMsgRemote(t, listener2, chat1.MessageType_SYSTEM)
  4464  		consumeNewMsgRemote(t, listener2, chat1.MessageType_SYSTEM)
  4465  		_, err = postLocalForTest(t, ctc, users[1], ncres.Conv.Info, chat1.NewMessageBodyWithText(chat1.MessageText{
  4466  			Body: "JOINME",
  4467  		}))
  4468  		require.NoError(t, err)
  4469  		consumeAllMsgJoins := func(listener *serverChatListener) {
  4470  			msgMap := make(map[chat1.MessageType]bool)
  4471  			rounds := 2
  4472  			for i := 0; i < rounds; i++ {
  4473  				select {
  4474  				case msg := <-listener.newMessageRemote:
  4475  					t.Logf("recvd: %v convID: %s", msg.Message.GetMessageType(), msg.ConvID)
  4476  					msgMap[msg.Message.GetMessageType()] = true
  4477  				case <-time.After(20 * time.Second):
  4478  					require.Fail(t, "missing incoming")
  4479  				}
  4480  			}
  4481  			require.True(t, msgMap[chat1.MessageType_TEXT])
  4482  			require.True(t, msgMap[chat1.MessageType_JOIN])
  4483  		}
  4484  		consumeAllMsgJoins(listener0)
  4485  		consumeAllMsgJoins(listener1)
  4486  		select {
  4487  		case conv := <-listener1.joinedConv:
  4488  			require.Equal(t, conv.GetConvID(), ncres.Conv.GetConvID())
  4489  			require.Equal(t, topicName, conv.Channel)
  4490  		case <-time.After(20 * time.Second):
  4491  			require.Fail(t, "failed to get joined notification")
  4492  		}
  4493  		select {
  4494  		case act := <-listener0.membersUpdate:
  4495  			require.Equal(t, act.ConvID, ncres.Conv.GetConvID())
  4496  			require.Equal(t, 1, len(act.Members))
  4497  			require.Equal(t, chat1.ConversationMemberStatus_ACTIVE, act.Members[0].Status)
  4498  			require.Equal(t, users[1].Username, act.Members[0].Member)
  4499  		case <-time.After(20 * time.Second):
  4500  			require.Fail(t, "failed to get members update")
  4501  		}
  4502  
  4503  		t.Logf("send headline on first convo")
  4504  		headline := "The headline is foobar!"
  4505  		_, err = postLocalForTest(t, ctc, users[1], ncres.Conv.Info, chat1.NewMessageBodyWithHeadline(chat1.MessageHeadline{Headline: headline}))
  4506  		require.NoError(t, err)
  4507  		consumeNewMsgRemote(t, listener0, chat1.MessageType_HEADLINE)
  4508  		consumeNewMsgRemote(t, listener1, chat1.MessageType_HEADLINE)
  4509  
  4510  		t.Logf("create a new channel, and check GetTLFConversation result")
  4511  		topicName = "miketime"
  4512  		ncres, err = ctc.as(t, users[0]).chatLocalHandler().NewConversationLocal(ctx,
  4513  			chat1.NewConversationLocalArg{
  4514  				TlfName:       conv.TlfName,
  4515  				TopicName:     &topicName,
  4516  				TopicType:     chat1.TopicType_CHAT,
  4517  				TlfVisibility: keybase1.TLFVisibility_PRIVATE,
  4518  				MembersType:   chat1.ConversationMembersType_TEAM,
  4519  			})
  4520  		require.NoError(t, err)
  4521  		consumeNewConversation(t, listener0, ncres.Conv.GetConvID())
  4522  		assertNoNewConversation(t, listener1)
  4523  		assertNoNewConversation(t, listener2)
  4524  		consumeNewMsgRemote(t, listener0, chat1.MessageType_JOIN)
  4525  		consumeNewMsgRemote(t, listener0, chat1.MessageType_SYSTEM)
  4526  		consumeNewMsgRemote(t, listener1, chat1.MessageType_SYSTEM)
  4527  		consumeNewMsgRemote(t, listener2, chat1.MessageType_SYSTEM)
  4528  		getTLFRes, err := ctc.as(t, users[1]).chatLocalHandler().GetTLFConversationsLocal(ctx1,
  4529  			chat1.GetTLFConversationsLocalArg{
  4530  				TlfName:     conv.TlfName,
  4531  				TopicType:   chat1.TopicType_CHAT,
  4532  				MembersType: chat1.ConversationMembersType_TEAM,
  4533  			})
  4534  		require.NoError(t, err)
  4535  		require.Equal(t, 3, len(getTLFRes.Convs))
  4536  		require.Equal(t, globals.DefaultTeamTopic, getTLFRes.Convs[0].Channel)
  4537  		require.Equal(t, topicName, getTLFRes.Convs[1].Channel)
  4538  		creatorInfo := getTLFRes.Convs[2].CreatorInfo
  4539  		require.NotNil(t, creatorInfo)
  4540  		require.Equal(t, creatorInfo.Username, users[0].Username)
  4541  		tvres, err := ctc.as(t, users[0]).chatLocalHandler().GetThreadLocal(ctx, chat1.GetThreadLocalArg{
  4542  			ConversationID: getTLFRes.Convs[2].GetConvID(),
  4543  		})
  4544  		require.NoError(t, err)
  4545  		require.Equal(t, headline, tvres.Thread.Messages[0].Valid().MessageBody.Headline().Headline)
  4546  
  4547  		t.Logf("join user 1 into new convo manually")
  4548  		_, err = ctc.as(t, users[1]).chatLocalHandler().JoinConversationLocal(ctx1, chat1.JoinConversationLocalArg{
  4549  			TlfName:    conv.TlfName,
  4550  			TopicType:  chat1.TopicType_CHAT,
  4551  			Visibility: keybase1.TLFVisibility_PRIVATE,
  4552  			TopicName:  topicName,
  4553  		})
  4554  		require.NoError(t, err)
  4555  		consumeNewMsgRemote(t, listener0, chat1.MessageType_JOIN)
  4556  		consumeNewMsgRemote(t, listener1, chat1.MessageType_JOIN)
  4557  		select {
  4558  		case conv := <-listener1.joinedConv:
  4559  			require.Equal(t, conv.GetConvID(), getTLFRes.Convs[1].GetConvID())
  4560  			require.Equal(t, topicName, conv.Channel)
  4561  		case <-time.After(20 * time.Second):
  4562  			require.Fail(t, "failed to get joined notification")
  4563  		}
  4564  		select {
  4565  		case act := <-listener0.membersUpdate:
  4566  			require.Equal(t, act.ConvID, getTLFRes.Convs[1].GetConvID())
  4567  			require.Equal(t, 1, len(act.Members))
  4568  			require.Equal(t, chat1.ConversationMemberStatus_ACTIVE, act.Members[0].Status)
  4569  			require.Equal(t, users[1].Username, act.Members[0].Member)
  4570  		case <-time.After(20 * time.Second):
  4571  			require.Fail(t, "failed to get members update")
  4572  		}
  4573  
  4574  		t.Logf("user 0 removes user 1 from the new convo")
  4575  		_, err = ctc.as(t, users[0]).chatLocalHandler().RemoveFromConversationLocal(ctx, chat1.RemoveFromConversationLocalArg{
  4576  			ConvID:    ncres.Conv.GetConvID(),
  4577  			Usernames: []string{users[1].Username},
  4578  		})
  4579  		require.NoError(t, err)
  4580  		select {
  4581  		case convID := <-listener1.leftConv:
  4582  			require.Equal(t, convID, getTLFRes.Convs[1].GetConvID())
  4583  		case <-time.After(20 * time.Second):
  4584  			require.Fail(t, "failed to get left notification")
  4585  		}
  4586  		select {
  4587  		case act := <-listener0.membersUpdate:
  4588  			require.Equal(t, act.ConvID, getTLFRes.Convs[1].GetConvID())
  4589  			require.Equal(t, 1, len(act.Members))
  4590  			require.Equal(t, chat1.ConversationMemberStatus_REMOVED, act.Members[0].Status)
  4591  			require.Equal(t, users[1].Username, act.Members[0].Member)
  4592  		case <-time.After(20 * time.Second):
  4593  			require.Fail(t, "failed to get members update")
  4594  		}
  4595  
  4596  		t.Logf("rejoin user 1 into new convo manually")
  4597  		_, err = ctc.as(t, users[1]).chatLocalHandler().JoinConversationLocal(ctx1, chat1.JoinConversationLocalArg{
  4598  			TlfName:    conv.TlfName,
  4599  			TopicType:  chat1.TopicType_CHAT,
  4600  			Visibility: keybase1.TLFVisibility_PRIVATE,
  4601  			TopicName:  topicName,
  4602  		})
  4603  		require.NoError(t, err)
  4604  		consumeNewMsgRemote(t, listener0, chat1.MessageType_JOIN)
  4605  		consumeNewMsgRemote(t, listener1, chat1.MessageType_JOIN)
  4606  		select {
  4607  		case conv := <-listener1.joinedConv:
  4608  			require.Equal(t, conv.GetConvID(), getTLFRes.Convs[1].GetConvID())
  4609  			require.Equal(t, topicName, conv.Channel)
  4610  		case <-time.After(20 * time.Second):
  4611  			require.Fail(t, "failed to get joined notification")
  4612  		}
  4613  		select {
  4614  		case act := <-listener0.membersUpdate:
  4615  			require.Equal(t, act.ConvID, getTLFRes.Convs[1].GetConvID())
  4616  			require.Equal(t, 1, len(act.Members))
  4617  			require.Equal(t, chat1.ConversationMemberStatus_ACTIVE, act.Members[0].Status)
  4618  			require.Equal(t, users[1].Username, act.Members[0].Member)
  4619  		case <-time.After(20 * time.Second):
  4620  			require.Fail(t, "failed to get members update")
  4621  		}
  4622  
  4623  		t.Logf("@mention in user2")
  4624  		_, err = postLocalForTest(t, ctc, users[1], ncres.Conv.Info, chat1.NewMessageBodyWithText(chat1.MessageText{
  4625  			Body: fmt.Sprintf("FAIL: @%s", users[2].Username),
  4626  		}))
  4627  		require.NoError(t, err)
  4628  		consumeJoinConv(t, listener2)
  4629  		consumeNewMsgRemote(t, listener0, chat1.MessageType_TEXT)
  4630  		consumeNewMsgRemote(t, listener1, chat1.MessageType_TEXT)
  4631  		consumeNewMsgRemote(t, listener2, chat1.MessageType_TEXT)
  4632  
  4633  		t.Logf("user1 leaves: %s", ncres.Conv.GetConvID())
  4634  		_, err = ctc.as(t, users[1]).chatLocalHandler().LeaveConversationLocal(ctx1,
  4635  			ncres.Conv.GetConvID())
  4636  		require.NoError(t, err)
  4637  		consumeNewMsgRemote(t, listener0, chat1.MessageType_LEAVE)
  4638  		consumeNewMsgRemote(t, listener2, chat1.MessageType_LEAVE)
  4639  		select {
  4640  		case convID := <-listener1.leftConv:
  4641  			require.Equal(t, convID, getTLFRes.Convs[1].GetConvID())
  4642  		case <-time.After(20 * time.Second):
  4643  			require.Fail(t, "failed to get left notification")
  4644  		}
  4645  		select {
  4646  		case act := <-listener0.membersUpdate:
  4647  			require.Equal(t, act.ConvID, getTLFRes.Convs[1].GetConvID())
  4648  			require.Equal(t, 1, len(act.Members))
  4649  			require.Equal(t, chat1.ConversationMemberStatus_REMOVED, act.Members[0].Status)
  4650  			require.Equal(t, users[1].Username, act.Members[0].Member)
  4651  		case <-time.After(20 * time.Second):
  4652  			require.Fail(t, "failed to get members update")
  4653  		}
  4654  
  4655  		_, err = postLocalForTest(t, ctc, users[2], ncres.Conv.Info, chat1.NewMessageBodyWithText(chat1.MessageText{
  4656  			Body: "FAIL",
  4657  		}))
  4658  		require.NoError(t, err)
  4659  		consumeAllMsgJoins(listener0)
  4660  		consumeAllMsgJoins(listener2)
  4661  		select {
  4662  		case conv := <-listener2.joinedConv:
  4663  			require.Equal(t, conv.GetConvID(), getTLFRes.Convs[1].GetConvID())
  4664  			require.Equal(t, topicName, conv.Channel)
  4665  		case <-time.After(20 * time.Second):
  4666  			require.Fail(t, "failed to get joined notification")
  4667  		}
  4668  		select {
  4669  		case act := <-listener0.membersUpdate:
  4670  			require.Equal(t, act.ConvID, getTLFRes.Convs[1].GetConvID())
  4671  			require.Equal(t, 1, len(act.Members))
  4672  			require.Equal(t, chat1.ConversationMemberStatus_ACTIVE, act.Members[0].Status)
  4673  			require.Equal(t, users[2].Username, act.Members[0].Member)
  4674  		case <-time.After(20 * time.Second):
  4675  			require.Fail(t, "failed to get members update")
  4676  		}
  4677  
  4678  		_, err = ctc.as(t, users[1]).chatLocalHandler().JoinConversationLocal(ctx1, chat1.JoinConversationLocalArg{
  4679  			TlfName:    conv.TlfName,
  4680  			TopicType:  chat1.TopicType_CHAT,
  4681  			Visibility: keybase1.TLFVisibility_PRIVATE,
  4682  			TopicName:  topicName,
  4683  		})
  4684  		require.NoError(t, err)
  4685  		consumeNewMsgRemote(t, listener0, chat1.MessageType_JOIN)
  4686  		consumeNewMsgRemote(t, listener1, chat1.MessageType_JOIN)
  4687  		consumeNewMsgRemote(t, listener2, chat1.MessageType_JOIN)
  4688  
  4689  		t.Logf("u2 gets messages and looks for u1's LEAVE message")
  4690  		tvres, err = ctc.as(t, users[2]).chatLocalHandler().GetThreadLocal(ctx, chat1.GetThreadLocalArg{
  4691  			ConversationID: ncres.Conv.GetConvID(),
  4692  			Query: &chat1.GetThreadQuery{
  4693  				MessageTypes: []chat1.MessageType{chat1.MessageType_LEAVE},
  4694  			},
  4695  		})
  4696  		require.NoError(t, err)
  4697  		require.Len(t, tvres.Thread.Messages, 1, "expected number of LEAVE messages")
  4698  
  4699  		t.Logf("u2 leaves and explicitly previews channel")
  4700  		_, err = ctc.as(t, users[2]).chatLocalHandler().LeaveConversationLocal(ctx1,
  4701  			ncres.Conv.GetConvID())
  4702  		require.NoError(t, err)
  4703  		consumeNewMsgRemote(t, listener0, chat1.MessageType_LEAVE)
  4704  		consumeNewMsgRemote(t, listener1, chat1.MessageType_LEAVE)
  4705  		consumeLeaveConv(t, listener2)
  4706  		_, err = ctc.as(t, users[2]).chatLocalHandler().PreviewConversationByIDLocal(ctx2, ncres.Conv.Info.Id)
  4707  		require.NoError(t, err)
  4708  		consumeJoinConv(t, listener2)
  4709  		iboxRes, err := ctc.as(t, users[2]).chatLocalHandler().GetInboxAndUnboxLocal(ctx2,
  4710  			chat1.GetInboxAndUnboxLocalArg{})
  4711  		require.NoError(t, err)
  4712  		require.Equal(t, 2, len(iboxRes.Conversations))
  4713  		for _, conv := range iboxRes.Conversations {
  4714  			if conv.GetConvID().Eq(ncres.Conv.Info.Id) {
  4715  				require.Equal(t, chat1.ConversationMemberStatus_PREVIEW, conv.Info.MemberStatus)
  4716  			} else {
  4717  				require.Equal(t, chat1.ConversationMemberStatus_ACTIVE, conv.Info.MemberStatus)
  4718  			}
  4719  		}
  4720  	})
  4721  }
  4722  
  4723  func TestChatSrvTLFConversationsLocal(t *testing.T) {
  4724  	runWithMemberTypes(t, func(mt chat1.ConversationMembersType) {
  4725  		ctc := makeChatTestContext(t, "TestChatSrvTLFConversationsLocal", 2)
  4726  		defer ctc.cleanup()
  4727  		users := ctc.users()
  4728  
  4729  		// Only run this test for teams
  4730  		switch mt {
  4731  		case chat1.ConversationMembersType_TEAM:
  4732  		default:
  4733  			return
  4734  		}
  4735  
  4736  		ctx := ctc.as(t, users[0]).startCtx
  4737  		ctx1 := ctc.as(t, users[1]).startCtx
  4738  		uid1 := users[1].User.GetUID().ToBytes()
  4739  
  4740  		listener0 := newServerChatListener()
  4741  		ctc.as(t, users[0]).h.G().NotifyRouter.AddListener(listener0)
  4742  		ctc.world.Tcs[users[0].Username].ChatG.Syncer.(*Syncer).isConnected = true
  4743  
  4744  		listener1 := newServerChatListener()
  4745  		ctc.as(t, users[1]).h.G().NotifyRouter.AddListener(listener1)
  4746  		ctc.world.Tcs[users[1].Username].ChatG.Syncer.(*Syncer).isConnected = true
  4747  
  4748  		conv := mustCreateConversationForTest(t, ctc, users[0], chat1.TopicType_CHAT,
  4749  			mt, ctc.as(t, users[1]).user())
  4750  		t.Logf("first conv: %s", conv.Id)
  4751  		t.Logf("create a conversation, and join user 1 into by sending a message")
  4752  		topicName := "zjoinonsend"
  4753  		ncres, err := ctc.as(t, users[0]).chatLocalHandler().NewConversationLocal(ctx,
  4754  			chat1.NewConversationLocalArg{
  4755  				TlfName:       conv.TlfName,
  4756  				TopicName:     &topicName,
  4757  				TopicType:     chat1.TopicType_CHAT,
  4758  				TlfVisibility: keybase1.TLFVisibility_PRIVATE,
  4759  				MembersType:   chat1.ConversationMembersType_TEAM,
  4760  			})
  4761  		require.NoError(t, err)
  4762  
  4763  		_, err = postLocalForTest(t, ctc, users[1], ncres.Conv.Info, chat1.NewMessageBodyWithText(chat1.MessageText{
  4764  			Body: "JOINME",
  4765  		}))
  4766  		require.NoError(t, err)
  4767  
  4768  		getTLFRes, err := ctc.as(t, users[1]).chatLocalHandler().GetTLFConversationsLocal(ctx1,
  4769  			chat1.GetTLFConversationsLocalArg{
  4770  				TlfName:     conv.TlfName,
  4771  				TopicType:   chat1.TopicType_CHAT,
  4772  				MembersType: chat1.ConversationMembersType_TEAM,
  4773  			})
  4774  		require.NoError(t, err)
  4775  		require.Equal(t, 2, len(getTLFRes.Convs))
  4776  		require.Equal(t, globals.DefaultTeamTopic, getTLFRes.Convs[0].Channel)
  4777  		require.Equal(t, chat1.ConversationMemberStatus_ACTIVE, getTLFRes.Convs[1].MemberStatus)
  4778  		parts, err := ctc.world.Tcs[users[1].Username].Context().ParticipantsSource.Get(context.TODO(),
  4779  			uid1, getTLFRes.Convs[1].GetConvID(), types.InboxSourceDataSourceAll)
  4780  		require.NoError(t, err)
  4781  		require.Equal(t, 2, len(parts))
  4782  
  4783  		_, err = ctc.as(t, users[1]).chatLocalHandler().LeaveConversationLocal(ctx1,
  4784  			ncres.Conv.GetConvID())
  4785  		require.NoError(t, err)
  4786  		ignoreTypes := []chat1.MessageType{chat1.MessageType_SYSTEM, chat1.MessageType_JOIN, chat1.MessageType_TEXT}
  4787  		consumeNewMsgWhileIgnoring(t, listener0, chat1.MessageType_LEAVE, ignoreTypes, chat1.ChatActivitySource_REMOTE)
  4788  
  4789  		// make sure both users have processed the leave in their inbox
  4790  		for i, user := range users {
  4791  			getTLFRes, err = ctc.as(t, user).chatLocalHandler().GetTLFConversationsLocal(
  4792  				ctc.as(t, user).startCtx,
  4793  				chat1.GetTLFConversationsLocalArg{
  4794  					TlfName:     conv.TlfName,
  4795  					TopicType:   chat1.TopicType_CHAT,
  4796  					MembersType: chat1.ConversationMembersType_TEAM,
  4797  				})
  4798  			require.NoError(t, err)
  4799  			require.Equal(t, 2, len(getTLFRes.Convs))
  4800  			require.Equal(t, globals.DefaultTeamTopic, getTLFRes.Convs[0].Channel)
  4801  			if i == 1 {
  4802  				require.Equal(t, chat1.ConversationMemberStatus_LEFT, getTLFRes.Convs[1].MemberStatus)
  4803  			} else {
  4804  				require.Equal(t, chat1.ConversationMemberStatus_ACTIVE, getTLFRes.Convs[1].MemberStatus)
  4805  			}
  4806  			t.Logf("checking num participants: i: %d uid: %s", i, user.Username)
  4807  			uid := user.GetUID().ToBytes()
  4808  			parts, err := ctc.world.Tcs[user.Username].Context().ParticipantsSource.Get(context.TODO(),
  4809  				uid, getTLFRes.Convs[1].GetConvID(), types.InboxSourceDataSourceAll)
  4810  			require.NoError(t, err)
  4811  			require.Equal(t, 1, len(parts))
  4812  		}
  4813  
  4814  		// delete the channel make sure it's gone from both inboxes
  4815  		_, err = ctc.as(t, users[0]).chatLocalHandler().DeleteConversationLocal(ctx,
  4816  			chat1.DeleteConversationLocalArg{
  4817  				ConvID:    ncres.Conv.GetConvID(),
  4818  				Confirmed: true,
  4819  			})
  4820  		require.NoError(t, err)
  4821  		consumeLeaveConv(t, listener0)
  4822  		consumeTeamType(t, listener0)
  4823  		consumeLeaveConv(t, listener1)
  4824  		consumeTeamType(t, listener1)
  4825  
  4826  		for _, user := range users {
  4827  			getTLFRes, err = ctc.as(t, user).chatLocalHandler().GetTLFConversationsLocal(ctc.as(t, user).startCtx,
  4828  				chat1.GetTLFConversationsLocalArg{
  4829  					TlfName:     conv.TlfName,
  4830  					TopicType:   chat1.TopicType_CHAT,
  4831  					MembersType: chat1.ConversationMembersType_TEAM,
  4832  				})
  4833  			require.NoError(t, err)
  4834  			require.Equal(t, 1, len(getTLFRes.Convs))
  4835  			require.Equal(t, globals.DefaultTeamTopic, getTLFRes.Convs[0].Channel)
  4836  		}
  4837  	})
  4838  }
  4839  
  4840  func TestChatSrvChatMembershipsLocal(t *testing.T) {
  4841  	runWithMemberTypes(t, func(mt chat1.ConversationMembersType) {
  4842  		ctc := makeChatTestContext(t, "TestChatSrvChatMembershipsLocal", 2)
  4843  		defer ctc.cleanup()
  4844  		users := ctc.users()
  4845  
  4846  		// Only run this test for teams
  4847  		switch mt {
  4848  		case chat1.ConversationMembersType_TEAM:
  4849  		default:
  4850  			return
  4851  		}
  4852  
  4853  		ctx := ctc.as(t, users[0]).startCtx
  4854  		ctx1 := ctc.as(t, users[1]).startCtx
  4855  
  4856  		listener0 := newServerChatListener()
  4857  		ctc.as(t, users[0]).h.G().NotifyRouter.AddListener(listener0)
  4858  		ctc.world.Tcs[users[0].Username].ChatG.Syncer.(*Syncer).isConnected = true
  4859  
  4860  		listener1 := newServerChatListener()
  4861  		ctc.as(t, users[1]).h.G().NotifyRouter.AddListener(listener1)
  4862  		ctc.world.Tcs[users[1].Username].ChatG.Syncer.(*Syncer).isConnected = true
  4863  
  4864  		conv := mustCreateConversationForTest(t, ctc, users[0], chat1.TopicType_CHAT,
  4865  			mt, ctc.as(t, users[1]).user())
  4866  
  4867  		teamID, err := ctc.as(t, users[0]).chatLocalHandler().TeamIDFromTLFName(ctx, chat1.TeamIDFromTLFNameArg{
  4868  			TlfName:     conv.TlfName,
  4869  			MembersType: chat1.ConversationMembersType_TEAM,
  4870  		})
  4871  		require.NoError(t, err)
  4872  		t.Logf("first conv: %s", conv.Id)
  4873  		t.Logf("create a conversation, and join user 1 into by sending a message")
  4874  		topicName := "zjoinonsend"
  4875  		ncres, err := ctc.as(t, users[0]).chatLocalHandler().NewConversationLocal(ctx,
  4876  			chat1.NewConversationLocalArg{
  4877  				TlfName:       conv.TlfName,
  4878  				TopicName:     &topicName,
  4879  				TopicType:     chat1.TopicType_CHAT,
  4880  				TlfVisibility: keybase1.TLFVisibility_PRIVATE,
  4881  				MembersType:   chat1.ConversationMembersType_TEAM,
  4882  			})
  4883  		require.NoError(t, err)
  4884  
  4885  		_, err = postLocalForTest(t, ctc, users[1], ncres.Conv.Info, chat1.NewMessageBodyWithText(chat1.MessageText{
  4886  			Body: "JOINME",
  4887  		}))
  4888  		require.NoError(t, err)
  4889  
  4890  		getChannelsRes, err := ctc.as(t, users[1]).chatLocalHandler().GetChannelMembershipsLocal(ctx1,
  4891  			chat1.GetChannelMembershipsLocalArg{
  4892  				TeamID: teamID,
  4893  				Uid:    users[1].GetUID().ToBytes(),
  4894  			})
  4895  		require.NoError(t, err)
  4896  		require.Equal(t, 2, len(getChannelsRes.Channels))
  4897  		require.Contains(t, getChannelsRes.Channels, chat1.ChannelNameMention{
  4898  			ConvID:    ncres.Conv.GetConvID(),
  4899  			TopicName: topicName,
  4900  		})
  4901  
  4902  		// users[1] leaves the new convo
  4903  		_, err = ctc.as(t, users[1]).chatLocalHandler().LeaveConversationLocal(ctx1,
  4904  			ncres.Conv.GetConvID())
  4905  		require.NoError(t, err)
  4906  		ignoreTypes := []chat1.MessageType{chat1.MessageType_SYSTEM, chat1.MessageType_JOIN, chat1.MessageType_TEXT}
  4907  		consumeNewMsgWhileIgnoring(t, listener0, chat1.MessageType_LEAVE, ignoreTypes, chat1.ChatActivitySource_REMOTE)
  4908  
  4909  		// make sure users processed leave correctly
  4910  		for i, user := range users {
  4911  			getChannelsRes, err = ctc.as(t, user).chatLocalHandler().GetChannelMembershipsLocal(ctc.as(t, user).startCtx,
  4912  				chat1.GetChannelMembershipsLocalArg{
  4913  					TeamID: teamID,
  4914  					Uid:    user.GetUID().ToBytes(),
  4915  				})
  4916  			require.NoError(t, err)
  4917  			if i == 1 {
  4918  				require.Equal(t, 1, len(getChannelsRes.Channels))
  4919  				require.Equal(t, globals.DefaultTeamTopic, getChannelsRes.Channels[0].TopicName)
  4920  			} else {
  4921  				require.Equal(t, 2, len(getChannelsRes.Channels))
  4922  			}
  4923  		}
  4924  
  4925  		// delete the channel make sure it's gone from both inboxes
  4926  		_, err = ctc.as(t, users[0]).chatLocalHandler().DeleteConversationLocal(ctx,
  4927  			chat1.DeleteConversationLocalArg{
  4928  				ConvID:    ncres.Conv.GetConvID(),
  4929  				Confirmed: true,
  4930  			})
  4931  		require.NoError(t, err)
  4932  		consumeLeaveConv(t, listener0)
  4933  		consumeTeamType(t, listener0)
  4934  		consumeLeaveConv(t, listener1)
  4935  		consumeTeamType(t, listener1)
  4936  
  4937  		for _, user := range users {
  4938  			getChannelsRes, err = ctc.as(t, user).chatLocalHandler().GetChannelMembershipsLocal(ctc.as(t, user).startCtx,
  4939  				chat1.GetChannelMembershipsLocalArg{
  4940  					TeamID: teamID,
  4941  					Uid:    user.GetUID().ToBytes(),
  4942  				})
  4943  			require.NoError(t, err)
  4944  			require.Equal(t, 1, len(getChannelsRes.Channels))
  4945  			require.Equal(t, globals.DefaultTeamTopic, getChannelsRes.Channels[0].TopicName)
  4946  		}
  4947  	})
  4948  }
  4949  
  4950  func TestChatSrvMutualTeamsLocal(t *testing.T) {
  4951  	runWithMemberTypes(t, func(mt chat1.ConversationMembersType) {
  4952  		ctc := makeChatTestContext(t, "TestChatSrvChatMembershipsLocal", 2)
  4953  		defer ctc.cleanup()
  4954  		users := ctc.users()
  4955  
  4956  		// Only run this test for teams
  4957  		switch mt {
  4958  		case chat1.ConversationMembersType_TEAM:
  4959  		default:
  4960  			return
  4961  		}
  4962  
  4963  		ctx := ctc.as(t, users[0]).startCtx
  4964  
  4965  		listener0 := newServerChatListener()
  4966  		ctc.as(t, users[0]).h.G().NotifyRouter.AddListener(listener0)
  4967  		ctc.world.Tcs[users[0].Username].ChatG.Syncer.(*Syncer).isConnected = true
  4968  
  4969  		listener1 := newServerChatListener()
  4970  		ctc.as(t, users[1]).h.G().NotifyRouter.AddListener(listener1)
  4971  		ctc.world.Tcs[users[1].Username].ChatG.Syncer.(*Syncer).isConnected = true
  4972  
  4973  		t.Logf("create team only one user is in")
  4974  		conv1 := mustCreateConversationForTest(t, ctc, users[0], chat1.TopicType_CHAT, mt)
  4975  		teamID1, err := ctc.as(t, users[0]).chatLocalHandler().TeamIDFromTLFName(ctx, chat1.TeamIDFromTLFNameArg{
  4976  			TlfName:     conv1.TlfName,
  4977  			MembersType: chat1.ConversationMembersType_TEAM,
  4978  		})
  4979  		require.NoError(t, err)
  4980  		t.Logf("teamID 1: %s", teamID1)
  4981  		t.Logf("check that users share no mutual teams")
  4982  		emptyMutualTeamsRes, err := ctc.as(t, users[0]).chatLocalHandler().GetMutualTeamsLocal(ctx, []string{users[1].Username})
  4983  		require.NoError(t, err)
  4984  		require.Equal(t, 0, len(emptyMutualTeamsRes.TeamIDs))
  4985  
  4986  		t.Logf("create team with both users")
  4987  		conv2 := mustCreateConversationForTest(t, ctc, users[0], chat1.TopicType_CHAT, mt, users[1])
  4988  		teamID2, err := ctc.as(t, users[0]).chatLocalHandler().TeamIDFromTLFName(ctx, chat1.TeamIDFromTLFNameArg{
  4989  			TlfName:     conv2.TlfName,
  4990  			MembersType: chat1.ConversationMembersType_TEAM,
  4991  		})
  4992  		require.NoError(t, err)
  4993  		t.Logf("check that users have 1 mutual team")
  4994  
  4995  		getMutualTeamsRes, err := ctc.as(t, users[0]).chatLocalHandler().GetMutualTeamsLocal(ctx, []string{users[1].Username})
  4996  		require.NoError(t, err)
  4997  		require.Equal(t, 1, len(getMutualTeamsRes.TeamIDs))
  4998  		require.Equal(t, teamID2, getMutualTeamsRes.TeamIDs[0])
  4999  	})
  5000  }
  5001  
  5002  func TestChatSrvSetAppNotificationSettings(t *testing.T) {
  5003  	runWithMemberTypes(t, func(mt chat1.ConversationMembersType) {
  5004  		ctc := makeChatTestContext(t, "TestChatSrvSetAppNotificationSettings", 2)
  5005  		defer ctc.cleanup()
  5006  		users := ctc.users()
  5007  
  5008  		// Only run this test for teams
  5009  		switch mt {
  5010  		case chat1.ConversationMembersType_TEAM:
  5011  		default:
  5012  			return
  5013  		}
  5014  
  5015  		listener0 := newServerChatListener()
  5016  		ctc.as(t, users[0]).h.G().NotifyRouter.AddListener(listener0)
  5017  
  5018  		conv := mustCreateConversationForTest(t, ctc, users[0], chat1.TopicType_CHAT, mt,
  5019  			ctc.as(t, users[1]).user())
  5020  		ctx := ctc.as(t, users[0]).startCtx
  5021  
  5022  		gilres, err := ctc.as(t, users[0]).chatLocalHandler().GetInboxAndUnboxLocal(ctx, chat1.GetInboxAndUnboxLocalArg{
  5023  			Query: &chat1.GetInboxLocalQuery{
  5024  				ConvIDs: []chat1.ConversationID{conv.Id},
  5025  			},
  5026  		})
  5027  		require.NoError(t, err)
  5028  		require.Equal(t, 1, len(gilres.Conversations))
  5029  		require.Equal(t, conv.Id, gilres.Conversations[0].GetConvID())
  5030  		gconv := gilres.Conversations[0]
  5031  		require.True(t, gconv.Notifications.Settings[keybase1.DeviceType_DESKTOP][chat1.NotificationKind_GENERIC])
  5032  		require.Equal(t, 2, len(gconv.Notifications.Settings))
  5033  		require.Equal(t, 2, len(gconv.Notifications.Settings[keybase1.DeviceType_DESKTOP]))
  5034  		require.Equal(t, 2, len(gconv.Notifications.Settings[keybase1.DeviceType_MOBILE]))
  5035  
  5036  		mustPostLocalForTest(t, ctc, users[1], conv,
  5037  			chat1.NewMessageBodyWithText(chat1.MessageText{Body: "hello!"}))
  5038  		select {
  5039  		case info := <-listener0.newMessageRemote:
  5040  			require.Equal(t, chat1.MessageType_TEXT, info.Message.GetMessageType())
  5041  			require.True(t, info.DisplayDesktopNotification)
  5042  			require.NotEqual(t, "", info.DesktopNotificationSnippet)
  5043  		case <-time.After(20 * time.Second):
  5044  			require.Fail(t, "no new message event")
  5045  		}
  5046  		setting := chat1.AppNotificationSettingLocal{
  5047  			DeviceType: keybase1.DeviceType_DESKTOP,
  5048  			Kind:       chat1.NotificationKind_ATMENTION,
  5049  			Enabled:    false,
  5050  		}
  5051  		_, err = ctc.as(t, users[0]).chatLocalHandler().SetAppNotificationSettingsLocal(ctx,
  5052  			chat1.SetAppNotificationSettingsLocalArg{
  5053  				ConvID:   conv.Id,
  5054  				Settings: []chat1.AppNotificationSettingLocal{setting},
  5055  			})
  5056  		require.NoError(t, err)
  5057  		select {
  5058  		case rsettings := <-listener0.appNotificationSettings:
  5059  			require.Equal(t, gconv.GetConvID(), rsettings.ConvID)
  5060  			require.Equal(t, 2, len(rsettings.Settings.Settings))
  5061  			require.False(t, rsettings.Settings.ChannelWide)
  5062  		case <-time.After(20 * time.Second):
  5063  			require.Fail(t, "no app notification received")
  5064  		}
  5065  		mustPostLocalForTest(t, ctc, users[1], conv,
  5066  			chat1.NewMessageBodyWithText(chat1.MessageText{Body: fmt.Sprintf("@%s", users[0].Username)}))
  5067  		select {
  5068  		case info := <-listener0.newMessageRemote:
  5069  			require.Equal(t, chat1.MessageType_TEXT, info.Message.GetMessageType())
  5070  			require.True(t, info.DisplayDesktopNotification)
  5071  			require.NotEqual(t, "", info.DesktopNotificationSnippet)
  5072  		case <-time.After(20 * time.Second):
  5073  			require.Fail(t, "no new message event")
  5074  		}
  5075  
  5076  		setting = chat1.AppNotificationSettingLocal{
  5077  			DeviceType: keybase1.DeviceType_DESKTOP,
  5078  			Kind:       chat1.NotificationKind_GENERIC,
  5079  			Enabled:    false,
  5080  		}
  5081  		setting2 := chat1.AppNotificationSettingLocal{
  5082  			DeviceType: keybase1.DeviceType_DESKTOP,
  5083  			Kind:       chat1.NotificationKind_ATMENTION,
  5084  			Enabled:    true,
  5085  		}
  5086  		_, err = ctc.as(t, users[0]).chatLocalHandler().SetAppNotificationSettingsLocal(ctx,
  5087  			chat1.SetAppNotificationSettingsLocalArg{
  5088  				ConvID:   conv.Id,
  5089  				Settings: []chat1.AppNotificationSettingLocal{setting, setting2},
  5090  			})
  5091  		require.NoError(t, err)
  5092  		select {
  5093  		case rsettings := <-listener0.appNotificationSettings:
  5094  			require.Equal(t, gconv.GetConvID(), rsettings.ConvID)
  5095  			require.Equal(t, 2, len(rsettings.Settings.Settings))
  5096  			require.False(t, rsettings.Settings.ChannelWide)
  5097  		case <-time.After(20 * time.Second):
  5098  			require.Fail(t, "no app notification received")
  5099  		}
  5100  
  5101  		gilres, err = ctc.as(t, users[0]).chatLocalHandler().GetInboxAndUnboxLocal(ctx, chat1.GetInboxAndUnboxLocalArg{
  5102  			Query: &chat1.GetInboxLocalQuery{
  5103  				ConvIDs: []chat1.ConversationID{conv.Id},
  5104  			},
  5105  		})
  5106  		require.NoError(t, err)
  5107  		require.Equal(t, 1, len(gilres.Conversations))
  5108  		require.Equal(t, conv.Id, gilres.Conversations[0].GetConvID())
  5109  		gconv = gilres.Conversations[0]
  5110  		require.False(t, gconv.Notifications.Settings[keybase1.DeviceType_DESKTOP][chat1.NotificationKind_GENERIC])
  5111  		require.Equal(t, 2, len(gconv.Notifications.Settings))
  5112  		require.Equal(t, 2, len(gconv.Notifications.Settings[keybase1.DeviceType_DESKTOP]))
  5113  		require.Equal(t, 2, len(gconv.Notifications.Settings[keybase1.DeviceType_MOBILE]))
  5114  		require.False(t, gconv.Notifications.ChannelWide)
  5115  
  5116  		mustPostLocalForTest(t, ctc, users[1], conv,
  5117  			chat1.NewMessageBodyWithText(chat1.MessageText{Body: "hello!"}))
  5118  		select {
  5119  		case info := <-listener0.newMessageRemote:
  5120  			require.False(t, info.DisplayDesktopNotification)
  5121  			require.Equal(t, "", info.DesktopNotificationSnippet)
  5122  		case <-time.After(20 * time.Second):
  5123  			require.Fail(t, "no new message event")
  5124  		}
  5125  
  5126  		validateDisplayAtMention := func(name string) {
  5127  			text := fmt.Sprintf("@%s", name)
  5128  			mustPostLocalForTest(t, ctc, users[1], conv,
  5129  				chat1.NewMessageBodyWithText(chat1.MessageText{Body: text}))
  5130  			select {
  5131  			case info := <-listener0.newMessageRemote:
  5132  				require.True(t, info.DisplayDesktopNotification)
  5133  				require.NotEqual(t, "", info.DesktopNotificationSnippet)
  5134  			case <-time.After(20 * time.Second):
  5135  				require.Fail(t, "no new message event")
  5136  			}
  5137  		}
  5138  		validateDisplayAtMention(users[0].Username)
  5139  
  5140  		_, err = ctc.as(t, users[0]).chatLocalHandler().SetAppNotificationSettingsLocal(ctx,
  5141  			chat1.SetAppNotificationSettingsLocalArg{
  5142  				ConvID:      conv.Id,
  5143  				ChannelWide: true,
  5144  			})
  5145  		require.NoError(t, err)
  5146  		select {
  5147  		case rsettings := <-listener0.appNotificationSettings:
  5148  			require.Equal(t, gconv.GetConvID(), rsettings.ConvID)
  5149  			require.True(t, rsettings.Settings.ChannelWide)
  5150  		case <-time.After(20 * time.Second):
  5151  			require.Fail(t, "no app notification received")
  5152  		}
  5153  		validateDisplayAtMention("channel")
  5154  		validateDisplayAtMention("everyone")
  5155  		validateDisplayAtMention("here")
  5156  	})
  5157  
  5158  }
  5159  
  5160  func randSweepChannel() uint64 {
  5161  	for {
  5162  		buf := make([]byte, 8)
  5163  		_, err := rand.Read(buf)
  5164  		if err != nil {
  5165  			panic(err)
  5166  		}
  5167  		x := binary.LittleEndian.Uint64(buf)
  5168  		// sql driver doesn't support all the bits
  5169  		// https://golang.org/src/database/sql/driver/types.go#L265
  5170  		if x < 1<<63 {
  5171  			return x
  5172  		}
  5173  	}
  5174  }
  5175  
  5176  func TestChatSrvRetentionSweepConv(t *testing.T) {
  5177  	sweepChannel := randSweepChannel()
  5178  	t.Logf("sweepChannel: %v", sweepChannel)
  5179  	runWithMemberTypes(t, func(mt chat1.ConversationMembersType) {
  5180  		switch mt {
  5181  		case chat1.ConversationMembersType_KBFS:
  5182  			t.Logf("skipping kbfs stage")
  5183  			return
  5184  		default:
  5185  			// Fall through for other member types.
  5186  		}
  5187  		runWithRetentionPolicyTypes(t, func(policy chat1.RetentionPolicy, ephemeralLifetime *gregor1.DurationSec) {
  5188  
  5189  			ctc := makeChatTestContext(t, "TestChatSrvRetention", 2)
  5190  			defer ctc.cleanup()
  5191  			users := ctc.users()
  5192  			ctx := ctc.as(t, users[0]).startCtx
  5193  
  5194  			listener := newServerChatListener()
  5195  			ctc.as(t, users[1]).h.G().NotifyRouter.AddListener(listener)
  5196  
  5197  			conv := mustCreateConversationForTest(t, ctc, users[0], chat1.TopicType_CHAT,
  5198  				mt, ctc.as(t, users[1]).user())
  5199  
  5200  			mustPostLocalForTest(t, ctc, users[0], conv, chat1.NewMessageBodyWithText(chat1.MessageText{Body: "hello!"}))
  5201  			consumeNewMsgRemote(t, listener, chat1.MessageType_TEXT)
  5202  
  5203  			mustPostLocalForTest(t, ctc, users[1], conv, chat1.NewMessageBodyWithText(chat1.MessageText{Body: "hello!"}))
  5204  			consumeNewMsgRemote(t, listener, chat1.MessageType_TEXT)
  5205  
  5206  			mustSetConvRetention(t, ctc, users[0], conv.Id, policy, sweepChannel)
  5207  			require.True(t, consumeSetConvRetention(t, listener).Eq(conv.Id))
  5208  
  5209  			// This will take at least 1 second. For the deletable message to get old enough.
  5210  			expungeInfo := sweepPollForDeletion(t, ctc, users[1], listener, conv.Id, 4)
  5211  			require.True(t, expungeInfo.ConvID.Eq(conv.Id))
  5212  			require.Equal(t, chat1.Expunge{Upto: 4}, expungeInfo.Expunge, "expunge upto")
  5213  
  5214  			tvres, err := ctc.as(t, users[1]).chatLocalHandler().GetThreadLocal(ctx, chat1.GetThreadLocalArg{ConversationID: conv.Id})
  5215  			require.NoError(t, err)
  5216  			t.Logf("messages: %v", chat1.MessageUnboxedDebugList(tvres.Thread.Messages))
  5217  			for _, msg := range tvres.Thread.Messages {
  5218  				if msg.IsJourneycard() {
  5219  					continue
  5220  				}
  5221  				switch msg.GetMessageType() {
  5222  				case chat1.MessageType_METADATA, chat1.MessageType_TLFNAME:
  5223  					continue
  5224  				}
  5225  				require.FailNowf(t, "the TEXTs should be deleted", "%v, %v", chat1.MessageUnboxedDebugList(tvres.Thread.Messages), spew.Sdump(msg))
  5226  			}
  5227  
  5228  			// If we are using an ephemeral policy make sure messages with a lifetime exceeding
  5229  			// the policy age are blocked.
  5230  			if ephemeralLifetime != nil {
  5231  				badLifetime := *ephemeralLifetime + 1
  5232  				_, err := postLocalEphemeralForTest(t, ctc, users[0], conv, chat1.NewMessageBodyWithText(chat1.MessageText{Body: "hello!"}), &badLifetime)
  5233  				require.Error(t, err)
  5234  				require.IsType(t, libkb.ChatEphemeralRetentionPolicyViolatedError{}, err)
  5235  
  5236  				mustPostLocalEphemeralForTest(t, ctc, users[0], conv,
  5237  					chat1.NewMessageBodyWithText(chat1.MessageText{Body: "hello!"}), ephemeralLifetime)
  5238  			}
  5239  		})
  5240  	})
  5241  }
  5242  
  5243  func tlfIDToTeamIDForce(t *testing.T, tlfID chat1.TLFID) keybase1.TeamID {
  5244  	res, err := keybase1.TeamIDFromString(tlfID.String())
  5245  	require.NoError(t, err)
  5246  	return res
  5247  }
  5248  
  5249  func TestChatSrvRetentionSweepTeam(t *testing.T) {
  5250  	sweepChannel := randSweepChannel()
  5251  	t.Logf("sweepChannel: %v", sweepChannel)
  5252  	runWithMemberTypes(t, func(mt chat1.ConversationMembersType) {
  5253  		switch mt {
  5254  		case chat1.ConversationMembersType_TEAM:
  5255  		default:
  5256  			t.Logf("skipping %v stage", mt)
  5257  			return
  5258  		}
  5259  		runWithRetentionPolicyTypes(t, func(policy chat1.RetentionPolicy, ephemeralLifetime *gregor1.DurationSec) {
  5260  			ctc := makeChatTestContext(t, "TestChatSrvTeamRetention", 2)
  5261  			defer ctc.cleanup()
  5262  			users := ctc.users()
  5263  			ctx := ctc.as(t, users[0]).startCtx
  5264  			_ = ctc.as(t, users[1]).startCtx
  5265  			for i, u := range users {
  5266  				t.Logf("user[%v] %v %v", i, u.Username, u.User.GetUID())
  5267  				ctc.world.Tcs[u.Username].ChatG.Syncer.(*Syncer).isConnected = true
  5268  			}
  5269  
  5270  			listener := newServerChatListener()
  5271  			ctc.as(t, users[1]).h.G().NotifyRouter.AddListener(listener)
  5272  
  5273  			// 3 convs
  5274  			// convA: inherit team expire policy (default)
  5275  			// convB: expire policy
  5276  			// convC: retain policy
  5277  			var convs []chat1.ConversationInfoLocal
  5278  			for i := 0; i < 3; i++ {
  5279  				t.Logf("creating conv %v", i)
  5280  				var topicName *string
  5281  				if i > 0 {
  5282  					s := fmt.Sprintf("regarding-%v-gons", i)
  5283  					topicName = &s
  5284  				}
  5285  				conv := mustCreateChannelForTest(t, ctc, users[0], chat1.TopicType_CHAT,
  5286  					topicName, mt, ctc.as(t, users[1]).user())
  5287  				convs = append(convs, conv)
  5288  				if i > 0 {
  5289  					mustJoinConversationByID(t, ctc, users[1], conv.Id)
  5290  					consumeJoinConv(t, listener)
  5291  				}
  5292  			}
  5293  			convA := convs[0]
  5294  			convB := convs[1]
  5295  			convC := convs[2]
  5296  			teamID := tlfIDToTeamIDForce(t, convA.Triple.Tlfid)
  5297  
  5298  			// policy can be EXPIRE or EPHEMERAL here.
  5299  			teamPolicy := policy
  5300  			convExpirePolicy := policy
  5301  			convRetainPolicy := chat1.NewRetentionPolicyWithRetain(chat1.RpRetain{})
  5302  
  5303  			latestMsgMap := make(map[chat1.ConvIDStr]chat1.MessageID)
  5304  			latestMsg := func(convID chat1.ConversationID) chat1.MessageID {
  5305  				return latestMsgMap[convID.ConvIDStr()]
  5306  			}
  5307  			for i, conv := range convs {
  5308  				t.Logf("conv (%v/%v) %v in team %v", i+1, len(convs), conv.Id, tlfIDToTeamIDForce(t, conv.Triple.Tlfid))
  5309  				msgID := mustPostLocalForTest(t, ctc, users[0], conv, chat1.NewMessageBodyWithText(chat1.MessageText{Body: "hello!"}))
  5310  				latestMsgMap[conv.Id.ConvIDStr()] = msgID
  5311  
  5312  				ignoreTypes := []chat1.MessageType{chat1.MessageType_SYSTEM, chat1.MessageType_JOIN}
  5313  				consumeNewMsgWhileIgnoring(t, listener, chat1.MessageType_TEXT, ignoreTypes, chat1.ChatActivitySource_REMOTE)
  5314  			}
  5315  
  5316  			mustSetConvRetention(t, ctc, users[0], convB.Id, convExpirePolicy, sweepChannel)
  5317  			require.True(t, consumeSetConvRetention(t, listener).Eq(convB.Id))
  5318  			mustSetTeamRetention(t, ctc, users[0], teamID, teamPolicy, sweepChannel)
  5319  			require.True(t, consumeSetTeamRetention(t, listener).Eq(teamID))
  5320  			mustSetConvRetention(t, ctc, users[0], convC.Id, convRetainPolicy, sweepChannel)
  5321  			require.True(t, consumeSetConvRetention(t, listener).Eq(convC.Id))
  5322  
  5323  			// This will take at least 1 second.
  5324  			sweepPollForDeletion(t, ctc, users[0], listener, convB.Id, latestMsg(convB.Id)+1)
  5325  			sweepPollForDeletion(t, ctc, users[0], listener, convA.Id, latestMsg(convA.Id)+1)
  5326  			sweepNoDeletion(t, ctc, users[0], convC.Id)
  5327  
  5328  			checkThread := func(convID chat1.ConversationID, expectDeleted bool) {
  5329  				tvres, err := ctc.as(t, users[1]).chatLocalHandler().GetThreadLocal(ctx, chat1.GetThreadLocalArg{ConversationID: convID})
  5330  				require.NoError(t, err)
  5331  				var nText int
  5332  				for _, msg := range tvres.Thread.Messages {
  5333  					if msg.IsJourneycard() {
  5334  						continue
  5335  					}
  5336  					require.True(t, msg.IsValidFull())
  5337  					require.Equal(t, chat1.MessageID(0), msg.Valid().ServerHeader.SupersededBy)
  5338  					if msg.GetMessageType() == chat1.MessageType_TEXT {
  5339  						nText++
  5340  					}
  5341  				}
  5342  				if expectDeleted {
  5343  					require.Equal(t, 0, nText, "conv contents should be deleted: %v", convID.DbShortFormString())
  5344  				} else {
  5345  					require.Equal(t, 1, nText)
  5346  				}
  5347  			}
  5348  
  5349  			checkThread(convA.Id, true)
  5350  			checkThread(convB.Id, true)
  5351  			checkThread(convC.Id, false)
  5352  			if ephemeralLifetime != nil {
  5353  				for _, conv := range []chat1.ConversationInfoLocal{convA, convB} {
  5354  					// If we are using an ephemeral policy make sure messages with a lifetime exceeding
  5355  					// the policy age are blocked.
  5356  					badLifetime := *ephemeralLifetime + 1
  5357  					_, err := postLocalEphemeralForTest(t, ctc, users[0], conv, chat1.NewMessageBodyWithText(chat1.MessageText{Body: "hello!"}), &badLifetime)
  5358  					require.Error(t, err)
  5359  					require.IsType(t, libkb.ChatEphemeralRetentionPolicyViolatedError{}, err)
  5360  
  5361  					mustPostLocalEphemeralForTest(t, ctc, users[0], conv,
  5362  						chat1.NewMessageBodyWithText(chat1.MessageText{Body: "hello!"}), ephemeralLifetime)
  5363  				}
  5364  			}
  5365  		})
  5366  	})
  5367  }
  5368  
  5369  func verifyChangeRetentionSystemMessage(t *testing.T, msg chat1.UIMessage, expectedMsg chat1.MessageSystemChangeRetention) {
  5370  	require.True(t, msg.IsValid())
  5371  	body := msg.Valid().MessageBody
  5372  	typ, err := body.MessageType()
  5373  	require.NoError(t, err)
  5374  	require.Equal(t, chat1.MessageType_SYSTEM, typ)
  5375  	sysMsg := body.System()
  5376  	sysTyp, err := sysMsg.SystemType()
  5377  	require.NoError(t, err)
  5378  	require.Equal(t, chat1.MessageSystemType_CHANGERETENTION, sysTyp)
  5379  	retMsg := sysMsg.Changeretention()
  5380  	require.Equal(t, expectedMsg, retMsg)
  5381  }
  5382  
  5383  func TestChatSrvEphemeralConvRetention(t *testing.T) {
  5384  	runWithMemberTypes(t, func(mt chat1.ConversationMembersType) {
  5385  		switch mt {
  5386  		case chat1.ConversationMembersType_KBFS:
  5387  			t.Logf("skipping kbfs stage")
  5388  			return
  5389  		default:
  5390  			// Fall through for other member types.
  5391  		}
  5392  
  5393  		ctc := makeChatTestContext(t, "TestChatSrvRetention", 2)
  5394  		defer ctc.cleanup()
  5395  		users := ctc.users()
  5396  		ctx := ctc.as(t, users[0]).startCtx
  5397  
  5398  		listener := newServerChatListener()
  5399  		ctc.as(t, users[1]).h.G().NotifyRouter.AddListener(listener)
  5400  
  5401  		conv := mustCreateConversationForTest(t, ctc, users[0], chat1.TopicType_CHAT,
  5402  			mt, ctc.as(t, users[1]).user())
  5403  
  5404  		msgID := mustPostLocalForTest(t, ctc, users[0], conv, chat1.NewMessageBodyWithText(chat1.MessageText{Body: "hello!"}))
  5405  		consumeNewMsgRemote(t, listener, chat1.MessageType_TEXT)
  5406  
  5407  		// set an ephemeral policy
  5408  		age := gregor1.ToDurationSec(time.Hour * 24)
  5409  		policy := chat1.NewRetentionPolicyWithEphemeral(chat1.RpEphemeral{Age: age})
  5410  		mustSetConvRetentionLocal(t, ctc, users[0], conv.Id, policy)
  5411  		require.True(t, consumeSetConvRetention(t, listener).Eq(conv.Id))
  5412  		msg := consumeNewMsgRemote(t, listener, chat1.MessageType_SYSTEM)
  5413  		verifyChangeRetentionSystemMessage(t, msg, chat1.MessageSystemChangeRetention{
  5414  			IsTeam:      false,
  5415  			IsInherit:   false,
  5416  			Policy:      policy,
  5417  			MembersType: mt,
  5418  			User:        users[0].Username,
  5419  		})
  5420  
  5421  		// make sure we can supersede existing messages
  5422  		mustReactToMsg(ctx, t, ctc, users[0], conv, msgID, ":+1:")
  5423  
  5424  		ephemeralMsgID := mustPostLocalEphemeralForTest(t, ctc, users[0], conv,
  5425  			chat1.NewMessageBodyWithText(chat1.MessageText{Body: "hello!"}), &age)
  5426  		mustReactToMsg(ctx, t, ctc, users[0], conv, ephemeralMsgID, ":+1:")
  5427  	})
  5428  }
  5429  
  5430  func TestChatSrvEphemeralTeamRetention(t *testing.T) {
  5431  	runWithMemberTypes(t, func(mt chat1.ConversationMembersType) {
  5432  		switch mt {
  5433  		case chat1.ConversationMembersType_TEAM:
  5434  		default:
  5435  			t.Logf("skipping %v stage", mt)
  5436  			return
  5437  		}
  5438  		ctc := makeChatTestContext(t, "TestChatSrvTeamRetention", 2)
  5439  		defer ctc.cleanup()
  5440  		users := ctc.users()
  5441  		ctx := ctc.as(t, users[0]).startCtx
  5442  		_ = ctc.as(t, users[1]).startCtx
  5443  		for i, u := range users {
  5444  			t.Logf("user[%v] %v %v", i, u.Username, u.User.GetUID())
  5445  			ctc.world.Tcs[u.Username].ChatG.Syncer.(*Syncer).isConnected = true
  5446  		}
  5447  		listener := newServerChatListener()
  5448  		ctc.as(t, users[1]).h.G().NotifyRouter.AddListener(listener)
  5449  
  5450  		// 3 convs
  5451  		// convA: inherit team expire policy (default)
  5452  		// convB: expire policy
  5453  		// convC: retain policy
  5454  		var convs []chat1.ConversationInfoLocal
  5455  		for i := 0; i < 3; i++ {
  5456  			t.Logf("creating conv %v", i)
  5457  			var topicName *string
  5458  			if i > 0 {
  5459  				s := fmt.Sprintf("regarding-%v-gons", i)
  5460  				topicName = &s
  5461  			}
  5462  			conv := mustCreateChannelForTest(t, ctc, users[0], chat1.TopicType_CHAT,
  5463  				topicName, mt, ctc.as(t, users[1]).user())
  5464  			convs = append(convs, conv)
  5465  			if i > 0 {
  5466  				mustJoinConversationByID(t, ctc, users[1], conv.Id)
  5467  				consumeJoinConv(t, listener)
  5468  			}
  5469  		}
  5470  		convA := convs[0]
  5471  		convB := convs[1]
  5472  		convC := convs[2]
  5473  		teamID := tlfIDToTeamIDForce(t, convA.Triple.Tlfid)
  5474  
  5475  		age := gregor1.ToDurationSec(time.Hour * 24)
  5476  		policy := chat1.NewRetentionPolicyWithEphemeral(chat1.RpEphemeral{Age: age})
  5477  		teamPolicy := policy
  5478  		convExpirePolicy := policy
  5479  		convRetainPolicy := chat1.NewRetentionPolicyWithRetain(chat1.RpRetain{})
  5480  
  5481  		latestMsgMap := make(map[string] /*convID*/ chat1.MessageID)
  5482  		latestMsg := func(convID chat1.ConversationID) chat1.MessageID {
  5483  			return latestMsgMap[convID.String()]
  5484  		}
  5485  		for i, conv := range convs {
  5486  			t.Logf("conv (%v/%v) %v in team %v", i+1, len(convs), conv.Id, tlfIDToTeamIDForce(t, conv.Triple.Tlfid))
  5487  			msgID := mustPostLocalForTest(t, ctc, users[0], conv, chat1.NewMessageBodyWithText(chat1.MessageText{Body: "hello!"}))
  5488  			latestMsgMap[conv.Id.String()] = msgID
  5489  		}
  5490  
  5491  		// drain remote messages
  5492  		drain := func() {
  5493  			for {
  5494  				select {
  5495  				case msg := <-listener.newMessageRemote:
  5496  					t.Logf("drained %v", msg.Message.GetMessageType())
  5497  				case <-time.After(100 * time.Millisecond):
  5498  					return
  5499  				}
  5500  			}
  5501  		}
  5502  		drain()
  5503  
  5504  		mustSetConvRetentionLocal(t, ctc, users[0], convB.Id, convExpirePolicy)
  5505  		require.True(t, consumeSetConvRetention(t, listener).Eq(convB.Id))
  5506  		msg := consumeNewMsgRemote(t, listener, chat1.MessageType_SYSTEM)
  5507  		verifyChangeRetentionSystemMessage(t, msg, chat1.MessageSystemChangeRetention{
  5508  			IsTeam:      false,
  5509  			IsInherit:   false,
  5510  			Policy:      convExpirePolicy,
  5511  			MembersType: mt,
  5512  			User:        users[0].Username,
  5513  		})
  5514  
  5515  		mustSetTeamRetentionLocal(t, ctc, users[0], teamID, teamPolicy)
  5516  		require.True(t, consumeSetTeamRetention(t, listener).Eq(teamID))
  5517  		msg = consumeNewMsgRemote(t, listener, chat1.MessageType_SYSTEM)
  5518  		verifyChangeRetentionSystemMessage(t, msg, chat1.MessageSystemChangeRetention{
  5519  			IsTeam:      true,
  5520  			IsInherit:   false,
  5521  			Policy:      teamPolicy,
  5522  			MembersType: mt,
  5523  			User:        users[0].Username,
  5524  		})
  5525  
  5526  		mustSetConvRetentionLocal(t, ctc, users[0], convC.Id, convRetainPolicy)
  5527  		require.True(t, consumeSetConvRetention(t, listener).Eq(convC.Id))
  5528  		msg = consumeNewMsgRemote(t, listener, chat1.MessageType_SYSTEM)
  5529  		verifyChangeRetentionSystemMessage(t, msg, chat1.MessageSystemChangeRetention{
  5530  			IsTeam:      false,
  5531  			IsInherit:   false,
  5532  			Policy:      convRetainPolicy,
  5533  			MembersType: mt,
  5534  			User:        users[0].Username,
  5535  		})
  5536  
  5537  		for _, conv := range []chat1.ConversationInfoLocal{convA, convB} {
  5538  			mustReactToMsg(ctx, t, ctc, users[0], conv, latestMsg(conv.Id), ":+1:")
  5539  			consumeNewMsgRemote(t, listener, chat1.MessageType_REACTION)
  5540  			ephemeralMsgID := mustPostLocalEphemeralForTest(t, ctc, users[0], conv,
  5541  				chat1.NewMessageBodyWithText(chat1.MessageText{Body: "hello!"}), &age)
  5542  			consumeNewMsgRemote(t, listener, chat1.MessageType_TEXT)
  5543  			mustReactToMsg(ctx, t, ctc, users[0], conv, ephemeralMsgID, ":+1:")
  5544  			consumeNewMsgRemote(t, listener, chat1.MessageType_REACTION)
  5545  		}
  5546  
  5547  		// revert convC to inherit
  5548  		convInheritPolicy := chat1.NewRetentionPolicyWithInherit(chat1.RpInherit{})
  5549  		mustSetConvRetentionLocal(t, ctc, users[0], convC.Id, convInheritPolicy)
  5550  		require.True(t, consumeSetConvRetention(t, listener).Eq(convC.Id))
  5551  		msg = consumeNewMsgRemote(t, listener, chat1.MessageType_SYSTEM)
  5552  		verifyChangeRetentionSystemMessage(t, msg, chat1.MessageSystemChangeRetention{
  5553  			IsTeam:      false,
  5554  			IsInherit:   true,
  5555  			MembersType: mt,
  5556  			Policy:      teamPolicy,
  5557  			User:        users[0].Username,
  5558  		})
  5559  	})
  5560  }
  5561  func TestChatSrvSetConvMinWriterRole(t *testing.T) {
  5562  	runWithMemberTypes(t, func(mt chat1.ConversationMembersType) {
  5563  		// Only run this test for teams
  5564  		switch mt {
  5565  		case chat1.ConversationMembersType_TEAM:
  5566  		default:
  5567  			t.Logf("skipping %v stage", mt)
  5568  			return
  5569  		}
  5570  
  5571  		ctc := makeChatTestContext(t, "TestChatSrvSetConvMinWriterRole", 2)
  5572  		defer ctc.cleanup()
  5573  		users := ctc.users()
  5574  		ctx := ctc.as(t, users[0]).startCtx
  5575  
  5576  		tc1 := ctc.as(t, users[0])
  5577  		tc2 := ctc.as(t, users[1])
  5578  
  5579  		listener1 := newServerChatListener()
  5580  		tc1.h.G().NotifyRouter.AddListener(listener1)
  5581  		listener2 := newServerChatListener()
  5582  		tc2.h.G().NotifyRouter.AddListener(listener2)
  5583  
  5584  		created := mustCreateConversationForTest(t, ctc, users[0], chat1.TopicType_CHAT,
  5585  			mt, tc2.user())
  5586  		convID := created.Id
  5587  		consumeNewConversation(t, listener1, convID)
  5588  		consumeNewConversation(t, listener2, convID)
  5589  
  5590  		verifyMinWriterRoleInfoOnConv := func(user *kbtest.FakeUser, role *keybase1.TeamRole, cannotWrite bool) {
  5591  			tc := ctc.as(t, user)
  5592  
  5593  			var expectedInfo *chat1.ConversationMinWriterRoleInfo
  5594  			var expectedInfoLocal *chat1.ConversationMinWriterRoleInfoLocal
  5595  			if role != nil {
  5596  				expectedInfo = &chat1.ConversationMinWriterRoleInfo{
  5597  					Role: *role,
  5598  					Uid:  gregor1.UID(users[0].GetUID().ToBytes()),
  5599  				}
  5600  				expectedInfoLocal = &chat1.ConversationMinWriterRoleInfoLocal{
  5601  					Role:        *role,
  5602  					ChangedBy:   users[0].Username,
  5603  					CannotWrite: cannotWrite,
  5604  				}
  5605  			}
  5606  
  5607  			conv, err := utils.GetUnverifiedConv(ctx, ctc.world.Tcs[user.Username].Context(),
  5608  				gregor1.UID(user.GetUID().ToBytes()), convID, types.InboxSourceDataSourceRemoteOnly)
  5609  			require.NoError(t, err)
  5610  			if role == nil {
  5611  				require.Nil(t, conv.Conv.ConvSettings)
  5612  			} else {
  5613  				require.NotNil(t, conv.Conv.ConvSettings)
  5614  				require.Equal(t, expectedInfo, conv.Conv.ConvSettings.MinWriterRoleInfo)
  5615  			}
  5616  
  5617  			gilres, err := tc.chatLocalHandler().GetInboxAndUnboxLocal(ctx, chat1.GetInboxAndUnboxLocalArg{
  5618  				Query: &chat1.GetInboxLocalQuery{
  5619  					ConvIDs: []chat1.ConversationID{convID},
  5620  				},
  5621  				IdentifyBehavior: keybase1.TLFIdentifyBehavior_CHAT_CLI,
  5622  			})
  5623  			require.NoError(t, err)
  5624  			require.Len(t, gilres.Conversations, 1)
  5625  			convSettings := gilres.Conversations[0].ConvSettings
  5626  			if role == nil {
  5627  				require.Nil(t, convSettings)
  5628  			} else {
  5629  				require.NotNil(t, convSettings)
  5630  				require.Equal(t, expectedInfoLocal, convSettings.MinWriterRoleInfo)
  5631  			}
  5632  		}
  5633  
  5634  		mustPostLocalForTest(t, ctc, users[0], created, chat1.NewMessageBodyWithText(chat1.MessageText{Body: "hello!"}))
  5635  		consumeNewMsgRemote(t, listener1, chat1.MessageType_TEXT)
  5636  		consumeNewMsgRemote(t, listener2, chat1.MessageType_TEXT)
  5637  		verifyMinWriterRoleInfoOnConv(users[0], nil, false)
  5638  		verifyMinWriterRoleInfoOnConv(users[1], nil, false)
  5639  
  5640  		role := keybase1.TeamRole_ADMIN
  5641  		err := tc1.chatLocalHandler().SetConvMinWriterRoleLocal(tc1.startCtx, chat1.SetConvMinWriterRoleLocalArg{
  5642  			ConvID: convID,
  5643  			Role:   role,
  5644  		})
  5645  		require.NoError(t, err)
  5646  		require.True(t, consumeSetConvSettings(t, listener1).Eq(created.Id))
  5647  		require.True(t, consumeSetConvSettings(t, listener2).Eq(created.Id))
  5648  
  5649  		// u2 can't set this since they are not an admin
  5650  		err = tc2.chatLocalHandler().SetConvMinWriterRoleLocal(tc2.startCtx, chat1.SetConvMinWriterRoleLocalArg{
  5651  			ConvID: convID,
  5652  			Role:   keybase1.TeamRole_NONE,
  5653  		})
  5654  		require.Error(t, err)
  5655  		// Only u1's role update went through
  5656  		verifyMinWriterRoleInfoOnConv(users[0], &role, false)
  5657  		verifyMinWriterRoleInfoOnConv(users[1], &role, true)
  5658  
  5659  		// u2 can't write anymore, only u1 can.
  5660  		_, err = postLocalForTest(t, ctc, users[1], created, chat1.NewMessageBodyWithText(chat1.MessageText{Body: "hello!"}))
  5661  		require.Error(t, err)
  5662  
  5663  		mustPostLocalForTest(t, ctc, users[0], created, chat1.NewMessageBodyWithText(chat1.MessageText{Body: "hello!"}))
  5664  		consumeNewMsgRemote(t, listener1, chat1.MessageType_TEXT)
  5665  		consumeNewMsgRemote(t, listener2, chat1.MessageType_TEXT)
  5666  
  5667  		// Both users can fully ready without issue
  5668  		for _, user := range users {
  5669  			tvres, err := ctc.as(t, user).chatLocalHandler().GetThreadLocal(ctx, chat1.GetThreadLocalArg{ConversationID: created.Id,
  5670  				Query: &chat1.GetThreadQuery{
  5671  					MessageTypes: []chat1.MessageType{chat1.MessageType_TEXT},
  5672  				},
  5673  			})
  5674  			filterOutJourneycards(&tvres.Thread)
  5675  			require.NoError(t, err)
  5676  			if len(tvres.Thread.Messages) != 2 {
  5677  				t.Logf("messages: %v", chat1.MessageUnboxedDebugList(tvres.Thread.Messages))
  5678  			}
  5679  			require.Len(t, tvres.Thread.Messages, 2, "messages are accessible")
  5680  		}
  5681  
  5682  		role = keybase1.TeamRole_NONE
  5683  		err = tc1.chatLocalHandler().SetConvMinWriterRoleLocal(tc1.startCtx, chat1.SetConvMinWriterRoleLocalArg{
  5684  			ConvID: convID,
  5685  			Role:   role,
  5686  		})
  5687  		require.NoError(t, err)
  5688  		require.True(t, consumeSetConvSettings(t, listener1).Eq(created.Id))
  5689  		require.True(t, consumeSetConvSettings(t, listener2).Eq(created.Id))
  5690  		verifyMinWriterRoleInfoOnConv(users[0], nil, false)
  5691  		verifyMinWriterRoleInfoOnConv(users[1], nil, false)
  5692  
  5693  		// Both users can write again
  5694  		mustPostLocalForTest(t, ctc, users[0], created, chat1.NewMessageBodyWithText(chat1.MessageText{Body: "hello!"}))
  5695  		consumeNewMsgRemote(t, listener1, chat1.MessageType_TEXT)
  5696  		consumeNewMsgRemote(t, listener2, chat1.MessageType_TEXT)
  5697  		mustPostLocalForTest(t, ctc, users[1], created, chat1.NewMessageBodyWithText(chat1.MessageText{Body: "hello!"}))
  5698  		consumeNewMsgRemote(t, listener1, chat1.MessageType_TEXT)
  5699  		consumeNewMsgRemote(t, listener2, chat1.MessageType_TEXT)
  5700  
  5701  		for _, user := range users {
  5702  			tvres, err := ctc.as(t, user).chatLocalHandler().GetThreadLocal(ctx, chat1.GetThreadLocalArg{
  5703  				ConversationID: created.Id,
  5704  				Query: &chat1.GetThreadQuery{
  5705  					MessageTypes: []chat1.MessageType{chat1.MessageType_TEXT},
  5706  				},
  5707  			})
  5708  			filterOutJourneycards(&tvres.Thread)
  5709  			require.NoError(t, err)
  5710  			require.Len(t, tvres.Thread.Messages, 4, "messages are accessible")
  5711  		}
  5712  
  5713  		// create a new channel with a MinWriterRole set to ADMIN and ensure
  5714  		// new users can join/leave
  5715  		topicName := "zjoinonsend"
  5716  		channel, err := ctc.as(t, users[0]).chatLocalHandler().NewConversationLocal(ctx,
  5717  			chat1.NewConversationLocalArg{
  5718  				TlfName:       created.TlfName,
  5719  				TopicName:     &topicName,
  5720  				TopicType:     chat1.TopicType_CHAT,
  5721  				TlfVisibility: keybase1.TLFVisibility_PRIVATE,
  5722  				MembersType:   chat1.ConversationMembersType_TEAM,
  5723  			})
  5724  		require.NoError(t, err)
  5725  		channelInfo := channel.Conv.Info
  5726  		consumeNewMsgRemote(t, listener1, chat1.MessageType_JOIN)
  5727  		consumeNewConversation(t, listener1, channelInfo.Id)
  5728  		assertNoNewConversation(t, listener2)
  5729  		consumeTeamType(t, listener1)
  5730  		consumeTeamType(t, listener2)
  5731  		consumeNewMsgRemote(t, listener1, chat1.MessageType_SYSTEM)
  5732  		consumeNewMsgRemote(t, listener1, chat1.MessageType_SYSTEM)
  5733  		consumeNewMsgRemote(t, listener2, chat1.MessageType_SYSTEM)
  5734  		consumeNewMsgRemote(t, listener2, chat1.MessageType_SYSTEM)
  5735  
  5736  		channelID := channelInfo.Id
  5737  		role = keybase1.TeamRole_ADMIN
  5738  		err = tc1.chatLocalHandler().SetConvMinWriterRoleLocal(tc1.startCtx, chat1.SetConvMinWriterRoleLocalArg{
  5739  			ConvID: channelID,
  5740  			Role:   role,
  5741  		})
  5742  		require.NoError(t, err)
  5743  
  5744  		_, err = ctc.as(t, users[1]).chatLocalHandler().JoinConversationLocal(tc2.startCtx, chat1.JoinConversationLocalArg{
  5745  			TlfName:    channel.Conv.Info.TlfName,
  5746  			TopicType:  chat1.TopicType_CHAT,
  5747  			Visibility: keybase1.TLFVisibility_PRIVATE,
  5748  			TopicName:  topicName,
  5749  		})
  5750  		require.NoError(t, err)
  5751  		consumeNewMsgRemote(t, listener1, chat1.MessageType_JOIN)
  5752  		consumeNewMsgRemote(t, listener2, chat1.MessageType_JOIN)
  5753  
  5754  		_, err = ctc.as(t, users[1]).chatLocalHandler().LeaveConversationLocal(tc2.startCtx, channelID)
  5755  		require.NoError(t, err)
  5756  		consumeNewMsgRemote(t, listener1, chat1.MessageType_LEAVE)
  5757  	})
  5758  }
  5759  
  5760  func TestChatSrvTopicNameState(t *testing.T) {
  5761  	runWithMemberTypes(t, func(mt chat1.ConversationMembersType) {
  5762  		// Only run this test for teams
  5763  		switch mt {
  5764  		case chat1.ConversationMembersType_TEAM:
  5765  		default:
  5766  			return
  5767  		}
  5768  
  5769  		ctc := makeChatTestContext(t, "TestChatSrvTopicNameState", 1)
  5770  		defer ctc.cleanup()
  5771  		users := ctc.users()
  5772  
  5773  		ui := kbtest.NewChatUI()
  5774  		ctc.as(t, users[0]).h.mockChatUI = ui
  5775  		listener0 := newServerChatListener()
  5776  		ctc.as(t, users[0]).h.G().NotifyRouter.AddListener(listener0)
  5777  		ctc.world.Tcs[users[0].Username].ChatG.Syncer.(*Syncer).isConnected = true
  5778  		tc := ctc.world.Tcs[users[0].Username]
  5779  		ri := ctc.as(t, users[0]).ri
  5780  
  5781  		firstConv := mustCreateConversationForTest(t, ctc, users[0], chat1.TopicType_CHAT, mt)
  5782  		consumeNewConversation(t, listener0, firstConv.Id)
  5783  
  5784  		topicName := "MIKE"
  5785  		ctx := ctc.as(t, users[0]).startCtx
  5786  		ncres, err := ctc.as(t, users[0]).chatLocalHandler().NewConversationLocal(ctx,
  5787  			chat1.NewConversationLocalArg{
  5788  				TlfName:       firstConv.TlfName,
  5789  				TopicName:     &topicName,
  5790  				TopicType:     chat1.TopicType_CHAT,
  5791  				TlfVisibility: keybase1.TLFVisibility_PRIVATE,
  5792  				MembersType:   chat1.ConversationMembersType_TEAM,
  5793  			})
  5794  		require.NoError(t, err)
  5795  		convInfo := ncres.Conv.Info
  5796  		consumeNewConversation(t, listener0, convInfo.Id)
  5797  		consumeNewMsgRemote(t, listener0, chat1.MessageType_JOIN)
  5798  		consumeTeamType(t, listener0)
  5799  		consumeNewMsgRemote(t, listener0, chat1.MessageType_SYSTEM)
  5800  		consumeNewMsgRemote(t, listener0, chat1.MessageType_SYSTEM)
  5801  
  5802  		// Delete the conv, make sure we can still create a new channel after
  5803  		_, err = ctc.as(t, users[0]).chatLocalHandler().DeleteConversationLocal(ctx,
  5804  			chat1.DeleteConversationLocalArg{
  5805  				ConvID: convInfo.Id,
  5806  			})
  5807  		require.NoError(t, err)
  5808  		consumeLeaveConv(t, listener0)
  5809  		consumeTeamType(t, listener0)
  5810  		t.Logf("Deleted conv")
  5811  
  5812  		topicName = "josh"
  5813  		ncres, err = ctc.as(t, users[0]).chatLocalHandler().NewConversationLocal(ctx,
  5814  			chat1.NewConversationLocalArg{
  5815  				TlfName:       firstConv.TlfName,
  5816  				TopicName:     &topicName,
  5817  				TopicType:     chat1.TopicType_CHAT,
  5818  				TlfVisibility: keybase1.TLFVisibility_PRIVATE,
  5819  				MembersType:   chat1.ConversationMembersType_TEAM,
  5820  			})
  5821  		require.NoError(t, err)
  5822  		conv := ncres.Conv
  5823  		convInfo = conv.Info
  5824  		consumeNewConversation(t, listener0, convInfo.Id)
  5825  		consumeNewMsgRemote(t, listener0, chat1.MessageType_JOIN)
  5826  		consumeNewMsgRemote(t, listener0, chat1.MessageType_SYSTEM)
  5827  		consumeNewMsgRemote(t, listener0, chat1.MessageType_SYSTEM)
  5828  
  5829  		// Creating a conversation with same topic name just returns the matching one
  5830  		topicName = "random"
  5831  		ncarg := chat1.NewConversationLocalArg{
  5832  			TlfName:       convInfo.TlfName,
  5833  			TopicName:     &topicName,
  5834  			TopicType:     chat1.TopicType_CHAT,
  5835  			TlfVisibility: keybase1.TLFVisibility_PRIVATE,
  5836  			MembersType:   chat1.ConversationMembersType_TEAM,
  5837  		}
  5838  		ncres, err = ctc.as(t, users[0]).chatLocalHandler().NewConversationLocal(ctx, ncarg)
  5839  		require.NoError(t, err)
  5840  		randomConvID := ncres.Conv.GetConvID()
  5841  		consumeNewConversation(t, listener0, randomConvID)
  5842  		consumeNewMsgRemote(t, listener0, chat1.MessageType_JOIN)
  5843  		consumeNewMsgRemote(t, listener0, chat1.MessageType_SYSTEM)
  5844  
  5845  		ncres, err = ctc.as(t, users[0]).chatLocalHandler().NewConversationLocal(ctx, ncarg)
  5846  		require.NoError(t, err)
  5847  		require.Equal(t, randomConvID, ncres.Conv.GetConvID())
  5848  		assertNoNewConversation(t, listener0)
  5849  
  5850  		// Try to change topic name to one that exists
  5851  		plarg := chat1.PostLocalArg{
  5852  			ConversationID: convInfo.Id,
  5853  			Msg: chat1.MessagePlaintext{
  5854  				ClientHeader: chat1.MessageClientHeader{
  5855  					Conv:        convInfo.Triple,
  5856  					MessageType: chat1.MessageType_METADATA,
  5857  					TlfName:     convInfo.TlfName,
  5858  				},
  5859  				MessageBody: chat1.NewMessageBodyWithMetadata(chat1.MessageConversationMetadata{
  5860  					ConversationTitle: topicName,
  5861  				}),
  5862  			},
  5863  			IdentifyBehavior: keybase1.TLFIdentifyBehavior_CHAT_CLI,
  5864  		}
  5865  		_, err = ctc.as(t, users[0]).chatLocalHandler().PostLocal(ctx, plarg)
  5866  		require.Error(t, err)
  5867  		require.IsType(t, DuplicateTopicNameError{}, err)
  5868  		plarg.Msg.MessageBody = chat1.NewMessageBodyWithMetadata(chat1.MessageConversationMetadata{
  5869  			ConversationTitle: "EULALIA",
  5870  		})
  5871  		_, err = ctc.as(t, users[0]).chatLocalHandler().PostLocal(ctx, plarg)
  5872  		require.NoError(t, err)
  5873  		consumeNewMsgRemote(t, listener0, chat1.MessageType_METADATA)
  5874  
  5875  		// Create race with topic name state, and make sure we do the right thing
  5876  		plarg.Msg.MessageBody = chat1.NewMessageBodyWithMetadata(chat1.MessageConversationMetadata{
  5877  			ConversationTitle: "ANOTHERONE",
  5878  		})
  5879  		sender := NewBlockingSender(tc.Context(), NewBoxer(tc.Context()),
  5880  			func() chat1.RemoteInterface { return ri })
  5881  		prepareRes, err := sender.Prepare(ctx, plarg.Msg, mt, &conv, nil)
  5882  		require.NoError(t, err)
  5883  		msg1 := prepareRes.Boxed
  5884  		ts1 := prepareRes.TopicNameState
  5885  		prepareRes, err = sender.Prepare(ctx, plarg.Msg, mt, &conv, nil)
  5886  		require.NoError(t, err)
  5887  		msg2 := prepareRes.Boxed
  5888  		ts2 := prepareRes.TopicNameState
  5889  		require.True(t, ts1.Eq(*ts2))
  5890  
  5891  		_, err = ri.PostRemote(ctx, chat1.PostRemoteArg{
  5892  			ConversationID: convInfo.Id,
  5893  			MessageBoxed:   msg1,
  5894  			TopicNameState: ts1,
  5895  		})
  5896  		require.NoError(t, err)
  5897  		consumeNewMsgRemote(t, listener0, chat1.MessageType_METADATA)
  5898  
  5899  		_, err = ri.PostRemote(ctx, chat1.PostRemoteArg{
  5900  			ConversationID: convInfo.Id,
  5901  			MessageBoxed:   msg2,
  5902  			TopicNameState: ts2,
  5903  		})
  5904  		require.Error(t, err)
  5905  		require.IsType(t, libkb.ChatStalePreviousStateError{}, err)
  5906  	})
  5907  }
  5908  
  5909  func TestChatSrvUnboxMobilePushNotification(t *testing.T) {
  5910  	runWithMemberTypes(t, func(mt chat1.ConversationMembersType) {
  5911  		ctc := makeChatTestContext(t, "TestChatSrvUnboxMobilePushNotification", 1)
  5912  		defer ctc.cleanup()
  5913  		users := ctc.users()
  5914  
  5915  		// Only run this test for teams
  5916  		switch mt {
  5917  		case chat1.ConversationMembersType_TEAM:
  5918  		default:
  5919  			return
  5920  		}
  5921  
  5922  		ctx := ctc.as(t, users[0]).startCtx
  5923  		tc := ctc.world.Tcs[users[0].Username]
  5924  		uid := users[0].User.GetUID().ToBytes()
  5925  		convInfo := mustCreateConversationForTest(t, ctc, users[0], chat1.TopicType_CHAT, mt)
  5926  		conv, err := utils.GetVerifiedConv(ctx, tc.Context(), uid, convInfo.Id,
  5927  			types.InboxSourceDataSourceAll)
  5928  		require.NoError(t, err)
  5929  		ri := ctc.as(t, users[0]).ri
  5930  		sender := NewBlockingSender(tc.Context(), NewBoxer(tc.Context()),
  5931  			func() chat1.RemoteInterface { return ri })
  5932  
  5933  		assertUnboxMobilePushNotif := func(msgArg chat1.MessagePlaintext, expectedMsg string) {
  5934  
  5935  			prepareRes, err := sender.Prepare(ctx, msgArg, mt, &conv, nil)
  5936  			require.NoError(t, err)
  5937  			msg := prepareRes.Boxed
  5938  			msg.ServerHeader = &chat1.MessageServerHeader{
  5939  				MessageID: 10,
  5940  			}
  5941  
  5942  			mh := codec.MsgpackHandle{WriteExt: true}
  5943  			var data []byte
  5944  			enc := codec.NewEncoderBytes(&data, &mh)
  5945  			require.NoError(t, enc.Encode(msg))
  5946  			encMsg := base64.StdEncoding.EncodeToString(data)
  5947  			unboxRes, err := ctc.as(t, users[0]).chatLocalHandler().UnboxMobilePushNotification(context.TODO(),
  5948  				chat1.UnboxMobilePushNotificationArg{
  5949  					ConvID:      convInfo.Id.String(),
  5950  					MembersType: mt,
  5951  					Payload:     encMsg,
  5952  				})
  5953  			require.NoError(t, err)
  5954  			require.Equal(t, unboxRes, expectedMsg)
  5955  		}
  5956  
  5957  		// TEXT msg
  5958  		msgArg := chat1.MessagePlaintext{
  5959  			ClientHeader: chat1.MessageClientHeader{
  5960  				Conv:        convInfo.Triple,
  5961  				MessageType: chat1.MessageType_TEXT,
  5962  				TlfName:     convInfo.TlfName,
  5963  				Sender:      uid,
  5964  			},
  5965  			MessageBody: chat1.NewMessageBodyWithText(chat1.MessageText{
  5966  				Body: "PUSH",
  5967  			}),
  5968  		}
  5969  		assertUnboxMobilePushNotif(msgArg, "PUSH")
  5970  
  5971  		// non-text type
  5972  		msgArg = chat1.MessagePlaintext{
  5973  			ClientHeader: chat1.MessageClientHeader{
  5974  				Conv:        convInfo.Triple,
  5975  				MessageType: chat1.MessageType_HEADLINE,
  5976  				TlfName:     convInfo.TlfName,
  5977  				Sender:      uid,
  5978  			},
  5979  			MessageBody: chat1.NewMessageBodyWithHeadline(chat1.MessageHeadline{
  5980  				Headline: "hi",
  5981  			}),
  5982  		}
  5983  		assertUnboxMobilePushNotif(msgArg, "")
  5984  	})
  5985  }
  5986  
  5987  func TestChatSrvImplicitConversation(t *testing.T) {
  5988  	runWithMemberTypes(t, func(mt chat1.ConversationMembersType) {
  5989  		if mt != chat1.ConversationMembersType_IMPTEAMNATIVE {
  5990  			return
  5991  		}
  5992  		ctc := makeChatTestContext(t, "ImplicitConversation", 2)
  5993  		defer ctc.cleanup()
  5994  
  5995  		users := ctc.users()
  5996  		displayName := users[0].Username + "," + users[1].Username
  5997  		tc := ctc.world.Tcs[users[0].Username]
  5998  		tc1 := ctc.world.Tcs[users[1].Username]
  5999  
  6000  		listener0 := newServerChatListener()
  6001  		ctc.as(t, users[0]).h.G().NotifyRouter.AddListener(listener0)
  6002  		listener1 := newServerChatListener()
  6003  		ctc.as(t, users[1]).h.G().NotifyRouter.AddListener(listener1)
  6004  
  6005  		consumeIdentify := func(ctx context.Context, listener *serverChatListener) {
  6006  			// check identify updates
  6007  			var update keybase1.CanonicalTLFNameAndIDWithBreaks
  6008  			select {
  6009  			case update = <-listener.identifyUpdate:
  6010  				t.Logf("identify update: %+v", update)
  6011  			case <-time.After(20 * time.Second):
  6012  				require.Fail(t, "no identify")
  6013  			}
  6014  			require.Empty(t, update.Breaks.Breaks)
  6015  			globals.CtxIdentifyNotifier(ctx).Reset()
  6016  			globals.CtxKeyFinder(ctx, tc.Context()).Reset()
  6017  		}
  6018  
  6019  		ctx := ctc.as(t, users[0]).startCtx
  6020  		ctx = globals.CtxModifyIdentifyNotifier(ctx, NewSimpleIdentifyNotifier(tc.Context()))
  6021  		tc.Context().PushHandler.(*PushHandler).identNotifier = DummyIdentifyNotifier{}
  6022  		tc1.Context().PushHandler.(*PushHandler).identNotifier = DummyIdentifyNotifier{}
  6023  
  6024  		res, err := ctc.as(t, users[0]).chatLocalHandler().FindConversationsLocal(ctx,
  6025  			chat1.FindConversationsLocalArg{
  6026  				TlfName:          displayName,
  6027  				MembersType:      chat1.ConversationMembersType_IMPTEAMNATIVE,
  6028  				Visibility:       keybase1.TLFVisibility_PRIVATE,
  6029  				TopicType:        chat1.TopicType_CHAT,
  6030  				IdentifyBehavior: keybase1.TLFIdentifyBehavior_CHAT_CLI,
  6031  			})
  6032  		require.NoError(t, err)
  6033  		require.Equal(t, 0, len(res.Conversations), "conv found")
  6034  
  6035  		// create a new conversation
  6036  		ncres, err := ctc.as(t, users[0]).chatLocalHandler().NewConversationLocal(ctx,
  6037  			chat1.NewConversationLocalArg{
  6038  				TlfName:          displayName,
  6039  				TlfVisibility:    keybase1.TLFVisibility_PRIVATE,
  6040  				TopicType:        chat1.TopicType_CHAT,
  6041  				MembersType:      chat1.ConversationMembersType_IMPTEAMNATIVE,
  6042  				IdentifyBehavior: keybase1.TLFIdentifyBehavior_CHAT_CLI,
  6043  			})
  6044  		require.NoError(t, err)
  6045  		consumeNewConversation(t, listener0, ncres.Conv.GetConvID())
  6046  		assertNoNewConversation(t, listener1)
  6047  		consumeIdentify(ctx, listener0) // encrypt for first message
  6048  
  6049  		uid := users[0].User.GetUID().ToBytes()
  6050  		conv, err := utils.GetUnverifiedConv(ctx, tc.Context(), uid, ncres.Conv.Info.Id,
  6051  			types.InboxSourceDataSourceRemoteOnly)
  6052  		require.NoError(t, err)
  6053  		require.NotEmpty(t, conv.Conv.MaxMsgSummaries, "created conversation does not have a message")
  6054  		require.Equal(t, ncres.Conv.Info.MembersType, chat1.ConversationMembersType_IMPTEAMNATIVE,
  6055  			"implicit team")
  6056  
  6057  		t.Logf("ncres tlf name: %s", ncres.Conv.Info.TlfName)
  6058  
  6059  		// user 0 sends a message to conv
  6060  		_, err = ctc.as(t, users[0]).chatLocalHandler().PostLocal(ctx, chat1.PostLocalArg{
  6061  			ConversationID: ncres.Conv.Info.Id,
  6062  			Msg: chat1.MessagePlaintext{
  6063  				ClientHeader: chat1.MessageClientHeader{
  6064  					Conv:        ncres.Conv.Info.Triple,
  6065  					MessageType: chat1.MessageType_TEXT,
  6066  					TlfName:     ncres.Conv.Info.TlfName,
  6067  				},
  6068  				MessageBody: chat1.NewMessageBodyWithText(chat1.MessageText{
  6069  					Body: "HI",
  6070  				}),
  6071  			},
  6072  		})
  6073  		require.NoError(t, err)
  6074  		consumeIdentify(ctx, listener0) // EncryptionKeys
  6075  
  6076  		// user 1 sends a message to conv
  6077  		ctx = ctc.as(t, users[1]).startCtx
  6078  		ctx = globals.CtxModifyIdentifyNotifier(ctx, NewSimpleIdentifyNotifier(tc1.Context()))
  6079  		_, err = ctc.as(t, users[1]).chatLocalHandler().PostLocal(ctx, chat1.PostLocalArg{
  6080  			ConversationID: ncres.Conv.Info.Id,
  6081  			Msg: chat1.MessagePlaintext{
  6082  				ClientHeader: chat1.MessageClientHeader{
  6083  					Conv:        ncres.Conv.Info.Triple,
  6084  					MessageType: chat1.MessageType_TEXT,
  6085  					TlfName:     ncres.Conv.Info.TlfName,
  6086  				},
  6087  				MessageBody: chat1.NewMessageBodyWithText(chat1.MessageText{
  6088  					Body: "Hello",
  6089  				}),
  6090  			},
  6091  		})
  6092  		require.NoError(t, err)
  6093  		consumeIdentify(ctx, listener1) // EncryptionKeys
  6094  
  6095  		// user 1 finds the conversation
  6096  		res, err = ctc.as(t, users[1]).chatLocalHandler().FindConversationsLocal(ctx,
  6097  			chat1.FindConversationsLocalArg{
  6098  				TlfName:          displayName,
  6099  				MembersType:      chat1.ConversationMembersType_IMPTEAMNATIVE,
  6100  				Visibility:       keybase1.TLFVisibility_PRIVATE,
  6101  				TopicType:        chat1.TopicType_CHAT,
  6102  				IdentifyBehavior: keybase1.TLFIdentifyBehavior_CHAT_CLI,
  6103  			})
  6104  		require.NoError(t, err)
  6105  		require.Equal(t, 1, len(res.Conversations), "no convs found")
  6106  	})
  6107  }
  6108  
  6109  func TestChatSrvImpTeamExistingKBFS(t *testing.T) {
  6110  	os.Setenv("KEYBASE_FEATURES", "admin")
  6111  	defer os.Setenv("KEYBASE_FEATURES", "")
  6112  	ctc := makeChatTestContext(t, "NewConversationLocal", 2)
  6113  	defer ctc.cleanup()
  6114  	users := ctc.users()
  6115  
  6116  	c1 := mustCreateConversationForTest(t, ctc, users[0], chat1.TopicType_CHAT, chat1.ConversationMembersType_KBFS, ctc.as(t, users[1]).user())
  6117  	c2 := mustCreateConversationForTest(t, ctc, users[0], chat1.TopicType_CHAT, chat1.ConversationMembersType_IMPTEAMNATIVE, ctc.as(t, users[1]).user())
  6118  
  6119  	t.Logf("c1: %v c2: %v", c1, c2)
  6120  	if !c2.Id.Eq(c1.Id) {
  6121  		t.Fatalf("2nd call to NewConversationLocal as IMPTEAM for a KBFS conversation did not return the same conversation ID")
  6122  	}
  6123  }
  6124  
  6125  func TestChatSrvTeamTypeChanged(t *testing.T) {
  6126  	runWithMemberTypes(t, func(mt chat1.ConversationMembersType) {
  6127  		ctc := makeChatTestContext(t, "TestChatSrvTeamTypeChanged", 2)
  6128  		defer ctc.cleanup()
  6129  		users := ctc.users()
  6130  
  6131  		// Only run this test for teams
  6132  		switch mt {
  6133  		case chat1.ConversationMembersType_TEAM:
  6134  		default:
  6135  			return
  6136  		}
  6137  
  6138  		ctx := ctc.as(t, users[0]).startCtx
  6139  		listener0 := newServerChatListener()
  6140  		ctc.as(t, users[0]).h.G().NotifyRouter.AddListener(listener0)
  6141  		listener1 := newServerChatListener()
  6142  		ctc.as(t, users[1]).h.G().NotifyRouter.AddListener(listener1)
  6143  
  6144  		conv := mustCreateConversationForTest(t, ctc, users[0], chat1.TopicType_CHAT, mt,
  6145  			ctc.as(t, users[1]).user())
  6146  		inboxRes, err := ctc.as(t, users[0]).chatLocalHandler().GetInboxAndUnboxLocal(ctx,
  6147  			chat1.GetInboxAndUnboxLocalArg{
  6148  				Query: &chat1.GetInboxLocalQuery{
  6149  					ConvIDs: []chat1.ConversationID{conv.Id},
  6150  				},
  6151  			})
  6152  		require.NoError(t, err)
  6153  		require.NotNil(t, inboxRes.Conversations[0].Notifications)
  6154  		require.True(t, inboxRes.Conversations[0].Notifications.Settings[keybase1.DeviceType_DESKTOP][chat1.NotificationKind_GENERIC])
  6155  
  6156  		topicName := "zjoinonsend"
  6157  		channel, err := ctc.as(t, users[0]).chatLocalHandler().NewConversationLocal(ctx,
  6158  			chat1.NewConversationLocalArg{
  6159  				TlfName:       conv.TlfName,
  6160  				TopicName:     &topicName,
  6161  				TopicType:     chat1.TopicType_CHAT,
  6162  				TlfVisibility: keybase1.TLFVisibility_PRIVATE,
  6163  				MembersType:   chat1.ConversationMembersType_TEAM,
  6164  			})
  6165  		t.Logf("conv: %s chan: %s", conv.Id, channel.Conv.GetConvID())
  6166  		require.NoError(t, err)
  6167  		consumeNewMsgRemote(t, listener0, chat1.MessageType_JOIN)
  6168  		select {
  6169  		case info := <-listener1.teamType:
  6170  			require.Equal(t, conv.Id, info.ConvID)
  6171  			require.Equal(t, chat1.TeamType_COMPLEX, info.TeamType)
  6172  		case <-time.After(20 * time.Second):
  6173  			require.Fail(t, "no team type")
  6174  		}
  6175  		select {
  6176  		case info := <-listener0.teamType:
  6177  			require.Equal(t, conv.Id, info.ConvID)
  6178  			require.Equal(t, chat1.TeamType_COMPLEX, info.TeamType)
  6179  		case <-time.After(20 * time.Second):
  6180  			require.Fail(t, "no team type")
  6181  		}
  6182  
  6183  		// Check remote notifications
  6184  		uconv, err := utils.GetUnverifiedConv(ctx, ctc.as(t, users[0]).h.G(), users[0].GetUID().ToBytes(),
  6185  			conv.Id, types.InboxSourceDataSourceRemoteOnly)
  6186  		require.NoError(t, err)
  6187  		require.NotNil(t, uconv.Conv.Notifications)
  6188  		require.True(t,
  6189  			uconv.Conv.Notifications.Settings[keybase1.DeviceType_DESKTOP][chat1.NotificationKind_GENERIC])
  6190  
  6191  		inboxRes, err = ctc.as(t, users[0]).chatLocalHandler().GetInboxAndUnboxLocal(ctx,
  6192  			chat1.GetInboxAndUnboxLocalArg{
  6193  				Query: &chat1.GetInboxLocalQuery{
  6194  					ConvIDs: []chat1.ConversationID{conv.Id},
  6195  				},
  6196  			})
  6197  		require.NoError(t, err)
  6198  		require.Equal(t, 1, len(inboxRes.Conversations))
  6199  		require.Equal(t, chat1.TeamType_COMPLEX, inboxRes.Conversations[0].Info.TeamType)
  6200  		require.NotNil(t, inboxRes.Conversations[0].Notifications)
  6201  		require.True(t, inboxRes.Conversations[0].Notifications.Settings[keybase1.DeviceType_DESKTOP][chat1.NotificationKind_GENERIC])
  6202  	})
  6203  }
  6204  
  6205  func TestChatSrvDeleteConversation(t *testing.T) {
  6206  	runWithMemberTypes(t, func(mt chat1.ConversationMembersType) {
  6207  		// Only run this test for teams
  6208  		switch mt {
  6209  		case chat1.ConversationMembersType_TEAM:
  6210  		default:
  6211  			return
  6212  		}
  6213  
  6214  		ctc := makeChatTestContext(t, "TestChatSrvDeleteConversation", 2)
  6215  		defer ctc.cleanup()
  6216  		users := ctc.users()
  6217  
  6218  		ctx := ctc.as(t, users[0]).startCtx
  6219  		ctx1 := ctc.as(t, users[1]).startCtx
  6220  		listener0 := newServerChatListener()
  6221  		ctc.as(t, users[0]).h.G().NotifyRouter.AddListener(listener0)
  6222  		listener1 := newServerChatListener()
  6223  		ctc.as(t, users[1]).h.G().NotifyRouter.AddListener(listener1)
  6224  		ui := kbtest.NewChatUI()
  6225  		ctc.as(t, users[0]).h.mockChatUI = ui
  6226  		ctc.as(t, users[1]).h.mockChatUI = ui
  6227  		ctc.world.Tcs[users[0].Username].ChatG.Syncer.(*Syncer).isConnected = true
  6228  		ctc.world.Tcs[users[1].Username].ChatG.Syncer.(*Syncer).isConnected = true
  6229  
  6230  		conv := mustCreateConversationForTest(t, ctc, users[0], chat1.TopicType_CHAT, mt,
  6231  			ctc.as(t, users[1]).user())
  6232  		consumeNewConversation(t, listener0, conv.Id)
  6233  		consumeNewConversation(t, listener1, conv.Id)
  6234  
  6235  		_, err := ctc.as(t, users[0]).chatLocalHandler().DeleteConversationLocal(ctx,
  6236  			chat1.DeleteConversationLocalArg{
  6237  				ConvID: conv.Id,
  6238  			})
  6239  		require.Error(t, err)
  6240  		require.IsType(t, libkb.ChatClientError{}, err)
  6241  
  6242  		topicName := "zjoinonsend"
  6243  		channel, err := ctc.as(t, users[0]).chatLocalHandler().NewConversationLocal(ctx,
  6244  			chat1.NewConversationLocalArg{
  6245  				TlfName:       conv.TlfName,
  6246  				TopicName:     &topicName,
  6247  				TopicType:     chat1.TopicType_CHAT,
  6248  				TlfVisibility: keybase1.TLFVisibility_PRIVATE,
  6249  				MembersType:   chat1.ConversationMembersType_TEAM,
  6250  			})
  6251  		require.NoError(t, err)
  6252  		channelConvID := channel.Conv.GetConvID()
  6253  		t.Logf("conv: %s chan: %s", conv.Id, channelConvID)
  6254  		consumeNewConversation(t, listener0, channelConvID)
  6255  		assertNoNewConversation(t, listener1)
  6256  		consumeNewMsgRemote(t, listener0, chat1.MessageType_JOIN)
  6257  		consumeTeamType(t, listener0)
  6258  		consumeTeamType(t, listener1)
  6259  		consumeNewMsgRemote(t, listener0, chat1.MessageType_SYSTEM)
  6260  		consumeNewMsgRemote(t, listener0, chat1.MessageType_SYSTEM)
  6261  		consumeNewMsgRemote(t, listener1, chat1.MessageType_SYSTEM)
  6262  		consumeNewMsgRemote(t, listener1, chat1.MessageType_SYSTEM)
  6263  
  6264  		uid := users[0].User.GetUID().ToBytes()
  6265  		g := ctc.world.Tcs[users[0].Username].Context()
  6266  		_, lconvs, err := storage.NewInbox(g).Read(context.TODO(), uid, &chat1.GetInboxQuery{
  6267  			ConvID: &channelConvID,
  6268  		})
  6269  		require.NoError(t, err)
  6270  		require.Equal(t, 1, len(lconvs))
  6271  		require.Equal(t, lconvs[0].GetConvID(), channelConvID)
  6272  		require.Equal(t, chat1.ConversationExistence_ACTIVE, lconvs[0].Conv.Metadata.Existence)
  6273  
  6274  		_, err = ctc.as(t, users[1]).chatLocalHandler().JoinConversationByIDLocal(ctx1,
  6275  			channelConvID)
  6276  		require.NoError(t, err)
  6277  		consumeNewMsgRemote(t, listener0, chat1.MessageType_JOIN)
  6278  		consumeNewMsgRemote(t, listener1, chat1.MessageType_JOIN)
  6279  		consumeMembersUpdate(t, listener0)
  6280  		consumeJoinConv(t, listener1)
  6281  		// second join attempt doesn't error
  6282  		_, err = ctc.as(t, users[1]).chatLocalHandler().JoinConversationByIDLocal(ctx1,
  6283  			channelConvID)
  6284  		require.NoError(t, err)
  6285  
  6286  		_, err = ctc.as(t, users[1]).chatLocalHandler().DeleteConversationLocal(ctx1,
  6287  			chat1.DeleteConversationLocalArg{
  6288  				ConvID: channelConvID,
  6289  			})
  6290  		require.Error(t, err)
  6291  		require.IsType(t, libkb.ChatClientError{}, err)
  6292  
  6293  		_, err = ctc.as(t, users[0]).chatLocalHandler().DeleteConversationLocal(ctx,
  6294  			chat1.DeleteConversationLocalArg{
  6295  				ConvID: channelConvID,
  6296  			})
  6297  		require.NoError(t, err)
  6298  		consumeLeaveConv(t, listener0)
  6299  		consumeLeaveConv(t, listener1)
  6300  		consumeMembersUpdate(t, listener0)
  6301  		consumeMembersUpdate(t, listener1)
  6302  		consumeTeamType(t, listener0)
  6303  		consumeTeamType(t, listener1)
  6304  
  6305  		updates := consumeNewThreadsStale(t, listener0)
  6306  		require.Equal(t, 1, len(updates))
  6307  		require.Equal(t, channelConvID, updates[0].ConvID, "wrong cid")
  6308  		require.Equal(t, chat1.StaleUpdateType_CLEAR, updates[0].UpdateType)
  6309  
  6310  		updates = consumeNewThreadsStale(t, listener1)
  6311  		require.Equal(t, 1, len(updates))
  6312  		require.Equal(t, channelConvID, updates[0].ConvID, "wrong cid")
  6313  		require.Equal(t, chat1.StaleUpdateType_CLEAR, updates[0].UpdateType)
  6314  
  6315  		_, lconvs, err = storage.NewInbox(g).Read(context.TODO(), uid, &chat1.GetInboxQuery{
  6316  			ConvID:       &channelConvID,
  6317  			MemberStatus: []chat1.ConversationMemberStatus{chat1.ConversationMemberStatus_LEFT},
  6318  			Existences:   []chat1.ConversationExistence{chat1.ConversationExistence_DELETED},
  6319  		})
  6320  		require.NoError(t, err)
  6321  		require.Equal(t, 1, len(lconvs))
  6322  		require.Equal(t, lconvs[0].GetConvID(), channelConvID)
  6323  
  6324  		iboxRes, err := ctc.as(t, users[0]).chatLocalHandler().GetInboxAndUnboxLocal(ctx,
  6325  			chat1.GetInboxAndUnboxLocalArg{})
  6326  		require.NoError(t, err)
  6327  		require.Equal(t, 1, len(iboxRes.Conversations))
  6328  		require.Equal(t, conv.Id, iboxRes.Conversations[0].GetConvID())
  6329  	})
  6330  }
  6331  
  6332  func kickTeamRekeyd(g *libkb.GlobalContext, t libkb.TestingTB) {
  6333  	mctx := libkb.NewMetaContextBackground(g)
  6334  	apiArg := libkb.APIArg{
  6335  		Endpoint:    "test/accelerate_team_rekeyd",
  6336  		Args:        libkb.HTTPArgs{},
  6337  		SessionType: libkb.APISessionTypeREQUIRED,
  6338  	}
  6339  
  6340  	_, err := g.API.Post(mctx, apiArg)
  6341  	if err != nil {
  6342  		t.Fatalf("Failed to accelerate team rekeyd: %s", err)
  6343  	}
  6344  }
  6345  
  6346  func logout(g *libkb.GlobalContext) error {
  6347  	m := libkb.NewMetaContextBackground(g)
  6348  	return m.LogoutKillSecrets()
  6349  }
  6350  
  6351  func TestChatSrvNewConvAfterReset(t *testing.T) {
  6352  	useRemoteMock = false
  6353  	defer func() { useRemoteMock = true }()
  6354  	ctc := makeChatTestContext(t, "TestChatSrvNewConvAfterReset", 2)
  6355  	defer ctc.cleanup()
  6356  
  6357  	users := ctc.users()
  6358  	ctx := ctc.as(t, users[0]).startCtx
  6359  	tc := ctc.world.Tcs[users[0].Username]
  6360  	uid := gregor1.UID(users[0].GetUID().ToBytes())
  6361  	conv := mustCreateConversationForTest(t, ctc, users[0], chat1.TopicType_CHAT,
  6362  		chat1.ConversationMembersType_IMPTEAMNATIVE, users[1])
  6363  	mustPostLocalForTest(t, ctc, users[0], conv, chat1.NewMessageBodyWithText(chat1.MessageText{
  6364  		Body: "HI",
  6365  	}))
  6366  	t.Logf("TLF: %s", conv.TlfName)
  6367  	require.NoError(t, libkb.ResetAccount(ctc.as(t, users[0]).m, users[0].NormalizedUsername(),
  6368  		users[0].Passphrase))
  6369  	require.NoError(t, users[0].Login(tc.G))
  6370  	conv2, created, err := NewConversation(ctx, tc.Context(), uid, users[0].Username+","+users[1].Username, nil,
  6371  		chat1.TopicType_CHAT, chat1.ConversationMembersType_IMPTEAMNATIVE, keybase1.TLFVisibility_PRIVATE,
  6372  		nil, func() chat1.RemoteInterface { return ctc.as(t, users[0]).ri }, NewConvFindExistingNormal)
  6373  	require.NoError(t, err)
  6374  	require.False(t, created)
  6375  	require.Equal(t, conv.Id, conv2.Info.Id)
  6376  }
  6377  
  6378  func TestChatSrvUserResetAndDeleted(t *testing.T) {
  6379  	runWithMemberTypes(t, func(mt chat1.ConversationMembersType) {
  6380  		// Only run this test for teams
  6381  		switch mt {
  6382  		case chat1.ConversationMembersType_TEAM, chat1.ConversationMembersType_IMPTEAMNATIVE,
  6383  			chat1.ConversationMembersType_IMPTEAMUPGRADE:
  6384  		default:
  6385  			return
  6386  		}
  6387  
  6388  		ctc := makeChatTestContext(t, "TestChatSrvUserResetAndDeleted", 4)
  6389  		defer ctc.cleanup()
  6390  		users := ctc.users()
  6391  
  6392  		ctx := ctc.as(t, users[0]).startCtx
  6393  		ctx1 := ctc.as(t, users[1]).startCtx
  6394  		ctx2 := ctc.as(t, users[2]).startCtx
  6395  		ctx3 := ctc.as(t, users[3]).startCtx
  6396  		uid0 := users[0].User.GetUID().ToBytes()
  6397  		tc0 := ctc.world.Tcs[users[0].Username]
  6398  		listener0 := newServerChatListener()
  6399  		ctc.as(t, users[0]).h.G().NotifyRouter.AddListener(listener0)
  6400  		listener1 := newServerChatListener()
  6401  		ctc.as(t, users[1]).h.G().NotifyRouter.AddListener(listener1)
  6402  		listener2 := newServerChatListener()
  6403  		ctc.as(t, users[2]).h.G().NotifyRouter.AddListener(listener2)
  6404  		listener3 := newServerChatListener()
  6405  		ctc.as(t, users[3]).h.G().NotifyRouter.AddListener(listener3)
  6406  		t.Logf("u0: %s, u1: %s, u2: %s, u3: %s", users[0].GetUID(), users[1].GetUID(), users[2].GetUID(), users[3].GetUID())
  6407  
  6408  		conv := mustCreateConversationForTest(t, ctc, users[0], chat1.TopicType_CHAT, mt,
  6409  			ctc.as(t, users[1]).user(), ctc.as(t, users[2]).user(), ctc.as(t, users[3]).user())
  6410  
  6411  		t.Logf("reset user 1")
  6412  		ctcForUser := ctc.as(t, users[1])
  6413  		require.NoError(t, libkb.ResetAccount(ctcForUser.m, users[1].NormalizedUsername(), users[1].Passphrase))
  6414  		select {
  6415  		case act := <-listener0.membersUpdate:
  6416  			require.Equal(t, act.ConvID, conv.Id)
  6417  			require.Equal(t, 1, len(act.Members))
  6418  			require.Equal(t, chat1.ConversationMemberStatus_RESET, act.Members[0].Status)
  6419  			require.Equal(t, users[1].Username, act.Members[0].Member)
  6420  		case <-time.After(20 * time.Second):
  6421  			require.Fail(t, "failed to get members update")
  6422  		}
  6423  		select {
  6424  		case act := <-listener2.membersUpdate:
  6425  			require.Equal(t, act.ConvID, conv.Id)
  6426  			require.Equal(t, 1, len(act.Members))
  6427  			require.Equal(t, chat1.ConversationMemberStatus_RESET, act.Members[0].Status)
  6428  			require.Equal(t, users[1].Username, act.Members[0].Member)
  6429  		case <-time.After(20 * time.Second):
  6430  			require.Fail(t, "failed to get members update")
  6431  		}
  6432  		select {
  6433  		case act := <-listener3.membersUpdate:
  6434  			require.Equal(t, act.ConvID, conv.Id)
  6435  			require.Equal(t, 1, len(act.Members))
  6436  			require.Equal(t, chat1.ConversationMemberStatus_RESET, act.Members[0].Status)
  6437  			require.Equal(t, users[1].Username, act.Members[0].Member)
  6438  		case <-time.After(20 * time.Second):
  6439  			require.Fail(t, "failed to get members update")
  6440  		}
  6441  		select {
  6442  		case convID := <-listener1.resetConv:
  6443  			require.Equal(t, convID, conv.Id)
  6444  		case <-time.After(20 * time.Second):
  6445  			require.Fail(t, "failed to get members update")
  6446  		}
  6447  
  6448  		t.Logf("check for correct state after user 1 reset")
  6449  		iboxRes, err := ctc.as(t, users[0]).chatLocalHandler().GetInboxAndUnboxLocal(ctx,
  6450  			chat1.GetInboxAndUnboxLocalArg{})
  6451  		require.NoError(t, err)
  6452  		require.Equal(t, 1, len(iboxRes.Conversations))
  6453  		require.Equal(t, conv.Id, iboxRes.Conversations[0].GetConvID())
  6454  		parts, err := tc0.Context().ParticipantsSource.Get(ctx, uid0, iboxRes.Conversations[0].GetConvID(),
  6455  			types.InboxSourceDataSourceAll)
  6456  		require.NoError(t, err)
  6457  		require.Equal(t, 4, len(parts))
  6458  		switch mt {
  6459  		case chat1.ConversationMembersType_TEAM:
  6460  			require.Zero(t, len(iboxRes.Conversations[0].Info.ResetNames))
  6461  		default:
  6462  			require.Equal(t, 1, len(iboxRes.Conversations[0].Info.ResetNames))
  6463  			require.Equal(t, users[1].Username, iboxRes.Conversations[0].Info.ResetNames[0])
  6464  		}
  6465  		iboxRes, err = ctc.as(t, users[1]).chatLocalHandler().GetInboxAndUnboxLocal(ctx1,
  6466  			chat1.GetInboxAndUnboxLocalArg{})
  6467  		require.NoError(t, err)
  6468  		switch mt {
  6469  		case chat1.ConversationMembersType_TEAM:
  6470  			require.Zero(t, len(iboxRes.Conversations))
  6471  		default:
  6472  			require.Equal(t, 1, len(iboxRes.Conversations))
  6473  			require.Equal(t, conv.Id, iboxRes.Conversations[0].GetConvID())
  6474  			require.Equal(t, chat1.ConversationMemberStatus_RESET, iboxRes.Conversations[0].Info.MemberStatus)
  6475  		}
  6476  		_, err = ctc.as(t, users[1]).chatLocalHandler().PostLocal(ctx1, chat1.PostLocalArg{
  6477  			ConversationID: conv.Id,
  6478  			Msg: chat1.MessagePlaintext{
  6479  				ClientHeader: chat1.MessageClientHeader{
  6480  					Conv:        conv.Triple,
  6481  					MessageType: chat1.MessageType_TEXT,
  6482  					TlfName:     conv.TlfName,
  6483  				},
  6484  				MessageBody: chat1.NewMessageBodyWithText(chat1.MessageText{
  6485  					Body: "Hello",
  6486  				}),
  6487  			},
  6488  		})
  6489  		require.Error(t, err)
  6490  
  6491  		t.Logf("delete user 3")
  6492  		ctcForUser3 := ctc.as(t, users[3])
  6493  		require.NoError(t, libkb.DeleteAccount(ctcForUser3.m, users[3].NormalizedUsername(), &users[3].Passphrase))
  6494  		select {
  6495  		case <-listener0.membersUpdate:
  6496  			require.Fail(t, "got members update after delete")
  6497  		case <-listener2.membersUpdate:
  6498  			require.Fail(t, "got members update after delete")
  6499  		default:
  6500  		}
  6501  
  6502  		select {
  6503  		case <-listener2.resetConv:
  6504  			require.Fail(t, "got reset conv after delete")
  6505  		default:
  6506  		}
  6507  
  6508  		// Once deleted we can't log back in or send
  6509  		g3 := ctc.as(t, users[3]).h.G().ExternalG()
  6510  		require.NoError(t, logout(g3))
  6511  		require.Error(t, users[3].Login(g3))
  6512  		_, err = ctc.as(t, users[3]).chatLocalHandler().PostLocal(ctx3, chat1.PostLocalArg{
  6513  			ConversationID: conv.Id,
  6514  			Msg: chat1.MessagePlaintext{
  6515  				ClientHeader: chat1.MessageClientHeader{
  6516  					Conv:        conv.Triple,
  6517  					MessageType: chat1.MessageType_TEXT,
  6518  					TlfName:     conv.TlfName,
  6519  				},
  6520  				MessageBody: chat1.NewMessageBodyWithText(chat1.MessageText{
  6521  					Body: "Hello",
  6522  				}),
  6523  			},
  6524  		})
  6525  		require.Error(t, err)
  6526  
  6527  		t.Logf("reset user 2")
  6528  		ctcForUser2 := ctc.as(t, users[2])
  6529  		require.NoError(t, libkb.ResetAccount(ctcForUser2.m, users[2].NormalizedUsername(), users[2].Passphrase))
  6530  		select {
  6531  		case act := <-listener0.membersUpdate:
  6532  			require.Equal(t, act.ConvID, conv.Id)
  6533  			require.Equal(t, 1, len(act.Members))
  6534  			require.Equal(t, chat1.ConversationMemberStatus_RESET, act.Members[0].Status)
  6535  			require.Equal(t, users[2].Username, act.Members[0].Member)
  6536  		case <-time.After(20 * time.Second):
  6537  			require.Fail(t, "failed to get members update")
  6538  		}
  6539  		select {
  6540  		case convID := <-listener2.resetConv:
  6541  			require.Equal(t, convID, conv.Id)
  6542  		case <-time.After(20 * time.Second):
  6543  			require.Fail(t, "failed to get members update")
  6544  		}
  6545  
  6546  		t.Logf("get a PUK for user 1 and not for user 2")
  6547  		g1 := ctc.as(t, users[1]).h.G().ExternalG()
  6548  		require.NoError(t, logout(g1))
  6549  		require.NoError(t, users[1].Login(g1))
  6550  		g2 := ctc.as(t, users[2]).h.G().ExternalG()
  6551  		require.NoError(t, logout(g2))
  6552  
  6553  		require.NoError(t, ctc.as(t, users[0]).chatLocalHandler().AddTeamMemberAfterReset(ctx,
  6554  			chat1.AddTeamMemberAfterResetArg{
  6555  				Username: users[1].Username,
  6556  				ConvID:   conv.Id,
  6557  			}))
  6558  		consumeMembersUpdate(t, listener0)
  6559  		consumeJoinConv(t, listener1)
  6560  		require.NoError(t, ctc.as(t, users[0]).chatLocalHandler().AddTeamMemberAfterReset(ctx,
  6561  			chat1.AddTeamMemberAfterResetArg{
  6562  				Username: users[2].Username,
  6563  				ConvID:   conv.Id,
  6564  			}))
  6565  		consumeMembersUpdate(t, listener0)
  6566  		consumeMembersUpdate(t, listener1)
  6567  
  6568  		iboxRes, err = ctc.as(t, users[0]).chatLocalHandler().GetInboxAndUnboxLocal(ctx,
  6569  			chat1.GetInboxAndUnboxLocalArg{})
  6570  		require.NoError(t, err)
  6571  		require.Equal(t, 1, len(iboxRes.Conversations))
  6572  		require.Equal(t, conv.Id, iboxRes.Conversations[0].GetConvID())
  6573  		parts, err = tc0.Context().ParticipantsSource.Get(ctx, uid0, iboxRes.Conversations[0].GetConvID(),
  6574  			types.InboxSourceDataSourceAll)
  6575  		require.NoError(t, err)
  6576  		require.Equal(t, 4, len(parts))
  6577  		require.Zero(t, len(iboxRes.Conversations[0].Info.ResetNames))
  6578  
  6579  		iboxRes, err = ctc.as(t, users[1]).chatLocalHandler().GetInboxAndUnboxLocal(ctx,
  6580  			chat1.GetInboxAndUnboxLocalArg{})
  6581  		require.NoError(t, err)
  6582  		require.Equal(t, 1, len(iboxRes.Conversations))
  6583  		require.Equal(t, conv.Id, iboxRes.Conversations[0].GetConvID())
  6584  		require.Nil(t, iboxRes.Conversations[0].Error)
  6585  		parts, err = tc0.Context().ParticipantsSource.Get(ctx, uid0, iboxRes.Conversations[0].GetConvID(),
  6586  			types.InboxSourceDataSourceAll)
  6587  		require.NoError(t, err)
  6588  		require.Equal(t, 4, len(parts))
  6589  		require.Zero(t, len(iboxRes.Conversations[0].Info.ResetNames))
  6590  		require.Equal(t, chat1.ConversationMemberStatus_ACTIVE, iboxRes.Conversations[0].Info.MemberStatus)
  6591  
  6592  		_, err = ctc.as(t, users[1]).chatLocalHandler().PostLocal(ctx1, chat1.PostLocalArg{
  6593  			ConversationID: conv.Id,
  6594  			Msg: chat1.MessagePlaintext{
  6595  				ClientHeader: chat1.MessageClientHeader{
  6596  					Conv:        conv.Triple,
  6597  					MessageType: chat1.MessageType_TEXT,
  6598  					TlfName:     conv.TlfName,
  6599  				},
  6600  				MessageBody: chat1.NewMessageBodyWithText(chat1.MessageText{
  6601  					Body: "Hello",
  6602  				}),
  6603  			},
  6604  		})
  6605  		require.NoError(t, err)
  6606  
  6607  		t.Logf("user 2 gets PUK and tries to do stuff")
  6608  		require.NoError(t, users[2].Login(g2))
  6609  		kickTeamRekeyd(g2, t)
  6610  		for i := 0; i < 200; i++ {
  6611  			_, err = ctc.as(t, users[2]).chatLocalHandler().PostLocal(ctx2, chat1.PostLocalArg{
  6612  				ConversationID: conv.Id,
  6613  				Msg: chat1.MessagePlaintext{
  6614  					ClientHeader: chat1.MessageClientHeader{
  6615  						Conv:        conv.Triple,
  6616  						MessageType: chat1.MessageType_TEXT,
  6617  						TlfName:     conv.TlfName,
  6618  					},
  6619  					MessageBody: chat1.NewMessageBodyWithText(chat1.MessageText{
  6620  						Body: "Hello",
  6621  					}),
  6622  				},
  6623  			})
  6624  			if err == nil {
  6625  				break
  6626  			}
  6627  			time.Sleep(50 * time.Millisecond)
  6628  		}
  6629  		require.NoError(t, err)
  6630  
  6631  		// Ensure everyone but user3 can access the conversation
  6632  		for i := range users {
  6633  			g := ctc.as(t, users[i]).h.G()
  6634  			ctx = ctc.as(t, users[i]).startCtx
  6635  			uid := gregor1.UID(users[i].GetUID().ToBytes())
  6636  			tv, err := g.ConvSource.Pull(ctx, conv.Id, uid,
  6637  				chat1.GetThreadReason_GENERAL, nil, nil, nil)
  6638  			if i == 3 {
  6639  				require.Error(t, err)
  6640  			} else {
  6641  				require.NoError(t, err)
  6642  				require.NotZero(t, len(tv.Messages))
  6643  			}
  6644  		}
  6645  	})
  6646  }
  6647  
  6648  func TestChatSrvTeamChannelNameMentions(t *testing.T) {
  6649  	runWithMemberTypes(t, func(mt chat1.ConversationMembersType) {
  6650  		// Only run this test for teams
  6651  		switch mt {
  6652  		case chat1.ConversationMembersType_TEAM:
  6653  		default:
  6654  			return
  6655  		}
  6656  
  6657  		ctc := makeChatTestContext(t, "TestChatSrvTeamChannelNameMentions", 2)
  6658  		defer ctc.cleanup()
  6659  		users := ctc.users()
  6660  
  6661  		ctx := ctc.as(t, users[0]).startCtx
  6662  		ctx1 := ctc.as(t, users[1]).startCtx
  6663  		listener0 := newServerChatListener()
  6664  		ctc.as(t, users[0]).h.G().NotifyRouter.AddListener(listener0)
  6665  		listener1 := newServerChatListener()
  6666  		ctc.as(t, users[1]).h.G().NotifyRouter.AddListener(listener1)
  6667  
  6668  		conv := mustCreateConversationForTest(t, ctc, users[0], chat1.TopicType_CHAT, mt,
  6669  			ctc.as(t, users[1]).user())
  6670  		consumeNewConversation(t, listener0, conv.Id)
  6671  		consumeNewConversation(t, listener1, conv.Id)
  6672  
  6673  		topicNames := []string{"miketime", "random", "hi"}
  6674  		for index, topicName := range topicNames {
  6675  			channel, err := ctc.as(t, users[1]).chatLocalHandler().NewConversationLocal(ctx,
  6676  				chat1.NewConversationLocalArg{
  6677  					TlfName:       conv.TlfName,
  6678  					TopicName:     &topicName,
  6679  					TopicType:     chat1.TopicType_CHAT,
  6680  					TlfVisibility: keybase1.TLFVisibility_PRIVATE,
  6681  					MembersType:   chat1.ConversationMembersType_TEAM,
  6682  				})
  6683  			t.Logf("conv: %s chan: %s, err: %v", conv.Id, channel.Conv.GetConvID(), err)
  6684  			require.NoError(t, err)
  6685  			assertNoNewConversation(t, listener0)
  6686  			consumeNewConversation(t, listener1, channel.Conv.GetConvID())
  6687  			consumeNewMsgRemote(t, listener1, chat1.MessageType_JOIN)
  6688  			if index == 0 {
  6689  				consumeNewMsgRemote(t, listener0, chat1.MessageType_SYSTEM)
  6690  				consumeNewMsgRemote(t, listener1, chat1.MessageType_SYSTEM)
  6691  			}
  6692  			consumeNewMsgRemote(t, listener0, chat1.MessageType_SYSTEM)
  6693  			consumeNewMsgRemote(t, listener1, chat1.MessageType_SYSTEM)
  6694  
  6695  			_, err = ctc.as(t, users[0]).chatLocalHandler().JoinConversationByIDLocal(ctx1,
  6696  				channel.Conv.GetConvID())
  6697  			require.NoError(t, err)
  6698  			consumeNewMsgRemote(t, listener0, chat1.MessageType_JOIN)
  6699  			consumeNewMsgRemote(t, listener1, chat1.MessageType_JOIN)
  6700  
  6701  			_, err = ctc.as(t, users[1]).chatLocalHandler().PostLocal(ctx1, chat1.PostLocalArg{
  6702  				ConversationID: channel.Conv.GetConvID(),
  6703  				Msg: chat1.MessagePlaintext{
  6704  					ClientHeader: chat1.MessageClientHeader{
  6705  						Conv:        channel.Conv.Info.Triple,
  6706  						MessageType: chat1.MessageType_TEXT,
  6707  						TlfName:     channel.Conv.Info.TlfName,
  6708  					},
  6709  					MessageBody: chat1.NewMessageBodyWithText(chat1.MessageText{
  6710  						Body: fmt.Sprintf("The worst channel is #%s. #error", topicName),
  6711  					}),
  6712  				},
  6713  			})
  6714  			require.NoError(t, err)
  6715  			consumeNewMsgRemote(t, listener0, chat1.MessageType_TEXT)
  6716  			consumeNewMsgRemote(t, listener1, chat1.MessageType_TEXT)
  6717  
  6718  			tv, err := ctc.as(t, users[0]).chatLocalHandler().GetThreadLocal(ctx, chat1.GetThreadLocalArg{
  6719  				ConversationID: channel.Conv.GetConvID(),
  6720  				Query: &chat1.GetThreadQuery{
  6721  					MessageTypes: []chat1.MessageType{chat1.MessageType_TEXT},
  6722  				},
  6723  			})
  6724  			require.NoError(t, err)
  6725  			uid := users[0].User.GetUID().ToBytes()
  6726  			ptv := utils.PresentThreadView(ctx, ctc.as(t, users[0]).h.G(), uid, tv.Thread,
  6727  				channel.Conv.GetConvID())
  6728  			require.Equal(t, 1, len(ptv.Messages))
  6729  			require.Equal(t, 1, len(ptv.Messages[0].Valid().ChannelNameMentions))
  6730  			require.Equal(t, topicName, ptv.Messages[0].Valid().ChannelNameMentions[0].Name)
  6731  		}
  6732  	})
  6733  }
  6734  
  6735  func TestChatSrvGetStaticConfig(t *testing.T) {
  6736  	ctc := makeChatTestContext(t, "GetStaticConfig", 2)
  6737  	defer ctc.cleanup()
  6738  	users := ctc.users()
  6739  	ctx := ctc.as(t, users[0]).startCtx
  6740  	tc := ctc.world.Tcs[users[0].Username]
  6741  	res, err := ctc.as(t, ctc.users()[0]).chatLocalHandler().GetStaticConfig(ctx)
  6742  	require.NoError(t, err)
  6743  	require.Equal(t, chat1.StaticConfig{
  6744  		DeletableByDeleteHistory: chat1.DeletableMessageTypesByDeleteHistory(),
  6745  		BuiltinCommands:          tc.Context().CommandsSource.GetBuiltins(ctx),
  6746  	}, res)
  6747  }
  6748  
  6749  type mockStellar struct {
  6750  	libkb.Stellar
  6751  	specFn func([]libkb.MiniChatPayment) (*libkb.MiniChatPaymentSummary, error)
  6752  }
  6753  
  6754  func (m *mockStellar) SendMiniChatPayments(mctx libkb.MetaContext, convID chat1.ConversationID, payments []libkb.MiniChatPayment) (res []libkb.MiniChatPaymentResult, err error) {
  6755  	for _, p := range payments {
  6756  		res = append(res, libkb.MiniChatPaymentResult{
  6757  			Username:  p.Username,
  6758  			PaymentID: stellar1.PaymentID("AHHH"),
  6759  			Error:     nil,
  6760  		})
  6761  	}
  6762  	return res, nil
  6763  }
  6764  
  6765  func (m *mockStellar) SpecMiniChatPayments(mctx libkb.MetaContext, payments []libkb.MiniChatPayment) (res *libkb.MiniChatPaymentSummary, err error) {
  6766  	return m.specFn(payments)
  6767  }
  6768  
  6769  func (m *mockStellar) KnownCurrencyCodeInstant(context.Context, string) (bool, bool) {
  6770  	return false, false
  6771  }
  6772  
  6773  type xlmDeclineChatUI struct {
  6774  	*kbtest.ChatUI
  6775  }
  6776  
  6777  func (c *xlmDeclineChatUI) ChatStellarDataConfirm(ctx context.Context, summary chat1.UIChatPaymentSummary) (bool, error) {
  6778  	c.StellarDataConfirm <- summary
  6779  	return false, nil
  6780  }
  6781  
  6782  func TestChatSrvStellarUI(t *testing.T) {
  6783  	useRemoteMock = false
  6784  	defer func() { useRemoteMock = true }()
  6785  	ctc := makeChatTestContext(t, "TestChatSrvStellarUI", 3)
  6786  	defer ctc.cleanup()
  6787  	users := ctc.users()
  6788  
  6789  	delay := 2 * time.Second
  6790  	// uid := users[0].User.GetUID().ToBytes()
  6791  	listener := newServerChatListener()
  6792  	ctc.as(t, users[0]).h.G().NotifyRouter.AddListener(listener)
  6793  	tc := ctc.world.Tcs[users[0].Username]
  6794  	ui := kbtest.NewChatUI()
  6795  	declineUI := &xlmDeclineChatUI{ChatUI: ui}
  6796  	ctx := ctc.as(t, users[0]).startCtx
  6797  	ctc.as(t, users[0]).h.mockChatUI = ui
  6798  	conv := mustCreateConversationForTest(t, ctc, users[0], chat1.TopicType_CHAT,
  6799  		chat1.ConversationMembersType_IMPTEAMNATIVE, ctc.as(t, users[1]).user(), ctc.as(t, users[2]).user())
  6800  	mst := &mockStellar{}
  6801  	tc.G.SetStellar(mst)
  6802  	tc.ChatG.StellarSender = wallet.NewSender(tc.Context())
  6803  	specSuccess := func(payments []libkb.MiniChatPayment) (res *libkb.MiniChatPaymentSummary, err error) {
  6804  		res = new(libkb.MiniChatPaymentSummary)
  6805  		res.XLMTotal = "10 XLM"
  6806  		res.DisplayTotal = "10 USD"
  6807  		for _, p := range payments {
  6808  			res.Specs = append(res.Specs, libkb.MiniChatPaymentSpec{
  6809  				Username:  p.Username,
  6810  				XLMAmount: p.Amount + " XLM",
  6811  			})
  6812  		}
  6813  		return res, nil
  6814  	}
  6815  	specFail := func(payments []libkb.MiniChatPayment) (res *libkb.MiniChatPaymentSummary, err error) {
  6816  		return res, errors.New("failed")
  6817  	}
  6818  	successCase := func(expectError bool) {
  6819  		mst.specFn = specSuccess
  6820  		_, err := ctc.as(t, users[0]).chatLocalHandler().PostTextNonblock(ctx,
  6821  			chat1.PostTextNonblockArg{
  6822  				ConversationID: conv.Id,
  6823  				TlfName:        conv.TlfName,
  6824  				Body:           fmt.Sprintf("+1xlm@%s +5xlm@%s", users[1].Username, users[2].Username),
  6825  			})
  6826  		if expectError {
  6827  			require.Error(t, err)
  6828  		} else {
  6829  			require.NoError(t, err)
  6830  		}
  6831  		select {
  6832  		case <-ui.StellarShowConfirm:
  6833  		case <-time.After(delay):
  6834  			require.Fail(t, "no confirm")
  6835  		}
  6836  		select {
  6837  		case data := <-ui.StellarDataConfirm:
  6838  			require.Equal(t, 2, len(data.Payments))
  6839  			require.Equal(t, "10 XLM", data.XlmTotal)
  6840  			require.Equal(t, "1 XLM", data.Payments[0].XlmAmount)
  6841  			require.Equal(t, "5 XLM", data.Payments[1].XlmAmount)
  6842  			require.Equal(t, users[1].Username, data.Payments[0].Username)
  6843  			require.Equal(t, users[2].Username, data.Payments[1].Username)
  6844  		case <-time.After(delay):
  6845  			require.Fail(t, "no confirm")
  6846  		}
  6847  		select {
  6848  		case <-ui.StellarDone:
  6849  		case <-time.After(delay):
  6850  			require.Fail(t, "no done")
  6851  		}
  6852  		if !expectError {
  6853  			consumeNewPendingMsg(t, listener)
  6854  			select {
  6855  			case msg := <-listener.newMessageLocal:
  6856  				require.True(t, msg.Message.IsValid())
  6857  				require.Equal(t, 2, len(msg.Message.Valid().AtMentions))
  6858  			case <-time.After(delay):
  6859  				require.Fail(t, "no local msg")
  6860  			}
  6861  			select {
  6862  			case msg := <-listener.newMessageRemote:
  6863  				require.True(t, msg.Message.IsValid())
  6864  				require.Equal(t, 2, len(msg.Message.Valid().AtMentions))
  6865  			case <-time.After(delay):
  6866  				require.Fail(t, "no remote msg")
  6867  			}
  6868  		}
  6869  	}
  6870  	t.Logf("success accept")
  6871  	successCase(false)
  6872  	t.Logf("success decline")
  6873  	ctc.as(t, users[0]).h.mockChatUI = declineUI
  6874  	successCase(true)
  6875  	ctc.as(t, users[0]).h.mockChatUI = ui
  6876  
  6877  	t.Logf("fail")
  6878  	mst.specFn = specFail
  6879  	_, err := ctc.as(t, users[0]).chatLocalHandler().PostTextNonblock(ctx, chat1.PostTextNonblockArg{
  6880  		ConversationID: conv.Id,
  6881  		TlfName:        conv.TlfName,
  6882  		Body:           fmt.Sprintf("+1xlm@%s +5xlm@%s", users[1].Username, users[2].Username),
  6883  	})
  6884  	require.Error(t, err)
  6885  	select {
  6886  	case <-ui.StellarShowConfirm:
  6887  	case <-time.After(delay):
  6888  		require.Fail(t, "no confirm")
  6889  	}
  6890  	select {
  6891  	case <-ui.StellarDataConfirm:
  6892  		require.Fail(t, "no confirm")
  6893  	default:
  6894  	}
  6895  	select {
  6896  	case <-ui.StellarDataError:
  6897  	case <-time.After(delay):
  6898  		require.Fail(t, "no error")
  6899  	}
  6900  	select {
  6901  	case <-ui.StellarDone:
  6902  	case <-time.After(delay):
  6903  		require.Fail(t, "no done")
  6904  	}
  6905  
  6906  	t.Logf("no payments")
  6907  	_, err = ctc.as(t, users[0]).chatLocalHandler().PostTextNonblock(ctx, chat1.PostTextNonblockArg{
  6908  		ConversationID: conv.Id,
  6909  		TlfName:        conv.TlfName,
  6910  		Body:           "pay me back",
  6911  	})
  6912  	require.NoError(t, err)
  6913  	select {
  6914  	case <-ui.StellarShowConfirm:
  6915  		require.Fail(t, "confirm")
  6916  	default:
  6917  	}
  6918  	consumeNewPendingMsg(t, listener)
  6919  }
  6920  
  6921  func TestChatSrvEphemeralPolicy(t *testing.T) {
  6922  	ctc := makeChatTestContext(t, "TestChatSrvEphemeralPolicy", 1)
  6923  	defer ctc.cleanup()
  6924  	users := ctc.users()
  6925  	useRemoteMock = false
  6926  	defer func() { useRemoteMock = true }()
  6927  
  6928  	timeout := 20 * time.Second
  6929  	ctx := ctc.as(t, users[0]).startCtx
  6930  	tc := ctc.world.Tcs[users[0].Username]
  6931  	listener0 := newServerChatListener()
  6932  	ctc.as(t, users[0]).h.G().NotifyRouter.AddListener(listener0)
  6933  	getMsg := func(ctx context.Context, convID chat1.ConversationID, msgID chat1.MessageID) chat1.MessageUnboxed {
  6934  		res, err := ctc.as(t, users[0]).chatLocalHandler().GetMessagesLocal(ctx, chat1.GetMessagesLocalArg{
  6935  			ConversationID: convID,
  6936  			MessageIDs:     []chat1.MessageID{msgID},
  6937  		})
  6938  		require.NoError(t, err)
  6939  		require.Equal(t, 1, len(res.Messages))
  6940  		return res.Messages[0]
  6941  	}
  6942  	checkEph := func(convID chat1.ConversationID, exp int) {
  6943  		select {
  6944  		case rmsg := <-listener0.newMessageRemote:
  6945  			msg := getMsg(ctx, convID, rmsg.Message.GetMessageID())
  6946  			require.True(t, msg.IsValid())
  6947  			require.NotNil(t, msg.Valid().ClientHeader.EphemeralMetadata)
  6948  			require.Equal(t, gregor1.DurationSec(exp), msg.Valid().ClientHeader.EphemeralMetadata.Lifetime)
  6949  		case <-time.After(timeout):
  6950  			require.Fail(t, "no message")
  6951  		}
  6952  	}
  6953  
  6954  	impconv := mustCreateConversationForTest(t, ctc, users[0], chat1.TopicType_CHAT,
  6955  		chat1.ConversationMembersType_IMPTEAMNATIVE)
  6956  	policy := chat1.NewRetentionPolicyWithEphemeral(chat1.RpEphemeral{
  6957  		Age: gregor1.DurationSec(86400),
  6958  	})
  6959  	mustSetConvRetentionLocal(t, ctc, users[0], impconv.Id, policy)
  6960  	consumeSetConvRetention(t, listener0)
  6961  	msg := consumeNewMsgRemote(t, listener0, chat1.MessageType_SYSTEM)
  6962  	verifyChangeRetentionSystemMessage(t, msg, chat1.MessageSystemChangeRetention{
  6963  		IsTeam:      false,
  6964  		IsInherit:   false,
  6965  		Policy:      policy,
  6966  		MembersType: chat1.ConversationMembersType_IMPTEAMNATIVE,
  6967  		User:        users[0].Username,
  6968  	})
  6969  
  6970  	mustPostLocalForTest(t, ctc, users[0], impconv,
  6971  		chat1.NewMessageBodyWithText(chat1.MessageText{
  6972  			Body: "HI",
  6973  		}))
  6974  	checkEph(impconv.Id, 86400)
  6975  	_, err := tc.Context().GregorState.InjectItem(ctx, fmt.Sprintf("exploding:%s", impconv.Id),
  6976  		[]byte("3600"), gregor1.TimeOrOffset{})
  6977  	require.NoError(t, err)
  6978  	msgID := mustPostLocalForTest(t, ctc, users[0], impconv,
  6979  		chat1.NewMessageBodyWithText(chat1.MessageText{
  6980  			Body: "HI",
  6981  		}))
  6982  	checkEph(impconv.Id, 3600)
  6983  	mustDeleteMsg(ctx, t, ctc, users[0], impconv, msgID)
  6984  	consumeNewMsgRemote(t, listener0, chat1.MessageType_DELETE)
  6985  
  6986  	teamconv := mustCreateConversationForTest(t, ctc, users[0], chat1.TopicType_CHAT,
  6987  		chat1.ConversationMembersType_TEAM)
  6988  	policy = chat1.NewRetentionPolicyWithEphemeral(chat1.RpEphemeral{
  6989  		Age: gregor1.DurationSec(86400),
  6990  	})
  6991  	mustSetTeamRetentionLocal(t, ctc, users[0], keybase1.TeamID(teamconv.Triple.Tlfid.String()), policy)
  6992  	consumeSetTeamRetention(t, listener0)
  6993  	msg = consumeNewMsgRemote(t, listener0, chat1.MessageType_SYSTEM)
  6994  	verifyChangeRetentionSystemMessage(t, msg, chat1.MessageSystemChangeRetention{
  6995  		IsTeam:      true,
  6996  		IsInherit:   false,
  6997  		Policy:      policy,
  6998  		MembersType: chat1.ConversationMembersType_TEAM,
  6999  		User:        users[0].Username,
  7000  	})
  7001  	msgID = mustPostLocalForTest(t, ctc, users[0], teamconv,
  7002  		chat1.NewMessageBodyWithText(chat1.MessageText{
  7003  			Body: "HI",
  7004  		}))
  7005  	checkEph(teamconv.Id, 86400)
  7006  	mustDeleteMsg(ctx, t, ctc, users[0], teamconv, msgID)
  7007  	consumeNewMsgRemote(t, listener0, chat1.MessageType_DELETE)
  7008  }
  7009  
  7010  func TestChatSrvStellarMessages(t *testing.T) {
  7011  	runWithMemberTypes(t, func(mt chat1.ConversationMembersType) {
  7012  		runWithEphemeral(t, mt, func(ephemeralLifetime *gregor1.DurationSec) {
  7013  			switch mt {
  7014  			case chat1.ConversationMembersType_KBFS:
  7015  				return
  7016  			default:
  7017  				// Fall through for other member types.
  7018  			}
  7019  
  7020  			ctc := makeChatTestContext(t, "SrvStellarMessages", 2)
  7021  			defer ctc.cleanup()
  7022  			users := ctc.users()
  7023  
  7024  			uid := users[0].User.GetUID().ToBytes()
  7025  			tc := ctc.world.Tcs[users[0].Username]
  7026  			ctx := ctc.as(t, users[0]).startCtx
  7027  			listener := newServerChatListener()
  7028  			ctc.as(t, users[0]).h.G().NotifyRouter.AddListener(listener)
  7029  			tc.ChatG.Syncer.(*Syncer).isConnected = true
  7030  
  7031  			created := mustCreateConversationForTest(t, ctc, users[0], chat1.TopicType_CHAT,
  7032  				mt, ctc.as(t, users[1]).user())
  7033  
  7034  			t.Logf("send a request message")
  7035  			body := chat1.NewMessageBodyWithRequestpayment(chat1.MessageRequestPayment{
  7036  				RequestID: stellar1.KeybaseRequestID("dummy id"),
  7037  				Note:      "Test note",
  7038  			})
  7039  
  7040  			_, err := postLocalEphemeralForTest(t, ctc, users[0], created, body, ephemeralLifetime)
  7041  			require.NoError(t, err)
  7042  
  7043  			var unboxed chat1.UIMessage
  7044  			select {
  7045  			case info := <-listener.newMessageRemote:
  7046  				unboxed = info.Message
  7047  				require.True(t, unboxed.IsValid(), "invalid message")
  7048  				require.Equal(t, chat1.MessageType_REQUESTPAYMENT, unboxed.GetMessageType(), "invalid type")
  7049  				require.Equal(t, body.Requestpayment(), unboxed.Valid().MessageBody.Requestpayment())
  7050  				require.False(t, unboxed.IsEphemeral())
  7051  			case <-time.After(20 * time.Second):
  7052  				require.Fail(t, "no event received")
  7053  			}
  7054  			consumeNewMsgLocal(t, listener, chat1.MessageType_REQUESTPAYMENT)
  7055  
  7056  			tv, err := tc.Context().ConvSource.Pull(ctx, created.Id, uid,
  7057  				chat1.GetThreadReason_GENERAL, nil, nil, nil)
  7058  			require.NoError(t, err)
  7059  			require.NotZero(t, len(tv.Messages))
  7060  			require.Equal(t, chat1.MessageType_REQUESTPAYMENT, tv.Messages[0].GetMessageType())
  7061  
  7062  			t.Logf("delete the message")
  7063  			darg := chat1.PostDeleteNonblockArg{
  7064  				ConversationID:   created.Id,
  7065  				TlfName:          created.TlfName,
  7066  				TlfPublic:        created.Visibility == keybase1.TLFVisibility_PUBLIC,
  7067  				Supersedes:       unboxed.GetMessageID(),
  7068  				IdentifyBehavior: keybase1.TLFIdentifyBehavior_CHAT_CLI,
  7069  			}
  7070  			res, err := ctc.as(t, users[0]).chatLocalHandler().PostDeleteNonblock(ctx, darg)
  7071  			require.NoError(t, err)
  7072  			select {
  7073  			case info := <-listener.newMessageRemote:
  7074  				unboxed = info.Message
  7075  				require.True(t, unboxed.IsValid(), "invalid message")
  7076  				require.NotNil(t, unboxed.Valid().OutboxID, "no outbox ID")
  7077  				require.Equal(t, res.OutboxID.String(), *unboxed.Valid().OutboxID, "mismatch outbox ID")
  7078  				require.Equal(t, chat1.MessageType_DELETE, unboxed.GetMessageType(), "invalid type")
  7079  			case <-time.After(20 * time.Second):
  7080  				require.Fail(t, "no event (DELETE) received")
  7081  			}
  7082  			consumeNewMsgLocal(t, listener, chat1.MessageType_DELETE)
  7083  
  7084  			tv, err = tc.Context().ConvSource.Pull(ctx, created.Id, uid,
  7085  				chat1.GetThreadReason_GENERAL, nil, nil, nil)
  7086  			require.NoError(t, err)
  7087  			require.NotZero(t, len(tv.Messages))
  7088  			require.Equal(t, chat1.MessageType_DELETE, tv.Messages[0].GetMessageType())
  7089  			for _, msg := range tv.Messages {
  7090  				require.NotEqual(t, chat1.MessageType_REQUESTPAYMENT, msg.GetMessageType())
  7091  			}
  7092  		})
  7093  	})
  7094  }
  7095  
  7096  func TestChatBulkAddToConv(t *testing.T) {
  7097  	runWithMemberTypes(t, func(mt chat1.ConversationMembersType) {
  7098  		switch mt {
  7099  		case chat1.ConversationMembersType_TEAM:
  7100  		default:
  7101  			return
  7102  		}
  7103  
  7104  		ctc := makeChatTestContext(t, "BulkAddToConv", 2)
  7105  		defer ctc.cleanup()
  7106  		users := ctc.users()
  7107  		t.Logf("uid1: %v, uid2: %v", users[0].User.GetUID(), users[1].User.GetUID())
  7108  
  7109  		tc1 := ctc.world.Tcs[users[0].Username]
  7110  		tc2 := ctc.world.Tcs[users[0].Username]
  7111  		ctx := ctc.as(t, users[0]).startCtx
  7112  
  7113  		listener0 := newServerChatListener()
  7114  		ctc.as(t, users[0]).h.G().NotifyRouter.AddListener(listener0)
  7115  		tc1.ChatG.Syncer.(*Syncer).isConnected = true
  7116  
  7117  		listener1 := newServerChatListener()
  7118  		ctc.as(t, users[1]).h.G().NotifyRouter.AddListener(listener1)
  7119  		tc2.ChatG.Syncer.(*Syncer).isConnected = true
  7120  
  7121  		conv := mustCreateConversationForTest(t, ctc, users[0], chat1.TopicType_CHAT,
  7122  			mt, ctc.as(t, users[1]).user())
  7123  
  7124  		// create a channel and bulk add user1 to it
  7125  		topicName := "bulk"
  7126  		channel, err := ctc.as(t, users[0]).chatLocalHandler().NewConversationLocal(ctx,
  7127  			chat1.NewConversationLocalArg{
  7128  				TlfName:       conv.TlfName,
  7129  				TopicName:     &topicName,
  7130  				TopicType:     chat1.TopicType_CHAT,
  7131  				TlfVisibility: keybase1.TLFVisibility_PRIVATE,
  7132  				MembersType:   mt,
  7133  			})
  7134  		t.Logf("conv: %s chan: %s, err: %v", conv.Id, channel.Conv.GetConvID(), err)
  7135  		require.NoError(t, err)
  7136  		consumeNewMsgRemote(t, listener0, chat1.MessageType_JOIN)
  7137  		consumeNewMsgRemote(t, listener0, chat1.MessageType_SYSTEM)
  7138  		consumeNewMsgRemote(t, listener0, chat1.MessageType_SYSTEM)
  7139  		consumeNewMsgRemote(t, listener1, chat1.MessageType_SYSTEM)
  7140  		consumeNewMsgRemote(t, listener1, chat1.MessageType_SYSTEM)
  7141  
  7142  		usernames := []string{users[1].Username}
  7143  		err = ctc.as(t, users[0]).chatLocalHandler().BulkAddToConv(ctx,
  7144  			chat1.BulkAddToConvArg{
  7145  				Usernames: usernames,
  7146  				ConvID:    channel.Conv.GetConvID(),
  7147  			})
  7148  		require.NoError(t, err)
  7149  
  7150  		assertSysMsg := func(expectedMentions, expectedBody []string, listener *serverChatListener) {
  7151  			msg := consumeNewMsgRemote(t, listener, chat1.MessageType_SYSTEM)
  7152  			body := msg.Valid().MessageBody
  7153  			typ, err := body.MessageType()
  7154  			require.NoError(t, err)
  7155  			require.Equal(t, chat1.MessageType_SYSTEM, typ)
  7156  			sysMsg := body.System()
  7157  			sysTyp, err := sysMsg.SystemType()
  7158  			require.NoError(t, err)
  7159  			require.Equal(t, chat1.MessageSystemType_BULKADDTOCONV, sysTyp)
  7160  			retMsg := sysMsg.Bulkaddtoconv()
  7161  			require.Equal(t, expectedBody, retMsg.Usernames)
  7162  			require.True(t, msg.IsValid())
  7163  			require.Equal(t, expectedMentions, msg.Valid().AtMentions)
  7164  		}
  7165  		assertSysMsg(usernames, usernames, listener0)
  7166  		assertSysMsg(usernames, usernames, listener1)
  7167  		consumeMembersUpdate(t, listener0)
  7168  		consumeJoinConv(t, listener1)
  7169  		// u1 can now send
  7170  		mustPostLocalForTest(t, ctc, users[1], channel.Conv.Info,
  7171  			chat1.NewMessageBodyWithText(chat1.MessageText{
  7172  				Body: "hi",
  7173  			}))
  7174  		consumeNewMsgRemote(t, listener0, chat1.MessageType_TEXT)
  7175  		consumeNewMsgRemote(t, listener1, chat1.MessageType_TEXT)
  7176  
  7177  		// some users required
  7178  		err = ctc.as(t, users[0]).chatLocalHandler().BulkAddToConv(ctx,
  7179  			chat1.BulkAddToConvArg{
  7180  				Usernames: nil,
  7181  				ConvID:    channel.Conv.GetConvID(),
  7182  			})
  7183  		require.Error(t, err)
  7184  
  7185  		usernames = []string{"foo"}
  7186  		err = ctc.as(t, users[0]).chatLocalHandler().BulkAddToConv(ctx,
  7187  			chat1.BulkAddToConvArg{
  7188  				Usernames: usernames,
  7189  				ConvID:    channel.Conv.GetConvID(),
  7190  			})
  7191  		require.NoError(t, err)
  7192  		assertSysMsg(nil, usernames, listener0)
  7193  		assertSysMsg(nil, usernames, listener1)
  7194  	})
  7195  }
  7196  
  7197  func TestChatBulkAddToManyConvs(t *testing.T) {
  7198  	runWithMemberTypes(t, func(mt chat1.ConversationMembersType) {
  7199  		switch mt {
  7200  		case chat1.ConversationMembersType_TEAM:
  7201  		default:
  7202  			return
  7203  		}
  7204  
  7205  		ctc := makeChatTestContext(t, "BulkAddToManyConvs", 2)
  7206  		defer ctc.cleanup()
  7207  		users := ctc.users()
  7208  		t.Logf("uid1: %v, uid2: %v", users[0].User.GetUID(), users[1].User.GetUID())
  7209  
  7210  		tc1 := ctc.world.Tcs[users[0].Username]
  7211  		tc2 := ctc.world.Tcs[users[0].Username]
  7212  		ctx := ctc.as(t, users[0]).startCtx
  7213  
  7214  		listener0 := newServerChatListener()
  7215  		ctc.as(t, users[0]).h.G().NotifyRouter.AddListener(listener0)
  7216  		tc1.ChatG.Syncer.(*Syncer).isConnected = true
  7217  
  7218  		listener1 := newServerChatListener()
  7219  		ctc.as(t, users[1]).h.G().NotifyRouter.AddListener(listener1)
  7220  		tc2.ChatG.Syncer.(*Syncer).isConnected = true
  7221  
  7222  		conv := mustCreateConversationForTest(t, ctc, users[0], chat1.TopicType_CHAT,
  7223  			mt, ctc.as(t, users[1]).user())
  7224  
  7225  		// create channels and bulk add user1 to them
  7226  		topicName0 := "bulk0"
  7227  		channel0, err := ctc.as(t, users[0]).chatLocalHandler().NewConversationLocal(ctx,
  7228  			chat1.NewConversationLocalArg{
  7229  				TlfName:       conv.TlfName,
  7230  				TopicName:     &topicName0,
  7231  				TopicType:     chat1.TopicType_CHAT,
  7232  				TlfVisibility: keybase1.TLFVisibility_PRIVATE,
  7233  				MembersType:   mt,
  7234  			})
  7235  		t.Logf("conv: %s chan: %s, err: %v", conv.Id, channel0.Conv.GetConvID(), err)
  7236  		require.NoError(t, err)
  7237  
  7238  		topicName1 := "bulk1"
  7239  		channel1, err := ctc.as(t, users[0]).chatLocalHandler().NewConversationLocal(ctx,
  7240  			chat1.NewConversationLocalArg{
  7241  				TlfName:       conv.TlfName,
  7242  				TopicName:     &topicName1,
  7243  				TopicType:     chat1.TopicType_CHAT,
  7244  				TlfVisibility: keybase1.TLFVisibility_PRIVATE,
  7245  				MembersType:   mt,
  7246  			})
  7247  		t.Logf("conv: %s chan: %s, err: %v", conv.Id, channel1.Conv.GetConvID(), err)
  7248  		require.NoError(t, err)
  7249  		consumeNewMsgRemote(t, listener0, chat1.MessageType_JOIN)
  7250  		consumeNewMsgRemote(t, listener0, chat1.MessageType_SYSTEM)
  7251  		consumeNewMsgRemote(t, listener0, chat1.MessageType_SYSTEM)
  7252  		consumeNewMsgRemote(t, listener0, chat1.MessageType_JOIN)
  7253  		consumeNewMsgRemote(t, listener0, chat1.MessageType_SYSTEM)
  7254  		consumeNewMsgRemote(t, listener1, chat1.MessageType_SYSTEM)
  7255  		consumeNewMsgRemote(t, listener1, chat1.MessageType_SYSTEM)
  7256  		consumeNewMsgRemote(t, listener1, chat1.MessageType_SYSTEM)
  7257  
  7258  		usernames := []string{users[1].Username}
  7259  		err = ctc.as(t, users[0]).chatLocalHandler().BulkAddToManyConvs(ctx,
  7260  			chat1.BulkAddToManyConvsArg{
  7261  				Usernames:     usernames,
  7262  				Conversations: []chat1.ConversationID{channel0.Conv.GetConvID(), channel1.Conv.GetConvID()},
  7263  			})
  7264  		require.NoError(t, err)
  7265  
  7266  		assertSysMsg := func(expectedMentions, expectedBody []string, listener *serverChatListener) {
  7267  			msg := consumeNewMsgRemote(t, listener, chat1.MessageType_SYSTEM)
  7268  			body := msg.Valid().MessageBody
  7269  			typ, err := body.MessageType()
  7270  			require.NoError(t, err)
  7271  			require.Equal(t, chat1.MessageType_SYSTEM, typ)
  7272  			sysMsg := body.System()
  7273  			sysTyp, err := sysMsg.SystemType()
  7274  			require.NoError(t, err)
  7275  			require.Equal(t, chat1.MessageSystemType_BULKADDTOCONV, sysTyp)
  7276  			retMsg := sysMsg.Bulkaddtoconv()
  7277  			require.Equal(t, expectedBody, retMsg.Usernames)
  7278  			require.True(t, msg.IsValid())
  7279  			require.Equal(t, expectedMentions, msg.Valid().AtMentions)
  7280  		}
  7281  		//TODO: not sure how to deal with multiple system messages coming in for this?
  7282  		assertSysMsg(usernames, usernames, listener0)
  7283  		assertSysMsg(usernames, usernames, listener1)
  7284  		assertSysMsg(usernames, usernames, listener0)
  7285  		assertSysMsg(usernames, usernames, listener1)
  7286  		consumeMembersUpdate(t, listener0)
  7287  		consumeJoinConv(t, listener1)
  7288  
  7289  		// u1 can now send to both channels
  7290  		mustPostLocalForTest(t, ctc, users[1], channel0.Conv.Info,
  7291  			chat1.NewMessageBodyWithText(chat1.MessageText{
  7292  				Body: "hi",
  7293  			}))
  7294  		// consumeNewMsgRemote(t, listener0, chat1.MessageType_TEXT)
  7295  		// consumeNewMsgRemote(t, listener1, chat1.MessageType_TEXT)
  7296  		mustPostLocalForTest(t, ctc, users[1], channel1.Conv.Info,
  7297  			chat1.NewMessageBodyWithText(chat1.MessageText{
  7298  				Body: "hi",
  7299  			}))
  7300  		// consumeNewMsgRemote(t, listener0, chat1.MessageType_TEXT)
  7301  		// consumeNewMsgRemote(t, listener1, chat1.MessageType_TEXT)
  7302  
  7303  		// some users required
  7304  		err = ctc.as(t, users[0]).chatLocalHandler().BulkAddToManyConvs(ctx,
  7305  			chat1.BulkAddToManyConvsArg{
  7306  				Usernames:     nil,
  7307  				Conversations: []chat1.ConversationID{channel1.Conv.GetConvID(), channel0.Conv.GetConvID()},
  7308  			})
  7309  		require.Error(t, err)
  7310  	})
  7311  }
  7312  
  7313  func TestReacjiStore(t *testing.T) {
  7314  	runWithMemberTypes(t, func(mt chat1.ConversationMembersType) {
  7315  		switch mt {
  7316  		case chat1.ConversationMembersType_IMPTEAMNATIVE:
  7317  		default:
  7318  			return
  7319  		}
  7320  		ctc := makeChatTestContext(t, "ReacjiStore", 1)
  7321  		defer ctc.cleanup()
  7322  
  7323  		user := ctc.users()[0]
  7324  		uid := user.User.GetUID().ToBytes()
  7325  		tc := ctc.world.Tcs[user.Username]
  7326  		ctx := ctc.as(t, user).startCtx
  7327  
  7328  		listener := newServerChatListener()
  7329  		ctc.as(t, user).h.G().NotifyRouter.AddListener(listener)
  7330  		tc.ChatG.Syncer.(*Syncer).isConnected = true
  7331  		reacjiStore := storage.NewReacjiStore(ctc.as(t, user).h.G())
  7332  		assertReacjiStore := func(actual, expected keybase1.UserReacjis, expectedData storage.ReacjiInternalStorage) {
  7333  			require.Equal(t, actual, expected)
  7334  			data := reacjiStore.GetInternalStore(ctx, uid)
  7335  			require.Equal(t, len(data.FrequencyMap), len(data.MtimeMap))
  7336  			for name := range data.MtimeMap {
  7337  				_, ok := data.FrequencyMap[name]
  7338  				require.True(t, ok)
  7339  			}
  7340  			require.Equal(t, expectedData.FrequencyMap, data.FrequencyMap)
  7341  			require.Equal(t, expectedData.SkinTone, data.SkinTone)
  7342  		}
  7343  
  7344  		expectedData := storage.NewReacjiInternalStorage()
  7345  		for _, el := range storage.DefaultTopReacjis {
  7346  			expectedData.FrequencyMap[el.Name] = 0
  7347  		}
  7348  		conv := mustCreateConversationForTest(t, ctc, user, chat1.TopicType_CHAT, mt)
  7349  		// if the user has no history we return the default list
  7350  		userReacjis := tc.G.ChatHelper.UserReacjis(ctx, uid)
  7351  		assertReacjiStore(userReacjis, keybase1.UserReacjis{TopReacjis: storage.DefaultTopReacjis}, expectedData)
  7352  
  7353  		// post a bunch of reactions, we should end up with these reactions
  7354  		// replacing the defaults sorted alphabetically (since they tie on
  7355  		// being used once each)
  7356  		reactionKeys := []keybase1.UserReacji{
  7357  			{Name: ":third_place_medal:"},
  7358  			{Name: ":second_place_medal:"},
  7359  			{Name: ":first_place_medal:"},
  7360  			{Name: ":a:"},
  7361  			{Name: ":8ball:"},
  7362  			{Name: ":1234:"},
  7363  			{Name: ":100:"},
  7364  		}
  7365  		msg := chat1.NewMessageBodyWithText(chat1.MessageText{Body: "hi"})
  7366  		textID := mustPostLocalForTest(t, ctc, user, conv, msg)
  7367  		consumeNewMsgRemote(t, listener, chat1.MessageType_TEXT)
  7368  		expected := keybase1.UserReacjis{}
  7369  		for i, reaction := range reactionKeys {
  7370  			expectedData.FrequencyMap[reaction.Name]++
  7371  			mustReactToMsg(ctx, t, ctc, user, conv, textID, reaction.Name)
  7372  			consumeNewMsgRemote(t, listener, chat1.MessageType_REACTION)
  7373  			info := consumeReactionUpdate(t, listener)
  7374  			t.Logf("DEBUG: info: %+v", info.UserReacjis)
  7375  			expected.TopReacjis = append([]keybase1.UserReacji{reaction}, expected.TopReacjis...)
  7376  			if i < 5 {
  7377  				// remove defaults as user values are added
  7378  				top := storage.DefaultTopReacjis[len(storage.DefaultTopReacjis)-i-1]
  7379  				delete(expectedData.FrequencyMap, top.Name)
  7380  				expected.TopReacjis = append(expected.TopReacjis,
  7381  					storage.DefaultTopReacjis...)[:len(storage.DefaultTopReacjis)]
  7382  			}
  7383  			assertReacjiStore(info.UserReacjis, expected, expectedData)
  7384  		}
  7385  		// bump "a" to the most frequent
  7386  		msg = chat1.NewMessageBodyWithText(chat1.MessageText{Body: "hi"})
  7387  		textID2 := mustPostLocalForTest(t, ctc, user, conv, msg)
  7388  		consumeNewMsgRemote(t, listener, chat1.MessageType_TEXT)
  7389  		mustReactToMsg(ctx, t, ctc, user, conv, textID2, ":100:")
  7390  		consumeNewMsgRemote(t, listener, chat1.MessageType_REACTION)
  7391  		expectedData.FrequencyMap[":100:"]++
  7392  		info := consumeReactionUpdate(t, listener)
  7393  		assertReacjiStore(info.UserReacjis, expected, expectedData)
  7394  
  7395  		// putSkinTone
  7396  		expectedSkinTone := keybase1.ReacjiSkinTone(4)
  7397  		userReacjis, err := ctc.as(t, user).chatLocalHandler().PutReacjiSkinTone(ctx, expectedSkinTone)
  7398  		require.NoError(t, err)
  7399  		expected.SkinTone = expectedSkinTone
  7400  		expectedData.SkinTone = expectedSkinTone
  7401  		assertReacjiStore(userReacjis, expected, expectedData)
  7402  	})
  7403  }
  7404  
  7405  func TestGlobalAppNotificationSettings(t *testing.T) {
  7406  	runWithMemberTypes(t, func(mt chat1.ConversationMembersType) {
  7407  		switch mt {
  7408  		case chat1.ConversationMembersType_IMPTEAMNATIVE:
  7409  		default:
  7410  			return
  7411  		}
  7412  		ctc := makeChatTestContext(t, "GlobalAppNotificationSettings", 1)
  7413  		defer ctc.cleanup()
  7414  
  7415  		user := ctc.users()[0]
  7416  		// tc := ctc.world.Tcs[user.Username]
  7417  		ctx := ctc.as(t, user).startCtx
  7418  		expectedSettings := map[chat1.GlobalAppNotificationSetting]bool{
  7419  			chat1.GlobalAppNotificationSetting_NEWMESSAGES:      true,
  7420  			chat1.GlobalAppNotificationSetting_PLAINTEXTDESKTOP: true,
  7421  			chat1.GlobalAppNotificationSetting_PLAINTEXTMOBILE:  false,
  7422  			chat1.GlobalAppNotificationSetting_DISABLETYPING:    false,
  7423  			chat1.GlobalAppNotificationSetting_CONVERTHEIC:      true,
  7424  		}
  7425  
  7426  		// convert the expectedSettings to the RPC format
  7427  		strSettings := func() map[string]bool {
  7428  			s := make(map[string]bool)
  7429  			for k, v := range expectedSettings {
  7430  				s[strconv.Itoa(int(k))] = v
  7431  			}
  7432  			return s
  7433  		}
  7434  
  7435  		// Test default settings
  7436  		s, err := ctc.as(t, user).chatLocalHandler().GetGlobalAppNotificationSettingsLocal(ctx)
  7437  		require.NoError(t, err)
  7438  		for k, v := range expectedSettings {
  7439  			require.Equal(t, v, s.Settings[k], fmt.Sprintf("Not equal %v", k))
  7440  			// flip all the defaults for the next test
  7441  			expectedSettings[k] = !v
  7442  		}
  7443  
  7444  		err = ctc.as(t, user).chatLocalHandler().SetGlobalAppNotificationSettingsLocal(ctx, strSettings())
  7445  		require.NoError(t, err)
  7446  		s, err = ctc.as(t, user).chatLocalHandler().GetGlobalAppNotificationSettingsLocal(ctx)
  7447  		require.NoError(t, err)
  7448  		for k, v := range expectedSettings {
  7449  			require.Equal(t, v, s.Settings[k], fmt.Sprintf("Not equal %v", k))
  7450  		}
  7451  	})
  7452  }
  7453  
  7454  func TestMessageDrafts(t *testing.T) {
  7455  	useRemoteMock = false
  7456  	defer func() { useRemoteMock = true }()
  7457  	ctc := makeChatTestContext(t, "TestMessageDrafts", 1)
  7458  	defer ctc.cleanup()
  7459  
  7460  	user := ctc.users()[0]
  7461  	conv := mustCreateConversationForTest(t, ctc, user, chat1.TopicType_CHAT,
  7462  		chat1.ConversationMembersType_IMPTEAMNATIVE)
  7463  
  7464  	draft := "NEW MESSAAGE"
  7465  	require.NoError(t, ctc.as(t, user).chatLocalHandler().UpdateUnsentText(context.TODO(),
  7466  		chat1.UpdateUnsentTextArg{
  7467  			ConversationID: conv.Id,
  7468  			TlfName:        conv.TlfName,
  7469  			Text:           draft,
  7470  		}))
  7471  	ibres, err := ctc.as(t, user).chatLocalHandler().GetInboxAndUnboxLocal(context.TODO(),
  7472  		chat1.GetInboxAndUnboxLocalArg{
  7473  			Query: &chat1.GetInboxLocalQuery{
  7474  				ConvIDs: []chat1.ConversationID{conv.Id},
  7475  			},
  7476  		})
  7477  	require.NoError(t, err)
  7478  	require.Equal(t, 1, len(ibres.Conversations))
  7479  	require.NotNil(t, ibres.Conversations[0].Info.Draft)
  7480  	require.Equal(t, draft, *ibres.Conversations[0].Info.Draft)
  7481  
  7482  	_, err = ctc.as(t, user).chatLocalHandler().PostLocalNonblock(context.TODO(), chat1.PostLocalNonblockArg{
  7483  		ConversationID: conv.Id,
  7484  		Msg: chat1.MessagePlaintext{
  7485  			ClientHeader: chat1.MessageClientHeader{
  7486  				TlfName:     conv.TlfName,
  7487  				MessageType: chat1.MessageType_TEXT,
  7488  			},
  7489  			MessageBody: chat1.NewMessageBodyWithText(chat1.MessageText{
  7490  				Body: "HIHIHI",
  7491  			}),
  7492  		},
  7493  	})
  7494  	require.NoError(t, err)
  7495  
  7496  	worked := false
  7497  	for i := 0; i < 5; i++ {
  7498  		ibres, err = ctc.as(t, user).chatLocalHandler().GetInboxAndUnboxLocal(context.TODO(),
  7499  			chat1.GetInboxAndUnboxLocalArg{
  7500  				Query: &chat1.GetInboxLocalQuery{
  7501  					ConvIDs: []chat1.ConversationID{conv.Id},
  7502  				},
  7503  			})
  7504  		require.NoError(t, err)
  7505  		require.Equal(t, 1, len(ibres.Conversations))
  7506  		if ibres.Conversations[0].Info.Draft == nil {
  7507  			worked = true
  7508  			break
  7509  		}
  7510  		time.Sleep(time.Second)
  7511  	}
  7512  	require.True(t, worked)
  7513  }
  7514  
  7515  func TestTeamBotSettings(t *testing.T) {
  7516  	runWithMemberTypes(t, func(mt chat1.ConversationMembersType) {
  7517  		runWithEphemeral(t, mt, func(ephemeralLifetime *gregor1.DurationSec) {
  7518  			if mt == chat1.ConversationMembersType_KBFS {
  7519  				return
  7520  			}
  7521  			ctc := makeChatTestContext(t, "TeamBotSettings", 3)
  7522  			defer ctc.cleanup()
  7523  			users := ctc.users()
  7524  
  7525  			ui := kbtest.NewChatUI()
  7526  			ctc.as(t, users[0]).h.mockChatUI = ui
  7527  			tc := ctc.as(t, users[0])
  7528  			listener := newServerChatListener()
  7529  			ctc.as(t, users[0]).h.G().NotifyRouter.AddListener(listener)
  7530  			ctc.world.Tcs[users[0].Username].ChatG.Syncer.(*Syncer).isConnected = true
  7531  
  7532  			botua := users[1]
  7533  			botua2 := users[2]
  7534  			botuaUID := gregor1.UID(botua.User.GetUID().ToBytes())
  7535  			botuaUID2 := gregor1.UID(botua2.User.GetUID().ToBytes())
  7536  
  7537  			botuaListener := newServerChatListener()
  7538  			ctc.as(t, botua).h.G().NotifyRouter.AddListener(botuaListener)
  7539  			ctc.world.Tcs[botua.Username].ChatG.Syncer.(*Syncer).isConnected = true
  7540  			botuaListener2 := newServerChatListener()
  7541  			ctc.as(t, botua2).h.G().NotifyRouter.AddListener(botuaListener2)
  7542  			ctc.world.Tcs[botua2.Username].ChatG.Syncer.(*Syncer).isConnected = true
  7543  			ctx := context.TODO()
  7544  
  7545  			var err error
  7546  			created := mustCreateConversationForTest(t, ctc, users[0], chat1.TopicType_CHAT, mt)
  7547  
  7548  			teamID, err := keybase1.TeamIDFromString(created.Triple.Tlfid.String())
  7549  			require.NoError(t, err)
  7550  
  7551  			pollForSeqno := func(expectedSeqno keybase1.Seqno) {
  7552  				found := false
  7553  				for !found {
  7554  					select {
  7555  					case teamChange := <-listener.teamChangedByID:
  7556  						found = teamChange.TeamID == teamID &&
  7557  							teamChange.LatestSeqno == expectedSeqno
  7558  					case <-time.After(20 * time.Second):
  7559  						require.Fail(t, "no event received")
  7560  					}
  7561  				}
  7562  			}
  7563  
  7564  			var unboxed chat1.UIMessage
  7565  			consumeBotMessage := func(botUsername string, msgTyp chat1.MessageType, l *serverChatListener) {
  7566  				select {
  7567  				case info := <-l.newMessageRemote:
  7568  					unboxed = info.Message
  7569  					require.True(t, unboxed.IsValid(), "invalid message")
  7570  					t.Logf("got message with searchable text: %v", unboxed.SearchableText())
  7571  					require.Equal(t, msgTyp, unboxed.GetMessageType(), "invalid type")
  7572  					require.Equal(t, botUsername, unboxed.Valid().BotUsername)
  7573  				case <-time.After(20 * time.Second):
  7574  					require.Fail(t, "no event received")
  7575  				}
  7576  			}
  7577  
  7578  			assertNoMessage := func(l *serverChatListener) {
  7579  				select {
  7580  				case info := <-l.newMessageRemote:
  7581  					unboxed = info.Message
  7582  					require.Fail(t, "unexpected message received type %v", unboxed.GetMessageType())
  7583  				default:
  7584  				}
  7585  			}
  7586  
  7587  			botSettings := keybase1.TeamBotSettings{
  7588  				Triggers: []string{"HI"},
  7589  			}
  7590  			err = ctc.as(t, users[0]).chatLocalHandler().AddBotMember(tc.startCtx, chat1.AddBotMemberArg{
  7591  				ConvID:      created.Id,
  7592  				Username:    botua.Username,
  7593  				Role:        keybase1.TeamRole_RESTRICTEDBOT,
  7594  				BotSettings: &botSettings,
  7595  			})
  7596  			require.NoError(t, err)
  7597  			pollForSeqno(3)
  7598  
  7599  			// system message for adding the bot to the team
  7600  			consumeNewPendingMsg(t, listener)
  7601  			consumeBotMessage("", chat1.MessageType_SYSTEM, listener)
  7602  			assertNoMessage(botuaListener)
  7603  			assertNoMessage(botuaListener2)
  7604  			consumeNewMsgLocal(t, listener, chat1.MessageType_SYSTEM)
  7605  
  7606  			gilres, err := ctc.as(t, users[1]).chatLocalHandler().GetInboxAndUnboxLocal(ctx, chat1.GetInboxAndUnboxLocalArg{
  7607  				Query: &chat1.GetInboxLocalQuery{
  7608  					ConvIDs: []chat1.ConversationID{created.Id},
  7609  				},
  7610  			})
  7611  			require.NoError(t, err)
  7612  			require.Equal(t, 1, len(gilres.Conversations))
  7613  			require.Equal(t, created.Id, gilres.Conversations[0].GetConvID())
  7614  			gconv := gilres.Conversations[0]
  7615  			require.NotNil(t, gconv.ReaderInfo)
  7616  			require.Equal(t, keybase1.TeamRole_RESTRICTEDBOT, gconv.ReaderInfo.UntrustedTeamRole)
  7617  
  7618  			botSettings2 := keybase1.TeamBotSettings{
  7619  				Mentions: true,
  7620  			}
  7621  			err = ctc.as(t, users[0]).chatLocalHandler().AddBotMember(tc.startCtx, chat1.AddBotMemberArg{
  7622  				ConvID:      created.Id,
  7623  				Username:    botua2.Username,
  7624  				Role:        keybase1.TeamRole_RESTRICTEDBOT,
  7625  				BotSettings: &botSettings2,
  7626  			})
  7627  			require.NoError(t, err)
  7628  			pollForSeqno(5)
  7629  
  7630  			// system message for adding the bot to the team
  7631  			consumeNewPendingMsg(t, listener)
  7632  			consumeBotMessage("", chat1.MessageType_SYSTEM, listener)
  7633  			assertNoMessage(botuaListener)
  7634  			assertNoMessage(botuaListener2)
  7635  			consumeNewMsgLocal(t, listener, chat1.MessageType_SYSTEM)
  7636  
  7637  			gilres, err = ctc.as(t, users[2]).chatLocalHandler().GetInboxAndUnboxLocal(ctx, chat1.GetInboxAndUnboxLocalArg{
  7638  				Query: &chat1.GetInboxLocalQuery{
  7639  					ConvIDs: []chat1.ConversationID{created.Id},
  7640  				},
  7641  			})
  7642  			require.NoError(t, err)
  7643  			require.Equal(t, 1, len(gilres.Conversations))
  7644  			require.Equal(t, created.Id, gilres.Conversations[0].GetConvID())
  7645  			gconv = gilres.Conversations[0]
  7646  			require.NotNil(t, gconv.ReaderInfo)
  7647  			require.Equal(t, keybase1.TeamRole_RESTRICTEDBOT, gconv.ReaderInfo.UntrustedTeamRole)
  7648  
  7649  			t.Logf("send a text message")
  7650  			arg := chat1.PostTextNonblockArg{
  7651  				ConversationID:   created.Id,
  7652  				TlfName:          created.TlfName,
  7653  				TlfPublic:        created.Visibility == keybase1.TLFVisibility_PUBLIC,
  7654  				Body:             "hi",
  7655  				IdentifyBehavior: keybase1.TLFIdentifyBehavior_CHAT_CLI,
  7656  			}
  7657  			_, err = ctc.as(t, users[0]).chatLocalHandler().PostTextNonblock(tc.startCtx, arg)
  7658  			require.NoError(t, err)
  7659  			consumeNewPendingMsg(t, listener)
  7660  			consumeBotMessage(botua.Username, chat1.MessageType_TEXT, listener)
  7661  			consumeBotMessage(botua.Username, chat1.MessageType_TEXT, botuaListener)
  7662  			assertNoMessage(botuaListener2)
  7663  			consumeNewMsgLocal(t, listener, chat1.MessageType_TEXT)
  7664  
  7665  			t.Logf("send ephemeral message")
  7666  			arg.EphemeralLifetime = ephemeralLifetime
  7667  			_, err = ctc.as(t, users[0]).chatLocalHandler().PostTextNonblock(tc.startCtx, arg)
  7668  			require.NoError(t, err)
  7669  			consumeNewPendingMsg(t, listener)
  7670  			consumeBotMessage(botua.Username, chat1.MessageType_TEXT, listener)
  7671  			consumeBotMessage(botua.Username, chat1.MessageType_TEXT, botuaListener)
  7672  			assertNoMessage(botuaListener2)
  7673  			consumeNewMsgLocal(t, listener, chat1.MessageType_TEXT)
  7674  
  7675  			textUnboxed := unboxed
  7676  
  7677  			t.Logf("react to the message")
  7678  			// An ephemeralLifetime is added if we are reacting to an ephemeral message
  7679  			reactionKey := ":+1:"
  7680  			rarg := chat1.PostReactionNonblockArg{
  7681  				ConversationID:   created.Id,
  7682  				TlfName:          created.TlfName,
  7683  				TlfPublic:        created.Visibility == keybase1.TLFVisibility_PUBLIC,
  7684  				Supersedes:       textUnboxed.GetMessageID(),
  7685  				Body:             reactionKey,
  7686  				IdentifyBehavior: keybase1.TLFIdentifyBehavior_CHAT_CLI,
  7687  			}
  7688  			_, err = ctc.as(t, users[0]).chatLocalHandler().PostReactionNonblock(tc.startCtx, rarg)
  7689  			require.NoError(t, err)
  7690  			consumeNewPendingMsg(t, listener)
  7691  			consumeBotMessage(botua.Username, chat1.MessageType_REACTION, listener)
  7692  			consumeBotMessage(botua.Username, chat1.MessageType_REACTION, botuaListener)
  7693  			assertNoMessage(botuaListener2)
  7694  			consumeNewMsgLocal(t, listener, chat1.MessageType_REACTION)
  7695  
  7696  			t.Logf("edit the message")
  7697  			// An ephemeralLifetime is added if we are editing an ephemeral message
  7698  			targetMsgID := textUnboxed.GetMessageID()
  7699  			earg := chat1.PostEditNonblockArg{
  7700  				ConversationID: created.Id,
  7701  				TlfName:        created.TlfName,
  7702  				TlfPublic:      created.Visibility == keybase1.TLFVisibility_PUBLIC,
  7703  				Target: chat1.EditTarget{
  7704  					MessageID: &targetMsgID,
  7705  				},
  7706  				Body:             "hi2",
  7707  				IdentifyBehavior: keybase1.TLFIdentifyBehavior_CHAT_CLI,
  7708  			}
  7709  			_, err = ctc.as(t, users[0]).chatLocalHandler().PostEditNonblock(tc.startCtx, earg)
  7710  			require.NoError(t, err)
  7711  			consumeNewPendingMsg(t, listener)
  7712  			consumeBotMessage(botua.Username, chat1.MessageType_EDIT, listener)
  7713  			consumeBotMessage(botua.Username, chat1.MessageType_EDIT, botuaListener)
  7714  			assertNoMessage(botuaListener2)
  7715  			consumeNewMsgLocal(t, listener, chat1.MessageType_EDIT)
  7716  
  7717  			_, err = ctc.as(t, users[0]).chatLocalHandler().PostLocal(ctx, chat1.PostLocalArg{
  7718  				ConversationID: created.Id,
  7719  				Msg: chat1.MessagePlaintext{
  7720  					ClientHeader: chat1.MessageClientHeader{
  7721  						Conv:        created.Triple,
  7722  						MessageType: chat1.MessageType_TEXT,
  7723  						TlfName:     created.TlfName,
  7724  					},
  7725  					MessageBody: chat1.NewMessageBodyWithText(chat1.MessageText{
  7726  						Body: "REPLY",
  7727  					}),
  7728  				},
  7729  				ReplyTo: &targetMsgID,
  7730  			})
  7731  			require.NoError(t, err)
  7732  			consumeBotMessage("", chat1.MessageType_TEXT, listener)
  7733  			assertNoMessage(botuaListener)
  7734  			assertNoMessage(botuaListener2)
  7735  			consumeNewMsgLocal(t, listener, chat1.MessageType_TEXT)
  7736  
  7737  			// Repost a reaction and ensure it is deleted
  7738  			t.Logf("repost reaction = delete reaction")
  7739  			_, err = ctc.as(t, users[0]).chatLocalHandler().PostReactionNonblock(tc.startCtx, rarg)
  7740  			require.NoError(t, err)
  7741  			consumeNewPendingMsg(t, listener)
  7742  			consumeBotMessage(botua.Username, chat1.MessageType_DELETE, listener)
  7743  			consumeBotMessage(botua.Username, chat1.MessageType_DELETE, botuaListener)
  7744  			assertNoMessage(botuaListener2)
  7745  			consumeNewMsgLocal(t, listener, chat1.MessageType_DELETE)
  7746  
  7747  			t.Logf("delete the message")
  7748  			darg := chat1.PostDeleteNonblockArg{
  7749  				ConversationID:   created.Id,
  7750  				TlfName:          created.TlfName,
  7751  				TlfPublic:        created.Visibility == keybase1.TLFVisibility_PUBLIC,
  7752  				Supersedes:       textUnboxed.GetMessageID(),
  7753  				IdentifyBehavior: keybase1.TLFIdentifyBehavior_CHAT_CLI,
  7754  			}
  7755  			_, err = ctc.as(t, users[0]).chatLocalHandler().PostDeleteNonblock(tc.startCtx, darg)
  7756  			require.NoError(t, err)
  7757  			consumeNewPendingMsg(t, listener)
  7758  			consumeBotMessage(botua.Username, chat1.MessageType_DELETE, listener)
  7759  			consumeBotMessage(botua.Username, chat1.MessageType_DELETE, botuaListener)
  7760  			assertNoMessage(botuaListener2)
  7761  			consumeNewMsgLocal(t, listener, chat1.MessageType_DELETE)
  7762  
  7763  			t.Logf("post headline")
  7764  			headline := "SILENCE!"
  7765  			harg := chat1.PostHeadlineNonblockArg{
  7766  				ConversationID:   created.Id,
  7767  				TlfName:          created.TlfName,
  7768  				TlfPublic:        created.Visibility == keybase1.TLFVisibility_PUBLIC,
  7769  				IdentifyBehavior: keybase1.TLFIdentifyBehavior_CHAT_CLI,
  7770  				Headline:         headline,
  7771  			}
  7772  			_, err = ctc.as(t, users[0]).chatLocalHandler().PostHeadlineNonblock(tc.startCtx, harg)
  7773  			require.NoError(t, err)
  7774  			consumeNewPendingMsg(t, listener)
  7775  			consumeBotMessage("", chat1.MessageType_HEADLINE, listener)
  7776  			assertNoMessage(botuaListener)
  7777  			assertNoMessage(botuaListener2)
  7778  			consumeNewMsgLocal(t, listener, chat1.MessageType_HEADLINE)
  7779  
  7780  			topicName := "zjoinonsend"
  7781  			_, err = ctc.as(t, botua).chatLocalHandler().NewConversationLocal(context.TODO(),
  7782  				chat1.NewConversationLocalArg{
  7783  					TlfName:       created.TlfName,
  7784  					TopicName:     &topicName,
  7785  					TopicType:     chat1.TopicType_DEV,
  7786  					TlfVisibility: keybase1.TLFVisibility_PRIVATE,
  7787  					MembersType:   mt,
  7788  				})
  7789  			require.NoError(t, err)
  7790  
  7791  			// take out botua1 by restricting them to a nonexistent conv.
  7792  			botSettings.Convs = []string{chat1.ConversationID("foo").String()}
  7793  			err = ctc.as(t, users[0]).chatLocalHandler().SetBotMemberSettings(tc.startCtx, chat1.SetBotMemberSettingsArg{
  7794  				ConvID:      created.Id,
  7795  				Username:    botua.Username,
  7796  				BotSettings: botSettings,
  7797  			})
  7798  			require.NoError(t, err)
  7799  
  7800  			actualBotSettings, err := ctc.as(t, users[0]).chatLocalHandler().GetBotMemberSettings(tc.startCtx, chat1.GetBotMemberSettingsArg{
  7801  				ConvID:   created.Id,
  7802  				Username: botua.Username,
  7803  			})
  7804  			require.NoError(t, err)
  7805  			require.Equal(t, botSettings, actualBotSettings)
  7806  
  7807  			pollForSeqno(6)
  7808  
  7809  			arg = chat1.PostTextNonblockArg{
  7810  				ConversationID:   created.Id,
  7811  				TlfName:          created.TlfName,
  7812  				TlfPublic:        created.Visibility == keybase1.TLFVisibility_PUBLIC,
  7813  				Body:             "@" + botua2.Username,
  7814  				IdentifyBehavior: keybase1.TLFIdentifyBehavior_CHAT_CLI,
  7815  			}
  7816  			_, err = ctc.as(t, users[0]).chatLocalHandler().PostTextNonblock(tc.startCtx, arg)
  7817  			require.NoError(t, err)
  7818  			consumeNewPendingMsg(t, listener)
  7819  			consumeBotMessage(botua2.Username, chat1.MessageType_TEXT, listener)
  7820  			consumeBotMessage(botua2.Username, chat1.MessageType_TEXT, botuaListener2)
  7821  			assertNoMessage(botuaListener)
  7822  			consumeNewMsgLocal(t, listener, chat1.MessageType_TEXT)
  7823  
  7824  			// send as a bot
  7825  			larg := chat1.PostLocalArg{
  7826  				ConversationID: created.Id,
  7827  				Msg: chat1.MessagePlaintext{
  7828  					ClientHeader: chat1.MessageClientHeader{
  7829  						Conv:        created.Triple,
  7830  						TlfName:     created.TlfName,
  7831  						MessageType: chat1.MessageType_TEXT,
  7832  					},
  7833  					MessageBody: chat1.NewMessageBodyWithText(chat1.MessageText{Body: "blah"}),
  7834  				},
  7835  			}
  7836  			_, err = ctc.as(t, botua).chatLocalHandler().PostLocal(ctc.as(t, botua).startCtx, larg)
  7837  			require.Error(t, err)
  7838  
  7839  			_, err = ctc.as(t, botua2).chatLocalHandler().PostLocal(ctc.as(t, botua2).startCtx, larg)
  7840  			require.NoError(t, err)
  7841  			consumeBotMessage(botua2.Username, chat1.MessageType_TEXT, listener)
  7842  			consumeBotMessage(botua2.Username, chat1.MessageType_TEXT, botuaListener2)
  7843  			assertNoMessage(botuaListener)
  7844  
  7845  			// ensure gregor withholds messages for restricted bot members
  7846  			// unless it is specifically keyed for them.
  7847  			tv, err := ctc.world.Tcs[botua.Username].Context().ConvSource.Pull(ctc.as(t, botua).startCtx,
  7848  				created.Id, botuaUID, chat1.GetThreadReason_GENERAL, nil, nil, nil)
  7849  			require.NoError(t, err)
  7850  			// expected botua keyed messages
  7851  			// NOTE that for ephemeral messages we include deleted messages in the thread
  7852  			var expectedBotuaTyps []chat1.MessageType
  7853  			if ephemeralLifetime == nil {
  7854  				expectedBotuaTyps = []chat1.MessageType{
  7855  					chat1.MessageType_DELETE,
  7856  					chat1.MessageType_DELETE,
  7857  					chat1.MessageType_TEXT,
  7858  				}
  7859  			} else {
  7860  				expectedBotuaTyps = []chat1.MessageType{
  7861  					chat1.MessageType_DELETE,
  7862  					chat1.MessageType_DELETE,
  7863  					chat1.MessageType_EDIT,
  7864  					chat1.MessageType_REACTION,
  7865  					chat1.MessageType_TEXT,
  7866  					chat1.MessageType_TEXT,
  7867  				}
  7868  			}
  7869  			validIndex := 0
  7870  			for i, msg := range tv.Messages {
  7871  				t.Logf("INDEX: %d VALID: %v, %s", i, msg.IsValid(), msg.DebugString())
  7872  				if msg.IsValid() {
  7873  					require.Equal(t, expectedBotuaTyps[validIndex], msg.GetMessageType())
  7874  					validIndex++
  7875  				}
  7876  			}
  7877  			require.Equal(t, len(expectedBotuaTyps), validIndex)
  7878  
  7879  			tv, err = ctc.world.Tcs[botua2.Username].Context().ConvSource.Pull(ctc.as(t, botua2).startCtx,
  7880  				created.Id, botuaUID2, chat1.GetThreadReason_GENERAL, nil, nil, nil)
  7881  			require.NoError(t, err)
  7882  			// expected botua2 keyed messages
  7883  			// NOTE that for ephemeral messages we include deleted messages in the thread
  7884  			expectedBotua2Typs := []chat1.MessageType{
  7885  				chat1.MessageType_TEXT,
  7886  				chat1.MessageType_TEXT,
  7887  			}
  7888  			validIndex = 0
  7889  			require.Equal(t, 13, len(tv.Messages))
  7890  			for _, msg := range tv.Messages {
  7891  				if msg.IsValid() {
  7892  					require.Equal(t, expectedBotua2Typs[validIndex], msg.GetMessageType())
  7893  					validIndex++
  7894  				}
  7895  			}
  7896  			require.Equal(t, 2, validIndex)
  7897  
  7898  			// take out botua2 by upgrading them to BOT
  7899  			err = ctc.as(t, users[0]).chatLocalHandler().EditBotMember(tc.startCtx, chat1.EditBotMemberArg{
  7900  				ConvID:   created.Id,
  7901  				Username: botua2.Username,
  7902  				Role:     keybase1.TeamRole_BOT,
  7903  			})
  7904  			require.NoError(t, err)
  7905  			pollForSeqno(7)
  7906  
  7907  			// messages is not keyed for any restricted bot
  7908  			_, err = ctc.as(t, users[0]).chatLocalHandler().PostTextNonblock(tc.startCtx, arg)
  7909  			require.NoError(t, err)
  7910  			consumeNewPendingMsg(t, listener)
  7911  			consumeBotMessage("", chat1.MessageType_TEXT, listener)
  7912  			consumeBotMessage("", chat1.MessageType_TEXT, botuaListener2)
  7913  			assertNoMessage(botuaListener)
  7914  
  7915  			// botua2 can send without issue
  7916  			_, err = ctc.as(t, botua2).chatLocalHandler().PostLocal(ctc.as(t, botua2).startCtx, larg)
  7917  			require.NoError(t, err)
  7918  			consumeBotMessage("", chat1.MessageType_TEXT, listener)
  7919  			consumeBotMessage("", chat1.MessageType_TEXT, botuaListener2)
  7920  			assertNoMessage(botuaListener)
  7921  
  7922  			// remove both bots.
  7923  			err = ctc.as(t, users[0]).chatLocalHandler().RemoveBotMember(tc.startCtx, chat1.RemoveBotMemberArg{
  7924  				ConvID:   created.Id,
  7925  				Username: botua.Username,
  7926  			})
  7927  			require.NoError(t, err)
  7928  			err = ctc.as(t, users[0]).chatLocalHandler().RemoveBotMember(tc.startCtx, chat1.RemoveBotMemberArg{
  7929  				ConvID:   created.Id,
  7930  				Username: botua2.Username,
  7931  			})
  7932  			require.NoError(t, err)
  7933  			team, err := teams.Load(ctx, tc.m.G(), keybase1.LoadTeamArg{
  7934  				ID: teamID,
  7935  			})
  7936  			require.NoError(t, err)
  7937  			isMember := team.IsMember(ctx, botua.GetUserVersion())
  7938  			require.False(t, isMember)
  7939  			isMember = team.IsMember(ctx, botua2.GetUserVersion())
  7940  			require.False(t, isMember)
  7941  		})
  7942  	})
  7943  }
  7944  
  7945  // Filter out journey cards. The test doesn't need to know about them. Mutates thread.Messages
  7946  func filterOutJourneycards(thread *chat1.ThreadView) {
  7947  	filtered := thread.Messages[:0]
  7948  	for _, msg := range thread.Messages {
  7949  		if msg.Journeycard__ == nil {
  7950  			filtered = append(filtered, msg)
  7951  		}
  7952  	}
  7953  	thread.Messages = filtered
  7954  }
  7955  
  7956  func TestTeamBotChannelMembership(t *testing.T) {
  7957  	runWithMemberTypes(t, func(mt chat1.ConversationMembersType) {
  7958  		// Only test for TEAM type
  7959  		if mt != chat1.ConversationMembersType_TEAM {
  7960  			return
  7961  		}
  7962  		ctc := makeChatTestContext(t, "TeamBotChannelMembership", 3)
  7963  		defer ctc.cleanup()
  7964  		users := ctc.users()
  7965  
  7966  		ui := kbtest.NewChatUI()
  7967  		ctc.as(t, users[0]).h.mockChatUI = ui
  7968  		tc := ctc.as(t, users[0])
  7969  		listener := newServerChatListener()
  7970  		ctc.as(t, users[0]).h.G().NotifyRouter.AddListener(listener)
  7971  		ctc.world.Tcs[users[0].Username].ChatG.Syncer.(*Syncer).isConnected = true
  7972  
  7973  		botua := users[1]
  7974  		botua2 := users[2]
  7975  
  7976  		botuaListener := newServerChatListener()
  7977  		ctc.as(t, botua).h.G().NotifyRouter.AddListener(botuaListener)
  7978  		ctc.world.Tcs[botua.Username].ChatG.Syncer.(*Syncer).isConnected = true
  7979  		botuaListener2 := newServerChatListener()
  7980  		ctc.as(t, botua2).h.G().NotifyRouter.AddListener(botuaListener2)
  7981  		ctc.world.Tcs[botua2.Username].ChatG.Syncer.(*Syncer).isConnected = true
  7982  
  7983  		var err error
  7984  		created := mustCreateConversationForTest(t, ctc, users[0], chat1.TopicType_CHAT, mt)
  7985  
  7986  		teamID, err := keybase1.TeamIDFromString(created.Triple.Tlfid.String())
  7987  		require.NoError(t, err)
  7988  
  7989  		topicName := "zjoinonsend"
  7990  		_, err = tc.chatLocalHandler().NewConversationLocal(context.TODO(),
  7991  			chat1.NewConversationLocalArg{
  7992  				TlfName:       created.TlfName,
  7993  				TopicName:     &topicName,
  7994  				TopicType:     chat1.TopicType_CHAT,
  7995  				TlfVisibility: keybase1.TLFVisibility_PRIVATE,
  7996  				MembersType:   mt,
  7997  			})
  7998  		require.NoError(t, err)
  7999  
  8000  		pollForSeqno := func(expectedSeqno keybase1.Seqno) {
  8001  			found := false
  8002  			timeout := time.After(30 * time.Second)
  8003  			for !found {
  8004  				select {
  8005  				case teamChange := <-listener.teamChangedByID:
  8006  					found = teamChange.TeamID == teamID &&
  8007  						teamChange.LatestSeqno == expectedSeqno
  8008  					if !found {
  8009  						t.Logf("Got teamChangedByID, but not one we are looking for: %s", spew.Sdump(teamChange))
  8010  					}
  8011  				case <-timeout:
  8012  					require.Failf(t, "teamChangedByID not received",
  8013  						"team id: %s, waiting for seqno: %d", teamID, expectedSeqno)
  8014  				}
  8015  			}
  8016  		}
  8017  
  8018  		// add botua as a RESTRICTEDBOT with no conv restrictions, make sure
  8019  		// they are auto-added to the conv
  8020  		err = ctc.as(t, users[0]).chatLocalHandler().AddBotMember(tc.startCtx, chat1.AddBotMemberArg{
  8021  			ConvID:      created.Id,
  8022  			Username:    botua.Username,
  8023  			Role:        keybase1.TeamRole_RESTRICTEDBOT,
  8024  			BotSettings: &keybase1.TeamBotSettings{},
  8025  		})
  8026  		require.NoError(t, err)
  8027  		// AddBotMember should have posted two links: change_membership and bot_settings. Wait
  8028  		// for seqno=3.
  8029  		pollForSeqno(3)
  8030  		consumeMembersUpdate(t, listener)
  8031  		consumeMembersUpdate(t, listener)
  8032  		consumeJoinConv(t, botuaListener)
  8033  		consumeJoinConv(t, botuaListener)
  8034  
  8035  		// add botua2 as a BOT, make sure they auto-added to the convs
  8036  		err = ctc.as(t, users[0]).chatLocalHandler().AddBotMember(tc.startCtx, chat1.AddBotMemberArg{
  8037  			ConvID:   created.Id,
  8038  			Username: botua2.Username,
  8039  			Role:     keybase1.TeamRole_BOT,
  8040  		})
  8041  		require.NoError(t, err)
  8042  		pollForSeqno(4)
  8043  		consumeMembersUpdate(t, listener)
  8044  		consumeMembersUpdate(t, listener)
  8045  		consumeMembersUpdate(t, botuaListener)
  8046  		consumeMembersUpdate(t, botuaListener)
  8047  		consumeJoinConv(t, botuaListener2)
  8048  		consumeJoinConv(t, botuaListener2)
  8049  
  8050  		// downgrade botua to be restricted to 0 channels
  8051  		err = ctc.as(t, users[0]).chatLocalHandler().EditBotMember(tc.startCtx, chat1.EditBotMemberArg{
  8052  			ConvID:   created.Id,
  8053  			Username: botua.Username,
  8054  			Role:     keybase1.TeamRole_RESTRICTEDBOT,
  8055  			BotSettings: &keybase1.TeamBotSettings{
  8056  				Convs: []string{"deadbeef"},
  8057  			},
  8058  		})
  8059  		require.NoError(t, err)
  8060  		// `EditBotMember` should have posted two links: change_membership and bot_settings. Wait
  8061  		// for seqno=6.
  8062  		pollForSeqno(6)
  8063  		consumeMembersUpdate(t, listener)
  8064  		consumeMembersUpdate(t, listener)
  8065  		consumeMembersUpdate(t, botuaListener2)
  8066  		consumeMembersUpdate(t, botuaListener2)
  8067  		consumeLeaveConv(t, botuaListener)
  8068  		consumeLeaveConv(t, botuaListener)
  8069  
  8070  		// create a new conv and makes sure bots are auto-added
  8071  		topicName = "zjoinonsend2"
  8072  		convres, err := tc.chatLocalHandler().NewConversationLocal(context.TODO(),
  8073  			chat1.NewConversationLocalArg{
  8074  				TlfName:       created.TlfName,
  8075  				TopicName:     &topicName,
  8076  				TopicType:     chat1.TopicType_CHAT,
  8077  				TlfVisibility: keybase1.TLFVisibility_PRIVATE,
  8078  				MembersType:   mt,
  8079  			})
  8080  		require.NoError(t, err)
  8081  
  8082  		expectedUsers := []gregor1.UID{users[0].User.GetUID().ToBytes(), users[2].User.GetUID().ToBytes()}
  8083  		sort.Slice(expectedUsers, func(i, j int) bool {
  8084  			return bytes.Compare(expectedUsers[i], expectedUsers[j]) < 0
  8085  		})
  8086  		for _, user := range users {
  8087  			g := ctc.world.Tcs[user.Username].Context()
  8088  			uids, err := g.ParticipantsSource.Get(context.TODO(), user.GetUID().ToBytes(),
  8089  				convres.Conv.GetConvID(), types.InboxSourceDataSourceAll)
  8090  			require.NoError(t, err)
  8091  			sort.Slice(uids, func(i, j int) bool {
  8092  				return bytes.Compare(expectedUsers[i], expectedUsers[j]) < 0
  8093  			})
  8094  			require.Equal(t, expectedUsers, uids)
  8095  		}
  8096  	})
  8097  }
  8098  
  8099  func TestChatSrvNewConversationsLocal(t *testing.T) {
  8100  	runWithMemberTypes(t, func(mt chat1.ConversationMembersType) {
  8101  		switch mt {
  8102  		case chat1.ConversationMembersType_TEAM:
  8103  		default:
  8104  			return
  8105  		}
  8106  
  8107  		ctc := makeChatTestContext(t, "NewConversationsLocal", 3)
  8108  		defer ctc.cleanup()
  8109  		users := ctc.users()
  8110  
  8111  		key := teamKey(users)
  8112  		name := createTeamWithWriters(ctc.world.Tcs[users[0].Username].TestContext, users[1:])
  8113  		ctc.teamCache[key] = name
  8114  		tlfName := strings.ToLower(name)
  8115  
  8116  		type L struct {
  8117  			tlfName     string
  8118  			channelName *string
  8119  			ok          bool
  8120  		}
  8121  		sp := func(x string) *string { return &x }
  8122  		argument := func(ll []L) (arg chat1.NewConversationsLocalArg) {
  8123  			arg.IdentifyBehavior = keybase1.TLFIdentifyBehavior_CHAT_CLI
  8124  			for _, l := range ll {
  8125  				arg.NewConversationLocalArguments = append(arg.NewConversationLocalArguments, chat1.NewConversationLocalArgument{
  8126  					TlfName:       name,
  8127  					TopicType:     chat1.TopicType_CHAT,
  8128  					TopicName:     l.channelName,
  8129  					TlfVisibility: keybase1.TLFVisibility_PRIVATE,
  8130  					MembersType:   chat1.ConversationMembersType_TEAM,
  8131  				})
  8132  			}
  8133  			return arg
  8134  		}
  8135  
  8136  		t.Logf("creating many channels, some with bad names")
  8137  		ll := []L{
  8138  			{tlfName: tlfName, channelName: nil, ok: true},
  8139  			{tlfName: tlfName, channelName: sp("now"), ok: true},
  8140  			{tlfName: tlfName, channelName: sp("i"), ok: true},
  8141  			{tlfName: tlfName, channelName: sp("am"), ok: true},
  8142  			{tlfName: tlfName, channelName: sp("#become"), ok: false},
  8143  			{tlfName: tlfName, channelName: sp("death"), ok: true},
  8144  			{tlfName: tlfName, channelName: sp("destroyer.of"), ok: false},
  8145  			{tlfName: tlfName, channelName: sp("worlds/"), ok: false},
  8146  			{tlfName: tlfName, channelName: sp("!"), ok: false},
  8147  		}
  8148  		tc := ctc.as(t, users[0])
  8149  		res, err := tc.chatLocalHandler().NewConversationsLocal(tc.startCtx, argument(ll))
  8150  		require.Error(t, err)
  8151  		require.Equal(t, len(ll), len(res.Results))
  8152  		for idx, l := range ll {
  8153  			result := res.Results[idx]
  8154  			if l.ok {
  8155  				require.NotNil(t, result.Result)
  8156  				require.Equal(t, strings.ToLower(l.tlfName), result.Result.Conv.Info.TlfName)
  8157  				if n := l.channelName; n != nil {
  8158  					require.Equal(t, *n, result.Result.Conv.Info.TopicName)
  8159  				}
  8160  			} else {
  8161  				require.NotNil(t, result.Err)
  8162  				require.Nil(t, result.Result)
  8163  			}
  8164  		}
  8165  
  8166  		t.Logf("testing idempotency")
  8167  		ll[4].channelName = sp("become")
  8168  		ll[6].channelName = sp("destroyerof")
  8169  		ll[7].channelName = sp("worlds")
  8170  		ll[8].channelName = sp("exclam")
  8171  		res, err = tc.chatLocalHandler().NewConversationsLocal(tc.startCtx, argument(ll))
  8172  		require.NoError(t, err)
  8173  		require.Equal(t, len(ll), len(res.Results))
  8174  	})
  8175  }
  8176  
  8177  func TestChatSrvDefaultTeamChannels(t *testing.T) {
  8178  	runWithMemberTypes(t, func(mt chat1.ConversationMembersType) {
  8179  		switch mt {
  8180  		case chat1.ConversationMembersType_TEAM:
  8181  		default:
  8182  			return
  8183  		}
  8184  
  8185  		ctc := makeChatTestContext(t, "DefaultTeamChannels", 2)
  8186  		defer ctc.cleanup()
  8187  		users := ctc.users()
  8188  
  8189  		created := mustCreateConversationForTest(t, ctc, users[0], chat1.TopicType_CHAT, mt)
  8190  		listener := newServerChatListener()
  8191  		ctc.as(t, users[0]).h.G().NotifyRouter.AddListener(listener)
  8192  		ctc.world.Tcs[users[0].Username].ChatG.Syncer.(*Syncer).isConnected = true
  8193  		teamID, err := keybase1.TeamIDFromString(created.Triple.Tlfid.String())
  8194  		require.NoError(t, err)
  8195  
  8196  		tc := ctc.as(t, users[0])
  8197  		_ = ctc.as(t, users[1])
  8198  		topicName := "zjoinonsend"
  8199  		convres, err := tc.chatLocalHandler().NewConversationLocal(context.TODO(),
  8200  			chat1.NewConversationLocalArg{
  8201  				TlfName:       created.TlfName,
  8202  				TopicName:     &topicName,
  8203  				TopicType:     chat1.TopicType_CHAT,
  8204  				TlfVisibility: keybase1.TLFVisibility_PRIVATE,
  8205  				MembersType:   mt,
  8206  			})
  8207  		require.NoError(t, err)
  8208  
  8209  		res, err := tc.chatLocalHandler().GetDefaultTeamChannelsLocal(context.TODO(), teamID)
  8210  		require.NoError(t, err)
  8211  		require.Zero(t, len(res.Convs))
  8212  
  8213  		_, err = tc.chatLocalHandler().SetDefaultTeamChannelsLocal(context.TODO(), chat1.SetDefaultTeamChannelsLocalArg{
  8214  			TeamID: teamID,
  8215  			Convs:  []chat1.ConvIDStr{convres.Conv.GetConvID().ConvIDStr()},
  8216  		})
  8217  		require.NoError(t, err)
  8218  
  8219  		res, err = tc.chatLocalHandler().GetDefaultTeamChannelsLocal(context.TODO(), teamID)
  8220  		require.NoError(t, err)
  8221  		require.Len(t, res.Convs, 1)
  8222  		require.Equal(t, topicName, res.Convs[0].Channel)
  8223  
  8224  		pollForSeqno := func(expectedSeqno keybase1.Seqno) {
  8225  			found := false
  8226  			for !found {
  8227  				select {
  8228  				case teamChange := <-listener.teamChangedByID:
  8229  					found = teamChange.TeamID == teamID &&
  8230  						teamChange.LatestSeqno == expectedSeqno
  8231  				case <-time.After(20 * time.Second):
  8232  					require.Fail(t, "no event received")
  8233  				}
  8234  			}
  8235  		}
  8236  		err = ctc.as(t, users[0]).chatLocalHandler().AddBotMember(tc.startCtx, chat1.AddBotMemberArg{
  8237  			ConvID:   created.Id,
  8238  			Username: users[1].Username,
  8239  			Role:     keybase1.TeamRole_BOT,
  8240  		})
  8241  		require.NoError(t, err)
  8242  		pollForSeqno(2)
  8243  
  8244  		expectedUsers := []gregor1.UID{users[0].User.GetUID().ToBytes(), users[1].User.GetUID().ToBytes()}
  8245  		sort.Slice(expectedUsers, func(i, j int) bool {
  8246  			return bytes.Compare(expectedUsers[i], expectedUsers[j]) < 0
  8247  		})
  8248  		for _, user := range users {
  8249  			g := ctc.world.Tcs[user.Username].Context()
  8250  			uids, err := g.ParticipantsSource.Get(context.TODO(), user.GetUID().ToBytes(),
  8251  				convres.Conv.GetConvID(), types.InboxSourceDataSourceAll)
  8252  			require.NoError(t, err)
  8253  			sort.Slice(uids, func(i, j int) bool {
  8254  				return bytes.Compare(expectedUsers[i], expectedUsers[j]) < 0
  8255  			})
  8256  			require.Equal(t, expectedUsers, uids)
  8257  		}
  8258  	})
  8259  }
  8260  
  8261  func TestChatSrvTeamActivity(t *testing.T) {
  8262  	runWithMemberTypes(t, func(mt chat1.ConversationMembersType) {
  8263  		switch mt {
  8264  		case chat1.ConversationMembersType_TEAM:
  8265  		default:
  8266  			return
  8267  		}
  8268  
  8269  		ctc := makeChatTestContext(t, "TeamActivity", 2)
  8270  		defer ctc.cleanup()
  8271  		users := ctc.users()
  8272  
  8273  		created := mustCreateConversationForTest(t, ctc, users[0], chat1.TopicType_CHAT, mt)
  8274  		tlfID1 := created.Triple.Tlfid.TLFIDStr()
  8275  		created2 := mustCreateConversationForTest(t, ctc, users[0], chat1.TopicType_CHAT, mt, users[1])
  8276  		tlfID2 := created2.Triple.Tlfid.TLFIDStr()
  8277  
  8278  		tc := ctc.as(t, users[0])
  8279  		topicName := "zjoinonsend"
  8280  		nc1, err := tc.chatLocalHandler().NewConversationLocal(context.TODO(),
  8281  			chat1.NewConversationLocalArg{
  8282  				TlfName:       created.TlfName,
  8283  				TopicName:     &topicName,
  8284  				TopicType:     chat1.TopicType_CHAT,
  8285  				TlfVisibility: keybase1.TLFVisibility_PRIVATE,
  8286  				MembersType:   mt,
  8287  			})
  8288  		require.NoError(t, err)
  8289  		nc2, err := tc.chatLocalHandler().NewConversationLocal(context.TODO(),
  8290  			chat1.NewConversationLocalArg{
  8291  				TlfName:       created2.TlfName,
  8292  				TopicName:     &topicName,
  8293  				TopicType:     chat1.TopicType_CHAT,
  8294  				TlfVisibility: keybase1.TLFVisibility_PRIVATE,
  8295  				MembersType:   mt,
  8296  			})
  8297  		require.NoError(t, err)
  8298  
  8299  		lastActive, err := tc.chatLocalHandler().GetLastActiveForTLF(context.TODO(), tlfID1)
  8300  		require.NoError(t, err)
  8301  		require.Equal(t, chat1.LastActiveStatus_ACTIVE, lastActive)
  8302  
  8303  		res, err := tc.chatLocalHandler().GetLastActiveForTeams(context.TODO())
  8304  		require.NoError(t, err)
  8305  		require.Equal(t, 2, len(res.Teams))
  8306  		require.Equal(t, chat1.LastActiveStatus_ACTIVE, res.Teams[tlfID1])
  8307  		require.Equal(t, chat1.LastActiveStatus_ACTIVE, res.Teams[tlfID2])
  8308  		require.Equal(t, 4, len(res.Channels))
  8309  		require.Equal(t, chat1.LastActiveStatus_ACTIVE, res.Channels[nc1.UiConv.ConvID])
  8310  		require.Equal(t, chat1.LastActiveStatus_ACTIVE, res.Channels[nc2.UiConv.ConvID])
  8311  	})
  8312  }
  8313  
  8314  func TestChatSrvGetRecentJoins(t *testing.T) {
  8315  	runWithMemberTypes(t, func(mt chat1.ConversationMembersType) {
  8316  		switch mt {
  8317  		case chat1.ConversationMembersType_TEAM:
  8318  		default:
  8319  			return
  8320  		}
  8321  
  8322  		ctc := makeChatTestContext(t, "TestChatSrvGetRecentJoins", 2)
  8323  		defer ctc.cleanup()
  8324  		users := ctc.users()
  8325  		tc := ctc.as(t, users[0])
  8326  		created := mustCreateConversationForTest(t, ctc, users[0], chat1.TopicType_CHAT, mt, users[1])
  8327  		numJoins, err := tc.chatLocalHandler().GetRecentJoinsLocal(context.TODO(), created.Id)
  8328  		require.NoError(t, err)
  8329  		require.Equal(t, 2, numJoins)
  8330  	})
  8331  }
  8332  
  8333  func TestChatSrvGetLastActiveAt(t *testing.T) {
  8334  	runWithMemberTypes(t, func(mt chat1.ConversationMembersType) {
  8335  		switch mt {
  8336  		case chat1.ConversationMembersType_TEAM:
  8337  		default:
  8338  			return
  8339  		}
  8340  
  8341  		ctc := makeChatTestContext(t, "TestChatSrvGetLastActiveAt", 2)
  8342  		defer ctc.cleanup()
  8343  		users := ctc.users()
  8344  
  8345  		username1 := users[0].User.GetName()
  8346  		username2 := users[1].User.GetName()
  8347  		tc := ctc.as(t, users[0])
  8348  		created := mustCreateConversationForTest(t, ctc, users[0], chat1.TopicType_CHAT, mt, users[1])
  8349  		mustPostLocalForTest(t, ctc, users[0], created, chat1.NewMessageBodyWithText(chat1.MessageText{
  8350  			Body: "HI",
  8351  		}))
  8352  
  8353  		teamID := keybase1.TeamID(created.Triple.Tlfid.String())
  8354  		t.Logf("teamID: %v, username1: %v, username2: %v", teamID, username1, username2)
  8355  		lastActiveAt, err := tc.chatLocalHandler().GetLastActiveAtLocal(context.TODO(), chat1.GetLastActiveAtLocalArg{
  8356  			TeamID:   teamID,
  8357  			Username: username1,
  8358  		})
  8359  		require.NoError(t, err)
  8360  		require.NotZero(t, lastActiveAt)
  8361  
  8362  		lastActiveAt, err = tc.chatLocalHandler().GetLastActiveAtLocal(context.TODO(), chat1.GetLastActiveAtLocalArg{
  8363  			TeamID:   teamID,
  8364  			Username: username2,
  8365  		})
  8366  		require.NoError(t, err)
  8367  		require.Zero(t, lastActiveAt)
  8368  
  8369  		mustPostLocalForTest(t, ctc, users[1], created, chat1.NewMessageBodyWithText(chat1.MessageText{
  8370  			Body: "HI",
  8371  		}))
  8372  		lastActiveAt, err = tc.chatLocalHandler().GetLastActiveAtLocal(context.TODO(), chat1.GetLastActiveAtLocalArg{
  8373  			TeamID:   teamID,
  8374  			Username: username2,
  8375  		})
  8376  		require.NoError(t, err)
  8377  		require.Zero(t, lastActiveAt)
  8378  
  8379  		err = tc.h.G().TeamChannelSource.OnDbNuke(tc.m)
  8380  		require.NoError(t, err)
  8381  		lastActiveAt, err = tc.chatLocalHandler().GetLastActiveAtLocal(context.TODO(), chat1.GetLastActiveAtLocalArg{
  8382  			TeamID:   teamID,
  8383  			Username: username2,
  8384  		})
  8385  		require.NoError(t, err)
  8386  		require.NotZero(t, lastActiveAt)
  8387  	})
  8388  }