From 921f2dfcdf1a6263220b55eb55716e497373dfcf Mon Sep 17 00:00:00 2001 From: cori hudson <54032873+hyperobject@users.noreply.github.com> Date: Mon, 26 Aug 2019 15:00:31 -0400 Subject: Add initial Keybase Chat support (#877) * initial work on native keybase bridging * Hopefully make a functional keybase bridge * add keybase to bridgemap * send to right channel, try to figure out received msgs * add account and userid * i am a Dam Fool * Fix formatting for messages, handle /me * update vendors, ran golint and goimports * move handlers to handlers.go, clean up unused config options * add sample config, fix inconsistent remote nick handling * Update readme with keybase links * Resolve fixmie errors * Error -> Errorf * fix linting errors in go.mod and go.sum * explicitly join channels, ignore messages from non-specified channels * check that team names match before bridging message --- .../github.com/keybase/go-keybase-chat-bot/LICENSE | 27 + .../keybase/go-keybase-chat-bot/kbchat/kbchat.go | 693 +++++++++++++++++++++ .../keybase/go-keybase-chat-bot/kbchat/team.go | 89 +++ .../kbchat/test_config.example.yaml | 16 + .../go-keybase-chat-bot/kbchat/test_utils.go | 54 ++ .../keybase/go-keybase-chat-bot/kbchat/types.go | 159 +++++ .../keybase/go-keybase-chat-bot/kbchat/wallet.go | 48 ++ 7 files changed, 1086 insertions(+) create mode 100644 vendor/github.com/keybase/go-keybase-chat-bot/LICENSE create mode 100644 vendor/github.com/keybase/go-keybase-chat-bot/kbchat/kbchat.go create mode 100644 vendor/github.com/keybase/go-keybase-chat-bot/kbchat/team.go create mode 100644 vendor/github.com/keybase/go-keybase-chat-bot/kbchat/test_config.example.yaml create mode 100644 vendor/github.com/keybase/go-keybase-chat-bot/kbchat/test_utils.go create mode 100644 vendor/github.com/keybase/go-keybase-chat-bot/kbchat/types.go create mode 100644 vendor/github.com/keybase/go-keybase-chat-bot/kbchat/wallet.go (limited to 'vendor/github.com/keybase/go-keybase-chat-bot') diff --git a/vendor/github.com/keybase/go-keybase-chat-bot/LICENSE b/vendor/github.com/keybase/go-keybase-chat-bot/LICENSE new file mode 100644 index 00000000..5bc0d94c --- /dev/null +++ b/vendor/github.com/keybase/go-keybase-chat-bot/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) 2017, Keybase +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of keybase nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/vendor/github.com/keybase/go-keybase-chat-bot/kbchat/kbchat.go b/vendor/github.com/keybase/go-keybase-chat-bot/kbchat/kbchat.go new file mode 100644 index 00000000..c735052c --- /dev/null +++ b/vendor/github.com/keybase/go-keybase-chat-bot/kbchat/kbchat.go @@ -0,0 +1,693 @@ +package kbchat + +import ( + "bufio" + "encoding/json" + "errors" + "fmt" + "io" + "log" + "os/exec" + "strings" + "sync" + "time" +) + +// API is the main object used for communicating with the Keybase JSON API +type API struct { + sync.Mutex + apiInput io.Writer + apiOutput *bufio.Reader + apiCmd *exec.Cmd + username string + runOpts RunOptions +} + +func getUsername(runOpts RunOptions) (username string, err error) { + p := runOpts.Command("status") + output, err := p.StdoutPipe() + if err != nil { + return "", err + } + if err = p.Start(); err != nil { + return "", err + } + + doneCh := make(chan error) + go func() { + scanner := bufio.NewScanner(output) + if !scanner.Scan() { + doneCh <- errors.New("unable to find Keybase username") + return + } + toks := strings.Fields(scanner.Text()) + if len(toks) != 2 { + doneCh <- errors.New("invalid Keybase username output") + return + } + username = toks[1] + doneCh <- nil + }() + + select { + case err = <-doneCh: + if err != nil { + return "", err + } + case <-time.After(5 * time.Second): + return "", errors.New("unable to run Keybase command") + } + + return username, nil +} + +type OneshotOptions struct { + Username string + PaperKey string +} + +type RunOptions struct { + KeybaseLocation string + HomeDir string + Oneshot *OneshotOptions + StartService bool +} + +func (r RunOptions) Location() string { + if r.KeybaseLocation == "" { + return "keybase" + } + return r.KeybaseLocation +} + +func (r RunOptions) Command(args ...string) *exec.Cmd { + var cmd []string + if r.HomeDir != "" { + cmd = append(cmd, "--home", r.HomeDir) + } + cmd = append(cmd, args...) + return exec.Command(r.Location(), cmd...) +} + +// Start fires up the Keybase JSON API in stdin/stdout mode +func Start(runOpts RunOptions) (*API, error) { + api := &API{ + runOpts: runOpts, + } + if err := api.startPipes(); err != nil { + return nil, err + } + return api, nil +} + +func (a *API) auth() (string, error) { + username, err := getUsername(a.runOpts) + if err == nil { + return username, nil + } + if a.runOpts.Oneshot == nil { + return "", err + } + username = "" + // If a paper key is specified, then login with oneshot mode (logout first) + if a.runOpts.Oneshot != nil { + if username == a.runOpts.Oneshot.Username { + // just get out if we are on the desired user already + return username, nil + } + if err := a.runOpts.Command("logout", "-f").Run(); err != nil { + return "", err + } + if err := a.runOpts.Command("oneshot", "--username", a.runOpts.Oneshot.Username, "--paperkey", + a.runOpts.Oneshot.PaperKey).Run(); err != nil { + return "", err + } + username = a.runOpts.Oneshot.Username + return username, nil + } + return "", errors.New("unable to auth") +} + +func (a *API) startPipes() (err error) { + a.Lock() + defer a.Unlock() + if a.apiCmd != nil { + a.apiCmd.Process.Kill() + } + a.apiCmd = nil + + if a.runOpts.StartService { + a.runOpts.Command("service").Start() + } + + if a.username, err = a.auth(); err != nil { + return err + } + a.apiCmd = a.runOpts.Command("chat", "api") + if a.apiInput, err = a.apiCmd.StdinPipe(); err != nil { + return err + } + output, err := a.apiCmd.StdoutPipe() + if err != nil { + return err + } + if err := a.apiCmd.Start(); err != nil { + return err + } + a.apiOutput = bufio.NewReader(output) + return nil +} + +var errAPIDisconnected = errors.New("chat API disconnected") + +func (a *API) getAPIPipesLocked() (io.Writer, *bufio.Reader, error) { + // this should only be called inside a lock + if a.apiCmd == nil { + return nil, nil, errAPIDisconnected + } + return a.apiInput, a.apiOutput, nil +} + +// GetConversations reads all conversations from the current user's inbox. +func (a *API) GetConversations(unreadOnly bool) ([]Conversation, 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 + } + return inbox.Result.Convs, nil +} + +// GetTextMessages fetches all text messages from a given channel. Optionally can filter +// ont unread status. +func (a *API) GetTextMessages(channel Channel, unreadOnly bool) ([]Message, error) { + channelBytes, err := json.Marshal(channel) + if err != nil { + return nil, err + } + apiInput := fmt.Sprintf(`{"method": "read", "params": {"options": {"channel": %s}}}`, string(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: %s", err.Error()) + } + + var res []Message + for _, msg := range thread.Result.Messages { + if msg.Msg.Content.Type == "text" { + res = append(res, msg.Msg) + } + } + + return res, nil +} + +type sendMessageBody struct { + Body string +} + +type sendMessageOptions struct { + Channel Channel `json:"channel,omitempty"` + ConversationID string `json:"conversation_id,omitempty"` + Message sendMessageBody `json:",omitempty"` + Filename string `json:"filename,omitempty"` + Title string `json:"title,omitempty"` + MsgID int `json:"message_id,omitempty"` +} + +type sendMessageParams struct { + Options sendMessageOptions +} + +type sendMessageArg struct { + Method string + Params sendMessageParams +} + +func (a *API) doSend(arg interface{}) (response SendResponse, err error) { + a.Lock() + defer a.Unlock() + + bArg, err := json.Marshal(arg) + if err != nil { + return SendResponse{}, err + } + input, output, err := a.getAPIPipesLocked() + if err != nil { + return SendResponse{}, err + } + if _, err := io.WriteString(input, string(bArg)); err != nil { + return SendResponse{}, err + } + responseRaw, err := output.ReadBytes('\n') + if err != nil { + return SendResponse{}, err + } + if err := json.Unmarshal(responseRaw, &response); err != nil { + return SendResponse{}, fmt.Errorf("failed to decode API response: %s", err) + } + return response, nil +} + +func (a *API) doFetch(apiInput string) ([]byte, error) { + a.Lock() + defer a.Unlock() + + input, output, err := a.getAPIPipesLocked() + if err != nil { + return nil, err + } + if _, err := io.WriteString(input, apiInput); err != nil { + return nil, err + } + byteOutput, err := output.ReadBytes('\n') + if err != nil { + return nil, err + } + + return byteOutput, nil +} + +func (a *API) SendMessage(channel Channel, body string) (SendResponse, error) { + arg := sendMessageArg{ + Method: "send", + Params: sendMessageParams{ + Options: sendMessageOptions{ + Channel: channel, + Message: sendMessageBody{ + Body: body, + }, + }, + }, + } + return a.doSend(arg) +} + +func (a *API) SendMessageByConvID(convID string, body string) (SendResponse, error) { + arg := sendMessageArg{ + Method: "send", + Params: sendMessageParams{ + Options: sendMessageOptions{ + ConversationID: convID, + Message: sendMessageBody{ + Body: body, + }, + }, + }, + } + return a.doSend(arg) +} + +// SendMessageByTlfName sends a message on the given TLF name +func (a *API) SendMessageByTlfName(tlfName string, body string) (SendResponse, error) { + arg := sendMessageArg{ + Method: "send", + Params: sendMessageParams{ + Options: sendMessageOptions{ + Channel: Channel{ + Name: tlfName, + }, + Message: sendMessageBody{ + Body: body, + }, + }, + }, + } + return a.doSend(arg) +} + +func (a *API) SendMessageByTeamName(teamName string, body string, inChannel *string) (SendResponse, error) { + channel := "general" + if inChannel != nil { + channel = *inChannel + } + arg := sendMessageArg{ + Method: "send", + Params: sendMessageParams{ + Options: sendMessageOptions{ + Channel: Channel{ + MembersType: "team", + Name: teamName, + TopicName: channel, + }, + Message: sendMessageBody{ + Body: body, + }, + }, + }, + } + return a.doSend(arg) +} + +func (a *API) SendAttachmentByTeam(teamName string, filename string, title string, inChannel *string) (SendResponse, error) { + channel := "general" + if inChannel != nil { + channel = *inChannel + } + arg := sendMessageArg{ + Method: "attach", + Params: sendMessageParams{ + Options: sendMessageOptions{ + Channel: Channel{ + MembersType: "team", + Name: teamName, + TopicName: channel, + }, + Filename: filename, + Title: title, + }, + }, + } + return a.doSend(arg) +} + +type reactionOptions struct { + ConversationID string `json:"conversation_id"` + Message sendMessageBody + MsgID int `json:"message_id"` + Channel Channel `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 Channel, msgID int, reaction string) (SendResponse, error) { + arg := newReactionArg(reactionOptions{ + Message: sendMessageBody{Body: reaction}, + MsgID: msgID, + Channel: channel, + }) + return a.doSend(arg) +} + +func (a *API) ReactByConvID(convID string, msgID int, reaction string) (SendResponse, error) { + arg := newReactionArg(reactionOptions{ + Message: sendMessageBody{Body: reaction}, + MsgID: msgID, + ConversationID: convID, + }) + return a.doSend(arg) +} + +type advertiseParams struct { + Options Advertisement +} + +type advertiseMsgArg struct { + Method string + Params advertiseParams +} + +func newAdvertiseMsgArg(ad Advertisement) advertiseMsgArg { + return advertiseMsgArg{ + Method: "advertisecommands", + Params: advertiseParams{ + Options: ad, + }, + } +} + +func (a *API) AdvertiseCommands(ad Advertisement) (SendResponse, error) { + return a.doSend(newAdvertiseMsgArg(ad)) +} + +func (a *API) Username() string { + return a.username +} + +// SubscriptionMessage contains a message and conversation object +type SubscriptionMessage struct { + Message Message + Conversation Conversation +} + +type SubscriptionWalletEvent struct { + Payment Payment +} + +// NewSubscription has methods to control the background message fetcher loop +type NewSubscription struct { + newMsgsCh <-chan SubscriptionMessage + newWalletCh <-chan SubscriptionWalletEvent + errorCh <-chan error + shutdownCh chan struct{} +} + +// Read blocks until a new message arrives +func (m NewSubscription) Read() (SubscriptionMessage, error) { + select { + case msg := <-m.newMsgsCh: + return msg, nil + case err := <-m.errorCh: + return SubscriptionMessage{}, err + } +} + +// Read blocks until a new message arrives +func (m NewSubscription) ReadWallet() (SubscriptionWalletEvent, error) { + select { + case msg := <-m.newWalletCh: + return msg, nil + case err := <-m.errorCh: + return SubscriptionWalletEvent{}, err + } +} + +// Shutdown terminates the background process +func (m NewSubscription) Shutdown() { + m.shutdownCh <- struct{}{} +} + +type ListenOptions struct { + Wallet bool +} + +// ListenForNewTextMessages proxies to Listen without wallet events +func (a *API) ListenForNewTextMessages() (NewSubscription, error) { + opts := ListenOptions{Wallet: false} + return a.Listen(opts) +} + +// Listen fires of a background loop and puts chat messages and wallet +// events into channels +func (a *API) Listen(opts ListenOptions) (NewSubscription, error) { + newMsgCh := make(chan SubscriptionMessage, 100) + newWalletCh := make(chan SubscriptionWalletEvent, 100) + errorCh := make(chan error, 100) + shutdownCh := make(chan struct{}) + done := make(chan struct{}) + + sub := NewSubscription{ + newMsgsCh: newMsgCh, + newWalletCh: newWalletCh, + shutdownCh: shutdownCh, + errorCh: errorCh, + } + pause := 2 * time.Second + readScanner := func(boutput *bufio.Scanner) { + for { + boutput.Scan() + t := boutput.Text() + var typeHolder TypeHolder + if err := json.Unmarshal([]byte(t), &typeHolder); err != nil { + errorCh <- err + break + } + switch typeHolder.Type { + case "chat": + var holder MessageHolder + if err := json.Unmarshal([]byte(t), &holder); err != nil { + errorCh <- err + break + } + subscriptionMessage := SubscriptionMessage{ + Message: holder.Msg, + Conversation: Conversation{ + ID: holder.Msg.ConversationID, + Channel: holder.Msg.Channel, + }, + } + newMsgCh <- subscriptionMessage + case "wallet": + var holder PaymentHolder + if err := json.Unmarshal([]byte(t), &holder); err != nil { + errorCh <- err + break + } + subscriptionPayment := SubscriptionWalletEvent{ + Payment: holder.Payment, + } + newWalletCh <- subscriptionPayment + default: + continue + } + } + done <- struct{}{} + } + + attempts := 0 + maxAttempts := 1800 + go func() { + for { + if attempts >= maxAttempts { + panic("Listen: failed to auth, giving up") + } + attempts++ + if _, err := a.auth(); err != nil { + log.Printf("Listen: failed to auth: %s", err) + time.Sleep(pause) + continue + } + cmdElements := []string{"chat", "api-listen"} + if opts.Wallet { + cmdElements = append(cmdElements, "--wallet") + } + p := a.runOpts.Command(cmdElements...) + output, err := p.StdoutPipe() + if err != nil { + log.Printf("Listen: failed to listen: %s", err) + time.Sleep(pause) + continue + } + boutput := bufio.NewScanner(output) + if err := p.Start(); err != nil { + log.Printf("Listen: failed to make listen scanner: %s", err) + time.Sleep(pause) + continue + } + attempts = 0 + go readScanner(boutput) + <-done + p.Wait() + time.Sleep(pause) + } + }() + return sub, nil +} + +func (a *API) GetUsername() string { + return a.username +} + +func (a *API) ListChannels(teamName string) ([]string, error) { + apiInput := fmt.Sprintf(`{"method": "listconvsonname", "params": {"options": {"topic_type": "CHAT", "members_type": "team", "name": "%s"}}}`, teamName) + 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 + } + + 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) (JoinChannelResult, error) { + empty := JoinChannelResult{} + + apiInput := fmt.Sprintf(`{"method": "join", "params": {"options": {"channel": {"name": "%s", "members_type": "team", "topic_name": "%s"}}}}`, teamName, channelName) + 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) + } + if joinChannel.Error.Message != "" { + return empty, fmt.Errorf("received error from keybase team api: %s", joinChannel.Error.Message) + } + + return joinChannel.Result, nil +} + +func (a *API) LeaveChannel(teamName string, channelName string) (LeaveChannelResult, error) { + empty := LeaveChannelResult{} + + apiInput := fmt.Sprintf(`{"method": "leave", "params": {"options": {"channel": {"name": "%s", "members_type": "team", "topic_name": "%s"}}}}`, teamName, channelName) + 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) + } + if leaveChannel.Error.Message != "" { + return empty, fmt.Errorf("received error from keybase team api: %s", leaveChannel.Error.Message) + } + + return leaveChannel.Result, nil +} + +func (a *API) LogSend(feedback string) error { + feedback = "go-keybase-chat-bot log send\n" + + "username: " + a.GetUsername() + "\n" + + feedback + + args := []string{ + "log", "send", + "--no-confirm", + "--feedback", feedback, + } + + // We're determining whether the service is already running by running status + // with autofork disabled. + if err := a.runOpts.Command("--no-auto-fork", "status"); err != nil { + // Assume that there's no service running, so log send as standalone + args = append([]string{"--standalone"}, args...) + } + + return a.runOpts.Command(args...).Run() +} + +func (a *API) Shutdown() error { + if a.runOpts.Oneshot != nil { + err := a.runOpts.Command("logout", "--force").Run() + if err != nil { + return err + } + } + + if a.runOpts.StartService { + err := a.runOpts.Command("ctl", "stop", "--shutdown").Run() + if err != nil { + return err + } + } + + return nil +} diff --git a/vendor/github.com/keybase/go-keybase-chat-bot/kbchat/team.go b/vendor/github.com/keybase/go-keybase-chat-bot/kbchat/team.go new file mode 100644 index 00000000..89c55c4f --- /dev/null +++ b/vendor/github.com/keybase/go-keybase-chat-bot/kbchat/team.go @@ -0,0 +1,89 @@ +package kbchat + +import ( + "encoding/json" + "fmt" + "strings" +) + +type ListTeamMembers struct { + Result ListTeamMembersResult `json:"result"` + Error Error `json:"error"` +} + +type ListTeamMembersResult struct { + Members ListTeamMembersResultMembers `json:"members"` +} + +type ListTeamMembersResultMembers struct { + Owners []ListMembersOutputMembersCategory `json:"owners"` + Admins []ListMembersOutputMembersCategory `json:"admins"` + Writers []ListMembersOutputMembersCategory `json:"writers"` + Readers []ListMembersOutputMembersCategory `json:"readers"` +} + +type ListMembersOutputMembersCategory struct { + Username string `json:"username"` + FullName string `json:"fullName"` +} + +type ListUserMemberships struct { + Result ListUserMembershipsResult `json:"result"` + Error Error `json:"error"` +} + +type ListUserMembershipsResult struct { + Teams []ListUserMembershipsResultTeam `json:"teams"` +} + +type ListUserMembershipsResultTeam struct { + TeamName string `json:"fq_name"` + IsImplicitTeam bool `json:"is_implicit_team"` + IsOpenTeam bool `json:"is_open_team"` + Role int `json:"role"` + MemberCount int `json:"member_count"` +} + +func (a *API) ListMembersOfTeam(teamName string) (ListTeamMembersResultMembers, error) { + empty := ListTeamMembersResultMembers{} + + apiInput := fmt.Sprintf(`{"method": "list-team-memberships", "params": {"options": {"team": "%s"}}}`, teamName) + cmd := a.runOpts.Command("team", "api") + cmd.Stdin = strings.NewReader(apiInput) + bytes, err := cmd.CombinedOutput() + if err != nil { + return empty, fmt.Errorf("failed to call keybase team api: %v", err) + } + + members := ListTeamMembers{} + err = json.Unmarshal(bytes, &members) + if err != nil { + return empty, fmt.Errorf("failed to parse output from keybase team api: %v", err) + } + if members.Error.Message != "" { + return empty, fmt.Errorf("received error from keybase team api: %s", members.Error.Message) + } + return members.Result.Members, nil +} + +func (a *API) ListUserMemberships(username string) ([]ListUserMembershipsResultTeam, error) { + empty := []ListUserMembershipsResultTeam{} + + apiInput := fmt.Sprintf(`{"method": "list-user-memberships", "params": {"options": {"username": "%s"}}}`, username) + cmd := a.runOpts.Command("team", "api") + cmd.Stdin = strings.NewReader(apiInput) + bytes, err := cmd.CombinedOutput() + if err != nil { + return empty, fmt.Errorf("failed to call keybase team api: %v", err) + } + + members := ListUserMemberships{} + err = json.Unmarshal(bytes, &members) + if err != nil { + return empty, fmt.Errorf("failed to parse output from keybase team api: %v", err) + } + if members.Error.Message != "" { + return empty, fmt.Errorf("received error from keybase team api: %s", members.Error.Message) + } + return members.Result.Teams, nil +} diff --git a/vendor/github.com/keybase/go-keybase-chat-bot/kbchat/test_config.example.yaml b/vendor/github.com/keybase/go-keybase-chat-bot/kbchat/test_config.example.yaml new file mode 100644 index 00000000..87078ed1 --- /dev/null +++ b/vendor/github.com/keybase/go-keybase-chat-bot/kbchat/test_config.example.yaml @@ -0,0 +1,16 @@ +# Rename this file to `test_config.yaml` + +config: + bots: + alice: + username: "alice" + paperkey: "foo bar car..." + bob: + username: "bob" + paperkey: "one two three four..." + teams: + acme: + # A real team that you add your alice1 and bob1 into + name: "acme" + # The channel to use + topicname: "mysupercoolchannel" diff --git a/vendor/github.com/keybase/go-keybase-chat-bot/kbchat/test_utils.go b/vendor/github.com/keybase/go-keybase-chat-bot/kbchat/test_utils.go new file mode 100644 index 00000000..1a163951 --- /dev/null +++ b/vendor/github.com/keybase/go-keybase-chat-bot/kbchat/test_utils.go @@ -0,0 +1,54 @@ +package kbchat + +import ( + "crypto/rand" + "encoding/hex" + "io/ioutil" + "os" + "os/exec" + "path" + "strings" + "testing" + + "github.com/stretchr/testify/require" +) + +func randomString(t *testing.T) string { + bytes := make([]byte, 16) + _, err := rand.Read(bytes) + require.NoError(t, err) + return hex.EncodeToString(bytes) +} + +func randomTempDir(t *testing.T) string { + return path.Join(os.TempDir(), "keybase_bot_"+randomString(t)) +} + +func whichKeybase(t *testing.T) string { + cmd := exec.Command("which", "keybase") + out, err := cmd.Output() + require.NoError(t, err) + location := strings.TrimSpace(string(out)) + return location +} + +func copyFile(t *testing.T, source, dest string) { + sourceData, err := ioutil.ReadFile(source) + require.NoError(t, err) + err = ioutil.WriteFile(dest, sourceData, 0777) + require.NoError(t, err) +} + +// Creates the working directory and copies over the keybase binary in PATH. +// We do this to avoid any version mismatch issues. +func prepWorkingDir(t *testing.T, workingDir string) string { + kbLocation := whichKeybase(t) + + err := os.Mkdir(workingDir, 0777) + require.NoError(t, err) + kbDestination := path.Join(workingDir, "keybase") + + copyFile(t, kbLocation, kbDestination) + + return kbDestination +} diff --git a/vendor/github.com/keybase/go-keybase-chat-bot/kbchat/types.go b/vendor/github.com/keybase/go-keybase-chat-bot/kbchat/types.go new file mode 100644 index 00000000..74a81646 --- /dev/null +++ b/vendor/github.com/keybase/go-keybase-chat-bot/kbchat/types.go @@ -0,0 +1,159 @@ +package kbchat + +type Sender struct { + Uid string `json:"uid"` + Username string `json:"username"` + DeviceID string `json:"device_id"` + DeviceName string `json:"device_name"` +} + +type Channel struct { + Name string `json:"name"` + Public bool `json:"public"` + TopicType string `json:"topic_type"` + TopicName string `json:"topic_name"` + MembersType string `json:"members_type"` +} + +type Conversation struct { + ID string `json:"id"` + Unread bool `json:"unread"` + Channel Channel `json:"channel"` +} + +type PaymentHolder struct { + Payment Payment `json:"notification"` +} + +type Payment struct { + TxID string `json:"txID"` + StatusDescription string `json:"statusDescription"` + FromAccountID string `json:"fromAccountID"` + FromUsername string `json:"fromUsername"` + ToAccountID string `json:"toAccountID"` + ToUsername string `json:"toUsername"` + AmountDescription string `json:"amountDescription"` + WorthAtSendTime string `json:"worthAtSendTime"` + ExternalTxURL string `json:"externalTxURL"` +} + +type Result struct { + Convs []Conversation `json:"conversations"` +} + +type Inbox struct { + Result Result `json:"result"` +} + +type ChannelsList struct { + Result Result `json:"result"` +} + +type MsgPaymentDetails struct { + ResultType int `json:"resultTyp"` // 0 good. 1 error + PaymentID string `json:"sent"` +} + +type MsgPayment struct { + Username string `json:"username"` + PaymentText string `json:"paymentText"` + Details MsgPaymentDetails `json:"result"` +} + +type Text struct { + Body string `json:"body"` + Payments []MsgPayment `json:"payments"` + ReplyTo int `json:"replyTo"` +} + +type Content struct { + Type string `json:"type"` + Text Text `json:"text"` +} + +type Message struct { + Content Content `json:"content"` + Sender Sender `json:"sender"` + Channel Channel `json:"channel"` + ConversationID string `json:"conversation_id"` + MsgID int `json:"id"` +} + +type SendResult struct { + MsgID int `json:"id"` +} + +type SendResponse struct { + Result SendResult `json:"result"` +} + +type TypeHolder struct { + Type string `json:"type"` +} + +type MessageHolder struct { + Msg Message `json:"msg"` + Source string `json:"source"` +} + +type ThreadResult struct { + Messages []MessageHolder `json:"messages"` +} + +type Thread struct { + Result ThreadResult `json:"result"` +} + +type CommandExtendedDescription struct { + Title string `json:"title"` + DesktopBody string `json:"desktop_body"` + MobileBody string `json:"mobile_body"` +} + +type Command struct { + Name string `json:"name"` + Description string `json:"description"` + Usage string `json:"usage"` + ExtendedDescription *CommandExtendedDescription `json:"extended_description,omitempty"` +} + +type CommandsAdvertisement struct { + Typ string `json:"type"` + Commands []Command + TeamName string `json:"team_name,omitempty"` +} + +type Advertisement struct { + Alias string `json:"alias,omitempty"` + Advertisements []CommandsAdvertisement +} + +type Error struct { + Code int `json:"code"` + Message string `json:"message"` +} + +type JoinChannel struct { + Error Error `json:"error"` + Result JoinChannelResult `json:"result"` +} + +type JoinChannelResult struct { + RateLimit []RateLimit `json:"ratelimits"` +} + +type LeaveChannel struct { + Error Error `json:"error"` + Result LeaveChannelResult `json:"result"` +} + +type LeaveChannelResult struct { + RateLimit []RateLimit `json:"ratelimits"` +} + +type RateLimit struct { + Tank string `json:"tank"` + Capacity int `json:"capacity"` + Reset int `json:"reset"` + Gas int `json:"gas"` +} diff --git a/vendor/github.com/keybase/go-keybase-chat-bot/kbchat/wallet.go b/vendor/github.com/keybase/go-keybase-chat-bot/kbchat/wallet.go new file mode 100644 index 00000000..7dfdab68 --- /dev/null +++ b/vendor/github.com/keybase/go-keybase-chat-bot/kbchat/wallet.go @@ -0,0 +1,48 @@ +package kbchat + +import ( + "bytes" + "encoding/json" + "fmt" + "strings" +) + +type WalletOutput struct { + Result WalletResult `json:"result"` +} + +type WalletResult struct { + TxID string `json:"txID"` + Status string `json:"status"` + Amount string `json:"amount"` + Asset WalletAsset `json:"asset"` + FromUsername string `json:"fromUsername"` + ToUsername string `json:"toUsername"` +} + +type WalletAsset struct { + Type string `json:"type"` + Code string `json:"code"` + Issuer string `json:"issuer"` +} + +func (a *API) GetWalletTxDetails(txID string) (wOut WalletOutput, err error) { + a.Lock() + defer a.Unlock() + + apiInput := fmt.Sprintf(`{"method": "details", "params": {"options": {"txid": "%s"}}}`, txID) + cmd := a.runOpts.Command("wallet", "api") + cmd.Stdin = strings.NewReader(apiInput) + var out bytes.Buffer + cmd.Stdout = &out + err = cmd.Run() + if err != nil { + return wOut, err + } + + if err := json.Unmarshal(out.Bytes(), &wOut); err != nil { + return wOut, fmt.Errorf("unable to decode wallet output: %s", err.Error()) + } + + return wOut, nil +} -- cgit v1.2.3