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