Browse Source

first proposal for chat

main
David Haukeness 2 years ago
parent
commit
6bf533c15a
  1. 264
      libkeybase.go

264
libkeybase.go

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

Loading…
Cancel
Save