1package kbchat
2
3import (
4	"encoding/json"
5	"errors"
6	"fmt"
7
8	"github.com/keybase/go-keybase-chat-bot/kbchat/types/chat1"
9	"github.com/keybase/go-keybase-chat-bot/kbchat/types/keybase1"
10)
11
12type Thread struct {
13	Result chat1.Thread `json:"result"`
14	Error  *Error       `json:"error,omitempty"`
15}
16
17type Inbox struct {
18	Result Result `json:"result"`
19	Error  *Error `json:"error,omitempty"`
20}
21
22type sendMessageBody struct {
23	Body string
24}
25
26type sendMessageOptions struct {
27	Channel          chat1.ChatChannel `json:"channel,omitempty"`
28	ConversationID   chat1.ConvIDStr   `json:"conversation_id,omitempty"`
29	Message          sendMessageBody   `json:",omitempty"`
30	Filename         string            `json:"filename,omitempty"`
31	Title            string            `json:"title,omitempty"`
32	MsgID            chat1.MessageID   `json:"message_id,omitempty"`
33	ConfirmLumenSend bool              `json:"confirm_lumen_send"`
34	ReplyTo          *chat1.MessageID  `json:"reply_to,omitempty"`
35}
36
37type sendMessageParams struct {
38	Options sendMessageOptions
39}
40
41type sendMessageArg struct {
42	Method string
43	Params sendMessageParams
44}
45
46func newSendArg(options sendMessageOptions) sendMessageArg {
47	return sendMessageArg{
48		Method: "send",
49		Params: sendMessageParams{
50			Options: options,
51		},
52	}
53}
54
55// GetConversations reads all conversations from the current user's inbox.
56func (a *API) GetConversations(unreadOnly bool) ([]chat1.ConvSummary, error) {
57	apiInput := fmt.Sprintf(`{"method":"list", "params": { "options": { "unread_only": %v}}}`, unreadOnly)
58	output, err := a.doFetch(apiInput)
59	if err != nil {
60		return nil, err
61	}
62
63	var inbox Inbox
64	if err := json.Unmarshal(output, &inbox); err != nil {
65		return nil, err
66	} else if inbox.Error != nil {
67		return nil, errors.New(inbox.Error.Message)
68	}
69	return inbox.Result.Convs, nil
70}
71
72func (a *API) GetConversation(convID chat1.ConvIDStr) (res chat1.ConvSummary, err error) {
73	convIDEscaped, err := json.Marshal(convID)
74	if err != nil {
75		return res, err
76	}
77	apiInput := fmt.Sprintf(`{"method":"list", "params": { "options": { "conversation_id": %s}}}`, convIDEscaped)
78	output, err := a.doFetch(apiInput)
79	if err != nil {
80		return res, err
81	}
82
83	var inbox Inbox
84	if err := json.Unmarshal(output, &inbox); err != nil {
85		return res, err
86	} else if inbox.Error != nil {
87		return res, errors.New(inbox.Error.Message)
88	} else if len(inbox.Result.Convs) == 0 {
89		return res, errors.New("conversation not found")
90	}
91	return inbox.Result.Convs[0], nil
92}
93
94// GetTextMessages fetches all text messages from a given channel. Optionally can filter
95// ont unread status.
96func (a *API) GetTextMessages(channel chat1.ChatChannel, unreadOnly bool) ([]chat1.MsgSummary, error) {
97	channelBytes, err := json.Marshal(channel)
98	if err != nil {
99		return nil, err
100	}
101	apiInput := fmt.Sprintf(`{"method": "read", "params": {"options": {"channel": %s}}}`, channelBytes)
102	output, err := a.doFetch(apiInput)
103	if err != nil {
104		return nil, err
105	}
106
107	var thread Thread
108
109	if err := json.Unmarshal(output, &thread); err != nil {
110		return nil, fmt.Errorf("unable to decode thread: %v", err)
111	} else if thread.Error != nil {
112		return nil, errors.New(thread.Error.Message)
113	}
114
115	var res []chat1.MsgSummary
116	for _, msg := range thread.Result.Messages {
117		if msg.Msg.Content.TypeName == "text" {
118			res = append(res, *msg.Msg)
119		}
120	}
121
122	return res, nil
123}
124
125func (a *API) SendMessage(channel chat1.ChatChannel, body string, args ...interface{}) (SendResponse, error) {
126	arg := newSendArg(sendMessageOptions{
127		Channel: channel,
128		Message: sendMessageBody{
129			Body: fmt.Sprintf(body, args...),
130		},
131	})
132	return a.doSend(arg)
133}
134
135func (a *API) Broadcast(body string, args ...interface{}) (SendResponse, error) {
136	return a.SendMessage(chat1.ChatChannel{
137		Name:   a.GetUsername(),
138		Public: true,
139	}, fmt.Sprintf(body, args...))
140}
141
142func (a *API) SendMessageByConvID(convID chat1.ConvIDStr, body string, args ...interface{}) (SendResponse, error) {
143	arg := newSendArg(sendMessageOptions{
144		ConversationID: convID,
145		Message: sendMessageBody{
146			Body: fmt.Sprintf(body, args...),
147		},
148	})
149	return a.doSend(arg)
150}
151
152// SendMessageByTlfName sends a message on the given TLF name
153func (a *API) SendMessageByTlfName(tlfName string, body string, args ...interface{}) (SendResponse, error) {
154	arg := newSendArg(sendMessageOptions{
155		Channel: chat1.ChatChannel{
156			Name: tlfName,
157		},
158		Message: sendMessageBody{
159			Body: fmt.Sprintf(body, args...),
160		},
161	})
162	return a.doSend(arg)
163}
164
165func (a *API) SendMessageByTeamName(teamName string, inChannel *string, body string, args ...interface{}) (SendResponse, error) {
166	channel := "general"
167	if inChannel != nil {
168		channel = *inChannel
169	}
170	arg := newSendArg(sendMessageOptions{
171		Channel: chat1.ChatChannel{
172			MembersType: "team",
173			Name:        teamName,
174			TopicName:   channel,
175		},
176		Message: sendMessageBody{
177			Body: fmt.Sprintf(body, args...),
178		},
179	})
180	return a.doSend(arg)
181}
182
183func (a *API) SendReply(channel chat1.ChatChannel, replyTo *chat1.MessageID, body string, args ...interface{}) (SendResponse, error) {
184	arg := newSendArg(sendMessageOptions{
185		Channel: channel,
186		Message: sendMessageBody{
187			Body: fmt.Sprintf(body, args...),
188		},
189		ReplyTo: replyTo,
190	})
191	return a.doSend(arg)
192}
193
194func (a *API) SendReplyByConvID(convID chat1.ConvIDStr, replyTo *chat1.MessageID, body string, args ...interface{}) (SendResponse, error) {
195	arg := newSendArg(sendMessageOptions{
196		ConversationID: convID,
197		Message: sendMessageBody{
198			Body: fmt.Sprintf(body, args...),
199		},
200		ReplyTo: replyTo,
201	})
202	return a.doSend(arg)
203}
204
205func (a *API) SendReplyByTlfName(tlfName string, replyTo *chat1.MessageID, body string, args ...interface{}) (SendResponse, error) {
206	arg := newSendArg(sendMessageOptions{
207		Channel: chat1.ChatChannel{
208			Name: tlfName,
209		},
210		Message: sendMessageBody{
211			Body: fmt.Sprintf(body, args...),
212		},
213		ReplyTo: replyTo,
214	})
215	return a.doSend(arg)
216}
217
218func (a *API) SendAttachmentByTeam(teamName string, inChannel *string, filename string, title string) (SendResponse, error) {
219	channel := "general"
220	if inChannel != nil {
221		channel = *inChannel
222	}
223	arg := sendMessageArg{
224		Method: "attach",
225		Params: sendMessageParams{
226			Options: sendMessageOptions{
227				Channel: chat1.ChatChannel{
228					MembersType: "team",
229					Name:        teamName,
230					TopicName:   channel,
231				},
232				Filename: filename,
233				Title:    title,
234			},
235		},
236	}
237	return a.doSend(arg)
238}
239
240func (a *API) SendAttachmentByConvID(convID chat1.ConvIDStr, filename string, title string) (SendResponse, error) {
241	arg := sendMessageArg{
242		Method: "attach",
243		Params: sendMessageParams{
244			Options: sendMessageOptions{
245				ConversationID: convID,
246				Filename:       filename,
247				Title:          title,
248			},
249		},
250	}
251	return a.doSend(arg)
252}
253
254////////////////////////////////////////////////////////
255// React to chat ///////////////////////////////////////
256////////////////////////////////////////////////////////
257
258type reactionOptions struct {
259	ConversationID chat1.ConvIDStr `json:"conversation_id"`
260	Message        sendMessageBody
261	MsgID          chat1.MessageID   `json:"message_id"`
262	Channel        chat1.ChatChannel `json:"channel"`
263}
264
265type reactionParams struct {
266	Options reactionOptions
267}
268
269type reactionArg struct {
270	Method string
271	Params reactionParams
272}
273
274func newReactionArg(options reactionOptions) reactionArg {
275	return reactionArg{
276		Method: "reaction",
277		Params: reactionParams{Options: options},
278	}
279}
280
281func (a *API) ReactByChannel(channel chat1.ChatChannel, msgID chat1.MessageID, reaction string) (SendResponse, error) {
282	arg := newReactionArg(reactionOptions{
283		Message: sendMessageBody{Body: reaction},
284		MsgID:   msgID,
285		Channel: channel,
286	})
287	return a.doSend(arg)
288}
289
290func (a *API) ReactByConvID(convID chat1.ConvIDStr, msgID chat1.MessageID, reaction string) (SendResponse, error) {
291	arg := newReactionArg(reactionOptions{
292		Message:        sendMessageBody{Body: reaction},
293		MsgID:          msgID,
294		ConversationID: convID,
295	})
296	return a.doSend(arg)
297}
298
299func (a *API) EditByConvID(convID chat1.ConvIDStr, msgID chat1.MessageID, text string) (SendResponse, error) {
300	arg := reactionArg{
301		Method: "edit",
302		Params: reactionParams{Options: reactionOptions{
303			Message:        sendMessageBody{Body: text},
304			MsgID:          msgID,
305			ConversationID: convID,
306		}},
307	}
308	return a.doSend(arg)
309}
310
311////////////////////////////////////////////////////////
312// Manage channels /////////////////////////////////////
313////////////////////////////////////////////////////////
314
315type ChannelsList struct {
316	Result Result `json:"result"`
317	Error  *Error `json:"error,omitempty"`
318}
319
320type JoinChannel struct {
321	Error  *Error         `json:"error,omitempty"`
322	Result chat1.EmptyRes `json:"result"`
323}
324
325type LeaveChannel struct {
326	Error  *Error         `json:"error,omitempty"`
327	Result chat1.EmptyRes `json:"result"`
328}
329
330func (a *API) ListChannels(teamName string) ([]string, error) {
331	teamNameEscaped, err := json.Marshal(teamName)
332	if err != nil {
333		return nil, err
334	}
335	apiInput := fmt.Sprintf(`{"method": "listconvsonname", "params": {"options": {"topic_type": "CHAT", "members_type": "team", "name": %s}}}`, teamNameEscaped)
336	output, err := a.doFetch(apiInput)
337	if err != nil {
338		return nil, err
339	}
340
341	var channelsList ChannelsList
342	if err := json.Unmarshal(output, &channelsList); err != nil {
343		return nil, err
344	} else if channelsList.Error != nil {
345		return nil, errors.New(channelsList.Error.Message)
346	}
347
348	var channels []string
349	for _, conv := range channelsList.Result.Convs {
350		channels = append(channels, conv.Channel.TopicName)
351	}
352	return channels, nil
353}
354
355func (a *API) JoinChannel(teamName string, channelName string) (chat1.EmptyRes, error) {
356	empty := chat1.EmptyRes{}
357
358	teamNameEscaped, err := json.Marshal(teamName)
359	if err != nil {
360		return empty, err
361	}
362	channelNameEscaped, err := json.Marshal(channelName)
363	if err != nil {
364		return empty, err
365	}
366	apiInput := fmt.Sprintf(`{"method": "join", "params": {"options": {"channel": {"name": %s, "members_type": "team", "topic_name": %s}}}}`,
367		teamNameEscaped, channelNameEscaped)
368	output, err := a.doFetch(apiInput)
369	if err != nil {
370		return empty, err
371	}
372
373	joinChannel := JoinChannel{}
374	err = json.Unmarshal(output, &joinChannel)
375	if err != nil {
376		return empty, fmt.Errorf("failed to parse output from keybase team api: %v", err)
377	} else if joinChannel.Error != nil {
378		return empty, errors.New(joinChannel.Error.Message)
379	}
380
381	return joinChannel.Result, nil
382}
383
384func (a *API) LeaveChannel(teamName string, channelName string) (chat1.EmptyRes, error) {
385	empty := chat1.EmptyRes{}
386
387	teamNameEscaped, err := json.Marshal(teamName)
388	if err != nil {
389		return empty, err
390	}
391	channelNameEscaped, err := json.Marshal(channelName)
392	if err != nil {
393		return empty, err
394	}
395	apiInput := fmt.Sprintf(`{"method": "leave", "params": {"options": {"channel": {"name": %s, "members_type": "team", "topic_name": %s}}}}`,
396		teamNameEscaped, channelNameEscaped)
397	output, err := a.doFetch(apiInput)
398	if err != nil {
399		return empty, err
400	}
401
402	leaveChannel := LeaveChannel{}
403	err = json.Unmarshal(output, &leaveChannel)
404	if err != nil {
405		return empty, fmt.Errorf("failed to parse output from keybase team api: %v", err)
406	} else if leaveChannel.Error != nil {
407		return empty, errors.New(leaveChannel.Error.Message)
408	}
409
410	return leaveChannel.Result, nil
411}
412
413////////////////////////////////////////////////////////
414// Send lumens in chat /////////////////////////////////
415////////////////////////////////////////////////////////
416
417func (a *API) InChatSend(channel chat1.ChatChannel, body string, args ...interface{}) (SendResponse, error) {
418	arg := newSendArg(sendMessageOptions{
419		Channel: channel,
420		Message: sendMessageBody{
421			Body: fmt.Sprintf(body, args...),
422		},
423		ConfirmLumenSend: true,
424	})
425	return a.doSend(arg)
426}
427
428func (a *API) InChatSendByConvID(convID chat1.ConvIDStr, body string, args ...interface{}) (SendResponse, error) {
429	arg := newSendArg(sendMessageOptions{
430		ConversationID: convID,
431		Message: sendMessageBody{
432			Body: fmt.Sprintf(body, args...),
433		},
434		ConfirmLumenSend: true,
435	})
436	return a.doSend(arg)
437}
438
439func (a *API) InChatSendByTlfName(tlfName string, body string, args ...interface{}) (SendResponse, error) {
440	arg := newSendArg(sendMessageOptions{
441		Channel: chat1.ChatChannel{
442			Name: tlfName,
443		},
444		Message: sendMessageBody{
445			Body: fmt.Sprintf(body, args...),
446		},
447		ConfirmLumenSend: true,
448	})
449	return a.doSend(arg)
450}
451
452////////////////////////////////////////////////////////
453// Misc commands ///////////////////////////////////////
454////////////////////////////////////////////////////////
455
456type Advertisement struct {
457	Alias          string `json:"alias,omitempty"`
458	Advertisements []chat1.AdvertiseCommandAPIParam
459}
460
461type ListCommandsResponse struct {
462	Result struct {
463		Commands []chat1.UserBotCommandOutput `json:"commands"`
464	} `json:"result"`
465	Error *Error `json:"error,omitempty"`
466}
467
468type advertiseCmdsParams struct {
469	Options Advertisement
470}
471
472type advertiseCmdsMsgArg struct {
473	Method string
474	Params advertiseCmdsParams
475}
476
477func newAdvertiseCmdsMsgArg(ad Advertisement) advertiseCmdsMsgArg {
478	return advertiseCmdsMsgArg{
479		Method: "advertisecommands",
480		Params: advertiseCmdsParams{
481			Options: ad,
482		},
483	}
484}
485
486func (a *API) AdvertiseCommands(ad Advertisement) (SendResponse, error) {
487	return a.doSend(newAdvertiseCmdsMsgArg(ad))
488}
489
490type clearCmdsOptions struct {
491	Filter *chat1.ClearCommandAPIParam `json:"filter"`
492}
493
494type clearCmdsParams struct {
495	Options clearCmdsOptions `json:"options"`
496}
497
498type clearCmdsArg struct {
499	Method string          `json:"method"`
500	Params clearCmdsParams `json:"params,omitempty"`
501}
502
503func (a *API) ClearCommands(filter *chat1.ClearCommandAPIParam) error {
504	_, err := a.doSend(clearCmdsArg{
505		Method: "clearcommands",
506		Params: clearCmdsParams{
507			Options: clearCmdsOptions{
508				Filter: filter,
509			},
510		},
511	})
512	return err
513}
514
515type listCmdsOptions struct {
516	Channel        chat1.ChatChannel `json:"channel,omitempty"`
517	ConversationID chat1.ConvIDStr   `json:"conversation_id,omitempty"`
518}
519
520type listCmdsParams struct {
521	Options listCmdsOptions
522}
523
524type listCmdsArg struct {
525	Method string
526	Params listCmdsParams
527}
528
529func newListCmdsArg(options listCmdsOptions) listCmdsArg {
530	return listCmdsArg{
531		Method: "listcommands",
532		Params: listCmdsParams{
533			Options: options,
534		},
535	}
536}
537
538func (a *API) ListCommands(channel chat1.ChatChannel) ([]chat1.UserBotCommandOutput, error) {
539	arg := newListCmdsArg(listCmdsOptions{
540		Channel: channel,
541	})
542	return a.listCommands(arg)
543}
544
545func (a *API) ListCommandsByConvID(convID chat1.ConvIDStr) ([]chat1.UserBotCommandOutput, error) {
546	arg := newListCmdsArg(listCmdsOptions{
547		ConversationID: convID,
548	})
549	return a.listCommands(arg)
550}
551
552func (a *API) listCommands(arg listCmdsArg) ([]chat1.UserBotCommandOutput, error) {
553	bArg, err := json.Marshal(arg)
554	if err != nil {
555		return nil, err
556	}
557	output, err := a.doFetch(string(bArg))
558	if err != nil {
559		return nil, err
560	}
561	var res ListCommandsResponse
562	if err := json.Unmarshal(output, &res); err != nil {
563		return nil, err
564	} else if res.Error != nil {
565		return nil, errors.New(res.Error.Message)
566	}
567	return res.Result.Commands, nil
568}
569
570type listMembersOptions struct {
571	Channel        chat1.ChatChannel `json:"channel,omitempty"`
572	ConversationID chat1.ConvIDStr   `json:"conversation_id,omitempty"`
573}
574
575type listMembersParams struct {
576	Options listMembersOptions
577}
578
579type listMembersArg struct {
580	Method string
581	Params listMembersParams
582}
583
584func newListMembersArg(options listMembersOptions) listMembersArg {
585	return listMembersArg{
586		Method: "listmembers",
587		Params: listMembersParams{
588			Options: options,
589		},
590	}
591}
592
593func (a *API) ListMembers(channel chat1.ChatChannel) (keybase1.TeamMembersDetails, error) {
594	arg := newListMembersArg(listMembersOptions{
595		Channel: channel,
596	})
597	return a.listMembers(arg)
598}
599
600func (a *API) ListMembersByConvID(conversationID chat1.ConvIDStr) (keybase1.TeamMembersDetails, error) {
601	arg := newListMembersArg(listMembersOptions{
602		ConversationID: conversationID,
603	})
604	return a.listMembers(arg)
605}
606
607func (a *API) listMembers(arg listMembersArg) (res keybase1.TeamMembersDetails, err error) {
608	bArg, err := json.Marshal(arg)
609	if err != nil {
610		return res, err
611	}
612	output, err := a.doFetch(string(bArg))
613	if err != nil {
614		return res, err
615	}
616	members := ListTeamMembers{}
617	err = json.Unmarshal(output, &members)
618	if err != nil {
619		return res, UnmarshalError{err}
620	}
621	if members.Error.Message != "" {
622		return res, members.Error
623	}
624	return members.Result.Members, nil
625}
626
627type GetMessagesResult struct {
628	Result struct {
629		Messages []chat1.Message `json:"messages"`
630	} `json:"result"`
631	Error *Error `json:"error,omitempty"`
632}
633
634type getMessagesOptions struct {
635	Channel        chat1.ChatChannel `json:"channel,omitempty"`
636	ConversationID chat1.ConvIDStr   `json:"conversation_id,omitempty"`
637	MessageIDs     []chat1.MessageID `json:"message_ids,omitempty"`
638}
639
640type getMessagesParams struct {
641	Options getMessagesOptions
642}
643
644type getMessagesArg struct {
645	Method string
646	Params getMessagesParams
647}
648
649func newGetMessagesArg(options getMessagesOptions) getMessagesArg {
650	return getMessagesArg{
651		Method: "get",
652		Params: getMessagesParams{
653			Options: options,
654		},
655	}
656}
657
658func (a *API) GetMessages(channel chat1.ChatChannel, msgIDs []chat1.MessageID) ([]chat1.Message, error) {
659	arg := newGetMessagesArg(getMessagesOptions{
660		Channel:    channel,
661		MessageIDs: msgIDs,
662	})
663	return a.getMessages(arg)
664}
665
666func (a *API) GetMessagesByConvID(conversationID chat1.ConvIDStr, msgIDs []chat1.MessageID) ([]chat1.Message, error) {
667	arg := newGetMessagesArg(getMessagesOptions{
668		ConversationID: conversationID,
669		MessageIDs:     msgIDs,
670	})
671	return a.getMessages(arg)
672}
673
674func (a *API) getMessages(arg getMessagesArg) ([]chat1.Message, error) {
675	bArg, err := json.Marshal(arg)
676	if err != nil {
677		return nil, err
678	}
679	output, err := a.doFetch(string(bArg))
680	if err != nil {
681		return nil, err
682	}
683	var res GetMessagesResult
684	err = json.Unmarshal(output, &res)
685	if err != nil {
686		return nil, UnmarshalError{err}
687	}
688	if res.Error != nil {
689		return nil, res.Error
690	}
691	return res.Result.Messages, nil
692}
693