-- Copyright (C) 2018 Jérôme Leclercq -- This file is part of the "Not a Bot" application -- For conditions of distribution and use, see copyright notice in LICENSE local discordia = require('discordia') local enums = discordia.enums local wrap = coroutine.wrap discordia.extensions() -- load all helpful Lua library extensions local client = discordia.Client({ cacheAllMembers = true, -- logLevel = 4, gatewayIntents = bit.bor( -- All intents except guildIntegrations enums.gatewayIntent.guilds, enums.gatewayIntent.guildMembers, enums.gatewayIntent.guildModeration, enums.gatewayIntent.guildEmojis, enums.gatewayIntent.guildWebhooks, enums.gatewayIntent.guildInvites, enums.gatewayIntent.guildVoiceStates, enums.gatewayIntent.guildMessages, enums.gatewayIntent.guildMessageReactions, enums.gatewayIntent.guildMessageTyping, enums.gatewayIntent.directMessage, enums.gatewayIntent.directMessageRections, enums.gatewayIntent.directMessageTyping, -- privileged intents enums.gatewayIntent.guildPresences ) }) local function code(str) return string.format('```\n%s```', str) end local function printLine(...) local ret = {} for i = 1, select('#', ...) do local arg = tostring(select(i, ...)) table.insert(ret, arg) end return table.concat(ret, '\t') end dofile("utils.lua") -- Config Config = {} local func, err = loadfile("config.lua", "bt", Config) if (not func) then print("Failed to load config file:\n" .. code(tostring(err))) return end local ret, err = pcall(func) if (not ret) then print("Failed to execute config file:\n" .. code(tostring(err))) return end -- Bot code Bot = {} Bot.Client = client Bot.Commands = {} Bot.Events = {} Bot.Modules = {} Bot.ConfigType = enums.enum { Boolean = 0, Category = 1, Channel = 2, Custom = 3, Duration = 4, Emoji = 5, Guild = 6, Integer = 7, Member = 8, Message = 9, Number = 10, Role = 11, String = 12, User = 13 } Bot.ConfigTypeString = {} for name,value in pairs(Bot.ConfigType) do Bot.ConfigTypeString[value] = name end Bot.ConfigTypeToString = { [Bot.ConfigType.Boolean] = tostring, [Bot.ConfigType.Category] = function (value, guild) local channel = guild:getChannel(value) return channel and channel.mentionString or "" end, [Bot.ConfigType.Channel] = function (value, guild) local channel = guild:getChannel(value) return channel and channel.mentionString or "" end, [Bot.ConfigType.Custom] = function (value, guild) return "" end, [Bot.ConfigType.Duration] = function (value, guild) return Bot:FormatDuration(guild, value) end, [Bot.ConfigType.Emoji] = function (value, guild) local emojiData = Bot:GetEmojiData(guild, value) return emojiData and emojiData.MentionString or "" end, [Bot.ConfigType.Integer] = tostring, [Bot.ConfigType.Guild] = function (value) local guild = client:getGuild(value) return guild and guild.name or "" end, [Bot.ConfigType.Member] = function (value, guild) local member = guild:getMember(value) return member and member.user.mentionString or "" end, [Bot.ConfigType.Message] = tostring, [Bot.ConfigType.Number] = function (value) return type(value) == "number" end, [Bot.ConfigType.Role] = function (value, guild) local role = guild:getRole(value) return role and role.mentionString or "" end, [Bot.ConfigType.String] = tostring, [Bot.ConfigType.User] = function (value, guild) local user = client:getUser(value) return user and user.mentionString or "" end } Bot.ConfigTypeParameter = { [Bot.ConfigType.Boolean] = function (value, guild) if (value == "yes" or value == "1" or value == "true") then return true elseif (value == "no" or value == "0" or value == "false") then return false end end, [Bot.ConfigType.Category] = function (value, guild) local channel, err = Bot:DecodeChannel(guild, value) if (not channel) then return nil, err end if (channel.type ~= enums.channelType.category) then return nil, "expected category" end return channel end, [Bot.ConfigType.Channel] = function (value, guild) return Bot:DecodeChannel(guild, value) end, [Bot.ConfigType.Custom] = function (value, guild) return nil end, [Bot.ConfigType.Duration] = function (value, guild) return string.ConvertToTime(value) end, [Bot.ConfigType.Emoji] = function (value, guild) return Bot:DecodeEmoji(guild, value) end, [Bot.ConfigType.Integer] = function (value, guild) return tonumber(value:match("^(%d+)$")) end, [Bot.ConfigType.Guild] = function (value) local success, err = util.ValidateSnowflake(value) if not success then return nil, err end local guild = client:getGuild(value) if not guild then return nil, value .. " is not a guild I know" end return guild end, [Bot.ConfigType.Member] = function (value, guild) return Bot:DecodeMember(guild, value) end, [Bot.ConfigType.Message] = function (value, guild) return Bot:DecodeMessage(value, nil, true) end, [Bot.ConfigType.Number] = function (value) return tonumber(value) end, [Bot.ConfigType.Role] = function (value, guild) return Bot:DecodeRole(guild, value) end, [Bot.ConfigType.String] = function (value, guild) return value end, [Bot.ConfigType.User] = function (value, guild) return Bot:DecodeUser(value) end } Bot.ConfigTypeParser = { [Bot.ConfigType.Boolean] = function (value, guild) if (value == "yes" or value == "1" or value == "true") then return true elseif (value == "no" or value == "0" or value == "false") then return false end end, [Bot.ConfigType.Category] = function (value, guild) local channel, err = Bot:DecodeChannel(guild, value) if (not channel) then return nil, err end if (channel.type ~= enums.channelType.category) then return nil, "expected category" end return channel.id end, [Bot.ConfigType.Channel] = function (value, guild) local channel = Bot:DecodeChannel(guild, value) return channel and channel.id end, [Bot.ConfigType.Custom] = function (value, guild) return nil end, [Bot.ConfigType.Duration] = function (value, guild) return string.ConvertToTime(value) end, [Bot.ConfigType.Emoji] = function (value, guild) local emojiData = Bot:DecodeEmoji(guild, value) return emojiData and emojiData.Name end, [Bot.ConfigType.Integer] = function (value, guild) return tonumber(value:match("^(%d+)$")) end, [Bot.ConfigType.Guild] = function (value) local success, err = util.ValidateSnowflake(value) if not success then return nil, err end local guild = client:getGuild(value) if not guild then return nil, value .. " is not a guild I know" end return guild.id end, [Bot.ConfigType.Member] = function (value, guild) local member = Bot:DecodeMember(guild, value) return member and member.id end, [Bot.ConfigType.Message] = function (value, guild) local message = Bot:DecodeMessage(value, nil, true) return message and Bot:GenerateMessageLink(message) end, [Bot.ConfigType.Number] = function (value) return tonumber(value) end, [Bot.ConfigType.Role] = function (value, guild) local role = Bot:DecodeRole(guild, value) return role and role.id end, [Bot.ConfigType.String] = function (value, guild) return value end, [Bot.ConfigType.User] = function (value, guild) local user = Bot:DecodeUser(value) return user and user.id end } client:onSync("ready", function () print("Logged in as " .. client.user.username) end) client:on("guildAvailable", function (guild) print(string.format("Guild %s (%d members)", guild.name, guild.totalMemberCount)) end) client:on("guildCreate", function (guild) client:info("Bot was added to guild %s", guild.name) end) client:on("guildDelete", function (guild) client:info("Bot was removed from guild %s", guild.name) end) function Bot:Save() local stopwatch = discordia.Stopwatch() for _, moduleTable in pairs(self.Modules) do self:ProtectedCall(string.format("Module (%s) persistent data save", moduleTable.Name), moduleTable.SavePersistentData, moduleTable) end client:info("Modules data saved (%.3fs)", stopwatch.milliseconds / 1000) end -- Why is this required Oo local env = setmetatable({}, { __index = _G }) env.Bot = Bot env.Client = client env.Config = Config env.discordia = discordia env.require = require local function loadbotfile(file) local f, err = loadfile(file, "t", env) if (not f) then error(file .. " failed to compile: " .. err) end local success, err = pcall(f) if (not success) then error(file .. " failed to execute: " .. err) end end loadbotfile("bot_emoji.lua") loadbotfile("bot_utility.lua") loadbotfile("bot_localization.lua") loadbotfile("bot_commands.lua") loadbotfile("bot_modules.lua") loadbotfile("bot_timers.lua") Bot:CreateRepeatTimer(5 * 60, -1, function() Bot:Save() end) Bot:RegisterCommand({ Name = "exec", Args = { {Name = "filename", Type = Bot.ConfigType.String} }, PrivilegeCheck = function (member) return member.id == Config.OwnerUserId end, Help = "Executes a file", Func = function (message, fileName) local sandbox = setmetatable({ }, { __index = _G }) sandbox.Bot = Bot sandbox.Client = client sandbox.Config = Config sandbox.CommandMessage = message sandbox.Discordia = discordia sandbox.require = require local lines = {} sandbox.print = function(...) table.insert(lines, printLine(...)) end local func, err = loadfile(fileName, "bt", sandbox) if (not func) then message:reply("Failed to load file:\n" .. code(tostring(err))) return end local ret, err = pcall(func) if (not ret) then message:reply("Failed to call file:\n" .. code(tostring(err))) return end if (#lines > 0) then lines = table.concat(lines, '\n') if #lines > 1990 then -- truncate long messages lines = lines:sub(1, 1990) end message:reply(code(lines)) end end }) Bot:RegisterCommand({ Name = "save", Args = {}, PrivilegeCheck = function (member) return member.id == Config.OwnerUserId end, Help = "Saves bot data", Func = function (message) Bot:Save() message:reply("Bot data saved") end }) Bot:RegisterCommand({ Name = "reboot", Args = {}, PrivilegeCheck = function (member) return member.id == Config.OwnerUserId end, Help = "Restart bot", Func = function (message) Bot:Save() message:reply("Saving and rebooting...") os.exit(0) end }) Bot:RegisterCommand({ Name = "debugtimer", Args = {}, PrivilegeCheck = function (member) return member.id == Config.OwnerUserId end, Help = "Gets last timer check timestamp", Func = function (message) message:reply(string.format("Last timer check occurred on ", Bot.LastTimerExecution)) end }) client:run('Bot ' .. Config.Token) for k,moduleFile in pairs(Config.AutoloadModules) do wrap(function () local moduleTable, err, codeErr = Bot:LoadModuleFile(moduleFile) if (moduleTable) then client:info("Auto-loaded module \"%s\"", moduleTable.Name) else local errorMessage = err if (codeErr) then errorMessage = errorMessage .. "\n" .. codeErr end client:error(errorMessage) end end)() end