Skip to content
This repository has been archived by the owner on Sep 2, 2020. It is now read-only.

Commit

Permalink
Add RPC ruleset for cross-language integration
Browse files Browse the repository at this point in the history
  • Loading branch information
ucirello committed Dec 5, 2015
1 parent 4c7d063 commit 49f920f
Show file tree
Hide file tree
Showing 5 changed files with 220 additions and 3 deletions.
40 changes: 40 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
20 changes: 17 additions & 3 deletions gochatbot.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,15 @@ import (
"cirello.io/gochatbot/providers"
"cirello.io/gochatbot/rules/cron"
"cirello.io/gochatbot/rules/regex"
"cirello.io/gochatbot/rules/rpc"
)

func main() {
name := os.Getenv("GOCHATBOT_NAME")
if name == "" {
name = "gochatbot"
}

provider := providers.Detect(os.Getenv)
if err := provider.Error(); err != nil {
log.SetOutput(os.Stderr)
Expand All @@ -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()
}
58 changes: 58 additions & 0 deletions rpc-example.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
<?php
// This implementation executes a echo service in gochatbot and PHP.
$rpcServer = getenv('GOCHATBOT_RPC_BIND');

function botPop($rpcServer) {
$raw = file_get_contents(sprintf('http://%s/pop', $rpcServer));

$ret = json_decode($raw, true);
$err = json_last_error();
if (JSON_ERROR_NONE != $err) {
die(json_last_error_msg());
}

return $ret;
}

function botSend($rpcServer, $msg) {
$url = sprintf('http://%s/send', $rpcServer);

$json = json_encode($msg);
$err = json_last_error();
if (JSON_ERROR_NONE != $err) {
die(json_last_error_msg());
}

$options = [
'http' => [
'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);
}
46 changes: 46 additions & 0 deletions rules/rpc/http.go
Original file line number Diff line number Diff line change
@@ -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")
}
59 changes: 59 additions & 0 deletions rules/rpc/rpc.go
Original file line number Diff line number Diff line change
@@ -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
}

0 comments on commit 49f920f

Please # to comment.