Simple yet customizable bot framework written in Go.

Overview

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

Issues
  • 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))
    }
    
    enhancement 
    opened by richp10 9
  • 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.

    opened by deepoil 6
  • 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.

    enhancement v2 pending 
    opened by oklahomer 4
  • 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

    opened by jselleck 4
  • fix newBot return

    fix newBot return

    Hi, Sorry for being late. Fixed the return value of function NewBot. In addition, other corrections are also pushed together.

    v4 
    opened by deepoil 4
  • Add some more tests

    Add some more tests

    For God's sake, please.

    opened by oklahomer 2
  • 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
    }
    
    enhancement thinking 
    opened by oklahomer 2
  • 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.

    breaking change v2 
    opened by oklahomer 2
  • Race condition on live configuration update

    Race condition on live configuration update

    Sarah provides a mechanism to update configuration struct for both Command and ScheduledTask when corresponding configuration file is updated. Race condition may occur in below situations:

    1. Configuration file content is mapped to existing struct's instance.
    2. Re-built Command or ScheduledTask replaces existing one.

    The first one occurs because updated configuration values are set to existing config instance; Config struct is not cloned or instantiated every time live update runs. This is by design. CommandPropsBuilder.ConfigurableFunc and ScheduledTaskPropsBuilder.ConfigurableFunc are designed to receive config instance instead of reflect.Type so a struct instance with non-zero value field can be passed. e.g. 1) NewConfig returns config with pre-set default value and other fields are read from configuration file on Command (re-)build. 2) A DB connection is set to config struct beforehand and is expected to stay while other fields with json/yaml tags are updated on live-update. So cloning or instantiating config struct on live update is not an option in this case. In addition, cloning or deep copying can be complicated when field value is IO related object such as file handle. A locking mechanism seems appropriate.

    The Second case is rather a typical read/write collision. Commands type is merely an extension of slice, and therefore locking mechanism is required when replacing its element.

    opened by oklahomer 1
  • Exclude Message() method from sarah.Input interface

    Exclude Message() method from sarah.Input interface

    Current sarah.Input interface is defined as below:

    // Input defines interface that each incoming message must satisfy.
    // Each Bot/Adapter implementation may define customized Input implementation for each messaging content.
    //
    // See slack.MessageInput.
    type Input interface {
    	// SenderKey returns the text form of sender identifier.
    	// This value can be used internally as a key to store the sender's conversational context in UserContextStorage.
    	// Generally, When connecting chat service has the concept of group or chat room,
    	// this sender key should contain the group/room identifier along with user identifier
    	// so the user's conversational context is only applied in the exact same group/room.
    	//
    	// e.g. senderKey := fmt.Sprintf("%d_%d", roomID, userID)
    	SenderKey() string
    
    	// Message returns the text form of user input.
    	// This may return empty string when this Input implementation represents non-text payload such as photo,
    	// video clip or file.
    	Message() string
    
    	// SentAt returns the timestamp when the message is sent.
    	// This may return a message reception time if the connecting chat service does not provide one.
    	// e.g. XMPP server only provides timestamp as part of XEP-0203 when delayed message is delivered.
    	SentAt() time.Time
    
    	// ReplyTo returns the sender's address or location to be used to reply message.
    	// This may be passed to Bot.SendMessage() as part of Output value to specify the sending destination.
    	// This typically contains chat room, member id or mail address.
    	// e.g. JID of XMPP server/client.
    	ReplyTo() OutputDestination
    }
    

    This was meant to be a representation of an incoming event in general, which means the implementation could be a text message, photo, video or any other form that may or may not include text. As a matter of fact, recent chat services employ many messaging events that do not include any text message, and indeed the document for Message() says "This may return empty string when this Input implementation represents non-text payload such as photo, video clip or file." Then this could be more natural to remove Message() as the incoming event may not be representing any text-based event. However sarah.Command will have to add an extra effort of type assertions to see if the incoming event is a text-based one, and then extract text message from the event. This should be planned with care.

    thinking breaking change v2 
    opened by oklahomer 1
Releases(v4.0.0)
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

Gleb Sinyavskiy 63 Jul 27, 2021
easy-peasy wg tg bot

wireguard-telegram-bot Simple-Dimple Telegram Bot for Wireguard VPN config generation Functionality /menu — list available commands /newkeys — create

Sergey Skaredov 12 Sep 17, 2021
Flexible message router add-on for go-telegram-bot-api library.

telemux Flexible message router add-on for go-telegram-bot-api library. Table of contents Motivation Features Minimal example Documentation Changelog

Andrew Dunai 15 Aug 20, 2021
Chatto is a minimal chatbot framework in Go.

chatto Simple chatbot framework written in Go, with configurations in YAML. The aim of this project is to create very simple text-based chatbots using

Jaime Tenorio 89 Sep 5, 2021
Golang bindings for the Telegram Bot API

Golang bindings for the Telegram Bot API All methods are fairly self explanatory, and reading the godoc page should explain everything. If something i

null 3.1k Sep 21, 2021
Signum Explorer Telegram Bot - it's a simplified version of the web Signum Explorer

Signum Explorer Telegram Bot - it's a simplified version of the web Signum Explorer. Bot allows you to easily monitor the status of your account and to receive notifications about new transactions and blocks.

Anatoliy Bezgubenko 26 Sep 23, 2021
A golang implementation of a console-based trading bot for cryptocurrency exchanges

Golang Crypto Trading Bot A golang implementation of a console-based trading bot for cryptocurrency exchanges. Usage Download a release or directly bu

Alessandro Sanino 601 Sep 18, 2021
Kelp is a free and open-source trading bot for the Stellar DEX and 100+ centralized exchanges

Kelp Kelp is a free and open-source trading bot for the Stellar universal marketplace and for centralized exchanges such as Binance, Kraken, CoinbaseP

Stellar 748 Sep 23, 2021
Bot that polls activity API for Github organisation and pushes updates to Telegram.

git-telegram-bot Telegram bot for notifying org events Requirements (for building) Go version 1.16.x Setup If you don't have a telegram bot token yet,

Skycoin 3 May 13, 2021
A tip bot and Lightning wallet on Telegram ⚡️

@LightningTipBot ?? A Telegram Lightning ⚡️ Bitcoin wallet and tip bot for group chats. This repository contains everything you need to set up and run

null 9 Sep 14, 2021
A bot that greets new members on telegram groups with a cute, personalised gif.

Telegram-senko-bot About The Bot This bot greets new members on telegram groups with a cute, personalised gif with Senko-san. In case you're wondering

4kaze 5 Sep 16, 2021
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

Ian P Badtrousers 2.1k Sep 21, 2021
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.

Joe Bot 429 Sep 13, 2021
Simple yet customizable bot framework written in Go.

Introduction Sarah is a general-purpose bot framework named after the author's firstborn daughter. This comes with a unique feature called "stateful c

Go Hagiwara 202 Sep 16, 2021
Instagram to Telegram Channel Bot.

InstaTG Instagram to Telegram Channel Bot. Can access posts from any public Instagram account or an account that you follow. Features Keeps track of e

Anchit Bajaj 4 Sep 11, 2021
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

Yo-An Lin 346 Sep 6, 2021
A lightweight, universal cloud drive upload tool for all platforms

简体中文 LightUploader MoeClub wrote a very good version, but unfortunately it's not open source and hasn't been updated in a while. This project is a sim

高玩梁 194 Sep 22, 2021
A telegram bot that fetches multiple RSS cryptocurrency news feeds for sentiment analysis

Crypto News Telegram Bot A simple telegram bot that will help you stay updated on your latest crypto news This bot will help you keep track of the lat

Cha 4 Aug 22, 2021
A cowin bot that gives you an update whenever it finds a vacancy in your region

go-cowin-bot A cowin bot that will give you an update on discord whenever it finds a vacancy for the parameters provided Setup: download go-cowin-bot

Varun Sapre 4 May 24, 2021