Simple yet customizable bot framework written in Go.

GoDoc Go Report Card Build Status Coverage Status Maintainability Join the chat at https://gitter.im/go-sarah-dev/community

Introduction

Sarah is a general-purpose bot framework named after the author's firstborn daughter.

This comes with a unique feature called "stateful command" as well as some basic features such as command and scheduled task. In addition to those fundamental features, this project provides rich life cycle management including live configuration update, customizable alerting mechanism, automated command/task (re-)building, and panic-proofed concurrent command/task execution.

Such features are achieved with a composition of fine-grained components. Each component has its own interface and a default implementation, so developers are free to customize their bot experience by replacing the default implementation for a particular component with their own implementation. Thanks to such segmentalized lifecycle management architecture, the adapter component to interact with each chat service has fewer responsibilities comparing to other bot frameworks; An adapter developer may focus on implementing the protocol to interacting with the corresponding chat service. To take a look at those components and their relations, see Components.

IMPORTANT NOTICE

v3 Release

This is the third major version of go-sarah, which introduces the Slack adapter's improvement to support both RTM and Events API. Breaking interface change for Slack adapter was inevitable and that is the sole reason for this major version up. Other than that, this does not include any breaking change. See Migrating from v2.x to v3.x for details.

v2 Release

The second major version introduced some breaking changes to go-sarah. This version still supports and maintains all functionalities, better interfaces for easier integration are added. See Migrating from v1.x to v2.x to migrate from the older version.

Supported Chat Services/Protocols

Although a developer may implement sarah.Adapter to integrate with the desired chat service, some adapters are provided as reference implementations:

At a Glance

General Command Execution

hello world

Above is a general use of go-sarah. Registered commands are checked against user input and matching one is executed; when a user inputs ".hello," hello command is executed and a message "Hello, 世界" is returned.

Stateful Command Execution

The below image depicts how a command with a user's conversational context works. The idea and implementation of "user's conversational context" is go-sarah's signature feature that makes bot command "state-aware."

The above example is a good way to let a user input a series of arguments in a conversational manner. Below is another example that uses a stateful command to entertain the user.

Example Code

Following is the minimal code that implements such general command and stateful command introduced above. In this example, two ways to implement sarah.Command are shown. One simply implements sarah.Command interface; while another uses sarah.CommandPropsBuilder for lazy construction. Detailed benefits of using sarah.CommandPropsBuilder and sarah.CommandProps are described at its wiki page, CommandPropsBuilder.

For more practical examples, see ./examples.

package main

import (
	"context"
	"fmt"
	"github.com/oklahomer/go-sarah/v3"
	"github.com/oklahomer/go-sarah/v3/slack"
	
	"os"
	"os/signal"
	"syscall"
	
	// Below packages register commands in their init().
	// Importing with blank identifier will do the magic.
	_ "guess"
	_ "hello"
)

func main() {
	// Setup Slack adapter
	setupSlack()
	
	// Prepare go-sarah's core context.
	ctx, cancel := context.WithCancel(context.Background())

	// Run
	config := sarah.NewConfig()
	err := sarah.Run(ctx, config)
	if err != nil {
		panic(fmt.Errorf("failed to run: %s", err.Error()))
	}
	
	// Stop when signal is sent.
	c := make(chan os.Signal, 1)
   	signal.Notify(c, syscall.SIGTERM)
   	select {
   	case <-c:
   		cancel()
   
   	}
}

func setupSlack() {
	// Setup slack adapter.
	slackConfig := slack.NewConfig()
	slackConfig.Token = "REPLACE THIS"
	adapter, err := slack.NewAdapter(slackConfig, slack.WithRTMPayloadHandler(slack.DefaultRTMPayloadHandler))
	if err != nil {
		panic(fmt.Errorf("faileld to setup Slack Adapter: %s", err.Error()))
	}

	// Setup optional storage so conversational context can be stored.
	cacheConfig := sarah.NewCacheConfig()
	storage := sarah.NewUserContextStorage(cacheConfig)

	// Setup Bot with slack adapter and default storage.
	bot, err := sarah.NewBot(adapter, sarah.BotWithStorage(storage))
	if err != nil {
		panic(fmt.Errorf("faileld to setup Slack Bot: %s", err.Error()))
	}
	sarah.RegisterBot(bot)
}

package guess

import (
	"context"
	"github.com/oklahomer/go-sarah/v3"
	"github.com/oklahomer/go-sarah/v3/slack"
	"math/rand"
	"strconv"
	"strings"
	"time"
)

func init() {
	sarah.RegisterCommandProps(props)
}

var props = sarah.NewCommandPropsBuilder().
	BotType(slack.SLACK).
	Identifier("guess").
	Instruction("Input .guess to start a game.").
	MatchFunc(func(input sarah.Input) bool {
		return strings.HasPrefix(strings.TrimSpace(input.Message()), ".guess")
	}).
	Func(func(ctx context.Context, input sarah.Input) (*sarah.CommandResponse, error) {
		// Generate answer value at the very beginning.
		rand.Seed(time.Now().UnixNano())
		answer := rand.Intn(10)

		// Let user guess the right answer.
		return slack.NewResponse(input, "Input number.", slack.RespWithNext(func(c context.Context, i sarah.Input) (*sarah.CommandResponse, error){
			return guessFunc(c, i, answer)
		}))
	}).
	MustBuild()

func guessFunc(_ context.Context, input sarah.Input, answer int) (*sarah.CommandResponse, error) {
	// For handiness, create a function that recursively calls guessFunc until user input right answer.
	retry := func(c context.Context, i sarah.Input) (*sarah.CommandResponse, error) {
		return guessFunc(c, i, answer)
	}

	// See if user inputs valid number.
	guess, err := strconv.Atoi(strings.TrimSpace(input.Message()))
	if err != nil {
		return slack.NewResponse(input, "Invalid input format.", slack.RespWithNext(retry))
	}

	// If guess is right, tell user and finish current user context.
	// Otherwise let user input next guess with bit of a hint.
	if guess == answer {
		return slack.NewResponse(input, "Correct!")
	} else if guess > answer {
		return slack.NewResponse(input, "Smaller!", slack.RespWithNext(retry))
	} else {
		return slack.NewResponse(input, "Bigger!", slack.RespWithNext(retry))
	}
}

package hello

import (
	"context"
	"github.com/oklahomer/go-sarah/v3"
	"github.com/oklahomer/go-sarah/v3/slack"
	"strings"
)

func init() {
    sarah.RegisterCommand(slack.SLACK, &command{})	
}

type command struct {
}

var _ sarah.Command = (*command)(nil)

func (hello *command) Identifier() string {
	return "hello"
}

func (hello *command) Execute(_ context.Context, i sarah.Input) (*sarah.CommandResponse, error) {
	return slack.NewResponse(i, "Hello!")
}

func (hello *command) Instruction(input *sarah.HelpInput) string {
	if 12 < input.SentAt().Hour() {
		// This command is only active in the morning.
		// Do not show instruction in the afternoon.
		return ""
	}
	return "Input .hello to greet"
}

func (hello *command) Match(input sarah.Input) bool {
	return strings.TrimSpace(input.Message()) == ".hello"
}

Supported Golang Versions

Official Release Policy says "each major Go release is supported until there are two newer major releases." Following this policy would help this project enjoy the improvements introduced in the later versions. However, not all projects can immediately switch to a newer environment. Migration could especially be difficult when this project cuts off the older version's support right after a new major Go release.

As a transition period, this project includes support for one older version than Go project does. Such a version is guaranteed to be listed in .travis.ci. In other words, new features/interfaces introduced in 1.10 can be used in this project only after 1.12 is out.

Further Readings

Comments
  • xmpp adapter

    xmpp adapter

    Is there an xmpp adapter for sarah - I can't see one.

    Writing one would probably be a little beyond my current competence with go but I may have a go OR (more likely) pay a bounty for this. @oklahomer would you be interested?

    I assume the task would involved taking the /slack folder and creating an /xmpp package with the same api but using an xmpp library rather than golack - so we then just use the adapter like snippet below - but everything else in Sarah would work the same as with the slack adapter. Do I understand this correctly?

    func setupXmpp(config *slack.Config, storage sarah.UserContextStorage) (sarah.Bot, error) {
    	adapter, err := xmpp.NewAdapter(config)
    	if err != nil {
    		return nil, err
    	}
    
    	return sarah.NewBot(adapter, sarah.BotWithStorage(storage))
    }
    
  • fix the return value of the function

    fix the return value of the function

    Hi, This PR is Fixed the following:

    1. NewBot, Delete and Flush function seemed not to return error. (Always only nil is entered in error) → Remove return error

    2. In the Get function, there was a place where if was judged for both value and bool, but I thought it would be okay to judge only bool. → Change to bool only judgment

    3. I fixed the part that seems to be a typo. *I'm sorry if it is not what I expected. stringified → string field

    Please check when you have time.

  • Mattermost Adapter

    Mattermost Adapter

    Create Mattermost adapter so that the project can be used on premises with Mattermost.

    This may be easy through an adaption of existing go-mattermost integrations.

    i.e. https://github.com/mattermost/mattermost-bot-sample-golang

  • Find suitable replacement for forked github.com/robfig/cron.

    Find suitable replacement for forked github.com/robfig/cron.

    Instead of simply employing original github.com/robfig/cron, this project uses forked and customized version of this. The customization was required to assign identifier for each registered cron job to remove or replace on live configuration update. This customization however lowers maintenancibility, and hence a suitable replacement with well-maintained equivalent package is wanted.

    The replacing package must have abilities to:

    • execute registered jobs in a scheduled manner
    • execute jobs in a panic-proof manner
    • register a job with pre-declared identifier or dispense one on schedule registration
    • remove registered scheduled job by above identifier
    • cancel all scheduled jobs by context cancellation or by calling one method
    • parse crontab-styled schedule declaration

    When two or more packages meet above requirements and have equal popularity, the one with minimal features and simple implementation should have higher priority.

  • Plan v2 release

    Plan v2 release

    Ever since go-sarah's initial release two years ago, this has been a huge help to the author's ChatOps. During these years, some minor improvements and bug fixes mostly without interface changes were introduced. However, a desire to add some improvements that involve drastic interface changes is still not fulfilled, leaving the author in a dilemma of either maintaining the API or adding breaking changes.

    Version 2 development is one such project to introduce API changes to improve go-sarah as a whole. Those developers who wish to keep using the previous APIs should use v1.X releases. Issues and pull requests with a v2 label should be included to v2 release unless otherwise closed individually.

  • On JSON deserialization, let time.Duration-type field accept time.ParseDuration friendly format

    On JSON deserialization, let time.Duration-type field accept time.ParseDuration friendly format

    Problem

    With current definitions for some Config structs, fields are typed as time.Duration to express time intervals. The underlying type of time.Duration is merely an int64 and its minimal unit is nano-second, so JSON/YAML mapping is always painful.

    type Example struct {
        RetryInterval time.Duration `json:"retry_interval"`
    }
    
    {
        "retry_interval": 1000000000 // 1 sec!
    }
    

    With above example, although the type definition correctly reflects author's intention, the JSON structure and its value significantly lack readability. A human-readable value such as the one time.ParsDuration accepts should also be allowed.

    Solutions

    Change field type

    Never. This breaks backwards compatibility.

    Add some implementation for JSON/YAML unmarshaler

    To convert time.ParseDuration-friendly value to time.Duration, let Config structs implement json.Unmarshaler and yaml.Unmarshaler. Some improvements may be applied, but below example seems to work.

    type Config struct {
    	Token          string        `json:"token" yaml:"token"`
    	RequestTimeout time.Duration `json:"timeout" yaml:"timeout"`
    }
    
    func (config *Config) UnmarshalJSON(raw []byte) error {
    	tmp := &struct {
    		Token          string      `json:"token"`
    		RequestTimeout json.Number `json:"timeout,Number"`
    	}{}
    
    	err := json.Unmarshal(raw, tmp)
    	if err != nil {
    		return err
    	}
    
    	config.Token = tmp.Token
    
    	i, err := strconv.Atoi(tmp.RequestTimeout.String())
    	if err == nil {
    		config.RequestTimeout = time.Duration(i)
    	} else {
    		duration, err := time.ParseDuration(tmp.RequestTimeout.String())
    		if err != nil {
    			return fmt.Errorf("failed to parse timeout field: %s", err.Error())
    		}
    		config.RequestTimeout = duration
    	}
    
    	return nil
    }
    
    func (config *Config) UnmarshalYAML(unmarshal func(interface{}) error) error {
    	tmp := &struct {
    		Token          string `yaml:"token"`
    		RequestTimeout string `yaml:"timeout"`
    	}{}
    
    	err := unmarshal(tmp)
    	if err != nil {
    		return err
    	}
    
    	unmarshal(tmp)
    
    	config.Token = tmp.Token
    
    	i, err := strconv.Atoi(tmp.RequestTimeout)
    	if err == nil {
    		config.RequestTimeout = time.Duration(i)
    	} else {
    		duration, err := time.ParseDuration(tmp.RequestTimeout)
    		if err != nil {
    			return fmt.Errorf("failed to parse timeout field: %s", err.Error())
    		}
    		config.RequestTimeout = duration
    	}
    
    	return nil
    }
    
  • Revert workaround for coverage test

    Revert workaround for coverage test

    #84 introduced a workaround to properly measure the coverage with Go 1.9 or older versions. This project no longer supports such old versions so it is now safe to revert changes made in #84.

  • Follow golint instruction and fix document

    Follow golint instruction and fix document

    This fixes below warnings.

    image

    go-sarah/slack/adapter.go
    Line 603: warning: comment on exported type RespOption should be of the form "RespOption ..." (with optional leading article) (golint)
    go-sarah/gitter/adapter.go
    Line 182: warning: comment on exported type RespOption should be of the form "RespOption ..." (with optional leading article) (golint)
    go-sarah/status.go
    Line 11: warning: exported var ErrRunnerAlreadyRunning should have comment or be unexported (golint)
    Line 13: warning: exported function CurrentStatus should have comment or be unexported (golint)
    
  • Migrate to v3 of github.com/robfig/cron

    Migrate to v3 of github.com/robfig/cron

    This solves #48. The version specification with go get was a bit complicated as noted in https://github.com/oklahomer/go-sarah/issues/48#issuecomment-489394619.

  • Employ xerrors to propagate original error values

    Employ xerrors to propagate original error values

    A new error handling library "xerrors" is now available and is going to be incorporated with 1.13 as a standard library. Although some other libraries including "pkg/errors" enables developers to propagate an original error value in a hierarchical manner, go-sarah's author has been wondering if such non-standard library should be employed. This project is a third party library for most developers and it is usually not preferred that such library includes additional dependencies. Now that "xerrors" is officially confirmed to be a standard library in the near future and is ported to older Golang versions, the author believes this is safe to employ such library to express errors in a more informative manner.

    Use xerrors.Errorf("some message: %w", err) and xerrors.New("some message") for error initialization instead of fmt.Errorf() and errors.New() where it is appropriate.

  • Bot supervisor should also receive Bot's noteworthy state in addition to currently handled critical state

    Bot supervisor should also receive Bot's noteworthy state in addition to currently handled critical state

    In this project, sarah.Runner takes care of subordinate Bot's critical state. A sarah.Bot implementation calls a function to escalate its critical state to sarah.Runner so sarah.Runner can safely cancel failing sarah.Bot's context and notify such state to administrators via one or more sarah.Alerter implementations. https://github.com/oklahomer/go-sarah/blob/a7408dc6d69b4e779c97b2a46684958fbb984b06/runner.go#L565-L584

    In this way, sarah.Bot's implementation can be separated from its state handling and alert sending; sarah.Bot's lifecycle can be solely managed by sarah.Runner. So the responsibility of sarah.Bot implementation and its developer is minimal.

    However only BotNonContinuableError is received and handled by sarah.Runner; Other noteworthy states are not handled or notified to developers. Define an error type that represents a noteworthy state change and let sarah.Runner handle this. This handling should not cancel sarah.Bot context but only notify such event to administrators.

A bot based on Telegram Bot API written in Golang allows users to download public Instagram photos, videos, and albums without receiving the user's credentials.

InstagramRobot InstagramRobot is a bot based on Telegram Bot API written in Golang that allows users to download public Instagram photos, videos, and

Dec 16, 2021
Slack bot core/framework written in Go with support for reactions to message updates/deletes
Slack bot core/framework written in Go with support for reactions to message updates/deletes

Overview Requirements Features Demo The Name Concepts Create Your Own Slackscot Assembling the Parts and Bringing Your slackscot to Life Configuration

Oct 28, 2022
Dlercloud-telegram-bot - A Telegram bot for managing your Dler Cloud account

Dler Cloud Telegram Bot A Telegram bot for managing your Dler Cloud account. Usa

Dec 30, 2021
Quote-bot - Un bot utilisant l'API Twitter pour tweeter une citation par jour sur la programmation et les mathématiques.

Description Ceci est un simple bot programmé en Golang qui tweet une citation sur la programmation tout les jours. Ce bot est host sur le compte Twitt

Jan 1, 2022
Discord-bot - A Discord bot with golang

JS discord bots Install Clone repo git clone https://github.com/fu-js/discord-bo

Aug 2, 2022
Bot - Telegram Music Bot in Go

Telegram Music Bot in Go An example bot using gotgcalls. Setup Install the serve

Jun 28, 2022
Pro-bot - A telegram bot to play around with the community telegram channels

pro-bot ?? Pro Bot A Telegram Bot to Play Around With The Community Telegram Cha

Jan 24, 2022
Slack-emoji-bot - This Slack bot will post the newly created custom Slack emojis to the channel of your choice

Slack Emoji Bot This Slack bot will post the newly created custom Slack emojis t

Oct 21, 2022
Sex-bot - The sex bot and its uncreative responses
Sex-bot - The sex bot and its uncreative responses

Sex Bot The sex bot, made with golang! The sex bot can't hear the word "sexo" he

Nov 11, 2022
Feline-bot - Feline Bot for Discord using Golang

Feline Bot for Discord Development This bot is implemented using Golang. Feature

Feb 10, 2022
Yet another QQbot developed by arttnba3 with golang, based on go-cqhttp

a3bot3 - Documentation Introduction Yet another QQbot developed by arttnba3 with golang, based on go-cqhttp Usage *Requirement You need to have a go-c

May 23, 2022
Telegram Bot Framework for Go

Margelet Telegram Bot Framework for Go is based on telegram-bot-api It uses Redis to store it's states, configs and so on. Any low-level interactions

Dec 22, 2022
Slack Bot Framework

slacker Built on top of the Slack API github.com/slack-go/slack with the idea to simplify the Real-Time Messaging feature to easily create Slack Bots,

Dec 25, 2022
Telebot is a Telegram bot framework in Go.

Telebot "I never knew creating Telegram bots could be so sexy!" go get -u gopkg.in/tucnak/telebot.v2 Overview Getting Started Poller Commands Files Se

Dec 30, 2022
Parr(B)ot is a Telegram bot framework based on top of Echotron

Parr(B)ot framework A just born Telegram bot framework in Go based on top of the echotron library. You can call it Parrot, Parr-Bot, Parrot Bot, is up

Aug 22, 2022
IRC, Slack, Telegram and RocketChat bot written in go
IRC, Slack, Telegram and RocketChat bot written in go

go-bot IRC, Slack & Telegram bot written in Go using go-ircevent for IRC connectivity, nlopes/slack for Slack and Syfaro/telegram-bot-api for Telegram

Dec 20, 2022
The modern cryptocurrency trading bot written in Go.

bbgo A trading bot framework written in Go. The name bbgo comes from the BB8 bot in the Star Wars movie. aka Buy BitCoin Go! Current Status Features E

Jan 2, 2023
A general-purpose bot library inspired by Hubot but written in Go. :robot:

Joe Bot ?? A general-purpose bot library inspired by Hubot but written in Go. Joe is a library used to write chat bots in the Go programming language.

Dec 24, 2022
An easy-to-use discord bot written in go

Discord Bot An easy-to-use discord bot template written in golang using discordgo. This template was written for learning golang. It will be updated a

Jan 23, 2022