diff --git a/libkeybase.go b/libkeybase.go index 88aed9c..47ce530 100644 --- a/libkeybase.go +++ b/libkeybase.go @@ -2,29 +2,27 @@ package libkeybase import ( "bufio" - "bytes" + "context" + "encoding/json" "fmt" "os/exec" - "sync" - "time" ) -// RunOptions holds... run... options... -type RunOptions struct { - KeybaseLoction string - HomeDir string - Username string - PaperKey string - EnableTyping bool - BotLiteMode bool - ChannelCapacity int +// Options holds... run... options... +type Options struct { + KeybaseLoction string + HomeDir string + Username string + PaperKey string + EnableTyping bool + BotLiteMode bool } -// Location attempts to find the location of the keybase binary in the following order: +// 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 RunOptions) Location() string { +func (r *Options) locateKeybase() string { if r.KeybaseLoction != "" { return r.KeybaseLoction } @@ -37,7 +35,7 @@ func (r RunOptions) Location() string { } // buildBaseCommand adds the homedirectory before the args, when required -func (r RunOptions) buildBaseCommand(args ...string) []string { +func (r *Options) buildBaseCommand(args ...string) []string { var cmd []string if r.HomeDir != "" { cmd = append(cmd, "--home", r.HomeDir) @@ -46,169 +44,131 @@ func (r RunOptions) buildBaseCommand(args ...string) []string { return cmd } -// apiPrimitive is re-used between ApiWriter and ApiReader -type apiPrimitive struct { - sync.Mutex - cmd *exec.Cmd - opts RunOptions - Input chan string - Output chan string - stop bool - running bool - die chan bool +type ChatAPI struct { + opts *Options + ctx context.Context + Capacity int + Reset int + Gas int } -// SetOptions sets the primitve's options -func (a *apiPrimitive) SetOptions(arg RunOptions) { - a.opts = arg +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"` } -// Stop halts the subprocess -func (a *apiPrimitive) Stop() { - a.cmd.Process.Kill() - a.stop = true - a.die <- true - a.running = false +func NewApiWithContext(ctx context.Context, options *Options) *ChatAPI { + return &ChatAPI{ + opts: options, + ctx: ctx, + } } -// _start begins the subprocess call -func (a *apiPrimitive) _start(args ...string) (err error) { - // build the base command (homedir and stuff) and add the args to it +func (a *ChatAPI) Start() (in, out chan string, err chan error) { + in = make(chan string, 10) + out = make(chan string, 10) + err = make(chan error, 10) + a._run(in, out, err) + return +} + +func (a *ChatAPI) _run(in, out chan string, e chan error, args ...string) { + // build the base command, add homedir and args cmdStrings := a.opts.buildBaseCommand(args...) - // set up the command execution - a.cmd = exec.Command(a.opts.Location(), cmdStrings...) - // grab the process stdin pipe - keyIn, err := a.cmd.StdinPipe() + + // set up the command + cmd_reader := exec.CommandContext(a.ctx, a.opts.locateKeybase(), cmdStrings...) + cmd_writer := exec.CommandContext(a.ctx, a.opts.locateKeybase(), cmdStrings...) + + // grab the process stdin and stdout pipes + keyOut, err := cmd_reader.StdoutPipe() + if err != nil { + e <- err + return + } + keyIn, err := cmd_writer.StdinPipe() if err != nil { + e <- err return } - // grab the process stdout pipe - keyOut, err := a.cmd.StdoutPipe() + keyResp, err := cmd_writer.StdoutPipe() if err != nil { + e <- err return } - // buffer the I/O channels - a.die = make(chan bool, 1) - a.Input = make(chan string, a.opts.ChannelCapacity) - a.Output = make(chan string, a.opts.ChannelCapacity) - // now we need to start a select where anything in the channel goes to stdin + + // now start read and write goroutines + // reader go func() { + r := bufio.NewReader(keyOut) for { select { - case msg := <-a.Input: - _, err = keyIn.Write([]byte(msg)) + case <-a.ctx.Done(): + return + default: + line, err := r.ReadString('\n') if err != nil { - return + e <- err + } else { + out <- line } - case <-a.die: - return } } }() - // then we need to start a loop where anything in the output goes to the channel + + // writer go func() { - r := bufio.NewReader(keyOut) - buf := bytes.NewBufferString("") - for !a.stop { - line, isPrefix, err := r.ReadLine() - if err != nil { + for { + select { + case msg := <-in: + _, err := keyIn.Write([]byte(msg)) + if err != nil { + e <- err + } + case <-a.ctx.Done(): return } - if isPrefix { - // if its not a complete line, write it to the buffer only so we loop again - buf.Write(line) - } else { - // if the line is complete, write it to the buffer - buf.Write(line) - // then write the entire buffer to the channel - a.Output <- buf.String() - // then reset the buffer so it can be re-used - buf.Reset() - } } }() - // if you've made it this far, return the IO channels - a.cmd.Start() - a.running = true - return nil -} - -type apiWriter struct { - apiPrimitive -} - -func (a *apiWriter) start() (err error) { - err = a._start("chat", "api") - return -} - -func (a *apiWriter) write(in string) (out string, err error) { - if !a.running { - return "", fmt.Errorf("api is not running") - } - // write the input - a.Input <- in - // now wait for the output - out = <-a.Output - return -} - -type apiReader struct { - apiPrimitive -} -func (a *apiReader) start() (err error) { - err = a._start("chat", "api-listen") - return -} - -func (a *apiReader) listen() (out chan string, err error) { - if !a.running { - err = fmt.Errorf("apiReader is not running") - } - out = a.Output - return -} - -// API is the basic primitive of the API, holding the actual application calls and pipes -type API struct { - writer apiWriter - reader apiReader - Timeout time.Duration -} - -func (a *API) Start() error { - err := a.reader.start() - if err != nil { - return err - } - err = a.writer.start() - if err != nil { - return err - } - return nil -} - -func (a *API) Stop() { - a.writer.Stop() - a.reader.Stop() -} - -func (a *API) Write(in string) (out string, err error) { - a.writer.Lock() - out, err = a.writer.write(in) - a.writer.Unlock() - return -} - -func (a *API) Listen() (out chan string, err error) { - out, err = a.reader.listen() - return -} + // writer responses to fill up gas limits + go func() { + r := bufio.NewReader(keyResp) + for { + select { + case <-a.ctx.Done(): + return + default: + line, err := r.ReadBytes('\n') + if err != nil { + e <- err + } + var resp *KeybaseApiResponse + err = json.Unmarshal(line, resp) + if err != nil { + e <- err + } else { + for _, limit := range resp.Result.Ratelimits { + if limit.Tank == "chat" { + a.Capacity = limit.Capacity + a.Gas = limit.Gas + a.Reset = limit.Reset + } + } + } + } + } + }() -func NewAPI(arg RunOptions) *API { - var result API - result.reader.SetOptions(arg) - result.writer.SetOptions(arg) - return &result + // now start the process + cmd_reader.Start() + cmd_writer.Start() }