package kbchat import ( "encoding/json" "errors" "fmt" "github.com/keybase/go-keybase-chat-bot/kbchat/types/chat1" "github.com/keybase/go-keybase-chat-bot/kbchat/types/keybase1" ) type Thread struct { Result chat1.Thread `json:"result"` Error *Error `json:"error,omitempty"` } type Inbox struct { Result Result `json:"result"` Error *Error `json:"error,omitempty"` } type sendMessageBody struct { Body string } type sendMessageOptions struct { Channel chat1.ChatChannel `json:"channel,omitempty"` ConversationID chat1.ConvIDStr `json:"conversation_id,omitempty"` Message sendMessageBody `json:",omitempty"` Filename string `json:"filename,omitempty"` Title string `json:"title,omitempty"` MsgID chat1.MessageID `json:"message_id,omitempty"` ConfirmLumenSend bool `json:"confirm_lumen_send"` ReplyTo *chat1.MessageID `json:"reply_to,omitempty"` } type sendMessageParams struct { Options sendMessageOptions } type sendMessageArg struct { Method string Params sendMessageParams } func newSendArg(options sendMessageOptions) sendMessageArg { return sendMessageArg{ Method: "send", Params: sendMessageParams{ Options: options, }, } } // GetConversations reads all conversations from the current user's inbox. func (a *API) GetConversations(unreadOnly bool) ([]chat1.ConvSummary, error) { apiInput := fmt.Sprintf(`{"method":"list", "params": { "options": { "unread_only": %v}}}`, unreadOnly) output, err := a.doFetch(apiInput) if err != nil { return nil, err } var inbox Inbox if err := json.Unmarshal(output, &inbox); err != nil { return nil, err } else if inbox.Error != nil { return nil, errors.New(inbox.Error.Message) } return inbox.Result.Convs, nil } func (a *API) GetConversation(convID chat1.ConvIDStr) (res chat1.ConvSummary, err error) { convIDEscaped, err := json.Marshal(convID) if err != nil { return res, err } apiInput := fmt.Sprintf(`{"method":"list", "params": { "options": { "conversation_id": %s}}}`, convIDEscaped) output, err := a.doFetch(apiInput) if err != nil { return res, err } var inbox Inbox if err := json.Unmarshal(output, &inbox); err != nil { return res, err } else if inbox.Error != nil { return res, errors.New(inbox.Error.Message) } else if len(inbox.Result.Convs) == 0 { return res, errors.New("conversation not found") } return inbox.Result.Convs[0], nil } // GetTextMessages fetches all text messages from a given channel. Optionally can filter // ont unread status. func (a *API) GetTextMessages(channel chat1.ChatChannel, unreadOnly bool) ([]chat1.MsgSummary, error) { channelBytes, err := json.Marshal(channel) if err != nil { return nil, err } apiInput := fmt.Sprintf(`{"method": "read", "params": {"options": {"channel": %s}}}`, channelBytes) output, err := a.doFetch(apiInput) if err != nil { return nil, err } var thread Thread if err := json.Unmarshal(output, &thread); err != nil { return nil, fmt.Errorf("unable to decode thread: %v", err) } else if thread.Error != nil { return nil, errors.New(thread.Error.Message) } var res []chat1.MsgSummary for _, msg := range thread.Result.Messages { if msg.Msg != nil && msg.Msg.Content.TypeName == "text" { res = append(res, *msg.Msg) } } return res, nil } func (a *API) SendMessage(channel chat1.ChatChannel, body string, args ...interface{}) (resp SendResponse, err error) { defer a.Trace(&err, "SendMessage")() arg := newSendArg(sendMessageOptions{ Channel: channel, Message: sendMessageBody{ Body: fmt.Sprintf(body, args...), }, }) return a.doSend(arg) } func (a *API) Broadcast(body string, args ...interface{}) (SendResponse, error) { return a.SendMessage(chat1.ChatChannel{ Name: a.GetUsername(), Public: true, }, fmt.Sprintf(body, args...)) } func (a *API) SendMessageByConvID(convID chat1.ConvIDStr, body string, args ...interface{}) (resp SendResponse, err error) { defer a.Trace(&err, "SendMessageByConvID")() arg := newSendArg(sendMessageOptions{ ConversationID: convID, Message: sendMessageBody{ Body: fmt.Sprintf(body, args...), }, }) return a.doSend(arg) } // SendMessageByTlfName sends a message on the given TLF name func (a *API) SendMessageByTlfName(tlfName string, body string, args ...interface{}) (resp SendResponse, err error) { defer a.Trace(&err, "SendMessageByTlfName")() arg := newSendArg(sendMessageOptions{ Channel: chat1.ChatChannel{ Name: tlfName, }, Message: sendMessageBody{ Body: fmt.Sprintf(body, args...), }, }) return a.doSend(arg) } func (a *API) SendMessageByTeamName(teamName string, inChannel *string, body string, args ...interface{}) (resp SendResponse, err error) { defer a.Trace(&err, "SendMessageByTeamName")() channel := "general" if inChannel != nil { channel = *inChannel } arg := newSendArg(sendMessageOptions{ Channel: chat1.ChatChannel{ MembersType: "team", Name: teamName, TopicName: channel, }, Message: sendMessageBody{ Body: fmt.Sprintf(body, args...), }, }) return a.doSend(arg) } func (a *API) SendReply(channel chat1.ChatChannel, replyTo *chat1.MessageID, body string, args ...interface{}) (SendResponse, error) { arg := newSendArg(sendMessageOptions{ Channel: channel, Message: sendMessageBody{ Body: fmt.Sprintf(body, args...), }, ReplyTo: replyTo, }) return a.doSend(arg) } func (a *API) SendReplyByConvID(convID chat1.ConvIDStr, replyTo *chat1.MessageID, body string, args ...interface{}) (SendResponse, error) { arg := newSendArg(sendMessageOptions{ ConversationID: convID, Message: sendMessageBody{ Body: fmt.Sprintf(body, args...), }, ReplyTo: replyTo, }) return a.doSend(arg) } func (a *API) SendReplyByTlfName(tlfName string, replyTo *chat1.MessageID, body string, args ...interface{}) (SendResponse, error) { arg := newSendArg(sendMessageOptions{ Channel: chat1.ChatChannel{ Name: tlfName, }, Message: sendMessageBody{ Body: fmt.Sprintf(body, args...), }, ReplyTo: replyTo, }) return a.doSend(arg) } func (a *API) SendAttachmentByTeam(teamName string, inChannel *string, filename string, title string) (SendResponse, error) { channel := "general" if inChannel != nil { channel = *inChannel } arg := sendMessageArg{ Method: "attach", Params: sendMessageParams{ Options: sendMessageOptions{ Channel: chat1.ChatChannel{ MembersType: "team", Name: teamName, TopicName: channel, }, Filename: filename, Title: title, }, }, } return a.doSend(arg) } func (a *API) SendAttachmentByConvID(convID chat1.ConvIDStr, filename string, title string) (SendResponse, error) { arg := sendMessageArg{ Method: "attach", Params: sendMessageParams{ Options: sendMessageOptions{ ConversationID: convID, Filename: filename, Title: title, }, }, } return a.doSend(arg) } //////////////////////////////////////////////////////// // React to chat /////////////////////////////////////// //////////////////////////////////////////////////////// type reactionOptions struct { ConversationID chat1.ConvIDStr `json:"conversation_id"` Message sendMessageBody MsgID chat1.MessageID `json:"message_id"` Channel chat1.ChatChannel `json:"channel"` } type reactionParams struct { Options reactionOptions } type reactionArg struct { Method string Params reactionParams } func newReactionArg(options reactionOptions) reactionArg { return reactionArg{ Method: "reaction", Params: reactionParams{Options: options}, } } func (a *API) ReactByChannel(channel chat1.ChatChannel, msgID chat1.MessageID, reaction string) (SendResponse, error) { arg := newReactionArg(reactionOptions{ Message: sendMessageBody{Body: reaction}, MsgID: msgID, Channel: channel, }) return a.doSend(arg) } func (a *API) ReactByConvID(convID chat1.ConvIDStr, msgID chat1.MessageID, reaction string) (SendResponse, error) { arg := newReactionArg(reactionOptions{ Message: sendMessageBody{Body: reaction}, MsgID: msgID, ConversationID: convID, }) return a.doSend(arg) } func (a *API) EditByConvID(convID chat1.ConvIDStr, msgID chat1.MessageID, text string) (SendResponse, error) { arg := reactionArg{ Method: "edit", Params: reactionParams{Options: reactionOptions{ Message: sendMessageBody{Body: text}, MsgID: msgID, ConversationID: convID, }}, } return a.doSend(arg) } //////////////////////////////////////////////////////// // Manage channels ///////////////////////////////////// //////////////////////////////////////////////////////// type ChannelsList struct { Result Result `json:"result"` Error *Error `json:"error,omitempty"` } type JoinChannel struct { Error *Error `json:"error,omitempty"` Result chat1.EmptyRes `json:"result"` } type LeaveChannel struct { Error *Error `json:"error,omitempty"` Result chat1.EmptyRes `json:"result"` } func (a *API) ListChannels(teamName string) ([]string, error) { teamNameEscaped, err := json.Marshal(teamName) if err != nil { return nil, err } apiInput := fmt.Sprintf(`{"method": "listconvsonname", "params": {"options": {"topic_type": "CHAT", "members_type": "team", "name": %s}}}`, teamNameEscaped) output, err := a.doFetch(apiInput) if err != nil { return nil, err } var channelsList ChannelsList if err := json.Unmarshal(output, &channelsList); err != nil { return nil, err } else if channelsList.Error != nil { return nil, errors.New(channelsList.Error.Message) } var channels []string for _, conv := range channelsList.Result.Convs { channels = append(channels, conv.Channel.TopicName) } return channels, nil } func (a *API) JoinChannel(teamName string, channelName string) (chat1.EmptyRes, error) { empty := chat1.EmptyRes{} teamNameEscaped, err := json.Marshal(teamName) if err != nil { return empty, err } channelNameEscaped, err := json.Marshal(channelName) if err != nil { return empty, err } apiInput := fmt.Sprintf(`{"method": "join", "params": {"options": {"channel": {"name": %s, "members_type": "team", "topic_name": %s}}}}`, teamNameEscaped, channelNameEscaped) output, err := a.doFetch(apiInput) if err != nil { return empty, err } joinChannel := JoinChannel{} err = json.Unmarshal(output, &joinChannel) if err != nil { return empty, fmt.Errorf("failed to parse output from keybase team api: %v", err) } else if joinChannel.Error != nil { return empty, errors.New(joinChannel.Error.Message) } return joinChannel.Result, nil } func (a *API) LeaveChannel(teamName string, channelName string) (chat1.EmptyRes, error) { empty := chat1.EmptyRes{} teamNameEscaped, err := json.Marshal(teamName) if err != nil { return empty, err } channelNameEscaped, err := json.Marshal(channelName) if err != nil { return empty, err } apiInput := fmt.Sprintf(`{"method": "leave", "params": {"options": {"channel": {"name": %s, "members_type": "team", "topic_name": %s}}}}`, teamNameEscaped, channelNameEscaped) output, err := a.doFetch(apiInput) if err != nil { return empty, err } leaveChannel := LeaveChannel{} err = json.Unmarshal(output, &leaveChannel) if err != nil { return empty, fmt.Errorf("failed to parse output from keybase team api: %v", err) } else if leaveChannel.Error != nil { return empty, errors.New(leaveChannel.Error.Message) } return leaveChannel.Result, nil } //////////////////////////////////////////////////////// // Send lumens in chat ///////////////////////////////// //////////////////////////////////////////////////////// func (a *API) InChatSend(channel chat1.ChatChannel, body string, args ...interface{}) (SendResponse, error) { arg := newSendArg(sendMessageOptions{ Channel: channel, Message: sendMessageBody{ Body: fmt.Sprintf(body, args...), }, ConfirmLumenSend: true, }) return a.doSend(arg) } func (a *API) InChatSendByConvID(convID chat1.ConvIDStr, body string, args ...interface{}) (SendResponse, error) { arg := newSendArg(sendMessageOptions{ ConversationID: convID, Message: sendMessageBody{ Body: fmt.Sprintf(body, args...), }, ConfirmLumenSend: true, }) return a.doSend(arg) } func (a *API) InChatSendByTlfName(tlfName string, body string, args ...interface{}) (SendResponse, error) { arg := newSendArg(sendMessageOptions{ Channel: chat1.ChatChannel{ Name: tlfName, }, Message: sendMessageBody{ Body: fmt.Sprintf(body, args...), }, ConfirmLumenSend: true, }) return a.doSend(arg) } //////////////////////////////////////////////////////// // Misc commands /////////////////////////////////////// //////////////////////////////////////////////////////// type Advertisement struct { Alias string `json:"alias,omitempty"` Advertisements []chat1.AdvertiseCommandAPIParam } type ListCommandsResponse struct { Result struct { Commands []chat1.UserBotCommandOutput `json:"commands"` } `json:"result"` Error *Error `json:"error,omitempty"` } type advertiseCmdsParams struct { Options Advertisement } type advertiseCmdsMsgArg struct { Method string Params advertiseCmdsParams } func newAdvertiseCmdsMsgArg(ad Advertisement) advertiseCmdsMsgArg { return advertiseCmdsMsgArg{ Method: "advertisecommands", Params: advertiseCmdsParams{ Options: ad, }, } } func (a *API) AdvertiseCommands(ad Advertisement) (SendResponse, error) { return a.doSend(newAdvertiseCmdsMsgArg(ad)) } type clearCmdsOptions struct { Filter *chat1.ClearCommandAPIParam `json:"filter"` } type clearCmdsParams struct { Options clearCmdsOptions `json:"options"` } type clearCmdsArg struct { Method string `json:"method"` Params clearCmdsParams `json:"params,omitempty"` } func (a *API) ClearCommands(filter *chat1.ClearCommandAPIParam) error { _, err := a.doSend(clearCmdsArg{ Method: "clearcommands", Params: clearCmdsParams{ Options: clearCmdsOptions{ Filter: filter, }, }, }) return err } type listCmdsOptions struct { Channel chat1.ChatChannel `json:"channel,omitempty"` ConversationID chat1.ConvIDStr `json:"conversation_id,omitempty"` } type listCmdsParams struct { Options listCmdsOptions } type listCmdsArg struct { Method string Params listCmdsParams } func newListCmdsArg(options listCmdsOptions) listCmdsArg { return listCmdsArg{ Method: "listcommands", Params: listCmdsParams{ Options: options, }, } } func (a *API) ListCommands(channel chat1.ChatChannel) ([]chat1.UserBotCommandOutput, error) { arg := newListCmdsArg(listCmdsOptions{ Channel: channel, }) return a.listCommands(arg) } func (a *API) ListCommandsByConvID(convID chat1.ConvIDStr) ([]chat1.UserBotCommandOutput, error) { arg := newListCmdsArg(listCmdsOptions{ ConversationID: convID, }) return a.listCommands(arg) } func (a *API) listCommands(arg listCmdsArg) ([]chat1.UserBotCommandOutput, error) { bArg, err := json.Marshal(arg) if err != nil { return nil, err } output, err := a.doFetch(string(bArg)) if err != nil { return nil, err } var res ListCommandsResponse if err := json.Unmarshal(output, &res); err != nil { return nil, err } else if res.Error != nil { return nil, errors.New(res.Error.Message) } return res.Result.Commands, nil } type listMembersOptions struct { Channel chat1.ChatChannel `json:"channel,omitempty"` ConversationID chat1.ConvIDStr `json:"conversation_id,omitempty"` } type listMembersParams struct { Options listMembersOptions } type listMembersArg struct { Method string Params listMembersParams } func newListMembersArg(options listMembersOptions) listMembersArg { return listMembersArg{ Method: "listmembers", Params: listMembersParams{ Options: options, }, } } func (a *API) ListMembers(channel chat1.ChatChannel) (keybase1.TeamMembersDetails, error) { arg := newListMembersArg(listMembersOptions{ Channel: channel, }) return a.listMembers(arg) } func (a *API) ListMembersByConvID(conversationID chat1.ConvIDStr) (keybase1.TeamMembersDetails, error) { arg := newListMembersArg(listMembersOptions{ ConversationID: conversationID, }) return a.listMembers(arg) } func (a *API) listMembers(arg listMembersArg) (res keybase1.TeamMembersDetails, err error) { bArg, err := json.Marshal(arg) if err != nil { return res, err } output, err := a.doFetch(string(bArg)) if err != nil { return res, err } members := ListTeamMembers{} err = json.Unmarshal(output, &members) if err != nil { return res, UnmarshalError{err} } if members.Error.Message != "" { return res, members.Error } return members.Result.Members, nil } type GetMessagesResult struct { Result struct { Messages []chat1.Message `json:"messages"` } `json:"result"` Error *Error `json:"error,omitempty"` } type getMessagesOptions struct { Channel chat1.ChatChannel `json:"channel,omitempty"` ConversationID chat1.ConvIDStr `json:"conversation_id,omitempty"` MessageIDs []chat1.MessageID `json:"message_ids,omitempty"` } type getMessagesParams struct { Options getMessagesOptions } type getMessagesArg struct { Method string Params getMessagesParams } func newGetMessagesArg(options getMessagesOptions) getMessagesArg { return getMessagesArg{ Method: "get", Params: getMessagesParams{ Options: options, }, } } func (a *API) GetMessages(channel chat1.ChatChannel, msgIDs []chat1.MessageID) ([]chat1.Message, error) { arg := newGetMessagesArg(getMessagesOptions{ Channel: channel, MessageIDs: msgIDs, }) return a.getMessages(arg) } func (a *API) GetMessagesByConvID(conversationID chat1.ConvIDStr, msgIDs []chat1.MessageID) ([]chat1.Message, error) { arg := newGetMessagesArg(getMessagesOptions{ ConversationID: conversationID, MessageIDs: msgIDs, }) return a.getMessages(arg) } func (a *API) getMessages(arg getMessagesArg) ([]chat1.Message, error) { bArg, err := json.Marshal(arg) if err != nil { return nil, err } output, err := a.doFetch(string(bArg)) if err != nil { return nil, err } var res GetMessagesResult err = json.Unmarshal(output, &res) if err != nil { return nil, UnmarshalError{err} } if res.Error != nil { return nil, res.Error } return res.Result.Messages, nil }