bots

A little library to write chatbots in go.
web
go get suy.io/bots/web
more examples in examples/web/bots. Small Deployment example in examples/web/app. Also deployed online on https://app-ruhxhowvkv.now.sh.
package main
import (
"fmt"
"log"
"net/http"
"suy.io/bots/web"
)
func main() {
c, err := web.NewController()
if err != nil {
log.Fatal(err)
}
http.HandleFunc("/", index)
http.HandleFunc("/chat", c.ConnectionHandler())
go handleMessages(c)
log.Fatal(http.ListenAndServe(":8080", nil))
}
func handleMessages(c *web.Controller) {
for msg := range c.DirectMessages() {
msg.Reply(web.TextMessage(msg.Text))
}
}
func index(res http.ResponseWriter, req *http.Request) {
fmt.Fprintf(res, "%s", `
<!DOCTYPE html>
<html lang="en">
<head>
...
</head>
<body>
<template id="user-message">
...
</template>
<template id="bot-message">
...
</template>
<div id="chat">
<div id="messages"></div>
<form id="form" action="/" method="post">
<input type="text" name="message" id="message" placeholder="enter" required="required">
<button type="submit">Send</button>
</form>
</div>
<script src="/lib/chat.js"></script>
</body>
</html>`,
)
}
lib/chat.js
import Chat from "@suy/bots-web-client";
window.addEventListener("DOMContentLoaded", loaded);
async function loaded() {
const chat = new Chat(`ws://${window.location.host}/chat`);
await chat.open();
const form = document.querySelector("#form");
const message = document.querySelector("#message");
form.addEventListener("submit", (e) => {
e.preventDefault();
chat.say({ text: message.value });
...
});
for await (const msg of chat.messages()) {
log("received", msg);
...
}
log("closed")
}
Client
There is a very simple browser client implemented at web/browser that provides incoming messages over an async iterator (for-await in the above example).
Conversations
full example
password := web.NewConversation()
password.On("start", func(msg *web.Message, controls *web.Controls) {
msg.Text = "Please specify a length"
controls.Bot().Say(msg)
controls.To("length")
})
password.On("length", func(msg *web.Message, controls *web.Controls) {
_, err := strconv.ParseInt(msg.Text, 10, 64)
if err != nil {
controls.Bot().Say(&web.Message{Text: "Invalid Value, Please try again"})
return
}
controls.Set("length", msg.Text)
msg.Text = "Do you want numbers"
controls.Bot().Say(msg)
controls.To("numbers")
})
password.On("numbers", func(msg *web.Message, controls *web.Controls) {
if lt := strings.ToLower(msg.Text); lt == "no" || lt == "nope" {
controls.Bot().Say(&web.Message{Text: "Not Using Numbers"})
controls.Set("numbers", "false")
} else {
controls.Bot().Say(&web.Message{Text: "Using Numbers"})
controls.Set("numbers", "true")
}
controls.Bot().Say(&web.Message{Text: "Do you want special characters"})
controls.To("characters")
})
password.On("characters", func(msg *web.Message, controls *web.Controls) {
characters := true
...
controls.Bot().Say(&web.Message{Text: "Your Password is '" + ans + "'"})
controls.End()
})
Storage
There are 3 main storage interfaces,
BotID Creation
BotIDCreator is a function that can be passed to WithBotIDCreator when initializing controller to generate bot IDs. The default one generates a timestamp to get an id, so every connection is unique. The actual request is passed along in the function so any request parameters can be used to detect identity. A very simple example using cookies can be seen at https://github.com/suyash/bots/blob/master/examples/web/bots/redis.go#L31-L43
Issues
Slack
go get suy.io/bots/slack
more examples in examples/slack
package main
import (
"log"
"suy.io/bots/slack"
"suy.io/bots/slack/api/chat"
)
func main() {
c, err := slack.NewController()
if err != nil {
log.Fatal(err)
}
b, err := c.CreateBot("BOT_TOKEN")
if err != nil {
log.Fatal(err)
}
if err := b.Start(); err != nil {
log.Fatal(err)
}
for msg := range c.DirectMessages() {
_, err := msg.Reply(chat.TextMessage(msg.Text))
if err != nil {
log.Fatal("error:", err)
}
}
}
Conversations
The following example is a password generation conversation. (full example)
password := slack.NewConversation()
password.On("start", func(msg *chat.Message, controls *slack.Controls) {
controls.Bot().Reply(chat.RTMMessage(msg), chat.TextMessage("Please specify a length"))
controls.To("length")
})
password.On("length", func(msg *chat.Message, controls *slack.Controls) {
_, err := strconv.ParseInt(msg.Text, 10, 64)
if err != nil {
controls.Bot().Reply(chat.RTMMessage(msg), &chat.Message{Text: "Invalid Value, Please try again"})
return
}
controls.Set("length", msg.Text)
controls.Bot().Reply(chat.RTMMessage(msg), chat.TextMessage("Do you want numbers"))
controls.To("numbers")
})
password.On("numbers", func(msg *chat.Message, controls *slack.Controls) {
if lt := strings.ToLower(msg.Text); lt == "no" || lt == "nope" {
controls.Bot().Reply(chat.RTMMessage(msg), chat.TextMessage("Not Using Numbers"))
controls.Set("numbers", "false")
} else {
controls.Bot().Reply(chat.RTMMessage(msg), chat.TextMessage("Using Numbers"))
controls.Set("numbers", "true")
}
controls.Bot().Reply(chat.RTMMessage(msg), chat.TextMessage("Do you want special characters"))
controls.To("characters")
})
password.On("characters", func(msg *chat.Message, controls *slack.Controls) {
...
controls.Bot().Reply(chat.RTMMessage(msg), chat.TextMessage("Your Password is '"+ans+"'"))
controls.End()
})
Connector
A connector is a websocket connection pool defined at https://godoc.org/suy.io/bots/slack#Connector. The connector package provides a type that can manage connections. By default all connections are also a part of the same service, but if required, can be abstracted out and the two services can talk using any transport mechanism. Sample HTTP implementations are by httpserver and httpclient respectively. There is also an example.
Storage
There are 3 main storage interfaces
-
BotStore
This essentially stores oauth.AccessResponses of all bots that have been authenticated with the service. A custom implementation can be provided by passing it inside WithBotStore function when initializing a controller. An example redis implementation.
-
ConversationStore
This stores and manages conversation data and state. A custom implementation can be provided at initialization by using WithConversationStore when initializing a controller. An example redis implementation.
Issues
build
The project uses ffjson to optimize JSON encoding/decoding.
To regenerate
go generate ./...