From f03a76897a16802aba84bf1ffbd69dad6a103e3c Mon Sep 17 00:00:00 2001 From: Astoria Floyd Date: Fri, 6 Aug 2021 16:22:24 -0500 Subject: [PATCH] Potential slash-command compatibility, some code cleanup --- bot.lua | 6 + deps/discordia-slash/Application.lua | 177 ++++++++++++ deps/discordia-slash/ApplicationCommand.lua | 283 ++++++++++++++++++++ deps/discordia-slash/Interaction.lua | 121 +++++++++ deps/discordia-slash/constructor.lua | 227 ++++++++++++++++ deps/discordia-slash/endpoints.lua | 12 + deps/discordia-slash/enums.lua | 27 ++ deps/discordia-slash/init.lua | 11 + deps/discordia-slash/package.lua | 15 ++ docs/help | 20 +- gateway.json | 2 +- 11 files changed, 897 insertions(+), 4 deletions(-) create mode 100644 deps/discordia-slash/Application.lua create mode 100644 deps/discordia-slash/ApplicationCommand.lua create mode 100644 deps/discordia-slash/Interaction.lua create mode 100644 deps/discordia-slash/constructor.lua create mode 100644 deps/discordia-slash/endpoints.lua create mode 100644 deps/discordia-slash/enums.lua create mode 100644 deps/discordia-slash/init.lua create mode 100644 deps/discordia-slash/package.lua diff --git a/bot.lua b/bot.lua index 3cb9ad3..9330c38 100644 --- a/bot.lua +++ b/bot.lua @@ -23,6 +23,12 @@ function printFile(file) return message end +client:on('messageCreate', function(message) + if messageDectection(message, "lenny") == true then + message.channel:send("( ͡° ͜ʖ ͡°)") + end +end) + client:on('ready', function() print('Logged in as '.. client.user.username) end) diff --git a/deps/discordia-slash/Application.lua b/deps/discordia-slash/Application.lua new file mode 100644 index 0000000..eabf5b3 --- /dev/null +++ b/deps/discordia-slash/Application.lua @@ -0,0 +1,177 @@ +local discordia = require("discordia") +local endpoints = require('./endpoints') +local f = string.format +local AC = require('./ApplicationCommand') +local IA = require('./Interaction') +local client_m = discordia.Client +local guild_m = discordia.class.classes.Guild +local cache_m = discordia.class.classes.Cache +local enums = require('./enums') + +local typeConverter = { + [enums.optionType.string] = function(val) return val end, + [enums.optionType.integer] = function(val) return val end, + [enums.optionType.boolean] = function(val) return val end, + [enums.optionType.user] = function(val, args) return args:getMember(val) end, + [enums.optionType.channel] = function(val, args) return args:getChannel(val) end, + [enums.optionType.role] = function(val, args) return args:getRole(val) end, +} + +local subCommand = enums.optionType.subCommand +local subCommandGroup = enums.optionType.subCommandGroup + +local function makeParams(data, guild, output) + output = output or {} + + for k, v in ipairs(data) do + if v.type == subCommand or v.type == subCommandGroup then + local t = {} + output[v.name] = t + makeParams(v.options, guild, t) + else + output[v.name] = typeConverter[v.type](v.value, guild) + end + end + + return output +end + +function client_m:useSlashCommands() + self._slashCommandsInjected = true + + function self._events.INTERACTION_CREATE(args, client) + local data = args.data + local cmd = client:getSlashCommand(data.id) + if not cmd then return client:warning('Uncached slash command (%s) on INTERACTION_CREATE', data.id) end + if data.name ~= cmd._name then return client:warning('Slash command %s "%s" name doesn\'t match with interaction response, got "%s"! Guild %s, channel %s, member %s', cmd._id, cmd._name, data.name, args.guild_id, args.channel_id, args.member.user.id) end + local ia = IA(args, client) + local params = makeParams(data.options, ia.guild) + local cb = cmd._callback + if not cb then return client:warning('Unhandled slash command interaction: %s "%s" (%s)!', cmd._id, cmd._name, cmd._guild and "Guild " .. cmd._guild.id or "Global") end + cb(ia, params, cmd) + end + + self:once("ready", function() + local id = self:getApplicationInformation().id + self._slashid = id + self._globalCommands = {} + self._guildCommands = {} + self:getSlashCommands() + self:emit("slashCommandsReady") + end) + + return self +end + +function client_m:slashCommand(data) + local found + + if not self._globalCommands then + self:getSlashCommands() + end + + do + local name = data.name + + for _, v in pairs(self._globalCommands) do + if v._name == name then + found = v + break + end + end + end + + local cmd = AC(data, self) + + if found then + if not found:_compare(cmd) then + found:_merge(cmd) + elseif not found._callback then + found._callback = cmd._callback + end + + return found + else + if cmd:publish() then + self._globalCommands:_insert(cmd) + else + return nil + end + end + + return cmd +end + +function guild_m:slashCommand(data) + local found + + if not self._slashCommands then + self:getSlashCommands() + end + + do + local name = data.name + + for _, v in pairs(self._slashCommands) do + if v._name == name then + found = v + break + end + end + end + + local cmd = AC(data, self) + + if found then + if not found:_compare(cmd) then + found:_merge(cmd) + elseif not found._callback then + found._callback = cmd._callback + end + + return found + else + if cmd:publish() then + self._slashCommands:_insert(cmd) + else + return nil + end + end + + return cmd +end + +function client_m:getSlashCommands() + local list, err = self._api:request('GET', f(endpoints.COMMANDS, self._slashid)) + if not list then return nil, err end + local cache = cache_m(list, AC, self) + self._globalCommands = cache + + return cache +end + +function guild_m:getSlashCommands() + local list, err = self.client._api:request('GET', f(endpoints.COMMANDS_GUILD, self.client._slashid, self.id)) + if not list then return nil, err end + local cache = cache_m(list, AC, self) + self._slashCommands = cache + self.client._guildCommands[self] = cache + + return cache +end + +function client_m:getSlashCommand(id) + if not self._globalCommands then + self:getSlashCommands() + end + + local g = self._globalCommands:get(id) + if g then return g end + + for _, v in pairs(self._guildCommands) do + g = v:get(id) + if g then return g end + end + + return nil +end \ No newline at end of file diff --git a/deps/discordia-slash/ApplicationCommand.lua b/deps/discordia-slash/ApplicationCommand.lua new file mode 100644 index 0000000..005b4b9 --- /dev/null +++ b/deps/discordia-slash/ApplicationCommand.lua @@ -0,0 +1,283 @@ +local discordia = require("discordia") +local endpoints = require('./endpoints') +local f = string.format +local Snowflake_m = discordia.class.classes.Snowflake +local AC, ACgetters = discordia.class('ApplicationCommand', Snowflake_m) + +local function recursiveOptionsMap(t) + local map = {} + + for _, v in ipairs(t) do + local name = string.lower(v.name) + v.name = name + map[name] = v + + if v.options then + v.mapoptions = recursiveOptionsMap(v.options) + end + end + + return map +end + +function AC:__init(data, parent) + self._id = data.id + self._parent = parent + self._name = data.name + self._description = data.description + self._default_permission = data.default_permission + self._version = data.version + self._callback = data.callback + self._guild = parent._id and parent + + if not self._options then + self._options = data.options or {} + end +end + +function AC:publish() + if self._id then return self:edit() end + local g = self._guild + + if not g then + local res, err = self.client._api:request('POST', f(endpoints.COMMANDS, self.client._slashid), { + name = self._name, + description = self._description, + options = self._options, + default_permission = self._default_permission + }) + + if not res then + return nil, err + else + self._id = res.id + + return self + end + else + local res, err = self.client._api:request('POST', f(endpoints.COMMANDS_GUILD, self.client._slashid, g._id), { + name = self._name, + description = self._description, + options = self._options, + default_permission = self._default_permission + }) + + if not res then + return nil, err + else + self._id = res.id + + return true + end + end +end + +function AC:edit() + local g = self._guild + + if not g then + local res, err = self.client._api:request('PATCH', f(endpoints.COMMANDS_MODIFY, self.client._slashid, self._id), { + name = self._name, + description = self._description, + options = self._options, + default_permission = self._default_permission + }) + + if not res then + return nil, err + else + return true + end + else + local res, err = self.client._api:request('PATCH', f(endpoints.COMMANDS_MODIFY_GUILD, self.client._slashid, g._id, self._id), { + name = self._name, + description = self._description, + options = self._options, + default_permission = self._default_permission + }) + + if not res then + return nil, err + else + return true + end + end +end + +function AC:setName(name) + self._name = name +end + +function AC:setDescription(description) + self._description = description +end + +function AC:setOptions(options) + self._options = options +end + +function AC:setCallback(callback) + self._callback = callback +end + +function AC:delete() + local g = self._guild + + if not g then + self.client._api:request('DELETE', f(endpoints.COMMANDS_MODIFY, self.client._slashid, self._id)) + self.client._globalCommands:_delete(self._id) + else + self.client._api:request('DELETE', f(endpoints.COMMANDS_MODIFY_GUILD, self.client._slashid, g._id, self._id)) + g._slashCommands:_delete(self._id) + end +end + +function AC:getPermissions(g) + g = self._guild or g + + if not g then + error("Guild is required") + end + + local stat, err = self.client._api:request('GET', f(endpoints.COMMAND_PERMISSIONS_MODIFY, self.client._slashid, g._id, self._id)) + + if stat then + return stat.permissions + else + return stat, err + end +end + +function AC:addPermission(perm, g) + g = self._guild or g + + if not g then + error("Guild is required") + end + + if not self._permissions then + self._permissions = self:getPermissions(g) or {} + end + + for k, v in ipairs(self._permissions) do + if v.id == perm.id and v.type == perm.type then + if v.permission == perm.permission then return end + self._permissions[k] = perm + goto found + end + end + + do + self._permissions[#self._permissions + 1] = perm + end + + ::found:: + + return self.client._api:request('PUT', f(endpoints.COMMAND_PERMISSIONS_MODIFY, self.client._slashid, g._id, self._id), { + permissions = self._permissions + }) +end + +function AC:removePermission(perm, g) + g = self._guild or g + + if not g then + error("Guild is required") + end + + if not self._permissions then + self._permissions = self:getPermissions(g) or {} + end + + for k, v in ipairs(self._permissions) do + if v.id == perm.id and v.type == perm.type then + table.remove(self._permissions, k) + return + end + end + + return self.client._api:request('PUT', f(endpoints.COMMAND_PERMISSIONS_MODIFY, self.client._slashid, g._id, self._id), { + permissions = self._permissions + }) +end + +local function recursiveCompare(a, b, checked) + checked = checked or {} + if checked[a] or checked[b] then return true end + local inner_checked = {} + + for k, v in pairs(a) do + if type(v) == "table" and type(b[k]) == "table" then + if not recursiveCompare(v, b[k], checked) then return false end + elseif v ~= b[k] then + print("k: ", k, "a[k]:", v, "b[k]: ", b[k]) + + return false + else + inner_checked[k] = true + end + end + + for k, v in pairs(b) do + if inner_checked[k] then + goto skip + end + + if type(v) == "table" and type(a[k]) == "table" then + if not recursiveCompare(v, a[k], checked) then return false end + elseif v ~= a[k] then + print("k: ", k, "a[k]:", a[k], "b[k]: ", v) + + return false + end + + ::skip:: + end + + checked[a], checked[b] = true, true + + return true +end + +function AC:_compare(cmd) + if self._name ~= cmd._name or self._description ~= cmd._description or self._default_permission ~= cmd._default_permission then return false end + if not self._options and cmd._options then return false end + if not recursiveCompare(self._options, cmd._options) then return false end + + return true +end + +function AC:_merge(cmd) + self._name = cmd._name + self._description = cmd._description + self._options = cmd._options + self._callback = cmd._callback + self._default_permission = cmd._default_permission + self:edit() +end + +function ACgetters:name() + return self._name +end + +function ACgetters:description() + return self._description +end + +function ACgetters:options() + return self._options +end + +function ACgetters:guild() + return self._guild +end + +function ACgetters:callback() + return self._callback +end + +function ACgetters:version() + return self._version +end + +return AC \ No newline at end of file diff --git a/deps/discordia-slash/Interaction.lua b/deps/discordia-slash/Interaction.lua new file mode 100644 index 0000000..b70c0d2 --- /dev/null +++ b/deps/discordia-slash/Interaction.lua @@ -0,0 +1,121 @@ +local discordia = require("discordia") +local endpoints = require('./endpoints') +local enums = require('./enums') +local f = string.format +local Snowflake_m = discordia.class.classes.Snowflake +local IA, IAgetters = discordia.class('Interaction', Snowflake_m) + +function IA:__init(data, parent) + self._id = data.id + self._parent = parent + self._type = data.type + self._token = data.token + self._version = data.version + local g = parent:getGuild(data.guild_id) + if not g then return parent:warning('Uncached Guild (%s) on INTERACTION_CREATE', data.guild_id) end + self._guild = g + self._channel = g:getChannel(data.channel_id) + self._member = g:getMember(data.member.user.id) +end + +function IA:createResponse(type, data) + self._type = type + + return self._parent._api:request('POST', f(endpoints.INTERACTION_RESPONSE, self._id, self._token), { + type = type, + data = data, + }) +end + +local deferredChannelMessageWithSource = enums.interactionResponseType.deferredChannelMessageWithSource +local channelMessageWithSource = enums.interactionResponseType.channelMessageWithSource + +function IA:ack() + return self:createResponse(deferredChannelMessageWithSource) +end + +function IA:reply(data, private) + if type(data) == "string" then + data = { + content = data + } + end + + if private then + data.flags = 64 + end + + return self:createResponse(channelMessageWithSource, data) +end + +function IA:update(data) + if type(data) == "string" then + data = { + content = data + } + end + + return self._parent._api:request('PATCH', f(endpoints.INTERACTION_RESPONSE_MODIFY, self._parent._slashid, self._token), data) +end + +function IA:delete() + return self._parent._api:request('DELETE', f(endpoints.INTERACTION_RESPONSE_MODIFY, self._parent._slashid, self._token)) +end + +function IA:followUp(data, private) + if type(data) == "string" then + data = { + content = data + } + end + + if private then + if self._type == deferredChannelMessageWithSource then + private = false + else + data.flags = 64 + end + end + + local res = self._parent._api:request('POST', f(endpoints.INTERACTION_FOLLOWUP_CREATE, self._parent._slashid, self._token), data) + + if res.id then + local msg + + if not private then + msg = self._channel:getMessage(res.id) + end + + return res.id, msg, res + end + + return res +end + +function IA:updateFollowUp(id, data) + if type(data) == "string" then + data = { + content = data + } + end + + return self._parent._api:request('PATCH', f(endpoints.INTERACTION_FOLLOWUP_MODIFY, self._parent._slashid, self._token, id), data) +end + +function IA:deleteFollowUp(id) + return self._parent._api:request('DELETE', f(endpoints.INTERACTION_FOLLOWUP_MODIFY, self._parent._slashid, self._token, id)) +end + +function IAgetters:guild() + return self._guild +end + +function IAgetters:channel() + return self._channel +end + +function IAgetters:member() + return self._member +end + +return IA \ No newline at end of file diff --git a/deps/discordia-slash/constructor.lua b/deps/discordia-slash/constructor.lua new file mode 100644 index 0000000..0ae33a1 --- /dev/null +++ b/deps/discordia-slash/constructor.lua @@ -0,0 +1,227 @@ +local optionMeta = {} +optionMeta.__index = optionMeta + +function optionMeta:option(name, description, type, required) + if not name then + error("Name is required") + elseif not description then + error("Description is required") + elseif not type then + error("Type is required") + elseif #name == 0 or #name > 32 then + error("Must be between 1 and 32 in length") + elseif string.find(name, "^[^%w_-]$") then + error("The name should match ^[\\w-]{1,32}$ pattern") + elseif #description == 0 or #description > 100 then + error("Must be between 1 and 100 in length") + elseif type < 1 or type > 8 then + error("Value type must be between 1 and 8 (See ApplicationCommandOptionType)") + end + + local ctnr = self[1] + local selfType = ctnr.type + + if not self[2] then + if selfType <= 2 then + if (selfType == 1 and type <= 2) or (selfType == 2 and type == 2) then + error("Nesting of sub-commands is unsupported at this time") + end + else + error("Sub-options cannot be configured for this type of option") + end + end + + local t = setmetatable({ + parent = self, + { + name = name, + description = description, + type = type, + } + }, optionMeta) + + if not ctnr.options then + ctnr.options = {} + end + + ctnr.options[#ctnr.options + 1] = t + + if required then + t:required() + end + + return t +end + +function optionMeta:suboption(name, description) + return self:option(name, description, 1) +end + +function optionMeta:group(name, description) + return self:option(name, description, 2) +end + +function optionMeta:required(no) + local ctnr = self[1] + local type = ctnr.type + + if type <= 2 then + error("Required cannot be configured for this type of option") + end + + for _, v in ipairs(self.parent[1].options) do + if not v.required then + error("Required options must be placed before non-required options") + end + + if v == self then break end + end + + ctnr.required = not no +end + +-- function optionMeta:default(no) +-- local ctnr = self[1] +-- local type = ctnr.type +-- if type <= 2 then +-- error("Default cannot be configured for this type of option") +-- end +-- if not self[1].required then +-- error("Default cannot be configured with required = false") +-- end +-- for _, v in ipairs(self.parent[1].options) do +-- if v[1].default then +-- error("There can be 1 default option within command, sub-command, and sub-command group options") +-- end +-- end +-- ctnr.default = not no +-- end +function optionMeta:choices(...) + local ctnr = self[1] + local opttype = ctnr.type + local acceptedType + + if opttype == 3 then + acceptedType = "string" + elseif opttype == 4 then + acceptedType = "number" + else + error("Choices cannot be configured for this type of option") + end + + local t = {} + ctnr.choices = t + + for i = 1, select("#", ...) do + local v = select(i, ...) + + if type(v) == acceptedType then + t[i] = { + name = tostring(v), + value = v + } + else + t[i] = v + end + end +end + +function optionMeta:finish() + local t = {} + + for k, v in pairs(self[1]) do + t[k] = v + end + + if t.options then + local options = {} + + for k, v in ipairs(t.options) do + options[k] = v:finish() + end + + t.options = options + end + + return t +end + +local commandMeta = {} +commandMeta.__index = commandMeta +commandMeta.option = optionMeta.option +commandMeta.finish = optionMeta.finish +commandMeta.suboption = optionMeta.suboption +commandMeta.group = optionMeta.group + +function commandMeta:disableForEveryone(no) + if not no then + no = false + end + + self[1].default_permission = no +end + + +function commandMeta:callback(cb) + self[1].callback = cb +end + +local function new(name, description, cb) + if not name then + error("Name is required") + elseif not description then + error("Description is required") + elseif #name == 0 or #name > 32 then + error("Must be between 1 and 32 in length") + elseif string.find(name, "^[^%w_-]$") then + error("The name should match ^[\\w-]{1,32}$ pattern") + elseif #description == 0 or #description > 100 then + error("Must be between 1 and 100 in length") + end + + return setmetatable({ + { + name = name, + description = description, + options = {}, + default_permission = true, + callback = cb + }, + true + }, commandMeta) +end + +local discordia = require("discordia") +local enums = require("./enums") +local enum_user = enums.applicationCommandPermissionType.user +local enum_role = enums.applicationCommandPermissionType.role + +local function perm(obj, allow, _type) + if type(obj) == "string" then + if not _type then + error("Type required") + end + + return { + id = obj, + type = _type, + permission = allow and true or false + } + end + + local t = discordia.class.type(obj) + + if t == "Member" or t == "User" then + _type = enum_user + elseif t == "Role" then + _type = enum_role + end + + return { + id = obj.id, + type = _type, + permission = allow and true or false + } +end + +return {new, perm} \ No newline at end of file diff --git a/deps/discordia-slash/endpoints.lua b/deps/discordia-slash/endpoints.lua new file mode 100644 index 0000000..d994be4 --- /dev/null +++ b/deps/discordia-slash/endpoints.lua @@ -0,0 +1,12 @@ +return { + COMMANDS = "/applications/%s/commands", + COMMANDS_GUILD = "/applications/%s/guilds/%s/commands", + COMMANDS_MODIFY = "/applications/%s/commands/%s", + COMMANDS_MODIFY_GUILD = "/applications/%s/guilds/%s/commands/%s", + COMMAND_PERMISSIONS = "/applications/%s/guilds/%s/commands/permissions", + COMMAND_PERMISSIONS_MODIFY = "/applications/%s/guilds/%s/commands/%s/permissions", + INTERACTION_RESPONSE = "/interactions/%s/%s/callback", + INTERACTION_RESPONSE_MODIFY = "/webhooks/%s/%s/messages/@original", + INTERACTION_FOLLOWUP_CREATE = "/webhooks/%s/%s", + INTERACTION_FOLLOWUP_MODIFY = "/webhooks/%s/%s/messages/%s" +} \ No newline at end of file diff --git a/deps/discordia-slash/enums.lua b/deps/discordia-slash/enums.lua new file mode 100644 index 0000000..dc4b5ca --- /dev/null +++ b/deps/discordia-slash/enums.lua @@ -0,0 +1,27 @@ +local enum = require('discordia').enums.enum + +return { + optionType = enum({ + subCommand = 1, + subCommandGroup = 2, + string = 3, + integer = 4, + boolean = 5, + user = 6, + channel = 7, + role = 8 + }), + interactionType = enum({ + ping = 1, + applicationCommand = 2 + }), + interactionResponseType = enum({ + pong = 1, + channelMessageWithSource = 4, + deferredChannelMessageWithSource = 5 + }), + applicationCommandPermissionType = enum({ + role = 1, + user = 2, + }) +} \ No newline at end of file diff --git a/deps/discordia-slash/init.lua b/deps/discordia-slash/init.lua new file mode 100644 index 0000000..21a5db8 --- /dev/null +++ b/deps/discordia-slash/init.lua @@ -0,0 +1,11 @@ +require("./Application") + +local ret = { + enums = require("./enums") +} + +ret.constructor = function() + ret.new, ret.permission = unpack(require("./constructor")) +end + +return ret \ No newline at end of file diff --git a/deps/discordia-slash/package.lua b/deps/discordia-slash/package.lua new file mode 100644 index 0000000..34b2e24 --- /dev/null +++ b/deps/discordia-slash/package.lua @@ -0,0 +1,15 @@ +return { + name = 'GitSparTV/discordia-slash', + description = 'Discordia 2.0 slash commands extension', + version = '2.0.0', + homepage = 'https://github.com/GitSparTV/discordia-slash', + dependencies = { + 'SinisterRectus/discordia@2.8.4', + }, + tags = {'discord', 'slash', 'discordia'}, + license = 'MIT', + author = 'Spar', + files = { + '**.lua', + }, +} \ No newline at end of file diff --git a/docs/help b/docs/help index ee77fa4..55264c7 100644 --- a/docs/help +++ b/docs/help @@ -1,3 +1,17 @@ -Hello! -Line 2 -Line 3! +```fix +Basic functions +--------------- +-ping +Pong! +-roll +Rolls a d20 +-time +Displays the time in military time, as if you were in chicago +-help +^-^ +Basic Information +----------------- +All functions start with '!' though this may change in the future +Under construction! More or less from scratch(Only just got basic I/O working!) +May have secret functions! +``` diff --git a/gateway.json b/gateway.json index 8d23268..0a25eb3 100644 --- a/gateway.json +++ b/gateway.json @@ -1 +1 @@ -{"873255296024322059":{"timestamp":1628279979,"owner":{"username":"team872299874857676820","id":"872299874857676820","public_flags":1024,"flags":1024,"discriminator":"0000","avatar":null},"shards":1},"url":"wss://gateway.discord.gg"} \ No newline at end of file +{"873255296024322059":{"timestamp":1628283774,"owner":{"discriminator":"0000","avatar":null,"id":"872299874857676820","flags":1024,"public_flags":1024,"username":"team872299874857676820"},"shards":1},"url":"wss://gateway.discord.gg"} \ No newline at end of file