You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

712 lines
22 KiB
Lua

local json = require('json')
local timer = require('timer')
local http = require('coro-http')
local package = require('../../package.lua')
local Mutex = require('utils/Mutex')
local endpoints = require('endpoints')
local request = http.request
local f, gsub, byte = string.format, string.gsub, string.byte
local max, random = math.max, math.random
local encode, decode, null = json.encode, json.decode, json.null
local insert, concat = table.insert, table.concat
local sleep = timer.sleep
local running = coroutine.running
local BASE_URL = "https://discord.com/api/v7"
local JSON = 'application/json'
local PRECISION = 'millisecond'
local MULTIPART = 'multipart/form-data;boundary='
local USER_AGENT = f('DiscordBot (%s, %s)', package.homepage, package.version)
local majorRoutes = {guilds = true, channels = true, webhooks = true}
local payloadRequired = {PUT = true, PATCH = true, POST = true}
local function parseErrors(ret, errors, key)
for k, v in pairs(errors) do
if k == '_errors' then
for _, err in ipairs(v) do
insert(ret, f('%s in %s : %s', err.code, key or 'payload', err.message))
end
else
if key then
parseErrors(ret, v, f(k:find("^[%a_][%a%d_]*$") and '%s.%s' or tonumber(k) and '%s[%d]' or '%s[%q]', key, k))
else
parseErrors(ret, v, k)
end
end
end
return concat(ret, '\n\t')
end
local function sub(path)
return not majorRoutes[path] and path .. '/:id'
end
local function route(method, endpoint)
-- special case for reactions
if endpoint:find('reactions') then
endpoint = endpoint:match('.*/reactions')
end
-- remove the ID from minor routes
endpoint = endpoint:gsub('(%a+)/%d+', sub)
-- special case for message deletions
if method == 'DELETE' then
local i, j = endpoint:find('/channels/%d+/messages')
if i == 1 and j == #endpoint then
endpoint = method .. endpoint
end
end
return endpoint
end
local function generateBoundary(files, boundary)
boundary = boundary or tostring(random(0, 9))
for _, v in ipairs(files) do
if v[2]:find(boundary, 1, true) then
return generateBoundary(files, boundary .. random(0, 9))
end
end
return boundary
end
local function attachFiles(payload, files)
local boundary = generateBoundary(files)
local ret = {
'--' .. boundary,
'Content-Disposition:form-data;name="payload_json"',
'Content-Type:application/json\r\n',
payload,
}
for i, v in ipairs(files) do
insert(ret, '--' .. boundary)
insert(ret, f('Content-Disposition:form-data;name="file%i";filename=%q', i, v[1]))
insert(ret, 'Content-Type:application/octet-stream\r\n')
insert(ret, v[2])
end
insert(ret, '--' .. boundary .. '--')
return concat(ret, '\r\n'), boundary
end
local mutexMeta = {
__mode = 'v',
__index = function(self, k)
self[k] = Mutex()
return self[k]
end
}
local function tohex(char)
return f('%%%02X', byte(char))
end
local function urlencode(obj)
return (gsub(tostring(obj), '%W', tohex))
end
local API = require('class')('API')
function API:__init(client)
self._client = client
self._mutexes = setmetatable({}, mutexMeta)
end
function API:authenticate(token)
self._token = token
return self:getCurrentUser()
end
function API:request(method, endpoint, payload, query, files)
local _, main = running()
if main then
return error('Cannot make HTTP request outside of a coroutine', 2)
end
local url = BASE_URL .. endpoint
if query and next(query) then
url = {url}
for k, v in pairs(query) do
insert(url, #url == 1 and '?' or '&')
insert(url, urlencode(k))
insert(url, '=')
insert(url, urlencode(v))
end
url = concat(url)
end
local req = {
{'User-Agent', USER_AGENT},
{'X-RateLimit-Precision', PRECISION},
{'Authorization', self._token},
}
if payloadRequired[method] then
payload = payload and encode(payload) or '{}'
if files and next(files) then
local boundary
payload, boundary = attachFiles(payload, files)
insert(req, {'Content-Type', MULTIPART .. boundary})
else
insert(req, {'Content-Type', JSON})
end
insert(req, {'Content-Length', #payload})
end
local mutex = self._mutexes[route(method, endpoint)]
mutex:lock()
local data, err, delay = self:commit(method, url, req, payload, 0)
mutex:unlockAfter(delay)
if data then
return data
else
return nil, err
end
end
function API:commit(method, url, req, payload, retries)
local client = self._client
local options = client._options
local delay = options.routeDelay
local success, res, msg = pcall(request, method, url, req, payload)
if not success then
return nil, res, delay
end
for i, v in ipairs(res) do
res[v[1]:lower()] = v[2]
res[i] = nil
end
if res['x-ratelimit-remaining'] == '0' then
delay = max(1000 * res['x-ratelimit-reset-after'], delay)
end
local data = res['content-type'] == JSON and decode(msg, 1, null) or msg
if res.code < 300 then
client:debug('%i - %s : %s %s', res.code, res.reason, method, url)
return data, nil, delay
else
if type(data) == 'table' then
local retry
if res.code == 429 then -- TODO: global ratelimiting
delay = data.retry_after
retry = retries < options.maxRetries
elseif res.code == 502 then
delay = delay + random(2000)
retry = retries < options.maxRetries
end
if retry then
client:warning('%i - %s : retrying after %i ms : %s %s', res.code, res.reason, delay, method, url)
sleep(delay)
return self:commit(method, url, req, payload, retries + 1)
end
if data.code and data.message then
msg = f('HTTP Error %i : %s', data.code, data.message)
else
msg = 'HTTP Error'
end
if data.errors then
msg = parseErrors({msg}, data.errors)
end
end
client:error('%i - %s : %s %s', res.code, res.reason, method, url)
return nil, msg, delay
end
end
-- start of auto-generated methods --
function API:getGuildAuditLog(guild_id, query)
local endpoint = f(endpoints.GUILD_AUDIT_LOGS, guild_id)
return self:request("GET", endpoint, nil, query)
end
function API:getChannel(channel_id) -- not exposed, use cache
local endpoint = f(endpoints.CHANNEL, channel_id)
return self:request("GET", endpoint)
end
function API:modifyChannel(channel_id, payload) -- Channel:_modify
local endpoint = f(endpoints.CHANNEL, channel_id)
return self:request("PATCH", endpoint, payload)
end
function API:deleteChannel(channel_id) -- Channel:delete
local endpoint = f(endpoints.CHANNEL, channel_id)
return self:request("DELETE", endpoint)
end
function API:getChannelMessages(channel_id, query) -- TextChannel:get[First|Last]Message, TextChannel:getMessages
local endpoint = f(endpoints.CHANNEL_MESSAGES, channel_id)
return self:request("GET", endpoint, nil, query)
end
function API:getChannelMessage(channel_id, message_id) -- TextChannel:getMessage fallback
local endpoint = f(endpoints.CHANNEL_MESSAGE, channel_id, message_id)
return self:request("GET", endpoint)
end
function API:createMessage(channel_id, payload, files) -- TextChannel:send
local endpoint = f(endpoints.CHANNEL_MESSAGES, channel_id)
return self:request("POST", endpoint, payload, nil, files)
end
function API:createReaction(channel_id, message_id, emoji, payload) -- Message:addReaction
local endpoint = f(endpoints.CHANNEL_MESSAGE_REACTION_ME, channel_id, message_id, urlencode(emoji))
return self:request("PUT", endpoint, payload)
end
function API:deleteOwnReaction(channel_id, message_id, emoji) -- Message:removeReaction
local endpoint = f(endpoints.CHANNEL_MESSAGE_REACTION_ME, channel_id, message_id, urlencode(emoji))
return self:request("DELETE", endpoint)
end
function API:deleteUserReaction(channel_id, message_id, emoji, user_id) -- Message:removeReaction
local endpoint = f(endpoints.CHANNEL_MESSAGE_REACTION_USER, channel_id, message_id, urlencode(emoji), user_id)
return self:request("DELETE", endpoint)
end
function API:getReactions(channel_id, message_id, emoji, query) -- Reaction:getUsers
local endpoint = f(endpoints.CHANNEL_MESSAGE_REACTION, channel_id, message_id, urlencode(emoji))
return self:request("GET", endpoint, nil, query)
end
function API:deleteAllReactions(channel_id, message_id) -- Message:clearReactions
local endpoint = f(endpoints.CHANNEL_MESSAGE_REACTIONS, channel_id, message_id)
return self:request("DELETE", endpoint)
end
function API:editMessage(channel_id, message_id, payload) -- Message:_modify
local endpoint = f(endpoints.CHANNEL_MESSAGE, channel_id, message_id)
return self:request("PATCH", endpoint, payload)
end
function API:deleteMessage(channel_id, message_id) -- Message:delete
local endpoint = f(endpoints.CHANNEL_MESSAGE, channel_id, message_id)
return self:request("DELETE", endpoint)
end
function API:bulkDeleteMessages(channel_id, payload) -- GuildTextChannel:bulkDelete
local endpoint = f(endpoints.CHANNEL_MESSAGES_BULK_DELETE, channel_id)
return self:request("POST", endpoint, payload)
end
function API:editChannelPermissions(channel_id, overwrite_id, payload) -- various PermissionOverwrite methods
local endpoint = f(endpoints.CHANNEL_PERMISSION, channel_id, overwrite_id)
return self:request("PUT", endpoint, payload)
end
function API:getChannelInvites(channel_id) -- GuildChannel:getInvites
local endpoint = f(endpoints.CHANNEL_INVITES, channel_id)
return self:request("GET", endpoint)
end
function API:createChannelInvite(channel_id, payload) -- GuildChannel:createInvite
local endpoint = f(endpoints.CHANNEL_INVITES, channel_id)
return self:request("POST", endpoint, payload)
end
function API:deleteChannelPermission(channel_id, overwrite_id) -- PermissionOverwrite:delete
local endpoint = f(endpoints.CHANNEL_PERMISSION, channel_id, overwrite_id)
return self:request("DELETE", endpoint)
end
function API:triggerTypingIndicator(channel_id, payload) -- TextChannel:broadcastTyping
local endpoint = f(endpoints.CHANNEL_TYPING, channel_id)
return self:request("POST", endpoint, payload)
end
function API:getPinnedMessages(channel_id) -- TextChannel:getPinnedMessages
local endpoint = f(endpoints.CHANNEL_PINS, channel_id)
return self:request("GET", endpoint)
end
function API:addPinnedChannelMessage(channel_id, message_id, payload) -- Message:pin
local endpoint = f(endpoints.CHANNEL_PIN, channel_id, message_id)
return self:request("PUT", endpoint, payload)
end
function API:deletePinnedChannelMessage(channel_id, message_id) -- Message:unpin
local endpoint = f(endpoints.CHANNEL_PIN, channel_id, message_id)
return self:request("DELETE", endpoint)
end
function API:groupDMAddRecipient(channel_id, user_id, payload) -- GroupChannel:addRecipient
local endpoint = f(endpoints.CHANNEL_RECIPIENT, channel_id, user_id)
return self:request("PUT", endpoint, payload)
end
function API:groupDMRemoveRecipient(channel_id, user_id) -- GroupChannel:removeRecipient
local endpoint = f(endpoints.CHANNEL_RECIPIENT, channel_id, user_id)
return self:request("DELETE", endpoint)
end
function API:listGuildEmojis(guild_id) -- not exposed, use cache
local endpoint = f(endpoints.GUILD_EMOJIS, guild_id)
return self:request("GET", endpoint)
end
function API:getGuildEmoji(guild_id, emoji_id) -- not exposed, use cache
local endpoint = f(endpoints.GUILD_EMOJI, guild_id, emoji_id)
return self:request("GET", endpoint)
end
function API:createGuildEmoji(guild_id, payload) -- Guild:createEmoji
local endpoint = f(endpoints.GUILD_EMOJIS, guild_id)
return self:request("POST", endpoint, payload)
end
function API:modifyGuildEmoji(guild_id, emoji_id, payload) -- Emoji:_modify
local endpoint = f(endpoints.GUILD_EMOJI, guild_id, emoji_id)
return self:request("PATCH", endpoint, payload)
end
function API:deleteGuildEmoji(guild_id, emoji_id) -- Emoji:delete
local endpoint = f(endpoints.GUILD_EMOJI, guild_id, emoji_id)
return self:request("DELETE", endpoint)
end
function API:createGuild(payload) -- Client:createGuild
local endpoint = endpoints.GUILDS
return self:request("POST", endpoint, payload)
end
function API:getGuild(guild_id) -- not exposed, use cache
local endpoint = f(endpoints.GUILD, guild_id)
return self:request("GET", endpoint)
end
function API:modifyGuild(guild_id, payload) -- Guild:_modify
local endpoint = f(endpoints.GUILD, guild_id)
return self:request("PATCH", endpoint, payload)
end
function API:deleteGuild(guild_id) -- Guild:delete
local endpoint = f(endpoints.GUILD, guild_id)
return self:request("DELETE", endpoint)
end
function API:getGuildChannels(guild_id) -- not exposed, use cache
local endpoint = f(endpoints.GUILD_CHANNELS, guild_id)
return self:request("GET", endpoint)
end
function API:createGuildChannel(guild_id, payload) -- Guild:create[Text|Voice]Channel
local endpoint = f(endpoints.GUILD_CHANNELS, guild_id)
return self:request("POST", endpoint, payload)
end
function API:modifyGuildChannelPositions(guild_id, payload) -- GuildChannel:move[Up|Down]
local endpoint = f(endpoints.GUILD_CHANNELS, guild_id)
return self:request("PATCH", endpoint, payload)
end
function API:getGuildMember(guild_id, user_id) -- Guild:getMember fallback
local endpoint = f(endpoints.GUILD_MEMBER, guild_id, user_id)
return self:request("GET", endpoint)
end
function API:listGuildMembers(guild_id) -- not exposed, use cache
local endpoint = f(endpoints.GUILD_MEMBERS, guild_id)
return self:request("GET", endpoint)
end
function API:addGuildMember(guild_id, user_id, payload) -- not exposed, limited use
local endpoint = f(endpoints.GUILD_MEMBER, guild_id, user_id)
return self:request("PUT", endpoint, payload)
end
function API:modifyGuildMember(guild_id, user_id, payload) -- various Member methods
local endpoint = f(endpoints.GUILD_MEMBER, guild_id, user_id)
return self:request("PATCH", endpoint, payload)
end
function API:modifyCurrentUsersNick(guild_id, payload) -- Member:setNickname
local endpoint = f(endpoints.GUILD_MEMBER_ME_NICK, guild_id)
return self:request("PATCH", endpoint, payload)
end
function API:addGuildMemberRole(guild_id, user_id, role_id, payload) -- Member:addrole
local endpoint = f(endpoints.GUILD_MEMBER_ROLE, guild_id, user_id, role_id)
return self:request("PUT", endpoint, payload)
end
function API:removeGuildMemberRole(guild_id, user_id, role_id) -- Member:removeRole
local endpoint = f(endpoints.GUILD_MEMBER_ROLE, guild_id, user_id, role_id)
return self:request("DELETE", endpoint)
end
function API:removeGuildMember(guild_id, user_id, query) -- Guild:kickUser
local endpoint = f(endpoints.GUILD_MEMBER, guild_id, user_id)
return self:request("DELETE", endpoint, nil, query)
end
function API:getGuildBans(guild_id) -- Guild:getBans
local endpoint = f(endpoints.GUILD_BANS, guild_id)
return self:request("GET", endpoint)
end
function API:getGuildBan(guild_id, user_id) -- Guild:getBan
local endpoint = f(endpoints.GUILD_BAN, guild_id, user_id)
return self:request("GET", endpoint)
end
function API:createGuildBan(guild_id, user_id, query) -- Guild:banUser
local endpoint = f(endpoints.GUILD_BAN, guild_id, user_id)
return self:request("PUT", endpoint, nil, query)
end
function API:removeGuildBan(guild_id, user_id, query) -- Guild:unbanUser / Ban:delete
local endpoint = f(endpoints.GUILD_BAN, guild_id, user_id)
return self:request("DELETE", endpoint, nil, query)
end
function API:getGuildRoles(guild_id) -- not exposed, use cache
local endpoint = f(endpoints.GUILD_ROLES, guild_id)
return self:request("GET", endpoint)
end
function API:createGuildRole(guild_id, payload) -- Guild:createRole
local endpoint = f(endpoints.GUILD_ROLES, guild_id)
return self:request("POST", endpoint, payload)
end
function API:modifyGuildRolePositions(guild_id, payload) -- Role:move[Up|Down]
local endpoint = f(endpoints.GUILD_ROLES, guild_id)
return self:request("PATCH", endpoint, payload)
end
function API:modifyGuildRole(guild_id, role_id, payload) -- Role:_modify
local endpoint = f(endpoints.GUILD_ROLE, guild_id, role_id)
return self:request("PATCH", endpoint, payload)
end
function API:deleteGuildRole(guild_id, role_id) -- Role:delete
local endpoint = f(endpoints.GUILD_ROLE, guild_id, role_id)
return self:request("DELETE", endpoint)
end
function API:getGuildPruneCount(guild_id, query) -- Guild:getPruneCount
local endpoint = f(endpoints.GUILD_PRUNE, guild_id)
return self:request("GET", endpoint, nil, query)
end
function API:beginGuildPrune(guild_id, payload, query) -- Guild:pruneMembers
local endpoint = f(endpoints.GUILD_PRUNE, guild_id)
return self:request("POST", endpoint, payload, query)
end
function API:getGuildVoiceRegions(guild_id) -- Guild:listVoiceRegions
local endpoint = f(endpoints.GUILD_REGIONS, guild_id)
return self:request("GET", endpoint)
end
function API:getGuildInvites(guild_id) -- Guild:getInvites
local endpoint = f(endpoints.GUILD_INVITES, guild_id)
return self:request("GET", endpoint)
end
function API:getGuildIntegrations(guild_id) -- not exposed, maybe in the future
local endpoint = f(endpoints.GUILD_INTEGRATIONS, guild_id)
return self:request("GET", endpoint)
end
function API:createGuildIntegration(guild_id, payload) -- not exposed, maybe in the future
local endpoint = f(endpoints.GUILD_INTEGRATIONS, guild_id)
return self:request("POST", endpoint, payload)
end
function API:modifyGuildIntegration(guild_id, integration_id, payload) -- not exposed, maybe in the future
local endpoint = f(endpoints.GUILD_INTEGRATION, guild_id, integration_id)
return self:request("PATCH", endpoint, payload)
end
function API:deleteGuildIntegration(guild_id, integration_id) -- not exposed, maybe in the future
local endpoint = f(endpoints.GUILD_INTEGRATION, guild_id, integration_id)
return self:request("DELETE", endpoint)
end
function API:syncGuildIntegration(guild_id, integration_id, payload) -- not exposed, maybe in the future
local endpoint = f(endpoints.GUILD_INTEGRATION_SYNC, guild_id, integration_id)
return self:request("POST", endpoint, payload)
end
function API:getGuildEmbed(guild_id) -- not exposed, maybe in the future
local endpoint = f(endpoints.GUILD_EMBED, guild_id)
return self:request("GET", endpoint)
end
function API:modifyGuildEmbed(guild_id, payload) -- not exposed, maybe in the future
local endpoint = f(endpoints.GUILD_EMBED, guild_id)
return self:request("PATCH", endpoint, payload)
end
function API:getInvite(invite_code, query) -- Client:getInvite
local endpoint = f(endpoints.INVITE, invite_code)
return self:request("GET", endpoint, nil, query)
end
function API:deleteInvite(invite_code) -- Invite:delete
local endpoint = f(endpoints.INVITE, invite_code)
return self:request("DELETE", endpoint)
end
function API:acceptInvite(invite_code, payload) -- not exposed, invalidates tokens
local endpoint = f(endpoints.INVITE, invite_code)
return self:request("POST", endpoint, payload)
end
function API:getCurrentUser() -- API:authenticate
local endpoint = endpoints.USER_ME
return self:request("GET", endpoint)
end
function API:getUser(user_id) -- Client:getUser
local endpoint = f(endpoints.USER, user_id)
return self:request("GET", endpoint)
end
function API:modifyCurrentUser(payload) -- Client:_modify
local endpoint = endpoints.USER_ME
return self:request("PATCH", endpoint, payload)
end
function API:getCurrentUserGuilds() -- not exposed, use cache
local endpoint = endpoints.USER_ME_GUILDS
return self:request("GET", endpoint)
end
function API:leaveGuild(guild_id) -- Guild:leave
local endpoint = f(endpoints.USER_ME_GUILD, guild_id)
return self:request("DELETE", endpoint)
end
function API:getUserDMs() -- not exposed, use cache
local endpoint = endpoints.USER_ME_CHANNELS
return self:request("GET", endpoint)
end
function API:createDM(payload) -- User:getPrivateChannel fallback
local endpoint = endpoints.USER_ME_CHANNELS
return self:request("POST", endpoint, payload)
end
function API:createGroupDM(payload) -- Client:createGroupChannel
local endpoint = endpoints.USER_ME_CHANNELS
return self:request("POST", endpoint, payload)
end
function API:getUsersConnections() -- Client:getConnections
local endpoint = endpoints.USER_ME_CONNECTIONS
return self:request("GET", endpoint)
end
function API:listVoiceRegions() -- Client:listVoiceRegions
local endpoint = endpoints.VOICE_REGIONS
return self:request("GET", endpoint)
end
function API:createWebhook(channel_id, payload) -- GuildTextChannel:createWebhook
local endpoint = f(endpoints.CHANNEL_WEBHOOKS, channel_id)
return self:request("POST", endpoint, payload)
end
function API:getChannelWebhooks(channel_id) -- GuildTextChannel:getWebhooks
local endpoint = f(endpoints.CHANNEL_WEBHOOKS, channel_id)
return self:request("GET", endpoint)
end
function API:getGuildWebhooks(guild_id) -- Guild:getWebhooks
local endpoint = f(endpoints.GUILD_WEBHOOKS, guild_id)
return self:request("GET", endpoint)
end
function API:getWebhook(webhook_id) -- Client:getWebhook
local endpoint = f(endpoints.WEBHOOK, webhook_id)
return self:request("GET", endpoint)
end
function API:getWebhookWithToken(webhook_id, webhook_token) -- not exposed, needs webhook client
local endpoint = f(endpoints.WEBHOOK_TOKEN, webhook_id, webhook_token)
return self:request("GET", endpoint)
end
function API:modifyWebhook(webhook_id, payload) -- Webhook:_modify
local endpoint = f(endpoints.WEBHOOK, webhook_id)
return self:request("PATCH", endpoint, payload)
end
function API:modifyWebhookWithToken(webhook_id, webhook_token, payload) -- not exposed, needs webhook client
local endpoint = f(endpoints.WEBHOOK_TOKEN, webhook_id, webhook_token)
return self:request("PATCH", endpoint, payload)
end
function API:deleteWebhook(webhook_id) -- Webhook:delete
local endpoint = f(endpoints.WEBHOOK, webhook_id)
return self:request("DELETE", endpoint)
end
function API:deleteWebhookWithToken(webhook_id, webhook_token) -- not exposed, needs webhook client
local endpoint = f(endpoints.WEBHOOK_TOKEN, webhook_id, webhook_token)
return self:request("DELETE", endpoint)
end
function API:executeWebhook(webhook_id, webhook_token, payload) -- not exposed, needs webhook client
local endpoint = f(endpoints.WEBHOOK_TOKEN, webhook_id, webhook_token)
return self:request("POST", endpoint, payload)
end
function API:executeSlackCompatibleWebhook(webhook_id, webhook_token, payload) -- not exposed, needs webhook client
local endpoint = f(endpoints.WEBHOOK_TOKEN_SLACK, webhook_id, webhook_token)
return self:request("POST", endpoint, payload)
end
function API:executeGitHubCompatibleWebhook(webhook_id, webhook_token, payload) -- not exposed, needs webhook client
local endpoint = f(endpoints.WEBHOOK_TOKEN_GITHUB, webhook_id, webhook_token)
return self:request("POST", endpoint, payload)
end
function API:getGateway() -- Client:run
local endpoint = endpoints.GATEWAY
return self:request("GET", endpoint)
end
function API:getGatewayBot() -- Client:run
local endpoint = endpoints.GATEWAY_BOT
return self:request("GET", endpoint)
end
function API:getCurrentApplicationInformation() -- Client:run
local endpoint = endpoints.OAUTH2_APPLICATION_ME
return self:request("GET", endpoint)
end
-- end of auto-generated methods --
return API