4
0
mirror of https://github.com/cwinfo/matterbridge.git synced 2025-07-03 09:37:44 +00:00

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
This commit is contained in:
cori hudson
2019-08-26 15:00:31 -04:00
committed by Wim
parent 79a006c8de
commit 921f2dfcdf
152 changed files with 5407 additions and 565 deletions

View File

@ -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
}

View File

@ -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
}

View File

@ -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"

View File

@ -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
}

View File

@ -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"`
}

View File

@ -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
}