From 26e9f891503d7a9d586c5e98ff54331fcffd93f5 Mon Sep 17 00:00:00 2001 From: Astoria Floyd Date: Fri, 6 Aug 2021 13:46:54 -0500 Subject: [PATCH] add Initial Bot files --- bot.lua | 38 + deps/base64.lua | 114 +++ deps/coro-channel.lua | 190 ++++ deps/coro-http.lua | 206 ++++ deps/coro-net.lua | 196 ++++ deps/coro-websocket.lua | 196 ++++ deps/coro-wrapper.lua | 151 +++ deps/discordia/docgen.lua | 373 +++++++ deps/discordia/examples/appender.lua | 28 + deps/discordia/examples/basicCommands.lua | 25 + deps/discordia/examples/embed.lua | 45 + .../discordia/examples/helpCommandExample.lua | 44 + deps/discordia/examples/pingPong.lua | 20 + deps/discordia/init.lua | 18 + deps/discordia/libs/class.lua | 165 ++++ deps/discordia/libs/client/API.lua | 711 ++++++++++++++ deps/discordia/libs/client/Client.lua | 679 +++++++++++++ deps/discordia/libs/client/EventHandler.lua | 540 ++++++++++ deps/discordia/libs/client/Resolver.lua | 202 ++++ deps/discordia/libs/client/Shard.lua | 257 +++++ deps/discordia/libs/client/WebSocket.lua | 121 +++ deps/discordia/libs/constants.lua | 17 + deps/discordia/libs/containers/Activity.lua | 157 +++ .../libs/containers/AuditLogEntry.lua | 227 +++++ deps/discordia/libs/containers/Ban.lua | 52 + deps/discordia/libs/containers/Emoji.lua | 168 ++++ .../libs/containers/GroupChannel.lua | 122 +++ deps/discordia/libs/containers/Guild.lua | 921 ++++++++++++++++++ .../libs/containers/GuildCategoryChannel.lua | 83 ++ .../libs/containers/GuildTextChannel.lua | 165 ++++ .../libs/containers/GuildVoiceChannel.lua | 138 +++ deps/discordia/libs/containers/Invite.lua | 187 ++++ deps/discordia/libs/containers/Member.lua | 540 ++++++++++ deps/discordia/libs/containers/Message.lua | 601 ++++++++++++ .../libs/containers/PermissionOverwrite.lua | 234 +++++ .../libs/containers/PrivateChannel.lua | 37 + deps/discordia/libs/containers/Reaction.lua | 149 +++ .../libs/containers/Relationship.lua | 27 + deps/discordia/libs/containers/Role.lua | 383 ++++++++ deps/discordia/libs/containers/User.lua | 186 ++++ deps/discordia/libs/containers/Webhook.lua | 147 +++ .../libs/containers/abstract/Channel.lua | 66 ++ .../libs/containers/abstract/Container.lua | 68 ++ .../libs/containers/abstract/GuildChannel.lua | 281 ++++++ .../libs/containers/abstract/Snowflake.lua | 63 ++ .../libs/containers/abstract/TextChannel.lua | 337 +++++++ .../libs/containers/abstract/UserPresence.lua | 127 +++ deps/discordia/libs/endpoints.lua | 54 + deps/discordia/libs/enums.lua | 214 ++++ deps/discordia/libs/extensions.lua | 253 +++++ .../libs/iterables/ArrayIterable.lua | 108 ++ deps/discordia/libs/iterables/Cache.lua | 150 +++ .../libs/iterables/FilteredIterable.lua | 27 + deps/discordia/libs/iterables/Iterable.lua | 278 ++++++ .../libs/iterables/SecondaryCache.lua | 78 ++ .../libs/iterables/TableIterable.lua | 53 + deps/discordia/libs/iterables/WeakCache.lua | 25 + deps/discordia/libs/utils/Clock.lua | 56 ++ deps/discordia/libs/utils/Color.lua | 313 ++++++ deps/discordia/libs/utils/Date.lua | 394 ++++++++ deps/discordia/libs/utils/Deque.lua | 105 ++ deps/discordia/libs/utils/Emitter.lua | 226 +++++ deps/discordia/libs/utils/Logger.lua | 82 ++ deps/discordia/libs/utils/Mutex.lua | 68 ++ deps/discordia/libs/utils/Permissions.lua | 254 +++++ deps/discordia/libs/utils/Stopwatch.lua | 85 ++ deps/discordia/libs/utils/Time.lua | 277 ++++++ deps/discordia/libs/voice/VoiceConnection.lua | 432 ++++++++ deps/discordia/libs/voice/VoiceManager.lua | 33 + deps/discordia/libs/voice/VoiceSocket.lua | 197 ++++ deps/discordia/libs/voice/opus.lua | 241 +++++ deps/discordia/libs/voice/sodium.lua | 85 ++ .../libs/voice/streams/FFmpegProcess.lua | 88 ++ .../libs/voice/streams/PCMGenerator.lua | 18 + .../libs/voice/streams/PCMStream.lua | 28 + .../libs/voice/streams/PCMString.lua | 28 + deps/discordia/package.lua | 36 + deps/http-codec.lua | 301 ++++++ deps/pathjoin.lua | 124 +++ deps/resource.lua | 88 ++ deps/secure-socket/biowrap.lua | 115 +++ deps/secure-socket/context.lua | 121 +++ deps/secure-socket/init.lua | 41 + deps/secure-socket/package.lua | 12 + deps/secure-socket/root_ca.dat | Bin 0 -> 146914 bytes deps/sha1/init.lua | 194 ++++ deps/sha1/test-sha1.lua | 22 + deps/websocket-codec.lua | 301 ++++++ gateway.json | 1 + 89 files changed, 15408 insertions(+) create mode 100644 bot.lua create mode 100644 deps/base64.lua create mode 100644 deps/coro-channel.lua create mode 100644 deps/coro-http.lua create mode 100644 deps/coro-net.lua create mode 100644 deps/coro-websocket.lua create mode 100644 deps/coro-wrapper.lua create mode 100644 deps/discordia/docgen.lua create mode 100644 deps/discordia/examples/appender.lua create mode 100644 deps/discordia/examples/basicCommands.lua create mode 100644 deps/discordia/examples/embed.lua create mode 100644 deps/discordia/examples/helpCommandExample.lua create mode 100644 deps/discordia/examples/pingPong.lua create mode 100644 deps/discordia/init.lua create mode 100644 deps/discordia/libs/class.lua create mode 100644 deps/discordia/libs/client/API.lua create mode 100644 deps/discordia/libs/client/Client.lua create mode 100644 deps/discordia/libs/client/EventHandler.lua create mode 100644 deps/discordia/libs/client/Resolver.lua create mode 100644 deps/discordia/libs/client/Shard.lua create mode 100644 deps/discordia/libs/client/WebSocket.lua create mode 100644 deps/discordia/libs/constants.lua create mode 100644 deps/discordia/libs/containers/Activity.lua create mode 100644 deps/discordia/libs/containers/AuditLogEntry.lua create mode 100644 deps/discordia/libs/containers/Ban.lua create mode 100644 deps/discordia/libs/containers/Emoji.lua create mode 100644 deps/discordia/libs/containers/GroupChannel.lua create mode 100644 deps/discordia/libs/containers/Guild.lua create mode 100644 deps/discordia/libs/containers/GuildCategoryChannel.lua create mode 100644 deps/discordia/libs/containers/GuildTextChannel.lua create mode 100644 deps/discordia/libs/containers/GuildVoiceChannel.lua create mode 100644 deps/discordia/libs/containers/Invite.lua create mode 100644 deps/discordia/libs/containers/Member.lua create mode 100644 deps/discordia/libs/containers/Message.lua create mode 100644 deps/discordia/libs/containers/PermissionOverwrite.lua create mode 100644 deps/discordia/libs/containers/PrivateChannel.lua create mode 100644 deps/discordia/libs/containers/Reaction.lua create mode 100644 deps/discordia/libs/containers/Relationship.lua create mode 100644 deps/discordia/libs/containers/Role.lua create mode 100644 deps/discordia/libs/containers/User.lua create mode 100644 deps/discordia/libs/containers/Webhook.lua create mode 100644 deps/discordia/libs/containers/abstract/Channel.lua create mode 100644 deps/discordia/libs/containers/abstract/Container.lua create mode 100644 deps/discordia/libs/containers/abstract/GuildChannel.lua create mode 100644 deps/discordia/libs/containers/abstract/Snowflake.lua create mode 100644 deps/discordia/libs/containers/abstract/TextChannel.lua create mode 100644 deps/discordia/libs/containers/abstract/UserPresence.lua create mode 100644 deps/discordia/libs/endpoints.lua create mode 100644 deps/discordia/libs/enums.lua create mode 100644 deps/discordia/libs/extensions.lua create mode 100644 deps/discordia/libs/iterables/ArrayIterable.lua create mode 100644 deps/discordia/libs/iterables/Cache.lua create mode 100644 deps/discordia/libs/iterables/FilteredIterable.lua create mode 100644 deps/discordia/libs/iterables/Iterable.lua create mode 100644 deps/discordia/libs/iterables/SecondaryCache.lua create mode 100644 deps/discordia/libs/iterables/TableIterable.lua create mode 100644 deps/discordia/libs/iterables/WeakCache.lua create mode 100644 deps/discordia/libs/utils/Clock.lua create mode 100644 deps/discordia/libs/utils/Color.lua create mode 100644 deps/discordia/libs/utils/Date.lua create mode 100644 deps/discordia/libs/utils/Deque.lua create mode 100644 deps/discordia/libs/utils/Emitter.lua create mode 100644 deps/discordia/libs/utils/Logger.lua create mode 100644 deps/discordia/libs/utils/Mutex.lua create mode 100644 deps/discordia/libs/utils/Permissions.lua create mode 100644 deps/discordia/libs/utils/Stopwatch.lua create mode 100644 deps/discordia/libs/utils/Time.lua create mode 100644 deps/discordia/libs/voice/VoiceConnection.lua create mode 100644 deps/discordia/libs/voice/VoiceManager.lua create mode 100644 deps/discordia/libs/voice/VoiceSocket.lua create mode 100644 deps/discordia/libs/voice/opus.lua create mode 100644 deps/discordia/libs/voice/sodium.lua create mode 100644 deps/discordia/libs/voice/streams/FFmpegProcess.lua create mode 100644 deps/discordia/libs/voice/streams/PCMGenerator.lua create mode 100644 deps/discordia/libs/voice/streams/PCMStream.lua create mode 100644 deps/discordia/libs/voice/streams/PCMString.lua create mode 100644 deps/discordia/package.lua create mode 100644 deps/http-codec.lua create mode 100644 deps/pathjoin.lua create mode 100644 deps/resource.lua create mode 100644 deps/secure-socket/biowrap.lua create mode 100644 deps/secure-socket/context.lua create mode 100644 deps/secure-socket/init.lua create mode 100644 deps/secure-socket/package.lua create mode 100644 deps/secure-socket/root_ca.dat create mode 100644 deps/sha1/init.lua create mode 100644 deps/sha1/test-sha1.lua create mode 100644 deps/websocket-codec.lua create mode 100644 gateway.json diff --git a/bot.lua b/bot.lua new file mode 100644 index 0000000..7cfb11e --- /dev/null +++ b/bot.lua @@ -0,0 +1,38 @@ +local discordia = require('discordia') +local client = discordia.Client() +seed = os.time() +math.randomseed(seed) + +client:on('ready', function() + print('Logged in as '.. client.user.username) +end) + +client:on('messageCreate', function(message) + if message.content == '!ping' then + message.channel:send('Pong!') + end +end) + +client:on('messageCreate', function(message) + if message.content == '!roll' then + dice = math.random(20) + message.channel:send('You rolled a ' .. dice .. ' out of 20') + if dice==20 then + message.channel:send('Nat 20! Crititcal Hit') + print('nat20check') + else if dice==1 then + message.channel:send('Nat 1! Critical Miss!') + print('nat1check') + end + end + end +end) + +client:on('messageCreate', function(message) + if message.content == '!time' then + message.channel:send('The current time in military time is ' .. os.date() .. ', atleast in Chicago!') + end +end) + + +client:run('Bot ODczMjU1Mjk2MDI0MzIyMDU5.YQ1wXg.BbXq1fu-4nlG95EkLkHujVEObG4') diff --git a/deps/base64.lua b/deps/base64.lua new file mode 100644 index 0000000..abe3191 --- /dev/null +++ b/deps/base64.lua @@ -0,0 +1,114 @@ +--[[lit-meta + name = "creationix/base64" + description = "A pure lua implemention of base64 using bitop" + tags = {"crypto", "base64", "bitop"} + version = "2.0.0" + license = "MIT" + author = { name = "Tim Caswell" } +]] + +local bit = require 'bit' +local rshift = bit.rshift +local lshift = bit.lshift +local bor = bit.bor +local band = bit.band +local char = string.char +local byte = string.byte +local concat = table.concat +local codes = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=' + +-- Loop over input 3 bytes at a time +-- a,b,c are 3 x 8-bit numbers +-- they are encoded into groups of 4 x 6-bit numbers +-- aaaaaa aabbbb bbbbcc cccccc +-- if there is no c, then pad the 4th with = +-- if there is also no b then pad the 3rd with = +local function base64Encode(str) + local parts = {} + local j = 1 + for i = 1, #str, 3 do + local a, b, c = byte(str, i, i + 2) + parts[j] = char( + -- Higher 6 bits of a + byte(codes, rshift(a, 2) + 1), + -- Lower 2 bits of a + high 4 bits of b + byte(codes, bor( + lshift(band(a, 3), 4), + b and rshift(b, 4) or 0 + ) + 1), + -- Low 4 bits of b + High 2 bits of c + b and byte(codes, bor( + lshift(band(b, 15), 2), + c and rshift(c, 6) or 0 + ) + 1) or 61, -- 61 is '=' + -- Lower 6 bits of c + c and byte(codes, band(c, 63) + 1) or 61 -- 61 is '=' + ) + j = j + 1 + end + return concat(parts) +end + +-- Reverse map from character code to 6-bit integer +local map = {} +for i = 1, #codes do + map[byte(codes, i)] = i - 1 +end + +-- loop over input 4 characters at a time +-- The characters are mapped to 4 x 6-bit integers a,b,c,d +-- They need to be reassalbled into 3 x 8-bit bytes +-- aaaaaabb bbbbcccc ccdddddd +-- if d is padding then there is no 3rd byte +-- if c is padding then there is no 2nd byte +local function base64Decode(data) + local bytes = {} + local j = 1 + for i = 1, #data, 4 do + local a = map[byte(data, i)] + local b = map[byte(data, i + 1)] + local c = map[byte(data, i + 2)] + local d = map[byte(data, i + 3)] + + -- higher 6 bits are the first char + -- lower 2 bits are upper 2 bits of second char + bytes[j] = char(bor(lshift(a, 2), rshift(b, 4))) + + -- if the third char is not padding, we have a second byte + if c < 64 then + -- high 4 bits come from lower 4 bits in b + -- low 4 bits come from high 4 bits in c + bytes[j + 1] = char(bor(lshift(band(b, 0xf), 4), rshift(c, 2))) + + -- if the fourth char is not padding, we have a third byte + if d < 64 then + -- Upper 2 bits come from Lower 2 bits of c + -- Lower 6 bits come from d + bytes[j + 2] = char(bor(lshift(band(c, 3), 6), d)) + end + end + j = j + 3 + end + return concat(bytes) +end + +assert(base64Encode("") == "") +assert(base64Encode("f") == "Zg==") +assert(base64Encode("fo") == "Zm8=") +assert(base64Encode("foo") == "Zm9v") +assert(base64Encode("foob") == "Zm9vYg==") +assert(base64Encode("fooba") == "Zm9vYmE=") +assert(base64Encode("foobar") == "Zm9vYmFy") + +assert(base64Decode("") == "") +assert(base64Decode("Zg==") == "f") +assert(base64Decode("Zm8=") == "fo") +assert(base64Decode("Zm9v") == "foo") +assert(base64Decode("Zm9vYg==") == "foob") +assert(base64Decode("Zm9vYmE=") == "fooba") +assert(base64Decode("Zm9vYmFy") == "foobar") + +return { + encode = base64Encode, + decode = base64Decode, +} diff --git a/deps/coro-channel.lua b/deps/coro-channel.lua new file mode 100644 index 0000000..90ae107 --- /dev/null +++ b/deps/coro-channel.lua @@ -0,0 +1,190 @@ +--[[lit-meta + name = "creationix/coro-channel" + version = "3.0.3" + homepage = "https://github.com/luvit/lit/blob/master/deps/coro-channel.lua" + description = "An adapter for wrapping uv streams as coro-streams." + tags = {"coro", "adapter"} + license = "MIT" + author = { name = "Tim Caswell" } +]] + +-- local p = require('pretty-print').prettyPrint + +local function assertResume(thread, ...) + local success, err = coroutine.resume(thread, ...) + if not success then + error(debug.traceback(thread, err), 0) + end +end + +local function makeCloser(socket) + local closer = { + read = false, + written = false, + errored = false, + } + + local closed = false + + local function close() + if closed then return end + closed = true + if not closer.readClosed then + closer.readClosed = true + if closer.onClose then + closer.onClose() + end + end + if not socket:is_closing() then + socket:close() + end + end + + closer.close = close + + function closer.check() + if closer.errored or (closer.read and closer.written) then + return close() + end + end + + return closer +end + +local function makeRead(socket, closer) + local paused = true + + local queue = {} + local tindex = 0 + local dindex = 0 + + local function dispatch(data) + + -- p("<-", data[1]) + + if tindex > dindex then + local thread = queue[dindex] + queue[dindex] = nil + dindex = dindex + 1 + assertResume(thread, unpack(data)) + else + queue[dindex] = data + dindex = dindex + 1 + if not paused then + paused = true + assert(socket:read_stop()) + end + end + end + + closer.onClose = function () + if not closer.read then + closer.read = true + return dispatch {nil, closer.errored} + end + end + + local function onRead(err, chunk) + if err then + closer.errored = err + return closer.check() + end + if not chunk then + if closer.read then return end + closer.read = true + dispatch {} + return closer.check() + end + return dispatch {chunk} + end + + local function read() + if dindex > tindex then + local data = queue[tindex] + queue[tindex] = nil + tindex = tindex + 1 + return unpack(data) + end + if paused then + paused = false + assert(socket:read_start(onRead)) + end + queue[tindex] = coroutine.running() + tindex = tindex + 1 + return coroutine.yield() + end + + -- Auto use wrapper library for backwards compat + return read +end + +local function makeWrite(socket, closer) + + local function wait() + local thread = coroutine.running() + return function (err) + assertResume(thread, err) + end + end + + local function write(chunk) + if closer.written then + return nil, "already shutdown" + end + + -- p("->", chunk) + + if chunk == nil then + closer.written = true + closer.check() + local success, err = socket:shutdown(wait()) + if not success then + return nil, err + end + err = coroutine.yield() + return not err, err + end + + local success, err = socket:write(chunk, wait()) + if not success then + closer.errored = err + closer.check() + return nil, err + end + err = coroutine.yield() + return not err, err + end + + return write +end + +local function wrapRead(socket) + local closer = makeCloser(socket) + closer.written = true + return makeRead(socket, closer), closer.close +end + +local function wrapWrite(socket) + local closer = makeCloser(socket) + closer.read = true + return makeWrite(socket, closer), closer.close +end + +local function wrapStream(socket) + assert(socket + and socket.write + and socket.shutdown + and socket.read_start + and socket.read_stop + and socket.is_closing + and socket.close, "socket does not appear to be a socket/uv_stream_t") + + local closer = makeCloser(socket) + return makeRead(socket, closer), makeWrite(socket, closer), closer.close +end + +return { + wrapRead = wrapRead, + wrapWrite = wrapWrite, + wrapStream = wrapStream, +} diff --git a/deps/coro-http.lua b/deps/coro-http.lua new file mode 100644 index 0000000..8873ae7 --- /dev/null +++ b/deps/coro-http.lua @@ -0,0 +1,206 @@ +--[[lit-meta + name = "creationix/coro-http" + version = "3.2.2" + dependencies = { + "creationix/coro-net@3.3.0", + "luvit/http-codec@3.0.0" + } + homepage = "https://github.com/luvit/lit/blob/master/deps/coro-http.lua" + description = "An coro style http(s) client and server helper." + tags = {"coro", "http"} + license = "MIT" + author = { name = "Tim Caswell" } +]] + +local httpCodec = require('http-codec') +local net = require('coro-net') + +local function createServer(host, port, onConnect) + return net.createServer({ + host = host, + port = port, + encoder = httpCodec.encoder, + decoder = httpCodec.decoder, + }, function (read, write, socket) + for head in read do + local parts = {} + for part in read do + if #part > 0 then + parts[#parts + 1] = part + else + break + end + end + local body = table.concat(parts) + head, body = onConnect(head, body, socket) + write(head) + if body then write(body) end + write("") + if not head.keepAlive then break end + end + write() + end) +end + +local function parseUrl(url) + local protocol, host, hostname, port, path = url:match("^(https?:)//(([^/:]+):?([0-9]*))(/?.*)$") + if not protocol then error("Not a valid http url: " .. url) end + local tls = protocol == "https:" + port = port and tonumber(port) or (tls and 443 or 80) + if path == "" then path = "/" end + return { + tls = tls, + host = host, + hostname = hostname, + port = port, + path = path + } +end + +local connections = {} + +local function getConnection(host, port, tls, timeout) + for i = #connections, 1, -1 do + local connection = connections[i] + if connection.host == host and connection.port == port and connection.tls == tls then + table.remove(connections, i) + -- Make sure the connection is still alive before reusing it. + if not connection.socket:is_closing() then + connection.reused = true + connection.socket:ref() + return connection + end + end + end + local read, write, socket, updateDecoder, updateEncoder = assert(net.connect { + host = host, + port = port, + tls = tls, + timeout = timeout, + encoder = httpCodec.encoder, + decoder = httpCodec.decoder + }) + return { + socket = socket, + host = host, + port = port, + tls = tls, + read = read, + write = write, + updateEncoder = updateEncoder, + updateDecoder = updateDecoder, + reset = function () + -- This is called after parsing the response head from a HEAD request. + -- If you forget, the codec might hang waiting for a body that doesn't exist. + updateDecoder(httpCodec.decoder()) + end + } +end + +local function saveConnection(connection) + if connection.socket:is_closing() then return end + connections[#connections + 1] = connection + connection.socket:unref() +end + +local function request(method, url, headers, body, customOptions) + -- customOptions = { timeout = number, followRedirects = boolean } + local options = {} + if type(customOptions) == "number" then + -- Ensure backwards compatibility, where customOptions used to just be timeout + options.timeout = customOptions + else + options = customOptions or {} + end + options.followRedirects = options.followRedirects == nil and true or options.followRedirects -- Follow any redirects, Default: true + + local uri = parseUrl(url) + local connection = getConnection(uri.hostname, uri.port, uri.tls, options.timeout) + local read = connection.read + local write = connection.write + + local req = { + method = method, + path = uri.path, + {"Host", uri.host} + } + local contentLength + local chunked + if headers then + for i = 1, #headers do + local key, value = unpack(headers[i]) + key = key:lower() + if key == "content-length" then + contentLength = value + elseif key == "content-encoding" and value:lower() == "chunked" then + chunked = true + end + req[#req + 1] = headers[i] + end + end + + if type(body) == "string" then + if not chunked and not contentLength then + req[#req + 1] = {"Content-Length", #body} + end + end + + write(req) + if body then write(body) end + local res = read() + if not res then + if not connection.socket:is_closing() then + connection.socket:close() + end + -- If we get an immediate close on a reused socket, try again with a new socket. + -- TODO: think about if this could resend requests with side effects and cause + -- them to double execute in the remote server. + if connection.reused then + return request(method, url, headers, body) + end + error("Connection closed") + end + + body = {} + if req.method == "HEAD" then + connection.reset() + else + while true do + local item = read() + if not item then + res.keepAlive = false + break + end + if #item == 0 then + break + end + body[#body + 1] = item + end + end + + if res.keepAlive then + saveConnection(connection) + else + write() + end + + -- Follow redirects + if method == "GET" and (res.code == 302 or res.code == 307) and options.followRedirects then + for i = 1, #res do + local key, location = unpack(res[i]) + if key:lower() == "location" then + return request(method, location, headers) + end + end + end + + return res, table.concat(body) +end + +return { + createServer = createServer, + parseUrl = parseUrl, + getConnection = getConnection, + saveConnection = saveConnection, + request = request, +} diff --git a/deps/coro-net.lua b/deps/coro-net.lua new file mode 100644 index 0000000..b621bbe --- /dev/null +++ b/deps/coro-net.lua @@ -0,0 +1,196 @@ +--[[lit-meta + name = "creationix/coro-net" + version = "3.3.0" + dependencies = { + "creationix/coro-channel@3.0.0", + "creationix/coro-wrapper@3.0.0", + } + optionalDependencies = { + "luvit/secure-socket@1.0.0" + } + homepage = "https://github.com/luvit/lit/blob/master/deps/coro-net.lua" + description = "An coro style client and server helper for tcp and pipes." + tags = {"coro", "tcp", "pipe", "net"} + license = "MIT" + author = { name = "Tim Caswell" } +]] + +local uv = require('uv') +local wrapStream = require('coro-channel').wrapStream +local wrapper = require('coro-wrapper') +local merger = wrapper.merger +local decoder = wrapper.decoder +local encoder = wrapper.encoder +local secureSocket -- Lazy required from "secure-socket" on first use. + +local function assertResume(thread, ...) + local success, err = coroutine.resume(thread, ...) + if not success then + error(debug.traceback(thread, err), 0) + end +end + +local function makeCallback(timeout) + local thread = coroutine.running() + local timer, done + if timeout then + timer = uv.new_timer() + timer:start(timeout, 0, function () + if done then return end + done = true + timer:close() + return assertResume(thread, nil, "timeout") + end) + end + return function (err, data) + if done then return end + done = true + if timer then timer:close() end + if err then + return assertResume(thread, nil, err) + end + return assertResume(thread, data or true) + end +end + +local function normalize(options, server) + local t = type(options) + if t == "string" then + options = {path=options} + elseif t == "number" then + options = {port=options} + elseif t ~= "table" then + assert("Net options must be table, string, or number") + end + if options.port or options.host then + options.isTcp = true + options.host = options.host or "127.0.0.1" + assert(options.port, "options.port is required for tcp connections") + elseif options.path then + options.isTcp = false + else + error("Must set either options.path or options.port") + end + if options.tls == true then + options.tls = {} + end + if options.tls then + if server then + options.tls.server = true + assert(options.tls.cert, "TLS servers require a certificate") + assert(options.tls.key, "TLS servers require a key") + else + options.tls.server = false + options.tls.servername = options.host + end + end + return options +end + +local function connect(options) + local socket, success, err + options = normalize(options) + if options.isTcp then + success, err = uv.getaddrinfo(options.host, options.port, { + socktype = options.socktype or "stream", + family = options.family or "inet", + }, makeCallback(options.timeout)) + if not success then return nil, err end + local res + res, err = coroutine.yield() + if not res then return nil, err end + socket = uv.new_tcp() + socket:connect(res[1].addr, res[1].port, makeCallback(options.timeout)) + else + socket = uv.new_pipe(false) + socket:connect(options.path, makeCallback(options.timeout)) + end + success, err = coroutine.yield() + if not success then return nil, err end + local dsocket + if options.tls then + if not secureSocket then secureSocket = require('secure-socket') end + dsocket, err = secureSocket(socket, options.tls) + if not dsocket then + return nil, err + end + else + dsocket = socket + end + + local read, write, close = wrapStream(dsocket) + local updateDecoder, updateEncoder + if options.scan then + -- TODO: Should we expose updateScan somehow? + read = merger(read, options.scan) + end + if options.decoder then + read, updateDecoder = decoder(read, options.decoder()) + elseif options.decode then + read, updateDecoder = decoder(read, options.decode) + end + if options.encoder then + write, updateEncoder = encoder(write, options.encoder()) + elseif options.encode then + write, updateEncoder = encoder(write, options.encode) + end + return read, write, dsocket, updateDecoder, updateEncoder, close +end + +local function createServer(options, onConnect) + local server + options = normalize(options, true) + if options.isTcp then + server = uv.new_tcp() + assert(server:bind(options.host, options.port)) + else + server = uv.new_pipe(false) + assert(server:bind(options.path)) + end + assert(server:listen(256, function (err) + assert(not err, err) + local socket = options.isTcp and uv.new_tcp() or uv.new_pipe(false) + server:accept(socket) + coroutine.wrap(function () + local success, failure = xpcall(function () + local dsocket + if options.tls then + if not secureSocket then secureSocket = require('secure-socket') end + dsocket = assert(secureSocket(socket, options.tls)) + dsocket.socket = socket + else + dsocket = socket + end + + local read, write = wrapStream(dsocket) + local updateDecoder, updateEncoder + if options.scan then + -- TODO: should we expose updateScan somehow? + read = merger(read, options.scan) + end + if options.decoder then + read, updateDecoder = decoder(read, options.decoder()) + elseif options.decode then + read, updateDecoder = decoder(read, options.decode) + end + if options.encoder then + write, updateEncoder = encoder(write, options.encoder()) + elseif options.encode then + write, updateEncoder = encoder(write, options.encode) + end + + return onConnect(read, write, dsocket, updateDecoder, updateEncoder) + end, debug.traceback) + if not success then + print(failure) + end + end)() + end)) + return server +end + +return { + makeCallback = makeCallback, + connect = connect, + createServer = createServer, +} diff --git a/deps/coro-websocket.lua b/deps/coro-websocket.lua new file mode 100644 index 0000000..d225bef --- /dev/null +++ b/deps/coro-websocket.lua @@ -0,0 +1,196 @@ +--[[lit-meta + name = "creationix/coro-websocket" + version = "3.1.1" + dependencies = { + "luvit/http-codec@3.0.0", + "creationix/websocket-codec@3.0.0", + "creationix/coro-net@3.3.0", + } + homepage = "https://github.com/luvit/lit/blob/master/deps/coro-websocket.lua" + description = "Websocket helpers assuming coro style I/O." + tags = {"coro", "websocket"} + license = "MIT" + author = { name = "Tim Caswell" } +]] + +local uv = require('uv') +local httpCodec = require('http-codec') +local websocketCodec = require('websocket-codec') +local net = require('coro-net') + +local function parseUrl(url) + local protocol, host, port, pathname = string.match(url, "^(wss?)://([^:/]+):?(%d*)(/?[^#?]*)") + local tls + if protocol == "ws" then + port = tonumber(port) or 80 + tls = false + elseif protocol == "wss" then + port = tonumber(port) or 443 + tls = true + else + return nil, "Sorry, only ws:// or wss:// protocols supported" + end + return { + host = host, + port = port, + tls = tls, + pathname = pathname + } +end + +local function wrapIo(rawRead, rawWrite, options) + + local closeSent = false + + local timer + + local function cleanup() + if timer then + if not timer:is_closing() then + timer:close() + end + timer = nil + end + end + + local function write(message) + if message then + message.mask = options.mask + if message.opcode == 8 then + closeSent = true + rawWrite(message) + cleanup() + return rawWrite() + end + else + if not closeSent then + return write({ + opcode = 8, + payload = "" + }) + end + end + return rawWrite(message) + end + + + local function read() + while true do + local message = rawRead() + if not message then + return cleanup() + end + if message.opcode < 8 then + return message + end + if not closeSent then + if message.opcode == 8 then + write { + opcode = 8, + payload = message.payload + } + elseif message.opcode == 9 then + write { + opcode = 10, + payload = message.payload + } + end + return message + end + end + end + + if options.heartbeat then + local interval = options.heartbeat + timer = uv.new_timer() + timer:unref() + timer:start(interval, interval, function () + coroutine.wrap(function () + local success, err = write { + opcode = 10, + payload = "" + } + if not success then + timer:close() + print(err) + end + end)() + end) + end + + return read, write +end + +-- options table to configure connection +-- options.path +-- options.host +-- options.port +-- options.tls +-- options.pathname +-- options.subprotocol +-- options.headers (as list of header/value pairs) +-- options.timeout +-- options.heartbeat +-- returns res, read, write (res.socket has socket) +local function connect(options) + options = options or {} + local config = { + path = options.path, + host = options.host, + port = options.port, + tls = options.tls, + encoder = httpCodec.encoder, + decoder = httpCodec.decoder, + } + local read, write, socket, updateDecoder, updateEncoder + = net.connect(config, options.timeout or 10000) + if not read then + return nil, write + end + + local res + + local success, err = websocketCodec.handshake({ + host = options.host, + path = options.pathname, + protocol = options.subprotocol + }, function (req) + local headers = options.headers + if headers then + for i = 1, #headers do + req[#req + 1] = headers[i] + end + end + write(req) + res = read() + if not res then error("Missing server response") end + if res.code == 400 then + -- p { req = req, res = res } + local reason = read() or res.reason + error("Invalid request: " .. reason) + end + return res + end) + if not success then + return nil, err + end + + -- Upgrade the protocol to websocket + updateDecoder(websocketCodec.decode) + updateEncoder(websocketCodec.encode) + + read, write = wrapIo(read, write, { + mask = true, + heartbeat = options.heartbeat + }) + + res.socket = socket + return res, read, write + +end + +return { + parseUrl = parseUrl, + wrapIo = wrapIo, + connect = connect, +} diff --git a/deps/coro-wrapper.lua b/deps/coro-wrapper.lua new file mode 100644 index 0000000..c43671e --- /dev/null +++ b/deps/coro-wrapper.lua @@ -0,0 +1,151 @@ +--[[lit-meta + name = "creationix/coro-wrapper" + version = "3.1.0" + homepage = "https://github.com/luvit/lit/blob/master/deps/coro-wrapper.lua" + description = "An adapter for applying decoders to coro-streams." + tags = {"coro", "decoder", "adapter"} + license = "MIT" + author = { name = "Tim Caswell" } +]] + +local concat = table.concat +local sub = string.sub + +-- Merger allows for effecient merging of many chunks. +-- The scan function returns truthy when the chunk contains a useful delimeter +-- Or in other words, when there is enough data to flush to the decoder. +-- merger(read, scan) -> read, updateScan +-- read() -> chunk or nil +-- scan(chunk) -> should_flush +-- updateScan(scan) +local function merger(read, scan) + local parts = {} + + -- Return a new read function that combines chunks smartly + return function () + + while true do + -- Read the next event from upstream. + local chunk = read() + + -- We got an EOS (end of stream) + if not chunk then + -- If there is nothing left to flush, emit EOS here. + if #parts == 0 then return end + + -- Flush the buffer + chunk = concat(parts) + parts = {} + return chunk + end + + -- Accumulate the chunk + parts[#parts + 1] = chunk + + -- Flush the buffer if scan tells us to. + if scan(chunk) then + chunk = concat(parts) + parts = {} + return chunk + end + + end + end, + + -- This is used to update or disable the scan function. It's useful for + -- protocols that change mid-stream (like HTTP upgrades in websockets) + function (newScan) + scan = newScan + end +end + +-- Decoder takes in a read function and a decode function and returns a new +-- read function that emits decoded events. When decode returns `nil` it means +-- that it needs more data before it can parse. The index output in decode is +-- the index to start the next decode. If output index if nil it means nothing +-- is leftover and next decode starts fresh. +-- decoder(read, decode) -> read, updateDecode +-- read() -> chunk or nil +-- decode(chunk, index) -> nil or (data, index) +-- updateDecode(Decode) +local function decoder(read, decode) + local buffer, index + local want = true + return function () + + while true do + -- If there isn't enough data to decode then get more data. + if want then + local chunk = read() + if buffer then + -- If we had leftover data in the old buffer, trim it down. + if index > 1 then + buffer = sub(buffer, index) + index = 1 + end + if chunk then + -- Concatenate the chunk with the old data + buffer = buffer .. chunk + end + else + -- If there was no leftover data, set new data in the buffer + if chunk then + buffer = chunk + index = 1 + else + buffer = nil + index = nil + end + end + end + + -- Return nil if the buffer is empty + if buffer == '' or buffer == nil then + return nil + end + + -- If we have data, lets try to decode it + local item, newIndex = decode(buffer, index) + + want = not newIndex + if item or newIndex then + -- There was enough data to emit an event! + if newIndex then + assert(type(newIndex) == "number", "index must be a number if set") + -- There was leftover data + index = newIndex + else + want = true + -- There was no leftover data + buffer = nil + index = nil + end + -- Emit the event + return item + end + + + end + end, + function (newDecode) + decode = newDecode + end +end + +local function encoder(write, encode) + return function (item) + if not item then + return write() + end + return write(encode(item)) + end, + function (newEncode) + encode = newEncode + end +end + +return { + merger = merger, + decoder = decoder, + encoder = encoder, +} diff --git a/deps/discordia/docgen.lua b/deps/discordia/docgen.lua new file mode 100644 index 0000000..fd1dc48 --- /dev/null +++ b/deps/discordia/docgen.lua @@ -0,0 +1,373 @@ +--[=[ +@c ClassName [x base_1 x base_2 ... x base_n] +@t tag +@mt methodTag (applies to all class methods) +@p parameterName type +@op optionalParameterName type +@d description+ +]=] + +--[=[ +@m methodName +@t tag +@p parameterName type +@op optionalParameterName type +@r return +@d description+ +]=] + +--[=[ +@p propertyName type description+ +]=] + +local fs = require('fs') +local pathjoin = require('pathjoin') + +local insert, sort, concat = table.insert, table.sort, table.concat +local format = string.format +local pathJoin = pathjoin.pathJoin + +local function scan(dir) + for fileName, fileType in fs.scandirSync(dir) do + local path = pathJoin(dir, fileName) + if fileType == 'file' then + coroutine.yield(path) + else + scan(path) + end + end +end + +local function match(s, pattern) -- only useful for one capture + return assert(s:match(pattern), s) +end + +local function gmatch(s, pattern, hash) -- only useful for one capture + local tbl = {} + if hash then + for k in s:gmatch(pattern) do + tbl[k] = true + end + else + for v in s:gmatch(pattern) do + insert(tbl, v) + end + end + return tbl +end + +local function matchType(s) + return s:match('^@(%S+)') +end + +local function matchComments(s) + return s:gmatch('--%[=%[%s*(.-)%s*%]=%]') +end + +local function matchClassName(s) + return match(s, '@c (%S+)') +end + +local function matchMethodName(s) + return match(s, '@m (%S+)') +end + +local function matchDescription(s) + return match(s, '@d (.+)'):gsub('%s+', ' ') +end + +local function matchParents(s) + return gmatch(s, 'x (%S+)') +end + +local function matchReturns(s) + return gmatch(s, '@r (%S+)') +end + +local function matchTags(s) + return gmatch(s, '@t (%S+)', true) +end + +local function matchMethodTags(s) + return gmatch(s, '@mt (%S+)', true) +end + +local function matchProperty(s) + local a, b, c = s:match('@p (%S+) (%S+) (.+)') + return { + name = assert(a, s), + type = assert(b, s), + desc = assert(c, s):gsub('%s+', ' '), + } +end + +local function matchParameters(s) + local ret = {} + for optional, paramName, paramType in s:gmatch('@(o?)p (%S+) (%S+)') do + insert(ret, {paramName, paramType, optional == 'o'}) + end + return ret +end + +local function matchMethod(s) + return { + name = matchMethodName(s), + desc = matchDescription(s), + parameters = matchParameters(s), + returns = matchReturns(s), + tags = matchTags(s), + } +end + +---- + +local docs = {} + +local function newClass() + + local class = { + methods = {}, + statics = {}, + properties = {}, + } + + local function init(s) + class.name = matchClassName(s) + class.parents = matchParents(s) + class.desc = matchDescription(s) + class.parameters = matchParameters(s) + class.tags = matchTags(s) + class.methodTags = matchMethodTags(s) + assert(not docs[class.name], 'duplicate class: ' .. class.name) + docs[class.name] = class + end + + return class, init + +end + +for f in coroutine.wrap(scan), './libs' do + + local d = assert(fs.readFileSync(f)) + + local class, initClass = newClass() + for s in matchComments(d) do + local t = matchType(s) + if t == 'c' then + initClass(s) + elseif t == 'm' then + local method = matchMethod(s) + for k, v in pairs(class.methodTags) do + method.tags[k] = v + end + method.class = class + insert(method.tags.static and class.statics or class.methods, method) + elseif t == 'p' then + insert(class.properties, matchProperty(s)) + end + end + +end + +---- + +local output = 'docs' + +local function link(str) + if type(str) == 'table' then + local ret = {} + for i, v in ipairs(str) do + ret[i] = link(v) + end + return concat(ret, ', ') + else + local ret = {} + for t in str:gmatch('[^/]+') do + insert(ret, docs[t] and format('[[%s]]', t) or t) + end + return concat(ret, '/') + end +end + +local function sorter(a, b) + return a.name < b.name +end + +local function writeHeading(f, heading) + f:write('## ', heading, '\n\n') +end + +local function writeProperties(f, properties) + sort(properties, sorter) + f:write('| Name | Type | Description |\n') + f:write('|-|-|-|\n') + for _, v in ipairs(properties) do + f:write('| ', v.name, ' | ', link(v.type), ' | ', v.desc, ' |\n') + end + f:write('\n') +end + +local function writeParameters(f, parameters) + f:write('(') + local optional + if #parameters > 0 then + for i, param in ipairs(parameters) do + f:write(param[1]) + if i < #parameters then + f:write(', ') + end + if param[3] then + optional = true + end + end + f:write(')\n\n') + if optional then + f:write('| Parameter | Type | Optional |\n') + f:write('|-|-|:-:|\n') + for _, param in ipairs(parameters) do + local o = param[3] and '✔' or '' + f:write('| ', param[1], ' | ', link(param[2]), ' | ', o, ' |\n') + end + f:write('\n') + else + f:write('| Parameter | Type |\n') + f:write('|-|-|\n') + for _, param in ipairs(parameters) do + f:write('| ', param[1], ' | ', link(param[2]), ' |\n') + end + f:write('\n') + end + else + f:write(')\n\n') + end +end + +local methodTags = {} + +methodTags['http'] = 'This method always makes an HTTP request.' +methodTags['http?'] = 'This method may make an HTTP request.' +methodTags['ws'] = 'This method always makes a WebSocket request.' +methodTags['mem'] = 'This method only operates on data in memory.' + +local function checkTags(tbl, check) + for i, v in ipairs(check) do + if tbl[v] then + for j, w in ipairs(check) do + if i ~= j then + if tbl[w] then + return error(string.format('mutually exclusive tags encountered: %s and %s', v, w), 1) + end + end + end + end + end +end + +local function writeMethods(f, methods) + + sort(methods, sorter) + for _, method in ipairs(methods) do + + f:write('### ', method.name) + writeParameters(f, method.parameters) + f:write(method.desc, '\n\n') + + local tags = method.tags + checkTags(tags, {'http', 'http?', 'mem'}) + checkTags(tags, {'ws', 'mem'}) + + for k in pairs(tags) do + if k ~= 'static' then + assert(methodTags[k], k) + f:write('*', methodTags[k], '*\n\n') + end + end + + f:write('**Returns:** ', link(method.returns), '\n\n----\n\n') + + end + +end + +if not fs.existsSync(output) then + fs.mkdirSync(output) +end + +local function collectParents(parents, k, ret, seen) + ret = ret or {} + seen = seen or {} + for _, parent in ipairs(parents) do + parent = docs[parent] + if parent then + for _, v in ipairs(parent[k]) do + if not seen[v] then + seen[v] = true + insert(ret, v) + end + end + end + collectParents(parent.parents, k, ret, seen) + end + return ret +end + +for _, class in pairs(docs) do + + local f = io.open(pathJoin(output, class.name .. '.md'), 'w') + + local parents = class.parents + local parentLinks = link(parents) + + if next(parents) then + f:write('#### *extends ', parentLinks, '*\n\n') + end + + f:write(class.desc, '\n\n') + + checkTags(class.tags, {'ui', 'abc'}) + if class.tags.ui then + writeHeading(f, 'Constructor') + f:write('### ', class.name) + writeParameters(f, class.parameters) + elseif class.tags.abc then + f:write('*This is an abstract base class. Direct instances should never exist.*\n\n') + else + f:write('*Instances of this class should not be constructed by users.*\n\n') + end + + local properties = collectParents(parents, 'properties') + if next(properties) then + writeHeading(f, 'Properties Inherited From ' .. parentLinks) + writeProperties(f, properties) + end + + if next(class.properties) then + writeHeading(f, 'Properties') + writeProperties(f, class.properties) + end + + local statics = collectParents(parents, 'statics') + if next(statics) then + writeHeading(f, 'Static Methods Inherited From ' .. parentLinks) + writeMethods(f, statics) + end + + local methods = collectParents(parents, 'methods') + if next(methods) then + writeHeading(f, 'Methods Inherited From ' .. parentLinks) + writeMethods(f, methods) + end + + if next(class.statics) then + writeHeading(f, 'Static Methods') + writeMethods(f, class.statics) + end + + if next(class.methods) then + writeHeading(f, 'Methods') + writeMethods(f, class.methods) + end + + f:close() + +end diff --git a/deps/discordia/examples/appender.lua b/deps/discordia/examples/appender.lua new file mode 100644 index 0000000..291d85c --- /dev/null +++ b/deps/discordia/examples/appender.lua @@ -0,0 +1,28 @@ +local discordia = require("discordia") +local client = discordia.Client() + +local lines = {} -- blank table of messages + +client:on("ready", function() -- bot is ready + print("Logged in as " .. client.user.username) +end) + +client:on("messageCreate", function(message) + + local content = message.content + local author = message.author + + if author == client.user then return end -- the bot should not append its own messages + + if content == "!lines" then -- if the lines command is activated + message.channel:send { + file = {"lines.txt", table.concat(lines, "\n")} -- concatenate and send the collected lines in a file + } + lines = {} -- empty the lines table + else -- if the lines command is NOT activated + table.insert(lines, content) -- append the message as a new line + end + +end) + +client:run("Bot BOT_TOKEN") -- replace BOT_TOKEN with your bot token diff --git a/deps/discordia/examples/basicCommands.lua b/deps/discordia/examples/basicCommands.lua new file mode 100644 index 0000000..57498e9 --- /dev/null +++ b/deps/discordia/examples/basicCommands.lua @@ -0,0 +1,25 @@ +local discordia = require("discordia") +local client = discordia.Client() + +discordia.extensions() -- load all helpful extensions + +client:on("ready", function() -- bot is ready + print("Logged in as " .. client.user.username) +end) + +client:on("messageCreate", function(message) + + local content = message.content + local args = content:split(" ") -- split all arguments into a table + + if args[1] == "!ping" then + message:reply("Pong!") + elseif args[1] == "!echo" then + table.remove(args, 1) -- remove the first argument (!echo) from the table + message:reply(table.concat(args, " ")) -- concatenate the arguments into a string, then reply with it + end + +end) + + +client:run("Bot BOT_TOKEN") -- replace BOT_TOKEN with your bot token diff --git a/deps/discordia/examples/embed.lua b/deps/discordia/examples/embed.lua new file mode 100644 index 0000000..3f546f6 --- /dev/null +++ b/deps/discordia/examples/embed.lua @@ -0,0 +1,45 @@ +local discordia = require("discordia") +local client = discordia.Client() + + +client:on("ready", function() -- bot is ready + print("Logged in as " .. client.user.username) +end) + +client:on("messageCreate", function(message) + + local content = message.content + local author = message.author + + if content == "!embed" then + message:reply { + embed = { + title = "Embed Title", + description = "Here is my fancy description!", + author = { + name = author.username, + icon_url = author.avatarURL + }, + fields = { -- array of fields + { + name = "Field 1", + value = "This is some information", + inline = true + }, + { + name = "Field 2", + value = "This is some more information", + inline = false + } + }, + footer = { + text = "Created with Discordia" + }, + color = 0x000000 -- hex color code + } + } + end + +end) + +client:run("Bot BOT_TOKEN") -- replace BOT_TOKEN with your bot token diff --git a/deps/discordia/examples/helpCommandExample.lua b/deps/discordia/examples/helpCommandExample.lua new file mode 100644 index 0000000..e22a3e6 --- /dev/null +++ b/deps/discordia/examples/helpCommandExample.lua @@ -0,0 +1,44 @@ +local discordia = require('discordia') +local client = discordia.Client() +discordia.extensions() -- load all helpful extensions + +local prefix = "." +local commands = { + [prefix .. "ping"] = { + description = "Answers with pong.", + exec = function(message) + message.channel:send("Pong!") + end + }, + [prefix .. "hello"] = { + description = "Answers with world.", + exec = function(message) + message.channel:send("world!") + end + } +} + +client:on('ready', function() + p(string.format('Logged in as %s', client.user.username)) +end) + +client:on("messageCreate", function(message) + local args = message.content:split(" ") -- split all arguments into a table + + local command = commands[args[1]] + if command then -- ping or hello + command.exec(message) -- execute the command + end + + if args[1] == prefix.."help" then -- display all the commands + local output = {} + for word, tbl in pairs(commands) do + table.insert(output, "Command: " .. word .. "\nDescription: " .. tbl.description) + end + + message:reply(table.concat(output, "\n\n")) + end +end) + + +client:run("Bot BOT_TOKEN") -- replace BOT_TOKEN with your bot token diff --git a/deps/discordia/examples/pingPong.lua b/deps/discordia/examples/pingPong.lua new file mode 100644 index 0000000..e78e022 --- /dev/null +++ b/deps/discordia/examples/pingPong.lua @@ -0,0 +1,20 @@ +local discordia = require("discordia") +local client = discordia.Client() + +client:on("ready", function() -- bot is ready + print("Logged in as " .. client.user.username) +end) + +client:on("messageCreate", function(message) + + local content = message.content + + if content == "!ping" then + message:reply("Pong!") + elseif content == "!pong" then + message:reply("Ping!") + end + +end) + +client:run("Bot BOT_TOKEN") -- replace BOT_TOKEN with your bot token diff --git a/deps/discordia/init.lua b/deps/discordia/init.lua new file mode 100644 index 0000000..b10704a --- /dev/null +++ b/deps/discordia/init.lua @@ -0,0 +1,18 @@ +return { + class = require('class'), + enums = require('enums'), + extensions = require('extensions'), + package = require('./package.lua'), + Client = require('client/Client'), + Clock = require('utils/Clock'), + Color = require('utils/Color'), + Date = require('utils/Date'), + Deque = require('utils/Deque'), + Emitter = require('utils/Emitter'), + Logger = require('utils/Logger'), + Mutex = require('utils/Mutex'), + Permissions = require('utils/Permissions'), + Stopwatch = require('utils/Stopwatch'), + Time = require('utils/Time'), + storage = {}, +} diff --git a/deps/discordia/libs/class.lua b/deps/discordia/libs/class.lua new file mode 100644 index 0000000..6c83d43 --- /dev/null +++ b/deps/discordia/libs/class.lua @@ -0,0 +1,165 @@ +local format = string.format + +local meta = {} +local names = {} +local classes = {} +local objects = setmetatable({}, {__mode = 'k'}) + +function meta:__call(...) + local obj = setmetatable({}, self) + objects[obj] = true + obj:__init(...) + return obj +end + +function meta:__tostring() + return 'class ' .. self.__name +end + +local default = {} + +function default:__tostring() + return self.__name +end + +function default:__hash() + return self +end + +local function isClass(cls) + return classes[cls] +end + +local function isObject(obj) + return objects[obj] +end + +local function isSubclass(sub, cls) + if isClass(sub) and isClass(cls) then + if sub == cls then + return true + else + for _, base in ipairs(sub.__bases) do + if isSubclass(base, cls) then + return true + end + end + end + end + return false +end + +local function isInstance(obj, cls) + return isObject(obj) and isSubclass(obj.__class, cls) +end + +local function profile() + local ret = setmetatable({}, {__index = function() return 0 end}) + for obj in pairs(objects) do + local name = obj.__name + ret[name] = ret[name] + 1 + end + return ret +end + +local types = {['string'] = true, ['number'] = true, ['boolean'] = true} + +local function _getPrimitive(v) + return types[type(v)] and v or v ~= nil and tostring(v) or nil +end + +local function serialize(obj) + if isObject(obj) then + local ret = {} + for k, v in pairs(obj.__getters) do + ret[k] = _getPrimitive(v(obj)) + end + return ret + else + return _getPrimitive(obj) + end +end + +local rawtype = type +local function type(obj) + return isObject(obj) and obj.__name or rawtype(obj) +end + +return setmetatable({ + + classes = names, + isClass = isClass, + isObject = isObject, + isSubclass = isSubclass, + isInstance = isInstance, + type = type, + profile = profile, + serialize = serialize, + +}, {__call = function(_, name, ...) + + if names[name] then return error(format('Class %q already defined', name)) end + + local class = setmetatable({}, meta) + classes[class] = true + + for k, v in pairs(default) do + class[k] = v + end + + local bases = {...} + local getters = {} + local setters = {} + + for _, base in ipairs(bases) do + for k1, v1 in pairs(base) do + class[k1] = v1 + for k2, v2 in pairs(base.__getters) do + getters[k2] = v2 + end + for k2, v2 in pairs(base.__setters) do + setters[k2] = v2 + end + end + end + + class.__name = name + class.__class = class + class.__bases = bases + class.__getters = getters + class.__setters = setters + + local pool = {} + local n = #pool + + function class:__index(k) + if getters[k] then + return getters[k](self) + elseif pool[k] then + return rawget(self, pool[k]) + else + return class[k] + end + end + + function class:__newindex(k, v) + if setters[k] then + return setters[k](self, v) + elseif class[k] or getters[k] then + return error(format('Cannot overwrite protected property: %s.%s', name, k)) + elseif k:find('_', 1, true) ~= 1 then + return error(format('Cannot write property to object without leading underscore: %s.%s', name, k)) + else + if not pool[k] then + n = n + 1 + pool[k] = n + end + return rawset(self, pool[k], v) + end + end + + names[name] = class + + return class, getters, setters + +end}) diff --git a/deps/discordia/libs/client/API.lua b/deps/discordia/libs/client/API.lua new file mode 100644 index 0000000..f9228d4 --- /dev/null +++ b/deps/discordia/libs/client/API.lua @@ -0,0 +1,711 @@ +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 diff --git a/deps/discordia/libs/client/Client.lua b/deps/discordia/libs/client/Client.lua new file mode 100644 index 0000000..1bb70e6 --- /dev/null +++ b/deps/discordia/libs/client/Client.lua @@ -0,0 +1,679 @@ +--[=[ +@c Client x Emitter +@t ui +@op options table +@d The main point of entry into a Discordia application. All data relevant to +Discord is accessible through a client instance or its child objects after a +connection to Discord is established with the `run` method. In other words, +client data should not be expected and most client methods should not be called +until after the `ready` event is received. Base emitter methods may be called +at any time. See [[client options]]. +]=] + +local fs = require('fs') +local json = require('json') + +local constants = require('constants') +local enums = require('enums') +local package = require('../../package.lua') + +local API = require('client/API') +local Shard = require('client/Shard') +local Resolver = require('client/Resolver') + +local GroupChannel = require('containers/GroupChannel') +local Guild = require('containers/Guild') +local PrivateChannel = require('containers/PrivateChannel') +local User = require('containers/User') +local Invite = require('containers/Invite') +local Webhook = require('containers/Webhook') +local Relationship = require('containers/Relationship') + +local Cache = require('iterables/Cache') +local WeakCache = require('iterables/WeakCache') +local Emitter = require('utils/Emitter') +local Logger = require('utils/Logger') +local Mutex = require('utils/Mutex') + +local VoiceManager = require('voice/VoiceManager') + +local encode, decode, null = json.encode, json.decode, json.null +local readFileSync, writeFileSync = fs.readFileSync, fs.writeFileSync + +local logLevel = enums.logLevel +local gameType = enums.gameType + +local wrap = coroutine.wrap +local time, difftime = os.time, os.difftime +local format = string.format + +local CACHE_AGE = constants.CACHE_AGE +local GATEWAY_VERSION = constants.GATEWAY_VERSION + +-- do not change these options here +-- pass a custom table on client initialization instead +local defaultOptions = { + routeDelay = 250, + maxRetries = 5, + shardCount = 0, + firstShard = 0, + lastShard = -1, + largeThreshold = 100, + cacheAllMembers = false, + autoReconnect = true, + compress = true, + bitrate = 64000, + logFile = 'discordia.log', + logLevel = logLevel.info, + gatewayFile = 'gateway.json', + dateTime = '%F %T', + syncGuilds = false, +} + +local function parseOptions(customOptions) + if type(customOptions) == 'table' then + local options = {} + for k, default in pairs(defaultOptions) do -- load options + local custom = customOptions[k] + if custom ~= nil then + options[k] = custom + else + options[k] = default + end + end + for k, v in pairs(customOptions) do -- validate options + local default = type(defaultOptions[k]) + local custom = type(v) + if default ~= custom then + return error(format('invalid client option %q (%s expected, got %s)', k, default, custom), 3) + end + if custom == 'number' and (v < 0 or v % 1 ~= 0) then + return error(format('invalid client option %q (number must be a positive integer)', k), 3) + end + end + return options + else + return defaultOptions + end +end + +local Client, get = require('class')('Client', Emitter) + +function Client:__init(options) + Emitter.__init(self) + options = parseOptions(options) + self._options = options + self._shards = {} + self._api = API(self) + self._mutex = Mutex() + self._users = Cache({}, User, self) + self._guilds = Cache({}, Guild, self) + self._group_channels = Cache({}, GroupChannel, self) + self._private_channels = Cache({}, PrivateChannel, self) + self._relationships = Cache({}, Relationship, self) + self._webhooks = WeakCache({}, Webhook, self) -- used for audit logs + self._logger = Logger(options.logLevel, options.dateTime, options.logFile) + self._voice = VoiceManager(self) + self._role_map = {} + self._emoji_map = {} + self._channel_map = {} + self._events = require('client/EventHandler') +end + +for name, level in pairs(logLevel) do + Client[name] = function(self, fmt, ...) + local msg = self._logger:log(level, fmt, ...) + return self:emit(name, msg or format(fmt, ...)) + end +end + +function Client:_deprecated(clsName, before, after) + local info = debug.getinfo(3) + return self:warning( + '%s:%s: %s.%s is deprecated; use %s.%s instead', + info.short_src, + info.currentline, + clsName, + before, + clsName, + after + ) +end + +local function run(self, token) + + self:info('Discordia %s', package.version) + self:info('Connecting to Discord...') + + local api = self._api + local users = self._users + local options = self._options + + local user, err1 = api:authenticate(token) + if not user then + return self:error('Could not authenticate, check token: ' .. err1) + end + self._user = users:_insert(user) + self._token = token + + self:info('Authenticated as %s#%s', user.username, user.discriminator) + + local now = time() + local url, count, owner + + local cache = readFileSync(options.gatewayFile) + cache = cache and decode(cache) + + if cache then + local d = cache[user.id] + if d and difftime(now, d.timestamp) < CACHE_AGE then + url = cache.url + if user.bot then + count = d.shards + owner = d.owner + else + count = 1 + owner = user + end + end + else + cache = {} + end + + if not url or not owner then + + if user.bot then + + local gateway, err2 = api:getGatewayBot() + if not gateway then + return self:error('Could not get gateway: ' .. err2) + end + + local app, err3 = api:getCurrentApplicationInformation() + if not app then + return self:error('Could not get application information: ' .. err3) + end + + url = gateway.url + count = gateway.shards + owner = app.owner + + cache[user.id] = {owner = owner, shards = count, timestamp = now} + + else + + local gateway, err2 = api:getGateway() + if not gateway then + return self:error('Could not get gateway: ' .. err2) + end + + url = gateway.url + count = 1 + owner = user + + cache[user.id] = {timestamp = now} + + end + + cache.url = url + + writeFileSync(options.gatewayFile, encode(cache)) + + end + + self._owner = users:_insert(owner) + + if options.shardCount > 0 then + if count ~= options.shardCount then + self:warning('Requested shard count (%i) is different from recommended count (%i)', options.shardCount, count) + end + count = options.shardCount + end + + local first, last = options.firstShard, options.lastShard + + if last < 0 then + last = count - 1 + end + + if last < first then + return self:error('First shard ID (%i) is greater than last shard ID (%i)', first, last) + end + + local d = last - first + 1 + if d > count then + return self:error('Shard count (%i) is less than target shard range (%i)', count, d) + end + + if first == last then + self:info('Launching shard %i (%i out of %i)...', first, d, count) + else + self:info('Launching shards %i through %i (%i out of %i)...', first, last, d, count) + end + + self._total_shard_count = count + self._shard_count = d + + for id = first, last do + self._shards[id] = Shard(id, self) + end + + local path = format('/?v=%i&encoding=json', GATEWAY_VERSION) + for _, shard in pairs(self._shards) do + wrap(shard.connect)(shard, url, path) + shard:identifyWait() + end + +end + +--[=[ +@m run +@p token string +@op presence table +@r nil +@d Authenticates the current user via HTTPS and launches as many WSS gateway +shards as are required or requested. By using coroutines that are automatically +managed by Luvit libraries and a libuv event loop, multiple clients per process +and multiple shards per client can operate concurrently. This should be the last +method called after all other code and event handlers have been initialized. If +a presence table is provided, it will act as if the user called `setStatus` +and `setGame` after `run`. +]=] +function Client:run(token, presence) + self._presence = presence or {} + return wrap(run)(self, token) +end + +--[=[ +@m stop +@t ws +@r nil +@d Disconnects all shards and effectively stops their loops. This does not +empty any data that the client may have cached. +]=] +function Client:stop() + for _, shard in pairs(self._shards) do + shard:disconnect() + end +end + +function Client:_modify(payload) + local data, err = self._api:modifyCurrentUser(payload) + if data then + data.token = nil + self._user:_load(data) + return true + else + return false, err + end +end + +--[=[ +@m setUsername +@t http +@p username string +@r boolean +@d Sets the client's username. This must be between 2 and 32 characters in +length. This does not change the application name. +]=] +function Client:setUsername(username) + return self:_modify({username = username or null}) +end + +--[=[ +@m setAvatar +@t http +@p avatar Base64-Resolvable +@r boolean +@d Sets the client's avatar. To remove the avatar, pass an empty string or nil. +This does not change the application image. +]=] +function Client:setAvatar(avatar) + avatar = avatar and Resolver.base64(avatar) + return self:_modify({avatar = avatar or null}) +end + +--[=[ +@m createGuild +@t http +@p name string +@r boolean +@d Creates a new guild. The name must be between 2 and 100 characters in length. +This method may not work if the current user is in too many guilds. Note that +this does not return the created guild object; wait for the corresponding +`guildCreate` event if you need the object. +]=] +function Client:createGuild(name) + local data, err = self._api:createGuild({name = name}) + if data then + return true + else + return false, err + end +end + +--[=[ +@m createGroupChannel +@t http +@r GroupChannel +@d Creates a new group channel. This method is only available for user accounts. +]=] +function Client:createGroupChannel() + local data, err = self._api:createGroupDM() + if data then + return self._group_channels:_insert(data) + else + return nil, err + end +end + +--[=[ +@m getWebhook +@t http +@p id string +@r Webhook +@d Gets a webhook object by ID. This always makes an HTTP request to obtain a +static object that is not cached and is not updated by gateway events. +]=] +function Client:getWebhook(id) + local data, err = self._api:getWebhook(id) + if data then + return Webhook(data, self) + else + return nil, err + end +end + +--[=[ +@m getInvite +@t http +@p code string +@op counts boolean +@r Invite +@d Gets an invite object by code. This always makes an HTTP request to obtain a +static object that is not cached and is not updated by gateway events. +]=] +function Client:getInvite(code, counts) + local data, err = self._api:getInvite(code, counts and {with_counts = true}) + if data then + return Invite(data, self) + else + return nil, err + end +end + +--[=[ +@m getUser +@t http? +@p id User-ID-Resolvable +@r User +@d Gets a user object by ID. If the object is already cached, then the cached +object will be returned; otherwise, an HTTP request is made. Under circumstances +which should be rare, the user object may be an old version, not updated by +gateway events. +]=] +function Client:getUser(id) + id = Resolver.userId(id) + local user = self._users:get(id) + if user then + return user + else + local data, err = self._api:getUser(id) + if data then + return self._users:_insert(data) + else + return nil, err + end + end +end + +--[=[ +@m getGuild +@t mem +@p id Guild-ID-Resolvable +@r Guild +@d Gets a guild object by ID. The current user must be in the guild and the client +must be running the appropriate shard that serves this guild. This method never +makes an HTTP request to obtain a guild. +]=] +function Client:getGuild(id) + id = Resolver.guildId(id) + return self._guilds:get(id) +end + +--[=[ +@m getChannel +@t mem +@p id Channel-ID-Resolvable +@r Channel +@d Gets a channel object by ID. For guild channels, the current user must be in +the channel's guild and the client must be running the appropriate shard that +serves the channel's guild. + +For private channels, the channel must have been previously opened and cached. +If the channel is not cached, `User:getPrivateChannel` should be used instead. +]=] +function Client:getChannel(id) + id = Resolver.channelId(id) + local guild = self._channel_map[id] + if guild then + return guild._text_channels:get(id) or guild._voice_channels:get(id) or guild._categories:get(id) + else + return self._private_channels:get(id) or self._group_channels:get(id) + end +end + +--[=[ +@m getRole +@t mem +@p id Role-ID-Resolvable +@r Role +@d Gets a role object by ID. The current user must be in the role's guild and +the client must be running the appropriate shard that serves the role's guild. +]=] +function Client:getRole(id) + id = Resolver.roleId(id) + local guild = self._role_map[id] + return guild and guild._roles:get(id) +end + +--[=[ +@m getEmoji +@t mem +@p id Emoji-ID-Resolvable +@r Emoji +@d Gets an emoji object by ID. The current user must be in the emoji's guild and +the client must be running the appropriate shard that serves the emoji's guild. +]=] +function Client:getEmoji(id) + id = Resolver.emojiId(id) + local guild = self._emoji_map[id] + return guild and guild._emojis:get(id) +end + +--[=[ +@m listVoiceRegions +@t http +@r table +@d Returns a raw data table that contains a list of voice regions as provided by +Discord, with no formatting beyond what is provided by the Discord API. +]=] +function Client:listVoiceRegions() + return self._api:listVoiceRegions() +end + +--[=[ +@m getConnections +@t http +@r table +@d Returns a raw data table that contains a list of connections as provided by +Discord, with no formatting beyond what is provided by the Discord API. +This is unrelated to voice connections. +]=] +function Client:getConnections() + return self._api:getUsersConnections() +end + +--[=[ +@m getApplicationInformation +@t http +@r table +@d Returns a raw data table that contains information about the current OAuth2 +application, with no formatting beyond what is provided by the Discord API. +]=] +function Client:getApplicationInformation() + return self._api:getCurrentApplicationInformation() +end + +local function updateStatus(self) + local presence = self._presence + presence.afk = presence.afk or null + presence.game = presence.game or null + presence.since = presence.since or null + presence.status = presence.status or null + for _, shard in pairs(self._shards) do + shard:updateStatus(presence) + end +end + +--[=[ +@m setStatus +@t ws +@p status string +@r nil +@d Sets the current user's status on all shards that are managed by this client. +See the `status` enumeration for acceptable status values. +]=] +function Client:setStatus(status) + if type(status) == 'string' then + self._presence.status = status + if status == 'idle' then + self._presence.since = 1000 * time() + else + self._presence.since = null + end + else + self._presence.status = null + self._presence.since = null + end + return updateStatus(self) +end + +--[=[ +@m setGame +@t ws +@p game string/table +@r nil +@d Sets the current user's game on all shards that are managed by this client. +If a string is passed, it is treated as the game name. If a table is passed, it +must have a `name` field and may optionally have a `url` or `type` field. Pass `nil` to +remove the game status. +]=] +function Client:setGame(game) + if type(game) == 'string' then + game = {name = game, type = gameType.default} + elseif type(game) == 'table' then + if type(game.name) == 'string' then + if type(game.type) ~= 'number' then + if type(game.url) == 'string' then + game.type = gameType.streaming + else + game.type = gameType.default + end + end + else + game = null + end + else + game = null + end + self._presence.game = game + return updateStatus(self) +end + +--[=[ +@m setAFK +@t ws +@p afk boolean +@r nil +@d Set the current user's AFK status on all shards that are managed by this client. +This generally applies to user accounts and their push notifications. +]=] +function Client:setAFK(afk) + if type(afk) == 'boolean' then + self._presence.afk = afk + else + self._presence.afk = null + end + return updateStatus(self) +end + +--[=[@p shardCount number/nil The number of shards that this client is managing.]=] +function get.shardCount(self) + return self._shard_count +end + +--[=[@p totalShardCount number/nil The total number of shards that the current user is on.]=] +function get.totalShardCount(self) + return self._total_shard_count +end + +--[=[@p user User/nil User object representing the current user.]=] +function get.user(self) + return self._user +end + +--[=[@p owner User/nil User object representing the current user's owner.]=] +function get.owner(self) + return self._owner +end + +--[=[@p verified boolean/nil Whether the current user's owner's account is verified.]=] +function get.verified(self) + return self._user and self._user._verified +end + +--[=[@p mfaEnabled boolean/nil Whether the current user's owner's account has multi-factor (or two-factor) +authentication enabled. This is equivalent to `verified`]=] +function get.mfaEnabled(self) + return self._user and self._user._verified +end + +--[=[@p email string/nil The current user's owner's account's email address (user-accounts only).]=] +function get.email(self) + return self._user and self._user._email +end + +--[=[@p guilds Cache An iterable cache of all guilds that are visible to the client. Note that the +guilds present here correspond to which shards the client is managing. If all +shards are managed by one client, then all guilds will be present.]=] +function get.guilds(self) + return self._guilds +end + +--[=[@p users Cache An iterable cache of all users that are visible to the client. +To access a user that may exist but is not cached, use `Client:getUser`.]=] +function get.users(self) + return self._users +end + +--[=[@p privateChannels Cache An iterable cache of all private channels that are visible to the client. The +channel must exist and must be open for it to be cached here. To access a +private channel that may exist but is not cached, `User:getPrivateChannel`.]=] +function get.privateChannels(self) + return self._private_channels +end + +--[=[@p groupChannels Cache An iterable cache of all group channels that are visible to the client. Only +user-accounts should have these.]=] +function get.groupChannels(self) + return self._group_channels +end + +--[=[@p relationships Cache An iterable cache of all relationships that are visible to the client. Only +user-accounts should have these.]=] +function get.relationships(self) + return self._relationships +end + +return Client diff --git a/deps/discordia/libs/client/EventHandler.lua b/deps/discordia/libs/client/EventHandler.lua new file mode 100644 index 0000000..8457713 --- /dev/null +++ b/deps/discordia/libs/client/EventHandler.lua @@ -0,0 +1,540 @@ +local enums = require('enums') +local json = require('json') + +local channelType = enums.channelType +local insert = table.insert +local null = json.null + +local function warning(client, object, id, event) + return client:warning('Uncached %s (%s) on %s', object, id, event) +end + +local function checkReady(shard) + for _, v in pairs(shard._loading) do + if next(v) then return end + end + shard._ready = true + shard._loading = nil + collectgarbage() + local client = shard._client + client:emit('shardReady', shard._id) + for _, other in pairs(client._shards) do + if not other._ready then return end + end + return client:emit('ready') +end + +local function getChannel(client, id) + local guild = client._channel_map[id] + if guild then + return guild._text_channels:get(id) + else + return client._private_channels:get(id) or client._group_channels:get(id) + end +end + +local EventHandler = setmetatable({}, {__index = function(self, k) + self[k] = function(_, _, shard) + return shard:warning('Unhandled gateway event: %s', k) + end + return self[k] +end}) + +function EventHandler.READY(d, client, shard) + + shard:info('Received READY') + shard:emit('READY') + + shard._session_id = d.session_id + client._user = client._users:_insert(d.user) + + local guilds = client._guilds + local group_channels = client._group_channels + local private_channels = client._private_channels + local relationships = client._relationships + + for _, channel in ipairs(d.private_channels) do + if channel.type == channelType.private then + private_channels:_insert(channel) + elseif channel.type == channelType.group then + group_channels:_insert(channel) + end + end + + local loading = shard._loading + + if d.user.bot then + for _, guild in ipairs(d.guilds) do + guilds:_insert(guild) + loading.guilds[guild.id] = true + end + else + if client._options.syncGuilds then + local ids = {} + for _, guild in ipairs(d.guilds) do + guilds:_insert(guild) + if not guild.unavailable then + loading.syncs[guild.id] = true + insert(ids, guild.id) + end + end + shard:syncGuilds(ids) + else + guilds:_load(d.guilds) + end + end + + relationships:_load(d.relationships) + + for _, presence in ipairs(d.presences) do + local relationship = relationships:get(presence.user.id) + if relationship then + relationship:_loadPresence(presence) + end + end + + return checkReady(shard) + +end + +function EventHandler.RESUMED(_, client, shard) + shard:info('Received RESUMED') + return client:emit('shardResumed', shard._id) +end + +function EventHandler.GUILD_MEMBERS_CHUNK(d, client, shard) + local guild = client._guilds:get(d.guild_id) + if not guild then return warning(client, 'Guild', d.guild_id, 'GUILD_MEMBERS_CHUNK') end + guild._members:_load(d.members) + if shard._loading and guild._member_count == #guild._members then + shard._loading.chunks[d.guild_id] = nil + return checkReady(shard) + end +end + +function EventHandler.GUILD_SYNC(d, client, shard) + local guild = client._guilds:get(d.id) + if not guild then return warning(client, 'Guild', d.id, 'GUILD_SYNC') end + guild._large = d.large + guild:_loadMembers(d, shard) + if shard._loading then + shard._loading.syncs[d.id] = nil + return checkReady(shard) + end +end + +function EventHandler.CHANNEL_CREATE(d, client) + local channel + local t = d.type + if t == channelType.text or t == channelType.news then + local guild = client._guilds:get(d.guild_id) + if not guild then return warning(client, 'Guild', d.guild_id, 'CHANNEL_CREATE') end + channel = guild._text_channels:_insert(d) + elseif t == channelType.voice then + local guild = client._guilds:get(d.guild_id) + if not guild then return warning(client, 'Guild', d.guild_id, 'CHANNEL_CREATE') end + channel = guild._voice_channels:_insert(d) + elseif t == channelType.private then + channel = client._private_channels:_insert(d) + elseif t == channelType.group then + channel = client._group_channels:_insert(d) + elseif t == channelType.category then + local guild = client._guilds:get(d.guild_id) + if not guild then return warning(client, 'Guild', d.guild_id, 'CHANNEL_CREATE') end + channel = guild._categories:_insert(d) + else + return client:warning('Unhandled CHANNEL_CREATE (type %s)', d.type) + end + return client:emit('channelCreate', channel) +end + +function EventHandler.CHANNEL_UPDATE(d, client) + local channel + local t = d.type + if t == channelType.text or t == channelType.news then + local guild = client._guilds:get(d.guild_id) + if not guild then return warning(client, 'Guild', d.guild_id, 'CHANNEL_UPDATE') end + channel = guild._text_channels:_insert(d) + elseif t == channelType.voice then + local guild = client._guilds:get(d.guild_id) + if not guild then return warning(client, 'Guild', d.guild_id, 'CHANNEL_UPDATE') end + channel = guild._voice_channels:_insert(d) + elseif t == channelType.private then -- private channels should never update + channel = client._private_channels:_insert(d) + elseif t == channelType.group then + channel = client._group_channels:_insert(d) + elseif t == channelType.category then + local guild = client._guilds:get(d.guild_id) + if not guild then return warning(client, 'Guild', d.guild_id, 'CHANNEL_UPDATE') end + channel = guild._categories:_insert(d) + else + return client:warning('Unhandled CHANNEL_UPDATE (type %s)', d.type) + end + return client:emit('channelUpdate', channel) +end + +function EventHandler.CHANNEL_DELETE(d, client) + local channel + local t = d.type + if t == channelType.text or t == channelType.news then + local guild = client._guilds:get(d.guild_id) + if not guild then return warning(client, 'Guild', d.guild_id, 'CHANNEL_DELETE') end + channel = guild._text_channels:_remove(d) + elseif t == channelType.voice then + local guild = client._guilds:get(d.guild_id) + if not guild then return warning(client, 'Guild', d.guild_id, 'CHANNEL_DELETE') end + channel = guild._voice_channels:_remove(d) + elseif t == channelType.private then + channel = client._private_channels:_remove(d) + elseif t == channelType.group then + channel = client._group_channels:_remove(d) + elseif t == channelType.category then + local guild = client._guilds:get(d.guild_id) + if not guild then return warning(client, 'Guild', d.guild_id, 'CHANNEL_DELETE') end + channel = guild._categories:_remove(d) + else + return client:warning('Unhandled CHANNEL_DELETE (type %s)', d.type) + end + return client:emit('channelDelete', channel) +end + +function EventHandler.CHANNEL_RECIPIENT_ADD(d, client) + local channel = client._group_channels:get(d.channel_id) + if not channel then return warning(client, 'GroupChannel', d.channel_id, 'CHANNEL_RECIPIENT_ADD') end + local user = channel._recipients:_insert(d.user) + return client:emit('recipientAdd', channel, user) +end + +function EventHandler.CHANNEL_RECIPIENT_REMOVE(d, client) + local channel = client._group_channels:get(d.channel_id) + if not channel then return warning(client, 'GroupChannel', d.channel_id, 'CHANNEL_RECIPIENT_REMOVE') end + local user = channel._recipients:_remove(d.user) + return client:emit('recipientRemove', channel, user) +end + +function EventHandler.GUILD_CREATE(d, client, shard) + if client._options.syncGuilds and not d.unavailable and not client._user._bot then + shard:syncGuilds({d.id}) + end + local guild = client._guilds:get(d.id) + if guild then + if guild._unavailable and not d.unavailable then + guild:_load(d) + guild:_makeAvailable(d) + client:emit('guildAvailable', guild) + end + if shard._loading then + shard._loading.guilds[d.id] = nil + return checkReady(shard) + end + else + guild = client._guilds:_insert(d) + return client:emit('guildCreate', guild) + end +end + +function EventHandler.GUILD_UPDATE(d, client) + local guild = client._guilds:_insert(d) + return client:emit('guildUpdate', guild) +end + +function EventHandler.GUILD_DELETE(d, client) + if d.unavailable then + local guild = client._guilds:_insert(d) + return client:emit('guildUnavailable', guild) + else + local guild = client._guilds:_remove(d) + return client:emit('guildDelete', guild) + end +end + +function EventHandler.GUILD_BAN_ADD(d, client) + local guild = client._guilds:get(d.guild_id) + if not guild then return warning(client, 'Guild', d.guild_id, 'GUILD_BAN_ADD') end + local user = client._users:_insert(d.user) + return client:emit('userBan', user, guild) +end + +function EventHandler.GUILD_BAN_REMOVE(d, client) + local guild = client._guilds:get(d.guild_id) + if not guild then return warning(client, 'Guild', d.guild_id, 'GUILD_BAN_REMOVE') end + local user = client._users:_insert(d.user) + return client:emit('userUnban', user, guild) +end + +function EventHandler.GUILD_EMOJIS_UPDATE(d, client) + local guild = client._guilds:get(d.guild_id) + if not guild then return warning(client, 'Guild', d.guild_id, 'GUILD_EMOJIS_UPDATE') end + guild._emojis:_load(d.emojis, true) + return client:emit('emojisUpdate', guild) +end + +function EventHandler.GUILD_MEMBER_ADD(d, client) + local guild = client._guilds:get(d.guild_id) + if not guild then return warning(client, 'Guild', d.guild_id, 'GUILD_MEMBER_ADD') end + local member = guild._members:_insert(d) + guild._member_count = guild._member_count + 1 + return client:emit('memberJoin', member) +end + +function EventHandler.GUILD_MEMBER_UPDATE(d, client) + local guild = client._guilds:get(d.guild_id) + if not guild then return warning(client, 'Guild', d.guild_id, 'GUILD_MEMBER_UPDATE') end + local member = guild._members:_insert(d) + return client:emit('memberUpdate', member) +end + +function EventHandler.GUILD_MEMBER_REMOVE(d, client) + local guild = client._guilds:get(d.guild_id) + if not guild then return warning(client, 'Guild', d.guild_id, 'GUILD_MEMBER_REMOVE') end + local member = guild._members:_remove(d) + guild._member_count = guild._member_count - 1 + return client:emit('memberLeave', member) +end + +function EventHandler.GUILD_ROLE_CREATE(d, client) + local guild = client._guilds:get(d.guild_id) + if not guild then return warning(client, 'Guild', d.guild_id, 'GUILD_ROLE_CREATE') end + local role = guild._roles:_insert(d.role) + return client:emit('roleCreate', role) +end + +function EventHandler.GUILD_ROLE_UPDATE(d, client) + local guild = client._guilds:get(d.guild_id) + if not guild then return warning(client, 'Guild', d.guild_id, 'GUILD_ROLE_UPDATE') end + local role = guild._roles:_insert(d.role) + return client:emit('roleUpdate', role) +end + +function EventHandler.GUILD_ROLE_DELETE(d, client) -- role object not provided + local guild = client._guilds:get(d.guild_id) + if not guild then return warning(client, 'Guild', d.guild_id, 'GUILD_ROLE_DELETE') end + local role = guild._roles:_delete(d.role_id) + if not role then return warning(client, 'Role', d.role_id, 'GUILD_ROLE_DELETE') end + return client:emit('roleDelete', role) +end + +function EventHandler.MESSAGE_CREATE(d, client) + local channel = getChannel(client, d.channel_id) + if not channel then return warning(client, 'TextChannel', d.channel_id, 'MESSAGE_CREATE') end + local message = channel._messages:_insert(d) + return client:emit('messageCreate', message) +end + +function EventHandler.MESSAGE_UPDATE(d, client) -- may not contain the whole message + local channel = getChannel(client, d.channel_id) + if not channel then return warning(client, 'TextChannel', d.channel_id, 'MESSAGE_UPDATE') end + local message = channel._messages:get(d.id) + if message then + message:_setOldContent(d) + message:_load(d) + return client:emit('messageUpdate', message) + else + return client:emit('messageUpdateUncached', channel, d.id) + end +end + +function EventHandler.MESSAGE_DELETE(d, client) -- message object not provided + local channel = getChannel(client, d.channel_id) + if not channel then return warning(client, 'TextChannel', d.channel_id, 'MESSAGE_DELETE') end + local message = channel._messages:_delete(d.id) + if message then + return client:emit('messageDelete', message) + else + return client:emit('messageDeleteUncached', channel, d.id) + end +end + +function EventHandler.MESSAGE_DELETE_BULK(d, client) + local channel = getChannel(client, d.channel_id) + if not channel then return warning(client, 'TextChannel', d.channel_id, 'MESSAGE_DELETE_BULK') end + for _, id in ipairs(d.ids) do + local message = channel._messages:_delete(id) + if message then + client:emit('messageDelete', message) + else + client:emit('messageDeleteUncached', channel, id) + end + end +end + +function EventHandler.MESSAGE_REACTION_ADD(d, client) + local channel = getChannel(client, d.channel_id) + if not channel then return warning(client, 'TextChannel', d.channel_id, 'MESSAGE_REACTION_ADD') end + local message = channel._messages:get(d.message_id) + if message then + local reaction = message:_addReaction(d) + return client:emit('reactionAdd', reaction, d.user_id) + else + local k = d.emoji.id ~= null and d.emoji.id or d.emoji.name + return client:emit('reactionAddUncached', channel, d.message_id, k, d.user_id) + end +end + +function EventHandler.MESSAGE_REACTION_REMOVE(d, client) + local channel = getChannel(client, d.channel_id) + if not channel then return warning(client, 'TextChannel', d.channel_id, 'MESSAGE_REACTION_REMOVE') end + local message = channel._messages:get(d.message_id) + if message then + local reaction = message:_removeReaction(d) + if not reaction then -- uncached reaction? + local k = d.emoji.id ~= null and d.emoji.id or d.emoji.name + return warning(client, 'Reaction', k, 'MESSAGE_REACTION_REMOVE') + end + return client:emit('reactionRemove', reaction, d.user_id) + else + local k = d.emoji.id ~= null and d.emoji.id or d.emoji.name + return client:emit('reactionRemoveUncached', channel, d.message_id, k, d.user_id) + end +end + +function EventHandler.MESSAGE_REACTION_REMOVE_ALL(d, client) + local channel = getChannel(client, d.channel_id) + if not channel then return warning(client, 'TextChannel', d.channel_id, 'MESSAGE_REACTION_REMOVE_ALL') end + local message = channel._messages:get(d.message_id) + if message then + local reactions = message._reactions + if reactions then + for reaction in reactions:iter() do + reaction._count = 0 + end + message._reactions = nil + end + return client:emit('reactionRemoveAll', message) + else + return client:emit('reactionRemoveAllUncached', channel, d.message_id) + end +end + +function EventHandler.CHANNEL_PINS_UPDATE(d, client) + local channel = getChannel(client, d.channel_id) + if not channel then return warning(client, 'TextChannel', d.channel_id, 'CHANNEL_PINS_UPDATE') end + return client:emit('pinsUpdate', channel) +end + +function EventHandler.PRESENCE_UPDATE(d, client) -- may have incomplete data + local user = client._users:get(d.user.id) + if user then + user:_load(d.user) + end + if d.guild_id then + local guild = client._guilds:get(d.guild_id) + if not guild then return warning(client, 'Guild', d.guild_id, 'PRESENCE_UPDATE') end + local member + if client._options.cacheAllMembers then + member = guild._members:get(d.user.id) + if not member then return end -- still loading or member left + else + if d.status == 'offline' then -- uncache offline members + member = guild._members:_delete(d.user.id) + else + if d.user.username then -- member was offline + member = guild._members:_insert(d) + elseif user then -- member was invisible, user is still cached + member = guild._members:_insert(d) + member._user = user + end + end + end + if member then + member:_loadPresence(d) + return client:emit('presenceUpdate', member) + end + else + local relationship = client._relationships:get(d.user.id) + if relationship then + relationship:_loadPresence(d) + return client:emit('relationshipUpdate', relationship) + end + end +end + +function EventHandler.RELATIONSHIP_ADD(d, client) + local relationship = client._relationships:_insert(d) + return client:emit('relationshipAdd', relationship) +end + +function EventHandler.RELATIONSHIP_REMOVE(d, client) + local relationship = client._relationships:_remove(d) + return client:emit('relationshipRemove', relationship) +end + +function EventHandler.TYPING_START(d, client) + return client:emit('typingStart', d.user_id, d.channel_id, d.timestamp) +end + +function EventHandler.USER_UPDATE(d, client) + client._user:_load(d) + return client:emit('userUpdate', client._user) +end + +local function load(obj, d) + for k, v in pairs(d) do obj[k] = v end +end + +function EventHandler.VOICE_STATE_UPDATE(d, client) + local guild = client._guilds:get(d.guild_id) + if not guild then return warning(client, 'Guild', d.guild_id, 'VOICE_STATE_UPDATE') end + local member = d.member and guild._members:_insert(d.member) or guild._members:get(d.user_id) + if not member then return warning(client, 'Member', d.user_id, 'VOICE_STATE_UPDATE') end + local states = guild._voice_states + local channels = guild._voice_channels + local new_channel_id = d.channel_id + local state = states[d.user_id] + if state then -- user is already connected + local old_channel_id = state.channel_id + load(state, d) + if new_channel_id ~= null then -- state changed, but user has not disconnected + if new_channel_id == old_channel_id then -- user did not change channels + client:emit('voiceUpdate', member) + else -- user changed channels + local old = channels:get(old_channel_id) + local new = channels:get(new_channel_id) + if d.user_id == client._user._id then -- move connection to new channel + local connection = old._connection + if connection then + new._connection = connection + old._connection = nil + connection._channel = new + connection:_continue(true) + end + end + client:emit('voiceChannelLeave', member, old) + client:emit('voiceChannelJoin', member, new) + end + else -- user has disconnected + states[d.user_id] = nil + local old = channels:get(old_channel_id) + client:emit('voiceChannelLeave', member, old) + client:emit('voiceDisconnect', member) + end + else -- user has connected + states[d.user_id] = d + local new = channels:get(new_channel_id) + client:emit('voiceConnect', member) + client:emit('voiceChannelJoin', member, new) + end +end + +function EventHandler.VOICE_SERVER_UPDATE(d, client) + local guild = client._guilds:get(d.guild_id) + if not guild then return warning(client, 'Guild', d.guild_id, 'VOICE_SERVER_UPDATE') end + local state = guild._voice_states[client._user._id] + if not state then return client:warning('Voice state not initialized before VOICE_SERVER_UPDATE') end + load(state, d) + local channel = guild._voice_channels:get(state.channel_id) + if not channel then return warning(client, 'GuildVoiceChannel', state.channel_id, 'VOICE_SERVER_UPDATE') end + local connection = channel._connection + if not connection then return client:warning('Voice connection not initialized before VOICE_SERVER_UPDATE') end + return client._voice:_prepareConnection(state, connection) +end + +function EventHandler.WEBHOOKS_UPDATE(d, client) -- webhook object is not provided + local guild = client._guilds:get(d.guild_id) + if not guild then return warning(client, 'Guild', d.guild_id, 'WEBHOOKS_UDPATE') end + local channel = guild._text_channels:get(d.channel_id) + if not channel then return warning(client, 'TextChannel', d.channel_id, 'WEBHOOKS_UPDATE') end + return client:emit('webhooksUpdate', channel) +end + +return EventHandler diff --git a/deps/discordia/libs/client/Resolver.lua b/deps/discordia/libs/client/Resolver.lua new file mode 100644 index 0000000..0ee2648 --- /dev/null +++ b/deps/discordia/libs/client/Resolver.lua @@ -0,0 +1,202 @@ +local fs = require('fs') +local ffi = require('ffi') +local ssl = require('openssl') +local class = require('class') +local enums = require('enums') + +local permission = enums.permission +local actionType = enums.actionType +local messageFlag = enums.messageFlag +local base64 = ssl.base64 +local readFileSync = fs.readFileSync +local classes = class.classes +local isInstance = class.isInstance +local isObject = class.isObject +local insert = table.insert +local format = string.format + +local Resolver = {} + +local istype = ffi.istype +local int64_t = ffi.typeof('int64_t') +local uint64_t = ffi.typeof('uint64_t') + +local function int(obj) + local t = type(obj) + if t == 'string' then + if tonumber(obj) then + return obj + end + elseif t == 'cdata' then + if istype(int64_t, obj) or istype(uint64_t, obj) then + return tostring(obj):match('%d*') + end + elseif t == 'number' then + return format('%i', obj) + elseif isInstance(obj, classes.Date) then + return obj:toSnowflake() + end +end + +function Resolver.userId(obj) + if isObject(obj) then + if isInstance(obj, classes.User) then + return obj.id + elseif isInstance(obj, classes.Member) then + return obj.user.id + elseif isInstance(obj, classes.Message) then + return obj.author.id + elseif isInstance(obj, classes.Guild) then + return obj.ownerId + end + end + return int(obj) +end + +function Resolver.messageId(obj) + if isInstance(obj, classes.Message) then + return obj.id + end + return int(obj) +end + +function Resolver.channelId(obj) + if isInstance(obj, classes.Channel) then + return obj.id + end + return int(obj) +end + +function Resolver.roleId(obj) + if isInstance(obj, classes.Role) then + return obj.id + end + return int(obj) +end + +function Resolver.emojiId(obj) + if isInstance(obj, classes.Emoji) then + return obj.id + elseif isInstance(obj, classes.Reaction) then + return obj.emojiId + elseif isInstance(obj, classes.Activity) then + return obj.emojiId + end + return int(obj) +end + +function Resolver.guildId(obj) + if isInstance(obj, classes.Guild) then + return obj.id + end + return int(obj) +end + +function Resolver.entryId(obj) + if isInstance(obj, classes.AuditLogEntry) then + return obj.id + end + return int(obj) +end + +function Resolver.messageIds(objs) + local ret = {} + if isInstance(objs, classes.Iterable) then + for obj in objs:iter() do + insert(ret, Resolver.messageId(obj)) + end + elseif type(objs) == 'table' then + for _, obj in pairs(objs) do + insert(ret, Resolver.messageId(obj)) + end + end + return ret +end + +function Resolver.roleIds(objs) + local ret = {} + if isInstance(objs, classes.Iterable) then + for obj in objs:iter() do + insert(ret, Resolver.roleId(obj)) + end + elseif type(objs) == 'table' then + for _, obj in pairs(objs) do + insert(ret, Resolver.roleId(obj)) + end + end + return ret +end + +function Resolver.emoji(obj) + if isInstance(obj, classes.Emoji) then + return obj.hash + elseif isInstance(obj, classes.Reaction) then + return obj.emojiHash + elseif isInstance(obj, classes.Activity) then + return obj.emojiHash + end + return tostring(obj) +end + +function Resolver.color(obj) + if isInstance(obj, classes.Color) then + return obj.value + end + return tonumber(obj) +end + +function Resolver.permissions(obj) + if isInstance(obj, classes.Permissions) then + return obj.value + end + return tonumber(obj) +end + +function Resolver.permission(obj) + local t = type(obj) + local n = nil + if t == 'string' then + n = permission[obj] + elseif t == 'number' then + n = permission(obj) and obj + end + return n +end + +function Resolver.actionType(obj) + local t = type(obj) + local n = nil + if t == 'string' then + n = actionType[obj] + elseif t == 'number' then + n = actionType(obj) and obj + end + return n +end + +function Resolver.messageFlag(obj) + local t = type(obj) + local n = nil + if t == 'string' then + n = messageFlag[obj] + elseif t == 'number' then + n = messageFlag(obj) and obj + end + return n +end + +function Resolver.base64(obj) + if type(obj) == 'string' then + if obj:find('data:.*;base64,') == 1 then + return obj + end + local data, err = readFileSync(obj) + if not data then + return nil, err + end + return 'data:;base64,' .. base64(data) + end + return nil +end + +return Resolver diff --git a/deps/discordia/libs/client/Shard.lua b/deps/discordia/libs/client/Shard.lua new file mode 100644 index 0000000..9c82673 --- /dev/null +++ b/deps/discordia/libs/client/Shard.lua @@ -0,0 +1,257 @@ +local json = require('json') +local timer = require('timer') + +local EventHandler = require('client/EventHandler') +local WebSocket = require('client/WebSocket') + +local constants = require('constants') +local enums = require('enums') + +local logLevel = enums.logLevel +local min, max, random = math.min, math.max, math.random +local null = json.null +local format = string.format +local sleep = timer.sleep +local setInterval, clearInterval = timer.setInterval, timer.clearInterval +local wrap = coroutine.wrap + +local ID_DELAY = constants.ID_DELAY + +local DISPATCH = 0 +local HEARTBEAT = 1 +local IDENTIFY = 2 +local STATUS_UPDATE = 3 +local VOICE_STATE_UPDATE = 4 +-- local VOICE_SERVER_PING = 5 -- TODO +local RESUME = 6 +local RECONNECT = 7 +local REQUEST_GUILD_MEMBERS = 8 +local INVALID_SESSION = 9 +local HELLO = 10 +local HEARTBEAT_ACK = 11 +local GUILD_SYNC = 12 + +local ignore = { + ['CALL_DELETE'] = true, + ['CHANNEL_PINS_ACK'] = true, + ['GUILD_INTEGRATIONS_UPDATE'] = true, + ['MESSAGE_ACK'] = true, + ['PRESENCES_REPLACE'] = true, + ['USER_SETTINGS_UPDATE'] = true, + ['USER_GUILD_SETTINGS_UPDATE'] = true, + ['SESSIONS_REPLACE'] = true, + ['INVITE_CREATE'] = true, + ['INVITE_DELETE'] = true, + ['INTEGRATION_CREATE'] = true, + ['INTEGRATION_UPDATE'] = true, + ['INTEGRATION_DELETE'] = true, +} + +local Shard = require('class')('Shard', WebSocket) + +function Shard:__init(id, client) + WebSocket.__init(self, client) + self._id = id + self._client = client + self._backoff = 1000 +end + +for name in pairs(logLevel) do + Shard[name] = function(self, fmt, ...) + local client = self._client + return client[name](client, format('Shard %i : %s', self._id, fmt), ...) + end +end + +function Shard:__tostring() + return format('Shard: %i', self._id) +end + +local function getReconnectTime(self, n, m) + return self._backoff * (n + random() * (m - n)) +end + +local function incrementReconnectTime(self) + self._backoff = min(self._backoff * 2, 60000) +end + +local function decrementReconnectTime(self) + self._backoff = max(self._backoff / 2, 1000) +end + +function Shard:handleDisconnect(url, path) + self._client:emit('shardDisconnect', self._id) + if self._reconnect then + self:info('Reconnecting...') + return self:connect(url, path) + elseif self._reconnect == nil and self._client._options.autoReconnect then + local backoff = getReconnectTime(self, 0.9, 1.1) + incrementReconnectTime(self) + self:info('Reconnecting after %i ms...', backoff) + sleep(backoff) + return self:connect(url, path) + end +end + +function Shard:handlePayload(payload) + + local client = self._client + + local s = payload.s + local t = payload.t + local d = payload.d + local op = payload.op + + if t ~= null then + self:debug('WebSocket OP %s : %s : %s', op, t, s) + else + self:debug('WebSocket OP %s', op) + end + + if op == DISPATCH then + + self._seq = s + if not ignore[t] then + EventHandler[t](d, client, self) + end + + elseif op == HEARTBEAT then + + self:heartbeat() + + elseif op == RECONNECT then + + self:info('Discord has requested a reconnection') + self:disconnect(true) + + elseif op == INVALID_SESSION then + + if payload.d and self._session_id then + self:info('Session invalidated, resuming...') + self:resume() + else + self:info('Session invalidated, re-identifying...') + sleep(random(1000, 5000)) + self:identify() + end + + elseif op == HELLO then + + self:info('Received HELLO') + self:startHeartbeat(d.heartbeat_interval) + if self._session_id then + self:resume() + else + self:identify() + end + + elseif op == HEARTBEAT_ACK then + + client:emit('heartbeat', self._id, self._sw.milliseconds) + + elseif op then + + self:warning('Unhandled WebSocket payload OP %i', op) + + end + +end + +local function loop(self) + decrementReconnectTime(self) + return wrap(self.heartbeat)(self) +end + +function Shard:startHeartbeat(interval) + if self._heartbeat then + clearInterval(self._heartbeat) + end + self._heartbeat = setInterval(interval, loop, self) +end + +function Shard:stopHeartbeat() + if self._heartbeat then + clearInterval(self._heartbeat) + end + self._heartbeat = nil +end + +function Shard:identifyWait() + if self:waitFor('READY', 1.5 * ID_DELAY) then + return sleep(ID_DELAY) + end +end + +function Shard:heartbeat() + self._sw:reset() + return self:_send(HEARTBEAT, self._seq or json.null) +end + +function Shard:identify() + + local client = self._client + local mutex = client._mutex + local options = client._options + + mutex:lock() + wrap(function() + self:identifyWait() + mutex:unlock() + end)() + + self._seq = nil + self._session_id = nil + self._ready = false + self._loading = {guilds = {}, chunks = {}, syncs = {}} + + return self:_send(IDENTIFY, { + token = client._token, + properties = { + ['$os'] = jit.os, + ['$browser'] = 'Discordia', + ['$device'] = 'Discordia', + ['$referrer'] = '', + ['$referring_domain'] = '', + }, + compress = options.compress, + large_threshold = options.largeThreshold, + shard = {self._id, client._total_shard_count}, + presence = next(client._presence) and client._presence, + }, true) + +end + +function Shard:resume() + return self:_send(RESUME, { + token = self._client._token, + session_id = self._session_id, + seq = self._seq + }) +end + +function Shard:requestGuildMembers(id) + return self:_send(REQUEST_GUILD_MEMBERS, { + guild_id = id, + query = '', + limit = 0, + }) +end + +function Shard:updateStatus(presence) + return self:_send(STATUS_UPDATE, presence) +end + +function Shard:updateVoice(guild_id, channel_id, self_mute, self_deaf) + return self:_send(VOICE_STATE_UPDATE, { + guild_id = guild_id, + channel_id = channel_id or null, + self_mute = self_mute or false, + self_deaf = self_deaf or false, + }) +end + +function Shard:syncGuilds(ids) + return self:_send(GUILD_SYNC, ids) +end + +return Shard diff --git a/deps/discordia/libs/client/WebSocket.lua b/deps/discordia/libs/client/WebSocket.lua new file mode 100644 index 0000000..412d0d6 --- /dev/null +++ b/deps/discordia/libs/client/WebSocket.lua @@ -0,0 +1,121 @@ +local json = require('json') +local miniz = require('miniz') +local Mutex = require('utils/Mutex') +local Emitter = require('utils/Emitter') +local Stopwatch = require('utils/Stopwatch') + +local websocket = require('coro-websocket') +local constants = require('constants') + +local inflate = miniz.inflate +local encode, decode, null = json.encode, json.decode, json.null +local ws_parseUrl, ws_connect = websocket.parseUrl, websocket.connect + +local GATEWAY_DELAY = constants.GATEWAY_DELAY + +local TEXT = 1 +local BINARY = 2 +local CLOSE = 8 + +local function connect(url, path) + local options = assert(ws_parseUrl(url)) + options.pathname = path + return assert(ws_connect(options)) +end + +local WebSocket = require('class')('WebSocket', Emitter) + +function WebSocket:__init(parent) + Emitter.__init(self) + self._parent = parent + self._mutex = Mutex() + self._sw = Stopwatch() +end + +function WebSocket:connect(url, path) + + local success, res, read, write = pcall(connect, url, path) + + if success then + self._read = read + self._write = write + self._reconnect = nil + self:info('Connected to %s', url) + local parent = self._parent + for message in self._read do + local payload, str = self:parseMessage(message) + if not payload then break end + parent:emit('raw', str) + if self.handlePayload then -- virtual method + self:handlePayload(payload) + end + end + self:info('Disconnected') + else + self:error('Could not connect to %s (%s)', url, res) -- TODO: get new url? + end + + self._read = nil + self._write = nil + self._identified = nil + + if self.stopHeartbeat then -- virtual method + self:stopHeartbeat() + end + + if self.handleDisconnect then -- virtual method + return self:handleDisconnect(url, path) + end + +end + +function WebSocket:parseMessage(message) + + local opcode = message.opcode + local payload = message.payload + + if opcode == TEXT then + + return decode(payload, 1, null), payload + + elseif opcode == BINARY then + + payload = inflate(payload, 1) + return decode(payload, 1, null), payload + + elseif opcode == CLOSE then + + local code, i = ('>H'):unpack(payload) + local msg = #payload > i and payload:sub(i) or 'Connection closed' + self:warning('%i - %s', code, msg) + return nil + + end + +end + +function WebSocket:_send(op, d, identify) + self._mutex:lock() + local success, err + if identify or self._session_id then + if self._write then + success, err = self._write {opcode = TEXT, payload = encode {op = op, d = d}} + else + success, err = false, 'Not connected to gateway' + end + else + success, err = false, 'Invalid session' + end + self._mutex:unlockAfter(GATEWAY_DELAY) + return success, err +end + +function WebSocket:disconnect(reconnect) + if not self._write then return end + self._reconnect = not not reconnect + self._write() + self._read = nil + self._write = nil +end + +return WebSocket diff --git a/deps/discordia/libs/constants.lua b/deps/discordia/libs/constants.lua new file mode 100644 index 0000000..5836d94 --- /dev/null +++ b/deps/discordia/libs/constants.lua @@ -0,0 +1,17 @@ +return { + CACHE_AGE = 3600, -- seconds + ID_DELAY = 5000, -- milliseconds + GATEWAY_DELAY = 500, -- milliseconds, + DISCORD_EPOCH = 1420070400000, -- milliseconds + GATEWAY_VERSION = 6, + DEFAULT_AVATARS = 5, + ZWSP = '\226\128\139', + NS_PER_US = 1000, + US_PER_MS = 1000, + MS_PER_S = 1000, + S_PER_MIN = 60, + MIN_PER_HOUR = 60, + HOUR_PER_DAY = 24, + DAY_PER_WEEK = 7, + GATEWAY_VERSION_VOICE = 3, +} diff --git a/deps/discordia/libs/containers/Activity.lua b/deps/discordia/libs/containers/Activity.lua new file mode 100644 index 0000000..0ed9524 --- /dev/null +++ b/deps/discordia/libs/containers/Activity.lua @@ -0,0 +1,157 @@ +--[=[ +@c Activity +@d Represents a Discord user's presence data, either plain game or streaming presence or a rich presence. +Most if not all properties may be nil. +]=] + +local Container = require('containers/abstract/Container') + +local format = string.format + +local Activity, get = require('class')('Activity', Container) + +function Activity:__init(data, parent) + Container.__init(self, data, parent) + return self:_loadMore(data) +end + +function Activity:_load(data) + Container._load(self, data) + return self:_loadMore(data) +end + +function Activity:_loadMore(data) + local timestamps = data.timestamps + self._start = timestamps and timestamps.start + self._stop = timestamps and timestamps['end'] -- thanks discord + local assets = data.assets + self._small_text = assets and assets.small_text + self._large_text = assets and assets.large_text + self._small_image = assets and assets.small_image + self._large_image = assets and assets.large_image + local party = data.party + self._party_id = party and party.id + self._party_size = party and party.size and party.size[1] + self._party_max = party and party.size and party.size[2] + local emoji = data.emoji + self._emoji_name = emoji and emoji.name + self._emoji_id = emoji and emoji.id + self._emoji_animated = emoji and emoji.animated +end + +--[=[ +@m __hash +@r string +@d Returns `Activity.parent:__hash()` +]=] +function Activity:__hash() + return self._parent:__hash() +end + +--[=[@p start number/nil The Unix timestamp for when this Rich Presence activity was started.]=] +function get.start(self) + return self._start +end + +--[=[@p stop number/nil The Unix timestamp for when this Rich Presence activity was stopped.]=] +function get.stop(self) + return self._stop +end + +--[=[@p name string/nil The game that the user is currently playing.]=] +function get.name(self) + return self._name +end + +--[=[@p type number/nil The type of user's game status. See the `activityType` +enumeration for a human-readable representation.]=] +function get.type(self) + return self._type +end + +--[=[@p url string/nil The URL that is set for a user's streaming game status.]=] +function get.url(self) + return self._url +end + +--[=[@p applicationId string/nil The application id controlling this Rich Presence activity.]=] +function get.applicationId(self) + return self._application_id +end + +--[=[@p state string/nil string for the Rich Presence state section.]=] +function get.state(self) + return self._state +end + +--[=[@p details string/nil string for the Rich Presence details section.]=] +function get.details(self) + return self._details +end + +--[=[@p textSmall string/nil string for the Rich Presence small image text.]=] +function get.textSmall(self) + return self._small_text +end + +--[=[@p textLarge string/nil string for the Rich Presence large image text.]=] +function get.textLarge(self) + return self._large_text +end + +--[=[@p imageSmall string/nil URL for the Rich Presence small image.]=] +function get.imageSmall(self) + return self._small_image +end + +--[=[@p imageLarge string/nil URL for the Rich Presence large image.]=] +function get.imageLarge(self) + return self._large_image +end + +--[=[@p partyId string/nil Party id for this Rich Presence.]=] +function get.partyId(self) + return self._party_id +end + +--[=[@p partySize number/nil Size of the Rich Presence party.]=] +function get.partySize(self) + return self._party_size +end + +--[=[@p partyMax number/nil Max size for the Rich Presence party.]=] +function get.partyMax(self) + return self._party_max +end + +--[=[@p emojiId string/nil The ID of the emoji used in this presence if one is +set and if it is a custom emoji.]=] +function get.emojiId(self) + return self._emoji_id +end + +--[=[@p emojiName string/nil The name of the emoji used in this presence if one +is set and if it has a custom emoji. This will be the raw string for a standard emoji.]=] +function get.emojiName(self) + return self._emoji_name +end + +--[=[@p emojiHash string/nil The discord hash for the emoji used in this presence if one is +set. This will be the raw string for a standard emoji.]=] +function get.emojiHash(self) + if self._emoji_id then + return self._emoji_name .. ':' .. self._emoji_id + else + return self._emoji_name + end +end + +--[=[@p emojiURL string/nil string The URL that can be used to view a full +version of the emoji used in this activity if one is set and if it is a custom emoji.]=] +function get.emojiURL(self) + local id = self._emoji_id + local ext = self._emoji_animated and 'gif' or 'png' + return id and format('https://cdn.discordapp.com/emojis/%s.%s', id, ext) or nil +end + +return Activity diff --git a/deps/discordia/libs/containers/AuditLogEntry.lua b/deps/discordia/libs/containers/AuditLogEntry.lua new file mode 100644 index 0000000..c82fe90 --- /dev/null +++ b/deps/discordia/libs/containers/AuditLogEntry.lua @@ -0,0 +1,227 @@ +--[=[ +@c AuditLogEntry x Snowflake +@d Represents an entry made into a guild's audit log. +]=] + +local Snowflake = require('containers/abstract/Snowflake') + +local enums = require('enums') +local actionType = enums.actionType + +local AuditLogEntry, get = require('class')('AuditLogEntry', Snowflake) + +function AuditLogEntry:__init(data, parent) + Snowflake.__init(self, data, parent) + if data.changes then + for i, change in ipairs(data.changes) do + data.changes[change.key] = change + data.changes[i] = nil + change.key = nil + change.old = change.old_value + change.new = change.new_value + change.old_value = nil + change.new_value = nil + end + self._changes = data.changes + end + self._options = data.options +end + +--[=[ +@m getBeforeAfter +@t mem +@r table +@r table +@d Returns two tables of the target's properties before the change, and after the change. +]=] +function AuditLogEntry:getBeforeAfter() + local before, after = {}, {} + for k, change in pairs(self._changes) do + before[k], after[k] = change.old, change.new + end + return before, after +end + +local function unknown(self) + return nil, 'unknown audit log action type: ' .. self._action_type +end + +local targets = setmetatable({ + + [actionType.guildUpdate] = function(self) + return self._parent + end, + + [actionType.channelCreate] = function(self) + return self._parent:getChannel(self._target_id) + end, + + [actionType.channelUpdate] = function(self) + return self._parent:getChannel(self._target_id) + end, + + [actionType.channelDelete] = function(self) + return self._parent:getChannel(self._target_id) + end, + + [actionType.channelOverwriteCreate] = function(self) + return self._parent:getChannel(self._target_id) + end, + + [actionType.channelOverwriteUpdate] = function(self) + return self._parent:getChannel(self._target_id) + end, + + [actionType.channelOverwriteDelete] = function(self) + return self._parent:getChannel(self._target_id) + end, + + [actionType.memberKick] = function(self) + return self._parent._parent:getUser(self._target_id) + end, + + [actionType.memberPrune] = function() + return nil + end, + + [actionType.memberBanAdd] = function(self) + return self._parent._parent:getUser(self._target_id) + end, + + [actionType.memberBanRemove] = function(self) + return self._parent._parent:getUser(self._target_id) + end, + + [actionType.memberUpdate] = function(self) + return self._parent:getMember(self._target_id) + end, + + [actionType.memberRoleUpdate] = function(self) + return self._parent:getMember(self._target_id) + end, + + [actionType.roleCreate] = function(self) + return self._parent:getRole(self._target_id) + end, + + [actionType.roleUpdate] = function(self) + return self._parent:getRole(self._target_id) + end, + + [actionType.roleDelete] = function(self) + return self._parent:getRole(self._target_id) + end, + + [actionType.inviteCreate] = function() + return nil + end, + + [actionType.inviteUpdate] = function() + return nil + end, + + [actionType.inviteDelete] = function() + return nil + end, + + [actionType.webhookCreate] = function(self) + return self._parent._parent._webhooks:get(self._target_id) + end, + + [actionType.webhookUpdate] = function(self) + return self._parent._parent._webhooks:get(self._target_id) + end, + + [actionType.webhookDelete] = function(self) + return self._parent._parent._webhooks:get(self._target_id) + end, + + [actionType.emojiCreate] = function(self) + return self._parent:getEmoji(self._target_id) + end, + + [actionType.emojiUpdate] = function(self) + return self._parent:getEmoji(self._target_id) + end, + + [actionType.emojiDelete] = function(self) + return self._parent:getEmoji(self._target_id) + end, + + [actionType.messageDelete] = function(self) + return self._parent._parent:getUser(self._target_id) + end, + +}, {__index = function() return unknown end}) + +--[=[ +@m getTarget +@t http? +@r * +@d Gets the target object of the affected entity. The returned object can be: [[Guild]], +[[GuildChannel]], [[User]], [[Member]], [[Role]], [[Webhook]], [[Emoji]], nil +]=] +function AuditLogEntry:getTarget() + return targets[self._action_type](self) +end + +--[=[ +@m getUser +@t http? +@r User +@d Gets the user who performed the changes. +]=] +function AuditLogEntry:getUser() + return self._parent._parent:getUser(self._user_id) +end + +--[=[ +@m getMember +@t http? +@r Member/nil +@d Gets the member object of the user who performed the changes. +]=] +function AuditLogEntry:getMember() + return self._parent:getMember(self._user_id) +end + +--[=[@p changes table/nil A table of audit log change objects. The key represents +the property of the changed target and the value contains a table of `new` and +possibly `old`, representing the property's new and old value.]=] +function get.changes(self) + return self._changes +end + +--[=[@p options table/nil A table of optional audit log information.]=] +function get.options(self) + return self._options +end + +--[=[@p actionType number The action type. Use the `actionType `enumeration +for a human-readable representation.]=] +function get.actionType(self) + return self._action_type +end + +--[=[@p targetId string/nil The Snowflake ID of the affected entity. Will +be `nil` for certain targets.]=] +function get.targetId(self) + return self._target_id +end + +--[=[@p userId string The Snowflake ID of the user who commited the action.]=] +function get.userId(self) + return self._user_id +end + +--[=[@p reason string/nil The reason provided by the user for the change.]=] +function get.reason(self) + return self._reason +end + +--[=[@p guild Guild The guild in which this audit log entry was found.]=] +function get.guild(self) + return self._parent +end + +return AuditLogEntry diff --git a/deps/discordia/libs/containers/Ban.lua b/deps/discordia/libs/containers/Ban.lua new file mode 100644 index 0000000..09d37d5 --- /dev/null +++ b/deps/discordia/libs/containers/Ban.lua @@ -0,0 +1,52 @@ +--[=[ +@c Ban x Container +@d Represents a Discord guild ban. Essentially a combination of the banned user and +a reason explaining the ban, if one was provided. +]=] + +local Container = require('containers/abstract/Container') + +local Ban, get = require('class')('Ban', Container) + +function Ban:__init(data, parent) + Container.__init(self, data, parent) + self._user = self.client._users:_insert(data.user) +end + +--[=[ +@m __hash +@r string +@d Returns `Ban.user.id` +]=] +function Ban:__hash() + return self._user._id +end + +--[=[ +@m delete +@t http +@r boolean +@d Deletes the ban object, unbanning the corresponding user. +Equivalent to `Ban.guild:unbanUser(Ban.user)`. +]=] +function Ban:delete() + return self._parent:unbanUser(self._user) +end + +--[=[@p reason string/nil The reason for the ban, if one was set. This should be from 1 to 512 characters +in length.]=] +function get.reason(self) + return self._reason +end + +--[=[@p guild Guild The guild in which this ban object exists.]=] +function get.guild(self) + return self._parent +end + +--[=[@p user User The user that this ban object represents.]=] +function get.user(self) + return self._user +end + +return Ban diff --git a/deps/discordia/libs/containers/Emoji.lua b/deps/discordia/libs/containers/Emoji.lua new file mode 100644 index 0000000..52c4de6 --- /dev/null +++ b/deps/discordia/libs/containers/Emoji.lua @@ -0,0 +1,168 @@ +--[=[ +@c Emoji x Snowflake +@d Represents a custom emoji object usable in message content and reactions. +Standard unicode emojis do not have a class; they are just strings. +]=] + +local Snowflake = require('containers/abstract/Snowflake') +local Resolver = require('client/Resolver') +local ArrayIterable = require('iterables/ArrayIterable') +local json = require('json') + +local format = string.format + +local Emoji, get = require('class')('Emoji', Snowflake) + +function Emoji:__init(data, parent) + Snowflake.__init(self, data, parent) + self.client._emoji_map[self._id] = parent + return self:_loadMore(data) +end + +function Emoji:_load(data) + Snowflake._load(self, data) + return self:_loadMore(data) +end + +function Emoji:_loadMore(data) + if data.roles then + local roles = #data.roles > 0 and data.roles or nil + if self._roles then + self._roles._array = roles + else + self._roles_raw = roles + end + end +end + +function Emoji:_modify(payload) + local data, err = self.client._api:modifyGuildEmoji(self._parent._id, self._id, payload) + if data then + self:_load(data) + return true + else + return false, err + end +end + +--[=[ +@m setName +@t http +@p name string +@r boolean +@d Sets the emoji's name. The name must be between 2 and 32 characters in length. +]=] +function Emoji:setName(name) + return self:_modify({name = name or json.null}) +end + +--[=[ +@m setRoles +@t http +@p roles Role-ID-Resolvables +@r boolean +@d Sets the roles that can use the emoji. +]=] +function Emoji:setRoles(roles) + roles = Resolver.roleIds(roles) + return self:_modify({roles = roles or json.null}) +end + +--[=[ +@m delete +@t http +@r boolean +@d Permanently deletes the emoji. This cannot be undone! +]=] +function Emoji:delete() + local data, err = self.client._api:deleteGuildEmoji(self._parent._id, self._id) + if data then + local cache = self._parent._emojis + if cache then + cache:_delete(self._id) + end + return true + else + return false, err + end +end + +--[=[ +@m hasRole +@t mem +@p id Role-ID-Resolvable +@r boolean +@d Returns whether or not the provided role is allowed to use the emoji. +]=] +function Emoji:hasRole(id) + id = Resolver.roleId(id) + local roles = self._roles and self._roles._array or self._roles_raw + if roles then + for _, v in ipairs(roles) do + if v == id then + return true + end + end + end + return false +end + +--[=[@p name string The name of the emoji.]=] +function get.name(self) + return self._name +end + +--[=[@p guild Guild The guild in which the emoji exists.]=] +function get.guild(self) + return self._parent +end + +--[=[@p mentionString string A string that, when included in a message content, may resolve as an emoji image +in the official Discord client.]=] +function get.mentionString(self) + local fmt = self._animated and '' or '<:%s>' + return format(fmt, self.hash) +end + +--[=[@p url string The URL that can be used to view a full version of the emoji.]=] +function get.url(self) + local ext = self._animated and 'gif' or 'png' + return format('https://cdn.discordapp.com/emojis/%s.%s', self._id, ext) +end + +--[=[@p managed boolean Whether this emoji is managed by an integration such as Twitch or YouTube.]=] +function get.managed(self) + return self._managed +end + +--[=[@p requireColons boolean Whether this emoji requires colons to be used in the official Discord client.]=] +function get.requireColons(self) + return self._require_colons +end + +--[=[@p hash string String with the format `name:id`, used in HTTP requests. +This is different from `Emoji:__hash`, which returns only the Snowflake ID. +]=] +function get.hash(self) + return self._name .. ':' .. self._id +end + +--[=[@p animated boolean Whether this emoji is animated.]=] +function get.animated(self) + return self._animated +end + +--[=[@p roles ArrayIterable An iterable array of roles that may be required to use this emoji, generally +related to integration-managed emojis. Object order is not guaranteed.]=] +function get.roles(self) + if not self._roles then + local roles = self._parent._roles + self._roles = ArrayIterable(self._roles_raw, function(id) + return roles:get(id) + end) + self._roles_raw = nil + end + return self._roles +end + +return Emoji diff --git a/deps/discordia/libs/containers/GroupChannel.lua b/deps/discordia/libs/containers/GroupChannel.lua new file mode 100644 index 0000000..4fc4ced --- /dev/null +++ b/deps/discordia/libs/containers/GroupChannel.lua @@ -0,0 +1,122 @@ +--[=[ +@c GroupChannel x TextChannel +@d Represents a Discord group channel. Essentially a private channel that may have +more than one and up to ten recipients. This class should only be relevant to +user-accounts; bots cannot normally join group channels. +]=] + +local json = require('json') + +local TextChannel = require('containers/abstract/TextChannel') +local SecondaryCache = require('iterables/SecondaryCache') +local Resolver = require('client/Resolver') + +local format = string.format + +local GroupChannel, get = require('class')('GroupChannel', TextChannel) + +function GroupChannel:__init(data, parent) + TextChannel.__init(self, data, parent) + self._recipients = SecondaryCache(data.recipients, self.client._users) +end + +--[=[ +@m setName +@t http +@p name string +@r boolean +@d Sets the channel's name. This must be between 1 and 100 characters in length. +]=] +function GroupChannel:setName(name) + return self:_modify({name = name or json.null}) +end + +--[=[ +@m setIcon +@t http +@p icon Base64-Resolvable +@r boolean +@d Sets the channel's icon. To remove the icon, pass `nil`. +]=] +function GroupChannel:setIcon(icon) + icon = icon and Resolver.base64(icon) + return self:_modify({icon = icon or json.null}) +end + +--[=[ +@m addRecipient +@t http +@p id User-ID-Resolvable +@r boolean +@d Adds a user to the channel. +]=] +function GroupChannel:addRecipient(id) + id = Resolver.userId(id) + local data, err = self.client._api:groupDMAddRecipient(self._id, id) + if data then + return true + else + return false, err + end +end + +--[=[ +@m removeRecipient +@t http +@p id User-ID-Resolvable +@r boolean +@d Removes a user from the channel. +]=] +function GroupChannel:removeRecipient(id) + id = Resolver.userId(id) + local data, err = self.client._api:groupDMRemoveRecipient(self._id, id) + if data then + return true + else + return false, err + end +end + +--[=[ +@m leave +@t http +@r boolean +@d Removes the client's user from the channel. If no users remain, the channel +is destroyed. +]=] +function GroupChannel:leave() + return self:_delete() +end + +--[=[@p recipients SecondaryCache A secondary cache of users that are present in the channel.]=] +function get.recipients(self) + return self._recipients +end + +--[=[@p name string The name of the channel.]=] +function get.name(self) + return self._name +end + +--[=[@p ownerId string The Snowflake ID of the user that owns (created) the channel.]=] +function get.ownerId(self) + return self._owner_id +end + +--[=[@p owner User/nil Equivalent to `GroupChannel.recipients:get(GroupChannel.ownerId)`.]=] +function get.owner(self) + return self._recipients:get(self._owner_id) +end + +--[=[@p icon string/nil The hash for the channel's custom icon, if one is set.]=] +function get.icon(self) + return self._icon +end + +--[=[@p iconURL string/nil The URL that can be used to view the channel's icon, if one is set.]=] +function get.iconURL(self) + local icon = self._icon + return icon and format('https://cdn.discordapp.com/channel-icons/%s/%s.png', self._id, icon) +end + +return GroupChannel diff --git a/deps/discordia/libs/containers/Guild.lua b/deps/discordia/libs/containers/Guild.lua new file mode 100644 index 0000000..51603f4 --- /dev/null +++ b/deps/discordia/libs/containers/Guild.lua @@ -0,0 +1,921 @@ +--[=[ +@c Guild x Snowflake +@d Represents a Discord guild (or server). Guilds are a collection of members, +channels, and roles that represents one community. +]=] + +local Cache = require('iterables/Cache') +local Role = require('containers/Role') +local Emoji = require('containers/Emoji') +local Invite = require('containers/Invite') +local Webhook = require('containers/Webhook') +local Ban = require('containers/Ban') +local Member = require('containers/Member') +local Resolver = require('client/Resolver') +local AuditLogEntry = require('containers/AuditLogEntry') +local GuildTextChannel = require('containers/GuildTextChannel') +local GuildVoiceChannel = require('containers/GuildVoiceChannel') +local GuildCategoryChannel = require('containers/GuildCategoryChannel') +local Snowflake = require('containers/abstract/Snowflake') + +local json = require('json') +local enums = require('enums') + +local channelType = enums.channelType +local floor = math.floor +local format = string.format + +local Guild, get = require('class')('Guild', Snowflake) + +function Guild:__init(data, parent) + Snowflake.__init(self, data, parent) + self._roles = Cache({}, Role, self) + self._emojis = Cache({}, Emoji, self) + self._members = Cache({}, Member, self) + self._text_channels = Cache({}, GuildTextChannel, self) + self._voice_channels = Cache({}, GuildVoiceChannel, self) + self._categories = Cache({}, GuildCategoryChannel, self) + self._voice_states = {} + if not data.unavailable then + return self:_makeAvailable(data) + end +end + +function Guild:_load(data) + Snowflake._load(self, data) + return self:_loadMore(data) +end + +function Guild:_loadMore(data) + if data.features then + self._features = data.features + end +end + +function Guild:_makeAvailable(data) + + self._roles:_load(data.roles) + self._emojis:_load(data.emojis) + self:_loadMore(data) + + if not data.channels then return end -- incomplete guild + + local states = self._voice_states + for _, state in ipairs(data.voice_states) do + states[state.user_id] = state + end + + local text_channels = self._text_channels + local voice_channels = self._voice_channels + local categories = self._categories + + for _, channel in ipairs(data.channels) do + local t = channel.type + if t == channelType.text or t == channelType.news then + text_channels:_insert(channel) + elseif t == channelType.voice then + voice_channels:_insert(channel) + elseif t == channelType.category then + categories:_insert(channel) + end + end + + return self:_loadMembers(data) + +end + +function Guild:_loadMembers(data) + local members = self._members + members:_load(data.members) + for _, presence in ipairs(data.presences) do + local member = members:get(presence.user.id) + if member then -- rogue presence check + member:_loadPresence(presence) + end + end + if self._large and self.client._options.cacheAllMembers then + return self:requestMembers() + end +end + +function Guild:_modify(payload) + local data, err = self.client._api:modifyGuild(self._id, payload) + if data then + self:_load(data) + return true + else + return false, err + end +end + +--[=[ +@m requestMembers +@t ws +@r boolean +@d Asynchronously loads all members for this guild. You do not need to call this +if the `cacheAllMembers` client option (and the `syncGuilds` option for +user-accounts) is enabled on start-up. +]=] +function Guild:requestMembers() + local shard = self.client._shards[self.shardId] + if not shard then + return false, 'Invalid shard' + end + if shard._loading then + shard._loading.chunks[self._id] = true + end + return shard:requestGuildMembers(self._id) +end + +--[=[ +@m sync +@t ws +@r boolean +@d Asynchronously loads certain data and enables the receiving of certain events +for this guild. You do not need to call this if the `syncGuilds` client option +is enabled on start-up. + +Note: This is only for user accounts. Bot accounts never need to sync guilds! +]=] +function Guild:sync() + local shard = self.client._shards[self.shardId] + if not shard then + return false, 'Invalid shard' + end + if shard._loading then + shard._loading.syncs[self._id] = true + end + return shard:syncGuilds({self._id}) +end + +--[=[ +@m getMember +@t http? +@p id User-ID-Resolvable +@r Member +@d Gets a member object by ID. If the object is already cached, then the cached +object will be returned; otherwise, an HTTP request is made. +]=] +function Guild:getMember(id) + id = Resolver.userId(id) + local member = self._members:get(id) + if member then + return member + else + local data, err = self.client._api:getGuildMember(self._id, id) + if data then + return self._members:_insert(data) + else + return nil, err + end + end +end + +--[=[ +@m getRole +@t mem +@p id Role-ID-Resolvable +@r Role +@d Gets a role object by ID. +]=] +function Guild:getRole(id) + id = Resolver.roleId(id) + return self._roles:get(id) +end + +--[=[ +@m getEmoji +@t mem +@p id Emoji-ID-Resolvable +@r Emoji +@d Gets a emoji object by ID. +]=] +function Guild:getEmoji(id) + id = Resolver.emojiId(id) + return self._emojis:get(id) +end + +--[=[ +@m getChannel +@t mem +@p id Channel-ID-Resolvable +@r GuildChannel +@d Gets a text, voice, or category channel object by ID. +]=] +function Guild:getChannel(id) + id = Resolver.channelId(id) + return self._text_channels:get(id) or self._voice_channels:get(id) or self._categories:get(id) +end + +--[=[ +@m createTextChannel +@t http +@p name string +@r GuildTextChannel +@d Creates a new text channel in this guild. The name must be between 2 and 100 +characters in length. +]=] +function Guild:createTextChannel(name) + local data, err = self.client._api:createGuildChannel(self._id, {name = name, type = channelType.text}) + if data then + return self._text_channels:_insert(data) + else + return nil, err + end +end + +--[=[ +@m createVoiceChannel +@t http +@p name string +@r GuildVoiceChannel +@d Creates a new voice channel in this guild. The name must be between 2 and 100 +characters in length. +]=] +function Guild:createVoiceChannel(name) + local data, err = self.client._api:createGuildChannel(self._id, {name = name, type = channelType.voice}) + if data then + return self._voice_channels:_insert(data) + else + return nil, err + end +end + +--[=[ +@m createCategory +@t http +@p name string +@r GuildCategoryChannel +@d Creates a channel category in this guild. The name must be between 2 and 100 +characters in length. +]=] +function Guild:createCategory(name) + local data, err = self.client._api:createGuildChannel(self._id, {name = name, type = channelType.category}) + if data then + return self._categories:_insert(data) + else + return nil, err + end +end + +--[=[ +@m createRole +@t http +@p name string +@r Role +@d Creates a new role in this guild. The name must be between 1 and 100 characters +in length. +]=] +function Guild:createRole(name) + local data, err = self.client._api:createGuildRole(self._id, {name = name}) + if data then + return self._roles:_insert(data) + else + return nil, err + end +end + +--[=[ +@m createEmoji +@t http +@p name string +@p image Base64-Resolvable +@r Emoji +@d Creates a new emoji in this guild. The name must be between 2 and 32 characters +in length. The image must not be over 256kb, any higher will return a 400 Bad Request +]=] +function Guild:createEmoji(name, image) + image = Resolver.base64(image) + local data, err = self.client._api:createGuildEmoji(self._id, {name = name, image = image}) + if data then + return self._emojis:_insert(data) + else + return nil, err + end +end + +--[=[ +@m setName +@t http +@p name string +@r boolean +@d Sets the guilds name. This must be between 2 and 100 characters in length. +]=] +function Guild:setName(name) + return self:_modify({name = name or json.null}) +end + +--[=[ +@m setRegion +@t http +@p region string +@r boolean +@d Sets the guild's voice region (eg: `us-east`). See `listVoiceRegions` for a list +of acceptable regions. +]=] +function Guild:setRegion(region) + return self:_modify({region = region or json.null}) +end + +--[=[ +@m setVerificationLevel +@t http +@p verification_level number +@r boolean +@d Sets the guild's verification level setting. See the `verificationLevel` +enumeration for acceptable values. +]=] +function Guild:setVerificationLevel(verification_level) + return self:_modify({verification_level = verification_level or json.null}) +end + +--[=[ +@m setNotificationSetting +@t http +@p default_message_notifications number +@r boolean +@d Sets the guild's default notification setting. See the `notficationSetting` +enumeration for acceptable values. +]=] +function Guild:setNotificationSetting(default_message_notifications) + return self:_modify({default_message_notifications = default_message_notifications or json.null}) +end + +--[=[ +@m setExplicitContentSetting +@t http +@p explicit_content_filter number +@r boolean +@d Sets the guild's explicit content level setting. See the `explicitContentLevel` +enumeration for acceptable values. +]=] +function Guild:setExplicitContentSetting(explicit_content_filter) + return self:_modify({explicit_content_filter = explicit_content_filter or json.null}) +end + +--[=[ +@m setAFKTimeout +@t http +@p afk_timeout number +@r number +@d Sets the guild's AFK timeout in seconds. +]=] +function Guild:setAFKTimeout(afk_timeout) + return self:_modify({afk_timeout = afk_timeout or json.null}) +end + +--[=[ +@m setAFKChannel +@t http +@p id Channel-ID-Resolvable +@r boolean +@d Sets the guild's AFK channel. +]=] +function Guild:setAFKChannel(id) + id = id and Resolver.channelId(id) + return self:_modify({afk_channel_id = id or json.null}) +end + +--[=[ +@m setSystemChannel +@t http +@p id Channel-Id-Resolvable +@r boolean +@d Sets the guild's join message channel. +]=] +function Guild:setSystemChannel(id) + id = id and Resolver.channelId(id) + return self:_modify({system_channel_id = id or json.null}) +end + +--[=[ +@m setOwner +@t http +@p id User-ID-Resolvable +@r boolean +@d Transfers ownership of the guild to another user. Only the current guild owner +can do this. +]=] +function Guild:setOwner(id) + id = id and Resolver.userId(id) + return self:_modify({owner_id = id or json.null}) +end + +--[=[ +@m setIcon +@t http +@p icon Base64-Resolvable +@r boolean +@d Sets the guild's icon. To remove the icon, pass `nil`. +]=] +function Guild:setIcon(icon) + icon = icon and Resolver.base64(icon) + return self:_modify({icon = icon or json.null}) +end + +--[=[ +@m setBanner +@t http +@p banner Base64-Resolvable +@r boolean +@d Sets the guild's banner. To remove the banner, pass `nil`. +]=] +function Guild:setBanner(banner) + banner = banner and Resolver.base64(banner) + return self:_modify({banner = banner or json.null}) +end + +--[=[ +@m setSplash +@t http +@p splash Base64-Resolvable +@r boolean +@d Sets the guild's splash. To remove the splash, pass `nil`. +]=] +function Guild:setSplash(splash) + splash = splash and Resolver.base64(splash) + return self:_modify({splash = splash or json.null}) +end + +--[=[ +@m getPruneCount +@t http +@op days number +@r number +@d Returns the number of members that would be pruned from the guild if a prune +were to be executed. +]=] +function Guild:getPruneCount(days) + local data, err = self.client._api:getGuildPruneCount(self._id, days and {days = days} or nil) + if data then + return data.pruned + else + return nil, err + end +end + +--[=[ +@m pruneMembers +@t http +@op days number +@op count boolean +@r number +@d Prunes (removes) inactive, roleless members from the guild who have not been online in the last provided days. +If the `count` boolean is provided, the number of pruned members is returned; otherwise, `0` is returned. +]=] +function Guild:pruneMembers(days, count) + local t1 = type(days) + if t1 == 'number' then + count = type(count) == 'boolean' and count + elseif t1 == 'boolean' then + count = days + days = nil + end + local data, err = self.client._api:beginGuildPrune(self._id, nil, { + days = days, + compute_prune_count = count, + }) + if data then + return data.pruned + else + return nil, err + end +end + +--[=[ +@m getBans +@t http +@r Cache +@d Returns a newly constructed cache of all ban objects for the guild. The +cache and its objects are not automatically updated via gateway events. You must +call this method again to get the updated objects. +]=] +function Guild:getBans() + local data, err = self.client._api:getGuildBans(self._id) + if data then + return Cache(data, Ban, self) + else + return nil, err + end +end + +--[=[ +@m getBan +@t http +@p id User-ID-Resolvable +@r Ban +@d This will return a Ban object for a giver user if that user is banned +from the guild; otherwise, `nil` is returned. +]=] +function Guild:getBan(id) + id = Resolver.userId(id) + local data, err = self.client._api:getGuildBan(self._id, id) + if data then + return Ban(data, self) + else + return nil, err + end +end + +--[=[ +@m getInvites +@t http +@r Cache +@d Returns a newly constructed cache of all invite objects for the guild. The +cache and its objects are not automatically updated via gateway events. You must +call this method again to get the updated objects. +]=] +function Guild:getInvites() + local data, err = self.client._api:getGuildInvites(self._id) + if data then + return Cache(data, Invite, self.client) + else + return nil, err + end +end + +--[=[ +@m getAuditLogs +@t http +@op query table +@r Cache +@d Returns a newly constructed cache of audit log entry objects for the guild. The +cache and its objects are not automatically updated via gateway events. You must +call this method again to get the updated objects. + +If included, the query parameters include: query.limit: number, query.user: UserId Resolvable +query.before: EntryId Resolvable, query.type: ActionType Resolvable +]=] +function Guild:getAuditLogs(query) + if type(query) == 'table' then + query = { + limit = query.limit, + user_id = Resolver.userId(query.user), + before = Resolver.entryId(query.before), + action_type = Resolver.actionType(query.type), + } + end + local data, err = self.client._api:getGuildAuditLog(self._id, query) + if data then + self.client._users:_load(data.users) + self.client._webhooks:_load(data.webhooks) + return Cache(data.audit_log_entries, AuditLogEntry, self) + else + return nil, err + end +end + +--[=[ +@m getWebhooks +@t http +@r Cache +@d Returns a newly constructed cache of all webhook objects for the guild. The +cache and its objects are not automatically updated via gateway events. You must +call this method again to get the updated objects. +]=] +function Guild:getWebhooks() + local data, err = self.client._api:getGuildWebhooks(self._id) + if data then + return Cache(data, Webhook, self.client) + else + return nil, err + end +end + +--[=[ +@m listVoiceRegions +@t http +@r table +@d Returns a raw data table that contains a list of available voice regions for +this guild, as provided by Discord, with no additional parsing. +]=] +function Guild:listVoiceRegions() + return self.client._api:getGuildVoiceRegions(self._id) +end + +--[=[ +@m leave +@t http +@r boolean +@d Removes the current user from the guild. +]=] +function Guild:leave() + local data, err = self.client._api:leaveGuild(self._id) + if data then + return true + else + return false, err + end +end + +--[=[ +@m delete +@t http +@r boolean +@d Permanently deletes the guild. The current user must owner the server. This cannot be undone! +]=] +function Guild:delete() + local data, err = self.client._api:deleteGuild(self._id) + if data then + local cache = self._parent._guilds + if cache then + cache:_delete(self._id) + end + return true + else + return false, err + end +end + +--[=[ +@m kickUser +@t http +@p id User-ID-Resolvable +@op reason string +@r boolean +@d Kicks a user/member from the guild with an optional reason. +]=] +function Guild:kickUser(id, reason) + id = Resolver.userId(id) + local query = reason and {reason = reason} + local data, err = self.client._api:removeGuildMember(self._id, id, query) + if data then + return true + else + return false, err + end +end + +--[=[ +@m banUser +@t http +@p id User-ID-Resolvable +@op reason string +@op days number +@r boolean +@d Bans a user/member from the guild with an optional reason. The `days` parameter +is the number of days to consider when purging messages, up to 7. +]=] +function Guild:banUser(id, reason, days) + local query = reason and {reason = reason} + if days then + query = query or {} + query['delete-message-days'] = days + end + id = Resolver.userId(id) + local data, err = self.client._api:createGuildBan(self._id, id, query) + if data then + return true + else + return false, err + end +end + +--[=[ +@m unbanUser +@t http +@p id User-ID-Resolvable +@op reason string +@r boolean +@d Unbans a user/member from the guild with an optional reason. +]=] +function Guild:unbanUser(id, reason) + id = Resolver.userId(id) + local query = reason and {reason = reason} + local data, err = self.client._api:removeGuildBan(self._id, id, query) + if data then + return true + else + return false, err + end +end + +--[=[@p shardId number The ID of the shard on which this guild is served. If only one shard is in +operation, then this will always be 0.]=] +function get.shardId(self) + return floor(self._id / 2^22) % self.client._total_shard_count +end + +--[=[@p name string The guild's name. This should be between 2 and 100 characters in length.]=] +function get.name(self) + return self._name +end + +--[=[@p icon string/nil The hash for the guild's custom icon, if one is set.]=] +function get.icon(self) + return self._icon +end + +--[=[@p iconURL string/nil The URL that can be used to view the guild's icon, if one is set.]=] +function get.iconURL(self) + local icon = self._icon + return icon and format('https://cdn.discordapp.com/icons/%s/%s.png', self._id, icon) +end + +--[=[@p splash string/nil The hash for the guild's custom splash image, if one is set. Only partnered +guilds may have this.]=] +function get.splash(self) + return self._splash +end + +--[=[@p splashURL string/nil The URL that can be used to view the guild's custom splash image, if one is set. +Only partnered guilds may have this.]=] +function get.splashURL(self) + local splash = self._splash + return splash and format('https://cdn.discordapp.com/splashes/%s/%s.png', self._id, splash) +end + +--[=[@p banner string/nil The hash for the guild's custom banner, if one is set.]=] +function get.banner(self) + return self._banner +end + +--[=[@p bannerURL string/nil The URL that can be used to view the guild's banner, if one is set.]=] +function get.bannerURL(self) + local banner = self._banner + return banner and format('https://cdn.discordapp.com/banners/%s/%s.png', self._id, banner) +end + +--[=[@p large boolean Whether the guild has an arbitrarily large amount of members. Guilds that are +"large" will not initialize with all members cached.]=] +function get.large(self) + return self._large +end + +--[=[@p lazy boolean Whether the guild follows rules for the lazy-loading of client data.]=] +function get.lazy(self) + return self._lazy +end + +--[=[@p region string The voice region that is used for all voice connections in the guild.]=] +function get.region(self) + return self._region +end + +--[=[@p vanityCode string/nil The guild's vanity invite URL code, if one exists.]=] +function get.vanityCode(self) + return self._vanity_url_code +end + +--[=[@p description string/nil The guild's custom description, if one exists.]=] +function get.description(self) + return self._description +end + +--[=[@p maxMembers number/nil The guild's maximum member count, if available.]=] +function get.maxMembers(self) + return self._max_members +end + +--[=[@p maxPresences number/nil The guild's maximum presence count, if available.]=] +function get.maxPresences(self) + return self._max_presences +end + +--[=[@p mfaLevel number The guild's multi-factor (or two-factor) verification level setting. A value of +0 indicates that MFA is not required; a value of 1 indicates that MFA is +required for administrative actions.]=] +function get.mfaLevel(self) + return self._mfa_level +end + +--[=[@p joinedAt string The date and time at which the current user joined the guild, represented as +an ISO 8601 string plus microseconds when available.]=] +function get.joinedAt(self) + return self._joined_at +end + +--[=[@p afkTimeout number The guild's voice AFK timeout in seconds.]=] +function get.afkTimeout(self) + return self._afk_timeout +end + +--[=[@p unavailable boolean Whether the guild is unavailable. If the guild is unavailable, then no property +is guaranteed to exist except for this one and the guild's ID.]=] +function get.unavailable(self) + return self._unavailable or false +end + +--[=[@p totalMemberCount number The total number of members that belong to this guild. This should always be +greater than or equal to the total number of cached members.]=] +function get.totalMemberCount(self) + return self._member_count +end + +--[=[@p verificationLevel number The guild's verification level setting. See the `verificationLevel` +enumeration for a human-readable representation.]=] +function get.verificationLevel(self) + return self._verification_level +end + +--[=[@p notificationSetting number The guild's default notification setting. See the `notficationSetting` +enumeration for a human-readable representation.]=] +function get.notificationSetting(self) + return self._default_message_notifications +end + +--[=[@p explicitContentSetting number The guild's explicit content level setting. See the `explicitContentLevel` +enumeration for a human-readable representation.]=] +function get.explicitContentSetting(self) + return self._explicit_content_filter +end + +--[=[@p premiumTier number The guild's premier tier affected by nitro server +boosts. See the `premiumTier` enumeration for a human-readable representation]=] +function get.premiumTier(self) + return self._premium_tier +end + +--[=[@p premiumSubscriptionCount number The number of users that have upgraded +the guild with nitro server boosting.]=] +function get.premiumSubscriptionCount(self) + return self._premium_subscription_count +end + +--[=[@p features table Raw table of VIP features that are enabled for the guild.]=] +function get.features(self) + return self._features +end + +--[=[@p me Member/nil Equivalent to `Guild.members:get(Guild.client.user.id)`.]=] +function get.me(self) + return self._members:get(self.client._user._id) +end + +--[=[@p owner Member/nil Equivalent to `Guild.members:get(Guild.ownerId)`.]=] +function get.owner(self) + return self._members:get(self._owner_id) +end + +--[=[@p ownerId string The Snowflake ID of the guild member that owns the guild.]=] +function get.ownerId(self) + return self._owner_id +end + +--[=[@p afkChannelId string/nil The Snowflake ID of the channel that is used for AFK members, if one is set.]=] +function get.afkChannelId(self) + return self._afk_channel_id +end + +--[=[@p afkChannel GuildVoiceChannel/nil Equivalent to `Guild.voiceChannels:get(Guild.afkChannelId)`.]=] +function get.afkChannel(self) + return self._voice_channels:get(self._afk_channel_id) +end + +--[=[@p systemChannelId string/nil The channel id where Discord's join messages will be displayed.]=] +function get.systemChannelId(self) + return self._system_channel_id +end + +--[=[@p systemChannel GuildTextChannel/nil The channel where Discord's join messages will be displayed.]=] +function get.systemChannel(self) + return self._text_channels:get(self._system_channel_id) +end + +--[=[@p defaultRole Role Equivalent to `Guild.roles:get(Guild.id)`.]=] +function get.defaultRole(self) + return self._roles:get(self._id) +end + +--[=[@p connection VoiceConnection/nil The VoiceConnection for this guild if one exists.]=] +function get.connection(self) + return self._connection +end + +--[=[@p roles Cache An iterable cache of all roles that exist in this guild. This includes the +default everyone role.]=] +function get.roles(self) + return self._roles +end + +--[=[@p emojis Cache An iterable cache of all emojis that exist in this guild. Note that standard +unicode emojis are not found here; only custom emojis.]=] +function get.emojis(self) + return self._emojis +end + +--[=[@p members Cache An iterable cache of all members that exist in this guild and have been +already loaded. If the `cacheAllMembers` client option (and the `syncGuilds` +option for user-accounts) is enabled on start-up, then all members will be +cached. Otherwise, offline members may not be cached. To access a member that +may exist, but is not cached, use `Guild:getMember`.]=] +function get.members(self) + return self._members +end + +--[=[@p textChannels Cache An iterable cache of all text channels that exist in this guild.]=] +function get.textChannels(self) + return self._text_channels +end + +--[=[@p voiceChannels Cache An iterable cache of all voice channels that exist in this guild.]=] +function get.voiceChannels(self) + return self._voice_channels +end + +--[=[@p categories Cache An iterable cache of all channel categories that exist in this guild.]=] +function get.categories(self) + return self._categories +end + +return Guild diff --git a/deps/discordia/libs/containers/GuildCategoryChannel.lua b/deps/discordia/libs/containers/GuildCategoryChannel.lua new file mode 100644 index 0000000..8dedd10 --- /dev/null +++ b/deps/discordia/libs/containers/GuildCategoryChannel.lua @@ -0,0 +1,83 @@ +--[=[ +@c GuildCategoryChannel x GuildChannel +@d Represents a channel category in a Discord guild, used to organize individual +text or voice channels in that guild. +]=] + +local GuildChannel = require('containers/abstract/GuildChannel') +local FilteredIterable = require('iterables/FilteredIterable') +local enums = require('enums') + +local channelType = enums.channelType + +local GuildCategoryChannel, get = require('class')('GuildCategoryChannel', GuildChannel) + +function GuildCategoryChannel:__init(data, parent) + GuildChannel.__init(self, data, parent) +end + +--[=[ +@m createTextChannel +@t http +@p name string +@r GuildTextChannel +@d Creates a new GuildTextChannel with this category as it's parent. Similar to `Guild:createTextChannel(name)` +]=] +function GuildCategoryChannel:createTextChannel(name) + local guild = self._parent + local data, err = guild.client._api:createGuildChannel(guild._id, { + name = name, + type = channelType.text, + parent_id = self._id + }) + if data then + return guild._text_channels:_insert(data) + else + return nil, err + end +end + +--[=[ +@m createVoiceChannel +@t http +@p name string +@r GuildVoiceChannel +@d Creates a new GuildVoiceChannel with this category as it's parent. Similar to `Guild:createVoiceChannel(name)` +]=] +function GuildCategoryChannel:createVoiceChannel(name) + local guild = self._parent + local data, err = guild.client._api:createGuildChannel(guild._id, { + name = name, + type = channelType.voice, + parent_id = self._id + }) + if data then + return guild._voice_channels:_insert(data) + else + return nil, err + end +end + +--[=[@p textChannels FilteredIterable Iterable of all textChannels in the Category.]=] +function get.textChannels(self) + if not self._text_channels then + local id = self._id + self._text_channels = FilteredIterable(self._parent._text_channels, function(c) + return c._parent_id == id + end) + end + return self._text_channels +end + +--[=[@p voiceChannels FilteredIterable Iterable of all voiceChannels in the Category.]=] +function get.voiceChannels(self) + if not self._voice_channels then + local id = self._id + self._voice_channels = FilteredIterable(self._parent._voice_channels, function(c) + return c._parent_id == id + end) + end + return self._voice_channels +end + +return GuildCategoryChannel diff --git a/deps/discordia/libs/containers/GuildTextChannel.lua b/deps/discordia/libs/containers/GuildTextChannel.lua new file mode 100644 index 0000000..388f57b --- /dev/null +++ b/deps/discordia/libs/containers/GuildTextChannel.lua @@ -0,0 +1,165 @@ +--[=[ +@c GuildTextChannel x GuildChannel x TextChannel +@d Represents a text channel in a Discord guild, where guild members and webhooks +can send and receive messages. +]=] + +local json = require('json') + +local GuildChannel = require('containers/abstract/GuildChannel') +local TextChannel = require('containers/abstract/TextChannel') +local FilteredIterable = require('iterables/FilteredIterable') +local Webhook = require('containers/Webhook') +local Cache = require('iterables/Cache') +local Resolver = require('client/Resolver') + +local GuildTextChannel, get = require('class')('GuildTextChannel', GuildChannel, TextChannel) + +function GuildTextChannel:__init(data, parent) + GuildChannel.__init(self, data, parent) + TextChannel.__init(self, data, parent) +end + +function GuildTextChannel:_load(data) + GuildChannel._load(self, data) + TextChannel._load(self, data) +end + +--[=[ +@m createWebhook +@t http +@p name string +@r Webhook +@d Creates a webhook for this channel. The name must be between 2 and 32 characters +in length. +]=] +function GuildTextChannel:createWebhook(name) + local data, err = self.client._api:createWebhook(self._id, {name = name}) + if data then + return Webhook(data, self.client) + else + return nil, err + end +end + +--[=[ +@m getWebhooks +@t http +@r Cache +@d Returns a newly constructed cache of all webhook objects for the channel. The +cache and its objects are not automatically updated via gateway events. You must +call this method again to get the updated objects. +]=] +function GuildTextChannel:getWebhooks() + local data, err = self.client._api:getChannelWebhooks(self._id) + if data then + return Cache(data, Webhook, self.client) + else + return nil, err + end +end + +--[=[ +@m bulkDelete +@t http +@p messages Message-ID-Resolvables +@r boolean +@d Bulk deletes multiple messages, from 2 to 100, from the channel. Messages over +2 weeks old cannot be deleted and will return an error. +]=] +function GuildTextChannel:bulkDelete(messages) + messages = Resolver.messageIds(messages) + local data, err + if #messages == 1 then + data, err = self.client._api:deleteMessage(self._id, messages[1]) + else + data, err = self.client._api:bulkDeleteMessages(self._id, {messages = messages}) + end + if data then + return true + else + return false, err + end +end + +--[=[ +@m setTopic +@t http +@p topic string +@r boolean +@d Sets the channel's topic. This must be between 1 and 1024 characters. Pass `nil` +to remove the topic. +]=] +function GuildTextChannel:setTopic(topic) + return self:_modify({topic = topic or json.null}) +end + +--[=[ +@m setRateLimit +@t http +@p limit number +@r boolean +@d Sets the channel's slowmode rate limit in seconds. This must be between 0 and 120. +Passing 0 or `nil` will clear the limit. +]=] +function GuildTextChannel:setRateLimit(limit) + return self:_modify({rate_limit_per_user = limit or json.null}) +end + +--[=[ +@m enableNSFW +@t http +@r boolean +@d Enables the NSFW setting for the channel. NSFW channels are hidden from users +until the user explicitly requests to view them. +]=] +function GuildTextChannel:enableNSFW() + return self:_modify({nsfw = true}) +end + +--[=[ +@m disableNSFW +@t http +@r boolean +@d Disables the NSFW setting for the channel. NSFW channels are hidden from users +until the user explicitly requests to view them. +]=] +function GuildTextChannel:disableNSFW() + return self:_modify({nsfw = false}) +end + +--[=[@p topic string/nil The channel's topic. This should be between 1 and 1024 characters.]=] +function get.topic(self) + return self._topic +end + +--[=[@p nsfw boolean Whether this channel is marked as NSFW (not safe for work).]=] +function get.nsfw(self) + return self._nsfw or false +end + +--[=[@p rateLimit number Slowmode rate limit per guild member.]=] +function get.rateLimit(self) + return self._rate_limit_per_user or 0 +end + +--[=[@p isNews boolean Whether this channel is a news channel of type 5.]=] +function get.isNews(self) + return self._type == 5 +end + +--[=[@p members FilteredIterable A filtered iterable of guild members that have +permission to read this channel. If you want to check whether a specific member +has permission to read this channel, it would be better to get the member object +elsewhere and use `Member:hasPermission` rather than check whether the member +exists here.]=] +function get.members(self) + if not self._members then + self._members = FilteredIterable(self._parent._members, function(m) + return m:hasPermission(self, 'readMessages') + end) + end + return self._members +end + +return GuildTextChannel diff --git a/deps/discordia/libs/containers/GuildVoiceChannel.lua b/deps/discordia/libs/containers/GuildVoiceChannel.lua new file mode 100644 index 0000000..d7ce63c --- /dev/null +++ b/deps/discordia/libs/containers/GuildVoiceChannel.lua @@ -0,0 +1,138 @@ +--[=[ +@c GuildVoiceChannel x GuildChannel +@d Represents a voice channel in a Discord guild, where guild members can connect +and communicate via voice chat. +]=] + +local json = require('json') + +local GuildChannel = require('containers/abstract/GuildChannel') +local VoiceConnection = require('voice/VoiceConnection') +local TableIterable = require('iterables/TableIterable') + +local GuildVoiceChannel, get = require('class')('GuildVoiceChannel', GuildChannel) + +function GuildVoiceChannel:__init(data, parent) + GuildChannel.__init(self, data, parent) +end + +--[=[ +@m setBitrate +@t http +@p bitrate number +@r boolean +@d Sets the channel's audio bitrate in bits per second (bps). This must be between +8000 and 96000 (or 128000 for partnered servers). If `nil` is passed, the +default is set, which is 64000. +]=] +function GuildVoiceChannel:setBitrate(bitrate) + return self:_modify({bitrate = bitrate or json.null}) +end + +--[=[ +@m setUserLimit +@t http +@p user_limit number +@r boolean +@d Sets the channel's user limit. This must be between 0 and 99 (where 0 is +unlimited). If `nil` is passed, the default is set, which is 0. +]=] +function GuildVoiceChannel:setUserLimit(user_limit) + return self:_modify({user_limit = user_limit or json.null}) +end + +--[=[ +@m join +@t ws +@r VoiceConnection +@d Join this channel and form a connection to the Voice Gateway. +]=] +function GuildVoiceChannel:join() + + local success, err + + local connection = self._connection + + if connection then + + if connection._ready then + return connection + end + + else + + local guild = self._parent + local client = guild._parent + + success, err = client._shards[guild.shardId]:updateVoice(guild._id, self._id) + + if not success then + return nil, err + end + + connection = guild._connection + + if not connection then + connection = VoiceConnection(self) + guild._connection = connection + end + + self._connection = connection + + end + + success, err = connection:_await() + + if success then + return connection + else + return nil, err + end + +end + +--[=[ +@m leave +@t http +@r boolean +@d Leave this channel if there is an existing voice connection to it. +Equivalent to GuildVoiceChannel.connection:close() +]=] +function GuildVoiceChannel:leave() + if self._connection then + return self._connection:close() + else + return false, 'No voice connection exists for this channel' + end +end + +--[=[@p bitrate number The channel's bitrate in bits per second (bps). This should be between 8000 and +96000 (or 128000 for partnered servers).]=] +function get.bitrate(self) + return self._bitrate +end + +--[=[@p userLimit number The amount of users allowed to be in this channel. +Users with `moveMembers` permission ignore this limit.]=] +function get.userLimit(self) + return self._user_limit +end + +--[=[@p connectedMembers TableIterable An iterable of all users connected to the channel.]=] +function get.connectedMembers(self) + if not self._connected_members then + local id = self._id + local members = self._parent._members + self._connected_members = TableIterable(self._parent._voice_states, function(state) + return state.channel_id == id and members:get(state.user_id) + end) + end + return self._connected_members +end + +--[=[@p connection VoiceConnection/nil The VoiceConnection for this channel if one exists.]=] +function get.connection(self) + return self._connection +end + +return GuildVoiceChannel diff --git a/deps/discordia/libs/containers/Invite.lua b/deps/discordia/libs/containers/Invite.lua new file mode 100644 index 0000000..6479892 --- /dev/null +++ b/deps/discordia/libs/containers/Invite.lua @@ -0,0 +1,187 @@ +--[=[ +@c Invite x Container +@d Represents an invitation to a Discord guild channel. Invites can be used to join +a guild, though they are not always permanent. +]=] + +local Container = require('containers/abstract/Container') +local json = require('json') + +local format = string.format +local null = json.null + +local function load(v) + return v ~= null and v or nil +end + +local Invite, get = require('class')('Invite', Container) + +function Invite:__init(data, parent) + Container.__init(self, data, parent) + self._guild_id = load(data.guild.id) + self._channel_id = load(data.channel.id) + self._guild_name = load(data.guild.name) + self._guild_icon = load(data.guild.icon) + self._guild_splash = load(data.guild.splash) + self._guild_banner = load(data.guild.banner) + self._guild_description = load(data.guild.description) + self._guild_verification_level = load(data.guild.verification_level) + self._channel_name = load(data.channel.name) + self._channel_type = load(data.channel.type) + if data.inviter then + self._inviter = self.client._users:_insert(data.inviter) + end +end + +--[=[ +@m __hash +@r string +@d Returns `Invite.code` +]=] +function Invite:__hash() + return self._code +end + +--[=[ +@m delete +@t http +@r boolean +@d Permanently deletes the invite. This cannot be undone! +]=] +function Invite:delete() + local data, err = self.client._api:deleteInvite(self._code) + if data then + return true + else + return false, err + end +end + +--[=[@p code string The invite's code which can be used to identify the invite.]=] +function get.code(self) + return self._code +end + +--[=[@p guildId string The Snowflake ID of the guild to which this invite belongs.]=] +function get.guildId(self) + return self._guild_id +end + +--[=[@p guildName string The name of the guild to which this invite belongs.]=] +function get.guildName(self) + return self._guild_name +end + +--[=[@p channelId string The Snowflake ID of the channel to which this belongs.]=] +function get.channelId(self) + return self._channel_id +end + +--[=[@p channelName string The name of the channel to which this invite belongs.]=] +function get.channelName(self) + return self._channel_name +end + +--[=[@p channelType number The type of the channel to which this invite belongs. Use the `channelType` +enumeration for a human-readable representation.]=] +function get.channelType(self) + return self._channel_type +end + +--[=[@p guildIcon string/nil The hash for the guild's custom icon, if one is set.]=] +function get.guildIcon(self) + return self._guild_icon +end + +--[=[@p guildBanner string/nil The hash for the guild's custom banner, if one is set.]=] +function get.guildBanner(self) + return self._guild_banner +end + +--[=[@p guildSplash string/nil The hash for the guild's custom splash, if one is set.]=] +function get.guildSplash(self) + return self._guild_splash +end + +--[=[@p guildIconURL string/nil The URL that can be used to view the guild's icon, if one is set.]=] +function get.guildIconURL(self) + local icon = self._guild_icon + return icon and format('https://cdn.discordapp.com/icons/%s/%s.png', self._guild_id, icon) or nil +end + +--[=[@p guildBannerURL string/nil The URL that can be used to view the guild's banner, if one is set.]=] +function get.guildBannerURL(self) + local banner = self._guild_banner + return banner and format('https://cdn.discordapp.com/banners/%s/%s.png', self._guild_id, banner) or nil +end + +--[=[@p guildSplashURL string/nil The URL that can be used to view the guild's splash, if one is set.]=] +function get.guildSplashURL(self) + local splash = self._guild_splash + return splash and format('https://cdn.discordapp.com/splashs/%s/%s.png', self._guild_id, splash) or nil +end + +--[=[@p guildDescription string/nil The guild's custom description, if one is set.]=] +function get.guildDescription(self) + return self._guild_description +end + +--[=[@p guildVerificationLevel number/nil The guild's verification level, if available.]=] +function get.guildVerificationLevel(self) + return self._guild_verification_level +end + +--[=[@p inviter User/nil The object of the user that created the invite. This will not exist if the +invite is a guild widget or a vanity invite.]=] +function get.inviter(self) + return self._inviter +end + +--[=[@p uses number/nil How many times this invite has been used. This will not exist if the invite is +accessed via `Client:getInvite`.]=] +function get.uses(self) + return self._uses +end + +--[=[@p maxUses number/nil The maximum amount of times this invite can be used. This will not exist if the +invite is accessed via `Client:getInvite`.]=] +function get.maxUses(self) + return self._max_uses +end + +--[=[@p maxAge number/nil How long, in seconds, this invite lasts before it expires. This will not exist +if the invite is accessed via `Client:getInvite`.]=] +function get.maxAge(self) + return self._max_age +end + +--[=[@p temporary boolean/nil Whether the invite grants temporary membership. This will not exist if the +invite is accessed via `Client:getInvite`.]=] +function get.temporary(self) + return self._temporary +end + +--[=[@p createdAt string/nil The date and time at which the invite was created, represented as an ISO 8601 +string plus microseconds when available. This will not exist if the invite is +accessed via `Client:getInvite`.]=] +function get.createdAt(self) + return self._created_at +end + +--[=[@p revoked boolean/nil Whether the invite has been revoked. This will not exist if the invite is +accessed via `Client:getInvite`.]=] +function get.revoked(self) + return self._revoked +end + +--[=[@p approximatePresenceCount number/nil The approximate count of online members.]=] +function get.approximatePresenceCount(self) + return self._approximate_presence_count +end + +--[=[@p approximateMemberCount number/nil The approximate count of all members.]=] +function get.approximateMemberCount(self) + return self._approximate_member_count +end + +return Invite diff --git a/deps/discordia/libs/containers/Member.lua b/deps/discordia/libs/containers/Member.lua new file mode 100644 index 0000000..f3e24f1 --- /dev/null +++ b/deps/discordia/libs/containers/Member.lua @@ -0,0 +1,540 @@ +--[=[ +@c Member x UserPresence +@d Represents a Discord guild member. Though one user may be a member in more than +one guild, each presence is represented by a different member object associated +with that guild. Note that any method or property that exists for the User class is +also available in the Member class. +]=] + +local json = require('json') +local enums = require('enums') +local class = require('class') +local UserPresence = require('containers/abstract/UserPresence') +local ArrayIterable = require('iterables/ArrayIterable') +local Color = require('utils/Color') +local Resolver = require('client/Resolver') +local GuildChannel = require('containers/abstract/GuildChannel') +local Permissions = require('utils/Permissions') + +local insert, remove, sort = table.insert, table.remove, table.sort +local band, bor, bnot = bit.band, bit.bor, bit.bnot +local isInstance = class.isInstance +local permission = enums.permission + +local Member, get = class('Member', UserPresence) + +function Member:__init(data, parent) + UserPresence.__init(self, data, parent) + return self:_loadMore(data) +end + +function Member:_load(data) + UserPresence._load(self, data) + return self:_loadMore(data) +end + +function Member:_loadMore(data) + if data.roles then + local roles = #data.roles > 0 and data.roles or nil + if self._roles then + self._roles._array = roles + else + self._roles_raw = roles + end + end +end + +local function sorter(a, b) + if a._position == b._position then + return tonumber(a._id) < tonumber(b._id) + else + return a._position > b._position + end +end + +local function predicate(role) + return role._color > 0 +end + +--[=[ +@m getColor +@t mem +@r Color +@d Returns a color object that represents the member's color as determined by +its highest colored role. If the member has no colored roles, then the default +color with a value of 0 is returned. +]=] +function Member:getColor() + local roles = {} + for role in self.roles:findAll(predicate) do + insert(roles, role) + end + sort(roles, sorter) + return roles[1] and roles[1]:getColor() or Color() +end + +local function has(a, b) + return band(a, b) > 0 +end + +--[=[ +@m hasPermission +@t mem +@op channel GuildChannel +@p perm Permissions-Resolvable +@r boolean +@d Checks whether the member has a specific permission. If `channel` is omitted, +then only guild-level permissions are checked. This is a relatively expensive +operation. If you need to check multiple permissions at once, use the +`getPermissions` method and check the resulting object. +]=] +function Member:hasPermission(channel, perm) + + if not perm then + perm = channel + channel = nil + end + + local guild = self.guild + if channel then + if not isInstance(channel, GuildChannel) or channel.guild ~= guild then + return error('Invalid GuildChannel: ' .. tostring(channel), 2) + end + end + + local n = Resolver.permission(perm) + if not n then + return error('Invalid permission: ' .. tostring(perm), 2) + end + + if self.id == guild.ownerId then + return true + end + + local rolePermissions = guild.defaultRole.permissions + + for role in self.roles:iter() do + if role.id ~= guild.id then -- just in case + rolePermissions = bor(rolePermissions, role.permissions) + end + end + + if has(rolePermissions, permission.administrator) then + return true + end + + if channel then + + local overwrites = channel.permissionOverwrites + + local overwrite = overwrites:get(self.id) + if overwrite then + if has(overwrite.allowedPermissions, n) then + return true + end + if has(overwrite.deniedPermissions, n) then + return false + end + end + + local allow, deny = 0, 0 + for role in self.roles:iter() do + if role.id ~= guild.id then -- just in case + overwrite = overwrites:get(role.id) + if overwrite then + allow = bor(allow, overwrite.allowedPermissions) + deny = bor(deny, overwrite.deniedPermissions) + end + end + end + + if has(allow, n) then + return true + end + if has(deny, n) then + return false + end + + local everyone = overwrites:get(guild.id) + if everyone then + if has(everyone.allowedPermissions, n) then + return true + end + if has(everyone.deniedPermissions, n) then + return false + end + end + + end + + return has(rolePermissions, n) + +end + +--[=[ +@m getPermissions +@t mem +@op channel GuildChannel +@r Permissions +@d Returns a permissions object that represents the member's total permissions for +the guild, or for a specific channel if one is provided. If you just need to +check one permission, use the `hasPermission` method. +]=] +function Member:getPermissions(channel) + + local guild = self.guild + if channel then + if not isInstance(channel, GuildChannel) or channel.guild ~= guild then + return error('Invalid GuildChannel: ' .. tostring(channel), 2) + end + end + + if self.id == guild.ownerId then + return Permissions.all() + end + + local ret = guild.defaultRole.permissions + + for role in self.roles:iter() do + if role.id ~= guild.id then -- just in case + ret = bor(ret, role.permissions) + end + end + + if band(ret, permission.administrator) > 0 then + return Permissions.all() + end + + if channel then + + local overwrites = channel.permissionOverwrites + + local everyone = overwrites:get(guild.id) + if everyone then + ret = band(ret, bnot(everyone.deniedPermissions)) + ret = bor(ret, everyone.allowedPermissions) + end + + local allow, deny = 0, 0 + for role in self.roles:iter() do + if role.id ~= guild.id then -- just in case + local overwrite = overwrites:get(role.id) + if overwrite then + deny = bor(deny, overwrite.deniedPermissions) + allow = bor(allow, overwrite.allowedPermissions) + end + end + end + ret = band(ret, bnot(deny)) + ret = bor(ret, allow) + + local overwrite = overwrites:get(self.id) + if overwrite then + ret = band(ret, bnot(overwrite.deniedPermissions)) + ret = bor(ret, overwrite.allowedPermissions) + end + + end + + return Permissions(ret) + +end + +--[=[ +@m addRole +@t http? +@p id Role-ID-Resolvable +@r boolean +@d Adds a role to the member. If the member already has the role, then no action is +taken. Note that the everyone role cannot be explicitly added. +]=] +function Member:addRole(id) + if self:hasRole(id) then return true end + id = Resolver.roleId(id) + local data, err = self.client._api:addGuildMemberRole(self._parent._id, self.id, id) + if data then + local roles = self._roles and self._roles._array or self._roles_raw + if roles then + insert(roles, id) + else + self._roles_raw = {id} + end + return true + else + return false, err + end +end + +--[=[ +@m removeRole +@t http? +@p id Role-ID-Resolvable +@r boolean +@d Removes a role from the member. If the member does not have the role, then no +action is taken. Note that the everyone role cannot be removed. +]=] +function Member:removeRole(id) + if not self:hasRole(id) then return true end + id = Resolver.roleId(id) + local data, err = self.client._api:removeGuildMemberRole(self._parent._id, self.id, id) + if data then + local roles = self._roles and self._roles._array or self._roles_raw + if roles then + for i, v in ipairs(roles) do + if v == id then + remove(roles, i) + break + end + end + if #roles == 0 then + if self._roles then + self._roles._array = nil + else + self._roles_raw = nil + end + end + end + return true + else + return false, err + end +end + +--[=[ +@m hasRole +@t mem +@p id Role-ID-Resolvable +@r boolean +@d Checks whether the member has a specific role. This will return true for the +guild's default role in addition to any explicitly assigned roles. +]=] +function Member:hasRole(id) + id = Resolver.roleId(id) + if id == self._parent._id then return true end -- @everyone + local roles = self._roles and self._roles._array or self._roles_raw + if roles then + for _, v in ipairs(roles) do + if v == id then + return true + end + end + end + return false +end + +--[=[ +@m setNickname +@t http +@p nick string +@r boolean +@d Sets the member's nickname. This must be between 1 and 32 characters in length. +Pass `nil` to remove the nickname. +]=] +function Member:setNickname(nick) + nick = nick or '' + local data, err + if self.id == self.client._user._id then + data, err = self.client._api:modifyCurrentUsersNick(self._parent._id, {nick = nick}) + else + data, err = self.client._api:modifyGuildMember(self._parent._id, self.id, {nick = nick}) + end + if data then + self._nick = nick ~= '' and nick or nil + return true + else + return false, err + end +end + +--[=[ +@m setVoiceChannel +@t http +@p id Channel-ID-Resolvable +@r boolean +@d Moves the member to a new voice channel, but only if the member has an active +voice connection in the current guild. Due to complexities in voice state +handling, the member's `voiceChannel` property will update asynchronously via +WebSocket; not as a result of the HTTP request. +]=] +function Member:setVoiceChannel(id) + id = id and Resolver.channelId(id) + local data, err = self.client._api:modifyGuildMember(self._parent._id, self.id, {channel_id = id or json.null}) + if data then + return true + else + return false, err + end +end + +--[=[ +@m mute +@t http +@r boolean +@d Mutes the member in its guild. +]=] +function Member:mute() + local data, err = self.client._api:modifyGuildMember(self._parent._id, self.id, {mute = true}) + if data then + self._mute = true + return true + else + return false, err + end +end + +--[=[ +@m unmute +@t http +@r boolean +@d Unmutes the member in its guild. +]=] +function Member:unmute() + local data, err = self.client._api:modifyGuildMember(self._parent._id, self.id, {mute = false}) + if data then + self._mute = false + return true + else + return false, err + end +end + +--[=[ +@m deafen +@t http +@r boolean +@d Deafens the member in its guild. +]=] +function Member:deafen() + local data, err = self.client._api:modifyGuildMember(self._parent._id, self.id, {deaf = true}) + if data then + self._deaf = true + return true + else + return false, err + end +end + +--[=[ +@m undeafen +@t http +@r boolean +@d Undeafens the member in its guild. +]=] +function Member:undeafen() + local data, err = self.client._api:modifyGuildMember(self._parent._id, self.id, {deaf = false}) + if data then + self._deaf = false + return true + else + return false, err + end +end + +--[=[ +@m kick +@t http +@p reason string +@r boolean +@d Equivalent to `Member.guild:kickUser(Member.user, reason)` +]=] +function Member:kick(reason) + return self._parent:kickUser(self._user, reason) +end + +--[=[ +@m ban +@t http +@p reason string +@p days number +@r boolean +@d Equivalent to `Member.guild:banUser(Member.user, reason, days)` +]=] +function Member:ban(reason, days) + return self._parent:banUser(self._user, reason, days) +end + +--[=[ +@m unban +@t http +@p reason string +@r boolean +@d Equivalent to `Member.guild:unbanUser(Member.user, reason)` +]=] +function Member:unban(reason) + return self._parent:unbanUser(self._user, reason) +end + +--[=[@p roles ArrayIterable An iterable array of guild roles that the member has. This does not explicitly +include the default everyone role. Object order is not guaranteed.]=] +function get.roles(self) + if not self._roles then + local roles = self._parent._roles + self._roles = ArrayIterable(self._roles_raw, function(id) + return roles:get(id) + end) + self._roles_raw = nil + end + return self._roles +end + +--[=[@p name string If the member has a nickname, then this will be equivalent to that nickname. +Otherwise, this is equivalent to `Member.user.username`.]=] +function get.name(self) + return self._nick or self._user._username +end + +--[=[@p nickname string/nil The member's nickname, if one is set.]=] +function get.nickname(self) + return self._nick +end + +--[=[@p joinedAt string/nil The date and time at which the current member joined the guild, represented as +an ISO 8601 string plus microseconds when available. Member objects generated +via presence updates lack this property.]=] +function get.joinedAt(self) + return self._joined_at +end + +--[=[@p premiumSince string/nil The date and time at which the current member boosted the guild, represented as +an ISO 8601 string plus microseconds when available.]=] +function get.premiumSince(self) + return self._premium_since +end + +--[=[@p voiceChannel GuildVoiceChannel/nil The voice channel to which this member is connected in the current guild.]=] +function get.voiceChannel(self) + local guild = self._parent + local state = guild._voice_states[self:__hash()] + return state and guild._voice_channels:get(state.channel_id) +end + +--[=[@p muted boolean Whether the member is voice muted in its guild.]=] +function get.muted(self) + local state = self._parent._voice_states[self:__hash()] + return state and (state.mute or state.self_mute) or self._mute +end + +--[=[@p deafened boolean Whether the member is voice deafened in its guild.]=] +function get.deafened(self) + local state = self._parent._voice_states[self:__hash()] + return state and (state.deaf or state.self_deaf) or self._deaf +end + +--[=[@p guild Guild The guild in which this member exists.]=] +function get.guild(self) + return self._parent +end + +--[=[@p highestRole Role The highest positioned role that the member has. If the member has no +explicit roles, then this is equivalent to `Member.guild.defaultRole`.]=] +function get.highestRole(self) + local ret + for role in self.roles:iter() do + if not ret or sorter(role, ret) then + ret = role + end + end + return ret or self.guild.defaultRole +end + +return Member diff --git a/deps/discordia/libs/containers/Message.lua b/deps/discordia/libs/containers/Message.lua new file mode 100644 index 0000000..0fe0cf8 --- /dev/null +++ b/deps/discordia/libs/containers/Message.lua @@ -0,0 +1,601 @@ +--[=[ +@c Message x Snowflake +@d Represents a text message sent in a Discord text channel. Messages can contain +simple content strings, rich embeds, attachments, or reactions. +]=] + +local json = require('json') +local enums = require('enums') +local constants = require('constants') +local Cache = require('iterables/Cache') +local ArrayIterable = require('iterables/ArrayIterable') +local Snowflake = require('containers/abstract/Snowflake') +local Reaction = require('containers/Reaction') +local Resolver = require('client/Resolver') + +local insert = table.insert +local null = json.null +local format = string.format +local messageFlag = enums.messageFlag +local band, bor, bnot = bit.band, bit.bor, bit.bnot + +local Message, get = require('class')('Message', Snowflake) + +function Message:__init(data, parent) + Snowflake.__init(self, data, parent) + self._author = self.client._users:_insert(data.author) + if data.member then + data.member.user = data.author + self._parent._parent._members:_insert(data.member) + end + self._timestamp = nil -- waste of space; can be calculated from Snowflake ID + if data.reactions and #data.reactions > 0 then + self._reactions = Cache(data.reactions, Reaction, self) + end + return self:_loadMore(data) +end + +function Message:_load(data) + Snowflake._load(self, data) + return self:_loadMore(data) +end + +local function parseMentions(content, pattern) + if not content:find('%b<>') then return end + local mentions, seen = {}, {} + for id in content:gmatch(pattern) do + if not seen[id] then + insert(mentions, id) + seen[id] = true + end + end + return mentions +end + +function Message:_loadMore(data) + + local mentions = {} + if data.mentions then + for _, user in ipairs(data.mentions) do + mentions[user.id] = true + if user.member then + user.member.user = user + self._parent._parent._members:_insert(user.member) + else + self.client._users:_insert(user) + end + end + end + + if data.referenced_message and data.referenced_message ~= null then + if mentions[data.referenced_message.author.id] then + self._reply_target = data.referenced_message.author.id + end + self._referencedMessage = self._parent._messages:_insert(data.referenced_message) + end + + local content = data.content + if content then + if self._mentioned_users then + self._mentioned_users._array = parseMentions(content, '<@!?(%d+)>') + if self._reply_target then + insert(self._mentioned_users._array, 1, self._reply_target) + end + end + if self._mentioned_roles then + self._mentioned_roles._array = parseMentions(content, '<@&(%d+)>') + end + if self._mentioned_channels then + self._mentioned_channels._array = parseMentions(content, '<#(%d+)>') + end + if self._mentioned_emojis then + self._mentioned_emojis._array = parseMentions(content, '') + end + self._clean_content = nil + end + + if data.embeds then + self._embeds = #data.embeds > 0 and data.embeds or nil + end + + if data.attachments then + self._attachments = #data.attachments > 0 and data.attachments or nil + end + +end + +function Message:_addReaction(d) + + local reactions = self._reactions + + if not reactions then + reactions = Cache({}, Reaction, self) + self._reactions = reactions + end + + local emoji = d.emoji + local k = emoji.id ~= null and emoji.id or emoji.name + local reaction = reactions:get(k) + + if reaction then + reaction._count = reaction._count + 1 + if d.user_id == self.client._user._id then + reaction._me = true + end + else + d.me = d.user_id == self.client._user._id + d.count = 1 + reaction = reactions:_insert(d) + end + return reaction + +end + +function Message:_removeReaction(d) + + local reactions = self._reactions + if not reactions then return nil end + + local emoji = d.emoji + local k = emoji.id ~= null and emoji.id or emoji.name + local reaction = reactions:get(k) or nil + + if not reaction then return nil end -- uncached reaction? + + reaction._count = reaction._count - 1 + if d.user_id == self.client._user._id then + reaction._me = false + end + + if reaction._count == 0 then + reactions:_delete(k) + end + + return reaction + +end + +function Message:_setOldContent(d) + local ts = d.edited_timestamp + if not ts then return end + local old = self._old + if old then + old[ts] = old[ts] or self._content + else + self._old = {[ts] = self._content} + end +end + +function Message:_modify(payload) + local data, err = self.client._api:editMessage(self._parent._id, self._id, payload) + if data then + self:_setOldContent(data) + self:_load(data) + return true + else + return false, err + end +end + +--[=[ +@m setContent +@t http +@p content string +@r boolean +@d Sets the message's content. The message must be authored by the current user +(ie: you cannot change the content of messages sent by other users). The content +must be from 1 to 2000 characters in length. +]=] +function Message:setContent(content) + return self:_modify({content = content or null}) +end + +--[=[ +@m setEmbed +@t http +@p embed table +@r boolean +@d Sets the message's embed. The message must be authored by the current user. +(ie: you cannot change the embed of messages sent by other users). +]=] +function Message:setEmbed(embed) + return self:_modify({embed = embed or null}) +end + +--[=[ +@m hideEmbeds +@t http +@r boolean +@d Hides all embeds for this message. +]=] +function Message:hideEmbeds() + local flags = bor(self._flags or 0, messageFlag.suppressEmbeds) + return self:_modify({flags = flags}) +end + +--[=[ +@m showEmbeds +@t http +@r boolean +@d Shows all embeds for this message. +]=] +function Message:showEmbeds() + local flags = band(self._flags or 0, bnot(messageFlag.suppressEmbeds)) + return self:_modify({flags = flags}) +end + +--[=[ +@m hasFlag +@t mem +@p flag Message-Flag-Resolvable +@r boolean +@d Indicates whether the message has a particular flag set. +]=] +function Message:hasFlag(flag) + flag = Resolver.messageFlag(flag) + return band(self._flags or 0, flag) > 0 +end + +--[=[ +@m update +@t http +@p data table +@r boolean +@d Sets multiple properties of the message at the same time using a table similar +to the one supported by `TextChannel.send`, except only `content` and `embed` +are valid fields; `mention(s)`, `file(s)`, etc are not supported. The message +must be authored by the current user. (ie: you cannot change the embed of messages +sent by other users). +]=] +function Message:update(data) + return self:_modify({ + content = data.content or null, + embed = data.embed or null, + }) +end + +--[=[ +@m pin +@t http +@r boolean +@d Pins the message in the channel. +]=] +function Message:pin() + local data, err = self.client._api:addPinnedChannelMessage(self._parent._id, self._id) + if data then + self._pinned = true + return true + else + return false, err + end +end + +--[=[ +@m unpin +@t http +@r boolean +@d Unpins the message in the channel. +]=] +function Message:unpin() + local data, err = self.client._api:deletePinnedChannelMessage(self._parent._id, self._id) + if data then + self._pinned = false + return true + else + return false, err + end +end + +--[=[ +@m addReaction +@t http +@p emoji Emoji-Resolvable +@r boolean +@d Adds a reaction to the message. Note that this does not return the new reaction +object; wait for the `reactionAdd` event instead. +]=] +function Message:addReaction(emoji) + emoji = Resolver.emoji(emoji) + local data, err = self.client._api:createReaction(self._parent._id, self._id, emoji) + if data then + return true + else + return false, err + end +end + +--[=[ +@m removeReaction +@t http +@p emoji Emoji-Resolvable +@op id User-ID-Resolvable +@r boolean +@d Removes a reaction from the message. Note that this does not return the old +reaction object; wait for the `reactionRemove` event instead. If no user is +indicated, then this will remove the current user's reaction. +]=] +function Message:removeReaction(emoji, id) + emoji = Resolver.emoji(emoji) + local data, err + if id then + id = Resolver.userId(id) + data, err = self.client._api:deleteUserReaction(self._parent._id, self._id, emoji, id) + else + data, err = self.client._api:deleteOwnReaction(self._parent._id, self._id, emoji) + end + if data then + return true + else + return false, err + end +end + +--[=[ +@m clearReactions +@t http +@r boolean +@d Removes all reactions from the message. +]=] +function Message:clearReactions() + local data, err = self.client._api:deleteAllReactions(self._parent._id, self._id) + if data then + return true + else + return false, err + end +end + +--[=[ +@m delete +@t http +@r boolean +@d Permanently deletes the message. This cannot be undone! +]=] +function Message:delete() + local data, err = self.client._api:deleteMessage(self._parent._id, self._id) + if data then + local cache = self._parent._messages + if cache then + cache:_delete(self._id) + end + return true + else + return false, err + end +end + +--[=[ +@m reply +@t http +@p content string/table +@r Message +@d Equivalent to `Message.channel:send(content)`. +]=] +function Message:reply(content) + return self._parent:send(content) +end + +--[=[@p reactions Cache An iterable cache of all reactions that exist for this message.]=] +function get.reactions(self) + if not self._reactions then + self._reactions = Cache({}, Reaction, self) + end + return self._reactions +end + +--[=[@p mentionedUsers ArrayIterable An iterable array of all users that are mentioned in this message.]=] +function get.mentionedUsers(self) + if not self._mentioned_users then + local users = self.client._users + local mentions = parseMentions(self._content, '<@!?(%d+)>') + if self._reply_target then + insert(mentions, 1, self._reply_target) + end + self._mentioned_users = ArrayIterable(mentions, function(id) + return users:get(id) + end) + end + return self._mentioned_users +end + +--[=[@p mentionedRoles ArrayIterable An iterable array of known roles that are mentioned in this message, excluding +the default everyone role. The message must be in a guild text channel and the +roles must be cached in that channel's guild for them to appear here.]=] +function get.mentionedRoles(self) + if not self._mentioned_roles then + local client = self.client + local mentions = parseMentions(self._content, '<@&(%d+)>') + self._mentioned_roles = ArrayIterable(mentions, function(id) + local guild = client._role_map[id] + return guild and guild._roles:get(id) or nil + end) + end + return self._mentioned_roles +end + +--[=[@p mentionedEmojis ArrayIterable An iterable array of all known emojis that are mentioned in this message. If +the client does not have the emoji cached, then it will not appear here.]=] +function get.mentionedEmojis(self) + if not self._mentioned_emojis then + local client = self.client + local mentions = parseMentions(self._content, '') + self._mentioned_emojis = ArrayIterable(mentions, function(id) + local guild = client._emoji_map[id] + return guild and guild._emojis:get(id) + end) + end + return self._mentioned_emojis +end + +--[=[@p mentionedChannels ArrayIterable An iterable array of all known channels that are mentioned in this message. If +the client does not have the channel cached, then it will not appear here.]=] +function get.mentionedChannels(self) + if not self._mentioned_channels then + local client = self.client + local mentions = parseMentions(self._content, '<#(%d+)>') + self._mentioned_channels = ArrayIterable(mentions, function(id) + local guild = client._channel_map[id] + if guild then + return guild._text_channels:get(id) or guild._voice_channels:get(id) or guild._categories:get(id) + else + return client._private_channels:get(id) or client._group_channels:get(id) + end + end) + end + return self._mentioned_channels +end + +local usersMeta = {__index = function(_, k) return '@' .. k end} +local rolesMeta = {__index = function(_, k) return '@' .. k end} +local channelsMeta = {__index = function(_, k) return '#' .. k end} +local everyone = '@' .. constants.ZWSP .. 'everyone' +local here = '@' .. constants.ZWSP .. 'here' + +--[=[@p cleanContent string The message content with all recognized mentions replaced by names and with +@everyone and @here mentions escaped by a zero-width space (ZWSP).]=] +function get.cleanContent(self) + + if not self._clean_content then + + local content = self._content + local guild = self.guild + + local users = setmetatable({}, usersMeta) + for user in self.mentionedUsers:iter() do + local member = guild and guild._members:get(user._id) + users[user._id] = '@' .. (member and member._nick or user._username) + end + + local roles = setmetatable({}, rolesMeta) + for role in self.mentionedRoles:iter() do + roles[role._id] = '@' .. role._name + end + + local channels = setmetatable({}, channelsMeta) + for channel in self.mentionedChannels:iter() do + channels[channel._id] = '#' .. channel._name + end + + self._clean_content = content + :gsub('<@!?(%d+)>', users) + :gsub('<@&(%d+)>', roles) + :gsub('<#(%d+)>', channels) + :gsub('', '%1') + :gsub('@everyone', everyone) + :gsub('@here', here) + + end + + return self._clean_content + +end + +--[=[@p mentionsEveryone boolean Whether this message mentions @everyone or @here.]=] +function get.mentionsEveryone(self) + return self._mention_everyone +end + +--[=[@p pinned boolean Whether this message belongs to its channel's pinned messages.]=] +function get.pinned(self) + return self._pinned +end + +--[=[@p tts boolean Whether this message is a text-to-speech message.]=] +function get.tts(self) + return self._tts +end + +--[=[@p nonce string/number/boolean/nil Used by the official Discord client to detect the success of a sent message.]=] +function get.nonce(self) + return self._nonce +end + +--[=[@p editedTimestamp string/nil The date and time at which the message was most recently edited, represented as +an ISO 8601 string plus microseconds when available.]=] +function get.editedTimestamp(self) + return self._edited_timestamp +end + +--[=[@p oldContent string/table Yields a table containing keys as timestamps and +value as content of the message at that time.]=] +function get.oldContent(self) + return self._old +end + +--[=[@p content string The raw message content. This should be between 0 and 2000 characters in length.]=] +function get.content(self) + return self._content +end + +--[=[@p author User The object of the user that created the message.]=] +function get.author(self) + return self._author +end + +--[=[@p channel TextChannel The channel in which this message was sent.]=] +function get.channel(self) + return self._parent +end + +--[=[@p type number The message type. Use the `messageType` enumeration for a human-readable +representation.]=] +function get.type(self) + return self._type +end + +--[=[@p embed table/nil A raw data table that represents the first rich embed that exists in this +message. See the Discord documentation for more information.]=] +function get.embed(self) + return self._embeds and self._embeds[1] +end + +--[=[@p attachment table/nil A raw data table that represents the first file attachment that exists in this +message. See the Discord documentation for more information.]=] +function get.attachment(self) + return self._attachments and self._attachments[1] +end + +--[=[@p embeds table A raw data table that contains all embeds that exist for this message. If +there are none, this table will not be present.]=] +function get.embeds(self) + return self._embeds +end + +--[=[@p attachments table A raw data table that contains all attachments that exist for this message. If +there are none, this table will not be present.]=] +function get.attachments(self) + return self._attachments +end + +--[=[@p guild Guild/nil The guild in which this message was sent. This will not exist if the message +was not sent in a guild text channel. Equivalent to `Message.channel.guild`.]=] +function get.guild(self) + return self._parent.guild +end + +--[=[@p member Member/nil The member object of the message's author. This will not exist if the message +is not sent in a guild text channel or if the member object is not cached. +Equivalent to `Message.guild.members:get(Message.author.id)`.]=] +function get.member(self) + local guild = self.guild + return guild and guild._members:get(self._author._id) +end + +--[=[@p referencedMessage Message/nil If available, the previous message that +this current message references as seen in replies.]=] +function get.referencedMessage(self) + return self._referencedMessage +end + +--[=[@p link string URL that can be used to jump-to the message in the Discord client.]=] +function get.link(self) + local guild = self.guild + return format('https://discord.com/channels/%s/%s/%s', guild and guild._id or '@me', self._parent._id, self._id) +end + +--[=[@p webhookId string/nil The ID of the webhook that generated this message, if applicable.]=] +function get.webhookId(self) + return self._webhook_id +end + +return Message diff --git a/deps/discordia/libs/containers/PermissionOverwrite.lua b/deps/discordia/libs/containers/PermissionOverwrite.lua new file mode 100644 index 0000000..294ab97 --- /dev/null +++ b/deps/discordia/libs/containers/PermissionOverwrite.lua @@ -0,0 +1,234 @@ +--[=[ +@c PermissionOverwrite x Snowflake +@d Represents an object that is used to allow or deny specific permissions for a +role or member in a Discord guild channel. +]=] + +local Snowflake = require('containers/abstract/Snowflake') +local Permissions = require('utils/Permissions') +local Resolver = require('client/Resolver') + +local band, bnot = bit.band, bit.bnot + +local PermissionOverwrite, get = require('class')('PermissionOverwrite', Snowflake) + +function PermissionOverwrite:__init(data, parent) + Snowflake.__init(self, data, parent) +end + +--[=[ +@m delete +@t http +@r boolean +@d Deletes the permission overwrite. This can be undone by creating a new version of +the same overwrite. +]=] +function PermissionOverwrite:delete() + local data, err = self.client._api:deleteChannelPermission(self._parent._id, self._id) + if data then + local cache = self._parent._permission_overwrites + if cache then + cache:_delete(self._id) + end + return true + else + return false, err + end +end + +--[=[ +@m getObject +@t http? +@r Role/Member +@d Returns the object associated with this overwrite, either a role or member. +This may make an HTTP request if the object is not cached. +]=] +function PermissionOverwrite:getObject() + local guild = self._parent._parent + if self._type == 'role' then + return guild:getRole(self._id) + elseif self._type == 'member' then + return guild:getMember(self._id) + end +end + +local function getPermissions(self) + return Permissions(self._allow), Permissions(self._deny) +end + +local function setPermissions(self, allow, deny) + local data, err = self.client._api:editChannelPermissions(self._parent._id, self._id, { + allow = allow, deny = deny, type = self._type + }) + if data then + self._allow, self._deny = allow, deny + return true + else + return false, err + end +end + +--[=[ +@m getAllowedPermissions +@t mem +@r Permissions +@d Returns a permissions object that represents the permissions that this overwrite +explicitly allows. +]=] +function PermissionOverwrite:getAllowedPermissions() + return Permissions(self._allow) +end + +--[=[ +@m getDeniedPermissions +@t mem +@r Permissions +@d Returns a permissions object that represents the permissions that this overwrite +explicitly denies. +]=] +function PermissionOverwrite:getDeniedPermissions() + return Permissions(self._deny) +end + +--[=[ +@m setPermissions +@t http +@p allowed Permissions-Resolvables +@p denied Permissions-Resolvables +@r boolean +@d Sets the permissions that this overwrite explicitly allows and denies. This +method does NOT resolve conflicts. Please be sure to use the correct parameters. +]=] +function PermissionOverwrite:setPermissions(allowed, denied) + local allow = Resolver.permissions(allowed) + local deny = Resolver.permissions(denied) + return setPermissions(self, allow, deny) +end + +--[=[ +@m setAllowedPermissions +@t http +@p allowed Permissions-Resolvables +@r boolean +@d Sets the permissions that this overwrite explicitly allows. +]=] +function PermissionOverwrite:setAllowedPermissions(allowed) + local allow = Resolver.permissions(allowed) + local deny = band(bnot(allow), self._deny) -- un-deny the allowed permissions + return setPermissions(self, allow, deny) +end + +--[=[ +@m setDeniedPermissions +@t http +@p denied Permissions-Resolvables +@r boolean +@d Sets the permissions that this overwrite explicitly denies. +]=] +function PermissionOverwrite:setDeniedPermissions(denied) + local deny = Resolver.permissions(denied) + local allow = band(bnot(deny), self._allow) -- un-allow the denied permissions + return setPermissions(self, allow, deny) +end + +--[=[ +@m allowPermissions +@t http +@p ... Permission-Resolvables +@r boolean +@d Allows individual permissions in this overwrite. +]=] +function PermissionOverwrite:allowPermissions(...) + local allowed, denied = getPermissions(self) + allowed:enable(...); denied:disable(...) + return setPermissions(self, allowed._value, denied._value) +end + +--[=[ +@m denyPermissions +@t http +@p ... Permission-Resolvables +@r boolean +@d Denies individual permissions in this overwrite. +]=] +function PermissionOverwrite:denyPermissions(...) + local allowed, denied = getPermissions(self) + allowed:disable(...); denied:enable(...) + return setPermissions(self, allowed._value, denied._value) +end + +--[=[ +@m clearPermissions +@t http +@p ... Permission-Resolvables +@r boolean +@d Clears individual permissions in this overwrite. +]=] +function PermissionOverwrite:clearPermissions(...) + local allowed, denied = getPermissions(self) + allowed:disable(...); denied:disable(...) + return setPermissions(self, allowed._value, denied._value) +end + +--[=[ +@m allowAllPermissions +@t http +@r boolean +@d Allows all permissions in this overwrite. +]=] +function PermissionOverwrite:allowAllPermissions() + local allowed, denied = getPermissions(self) + allowed:enableAll(); denied:disableAll() + return setPermissions(self, allowed._value, denied._value) +end + +--[=[ +@m denyAllPermissions +@t http +@r boolean +@d Denies all permissions in this overwrite. +]=] +function PermissionOverwrite:denyAllPermissions() + local allowed, denied = getPermissions(self) + allowed:disableAll(); denied:enableAll() + return setPermissions(self, allowed._value, denied._value) +end + +--[=[ +@m clearAllPermissions +@t http +@r boolean +@d Clears all permissions in this overwrite. +]=] +function PermissionOverwrite:clearAllPermissions() + local allowed, denied = getPermissions(self) + allowed:disableAll(); denied:disableAll() + return setPermissions(self, allowed._value, denied._value) +end + +--[=[@p type string The overwrite type; either "role" or "member".]=] +function get.type(self) + return self._type +end + +--[=[@p channel GuildChannel The channel in which this overwrite exists.]=] +function get.channel(self) + return self._parent +end + +--[=[@p guild Guild The guild in which this overwrite exists. Equivalent to `PermissionOverwrite.channel.guild`.]=] +function get.guild(self) + return self._parent._parent +end + +--[=[@p allowedPermissions number The number representing the total permissions allowed by this overwrite.]=] +function get.allowedPermissions(self) + return self._allow +end + +--[=[@p deniedPermissions number The number representing the total permissions denied by this overwrite.]=] +function get.deniedPermissions(self) + return self._deny +end + +return PermissionOverwrite diff --git a/deps/discordia/libs/containers/PrivateChannel.lua b/deps/discordia/libs/containers/PrivateChannel.lua new file mode 100644 index 0000000..2ab1471 --- /dev/null +++ b/deps/discordia/libs/containers/PrivateChannel.lua @@ -0,0 +1,37 @@ +--[=[ +@c PrivateChannel x TextChannel +@d Represents a private Discord text channel used to track correspondences between +the current user and one other recipient. +]=] + +local TextChannel = require('containers/abstract/TextChannel') + +local PrivateChannel, get = require('class')('PrivateChannel', TextChannel) + +function PrivateChannel:__init(data, parent) + TextChannel.__init(self, data, parent) + self._recipient = self.client._users:_insert(data.recipients[1]) +end + +--[=[ +@m close +@t http +@r boolean +@d Closes the channel. This does not delete the channel. To re-open the channel, +use `User:getPrivateChannel`. +]=] +function PrivateChannel:close() + return self:_delete() +end + +--[=[@p name string Equivalent to `PrivateChannel.recipient.username`.]=] +function get.name(self) + return self._recipient._username +end + +--[=[@p recipient User The recipient of this channel's messages, other than the current user.]=] +function get.recipient(self) + return self._recipient +end + +return PrivateChannel diff --git a/deps/discordia/libs/containers/Reaction.lua b/deps/discordia/libs/containers/Reaction.lua new file mode 100644 index 0000000..9270b0f --- /dev/null +++ b/deps/discordia/libs/containers/Reaction.lua @@ -0,0 +1,149 @@ +--[=[ +@c Reaction x Container +@d Represents an emoji that has been used to react to a Discord text message. Both +standard and custom emojis can be used. +]=] + +local json = require('json') +local Container = require('containers/abstract/Container') +local SecondaryCache = require('iterables/SecondaryCache') +local Resolver = require('client/Resolver') + +local null = json.null +local format = string.format + +local Reaction, get = require('class')('Reaction', Container) + +function Reaction:__init(data, parent) + Container.__init(self, data, parent) + local emoji = data.emoji + self._emoji_id = emoji.id ~= null and emoji.id or nil + self._emoji_name = emoji.name + if emoji.animated ~= null and emoji.animated ~= nil then -- not always present + self._emoji_animated = emoji.animated + end +end + +--[=[ +@m __hash +@r string +@d Returns `Reaction.emojiId or Reaction.emojiName` +]=] +function Reaction:__hash() + return self._emoji_id or self._emoji_name +end + +local function getUsers(self, query) + local emoji = Resolver.emoji(self) + local message = self._parent + local channel = message._parent + local data, err = self.client._api:getReactions(channel._id, message._id, emoji, query) + if data then + return SecondaryCache(data, self.client._users) + else + return nil, err + end +end + +--[=[ +@m getUsers +@t http +@op limit number +@r SecondaryCache +@d Returns a newly constructed cache of all users that have used this reaction in +its parent message. The cache is not automatically updated via gateway events, +but the internally referenced user objects may be updated. You must call this +method again to guarantee that the objects are update to date. +]=] +function Reaction:getUsers(limit) + return getUsers(self, limit and {limit = limit}) +end + +--[=[ +@m getUsersBefore +@t http +@p id User-ID-Resolvable +@op limit number +@r SecondaryCache +@d Returns a newly constructed cache of all users that have used this reaction before the specified id in +its parent message. The cache is not automatically updated via gateway events, +but the internally referenced user objects may be updated. You must call this +method again to guarantee that the objects are update to date. +]=] +function Reaction:getUsersBefore(id, limit) + id = Resolver.userId(id) + return getUsers(self, {before = id, limit = limit}) +end + +--[=[ +@m getUsersAfter +@t http +@p id User-ID-Resolvable +@op limit number +@r SecondaryCache +@d Returns a newly constructed cache of all users that have used this reaction +after the specified id in its parent message. The cache is not automatically +updated via gateway events, but the internally referenced user objects may be +updated. You must call this method again to guarantee that the objects are update to date. +]=] +function Reaction:getUsersAfter(id, limit) + id = Resolver.userId(id) + return getUsers(self, {after = id, limit = limit}) +end + +--[=[ +@m delete +@t http +@op id User-ID-Resolvable +@r boolean +@d Equivalent to `Reaction.message:removeReaction(Reaction)` +]=] +function Reaction:delete(id) + return self._parent:removeReaction(self, id) +end + +--[=[@p emojiId string/nil The ID of the emoji used in this reaction if it is a custom emoji.]=] +function get.emojiId(self) + return self._emoji_id +end + +--[=[@p emojiName string The name of the emoji used in this reaction. +This will be the raw string for a standard emoji.]=] +function get.emojiName(self) + return self._emoji_name +end + +--[=[@p emojiHash string The discord hash for the emoji used in this reaction. +This will be the raw string for a standard emoji.]=] +function get.emojiHash(self) + if self._emoji_id then + return self._emoji_name .. ':' .. self._emoji_id + else + return self._emoji_name + end +end + +--[=[@p emojiURL string/nil string The URL that can be used to view a full +version of the emoji used in this reaction if it is a custom emoji.]=] +function get.emojiURL(self) + local id = self._emoji_id + local ext = self._emoji_animated and 'gif' or 'png' + return id and format('https://cdn.discordapp.com/emojis/%s.%s', id, ext) or nil +end + +--[=[@p me boolean Whether the current user has used this reaction.]=] +function get.me(self) + return self._me +end + +--[=[@p count number The total number of users that have used this reaction.]=] +function get.count(self) + return self._count +end + +--[=[@p message Message The message on which this reaction exists.]=] +function get.message(self) + return self._parent +end + +return Reaction diff --git a/deps/discordia/libs/containers/Relationship.lua b/deps/discordia/libs/containers/Relationship.lua new file mode 100644 index 0000000..d7acc7a --- /dev/null +++ b/deps/discordia/libs/containers/Relationship.lua @@ -0,0 +1,27 @@ +--[=[ +@c Relationship x UserPresence +@d Represents a relationship between the current user and another Discord user. +This is generally either a friend or a blocked user. This class should only be +relevant to user-accounts; bots cannot normally have relationships. +]=] + +local UserPresence = require('containers/abstract/UserPresence') + +local Relationship, get = require('class')('Relationship', UserPresence) + +function Relationship:__init(data, parent) + UserPresence.__init(self, data, parent) +end + +--[=[@p name string Equivalent to `Relationship.user.username`.]=] +function get.name(self) + return self._user._username +end + +--[=[@p type number The relationship type. See the `relationshipType` enumeration for a +human-readable representation.]=] +function get.type(self) + return self._type +end + +return Relationship diff --git a/deps/discordia/libs/containers/Role.lua b/deps/discordia/libs/containers/Role.lua new file mode 100644 index 0000000..4d35dcb --- /dev/null +++ b/deps/discordia/libs/containers/Role.lua @@ -0,0 +1,383 @@ +--[=[ +@c Role x Snowflake +@d Represents a Discord guild role, which is used to assign priority, permissions, +and a color to guild members. +]=] + +local json = require('json') +local Snowflake = require('containers/abstract/Snowflake') +local Color = require('utils/Color') +local Permissions = require('utils/Permissions') +local Resolver = require('client/Resolver') +local FilteredIterable = require('iterables/FilteredIterable') + +local format = string.format +local insert, sort = table.insert, table.sort +local min, max, floor = math.min, math.max, math.floor +local huge = math.huge + +local Role, get = require('class')('Role', Snowflake) + +function Role:__init(data, parent) + Snowflake.__init(self, data, parent) + self.client._role_map[self._id] = parent +end + +function Role:_modify(payload) + local data, err = self.client._api:modifyGuildRole(self._parent._id, self._id, payload) + if data then + self:_load(data) + return true + else + return false, err + end +end + +--[=[ +@m delete +@t http +@r boolean +@d Permanently deletes the role. This cannot be undone! +]=] +function Role:delete() + local data, err = self.client._api:deleteGuildRole(self._parent._id, self._id) + if data then + local cache = self._parent._roles + if cache then + cache:_delete(self._id) + end + return true + else + return false, err + end +end + +local function sorter(a, b) + if a.position == b.position then + return tonumber(a.id) < tonumber(b.id) + else + return a.position < b.position + end +end + +local function getSortedRoles(self) + local guild = self._parent + local id = self._parent._id + local ret = {} + for role in guild.roles:iter() do + if role._id ~= id then + insert(ret, {id = role._id, position = role._position}) + end + end + sort(ret, sorter) + return ret +end + +local function setSortedRoles(self, roles) + local id = self._parent._id + insert(roles, {id = id, position = 0}) + local data, err = self.client._api:modifyGuildRolePositions(id, roles) + if data then + return true + else + return false, err + end +end + +--[=[ +@m moveDown +@t http +@p n number +@r boolean +@d Moves a role down its list. The parameter `n` indicates how many spaces the +role should be moved, clamped to the lowest position, with a default of 1 if +it is omitted. This will also normalize the positions of all roles. Note that +the default everyone role cannot be moved. +]=] +function Role:moveDown(n) -- TODO: fix attempt to move roles that cannot be moved + + n = tonumber(n) or 1 + if n < 0 then + return self:moveDown(-n) + end + + local roles = getSortedRoles(self) + + local new = huge + for i = #roles, 1, -1 do + local v = roles[i] + if v.id == self._id then + new = max(1, i - floor(n)) + v.position = new + elseif i >= new then + v.position = i + 1 + else + v.position = i + end + end + + return setSortedRoles(self, roles) + +end + +--[=[ +@m moveUp +@t http +@p n number +@r boolean +@d Moves a role up its list. The parameter `n` indicates how many spaces the +role should be moved, clamped to the highest position, with a default of 1 if +it is omitted. This will also normalize the positions of all roles. Note that +the default everyone role cannot be moved. +]=] +function Role:moveUp(n) -- TODO: fix attempt to move roles that cannot be moved + + n = tonumber(n) or 1 + if n < 0 then + return self:moveUp(-n) + end + + local roles = getSortedRoles(self) + + local new = -huge + for i = 1, #roles do + local v = roles[i] + if v.id == self._id then + new = min(i + floor(n), #roles) + v.position = new + elseif i <= new then + v.position = i - 1 + else + v.position = i + end + end + + return setSortedRoles(self, roles) + +end + +--[=[ +@m setName +@t http +@p name string +@r boolean +@d Sets the role's name. The name must be between 1 and 100 characters in length. +]=] +function Role:setName(name) + return self:_modify({name = name or json.null}) +end + +--[=[ +@m setColor +@t http +@p color Color-Resolvable +@r boolean +@d Sets the role's display color. +]=] +function Role:setColor(color) + color = color and Resolver.color(color) + return self:_modify({color = color or json.null}) +end + +--[=[ +@m setPermissions +@t http +@p permissions Permissions-Resolvable +@r boolean +@d Sets the permissions that this role explicitly allows. +]=] +function Role:setPermissions(permissions) + permissions = permissions and Resolver.permissions(permissions) + return self:_modify({permissions = permissions or json.null}) +end + +--[=[ +@m hoist +@t http +@r boolean +@d Causes members with this role to display above unhoisted roles in the member +list. +]=] +function Role:hoist() + return self:_modify({hoist = true}) +end + +--[=[ +@m unhoist +@t http +@r boolean +@d Causes member with this role to display amongst other unhoisted members. +]=] +function Role:unhoist() + return self:_modify({hoist = false}) +end + +--[=[ +@m enableMentioning +@t http +@r boolean +@d Allows anyone to mention this role in text messages. +]=] +function Role:enableMentioning() + return self:_modify({mentionable = true}) +end + +--[=[ +@m disableMentioning +@t http +@r boolean +@d Disallows anyone to mention this role in text messages. +]=] +function Role:disableMentioning() + return self:_modify({mentionable = false}) +end + +--[=[ +@m enablePermissions +@t http +@p ... Permission-Resolvables +@r boolean +@d Enables individual permissions for this role. This does not necessarily fully +allow the permissions. +]=] +function Role:enablePermissions(...) + local permissions = self:getPermissions() + permissions:enable(...) + return self:setPermissions(permissions) +end + +--[=[ +@m disablePermissions +@t http +@p ... Permission-Resolvables +@r boolean +@d Disables individual permissions for this role. This does not necessarily fully +disallow the permissions. +]=] +function Role:disablePermissions(...) + local permissions = self:getPermissions() + permissions:disable(...) + return self:setPermissions(permissions) +end + +--[=[ +@m enableAllPermissions +@t http +@r boolean +@d Enables all permissions for this role. This does not necessarily fully +allow the permissions. +]=] +function Role:enableAllPermissions() + local permissions = self:getPermissions() + permissions:enableAll() + return self:setPermissions(permissions) +end + +--[=[ +@m disableAllPermissions +@t http +@r boolean +@d Disables all permissions for this role. This does not necessarily fully +disallow the permissions. +]=] +function Role:disableAllPermissions() + local permissions = self:getPermissions() + permissions:disableAll() + return self:setPermissions(permissions) +end + +--[=[ +@m getColor +@t mem +@r Color +@d Returns a color object that represents the role's display color. +]=] +function Role:getColor() + return Color(self._color) +end + +--[=[ +@m getPermissions +@t mem +@r Permissions +@d Returns a permissions object that represents the permissions that this role +has enabled. +]=] +function Role:getPermissions() + return Permissions(self._permissions) +end + +--[=[@p hoisted boolean Whether members with this role should be shown separated from other members +in the guild member list.]=] +function get.hoisted(self) + return self._hoist +end + +--[=[@p mentionable boolean Whether this role can be mentioned in a text channel message.]=] +function get.mentionable(self) + return self._mentionable +end + +--[=[@p managed boolean Whether this role is managed by some integration or bot inclusion.]=] +function get.managed(self) + return self._managed +end + +--[=[@p name string The name of the role. This should be between 1 and 100 characters in length.]=] +function get.name(self) + return self._name +end + +--[=[@p position number The position of the role, where 0 is the lowest.]=] +function get.position(self) + return self._position +end + +--[=[@p color number Represents the display color of the role as a decimal value.]=] +function get.color(self) + return self._color +end + +--[=[@p permissions number Represents the total permissions of the role as a decimal value.]=] +function get.permissions(self) + return self._permissions +end + +--[=[@p mentionString string A string that, when included in a message content, may resolve as a role +notification in the official Discord client.]=] +function get.mentionString(self) + return format('<@&%s>', self._id) +end + +--[=[@p guild Guild The guild in which this role exists.]=] +function get.guild(self) + return self._parent +end + +--[=[@p members FilteredIterable A filtered iterable of guild members that have +this role. If you want to check whether a specific member has this role, it would +be better to get the member object elsewhere and use `Member:hasRole` rather +than check whether the member exists here.]=] +function get.members(self) + if not self._members then + self._members = FilteredIterable(self._parent._members, function(m) + return m:hasRole(self) + end) + end + return self._members +end + +--[=[@p emojis FilteredIterable A filtered iterable of guild emojis that have +this role. If you want to check whether a specific emoji has this role, it would +be better to get the emoji object elsewhere and use `Emoji:hasRole` rather +than check whether the emoji exists here.]=] +function get.emojis(self) + if not self._emojis then + self._emojis = FilteredIterable(self._parent._emojis, function(e) + return e:hasRole(self) + end) + end + return self._emojis +end + +return Role diff --git a/deps/discordia/libs/containers/User.lua b/deps/discordia/libs/containers/User.lua new file mode 100644 index 0000000..81d5509 --- /dev/null +++ b/deps/discordia/libs/containers/User.lua @@ -0,0 +1,186 @@ +--[=[ +@c User x Snowflake +@d Represents a single user of Discord, either a human or a bot, outside of any +specific guild's context. +]=] + +local Snowflake = require('containers/abstract/Snowflake') +local FilteredIterable = require('iterables/FilteredIterable') +local constants = require('constants') + +local format = string.format +local DEFAULT_AVATARS = constants.DEFAULT_AVATARS + +local User, get = require('class')('User', Snowflake) + +function User:__init(data, parent) + Snowflake.__init(self, data, parent) +end + +--[=[ +@m getAvatarURL +@t mem +@op size number +@op ext string +@r string +@d Returns a URL that can be used to view the user's full avatar. If provided, the +size must be a power of 2 while the extension must be a valid image format. If +the user does not have a custom avatar, the default URL is returned. +]=] +function User:getAvatarURL(size, ext) + local avatar = self._avatar + if avatar then + ext = ext or avatar:find('a_') == 1 and 'gif' or 'png' + if size then + return format('https://cdn.discordapp.com/avatars/%s/%s.%s?size=%s', self._id, avatar, ext, size) + else + return format('https://cdn.discordapp.com/avatars/%s/%s.%s', self._id, avatar, ext) + end + else + return self:getDefaultAvatarURL(size) + end +end + +--[=[ +@m getDefaultAvatarURL +@t mem +@op size number +@r string +@d Returns a URL that can be used to view the user's default avatar. +]=] +function User:getDefaultAvatarURL(size) + local avatar = self.defaultAvatar + if size then + return format('https://cdn.discordapp.com/embed/avatars/%s.png?size=%s', avatar, size) + else + return format('https://cdn.discordapp.com/embed/avatars/%s.png', avatar) + end +end + +--[=[ +@m getPrivateChannel +@t http +@r PrivateChannel +@d Returns a private channel that can be used to communicate with the user. If the +channel is not cached an HTTP request is made to open one. +]=] +function User:getPrivateChannel() + local id = self._id + local client = self.client + local channel = client._private_channels:find(function(e) return e._recipient._id == id end) + if channel then + return channel + else + local data, err = client._api:createDM({recipient_id = id}) + if data then + return client._private_channels:_insert(data) + else + return nil, err + end + end +end + +--[=[ +@m send +@t http +@p content string/table +@r Message +@d Equivalent to `User:getPrivateChannel():send(content)` +]=] +function User:send(content) + local channel, err = self:getPrivateChannel() + if channel then + return channel:send(content) + else + return nil, err + end +end + +--[=[ +@m sendf +@t http +@p content string +@r Message +@d Equivalent to `User:getPrivateChannel():sendf(content)` +]=] +function User:sendf(content, ...) + local channel, err = self:getPrivateChannel() + if channel then + return channel:sendf(content, ...) + else + return nil, err + end +end + +--[=[@p bot boolean Whether this user is a bot.]=] +function get.bot(self) + return self._bot or false +end + +--[=[@p name string Equivalent to `User.username`.]=] +function get.name(self) + return self._username +end + +--[=[@p username string The name of the user. This should be between 2 and 32 characters in length.]=] +function get.username(self) + return self._username +end + +--[=[@p discriminator number The discriminator of the user. This is a 4-digit string that is used to +discriminate the user from other users with the same username.]=] +function get.discriminator(self) + return self._discriminator +end + +--[=[@p tag string The user's username and discriminator concatenated by an `#`.]=] +function get.tag(self) + return self._username .. '#' .. self._discriminator +end + +function get.fullname(self) + self.client:_deprecated(self.__name, 'fullname', 'tag') + return self._username .. '#' .. self._discriminator +end + +--[=[@p avatar string/nil The hash for the user's custom avatar, if one is set.]=] +function get.avatar(self) + return self._avatar +end + +--[=[@p defaultAvatar number The user's default avatar. See the `defaultAvatar` enumeration for a +human-readable representation.]=] +function get.defaultAvatar(self) + return self._discriminator % DEFAULT_AVATARS +end + +--[=[@p avatarURL string Equivalent to the result of calling `User:getAvatarURL()`.]=] +function get.avatarURL(self) + return self:getAvatarURL() +end + +--[=[@p defaultAvatarURL string Equivalent to the result of calling `User:getDefaultAvatarURL()`.]=] +function get.defaultAvatarURL(self) + return self:getDefaultAvatarURL() +end + +--[=[@p mentionString string A string that, when included in a message content, may resolve as user +notification in the official Discord client.]=] +function get.mentionString(self) + return format('<@%s>', self._id) +end + +--[=[@p mutualGuilds FilteredIterable A iterable cache of all guilds where this user shares a membership with the +current user. The guild must be cached on the current client and the user's +member object must be cached in that guild in order for it to appear here.]=] +function get.mutualGuilds(self) + if not self._mutual_guilds then + local id = self._id + self._mutual_guilds = FilteredIterable(self.client._guilds, function(g) + return g._members:get(id) + end) + end + return self._mutual_guilds +end + +return User diff --git a/deps/discordia/libs/containers/Webhook.lua b/deps/discordia/libs/containers/Webhook.lua new file mode 100644 index 0000000..610af95 --- /dev/null +++ b/deps/discordia/libs/containers/Webhook.lua @@ -0,0 +1,147 @@ +--[=[ +@c Webhook x Snowflake +@d Represents a handle used to send webhook messages to a guild text channel in a +one-way fashion. This class defines methods and properties for managing the +webhook, not for sending messages. +]=] + +local json = require('json') +local enums = require('enums') +local Snowflake = require('containers/abstract/Snowflake') +local User = require('containers/User') +local Resolver = require('client/Resolver') + +local defaultAvatar = enums.defaultAvatar + +local Webhook, get = require('class')('Webhook', Snowflake) + +function Webhook:__init(data, parent) + Snowflake.__init(self, data, parent) + self._user = data.user and self.client._users:_insert(data.user) -- DNE if getting by token +end + +function Webhook:_modify(payload) + local data, err = self.client._api:modifyWebhook(self._id, payload) + if data then + self:_load(data) + return true + else + return false, err + end +end + +--[=[ +@m getAvatarURL +@t mem +@op size number +@op ext string +@r string +@d Returns a URL that can be used to view the webhooks's full avatar. If provided, +the size must be a power of 2 while the extension must be a valid image format. +If the webhook does not have a custom avatar, the default URL is returned. +]=] +function Webhook:getAvatarURL(size, ext) + return User.getAvatarURL(self, size, ext) +end + +--[=[ +@m getDefaultAvatarURL +@t mem +@op size number +@r string +@d Returns a URL that can be used to view the webhooks's default avatar. +]=] +function Webhook:getDefaultAvatarURL(size) + return User.getDefaultAvatarURL(self, size) +end + +--[=[ +@m setName +@t http +@p name string +@r boolean +@d Sets the webhook's name. This must be between 2 and 32 characters in length. +]=] +function Webhook:setName(name) + return self:_modify({name = name or json.null}) +end + +--[=[ +@m setAvatar +@t http +@p avatar Base64-Resolvable +@r boolean +@d Sets the webhook's avatar. If `nil` is passed, the avatar is removed. +]=] +function Webhook:setAvatar(avatar) + avatar = avatar and Resolver.base64(avatar) + return self:_modify({avatar = avatar or json.null}) +end + +--[=[ +@m delete +@t http +@r boolean +@d Permanently deletes the webhook. This cannot be undone! +]=] +function Webhook:delete() + local data, err = self.client._api:deleteWebhook(self._id) + if data then + return true + else + return false, err + end +end + +--[=[@p guildId string The ID of the guild in which this webhook exists.]=] +function get.guildId(self) + return self._guild_id +end + +--[=[@p channelId string The ID of the channel in which this webhook exists.]=] +function get.channelId(self) + return self._channel_id +end + +--[=[@p user User/nil The user that created this webhook.]=] +function get.user(self) + return self._user +end + +--[=[@p token string The token that can be used to access this webhook.]=] +function get.token(self) + return self._token +end + +--[=[@p name string The name of the webhook. This should be between 2 and 32 characters in length.]=] +function get.name(self) + return self._name +end + +--[=[@p type number The type of the webhook. See the `webhookType` enum for a human-readable representation.]=] +function get.type(self) + return self._type +end + +--[=[@p avatar string/nil The hash for the webhook's custom avatar, if one is set.]=] +function get.avatar(self) + return self._avatar +end + +--[=[@p avatarURL string Equivalent to the result of calling `Webhook:getAvatarURL()`.]=] +function get.avatarURL(self) + return self:getAvatarURL() +end + +--[=[@p defaultAvatar number The default avatar for the webhook. See the `defaultAvatar` enumeration for +a human-readable representation. This should always be `defaultAvatar.blurple`.]=] +function get.defaultAvatar() + return defaultAvatar.blurple +end + +--[=[@p defaultAvatarURL string Equivalent to the result of calling `Webhook:getDefaultAvatarURL()`.]=] +function get.defaultAvatarURL(self) + return self:getDefaultAvatarURL() +end + +return Webhook diff --git a/deps/discordia/libs/containers/abstract/Channel.lua b/deps/discordia/libs/containers/abstract/Channel.lua new file mode 100644 index 0000000..d8f4d1a --- /dev/null +++ b/deps/discordia/libs/containers/abstract/Channel.lua @@ -0,0 +1,66 @@ +--[=[ +@c Channel x Snowflake +@t abc +@d Defines the base methods and properties for all Discord channel types. +]=] + +local Snowflake = require('containers/abstract/Snowflake') +local enums = require('enums') + +local format = string.format +local channelType = enums.channelType + +local Channel, get = require('class')('Channel', Snowflake) + +function Channel:__init(data, parent) + Snowflake.__init(self, data, parent) +end + +function Channel:_modify(payload) + local data, err = self.client._api:modifyChannel(self._id, payload) + if data then + self:_load(data) + return true + else + return false, err + end +end + +function Channel:_delete() + local data, err = self.client._api:deleteChannel(self._id) + if data then + local cache + local t = self._type + if t == channelType.text or t == channelType.news then + cache = self._parent._text_channels + elseif t == channelType.private then + cache = self._parent._private_channels + elseif t == channelType.group then + cache = self._parent._group_channels + elseif t == channelType.voice then + cache = self._parent._voice_channels + elseif t == channelType.category then + cache = self._parent._categories + end + if cache then + cache:_delete(self._id) + end + return true + else + return false, err + end +end + +--[=[@p type number The channel type. See the `channelType` enumeration for a +human-readable representation.]=] +function get.type(self) + return self._type +end + +--[=[@p mentionString string A string that, when included in a message content, +may resolve as a link to a channel in the official Discord client.]=] +function get.mentionString(self) + return format('<#%s>', self._id) +end + +return Channel diff --git a/deps/discordia/libs/containers/abstract/Container.lua b/deps/discordia/libs/containers/abstract/Container.lua new file mode 100644 index 0000000..7826267 --- /dev/null +++ b/deps/discordia/libs/containers/abstract/Container.lua @@ -0,0 +1,68 @@ +--[=[ +@c Container +@t abc +@d Defines the base methods and properties for all Discord objects and +structures. Container classes are constructed internally with information +received from Discord and should never be manually constructed. +]=] + +local json = require('json') + +local null = json.null +local format = string.format + +local Container, get = require('class')('Container') + +local types = {['string'] = true, ['number'] = true, ['boolean'] = true} + +local function load(self, data) + -- assert(type(data) == 'table') -- debug + for k, v in pairs(data) do + if types[type(v)] then + self['_' .. k] = v + elseif v == null then + self['_' .. k] = nil + end + end +end + +function Container:__init(data, parent) + -- assert(type(parent) == 'table') -- debug + self._parent = parent + return load(self, data) +end + +--[=[ +@m __eq +@r boolean +@d Defines the behavior of the `==` operator. Allows containers to be directly +compared according to their type and `__hash` return values. +]=] +function Container:__eq(other) + return self.__class == other.__class and self:__hash() == other:__hash() +end + +--[=[ +@m __tostring +@r string +@d Defines the behavior of the `tostring` function. All containers follow the format +`ClassName: hash`. +]=] +function Container:__tostring() + return format('%s: %s', self.__name, self:__hash()) +end + +Container._load = load + +--[=[@p client Client A shortcut to the client object to which this container is visible.]=] +function get.client(self) + return self._parent.client or self._parent +end + +--[=[@p parent Container/Client The parent object of to which this container is +a child. For example, the parent of a role is the guild in which the role exists.]=] +function get.parent(self) + return self._parent +end + +return Container diff --git a/deps/discordia/libs/containers/abstract/GuildChannel.lua b/deps/discordia/libs/containers/abstract/GuildChannel.lua new file mode 100644 index 0000000..3fa4b5f --- /dev/null +++ b/deps/discordia/libs/containers/abstract/GuildChannel.lua @@ -0,0 +1,281 @@ +--[=[ +@c GuildChannel x Channel +@t abc +@d Defines the base methods and properties for all Discord guild channels. +]=] + +local json = require('json') +local enums = require('enums') +local class = require('class') +local Channel = require('containers/abstract/Channel') +local PermissionOverwrite = require('containers/PermissionOverwrite') +local Invite = require('containers/Invite') +local Cache = require('iterables/Cache') +local Resolver = require('client/Resolver') + +local isInstance = class.isInstance +local classes = class.classes +local channelType = enums.channelType + +local insert, sort = table.insert, table.sort +local min, max, floor = math.min, math.max, math.floor +local huge = math.huge + +local GuildChannel, get = class('GuildChannel', Channel) + +function GuildChannel:__init(data, parent) + Channel.__init(self, data, parent) + self.client._channel_map[self._id] = parent + self._permission_overwrites = Cache({}, PermissionOverwrite, self) + return self:_loadMore(data) +end + +function GuildChannel:_load(data) + Channel._load(self, data) + return self:_loadMore(data) +end + +function GuildChannel:_loadMore(data) + return self._permission_overwrites:_load(data.permission_overwrites, true) +end + +--[=[ +@m setName +@t http +@p name string +@r boolean +@d Sets the channel's name. This must be between 2 and 100 characters in length. +]=] +function GuildChannel:setName(name) + return self:_modify({name = name or json.null}) +end + +--[=[ +@m setCategory +@t http +@p id Channel-ID-Resolvable +@r boolean +@d Sets the channel's parent category. +]=] +function GuildChannel:setCategory(id) + id = Resolver.channelId(id) + return self:_modify({parent_id = id or json.null}) +end + +local function sorter(a, b) + if a.position == b.position then + return tonumber(a.id) < tonumber(b.id) + else + return a.position < b.position + end +end + +local function getSortedChannels(self) + + local channels + local t = self._type + if t == channelType.text or t == channelType.news then + channels = self._parent._text_channels + elseif t == channelType.voice then + channels = self._parent._voice_channels + elseif t == channelType.category then + channels = self._parent._categories + end + + local ret = {} + for channel in channels:iter() do + insert(ret, {id = channel._id, position = channel._position}) + end + sort(ret, sorter) + + return ret + +end + +local function setSortedChannels(self, channels) + local data, err = self.client._api:modifyGuildChannelPositions(self._parent._id, channels) + if data then + return true + else + return false, err + end +end + +--[=[ +@m moveUp +@t http +@p n number +@r boolean +@d Moves a channel up its list. The parameter `n` indicates how many spaces the +channel should be moved, clamped to the highest position, with a default of 1 if +it is omitted. This will also normalize the positions of all channels. +]=] +function GuildChannel:moveUp(n) + + n = tonumber(n) or 1 + if n < 0 then + return self:moveDown(-n) + end + + local channels = getSortedChannels(self) + + local new = huge + for i = #channels - 1, 0, -1 do + local v = channels[i + 1] + if v.id == self._id then + new = max(0, i - floor(n)) + v.position = new + elseif i >= new then + v.position = i + 1 + else + v.position = i + end + end + + return setSortedChannels(self, channels) + +end + +--[=[ +@m moveDown +@t http +@p n number +@r boolean +@d Moves a channel down its list. The parameter `n` indicates how many spaces the +channel should be moved, clamped to the lowest position, with a default of 1 if +it is omitted. This will also normalize the positions of all channels. +]=] +function GuildChannel:moveDown(n) + + n = tonumber(n) or 1 + if n < 0 then + return self:moveUp(-n) + end + + local channels = getSortedChannels(self) + + local new = -huge + for i = 0, #channels - 1 do + local v = channels[i + 1] + if v.id == self._id then + new = min(i + floor(n), #channels - 1) + v.position = new + elseif i <= new then + v.position = i - 1 + else + v.position = i + end + end + + return setSortedChannels(self, channels) + +end + +--[=[ +@m createInvite +@t http +@op payload table +@r Invite +@d Creates an invite to the channel. Optional payload fields are: max_age: number +time in seconds until expiration, default = 86400 (24 hours), max_uses: number +total number of uses allowed, default = 0 (unlimited), temporary: boolean whether +the invite grants temporary membership, default = false, unique: boolean whether +a unique code should be guaranteed, default = false +]=] +function GuildChannel:createInvite(payload) + local data, err = self.client._api:createChannelInvite(self._id, payload) + if data then + return Invite(data, self.client) + else + return nil, err + end +end + +--[=[ +@m getInvites +@t http +@r Cache +@d Returns a newly constructed cache of all invite objects for the channel. The +cache and its objects are not automatically updated via gateway events. You must +call this method again to get the updated objects. +]=] +function GuildChannel:getInvites() + local data, err = self.client._api:getChannelInvites(self._id) + if data then + return Cache(data, Invite, self.client) + else + return nil, err + end +end + +--[=[ +@m getPermissionOverwriteFor +@t mem +@p obj Role/Member +@r PermissionOverwrite +@d Returns a permission overwrite object corresponding to the provided member or +role object. If a cached overwrite is not found, an empty overwrite with +zero-permissions is returned instead. Therefore, this can be used to create a +new overwrite when one does not exist. Note that the member or role must exist +in the same guild as the channel does. +]=] +function GuildChannel:getPermissionOverwriteFor(obj) + local id, type + if isInstance(obj, classes.Role) and self._parent == obj._parent then + id, type = obj._id, 'role' + elseif isInstance(obj, classes.Member) and self._parent == obj._parent then + id, type = obj._user._id, 'member' + else + return nil, 'Invalid Role or Member: ' .. tostring(obj) + end + local overwrites = self._permission_overwrites + return overwrites:get(id) or overwrites:_insert(setmetatable({ + id = id, type = type, allow = 0, deny = 0 + }, {__jsontype = 'object'})) +end + +--[=[ +@m delete +@t http +@r boolean +@d Permanently deletes the channel. This cannot be undone! +]=] +function GuildChannel:delete() + return self:_delete() +end + +--[=[@p permissionOverwrites Cache An iterable cache of all overwrites that exist in this channel. To access an +overwrite that may exist, but is not cached, use `GuildChannel:getPermissionOverwriteFor`.]=] +function get.permissionOverwrites(self) + return self._permission_overwrites +end + +--[=[@p name string The name of the channel. This should be between 2 and 100 characters in length.]=] +function get.name(self) + return self._name +end + +--[=[@p position number The position of the channel, where 0 is the highest.]=] +function get.position(self) + return self._position +end + +--[=[@p guild Guild The guild in which this channel exists.]=] +function get.guild(self) + return self._parent +end + +--[=[@p category GuildCategoryChannel/nil The parent channel category that may contain this channel.]=] +function get.category(self) + return self._parent._categories:get(self._parent_id) +end + +--[=[@p private boolean Whether the "everyone" role has permission to view this +channel. In the Discord channel, private text channels are indicated with a lock +icon and private voice channels are not visible.]=] +function get.private(self) + local overwrite = self._permission_overwrites:get(self._parent._id) + return overwrite and overwrite:getDeniedPermissions():has('readMessages') +end + +return GuildChannel diff --git a/deps/discordia/libs/containers/abstract/Snowflake.lua b/deps/discordia/libs/containers/abstract/Snowflake.lua new file mode 100644 index 0000000..8b36de6 --- /dev/null +++ b/deps/discordia/libs/containers/abstract/Snowflake.lua @@ -0,0 +1,63 @@ +--[=[ +@c Snowflake x Container +@t abc +@d Defines the base methods and/or properties for all Discord objects that have +a Snowflake ID. +]=] + +local Date = require('utils/Date') +local Container = require('containers/abstract/Container') + +local Snowflake, get = require('class')('Snowflake', Container) + +function Snowflake:__init(data, parent) + Container.__init(self, data, parent) +end + +--[=[ +@m __hash +@r string +@d Returns `Snowflake.id` +]=] +function Snowflake:__hash() + return self._id +end + +--[=[ +@m getDate +@t mem +@r Date +@d Returns a unique Date object that represents when the object was created by Discord. + +Equivalent to `Date.fromSnowflake(Snowflake.id)` +]=] +function Snowflake:getDate() + return Date.fromSnowflake(self._id) +end + +--[=[@p id string The Snowflake ID that can be used to identify the object. This is guaranteed to +be unique except in cases where an object shares the ID of its parent.]=] +function get.id(self) + return self._id +end + +--[=[@p createdAt number The Unix time in seconds at which this object was created by Discord. Additional +decimal points may be present, though only the first 3 (milliseconds) should be +considered accurate. + +Equivalent to `Date.parseSnowflake(Snowflake.id)`. +]=] +function get.createdAt(self) + return Date.parseSnowflake(self._id) +end + +--[=[@p timestamp string The date and time at which this object was created by Discord, represented as +an ISO 8601 string plus microseconds when available. + +Equivalent to `Date.fromSnowflake(Snowflake.id):toISO()`. +]=] +function get.timestamp(self) + return Date.fromSnowflake(self._id):toISO() +end + +return Snowflake diff --git a/deps/discordia/libs/containers/abstract/TextChannel.lua b/deps/discordia/libs/containers/abstract/TextChannel.lua new file mode 100644 index 0000000..3060a98 --- /dev/null +++ b/deps/discordia/libs/containers/abstract/TextChannel.lua @@ -0,0 +1,337 @@ +--[=[ +@c TextChannel x Channel +@t abc +@d Defines the base methods and properties for all Discord text channels. +]=] + +local pathjoin = require('pathjoin') +local Channel = require('containers/abstract/Channel') +local Message = require('containers/Message') +local WeakCache = require('iterables/WeakCache') +local SecondaryCache = require('iterables/SecondaryCache') +local Resolver = require('client/Resolver') +local fs = require('fs') + +local splitPath = pathjoin.splitPath +local insert, remove, concat = table.insert, table.remove, table.concat +local format = string.format +local readFileSync = fs.readFileSync + +local TextChannel, get = require('class')('TextChannel', Channel) + +function TextChannel:__init(data, parent) + Channel.__init(self, data, parent) + self._messages = WeakCache({}, Message, self) +end + +--[=[ +@m getMessage +@t http +@p id Message-ID-Resolvable +@r Message +@d Gets a message object by ID. If the object is already cached, then the cached +object will be returned; otherwise, an HTTP request is made. +]=] +function TextChannel:getMessage(id) + id = Resolver.messageId(id) + local message = self._messages:get(id) + if message then + return message + else + local data, err = self.client._api:getChannelMessage(self._id, id) + if data then + return self._messages:_insert(data) + else + return nil, err + end + end +end + +--[=[ +@m getFirstMessage +@t http +@r Message +@d Returns the first message found in the channel, if any exist. This is not a +cache shortcut; an HTTP request is made each time this method is called. +]=] +function TextChannel:getFirstMessage() + local data, err = self.client._api:getChannelMessages(self._id, {after = self._id, limit = 1}) + if data then + if data[1] then + return self._messages:_insert(data[1]) + else + return nil, 'Channel has no messages' + end + else + return nil, err + end +end + +--[=[ +@m getLastMessage +@t http +@r Message +@d Returns the last message found in the channel, if any exist. This is not a +cache shortcut; an HTTP request is made each time this method is called. +]=] +function TextChannel:getLastMessage() + local data, err = self.client._api:getChannelMessages(self._id, {limit = 1}) + if data then + if data[1] then + return self._messages:_insert(data[1]) + else + return nil, 'Channel has no messages' + end + else + return nil, err + end +end + +local function getMessages(self, query) + local data, err = self.client._api:getChannelMessages(self._id, query) + if data then + return SecondaryCache(data, self._messages) + else + return nil, err + end +end + +--[=[ +@m getMessages +@t http +@op limit number +@r SecondaryCache +@d Returns a newly constructed cache of between 1 and 100 (default = 50) message +objects found in the channel. While the cache will never automatically gain or +lose objects, the objects that it contains may be updated by gateway events. +]=] +function TextChannel:getMessages(limit) + return getMessages(self, limit and {limit = limit}) +end + +--[=[ +@m getMessagesAfter +@t http +@p id Message-ID-Resolvable +@op limit number +@r SecondaryCache +@d Returns a newly constructed cache of between 1 and 100 (default = 50) message +objects found in the channel after a specific id. While the cache will never +automatically gain or lose objects, the objects that it contains may be updated +by gateway events. +]=] +function TextChannel:getMessagesAfter(id, limit) + id = Resolver.messageId(id) + return getMessages(self, {after = id, limit = limit}) +end + +--[=[ +@m getMessagesBefore +@t http +@p id Message-ID-Resolvable +@op limit number +@r SecondaryCache +@d Returns a newly constructed cache of between 1 and 100 (default = 50) message +objects found in the channel before a specific id. While the cache will never +automatically gain or lose objects, the objects that it contains may be updated +by gateway events. +]=] +function TextChannel:getMessagesBefore(id, limit) + id = Resolver.messageId(id) + return getMessages(self, {before = id, limit = limit}) +end + +--[=[ +@m getMessagesAround +@t http +@p id Message-ID-Resolvable +@op limit number +@r SecondaryCache +@d Returns a newly constructed cache of between 1 and 100 (default = 50) message +objects found in the channel around a specific point. While the cache will never +automatically gain or lose objects, the objects that it contains may be updated +by gateway events. +]=] +function TextChannel:getMessagesAround(id, limit) + id = Resolver.messageId(id) + return getMessages(self, {around = id, limit = limit}) +end + +--[=[ +@m getPinnedMessages +@t http +@r SecondaryCache +@d Returns a newly constructed cache of up to 50 messages that are pinned in the +channel. While the cache will never automatically gain or lose objects, the +objects that it contains may be updated by gateway events. +]=] +function TextChannel:getPinnedMessages() + local data, err = self.client._api:getPinnedMessages(self._id) + if data then + return SecondaryCache(data, self._messages) + else + return nil, err + end +end + +--[=[ +@m broadcastTyping +@t http +@r boolean +@d Indicates in the channel that the client's user "is typing". +]=] +function TextChannel:broadcastTyping() + local data, err = self.client._api:triggerTypingIndicator(self._id) + if data then + return true + else + return false, err + end +end + +local function parseFile(obj, files) + if type(obj) == 'string' then + local data, err = readFileSync(obj) + if not data then + return nil, err + end + files = files or {} + insert(files, {remove(splitPath(obj)), data}) + elseif type(obj) == 'table' and type(obj[1]) == 'string' and type(obj[2]) == 'string' then + files = files or {} + insert(files, obj) + else + return nil, 'Invalid file object: ' .. tostring(obj) + end + return files +end + +local function parseMention(obj, mentions) + if type(obj) == 'table' and obj.mentionString then + mentions = mentions or {} + insert(mentions, obj.mentionString) + else + return nil, 'Unmentionable object: ' .. tostring(obj) + end + return mentions +end + +--[=[ +@m send +@t http +@p content string/table +@r Message +@d Sends a message to the channel. If `content` is a string, then this is simply +sent as the message content. If it is a table, more advanced formatting is +allowed. See [[managing messages]] for more information. +]=] +function TextChannel:send(content) + + local data, err + + if type(content) == 'table' then + + local tbl = content + content = tbl.content + + if type(tbl.code) == 'string' then + content = format('```%s\n%s\n```', tbl.code, content) + elseif tbl.code == true then + content = format('```\n%s\n```', content) + end + + local mentions + if tbl.mention then + mentions, err = parseMention(tbl.mention) + if err then + return nil, err + end + end + if type(tbl.mentions) == 'table' then + for _, mention in ipairs(tbl.mentions) do + mentions, err = parseMention(mention, mentions) + if err then + return nil, err + end + end + end + + if mentions then + insert(mentions, content) + content = concat(mentions, ' ') + end + + local files + if tbl.file then + files, err = parseFile(tbl.file) + if err then + return nil, err + end + end + if type(tbl.files) == 'table' then + for _, file in ipairs(tbl.files) do + files, err = parseFile(file, files) + if err then + return nil, err + end + end + end + + local refMessage, refMention + if tbl.reference then + refMessage = {message_id = Resolver.messageId(tbl.reference.message)} + refMention = { + parse = {'users', 'roles', 'everyone'}, + replied_user = not not tbl.reference.mention, + } + end + + data, err = self.client._api:createMessage(self._id, { + content = content, + tts = tbl.tts, + nonce = tbl.nonce, + embed = tbl.embed, + message_reference = refMessage, + allowed_mentions = refMention, + }, files) + + else + + data, err = self.client._api:createMessage(self._id, {content = content}) + + end + + if data then + return self._messages:_insert(data) + else + return nil, err + end + +end + +--[=[ +@m sendf +@t http +@p content string +@p ... * +@r Message +@d Sends a message to the channel with content formatted with `...` via `string.format` +]=] +function TextChannel:sendf(content, ...) + local data, err = self.client._api:createMessage(self._id, {content = format(content, ...)}) + if data then + return self._messages:_insert(data) + else + return nil, err + end +end + +--[=[@p messages WeakCache An iterable weak cache of all messages that are +visible to the client. Messages that are not referenced elsewhere are eventually +garbage collected. To access a message that may exist but is not cached, +use `TextChannel:getMessage`.]=] +function get.messages(self) + return self._messages +end + +return TextChannel diff --git a/deps/discordia/libs/containers/abstract/UserPresence.lua b/deps/discordia/libs/containers/abstract/UserPresence.lua new file mode 100644 index 0000000..022e615 --- /dev/null +++ b/deps/discordia/libs/containers/abstract/UserPresence.lua @@ -0,0 +1,127 @@ +--[=[ +@c UserPresence x Container +@t abc +@d Defines the base methods and/or properties for classes that represent a +user's current presence information. Note that any method or property that +exists for the User class is also available in the UserPresence class and its +subclasses. +]=] + +local null = require('json').null +local User = require('containers/User') +local Activity = require('containers/Activity') +local Container = require('containers/abstract/Container') + +local UserPresence, get = require('class')('UserPresence', Container) + +function UserPresence:__init(data, parent) + Container.__init(self, data, parent) + self._user = self.client._users:_insert(data.user) +end + +--[=[ +@m __hash +@r string +@d Returns `UserPresence.user.id` +]=] +function UserPresence:__hash() + return self._user._id +end + +local activities = setmetatable({}, {__mode = 'v'}) + +function UserPresence:_loadPresence(presence) + self._status = presence.status + local status = presence.client_status + if status then + self._web_status = status.web + self._mobile_status = status.mobile + self._desktop_status = status.desktop + end + local game = presence.game + if game == null then + self._activity = nil + elseif game then + local arr = presence.activities + if arr and arr[2] then + for i = 2, #arr do + for k, v in pairs(arr[i]) do + game[k] = v + end + end + end + if self._activity then + self._activity:_load(game) + else + local activity = activities[self:__hash()] + if activity then + activity:_load(game) + else + activity = Activity(game, self) + activities[self:__hash()] = activity + end + self._activity = activity + end + end +end + +function get.gameName(self) + self.client:_deprecated(self.__name, 'gameName', 'activity.name') + return self._activity and self._activity._name +end + +function get.gameType(self) + self.client:_deprecated(self.__name, 'gameType', 'activity.type') + return self._activity and self._activity._type +end + +function get.gameURL(self) + self.client:_deprecated(self.__name, 'gameURL', 'activity.url') + return self._activity and self._activity._url +end + +--[=[@p status string The user's overall status (online, dnd, idle, offline).]=] +function get.status(self) + return self._status or 'offline' +end + +--[=[@p webStatus string The user's web status (online, dnd, idle, offline).]=] +function get.webStatus(self) + return self._web_status or 'offline' +end + +--[=[@p mobileStatus string The user's mobile status (online, dnd, idle, offline).]=] +function get.mobileStatus(self) + return self._mobile_status or 'offline' +end + +--[=[@p desktopStatus string The user's desktop status (online, dnd, idle, offline).]=] +function get.desktopStatus(self) + return self._desktop_status or 'offline' +end + +--[=[@p user User The user that this presence represents.]=] +function get.user(self) + return self._user +end + +--[=[@p activity Activity/nil The Activity that this presence represents.]=] +function get.activity(self) + return self._activity +end + +-- user shortcuts + +for k, v in pairs(User) do + UserPresence[k] = UserPresence[k] or function(self, ...) + return v(self._user, ...) + end +end + +for k, v in pairs(User.__getters) do + get[k] = get[k] or function(self) + return v(self._user) + end +end + +return UserPresence diff --git a/deps/discordia/libs/endpoints.lua b/deps/discordia/libs/endpoints.lua new file mode 100644 index 0000000..e893980 --- /dev/null +++ b/deps/discordia/libs/endpoints.lua @@ -0,0 +1,54 @@ +return { + CHANNEL = "/channels/%s", + CHANNEL_INVITES = "/channels/%s/invites", + CHANNEL_MESSAGE = "/channels/%s/messages/%s", + CHANNEL_MESSAGES = "/channels/%s/messages", + CHANNEL_MESSAGES_BULK_DELETE = "/channels/%s/messages/bulk-delete", + CHANNEL_MESSAGE_REACTION = "/channels/%s/messages/%s/reactions/%s", + CHANNEL_MESSAGE_REACTIONS = "/channels/%s/messages/%s/reactions", + CHANNEL_MESSAGE_REACTION_ME = "/channels/%s/messages/%s/reactions/%s/@me", + CHANNEL_MESSAGE_REACTION_USER = "/channels/%s/messages/%s/reactions/%s/%s", + CHANNEL_PERMISSION = "/channels/%s/permissions/%s", + CHANNEL_PIN = "/channels/%s/pins/%s", + CHANNEL_PINS = "/channels/%s/pins", + CHANNEL_RECIPIENT = "/channels/%s/recipients/%s", + CHANNEL_TYPING = "/channels/%s/typing", + CHANNEL_WEBHOOKS = "/channels/%s/webhooks", + GATEWAY = "/gateway", + GATEWAY_BOT = "/gateway/bot", + GUILD = "/guilds/%s", + GUILDS = "/guilds", + GUILD_AUDIT_LOGS = "/guilds/%s/audit-logs", + GUILD_BAN = "/guilds/%s/bans/%s", + GUILD_BANS = "/guilds/%s/bans", + GUILD_CHANNELS = "/guilds/%s/channels", + GUILD_EMBED = "/guilds/%s/embed", + GUILD_EMOJI = "/guilds/%s/emojis/%s", + GUILD_EMOJIS = "/guilds/%s/emojis", + GUILD_INTEGRATION = "/guilds/%s/integrations/%s", + GUILD_INTEGRATIONS = "/guilds/%s/integrations", + GUILD_INTEGRATION_SYNC = "/guilds/%s/integrations/%s/sync", + GUILD_INVITES = "/guilds/%s/invites", + GUILD_MEMBER = "/guilds/%s/members/%s", + GUILD_MEMBERS = "/guilds/%s/members", + GUILD_MEMBER_ME_NICK = "/guilds/%s/members/@me/nick", + GUILD_MEMBER_ROLE = "/guilds/%s/members/%s/roles/%s", + GUILD_PRUNE = "/guilds/%s/prune", + GUILD_REGIONS = "/guilds/%s/regions", + GUILD_ROLE = "/guilds/%s/roles/%s", + GUILD_ROLES = "/guilds/%s/roles", + GUILD_WEBHOOKS = "/guilds/%s/webhooks", + INVITE = "/invites/%s", + OAUTH2_APPLICATION_ME = "/oauth2/applications/@me", + USER = "/users/%s", + USER_ME = "/users/@me", + USER_ME_CHANNELS = "/users/@me/channels", + USER_ME_CONNECTIONS = "/users/@me/connections", + USER_ME_GUILD = "/users/@me/guilds/%s", + USER_ME_GUILDS = "/users/@me/guilds", + VOICE_REGIONS = "/voice/regions", + WEBHOOK = "/webhooks/%s", + WEBHOOK_TOKEN = "/webhooks/%s/%s", + WEBHOOK_TOKEN_GITHUB = "/webhooks/%s/%s/github", + WEBHOOK_TOKEN_SLACK = "/webhooks/%s/%s/slack", +} diff --git a/deps/discordia/libs/enums.lua b/deps/discordia/libs/enums.lua new file mode 100644 index 0000000..f3b4327 --- /dev/null +++ b/deps/discordia/libs/enums.lua @@ -0,0 +1,214 @@ +local function enum(tbl) + local call = {} + for k, v in pairs(tbl) do + if call[v] then + return error(string.format('enum clash for %q and %q', k, call[v])) + end + call[v] = k + end + return setmetatable({}, { + __call = function(_, k) + if call[k] then + return call[k] + else + return error('invalid enumeration: ' .. tostring(k)) + end + end, + __index = function(_, k) + if tbl[k] then + return tbl[k] + else + return error('invalid enumeration: ' .. tostring(k)) + end + end, + __pairs = function() + return next, tbl + end, + __newindex = function() + return error('cannot overwrite enumeration') + end, + }) +end + +local enums = {enum = enum} + +enums.defaultAvatar = enum { + blurple = 0, + gray = 1, + green = 2, + orange = 3, + red = 4, +} + +enums.notificationSetting = enum { + allMessages = 0, + onlyMentions = 1, +} + +enums.channelType = enum { + text = 0, + private = 1, + voice = 2, + group = 3, + category = 4, + news = 5, +} + +enums.webhookType = enum { + incoming = 1, + channelFollower = 2, +} + +enums.messageType = enum { + default = 0, + recipientAdd = 1, + recipientRemove = 2, + call = 3, + channelNameChange = 4, + channelIconchange = 5, + pinnedMessage = 6, + memberJoin = 7, + premiumGuildSubscription = 8, + premiumGuildSubscriptionTier1 = 9, + premiumGuildSubscriptionTier2 = 10, + premiumGuildSubscriptionTier3 = 11, +} + +enums.relationshipType = enum { + none = 0, + friend = 1, + blocked = 2, + pendingIncoming = 3, + pendingOutgoing = 4, +} + +enums.activityType = enum { + default = 0, + streaming = 1, + listening = 2, + custom = 4, +} + +enums.status = enum { + online = 'online', + idle = 'idle', + doNotDisturb = 'dnd', + invisible = 'invisible', +} + +enums.gameType = enum { -- NOTE: deprecated; use activityType + default = 0, + streaming = 1, + listening = 2, + custom = 4, +} + +enums.verificationLevel = enum { + none = 0, + low = 1, + medium = 2, + high = 3, -- (╯°□°)╯︵ ┻━┻ + veryHigh = 4, -- ┻━┻ ミヽ(ಠ益ಠ)ノ彡┻━┻ +} + +enums.explicitContentLevel = enum { + none = 0, + medium = 1, + high = 2, +} + +enums.premiumTier = enum { + none = 0, + tier1 = 1, + tier2 = 2, + tier3 = 3, +} + +enums.permission = enum { + createInstantInvite = 0x00000001, + kickMembers = 0x00000002, + banMembers = 0x00000004, + administrator = 0x00000008, + manageChannels = 0x00000010, + manageGuild = 0x00000020, + addReactions = 0x00000040, + viewAuditLog = 0x00000080, + prioritySpeaker = 0x00000100, + stream = 0x00000200, + readMessages = 0x00000400, + sendMessages = 0x00000800, + sendTextToSpeech = 0x00001000, + manageMessages = 0x00002000, + embedLinks = 0x00004000, + attachFiles = 0x00008000, + readMessageHistory = 0x00010000, + mentionEveryone = 0x00020000, + useExternalEmojis = 0x00040000, + connect = 0x00100000, + speak = 0x00200000, + muteMembers = 0x00400000, + deafenMembers = 0x00800000, + moveMembers = 0x01000000, + useVoiceActivity = 0x02000000, + changeNickname = 0x04000000, + manageNicknames = 0x08000000, + manageRoles = 0x10000000, + manageWebhooks = 0x20000000, + manageEmojis = 0x40000000, +} + +enums.messageFlag = enum { + crossposted = 0x00000001, + isCrosspost = 0x00000002, + suppressEmbeds = 0x00000004, + sourceMessageDeleted = 0x00000008, + urgent = 0x00000010, +} + +enums.actionType = enum { + guildUpdate = 1, + channelCreate = 10, + channelUpdate = 11, + channelDelete = 12, + channelOverwriteCreate = 13, + channelOverwriteUpdate = 14, + channelOverwriteDelete = 15, + memberKick = 20, + memberPrune = 21, + memberBanAdd = 22, + memberBanRemove = 23, + memberUpdate = 24, + memberRoleUpdate = 25, + memberMove = 26, + memberDisconnect = 27, + botAdd = 28, + roleCreate = 30, + roleUpdate = 31, + roleDelete = 32, + inviteCreate = 40, + inviteUpdate = 41, + inviteDelete = 42, + webhookCreate = 50, + webhookUpdate = 51, + webhookDelete = 52, + emojiCreate = 60, + emojiUpdate = 61, + emojiDelete = 62, + messageDelete = 72, + messageBulkDelete = 73, + messagePin = 74, + messageUnpin = 75, + integrationCreate = 80, + integrationUpdate = 81, + integrationDelete = 82, +} + +enums.logLevel = enum { + none = 0, + error = 1, + warning = 2, + info = 3, + debug = 4, +} + +return enums diff --git a/deps/discordia/libs/extensions.lua b/deps/discordia/libs/extensions.lua new file mode 100644 index 0000000..5cb835f --- /dev/null +++ b/deps/discordia/libs/extensions.lua @@ -0,0 +1,253 @@ +--[[ NOTE: +These standard library extensions are NOT used in Discordia. They are here as a +convenience for those who wish to use them. + +There are multiple ways to implement some of these commonly used functions. +Please pay attention to the implementations used here and make sure that they +match your expectations. + +You may freely add to, remove, or edit any of the code here without any effect +on the rest of the library. If you do make changes, do be careful when sharing +your expectations with other users. + +You can inject these extensions into the standard Lua global tables by +calling either the main module (ex: discordia.extensions()) or each sub-module +(ex: discordia.extensions.string()) +]] + +local sort, concat = table.sort, table.concat +local insert, remove = table.insert, table.remove +local byte, char = string.byte, string.char +local gmatch, match = string.gmatch, string.match +local rep, find, sub = string.rep, string.find, string.sub +local min, max, random = math.min, math.max, math.random +local ceil, floor = math.ceil, math.floor + +local table = {} + +function table.count(tbl) + local n = 0 + for _ in pairs(tbl) do + n = n + 1 + end + return n +end + +function table.deepcount(tbl) + local n = 0 + for _, v in pairs(tbl) do + n = type(v) == 'table' and n + table.deepcount(v) or n + 1 + end + return n +end + +function table.copy(tbl) + local ret = {} + for k, v in pairs(tbl) do + ret[k] = v + end + return ret +end + +function table.deepcopy(tbl) + local ret = {} + for k, v in pairs(tbl) do + ret[k] = type(v) == 'table' and table.deepcopy(v) or v + end + return ret +end + +function table.reverse(tbl) + for i = 1, #tbl do + insert(tbl, i, remove(tbl)) + end +end + +function table.reversed(tbl) + local ret = {} + for i = #tbl, 1, -1 do + insert(ret, tbl[i]) + end + return ret +end + +function table.keys(tbl) + local ret = {} + for k in pairs(tbl) do + insert(ret, k) + end + return ret +end + +function table.values(tbl) + local ret = {} + for _, v in pairs(tbl) do + insert(ret, v) + end + return ret +end + +function table.randomipair(tbl) + local i = random(#tbl) + return i, tbl[i] +end + +function table.randompair(tbl) + local rand = random(table.count(tbl)) + local n = 0 + for k, v in pairs(tbl) do + n = n + 1 + if n == rand then + return k, v + end + end +end + +function table.sorted(tbl, fn) + local ret = {} + for i, v in ipairs(tbl) do + ret[i] = v + end + sort(ret, fn) + return ret +end + +function table.search(tbl, value) + for k, v in pairs(tbl) do + if v == value then + return k + end + end + return nil +end + +function table.slice(tbl, start, stop, step) + local ret = {} + for i = start or 1, stop or #tbl, step or 1 do + insert(ret, tbl[i]) + end + return ret +end + +local string = {} + +function string.split(str, delim) + local ret = {} + if not str then + return ret + end + if not delim or delim == '' then + for c in gmatch(str, '.') do + insert(ret, c) + end + return ret + end + local n = 1 + while true do + local i, j = find(str, delim, n) + if not i then break end + insert(ret, sub(str, n, i - 1)) + n = j + 1 + end + insert(ret, sub(str, n)) + return ret +end + +function string.trim(str) + return match(str, '^%s*(.-)%s*$') +end + +function string.pad(str, len, align, pattern) + pattern = pattern or ' ' + if align == 'right' then + return rep(pattern, (len - #str) / #pattern) .. str + elseif align == 'center' then + local pad = 0.5 * (len - #str) / #pattern + return rep(pattern, floor(pad)) .. str .. rep(pattern, ceil(pad)) + else -- left + return str .. rep(pattern, (len - #str) / #pattern) + end +end + +function string.startswith(str, pattern, plain) + local start = 1 + return find(str, pattern, start, plain) == start +end + +function string.endswith(str, pattern, plain) + local start = #str - #pattern + 1 + return find(str, pattern, start, plain) == start +end + +function string.levenshtein(str1, str2) + + if str1 == str2 then return 0 end + + local len1 = #str1 + local len2 = #str2 + + if len1 == 0 then + return len2 + elseif len2 == 0 then + return len1 + end + + local matrix = {} + for i = 0, len1 do + matrix[i] = {[0] = i} + end + for j = 0, len2 do + matrix[0][j] = j + end + + for i = 1, len1 do + for j = 1, len2 do + local cost = byte(str1, i) == byte(str2, j) and 0 or 1 + matrix[i][j] = min(matrix[i-1][j] + 1, matrix[i][j-1] + 1, matrix[i-1][j-1] + cost) + end + end + + return matrix[len1][len2] + +end + +function string.random(len, mn, mx) + local ret = {} + mn = mn or 0 + mx = mx or 255 + for _ = 1, len do + insert(ret, char(random(mn, mx))) + end + return concat(ret) +end + +local math = {} + +function math.clamp(n, minValue, maxValue) + return min(max(n, minValue), maxValue) +end + +function math.round(n, i) + local m = 10 ^ (i or 0) + return floor(n * m + 0.5) / m +end + +local ext = setmetatable({ + table = table, + string = string, + math = math, +}, {__call = function(self) + for _, v in pairs(self) do + v() + end +end}) + +for n, m in pairs(ext) do + setmetatable(m, {__call = function(self) + for k, v in pairs(self) do + _G[n][k] = v + end + end}) +end + +return ext diff --git a/deps/discordia/libs/iterables/ArrayIterable.lua b/deps/discordia/libs/iterables/ArrayIterable.lua new file mode 100644 index 0000000..1a4dede --- /dev/null +++ b/deps/discordia/libs/iterables/ArrayIterable.lua @@ -0,0 +1,108 @@ +--[=[ +@c ArrayIterable x Iterable +@mt mem +@d Iterable class that contains objects in a constant, ordered fashion, although +the order may change if the internal array is modified. Some versions may use a +map function to shape the objects before they are accessed. +]=] + +local Iterable = require('iterables/Iterable') + +local ArrayIterable, get = require('class')('ArrayIterable', Iterable) + +function ArrayIterable:__init(array, map) + self._array = array + self._map = map +end + +function ArrayIterable:__len() + local array = self._array + if not array or #array == 0 then + return 0 + end + local map = self._map + if map then -- map can return nil + return Iterable.__len(self) + else + return #array + end +end + +--[=[@p first * The first object in the array]=] +function get.first(self) + local array = self._array + if not array or #array == 0 then + return nil + end + local map = self._map + if map then + for i = 1, #array, 1 do + local v = array[i] + local obj = v and map(v) + if obj then + return obj + end + end + else + return array[1] + end +end + +--[=[@p last * The last object in the array]=] +function get.last(self) + local array = self._array + if not array or #array == 0 then + return nil + end + local map = self._map + if map then + for i = #array, 1, -1 do + local v = array[i] + local obj = v and map(v) + if obj then + return obj + end + end + else + return array[#array] + end +end + +--[=[ +@m iter +@r function +@d Returns an iterator for all contained objects in a consistent order. +]=] +function ArrayIterable:iter() + local array = self._array + if not array or #array == 0 then + return function() -- new closure for consistency + return nil + end + end + local map = self._map + if map then + local i = 0 + return function() + while true do + i = i + 1 + local v = array[i] + if not v then + return nil + end + v = map(v) + if v then + return v + end + end + end + else + local i = 0 + return function() + i = i + 1 + return array[i] + end + end +end + +return ArrayIterable diff --git a/deps/discordia/libs/iterables/Cache.lua b/deps/discordia/libs/iterables/Cache.lua new file mode 100644 index 0000000..a9c757b --- /dev/null +++ b/deps/discordia/libs/iterables/Cache.lua @@ -0,0 +1,150 @@ +--[=[ +@c Cache x Iterable +@mt mem +@d Iterable class that holds references to Discordia Class objects in no particular order. +]=] + +local json = require('json') +local Iterable = require('iterables/Iterable') + +local null = json.null + +local Cache = require('class')('Cache', Iterable) + +local meta = {__mode = 'v'} + +function Cache:__init(array, constructor, parent) + local objects = {} + for _, data in ipairs(array) do + local obj = constructor(data, parent) + objects[obj:__hash()] = obj + end + self._count = #array + self._objects = objects + self._constructor = constructor + self._parent = parent + self._deleted = setmetatable({}, meta) +end + +function Cache:__pairs() + return next, self._objects +end + +function Cache:__len() + return self._count +end + +local function insert(self, k, obj) + self._objects[k] = obj + self._count = self._count + 1 + return obj +end + +local function remove(self, k, obj) + self._objects[k] = nil + self._deleted[k] = obj + self._count = self._count - 1 + return obj +end + +local function hash(data) + -- local meta = getmetatable(data) -- debug + -- assert(meta and meta.__jsontype == 'object') -- debug + if data.id then -- snowflakes + return data.id + elseif data.user then -- members + return data.user.id + elseif data.emoji then -- reactions + return data.emoji.id ~= null and data.emoji.id or data.emoji.name + elseif data.code then -- invites + return data.code + else + return nil, 'json data could not be hashed' + end +end + +function Cache:_insert(data) + local k = assert(hash(data)) + local old = self._objects[k] + if old then + old:_load(data) + return old + elseif self._deleted[k] then + return insert(self, k, self._deleted[k]) + else + local obj = self._constructor(data, self._parent) + return insert(self, k, obj) + end +end + +function Cache:_remove(data) + local k = assert(hash(data)) + local old = self._objects[k] + if old then + old:_load(data) + return remove(self, k, old) + elseif self._deleted[k] then + return self._deleted[k] + else + return self._constructor(data, self._parent) + end +end + +function Cache:_delete(k) + local old = self._objects[k] + if old then + return remove(self, k, old) + elseif self._deleted[k] then + return self._deleted[k] + else + return nil + end +end + +function Cache:_load(array, update) + if update then + local updated = {} + for _, data in ipairs(array) do + local obj = self:_insert(data) + updated[obj:__hash()] = true + end + for obj in self:iter() do + local k = obj:__hash() + if not updated[k] then + self:_delete(k) + end + end + else + for _, data in ipairs(array) do + self:_insert(data) + end + end +end + +--[=[ +@m get +@p k * +@r * +@d Returns an individual object by key, where the key should match the result of +calling `__hash` on the contained objects. Unlike Iterable:get, this +method operates with O(1) complexity. +]=] +function Cache:get(k) + return self._objects[k] +end + +--[=[ +@m iter +@r function +@d Returns an iterator that returns all contained objects. The order of the objects +is not guaranteed. +]=] +function Cache:iter() + local objects, k, obj = self._objects + return function() + k, obj = next(objects, k) + return obj + end +end + +return Cache diff --git a/deps/discordia/libs/iterables/FilteredIterable.lua b/deps/discordia/libs/iterables/FilteredIterable.lua new file mode 100644 index 0000000..5fdfe1e --- /dev/null +++ b/deps/discordia/libs/iterables/FilteredIterable.lua @@ -0,0 +1,27 @@ +--[=[ +@c FilteredIterable x Iterable +@mt mem +@d Iterable class that wraps another iterable and serves a subset of the objects +that the original iterable contains. +]=] + +local Iterable = require('iterables/Iterable') + +local FilteredIterable = require('class')('FilteredIterable', Iterable) + +function FilteredIterable:__init(base, predicate) + self._base = base + self._predicate = predicate +end + +--[=[ +@m iter +@r function +@d Returns an iterator that returns all contained objects. The order of the objects +is not guaranteed. +]=] +function FilteredIterable:iter() + return self._base:findAll(self._predicate) +end + +return FilteredIterable diff --git a/deps/discordia/libs/iterables/Iterable.lua b/deps/discordia/libs/iterables/Iterable.lua new file mode 100644 index 0000000..ebfb7b4 --- /dev/null +++ b/deps/discordia/libs/iterables/Iterable.lua @@ -0,0 +1,278 @@ +--[=[ +@c Iterable +@mt mem +@d Abstract base class that defines the base methods and properties for a +general purpose data structure with features that are better suited for an +object-oriented environment. + +Note: All sub-classes should implement their own `__init` and `iter` methods and +all stored objects should have a `__hash` method. +]=] + +local random = math.random +local insert, sort, pack = table.insert, table.sort, table.pack + +local Iterable = require('class')('Iterable') + +--[=[ +@m __pairs +@r function +@d Defines the behavior of the `pairs` function. Returns an iterator that returns +a `key, value` pair, where `key` is the result of calling `__hash` on the `value`. +]=] +function Iterable:__pairs() + local gen = self:iter() + return function() + local obj = gen() + if not obj then + return nil + end + return obj:__hash(), obj + end +end + +--[=[ +@m __len +@r function +@d Defines the behavior of the `#` operator. Returns the total number of objects +stored in the iterable. +]=] +function Iterable:__len() + local n = 0 + for _ in self:iter() do + n = n + 1 + end + return n +end + +--[=[ +@m get +@p k * +@r * +@d Returns an individual object by key, where the key should match the result of +calling `__hash` on the contained objects. Operates with up to O(n) complexity. +]=] +function Iterable:get(k) -- objects must be hashable + for obj in self:iter() do + if obj:__hash() == k then + return obj + end + end + return nil +end + +--[=[ +@m find +@p fn function +@r * +@d Returns the first object that satisfies a predicate. +]=] +function Iterable:find(fn) + for obj in self:iter() do + if fn(obj) then + return obj + end + end + return nil +end + +--[=[ +@m findAll +@p fn function +@r function +@d Returns an iterator that returns all objects that satisfy a predicate. +]=] +function Iterable:findAll(fn) + local gen = self:iter() + return function() + while true do + local obj = gen() + if not obj then + return nil + end + if fn(obj) then + return obj + end + end + end +end + +--[=[ +@m forEach +@p fn function +@r nil +@d Iterates through all objects and calls a function `fn` that takes the +objects as an argument. +]=] +function Iterable:forEach(fn) + for obj in self:iter() do + fn(obj) + end +end + +--[=[ +@m random +@r * +@d Returns a random object that is contained in the iterable. +]=] +function Iterable:random() + local n = 1 + local rand = random(#self) + for obj in self:iter() do + if n == rand then + return obj + end + n = n + 1 + end +end + +--[=[ +@m count +@op fn function +@r number +@d If a predicate is provided, this returns the number of objects in the iterable +that satisfy the predicate; otherwise, the total number of objects. +]=] +function Iterable:count(fn) + if not fn then + return self:__len() + end + local n = 0 + for _ in self:findAll(fn) do + n = n + 1 + end + return n +end + +local function sorter(a, b) + local t1, t2 = type(a), type(b) + if t1 == 'string' then + if t2 == 'string' then + local n1 = tonumber(a) + if n1 then + local n2 = tonumber(b) + if n2 then + return n1 < n2 + end + end + return a:lower() < b:lower() + elseif t2 == 'number' then + local n1 = tonumber(a) + if n1 then + return n1 < b + end + return a:lower() < tostring(b) + end + elseif t1 == 'number' then + if t2 == 'number' then + return a < b + elseif t2 == 'string' then + local n2 = tonumber(b) + if n2 then + return a < n2 + end + return tostring(a) < b:lower() + end + end + local m1 = getmetatable(a) + if m1 and m1.__lt then + local m2 = getmetatable(b) + if m2 and m2.__lt then + return a < b + end + end + return tostring(a) < tostring(b) +end + +--[=[ +@m toArray +@op sortBy string +@op fn function +@r table +@d Returns a sequentially-indexed table that contains references to all objects. +If a `sortBy` string is provided, then the table is sorted by that particular +property. If a predicate is provided, then only objects that satisfy it will +be included. +]=] +function Iterable:toArray(sortBy, fn) + local t1 = type(sortBy) + if t1 == 'string' then + fn = type(fn) == 'function' and fn + elseif t1 == 'function' then + fn = sortBy + sortBy = nil + end + local ret = {} + for obj in self:iter() do + if not fn or fn(obj) then + insert(ret, obj) + end + end + if sortBy then + sort(ret, function(a, b) + return sorter(a[sortBy], b[sortBy]) + end) + end + return ret +end + +--[=[ +@m select +@p ... string +@r table +@d Similarly to an SQL query, this returns a sorted Lua table of rows where each +row corresponds to each object in the iterable, and each value in the row is +selected from the objects according to the keys provided. +]=] +function Iterable:select(...) + local rows = {} + local keys = pack(...) + for obj in self:iter() do + local row = {} + for i = 1, keys.n do + row[i] = obj[keys[i]] + end + insert(rows, row) + end + sort(rows, function(a, b) + for i = 1, keys.n do + if a[i] ~= b[i] then + return sorter(a[i], b[i]) + end + end + end) + return rows +end + +--[=[ +@m pick +@p ... string/function +@r function +@d This returns an iterator that, when called, returns the values from each +encountered object, picked by the provided keys. If a key is a string, the objects +are indexed with the string. If a key is a function, the function is called with +the object passed as its first argument. +]=] +function Iterable:pick(...) + local keys = pack(...) + local values = {} + local n = keys.n + local gen = self:iter() + return function() + local obj = gen() + if not obj then + return nil + end + for i = 1, n do + local k = keys[i] + if type(k) == 'function' then + values[i] = k(obj) + else + values[i] = obj[k] + end + end + return unpack(values, 1, n) + end +end + +return Iterable diff --git a/deps/discordia/libs/iterables/SecondaryCache.lua b/deps/discordia/libs/iterables/SecondaryCache.lua new file mode 100644 index 0000000..660fd2c --- /dev/null +++ b/deps/discordia/libs/iterables/SecondaryCache.lua @@ -0,0 +1,78 @@ +--[=[ +@c SecondaryCache x Iterable +@mt mem +@d Iterable class that wraps another cache. Objects added to or removed from a +secondary cache are also automatically added to or removed from the primary +cache that it wraps. +]=] + +local Iterable = require('iterables/Iterable') + +local SecondaryCache = require('class')('SecondaryCache', Iterable) + +function SecondaryCache:__init(array, primary) + local objects = {} + for _, data in ipairs(array) do + local obj = primary:_insert(data) + objects[obj:__hash()] = obj + end + self._count = #array + self._objects = objects + self._primary = primary +end + +function SecondaryCache:__pairs() + return next, self._objects +end + +function SecondaryCache:__len() + return self._count +end + +function SecondaryCache:_insert(data) + local obj = self._primary:_insert(data) + local k = obj:__hash() + if not self._objects[k] then + self._objects[k] = obj + self._count = self._count + 1 + end + return obj +end + +function SecondaryCache:_remove(data) + local obj = self._primary:_insert(data) -- yes, this is correct + local k = obj:__hash() + if self._objects[k] then + self._objects[k] = nil + self._count = self._count - 1 + end + return obj +end + +--[=[ +@m get +@p k * +@r * +@d Returns an individual object by key, where the key should match the result of +calling `__hash` on the contained objects. Unlike the default version, this +method operates with O(1) complexity. +]=] +function SecondaryCache:get(k) + return self._objects[k] +end + +--[=[ +@m iter +@r function +@d Returns an iterator that returns all contained objects. The order of the objects +is not guaranteed. +]=] +function SecondaryCache:iter() + local objects, k, obj = self._objects + return function() + k, obj = next(objects, k) + return obj + end +end + +return SecondaryCache diff --git a/deps/discordia/libs/iterables/TableIterable.lua b/deps/discordia/libs/iterables/TableIterable.lua new file mode 100644 index 0000000..699389d --- /dev/null +++ b/deps/discordia/libs/iterables/TableIterable.lua @@ -0,0 +1,53 @@ +--[=[ +@c TableIterable x Iterable +@mt mem +@d Iterable class that wraps a basic Lua table, where order is not guaranteed. +Some versions may use a map function to shape the objects before they are accessed. +]=] + +local Iterable = require('iterables/Iterable') + +local TableIterable = require('class')('TableIterable', Iterable) + +function TableIterable:__init(tbl, map) + self._tbl = tbl + self._map = map +end + +--[=[ +@m iter +@r function +@d Returns an iterator that returns all contained objects. The order of the objects is not guaranteed. +]=] +function TableIterable:iter() + local tbl = self._tbl + if not tbl then + return function() + return nil + end + end + local map = self._map + if map then + local k, v + return function() + while true do + k, v = next(tbl, k) + if not v then + return nil + end + v = map(v) + if v then + return v + end + end + end + else + local k, v + return function() + k, v = next(tbl, k) + return v + end + end +end + +return TableIterable diff --git a/deps/discordia/libs/iterables/WeakCache.lua b/deps/discordia/libs/iterables/WeakCache.lua new file mode 100644 index 0000000..8d89ba8 --- /dev/null +++ b/deps/discordia/libs/iterables/WeakCache.lua @@ -0,0 +1,25 @@ +--[=[ +@c WeakCache x Cache +@mt mem +@d Extends the functionality of a regular cache by making use of weak references +to the objects that are cached. If all references to an object are weak, as they +are here, then the object will be deleted on the next garbage collection cycle. +]=] + +local Cache = require('iterables/Cache') +local Iterable = require('iterables/Iterable') + +local WeakCache = require('class')('WeakCache', Cache) + +local meta = {__mode = 'v'} + +function WeakCache:__init(array, constructor, parent) + Cache.__init(self, array, constructor, parent) + setmetatable(self._objects, meta) +end + +function WeakCache:__len() -- NOTE: _count is not accurate for weak caches + return Iterable.__len(self) +end + +return WeakCache diff --git a/deps/discordia/libs/utils/Clock.lua b/deps/discordia/libs/utils/Clock.lua new file mode 100644 index 0000000..892abc2 --- /dev/null +++ b/deps/discordia/libs/utils/Clock.lua @@ -0,0 +1,56 @@ +--[=[ +@c Clock x Emitter +@t ui +@mt mem +@d Used to periodically execute code according to the ticking of the system clock instead of an arbitrary interval. +]=] + +local timer = require('timer') +local Emitter = require('utils/Emitter') + +local date = os.date +local setInterval, clearInterval = timer.setInterval, timer.clearInterval + +local Clock = require('class')('Clock', Emitter) + +function Clock:__init() + Emitter.__init(self) +end + +--[=[ +@m start +@op utc boolean +@r nil +@d Starts the main loop for the clock. If a truthy argument is passed, then UTC +time is used; otherwise, local time is used. As the clock ticks, an event is +emitted for every `os.date` value change. The event name is the key of the value +that changed and the event argument is the corresponding date table. +]=] +function Clock:start(utc) + if self._interval then return end + local fmt = utc and '!*t' or '*t' + local prev = date(fmt) + self._interval = setInterval(1000, function() + local now = date(fmt) + for k, v in pairs(now) do + if v ~= prev[k] then + self:emit(k, now) + end + end + prev = now + end) +end + +--[=[ +@m stop +@r nil +@d Stops the main loop for the clock. +]=] +function Clock:stop() + if self._interval then + clearInterval(self._interval) + self._interval = nil + end +end + +return Clock diff --git a/deps/discordia/libs/utils/Color.lua b/deps/discordia/libs/utils/Color.lua new file mode 100644 index 0000000..3d01bd9 --- /dev/null +++ b/deps/discordia/libs/utils/Color.lua @@ -0,0 +1,313 @@ +--[=[ +@c Color +@t ui +@mt mem +@p value number +@d Wrapper for 24-bit colors packed as a decimal value. See the static constructors for more information. +]=] + +local class = require('class') + +local format = string.format +local min, max, abs, floor = math.min, math.max, math.abs, math.floor +local lshift, rshift = bit.lshift, bit.rshift +local band, bor = bit.band, bit.bor +local bnot = bit.bnot +local isInstance = class.isInstance + +local Color, get = class('Color') + +local function check(self, other) + if not isInstance(self, Color) or not isInstance(other, Color) then + return error('Cannot perform operation with non-Color object', 2) + end +end + +local function clamp(n, mn, mx) + return min(max(n, mn), mx) +end + +function Color:__init(value) + value = tonumber(value) + self._value = value and band(value, 0xFFFFFF) or 0 +end + +function Color:__tostring() + return format('Color: %s (%i, %i, %i)', self:toHex(), self:toRGB()) +end + +function Color:__eq(other) check(self, other) + return self._value == other._value +end + +function Color:__add(other) check(self, other) + local r = clamp(self.r + other.r, 0, 0xFF) + local g = clamp(self.g + other.g, 0, 0xFF) + local b = clamp(self.b + other.b, 0, 0xFF) + return Color.fromRGB(r, g, b) +end + +function Color:__sub(other) check(self, other) + local r = clamp(self.r - other.r, 0, 0xFF) + local g = clamp(self.g - other.g, 0, 0xFF) + local b = clamp(self.b - other.b, 0, 0xFF) + return Color.fromRGB(r, g, b) +end + +function Color:__mul(other) + if not isInstance(self, Color) then + self, other = other, self + end + other = tonumber(other) + if other then + local r = clamp(self.r * other, 0, 0xFF) + local g = clamp(self.g * other, 0, 0xFF) + local b = clamp(self.b * other, 0, 0xFF) + return Color.fromRGB(r, g, b) + else + return error('Cannot perform operation with non-numeric object') + end +end + +function Color:__div(other) + if not isInstance(self, Color) then + return error('Division with Color is not commutative') + end + other = tonumber(other) + if other then + local r = clamp(self.r / other, 0, 0xFF) + local g = clamp(self.g / other, 0, 0xFF) + local b = clamp(self.b / other, 0, 0xFF) + return Color.fromRGB(r, g, b) + else + return error('Cannot perform operation with non-numeric object') + end +end + +--[=[ +@m fromHex +@t static +@p hex string +@r Color +@d Constructs a new Color object from a hexadecimal string. The string may or may +not be prefixed by `#`; all other characters are interpreted as a hex string. +]=] +function Color.fromHex(hex) + return Color(tonumber(hex:match('#?(.*)'), 16)) +end + +--[=[ +@m fromRGB +@t static +@p r number +@p g number +@p b number +@r Color +@d Constructs a new Color object from RGB values. Values are allowed to overflow +though one component will not overflow to the next component. +]=] +function Color.fromRGB(r, g, b) + r = band(lshift(r, 16), 0xFF0000) + g = band(lshift(g, 8), 0x00FF00) + b = band(b, 0x0000FF) + return Color(bor(bor(r, g), b)) +end + +local function fromHue(h, c, m) + local x = c * (1 - abs(h / 60 % 2 - 1)) + local r, g, b + if 0 <= h and h < 60 then + r, g, b = c, x, 0 + elseif 60 <= h and h < 120 then + r, g, b = x, c, 0 + elseif 120 <= h and h < 180 then + r, g, b = 0, c, x + elseif 180 <= h and h < 240 then + r, g, b = 0, x, c + elseif 240 <= h and h < 300 then + r, g, b = x, 0, c + elseif 300 <= h and h < 360 then + r, g, b = c, 0, x + end + r = (r + m) * 0xFF + g = (g + m) * 0xFF + b = (b + m) * 0xFF + return r, g, b +end + +local function toHue(r, g, b) + r = r / 0xFF + g = g / 0xFF + b = b / 0xFF + local mn = min(r, g, b) + local mx = max(r, g, b) + local d = mx - mn + local h + if d == 0 then + h = 0 + elseif mx == r then + h = (g - b) / d % 6 + elseif mx == g then + h = (b - r) / d + 2 + elseif mx == b then + h = (r - g) / d + 4 + end + h = floor(h * 60 + 0.5) + return h, d, mx, mn +end + +--[=[ +@m fromHSV +@t static +@p h number +@p s number +@p v number +@r Color +@d Constructs a new Color object from HSV values. Hue is allowed to overflow +while saturation and value are clamped to [0, 1]. +]=] +function Color.fromHSV(h, s, v) + h = h % 360 + s = clamp(s, 0, 1) + v = clamp(v, 0, 1) + local c = v * s + local m = v - c + local r, g, b = fromHue(h, c, m) + return Color.fromRGB(r, g, b) +end + +--[=[ +@m fromHSL +@t static +@p h number +@p s number +@p l number +@r Color +@d Constructs a new Color object from HSL values. Hue is allowed to overflow +while saturation and lightness are clamped to [0, 1]. +]=] +function Color.fromHSL(h, s, l) + h = h % 360 + s = clamp(s, 0, 1) + l = clamp(l, 0, 1) + local c = (1 - abs(2 * l - 1)) * s + local m = l - c * 0.5 + local r, g, b = fromHue(h, c, m) + return Color.fromRGB(r, g, b) +end + +--[=[ +@m toHex +@r string +@d Returns a 6-digit hexadecimal string that represents the color value. +]=] +function Color:toHex() + return format('#%06X', self._value) +end + +--[=[ +@m toRGB +@r number +@r number +@r number +@d Returns the red, green, and blue values that are packed into the color value. +]=] +function Color:toRGB() + return self.r, self.g, self.b +end + +--[=[ +@m toHSV +@r number +@r number +@r number +@d Returns the hue, saturation, and value that represents the color value. +]=] +function Color:toHSV() + local h, d, mx = toHue(self.r, self.g, self.b) + local v = mx + local s = mx == 0 and 0 or d / mx + return h, s, v +end + +--[=[ +@m toHSL +@r number +@r number +@r number +@d Returns the hue, saturation, and lightness that represents the color value. +]=] +function Color:toHSL() + local h, d, mx, mn = toHue(self.r, self.g, self.b) + local l = (mx + mn) * 0.5 + local s = d == 0 and 0 or d / (1 - abs(2 * l - 1)) + return h, s, l +end + +--[=[@p value number The raw decimal value that represents the color value.]=] +function get.value(self) + return self._value +end + +local function getByte(value, offset) + return band(rshift(value, offset), 0xFF) +end + +--[=[@p r number The value that represents the color's red-level.]=] +function get.r(self) + return getByte(self._value, 16) +end + +--[=[@p g number The value that represents the color's green-level.]=] +function get.g(self) + return getByte(self._value, 8) +end + +--[=[@p b number The value that represents the color's blue-level.]=] +function get.b(self) + return getByte(self._value, 0) +end + +local function setByte(value, offset, new) + local byte = lshift(0xFF, offset) + value = band(value, bnot(byte)) + return bor(value, band(lshift(new, offset), byte)) +end + +--[=[ +@m setRed +@r nil +@d Sets the color's red-level. +]=] +function Color:setRed(r) + self._value = setByte(self._value, 16, r) +end + +--[=[ +@m setGreen +@r nil +@d Sets the color's green-level. +]=] +function Color:setGreen(g) + self._value = setByte(self._value, 8, g) +end + +--[=[ +@m setBlue +@r nil +@d Sets the color's blue-level. +]=] +function Color:setBlue(b) + self._value = setByte(self._value, 0, b) +end + +--[=[ +@m copy +@r Color +@d Returns a new copy of the original color object. +]=] +function Color:copy() + return Color(self._value) +end + +return Color diff --git a/deps/discordia/libs/utils/Date.lua b/deps/discordia/libs/utils/Date.lua new file mode 100644 index 0000000..ec2ddfe --- /dev/null +++ b/deps/discordia/libs/utils/Date.lua @@ -0,0 +1,394 @@ +--[=[ +@c Date +@t ui +@mt mem +@op seconds number +@op microseconds number +@d Represents a single moment in time and provides utilities for converting to +and from different date and time formats. Although microsecond precision is available, +most formats are implemented with only second precision. +]=] + +local class = require('class') +local constants = require('constants') +local Time = require('utils/Time') + +local abs, modf, fmod, floor = math.abs, math.modf, math.fmod, math.floor +local format = string.format +local date, time, difftime = os.date, os.time, os.difftime +local isInstance = class.isInstance + +local MS_PER_S = constants.MS_PER_S +local US_PER_MS = constants.US_PER_MS +local US_PER_S = US_PER_MS * MS_PER_S + +local DISCORD_EPOCH = constants.DISCORD_EPOCH + +local months = { + Jan = 1, Feb = 2, Mar = 3, Apr = 4, May = 5, Jun = 6, + Jul = 7, Aug = 8, Sep = 9, Oct = 10, Nov = 11, Dec = 12 +} + +local function offset() -- difference between *t and !*t + return difftime(time(), time(date('!*t'))) +end + +local Date = class('Date') + +local function check(self, other) + if not isInstance(self, Date) or not isInstance(other, Date) then + return error('Cannot perform operation with non-Date object', 2) + end +end + +function Date:__init(seconds, micro) + + local f + seconds = tonumber(seconds) + if seconds then + seconds, f = modf(seconds) + else + seconds = time() + end + + micro = tonumber(micro) + if micro then + seconds = seconds + modf(micro / US_PER_S) + micro = fmod(micro, US_PER_S) + else + micro = 0 + end + + if f and f > 0 then + micro = micro + US_PER_S * f + end + + self._s = seconds + self._us = floor(micro + 0.5) + +end + +function Date:__tostring() + return 'Date: ' .. self:toString() +end + +--[=[ +@m toString +@op fmt string +@r string +@d Returns a string from this Date object via Lua's `os.date`. +If no format string is provided, the default is '%a %b %d %Y %T GMT%z (%Z)'. +]=] +function Date:toString(fmt) + if not fmt or fmt == '*t' or fmt == '!*t' then + fmt = '%a %b %d %Y %T GMT%z (%Z)' + end + return date(fmt, self._s) +end + +function Date:__eq(other) check(self, other) + return self._s == other._s and self._us == other._us +end + +function Date:__lt(other) check(self, other) + return self:toMicroseconds() < other:toMicroseconds() +end + +function Date:__le(other) check(self, other) + return self:toMicroseconds() <= other:toMicroseconds() +end + +function Date:__add(other) + if not isInstance(self, Date) then + self, other = other, self + end + if not isInstance(other, Time) then + return error('Cannot perform operation with non-Time object') + end + return Date(self:toSeconds() + other:toSeconds()) +end + +function Date:__sub(other) + if isInstance(self, Date) then + if isInstance(other, Date) then + return Time(abs(self:toMilliseconds() - other:toMilliseconds())) + elseif isInstance(other, Time) then + return Date(self:toSeconds() - other:toSeconds()) + else + return error('Cannot perform operation with non-Date/Time object') + end + else + return error('Cannot perform operation with non-Date object') + end +end + +--[=[ +@m parseISO +@t static +@p str string +@r number +@r number +@d Converts an ISO 8601 string into a Unix time in seconds. For compatibility +with Discord's timestamp format, microseconds are also provided as a second +return value. +]=] +function Date.parseISO(str) + local year, month, day, hour, min, sec, other = str:match( + '(%d+)-(%d+)-(%d+).(%d+):(%d+):(%d+)(.*)' + ) + other = other:match('%.%d+') + return Date.parseTableUTC { + day = day, month = month, year = year, + hour = hour, min = min, sec = sec, isdst = false, + }, other and other * US_PER_S or 0 +end + +--[=[ +@m parseHeader +@t static +@p str string +@r number +@d Converts an RFC 2822 string (an HTTP Date header) into a Unix time in seconds. +]=] +function Date.parseHeader(str) + local day, month, year, hour, min, sec = str:match( + '%a+, (%d+) (%a+) (%d+) (%d+):(%d+):(%d+) GMT' + ) + return Date.parseTableUTC { + day = day, month = months[month], year = year, + hour = hour, min = min, sec = sec, isdst = false, + } +end + +--[=[ +@m parseSnowflake +@t static +@p id string +@r number +@d Converts a Discord Snowflake ID into a Unix time in seconds. Additional +decimal points may be present, though only the first 3 (milliseconds) should be +considered accurate. +]=] +function Date.parseSnowflake(id) + return (id / 2^22 + DISCORD_EPOCH) / MS_PER_S +end + +--[=[ +@m parseTable +@t static +@p tbl table +@r number +@d Interprets a Lua date table as a local time and converts it to a Unix time in +seconds. Equivalent to `os.time(tbl)`. +]=] +function Date.parseTable(tbl) + return time(tbl) +end + +--[=[ +@m parseTableUTC +@t static +@p tbl table +@r number +@d Interprets a Lua date table as a UTC time and converts it to a Unix time in +seconds. Equivalent to `os.time(tbl)` with a correction for UTC. +]=] +function Date.parseTableUTC(tbl) + return time(tbl) + offset() +end + +--[=[ +@m fromISO +@t static +@p str string +@r Date +@d Constructs a new Date object from an ISO 8601 string. Equivalent to +`Date(Date.parseISO(str))`. +]=] +function Date.fromISO(str) + return Date(Date.parseISO(str)) +end + +--[=[ +@m fromHeader +@t static +@p str string +@r Date +@d Constructs a new Date object from an RFC 2822 string. Equivalent to +`Date(Date.parseHeader(str))`. +]=] +function Date.fromHeader(str) + return Date(Date.parseHeader(str)) +end + +--[=[ +@m fromSnowflake +@t static +@p id string +@r Date +@d Constructs a new Date object from a Discord/Twitter Snowflake ID. Equivalent to +`Date(Date.parseSnowflake(id))`. +]=] +function Date.fromSnowflake(id) + return Date(Date.parseSnowflake(id)) +end + +--[=[ +@m fromTable +@t static +@p tbl table +@r Date +@d Constructs a new Date object from a Lua date table interpreted as a local time. +Equivalent to `Date(Date.parseTable(tbl))`. +]=] +function Date.fromTable(tbl) + return Date(Date.parseTable(tbl)) +end + +--[=[ +@m fromTableUTC +@t static +@p tbl table +@r Date +@d Constructs a new Date object from a Lua date table interpreted as a UTC time. +Equivalent to `Date(Date.parseTableUTC(tbl))`. +]=] +function Date.fromTableUTC(tbl) + return Date(Date.parseTableUTC(tbl)) +end + +--[=[ +@m fromSeconds +@t static +@p s number +@r Date +@d Constructs a new Date object from a Unix time in seconds. +]=] +function Date.fromSeconds(s) + return Date(s) +end + +--[=[ +@m fromMilliseconds +@t static +@p ms number +@r Date +@d Constructs a new Date object from a Unix time in milliseconds. +]=] +function Date.fromMilliseconds(ms) + return Date(ms / MS_PER_S) +end + +--[=[ +@m fromMicroseconds +@t static +@p us number +@r Date +@d Constructs a new Date object from a Unix time in microseconds. +]=] +function Date.fromMicroseconds(us) + return Date(0, us) +end + +--[=[ +@m toISO +@op sep string +@op tz string +@r string +@d Returns an ISO 8601 string that represents the stored date and time. +If `sep` and `tz` are both provided, then they are used as a custom separator +and timezone; otherwise, `T` is used for the separator and `+00:00` is used for +the timezone, plus microseconds if available. +]=] +function Date:toISO(sep, tz) + if sep and tz then + local ret = date('!%F%%s%T%%s', self._s) + return format(ret, sep, tz) + else + if self._us == 0 then + return date('!%FT%T', self._s) .. '+00:00' + else + return date('!%FT%T', self._s) .. format('.%06i+00:00', self._us) + end + end +end + +--[=[ +@m toHeader +@r string +@d Returns an RFC 2822 string that represents the stored date and time. +]=] +function Date:toHeader() + return date('!%a, %d %b %Y %T GMT', self._s) +end + +--[=[ +@m toSnowflake +@r string +@d Returns a synthetic Discord Snowflake ID based on the stored date and time. +Due to the lack of native 64-bit support, the result may lack precision. +In other words, `Date.fromSnowflake(id):toSnowflake() == id` may be `false`. +]=] +function Date:toSnowflake() + local n = (self:toMilliseconds() - DISCORD_EPOCH) * 2^22 + return format('%f', n):match('%d*') +end + +--[=[ +@m toTable +@r table +@d Returns a Lua date table that represents the stored date and time as a local +time. Equivalent to `os.date('*t', s)` where `s` is the Unix time in seconds. +]=] +function Date:toTable() + return date('*t', self._s) +end + +--[=[ +@m toTableUTC +@r table +@d Returns a Lua date table that represents the stored date and time as a UTC +time. Equivalent to `os.date('!*t', s)` where `s` is the Unix time in seconds. +]=] +function Date:toTableUTC() + return date('!*t', self._s) +end + +--[=[ +@m toSeconds +@r number +@d Returns a Unix time in seconds that represents the stored date and time. +]=] +function Date:toSeconds() + return self._s + self._us / US_PER_S +end + +--[=[ +@m toMilliseconds +@r number +@d Returns a Unix time in milliseconds that represents the stored date and time. +]=] +function Date:toMilliseconds() + return self._s * MS_PER_S + self._us / US_PER_MS +end + +--[=[ +@m toMicroseconds +@r number +@d Returns a Unix time in microseconds that represents the stored date and time. +]=] +function Date:toMicroseconds() + return self._s * US_PER_S + self._us +end + +--[=[ +@m toParts +@r number +@r number +@d Returns the seconds and microseconds that are stored in the date object. +]=] +function Date:toParts() + return self._s, self._us +end + +return Date diff --git a/deps/discordia/libs/utils/Deque.lua b/deps/discordia/libs/utils/Deque.lua new file mode 100644 index 0000000..bfdabc0 --- /dev/null +++ b/deps/discordia/libs/utils/Deque.lua @@ -0,0 +1,105 @@ +--[=[ +@c Deque +@t ui +@mt mem +@d An implementation of a double-ended queue. +]=] + +local Deque = require('class')('Deque') + +function Deque:__init() + self._objects = {} + self._first = 0 + self._last = -1 +end + +--[=[ +@m getCount +@r number +@d Returns the total number of values stored. +]=] +function Deque:getCount() + return self._last - self._first + 1 +end + +--[=[ +@m pushLeft +@p obj * +@r nil +@d Adds a value of any type to the left side of the deque. +]=] +function Deque:pushLeft(obj) + self._first = self._first - 1 + self._objects[self._first] = obj +end + +--[=[ +@m pushRight +@p obj * +@r nil +@d Adds a value of any type to the right side of the deque. +]=] +function Deque:pushRight(obj) + self._last = self._last + 1 + self._objects[self._last] = obj +end + +--[=[ +@m popLeft +@r * +@d Removes and returns a value from the left side of the deque. +]=] +function Deque:popLeft() + if self._first > self._last then return nil end + local obj = self._objects[self._first] + self._objects[self._first] = nil + self._first = self._first + 1 + return obj +end + +--[=[ +@m popRight +@r * +@d Removes and returns a value from the right side of the deque. +]=] +function Deque:popRight() + if self._first > self._last then return nil end + local obj = self._objects[self._last] + self._objects[self._last] = nil + self._last = self._last - 1 + return obj +end + +--[=[ +@m peekLeft +@r * +@d Returns the value at the left side of the deque without removing it. +]=] +function Deque:peekLeft() + return self._objects[self._first] +end + +--[=[ +@m peekRight +@r * +@d Returns the value at the right side of the deque without removing it. +]=] +function Deque:peekRight() + return self._objects[self._last] +end + +--[=[ +@m iter +@r function +@d Iterates over the deque from left to right. +]=] +function Deque:iter() + local t = self._objects + local i = self._first - 1 + return function() + i = i + 1 + return t[i] + end +end + +return Deque diff --git a/deps/discordia/libs/utils/Emitter.lua b/deps/discordia/libs/utils/Emitter.lua new file mode 100644 index 0000000..d7a3972 --- /dev/null +++ b/deps/discordia/libs/utils/Emitter.lua @@ -0,0 +1,226 @@ +--[=[ +@c Emitter +@t ui +@mt mem +@d Implements an asynchronous event emitter where callbacks can be subscribed to +specific named events. When events are emitted, the callbacks are called in the +order that they were originally registered. +]=] + +local timer = require('timer') + +local wrap, yield = coroutine.wrap, coroutine.yield +local resume, running = coroutine.resume, coroutine.running +local insert, remove = table.insert, table.remove +local setTimeout, clearTimeout = timer.setTimeout, timer.clearTimeout + +local Emitter = require('class')('Emitter') + +function Emitter:__init() + self._listeners = {} +end + +local function new(self, name, listener) + local listeners = self._listeners[name] + if not listeners then + listeners = {} + self._listeners[name] = listeners + end + insert(listeners, listener) + return listener.fn +end + +--[=[ +@m on +@p name string +@p fn function +@r function +@d Subscribes a callback to be called every time the named event is emitted. +Callbacks registered with this method will automatically be wrapped as a new +coroutine when they are called. Returns the original callback for convenience. +]=] +function Emitter:on(name, fn) + return new(self, name, {fn = fn}) +end + +--[=[ +@m once +@p name string +@p fn function +@r function +@d Subscribes a callback to be called only the first time this event is emitted. +Callbacks registered with this method will automatically be wrapped as a new +coroutine when they are called. Returns the original callback for convenience. +]=] +function Emitter:once(name, fn) + return new(self, name, {fn = fn, once = true}) +end + +--[=[ +@m onSync +@p name string +@p fn function +@r function +@d Subscribes a callback to be called every time the named event is emitted. +Callbacks registered with this method are not automatically wrapped as a +coroutine. Returns the original callback for convenience. +]=] +function Emitter:onSync(name, fn) + return new(self, name, {fn = fn, sync = true}) +end + +--[=[ +@m onceSync +@p name string +@p fn function +@r function +@d Subscribes a callback to be called only the first time this event is emitted. +Callbacks registered with this method are not automatically wrapped as a coroutine. +Returns the original callback for convenience. +]=] +function Emitter:onceSync(name, fn) + return new(self, name, {fn = fn, once = true, sync = true}) +end + +--[=[ +@m emit +@p name string +@op ... * +@r nil +@d Emits the named event and a variable number of arguments to pass to the event callbacks. +]=] +function Emitter:emit(name, ...) + local listeners = self._listeners[name] + if not listeners then return end + for i = 1, #listeners do + local listener = listeners[i] + if listener then + local fn = listener.fn + if listener.once then + listeners[i] = false + end + if listener.sync then + fn(...) + else + wrap(fn)(...) + end + end + end + if listeners._removed then + for i = #listeners, 1, -1 do + if not listeners[i] then + remove(listeners, i) + end + end + if #listeners == 0 then + self._listeners[name] = nil + end + listeners._removed = nil + end +end + +--[=[ +@m getListeners +@p name string +@r function +@d Returns an iterator for all callbacks registered to the named event. +]=] +function Emitter:getListeners(name) + local listeners = self._listeners[name] + if not listeners then return function() end end + local i = 0 + return function() + while i < #listeners do + i = i + 1 + if listeners[i] then + return listeners[i].fn + end + end + end +end + +--[=[ +@m getListenerCount +@p name string +@r number +@d Returns the number of callbacks registered to the named event. +]=] +function Emitter:getListenerCount(name) + local listeners = self._listeners[name] + if not listeners then return 0 end + local n = 0 + for _, listener in ipairs(listeners) do + if listener then + n = n + 1 + end + end + return n +end + +--[=[ +@m removeListener +@p name string +@p fn function +@r nil +@d Unregisters all instances of the callback from the named event. +]=] +function Emitter:removeListener(name, fn) + local listeners = self._listeners[name] + if not listeners then return end + for i, listener in ipairs(listeners) do + if listener and listener.fn == fn then + listeners[i] = false + end + end + listeners._removed = true +end + +--[=[ +@m removeAllListeners +@p name string/nil +@r nil +@d Unregisters all callbacks for the emitter. If a name is passed, then only +callbacks for that specific event are unregistered. +]=] +function Emitter:removeAllListeners(name) + if name then + self._listeners[name] = nil + else + for k in pairs(self._listeners) do + self._listeners[k] = nil + end + end +end + +--[=[ +@m waitFor +@p name string +@op timeout number +@op predicate function +@r boolean +@r ... +@d When called inside of a coroutine, this will yield the coroutine until the +named event is emitted. If a timeout (in milliseconds) is provided, the function +will return after the time expires, regardless of whether the event is emitted, +and `false` will be returned; otherwise, `true` is returned. If a predicate is +provided, events that do not pass the predicate will be ignored. +]=] +function Emitter:waitFor(name, timeout, predicate) + local thread = running() + local fn + fn = self:onSync(name, function(...) + if predicate and not predicate(...) then return end + if timeout then + clearTimeout(timeout) + end + self:removeListener(name, fn) + return assert(resume(thread, true, ...)) + end) + timeout = timeout and setTimeout(timeout, function() + self:removeListener(name, fn) + return assert(resume(thread, false)) + end) + return yield() +end + +return Emitter diff --git a/deps/discordia/libs/utils/Logger.lua b/deps/discordia/libs/utils/Logger.lua new file mode 100644 index 0000000..cc582ec --- /dev/null +++ b/deps/discordia/libs/utils/Logger.lua @@ -0,0 +1,82 @@ +--[=[ +@c Logger +@t ui +@mt mem +@p level number +@p dateTime string +@op file string +@d Used to log formatted messages to stdout (the console) or to a file. +The `dateTime` argument should be a format string that is accepted by `os.date`. +The file argument should be a relative or absolute file path or `nil` if no log +file is desired. See the `logLevel` enumeration for acceptable log level values. +]=] + +local fs = require('fs') + +local date = os.date +local format = string.format +local stdout = _G.process.stdout.handle +local openSync, writeSync = fs.openSync, fs.writeSync + +-- local BLACK = 30 +local RED = 31 +local GREEN = 32 +local YELLOW = 33 +-- local BLUE = 34 +-- local MAGENTA = 35 +local CYAN = 36 +-- local WHITE = 37 + +local config = { + {'[ERROR] ', RED}, + {'[WARNING]', YELLOW}, + {'[INFO] ', GREEN}, + {'[DEBUG] ', CYAN}, +} + +do -- parse config + local bold = 1 + for _, v in ipairs(config) do + v[2] = format('\27[%i;%im%s\27[0m', bold, v[2], v[1]) + end +end + +local Logger = require('class')('Logger') + +function Logger:__init(level, dateTime, file) + self._level = level + self._dateTime = dateTime + self._file = file and openSync(file, 'a') +end + +--[=[ +@m log +@p level number +@p msg string +@p ... * +@r string +@d If the provided level is less than or equal to the log level set on +initialization, this logs a message to stdout as defined by Luvit's `process` +module and to a file if one was provided on initialization. The `msg, ...` pair +is formatted according to `string.format` and returned if the message is logged. +]=] +function Logger:log(level, msg, ...) + + if self._level < level then return end + + local tag = config[level] + if not tag then return end + + msg = format(msg, ...) + + local d = date(self._dateTime) + if self._file then + writeSync(self._file, -1, format('%s | %s | %s\n', d, tag[1], msg)) + end + stdout:write(format('%s | %s | %s\n', d, tag[2], msg)) + + return msg + +end + +return Logger diff --git a/deps/discordia/libs/utils/Mutex.lua b/deps/discordia/libs/utils/Mutex.lua new file mode 100644 index 0000000..fd57580 --- /dev/null +++ b/deps/discordia/libs/utils/Mutex.lua @@ -0,0 +1,68 @@ +--[=[ +@c Mutex +@t ui +@mt mem +@d Mutual exclusion class used to control Lua coroutine execution order. +]=] + +local Deque = require('./Deque') +local timer = require('timer') + +local yield = coroutine.yield +local resume = coroutine.resume +local running = coroutine.running +local setTimeout = timer.setTimeout + +local Mutex = require('class')('Mutex', Deque) + +function Mutex:__init() + Deque.__init(self) + self._active = false +end + +--[=[ +@m lock +@op prepend boolean +@r nil +@d If the mutex is not active (if a coroutine is not queued), this will activate +the mutex; otherwise, this will yield and queue the current coroutine. +]=] +function Mutex:lock(prepend) + if self._active then + if prepend then + return yield(self:pushLeft(running())) + else + return yield(self:pushRight(running())) + end + else + self._active = true + end +end + +--[=[ +@m unlock +@r nil +@d If the mutex is active (if a coroutine is queued), this will dequeue and +resume the next available coroutine; otherwise, this will deactivate the mutex. +]=] +function Mutex:unlock() + if self:getCount() > 0 then + return assert(resume(self:popLeft())) + else + self._active = false + end +end + +--[=[ +@m unlockAfter +@p delay number +@r uv_timer +@d Asynchronously unlocks the mutex after a specified time in milliseconds. +The relevant `uv_timer` object is returned. +]=] +local unlock = Mutex.unlock +function Mutex:unlockAfter(delay) + return setTimeout(delay, unlock, self) +end + +return Mutex diff --git a/deps/discordia/libs/utils/Permissions.lua b/deps/discordia/libs/utils/Permissions.lua new file mode 100644 index 0000000..b6df32c --- /dev/null +++ b/deps/discordia/libs/utils/Permissions.lua @@ -0,0 +1,254 @@ +--[=[ +@c Permissions +@t ui +@mt mem +@d Wrapper for a bitfield that is more specifically used to represent Discord +permissions. See the `permission` enumeration for acceptable permission values. +]=] + +local enums = require('enums') +local Resolver = require('client/Resolver') + +local permission = enums.permission + +local format = string.format +local band, bor, bnot, bxor = bit.band, bit.bor, bit.bnot, bit.bxor +local sort, insert, concat = table.sort, table.insert, table.concat + +local ALL = 0 +for _, value in pairs(permission) do + ALL = bor(ALL, value) +end + +local Permissions, get = require('class')('Permissions') + +function Permissions:__init(value) + self._value = tonumber(value) or 0 +end + +--[=[ +@m __tostring +@r string +@d Defines the behavior of the `tostring` function. Returns a readable list of +permissions stored for convenience of introspection. +]=] +function Permissions:__tostring() + if self._value == 0 then + return 'Permissions: 0 (none)' + else + local a = self:toArray() + sort(a) + return format('Permissions: %i (%s)', self._value, concat(a, ', ')) + end +end + +--[=[ +@m fromMany +@t static +@p ... Permission-Resolvables +@r Permissions +@d Returns a Permissions object with all of the defined permissions. +]=] +function Permissions.fromMany(...) + local ret = Permissions() + ret:enable(...) + return ret +end + +--[=[ +@m all +@t static +@r Permissions +@d Returns a Permissions object with all permissions. +]=] +function Permissions.all() + return Permissions(ALL) +end + +--[=[ +@m __eq +@r boolean +@d Defines the behavior of the `==` operator. Allows permissions to be directly +compared according to their value. +]=] +function Permissions:__eq(other) + return self._value == other._value +end + +local function getPerm(i, ...) + local v = select(i, ...) + local n = Resolver.permission(v) + if not n then + return error('Invalid permission: ' .. tostring(v), 2) + end + return n +end + +--[=[ +@m enable +@p ... Permission-Resolvables +@r nil +@d Enables a specific permission or permissions. See the `permission` enumeration +for acceptable permission values. +]=] +function Permissions:enable(...) + local value = self._value + for i = 1, select('#', ...) do + local perm = getPerm(i, ...) + value = bor(value, perm) + end + self._value = value +end + +--[=[ +@m disable +@p ... Permission-Resolvables +@r nil +@d Disables a specific permission or permissions. See the `permission` enumeration +for acceptable permission values. +]=] +function Permissions:disable(...) + local value = self._value + for i = 1, select('#', ...) do + local perm = getPerm(i, ...) + value = band(value, bnot(perm)) + end + self._value = value +end + +--[=[ +@m has +@p ... Permission-Resolvables +@r boolean +@d Returns whether this set has a specific permission or permissions. See the +`permission` enumeration for acceptable permission values. +]=] +function Permissions:has(...) + local value = self._value + for i = 1, select('#', ...) do + local perm = getPerm(i, ...) + if band(value, perm) == 0 then + return false + end + end + return true +end + +--[=[ +@m enableAll +@r nil +@d Enables all permissions values. +]=] +function Permissions:enableAll() + self._value = ALL +end + +--[=[ +@m disableAll +@r nil +@d Disables all permissions values. +]=] +function Permissions:disableAll() + self._value = 0 +end + +--[=[ +@m toHex +@r string +@d Returns the hexadecimal string that represents the permissions value. +]=] +function Permissions:toHex() + return format('0x%08X', self._value) +end + +--[=[ +@m toTable +@r table +@d Returns a table that represents the permissions value, where the keys are the +permission names and the values are `true` or `false`. +]=] +function Permissions:toTable() + local ret = {} + local value = self._value + for k, v in pairs(permission) do + ret[k] = band(value, v) > 0 + end + return ret +end + +--[=[ +@m toArray +@r table +@d Returns an array of the names of the permissions that this object represents. +]=] +function Permissions:toArray() + local ret = {} + local value = self._value + for k, v in pairs(permission) do + if band(value, v) > 0 then + insert(ret, k) + end + end + return ret +end + +--[=[ +@m union +@p other Permissions +@r Permissions +@d Returns a new Permissions object that contains the permissions that are in +either `self` or `other` (bitwise OR). +]=] +function Permissions:union(other) + return Permissions(bor(self._value, other._value)) +end + +--[=[ +@m intersection +@p other Permissions +@r Permissions +@d Returns a new Permissions object that contains the permissions that are in +both `self` and `other` (bitwise AND). +]=] +function Permissions:intersection(other) -- in both + return Permissions(band(self._value, other._value)) +end + +--[=[ +@m difference +@p other Permissions +@r Permissions +@d Returns a new Permissions object that contains the permissions that are not +in `self` or `other` (bitwise XOR). +]=] +function Permissions:difference(other) -- not in both + return Permissions(bxor(self._value, other._value)) +end + +--[=[ +@m complement +@p other Permissions +@r Permissions +@d Returns a new Permissions object that contains the permissions that are not +in `self`, but are in `other` (or the set of all permissions if omitted). +]=] +function Permissions:complement(other) -- in other not in self + local value = other and other._value or ALL + return Permissions(band(bnot(self._value), value)) +end + +--[=[ +@m copy +@r Permissions +@d Returns a new copy of the original permissions object. +]=] +function Permissions:copy() + return Permissions(self._value) +end + +--[=[@p value number The raw decimal value that represents the permissions value.]=] +function get.value(self) + return self._value +end + +return Permissions diff --git a/deps/discordia/libs/utils/Stopwatch.lua b/deps/discordia/libs/utils/Stopwatch.lua new file mode 100644 index 0000000..96715ba --- /dev/null +++ b/deps/discordia/libs/utils/Stopwatch.lua @@ -0,0 +1,85 @@ +--[=[ +@c Stopwatch +@t ui +@mt mem +@d Used to measure an elapsed period of time. If a truthy value is passed as an +argument, then the stopwatch will initialize in an idle state; otherwise, it will +initialize in an active state. Although nanosecond precision is available, Lua +can only reliably provide microsecond accuracy due to the lack of native 64-bit +integer support. Generally, milliseconds should be sufficient here. +]=] + +local hrtime = require('uv').hrtime +local constants = require('constants') +local Time = require('utils/Time') + +local format = string.format + +local MS_PER_NS = 1 / (constants.NS_PER_US * constants.US_PER_MS) + +local Stopwatch, get = require('class')('Stopwatch') + +function Stopwatch:__init(stopped) + local t = hrtime() + self._initial = t + self._final = stopped and t or nil +end + +--[=[ +@m __tostring +@r string +@d Defines the behavior of the `tostring` function. Returns a string that +represents the elapsed milliseconds for convenience of introspection. +]=] +function Stopwatch:__tostring() + return format('Stopwatch: %s ms', self.milliseconds) +end + +--[=[ +@m stop +@r nil +@d Effectively stops the stopwatch. +]=] +function Stopwatch:stop() + if self._final then return end + self._final = hrtime() +end + +--[=[ +@m start +@r nil +@d Effectively starts the stopwatch. +]=] +function Stopwatch:start() + if not self._final then return end + self._initial = self._initial + hrtime() - self._final + self._final = nil +end + +--[=[ +@m reset +@r nil +@d Effectively resets the stopwatch. +]=] +function Stopwatch:reset() + self._initial = self._final or hrtime() +end + +--[=[ +@m getTime +@r Time +@d Returns a new Time object that represents the currently elapsed time. This is +useful for "catching" the current time and comparing its many forms as required. +]=] +function Stopwatch:getTime() + return Time(self.milliseconds) +end + +--[=[@p milliseconds number The total number of elapsed milliseconds. If the +stopwatch is running, this will naturally be different each time that it is accessed.]=] +function get.milliseconds(self) + local ns = (self._final or hrtime()) - self._initial + return ns * MS_PER_NS +end + +return Stopwatch diff --git a/deps/discordia/libs/utils/Time.lua b/deps/discordia/libs/utils/Time.lua new file mode 100644 index 0000000..08efd31 --- /dev/null +++ b/deps/discordia/libs/utils/Time.lua @@ -0,0 +1,277 @@ +--[=[ +@c Time +@t ui +@mt mem +@d Represents a length of time and provides utilities for converting to and from +different formats. Supported units are: weeks, days, hours, minutes, seconds, +and milliseconds. +]=] + +local class = require('class') +local constants = require('constants') + +local MS_PER_S = constants.MS_PER_S +local MS_PER_MIN = MS_PER_S * constants.S_PER_MIN +local MS_PER_HOUR = MS_PER_MIN * constants.MIN_PER_HOUR +local MS_PER_DAY = MS_PER_HOUR * constants.HOUR_PER_DAY +local MS_PER_WEEK = MS_PER_DAY * constants.DAY_PER_WEEK + +local insert, concat = table.insert, table.concat +local modf, fmod = math.modf, math.fmod +local isInstance = class.isInstance + +local function decompose(value, mult) + return modf(value / mult), fmod(value, mult) +end + +local units = { + {'weeks', MS_PER_WEEK}, + {'days', MS_PER_DAY}, + {'hours', MS_PER_HOUR}, + {'minutes', MS_PER_MIN}, + {'seconds', MS_PER_S}, + {'milliseconds', 1}, +} + +local Time = class('Time') + +local function check(self, other) + if not isInstance(self, Time) or not isInstance(other, Time) then + return error('Cannot perform operation with non-Time object', 2) + end +end + +function Time:__init(value) + self._value = tonumber(value) or 0 +end + +function Time:__tostring() + return 'Time: ' .. self:toString() +end + +--[=[ +@m toString +@r string +@d Returns a human-readable string built from the set of normalized time values +that the object represents. +]=] +function Time:toString() + local ret = {} + local ms = self:toMilliseconds() + for _, unit in ipairs(units) do + local n + n, ms = decompose(ms, unit[2]) + if n == 1 then + insert(ret, n .. ' ' .. unit[1]:sub(1, -2)) + elseif n > 0 then + insert(ret, n .. ' ' .. unit[1]) + end + end + return #ret > 0 and concat(ret, ', ') or '0 milliseconds' +end + +function Time:__eq(other) check(self, other) + return self._value == other._value +end + +function Time:__lt(other) check(self, other) + return self._value < other._value +end + +function Time:__le(other) check(self, other) + return self._value <= other._value +end + +function Time:__add(other) check(self, other) + return Time(self._value + other._value) +end + +function Time:__sub(other) check(self, other) + return Time(self._value - other._value) +end + +function Time:__mul(other) + if not isInstance(self, Time) then + self, other = other, self + end + other = tonumber(other) + if other then + return Time(self._value * other) + else + return error('Cannot perform operation with non-numeric object') + end +end + +function Time:__div(other) + if not isInstance(self, Time) then + return error('Division with Time is not commutative') + end + other = tonumber(other) + if other then + return Time(self._value / other) + else + return error('Cannot perform operation with non-numeric object') + end +end + +--[=[ +@m fromWeeks +@t static +@p t number +@r Time +@d Constructs a new Time object from a value interpreted as weeks, where a week +is equal to 7 days. +]=] +function Time.fromWeeks(t) + return Time(t * MS_PER_WEEK) +end + +--[=[ +@m fromDays +@t static +@p t number +@r Time +@d Constructs a new Time object from a value interpreted as days, where a day is +equal to 24 hours. +]=] +function Time.fromDays(t) + return Time(t * MS_PER_DAY) +end + +--[=[ +@m fromHours +@t static +@p t number +@r Time +@d Constructs a new Time object from a value interpreted as hours, where an hour is +equal to 60 minutes. +]=] +function Time.fromHours(t) + return Time(t * MS_PER_HOUR) +end + +--[=[ +@m fromMinutes +@t static +@p t number +@r Time +@d Constructs a new Time object from a value interpreted as minutes, where a minute +is equal to 60 seconds. +]=] +function Time.fromMinutes(t) + return Time(t * MS_PER_MIN) +end + +--[=[ +@m fromSeconds +@t static +@p t number +@r Time +@d Constructs a new Time object from a value interpreted as seconds, where a second +is equal to 1000 milliseconds. +]=] +function Time.fromSeconds(t) + return Time(t * MS_PER_S) +end + +--[=[ +@m fromMilliseconds +@t static +@p t number +@r Time +@d Constructs a new Time object from a value interpreted as milliseconds, the base +unit represented. +]=] +function Time.fromMilliseconds(t) + return Time(t) +end + +--[=[ +@m fromTable +@t static +@p t table +@r Time +@d Constructs a new Time object from a table of time values where the keys are +defined in the constructors above (eg: `weeks`, `days`, `hours`). +]=] +function Time.fromTable(t) + local n = 0 + for _, v in ipairs(units) do + local m = tonumber(t[v[1]]) + if m then + n = n + m * v[2] + end + end + return Time(n) +end + +--[=[ +@m toWeeks +@r number +@d Returns the total number of weeks that the time object represents. +]=] +function Time:toWeeks() + return self:toMilliseconds() / MS_PER_WEEK +end + +--[=[ +@m toDays +@r number +@d Returns the total number of days that the time object represents. +]=] +function Time:toDays() + return self:toMilliseconds() / MS_PER_DAY +end + +--[=[ +@m toHours +@r number +@d Returns the total number of hours that the time object represents. +]=] +function Time:toHours() + return self:toMilliseconds() / MS_PER_HOUR +end + +--[=[ +@m toMinutes +@r number +@d Returns the total number of minutes that the time object represents. +]=] +function Time:toMinutes() + return self:toMilliseconds() / MS_PER_MIN +end + +--[=[ +@m toSeconds +@r number +@d Returns the total number of seconds that the time object represents. +]=] +function Time:toSeconds() + return self:toMilliseconds() / MS_PER_S +end + +--[=[ +@m toMilliseconds +@r number +@d Returns the total number of milliseconds that the time object represents. +]=] +function Time:toMilliseconds() + return self._value +end + +--[=[ +@m toTable +@r number +@d Returns a table of normalized time values that represent the time object in +a more accessible form. +]=] +function Time:toTable() + local ret = {} + local ms = self:toMilliseconds() + for _, unit in ipairs(units) do + ret[unit[1]], ms = decompose(ms, unit[2]) + end + return ret +end + +return Time diff --git a/deps/discordia/libs/voice/VoiceConnection.lua b/deps/discordia/libs/voice/VoiceConnection.lua new file mode 100644 index 0000000..d1fd744 --- /dev/null +++ b/deps/discordia/libs/voice/VoiceConnection.lua @@ -0,0 +1,432 @@ +--[=[ +@c VoiceConnection +@d Represents a connection to a Discord voice server. +]=] + +local PCMString = require('voice/streams/PCMString') +local PCMStream = require('voice/streams/PCMStream') +local PCMGenerator = require('voice/streams/PCMGenerator') +local FFmpegProcess = require('voice/streams/FFmpegProcess') + +local uv = require('uv') +local ffi = require('ffi') +local constants = require('constants') +local opus = require('voice/opus') +local sodium = require('voice/sodium') + +local CHANNELS = 2 +local SAMPLE_RATE = 48000 -- Hz +local FRAME_DURATION = 20 -- ms +local COMPLEXITY = 5 + +local MIN_BITRATE = 8000 -- bps +local MAX_BITRATE = 128000 -- bps +local MIN_COMPLEXITY = 0 +local MAX_COMPLEXITY = 10 + +local MAX_SEQUENCE = 0xFFFF +local MAX_TIMESTAMP = 0xFFFFFFFF + +local HEADER_FMT = '>BBI2I4I4' +local PADDING = string.rep('\0', 12) + +local MS_PER_NS = 1 / (constants.NS_PER_US * constants.US_PER_MS) +local MS_PER_S = constants.MS_PER_S + +local max = math.max +local hrtime = uv.hrtime +local ffi_string = ffi.string +local pack = string.pack -- luacheck: ignore +local format = string.format +local insert = table.insert +local running, resume, yield = coroutine.running, coroutine.resume, coroutine.yield + +-- timer.sleep is redefined here to avoid a memory leak in the luvit module +local function sleep(delay) + local thread = running() + local t = uv.new_timer() + t:start(delay, 0, function() + t:stop() + t:close() + return assert(resume(thread)) + end) + return yield() +end + +local function asyncResume(thread) + local t = uv.new_timer() + t:start(0, 0, function() + t:stop() + t:close() + return assert(resume(thread)) + end) +end + +local function check(n, mn, mx) + if not tonumber(n) or n < mn or n > mx then + return error(format('Value must be a number between %s and %s', mn, mx), 2) + end + return n +end + +local VoiceConnection, get = require('class')('VoiceConnection') + +function VoiceConnection:__init(channel) + self._channel = channel + self._pending = {} +end + +function VoiceConnection:_prepare(key, socket) + + self._key = sodium.key(key) + self._socket = socket + self._ip = socket._ip + self._port = socket._port + self._udp = socket._udp + self._ssrc = socket._ssrc + self._mode = socket._mode + self._manager = socket._manager + self._client = socket._client + + self._s = 0 + self._t = 0 + + self._encoder = opus.Encoder(SAMPLE_RATE, CHANNELS) + + self:setBitrate(self._client._options.bitrate) + self:setComplexity(COMPLEXITY) + + self._ready = true + self:_continue(true) + +end + +function VoiceConnection:_await() + local thread = running() + insert(self._pending, thread) + if not self._timeout then + local t = uv.new_timer() + t:start(10000, 0, function() + t:stop() + t:close() + self._timeout = nil + if not self._ready then + local id = self._channel and self._channel._id + return self:_cleanup(format('voice connection for channel %s failed to initialize', id)) + end + end) + self._timeout = t + end + return yield() +end + +function VoiceConnection:_continue(success, err) + local t = self._timeout + if t then + t:stop() + t:close() + self._timeout = nil + end + for i, thread in ipairs(self._pending) do + self._pending[i] = nil + assert(resume(thread, success, err)) + end +end + +function VoiceConnection:_cleanup(err) + self:stopStream() + self._ready = nil + self._channel._parent._connection = nil + self._channel._connection = nil + self:_continue(nil, err or 'connection closed') +end + +--[=[ +@m getBitrate +@t mem +@r nil +@d Returns the bitrate of the interal Opus encoder in bits per second (bps). +]=] +function VoiceConnection:getBitrate() + return self._encoder:get(opus.GET_BITRATE_REQUEST) +end + +--[=[ +@m setBitrate +@t mem +@p bitrate number +@r nil +@d Sets the bitrate of the interal Opus encoder in bits per second (bps). +This should be between 8000 and 128000, inclusive. +]=] +function VoiceConnection:setBitrate(bitrate) + bitrate = check(bitrate, MIN_BITRATE, MAX_BITRATE) + self._encoder:set(opus.SET_BITRATE_REQUEST, bitrate) +end + +--[=[ +@m getComplexity +@t mem +@r number +@d Returns the complexity of the interal Opus encoder. +]=] +function VoiceConnection:getComplexity() + return self._encoder:get(opus.GET_COMPLEXITY_REQUEST) +end + +--[=[ +@m setComplexity +@t mem +@p complexity number +@r nil +@d Sets the complexity of the interal Opus encoder. +This should be between 0 and 10, inclusive. +]=] +function VoiceConnection:setComplexity(complexity) + complexity = check(complexity, MIN_COMPLEXITY, MAX_COMPLEXITY) + self._encoder:set(opus.SET_COMPLEXITY_REQUEST, complexity) +end + +---- debugging +local t0, m0 +local t_sum, m_sum, n = 0, 0, 0 +local function open() -- luacheck: ignore + -- collectgarbage() + m0 = collectgarbage('count') + t0 = hrtime() +end +local function close() -- luacheck: ignore + local dt = (hrtime() - t0) * MS_PER_NS + local dm = collectgarbage('count') - m0 + n = n + 1 + t_sum = t_sum + dt + m_sum = m_sum + dm + print(format('dt: %g | dm: %g | avg dt: %g | avg dm: %g', dt, dm, t_sum / n, m_sum / n)) +end +---- debugging + +function VoiceConnection:_play(stream, duration) + + self:stopStream() + self:_setSpeaking(true) + + duration = tonumber(duration) or math.huge + + local elapsed = 0 + local udp, ip, port = self._udp, self._ip, self._port + local ssrc, key = self._ssrc, self._key + local encoder = self._encoder + + local frame_size = SAMPLE_RATE * FRAME_DURATION / MS_PER_S + local pcm_len = frame_size * CHANNELS + + local start = hrtime() + local reason + + while elapsed < duration do + + local pcm = stream:read(pcm_len) + if not pcm then + reason = 'stream exhausted or errored' + break + end + + local data, len = encoder:encode(pcm, pcm_len, frame_size, pcm_len * 2) + if not data then + reason = 'could not encode audio data' + break + end + + local s, t = self._s, self._t + local header = pack(HEADER_FMT, 0x80, 0x78, s, t, ssrc) + + s = s + 1 + t = t + frame_size + + self._s = s > MAX_SEQUENCE and 0 or s + self._t = t > MAX_TIMESTAMP and 0 or t + + local encrypted, encrypted_len = sodium.encrypt(data, len, header .. PADDING, key) + if not encrypted then + reason = 'could not encrypt audio data' + break + end + + local packet = header .. ffi_string(encrypted, encrypted_len) + udp:send(packet, ip, port) + + elapsed = elapsed + FRAME_DURATION + local delay = elapsed - (hrtime() - start) * MS_PER_NS + sleep(max(delay, 0)) + + if self._paused then + asyncResume(self._paused) + self._paused = running() + local pause = hrtime() + yield() + start = start + hrtime() - pause + asyncResume(self._resumed) + self._resumed = nil + end + + if self._stopped then + reason = 'stream stopped' + break + end + + end + + self:_setSpeaking(false) + + if self._stopped then + asyncResume(self._stopped) + self._stopped = nil + end + + return elapsed, reason + +end + +function VoiceConnection:_setSpeaking(speaking) + self._speaking = speaking + return self._socket:setSpeaking(speaking) +end + +--[=[ +@m playPCM +@t mem +@p source string/function/table/userdata +@op duration number +@r number +@r string +@d Plays PCM data over the established connection. If a duration (in milliseconds) +is provided, the audio stream will automatically stop after that time has elapsed; +otherwise, it will play until the source is exhausted. The returned number is the +time elapsed while streaming and the returned string is a message detailing the +reason why the stream stopped. For more information about acceptable sources, +see the [[voice]] page. +]=] +function VoiceConnection:playPCM(source, duration) + + if not self._ready then + return nil, 'Connection is not ready' + end + + local t = type(source) + + local stream + if t == 'string' then + stream = PCMString(source) + elseif t == 'function' then + stream = PCMGenerator(source) + elseif (t == 'table' or t == 'userdata') and type(source.read) == 'function' then + stream = PCMStream(source) + else + return error('Invalid audio source: ' .. tostring(source)) + end + + return self:_play(stream, duration) + +end + +--[=[ +@m playFFmpeg +@t mem +@p path string +@op duration number +@r number +@r string +@d Plays audio over the established connection using an FFmpeg process, assuming +FFmpeg is properly configured. If a duration (in milliseconds) +is provided, the audio stream will automatically stop after that time has elapsed; +otherwise, it will play until the source is exhausted. The returned number is the +time elapsed while streaming and the returned string is a message detailing the +reason why the stream stopped. For more information about using FFmpeg, +see the [[voice]] page. +]=] +function VoiceConnection:playFFmpeg(path, duration) + + if not self._ready then + return nil, 'Connection is not ready' + end + + local stream = FFmpegProcess(path, SAMPLE_RATE, CHANNELS) + + local elapsed, reason = self:_play(stream, duration) + stream:close() + return elapsed, reason + +end + +--[=[ +@m pauseStream +@t mem +@r nil +@d Temporarily pauses the audio stream for this connection, if one is active. +Like most Discordia methods, this must be called inside of a coroutine, as it +will yield until the stream is actually paused, usually on the next tick. +]=] +function VoiceConnection:pauseStream() + if not self._speaking then return end + if self._paused then return end + self._paused = running() + return yield() +end + +--[=[ +@m resumeStream +@t mem +@r nil +@d Resumes the audio stream for this connection, if one is active and paused. +Like most Discordia methods, this must be called inside of a coroutine, as it +will yield until the stream is actually resumed, usually on the next tick. +]=] +function VoiceConnection:resumeStream() + if not self._speaking then return end + if not self._paused then return end + asyncResume(self._paused) + self._paused = nil + self._resumed = running() + return yield() +end + +--[=[ +@m stopStream +@t mem +@r nil +@d Irreversibly stops the audio stream for this connection, if one is active. +Like most Discordia methods, this must be called inside of a coroutine, as it +will yield until the stream is actually stopped, usually on the next tick. +]=] +function VoiceConnection:stopStream() + if not self._speaking then return end + if self._stopped then return end + self._stopped = running() + self:resumeStream() + return yield() +end + +--[=[ +@m close +@t ws +@r boolean +@d Stops the audio stream for this connection, if one is active, disconnects from +the voice server, and leaves the corresponding voice channel. Like most Discordia +methods, this must be called inside of a coroutine. +]=] +function VoiceConnection:close() + self:stopStream() + if self._socket then + self._socket:disconnect() + end + local guild = self._channel._parent + return self._client._shards[guild.shardId]:updateVoice(guild._id) +end + +--[=[@p channel GuildVoiceChannel/nil The corresponding GuildVoiceChannel for +this connection, if one exists.]=] +function get.channel(self) + return self._channel +end + +return VoiceConnection diff --git a/deps/discordia/libs/voice/VoiceManager.lua b/deps/discordia/libs/voice/VoiceManager.lua new file mode 100644 index 0000000..84ab0a7 --- /dev/null +++ b/deps/discordia/libs/voice/VoiceManager.lua @@ -0,0 +1,33 @@ +local VoiceSocket = require('voice/VoiceSocket') +local Emitter = require('utils/Emitter') + +local opus = require('voice/opus') +local sodium = require('voice/sodium') +local constants = require('constants') + +local wrap = coroutine.wrap +local format = string.format + +local GATEWAY_VERSION_VOICE = constants.GATEWAY_VERSION_VOICE + +local VoiceManager = require('class')('VoiceManager', Emitter) + +function VoiceManager:__init(client) + Emitter.__init(self) + self._client = client +end + +function VoiceManager:_prepareConnection(state, connection) + if not next(opus) then + return self._client:error('Cannot prepare voice connection: libopus not found') + end + if not next(sodium) then + return self._client:error('Cannot prepare voice connection: libsodium not found') + end + local socket = VoiceSocket(state, connection, self) + local url = 'wss://' .. state.endpoint:gsub(':%d*$', '') + local path = format('/?v=%i', GATEWAY_VERSION_VOICE) + return wrap(socket.connect)(socket, url, path) +end + +return VoiceManager diff --git a/deps/discordia/libs/voice/VoiceSocket.lua b/deps/discordia/libs/voice/VoiceSocket.lua new file mode 100644 index 0000000..867de25 --- /dev/null +++ b/deps/discordia/libs/voice/VoiceSocket.lua @@ -0,0 +1,197 @@ +local uv = require('uv') +local class = require('class') +local timer = require('timer') +local enums = require('enums') + +local WebSocket = require('client/WebSocket') + +local logLevel = enums.logLevel +local format = string.format +local setInterval, clearInterval = timer.setInterval, timer.clearInterval +local wrap = coroutine.wrap +local time = os.time +local unpack = string.unpack -- luacheck: ignore + +local ENCRYPTION_MODE = 'xsalsa20_poly1305' +local PADDING = string.rep('\0', 70) + +local IDENTIFY = 0 +local SELECT_PROTOCOL = 1 +local READY = 2 +local HEARTBEAT = 3 +local DESCRIPTION = 4 +local SPEAKING = 5 +local HEARTBEAT_ACK = 6 +local RESUME = 7 +local HELLO = 8 +local RESUMED = 9 + +local function checkMode(modes) + for _, mode in ipairs(modes) do + if mode == ENCRYPTION_MODE then + return mode + end + end +end + +local VoiceSocket = class('VoiceSocket', WebSocket) + +for name in pairs(logLevel) do + VoiceSocket[name] = function(self, fmt, ...) + local client = self._client + return client[name](client, format('Voice : %s', fmt), ...) + end +end + +function VoiceSocket:__init(state, connection, manager) + WebSocket.__init(self, manager) + self._state = state + self._manager = manager + self._client = manager._client + self._connection = connection + self._session_id = state.session_id +end + +function VoiceSocket:handleDisconnect() + -- TODO: reconnecting and resuming + self._connection:_cleanup() +end + +function VoiceSocket:handlePayload(payload) + + local manager = self._manager + + local d = payload.d + local op = payload.op + + self:debug('WebSocket OP %s', op) + + if op == HELLO then + + self:info('Received HELLO') + self:startHeartbeat(d.heartbeat_interval * 0.75) -- NOTE: hotfix for API bug + self:identify() + + elseif op == READY then + + self:info('Received READY') + local mode = checkMode(d.modes) + if mode then + self._mode = mode + self._ssrc = d.ssrc + self:handshake(d.ip, d.port) + else + self:error('No supported encryption mode available') + self:disconnect() + end + + elseif op == RESUMED then + + self:info('Received RESUMED') + + elseif op == DESCRIPTION then + + if d.mode == self._mode then + self._connection:_prepare(d.secret_key, self) + else + self:error('%q encryption mode not available', self._mode) + self:disconnect() + end + + elseif op == HEARTBEAT_ACK then + + manager:emit('heartbeat', nil, self._sw.milliseconds) -- TODO: id + + elseif op == SPEAKING then + + return -- TODO + + elseif op == 12 or op == 13 then + + return -- ignore + + elseif op then + + self:warning('Unhandled WebSocket payload OP %i', op) + + end + +end + +local function loop(self) + return wrap(self.heartbeat)(self) +end + +function VoiceSocket:startHeartbeat(interval) + if self._heartbeat then + clearInterval(self._heartbeat) + end + self._heartbeat = setInterval(interval, loop, self) +end + +function VoiceSocket:stopHeartbeat() + if self._heartbeat then + clearInterval(self._heartbeat) + end + self._heartbeat = nil +end + +function VoiceSocket:heartbeat() + self._sw:reset() + return self:_send(HEARTBEAT, time()) +end + +function VoiceSocket:identify() + local state = self._state + return self:_send(IDENTIFY, { + server_id = state.guild_id, + user_id = state.user_id, + session_id = state.session_id, + token = state.token, + }, true) +end + +function VoiceSocket:resume() + local state = self._state + return self:_send(RESUME, { + server_id = state.guild_id, + session_id = state.session_id, + token = state.token, + }) +end + +function VoiceSocket:handshake(server_ip, server_port) + local udp = uv.new_udp() + self._udp = udp + self._ip = server_ip + self._port = server_port + udp:recv_start(function(err, data) + assert(not err, err) + udp:recv_stop() + local client_ip = unpack('xxxxz', data) + local client_port = unpack('= opus.OK and value or throw(value) +end + +local Encoder = {} +Encoder.__index = Encoder + +function Encoder:__new(sample_rate, channels, app) -- luacheck: ignore self + + app = app or opus.APPLICATION_AUDIO -- TODO: test different applications + + local err = int_ptr_t() + local state = lib.opus_encoder_create(sample_rate, channels, app, err) + check(err[0]) + + check(lib.opus_encoder_init(state, sample_rate, channels, app)) + + return gc(state, lib.opus_encoder_destroy) + +end + +function Encoder:encode(input, input_len, frame_size, max_data_bytes) + + local pcm = new('opus_int16[?]', input_len, input) + local data = new('unsigned char[?]', max_data_bytes) + + local ret = lib.opus_encode(self, pcm, frame_size, data, max_data_bytes) + + return data, check(ret) + +end + +function Encoder:get(id) + local ret = opus_int32_ptr_t() + lib.opus_encoder_ctl(self, id, ret) + return check(ret[0]) +end + +function Encoder:set(id, value) + if type(value) ~= 'number' then return throw(opus.BAD_ARG) end + local ret = lib.opus_encoder_ctl(self, id, opus_int32_t(value)) + return check(ret) +end + +opus.Encoder = ffi.metatype('OpusEncoder', Encoder) + +local Decoder = {} +Decoder.__index = Decoder + +function Decoder:__new(sample_rate, channels) -- luacheck: ignore self + + local err = int_ptr_t() + local state = lib.opus_decoder_create(sample_rate, channels, err) + check(err[0]) + + check(lib.opus_decoder_init(state, sample_rate, channels)) + + return gc(state, lib.opus_decoder_destroy) + +end + +function Decoder:decode(data, len, frame_size, output_len) + + local pcm = new('opus_int16[?]', output_len) + + local ret = lib.opus_decode(self, data, len, pcm, frame_size, 0) + + return pcm, check(ret) + +end + +function Decoder:get(id) + local ret = opus_int32_ptr_t() + lib.opus_decoder_ctl(self, id, ret) + return check(ret[0]) +end + +function Decoder:set(id, value) + if type(value) ~= 'number' then return throw(opus.BAD_ARG) end + local ret = lib.opus_decoder_ctl(self, id, opus_int32_t(value)) + return check(ret) +end + +opus.Decoder = ffi.metatype('OpusDecoder', Decoder) + +return opus diff --git a/deps/discordia/libs/voice/sodium.lua b/deps/discordia/libs/voice/sodium.lua new file mode 100644 index 0000000..573e996 --- /dev/null +++ b/deps/discordia/libs/voice/sodium.lua @@ -0,0 +1,85 @@ +local ffi = require('ffi') + +local loaded, lib = pcall(ffi.load, 'sodium') +if not loaded then + return nil, lib +end + +local typeof = ffi.typeof +local format = string.format + +ffi.cdef[[ +const char *sodium_version_string(void); +const char *crypto_secretbox_primitive(void); + +size_t crypto_secretbox_keybytes(void); +size_t crypto_secretbox_noncebytes(void); +size_t crypto_secretbox_macbytes(void); +size_t crypto_secretbox_zerobytes(void); + +int crypto_secretbox_easy( + unsigned char *c, + const unsigned char *m, + unsigned long long mlen, + const unsigned char *n, + const unsigned char *k +); + +int crypto_secretbox_open_easy( + unsigned char *m, + const unsigned char *c, + unsigned long long clen, + const unsigned char *n, + const unsigned char *k +); + +void randombytes(unsigned char* const buf, const unsigned long long buf_len); +]] + +local sodium = {} + +local MACBYTES = lib.crypto_secretbox_macbytes() +local NONCEBYTES = lib.crypto_secretbox_noncebytes() +local KEYBYTES = lib.crypto_secretbox_keybytes() + +local key_t = typeof(format('const unsigned char[%i]', tonumber(KEYBYTES))) +local nonce_t = typeof(format('unsigned char[%i] const', tonumber(NONCEBYTES))) +local unsigned_char_array_t = typeof('unsigned char[?]') + +function sodium.key(key) + return key_t(key) +end + +function sodium.nonce() + local nonce = nonce_t() + lib.randombytes(nonce, NONCEBYTES) + return nonce, NONCEBYTES +end + +function sodium.encrypt(decrypted, decrypted_len, nonce, key) + + local encrypted_len = decrypted_len + MACBYTES + local encrypted = unsigned_char_array_t(encrypted_len) + + if lib.crypto_secretbox_easy(encrypted, decrypted, decrypted_len, nonce, key) < 0 then + return error('libsodium encryption failed') + end + + return encrypted, encrypted_len + +end + +function sodium.decrypt(encrypted, encrypted_len, nonce, key) + + local decrypted_len = encrypted_len - MACBYTES + local decrypted = unsigned_char_array_t(decrypted_len) + + if lib.crypto_secretbox_open_easy(decrypted, encrypted, encrypted_len, nonce, key) < 0 then + return error('libsodium decryption failed') + end + + return decrypted, decrypted_len + +end + +return sodium diff --git a/deps/discordia/libs/voice/streams/FFmpegProcess.lua b/deps/discordia/libs/voice/streams/FFmpegProcess.lua new file mode 100644 index 0000000..19e22b6 --- /dev/null +++ b/deps/discordia/libs/voice/streams/FFmpegProcess.lua @@ -0,0 +1,88 @@ +local uv = require('uv') + +local remove = table.remove +local unpack = string.unpack -- luacheck: ignore +local rep = string.rep +local yield, resume, running = coroutine.yield, coroutine.resume, coroutine.running + +local function onExit() end + +local fmt = setmetatable({}, { + __index = function(self, n) + self[n] = '<' .. rep('i2', n) + return self[n] + end +}) + +local FFmpegProcess = require('class')('FFmpegProcess') + +function FFmpegProcess:__init(path, rate, channels) + + local stdout = uv.new_pipe(false) + + self._child = assert(uv.spawn('ffmpeg', { + args = {'-i', path, '-ar', rate, '-ac', channels, '-f', 's16le', 'pipe:1', '-loglevel', 'warning'}, + stdio = {0, stdout, 2}, + }, onExit), 'ffmpeg could not be started, is it installed and on your executable path?') + + local buffer + local thread = running() + stdout:read_start(function(err, chunk) + if err or not chunk then + self:close() + else + buffer = chunk + end + stdout:read_stop() + return assert(resume(thread)) + end) + + self._buffer = buffer or '' + self._stdout = stdout + + yield() + +end + +function FFmpegProcess:read(n) + + local buffer = self._buffer + local stdout = self._stdout + local bytes = n * 2 + + if not self._closed and #buffer < bytes then + + local thread = running() + stdout:read_start(function(err, chunk) + if err or not chunk then + self:close() + elseif #chunk > 0 then + buffer = buffer .. chunk + end + if #buffer >= bytes or self._closed then + stdout:read_stop() + return assert(resume(thread)) + end + end) + yield() + + end + + if #buffer >= bytes then + self._buffer = buffer:sub(bytes + 1) + local pcm = {unpack(fmt[n], buffer)} + remove(pcm) + return pcm + end + +end + +function FFmpegProcess:close() + self._closed = true + self._child:kill() + if not self._stdout:is_closing() then + self._stdout:close() + end +end + +return FFmpegProcess diff --git a/deps/discordia/libs/voice/streams/PCMGenerator.lua b/deps/discordia/libs/voice/streams/PCMGenerator.lua new file mode 100644 index 0000000..cbbede3 --- /dev/null +++ b/deps/discordia/libs/voice/streams/PCMGenerator.lua @@ -0,0 +1,18 @@ +local PCMGenerator = require('class')('PCMGenerator') + +function PCMGenerator:__init(fn) + self._fn = fn +end + +function PCMGenerator:read(n) + local pcm = {} + local fn = self._fn + for i = 1, n, 2 do + local left, right = fn() + pcm[i] = tonumber(left) or 0 + pcm[i + 1] = tonumber(right) or pcm[i] + end + return pcm +end + +return PCMGenerator diff --git a/deps/discordia/libs/voice/streams/PCMStream.lua b/deps/discordia/libs/voice/streams/PCMStream.lua new file mode 100644 index 0000000..186c2ff --- /dev/null +++ b/deps/discordia/libs/voice/streams/PCMStream.lua @@ -0,0 +1,28 @@ +local remove = table.remove +local unpack = string.unpack -- luacheck: ignore +local rep = string.rep + +local fmt = setmetatable({}, { + __index = function(self, n) + self[n] = '<' .. rep('i2', n) + return self[n] + end +}) + +local PCMStream = require('class')('PCMStream') + +function PCMStream:__init(stream) + self._stream = stream +end + +function PCMStream:read(n) + local m = n * 2 + local str = self._stream:read(m) + if str and #str == m then + local pcm = {unpack(fmt[n], str)} + remove(pcm) + return pcm + end +end + +return PCMStream diff --git a/deps/discordia/libs/voice/streams/PCMString.lua b/deps/discordia/libs/voice/streams/PCMString.lua new file mode 100644 index 0000000..2d6c5ce --- /dev/null +++ b/deps/discordia/libs/voice/streams/PCMString.lua @@ -0,0 +1,28 @@ +local remove = table.remove +local unpack = string.unpack -- luacheck: ignore +local rep = string.rep + +local fmt = setmetatable({}, { + __index = function(self, n) + self[n] = '<' .. rep('i2', n) + return self[n] + end +}) + +local PCMString = require('class')('PCMString') + +function PCMString:__init(str) + self._len = #str + self._str = str +end + +function PCMString:read(n) + local i = self._i or 1 + if i + n * 2 < self._len then + local pcm = {unpack(fmt[n], self._str, i)} + self._i = remove(pcm) + return pcm + end +end + +return PCMString diff --git a/deps/discordia/package.lua b/deps/discordia/package.lua new file mode 100644 index 0000000..cc04af5 --- /dev/null +++ b/deps/discordia/package.lua @@ -0,0 +1,36 @@ +--[[The MIT License (MIT) + +Copyright (c) 2016-2020 SinisterRectus + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE.]] + +return { + name = 'SinisterRectus/discordia', + version = '2.9.1', + homepage = 'https://github.com/SinisterRectus/Discordia', + dependencies = { + 'creationix/coro-http@3.1.0', + 'creationix/coro-websocket@3.1.0', + 'luvit/secure-socket@1.2.2', + }, + tags = {'discord', 'api'}, + license = 'MIT', + author = 'Sinister Rectus', + files = {'**.lua'}, +} diff --git a/deps/http-codec.lua b/deps/http-codec.lua new file mode 100644 index 0000000..b90af33 --- /dev/null +++ b/deps/http-codec.lua @@ -0,0 +1,301 @@ +--[[ + +Copyright 2014-2015 The Luvit Authors. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +--]] + +--[[lit-meta + name = "luvit/http-codec" + version = "3.0.6" + homepage = "https://github.com/luvit/luvit/blob/master/deps/http-codec.lua" + description = "A simple pair of functions for converting between hex and raw strings." + tags = {"codec", "http"} + license = "Apache 2" + author = { name = "Tim Caswell" } +]] + +local sub = string.sub +local gsub = string.gsub +local lower = string.lower +local find = string.find +local format = string.format +local concat = table.concat +local match = string.match + +local STATUS_CODES = { + [100] = 'Continue', + [101] = 'Switching Protocols', + [102] = 'Processing', -- RFC 2518, obsoleted by RFC 4918 + [200] = 'OK', + [201] = 'Created', + [202] = 'Accepted', + [203] = 'Non-Authoritative Information', + [204] = 'No Content', + [205] = 'Reset Content', + [206] = 'Partial Content', + [207] = 'Multi-Status', -- RFC 4918 + [300] = 'Multiple Choices', + [301] = 'Moved Permanently', + [302] = 'Moved Temporarily', + [303] = 'See Other', + [304] = 'Not Modified', + [305] = 'Use Proxy', + [307] = 'Temporary Redirect', + [400] = 'Bad Request', + [401] = 'Unauthorized', + [402] = 'Payment Required', + [403] = 'Forbidden', + [404] = 'Not Found', + [405] = 'Method Not Allowed', + [406] = 'Not Acceptable', + [407] = 'Proxy Authentication Required', + [408] = 'Request Time-out', + [409] = 'Conflict', + [410] = 'Gone', + [411] = 'Length Required', + [412] = 'Precondition Failed', + [413] = 'Request Entity Too Large', + [414] = 'Request-URI Too Large', + [415] = 'Unsupported Media Type', + [416] = 'Requested Range Not Satisfiable', + [417] = 'Expectation Failed', + [418] = "I'm a teapot", -- RFC 2324 + [422] = 'Unprocessable Entity', -- RFC 4918 + [423] = 'Locked', -- RFC 4918 + [424] = 'Failed Dependency', -- RFC 4918 + [425] = 'Unordered Collection', -- RFC 4918 + [426] = 'Upgrade Required', -- RFC 2817 + [428] = 'Precondition Required', -- RFC 6585 + [429] = 'Too Many Requests', -- RFC 6585 + [431] = 'Request Header Fields Too Large', -- RFC 6585 + [500] = 'Internal Server Error', + [501] = 'Not Implemented', + [502] = 'Bad Gateway', + [503] = 'Service Unavailable', + [504] = 'Gateway Time-out', + [505] = 'HTTP Version not supported', + [506] = 'Variant Also Negotiates', -- RFC 2295 + [507] = 'Insufficient Storage', -- RFC 4918 + [509] = 'Bandwidth Limit Exceeded', + [510] = 'Not Extended', -- RFC 2774 + [511] = 'Network Authentication Required' -- RFC 6585 +} + +local function encoder() + + local mode + local encodeHead, encodeRaw, encodeChunked + + function encodeHead(item) + if not item or item == "" then + return item + elseif not (type(item) == "table") then + error("expected a table but got a " .. type(item) .. " when encoding data") + end + local head, chunkedEncoding + local version = item.version or 1.1 + if item.method then + local path = item.path + assert(path and #path > 0, "expected non-empty path") + head = { item.method .. ' ' .. item.path .. ' HTTP/' .. version .. '\r\n' } + else + local reason = item.reason or STATUS_CODES[item.code] + head = { 'HTTP/' .. version .. ' ' .. item.code .. ' ' .. reason .. '\r\n' } + end + for i = 1, #item do + local key, value = unpack(item[i]) + local lowerKey = lower(key) + if lowerKey == "transfer-encoding" then + chunkedEncoding = lower(value) == "chunked" + end + value = gsub(tostring(value), "[\r\n]+", " ") + head[#head + 1] = key .. ': ' .. tostring(value) .. '\r\n' + end + head[#head + 1] = '\r\n' + + mode = chunkedEncoding and encodeChunked or encodeRaw + return concat(head) + end + + function encodeRaw(item) + if type(item) ~= "string" then + mode = encodeHead + return encodeHead(item) + end + return item + end + + function encodeChunked(item) + if type(item) ~= "string" then + mode = encodeHead + local extra = encodeHead(item) + if extra then + return "0\r\n\r\n" .. extra + else + return "0\r\n\r\n" + end + end + if #item == 0 then + mode = encodeHead + end + return format("%x", #item) .. "\r\n" .. item .. "\r\n" + end + + mode = encodeHead + return function (item) + return mode(item) + end +end + +local function decoder() + + -- This decoder is somewhat stateful with 5 different parsing states. + local decodeHead, decodeEmpty, decodeRaw, decodeChunked, decodeCounted + local mode -- state variable that points to various decoders + local bytesLeft -- For counted decoder + + -- This state is for decoding the status line and headers. + function decodeHead(chunk, index) + if not chunk or index > #chunk then return end + + local _, last = find(chunk, "\r?\n\r?\n", index) + -- First make sure we have all the head before continuing + if not last then + if (#chunk - index) <= 8 * 1024 then return end + -- But protect against evil clients by refusing heads over 8K long. + error("entity too large") + end + + -- Parse the status/request line + local head = {} + local _, offset + local version + _, offset, version, head.code, head.reason = + find(chunk, "^HTTP/(%d%.%d) (%d+) ([^\r\n]*)\r?\n", index) + if offset then + head.code = tonumber(head.code) + else + _, offset, head.method, head.path, version = + find(chunk, "^(%u+) ([^ ]+) HTTP/(%d%.%d)\r?\n", index) + if not offset then + error("expected HTTP data") + end + end + version = tonumber(version) + head.version = version + head.keepAlive = version > 1.0 + + -- We need to inspect some headers to know how to parse the body. + local contentLength + local chunkedEncoding + + -- Parse the header lines + while true do + local key, value + _, offset, key, value = find(chunk, "^([^:\r\n]+): *([^\r\n]*)\r?\n", offset + 1) + if not offset then break end + local lowerKey = lower(key) + + -- Inspect a few headers and remember the values + if lowerKey == "content-length" then + contentLength = tonumber(value) + elseif lowerKey == "transfer-encoding" then + chunkedEncoding = lower(value) == "chunked" + elseif lowerKey == "connection" then + head.keepAlive = lower(value) == "keep-alive" + end + head[#head + 1] = {key, value} + end + + if head.keepAlive and (not (chunkedEncoding or (contentLength and contentLength > 0))) + or (head.method == "GET" or head.method == "HEAD") then + mode = decodeEmpty + elseif chunkedEncoding then + mode = decodeChunked + elseif contentLength then + bytesLeft = contentLength + mode = decodeCounted + elseif not head.keepAlive then + mode = decodeRaw + end + return head, last + 1 + + end + + -- This is used for inserting a single empty string into the output string for known empty bodies + function decodeEmpty(chunk, index) + mode = decodeHead + return "", index + end + + function decodeRaw(chunk, index) + if #chunk < index then return end + return sub(chunk, index) + end + + function decodeChunked(chunk, index) + local len, term + len, term = match(chunk, "^(%x+)(..)", index) + if not len then return end + if term ~= "\r\n" then + -- Wait for full chunk-size\r\n header + if #chunk < 18 then return end + -- But protect against evil clients by refusing chunk-sizes longer than 16 hex digits. + error("chunk-size field too large") + end + index = index + #len + 2 + local offset = index - 1 + local length = tonumber(len, 16) + if #chunk < offset + length + 2 then return end + if length == 0 then + mode = decodeHead + end + assert(sub(chunk, index + length, index + length + 1) == "\r\n") + local piece = sub(chunk, index, index + length - 1) + return piece, index + length + 2 + end + + function decodeCounted(chunk, index) + if bytesLeft == 0 then + mode = decodeEmpty + return mode(chunk, index) + end + local offset = index - 1 + local length = #chunk - offset + -- Make sure we have at least one byte to process + if length == 0 then return end + + -- If there isn't enough data left, emit what we got so far + if length < bytesLeft then + bytesLeft = bytesLeft - length + return sub(chunk, index) + end + + mode = decodeEmpty + return sub(chunk, index, offset + bytesLeft), index + bytesLeft + end + + -- Switch between states by changing which decoder mode points to + mode = decodeHead + return function (chunk, index) + return mode(chunk, index) + end + +end + +return { + encoder = encoder, + decoder = decoder, +} diff --git a/deps/pathjoin.lua b/deps/pathjoin.lua new file mode 100644 index 0000000..ce20f77 --- /dev/null +++ b/deps/pathjoin.lua @@ -0,0 +1,124 @@ +--[[lit-meta + name = "creationix/pathjoin" + description = "The path utilities that used to be part of luvi" + version = "2.0.0" + tags = {"path"} + license = "MIT" + author = { name = "Tim Caswell" } +]] + +local getPrefix, splitPath, joinParts + +local isWindows +if _G.jit then + isWindows = _G.jit.os == "Windows" +else + isWindows = not not package.path:match("\\") +end + +if isWindows then + -- Windows aware path utilities + function getPrefix(path) + return path:match("^%a:\\") or + path:match("^/") or + path:match("^\\+") + end + function splitPath(path) + local parts = {} + for part in string.gmatch(path, '([^/\\]+)') do + table.insert(parts, part) + end + return parts + end + function joinParts(prefix, parts, i, j) + if not prefix then + return table.concat(parts, '/', i, j) + elseif prefix ~= '/' then + return prefix .. table.concat(parts, '\\', i, j) + else + return prefix .. table.concat(parts, '/', i, j) + end + end +else + -- Simple optimized versions for UNIX systems + function getPrefix(path) + return path:match("^/") + end + function splitPath(path) + local parts = {} + for part in string.gmatch(path, '([^/]+)') do + table.insert(parts, part) + end + return parts + end + function joinParts(prefix, parts, i, j) + if prefix then + return prefix .. table.concat(parts, '/', i, j) + end + return table.concat(parts, '/', i, j) + end +end + +local function pathJoin(...) + local inputs = {...} + local l = #inputs + + -- Find the last segment that is an absolute path + -- Or if all are relative, prefix will be nil + local i = l + local prefix + while true do + prefix = getPrefix(inputs[i]) + if prefix or i <= 1 then break end + i = i - 1 + end + + -- If there was one, remove its prefix from its segment + if prefix then + inputs[i] = inputs[i]:sub(#prefix) + end + + -- Split all the paths segments into one large list + local parts = {} + while i <= l do + local sub = splitPath(inputs[i]) + for j = 1, #sub do + parts[#parts + 1] = sub[j] + end + i = i + 1 + end + + -- Evaluate special segments in reverse order. + local skip = 0 + local reversed = {} + for idx = #parts, 1, -1 do + local part = parts[idx] + if part ~= '.' then + if part == '..' then + skip = skip + 1 + elseif skip > 0 then + skip = skip - 1 + else + reversed[#reversed + 1] = part + end + end + end + + -- Reverse the list again to get the correct order + parts = reversed + for idx = 1, #parts / 2 do + local j = #parts - idx + 1 + parts[idx], parts[j] = parts[j], parts[idx] + end + + local path = joinParts(prefix, parts) + return path +end + +return { + isWindows = isWindows, + getPrefix = getPrefix, + splitPath = splitPath, + joinParts = joinParts, + pathJoin = pathJoin, +} diff --git a/deps/resource.lua b/deps/resource.lua new file mode 100644 index 0000000..f8e73cc --- /dev/null +++ b/deps/resource.lua @@ -0,0 +1,88 @@ +--[[ + +Copyright 2014-2016 The Luvit Authors. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +--]] + +--[[lit-meta + name = "luvit/resource" + version = "2.1.0" + license = "Apache 2" + homepage = "https://github.com/luvit/luvit/blob/master/deps/resource.lua" + description = "Utilities for loading relative resources" + dependencies = { + "creationix/pathjoin@2.0.0" + } + tags = {"luvit", "relative", "resource"} +]] + +local pathJoin = require('pathjoin').pathJoin +local bundle = require('luvi').bundle +local uv = require('uv') + +local function getPath() + local caller = debug.getinfo(2, "S").source + if caller:sub(1,1) == "@" then + return caller:sub(2) + elseif caller:sub(1, 7) == "bundle:" then + return caller + end + error("Unknown file path type: " .. caller) +end + +local function getDir() + local caller = debug.getinfo(2, "S").source + if caller:sub(1,1) == "@" then + return pathJoin(caller:sub(2), "..") + elseif caller:sub(1, 7) == "bundle:" then + return "bundle:" .. pathJoin(caller:sub(8), "..") + end + error("Unknown file path type: " .. caller) +end + +local function innerResolve(path, resolveOnly) + local caller = debug.getinfo(2, "S").source + if caller:sub(1,1) == "@" then + path = pathJoin(caller:sub(2), "..", path) + if resolveOnly then return path end + local fd = assert(uv.fs_open(path, "r", 420)) + local stat = assert(uv.fs_fstat(fd)) + local data = assert(uv.fs_read(fd, stat.size, 0)) + uv.fs_close(fd) + return data, path + elseif caller:sub(1, 7) == "bundle:" then + path = pathJoin(caller:sub(8), "..", path) + if resolveOnly then return path end + return bundle.readfile(path), "bundle:" .. path + end +end + +local function resolve(path) + return innerResolve(path, true) +end + +local function load(path) + return innerResolve(path, false) +end + +local function getProp(self, key) + if key == "path" then return getPath() end + if key == "dir" then return getDir() end +end + +return setmetatable({ + resolve = resolve, + load = load, +}, { __index = getProp }) diff --git a/deps/secure-socket/biowrap.lua b/deps/secure-socket/biowrap.lua new file mode 100644 index 0000000..50f9f19 --- /dev/null +++ b/deps/secure-socket/biowrap.lua @@ -0,0 +1,115 @@ +--[[ + +Copyright 2016 The Luvit Authors. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +--]] +local openssl = require('openssl') + +-- writeCipher is called when ssl needs something written on the socket +-- handshakeComplete is called when the handhake is complete and it's safe +-- onPlain is called when plaintext comes out. +return function (ctx, isServer, socket, handshakeComplete, servername) + + local bin, bout = openssl.bio.mem(8192), openssl.bio.mem(8192) + local ssl = ctx:ssl(bin, bout, isServer) + + if not isServer and servername then + ssl:set('hostname', servername) + end + + local ssocket = {tls=true} + local onPlain + + local function flush(callback) + local chunks = {} + local i = 0 + while bout:pending() > 0 do + i = i + 1 + chunks[i] = bout:read() + end + if i == 0 then + if callback then callback() end + return true + end + return socket:write(chunks, callback) + end + + local function handshake(callback) + if ssl:handshake() then + local success, result = ssl:getpeerverification() + socket:read_stop() + if not success and result then + handshakeComplete("Error verifying peer: " .. result[1].error_string) + end + handshakeComplete(nil, ssocket) + end + return flush(callback) + end + + local function onCipher(err, data) + if not onPlain then + if err or not data then + return handshakeComplete(err or "Peer aborted the SSL handshake", data) + end + bin:write(data) + return handshake() + end + if err or not data then + return onPlain(err, data) + end + bin:write(data) + while true do + local plain = ssl:read() + if not plain then break end + onPlain(nil, plain) + end + end + + -- When requested to start reading, start the real socket and setup + -- onPlain handler + function ssocket.read_start(_, onRead) + onPlain = onRead + return socket:read_start(onCipher) + end + + -- When requested to write plain data, encrypt it and write to socket + function ssocket.write(_, plain, callback) + ssl:write(plain) + return flush(callback) + end + + function ssocket.shutdown(_, ...) + return socket:shutdown(...) + end + function ssocket.read_stop(_, ...) + return socket:read_stop(...) + end + function ssocket.is_closing(_, ...) + return socket:is_closing(...) + end + function ssocket.close(_, ...) + return socket:close(...) + end + function ssocket.unref(_, ...) + return socket:unref(...) + end + function ssocket.ref(_, ...) + return socket:ref(...) + end + + handshake() + socket:read_start(onCipher) + +end diff --git a/deps/secure-socket/context.lua b/deps/secure-socket/context.lua new file mode 100644 index 0000000..58b6fd0 --- /dev/null +++ b/deps/secure-socket/context.lua @@ -0,0 +1,121 @@ +--[[ + +Copyright 2016 The Luvit Authors. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +--]] +local openssl = require('openssl') + +local loadResource +if type(module) == "table" then + function loadResource(path) + return module:load(path) + end +else + loadResource = require('resource').load +end +local bit = require('bit') + +local DEFAULT_SECUREPROTOCOL +do + local _, _, V = openssl.version() + local isLibreSSL = V:find('^LibreSSL') + + _, _, V = openssl.version(true) + local isTLSv1_3 = not isLibreSSL and V >= 0x10101000 + + if isTLSv1_3 then + DEFAULT_SECUREPROTOCOL = 'TLS' + else + DEFAULT_SECUREPROTOCOL = 'SSLv23' + end +end +local DEFAULT_CIPHERS = 'TLS_AES_128_GCM_SHA256:TLS_AES_128_CCM_SHA256:' .. --TLS 1.3 + 'ECDHE-RSA-AES128-SHA256:AES128-GCM-SHA256:' .. --TLS 1.2 + 'RC4:HIGH:!MD5:!aNULL:!EDH' --TLS 1.0 +local DEFAULT_CA_STORE +do + local data = assert(loadResource("./root_ca.dat")) + DEFAULT_CA_STORE = openssl.x509.store:new() + local index = 1 + local dataLength = #data + while index < dataLength do + local len = bit.bor(bit.lshift(data:byte(index), 8), data:byte(index + 1)) + index = index + 2 + local cert = assert(openssl.x509.read(data:sub(index, index + len))) + index = index + len + assert(DEFAULT_CA_STORE:add(cert)) + end +end + +local function returnOne() + return 1 +end + +return function (options) + local ctx = openssl.ssl.ctx_new( + options.protocol or DEFAULT_SECUREPROTOCOL, + options.ciphers or DEFAULT_CIPHERS) + + local key, cert, ca + if options.key then + key = assert(openssl.pkey.read(options.key, true, 'pem')) + end + if options.cert then + cert = {} + for chunk in options.cert:gmatch("%-+BEGIN[^-]+%-+[^-]+%-+END[^-]+%-+") do + cert[#cert + 1] = assert(openssl.x509.read(chunk)) + end + end + if options.ca then + if type(options.ca) == "string" then + ca = { assert(openssl.x509.read(options.ca)) } + elseif type(options.ca) == "table" then + ca = {} + for i = 1, #options.ca do + ca[i] = assert(openssl.x509.read(options.ca[i])) + end + else + error("options.ca must be string or table of strings") + end + end + if key and cert then + local first = table.remove(cert, 1) + assert(ctx:use(key, first)) + if #cert > 0 then + -- TODO: find out if there is a way to not need to duplicate the last cert here + -- as a dummy fill for the root CA cert + assert(ctx:add(cert[#cert], cert)) + end + end + if ca then + local store = openssl.x509.store:new() + for i = 1, #ca do + assert(store:add(ca[i])) + end + ctx:cert_store(store) + elseif DEFAULT_CA_STORE then + ctx:cert_store(DEFAULT_CA_STORE) + end + if not (options.insecure or options.key) then + ctx:verify_mode(openssl.ssl.peer, returnOne) + end + + ctx:options(bit.bor( + openssl.ssl.no_sslv2, + openssl.ssl.no_sslv3, + openssl.ssl.no_compression)) + + return ctx +end diff --git a/deps/secure-socket/init.lua b/deps/secure-socket/init.lua new file mode 100644 index 0000000..9ba79eb --- /dev/null +++ b/deps/secure-socket/init.lua @@ -0,0 +1,41 @@ +--[[ + +Copyright 2016 The Luvit Authors. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +--]] +local getContext = require('./context') +local bioWrap = require('./biowrap') + +local function assertResume(thread, ...) + local success, err = coroutine.resume(thread, ...) + if not success then + error(debug.traceback(thread, err), 0) + end +end + +return function (socket, options, callback) + if options == true then options = {} end + local ctx = getContext(options) + local thread + if not callback then + thread = coroutine.running() + end + bioWrap(ctx, options.server, socket, callback or function (err, ssocket) + return assertResume(thread, ssocket, err) +end, options.servername) + if not callback then + return coroutine.yield() + end +end diff --git a/deps/secure-socket/package.lua b/deps/secure-socket/package.lua new file mode 100644 index 0000000..1a1c7f1 --- /dev/null +++ b/deps/secure-socket/package.lua @@ -0,0 +1,12 @@ +return { + name = "luvit/secure-socket", + version = "1.2.3", + homepage = "https://github.com/luvit/luvit/blob/master/deps/secure-socket", + description = "Wrapper for luv streams to apply ssl/tls", + dependencies = { + "luvit/resource@2.1.0" + }, + tags = {"ssl", "socket","tls"}, + license = "Apache 2", + author = { name = "Tim Caswell" } +} diff --git a/deps/secure-socket/root_ca.dat b/deps/secure-socket/root_ca.dat new file mode 100644 index 0000000000000000000000000000000000000000..8c9097e2a7d3f28e21492d413a46703e846aa086 GIT binary patch literal 146914 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

C7r`3MzDTH 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 z%0A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 McU94JO%e=n5<%E| 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%0A0l$wvqR5F;!7T|`E4qp@`<6~F?U+{5UWSZn zcfwJ76|dWsqki`8m6vd)6||a`w z-_2i$4J_n0E7QTo)tL#G5&OGC2<69HKf-N<5O7k!&t+>W9PE)33iew1=S6?MsLA<~s^M z;XY2{(jhj5s_ct2E%t>ilvMeXjd_vdmBm#{+E=|_$)20N1A6R>8CPsI@+qi^s(mIz zYGE@53zaYsVTrwx$ODm2md%py-rvv6dt#+uX&l6HD`>3D*(;gy5b$_9Ox!3jY?vGyF&ojFj%l91zP( zvx8hrZ%ukW%4S2yq4$L?P5Vn<9PA;jmcqXNR>7vth5~?nM}fnc^c+FB-e2 zpmwCArXkgKeXW+iK_y}qkZSL_VKHS? zQlZ8<;p!yF;CAG6o#|2A+mXl^6}9YL?@EHNMa>~`URkVb_InYheUtoP=||WSa%;zw zuWH6-yTU5B{dlY6G1{vGjP|de;Cfhal}qv;B@xdyq2<=m)F13j7(b{YE$x?%NU2m% z8Hj(=i^4J@^^c8p7(&?jXAfe z{V=U8LpyEn zL-KpKuykg>7719I&v|?)`qZ;X=Sja4`3BgYiC7kV!@x-_3tregdvt8eFC+tV^I@3Y zX%am_ZZOSQCl`#`20Z^?oWBb}#=xG@3o?DYB&c&S9|_AT4K)8@{{H2F-#Oh6$tH)V zYMA-!pp?UNRlKRCI&k z)>5_UhpuSUoK8}7i>Ps^oBF`DZJCg)-BLgj=w?1U-@EAy7w>@7oVeK2Rhk_=du zLDYYHLh52wxqSTCZB0HD=U5v@oBzBd156S|!GBzme^jo*LdiS)s9gP#1_O)w+k1b# zD08y1vHjjX>Bpk{@1NoCmgO5y@rC4GM!g6}fGTW3YjrQ=(#SSjmwy2g-ST!=l`dc0 z=P( zx^=%prfUtMo+}L6AK)E;A-;ZK6y#{B`I#nkTpI^ z#pSrhC+fu)PM>alD}B=1Xd{eDPr}M_yvDWqWJ!555kzowQjqnv%;-LG`DpC4^_b>p z78}bIjmo$1#U?69mZE0zGRVczv_3sk+Py%mjNS8TPt2nb!S!+@(9i6$MyJKb>YIyJ z#Z{d&&7ZbwDZ;mW({W`j`noY!SZEZ*rE#OL`CxA&SW$cKS=dNUhfk^d3tOx6MxT_e zWrc9eC~K4NimRaInz>j+GhMn9aBtSKNy$V`Ge`oVm)PY%>b!xcwqIeFg`srv z=eo3Jz^`{D;1Z zO@4<IIy&(WTTNQ!G6_k! z6kiKF#vDn6TQc0}zT4~n;M1I4&H?7NlFslLG_d8jj{`3QSYJ z*qdk;B!4v+Bp|sLYk7_#JsB)z^7AC{ucKpbF)=rI*Q`0Rn*MUKB(UZ-H8Em%t#u6) zC(WQn+;j;tuU0%;^!0v%uR8i?G$(y}>GG*8!h9*Hg3EQq@#1Ts@62V~m|5V;=RE?mk2ttpI*Onq0AB@zvmk+xt|sm@-9*8t~9m)vx41}hC)nApcy zOpZl&Bm=I%c>1Wp*DQ^pk5z9KtWwV@&y@$4Z1IsY@$ra`K+0MqnLhRf;yq_k0L*x& z>m)!;?Nq=X9v7d(aSJod6-z*&S+78hwX%9}dke)4feE!Vnl5o>Iz-kPOqk{DneK`z%RXCYwsm60`(u$j#ksgE|ueoxffBr{Qte*wC=mN=d5 z&FxCbF(kW^o#cPAgZ^De{QI@}-#^3Of$ngFdLc_9VgR%9D!@_Y#xsFXQ3v~c?nB#Gn-XtUY)9-}>^XgSQShVUwe!!0Fl)?t$rGF>9~NF< zJZx6T?|u!(cPW?e#D{8Nw^BP_|C$_bMhd<;BFrtdyp~?w6DU^q^r>9OoBz`R1_P{* zT~%SsKm{4J{oS-!>Ss2nD-e;yr}<98Bv1d;i1~1~Rz;iw4xfiKp7Q6;3b+`NnzUEw zh`xCC5$NKw9zR}WWQNc8dhIsW7Alyk0E`%8M(S- zt@1g5rgk)58ka!)4sR}g$j$scR2tM)1qQ0BLm%&3HE58!B_piWf%lAlm%QYzNbR#Y zbz#{M?@S%H-i~1xLm}cNgqvya@)|L}f6r~V7xE%9TO`WdnNm~xU}K4PxRBh*2)TYc zGf4DGvEWOW7peLne4zmMd!EQGx33P{@hj*$l-em+6!A}9@hYPUT+rxYZN_z%H3>Rv zc8%lvw?@pbMejQ)C>t1)bdf2WlR9FQF}LzC(Z8q_)Y2ke;&Bw+G+vEs>V#i^n0}w= zGBN#x({Qpqga7I??@-CcCFVI1f!(pJ1^6pET|1HRqORMwoq_mc%6|0+w2Qh|Rz8BH zU{2wjFA|0vKXB=SBVE~h+;G5g1ylT|K-Y@vPMG+5I~+(>&xa*wsL9Sf^xZ#z?q3f0 z33Pvi)l!Gb1$nnvfK&YF!ir+1^rYT3;qSCM-us)7mDXYNTj)}d7q?KapQr~xo5Yr4MJ|CFBMw&fU)yUZiZqR&whfqNiUG+3X%1d`rFW1fFIdGTsqQu`7Mzv z{}NtT%%r?CZpi$TpV2^)eZ%Cds21f9_8Fowp5u59b+b9Z7qt-8{aZJcB=ALQpQw=C zCFdE$A9+q5TpZ#Hqd6f7Oc)y)rSI7*5ToBJu^4B8u&MQ0w)c@}Q6yd!Am7lj@aC!& z>d#>99eG2xyN^(q3@0wgi7SMy-F7Sc>t%Bkdprx$Ncl#WfUOII952;bevSonYkv2NDolqYNKYz|h^KCzIst5i0v=@$h@Us{flc^zWbHpEw|;zG_s- z4w|iibLS&HtYMzm;kM8*!iLY!e9dqUDkVXSV$h7xT1MT^(xee-1V3bMX!$DizEU?g zWOyGB)hyrKSNu!j`A&mcE}%SJo+G817{49q;G&j5?;~pZ)_h9ZziJOjxPL^AG~$B& zWWTN;JC3Nal=Ry6ALj614*1y~vIJr@16VgPB}-W?u%H-sXnTANR4-CG1JGFn6Ko!C zkNh3%%VHLTKSW-#4T3M3*ju2l$HX-QJ~Ag=IP_s0(q<3yUTaxapA@DVh=}YsrWlmm z@r$?RmTvlu5CIQYLPC}*AjkdmzWa|pN+h5_#Ifqnf*YEEHaM-ufsucT#>)R>4*^6U ze$`5YMgas@0VU0LrY_%fBwW9>FZn(0D&b`B>hN3T77FZ3WhII47LvwypCub0K?WdQ z$;is`OO?RSn~eYEC;YpGyK!W2teKGd9YOcU%3f$F=K+yr)Ym zyDK1h%Ec9nP#e*Nh8p8XRAlQ;g3tR*8AQGlEW9m}m&=~3bxqI6r$p(}-4UUVmQrSB zHZi`u4xN=$#Ng+fUEBUuU!-Fi(|i*_awa8GywfB}ly^C+jgYQ?^|Wz2=7}}(++Hk5XCUt#M_h+r znMaI9zi+%Qx$fTrhDeQJhfLb}!jTNe?wWlM?^J`A(FSt+5^OGD8LXaqMrg0%gi@6o;LV2=%oQjdbJT#B^6+O^kKw{pEGl13dzmme0b_PR!=-XdYK=BqyaH$(~ z*@-520Xfb%rR~UG57E-i$fzUP7|4@av7teal?N<$3n=hRGAzu$*Yh3MR*!{tk@JqUj zDuG66TWyKLIBd-vwk0726=a2>t4@U7P2iR#!}OJ!baRt4qUIz~itmn$<9+RCgG#Ov zn0*W)wUAQ+HO;-2PiV)XSuw%>SDSlru}c^kMmS<8W=WsD?LX}E921NU%~VmG zmR1i`J^J{j-w}fzJkR^>$I%eiV8F+mMX+jX%sssLq|)3(J{<9yC0vFsm^X0sYqC06 z3vKIlPh-CvPhbbr0I{=k{KND-!rVf$@C3O1d5%0`GcJ8xKGcSv&X6*4_-#HZ`g>XPkzGhmaso0RATX0+GuAwL8^ zk(T6#FH~5qV^Y_Yh!|>55-jn&aXX{iedD@{!D7RweE(`lNdI{$Z`_b`J-MkuHqs`jG(Q#8JM-iTn9uMzRW3MK_u7{CJ+ z1n@vve_xjobQ9h%nzIjp<6-x!1Y1J<;hACN`3Y{tlz)l{iOb2V&?$+?{?^?a7EuVG zBrfz)i}1&;ZwV;?WlBasB`h1;x9mee0T!U06yV>l;h=x(;C~0BEsp+PGNE>U11Pp# zeLlEADKWILB&~?)f~DAvVXtp8)U58eUqwg8M;DXnY9cMN(>E$&5R^g^fMSqplyOL5 zu5NKnsXxwk)5g7s`$9Yg(xvg>E^!#c?;_Gt!TLs))6G1(dcdBMMZ;02{?giR>V-lR zim#1S{>uqMtgmfdRG6m4D^JJE(6K~2*yizo#3g-s#x{Ojv3kT=3qjN_!Mh!I46kjN z7*lQD`E;el_wCu@V|)j3V8C6;wQe%9h{-(h0L@8+wABt0HZgo)ou~ID8EIq-dgsI+ zxh^-U=_mULdTh#iajjl}Nilrx?Ji*@*EK8r3`}&3*$93qmu6p5E~B_RrhwV&KEMS< z`!b$6t)vwL!?RkecRRmF(^P^jNUwC44JNI1bA+%jLkgVhy~P7@%U9JX8oB-RnvI8s zwDf7=J4ac_u`qlqN1xlt`n^KOfL9;nB$5lS+(PmxWA^;*0+b7F^_wJbKiskx(Z76d zO8yW9QJ3*rOa0fxjRfZ+6#SZF#2pKlX&0dD&}npdsE9XR)H|~ z#{L%TI_41n%pv<^u<5w**^R?Z`^xV1`8y+K;siKut7ud+SUP1FbyNK5+GUaeoJl3}-2f`!XX6bXwj zSP8PTZ038}Ob7?En|vE6HpR8}7D#tCb=xP~-1%cV^>ukh*S9zankLgr`cA%t4Pdcqzaj3Rc;3YL$ z8lH*7xop3kgeDcwg?u-&AcmDZrHrQ??0M34EpZ$f(+ggeC1aHJEDm}z`KbYsOwjcS zz3h{eK`2AZF{AsY%9p0#G!6eUlt@KxdC9BDhB^>200;{Kj3lb>K#T%zlQWjq83oPt z<-{XGWIGwqGUX4?4WNt_h7syp_#GSt7{GlI0Q-N*I|LW9HT1H#`vJuuD2U(R_$>biGivNv6|6N%|t%twkpvUxv<2wm|1s5N?x!Kol7ikvP8bk)SF2OGtDuC%i zC*O*A$<-E&v$wqHc(W)u0D-|SbMf^xNk9Jq$K`vho0k^VE3?gRoOp)W1y6zOgKuk~#wA6N*)bbWs^Qo2KPIk`xAfrM;HqINq;;Tb2xXul8>P@ssWR#8CV^)qQb&7GOG(?{9qP68asGXdV3IA9%7gG^zc>L& z^Zd|D8*Lv3&(nl)K&4tiocPMg61 z(+CVOjR58cpkfRd_}eu4oXi%Q1C6j1>T#n@-WtL2^Y8Yp>G6O1S^u@_=^sL_fE3%` zXVt%R=)dDLmB~4DnNl)R0iKF!hY>^828t+cvbNw4)sSzNu@J5L$zj>%y6g#q_Y!L| zLO?!hFG}qW+{C)gYs+uQ?1Hn)4yRY}!N$_WPl00@RB(uTh6DswYH_T=4QCa}D7o#6 zWHo|LC|s$oO@A(W8M4Q*H|SfXlO!vdE>K0NW&1j5p0!B{TO_u+(*~@$!J~11e3P3O zS=s1W>g-hq8(1ka>&Cf>ixDj!&`T}KCuY&suw_#UU`L(>scV9Tk!vE=Sb2oQs=)Oe z>v^%s))W1(BVJmr&;@~&LnqY(&-}{8W$)@1+|h>>c{aB(gc}b6D|SV33PbI^?6H$P zRUdD%2z!S5UO*RbnnyTAM4NkkoHiWa-YS6XK2!WNjQT}-ZL|7b`7xZ^#L3r-(a_>G zyS;kL+*QjfmOe}R6qM}3uxfS00FF<}b*+9^@e`Tjm>2PO3qkTl#%VHz_j<=fCSHX4 zHYOoxLmYcXurSo%sq7zA zL39Z2bJ`^ic3w&(!N`RZL%m$PG{iscq>pg2^VfV<=e~7i&X?0aJfbw3mvufH4!|%t z8aY^6EFSFj1#Ul6fmC^{r-P!!P?1WVP+3D+vkTh2m-X z;6Gm7^)rlA!=A`6UVv!;OFpt}DZP^p-Y`@8!*uwCjg*!6$H05!ujNgRY+t5&@$3VE zRHwivv;pc-X``INN^2hcoF9!HJqp7##~9b)5NlDiYy&T?w*7P7g27m9N?7+>tu{Qm zGdpcz-a%cwO`z5N9OW9~W*Q3%;@QYVVqEDF1Qlu#JotP&MxGw;xicc&yJJ|4F?3#v zoRDB$yvv{*-(#RrJq)afsU7mhNYl8XvVJA~p8Oh!qDxnm!zJyLg5QQFz32ul+1EoE zX7~CW>^*5+!rp~P+%WV$k=?0Q@^mo=y;2wsS>ng#|CH4CwCvT)V zGqKWfC{>!mV~+=21NiaJOi24N-^o2oWJ6rBnHEe!t36+0nAjYb5R<)3XjlcpSkUm* z{o0FDJu#tAgxG(3-U19w0nvLp(?7X(4`pEUq6OfpZ2-Kqe}~a&mlSq00Unt2`Q2D{I!72^#55Z^4(F(_7iCSQl{`NvK{ArH(}{v%nSKTo{! zhQ(R%Y>o=XP>48({>&b%8jr4@e?X9zK#bym{!nWn@%b2yZ*FP83ll1^Q!Nag?$&e1 z?{Pg0ibT?Sn|c64GJ%gz6Eo|QZx8yYZyIQyxMNY#XQPZsC2?07eWopVUR(k0+Zo{H z@%z1-?QTVsp%U40eD#WU0J|Ihr&ELc=kFcsuO%kG->m<#d&m0EvW-5MM{vmJE5ki? zN4%0Qc_l7wTy1nkhqDOjNc0(K*SI~M;{JroPIsTL@KZopzBXa1m@fL_<1V7@M&{Z^ zyMD0Ixq}JI+aXd0hnCm5)NfsH0;-I_djb}@?5L{o@91^@)f+o`*w1ZhH@pszl~%jY zJP);1-wOEo*ZsBs8#iDgxBDVhjvs3zGt3-h`NqDX88cJ;NV`E<>HsYPf5M@O{7jdY zNGV4x&nC;F1@N8ky8_Aa^l&F4$9>We&mux`Dt?_Au_+_9m^@VxX>I)yS(&ABcC&-@ z;XSn5RACz;7$8v{43MY}QvUtEqeA-@txAh#FIbU=WN$a2O``j?*8B|i-_nW1f10;4 zs!Tr=L}B4%Ts?lNA^l#6O7II61qFdc|Ly(n-Qd2P6hDX%3nLq##}gA9$2Zpwz&8GE z^S|sof5(buLsB}CQuj`bf@FBZZk4S#V9pSD!$$3w$f??U!&^QD@m+r%)b6u=>0JLw zJRgS@MW8!eO#NcM=APYD`X2I&q$ARR9Zz=FYPcy!QOFgKC}u++2cwZ_e2S%yFeB+E z9BjC?@k;Ods$40mjq;k|=oJP79&h_Lu?$|qC1Ovm+o9D;ugz+5y_%qKml43(L5G(3 z_A?QADJBBFw$UeVY#rVG2Syes!k(RX{_@x zux52@X4!1Y&Sd-A=flW7$924)W;>n2}VDD@R zP$2iDw{i7w`9&_*#`RAnq(953J!4Y3-AaPd=2o53;Ly}6-$;Ac{ba-a^!?^!=~UId}psbKEZ&>_0)_e01^^ax|m1O7)>xb5w^atJVLMwI>BQ%SZU4x zZCMa5(>uWtSeY;p2K&4$(&9J}`xJ6H)sK+|v7Mp13ALr!ad-L zIg>QoX;kHW+&)ij=I|p2t*J(bIuQ05+ugw{CfyI+$b88Guam>OHFdnU{Tx_#g1*$O zuQzENPbF;{WS%6aI7J-<1Jj$v6f}PJGhZH<%+@I@EqF_Y3Z8K&?@I^chv$0AGIuoE zW-JFWY1gAD9g1{J83u?Wr){*3|YThw75;Twbl^!9KE;wL9NF9Hlh8UKj|8@@6O=#;BJWO5Vj zQ$k@&8%tYfQyW4zQ-I=t^*6x*D@$uaAtysXF{+cTA)&OZldG*O;20{Z!Y@^Xq?za# zX}%o;28C4G5U@iT;Lo%)v$QsJ_5`T;$eKD?n|fJ(JLNAsWtjk>({IB;Q0RU=z>htI z%E~f;3$y;|2;V-Z{Ox?d_E7=!3}j|w)S}ua{ByB zj!nU6Oft_PR|1}Ao08r>wVV2*2+{m<8<_{Z@ug#n_OZ>f(G%6{Jl3foYYuPR4^Qtw z06lZt>fiSs0U;=JP|IG(!v@`ljtxCU$pN~l6h{)NXL_xIn1|D5r5F|?HmSz?!P8N2 zx~%RzF32Ho$f6Mg53H>{AXG-oeo|2w=0O-l_YWRdRQnjM<`+)P!mu3z-CkIN!@e@P zL~F~_kJ7G5Lc6_Ha6Jcev>z)dT9VY7Fg@o2Oy)Slch0sXiW& zqh0|*wGn`N|Iv2G;twZ#AyON zzY2Wy+$XuEGbU!WeUsb$_gUy>!cOOYlQm2t4+YT}r?w>JiDoyI_Xv=7?j=MFAM&YXHzVd<@xKl{ax#X`j$CG?1y)r2i1nzc68!1Zx=yBg zZ&zHGvoEih9!b3=5G^k=#aBRij1m_i^Jn8zw1ywC$EFTdri6G zgQO^8F3)v3$e7`_xF(@%)NYc}kCo|SqII>iv?F~~Ulb6lbcO<>F)-PEQnp{GSIe?O zUC=gbnNe3`WMt#JWl5Pa39Sm;_ktheH27kRvtOGS^K5WolI)nfqE5T2$8)rISmBT> zoUxOG$M!nBFtK$rTaR0`gXl8!r>~`Fh;k^N;Nh){+q7%w;c+;#iBEwVk?_InI&aNJ zn2D@{XwX6!g@KhQ-r{d+_vz1Iduy1&E#!(ZOq1+R9Z!TmpX5epZZQVezB)qyWtIMA z*x9jw!3WvDQJGO(Pg*4n!TeLIdL^Y@UfD!t`TPlXyfp>Mtt}ruZHn_Q|lH@SFCW&S`ifjhN+P)%)&ZyDe&m9nF+VA@Niaut zbT>WMdkkK{w3q6&6_(2LQn8&OGm@;Mwx6kIJNA2D$%qi6UV4-Gk7(S?II8z6Y1nF0LDVB}cWChT5aRU16 z6_JPPCHy-95t!;+Auia${d?Ld`Z2eVd+{!BE+syf^UU0Pn8~sG-0sEifv!BhG=8wX zM|DgvXONI;Y<|Y4c6JbF?9@7A@GM+^13Q0PYs?YFVH)CKrK1hy)Ld!ITeE!gz}(3Z zy4f0?O5xi=fWGd4bOy3YX2eWEj>#OH^9;m|(vl8XLz++>h-(IEVn?aE9WEHM{LW(W zXS7xu#UHjq?}pY0-Mm_ghAzEVj0$TazpezDMaXe85(xMYRZ_IT>dlVVs13S1*1mVm zRVWn{!T3NqKc*C^JzzW~%>S0fOPu~1nowND*Giu0G{9Hbh11lT;%lFGQ}k>h_iSoz zW)!08n5o%&}ZcyINDnqu3Lte{rvovj8QM~%wI39&Y8<+jeJ0G9h=)Uge9eMjg)iD!<3D2W%MbcG z$-+296-l-h<)U8QYlxioln6f0X5#Rg_9Ly_ zY|$#BO>GL$ z@0n1EUWWch-#Zvs{NF}>EA0g^K4U`{)1L%Nen<4*2L7Tx0vHUCo1*(3V`OIp2uuNB zoSErcy2p17bU?EY7QiOs|B|oqcNDENx-sd?2XbU<7OZ|j4d`;svNu5a5B~iMucD_2 z$y=C_=SWmuk|7eOW{H39{**0;Lr)9Od5%qHfm@^nwv8Vt*_oQ`!f!da_uiurylomt z4+{E>!b|CaZn|c_KHQM>92;e^bWG_8mb9fg`_LE2w@79}h`+flibP>n*-DQ?@L8b|s&VkbM#jX|87~Hz)gj zt68?>b8PG}-&9jZeVD5NVaBM_SgvZftVtdgcrFABEDY7w>3(_s;d^M8*nS_Uvss;- zxZ}9U#-$^(ys>CAp0*rr;5CY6^wh4TDd>XDfOOYHw#-=l*Bs!L08!=Y3G+#;!SEes zEp}Y~z!I&5j=ph|xQ4O$2F$TV-_NG`MLp3NOx^dsZi|y48ydClUT!=#3<|hAi&y5J z1ebwH=@7-J?_!oHiOius&&X=wV&O)+>wHWk&J9o6s0m*ZUvXGwxV zC}f`D)$*jp0%rl}6)^yi1LIc?N0z%f$TFn)rhDMwQt~km`=nosHrl1jq<%8xI z(#m#Selxh1acIF@U>R=_1rq?T_Ld)enU)MSkRfH#MEpXfbctJqj)^8M@NAxX_Sg}q ziK(E&ixlFcFyUFmR6Hn-shW(<)O~}YmIO)toMn9@_qYo$tZQzRoLlbRB6T6p-Dvf* zuNCDZ1@1u29@VM3ok{(8H<~Sc2R*#_fZm1T(=og;H;-e>n7!s8>_&#}%O;sG>NTkm z!s?lB$H(mx7A2oApjzG8I>-Vcd(de`3DrtPk=&1U>+#C;tob9EuOJxILE{H6pRzve z?bb!hNCPv?Z}Lh8Qe0yOx=C_7)}Z8aP1EPW)(6`$EL3d(DJ-dyAR+lnx@ob$l!0s-huCdv{O_`XL3v1#S z6=S-81)B3uM1ppJ#^@P*nLeq6SX)tyMz#L{ntwUqUj|J8)yx*lhUIBEUgsY0o&e$- z`?3%IzLvWcT-w5GoFv7inDzv6&QohvD>z~fIUQ%-M@QD7xA3-nD?dDZ`NiGgGJrIu zELMvPpHCW2+W7fG0+@&itgRx(qE)U6j%$=x+ARa7`o<6>{=fWL1;aqbuK@8ig&{{ z@zaK5fr4Op1ulS8pyR+B(lxdbTN31cx47LKWnqqJUZ_NO(vueB*EwPR<-Nqr{CG&3 zY+?q2^qa3hS`v6jAQeKG@4_1GVYy=l$Cg04^8gxNkN$cC2Es3Ju$8r1If@$FT7viR zZLp_I7N$g=5+bF~Xg$#prxIXZ$K=k1qKd~!WKfbGt1HRr2xpdEzdWSa6~wPA3MY2T zyo6(^*T`q&S)Q^|N$9|qJx@ryeh7vU2qx5F1rhCCsBDAo1|GxTN}NBB)}E6&$vQN| zXhmgKfd!^Yz>g2T@;ly%2ZPsKPJ^VUzsm$tn(Q^B+AK~HEr6#CKw;~>vNzcl>X~L3 zW)&kF69P)+lcN8SoGLFTm6_7GND$9F@Qk2nH**LDN(xvo0O|xFkUywqa3HNduF<&Q zpQvW${{*V}YsUu=Soq(lrr4h;Cd;?p%fC@fPDT#EYVkk&^8b!nj>W7Yz0WCeb6Z?R zP&sxsrV6Z-=*zc0deUdP5mDU3W{G3Mcj?hVB~@@z0K^|WP;j^A zOR-`xUJ$u;`N|ze9L-!`1%6F7){E(sqJ6l=9#sMg@6V9aZUj_lVS-%CoMK3>wlN_fo@zHm*geXv)~>gZa&B5?$&+yFs&S74#%X)gi8A<(gn1Om&kjc&4Z)3wROI}+v-tf8qx^@c=TeLYf>`2w zs`IoE)OUG?&&X=0ycrH<`*OL_ymlIN*)FdnrP#&Hrdm=py8j9#MYAO@hixUN-b(vD zX_7JorKF{Y{sT(><$!+~B>~hDjGs*h%^~Aczq6se6V23zm@^Sw;L)DM=0v0*bKNiI zHm+`+t*~Y6eU%ak#!L9VGb{8@dsxhzAbzMe3WE`jl;sFzh^52%qOcoCgiQ=An-*R)CayPgOoE8Tf2q%ytc-xy`tkt zLH1!ABp?z|d81!7uRdpX8*Clm9$W^XMDMTE@)d2yB)UIy+8;r}`zMS5R2}n%+RYn{dAR`V42>=XH2YaVqngal8t{8p^9>7up z_Rt&K+y0uf`kMv%t%JaCBAJ9r%HNv<02m`c;gW-e3qTP8af5G7UVdyc{`X$p-x0xE zgyno1pP_N;r$h6Qq8)mi7Wegy%XbBbcT&1>J?awHnC{kYXO9V+dzt+mP>hMGRJpX-?e!(@f38a{e)9zA`*0&29+RWNdO;eC5IKo(zb=SAFq zgP*%myBx<7=U%h!gQ3_eX+b+AaFhyKLG9Me)aXAT#?07W*OS?A9TB z_+53&J)}$i)B!@aiTnd=p8|w{vh_OsdLdKuFlmJa>u&8iDWv86FZco^)?Rz>Ga=aBkpkuRY z#>ypJI63G88Fmqw<54K|LjuyIr99MpZGiL89)RHF_99f?#Z)o@$nrDPm`Py97!uJ z5;UTj-{I6bfIN`VxP-*=d01UEM;4~=yvK}d#V@`U)Q-DVO?!urG_HeJY_?I82m#B? zRkV*&?Ky{+=^(lhvD|Z;L=eF#BJd)lP@PZOGRgIKpK-v zx2t1sN6iU|*hQB01D0`Cej8{XCJb4tayUYrzru>xS5d$6L=ThH$t%j`usH`)mwWhT zl!Iaf!&2tt;g!}SV;RiWQo-xDDt3gg9bGZ^z3N$=)<`JabsA##L&*fok693gasoT@ z2=A}n(_wFa2&r*FxF0YDE>Lh?fjSR?2J!4?zL}SQ{K}B zyv8skyk3X}%K4-;%sj~EeaVL~M>Bg#Zu1G8H5q;1K@*$V(-WrQwI70#Bwwe^T%IWX zgj}PO#$-53(vxWU&1@u&U1l5YTkUBppm+Kl>(h6LFsYfLJLEB=Cx>M;Hra6wf)#A&-Dh@+Vmh0trEs+jNH zJ64v|I`BWG)F2wE23NhO5KxYFCn7v>h>QF?4idv|D!4u)+cjIiC%$04 z&mu7*bL->N(N4#AvgolMS~%YVrEYPg&i{5CQYdJLQizmb!wGHt1@^t314->r4%fz5 zuUuceEqULo*183XEa=d^01mLH6Gz9xkGOlrc^If!$2jJbA*#T@GcP|4YpvH@+cW2U zn&NpaRFc^~RK95Y3W^|4#wm7z2J&HyS7p>3p=3-q84IRo!|9-M!UvDyIQ{^!Q~QAD z-I)J!_vd6#Z7<(~QeIBC0;A}+TzdQNW4b_^F=n*<5PA7vQskE^)(PChsFGi8K_C9V2cPArjy%EkS=1W0OsmJXBYDJ$2*auTCN=)F7fwfs+ z9i~Ug`lc?t_fTx~uQE{{aS(L-Z3DQ~5Qcfy-vKiaSBMXa^S;HEnGvnmqt`(*gIXu2 zdg@a$Y^ia6{s^17Wn+~nfI29Mo)mnpSz4hlW0+t&!84qf1WR0GZZ1mSJpVaDsLl*V z>)wZ?@C>{9c_kw#k$ys8HNG+K9T<;k|67}y5hLV}u{axKqpWTA?rt@RAFsi%yJp_G z7)fHd$6Ll4;;tn5yQsuq;(j~_8Sg9*aSc_*^n#{`!e!5idkl;qmY#rDAX^>+fyMX~ z1OzF^CL0lXbOM}cMl0$!ltht?Q3=IZ(cp+-7m0C&3pGW@lUnFFse&?ZaDFLmCVzMO zl@R$&eT90Bm~n1}Tj_^;;8XzrNCdpFe+7J6s!VP4Be0}@;2?hgbzslR$ow7bY5t>N z|3@0a_Df*mHyZM7lkva(0{;mUDMOZsO~tuC{wN3$L@0O47?=k>aTXtHcy>i(F$mPt zzb^u$61w#Gea}+V~;=o_mAmi ze9af+0{fW^Y`-aMsQDaAXrHV$dy}c$u4~_IF3OzyrE0TGs!=rhvk-sIyVS0e?JIt= LKS-L@6XOK{;k%k6 literal 0 HcmV?d00001 diff --git a/deps/sha1/init.lua b/deps/sha1/init.lua new file mode 100644 index 0000000..d15a5d1 --- /dev/null +++ b/deps/sha1/init.lua @@ -0,0 +1,194 @@ +--[[lit-meta + name = "creationix/sha1" + version = "1.0.4" + homepage = "https://github.com/luvit/lit/blob/master/deps/sha1.lua" + description = "Pure Lua implementation of SHA1 using bitop" + authors = { + "Tim Caswell" + } +]] + +-- http://csrc.nist.gov/groups/ST/toolkit/documents/Examples/SHA_All.pdf + +local bit = require('bit') +local band = bit.band +local bor = bit.bor +local bxor = bit.bxor +local lshift = bit.lshift +local rshift = bit.rshift +local rol = bit.rol +local tobit = bit.tobit +local tohex = bit.tohex + +local byte = string.byte +local concat = table.concat +local floor = table.floor + +local hasFFi, ffi = pcall(require, "ffi") +local newBlock = hasFFi and function () + return ffi.new("uint32_t[80]") +end or function () + local t = {} + for i = 0, 79 do + t[i] = 0 + end + return t +end + +local shared = newBlock() + +local function unsigned(n) + return n < 0 and (n + 0x100000000) or n +end + +local function create(sync) + local h0 = 0x67452301 + local h1 = 0xEFCDAB89 + local h2 = 0x98BADCFE + local h3 = 0x10325476 + local h4 = 0xC3D2E1F0 + -- The first 64 bytes (16 words) is the data chunk + local W = sync and shared or newBlock() + local offset = 0 + local shift = 24 + local totalLength = 0 + + local update, write, processBlock, digest + + -- The user gave us more data. Store it! + function update(chunk) + local length = #chunk + totalLength = totalLength + length * 8 + for i = 1, length do + write(byte(chunk, i)) + end + end + + function write(data) + W[offset] = bor(W[offset], lshift(band(data, 0xff), shift)) + if shift > 0 then + shift = shift - 8 + else + offset = offset + 1 + shift = 24 + end + if offset == 16 then + return processBlock() + end + end + + -- No more data will come, pad the block, process and return the result. + function digest() + -- Pad + write(0x80) + if offset > 14 or (offset == 14 and shift < 24) then + processBlock() + end + offset = 14 + shift = 24 + + -- 64-bit length big-endian + write(0x00) -- numbers this big aren't accurate in lua anyway + write(0x00) -- ..So just hard-code to zero. + write(totalLength > 0xffffffffff and floor(totalLength / 0x10000000000) or 0x00) + write(totalLength > 0xffffffff and floor(totalLength / 0x100000000) or 0x00) + for s = 24, 0, -8 do + write(rshift(totalLength, s)) + end + + -- At this point one last processBlock() should trigger and we can pull out the result. + return concat { + tohex(h0), + tohex(h1), + tohex(h2), + tohex(h3), + tohex(h4) + } + end + + -- We have a full block to process. Let's do it! + function processBlock() + + -- Extend the sixteen 32-bit words into eighty 32-bit words: + for i = 16, 79, 1 do + W[i] = + rol(bxor(W[i - 3], W[i - 8], W[i - 14], W[i - 16]), 1) + end + + -- print("Block Contents:") + -- for i = 0, 15 do + -- print(string.format(" W[%d] = %s", i, tohex(W[i]))) + -- end + -- print() + + -- Initialize hash value for this chunk: + local a = h0 + local b = h1 + local c = h2 + local d = h3 + local e = h4 + local f, k + + -- print(" A B C D E") + -- local format = + -- "t=%02d: %s %s %s %s %s" + -- Main loop: + for t = 0, 79 do + if t < 20 then + f = bxor(d, band(b, bxor(c, d))) + k = 0x5A827999 + elseif t < 40 then + f = bxor(b, c, d) + k = 0x6ED9EBA1 + elseif t < 60 then + f = bor(band(b, c), (band(d, bor(b, c)))) + k = 0x8F1BBCDC + else + f = bxor(b, c, d) + k = 0xCA62C1D6 + end + e, d, c, b, a = + d, + c, + rol(b, 30), + a, + tobit( + unsigned(rol(a, 5)) + + unsigned(f) + + unsigned(e) + + unsigned(k) + + W[t] + ) + -- print(string.format(format, t, tohex(a), tohex(b), tohex(c), tohex(d), tohex(e))) + end + + -- Add this chunk's hash to result so far: + h0 = tobit(unsigned(h0) + a) + h1 = tobit(unsigned(h1) + b) + h2 = tobit(unsigned(h2) + c) + h3 = tobit(unsigned(h3) + d) + h4 = tobit(unsigned(h4) + e) + + -- The block is now reusable. + offset = 0 + for i = 0, 15 do + W[i] = 0 + end + end + + return { + update = update, + digest = digest + } + +end + +return function (buffer) + -- Pass in false or nil to get a streaming interface. + if not buffer then + return create(false) + end + local shasum = create(true) + shasum.update(buffer) + return shasum.digest() +end diff --git a/deps/sha1/test-sha1.lua b/deps/sha1/test-sha1.lua new file mode 100644 index 0000000..f4b4aa2 --- /dev/null +++ b/deps/sha1/test-sha1.lua @@ -0,0 +1,22 @@ + +local sha1 = require('./init') +assert(sha1("") == "da39a3ee5e6b4b0d3255bfef95601890afd80709") +assert(sha1("abc") == "a9993e364706816aba3e25717850c26c9cd0d89d") +assert(sha1("abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq") + == "84983e441c3bd26ebaae4aa1f95129e5e54670f1") +assert(sha1("abcdefghbcdefghicdefghijdefghijkefghijklfghijklmghijklmnhijklmnoijklmnopjklmnopqklmnopqrlmnopqrsmnopqrstnopqrstu") + == "a49b2446a02c645bf419f995b67091253a04a259") +assert(sha1(string.rep("a", 1000000)) + == "34aa973cd4c4daa4f61eeb2bdbad27316534016f") +local sum = sha1() +sum.update("a") +sum.update("bc") +assert(sum.digest() == "a9993e364706816aba3e25717850c26c9cd0d89d") +sum = sha1() +local aa = string.rep("a", 1000) +for i = 1, 1000 do + sum.update(aa) +end +assert(sum.digest() == "34aa973cd4c4daa4f61eeb2bdbad27316534016f") + +print("All tests pass") diff --git a/deps/websocket-codec.lua b/deps/websocket-codec.lua new file mode 100644 index 0000000..3ea1611 --- /dev/null +++ b/deps/websocket-codec.lua @@ -0,0 +1,301 @@ +--[[lit-meta + name = "creationix/websocket-codec" + description = "A codec implementing websocket framing and helpers for handshakeing" + version = "3.0.2" + dependencies = { + "creationix/base64@2.0.0", + "creationix/sha1@1.0.0", + } + homepage = "https://github.com/luvit/lit/blob/master/deps/websocket-codec.lua" + tags = {"http", "websocket", "codec"} + license = "MIT" + author = { name = "Tim Caswell" } +]] + +local base64 = require('base64').encode +local sha1 = require('sha1') +local bit = require('bit') + +local band = bit.band +local bor = bit.bor +local bxor = bit.bxor +local rshift = bit.rshift +local lshift = bit.lshift +local char = string.char +local byte = string.byte +local sub = string.sub +local gmatch = string.gmatch +local lower = string.lower +local gsub = string.gsub +local concat = table.concat +local floor = math.floor +local random = math.random + +local function rand4() + -- Generate 32 bits of pseudo random data + local num = floor(random() * 0x100000000) + -- Return as a 4-byte string + return char( + rshift(num, 24), + band(rshift(num, 16), 0xff), + band(rshift(num, 8), 0xff), + band(num, 0xff) + ) +end + +local function applyMask(data, mask) + local bytes = { + [0] = byte(mask, 1), + [1] = byte(mask, 2), + [2] = byte(mask, 3), + [3] = byte(mask, 4) + } + local out = {} + for i = 1, #data do + out[i] = char( + bxor(byte(data, i), bytes[(i - 1) % 4]) + ) + end + return concat(out) +end + +local function decode(chunk, index) + local start = index - 1 + local length = #chunk - start + if length < 2 then return end + local second = byte(chunk, start + 2) + local len = band(second, 0x7f) + local offset + if len == 126 then + if length < 4 then return end + len = bor( + lshift(byte(chunk, start + 3), 8), + byte(chunk, start + 4)) + offset = 4 + elseif len == 127 then + if length < 10 then return end + len = bor( + lshift(byte(chunk, start + 3), 24), + lshift(byte(chunk, start + 4), 16), + lshift(byte(chunk, start + 5), 8), + byte(chunk, start + 6) + ) * 0x100000000 + bor( + lshift(byte(chunk, start + 7), 24), + lshift(byte(chunk, start + 8), 16), + lshift(byte(chunk, start + 9), 8), + byte(chunk, start + 10) + ) + offset = 10 + else + offset = 2 + end + local mask = band(second, 0x80) > 0 + if mask then + offset = offset + 4 + end + offset = offset + start + if #chunk < offset + len then return end + + local first = byte(chunk, start + 1) + local payload = sub(chunk, offset + 1, offset + len) + assert(#payload == len, "Length mismatch") + if mask then + payload = applyMask(payload, sub(chunk, offset - 3, offset)) + end + return { + fin = band(first, 0x80) > 0, + rsv1 = band(first, 0x40) > 0, + rsv2 = band(first, 0x20) > 0, + rsv3 = band(first, 0x10) > 0, + opcode = band(first, 0xf), + mask = mask, + len = len, + payload = payload + }, offset + len + 1 +end + +local function encode(item) + if type(item) == "string" then + item = { + opcode = 2, + payload = item + } + end + local payload = item.payload + assert(type(payload) == "string", "payload must be string") + local len = #payload + local fin = item.fin + if fin == nil then fin = true end + local rsv1 = item.rsv1 + local rsv2 = item.rsv2 + local rsv3 = item.rsv3 + local opcode = item.opcode or 2 + local mask = item.mask + local chars = { + char(bor( + fin and 0x80 or 0, + rsv1 and 0x40 or 0, + rsv2 and 0x20 or 0, + rsv3 and 0x10 or 0, + opcode + )), + char(bor( + mask and 0x80 or 0, + len < 126 and len or (len < 0x10000) and 126 or 127 + )) + } + if len >= 0x10000 then + local high = len / 0x100000000 + chars[3] = char(band(rshift(high, 24), 0xff)) + chars[4] = char(band(rshift(high, 16), 0xff)) + chars[5] = char(band(rshift(high, 8), 0xff)) + chars[6] = char(band(high, 0xff)) + chars[7] = char(band(rshift(len, 24), 0xff)) + chars[8] = char(band(rshift(len, 16), 0xff)) + chars[9] = char(band(rshift(len, 8), 0xff)) + chars[10] = char(band(len, 0xff)) + elseif len >= 126 then + chars[3] = char(band(rshift(len, 8), 0xff)) + chars[4] = char(band(len, 0xff)) + end + if mask then + local key = rand4() + return concat(chars) .. key .. applyMask(payload, key) + end + return concat(chars) .. payload +end + +local websocketGuid = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" + +-- Given two hex characters, return a single character +local function hexToBin(cc) + return string.char(tonumber(cc, 16)) +end + +local function decodeHex(hex) + local bin = string.gsub(hex, "..", hexToBin) + return bin +end + +local function acceptKey(key) + return gsub(base64(decodeHex(sha1(key .. websocketGuid))), "\n", "") +end + +-- Make a client handshake connection +local function handshake(options, request) + -- Generate 20 bytes of pseudo-random data + local key = concat({rand4(), rand4(), rand4(), rand4(), rand4()}) + key = base64(key) + local host = options.host + local path = options.path or "/" + local protocol = options.protocol + local req = { + method = "GET", + path = path, + {"Connection", "Upgrade"}, + {"Upgrade", "websocket"}, + {"Sec-WebSocket-Version", "13"}, + {"Sec-WebSocket-Key", key}, + } + for i = 1, #options do + req[#req + 1] = options[i] + end + if host then + req[#req + 1] = {"Host", host} + end + if protocol then + req[#req + 1] = {"Sec-WebSocket-Protocol", protocol} + end + local res = request(req) + if not res then + return nil, "Missing response from server" + end + -- Parse the headers for quick reading + if res.code ~= 101 then + return nil, "response must be code 101" + end + + local headers = {} + for i = 1, #res do + local name, value = unpack(res[i]) + headers[lower(name)] = value + end + + if not headers.connection or lower(headers.connection) ~= "upgrade" then + return nil, "Invalid or missing connection upgrade header in response" + end + if headers["sec-websocket-accept"] ~= acceptKey(key) then + return nil, "challenge key missing or mismatched" + end + if protocol and headers["sec-websocket-protocol"] ~= protocol then + return nil, "protocol missing or mistmatched" + end + return true +end + +local function handleHandshake(head, protocol) + + -- WebSocket connections must be GET requests + if not head.method == "GET" then return end + + -- Parse the headers for quick reading + local headers = {} + for i = 1, #head do + local name, value = unpack(head[i]) + headers[lower(name)] = value + end + + -- Must have 'Upgrade: websocket' and 'Connection: Upgrade' headers + if not (headers.connection and headers.upgrade and + headers.connection:lower():find("upgrade", 1, true) and + headers.upgrade:lower():find("websocket", 1, true)) then return end + + -- Make sure it's a new client speaking v13 of the protocol + if tonumber(headers["sec-websocket-version"]) < 13 then + return nil, "only websocket protocol v13 supported" + end + + local key = headers["sec-websocket-key"] + if not key then + return nil, "websocket security key missing" + end + + -- If the server wants a specified protocol, check for it. + if protocol then + local foundProtocol = false + local list = headers["sec-websocket-protocol"] + if list then + for item in gmatch(list, "[^, ]+") do + if item == protocol then + foundProtocol = true + break + end + end + end + if not foundProtocol then + return nil, "specified protocol missing in request" + end + end + + local accept = acceptKey(key) + + local res = { + code = 101, + {"Upgrade", "websocket"}, + {"Connection", "Upgrade"}, + {"Sec-WebSocket-Accept", accept}, + } + if protocol then + res[#res + 1] = {"Sec-WebSocket-Protocol", protocol} + end + + return res +end + +return { + decode = decode, + encode = encode, + acceptKey = acceptKey, + handshake = handshake, + handleHandshake = handleHandshake, +} diff --git a/gateway.json b/gateway.json new file mode 100644 index 0000000..447da71 --- /dev/null +++ b/gateway.json @@ -0,0 +1 @@ +{"url":"wss://gateway.discord.gg","873255296024322059":{"shards":1,"timestamp":1628272516,"owner":{"avatar":null,"discriminator":"0000","id":"872299874857676820","public_flags":1024,"username":"team872299874857676820","flags":1024}}} \ No newline at end of file