|
|
|
@ -2,29 +2,27 @@ package libkeybase
@@ -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 {
@@ -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 {
@@ -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() |
|
|
|
|
} |
|
|
|
|