diff --git a/go.mod b/go.mod index e92dab9..c2b4451 100644 --- a/go.mod +++ b/go.mod @@ -5,9 +5,11 @@ go 1.22.0 require ( github.com/bwmarrin/discordgo v0.27.1 github.com/joho/godotenv v1.5.1 + github.com/juunini/palworld-rcon v1.0.0 ) require ( + github.com/gorcon/rcon v1.3.5 // indirect github.com/gorilla/websocket v1.4.2 // indirect golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b // indirect golang.org/x/sys v0.0.0-20201119102817-f84b799fce68 // indirect diff --git a/go.sum b/go.sum index 7d1da0c..ce9d86f 100644 --- a/go.sum +++ b/go.sum @@ -1,9 +1,13 @@ github.com/bwmarrin/discordgo v0.27.1 h1:ib9AIc/dom1E/fSIulrBwnez0CToJE113ZGt4HoliGY= github.com/bwmarrin/discordgo v0.27.1/go.mod h1:NJZpH+1AfhIcyQsPeuBKsUtYrRnjkyu0kIVMCHkZtRY= +github.com/gorcon/rcon v1.3.5 h1:YE/Vrw6R99uEP08wp0EjdPAP3Jwz/ys3J8qxI1nYoeU= +github.com/gorcon/rcon v1.3.5/go.mod h1:zR1qfKZttF8vAgH1NsP6CdpachOvLDq8jE64NboTpIM= github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/juunini/palworld-rcon v1.0.0 h1:H5R6ZW216xepxwQa+EVMexR+5pvchIUa2sQJx6Pa93o= +github.com/juunini/palworld-rcon v1.0.0/go.mod h1:hN2aABVdCDpOjea6aPynO6nEM64CKSss+60evi2jQhY= golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b h1:7mWr3k41Qtv8XlltBkDkl8LoP3mpSgBW8BUoxtEdbXg= golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= diff --git a/src/bot/commands/ban.go b/src/bot/commands/ban.go new file mode 100644 index 0000000..360f908 --- /dev/null +++ b/src/bot/commands/ban.go @@ -0,0 +1,30 @@ +package commands + +import ( + "strings" + + "github.com/juunini/palworld-discord-bot/src/i18n" + "github.com/juunini/palworld-discord-bot/src/utils" +) + +func ban(command string) string { + steamID, found := strings.CutPrefix(command, "ban ") + + if !found { + return i18n.WrongParameters + } + + client, err := utils.RconClient() + if err != nil { + return i18n.FailedToConnectRconServer + } + + defer client.Disconnect() + + response, err := client.BanPlayer(steamID) + if err != nil { + return i18n.FailedToBanCommand + } + + return response +} diff --git a/src/bot/commands/broadcast.go b/src/bot/commands/broadcast.go new file mode 100644 index 0000000..0aa2f28 --- /dev/null +++ b/src/bot/commands/broadcast.go @@ -0,0 +1,30 @@ +package commands + +import ( + "strings" + + "github.com/juunini/palworld-discord-bot/src/i18n" + "github.com/juunini/palworld-discord-bot/src/utils" +) + +func broadcast(command string) string { + message, found := strings.CutPrefix(command, "broadcast ") + + if !found { + return i18n.WrongParameters + } + + client, err := utils.RconClient() + if err != nil { + return i18n.FailedToConnectRconServer + } + + defer client.Disconnect() + + response, err := client.Broadcast(message) + if err != nil { + return i18n.FailedToBroadcastCommand + } + + return response +} diff --git a/src/bot/commands/doExit.go b/src/bot/commands/doExit.go new file mode 100644 index 0000000..00f357f --- /dev/null +++ b/src/bot/commands/doExit.go @@ -0,0 +1,24 @@ +package commands + +import ( + "github.com/juunini/palworld-discord-bot/src/console_decoration" + "github.com/juunini/palworld-discord-bot/src/i18n" + "github.com/juunini/palworld-discord-bot/src/utils" +) + +func doExit() string { + client, err := utils.RconClient() + if err != nil { + return i18n.FailedToConnectRconServer + } + + defer client.Disconnect() + + response, err := client.DoExit() + if err != nil { + console_decoration.PrintError(i18n.FailedToDoExitCommand + ": " + err.Error()) + return i18n.FailedToDoExitCommand + } + + return response +} diff --git a/src/bot/commands/help.go b/src/bot/commands/help.go deleted file mode 100644 index 63e3e4c..0000000 --- a/src/bot/commands/help.go +++ /dev/null @@ -1,7 +0,0 @@ -package commands - -import "github.com/juunini/palworld-discord-bot/src/i18n" - -func Help() string { - return i18n.Help -} diff --git a/src/bot/commands/kick.go b/src/bot/commands/kick.go new file mode 100644 index 0000000..a835176 --- /dev/null +++ b/src/bot/commands/kick.go @@ -0,0 +1,30 @@ +package commands + +import ( + "strings" + + "github.com/juunini/palworld-discord-bot/src/i18n" + "github.com/juunini/palworld-discord-bot/src/utils" +) + +func kick(command string) string { + steamID, found := strings.CutPrefix(command, "kick ") + + if !found { + return i18n.WrongParameters + } + + client, err := utils.RconClient() + if err != nil { + return i18n.FailedToConnectRconServer + } + + defer client.Disconnect() + + response, err := client.KickPlayer(steamID) + if err != nil { + return i18n.FailedToKickCommand + } + + return response +} diff --git a/src/bot/commands/response.go b/src/bot/commands/response.go index b6c7c18..7dccfa6 100644 --- a/src/bot/commands/response.go +++ b/src/bot/commands/response.go @@ -4,14 +4,36 @@ import ( "strings" "github.com/juunini/palworld-discord-bot/src/config" + "github.com/juunini/palworld-discord-bot/src/i18n" + "github.com/juunini/palworld-discord-bot/src/utils" ) -func Response(message string) string { - command, _ := strings.CutPrefix(message, config.DISCORD_COMMAND_PREFIX+" ") +func Response(message string, username string) string { + isAdmin := utils.IsAdmin(username) - if command == "help" { - return Help() + command, found := strings.CutPrefix(message, config.DISCORD_COMMAND_PREFIX+" ") + if command == "help" || !found { + return i18n.Help(config.DISCORD_COMMAND_PREFIX, isAdmin) } - return "Unknown command" + // Under commands, only admins can execute + if !isAdmin { + return i18n.UnknownCommand + } + + if command == "kick" { + return kick(command) + } else if command == "ban" { + return ban(command) + } else if strings.HasPrefix(command, "broadcast") { + return broadcast(command) + } else if strings.HasPrefix(command, "shutdown") { + return shutdown(command) + } else if command == "doExit" { + return doExit() + } else if command == "save" { + return save() + } + + return i18n.UnknownCommand } diff --git a/src/bot/commands/save.go b/src/bot/commands/save.go new file mode 100644 index 0000000..bc4d24f --- /dev/null +++ b/src/bot/commands/save.go @@ -0,0 +1,24 @@ +package commands + +import ( + "github.com/juunini/palworld-discord-bot/src/console_decoration" + "github.com/juunini/palworld-discord-bot/src/i18n" + "github.com/juunini/palworld-discord-bot/src/utils" +) + +func save() string { + client, err := utils.RconClient() + if err != nil { + return i18n.FailedToConnectRconServer + } + + defer client.Disconnect() + + response, err := client.Save() + if err != nil { + console_decoration.PrintError(i18n.FailedToSaveCommand + ": " + err.Error()) + return i18n.FailedToSaveCommand + } + + return response +} diff --git a/src/bot/commands/shutdown.go b/src/bot/commands/shutdown.go new file mode 100644 index 0000000..1209bf6 --- /dev/null +++ b/src/bot/commands/shutdown.go @@ -0,0 +1,50 @@ +package commands + +import ( + "strings" + + "github.com/juunini/palworld-discord-bot/src/console_decoration" + "github.com/juunini/palworld-discord-bot/src/i18n" + "github.com/juunini/palworld-discord-bot/src/utils" +) + +func shutdown(command string) string { + client, err := utils.RconClient() + if err != nil { + return i18n.FailedToConnectRconServer + } + + defer client.Disconnect() + + seconds, message, errorMessage := shutdownParameters(command) + if errorMessage != "" { + return errorMessage + } + + response, err := client.Shutdown(seconds, message) + if err != nil { + console_decoration.PrintError(i18n.FailedToShutdownCommand + ": " + err.Error()) + return i18n.FailedToShutdownCommand + } + + return response +} + +func shutdownParameters(command string) (uint, string, string) { + paramString, _ := strings.CutPrefix(command, "shutdown ") + params := strings.Split(paramString, " ") + + if len(params) < 2 { + console_decoration.PrintError(i18n.WrongParameters) + return 0, "", i18n.WrongParameters + } + + seconds, err := utils.ToUint(params[0]) + if err != nil { + console_decoration.PrintError(i18n.WrongParameters + ": " + err.Error()) + return 0, "", i18n.WrongParameters + } + + message := params[1] + return seconds, message, "" +} diff --git a/src/bot/handler.go b/src/bot/handler.go index 8bcb576..29fc57f 100644 --- a/src/bot/handler.go +++ b/src/bot/handler.go @@ -14,7 +14,7 @@ func Handler(s *discordgo.Session, m *discordgo.MessageCreate) { } if isCommand(m) { - s.ChannelMessageSend(m.ChannelID, commands.Response(m.Content)) + s.ChannelMessageSend(m.ChannelID, commands.Response(m.Content, m.Author.Username)) return } } diff --git a/src/config/config.go b/src/config/config.go index 7d85005..6c25269 100644 --- a/src/config/config.go +++ b/src/config/config.go @@ -5,7 +5,8 @@ var DISCORD_ADMIN_USERNAMES_STRING = "" var DISCORD_ADMIN_USERNAMES = []string{} var DISCORD_COMMAND_PREFIX = "!palbot" var PALWORLD_RCON_HOST = "127.0.0.1" -var PALWORLD_RCON_PORT = "25575" +var PALWORLD_RCON_PORT_STRING = "25575" +var PALWORLD_RCON_PORT uint64 = 25575 var PALWORLD_RCON_PASSWORD = "" var DISCORD_DASHBOARD_CHANNEL_ID = "" var DISCORD_LOG_CHANNEL_ID = "" diff --git a/src/config/init.go b/src/config/init.go index ef048fb..440527f 100644 --- a/src/config/init.go +++ b/src/config/init.go @@ -3,6 +3,7 @@ package config import ( "fmt" "os" + "strconv" "strings" "github.com/joho/godotenv" @@ -26,7 +27,7 @@ func askConfigToUser() { scanEnv("Discord Admin Usernames (comma separated)", &DISCORD_ADMIN_USERNAMES_STRING) scanEnvWithDefaultValue("Discord Command Prefix", &DISCORD_COMMAND_PREFIX, DISCORD_COMMAND_PREFIX) scanEnvWithDefaultValue("Palworld RCON Host", &PALWORLD_RCON_HOST, PALWORLD_RCON_HOST) - scanEnvWithDefaultValue("Palworld RCON Port", &PALWORLD_RCON_PORT, PALWORLD_RCON_PORT) + scanEnvWithDefaultValue("Palworld RCON Port", &PALWORLD_RCON_PORT_STRING, PALWORLD_RCON_PORT_STRING) scanEnv("Palworld RCON Password", &PALWORLD_RCON_PASSWORD) scanEnvWithDefaultValue("Language", &LANGUAGE, LANGUAGE) @@ -60,7 +61,15 @@ func loadEnv() { DISCORD_ADMIN_USERNAMES = trimSpaceAdminUsers() DISCORD_COMMAND_PREFIX = os.Getenv("DISCORD_COMMAND_PREFIX") PALWORLD_RCON_HOST = os.Getenv("PALWORLD_RCON_HOST") - PALWORLD_RCON_PORT = os.Getenv("PALWORLD_RCON_PORT") + PALWORLD_RCON_PORT_STRING = os.Getenv("PALWORLD_RCON_PORT") + + var err error + PALWORLD_RCON_PORT, err = strconv.ParseUint(PALWORLD_RCON_PORT_STRING, 10, 64) + if err != nil { + fmt.Printf("%sError parsing PALWORLD_RCON_PORT: %s%s\n", console_decoration.RED, err, console_decoration.RESET) + os.Exit(1) + } + PALWORLD_RCON_PASSWORD = os.Getenv("PALWORLD_RCON_PASSWORD") DISCORD_DASHBOARD_CHANNEL_ID = os.Getenv("DISCORD_DASHBOARD_CHANNEL_ID") DISCORD_LOG_CHANNEL_ID = os.Getenv("DISCORD_LOG_CHANNEL_ID") @@ -73,7 +82,7 @@ func envMap() map[string]string { "DISCORD_ADMIN_USERNAMES": DISCORD_ADMIN_USERNAMES_STRING, "DISCORD_COMMAND_PREFIX": DISCORD_COMMAND_PREFIX, "PALWORLD_RCON_HOST": PALWORLD_RCON_HOST, - "PALWORLD_RCON_PORT": PALWORLD_RCON_PORT, + "PALWORLD_RCON_PORT": PALWORLD_RCON_PORT_STRING, "PALWORLD_RCON_PASSWORD": PALWORLD_RCON_PASSWORD, "DISCORD_DASHBOARD_CHANNEL_ID": DISCORD_DASHBOARD_CHANNEL_ID, "DISCORD_LOG_CHANNEL_ID": DISCORD_LOG_CHANNEL_ID, diff --git a/src/i18n/en.go b/src/i18n/en.go index f471a79..f309b29 100644 --- a/src/i18n/en.go +++ b/src/i18n/en.go @@ -2,12 +2,32 @@ package i18n import ( "fmt" - - "github.com/juunini/palworld-discord-bot/src/config" ) func en() { BotRunningStart = "Bot is now running. Press CTRL-C to exit." - Help = fmt.Sprintf(`%s help - Display help message.`, config.DISCORD_COMMAND_PREFIX) + FailedToConnectRconServer = "Failed to connect RCON server" + FailedToSaveCommand = "Failed to Save command" + FailedToDoExitCommand = "Failed to DoExit command" + FailedToShutdownCommand = "Failed to Shutdown command" + FailedToBroadcastCommand = "Failed to Broadcast command" + FailedToKickCommand = "Failed to Kick command" + FailedToBanCommand = "Failed to Ban command" + WrongParameters = "Wrong parameters" + Help = func(commandPrefix string, isAdmin bool) string { + message := fmt.Sprintf("`%s help` - Show help\n", commandPrefix) + + if !isAdmin { + return message + } + + message += fmt.Sprintf("`%s kick ` - Kick from server. Can connect again.\n", commandPrefix) + message += fmt.Sprintf("`%s ban ` - Ban from server. Can't connect again.\n", commandPrefix) + message += fmt.Sprintf("`%s broadcast ` - Send to all users as SYSTEM message.\n", commandPrefix) + message += fmt.Sprintf("`%s shutdown ` - Shutdown server after with .\n", commandPrefix) + message += fmt.Sprintf("`%s doExit` - Force exit server.\n", commandPrefix) + message += fmt.Sprintf("`%s save` - Save server.", commandPrefix) + return message + } UnknownCommand = "Unknown command" } diff --git a/src/i18n/i18n.go b/src/i18n/i18n.go index 047eaaa..2575c06 100644 --- a/src/i18n/i18n.go +++ b/src/i18n/i18n.go @@ -1,16 +1,23 @@ package i18n func SetLanguage(language string) { + en() + switch language { case "ko": ko() return - - default: - en() } } var BotRunningStart string -var Help string +var FailedToConnectRconServer string +var FailedToSaveCommand string +var FailedToDoExitCommand string +var FailedToShutdownCommand string +var FailedToBroadcastCommand string +var FailedToKickCommand string +var FailedToBanCommand string +var WrongParameters string +var Help func(commandPrefix string, isAdmin bool) string var UnknownCommand string diff --git a/src/i18n/ko.go b/src/i18n/ko.go index a53dccd..181fd03 100644 --- a/src/i18n/ko.go +++ b/src/i18n/ko.go @@ -2,12 +2,32 @@ package i18n import ( "fmt" - - "github.com/juunini/palworld-discord-bot/src/config" ) func ko() { BotRunningStart = "봇이 실행되었습니다. 종료하려면 CTRL-C를 누르세요." - Help = fmt.Sprintf(`%s help - 도움말을 표시합니다.`, config.DISCORD_COMMAND_PREFIX) + FailedToConnectRconServer = "RCON 서버에 연결하는데 실패했습니다." + FailedToSaveCommand = "Save 커맨드를 실패했습니다." + FailedToDoExitCommand = "DoExit 커맨드를 실패했습니다." + FailedToShutdownCommand = "Shutdown 커맨드를 실패했습니다." + FailedToBroadcastCommand = "Broadcast 커맨드를 실패했습니다." + FailedToKickCommand = "Kick 커맨드를 실패했습니다." + FailedToBanCommand = "Ban 커맨드를 실패했습니다." + WrongParameters = "잘못된 파라미터를 입력하셨습니다. 다시 확인해주세요." + Help = func(commandPrefix string, isAdmin bool) string { + message := fmt.Sprintf("`%s help` - 도움말을 표시합니다.\n", commandPrefix) + + if !isAdmin { + return message + } + + message += fmt.Sprintf("`%s kick ` - 를 1회 추방합니다. 다시 접속이 가능합니다.\n", commandPrefix) + message += fmt.Sprintf("`%s ban ` - 를 다시 접속할 수 없게 블락시킵니다.\n", commandPrefix) + message += fmt.Sprintf("`%s broadcast <메시지>` - SYSTEM 메시지로 모든 유저에게 <메시지>를 전송합니다.\n", commandPrefix) + message += fmt.Sprintf("`%s shutdown <초> <메시지>` - <메시지>를 전송한 후 <초> 후에 서버를 종료합니다.\n", commandPrefix) + message += fmt.Sprintf("`%s doExit` - 서버를 강제 종료합니다.\n", commandPrefix) + message += fmt.Sprintf("`%s save` - 서버를 저장합니다.", commandPrefix) + return message + } UnknownCommand = "알 수 없는 명령어입니다." } diff --git a/src/utils/isAdmin.go b/src/utils/isAdmin.go new file mode 100644 index 0000000..29f5dad --- /dev/null +++ b/src/utils/isAdmin.go @@ -0,0 +1,13 @@ +package utils + +import "github.com/juunini/palworld-discord-bot/src/config" + +func IsAdmin(username string) bool { + for _, adminUsername := range config.DISCORD_ADMIN_USERNAMES { + if username == adminUsername { + return true + } + } + + return false +} diff --git a/src/utils/rcon.go b/src/utils/rcon.go new file mode 100644 index 0000000..a3aa398 --- /dev/null +++ b/src/utils/rcon.go @@ -0,0 +1,23 @@ +package utils + +import ( + "github.com/juunini/palworld-discord-bot/src/config" + "github.com/juunini/palworld-discord-bot/src/console_decoration" + "github.com/juunini/palworld-discord-bot/src/i18n" + palworldrcon "github.com/juunini/palworld-rcon" +) + +func RconClient() (palworldrcon.Client, error) { + client := palworldrcon.Client{ + Host: config.PALWORLD_RCON_HOST, + Port: uint(config.PALWORLD_RCON_PORT), + AdminPassword: config.PALWORLD_RCON_PASSWORD, + } + + err := client.Connect() + if err != nil { + console_decoration.PrintError(i18n.FailedToConnectRconServer + ": " + err.Error()) + } + + return client, err +} diff --git a/src/utils/strconv.go b/src/utils/strconv.go new file mode 100644 index 0000000..94de559 --- /dev/null +++ b/src/utils/strconv.go @@ -0,0 +1,8 @@ +package utils + +import "strconv" + +func ToUint(s string) (uint, error) { + value, err := strconv.ParseUint(s, 10, 64) + return uint(value), err +}