core

package
v0.0.0-...-daa8be9 Latest Latest
Warning

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

Go to latest
Published: Apr 13, 2023 License: AGPL-3.0 Imports: 24 Imported by: 0

Documentation

Index

Constants

View Source
const (
	AudioPlay int = iota
	AudioLoop
	AudioPause
	AudioStop
)
View Source
const (
	CommandCategoryGames      = "Games"
	CommandCategoryModerators = "Moderators"
	CommandCategoryOther      = "Other"
)

Variables

View Source
var (
	ErrMissingArgs = errors.New("not enough arguments provided")
	ErrSilence     = errors.New("if this error is returned don't send any message")
)
View Source
var (
	VirtualHost     string
	Port            string
	TikTokSessionID string
	YouTubeKey      string
	OpenAIKey       string
	MinGodInterval  time.Duration

	Gin = gin.Default()
)
View Source
var AliasesAdd = []string{
	"add",
	"new",
	"create",
}
View Source
var AliasesDelete = []string{
	"delete",
	"del",
	"remove",
	"rm",
}
View Source
var AliasesEdit = []string{
	"edit",
	"modify",
	"change",
}
View Source
var AliasesList = []string{
	"list",
	"ls",
}
View Source
var AliasesOff = []string{
	"off",
	"false",
	"no",
}
View Source
var AliasesOn = []string{
	"on",
	"true",
	"yes",
}
View Source
var AliasesSearch = []string{
	"search",
	"find",
}
View Source
var AliasesShow = []string{
	"show",
	"view",
	"get",
	"status",
	"state",
	"?",
}
View Source
var Hooks = hooks{}

Hooks are a list of functions that are applied one-by-one to incoming messages. All operations are thread safe.

View Source
var Prefixes = prefixes{}
View Source
var RDB *redis.Client

Functions

func AudioFFmpegBufferPipe

func AudioFFmpegBufferPipe(sp AudioSpeaker, inBuf io.ReadCloser, st *AudioState) error

AudioFFmpegBufferPipe will pipe audio coming from a buffer into ffmpeg and transform into audio that the speaker can transmit.

func AudioFFmpegCommandPipe

func AudioFFmpegCommandPipe(sp AudioSpeaker, cmd *exec.Cmd, st *AudioState) error

AudioFFmpegCommandPipe works exactly like FFmpegBufferPipe except it accepts a command instead of a buffer. Provided just for convenience.

func CacheScope

func CacheScope(key string, getScope func() (int64, error)) (int64, error)

CacheScope returns the scope by looking it up in the cache, if it doesn't exist then it fetches it from the DB using getScope and then caches it. The key should be globally unique.

func Clean

func Clean(s string) string

Clean returns a string with every character except the ones in the a-z, A-Z and 0-9 ranges stripped from the passed string. Assumes ASCII.

func FlagPerson

func FlagPerson(person *int64, f *Flags)

func FlagPlace

func FlagPlace(place *int64, f *Flags)

func Format

func Format(cmd CommandStatic, prefix string) string

Format will return a string representation of the given command in a format that can be shown to a user. Generally used in help messages to point the user to a specific command in order to avoid hardcoding it. Returns the command in the following format:

<prefix><command> [sub-command...] <usage-args>

For example: !command delete <command>

func IsValidURL

func IsValidURL(rawURL string) bool

IsValidURL returns true if the provided string is a valid URL with an http or https scheme and a host.

func OnlyOneBitSet

func OnlyOneBitSet(n int) bool

func Split

func Split(text string, lenCnt func(string) int, lenLim int) []string

Splits a message into submessages. Tries to not split words unless it absolutely has to in which case it splits based on grapheme clusters.

func TypeFlag

func TypeFlag(p *CommandType, value CommandType, f *Flags)

Types

type AudioSpeaker

type AudioSpeaker interface {
	// Enabled returns true if the frontend supports voice chat that the bot can
	// connect to.
	Enabled() bool

	// The audio's expected frame rate.
	FrameRate() int

	// The audio's expected number of channels.
	Channels() int

	// Join the message author's voice channel, if they are not connected to
	// any then returns an error. If in a specific frontend only one voice
	// channel will ever exist then the user doesn't have to be connected to it
	// for the bot to join (for example a discord server with only one voice
	// channel would not apply here as other ones *could* be created at any
	// point).
	Join() error

	// Send audio. Must have connected to a voice channel first, otherwise
	// returns an error.
	Say(buf io.Reader, s *AudioState) error

	// AuthorDeafened returns true if the author that originally made the bot
	// join the voice channel is currently deafened.
	AuthorDeafened() (bool, error)

	// AuthorConnected returns true if the author that originally made the bot
	// join the voice channel is currently connected to that same voice channel.
	AuthorConnected() (bool, error)
}

type AudioState

type AudioState struct {
	gosafe.Value[int]
}

A select with multiple ready cases chooses one pseudo-randomly. So if the goroutine is "slow" to check those channels, you might send a value on both pause and resume (assuming they are buffered) so receiving from both channels could be ready, and resume could be chosen first, and in a later iteration the pause when the goroutine should not be paused anymore.

Source: https://stackoverflow.com/a/60490371

type Author

type Author interface {
	// ID returns the author's ID, this should be a unique, static, identifier
	// in that frontend.
	ID() string

	// Name returns the author's username.
	Name() string

	// DisplayName return's the author's display name. If only usernames exist
	// for that frontend then returns the username.
	DisplayName() string

	// Mention return's a string that mention's the author. This should ideally
	// ping them in some way.
	Mention() string

	// BotAdmin returns true if the author is a bot admin. Otherwise returns
	// false.
	BotAdmin() bool

	// Admin checks if the author is considered an admin. Should return true
	// only if the author has basically every permission.
	Admin() bool

	// Mod checks if the author is considered a moderator. General rule of thumb
	// is that if the author can ban people, then they are mods.
	Mod() bool

	// Subcriber checks if the author is considered a subscriber. General rule of
	// is that if they are paying money in some way, then they are subs. If no
	// such thing exists for the specific frontend, then always returns false.
	Subscriber() bool

	// Scope return's the author's scope. If it doesn't exist it will create it
	// and add it to the database.
	Scope() (author int64, err error)
}

Author is the interface used to abstract a frontend's message author.

type Command

type Command struct {
	CommandStatic
	CommandRuntime
}

func (*Command) Usage

func (cmd *Command) Usage() string

type CommandCategory

type CommandCategory string

type CommandRuntime

type CommandRuntime struct {
	// The "path" taken to invoke the command, i.e. which names were used.
	// Includes all the sub-commands e.g. ["prefix", "add"] in order to be able
	// to display accurate help messages.
	Path []string

	// The arguments passed, includes everything that's not part of the
	// command's name.
	Args []string

	// The prefix used when the command was called.
	Prefix string
}

CommandRuntime holds a command's runtime information.

type CommandStatic

type CommandStatic interface {
	// Type returns the command's type.
	Type() CommandType

	// Permitted will perform checks required for a command to be executed.
	// Returns true if the command is allowed to be executed. Usually used to
	// chcek a user's permissions or to restrict a command to specific
	// frontends.
	Permitted(m *Message) bool

	// Names return a list of all the aliases a command has. The first item in
	// the list is considered the main name and so should be the simplest and
	// most intuitive one for the average person. For example if it's a delete
	// subcommand the first alias should be "delete" instead of "del" or "rm".
	Names() []string

	// Description will return a short description of what the command does.
	Description() string

	// UsageArgs will return the usage arguments. Should follow this format:
	// - <required>
	// - [optional]
	// - (literal-string) or (many | literals)
	UsageArgs() string

	// Category returns the general category the command belongs to. Mainly
	// to make displaying all the commands easier and less overwhelming (as
	// they are split up instead of having them all in a giant list).
	Category() CommandCategory

	// Example returns a list of strings with example usages of the command.
	// Only the arguments passed should be included, not the prefix and the
	// chain of command names.
	Examples() []string

	// Parent returns a command's parent, returns nil if there is no parent.
	Parent() CommandStatic

	// Children returns the command's sub-commands, returns nil if there are no
	// sub-commands.
	Children() CommandsStatic

	// Init is executed during bot startup. Should be used to set things up
	// necessary for the command, for example DB schemas.
	Init() error

	// Run is function that is called to run the command.
	Run(m *Message) (resp any, usrErr error, err error)
}

CommandStatic is the the interface used to implement commands.

type CommandType

type CommandType int
const (
	// A simplified command with might not give full control over something but
	// it has a very easy to use API.
	Normal CommandType = 1 << iota

	// The full command and usually consists of many subcommands which makes it
	// less intuitive for the average person.
	Advanced

	// Bot admin only command used to perform actions like setting an arbitrary
	// person's options, etc.
	Admin

	All = Normal | Advanced | Admin
)

The command types.

type CommandsStatic

type CommandsStatic []CommandStatic
var Commands *CommandsStatic

func (*CommandsStatic) Match

func (cmds *CommandsStatic) Match(t CommandType, m *Message, args []string) (CommandStatic, int, error)

Match will return the corresponding command based on the list of arguments. The arguments don't have to match a command exactly. For example:

args = [prefix add abc]

In this case the prefix's subcommand "add" will be matched and returned. Alongside it the index of the last valid command will be returned (in this case the index of "add", which is 1).

func (CommandsStatic) Recurse

func (cmds CommandsStatic) Recurse(exec func(CommandStatic))

Recurse will recursively go through all of the commands and execute the exec function on them.

func (CommandsStatic) Usage

func (cmds CommandsStatic) Usage() string

Usage returns the names of the children in a format that can be used in the UsageArgs function.

func (CommandsStatic) UsageOptional

func (cmds CommandsStatic) UsageOptional() string

UsageOptions returns the names of the children in a format that can be used in the UsageArgs function and indicates that the sub-commands are optional.

type Flags

type Flags struct {
	FlagSet *flag.FlagSet
	Msg     *Message
	// contains filtered or unexported fields
}

func NewFlags

func NewFlags(m *Message) *Flags

func (*Flags) Parse

func (f *Flags) Parse() ([]string, error)

func (*Flags) Usage

func (f *Flags) Usage()

func (*Flags) Write

func (f *Flags) Write(p []byte) (int, error)

required for the flagSet SetOutput option

type FrontendType

type FrontendType int

type Frontender

type Frontender interface {
	// Type returns the frontend type ID.
	Type() FrontendType

	// Init is responsible for starting up any frontend specific services and
	// connecting to frontend. When it receives the stop signal then it should
	// disconnect from everything.
	Init(wgInit, wgStop *sync.WaitGroup, stop chan struct{})

	// CreateMessage returns a Message object based on the given arguments.
	// Used to send messages that are not direct replies, e.g. reminders.
	CreateMessage(person, place int64, msgID string) (*Message, error)
}

type Frontenders

type Frontenders []Frontender
var Frontends Frontenders

func (Frontenders) CreateMessage

func (fs Frontenders) CreateMessage(person, place int64, msgID string) (*Message, error)

CreateMessage returns a Message object based on the given arguments. It detects what the frontend is based on the place. Used to send messages that are not direct replies, e.g. reminders.

type Here

type Here interface {
	// ID returns the channel's ID, this should be a unique, static, identifier
	// in that frontend.
	ID() string

	// Name return's the channel's name.
	Name() string

	// ScopeExact returns the here's exact scope. See the interface's doc
	// comment for more information on exact scopes.
	ScopeExact() (place int64, err error)

	// ScopeLogical returns the here's logical scope. See the interface's doc
	// comment for more information on logical scopes.
	ScopeLogical() (place int64, err error)
}

Here is the interface used to abstract the place where message came from, e.g. channel, server, etc.

Two type's of scopes exist for places, the exact and the logical. The logical is the area where things are generally expected to work. For example: if a user adds a custom command in a discord server they would probably expect it to work in the entire server and not just in the specific channel that they added it in. If on the other hand someone adds a custom command in a discord DM, then no guild exists and thus the channel's scope would have to be used. On the other hand the exact scope is, as its name suggests, the scope of the exact place the message came from and does not account for context, so using the previous discord server example, it would be the channel's scope where the message came from instead of the server's.

type Message

type Message struct {
	ID       string
	Raw      string
	Frontend Frontender
	Author   Author
	Here     Here
	Client   Messenger
	Speaker  AudioSpeaker
	Command  *Command
}

func Await

func Await(timeout time.Duration, check func(*Message) bool) *Message

Monitor incoming messages until `check` is true or until timeout. If nothing is matched then the returned object will be nil.

func (*Message) CommandParse

func (m *Message) CommandParse() (*Message, error)

func (*Message) CommandRun

func (m *Message) CommandRun() (*Message, error)

func (*Message) Fields

func (m *Message) Fields() []string

func (*Message) FieldsSpace

func (m *Message) FieldsSpace() []string

Split text into fields that include all trailing whitespace. For example: "example of text" will be split into ["example ", "of ", "text"]

func (*Message) Hooks

func (m *Message) Hooks()

func (*Message) Prefixes

func (m *Message) Prefixes() ([]Prefix, bool, error)

Returns the logical here's prefixes and also whether or not they were taken from the database (if not then that means the default ones were used).

func (*Message) RawArgs

func (m *Message) RawArgs(n int) string

Return the arguments including the whitespace between them. Skip over first n args. Pass 0 to not skip any.

func (*Message) Run

func (m *Message) Run()

func (*Message) Usage

func (m *Message) Usage() any

func (*Message) Write

func (m *Message) Write(msg any, usrErr error) (*Message, error)

Sends a message.

type Messenger

type Messenger interface {
	Parse() (*Message, error)

	// Returns the ID of the passed string. The returned ID must be valid.
	// Generally used for verifying an ID's validity and extracting IDs from
	// mentions.
	PlaceID(s string) (id string, err error)
	PersonID(s, placeID string) (id string, err error)

	// Gets the target's scope. If it doesn't exist it will create it and add
	// it to the database.
	Person(id string) (person int64, err error)

	// There exist 2 types of place scopes that are used, the exact place and
	// the logical place. The logical is the area where things are generally
	// expected to work. For example: if a user adds a custom command in a
	// server they would probably expect it to work in the entire server and not
	// just in the specific channel that they added it in. If on the other hand
	// someone adds a custom command in a discord DM message, then no guild
	// exists and thus the channel's scope would have to be used. On the other
	// hand `PlaceExact` returns exactly the scope of the id passed and does not
	// account for context.
	PlaceExact(id string) (place int64, err error)
	PlaceLogical(id string) (place int64, err error)

	Usage(usage string) any

	// Send sends a message to the appropriate scope, resp could be nil
	// depending on the frontend.
	Send(msg any, usrErr error) (resp *Message, err error)

	// Ping works the same as Send except the user is also pinged.
	Ping(msg any, usrErr error) (resp *Message, err error)

	// Write either calls Send or Ping depending on the frontend. This is what
	// should be used in most cases.
	Write(msg any, usrErr error) (resp *Message, err error)
}

The frontend abstraction layer, a frontend needs to implement this in order to be added.

type Prefix

type Prefix struct {
	Type   CommandType
	Prefix string
}

func PlacePrefixes

func PlacePrefixes(place int64) ([]Prefix, bool, error)

Returns the given place's prefixes and also whether or not they were taken from the database (if not then that means the default ones were used).

type SQLDB

type SQLDB struct {
	Lock sync.RWMutex
	DB   *sql.DB
}
var DB *SQLDB

func Open

func Open(driver, source string) (*SQLDB, error)

func (*SQLDB) Close

func (db *SQLDB) Close() error

func (*SQLDB) Init

func (db *SQLDB) Init(schema string) error

func (*SQLDB) PrefixList

func (db *SQLDB) PrefixList(place int64) ([]Prefix, error)

Returns the list of all prefixes for a specific scope.

func (*SQLDB) ScopeAdd

func (_ *SQLDB) ScopeAdd(tx *sql.Tx, frontendID string, frontend int) (int64, error)

func (*SQLDB) ScopeFrontend

func (db *SQLDB) ScopeFrontend(scope int64) (int64, error)

Returns then given scope's frontend id

func (*SQLDB) ScopeID

func (db *SQLDB) ScopeID(scope int64) (string, error)

Returns the given scope's frontend specific ID

func (*SQLDB) SettingPersonGet

func (db *SQLDB) SettingPersonGet(col string, person, place int64) (any, error)

SettingPersonGet returns the value of col in table for the specified person in the specified place.

func (*SQLDB) SettingPersonSet

func (db *SQLDB) SettingPersonSet(col string, person, place int64, val any) error

PlaceSettingSet sets the value of col in table for the specified person in the specified place.

func (*SQLDB) SettingPlaceGet

func (db *SQLDB) SettingPlaceGet(col string, place int64) (any, error)

SettingPlaceGet returns the value of col in table for the specified place.

func (*SQLDB) SettingPlaceSet

func (db *SQLDB) SettingPlaceSet(col string, place int64, val any) error

SettingPlaceSet sets the value of col in table for the specified place.

func (*SQLDB) SettingsPersonGenerate

func (db *SQLDB) SettingsPersonGenerate(person, place int64) error

SettingsPersonGenerate will check if settings for the specified person in the specified place exist, and if not will generate them.

func (*SQLDB) SettingsPlaceGenerate

func (db *SQLDB) SettingsPlaceGenerate(place int64) error

SettingsPlaceGenerate will check if settings for the specified place exist and if not will generate them.

type States

type States struct {
	gosafe.Slice[string]
}

OAuth state parameter

func (*States) Delete

func (s *States) Delete(token string)

func (*States) Generate

func (s *States) Generate() (string, error)

func (*States) New

func (s *States) New() (string, error)

Create a new state and add it to the list of states for 1 minute after which it is removed.

Jump to

Keyboard shortcuts

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