2018-06-04 00:24:34 +02:00
package main
import (
2019-01-10 00:16:39 +01:00
"fmt"
"github.com/bwmarrin/discordgo"
"github.com/deckarep/golang-set"
"log"
"regexp"
"strings"
"time"
2018-06-04 00:24:34 +02:00
)
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 (
2019-01-10 00:16:39 +01:00
CommandTypePrefix CommandType = 0
CommandTypeFullMatch CommandType = 1
CommandTypeRegex CommandType = 2
CommandTypeContains CommandType = 3
2018-06-04 00:24:34 +02:00
)
2019-01-10 00:16:39 +01: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 {
2019-01-10 00:16:39 +01:00
Trigger string // must be specified
Output string // no output if unspecified
OutputEmbed * discordgo . MessageEmbed // no embed output if unspecified
Type CommandType // defaults to Prefix
Cooldown int // defaults to 0 (no cooldown)
OutputIsReply bool
RequiresMention bool
DeleteInput bool
DMOnly bool
AdminOnly bool
IgnoreCase bool
// for custom commands that go beyond prints and deletions
2019-01-27 18:20:41 +01:00
Function func ( * discordgo . Session , * discordgo . MessageCreate )
AllowedChannels mapset . Set // allowed everywhere if blank
2019-01-10 00:16:39 +01: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 ) {
2019-01-10 00:16:39 +01:00
if command . Trigger == "" {
fmt . Println ( "Cannot register a command with no trigger. Skipping." )
return
}
if command . IgnoreCase {
command . Trigger = strings . ToLower ( command . Trigger )
}
command . UsersOnCooldown = mapset . NewSet ( )
2019-03-02 13:25:39 +01:00
if command . AllowedChannels == nil {
command . AllowedChannels = mapset . NewSet ( )
}
2019-01-10 00:16:39 +01:00
commands = append ( commands , & command )
2018-06-04 00:24:34 +02:00
}
2019-01-10 00:16:39 +01: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 ) {
2019-01-10 00:16:39 +01:00
if m . Author . ID == s . State . User . ID {
2019-01-27 17:09:29 +01:00
// Properly log embeds
if len ( m . Embeds ) > 0 {
log . Printf ( "<Self> %s" , m . Embeds [ 0 ] . Description )
} else {
log . Printf ( "<Self> %s" , m . Content )
}
2019-01-10 00:16:39 +01:00
return
}
for _ , command := range commands {
content := m . Content
if command . IgnoreCase {
content = strings . ToLower ( content )
}
if command . RequiresMention {
command . Trigger = fmt . Sprintf ( command . Trigger , s . State . User . ID )
}
switch command . Type {
case CommandTypePrefix :
if strings . HasPrefix ( content , command . Trigger ) {
executeCommand ( s , m , command )
return
}
case CommandTypeFullMatch :
if content == command . Trigger {
executeCommand ( s , m , command )
return
}
case CommandTypeRegex :
match , _ := regexp . MatchString ( command . Trigger , content )
if match {
executeCommand ( s , m , command )
return
}
case CommandTypeContains :
if strings . Contains ( content , command . Trigger ) {
executeCommand ( s , m , command )
return
}
}
}
2018-06-04 00:24:34 +02:00
}
2019-01-10 00:16:39 +01:00
/ *
Executes the given command on the given message and session .
Sets command cooldowns if necessary and also clears them again .
* /
2018-06-29 16:50:28 +02:00
func executeCommand ( session * discordgo . Session , message * discordgo . MessageCreate , command * Command ) {
2019-01-27 18:20:41 +01:00
if commandAllowed ( session , message , command ) {
2019-01-10 00:16:39 +01:00
log . Printf ( "Executed command %s triggered by user %s" , command . Trigger , userToString ( message . Author ) )
if command . Cooldown > 0 && ! isDM ( session , message ) && ! isAdmin ( message . Author ) {
command . UsersOnCooldown . Add ( message . Author . ID )
go removeCooldown ( command , message . Author . ID )
}
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 {
deleteAndSendViaDM ( session , message )
}
} else {
// execute custom function
command . Function ( session , message )
}
} else {
log . Printf ( "Denied command %s to user %s." , command . Trigger , userToString ( message . Author ) )
}
2018-06-04 00:24:34 +02:00
}
2019-01-27 18:20:41 +01:00
/ *
* Check if a user has the permission to trigger a given command .
* To be honest , this whole logic is a mess , but I don ’ t know a better way to handle it .
* /
func commandAllowed ( session * discordgo . Session , message * discordgo . MessageCreate , command * Command ) bool {
// no restrictions for admins
if isAdmin ( message . Author ) {
return true
}
// blacklist admin commands for everyone else
if command . AdminOnly {
return false
}
// cooldowns are irrelevant in DMs
if ! isDM ( session , message ) && command . UsersOnCooldown . Contains ( message . Author . ID ) {
return false
}
// the command is not limited to DMs or we are inside a DM chat
if command . DMOnly && ! isDM ( session , message ) {
return false
}
// no allowed channels = all channels are allowed.
// DMs are whitelisted by default
2019-03-02 13:25:39 +01:00
if command . AllowedChannels . Cardinality ( ) != 0 &&
! command . AllowedChannels . Contains ( message . ChannelID ) &&
isDM ( session , message ) {
2019-01-27 18:20:41 +01:00
return false
}
return true
}
2018-06-29 16:50:28 +02:00
func removeCooldown ( command * Command , uid string ) {
2019-01-10 00:16:39 +01:00
time . Sleep ( time . Duration ( command . Cooldown ) * time . Second )
if command . UsersOnCooldown . Contains ( uid ) {
command . UsersOnCooldown . Remove ( uid )
}
2018-06-05 12:31:26 +02:00
}
2018-12-30 16:35:18 +01:00
func deleteAndSendViaDM ( s * discordgo . Session , message * discordgo . MessageCreate ) {
2019-01-10 00:16:39 +01:00
s . ChannelMessageDelete ( message . ChannelID , message . ID )
dm := getDMChannelFromMessage ( s , message )
s . ChannelMessageSend ( dm . ID , "Deine Nachricht wurde gelöscht, weil sie ein verbotenes Wort enthielt. Falls du sie editieren und erneut abschicken willst, hier die Nachricht:" )
s . ChannelMessageSend ( dm . ID , message . Content )
2018-12-30 16:35:18 +01:00
}
2018-06-29 16:50:28 +02:00
func generateReply ( message * discordgo . MessageCreate , command * Command ) string {
2019-01-10 00:16:39 +01:00
output := command . Output
if command . OutputIsReply {
output = fmt . Sprintf ( output , message . Author . ID )
}
return output
2018-06-04 00:24:34 +02:00
}
2019-01-10 00:16:39 +01:00
/ *
2019-01-27 17:09:29 +01:00
* I ’ m beginning to doubt my own self - imposed limitations of
* only allowing func ( session , message ) to be attached to commands ,
* but refactoring that might be more effort than it ’ s worth .
* Hence , small wrappers around the redirect function .
* /
func modComplain ( s * discordgo . Session , m * discordgo . MessageCreate ) {
redirectMessage ( s , m , config . ModChannel , true )
2019-01-10 00:16:39 +01:00
dm , _ := s . UserChannelCreate ( m . Author . ID )
s . ChannelMessageSend ( dm . ID , config . ComplaintReceivedMessage )
2018-06-04 02:32:40 +02:00
}
2019-01-27 17:09:29 +01:00
func selphyComplain ( s * discordgo . Session , m * discordgo . MessageCreate ) {
2019-01-10 00:16:39 +01:00
dm_target , _ := s . UserChannelCreate ( "190958368301645824" )
2019-01-27 17:09:29 +01:00
redirectMessage ( s , m , dm_target . ID , true )
2019-01-10 00:16:39 +01:00
dm , _ := s . UserChannelCreate ( m . Author . ID )
s . ChannelMessageSend ( dm . ID , config . ComplaintReceivedMessage )
2018-07-26 00:02:06 +02:00
}
2019-01-27 17:09:29 +01:00
func redirectMessage ( s * discordgo . Session , m * discordgo . MessageCreate , target string , isEmbed bool ) {
if isEmbed {
embed := & discordgo . MessageEmbed {
// Embed are anonymized by default for now. Fits the use case.
Author : & discordgo . MessageEmbedAuthor { } ,
Color : 0xbb0000 ,
Description : m . Content ,
}
s . ChannelMessageSendEmbed ( target , embed )
return
}
s . ChannelMessageSend ( target , messageToString ( m . Message ) )
}
2018-06-05 11:50:40 +02:00
func echoMessage ( s * discordgo . Session , m * discordgo . MessageCreate ) {
2019-01-10 00:16:39 +01:00
s . ChannelMessageSend ( m . ChannelID , m . Content )
2018-06-05 11:50:40 +02:00
}
2018-06-05 13:47:09 +02:00
2018-06-06 17:28:43 +02:00
func giveAgeRole ( s * discordgo . Session , m * discordgo . MessageCreate ) {
2019-01-10 00:16:39 +01:00
Member , _ := s . GuildMember ( config . ServerID , m . Author . ID )
dm , _ := s . UserChannelCreate ( Member . User . ID )
2019-06-08 05:30:49 +02:00
required := mapset . NewSetWith ( "416184227672096780" , "416184208470310922" , "416184150404628480" , "416184132473847810" , "440996904948465664" )
2019-01-10 00:16:39 +01:00
for command , role := range config . RoleCommands {
if m . Content == command {
// Found the command that was triggered
// 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.
for _ , newRole := range config . RoleCommands {
2019-06-08 05:30:49 +02:00
// check if the user has a twitch sub or any of the patreon roles
isAllowed := false
for _ , curRole := range Member . Roles {
if required . Contains ( curRole ) {
isAllowed = true
}
}
if ! isAllowed {
s . ChannelMessageSend ( dm . ID , "Du kannst dir keine Rolle zuweisen, da weder Patreon noch Twitch mit deinem Account verlinkt ist oder du Selphy auf keiner dieser Plattformen unterstützt. Bei Problemen wende dich bitte an die `!mods`." )
log . Printf ( "Denied role %s to %s. User is neither patron nor sub." , roleName ( s . State , newRole ) , userToString ( m . Author ) )
return
}
2019-01-10 00:16:39 +01:00
for _ , curRole := range Member . Roles {
2019-01-13 11:37:44 +01:00
// If the user already has one of the available roles, tell them and exit
2019-01-10 00:16:39 +01:00
if newRole == curRole {
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 ) )
}
return
}
}
}
s . GuildMemberRoleAdd ( config . ServerID , m . Author . ID , role )
2019-01-13 11:37:44 +01:00
s . ChannelMessageSend ( dm . ID , "Haaai, Ryoukai desu~" )
2019-03-02 10:15:33 +01:00
log . Printf ( "Giving Role %s to %s" , roleName ( s . State , role ) , userToString ( m . Author ) )
2019-01-10 00:16:39 +01:00
}
}
2018-06-06 17:28:43 +02:00
}
2018-06-19 11:54:29 +02:00
func getHelpEmbed ( ) * discordgo . MessageEmbed {
2019-01-10 00:16:39 +01:00
commandList := "Im Folgenden findest du eine automatisch generierte Liste aller Commands. Um herauszufinden, was sie tun, probiere sie aus oder lies den Source Code (siehe unten).\n```- !complain\n- !scomplain\n"
for _ , command := range commands {
if command . Type != CommandTypeRegex && ! command . AdminOnly && ! command . DMOnly {
commandList += "- " + command . Trigger + "\n"
}
}
commandList += "```"
embed := & discordgo . MessageEmbed {
Author : & discordgo . MessageEmbedAuthor { } ,
Color : 0xffb90f ,
Description : "__Hilfe__" ,
Fields : [ ] * discordgo . MessageEmbedField {
& discordgo . MessageEmbedField {
Name : "__Commands__" ,
Value : commandList ,
Inline : true ,
} ,
& discordgo . MessageEmbedField {
Name : "__Bugs__" ,
Value : fmt . Sprintf ( "Bei Fragen zum Bot, Vorschlägen, Bugs etc. wende dich bitte an <@%s> oder öffne eine Issue auf https://git.kageru.moe/kageru/discord-selphybot." , config . Admins [ 0 ] ) ,
Inline : true ,
} ,
} ,
Thumbnail : & discordgo . MessageEmbedThumbnail {
URL : "https://static-cdn.jtvnw.net/emoticons/v1/1068185/3.0" ,
} ,
}
return embed
2018-06-19 11:54:29 +02:00
}