Skip to content
A small library to write scalable slack and web chat apps.
Branch: master
Clone or download
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Permalink
Type Name Latest commit message Commit time
Failed to load latest commit information.
examples
slack
web
.editorconfig
.gitignore
.travis.yml
LICENSE
README.md
bots.go
go.mod

README.md

bots

GoDoc Build Status

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 {
		// NOTE: we send a message and stay in this state, instead of transitioning
		// anywhere. This is how "repeat" works. This will repeat indefinitely until
		// a valid value is obtained, or can set a state variable to repeat a fixed
		// number of times before calling 'controls.End()'.
		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

  • support for sending more than text from client
  • shell client.
  • MemoryItemStore can be made O(nlogn).
  • HTTP + SSE?

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") // https://my.slack.com/services/new/bot
	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)) // echoes the same message back
		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 {
		// NOTE: we send a message and stay in this state, instead of transitioning
		// anywhere. This is how "repeat" works. This will repeat indefinitely until
		// a valid value is obtained, or can set a state variable to repeat a fixed
		// number of times before calling 'controls.End()'.
		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

  • *websocket.Conn does not Close() and throws an error.

  • ffjson not generating fflib import for interactions

  • does not work on appengine, because of using http.DefaultClient. Figure out a way to switch out the client.

build

The project uses ffjson to optimize JSON encoding/decoding.

To regenerate

go generate ./...
You can’t perform that action at this time.