2018-06-04 00:24:34 +02:00
package main
import (
"fmt"
"github.com/bwmarrin/discordgo"
2018-06-08 17:36:35 +02:00
"github.com/deckarep/golang-set"
2018-06-04 00:24:34 +02:00
"strings"
2018-06-06 17:28:43 +02:00
"time"
2018-06-04 00:24:34 +02:00
"log"
"regexp"
)
type CommandType int
2018-06-05 13:47:09 +02:00
// These are used to specify Command.CommandType when registering new commands
2018-06-04 00:24:34 +02:00
const (
CommandTypePrefix CommandType = 0
CommandTypeFullMatch CommandType = 1
CommandTypeRegex CommandType = 2
2018-06-04 02:32:40 +02:00
CommandTypeContains CommandType = 3
2018-06-04 00:24:34 +02:00
)
2018-06-05 13:47:09 +02:00
/ *
This struct represents a command object .
The options should be self - explanatory , but they are also explained in the readme .
A struct can be initialized by passing any number of its attributes as parameters .
Everything not set will be set to the go - usual defaults ( "" for string , 0 for int , false for bool , nil for the rest )
Any command that has a Trigger is valid ( but useless if nothing else is specified )
* /
2018-06-04 00:24:34 +02:00
type Command struct {
Trigger string // must be specified
Output string // no output if unspecified
OutputEmbed * discordgo . MessageEmbed // no embed output if unspecified
Type CommandType // defaults to Prefix
2018-06-05 12:31:26 +02:00
Cooldown int // defaults to 0 (no cooldown)
2018-06-05 13:47:09 +02:00
OutputIsReply bool
RequiresMention bool
DeleteInput bool
DMOnly bool
AdminOnly bool
IgnoreCase bool
2018-06-04 00:24:34 +02:00
// for custom commands that go beyond prints and deletions
Function func ( * discordgo . Session , * discordgo . MessageCreate )
2018-06-05 12:31:26 +02:00
2018-06-08 17:36:35 +02:00
UsersOnCooldown mapset . Set // don’t set this manually (it’s overwritten anyway)
2018-06-04 00:24:34 +02:00
}
2018-06-05 13:47:09 +02:00
// Performs basic input validation on a given command and adds it to the global command array
2018-06-04 00:24:34 +02:00
func registerCommand ( command Command ) {
if command . Trigger == "" {
fmt . Println ( "Cannot register a command with no trigger. Skipping." )
return
}
2018-06-05 11:50:40 +02:00
if command . IgnoreCase {
command . Trigger = strings . ToLower ( command . Trigger )
}
2018-06-08 17:36:35 +02:00
command . UsersOnCooldown = mapset . NewSet ( )
2018-06-04 00:24:34 +02:00
commands = append ( commands , command )
}
2018-06-05 13:47:09 +02:00
/ *
Any message that the bot can read is evaluated here .
The message is matched against each of the command triggers depending on the respective match type .
If one of the commands matches , execute that command and return .
Only one command can be executed per message . Earlier defined commands take precedence .
This is a deliberate choice ( for now ) .
* /
2018-06-04 00:24:34 +02:00
func evaluateMessage ( s * discordgo . Session , m * discordgo . MessageCreate ) {
if m . Author . ID == s . State . User . ID {
log . Printf ( "<Self> %s" , m . Content )
return
}
2018-06-05 12:31:26 +02:00
for i , command := range commands {
2018-06-05 11:50:40 +02:00
content := m . Content
if command . IgnoreCase {
content = strings . ToLower ( content )
}
2018-06-04 02:32:40 +02:00
if command . RequiresMention {
command . Trigger = fmt . Sprintf ( command . Trigger , s . State . User . ID )
}
2018-06-04 00:24:34 +02:00
switch command . Type {
case CommandTypePrefix :
2018-06-05 11:50:40 +02:00
if strings . HasPrefix ( content , command . Trigger ) {
2018-06-05 12:31:26 +02:00
executeCommand ( s , m , command , i )
2018-06-04 00:24:34 +02:00
return
}
case CommandTypeFullMatch :
2018-06-05 11:50:40 +02:00
if content == command . Trigger {
2018-06-05 12:31:26 +02:00
executeCommand ( s , m , command , i )
2018-06-04 00:24:34 +02:00
return
}
case CommandTypeRegex :
2018-06-05 11:50:40 +02:00
match , _ := regexp . MatchString ( command . Trigger , content )
2018-06-04 00:24:34 +02:00
if match {
2018-06-05 12:31:26 +02:00
executeCommand ( s , m , command , i )
2018-06-04 00:24:34 +02:00
return
}
2018-06-04 02:32:40 +02:00
case CommandTypeContains :
2018-06-05 11:50:40 +02:00
if strings . Contains ( content , command . Trigger ) {
2018-06-05 12:31:26 +02:00
executeCommand ( s , m , command , i )
2018-06-04 02:32:40 +02:00
return
}
2018-06-04 00:24:34 +02:00
}
}
}
2018-06-05 13:47:09 +02:00
/ *
Executes the given command on the given message and session .
Sets command cooldowns if necessary and also clears them again .
* /
2018-06-05 12:31:26 +02:00
func executeCommand ( session * discordgo . Session , message * discordgo . MessageCreate , command Command , commandIndex int ) {
2018-06-08 17:36:35 +02:00
if isAdmin ( message . Author ) || // no restrictions for admins
( ! command . AdminOnly && ( isDM ( session , message ) || ! commands [ commandIndex ] . UsersOnCooldown . Contains ( message . Author . ID ) ) &&
( ! command . DMOnly || isDM ( session , message ) ) ) {
2018-06-06 17:28:43 +02:00
2018-06-04 02:32:40 +02:00
log . Printf ( "Executed command %s triggered by user %s" , command . Trigger , userToString ( message . Author ) )
2018-06-08 17:36:35 +02:00
if command . Cooldown > 0 && ! isDM ( session , message ) && ! isAdmin ( message . Author ) {
commands [ commandIndex ] . UsersOnCooldown . Add ( message . Author . ID )
go removeCooldown ( commandIndex , message . Author . ID )
2018-06-05 12:31:26 +02:00
}
2018-06-04 00:24:34 +02:00
if command . Function == nil {
// simple reply
if command . OutputEmbed == nil {
messageContent := generateReply ( message , command )
session . ChannelMessageSend ( message . ChannelID , messageContent )
} else {
session . ChannelMessageSendEmbed ( message . ChannelID , command . OutputEmbed )
}
if command . DeleteInput {
session . ChannelMessageDelete ( message . ChannelID , message . ID )
}
2018-06-04 02:32:40 +02:00
} else {
// execute custom function
command . Function ( session , message )
2018-06-04 00:24:34 +02:00
}
} else {
log . Printf ( "Denied command %s to user %s." , command . Trigger , userToString ( message . Author ) )
}
}
2018-06-08 17:36:35 +02:00
func removeCooldown ( commandIndex int , uid string ) {
2018-06-05 12:31:26 +02:00
time . Sleep ( time . Duration ( commands [ commandIndex ] . Cooldown ) * time . Second )
2018-06-08 17:36:35 +02:00
if commands [ commandIndex ] . UsersOnCooldown . Contains ( uid ) {
commands [ commandIndex ] . UsersOnCooldown . Remove ( uid )
}
2018-06-05 12:31:26 +02:00
}
2018-06-04 00:24:34 +02:00
func generateReply ( message * discordgo . MessageCreate , command Command ) string {
output := command . Output
if command . OutputIsReply {
output = fmt . Sprintf ( output , message . Author . ID )
}
return output
}
2018-06-05 13:47:09 +02:00
/ *
Any message passed to this method will be redirected to config . ModChannel .
This is useful for anonymous complaints or similar messages .
* /
2018-06-04 02:32:40 +02:00
func redirectComplaint ( s * discordgo . Session , m * discordgo . MessageCreate ) {
embed := & discordgo . MessageEmbed {
Author : & discordgo . MessageEmbedAuthor { } ,
Color : 0xbb0000 ,
Description : m . Content ,
}
s . ChannelMessageSendEmbed ( config . ModChannel , embed )
}
2018-06-05 11:50:40 +02:00
func echoMessage ( s * discordgo . Session , m * discordgo . MessageCreate ) {
s . ChannelMessageSend ( m . ChannelID , m . Content )
}
2018-06-05 13:47:09 +02:00
2018-06-06 17:28:43 +02:00
func giveAgeRole ( s * discordgo . Session , m * discordgo . MessageCreate ) {
Member , _ := s . GuildMember ( config . ServerID , m . Author . ID )
dm , _ := s . UserChannelCreate ( Member . User . ID )
for command , role := range config . RoleCommands {
if m . Content == command {
// Found the command that was triggered
2018-06-10 21:06:23 +02:00
// This is a restriction imposed by my own wrapper,
// but working around it is not actually necessary for performance and makes the code uglier in other places.
2018-06-06 17:28:43 +02:00
for _ , newRole := range config . RoleCommands {
for _ , curRole := range Member . Roles {
// If the user already has one of the available roles, tell him and exit
if newRole == curRole {
2018-06-10 21:06:23 +02:00
if curRole == role {
// User is trying to get the role they already have
s . ChannelMessageSend ( dm . ID , "Baka, die Rolle hast du doch schon." )
log . Printf ( "Denied Role %s to %s. User already has %s" , roleName ( s . State , curRole ) , userToString ( m . Author ) , roleName ( s . State , curRole ) )
} else {
s . ChannelMessageSend ( dm . ID , "Baka, du kannst nur eine der Rollen haben." )
log . Printf ( "Denied Role %s to %s. User already has %s" , roleName ( s . State , curRole ) , userToString ( m . Author ) , roleName ( s . State , curRole ) )
}
2018-06-06 17:28:43 +02:00
return
}
}
}
log . Printf ( "Giving Role %s to %s" , roleName ( s . State , role ) , userToString ( m . Author ) )
s . ChannelMessageSend ( dm . ID , "Haaai, Ryoukai desu~" )
s . GuildMemberRoleAdd ( config . ServerID , m . Author . ID , role )
}
}
}