diff --git a/README.md b/README.md index e75a556..8399ac8 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,7 @@ It features: - Support for Slack, Telegram and IRC - Non-durable and durable memory with BoltDB and Redis - Two ready to use rulesets: regex parsed messages and cron events +- Easy integration with other programming languages through webservice RPC (JSON) - Container ready to use and easy to deploy Requirements: @@ -42,6 +43,9 @@ Slack message provider Telegram message provider * `GOCHATBOT_TELEGRAM_TOKEN` - Telegram user token for the chatbot +RPC + * `GOCHATBOT_RPC_BIND` - local IP address to bind the RPC HTTP server + ### Quick start (Docker version - Slack - Non-durable memory) ```ShellSession @@ -188,6 +192,42 @@ var cronRules = map[string]cron.Rule{ } ``` +### Integrating with other languages (RPC) + +If `GOCHATBOT_RPC_BIND` is set, gochatbot will open a HTTP server in the given +address and it will expose two endpoints: `/pop` and `/send`. + +Both of them use a JSON serialized version of the internal representation of +messages. Thus if you get from `/pop` this: + +```json +{ + "Room":"room", + "FromUserID":"fUID", + "FromUserName":"fName", + "ToUserID":"tUID", + "ToUserName":"tName", + "Message":"Message" +} +``` + +Probably you should be inverting From* with To* and returning something like +this (note the inversion of "from" with "to" values): + +```json +{ + "Room":"room", + "FromUserID":"tUID", + "FromUserName":"tName", + "ToUserID":"fUID", + "ToUserName":"fName", + "Message":"Message" +} +``` + +Check the [`rpc-example.php`](https://github.com/ccirello/gochatbot/blob/master/rpc-example.php) +file for an implementation of an echo service in PHP. + ### Guarantees I guarantee that I will maintain this chatops bot for the next 2 years, provide diff --git a/gochatbot.go b/gochatbot.go index e89d34c..a28f7d4 100644 --- a/gochatbot.go +++ b/gochatbot.go @@ -9,6 +9,7 @@ import ( "cirello.io/gochatbot/providers" "cirello.io/gochatbot/rules/cron" "cirello.io/gochatbot/rules/regex" + "cirello.io/gochatbot/rules/rpc" ) func main() { @@ -16,6 +17,7 @@ func main() { if name == "" { name = "gochatbot" } + provider := providers.Detect(os.Getenv) if err := provider.Error(); err != nil { log.SetOutput(os.Stderr) @@ -28,11 +30,23 @@ func main() { log.Fatalln("error in brain memory:", err) } - bot.New( - name, - memory, + options := []bot.Option{ bot.MessageProvider(provider), bot.RegisterRuleset(regex.New(regexRules)), bot.RegisterRuleset(cron.New(cronRules)), + } + + rpcHostAddr := os.Getenv("GOCHATBOT_RPC_BIND") + if rpcHostAddr != "" { + options = append( + options, + bot.RegisterRuleset(rpc.New(rpcHostAddr)), + ) + } + + bot.New( + name, + memory, + options..., ).Process() } diff --git a/rpc-example.php b/rpc-example.php new file mode 100644 index 0000000..885f7d0 --- /dev/null +++ b/rpc-example.php @@ -0,0 +1,58 @@ + [ + 'header' => "Content-type: application/json\r\n", + 'method' => 'POST', + 'content' => $json, + ], + ]; + $context = stream_context_create($options); + return file_get_contents($url, false, $context); +} + +while (true) { + $msg = botPop($rpcServer); + if (empty($msg['Message'])) { + continue; + } + echo 'Got:', PHP_EOL; + print_r($msg); + + $newMsg = [ + 'Room' => $msg['Room'], + 'FromUserID' => $msg['ToUserID'], + 'FromUserName' => $msg['ToUserName'], + 'ToUserID' => $msg['FromUserID'], + 'ToUserName' => $msg['FromUserName'], + 'Message' => 'echo: ' . $msg['Message'], + ]; + + echo 'Sending:', PHP_EOL; + print_r($newMsg); + + botSend($rpcServer, $newMsg); +} \ No newline at end of file diff --git a/rules/rpc/http.go b/rules/rpc/http.go new file mode 100644 index 0000000..bf5788f --- /dev/null +++ b/rules/rpc/http.go @@ -0,0 +1,46 @@ +package rpc // import "cirello.io/gochatbot/rules/rpc" + +import ( + "encoding/json" + "fmt" + "log" + "net/http" + + "cirello.io/gochatbot/messages" +) + +func (r *rpcRuleset) httpPop(w http.ResponseWriter, req *http.Request) { + r.mu.Lock() + defer r.mu.Unlock() + + var msg messages.Message + if len(r.inbox) > 1 { + msg, r.inbox = r.inbox[0], r.inbox[1:] + } else if len(r.inbox) == 1 { + msg = r.inbox[0] + r.inbox = []messages.Message{} + } else if len(r.inbox) == 0 { + fmt.Fprint(w, "{}") + return + } + + if err := json.NewEncoder(w).Encode(&msg); err != nil { + log.Fatal(err) + } +} + +func (r *rpcRuleset) httpSend(w http.ResponseWriter, req *http.Request) { + r.mu.Lock() + defer r.mu.Unlock() + + var msg messages.Message + if err := json.NewDecoder(req.Body).Decode(&msg); err != nil { + log.Fatal(err) + } + defer req.Body.Close() + + go func(m messages.Message) { + r.outCh <- m + }(msg) + fmt.Fprintln(w, "OK") +} diff --git a/rules/rpc/rpc.go b/rules/rpc/rpc.go new file mode 100644 index 0000000..6cdd8a4 --- /dev/null +++ b/rules/rpc/rpc.go @@ -0,0 +1,59 @@ +package rpc // import "cirello.io/gochatbot/rules/rpc" + +import ( + "fmt" + "log" + "net/http" + "sync" + + "cirello.io/gochatbot/bot" + "cirello.io/gochatbot/messages" +) + +type rpcRuleset struct { + mux *http.ServeMux + + bindAddr string + outCh chan messages.Message + + mu sync.Mutex + inbox []messages.Message +} + +// Name returns this rules name - meant for debugging. +func (r *rpcRuleset) Name() string { + return "RPC Ruleset" +} + +// Boot runs preparatory steps for ruleset execution +func (r *rpcRuleset) Boot(self *bot.Self) { + r.mux.HandleFunc("/pop", r.httpPop) + r.mux.HandleFunc("/send", r.httpSend) + log.Println("rpc: listening", r.bindAddr) + go http.ListenAndServe(r.bindAddr, r.mux) +} + +func (r rpcRuleset) HelpMessage(self bot.Self) string { + return fmt.Sprintln("RPC listens to", r.bindAddr, "for RPC calls") +} + +func (r *rpcRuleset) ParseMessage(self bot.Self, in messages.Message) []messages.Message { + r.mu.Lock() + defer r.mu.Unlock() + + r.inbox = append(r.inbox, in) + + return []messages.Message{} +} + +// New returns a RPC ruleset +func New(bindAddr string) *rpcRuleset { + return &rpcRuleset{ + mux: http.NewServeMux(), + bindAddr: bindAddr, + } +} + +func (r *rpcRuleset) SetOutgoingChannel(outCh chan messages.Message) { + r.outCh = outCh +}