You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
266 lines
7.1 KiB
266 lines
7.1 KiB
package keybase |
|
|
|
import ( |
|
"bufio" |
|
"context" |
|
"encoding/json" |
|
"fmt" |
|
"io" |
|
"log" |
|
"os/exec" |
|
) |
|
|
|
// Options holds... run... options... |
|
type Options struct { |
|
KeybaseLoction string // Optional, but required if keybase is not in your path |
|
HomeDir string // Only use this if you know what you're doing |
|
Username string // Your keybase username |
|
PaperKey string // Your keybase paperkey |
|
EnableTyping bool // Show others a typing notification while the bot is working on a command |
|
BotLiteMode bool // Defaults to true - only disable if you need to. |
|
} |
|
|
|
// NewOptions returns a new instance of *Options with sensible defaults |
|
func NewOptions() *Options { |
|
return &Options{ |
|
BotLiteMode: true, |
|
} |
|
} |
|
|
|
// locateKeybase attempts to find the location of the keybase binary in the following order: |
|
// 1. What the user has specified as the location [user specified] |
|
// 2. Looks up the binary location using exec.LookPath [default] |
|
// 3. Returns "keybase" and hopes its pathed on the users system [fallback] |
|
func (r *Options) locateKeybase() string { |
|
if r.KeybaseLoction != "" { |
|
return r.KeybaseLoction |
|
} |
|
path, err := exec.LookPath("keybase") |
|
if err != nil { |
|
log.Println("INFO: Could not detect keybase in path") |
|
return "keybase" |
|
} |
|
return path |
|
} |
|
|
|
// buildBaseCommand adds the homedirectory before the args, when required |
|
func (r *Options) buildBaseCommand(args ...string) []string { |
|
var cmd []string |
|
if r.HomeDir != "" { |
|
cmd = append(cmd, "--home", r.HomeDir) |
|
} |
|
if r.BotLiteMode { |
|
cmd = append(cmd, "--enable-bot-lite-mode") |
|
} |
|
cmd = append(cmd, args...) |
|
return cmd |
|
} |
|
|
|
type ChatAPI struct { |
|
opts *Options |
|
Capacity int |
|
Reset int |
|
Gas int |
|
inChan chan string |
|
outChan chan string |
|
errChan chan error |
|
} |
|
|
|
type KeybaseAPIResponse struct { |
|
Result struct { |
|
Message string `json:"message"` |
|
ID int `json:"id"` |
|
Ratelimits []struct { |
|
Tank string `json:"tank"` |
|
Capacity int `json:"capacity"` |
|
Reset int `json:"reset"` |
|
Gas int `json:"gas"` |
|
} `json:"ratelimits"` |
|
} `json:"result"` |
|
} |
|
|
|
// NewAPI returns a new instance of a ChatAPI after applying the given options |
|
func NewAPI(options *Options) *ChatAPI { |
|
return &ChatAPI{ |
|
opts: options, |
|
inChan: make(chan string, 10), |
|
outChan: make(chan string, 10), |
|
errChan: make(chan error, 10), |
|
} |
|
} |
|
|
|
// Start starts the message handlers and connects the pipes to the long-running keybase |
|
// api commands |
|
func (a *ChatAPI) Start(ctx context.Context) error { |
|
// setup api commands and start the pipes |
|
kbLoc := a.opts.locateKeybase() |
|
chatAPICmd := a.opts.buildBaseCommand("chat", "api") |
|
chatAPIListenCmd := a.opts.buildBaseCommand("chat", "api-listen") |
|
pipes, startPipes, err := getPipes(ctx, kbLoc, chatAPICmd, chatAPIListenCmd) |
|
if err != nil { |
|
return fmt.Errorf("failed to get pipes: %v", err) |
|
} |
|
a.initPipes(ctx, pipes) |
|
if err = startPipes(); err != nil { |
|
return fmt.Errorf("failed to start pipes: %v", err) |
|
} |
|
return nil |
|
} |
|
|
|
// CmdPipe holds the pipes that connect to the long-running keybase api commands |
|
type CmdPipe struct { |
|
Stderr io.ReadCloser |
|
Stdin io.WriteCloser |
|
Stdout io.ReadCloser |
|
} |
|
|
|
// Pipes is an interface that holds pointers to the necessary CmdPipes |
|
type Pipes interface { |
|
// ChatAPI returns a pointer to the ChatAPI pipe used to send outgoing requests to the |
|
// keybase chat api |
|
ChatAPI() *CmdPipe |
|
|
|
// APIListen returns a pointer to the APIListen pipe used to receive incoming messages |
|
// from the keybase chat api |
|
APIListen() *CmdPipe |
|
} |
|
|
|
// defaultPipes satisfies the Pipes interface, and provides standard CmdPipes that should |
|
// be used during normal operation |
|
type defaultPipes struct { |
|
chatAPI *CmdPipe |
|
chatAPIListen *CmdPipe |
|
} |
|
|
|
var _ Pipes = &defaultPipes{} |
|
|
|
// ChatAPI returns a pointer to the ChatAPI pipe used to send outgoing requests to the |
|
// keybase chat api |
|
func (p *defaultPipes) ChatAPI() *CmdPipe { |
|
return p.chatAPI |
|
} |
|
|
|
// APIListen returns a pointer to the APIListen pipe used to receive incoming messages from |
|
// the keybase chat api |
|
func (p *defaultPipes) APIListen() *CmdPipe { |
|
return p.chatAPIListen |
|
} |
|
|
|
// getPipes sets up and returns the default pipes |
|
func getPipes(ctx context.Context, kbLoc string, chatAPICmd, chatAPIListenCmd []string) (Pipes, func() error, error) { |
|
var err error |
|
|
|
// set up the commands |
|
chatAPI := exec.CommandContext(ctx, kbLoc, chatAPIListenCmd...) |
|
chatAPIListen := exec.CommandContext(ctx, kbLoc, chatAPICmd...) |
|
|
|
// create the pipes |
|
emptyStartFunc := func() error { return nil } |
|
chatAPIPipe := CmdPipe{} |
|
if chatAPIPipe.Stderr, err = chatAPI.StderrPipe(); err != nil { |
|
return nil, emptyStartFunc, fmt.Errorf("failed to get pipe: %v", err) |
|
} |
|
if chatAPIPipe.Stdin, err = chatAPI.StdinPipe(); err != nil { |
|
return nil, emptyStartFunc, fmt.Errorf("failed to get pipe: %v", err) |
|
} |
|
if chatAPIPipe.Stdout, err = chatAPI.StdoutPipe(); err != nil { |
|
return nil, emptyStartFunc, fmt.Errorf("failed to get pipe: %v", err) |
|
} |
|
|
|
chatAPIListenPipe := CmdPipe{} |
|
if chatAPIListenPipe.Stderr, err = chatAPIListen.StderrPipe(); err != nil { |
|
return nil, emptyStartFunc, fmt.Errorf("failed to get pipe: %v", err) |
|
} |
|
if chatAPIListenPipe.Stdin, err = chatAPIListen.StdinPipe(); err != nil { |
|
return nil, emptyStartFunc, fmt.Errorf("failed to get pipe: %v", err) |
|
} |
|
if chatAPIListenPipe.Stdout, err = chatAPIListen.StdoutPipe(); err != nil { |
|
return nil, emptyStartFunc, fmt.Errorf("failed to get pipe: %v", err) |
|
} |
|
|
|
// create an anonymous function to start the commands |
|
startFunc := func() error { |
|
if err := chatAPI.Start(); err != nil { |
|
return fmt.Errorf("failed to start chat api command: %v", err) |
|
} |
|
if err := chatAPIListen.Start(); err != nil { |
|
return fmt.Errorf("failed to start chat api listen command: %v", err) |
|
} |
|
return nil |
|
} |
|
|
|
return &defaultPipes{chatAPI: &chatAPIPipe, chatAPIListen: &chatAPIListenPipe}, startFunc, nil |
|
} |
|
|
|
// initPipes starts the goroutines that handle getting messages from the apis and sending |
|
// them into the appropriate channels |
|
func (a *ChatAPI) initPipes(ctx context.Context, pipes Pipes) { |
|
var ( |
|
keyIn = pipes.ChatAPI().Stdin |
|
keyResp = pipes.ChatAPI().Stdout |
|
keyOut = pipes.APIListen().Stdout |
|
) |
|
|
|
// now start read and write goroutines |
|
// reader |
|
go func() { |
|
r := bufio.NewReader(keyOut) |
|
for { |
|
select { |
|
case <-ctx.Done(): |
|
return |
|
default: |
|
line, err := r.ReadString('\n') |
|
if err != nil { |
|
a.errChan <- err |
|
} else { |
|
a.outChan <- line |
|
} |
|
} |
|
} |
|
}() |
|
|
|
// writer |
|
go func() { |
|
for { |
|
select { |
|
case msg := <-a.inChan: |
|
_, err := keyIn.Write([]byte(msg)) |
|
if err != nil { |
|
a.errChan <- err |
|
} |
|
case <-ctx.Done(): |
|
return |
|
} |
|
} |
|
}() |
|
|
|
// writer responses to fill up gas limits |
|
go func() { |
|
r := bufio.NewReader(keyResp) |
|
for { |
|
select { |
|
case <-ctx.Done(): |
|
return |
|
default: |
|
line, err := r.ReadBytes('\n') |
|
if err != nil { |
|
a.errChan <- err |
|
} |
|
var resp *KeybaseAPIResponse |
|
err = json.Unmarshal(line, resp) |
|
if err != nil { |
|
a.errChan <- err |
|
} else { |
|
for _, limit := range resp.Result.Ratelimits { |
|
if limit.Tank == "chat" { |
|
a.Capacity = limit.Capacity |
|
a.Gas = limit.Gas |
|
a.Reset = limit.Reset |
|
} |
|
} |
|
} |
|
} |
|
} |
|
}() |
|
}
|
|
|