Potential slash-command compatibility, some code cleanup

pull/1/head
Astoria Floyd 3 years ago
parent 22581c242b
commit f03a76897a

@ -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)

@ -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

@ -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

@ -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

@ -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}

@ -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"
}

@ -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,
})
}

@ -0,0 +1,11 @@
require("./Application")
local ret = {
enums = require("./enums")
}
ret.constructor = function()
ret.new, ret.permission = unpack(require("./constructor"))
end
return ret

@ -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',
},
}

@ -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!
```

@ -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"}
{"873255296024322059":{"timestamp":1628283774,"owner":{"discriminator":"0000","avatar":null,"id":"872299874857676820","flags":1024,"public_flags":1024,"username":"team872299874857676820"},"shards":1},"url":"wss://gateway.discord.gg"}
Loading…
Cancel
Save