sdbot

package module
v0.0.0-...-465877c Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jan 28, 2018 License: MIT Imports: 19 Imported by: 0

README

SDBot - A Pokemon Showdown Bot Framework

GoDoc Go Report Card

Description

SDBot is a bot framework written in Go for Pokemon Showdown designed to take advantage of Go's inherent concurrency.

Still in developmental stages. This means that the API is still likely to change at any time.

SDBot has the following dependencies.

Installation

You can go get it.

go get github.com/mikopits/sdbot

To install/update the package dependencies:

go get -u -v github.com/mikopits/sdbot

Example

To get the bot up and running and with a loaded example plugin:

package main

import (
  "github.com/mikopits/sdbot"
  "github.com/mikopits/sdbot/examples/plugins"
)

func main() {
  b := sdbot.NewBot("path/to/your/config.toml")
  b.RegisterPlugin(plugins.HelloWorldPlugin(), "hello world")
  b.RegisterPlugin(plugins.EchoPlugin(), "echo")
  b.RegisterTimedPlugin(plugins.CountPlugin(), "count")
  b.Connect()
}

And be sure to set your config.toml file in the same directory (See the example).

Contribute

If you've gotten panics or have any other problems, go ahead and open an issue.

Have an awesome feature you want to add? Looking at the source code and know you can solve a problem much more elegantly? Fork the project, develop the feature on its own branch, and submit a pull request. Or you can open an issue, tell me how dumb I am, and I can do it myself, I suppose.

TODOs

  • Tests
  • Battle logic
  • More example plugins that sweep more features

Documentation

Overview

Package sdbot implements the Pokemon Showdown protocol and provides a library to interact with users.

Usage

The Bot type represents the bot and its state. To create a Bot, call the NewBot function. This currently assumes that your configuration toml file is both called config.toml and is located in the same directory as your bot script. Expect this to change in later versions.

To connect to the server, call the bot's Connect method.

To register plugins, write your plugins under a package and import them. Register them by calling the bot's RegisterPlugin and RegisterTimedPlugin methods. It is recommended to register your plugins before connecting to the server.

Concurrency

Each bot will spawn multiple goroutines for both reading and writing to the socket, as well as for plugin loops listening for matches from the server messages. The bot internal state is designed for concurrent access, so two goroutines looking to read the bot's state, such as the userlist, will get the same result.

A bot will spawn a separate goroutine to run every Plugin event. For this reason, applications are responsible for ensuring that the plugin EventHandlers are safe for concurrent use. For this reason, Bot exports a Synchronize method that takes a pointer to a function and an identifying string that will run all functions corresponding to that identifying string on the same mutex.

Consider you want a plugin that reads and writes to a map defined in its event handler. Maps cannot be read and written to concurrently, so you need to ensure that another plugin event must wait until the current one is done.

func (eh *PluginEventHandler) HandleEvent(m *sdbot.Message, args []string) {
    var readVal interface{}
    rk := someKey()
    wk := anotherKey()
    rw := func() interface{} {
        eh.Map[wk] = someVal()
        readVal = eh.Map[rk]
        return nil
    }

    m.Bot.Synchronize("maprw", &rw)
}

Index

Constants

View Source
const (
	Administrator = `~`
	Leader        = `&`
	RoomOwner     = `#`
	Moderator     = `@`
	Driver        = `%`
	TheImmortal   = `>`
	Battler       = `★`
	Voiced        = `+`
	Unvoiced      = ` `
	Muted         = `?`
	Locked        = `‽`
)

String values of all the auth levels.

Variables

View Source
var ErrPluginAlreadyRegistered = errors.New("sdbot: plugin was already registered")

ErrPluginAlreadyRegistered is returned whenever the same plugin is attempted to be registered twice.

View Source
var ErrPluginNameAlreadyRegistered = errors.New("sdbot: plugin name was already in use (register under another name)")

ErrPluginNameAlreadyRegistered is returned whenever a plugin with a particular name is attempted to be registered, but the name has previously been registered.

View Source
var ErrUnexpectedMessageType = errors.New("sdbot: unexpected message type from the websocket")

ErrUnexpectedMessageType is returned when we receive a message from the websocket that isn't a websocket.TextMessage or a normal closure.

View Source
var LoginTime map[string]int = make(map[string]int)

LoginTime contains the unix login times as values to each particular room the bot has joined. This allows us to ignore messages that occurred before the bot has logged in.

Functions

func AddLogger

func AddLogger(lo Logger)

AddLogger adds a logger to the LoggerList Loggers.

func CheckErr

func CheckErr(err error)

CheckErr checks if an error is nil, and if it is not, logs the error to default os.Stderr logger. TODO Perhaps a nice stack trace here as well.

func CheckErrAll

func CheckErrAll(err error)

CheckErrAll checks if an error is nil, and if it is not, logs the error to all the loggers in the LoggerList. TODO Perhaps a nice stack trace here as well.

func Debug

func Debug(s string)

Debug logs debug messages to the default os.Stderr logger.

func Debugf

func Debugf(format string, a ...interface{})

Debugf logs debug messages with formatting to the default os.Stderr logger.

func Error

func Error(err error)

Error logs errors to the default os.Stderr logger.

func ErrorAll

func ErrorAll(err error)

ErrorAll logs errors to all the loggers in the LoggerList.

func Errorf

func Errorf(format string, a ...interface{})

Errorf logs errors with formatting to the default os.Stderr logger.

func Fatal

func Fatal(s string)

Fatal logs fatal messages to the default os.Stderr logger.

func Fatalf

func Fatalf(format string, a ...interface{})

Fatalf logs fatal messages with formatting to the default os.Stderr logger.

func Haste

func Haste(buf io.Reader, bodyType string) (string, error)

Haste uploads a file to Hastebin and returns the response URL. TODO Cute, but perhaps this should be its own package.

func Info

func Info(s string)

Info logs informatic messages to the default os.Stderr logger.

func Infof

func Infof(format string, a ...interface{})

Infof logs informatic messages with formatting to the default os.Stderr logger.

func Inspect

func Inspect(i interface{})

Inspect logs a debug message to the default os.Stderr logger, taking any interface and inpecting its contents.

func RemoveLogger

func RemoveLogger(lo Logger) bool

RemoveLogger removes a logger from the LoggerLit Loggers. Returns true if the logger was successfully removed.

func Rename

func Rename(old string, s string, r *Room, b *Bot, auth string)

Rename renames a user and updates their record in the UserList.

func Sanitize

func Sanitize(s string) string

Sanitize returns a new string with non-alphanumeric characters removed. This is particularly useful when it comes to identifying unique usernames. Keep in mind that Pokemon Showdown usernames are _not_ case sensitive, so a downcased and sanitized username is a unique identifier.

func SanitizeRoomid

func SanitizeRoomid(s string) string

SanitizeRoomid returns a new string with non-alphanumeric characters removed, excepting hyphens. This is used for returning the proper roomids used on PS! - groupchats are formatted as groupchat-CREATOR-NAME, and without these hyphens, we are given an incorrect identifier.

func Warn

func Warn(s string)

Warn logs warning messages to the default os.Stderr logger.

func Warnf

func Warnf(format string, a ...interface{})

Warnf logs warning messages with formatting to the default os.Stderr logger.

Types

type AnyLogger

type AnyLogger struct {
	Output io.Writer
	// contains filtered or unexported fields
}

AnyLogger represents a container for a logger. It knows WHERE to log the messages and uses a mutex so as to not log out of order. What it does not know is HOW to format its messages. See DefaultLogger and PrettyLogger to see how this works.

type BattleInfo

type BattleInfo struct {
	FirstPlayer  string `json:"p1"`
	SecondPlayer string `json:"p2"`
	MinElo       string `json:"minElo"`
}

BattleInfo holds the roomlist JSON unmarshalling for each battle.

type Bot

type Bot struct {
	Config                *Config
	Connection            *Connection
	UserList              map[string]*User
	RoomList              map[string]*Room
	Rooms                 []string
	Nick                  string
	Plugins               []*Plugin
	TimedPlugins          []*TimedPlugin
	PluginChatChannels    map[string]*chan *Message
	PluginPrivateChannels map[string]*chan *Message
	BattleFormats         []string
	RecentBattles         chan *RecentBattles
	// contains filtered or unexported fields
}

Bot represents the entrypoint to all the necessary behaviour of the bot. The bot runs its handlers in separate goroutines, so an API is provided to allow for thread-safe and concurrent access to the bot. See the Synchronize method for how this works.

func NewBot

func NewBot(path string) *Bot

NewBot creates a new instance of the Bot struct. In doing so it creates a new Connection as well as adds a PrettyLogger to the Loggers that logs to os.Stderr. It takes a path to the configuration TOML file.

func (*Bot) Connect

func (b *Bot) Connect()

Connect starts the bot and connects to the websocket.

func (*Bot) JoinRoom

func (b *Bot) JoinRoom(room *Room)

JoinRoom makes the bot join a room.

func (*Bot) LeaveRoom

func (b *Bot) LeaveRoom(room *Room)

LeaveRoom makes the bot leave a room.

func (*Bot) RegisterPlugin

func (b *Bot) RegisterPlugin(p *Plugin, name string) error

RegisterPlugin registers a plugin under a name and starts listening on its event handler.

func (*Bot) RegisterPlugins

func (b *Bot) RegisterPlugins(plugins map[string]*Plugin) error

RegisterPlugins registers a slice of plugins in one call. The map should be formatted with pairs of "plugin name"=>*Plugin.

func (*Bot) RegisterTimedPlugin

func (b *Bot) RegisterTimedPlugin(tp *TimedPlugin, name string) error

RegisterTimedPlugin registers a timed plugin under the provided name. Timed plugins are not started until the bot is logged in.

func (*Bot) Send

func (b *Bot) Send(s string)

Send queues a string onto the outgoing message queue.

func (*Bot) StartTimedPlugins

func (b *Bot) StartTimedPlugins()

StartTimedPlugins starts all registered TimedPlugins.

func (*Bot) StopTimedPlugins

func (b *Bot) StopTimedPlugins()

StopTimedPlugins stops all registered TimedPlugins.

func (*Bot) Synchronize

func (b *Bot) Synchronize(name string, lambda *func() interface{}) interface{}

Synchronize provides an API to keep your code thread-safe and concurrent. For example, if a plugin is going to write to a file, it would be a bad idea to have multiple threads with different state to try to access the file at the same time. So you should run such commands under Bot.Synchronize. The name is an arbitrary name you can choose for your mutex. The lambda is Then run in the mutex defined by the name. Choose unique names!

Example:

var doUnsafeAction = func() interface{} {
  someActionThatMustBePerformedSequentially()
  return nil
}

bot.Synchronize("uniqueIdentifierForUnsafeAction", &doUnsafeAction)

func (*Bot) UnregisterPlugin

func (b *Bot) UnregisterPlugin(p *Plugin) bool

UnregisterPlugin unregisters a plugin. Returns true if the plugin was successfully unregistered.

func (*Bot) UnregisterPlugins

func (b *Bot) UnregisterPlugins()

UnregisterPlugins unregisters all plugins.

func (*Bot) UnregisterTimedPlugin

func (b *Bot) UnregisterTimedPlugin(tp *TimedPlugin) bool

UnregisterTimedPlugin unregisters a timed plugin. Returns true if the plugin was successfully unregistered.

type Config

type Config struct {
	Server                string
	Port                  string
	Nick                  string
	Password              string
	MessagesPerSecond     float64
	Rooms                 []string
	Avatar                int
	PluginPrefixes        []string
	PluginSuffixes        []string
	PluginPrefix          *regexp.Regexp
	PluginSuffix          *regexp.Regexp
	CaseInsensitive       bool
	IgnorePrivateMessages bool
	IgnoreChatMessages    bool
}

Config holds the configuration information read from the config.toml file.

type Connection

type Connection struct {
	Bot       *Bot
	Connected bool
	LoginTime map[string]int
	// contains filtered or unexported fields
}

Connection represents the connection to the websocket.

func NewConnection

func NewConnection(b *Bot) *Connection

NewConnection creates a new connection for a bot.

func (*Connection) QueueMessage

func (c *Connection) QueueMessage(msg string)

QueueMessage adds a message to the outgoing queue.

type DefaultEventHandler

type DefaultEventHandler struct {
	Plugin *Plugin
}

DefaultEventHandler is the default event handler that you can make use of if you do not want to encapsulate your plugin with any custom behaviour. Refer to the example plugins to see how you can make use of this.

type DefaultLogger

type DefaultLogger struct {
	AnyLogger
}

DefaultLogger is the default logger type. Formats messages by doing nothing to them.

type DefaultTimedEventHandler

type DefaultTimedEventHandler struct {
	TimedPlugin *TimedPlugin
}

DefaultTimedEventHandler is the default event handler for a TimedPlugin. Refer to the example plugins to see how you can make use of this.

type EventHandler

type EventHandler interface {
	HandleEvent(*Message, []string)
}

EventHandler defines the behaviour and action of any event on a Plugin. Use the DefaultEventHandler unless you want to add custom behaviour. For example, you could keep track of variables that are known globally to the plugin. (Every Plugin event goes through the same handler)

type HasteKey

type HasteKey struct {
	Key string
}

HasteKey holds the JSON unmarshalling of a Hastebin POST request.

type Logger

type Logger interface {
	// contains filtered or unexported methods
}

Logger is an interface that allows for creation your own loggers with custom formatting. See PrettyLogger for how this is done.

type LoggerList

type LoggerList struct {
	Loggers []Logger
}

LoggerList represents a list of Loggers with methods that allow you to log to every one of them as per each loggers' individual logging behaviour.

func NewLoggerList

func NewLoggerList(loggers ...Logger) *LoggerList

NewLoggerList creates a new LoggerList from a slice of Loggers.

type LoggerProvider

type LoggerProvider interface {
	// contains filtered or unexported methods
}

LoggerProvider is an interface used to allow access to the struct fields of AnyLoggers. Somewhat of an golang hack.

type Message

type Message struct {
	Bot       *Bot
	Time      time.Time
	Command   string
	Params    []string
	Timestamp int
	Room      *Room
	User      *User
	Auth      string
	Target    Target
	Message   string
	Matches   map[string]map[*regexp.Regexp][]string
}

Message represents a message sent by a user to either a room the bot is currently in, or to the bot via private messages. A message also defines behaviour in its methods to reply to these messages.

func NewMessage

func NewMessage(s string, bot *Bot) *Message

NewMessage creates a new message and parses the message.

func (*Message) Match

func (m *Message) Match(r *regexp.Regexp, event string) bool

Match adds matches to the message and return true if there was no previous match and if there was indeed a match.

func (*Message) Private

func (m *Message) Private() bool

Private returns true if the message was sent in a private message.

func (*Message) RawReply

func (m *Message) RawReply(res string)

RawReply responds to a message without prepending anything to the message. Note that you should take care to not allow users to influence a raw reply message to do a client command. For this reason, prefer to use Reply unless you are responding with a static message. You may want to event freeze the string.

func (*Message) Reply

func (m *Message) Reply(res string)

Reply responds to a message and prepends the username of the user the bot is responding to.

type Plugin

type Plugin struct {
	Bot          *Bot
	Name         string
	Prefix       *regexp.Regexp
	Suffix       *regexp.Regexp
	Command      string
	NumArgs      int
	Cooldown     time.Duration
	LastUsed     time.Time
	EventHandler EventHandler
	// contains filtered or unexported fields
}

Plugin is a command that triggers on some regexp and performs a task based on the message that triggered it as defined by its EventHandler. It can trigger on several different formats (consider prefix "." and command "cmd"). Note that cooldowns will not affect command syntax, but will ignore the commands hould it have been sent less than Cooldown time from the last instance.

NewPlugin: ".cmd" NewPluginWithArgs:

1 arg:  ".cmd arg0"
2 args: ".cmd arg0, arg1" (space is optional)
n args: ".cmd arg0,arg1,arg2,arg3, ...,arg n" (space is optional)

It is possible to define a Command with a regexp string. For example, a command of "y|n" will trigger on either y or n.

Note that if a command is given as something like s(ay|peak) then the parsed args will be pushed back in the slice, and args[0] will contain the string of either "ay" or "peak", depending on which triggered the message.

Each fired event is run in its own separate goroutine, so for anything that must be run sequentially (ie. cannot read and write to the same file at once) use Bot.Synchronize.

func NewPlugin

func NewPlugin(cmd string) *Plugin

NewPlugin (and its variants) define convenient ways to make new Plugin structs. You must add an event handler after creation. If you want to add custom prefixes or suffixes, you can do it with the Plugin.SetPrefix and Plugin.SetSuffix methods. Otherwise the bot will load prefixes and suffixes from your Config.

NewPlugin in particular creates a Plugin that will trigger on a command.

func NewPluginWithArgs

func NewPluginWithArgs(cmd string, numArgs int) *Plugin

NewPluginWithArgs creates a new Plugin that will trigger on a command, and will parse comma-separated arguments provided along with the command. It will not trigger unless an adequate amount of commas are present.

func NewPluginWithArgsAndCooldown

func NewPluginWithArgsAndCooldown(cmd string, numArgs int, cooldown time.Duration) *Plugin

NewPluginWithArgsAndCooldown creates a new Plugin with arguments and a cooldown as described by both NewPluginWithArgs and NewPluginWithCooldown.

func NewPluginWithCooldown

func NewPluginWithCooldown(cmd string, cooldown time.Duration) *Plugin

NewPluginWithCooldown creates a new Plugin that will trigger on a command, but will not trigger if the last time it was used was not at least the provided time.Duration ago.

func NewPluginWithoutCommand

func NewPluginWithoutCommand() *Plugin

NewPluginWithoutCommand creates a new Plugin that will trigger on every chat and private event.

func (*Plugin) SetEventHandler

func (p *Plugin) SetEventHandler(eh EventHandler)

SetEventHandler sets the EventHandler of the Plugin. Allows you to use a custom EventHandler with any fields you want. The EventHandler of every Plugin MUST be set after the creation of a Plugin.

func (*Plugin) SetPrefix

func (p *Plugin) SetPrefix(prefixes []string)

SetPrefix overrides the Plugin's default Prefix as read by the Config.

func (*Plugin) SetSuffix

func (p *Plugin) SetSuffix(suffixes []string)

SetSuffix overrides the Plugin's default Suffix as read by the Config.

type PrettyLogger

type PrettyLogger struct {
	AnyLogger
}

PrettyLogger logs everything with pretty colours. Looks good with the Solarized terminal theme. Don't like how it looks? Make your own!

type RecentBattles

type RecentBattles struct {
	Battles map[string]BattleInfo
}

RecentBattles holds the roomlist JSON unmarshalling battle map.

type Room

type Room struct {
	Name  string
	Users []string // List of unique SANITIZED names of users in the room
}

Room represents a room with its name and the users currently in it.

func FindRoomEnsured

func FindRoomEnsured(name string, b *Bot) *Room

FindRoomEnsured finds a room if it exists, creates the room if it doesn't.

func (*Room) AddUser

func (r *Room) AddUser(name string)

AddUser adds a user to the room.

func (*Room) RawReply

func (r *Room) RawReply(m *Message, res string)

RawReply responds to a user in a room without prepending their username.

func (*Room) RemoveUser

func (r *Room) RemoveUser(name string)

RemoveUser removes a user from the room.

func (*Room) Reply

func (r *Room) Reply(m *Message, res string)

Reply responds to a user in a chat message and prepends the user's name to the response. The message is sent to the Room of the method's receiver.

type Target

type Target interface {
	Reply(*Message, string)
	RawReply(*Message, string)
}

Target represents either a Room or a User. The distinction is in where the bot will send its message in response.

type TimedEventHandler

type TimedEventHandler interface {
	HandleEvent()
}

TimedEventHandler defines the behaviour and action of any event on a TimedPlugin. Use the DefaultTimedEventHandler unless you want to add custom behaviour.

type TimedPlugin

type TimedPlugin struct {
	Bot               *Bot
	Name              string
	Ticker            *time.Ticker
	Period            time.Duration
	TimedEventHandler TimedEventHandler
	// contains filtered or unexported fields
}

TimedPlugin structs will fire an event on a regular schedule defined by the time.Duration provided to the time.Ticker. Each event is run in its own goroutine, so every event will fire regardless of whether or not the last event ticked had completed. For anything that must be run sequentially (ie. cannot read and write to the same file at once) use Bot.Synchronize.

Because each event fires in its own goroutine, you should take care to not have each event take longer than the duration of the ticker, or else you will be spawning goroutines faster than you can finish them, which is a recipe for disaster.

func NewTimedPlugin

func NewTimedPlugin(period time.Duration) *TimedPlugin

NewTimedPlugin creates a new TimedPlugin that fires events on its TimedEventHandler every given period of time.Duration.

func (*TimedPlugin) SetEventHandler

func (tp *TimedPlugin) SetEventHandler(teh TimedEventHandler)

SetEventHandler sets the TimedEventHandler of the TimedPlugin. The TimedEventHandler of every TimedPlugin MUST be set after its creation.

type User

type User struct {
	Name  string
	Auths map[string]string
}

User represents a user with their username and the auth levels in rooms that the bot knows about.

func FindUserEnsured

func FindUserEnsured(name string, b *Bot) *User

FindUserEnsured finds a user if it exists, creates the user if it doesn't.

func NewUser

func NewUser(name string) *User

NewUser creates a new User, initializing the Auths map.

func (*User) AddAuth

func (u *User) AddAuth(room string, auth string)

AddAuth adds a room authority level to a user.

func (*User) HasAuth

func (u *User) HasAuth(roomname string, level string) bool

HasAuth checks if a user has AT LEAST a given authorization level in a given room.

func (*User) RawReply

func (u *User) RawReply(m *Message, res string)

RawReply responds to a user in private message without prepending their username.

func (*User) Reply

func (u *User) Reply(m *Message, res string)

Reply responds to a user in private message and prepends the user's name to the response.

Directories

Path Synopsis

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL