framework

package module
v0.2.1 Latest Latest
Warning

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

Go to latest
Published: Aug 22, 2021 License: LGPL-2.1 Imports: 16 Imported by: 0

README

framework

license GitHub stars Go Reference

A discordgo bot framework with argument parsing, argument typesafety, slash command support, and some QOL message response code. Documentation coming soon™

Documentation

Index

Constants

This section is empty.

Variables

View Source
var (
	TimeRegexes = regex{
		"seconds": regexp2.MustCompile("^[0-9]+s$", 0),
		"minutes": regexp2.MustCompile("^[0-9]+m$", 0),
		"hours":   regexp2.MustCompile("^[0-9]+h$", 0),
		"days":    regexp2.MustCompile("^[0-9]+d$", 0),
		"weeks":   regexp2.MustCompile("^[0-9]+w$", 0),
		"years":   regexp2.MustCompile("[0-9]+y", 0),
		"all":     regexp2.MustCompile("(([0-9]+)(s|m|h|d|w|y))", 0),
	}
	MentionStringRegexes = regex{
		"all":     regexp2.MustCompile("<((@!?\\d+)|(#?\\d+)|(@&?\\d+))>", 0),
		"role":    regexp2.MustCompile("<((@&?\\d+))>", 0),
		"user":    regexp2.MustCompile("<((@!?\\d+))>", 0),
		"channel": regexp2.MustCompile("<((#?\\d+))>", 0),
		"id":      regexp2.MustCompile("^[0-9]{18}", 0),
	}
	TypeGuard = regex{
		"message_url": regexp2.MustCompile("((https:\\/\\/canary.discord.com\\/channels\\/)+([0-9]{18})\\/+([0-9]{18})\\/+([0-9]{18})$)", regexp2.IgnoreCase|regexp2.Multiline),
		"int":         regexp2.MustCompile("\\b(0*(?:[0-9]{1,8}))\\b", 0),
		"boolean":     regexp2.MustCompile("\\b((?:true|false))\\b", 0),
	}
)
View Source
var ColorFailure = 0xF45555

ColorFailure The color to use for response embeds reporting failure

View Source
var ColorSuccess = 0x55F485

ColorSuccess The color to use for response embeds reporting success

View Source
var Guilds = make(map[string]*Guild)

Guilds A map that stores the data for all known guilds We store pointers to the guilds, so that only one guild object is maintained across all contexts Otherwise, there will be information desync

View Source
var MessageState = 500

MessageState Tells discordgo the amount of messages to cache

Session The Discord session, made public so commands can use it

Functions

func AddAdmin

func AddAdmin(userId string)

AddAdmin A function that allows admins to be added, but not removed

func AddChildCommand

func AddChildCommand(info *CommandInfo, function BotFunction)

AddChildCommand Adds a child command to the bot.

func AddCommand

func AddCommand(info *CommandInfo, function BotFunction)

AddCommand Add a command to the bot

func AddDGOHandler

func AddDGOHandler(handler interface{})

AddDGOHandler This provides a way for commands to pass handler functions through to discordgo, and have them added properly during bot startup

func AddSlashCommand

func AddSlashCommand(info *CommandInfo)

AddSlashCommand Adds a slash command to the bot Allows for separation between normal commands and slash commands

func AddSlashCommands

func AddSlashCommands(guildId string, c chan string)

AddSlashCommands Defaults to adding Global slash commands Currently hard coded to guild commands for testing

func AddWorker

func AddWorker(worker func())

AddWorker Given a function that is passed through, append it to the list of worker functions

func CleanId

func CleanId(in string) string

CleanId Given a string, attempt to remove all numbers from it Additionally, ensure it is at least 17 characters in length This is a way of "cleaning" a Discord ping into a valid snowflake string

func CreateButton

func CreateButton(label string, style discordgo.ButtonStyle, customID string, url string, disabled bool) *discordgo.Button

func CreateComponentFields

func CreateComponentFields() []discordgo.MessageComponent

CreateComponentFields Returns a slice of a Message Component, containing a singular ActionsRow

func CreateDropDown

func CreateDropDown(customID string, placeholder string, options []discordgo.SelectMenuOption) discordgo.SelectMenu

func CreateEmbed

func CreateEmbed(color int, title string, description string, fields []*discordgo.MessageEmbedField) *discordgo.MessageEmbed

CreateEmbed Create an embed

func CreateField

func CreateField(name string, value string, inline bool) *discordgo.MessageEmbedField

CreateField Create message field to use for an embed

func EnsureLetters

func EnsureLetters(in string) string

EnsureLetters Given a string, ensure it contains only letters This is useful for stripping numbers from mute durations, and possibly other things

func EnsureNumbers

func EnsureNumbers(in string) string

EnsureNumbers Given a string, ensure it contains only numbers This is useful for stripping letters and formatting characters from user/role pings

func ErrorResponse

func ErrorResponse(i *discordgo.Interaction, errorMsg string, trigger string)

func ExtractCommand

func ExtractCommand(guild *GuildInfo, message string) (*string, *string)

ExtractCommand Given a message, attempt to extract a commands trigger and command arguments out of it If there is no prefix, try using a bot mention as the prefix

func FindAllString

func FindAllString(re *regexp2.Regexp, s string) []string

func GetCommands

func GetCommands() map[string]CommandInfo

GetCommands Provide a way to read commands without making it possible to modify their functions

func GetUser

func GetUser(userId string) (*discordgo.User, error)

GetUser Given a user ID, get that user's object (global to Discord, not in a guild)

func IsAdmin

func IsAdmin(userId string) bool

IsAdmin Allow commands to check if a user is an admin or not Since botAdmins is a boolean map, if they are not in the map, false is the default

func IsCommand

func IsCommand(trigger string) bool

IsCommand Check if a given string is a command registered to the core bot

func ParseInteractionArgs

func ParseInteractionArgs(options []*discordgo.ApplicationCommandInteractionDataOption) *map[string]CommandArg

ParseInteractionArgs Parses Interaction args

func ParseInteractionArgsR

func ParseInteractionArgsR(options []*discordgo.ApplicationCommandInteractionDataOption, args *map[string]CommandArg)

ParseInteractionArgsR Parses interaction args recursively

func ParseTime

func ParseTime(content string) (int, string)

ParseTime Parses time strings

func RemoveGuildSlashCommands

func RemoveGuildSlashCommands(guildID string)

RemoveGuildSlashCommands Removes all guild slash commands.

func RemoveItem

func RemoveItem(slice []string, delete string) []string

RemoveItem Remove an item from a slice by value

func RemoveItems

func RemoveItems(slice []string, indexes []int) []string

RemoveItems Removes items from a slice by index

func ReplyToUser

func ReplyToUser(channelID string, messageSend *discordgo.MessageSend) (*discordgo.Message, error)

func SendErrorReport

func SendErrorReport(guildId string, channelId string, userId string, title string, err error)

SendErrorReport Send an error report as a DM to all of the registered bot administrators

func SetInitProvider

func SetInitProvider(provider func() GuildProvider)

SetInitProvider Sets the init provider

func SetPresence

func SetPresence(presence discordgo.GatewayStatusUpdate)

SetPresence Sets the gateway field for bot presence

func SetTestingId

func SetTestingId(token string)

SetTestingId A function that allows a single id to be added, but not removed

func SetToken

func SetToken(token string)

SetToken A function that allows a single token to be added, but not removed

func Start

func Start()

Start the bot.

Types

type ArgInfo

type ArgInfo struct {
	Match         ArgTypes
	TypeGuard     ArgTypeGuards
	Description   string
	Required      bool
	Flag          bool
	DefaultOption string
	Choices       []string
	Regex         *regexp2.Regexp
}

ArgInfo Describes a CommandInfo argument

type ArgTypeGuards

type ArgTypeGuards string

ArgTypeGuards A way to get type safety in AddArg

var (
	Int       ArgTypeGuards = "int"
	String    ArgTypeGuards = "string"
	Channel   ArgTypeGuards = "channel"
	User      ArgTypeGuards = "user"
	Role      ArgTypeGuards = "role"
	GuildArg  ArgTypeGuards = "guild"
	Message   ArgTypeGuards = "message"
	Boolean   ArgTypeGuards = "bool"
	Id        ArgTypeGuards = "id"
	SubCmd    ArgTypeGuards = "subcmd"
	SubCmdGrp ArgTypeGuards = "subcmdgrp"
	ArrString ArgTypeGuards = "arrString"
	Time      ArgTypeGuards = "time"
)

type ArgTypes

type ArgTypes string

ArgTypes A way to get type safety in AddArg

var (
	ArgOption  ArgTypes = "option"
	ArgContent ArgTypes = "content"
	ArgFlag    ArgTypes = "flag"
)

type Arguments

type Arguments map[string]CommandArg

Arguments Type of the arguments field in the command ctx

func ParseArguments

func ParseArguments(args string, infoArgs *orderedmap.OrderedMap) *Arguments

ParseArguments Version two of the argument parser

type BotFunction

type BotFunction func(ctx *Context)

BotFunction This type defines the functions that are called when commands are triggered Contexts are also passed as pointers, so they are not re-allocated when passed through

type ChildCommand

type ChildCommand map[string]map[string]Command

ChildCommand Defines how child commands are stored

type Command

type Command struct {
	Info     CommandInfo
	Function BotFunction
}

Command The definition of a command, which is that command's information, along with the function it will run

type CommandArg

type CommandArg struct {
	Value interface{}
	// contains filtered or unexported fields
}

CommandArg Describes what a cmd ctx will receive

func (CommandArg) BoolValue

func (ag CommandArg) BoolValue() bool

BoolValue Returns the int value of the arg

func (CommandArg) ChannelValue

func (ag CommandArg) ChannelValue(s *discordgo.Session) (*discordgo.Channel, error)

ChannelValue is a utility function for casting value to a channel struct Returns a channel struct, partial channel struct, or a nil value

func (CommandArg) FloatValue

func (ag CommandArg) FloatValue() float64

FloatValue Returns the int value of the arg

func (CommandArg) Int64Value

func (ag CommandArg) Int64Value() int64

Int64Value Returns the int64 value of the arg

func (CommandArg) IntValue

func (ag CommandArg) IntValue() int

IntValue Returns the int value of the arg

func (CommandArg) MemberValue

func (ag CommandArg) MemberValue(s *discordgo.Session, g string) (*discordgo.Member, error)

MemberValue is a utility function for casting value to a member struct Returns a user struct, partial user struct, or a nil value

func (CommandArg) RoleValue

func (ag CommandArg) RoleValue(s *discordgo.Session, gID string) (*discordgo.Role, error)

RoleValue is a utility function for casting value to a user struct Returns a user struct, partial user struct, or a nil value

func (CommandArg) StringValue

func (ag CommandArg) StringValue() string

StringValue Returns the string value of the arg

func (CommandArg) UserValue

func (ag CommandArg) UserValue(s *discordgo.Session) (*discordgo.User, error)

UserValue is a utility function for casting value to a member struct Returns a user struct, partial user struct, or a nil value

type CommandInfo

type CommandInfo struct {
	Aliases     []string               // Aliases for the normal trigger
	Arguments   *orderedmap.OrderedMap // Arguments for the command
	Description string                 // A short description of what the command does
	Group       Group                  // The group this command belongs to
	ParentID    string                 // The ID of the parent command
	Public      bool                   // Whether non-admins and non-mods can use this command
	IsTyping    bool                   // Whether the command will show a typing thing when ran.
	IsParent    bool                   // If the command is the parent of a subcommand tree
	IsChild     bool                   // If the command is the child
	Trigger     string                 // The string that will trigger the command
}

CommandInfo The definition of a command's info. This is everything about the command, besides the function it will run

func CreateCommandInfo

func CreateCommandInfo(trigger string, description string, public bool, group Group) *CommandInfo

CreateCommandInfo Creates a pointer to a CommandInfo

func CreateRawCmdInfo

func CreateRawCmdInfo(cI *CommandInfo) *CommandInfo

CreateRawCmdInfo Creates a pointer to a CommandInfo

func (*CommandInfo) AddArg

func (cI *CommandInfo) AddArg(argument string, typeGuard ArgTypeGuards, match ArgTypes, description string, required bool, defaultOption string) *CommandInfo

AddArg Adds an arg to the CommandInfo

func (*CommandInfo) AddChoices

func (cI *CommandInfo) AddChoices(arg string, choices []string) *CommandInfo

AddChoices Adds SubCmd choices

func (*CommandInfo) AddCmdAlias

func (cI *CommandInfo) AddCmdAlias(aliases []string) *CommandInfo

AddCmdAlias Adds a list of strings as aliases for the command

func (*CommandInfo) AddFlagArg

func (cI *CommandInfo) AddFlagArg(flag string, typeGuard ArgTypeGuards, match ArgTypes, description string, required bool, defaultOption string) *CommandInfo

AddFlagArg Adds a flag arg, which is a special type of argument This type of argument allows for the user to place the "phrase" (e.g: --debug) anywhere in the command string and the parser will find it.

func (*CommandInfo) CreateAppOptSt

func (cI *CommandInfo) CreateAppOptSt() *discordgo.ApplicationCommandOption

CreateAppOptSt Creates an ApplicationOptionsStruct for all the args.

func (*CommandInfo) SetParent

func (cI *CommandInfo) SetParent(isParent bool, parentID string)

SetParent Sets the parent properties

func (*CommandInfo) SetTyping

func (cI *CommandInfo) SetTyping(isTyping bool) *CommandInfo

type Context

type Context struct {
	Guild       *Guild // NOTE: Guild is a pointer, since we want to use the SAME instance of the guild across the program!
	Cmd         CommandInfo
	Args        Arguments
	Message     *discordgo.Message
	Interaction *discordgo.Interaction
}

Context This is a context of a single command invocation This gives the command function access to all the information it might need

type Group

type Group string

Group Defines different "groups" of commands for ordering in a help command

var (
	Moderation Group = "moderation"
	Utility    Group = "utility"
)

type Guild

type Guild struct {
	ID   string
	Info GuildInfo
}

Guild The definition of a guild, which is simply its ID and Info

func (*Guild) AddChannelToIgnored

func (g *Guild) AddChannelToIgnored(channelId string) error

AddChannelToIgnored Add a channel to the list of channels that are ignored (where commands can't be run)

func (*Guild) AddChannelToWhitelist

func (g *Guild) AddChannelToWhitelist(channelId string) error

AddChannelToWhitelist Add a channel to the list of channels that are whitelisted (where commands can be run)

func (*Guild) AddMemberOrRoleToIgnored

func (g *Guild) AddMemberOrRoleToIgnored(addId string) error

AddMemberOrRoleToIgnored Add a user OR role ID to the list of ignored IDs

func (*Guild) AddMemberOrRoleToWhitelist

func (g *Guild) AddMemberOrRoleToWhitelist(addId string) error

AddMemberOrRoleToWhitelist Add a member OR role ID to the list of whitelisted ids

func (*Guild) AddMod

func (g *Guild) AddMod(addId string) error

AddMod Add a user or role ID as a moderator to the bot

func (*Guild) Ban

func (g *Guild) Ban(userId string, reason string, deleteDays int) error

Ban Bans a user, who may not be a member

func (*Guild) ChannelIsIgnored

func (g *Guild) ChannelIsIgnored(channelId string) bool

ChannelIsIgnored Determine if a channel ID is ignored. Return false if the ignore list is empty

func (*Guild) ChannelIsWhitelisted

func (g *Guild) ChannelIsWhitelisted(channelId string) bool

ChannelIsWhitelisted Determine if a channel ID is whitelisted. Return true if the whitelist is empty

func (*Guild) CommandIsDisabledInChannel

func (g *Guild) CommandIsDisabledInChannel(command string, channelId string) bool

CommandIsDisabledInChannel Check if a given command is disabled in the given channel

func (*Guild) DisableCommandGlobally

func (g *Guild) DisableCommandGlobally(command string) error

DisableCommandGlobally Add a command to the list of *globally disabled* commands

func (*Guild) DisableCommandInChannel

func (g *Guild) DisableCommandInChannel(command string, channelId string) error

DisableCommandInChannel Given a command and channel ID, add that command to that channel's list of blocked commands

func (*Guild) EnableCommandGlobally

func (g *Guild) EnableCommandGlobally(trigger string) error

EnableCommandGlobally Remove a command from the list of *globally disabled* triggers

func (*Guild) EnableCommandInChannel

func (g *Guild) EnableCommandInChannel(command string, channelId string) error

EnableCommandInChannel Given a command and channel ID, remove that command from that channel's list of blocked comamnds

func (*Guild) GetChannel

func (g *Guild) GetChannel(channelId string) (*discordgo.Channel, error)

GetChannel Retrieve a single channel belonging to this guild This function handles cleaning of the string so you don't have to

func (*Guild) GetCommandUsage

func (g *Guild) GetCommandUsage(cmd CommandInfo) string

GetCommandUsage // Compile the usage information for a single command, so it can be printed out

func (*Guild) GetInt64

func (g *Guild) GetInt64(key string) (int64, error)

GetInt64 Retrieve an int64 from this guild's arbitrary storage, and error if the cast fails

func (*Guild) GetMap

func (g *Guild) GetMap(key string) (map[string]interface{}, error)

GetMap Get a map from this guild's arbitrary storage, and error if the cast fails

func (*Guild) GetMember

func (g *Guild) GetMember(userId string) (*discordgo.Member, error)

GetMember Convenience function to get a member in this guild This function handles cleaning of the string so you don't have to

func (*Guild) GetRole

func (g *Guild) GetRole(roleId string) (*discordgo.Role, error)

GetRole Convenience function to get a single role in this guild This function handles cleaning of the string so you don't have to

func (*Guild) GetString

func (g *Guild) GetString(key string) (string, error)

GetString Retrieve a string from this guild's arbitrary storage, and error if the cast fails

func (*Guild) HasRole

func (g *Guild) HasRole(userId string, roleId string) bool

HasRole Determine if a given user ID has a certain role in this guild

func (*Guild) IsChannel

func (g *Guild) IsChannel(channelId string) bool

IsChannel Determine whether or not a given channelId is a valid channel in this guild

func (*Guild) IsGloballyDisabled

func (g *Guild) IsGloballyDisabled(trigger string) bool

IsGloballyDisabled Check if a given command is globally disabled

func (*Guild) IsMember

func (g *Guild) IsMember(userId string) bool

IsMember Determine whether or not a given userId is a member in this guild

func (*Guild) IsMod

func (g *Guild) IsMod(checkId string) bool

IsMod Check if a given ID is a moderator or not

func (*Guild) IsRole

func (g *Guild) IsRole(roleId string) bool

IsRole Determine whether or not a given roleId is a valid role in this guild

func (*Guild) Kick

func (g *Guild) Kick(userId string, reason string) error

Kick Kicks a member

func (*Guild) MemberOrRoleInList

func (g *Guild) MemberOrRoleInList(checkId string, list []string) bool

MemberOrRoleInList This is a higher-level function specifically for the Moderator, Ignored, and Whitelist checks Check if a given ID - member or role - exists in a given list, while automatically checking member roles if necessary

func (*Guild) MemberOrRoleIsIgnored

func (g *Guild) MemberOrRoleIsIgnored(checkId string) bool

MemberOrRoleIsIgnored Determine if a given user or role ID is on the ignored list, OR if they have a role on the ignored list On error, treat as if they are on this list

func (*Guild) MemberOrRoleIsWhitelisted

func (g *Guild) MemberOrRoleIsWhitelisted(checkId string) bool

MemberOrRoleIsWhitelisted Check if a given user or role is whitelisted If the whitelist is empty, return true

func (*Guild) PurgeChannel

func (g *Guild) PurgeChannel(channelId string, deleteCount int) (int, error)

PurgeChannel Purge the last N messages in a given channel, regardless of user

func (*Guild) PurgeUser

func (g *Guild) PurgeUser(userId string, deleteCount int) (int, error)

PurgeUser PurgeUser a user's messages in any channel

func (*Guild) PurgeUserInChannel

func (g *Guild) PurgeUserInChannel(userId string, channelId string, deleteCount int) (int, error)

PurgeUserInChannel Purge a user's messages in a certain channel Delete deleteCount messages, searching through a maximum of searchCount messages

func (*Guild) RemoveChannelFromIgnored

func (g *Guild) RemoveChannelFromIgnored(channelId string) error

RemoveChannelFromIgnored Remove a channel from the list of channels that are ignored (where commands can't be run)

func (*Guild) RemoveChannelFromWhitelist

func (g *Guild) RemoveChannelFromWhitelist(channelId string) error

RemoveChannelFromWhitelist Remove a channel from the list of channels that are whitelisted (where commands can be run)

func (*Guild) RemoveMemberOrRoleFromIgnored

func (g *Guild) RemoveMemberOrRoleFromIgnored(remId string) error

RemoveMemberOrRoleFromIgnored Remove a given ID from the list of ignored IDs

func (*Guild) RemoveMemberOrRoleFromWhitelist

func (g *Guild) RemoveMemberOrRoleFromWhitelist(remId string) error

RemoveMemberOrRoleFromWhitelist Remove a given ID from the list of whitelisted IDs

func (*Guild) RemoveMod

func (g *Guild) RemoveMod(remId string) error

RemoveMod Remove a user or role ID from the list of bot moderators

func (*Guild) SetDeletePolicy

func (g *Guild) SetDeletePolicy(policy bool)

SetDeletePolicy Set the delete policy, then save the guild data

func (*Guild) SetPrefix

func (g *Guild) SetPrefix(newPrefix string)

SetPrefix Set the prefix, then save the guild data

func (*Guild) SetResponseChannel

func (g *Guild) SetResponseChannel(channelId string) error

SetResponseChannel Check that the channel exists, set the response channel, then save the guild data

func (*Guild) StoreInt64

func (g *Guild) StoreInt64(key string, value int64)

StoreInt64 Store an int64 to this guild's arbitrary storage

func (*Guild) StoreMap

func (g *Guild) StoreMap(key string, value map[string]interface{})

StoreMap Store a map to this guild's arbitrary storage

func (*Guild) StoreString

func (g *Guild) StoreString(key string, value string)

StoreString Store a string to this guild's arbitrary storage

type GuildInfo

type GuildInfo struct {
	AddedDate               int64                  `json:"added_date"`
	ChannelDisabledCommands map[string][]string    `json:"channel_disabled_commands"`
	DeletePolicy            bool                   `json:"delete_policy"`
	GlobalDisabledCommands  []string               `json:"global_disabled_commands"`
	IgnoredChannels         []string               `json:"ignored_channels"`
	IgnoredIds              []string               `json:"ignored_ids"`
	ModeratorIds            []string               `json:"moderator_ids"`
	Prefix                  string                 `json:"prefix,"`
	ResponseChannelId       string                 `json:"response_channel_id"`
	Storage                 map[string]interface{} `json:"storage"`
	WhitelistedChannels     []string               `json:"whitelisted_channels"`
	WhitelistIds            []string               `json:"whitelist_ids"`
}

GuildInfo This is all the settings and data that needs to be stored about a single guild

type GuildProvider

type GuildProvider struct {
	Save func(guild *Guild)
	Load func() map[string]*Guild
}

GuildProvider Type that holds functions that can be easily modified to support a wide range of storage types

type Response

type Response struct {
	Ctx                *Context
	Success            bool
	Loading            bool
	Ephemeral          bool
	Reply              bool
	Embed              *discordgo.MessageEmbed
	ResponseComponents *ResponseComponents
}

Response The Response type, can be build and sent to a given guild

func NewResponse

func NewResponse(ctx *Context, messageComponents bool, ephemeral bool) *Response

NewResponse Create a response object for a guild, which starts off as an empty Embed which will have fields added to it The response starts with some "auditing" information The embed will be finalized in .Send()

func (*Response) AcknowledgeInteraction

func (r *Response) AcknowledgeInteraction()

func (*Response) AppendButton

func (r *Response) AppendButton(label string, style discordgo.ButtonStyle, url string, customID string, rowID int)

AppendButton Appends a button

func (*Response) AppendDropDown

func (r *Response) AppendDropDown(customID string, placeholder string, noNewRow bool)

AppendDropDown Adds a DropDown component

func (*Response) AppendField

func (r *Response) AppendField(name string, value string, inline bool)

AppendField Create a new basic field and append it to an existing Response

func (*Response) AppendUsage

func (r *Response) AppendUsage()

AppendUsage Add the command usage to the response. Intended for syntax error responses

func (*Response) PrependField

func (r *Response) PrependField(name string, value string, inline bool)

PrependField Create a new basic field and prepend it to an existing Response

func (*Response) Send

func (r *Response) Send(success bool, title string, description string)

Send Send a compiled response

type ResponseComponents

type ResponseComponents struct {
	Components        []discordgo.MessageComponent
	SelectMenuOptions []discordgo.SelectMenuOption
}

ResponseComponents Stores the components for response allows for functions to add data

Directories

Path Synopsis
providers
fs

Jump to

Keyboard shortcuts

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