github.com/keybase/client/go@v0.0.0-20241007131713-f10651d043c8/chat/utils/utils_test.go (about)

     1  package utils
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  	"regexp"
     8  	"testing"
     9  	"time"
    10  
    11  	"github.com/keybase/client/go/chat/globals"
    12  	"github.com/keybase/client/go/chat/types"
    13  	"github.com/keybase/client/go/externalstest"
    14  	"github.com/keybase/client/go/libkb"
    15  	"github.com/keybase/client/go/protocol/chat1"
    16  	"github.com/keybase/client/go/protocol/gregor1"
    17  	"github.com/keybase/client/go/protocol/keybase1"
    18  
    19  	"github.com/stretchr/testify/require"
    20  )
    21  
    22  func TestParseDurationExtended(t *testing.T) {
    23  	test := func(input string, expected time.Duration) {
    24  		d, err := ParseDurationExtended(input)
    25  		if err != nil {
    26  			t.Fatal(err)
    27  		}
    28  		if d != expected {
    29  			t.Fatalf("wrong parsed duration. Expected %v, got %v\n", expected, d)
    30  		}
    31  	}
    32  	test("1d", time.Hour*24)
    33  	test("123d12h2ns", 123*24*time.Hour+12*time.Hour+2*time.Nanosecond)
    34  }
    35  
    36  func TestParseAtMentionsNames(t *testing.T) {
    37  	text := "@Chat_1e2263952c hello! @Mike From @chat_5511c5e0ce. @ksjdskj 889@ds8 @_dskdjs @k1 @0011_"
    38  	matches := parseRegexpNames(context.TODO(), text, atMentionRegExp)
    39  	var names, normalizedNames []string
    40  	for _, m := range matches {
    41  		names = append(names, m.name)
    42  		normalizedNames = append(normalizedNames, m.normalizedName)
    43  	}
    44  
    45  	expected := []string{"Chat_1e2263952c", "Mike", "chat_5511c5e0ce", "ksjdskj", "k1", "0011_"}
    46  	require.Equal(t, expected, names)
    47  	expectedNormalized := []string{"chat_1e2263952c", "mike", "chat_5511c5e0ce", "ksjdskj", "k1", "0011_"}
    48  	require.Equal(t, expectedNormalized, normalizedNames)
    49  	text = "@mike@jim"
    50  	matches = parseRegexpNames(context.TODO(), text, atMentionRegExp)
    51  	names = []string{}
    52  	for _, m := range matches {
    53  		names = append(names, m.name)
    54  	}
    55  	expected = []string{"mike"}
    56  	require.Equal(t, expected, names)
    57  }
    58  
    59  type testTeamChannelSource struct {
    60  	channels []string
    61  }
    62  
    63  var _ types.TeamChannelSource = (*testTeamChannelSource)(nil)
    64  
    65  func newTestTeamChannelSource(channels []string) *testTeamChannelSource {
    66  	return &testTeamChannelSource{
    67  		channels: channels,
    68  	}
    69  }
    70  
    71  func (t *testTeamChannelSource) GetChannelsTopicName(ctx context.Context, uid gregor1.UID,
    72  	teamID chat1.TLFID, topicType chat1.TopicType) (res []chat1.ChannelNameMention, err error) {
    73  	for _, c := range t.channels {
    74  		res = append(res, chat1.ChannelNameMention{
    75  			TopicName: c,
    76  		})
    77  	}
    78  	return res, nil
    79  }
    80  
    81  func (t *testTeamChannelSource) GetLastActiveForTLF(ctx context.Context, uid gregor1.UID, tlfID chat1.TLFID,
    82  	topicType chat1.TopicType) (gregor1.Time, error) {
    83  	return 0, fmt.Errorf("testTeamChannelSource.GetLastActiveForTLF not implemented")
    84  }
    85  
    86  func (t *testTeamChannelSource) GetLastActiveForTeams(ctx context.Context, uid gregor1.UID,
    87  	topicType chat1.TopicType) (res chat1.LastActiveTimeAll, err error) {
    88  	return res, fmt.Errorf("testTeamChannelSource.GetLastActiveForTeams not implemented")
    89  }
    90  
    91  func (t *testTeamChannelSource) GetChannelTopicName(ctx context.Context, uid gregor1.UID,
    92  	teamID chat1.TLFID, topicType chat1.TopicType, convID chat1.ConversationID) (string, error) {
    93  	return "", fmt.Errorf("testTeamChannelSource.GetChannelTopicName not implemented")
    94  }
    95  
    96  func (t *testTeamChannelSource) GetChannelsFull(ctx context.Context, uid gregor1.UID,
    97  	teamID chat1.TLFID, topicType chat1.TopicType) (res []chat1.ConversationLocal, err error) {
    98  	return res, nil
    99  }
   100  
   101  func (t *testTeamChannelSource) GetRecentJoins(ctx context.Context, convID chat1.ConversationID, remoteClient chat1.RemoteInterface) (int, error) {
   102  	return 0, nil
   103  }
   104  
   105  func (t *testTeamChannelSource) GetLastActiveAt(ctx context.Context, teamID keybase1.TeamID, uid gregor1.UID, remoteClient chat1.RemoteInterface) (gregor1.Time, error) {
   106  	return 0, nil
   107  }
   108  
   109  func (t *testTeamChannelSource) OnDbNuke(mctx libkb.MetaContext) error {
   110  	return nil
   111  }
   112  
   113  func (t *testTeamChannelSource) OnLogout(mctx libkb.MetaContext) error {
   114  	return nil
   115  }
   116  
   117  func TestParseChannelNameMentions(t *testing.T) {
   118  	uid := gregor1.UID{0}
   119  	teamID := chat1.TLFID{0}
   120  	chans := []string{"general", "random", "miketime"}
   121  	text := "#miketime is secret. #general has everyone. #random exists. #offtopic does not."
   122  	matches := ParseChannelNameMentions(context.TODO(), text, uid, teamID,
   123  		newTestTeamChannelSource(chans))
   124  	expected := []chat1.ChannelNameMention{
   125  		{TopicName: "miketime"},
   126  		{TopicName: "general"},
   127  		{TopicName: "random"},
   128  	}
   129  	require.Equal(t, expected, matches)
   130  }
   131  
   132  type testUIDSource struct {
   133  	libkb.UPAKLoader
   134  	users map[string]keybase1.UID
   135  }
   136  
   137  func newTestUIDSource() *testUIDSource {
   138  	return &testUIDSource{
   139  		users: make(map[string]keybase1.UID),
   140  	}
   141  }
   142  
   143  type testInboxSource struct {
   144  	types.InboxSource
   145  }
   146  
   147  func (t testInboxSource) Read(ctx context.Context, uid gregor1.UID, localizeTyp types.ConversationLocalizerTyp,
   148  	dataSource types.InboxSourceDataSourceTyp, maxLocalize *int, query *chat1.GetInboxLocalQuery) (types.Inbox, chan types.AsyncInboxResult, error) {
   149  	return types.Inbox{
   150  		Convs: []chat1.ConversationLocal{{
   151  			Info: chat1.ConversationInfoLocal{
   152  				TopicName: "mike",
   153  			},
   154  		},
   155  		}}, nil, nil
   156  }
   157  
   158  func (s *testUIDSource) LookupUID(ctx context.Context, un libkb.NormalizedUsername) (uid keybase1.UID, err error) {
   159  	var ok bool
   160  	if uid, ok = s.users[un.String()]; ok {
   161  		return uid, nil
   162  	}
   163  	return uid, errors.New("invalid username")
   164  }
   165  
   166  func (s *testUIDSource) AddUser(username string, uid gregor1.UID) {
   167  	s.users[username] = keybase1.UID(uid.String())
   168  }
   169  
   170  func TestSystemMessageMentions(t *testing.T) {
   171  	tc := externalstest.SetupTest(t, "chat-utils", 0)
   172  	defer tc.Cleanup()
   173  
   174  	g := globals.NewContext(tc.G, &globals.ChatContext{InboxSource: testInboxSource{}})
   175  	// test all the system message types gives us the right mentions
   176  	u1 := gregor1.UID([]byte{4, 5, 6})
   177  	u2 := gregor1.UID([]byte{4, 5, 7})
   178  	u3 := gregor1.UID([]byte{4, 5, 8})
   179  	u1name := "mike"
   180  	u2name := "lisa"
   181  	u3name := "sara"
   182  	usource := newTestUIDSource()
   183  	usource.AddUser(u1name, u1)
   184  	usource.AddUser(u2name, u2)
   185  	usource.AddUser(u3name, u3)
   186  	tc.G.SetUPAKLoader(usource)
   187  	body := chat1.NewMessageSystemWithAddedtoteam(chat1.MessageSystemAddedToTeam{
   188  		Adder: u1name,
   189  		Addee: u2name,
   190  	})
   191  	atMentions, chanMention, _ := SystemMessageMentions(context.TODO(), g, u1, body)
   192  	require.Equal(t, 1, len(atMentions))
   193  	require.Equal(t, u2, atMentions[0])
   194  	require.Equal(t, chat1.ChannelMention_NONE, chanMention)
   195  	body = chat1.NewMessageSystemWithInviteaddedtoteam(chat1.MessageSystemInviteAddedToTeam{
   196  		Invitee: u3name,
   197  		Inviter: u1name,
   198  		Adder:   u2name,
   199  	})
   200  	atMentions, chanMention, _ = SystemMessageMentions(context.TODO(), g, u1, body)
   201  	require.Equal(t, 2, len(atMentions))
   202  	require.Equal(t, u1, atMentions[0])
   203  	require.Equal(t, u3, atMentions[1])
   204  	require.Equal(t, chat1.ChannelMention_NONE, chanMention)
   205  	body = chat1.NewMessageSystemWithComplexteam(chat1.MessageSystemComplexTeam{
   206  		Team: "MIKE",
   207  	})
   208  	atMentions, chanMention, _ = SystemMessageMentions(context.TODO(), g, u1, body)
   209  	require.Zero(t, len(atMentions))
   210  	require.Equal(t, chat1.ChannelMention_ALL, chanMention)
   211  
   212  	body = chat1.NewMessageSystemWithNewchannel(chat1.MessageSystemNewChannel{})
   213  	atMentions, chanMention, channelNameMentions := SystemMessageMentions(context.TODO(), g, u1, body)
   214  	require.Zero(t, len(atMentions))
   215  	require.Equal(t, chat1.ChannelMention_NONE, chanMention)
   216  	require.Equal(t, 1, len(channelNameMentions))
   217  	require.Equal(t, "mike", channelNameMentions[0].TopicName)
   218  }
   219  
   220  func TestFormatVideoDuration(t *testing.T) {
   221  	testCase := func(ms int, expected string) {
   222  		require.Equal(t, expected, formatVideoDuration(ms))
   223  	}
   224  	testCase(1000, "0:01")
   225  	testCase(10000, "0:10")
   226  	testCase(60000, "1:00")
   227  	testCase(60001, "1:00")
   228  	testCase(72000, "1:12")
   229  	testCase(3600000, "1:00:00")
   230  	testCase(4500000, "1:15:00")
   231  	testCase(4536000, "1:15:36")
   232  	testCase(3906000, "1:05:06")
   233  }
   234  
   235  func TestGetQueryRe(t *testing.T) {
   236  	queries := []string{
   237  		"foo",
   238  		"foo bar",
   239  		"foo bar, baz? :+1:",
   240  	}
   241  	expectedRe := []string{
   242  		"foo",
   243  		"foo bar",
   244  		"foo bar, baz\\? :\\+1:",
   245  	}
   246  	for i, query := range queries {
   247  		re, err := GetQueryRe(query)
   248  		require.NoError(t, err)
   249  		expected := regexp.MustCompile("(?i)" + expectedRe[i])
   250  		require.Equal(t, expected, re)
   251  		t.Logf("query: %v, expectedRe: %v, re: %v", query, expectedRe, re)
   252  		ok := re.MatchString(query)
   253  		require.True(t, ok)
   254  	}
   255  }
   256  
   257  type decorateMentionTest struct {
   258  	body                string
   259  	atMentions          []string
   260  	chanMention         chat1.ChannelMention
   261  	channelNameMentions []chat1.ChannelNameMention
   262  	result              string
   263  }
   264  
   265  func TestDecorateMentions(t *testing.T) {
   266  	convID := chat1.ConversationID([]byte{1, 2, 3, 4})
   267  	cases := []decorateMentionTest{
   268  		{
   269  			body:       "@mikem fix something",
   270  			atMentions: []string{"mikem"},
   271  			// {"typ":1,"atmention":"mikem"}
   272  			result: "$>kb$eyJ0eXAiOjEsImF0bWVudGlvbiI6Im1pa2VtIn0=$<kb$ fix something",
   273  		},
   274  		{
   275  			body:        "@Mikem,@Max @mikem/@max please check out #general, also @here you should too",
   276  			atMentions:  []string{"mikem", "max"},
   277  			chanMention: chat1.ChannelMention_HERE,
   278  			channelNameMentions: []chat1.ChannelNameMention{{
   279  				ConvID:    convID,
   280  				TopicName: "general",
   281  			}},
   282  			// {"typ":1,"atmention":"Mikem"}
   283  			// {"typ":1,"atmention":"Max"}
   284  			// {"typ":1,"atmention":"mikem"}
   285  			// {"typ":1,"atmention":"max"}
   286  			// {"typ":2,"channelnamemention":{"name":"general","convID":"01020304"}}
   287  			// {"typ":1,"atmention":"here"}
   288  			result: "$>kb$eyJ0eXAiOjEsImF0bWVudGlvbiI6Ik1pa2VtIn0=$<kb$,$>kb$eyJ0eXAiOjEsImF0bWVudGlvbiI6Ik1heCJ9$<kb$ $>kb$eyJ0eXAiOjEsImF0bWVudGlvbiI6Im1pa2VtIn0=$<kb$/$>kb$eyJ0eXAiOjEsImF0bWVudGlvbiI6Im1heCJ9$<kb$ please check out $>kb$eyJ0eXAiOjIsImNoYW5uZWxuYW1lbWVudGlvbiI6eyJuYW1lIjoiZ2VuZXJhbCIsImNvbnZJRCI6IjAxMDIwMzA0In19$<kb$, also $>kb$eyJ0eXAiOjEsImF0bWVudGlvbiI6ImhlcmUifQ==$<kb$ you should too",
   289  		},
   290  		{
   291  			body:       "@mikem talk to @patrick",
   292  			atMentions: []string{"mikem"},
   293  			result:     "$>kb$eyJ0eXAiOjEsImF0bWVudGlvbiI6Im1pa2VtIn0=$<kb$ talk to @patrick",
   294  		},
   295  		{
   296  			body:   "see #general",
   297  			result: "see #general",
   298  		},
   299  		{
   300  			body:   "@here what are you doing!",
   301  			result: "@here what are you doing!",
   302  		},
   303  		{
   304  			body:        `\@mikem,\@max \@mikem/\@max please check out \#general, also \@here you should too`,
   305  			atMentions:  []string{"mikem", "max"},
   306  			chanMention: chat1.ChannelMention_HERE,
   307  			channelNameMentions: []chat1.ChannelNameMention{{
   308  				ConvID:    convID,
   309  				TopicName: "general",
   310  			}},
   311  			result: `\@mikem,\@max \@mikem/\@max please check out \#general, also \@here you should too`,
   312  		},
   313  	}
   314  	for _, c := range cases {
   315  		res := DecorateWithMentions(context.TODO(), c.body, c.atMentions, nil, c.chanMention,
   316  			c.channelNameMentions)
   317  		require.Equal(t, c.result, res)
   318  	}
   319  }
   320  
   321  func BenchmarkDecorateLinks(b *testing.B) {
   322  	var messages = []string{
   323  		"The buttons have been \"encrypted\" and the plaintext is still there.",
   324  		":joy: ",
   325  		"it looked like \"CASINO\" to me ",
   326  		"I like it, and I think that if you look at it closely you can see where it has \"CRYPTO\" almost hidden in the _charaters_. Same thing for the Try it button in the what's new.",
   327  		"Actually I like it!",
   328  		"Maybe a checkbox in settings to keep it if you want to?",
   329  		"As in, once you click on the tab, the text stays normal",
   330  		"I do think it's cool. but it should be a one-time thing",
   331  		"then I realized it was a clever thing and embraced its quirkiness :D",
   332  		"I thought it was a corrupted system font or something :P ",
   333  		"I didn't see that label in the announce blog entry on the feature. If it's not there, I'd surmise it to be a bug.",
   334  		"I first thought it was in hebrew or something",
   335  		"I hope they'll remove the weird label, it's murdering my OCD",
   336  		"Oh really?! Oh.. that’s a shame... :( ",
   337  		"The program ended in December and was killed off by the influx of spammers and thieves. No January airdrop. There's a team for it #stellar where they will tell you the same (and worse) heh @rottentweetie Also search for a team called airdrop for more info and misc spammers galore.",
   338  		"there is none in january. it ended. would be simple to google @rottentweetie ",
   339  		"Hi there! How are ya’ll?? Question: I didn’t recieve the airdrop of lumens of januari. Did you guys do?! Or the same as I? ",
   340  		"I am currently trying the \"linux\" path so far it has bot crashed",
   341  		"Lovelly getting error saying that path from windows is not a KBFS path",
   342  		"you should be able to find out the exact path from your windows explorer portion though",
   343  		"i am not sure about windows. for linux/mac which i use it's /keybase/private/smms in my case. ",
   344  		"go to https://keybase.io/docs/kbfs/understanding_kbfs ; under `Time travel` portion, it explains in details about how you gonna restore",
   345  		"or something else? I am on windows",
   346  		"would my path be K:/public/idah6?",
   347  		":sweat_smile: ugh no...",
   348  		"The hero has arrived. :tada:",
   349  		"@idah6 go to console and `keybase fs stat`, it has the commands you need",
   350  		"thanks for the help though",
   351  		"you can restore, `keybase fs stat --show-archived <your_kbfs_path> `to check on the revision",
   352  		"@chindraba yeah this was not something I had planned for, I kept checking before performing any operations, but whatever, this happens every now and then",
   353  		"I don't know for sure, and don't know how to do it, but I think you have some hope. Be patient as this is sort of the dead time for here.",
   354  		"Something about the merkle tree. ",
   355  		"I think the files are stored in a manner that prior versions can be seen/copied/recoverd. Similar to git.",
   356  		"@idah6 I think there is. I don't know it, but I don know one thing: STOP. Do nothing further until you have the answer. To continue could ruin the chance of recovery.",
   357  		"I need help, I accidently was deleting files off of Keybase, and was asked to permanently deleting them (thinking it was the local copy in another location on the computer) I stopped the operation but is there any way of restoring those files?",
   358  		"Type an @ and the first few letters, click the one you want and then copy that.",
   359  		"Never had it self-activate though. And I very seldom close the main window anyway. It's set to show on all desktops, so I don't have to even look at the tray icon.",
   360  		"@nevezen I think, now that you describe it, that I've seen that. When I click on the KB icon in the tray, it show \"Show Keybase\" which opens that window. I've always just clicked on one of the chats, or the chat icon on top, as that's where I'm going then anyway.",
   361  		"why it need to copy a user's name btw?",
   362  		"MacOS, btw.",
   363  		"Hi. I love keybase, but I just had a really frustrating moment trying to copy a user's name. Ultimately I couldn't figure out a way to do it heh.",
   364  		"Ah ok, 👌🏼",
   365  		"\"Fast\"er than logging out user one and logging in with user two.",
   366  		"Windows feature which allows multiple users to be \"logged in\" at the same time, even though only one is able to use the system at once.",
   367  		"“Switch user” from the start menu",
   368  		"What do u mean by fast uset switching ?",
   369  		"More like a quick access contextual menu launched from the notifications bar icon.",
   370  		"Has icons for people, chat, files, teams, hamburger and a window list of recent chats and files.",
   371  		"Right now I’m just testing on a win 10 desktop with fast user switching. Tomorrow I’ll try on a true multi user server. ",
   372  		"Does the first user, on K:, loose access to their files on K: when the second user has X: open?",
   373  		"OK testing this... gone back and forth mis-interpreting results but... looks like first user to connect gets K:, second gets X:",
   374  		"Have not seen that happen, yet. Is it a new window for each conversation, or just a new window which handles all conversations?",
   375  		"The same with the try it! button on keybase fm. Looks intentional",
   376  		"ugh, keybase now shows a dedicated mini-window for messages?",
   377  		":)",
   378  		"Mouse over",
   379  		"I think intentional",
   380  		"Only one Windows user at a time?",
   381  		"Gak! Is this still true?",
   382  		"meanwhile, my kbfs is stuck",
   383  		"I had to join, and then turn off notifications ",
   384  		"Yeah....",
   385  		"It is",
   386  		"Is that the spacedrop replay?",
   387  		"why is keybase sending me notifications for a channel *I'm not even in* ugh",
   388  		"good point. I was assuming this would be the case.",
   389  		"So there will be some system storage usage ",
   390  		"Don’t forget though that kbfs requires local scratch space to encrypt blocks before they’re sent to kbfs",
   391  		"No worries",
   392  		"You're welcome.",
   393  		"NP, didn't do anything anyway.",
   394  		"Thank you @smms, @chindraba, @stefan_claas for the input. ",
   395  		"@alphydan maybe worth to take a look at GiHub and ask there. https://github.com/keybase/client/issues",
   396  		"yeah, files stored on kbfs won't use your local storage. it's under kbfs storage quota. you could check by `keybase fs quota` ; simplest way is to think of it as a network mounted disk",
   397  		"so if I `cp some-file /keybase/team/some-team/some-file` it doesn't occupy any space in the local machine?",
   398  		"You could just save the files at your keybase/private or public folder path. no need special command. As long as your kbfs is mounted, it's a non issue saving there ",
   399  		"(a use case is that I only have 2GB of space left on the server, but would like to store 4GB on kbfs, and whether it can be done programmatically) ",
   400  		"my question is whether it is possible to save a file to the kbfs (in a server, using shell commands or the API) without using local storage ",
   401  		"As I understand it, here. Answers might be ymmv styles though ",
   402  		"Family voice mail is rare. Email most likey, text possibly. Most times when it's a missed call no messages are left.",
   403  		"Where is the best place to ask some technical questions about the keybase api? ",
   404  		"It's kindof annoying but also cute. I listen to them once in a while when i miss her",
   405  		"I got too many from my grandmother saying something like: > Hey it's me [name]. I just wanted to hear how everything is going. you don't have to call back it's always the same. tried to say that it's nice and all, but if she only leaves a message when it is something out of the ordinary then i'll listen to them at that point. but nah",
   406  		"I'm also not dissing people using it as a communication-of-choice, if it is consentual. I'm just, myself, trying to be less wired in because it fucks with your brain",
   407  		"I don't even listen to phone voice mail except for family and doctors ",
   408  		"By phone, voip,  even voice chat like discord. But, not messages or passing thoughts and notes.",
   409  		"When I use voice it's for a conversation, not messages in a bottle.",
   410  		"I'm all for new tech and shit. and evolution of society and shit. And obviously phones have done a lot of good. But is your life really that much of an action movie that you can't either pull into the side of the road or wait ? I mean we used to have to go home to use the phone. again.. not a \"new-tech-bad\". just.. people could chill",
   411  		"Voice msg or not.. is personal preference, no real justification to remove it.  it's similar to request ability to send exloding message in group removed, and exploding msg should be available in 1/1 convo only. ",
   412  		"😹 Yeah. I don't even *listen* to or accept audio messages or voice calls from anyone I don't care about.",
   413  		"https://keybase.io/docs/teams/design",
   414  		"subteams and their parent teams and other subteams under the same parent are all separate and can not see files or messages from one another. however, they'll all share the quota of the parent team (100gb total across all sub teams), and an admin/owner of a team can add themself to any subteam of the team they're admin/owner of",
   415  		"😆 ",
   416  		"It annoys me when my wife does it, but she gets a pass because I’m married to her. You guys, on the other hand....",
   417  		`Right, and that’s exactly it. It places the burden on the listener. And this might be crazy, but hear me out:
   418  		 - I get that you’re busy or on the go, but that doesn’t mean I’m not
   419  		 - I get that sometimes you can’t type, like when you’re driving. Maybe you should wait? I’m probably not in a position that I can listen all the time.
   420  
   421  		I feel like it proxies the burden, and (unfortunately and unintentionally) makes an implicit statement that their time is more important than your time right now. `,
   422  		"If you're in group with peeps on the go it's easier for them to just record, but cumbersome to listen.",
   423  		"I get that some people like them, but I’d prefer if my client deleted them and auto responded “ain’t got time for that”",
   424  		"Making voice messages go away would be an equally acceptable solution 😆 ",
   425  		"Super bowl ",
   426  		"!en 슈퍼 볼 ",
   427  		"Superbowl ",
   428  		"!tl Superbowl",
   429  		"슈퍼 볼 ",
   430  		"!ko Superbowl",
   431  		"Hi, new here",
   432  		"오늘은 일요일입니다 ",
   433  		"!ko today is sunday",
   434  		"이것은 어느 봇입니까? ",
   435  		"!ko Which bot is this?",
   436  		"a bot that converts voice messages into text would also be very helpful. Voicy does it for Telegram.",
   437  		"I made the webooks work, wee",
   438  		"How does access to files belonging to teams work? Do members of subteams automatically have access to files of parents? Or do members of teams have access to files of subteams? Or none of the two?",
   439  		"would be nice if they made this page work with saltpack https://keybase.io/verify",
   440  		"thanks for finally implementing the direct messages \"blocks\" and moderation",
   441  		"Has anyone has any luck with incoming webhooks, and has some examples? I havent been able to find any doc for it",
   442  		":wave:",
   443  		"Hey all :)",
   444  		"Welcome",
   445  		"Thanks!",
   446  		"@greenarmor Here's an impressive list. https://awesomeopensource.com/projects/text-to-speech",
   447  		"Might not be as good as Google, but run locally, much more private.",
   448  		"Probably even OpenSource.",
   449  		"None I know, but I suspect there are some TTS programs available.",
   450  		"any alternative you can think of other than using Gtts?",
   451  		"not wise but sound cool",
   452  		":wave:",
   453  		"@greenarmor is that wise? The transcript going from an E2E secure channel into Google for TTS?",
   454  		"using google tts",
   455  		"im working on a bot recording minutes of a meeting inside a team then after the meeting, bot can send back the minutes as audio file",
   456  		"Learning curve for everyone. Huge teams, like this one, are bound to have more than a few complications.",
   457  		"actaully theres 2 triggers same with @sholebot the !price. i already changed",
   458  		"i cant changed mine the korean team used to it",
   459  		"Yep, imagine single word triggers 10+ bots info. ",
   460  		"Cute, for a puppy.",
   461  		"!eyebleach",
   462  		"Lesson to be had if you make your own bot :D",
   463  		"Ah ok",
   464  		"the help command was responded to by both bots",
   465  		"Some keyword triggered it?",
   466  		"How did this ssh0le bot suddenly appear?",
   467  		"!urban whelp",
   468  		"!cat",
   469  		"!help",
   470  		"Nada.",
   471  		"뭐야 ",
   472  		"All the work, all the blame, and none of the credit.",
   473  		"!ko what's up",
   474  		"No wonder they want to revolt.",
   475  		"lol",
   476  		"Sure.. blame the bot.",
   477  		"his fault :P",
   478  		"Syntactically probably more correct, however.",
   479  		"its google translate",
   480  		"That time it droped one of the commas.",
   481  		"좋은 아침, 축복받은 날 러시아 친구. ",
   482  		"!ko Good morning and blessed day my Russian friend.",
   483  		"Something's lost in the translation.",
   484  		"Good morning, blessed day, Russian friend. ",
   485  		"!en 좋은 아침, 축복받은 날, 러시아 친구.",
   486  		"im not sure",
   487  		"Does Korean lack conjuctions?",
   488  		"좋은 아침, 축복받은 날, 러시아 친구. ",
   489  		"!ko Доброе утро и благословенный день, мой русский друг.",
   490  		"어떻게 지내? ",
   491  		"!ko how are you?",
   492  		"!kor Доброе утро и благословенный день, мой русский друг.",
   493  		"Use en as the input for korean and tagalog?",
   494  		"all ready in",
   495  		"all major languages to english",
   496  		"So, the bot reads lots of stuff. only speaks the three?",
   497  		"Philippines",
   498  		"I don't even know what, or where from, tagalog is.",
   499  		"Good morning and blessed day, my Russian friend. ",
   500  		"!en Buenos días y bendito día, mi amigo ruso.",
   501  		"Buenos días y bendito día, mi amigo ruso.",
   502  		"i just add korean ang tagalog",
   503  		"Why? didn't teach the bot Spanish?",
   504  		"wont work",
   505  		"!es Доброе утро и благословенный день, мой русский друг.",
   506  		"ahaha",
   507  		"Make a bot do all your work for you.",
   508  		"cheating ?",
   509  		"Nice trick, even if it is _cheating_.",
   510  		"Good morning and blessed day, my Russian friend. ",
   511  		"!en Доброе утро и благословенный день, мой русский друг.",
   512  		"Доброе утро и благословенный день, мой русский друг.",
   513  		"Good morning from fairy cold Russia ",
   514  		":wave:",
   515  		":wave:",
   516  		"Mmm",
   517  		"I think it worked on block quotes too.",
   518  		"Something like \"see full text\".",
   519  		"I've seen someplace where code blocks were limited to a few lines, and reading more required a click, and it reset if you left the room and came back. Not sure where, so I can't demo it. )-:",
   520  		"the real feature request is always in the comments",
   521  		"Or, if the other recepiant has copied it, perhaps the sender could delete it. ",
   522  		"Or maybe allow us to collapse code blocks",
   523  		"Wow feature request: hide message",
   524  		"cool thanks @dxb ",
   525  		"Dang, that's a boat-load of text for a short message.",
   526  		"pm @b07 with `!help`, check out #discovery-channel, and check out keybase.io/popular-teams",
   527  		"Is there anyway to search for big chat rooms on keybase?",
   528  		"Well, that's a possible problem",
   529  		"No cli of their own",
   530  		"My mobiles only ssh to the desktop.",
   531  		"maybe make sure there's actually something on your clipboard? :p",
   532  		"Can't do that here. Only my desktop is usable for CLI",
   533  		"i tested it with `termux-clipboard-get` and it worked though",
   534  		"Perhaps the termux-clipboard-get isn't doing it correctly. Or something on the choosen options aren't set right.",
   535  		"So it seems, Works for me with various options and no --message/-m flag",
   536  		"you're right though, that's standard, and i'm sure it works that way too",
   537  		"you don't need `-m` at all when piping to it",
   538  		"The `-m` needs the clear text on the command line. To make  pipe work you need to use `-` as a the \"message\". `termux-clipboard-get | keybase encrypt -m - <user>`",
   539  		"also, since your `&` was outside of the code tag on your message i assume that wasn't part of it right?",
   540  		"try something simple first. `termux-clipboard-get | keybase encrypt <user>` (don't use -m when piping to stdin)",
   541  		"works fine for me… are you encrypting for a team or some ridiculously large group?",
   542  		"I tried with that param and without and it wouldn't read my piped input",
   543  		"are you using the `-m` option?",
   544  		" I'm curious; I'm trying to use `keybase encrypt` from the cli (as the feature isn't on mobile *yet*) and I'm unable to get keybase to read my `stdin` input. I'm piping the input in like so `termux-clipboard-get | keybase encrypt <params> <target> <flags>` & this isn't working",
   545  		"ttfn",
   546  		"Uncle did, August Dvorak",
   547  		"In college I was proficient equally with that and QWERTY.",
   548  		"Loved his keyboard, but don't use it anymore.",
   549  		"Never hear a single one, nor read anyting from him.",
   550  		"Dvorak",
   551  		"Which he never claimed anyway.",
   552  		"I was \"online\" years before Gore \"invented\" the Internet",
   553  		"10 characters per second print speed. No \"screen\" just paper.",
   554  		"Teletype Model 32 ASR connected to 110 baud dial-up.",
   555  		"Don't recall what my first computer was, but I remember it being upgraded to an UNIVAC 1110",
   556  		"Not my first computer. My first IGM-family computer.",
   557  		"First build was PC-XT clone, 20 MB HDD, 640K RAM, dual 5.25 180K floppy.",
   558  		"Until the current box I've always built my own box.",
   559  		"It's probably a bit dated, but a good idea of how to work the whole thing is spelled out in one place. https://nodakengineering.com/?page_id=501",
   560  		"The virtual box, usable linux, and linux from scratch are all free. ",
   561  		"@damccull If you want to play with linux, and learn what's really going on under the hood with everything. Try the Linux From Scratch project. Of course it requires a running linux to build it, but that can be in a virtual box.",
   562  		"Had full-suite integration working before MSO even tried to do it.",
   563  		"I even got to like the extras they had, Director I think it was called.",
   564  		"Up to WP 12 on XP",
   565  		"I had WP on Win and never had an issue with it.",
   566  		"WP had MSO beat 7 ways to sunday. Just didn't have the draconian marketing to keep it going",
   567  		"I've adapted to gui, reasonably well anywya",
   568  		"More of a cli-fan that finds gui handy, as long as it don't bury things too deep",
   569  		"Haven't liked MS Office since they added the ribbon. 2003 or 2007.",
   570  		"I've used win 8/8.1/10 enough to know I'm never going back.",
   571  		"I've always had, and still have, a seldom-used boot into Win. Mostly to help others.",
   572  		"Haven't really \"used\" win since vista came out and made my pc look like a new mac",
   573  		"Haven't used Mac since pre-osx days, and then very little.",
   574  		"That plus MS Office is just so....above all the competitors. If MS would port office365 to linux, and I could get a legit gaming capability that didn't require any tweaking and was completely transparent and could play all mah games, I'd switch today. Those are the only things holding me back.",
   575  		"Do you spend significant time doing other things, in blocks of time rather than piece-meal?",
   576  		"I keep wanting to switch to linux but I can't seem to break away from windows as a gaming platform, and that's basically what I do all the time so... :D",
   577  		"I'm not. Linux now. Then I had a few installs under multi-boot. 3rd physical partition on 1st of 3 hdd installed.",
   578  		"Not that it should be against the rules, but why are you using g:/ as the system disk?",
   579  		"didn't worg so well when i was using `g:/` as the system disk.",
   580  		"the official update did something relative to inventoring software in `c:/program files`",
   581  		"Rebuilding the FAT table with debug was an experience not to be forgotten, nor repeated.",
   582  		"It's hard to screw up windows unless you have a hobby of trying out warez",
   583  		"Well, back in the old days of MSDOS I did accidentally format the wrong drive. My fault for not double checking the assignments after adding a new drive.",
   584  		"Surprisingly enough the only time I've had anything happen to my system was back in my Windows days and M$ had a bad-acting update.",
   585  		"The goal is to protect you from the software you run. Since that presumably includes a web browser, which is running JavaScript from who-knows-where, some of which may be trying to leverage runtime vulnerabilities, it's a prudent idea.",
   586  		"Create `/etc/synthetic.conf` with `/keybase` in it.",
   587  		"It's possible, just has to be configured before boot time.",
   588  		"That's my only beef, so far anyway, with KDE",
   589  		"that makes sense, thanks. bit of a bummer but all in the name of security innit",
   590  		"I hate it when \"my\" system tries to protect me from _myself_.",
   591  		"things aren't allowed to make folders in `/` anymore",
   592  		"https://github.com/keybase/client/issues/17835",
   593  		`> Keybase 4.4.0 (out today) has better file system support for Catalina now. Please let us know if you have more problems with it. Note that the /keybase mount point is no longer viable on fresh installs of Catalina due to macOS namespace restrictions, so now we mount at /Volumes/Keybase if /keybase does not already exist.
   594  
   595  		`,
   596  		"system integrity made it impossible",
   597  		"can still run fs commands thru `/keybase` via `keybase` cli tho",
   598  		"Linux here. Still works.",
   599  		"did KBFS change in an update? I can no longer cd into `/keybase` like I used to but I can access through `/Volumes/Keybase (current_user)/` (this on macOS Catalina)",
   600  		"Called `Kebase Key ID`",
   601  		"`keybase pgp list` command gives the ID",
   602  		"Ok I see it.",
   603  		"Ah...I see. Thanks for pointing that out. I thought it was inconvenient to have to log into the website ;D",
   604  		"then use \"keybase Key ID\"",
   605  		"yeah you have to do `keybase pgp list`",
   606  		"the ID must be keybase's internal ID for it",
   607  		"ah, signing into the website gives a big long string when i click 'edit' next to the key",
   608  		"\"Error parsing command line arguments: bad key: KID wrong length; wanted 35 but got 8 bytes\"",
   609  		"Nope, same error. How strange.",
   610  		"try using `f94da63df218aa31` as given on the profile",
   611  		"grr. `keybase pgp drop` keeps telling me it wants 35 bytes but only got <number less than 35> bytes. Obviously I'm using the wrong thing. What hsould I be using?",
   612  		"fair enough",
   613  		"Not sure where I got that one from.",
   614  		"Yep. I see two keys next to my picture but neither has that code you sent earlier.",
   615  		"go to your main profile page",
   616  		"how do you see the key id you were sending me earlier? I don't see that number anywhere.",
   617  		"Same thing, except how you get it into your profile.",
   618  		"So using a custom key or generating one is the same thing?",
   619  		"Oh.",
   620  		"Actually, they key isn't signed by Keybase. It's entry into the merkle tree is signed by your KB identity.",
   621  		"If i had a preexisting cert, could I have keybase sign that somehow? I see that it signs your cert when you create one through them",
   622  		"They use a \"gossip\" network. What one server knows, eventually they all know.",
   623  		"Nice.",
   624  		"One is all.",
   625  		"Hmm. Are the key servers linked to share data, or is there a popular one I should use?",
   626  		"Have to upload it to a regular keyserver.",
   627  		"That's the part keybase doesn't work with.",
   628  		"That way there system doesn't accidentally try to use it later",
   629  		"So you upload the key back to keybase with the revoked part in it?",
   630  		"Later, if you use the key with other people, it's nice to let them know it's revoked, and send them the revoked key to update their keyring.",
   631  		"3) let others know it's revoked by sending the revoked key to the keyserver",
   632  		"2) Actually \"revoke\" the key  by importing the revocation cert into the key",
   633  		"1) create a revocation cert. Should have one handy for any key you make anyway.",
   634  		"hmm. interesting. how do you send revocations out?",
   635  		"Keybase, however, doesn't have a tool for revoking PGP certs. only removing them from the Keybase profile.",
   636  		"It was listed on  your profile, so anyone could have goten it from there. Made easy on purpose.",
   637  		"i only see F94DA63DF218AA31 and 43E0A440A5971D1B",
   638  		"I've never posted any keys to a public server on my own. Unless keybase did it outside their own server, that's the only place that should exist.",
   639  		"Now I see `f218aa31` not sure where I got that other one from",
   640  		"While expirementing, try to keep copies of everything, in case you need to \"clear the record\" later.",
   641  		"I'm enjoying it. I don't see af971d1b on my profile...there are two keys on the page though",
   642  		"It's lots of fun, imho",
   643  		"I'm newish to pgp.",
   644  		"Oh yeah I added that one as a test just now lol.",
   645  		"Nope, now I see another one `af971d1b`",
   646  		"Oh, interesting. Ok, it's saying no secret key available, so perhaps I have lost that in the past.",
   647  		"That's the id from your profile.",
   648  		"The revoked key can be sent to one of the pgp key servers, and anyone trying touse it, if they got it from somewhere, will know it's revoked.",
   649  		"No f21...found...should I replace with my own?",
   650  		"That generates a revocation cert, and then imports it back into the keychain, merging them, and making the key now revoked.",
   651  		"Then issue a revocation certificate for it, with `gpg --gen-revoke f218aa31 | gpg --import`",
   652  		"Ok I did that. Now I see it in my local keychain. Doesn't look like something I recognize, though it has my name. I must have generated it as a test when keybase released back in the day.",
   653  		"If not, add it with `keybase pgp export --unencrypted | gpg --import`",
   654  		"It may, or may not, be in your computer's GPG keychain.",
   655  		"Well, purging it would not cause any issues. However, I'd recomend a few other steps first.",
   656  		"I don't know what it's for anymore, I'm getting ready to create a new one that I will know what it's for, and I don't really want it anymore. I haven't ever used it that I'm aware of.",
   657  		"Why do you need to remove the key?",
   658  		"I have a pgp key in my `keybase pgp list` and I'm not sure if I generated it in the past or if it was created automatically. Can I safely `keybase pgp purge` without screwing up my account?",
   659  		"Achetez-en dans un échange. Ensuite, essayez d'utiliser l'anglais dans les zones où tout le monde l'utilise. Probabilité accrue de compréhension.",
   660  		`Bonjour,
   661  		Y-a-t-il des gens parlant français ici ? Besoin d'infos sur la façon la plus simple d'acquérir des Lumens.
   662  		Bonne soirée.
   663  		`,
   664  		"the exchange account she mentioned seems unknowned but funded by interstellar, i dont think interstellar need a memo",
   665  		"Np!! ",
   666  		"Great! Thank you so much for this help! Much appreciated.",
   667  		"You have to contact your exchange, usually they will able to resolve it for you with few additional steps fr them ",
   668  		"What happens with the XLM if I indeed forgot to enter the memo field?",
   669  		"I guess you forgot to key in memo field when you sent ? ",
   670  		"this friend of mine also tried to send her XLM to the same exchange and she found the same problem.....",
   671  		"Exchange usually requires you to enter memo field whereas personal wallet you don't have to. ",
   672  		"yes this is the one",
   673  		"yes",
   674  		"Lumen = XLM and yes it's 918 xlm and I can see that they were sent from my wallet in Keybase but they never arrived in my wallet at the exchange. Does that complicate things that the receiving wallet is one of an exchange?",
   675  		"@inep ",
   676  		"Hmmmm.... what is the lumen? I am not sure that I know what that is...",
   677  		"Is that 918xlm that you recently transferred. Looks like it went through .. ? Was it your another wallet or exchange ?",
   678  		"I am nieuw to Keybase and Crypto and am verry excited. I was introdeuced to Keybase via a friend who gave me the possibility of joining in time for the Airdrop. Being new to Keybase I wanted to transfer my Lumen to another wallet. The address is correct but I have not received the Lumen in the wallet that I sent it to. Can anyone explain to me what I did wrong and if I can correct it?",
   679  		"Hello Everyone,",
   680  		"Very nice, thank you @chindraba ",
   681  		":tada:",
   682  		"For \"denyability\" you can create a shadow account, encrypt to that account, removing yourself, and save the file. Anyone with access to the creating account (including rubber-hose) will not be able to decrypt it. As long as they don't know the other account is you, it remains private.",
   683  		"Being able to remove \"self\" from the encryption list, once there is someone else listed, is a nice trick too.",
   684  		"That does not happen, however for the encrypt function. So if the same file is to be encrypted to multiple recipients individually, it has to be renamed manually between encryptions.",
   685  		"Amazing and very useful for me. I have many folders and files and this greatly simplifies my workflow",
   686  		"The decrypt/verify functions will not overwrite existing files. Adding a (1), (2),... to the file name (not the extension if any).",
   687  		"It seems to be well behaved, in Linux at least.",
   688  		"Yes, just tried it myself on Linux. Works the same as you report on Mac. Decryption works just as easily too.",
   689  		"If I drag the file, I get binary which would be unsafe to paste in a message anywhere.",
   690  		"If I enter the text directly I get text I can paste in a message somewhere.",
   691  		"One problem is that it does not allow the option to have it ASCII armored, or what ever the term is relative to saltpack.",
   692  		"its the same",
   693  		"shut up",
   694  		"lol",
   695  		"never used a file manager",
   696  		"don't know how to drag and drop a file in linux",
   697  		"I tried the new update on a Mac. The crypto tab has an interesting behavior: If you drag and drop a file to sign or encrypt, the output is generated and stored in the same location as the original file. This works on both local and remote (Google Drive File Stream) folders. I am amazed! Does this work the same also on Windows and Linux?",
   698  		":wave:",
   699  		"Hello ",
   700  		"Where ",
   701  		":wave:",
   702  		"Submit GitHub issue. Haa.. ",
   703  		"True but... It can't be used for it's purpose anymore.",
   704  		"i mean you can leave the channel lol",
   705  		"So will it ever be deleted since it's basically useless now?",
   706  		"Ah ok. Thought there was some issue after the update",
   707  		"people would just flip constantly",
   708  		"but as keybasefriends grew it ended up causing issues",
   709  		"it was",
   710  		"Huh, thought that's the purpose of the channel",
   711  		"it ends up bogging down every single client connected and the server for minutes",
   712  		"(i know it sounds silly)",
   713  		"flip was disabled in #flip ",
   714  		"You don't have to wait. Tell others about Keybase and about the security threats built into Whatsapp, and get everyone to use Keybase instead. No waiting and better security. What could be better?",
   715  		"Quick question for any engineering managers here... for React Native (iOS, Android, Desktop + Web App) products, do you guys recommend typescript? Seems like extra overhead",
   716  		"Hey everyone nice to meet you all of you guys",
   717  		":wave:",
   718  		"i mean they have @keybase but that's only for keybase employees lol :) ",
   719  		"I hope it would be on phones soon. Can't wait to send encrypted messages on Whatsapp :grin:",
   720  		"this is that place :) thanks for the kind words",
   721  		"does the dev team at keybase have a team or channel (I assume they don't want to be TOO public). Bravo to them on the past few updates.",
   722  		"Thanks",
   723  		"that's what I thought",
   724  		"a name is either a user or a team",
   725  		"ok",
   726  		"they can't overlap",
   727  		"yes",
   728  		"Do team names and user names share a namespace? In other words can a user have the same name as a team?",
   729  		"༼ つ ◕‿◕ ༽つ ",
   730  		"Keybase seems to only render the lower half of some of the weirder characters ",
   731  		"ATM desktop only",
   732  		"How does one install it?",
   733  		"In development (hopefully)",
   734  		"https://www.fsf.org/facebook",
   735  		"Where is crypt on my android app?",
   736  	}
   737  	b.ResetTimer()
   738  	for i := 0; i < b.N; i++ {
   739  		for _, msg := range messages {
   740  			DecorateWithLinks(context.TODO(), msg)
   741  		}
   742  	}
   743  }
   744  
   745  type decorateLinkTest struct {
   746  	body   string
   747  	result string
   748  }
   749  
   750  func TestDecorateLinks(t *testing.T) {
   751  	cases := []decorateLinkTest{
   752  		{
   753  			body:   "click www.google.com",
   754  			result: "click $>kb$eyJ0eXAiOjQsImxpbmsiOnsidXJsIjoid3d3Lmdvb2dsZS5jb20iLCJwdW55Y29kZSI6IiJ9fQ==$<kb$",
   755  		},
   756  		{
   757  			body:   "https://maps.google.com?q=Goddess%20and%20the%20Baker,%20Legacy%20Tower,%20S%20Wabash%20Ave,%20Chicago,%20IL%2060603&ftid=0x880e2ca4623987cb:0x8b9a49f6050a873a&hl=en-US&gl=us",
   758  			result: "$>kb$eyJ0eXAiOjQsImxpbmsiOnsidXJsIjoiaHR0cHM6Ly9tYXBzLmdvb2dsZS5jb20/cT1Hb2RkZXNzJTIwYW5kJTIwdGhlJTIwQmFrZXIsJTIwTGVnYWN5JTIwVG93ZXIsJTIwUyUyMFdhYmFzaCUyMEF2ZSwlMjBDaGljYWdvLCUyMElMJTIwNjA2MDNcdTAwMjZmdGlkPTB4ODgwZTJjYTQ2MjM5ODdjYjoweDhiOWE0OWY2MDUwYTg3M2FcdTAwMjZobD1lbi1VU1x1MDAyNmdsPXVzIiwicHVueWNvZGUiOiIifX0=$<kb$",
   759  		},
   760  		{
   761  			body:   "10.0.0.24",
   762  			result: "$>kb$eyJ0eXAiOjQsImxpbmsiOnsidXJsIjoiMTAuMC4wLjI0IiwicHVueWNvZGUiOiIifX0=$<kb$",
   763  		},
   764  		{
   765  			body:   "ws-0.localdomain",
   766  			result: "ws-0.localdomain",
   767  		},
   768  		{
   769  			body:   "https://companyname.sharepoint.com/:f:/s/site-collection-name/subsite-name/Ds10TaJKAKhMp1hE0B_42WcBVhTHD3EQJKWhGprKFP3vpQ?e=14ohmf",
   770  			result: "$>kb$eyJ0eXAiOjQsImxpbmsiOnsidXJsIjoiaHR0cHM6Ly9jb21wYW55bmFtZS5zaGFyZXBvaW50LmNvbS86Zjovcy9zaXRlLWNvbGxlY3Rpb24tbmFtZS9zdWJzaXRlLW5hbWUvRHMxMFRhSktBS2hNcDFoRTBCXzQyV2NCVmhUSEQzRVFKS1doR3ByS0ZQM3ZwUT9lPTE0b2htZiIsInB1bnljb2RlIjoiIn19$<kb$",
   771  		},
   772  		{
   773  			body:   "http://keybase.io/mikem;",
   774  			result: "$>kb$eyJ0eXAiOjQsImxpbmsiOnsidXJsIjoiaHR0cDovL2tleWJhc2UuaW8vbWlrZW0iLCJwdW55Y29kZSI6IiJ9fQ==$<kb$;",
   775  		},
   776  		{
   777  			body:   "keybase.io, hi",
   778  			result: "$>kb$eyJ0eXAiOjQsImxpbmsiOnsidXJsIjoia2V5YmFzZS5pbyIsInB1bnljb2RlIjoiIn19$<kb$, hi",
   779  		},
   780  		{
   781  			body:   "https://en.wikipedia.org/wiki/J/Z_(New_York_City_Subway_service)",
   782  			result: "$>kb$eyJ0eXAiOjQsImxpbmsiOnsidXJsIjoiaHR0cHM6Ly9lbi53aWtpcGVkaWEub3JnL3dpa2kvSi9aXyhOZXdfWW9ya19DaXR5X1N1YndheV9zZXJ2aWNlKSIsInB1bnljb2RlIjoiIn19$<kb$",
   783  		},
   784  		{
   785  			body:   "(keybase.io)",
   786  			result: "($>kb$eyJ0eXAiOjQsImxpbmsiOnsidXJsIjoia2V5YmFzZS5pbyIsInB1bnljb2RlIjoiIn19$<kb$)",
   787  		},
   788  		{
   789  			body:   "https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/unicode-range",
   790  			result: "$>kb$eyJ0eXAiOjQsImxpbmsiOnsidXJsIjoiaHR0cHM6Ly9kZXZlbG9wZXIubW96aWxsYS5vcmcvZW4tVVMvZG9jcy9XZWIvQ1NTL0Bmb250LWZhY2UvdW5pY29kZS1yYW5nZSIsInB1bnljb2RlIjoiIn19$<kb$",
   791  		},
   792  		{
   793  			body:   "\u202ehttps://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/unicode-range",
   794  			result: "\u202ehttps://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/unicode-range",
   795  		},
   796  		{
   797  			body:   "\u202e\u202dhttps://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/unicode-range",
   798  			result: "\u202e\u202d$>kb$eyJ0eXAiOjQsImxpbmsiOnsidXJsIjoiaHR0cHM6Ly9kZXZlbG9wZXIubW96aWxsYS5vcmcvZW4tVVMvZG9jcy9XZWIvQ1NTL0Bmb250LWZhY2UvdW5pY29kZS1yYW5nZSIsInB1bnljb2RlIjoiIn19$<kb$",
   799  		},
   800  		{
   801  			body:   "`www.google.com`",
   802  			result: "`www.google.com`",
   803  		},
   804  		{
   805  			body:   "```www.google.com```",
   806  			result: "```www.google.com```",
   807  		},
   808  		{
   809  			body:   "> www.google.com",
   810  			result: "> $>kb$eyJ0eXAiOjQsImxpbmsiOnsidXJsIjoid3d3Lmdvb2dsZS5jb20iLCJwdW55Y29kZSI6IiJ9fQ==$<kb$",
   811  		},
   812  		{
   813  			body:   "nytimes.json",
   814  			result: "nytimes.json",
   815  		},
   816  		{
   817  			body:   "mike.maxim@gmail.com",
   818  			result: "$>kb$eyJ0eXAiOjUsIm1haWx0byI6eyJ1cmwiOiJtaWtlLm1heGltQGdtYWlsLmNvbSIsInB1bnljb2RlIjoiIn19$<kb$",
   819  		},
   820  		{
   821  			body:   "mailto:mike.maxim@gmail.com",
   822  			result: "mailto:$>kb$eyJ0eXAiOjUsIm1haWx0byI6eyJ1cmwiOiJtaWtlLm1heGltQGdtYWlsLmNvbSIsInB1bnljb2RlIjoiIn19$<kb$",
   823  		},
   824  		{
   825  			body:   "mike.maxim@gmail.com/google.com",
   826  			result: "$>kb$eyJ0eXAiOjUsIm1haWx0byI6eyJ1cmwiOiJtaWtlLm1heGltQGdtYWlsLmNvbSIsInB1bnljb2RlIjoiIn19$<kb$/google.com",
   827  		},
   828  		{
   829  			body:   "https://medium.com/@wouterarkink/https-medium-com-wouterarkink-how-to-send-money-to-anyone-in-the-world-by-only-knowing-their-social-handle-3180e6cd4e58",
   830  			result: "$>kb$eyJ0eXAiOjQsImxpbmsiOnsidXJsIjoiaHR0cHM6Ly9tZWRpdW0uY29tL0B3b3V0ZXJhcmtpbmsvaHR0cHMtbWVkaXVtLWNvbS13b3V0ZXJhcmtpbmstaG93LXRvLXNlbmQtbW9uZXktdG8tYW55b25lLWluLXRoZS13b3JsZC1ieS1vbmx5LWtub3dpbmctdGhlaXItc29jaWFsLWhhbmRsZS0zMTgwZTZjZDRlNTgiLCJwdW55Y29kZSI6IiJ9fQ==$<kb$",
   831  		},
   832  		{
   833  			body:   "https://drive.google.com/open?id=1BKcMML-uqOFAK-D4btEBlcoyodfvE4gg&authuser=cecile@keyba.se&usp=drive_fs",
   834  			result: "$>kb$eyJ0eXAiOjQsImxpbmsiOnsidXJsIjoiaHR0cHM6Ly9kcml2ZS5nb29nbGUuY29tL29wZW4/aWQ9MUJLY01NTC11cU9GQUstRDRidEVCbGNveW9kZnZFNGdnXHUwMDI2YXV0aHVzZXI9Y2VjaWxlQGtleWJhLnNlXHUwMDI2dXNwPWRyaXZlX2ZzIiwicHVueWNvZGUiOiIifX0=$<kb$",
   835  		},
   836  		{
   837  			body:   "@google.com",
   838  			result: "@google.com",
   839  		},
   840  		{
   841  			body:   "/keybase/team/keybase.staff_v8/candidates/feedback-template.md",
   842  			result: "/keybase/team/keybase.staff_v8/candidates/feedback-template.md",
   843  		},
   844  		{
   845  			body:   "#google.com",
   846  			result: "#$>kb$eyJ0eXAiOjQsImxpbmsiOnsidXJsIjoiZ29vZ2xlLmNvbSIsInB1bnljb2RlIjoiIn19$<kb$",
   847  		},
   848  		{
   849  			body:   "client/go/profiling/aggregate_timers.py",
   850  			result: "client/go/profiling/aggregate_timers.py",
   851  		},
   852  		{
   853  			body:   "cnn.com/@mike/index.html",
   854  			result: "$>kb$eyJ0eXAiOjQsImxpbmsiOnsidXJsIjoiY25uLmNvbS9AbWlrZS9pbmRleC5odG1sIiwicHVueWNvZGUiOiIifX0=$<kb$",
   855  		},
   856  		{
   857  			body:   "google.com/mike?email=mike@gmail.com",
   858  			result: "$>kb$eyJ0eXAiOjQsImxpbmsiOnsidXJsIjoiZ29vZ2xlLmNvbS9taWtlP2VtYWlsPW1pa2VAZ21haWwuY29tIiwicHVueWNvZGUiOiIifX0=$<kb$",
   859  		},
   860  		{
   861  			body:   "@keybase.bots.build.macos",
   862  			result: "@keybase.bots.build.macos",
   863  		},
   864  		{
   865  			body:   "keybase://team-page/keybasefriends",
   866  			result: "$>kb$eyJ0eXAiOjQsImxpbmsiOnsidXJsIjoia2V5YmFzZTovL3RlYW0tcGFnZS9rZXliYXNlZnJpZW5kcyIsInB1bnljb2RlIjoiIn19$<kb$",
   867  		},
   868  		{
   869  			body:   "keybase://team-page/keybasefriends https://github.com",
   870  			result: "$>kb$eyJ0eXAiOjQsImxpbmsiOnsidXJsIjoia2V5YmFzZTovL3RlYW0tcGFnZS9rZXliYXNlZnJpZW5kcyIsInB1bnljb2RlIjoiIn19$<kb$ $>kb$eyJ0eXAiOjQsImxpbmsiOnsidXJsIjoiaHR0cHM6Ly9naXRodWIuY29tIiwicHVueWNvZGUiOiIifX0=$<kb$",
   871  		},
   872  	}
   873  	for _, c := range cases {
   874  		res := DecorateWithLinks(context.TODO(), c.body)
   875  		require.Equal(t, c.result, res, "incorrect encoding for body %s", c.body)
   876  	}
   877  }
   878  
   879  type configUsernamer struct {
   880  	libkb.ConfigReader
   881  	username libkb.NormalizedUsername
   882  }
   883  
   884  func (c configUsernamer) GetUsername() libkb.NormalizedUsername {
   885  	return c.username
   886  }
   887  
   888  func TestAddUserToTlfName(t *testing.T) {
   889  	tc := externalstest.SetupTest(t, "chat-utils", 0)
   890  	defer tc.Cleanup()
   891  
   892  	g := globals.NewContext(tc.G, &globals.ChatContext{})
   893  	g.Env.SetConfig(
   894  		&configUsernamer{g.Env.GetConfig(), "charlie"}, g.Env.GetConfigWriter())
   895  
   896  	priv := keybase1.TLFVisibility_PRIVATE
   897  	mem := chat1.ConversationMembersType_IMPTEAMNATIVE
   898  	s := AddUserToTLFName(g, "alice,bob", priv, mem)
   899  	require.Equal(t, "alice,bob,charlie", s)
   900  	s = AddUserToTLFName(g, "charlie", priv, mem)
   901  	require.Equal(t, "charlie,charlie", s)
   902  	s = AddUserToTLFName(
   903  		g, "alice,bob (conflicted copy 2019-02-14 #1)", priv, mem)
   904  	require.Equal(t, "alice,bob,charlie (conflicted copy 2019-02-14 #1)", s)
   905  	s = AddUserToTLFName(
   906  		g, "alice#bob", priv, mem)
   907  	require.Equal(t, "alice,charlie#bob", s)
   908  	s = AddUserToTLFName(
   909  		g, "alice#bob (conflicted copy 2019-02-14 #1)", priv, mem)
   910  	require.Equal(t, "alice,charlie#bob (conflicted copy 2019-02-14 #1)", s)
   911  
   912  	pub := keybase1.TLFVisibility_PUBLIC
   913  	s = AddUserToTLFName(g, "alice,bob", pub, mem)
   914  	require.Equal(t, "alice,bob", s)
   915  }
   916  
   917  func TestPresentConversationParticipantsLocal(t *testing.T) {
   918  	tofurkeyhq := "Tofurkey HQ"
   919  	tofurus := "Tofu-R-Us"
   920  	danny := "Danny"
   921  	rawParticipants := []chat1.ConversationLocalParticipant{
   922  		{
   923  			Username:    "[tofurkey@example.com]@email",
   924  			ContactName: &tofurkeyhq,
   925  		},
   926  		{
   927  			Username:    "18005558638@phone",
   928  			ContactName: &tofurus,
   929  		},
   930  		{
   931  			Username: "ayoubd",
   932  			Fullname: &danny,
   933  		},
   934  		{
   935  			Username: "example@twitter",
   936  		},
   937  	}
   938  	res := PresentConversationParticipantsLocal(context.TODO(), rawParticipants)
   939  
   940  	require.Equal(t, res[0].ContactName, &tofurkeyhq)
   941  	require.Equal(t, res[0].Type, chat1.UIParticipantType_EMAIL)
   942  
   943  	require.Equal(t, res[1].ContactName, &tofurus)
   944  	require.Equal(t, res[1].Type, chat1.UIParticipantType_PHONENO)
   945  
   946  	require.Equal(t, res[2].Assertion, "ayoubd")
   947  	require.Equal(t, res[2].FullName, &danny)
   948  	require.Equal(t, res[2].Type, chat1.UIParticipantType_USER)
   949  
   950  	require.Equal(t, res[3].Assertion, "example@twitter")
   951  	require.Equal(t, res[3].Type, chat1.UIParticipantType_USER)
   952  }
   953  
   954  type contactStoreMock struct {
   955  	assertionToName map[string]string
   956  }
   957  
   958  func (c *contactStoreMock) SaveProcessedContacts(libkb.MetaContext, []keybase1.ProcessedContact) error {
   959  	return errors.New("contactStoreMock not impl")
   960  }
   961  
   962  func (c *contactStoreMock) RetrieveContacts(libkb.MetaContext) ([]keybase1.ProcessedContact, error) {
   963  	return nil, errors.New("contactStoreMock not impl")
   964  }
   965  
   966  func (c *contactStoreMock) RetrieveAssertionToName(libkb.MetaContext) (map[string]string, error) {
   967  	return c.assertionToName, nil
   968  }
   969  
   970  func (c *contactStoreMock) UnresolveContactsWithComponent(mctx libkb.MetaContext,
   971  	phoneNumber *keybase1.PhoneNumber, email *keybase1.EmailAddress) {
   972  	panic("unexpected call to UnresolveContactsWithComponent in mock")
   973  }
   974  
   975  func TestAttachContactNames(t *testing.T) {
   976  	tc := externalstest.SetupTest(t, "chat-utils", 0)
   977  	defer tc.Cleanup()
   978  
   979  	assertionToName := map[string]string{
   980  		"[tofurkey@example.com]@email": "Tofu R-Key",
   981  		"18005558638@phone":            "Alice",
   982  	}
   983  
   984  	mock := &contactStoreMock{assertionToName}
   985  	tc.G.SyncedContactList = mock
   986  
   987  	rawParticipants := []chat1.ConversationLocalParticipant{
   988  		{
   989  			Username: "[tofurkey@example.com]@email",
   990  		},
   991  		{
   992  			Username: "18005558638@phone",
   993  		},
   994  		{
   995  			Username: "ayoubd",
   996  		},
   997  		{
   998  			Username: "example@twitter",
   999  		},
  1000  	}
  1001  
  1002  	AttachContactNames(tc.MetaContext(), rawParticipants)
  1003  	require.NotNil(t, rawParticipants[0].ContactName)
  1004  	require.Equal(t, "Tofu R-Key", *rawParticipants[0].ContactName)
  1005  	require.NotNil(t, rawParticipants[1].ContactName)
  1006  	require.Equal(t, "Alice", *rawParticipants[1].ContactName)
  1007  	require.Nil(t, rawParticipants[2].ContactName)
  1008  	require.Nil(t, rawParticipants[3].ContactName)
  1009  }
  1010  
  1011  func TestTLFIsTeamID(t *testing.T) {
  1012  	teamID := keybase1.MakeTestTeamID(3, false)
  1013  	tlfID := chat1.TLFID(teamID.ToBytes())
  1014  	require.True(t, tlfID.IsTeamID())
  1015  
  1016  	tlfID = chat1.TLFID{0}
  1017  	require.False(t, tlfID.IsTeamID())
  1018  
  1019  	uid := keybase1.MakeTestUID(3)
  1020  	tlfID = chat1.TLFID(uid.ToBytes())
  1021  	require.False(t, tlfID.IsTeamID())
  1022  }
  1023  
  1024  func TestSearchableRemoteConversationName(t *testing.T) {
  1025  	require.Equal(t, "zoommikem", searchableRemoteConversationNameFromStr("mikem,zoommikem", "mikem"))
  1026  	require.Equal(t, "zoommikem", searchableRemoteConversationNameFromStr("zoommikem,mikem", "mikem"))
  1027  	require.Equal(t, "zoommikem,max",
  1028  		searchableRemoteConversationNameFromStr("zoommikem,mikem,max", "mikem"))
  1029  	require.Equal(t, "zoommikem,zoomua",
  1030  		searchableRemoteConversationNameFromStr("zoommikem,mikem,zoomua", "mikem"))
  1031  	require.Equal(t, "joshblum,zoommikem,zoomua",
  1032  		searchableRemoteConversationNameFromStr("joshblum,zoommikem,mikem,zoomua", "mikem"))
  1033  	require.Equal(t, "joshblum,zoommikem,zoomua",
  1034  		searchableRemoteConversationNameFromStr("joshblum,zoommikem,mikem,zoomua,mikem", "mikem"))
  1035  }