summaryrefslogtreecommitdiffstats
path: root/vendor/github.com/keybase
diff options
context:
space:
mode:
Diffstat (limited to 'vendor/github.com/keybase')
-rw-r--r--vendor/github.com/keybase/go-keybase-chat-bot/LICENSE27
-rw-r--r--vendor/github.com/keybase/go-keybase-chat-bot/kbchat/kbchat.go693
-rw-r--r--vendor/github.com/keybase/go-keybase-chat-bot/kbchat/team.go89
-rw-r--r--vendor/github.com/keybase/go-keybase-chat-bot/kbchat/test_config.example.yaml16
-rw-r--r--vendor/github.com/keybase/go-keybase-chat-bot/kbchat/test_utils.go54
-rw-r--r--vendor/github.com/keybase/go-keybase-chat-bot/kbchat/types.go159
-rw-r--r--vendor/github.com/keybase/go-keybase-chat-bot/kbchat/wallet.go48
7 files changed, 1086 insertions, 0 deletions
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
+}