David Haukeness
2 years ago
6 changed files with 242 additions and 253 deletions
@ -0,0 +1,112 @@ |
|||||||
|
package keybase |
||||||
|
|
||||||
|
import ( |
||||||
|
"bufio" |
||||||
|
"context" |
||||||
|
"fmt" |
||||||
|
"sync" |
||||||
|
|
||||||
|
"git.hugfreevikings.wtf/keybase/keybase/pkg/ctxreader" |
||||||
|
) |
||||||
|
|
||||||
|
type ChatAPI struct { |
||||||
|
sync.Mutex |
||||||
|
toStdin chan []byte |
||||||
|
fromStdout chan []byte |
||||||
|
fromListen chan []byte |
||||||
|
errors chan error |
||||||
|
} |
||||||
|
|
||||||
|
func NewChatAPI(ctx context.Context, opts *Options) (api *ChatAPI, err error) { |
||||||
|
// set up the channels
|
||||||
|
api.toStdin = make(chan []byte, opts.ChannelBufferSize) |
||||||
|
api.fromStdout = make(chan []byte, opts.ChannelBufferSize) |
||||||
|
api.fromListen = make(chan []byte, opts.ChannelBufferSize) |
||||||
|
api.errors = make(chan error, opts.ChannelBufferSize) |
||||||
|
|
||||||
|
// get the basics
|
||||||
|
kbCmd := opts.locateKeybase() |
||||||
|
chatApiArgs := opts.buildArgs("chat", "api") |
||||||
|
chatApiListenArgs := opts.buildArgs("chat", "api-listen") |
||||||
|
|
||||||
|
// build the commands
|
||||||
|
chatApi, err := newApiCmd(ctx, kbCmd, chatApiArgs...) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
chatListen, err := newApiCmd(ctx, kbCmd, chatApiListenArgs...) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
// create the goroutines
|
||||||
|
// listen reader
|
||||||
|
go func() { |
||||||
|
cr := ctxreader.NewContextReader(ctx, chatListen.Stdout()) |
||||||
|
scanner := bufio.NewScanner(cr) |
||||||
|
for scanner.Scan() { |
||||||
|
api.fromListen <- scanner.Bytes() |
||||||
|
} |
||||||
|
}() |
||||||
|
|
||||||
|
// writing to stdin
|
||||||
|
go func() { |
||||||
|
for { |
||||||
|
select { |
||||||
|
case msg := <-api.toStdin: |
||||||
|
_, err := chatApi.Stdin().Write(msg) |
||||||
|
if err != nil { |
||||||
|
api.errors <- err |
||||||
|
} |
||||||
|
case <-ctx.Done(): |
||||||
|
return |
||||||
|
} |
||||||
|
} |
||||||
|
}() |
||||||
|
|
||||||
|
// reading from stdout
|
||||||
|
go func() { |
||||||
|
cr := ctxreader.NewContextReader(ctx, chatApi.Stdout()) |
||||||
|
scanner := bufio.NewScanner(cr) |
||||||
|
for scanner.Scan() { |
||||||
|
api.fromStdout <- scanner.Bytes() |
||||||
|
} |
||||||
|
}() |
||||||
|
|
||||||
|
// reading from stderr
|
||||||
|
go func() { |
||||||
|
cr := ctxreader.NewContextReader(ctx, chatApi.Stderr()) |
||||||
|
scanner := bufio.NewScanner(cr) |
||||||
|
for scanner.Scan() { |
||||||
|
api.errors <- fmt.Errorf("%v", scanner.Text()) |
||||||
|
} |
||||||
|
}() |
||||||
|
|
||||||
|
// then start the cmds
|
||||||
|
err = chatApi.Start() |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
err = chatListen.Start() |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
func (c *ChatAPI) Listen() chan []byte { |
||||||
|
return c.fromListen |
||||||
|
} |
||||||
|
|
||||||
|
func (c *ChatAPI) SendRaw(msg []byte) ([]byte, error) { |
||||||
|
c.Lock() |
||||||
|
defer c.Unlock() |
||||||
|
c.toStdin <- msg |
||||||
|
select { |
||||||
|
case resp := <-c.fromStdout: |
||||||
|
return resp, nil |
||||||
|
case err := <-c.errors: |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,83 @@ |
|||||||
|
package keybase |
||||||
|
|
||||||
|
import ( |
||||||
|
"context" |
||||||
|
"fmt" |
||||||
|
"io" |
||||||
|
"os/exec" |
||||||
|
) |
||||||
|
|
||||||
|
// apiCmd holds the pipes that connect to an executed command
|
||||||
|
type apiCmd struct { |
||||||
|
Stderr io.ReadCloser |
||||||
|
Stdin io.WriteCloser |
||||||
|
Stdout io.ReadCloser |
||||||
|
startFunc func() error |
||||||
|
} |
||||||
|
|
||||||
|
type cmd interface { |
||||||
|
Start() error |
||||||
|
Stdin() io.WriteCloser |
||||||
|
Stdout() io.ReadCloser |
||||||
|
Stderr() io.ReadCloser |
||||||
|
} |
||||||
|
|
||||||
|
type defaultCmd struct { |
||||||
|
apiCmd *apiCmd |
||||||
|
} |
||||||
|
|
||||||
|
func (c *defaultCmd) Start() error { |
||||||
|
return c.apiCmd.startFunc() |
||||||
|
} |
||||||
|
|
||||||
|
func (c *defaultCmd) Stdin() io.WriteCloser { |
||||||
|
return c.apiCmd.Stdin |
||||||
|
} |
||||||
|
|
||||||
|
func (c *defaultCmd) Stdout() io.ReadCloser { |
||||||
|
return c.apiCmd.Stdout |
||||||
|
} |
||||||
|
|
||||||
|
func (c *defaultCmd) Stderr() io.ReadCloser { |
||||||
|
return c.apiCmd.Stderr |
||||||
|
} |
||||||
|
|
||||||
|
var _ cmd = &defaultCmd{} |
||||||
|
|
||||||
|
func newApiCmd(ctx context.Context, execCmd string, args ...string) (cmd, error) { |
||||||
|
var err error |
||||||
|
|
||||||
|
// create a new apiCmd
|
||||||
|
cmd := &apiCmd{} |
||||||
|
|
||||||
|
// set up the command
|
||||||
|
api := exec.CommandContext(ctx, execCmd, args...) |
||||||
|
|
||||||
|
// an empty start func for failures
|
||||||
|
cmd.startFunc = func() error { return nil } |
||||||
|
|
||||||
|
// hook up the stderr pipe
|
||||||
|
if cmd.Stderr, err = api.StderrPipe(); err != nil { |
||||||
|
return nil, fmt.Errorf("failed to get pipe: %v", err) |
||||||
|
} |
||||||
|
|
||||||
|
// hook up the stdout pipe
|
||||||
|
if cmd.Stdout, err = api.StdoutPipe(); err != nil { |
||||||
|
return nil, fmt.Errorf("failed to get pipe: %v", err) |
||||||
|
} |
||||||
|
|
||||||
|
// hook up the stdin pipe
|
||||||
|
if cmd.Stdin, err = api.StdinPipe(); err != nil { |
||||||
|
return nil, fmt.Errorf("failed to get pipe: %v", err) |
||||||
|
} |
||||||
|
|
||||||
|
// create an anonymous function to start the commands
|
||||||
|
cmd.startFunc = func() error { |
||||||
|
if err := api.Start(); err != nil { |
||||||
|
return fmt.Errorf("failed to start command: %v", err) |
||||||
|
} |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
return &defaultCmd{cmd}, nil |
||||||
|
} |
@ -0,0 +1,29 @@ |
|||||||
|
package ctxreader |
||||||
|
|
||||||
|
import ( |
||||||
|
"context" |
||||||
|
"io" |
||||||
|
) |
||||||
|
|
||||||
|
type ContextReader struct { |
||||||
|
ctx context.Context |
||||||
|
r io.ReadCloser |
||||||
|
} |
||||||
|
|
||||||
|
func (cr *ContextReader) Read(p []byte) (n int, err error) { |
||||||
|
if err := cr.ctx.Err(); err != nil { |
||||||
|
return 0, err |
||||||
|
} |
||||||
|
return cr.r.Read(p) |
||||||
|
} |
||||||
|
|
||||||
|
func (cr *ContextReader) Close() error { |
||||||
|
return cr.r.Close() |
||||||
|
} |
||||||
|
|
||||||
|
func NewContextReader(ctx context.Context, r io.ReadCloser) io.ReadCloser { |
||||||
|
return &ContextReader{ |
||||||
|
ctx: ctx, |
||||||
|
r: r, |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,16 @@ |
|||||||
|
package keybase |
||||||
|
|
||||||
|
type RateLimit struct { |
||||||
|
Tank string `json:"tank"` |
||||||
|
Capacity int `json:"capacity"` |
||||||
|
Reset int `json:"reset"` |
||||||
|
Gas int `json:"gas"` |
||||||
|
} |
||||||
|
|
||||||
|
type KeybaseApiResponse struct { |
||||||
|
Result struct { |
||||||
|
Message string `json:"message"` |
||||||
|
ID int `json:"id"` |
||||||
|
RateLimits []RateLimit `json:"ratelimits"` |
||||||
|
} `json:"result"` |
||||||
|
} |
Loading…
Reference in new issue