initial commit
This commit is contained in:
commit
299102fbc4
|
@ -0,0 +1 @@
|
||||||
|
/token
|
|
@ -0,0 +1,33 @@
|
||||||
|
package.path = "./libraries/?.lua;./libraries/?/init.lua;"..package.path
|
||||||
|
|
||||||
|
--load discordia
|
||||||
|
discordia = require("discordia")
|
||||||
|
client = discordia.Client()
|
||||||
|
|
||||||
|
--activate the import system
|
||||||
|
local import = require("import")(require)
|
||||||
|
|
||||||
|
--create server
|
||||||
|
local server = import("classes.server-handler")
|
||||||
|
client:on("ready",function()
|
||||||
|
print("starting test")
|
||||||
|
local new_server = server(client,client:getGuild("640251445949759499"),{
|
||||||
|
autosave_frequency = 20,
|
||||||
|
default_plugins = {
|
||||||
|
"meta",
|
||||||
|
"help",
|
||||||
|
"plugins",
|
||||||
|
"esolang",
|
||||||
|
"tools"
|
||||||
|
}
|
||||||
|
})
|
||||||
|
end)
|
||||||
|
|
||||||
|
--load token
|
||||||
|
local tempfile = io.open("./token","r")
|
||||||
|
if not tempfile then
|
||||||
|
error("./token file does not exist")
|
||||||
|
end
|
||||||
|
local nstr = tempfile:read("*l")
|
||||||
|
tempfile:close()
|
||||||
|
client:run('Bot '..nstr)
|
|
@ -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,
|
||||||
|
}
|
|
@ -0,0 +1,183 @@
|
||||||
|
--[[lit-meta
|
||||||
|
name = "creationix/coro-channel"
|
||||||
|
version = "3.0.1"
|
||||||
|
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 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
|
||||||
|
assert(coroutine.resume(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)
|
||||||
|
assert(coroutine.resume(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,
|
||||||
|
}
|
|
@ -0,0 +1,195 @@
|
||||||
|
--[[lit-meta
|
||||||
|
name = "creationix/coro-http"
|
||||||
|
version = "3.1.0"
|
||||||
|
dependencies = {
|
||||||
|
"creationix/coro-net@3.0.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)
|
||||||
|
net.createServer({
|
||||||
|
host = host,
|
||||||
|
port = port,
|
||||||
|
encode = httpCodec.encoder(),
|
||||||
|
decode = 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
|
||||||
|
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,
|
||||||
|
encode = httpCodec.encoder(),
|
||||||
|
decode = 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, timeout)
|
||||||
|
local uri = parseUrl(url)
|
||||||
|
local connection = getConnection(uri.hostname, uri.port, uri.tls, 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) 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,
|
||||||
|
}
|
|
@ -0,0 +1,181 @@
|
||||||
|
--[[lit-meta
|
||||||
|
name = "creationix/coro-net"
|
||||||
|
version = "3.2.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 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 assert(coroutine.resume(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 assert(coroutine.resume(thread, nil, err))
|
||||||
|
end
|
||||||
|
return assert(coroutine.resume(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.decode then
|
||||||
|
read, updateDecoder = decoder(read, options.decode)
|
||||||
|
end
|
||||||
|
if 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.decode then
|
||||||
|
read, updateDecoder = decoder(read, options.decode)
|
||||||
|
end
|
||||||
|
if 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,
|
||||||
|
}
|
|
@ -0,0 +1,196 @@
|
||||||
|
--[[lit-meta
|
||||||
|
name = "creationix/coro-websocket"
|
||||||
|
version = "3.1.0"
|
||||||
|
dependencies = {
|
||||||
|
"luvit/http-codec@3.0.0",
|
||||||
|
"creationix/websocket-codec@3.0.0",
|
||||||
|
"creationix/coro-net@3.0.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,
|
||||||
|
encode = httpCodec.encoder(),
|
||||||
|
decode = 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,
|
||||||
|
}
|
|
@ -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,
|
||||||
|
}
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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 = {},
|
||||||
|
}
|
|
@ -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})
|
|
@ -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
|
|
@ -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 stop 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-Resolveable
|
||||||
|
@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 users'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 users'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
|
|
@ -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
|
|
@ -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
|
|
@ -0,0 +1,252 @@
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
|
||||||
|
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:warning('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
|
|
@ -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
|
|
@ -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,
|
||||||
|
}
|
|
@ -0,0 +1,148 @@
|
||||||
|
--[=[
|
||||||
|
@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
|
||||||
|
|
||||||
|
--[=[@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
|
|
@ -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
|
|
@ -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
|
|
@ -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 '<a:%s>' 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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -0,0 +1,539 @@
|
||||||
|
--[=[
|
||||||
|
@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 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 = Resolver.channelId(id)
|
||||||
|
local data, err = self.client._api:modifyGuildMember(self._parent._id, self.id, {channel_id = id})
|
||||||
|
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
|
|
@ -0,0 +1,579 @@
|
||||||
|
--[=[
|
||||||
|
@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)
|
||||||
|
|
||||||
|
if data.mentions then
|
||||||
|
for _, user in ipairs(data.mentions) do
|
||||||
|
if user.member then
|
||||||
|
user.member.user = user
|
||||||
|
self._parent._parent._members:_insert(user.member)
|
||||||
|
else
|
||||||
|
self.client._users:_insert(user)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local content = data.content
|
||||||
|
if content then
|
||||||
|
if self._mentioned_users then
|
||||||
|
self._mentioned_users._array = parseMentions(content, '<@!?(%d+)>')
|
||||||
|
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, '<a?:[%w_]+:(%d+)>')
|
||||||
|
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
|
||||||
|
|
||||||
|
local emoji = d.emoji
|
||||||
|
local k = emoji.id ~= null and emoji.id or emoji.name
|
||||||
|
local reaction = reactions:get(k)
|
||||||
|
|
||||||
|
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+)>')
|
||||||
|
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, '<a?:[%w_]+:(%d+)>')
|
||||||
|
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('<a?(:[%w_]+:)%d+>', '%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 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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -0,0 +1,326 @@
|
||||||
|
--[=[
|
||||||
|
@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
|
||||||
|
|
||||||
|
data, err = self.client._api:createMessage(self._id, {
|
||||||
|
content = content,
|
||||||
|
tts = tbl.tts,
|
||||||
|
nonce = tbl.nonce,
|
||||||
|
embed = tbl.embed,
|
||||||
|
}, 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
|
|
@ -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
|
|
@ -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",
|
||||||
|
}
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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 satistfy 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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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 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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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 objects 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 name
|
||||||
|
@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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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('<I2', data, -2)
|
||||||
|
return wrap(self.selectProtocol)(self, client_ip, client_port)
|
||||||
|
end)
|
||||||
|
return udp:send(PADDING, server_ip, server_port)
|
||||||
|
end
|
||||||
|
|
||||||
|
function VoiceSocket:selectProtocol(address, port)
|
||||||
|
return self:_send(SELECT_PROTOCOL, {
|
||||||
|
protocol = 'udp',
|
||||||
|
data = {
|
||||||
|
address = address,
|
||||||
|
port = port,
|
||||||
|
mode = self._mode,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
function VoiceSocket:setSpeaking(speaking)
|
||||||
|
return self:_send(SPEAKING, {
|
||||||
|
speaking = speaking,
|
||||||
|
delay = 0,
|
||||||
|
ssrc = self._ssrc,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
return VoiceSocket
|
|
@ -0,0 +1,241 @@
|
||||||
|
local ffi = require('ffi')
|
||||||
|
|
||||||
|
local loaded, lib = pcall(ffi.load, 'opus')
|
||||||
|
if not loaded then
|
||||||
|
return nil, lib
|
||||||
|
end
|
||||||
|
|
||||||
|
local new, typeof, gc = ffi.new, ffi.typeof, ffi.gc
|
||||||
|
|
||||||
|
ffi.cdef[[
|
||||||
|
typedef int16_t opus_int16;
|
||||||
|
typedef int32_t opus_int32;
|
||||||
|
typedef uint16_t opus_uint16;
|
||||||
|
typedef uint32_t opus_uint32;
|
||||||
|
|
||||||
|
typedef struct OpusEncoder OpusEncoder;
|
||||||
|
typedef struct OpusDecoder OpusDecoder;
|
||||||
|
|
||||||
|
const char *opus_strerror(int error);
|
||||||
|
const char *opus_get_version_string(void);
|
||||||
|
|
||||||
|
OpusEncoder *opus_encoder_create(opus_int32 Fs, int channels, int application, int *error);
|
||||||
|
int opus_encoder_init(OpusEncoder *st, opus_int32 Fs, int channels, int application);
|
||||||
|
int opus_encoder_get_size(int channels);
|
||||||
|
int opus_encoder_ctl(OpusEncoder *st, int request, ...);
|
||||||
|
void opus_encoder_destroy(OpusEncoder *st);
|
||||||
|
|
||||||
|
opus_int32 opus_encode(
|
||||||
|
OpusEncoder *st,
|
||||||
|
const opus_int16 *pcm,
|
||||||
|
int frame_size,
|
||||||
|
unsigned char *data,
|
||||||
|
opus_int32 max_data_bytes
|
||||||
|
);
|
||||||
|
|
||||||
|
opus_int32 opus_encode_float(
|
||||||
|
OpusEncoder *st,
|
||||||
|
const float *pcm,
|
||||||
|
int frame_size,
|
||||||
|
unsigned char *data,
|
||||||
|
opus_int32 max_data_bytes
|
||||||
|
);
|
||||||
|
|
||||||
|
OpusDecoder *opus_decoder_create(opus_int32 Fs, int channels, int *error);
|
||||||
|
int opus_decoder_init(OpusDecoder *st, opus_int32 Fs, int channels);
|
||||||
|
int opus_decoder_get_size(int channels);
|
||||||
|
int opus_decoder_ctl(OpusDecoder *st, int request, ...);
|
||||||
|
void opus_decoder_destroy(OpusDecoder *st);
|
||||||
|
|
||||||
|
int opus_decode(
|
||||||
|
OpusDecoder *st,
|
||||||
|
const unsigned char *data,
|
||||||
|
opus_int32 len,
|
||||||
|
opus_int16 *pcm,
|
||||||
|
int frame_size,
|
||||||
|
int decode_fec
|
||||||
|
);
|
||||||
|
|
||||||
|
int opus_decode_float(
|
||||||
|
OpusDecoder *st,
|
||||||
|
const unsigned char *data,
|
||||||
|
opus_int32 len,
|
||||||
|
float *pcm,
|
||||||
|
int frame_size,
|
||||||
|
int decode_fec
|
||||||
|
);
|
||||||
|
]]
|
||||||
|
|
||||||
|
local opus = {}
|
||||||
|
|
||||||
|
opus.OK = 0
|
||||||
|
opus.BAD_ARG = -1
|
||||||
|
opus.BUFFER_TOO_SMALL = -2
|
||||||
|
opus.INTERNAL_ERROR = -3
|
||||||
|
opus.INVALID_PACKET = -4
|
||||||
|
opus.UNIMPLEMENTED = -5
|
||||||
|
opus.INVALID_STATE = -6
|
||||||
|
opus.ALLOC_FAIL = -7
|
||||||
|
|
||||||
|
opus.APPLICATION_VOIP = 2048
|
||||||
|
opus.APPLICATION_AUDIO = 2049
|
||||||
|
opus.APPLICATION_RESTRICTED_LOWDELAY = 2051
|
||||||
|
|
||||||
|
opus.AUTO = -1000
|
||||||
|
opus.BITRATE_MAX = -1
|
||||||
|
|
||||||
|
opus.SIGNAL_VOICE = 3001
|
||||||
|
opus.SIGNAL_MUSIC = 3002
|
||||||
|
opus.BANDWIDTH_NARROWBAND = 1101
|
||||||
|
opus.BANDWIDTH_MEDIUMBAND = 1102
|
||||||
|
opus.BANDWIDTH_WIDEBAND = 1103
|
||||||
|
opus.BANDWIDTH_SUPERWIDEBAND = 1104
|
||||||
|
opus.BANDWIDTH_FULLBAND = 1105
|
||||||
|
|
||||||
|
opus.SET_APPLICATION_REQUEST = 4000
|
||||||
|
opus.GET_APPLICATION_REQUEST = 4001
|
||||||
|
opus.SET_BITRATE_REQUEST = 4002
|
||||||
|
opus.GET_BITRATE_REQUEST = 4003
|
||||||
|
opus.SET_MAX_BANDWIDTH_REQUEST = 4004
|
||||||
|
opus.GET_MAX_BANDWIDTH_REQUEST = 4005
|
||||||
|
opus.SET_VBR_REQUEST = 4006
|
||||||
|
opus.GET_VBR_REQUEST = 4007
|
||||||
|
opus.SET_BANDWIDTH_REQUEST = 4008
|
||||||
|
opus.GET_BANDWIDTH_REQUEST = 4009
|
||||||
|
opus.SET_COMPLEXITY_REQUEST = 4010
|
||||||
|
opus.GET_COMPLEXITY_REQUEST = 4011
|
||||||
|
opus.SET_INBAND_FEC_REQUEST = 4012
|
||||||
|
opus.GET_INBAND_FEC_REQUEST = 4013
|
||||||
|
opus.SET_PACKET_LOSS_PERC_REQUEST = 4014
|
||||||
|
opus.GET_PACKET_LOSS_PERC_REQUEST = 4015
|
||||||
|
opus.SET_DTX_REQUEST = 4016
|
||||||
|
opus.GET_DTX_REQUEST = 4017
|
||||||
|
opus.SET_VBR_CONSTRAINT_REQUEST = 4020
|
||||||
|
opus.GET_VBR_CONSTRAINT_REQUEST = 4021
|
||||||
|
opus.SET_FORCE_CHANNELS_REQUEST = 4022
|
||||||
|
opus.GET_FORCE_CHANNELS_REQUEST = 4023
|
||||||
|
opus.SET_SIGNAL_REQUEST = 4024
|
||||||
|
opus.GET_SIGNAL_REQUEST = 4025
|
||||||
|
opus.GET_LOOKAHEAD_REQUEST = 4027
|
||||||
|
opus.GET_SAMPLE_RATE_REQUEST = 4029
|
||||||
|
opus.GET_FINAL_RANGE_REQUEST = 4031
|
||||||
|
opus.GET_PITCH_REQUEST = 4033
|
||||||
|
opus.SET_GAIN_REQUEST = 4034
|
||||||
|
opus.GET_GAIN_REQUEST = 4045
|
||||||
|
opus.SET_LSB_DEPTH_REQUEST = 4036
|
||||||
|
opus.GET_LSB_DEPTH_REQUEST = 4037
|
||||||
|
opus.GET_LAST_PACKET_DURATION_REQUEST = 4039
|
||||||
|
opus.SET_EXPERT_FRAME_DURATION_REQUEST = 4040
|
||||||
|
opus.GET_EXPERT_FRAME_DURATION_REQUEST = 4041
|
||||||
|
opus.SET_PREDICTION_DISABLED_REQUEST = 4042
|
||||||
|
opus.GET_PREDICTION_DISABLED_REQUEST = 4043
|
||||||
|
opus.SET_PHASE_INVERSION_DISABLED_REQUEST = 4046
|
||||||
|
opus.GET_PHASE_INVERSION_DISABLED_REQUEST = 4047
|
||||||
|
|
||||||
|
opus.FRAMESIZE_ARG = 5000
|
||||||
|
opus.FRAMESIZE_2_5_MS = 5001
|
||||||
|
opus.FRAMESIZE_5_MS = 5002
|
||||||
|
opus.FRAMESIZE_10_MS = 5003
|
||||||
|
opus.FRAMESIZE_20_MS = 5004
|
||||||
|
opus.FRAMESIZE_40_MS = 5005
|
||||||
|
opus.FRAMESIZE_60_MS = 5006
|
||||||
|
opus.FRAMESIZE_80_MS = 5007
|
||||||
|
opus.FRAMESIZE_100_MS = 5008
|
||||||
|
opus.FRAMESIZE_120_MS = 5009
|
||||||
|
|
||||||
|
local int_ptr_t = typeof('int[1]')
|
||||||
|
local opus_int32_t = typeof('opus_int32')
|
||||||
|
local opus_int32_ptr_t = typeof('opus_int32[1]')
|
||||||
|
|
||||||
|
local function throw(code)
|
||||||
|
local version = ffi.string(lib.opus_get_version_string())
|
||||||
|
local message = ffi.string(lib.opus_strerror(code))
|
||||||
|
return error(string.format('[%s] %s', version, message))
|
||||||
|
end
|
||||||
|
|
||||||
|
local function check(value)
|
||||||
|
return value >= 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 appplications
|
||||||
|
|
||||||
|
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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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.8.4',
|
||||||
|
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'},
|
||||||
|
}
|
|
@ -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.5"
|
||||||
|
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,
|
||||||
|
}
|
|
@ -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,
|
||||||
|
}
|
|
@ -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 })
|
|
@ -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
|
|
@ -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 > 0x10100000
|
||||||
|
|
||||||
|
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
|
|
@ -0,0 +1,34 @@
|
||||||
|
--[[
|
||||||
|
|
||||||
|
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')
|
||||||
|
|
||||||
|
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 assert(coroutine.resume(thread, ssocket, err))
|
||||||
|
end, options.servername)
|
||||||
|
if not callback then
|
||||||
|
return coroutine.yield()
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,12 @@
|
||||||
|
return {
|
||||||
|
name = "luvit/secure-socket",
|
||||||
|
version = "1.2.2",
|
||||||
|
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" }
|
||||||
|
}
|
Binary file not shown.
|
@ -0,0 +1,194 @@
|
||||||
|
--[[lit-meta
|
||||||
|
name = "creationix/sha1"
|
||||||
|
version = "1.0.3"
|
||||||
|
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
|
|
@ -0,0 +1,638 @@
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
-- A library for interfacing with SQLite3 databases.
|
||||||
|
--
|
||||||
|
-- Copyright (C) 2011-2016 Stefano Peluchetti. All rights reserved.
|
||||||
|
--
|
||||||
|
-- Features, documentation and more: http://www.scilua.org .
|
||||||
|
--
|
||||||
|
-- This file is part of the LJSQLite3 library, which is released under the MIT
|
||||||
|
-- license: full text in file LICENSE.TXT in the library's root folder.
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
--[[lit-meta
|
||||||
|
name = "SinisterRectus/sqlite3"
|
||||||
|
version = "1.0.1"
|
||||||
|
homepage = "http://scilua.org/ljsqlite3.html"
|
||||||
|
description = "SciLua's sqlite3 bindings repackaged for lit."
|
||||||
|
tags = {"database", "sqlite3"}
|
||||||
|
license = "MIT"
|
||||||
|
author = "Stefano Peluchetti"
|
||||||
|
contributors = {
|
||||||
|
"Sinister Rectus"
|
||||||
|
}
|
||||||
|
]]
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
-- TODO: Refactor according to latest style / coding guidelines.
|
||||||
|
-- TODO: introduce functionality to get of a defined type to avoid if check?
|
||||||
|
-- TODO: Add extended error codes from Sqlite?
|
||||||
|
-- TODO: Consider type checks?
|
||||||
|
-- TODO: Exposed cdef constants are ok?
|
||||||
|
-- TODO: Resultset (and so exec) could be optimized by avoiding loads/stores
|
||||||
|
-- TODO: of row table via _step?
|
||||||
|
|
||||||
|
local ffi = require "ffi"
|
||||||
|
local bit = require "bit"
|
||||||
|
local jit = require "jit"
|
||||||
|
|
||||||
|
---- xsys replacement ----------------------------------------------------------
|
||||||
|
|
||||||
|
local insert = table.insert
|
||||||
|
local match, gmatch = string.match, string.gmatch
|
||||||
|
|
||||||
|
local function split(str, delim)
|
||||||
|
local words = {}
|
||||||
|
for word in gmatch(str .. delim, '(.-)' .. delim) do
|
||||||
|
insert(words, word)
|
||||||
|
end
|
||||||
|
return words
|
||||||
|
end
|
||||||
|
|
||||||
|
local function trim(str)
|
||||||
|
return match(str, '^%s*(.-)%s*$')
|
||||||
|
end
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
local function err(code, msg)
|
||||||
|
error("ljsqlite3["..code.."] "..msg)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Codes -----------------------------------------------------------------------
|
||||||
|
local sqlconstants = {} -- SQLITE_* and OPEN_* declarations.
|
||||||
|
local codes = {
|
||||||
|
[0] = "OK", "ERROR", "INTERNAL", "PERM", "ABORT", "BUSY", "LOCKED", "NOMEM",
|
||||||
|
"READONLY", "INTERRUPT", "IOERR", "CORRUPT", "NOTFOUND", "FULL", "CANTOPEN",
|
||||||
|
"PROTOCOL", "EMPTY", "SCHEMA", "TOOBIG", "CONSTRAINT", "MISMATCH", "MISUSE",
|
||||||
|
"NOLFS", "AUTH", "FORMAT", "RANGE", "NOTADB", [100] = "ROW", [101] = "DONE"
|
||||||
|
} -- From 0 to 26.
|
||||||
|
|
||||||
|
do
|
||||||
|
local types = { "INTEGER", "FLOAT", "TEXT", "BLOB", "NULL" } -- From 1 to 5.
|
||||||
|
|
||||||
|
local opens = {
|
||||||
|
READONLY = 0x00000001;
|
||||||
|
READWRITE = 0x00000002;
|
||||||
|
CREATE = 0x00000004;
|
||||||
|
DELETEONCLOSE = 0x00000008;
|
||||||
|
EXCLUSIVE = 0x00000010;
|
||||||
|
AUTOPROXY = 0x00000020;
|
||||||
|
URI = 0x00000040;
|
||||||
|
MAIN_DB = 0x00000100;
|
||||||
|
TEMP_DB = 0x00000200;
|
||||||
|
TRANSIENT_DB = 0x00000400;
|
||||||
|
MAIN_JOURNAL = 0x00000800;
|
||||||
|
TEMP_JOURNAL = 0x00001000;
|
||||||
|
SUBJOURNAL = 0x00002000;
|
||||||
|
MASTER_JOURNAL = 0x00004000;
|
||||||
|
NOMUTEX = 0x00008000;
|
||||||
|
FULLMUTEX = 0x00010000;
|
||||||
|
SHAREDCACHE = 0x00020000;
|
||||||
|
PRIVATECACHE = 0x00040000;
|
||||||
|
WAL = 0x00080000;
|
||||||
|
}
|
||||||
|
|
||||||
|
local t = sqlconstants
|
||||||
|
local pre = "static const int32_t SQLITE_"
|
||||||
|
for i=0,26 do t[#t+1] = pre..codes[i].."="..i..";\n" end
|
||||||
|
for i=100,101 do t[#t+1] = pre..codes[i].."="..i..";\n" end
|
||||||
|
for i=1,5 do t[#t+1] = pre..types[i].."="..i..";\n" end
|
||||||
|
pre = pre.."OPEN_"
|
||||||
|
for k,v in pairs(opens) do t[#t+1] = pre..k.."="..bit.tobit(v)..";\n" end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Cdef ------------------------------------------------------------------------
|
||||||
|
-- SQLITE_*, OPEN_*
|
||||||
|
ffi.cdef(table.concat(sqlconstants))
|
||||||
|
|
||||||
|
-- sqlite3*, ljsqlite3_*
|
||||||
|
ffi.cdef[[
|
||||||
|
// Typedefs.
|
||||||
|
typedef struct sqlite3 sqlite3;
|
||||||
|
typedef struct sqlite3_stmt sqlite3_stmt;
|
||||||
|
typedef void (*sqlite3_destructor_type)(void*);
|
||||||
|
typedef struct sqlite3_context sqlite3_context;
|
||||||
|
typedef struct Mem sqlite3_value;
|
||||||
|
|
||||||
|
// Get informative error message.
|
||||||
|
const char *sqlite3_errmsg(sqlite3*);
|
||||||
|
|
||||||
|
// Connection.
|
||||||
|
int sqlite3_open_v2(const char *filename, sqlite3 **ppDb, int flags,
|
||||||
|
const char *zVfs);
|
||||||
|
int sqlite3_close(sqlite3*);
|
||||||
|
int sqlite3_busy_timeout(sqlite3*, int ms);
|
||||||
|
|
||||||
|
// Statement.
|
||||||
|
int sqlite3_prepare_v2(sqlite3 *conn, const char *zSql, int nByte,
|
||||||
|
sqlite3_stmt **ppStmt, const char **pzTail);
|
||||||
|
int sqlite3_step(sqlite3_stmt*);
|
||||||
|
int sqlite3_reset(sqlite3_stmt *pStmt);
|
||||||
|
int sqlite3_finalize(sqlite3_stmt *pStmt);
|
||||||
|
|
||||||
|
// Extra functions for SELECT.
|
||||||
|
int sqlite3_column_count(sqlite3_stmt *pStmt);
|
||||||
|
const char *sqlite3_column_name(sqlite3_stmt*, int N);
|
||||||
|
int sqlite3_column_type(sqlite3_stmt*, int iCol);
|
||||||
|
|
||||||
|
// Get value from SELECT.
|
||||||
|
int64_t sqlite3_column_int64(sqlite3_stmt*, int iCol);
|
||||||
|
double sqlite3_column_double(sqlite3_stmt*, int iCol);
|
||||||
|
int sqlite3_column_bytes(sqlite3_stmt*, int iCol);
|
||||||
|
const unsigned char *sqlite3_column_text(sqlite3_stmt*, int iCol);
|
||||||
|
const void *sqlite3_column_blob(sqlite3_stmt*, int iCol);
|
||||||
|
|
||||||
|
// Set value in bind.
|
||||||
|
int sqlite3_bind_int64(sqlite3_stmt*, int, int64_t);
|
||||||
|
int sqlite3_bind_double(sqlite3_stmt*, int, double);
|
||||||
|
int sqlite3_bind_null(sqlite3_stmt*, int);
|
||||||
|
int sqlite3_bind_text(sqlite3_stmt*, int, const char*, int n, void(*)(void*));
|
||||||
|
int sqlite3_bind_blob(sqlite3_stmt*, int, const void*, int n, void(*)(void*));
|
||||||
|
|
||||||
|
// Clear bindings.
|
||||||
|
int sqlite3_clear_bindings(sqlite3_stmt*);
|
||||||
|
|
||||||
|
// Get value in callbacks.
|
||||||
|
int sqlite3_value_type(sqlite3_value*);
|
||||||
|
int64_t sqlite3_value_int64(sqlite3_value*);
|
||||||
|
double sqlite3_value_double(sqlite3_value*);
|
||||||
|
int sqlite3_value_bytes(sqlite3_value*);
|
||||||
|
const unsigned char *sqlite3_value_text(sqlite3_value*); //Not used.
|
||||||
|
const void *sqlite3_value_blob(sqlite3_value*);
|
||||||
|
|
||||||
|
// Set value in callbacks.
|
||||||
|
void sqlite3_result_error(sqlite3_context*, const char*, int);
|
||||||
|
void sqlite3_result_int64(sqlite3_context*, int64_t);
|
||||||
|
void sqlite3_result_double(sqlite3_context*, double);
|
||||||
|
void sqlite3_result_null(sqlite3_context*);
|
||||||
|
void sqlite3_result_text(sqlite3_context*, const char*, int, void(*)(void*));
|
||||||
|
void sqlite3_result_blob(sqlite3_context*, const void*, int, void(*)(void*));
|
||||||
|
|
||||||
|
// Persistency of data in callbacks (here just a pointer for tagging).
|
||||||
|
void *sqlite3_aggregate_context(sqlite3_context*, int nBytes);
|
||||||
|
|
||||||
|
// Typedefs for callbacks.
|
||||||
|
typedef void (*ljsqlite3_cbstep)(sqlite3_context*,int,sqlite3_value**);
|
||||||
|
typedef void (*ljsqlite3_cbfinal)(sqlite3_context*);
|
||||||
|
|
||||||
|
// Register callbacks.
|
||||||
|
int sqlite3_create_function(
|
||||||
|
sqlite3 *conn,
|
||||||
|
const char *zFunctionName,
|
||||||
|
int nArg,
|
||||||
|
int eTextRep,
|
||||||
|
void *pApp,
|
||||||
|
void (*xFunc)(sqlite3_context*,int,sqlite3_value**),
|
||||||
|
void (*xStep)(sqlite3_context*,int,sqlite3_value**),
|
||||||
|
void (*xFinal)(sqlite3_context*)
|
||||||
|
);
|
||||||
|
]]
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
local sql = ffi.load("sqlite3")
|
||||||
|
|
||||||
|
local transient = ffi.cast("sqlite3_destructor_type", -1)
|
||||||
|
local int64_ct = ffi.typeof("int64_t")
|
||||||
|
|
||||||
|
local blob_mt = {} -- For tagging only.
|
||||||
|
|
||||||
|
local function blob(str)
|
||||||
|
return setmetatable({ str }, blob_mt)
|
||||||
|
end
|
||||||
|
|
||||||
|
local connstmt = {} -- Statements for a conn.
|
||||||
|
local conncb = {} -- Callbacks for a conn.
|
||||||
|
local aggregatestate = {} -- Aggregate states.
|
||||||
|
|
||||||
|
local stmt_step
|
||||||
|
|
||||||
|
local stmt_mt, stmt_ct = {}
|
||||||
|
stmt_mt.__index = stmt_mt
|
||||||
|
|
||||||
|
local conn_mt, conn_ct = {}
|
||||||
|
conn_mt.__index = conn_mt
|
||||||
|
|
||||||
|
-- Checks ----------------------------------------------------------------------
|
||||||
|
|
||||||
|
-- Helper function to get error msg and code from sqlite.
|
||||||
|
local function codemsg(pconn, code)
|
||||||
|
return codes[code]:lower(), ffi.string(sql.sqlite3_errmsg(pconn))
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Throw error for a given connection.
|
||||||
|
local function E_conn(pconn, code)
|
||||||
|
local code, msg = codemsg(pconn, code)
|
||||||
|
err(code, msg)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Test code is OK or throw error for a given connection.
|
||||||
|
local function T_okcode(pconn, code)
|
||||||
|
if code ~= sql.SQLITE_OK then
|
||||||
|
E_conn(pconn, code)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function T_open(x)
|
||||||
|
if x._closed then
|
||||||
|
err("misuse", "object is closed")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Getters / Setters to minimize code duplication ------------------------------
|
||||||
|
local sql_get_code = [=[
|
||||||
|
return function(stmt_or_value <opt_i>)
|
||||||
|
local t = sql.sqlite3_<variant>_type(stmt_or_value <opt_i>)
|
||||||
|
if t == sql.SQLITE_INTEGER then
|
||||||
|
return sql.sqlite3_<variant>_int64(stmt_or_value <opt_i>)
|
||||||
|
elseif t == sql.SQLITE_FLOAT then
|
||||||
|
return sql.sqlite3_<variant>_double(stmt_or_value <opt_i>)
|
||||||
|
elseif t == sql.SQLITE_TEXT then
|
||||||
|
local nb = sql.sqlite3_<variant>_bytes(stmt_or_value <opt_i>)
|
||||||
|
return ffi.string(sql.sqlite3_<variant>_text(stmt_or_value <opt_i>), nb)
|
||||||
|
elseif t == sql.SQLITE_BLOB then
|
||||||
|
local nb = sql.sqlite3_<variant>_bytes(stmt_or_value <opt_i>)
|
||||||
|
return ffi.string(sql.sqlite3_<variant>_blob(stmt_or_value <opt_i>), nb)
|
||||||
|
elseif t == sql.SQLITE_NULL then
|
||||||
|
return nil
|
||||||
|
else
|
||||||
|
err("constraint", "unexpected SQLite3 type")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
]=]
|
||||||
|
|
||||||
|
local sql_set_code = [=[
|
||||||
|
return function(stmt_or_value, v <opt_i>)
|
||||||
|
local t = type(v)
|
||||||
|
if ffi.istype(int64_ct, v) then
|
||||||
|
return sql.sqlite3_<variant>_int64(stmt_or_value <opt_i>, v)
|
||||||
|
elseif t == "number" then
|
||||||
|
return sql.sqlite3_<variant>_double(stmt_or_value <opt_i>, v)
|
||||||
|
elseif t == "string" then
|
||||||
|
return sql.sqlite3_<variant>_text(stmt_or_value <opt_i>, v, #v,
|
||||||
|
transient)
|
||||||
|
elseif t == "table" and getmetatable(v) == blob_mt then
|
||||||
|
v = v[1]
|
||||||
|
return sql.sqlite3_<variant>_blob(stmt_or_value <opt_i>, v, #v,
|
||||||
|
transient)
|
||||||
|
elseif t == "nil" then
|
||||||
|
return sql.sqlite3_<variant>_null(stmt_or_value <opt_i>)
|
||||||
|
else
|
||||||
|
err("constraint", "unexpected Lua type")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
]=]
|
||||||
|
|
||||||
|
-- Environment for setters/getters.
|
||||||
|
local sql_env = {
|
||||||
|
sql = sql,
|
||||||
|
transient = transient,
|
||||||
|
ffi = ffi,
|
||||||
|
int64_ct = int64_ct,
|
||||||
|
blob_mt = blob_mt,
|
||||||
|
getmetatable = getmetatable,
|
||||||
|
err = err,
|
||||||
|
type = type
|
||||||
|
}
|
||||||
|
|
||||||
|
local function sql_format(s, variant, index)
|
||||||
|
return s:gsub("<variant>", variant):gsub("<opt_i>", index)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function loadcode(s, env)
|
||||||
|
local ret = assert(loadstring(s))
|
||||||
|
if env then setfenv(ret, env) end
|
||||||
|
return ret()
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Must always be called from *:_* function due to error level 4.
|
||||||
|
local get_column = loadcode(sql_format(sql_get_code, "column", ",i"), sql_env)
|
||||||
|
local get_value = loadcode(sql_format(sql_get_code, "value" , " "), sql_env)
|
||||||
|
local set_column = loadcode(sql_format(sql_set_code, "bind" , ",i"), sql_env)
|
||||||
|
local set_value = loadcode(sql_format(sql_set_code, "result", " "), sql_env)
|
||||||
|
|
||||||
|
-- Connection ------------------------------------------------------------------
|
||||||
|
local open_modes = {
|
||||||
|
ro = sql.SQLITE_OPEN_READONLY,
|
||||||
|
rw = sql.SQLITE_OPEN_READWRITE,
|
||||||
|
rwc = bit.bor(sql.SQLITE_OPEN_READWRITE, sql.SQLITE_OPEN_CREATE)
|
||||||
|
}
|
||||||
|
|
||||||
|
local function open(str, mode)
|
||||||
|
mode = mode or "rwc"
|
||||||
|
mode = open_modes[mode]
|
||||||
|
if not mode then
|
||||||
|
err("constraint", "argument #2 to open must be ro, rw, or rwc")
|
||||||
|
end
|
||||||
|
local aptr = ffi.new("sqlite3*[1]")
|
||||||
|
-- Usually aptr is set even if error code, so conn always needs to be closed.
|
||||||
|
local code = sql.sqlite3_open_v2(str, aptr, mode, nil)
|
||||||
|
local conn = conn_ct(aptr[0], false)
|
||||||
|
-- Must create this anyway due to conn:close() function.
|
||||||
|
connstmt[conn] = setmetatable({}, { __mode = "k" })
|
||||||
|
conncb[conn] = { scalar = {}, step = {}, final = {} }
|
||||||
|
if code ~= sql.SQLITE_OK then
|
||||||
|
local code, msg = codemsg(conn._ptr, code) -- Before closing!
|
||||||
|
conn:close() -- Free resources, should not fail here in this case!
|
||||||
|
err(code, msg)
|
||||||
|
end
|
||||||
|
return conn
|
||||||
|
end
|
||||||
|
|
||||||
|
function conn_mt:close() T_open(self)
|
||||||
|
-- Close all stmt linked to conn.
|
||||||
|
for k,_ in pairs(connstmt[self]) do if not k._closed then k:close() end end
|
||||||
|
-- Close all callbacks linked to conn.
|
||||||
|
for _,v in pairs(conncb[self].scalar) do v:free() end
|
||||||
|
for _,v in pairs(conncb[self].step) do v:free() end
|
||||||
|
for _,v in pairs(conncb[self].final) do v:free() end
|
||||||
|
local code = sql.sqlite3_close(self._ptr)
|
||||||
|
T_okcode(self._ptr, code)
|
||||||
|
connstmt[self] = nil -- Table connstmt is not weak, need to clear manually.
|
||||||
|
conncb[self] = nil
|
||||||
|
self._closed = true -- Set only if close succeded.
|
||||||
|
end
|
||||||
|
|
||||||
|
function conn_mt:__gc()
|
||||||
|
if not self._closed then self:close() end
|
||||||
|
end
|
||||||
|
|
||||||
|
function conn_mt:prepare(stmtstr) T_open(self)
|
||||||
|
local aptr = ffi.new("sqlite3_stmt*[1]")
|
||||||
|
-- If error code aptr NULL, so no need to close anything.
|
||||||
|
local code = sql.sqlite3_prepare_v2(self._ptr, stmtstr, #stmtstr, aptr, nil)
|
||||||
|
T_okcode(self._ptr, code)
|
||||||
|
local stmt = stmt_ct(aptr[0], false, self._ptr, code)
|
||||||
|
connstmt[self][stmt] = true
|
||||||
|
return stmt
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Connection exec, __call, rowexec --------------------------------------------
|
||||||
|
function conn_mt:exec(commands, get) T_open(self)
|
||||||
|
local cmd1 = split(commands, ";")
|
||||||
|
local res, n
|
||||||
|
for i=1,#cmd1 do
|
||||||
|
local cmd = trim(cmd1[i])
|
||||||
|
if #cmd > 0 then
|
||||||
|
local stmt = self:prepare(cmd)
|
||||||
|
res, n = stmt:resultset(get)
|
||||||
|
stmt:close()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return res, n -- Only last record is returned.
|
||||||
|
end
|
||||||
|
|
||||||
|
function conn_mt:rowexec(command) T_open(self)
|
||||||
|
local stmt = self:prepare(command)
|
||||||
|
local res = stmt:_step()
|
||||||
|
if stmt:_step() then
|
||||||
|
err("misuse", "multiple records returned, 1 expected")
|
||||||
|
end
|
||||||
|
stmt:close()
|
||||||
|
if res then
|
||||||
|
return unpack(res)
|
||||||
|
else
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function conn_mt:__call(commands, out) T_open(self)
|
||||||
|
out = out or print
|
||||||
|
local cmd1 = split(commands, ";")
|
||||||
|
for c=1,#cmd1 do
|
||||||
|
local cmd = trim(cmd1[c])
|
||||||
|
if #cmd > 0 then
|
||||||
|
local stmt = self:prepare(cmd)
|
||||||
|
local ret, n = stmt:resultset()
|
||||||
|
if ret then -- All the results get handled, not only last one.
|
||||||
|
out(unpack(ret[0])) -- Headers are printed.
|
||||||
|
for i=1,n do
|
||||||
|
local o = {}
|
||||||
|
for j=1,#ret[0] do
|
||||||
|
local v = ret[j][i]
|
||||||
|
if type(v) == "nil" then v = "" end -- Empty strings for NULLs.
|
||||||
|
o[#o+1] = tostring(v)
|
||||||
|
end
|
||||||
|
out(unpack(o))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
stmt:close()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Callbacks -------------------------------------------------------------------
|
||||||
|
-- Update (one of) callbacks registry for sqlite functions.
|
||||||
|
local function updatecb(self, where, name, f)
|
||||||
|
local cbs = conncb[self][where]
|
||||||
|
if cbs[name] then -- Callback already present, free old one.
|
||||||
|
cbs[name]:free()
|
||||||
|
end
|
||||||
|
cbs[name] = f -- Could be nil and that's fine.
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Return manually casted callback that sqlite expects, scalar.
|
||||||
|
local function scalarcb(name, f)
|
||||||
|
local values = {} -- Conversion buffer.
|
||||||
|
local function sqlf(context, nvalues, pvalues)
|
||||||
|
-- Indexing 0,N-1.
|
||||||
|
for i=1,nvalues do values[i] = get_value(pvalues[i - 1]) end
|
||||||
|
-- Throw error via sqlite function if necessary.
|
||||||
|
local ok, result = pcall(f, unpack(values, 1, nvalues))
|
||||||
|
if not ok then
|
||||||
|
local msg = "Lua registered scalar function "..name.." error: "..result
|
||||||
|
sql.sqlite3_result_error(context, msg, #msg)
|
||||||
|
else
|
||||||
|
set_value(context, result)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return ffi.cast("ljsqlite3_cbstep", sqlf)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Return the state for aggregate case (created via initstate()). We use the ptr
|
||||||
|
-- returned from aggregate_context() for tagging only, all the state data is
|
||||||
|
-- handled from Lua side.
|
||||||
|
local function getstate(context, initstate, size)
|
||||||
|
-- Only pointer address relevant for indexing, size irrelevant.
|
||||||
|
local ptr = sql.sqlite3_aggregate_context(context, size)
|
||||||
|
local pid = tonumber(ffi.cast("intptr_t",ptr))
|
||||||
|
local state = aggregatestate[pid]
|
||||||
|
if type(state) == "nil" then
|
||||||
|
state = initstate()
|
||||||
|
aggregatestate[pid] = state
|
||||||
|
end
|
||||||
|
return state, pid
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Return manually casted callback that sqlite expects, stepper for aggregate.
|
||||||
|
local function stepcb(name, f, initstate)
|
||||||
|
local values = {} -- Conversion buffer.
|
||||||
|
local function sqlf(context, nvalues, pvalues)
|
||||||
|
-- Indexing 0,N-1.
|
||||||
|
for i=1,nvalues do values[i] = get_value(pvalues[i - 1]) end
|
||||||
|
local state = getstate(context, initstate, 1)
|
||||||
|
-- Throw error via sqlite function if necessary.
|
||||||
|
local ok, result = pcall(f, state, unpack(values, 1, nvalues))
|
||||||
|
if not ok then
|
||||||
|
local msg = "Lua registered step function "..name.." error: "..result
|
||||||
|
sql.sqlite3_result_error(context, msg, #msg)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return ffi.cast("ljsqlite3_cbstep", sqlf)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Return manually casted callback that sqlite expects, finalizer for aggregate.
|
||||||
|
local function finalcb(name, f, initstate)
|
||||||
|
local function sqlf(context)
|
||||||
|
local state, pid = getstate(context, initstate, 0)
|
||||||
|
aggregatestate[pid] = nil -- Clear the state.
|
||||||
|
local ok, result = pcall(f, state)
|
||||||
|
-- Throw error via sqlite function if necessary.
|
||||||
|
if not ok then
|
||||||
|
local msg = "Lua registered final function "..name.." error: "..result
|
||||||
|
sql.sqlite3_result_error(context, msg, #msg)
|
||||||
|
else
|
||||||
|
set_value(context, result)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return ffi.cast("ljsqlite3_cbfinal", sqlf)
|
||||||
|
end
|
||||||
|
|
||||||
|
function conn_mt:setscalar(name, f) T_open(self)
|
||||||
|
jit.off(stmt_step) -- Necessary to avoid bad calloc in some use cases.
|
||||||
|
local cbf = f and scalarcb(name, f) or nil
|
||||||
|
local code = sql.sqlite3_create_function(self._ptr, name, -1, 5, nil,
|
||||||
|
cbf, nil, nil) -- If cbf nil this clears the function is sqlite.
|
||||||
|
T_okcode(self._ptr, code)
|
||||||
|
updatecb(self, "scalar", name, cbf) -- Update and clear old.
|
||||||
|
end
|
||||||
|
|
||||||
|
function conn_mt:setaggregate(name, initstate, step, final) T_open(self)
|
||||||
|
jit.off(stmt_step) -- Necessary to avoid bad calloc in some use cases.
|
||||||
|
local cbs = step and stepcb (name, step, initstate) or nil
|
||||||
|
local cbf = final and finalcb(name, final, initstate) or nil
|
||||||
|
local code = sql.sqlite3_create_function(self._ptr, name, -1, 5, nil,
|
||||||
|
nil, cbs, cbf) -- If cbs, cbf nil this clears the function is sqlite.
|
||||||
|
T_okcode(self._ptr, code)
|
||||||
|
updatecb(self, "step", name, cbs) -- Update and clear old.
|
||||||
|
updatecb(self, "final", name, cbf) -- Update and clear old.
|
||||||
|
end
|
||||||
|
|
||||||
|
conn_ct = ffi.metatype("struct { sqlite3* _ptr; bool _closed; }", conn_mt)
|
||||||
|
|
||||||
|
-- Statement -------------------------------------------------------------------
|
||||||
|
function stmt_mt:reset() T_open(self)
|
||||||
|
-- Ignore possible error code, it would be repetition of error raised during
|
||||||
|
-- most recent evaluation of statement which would have been raised already.
|
||||||
|
sql.sqlite3_reset(self._ptr)
|
||||||
|
self._code = sql.SQLITE_OK -- Always succeds.
|
||||||
|
return self
|
||||||
|
end
|
||||||
|
|
||||||
|
function stmt_mt:close() T_open(self)
|
||||||
|
-- Ignore possible error code, it would be repetition of error raised during
|
||||||
|
-- most recent evaluation of statement which would have been raised already.
|
||||||
|
sql.sqlite3_finalize(self._ptr)
|
||||||
|
self._code = sql.SQLITE_OK -- Always succeds.
|
||||||
|
self._closed = true -- Must be called exaclty once.
|
||||||
|
end
|
||||||
|
|
||||||
|
function stmt_mt:__gc()
|
||||||
|
if not self._closed then self:close() end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Statement step, resultset ---------------------------------------------------
|
||||||
|
function stmt_mt:_ncol()
|
||||||
|
return sql.sqlite3_column_count(self._ptr)
|
||||||
|
end
|
||||||
|
|
||||||
|
function stmt_mt:_header(h)
|
||||||
|
for i=1,self:_ncol() do -- Here indexing 0,N-1.
|
||||||
|
h[i] = ffi.string(sql.sqlite3_column_name(self._ptr, i - 1))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
stmt_step = function(self, row, header)
|
||||||
|
-- Must check code ~= SQL_DONE or sqlite3_step --> undefined result.
|
||||||
|
if self._code == sql.SQLITE_DONE then return nil end -- Already finished.
|
||||||
|
self._code = sql.sqlite3_step(self._ptr)
|
||||||
|
if self._code == sql.SQLITE_ROW then
|
||||||
|
-- All the sql.* functions called never errors here.
|
||||||
|
row = row or {}
|
||||||
|
for i=1,self:_ncol() do
|
||||||
|
row[i] = get_column(self._ptr, i - 1)
|
||||||
|
end
|
||||||
|
if header then self:_header(header) end
|
||||||
|
return row, header
|
||||||
|
elseif self._code == sql.SQLITE_DONE then -- Have finished now.
|
||||||
|
return nil
|
||||||
|
else -- If code not DONE or ROW then it's error.
|
||||||
|
E_conn(self._conn, self._code)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
stmt_mt._step = stmt_step
|
||||||
|
|
||||||
|
function stmt_mt:step(row, header) T_open(self)
|
||||||
|
return self:_step(row, header)
|
||||||
|
end
|
||||||
|
|
||||||
|
function stmt_mt:resultset(get, maxrecords) T_open(self)
|
||||||
|
get = get or "hik"
|
||||||
|
maxrecords = maxrecords or math.huge
|
||||||
|
if maxrecords < 1 then
|
||||||
|
err("constraint", "agument #1 to resultset must be >= 1")
|
||||||
|
end
|
||||||
|
local hash, hasi, hask = get:find("h"), get:find("i"), get:find("k")
|
||||||
|
local r, h = self:_step({}, {})
|
||||||
|
if not r then return nil, 0 end -- No records case.
|
||||||
|
-- First record, o is a temporary table used to get records.
|
||||||
|
local o = hash and { [0] = h } or {}
|
||||||
|
for i=1,#h do o[i] = { r[i] } end
|
||||||
|
-- Other records.
|
||||||
|
local n = 1
|
||||||
|
while n < maxrecords and self:_step(r) do
|
||||||
|
n = n + 1
|
||||||
|
for i=1,#h do o[i][n] = r[i] end
|
||||||
|
end
|
||||||
|
|
||||||
|
local out = { [0] = o[0] } -- Eventually copy colnames.
|
||||||
|
if hasi then -- Use numeric indexes.
|
||||||
|
for i=1,#h do out[i] = o[i] end
|
||||||
|
end
|
||||||
|
if hask then -- Use colnames indexes.
|
||||||
|
for i=1,#h do out[h[i]] = o[i] end
|
||||||
|
end
|
||||||
|
return out, n
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Statement bind --------------------------------------------------------------
|
||||||
|
function stmt_mt:_bind1(i, v)
|
||||||
|
local code = set_column(self._ptr, v, i) -- Here indexing 1,N.
|
||||||
|
T_okcode(self._conn, code)
|
||||||
|
return self
|
||||||
|
end
|
||||||
|
|
||||||
|
function stmt_mt:bind1(i, v) T_open(self)
|
||||||
|
return self:_bind1(i, v)
|
||||||
|
end
|
||||||
|
|
||||||
|
function stmt_mt:bind(...) T_open(self)
|
||||||
|
for i=1,select("#", ...) do self:_bind1(i, select(i, ...)) end
|
||||||
|
return self
|
||||||
|
end
|
||||||
|
|
||||||
|
function stmt_mt:clearbind() T_open(self)
|
||||||
|
local code = sql.sqlite3_clear_bindings(self._ptr)
|
||||||
|
T_okcode(self._conn, code)
|
||||||
|
return self
|
||||||
|
end
|
||||||
|
|
||||||
|
stmt_ct = ffi.metatype([[struct {
|
||||||
|
sqlite3_stmt* _ptr;
|
||||||
|
bool _closed;
|
||||||
|
sqlite3* _conn;
|
||||||
|
int32_t _code;
|
||||||
|
}]], stmt_mt)
|
||||||
|
|
||||||
|
return {
|
||||||
|
open = open,
|
||||||
|
blob = blob,
|
||||||
|
}
|
|
@ -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,
|
||||||
|
}
|
Binary file not shown.
|
@ -0,0 +1,187 @@
|
||||||
|
--rewrite this lib (P.S: done)
|
||||||
|
--P.S: air stands for Advanced Input Recognition, although technically it's not all that advanced
|
||||||
|
air = {}
|
||||||
|
air.match_strings = function(string)
|
||||||
|
local strings = {}
|
||||||
|
string = string:gsub("\"(.-[^\\])\"",function(capt)
|
||||||
|
string_id = string_id + 1
|
||||||
|
strings["%str"..string_id] = capt:gsub("\\\"","\"")
|
||||||
|
return " %str"..string_id
|
||||||
|
end)
|
||||||
|
return string,strings
|
||||||
|
end
|
||||||
|
|
||||||
|
--this table will look up special types
|
||||||
|
special_case = {
|
||||||
|
["voiceChannel"] = function(id,client,guild_id)
|
||||||
|
local guild = client:getGuild(guild_id)
|
||||||
|
local channel = guild:getChannel(id:match("(%d+)[^%d]*$"))
|
||||||
|
if tostring(channel):match("^GuildVoiceChannel: ") then
|
||||||
|
return true,channel
|
||||||
|
else
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
["textChannel"] = function(id,client,guild_id)
|
||||||
|
local guild = client:getGuild(guild_id)
|
||||||
|
local channel = guild:getChannel(id:match("(%d+)[^%d]*$"))
|
||||||
|
if tostring(channel):match("^GuildTextChannel: ") then
|
||||||
|
return true,channel
|
||||||
|
else
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
["messageLink"] = function(id,client,guild_id)
|
||||||
|
local guild = client:getGuild(guild_id)
|
||||||
|
local channelId,messageId = id:match("(%d+)/(%d+)[^%d]*$")
|
||||||
|
channel = guild:getChannel(channelId)
|
||||||
|
if tostring(channel):find("GuildTextChannel") then
|
||||||
|
message = channel:getMessage(messageId)
|
||||||
|
if message then
|
||||||
|
return true,message
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return false
|
||||||
|
end,
|
||||||
|
["role"] = function(id,client,guild_id)
|
||||||
|
local guild = client:getGuild(guild_id)
|
||||||
|
local role = guild:getRole(id:match("(%d+)[^%d]*$"))
|
||||||
|
if role then
|
||||||
|
return true,role
|
||||||
|
else
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
["member"] = function(id,client,guild_id)
|
||||||
|
local guild = client:getGuild(guild_id)
|
||||||
|
local member = guild:getMember(id:match("(%d+)[^%d]*$"))
|
||||||
|
if member then
|
||||||
|
return true,member
|
||||||
|
else
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
["emoji"] = function(id,client,guild_id)
|
||||||
|
local guild = client:getGuild(guild_id)
|
||||||
|
local emoji = guild:getEmoji(id:match("(%d+)[^%d]*$"))
|
||||||
|
if emoji then
|
||||||
|
return true,emoji
|
||||||
|
else
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
["ban"] = function(id,client,guild_id)
|
||||||
|
local guild = client:getGuild(guild_id)
|
||||||
|
local ban = guild:getBan(id:match("(%d+)[^%d]*$"))
|
||||||
|
if ban then
|
||||||
|
return true,ban
|
||||||
|
else
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
["channel"] = function(id,client,guild_id)
|
||||||
|
local guild = client:getGuild(guild_id)
|
||||||
|
local channel = guild:getChannel(id:match("(%d+)[^%d]*$"))
|
||||||
|
if channel then
|
||||||
|
return true,channel
|
||||||
|
else
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
["user"] = function(id,client,guild_id)
|
||||||
|
local user = client:getUser(id:match("(%d+)[^%d]*$"))
|
||||||
|
if user then
|
||||||
|
return true,user
|
||||||
|
end
|
||||||
|
return false
|
||||||
|
end,
|
||||||
|
["id"] = function(id)
|
||||||
|
if tonumber(id:match("(%d+)[^%d]*$")) and tostring(id:match("(%d+)[^%d]*$")):len() > 10 then
|
||||||
|
return true,id
|
||||||
|
end
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
}
|
||||||
|
|
||||||
|
air.parse = function(string,argmatch,client,guild_id)
|
||||||
|
local args,opts = {},{}
|
||||||
|
local string_id = 0
|
||||||
|
local strings = {}
|
||||||
|
string = string:gsub("[%s\n]+\"\"",function(capt)
|
||||||
|
string_id = string_id + 1
|
||||||
|
strings["%str"..string_id] = ""
|
||||||
|
return " %str"..string_id
|
||||||
|
end)
|
||||||
|
string = string:gsub("[%s\n]+\"(.-[^\\])\"",function(capt)
|
||||||
|
string_id = string_id + 1
|
||||||
|
strings["%str"..string_id] = capt:gsub("\\\"","\"")
|
||||||
|
return " %str"..string_id
|
||||||
|
end)
|
||||||
|
string = string:gsub("[%s\n]+%-%-(%w+)=\"\"",function(name)
|
||||||
|
opts[name] = ""
|
||||||
|
return ""
|
||||||
|
end)
|
||||||
|
string = string:gsub("[%s\n]+%-%-(%w+)=\"(.-[^\\])\"",function(name,value)
|
||||||
|
opts[name] = value:gsub("\\\"","\"")
|
||||||
|
return ""
|
||||||
|
end)
|
||||||
|
string = string:gsub("[%s\n]+%-%-(%w+)=(%S+)",function(name,value)
|
||||||
|
opts[name] = value
|
||||||
|
return ""
|
||||||
|
end)
|
||||||
|
string = string:gsub("[%s\n]+%-%-(%w+)",function(name)
|
||||||
|
opts[name] = true
|
||||||
|
return ""
|
||||||
|
end)
|
||||||
|
string = string:gsub("[%s\n]+%-(%w+)",function(args)
|
||||||
|
args:gsub(".",function(key)
|
||||||
|
opts[key] = true
|
||||||
|
end)
|
||||||
|
return ""
|
||||||
|
end)
|
||||||
|
string:gsub("([^%s\n]+)",function(match)
|
||||||
|
table.insert(args,match)
|
||||||
|
end)
|
||||||
|
for k,v in pairs(args) do
|
||||||
|
if v:match("%%str%d+") then
|
||||||
|
if strings[v] then
|
||||||
|
args[k] = strings[v]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if argmatch and #argmatch > 0 then
|
||||||
|
local match,err = false
|
||||||
|
local new_args = {}
|
||||||
|
for k,v in pairs(argmatch) do
|
||||||
|
if not args[k] then
|
||||||
|
match = false
|
||||||
|
err = "Missing arguments: "..table.concat(argmatch,", ",k)
|
||||||
|
break
|
||||||
|
end
|
||||||
|
if v == "number" and tonumber(args[k]) then
|
||||||
|
match = true
|
||||||
|
new_args[k] = tonumber(args[k])
|
||||||
|
elseif v == "string" then
|
||||||
|
match = true
|
||||||
|
new_args[k] = args[k]
|
||||||
|
elseif special_case[v] then
|
||||||
|
match,new_args[k] = special_case[v](args[k],client,guild_id)
|
||||||
|
else
|
||||||
|
match = false
|
||||||
|
end
|
||||||
|
if match == false then
|
||||||
|
err = "Type mismatch for argument "..k..": "..argmatch[k].." expected."
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
for k,v in pairs(args) do
|
||||||
|
if not new_args[k] then
|
||||||
|
new_args[k] = v
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return match,new_args,opts,err
|
||||||
|
else
|
||||||
|
return true,args,opts
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return air
|
|
@ -0,0 +1 @@
|
||||||
|
../baseclass.lua
|
|
@ -0,0 +1,88 @@
|
||||||
|
class = require("baseclass")
|
||||||
|
color = require("tty-colors")
|
||||||
|
tests = {}
|
||||||
|
tests[1] = function()
|
||||||
|
print("Basic class initialization test")
|
||||||
|
local newclass = class("TestObject")
|
||||||
|
function newclass:__init(value)
|
||||||
|
self.prop = value
|
||||||
|
end
|
||||||
|
function newclass:setProp(value)
|
||||||
|
self.prop = value
|
||||||
|
print("Property for object "..tostring(self).." set to "..tostring(value))
|
||||||
|
end
|
||||||
|
function newclass:printProp()
|
||||||
|
print(self.prop)
|
||||||
|
end
|
||||||
|
local object_a = newclass(3)
|
||||||
|
object_a:printProp()
|
||||||
|
object_a:setProp(30)
|
||||||
|
object_a:printProp()
|
||||||
|
end
|
||||||
|
|
||||||
|
tests[2] = function()
|
||||||
|
print("Class instance independence test")
|
||||||
|
local newclass = class("TestObject")
|
||||||
|
function newclass:__init(value)
|
||||||
|
self.prop = value
|
||||||
|
end
|
||||||
|
function newclass:setProp(value)
|
||||||
|
self.prop = value
|
||||||
|
print("Property for object "..tostring(self).." set to "..tostring(value))
|
||||||
|
end
|
||||||
|
function newclass:printProp()
|
||||||
|
print(self.prop)
|
||||||
|
end
|
||||||
|
local object_a = newclass(3)
|
||||||
|
local object_b = newclass()
|
||||||
|
object_a:printProp()
|
||||||
|
object_b:printProp()
|
||||||
|
object_a:setProp(30)
|
||||||
|
object_b:setProp(20)
|
||||||
|
object_a:printProp()
|
||||||
|
object_b:printProp()
|
||||||
|
end
|
||||||
|
|
||||||
|
tests[3] = function()
|
||||||
|
print("Extension test")
|
||||||
|
local newclass = class("Accumulator")
|
||||||
|
function newclass:ret()
|
||||||
|
return self.acc
|
||||||
|
end
|
||||||
|
function newclass:setA(a)
|
||||||
|
self.a = a
|
||||||
|
end
|
||||||
|
function newclass:setB(b)
|
||||||
|
self.b = b
|
||||||
|
end
|
||||||
|
local adder = newclass:extend("Adder")
|
||||||
|
function adder:add()
|
||||||
|
self.acc = self.a + self.b
|
||||||
|
end
|
||||||
|
local subber = newclass:extend("Subtracter")
|
||||||
|
function subber:sub()
|
||||||
|
self.acc = self.a - self.b
|
||||||
|
end
|
||||||
|
obj1 = adder()
|
||||||
|
obj1:setA(1)
|
||||||
|
obj1:setB(2)
|
||||||
|
obj1:add()
|
||||||
|
print(obj1:ret())
|
||||||
|
obj2 = subber()
|
||||||
|
obj2:setA(1)
|
||||||
|
obj2:setB(2)
|
||||||
|
obj2:sub()
|
||||||
|
print(obj2:ret())
|
||||||
|
end
|
||||||
|
--here run tests
|
||||||
|
print("Deteceted "..#tests.." tests. Starting now.")
|
||||||
|
OK = 0
|
||||||
|
for k,v in pairs(tests) do
|
||||||
|
status,errcode = pcall(v)
|
||||||
|
stat_color = (status and "green") or "red"
|
||||||
|
print(color("TEST #"..k.." "..((status and "OK") or "ERROR")..(((not status) and errcode) or ""),stat_color))
|
||||||
|
if status then
|
||||||
|
OK = OK + 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
print(OK.."/"..#tests.." tests completed successfully")
|
|
@ -0,0 +1 @@
|
||||||
|
../../tty-colors.lua
|
|
@ -0,0 +1,92 @@
|
||||||
|
--Access control list class
|
||||||
|
--Note that it isn't directly used by anything,
|
||||||
|
--Instead it is extended to work with discord's permission system
|
||||||
|
--as command-acl
|
||||||
|
local class = import("classes.baseclass")
|
||||||
|
local table_utils = import("table-utils")
|
||||||
|
local acl = class("ACL")
|
||||||
|
function acl:__init()
|
||||||
|
self.user_rules = {}
|
||||||
|
self.group_rules = {}
|
||||||
|
end
|
||||||
|
function acl:set_user_rule(user_id,status)
|
||||||
|
assert(
|
||||||
|
(status == nil) or (status == 0) or (status == -1) or (status == 1),
|
||||||
|
"invalid status setting"
|
||||||
|
)
|
||||||
|
self.user_rules[user_id] = status
|
||||||
|
end
|
||||||
|
function acl:set_group_rule(group_id,status)
|
||||||
|
assert(
|
||||||
|
(status == nil) or (status == 0) or (status == -1) or (status == 1),
|
||||||
|
"invalid status setting"
|
||||||
|
)
|
||||||
|
self.group_rules[group_id] = status
|
||||||
|
end
|
||||||
|
function acl:check_user(user_id)
|
||||||
|
if self.user_rules[user_id] and self.user_rules[user_id] ~= 0 then
|
||||||
|
return true,(self.user_rules[user_id] == 1)
|
||||||
|
else
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
function acl:check_group(groups)
|
||||||
|
local allow = false
|
||||||
|
local found = false
|
||||||
|
for k,v in pairs(groups) do
|
||||||
|
if self.group_rules[v] then
|
||||||
|
found = true
|
||||||
|
allow = self.group_rules[v]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return found,(allow and allow == 1)
|
||||||
|
end
|
||||||
|
function acl:export_all_lists()
|
||||||
|
local lists = {
|
||||||
|
users = "",
|
||||||
|
groups = ""
|
||||||
|
}
|
||||||
|
for k,v in pairs(self.user_rules) do
|
||||||
|
lists.users = lists.users..k..":"..tostring(v)..";\n"
|
||||||
|
end
|
||||||
|
for k,v in pairs(self.group_rules) do
|
||||||
|
lists.groups = lists.groups..k..":"..tostring(v)..";\n"
|
||||||
|
end
|
||||||
|
return lists
|
||||||
|
end
|
||||||
|
function acl:export_user_list()
|
||||||
|
local list = ""
|
||||||
|
for k,v in pairs(self.user_rules) do
|
||||||
|
list = list..k..":"..tostring(v)..";\n"
|
||||||
|
end
|
||||||
|
return list
|
||||||
|
end
|
||||||
|
function acl:export_group_list()
|
||||||
|
local list = ""
|
||||||
|
for k,v in pairs(self.group_rules) do
|
||||||
|
list = list..k..":"..tostring(v)..";\n"
|
||||||
|
end
|
||||||
|
return list
|
||||||
|
end
|
||||||
|
function acl:export_snapshot()
|
||||||
|
return {
|
||||||
|
user_rules = bot_utils.deepcopy(self.user_rules),
|
||||||
|
group_rules = bot_utils.deepcopy(self.group_rules)
|
||||||
|
}
|
||||||
|
end
|
||||||
|
function acl:import_snapshot(t)
|
||||||
|
self.user_rules = t.user_rules
|
||||||
|
self.group_rules = t.group_rules
|
||||||
|
end
|
||||||
|
function acl:import_user_list(list)
|
||||||
|
list:gsub("(%w+):(%d+)",function(id,status)
|
||||||
|
self.user_rules[id] = status
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
function acl:import_group_list(list)
|
||||||
|
list:gsub("(%w+):(%d+)",function(id,status)
|
||||||
|
self.group_rules[id] = status
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
return acl
|
|
@ -0,0 +1,32 @@
|
||||||
|
--class generator (for the purpose of creating classes)
|
||||||
|
return function(name)
|
||||||
|
local new_class = {}
|
||||||
|
new_class.__classname = name or "Object"
|
||||||
|
new_class.__index = new_class
|
||||||
|
new_class.__new = function(self,...)
|
||||||
|
local obj = {}
|
||||||
|
--set metamethod proetection measures
|
||||||
|
setmetatable(obj,{__index = function(obj,key)
|
||||||
|
if key:find("^__") then
|
||||||
|
return nil
|
||||||
|
else
|
||||||
|
return self[key]
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
__name = new_class.__classname})
|
||||||
|
if self.__init then
|
||||||
|
self.__init(obj,...)
|
||||||
|
end
|
||||||
|
return obj
|
||||||
|
end
|
||||||
|
new_class.extend = function(self,name)
|
||||||
|
local new_class = {}
|
||||||
|
new_class.__classname = name or "Object"
|
||||||
|
new_class.__index = new_class
|
||||||
|
setmetatable(new_class,{__index = self,__call = function(...) return new_class.__new(...) end, __name = new_class.__classname.." (class)"})
|
||||||
|
return new_class
|
||||||
|
end
|
||||||
|
--make our class callable; on call, it will initialize a new instance of itself
|
||||||
|
setmetatable(new_class,{__call = function(...) return new_class.__new(...) end, __name = new_class.__classname.." (class)"})
|
||||||
|
return new_class
|
||||||
|
end
|
|
@ -0,0 +1,107 @@
|
||||||
|
--This class acts as a pipe between the incoming messages and commands.
|
||||||
|
--It observes the content of the incoming messages, and, depending on the optional flags,
|
||||||
|
--executes specific commands
|
||||||
|
--Remember: there can only be one command handler and plugin handler
|
||||||
|
--per a server handler. Side effects of using multiple command handlers and
|
||||||
|
--plugin handlers are unknown.
|
||||||
|
local class = import("classes.baseclass")
|
||||||
|
local command_handler = class("Command-handler")
|
||||||
|
local table_utils = import("table-utils")
|
||||||
|
local purify = import("purify")
|
||||||
|
function command_handler:__init(parent_server)
|
||||||
|
self.server_handler = assert(parent_server,"parent server handler not provided")
|
||||||
|
self.command_pool = {}
|
||||||
|
self.prefixes = {}
|
||||||
|
self.command_meta = {
|
||||||
|
plugins = {},
|
||||||
|
}
|
||||||
|
end
|
||||||
|
function command_handler:add_prefix(prefix)
|
||||||
|
local purified_prefix = purify.purify_escapes(prefix)
|
||||||
|
self.prefixes[purified_prefix] = purified_prefix
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
function command_handler:remove_prefix(prefix)
|
||||||
|
local purified_prefix = purify.purify_escapes(prefix)
|
||||||
|
if self.prefixes[purified_prefix] or table_utils.count(self.prefixes) <= 1 then
|
||||||
|
self.prefix[purified_prefix] = nil
|
||||||
|
return true
|
||||||
|
else
|
||||||
|
return false, (
|
||||||
|
(self.prefixes[purified_prefix] and "No such prefix") or
|
||||||
|
"Cannot remove the last remaining prefix"
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
function command_handler:get_prefixes()
|
||||||
|
return table_utils.deepcopy(self.prefixes)
|
||||||
|
end
|
||||||
|
function command_handler:add_command(command)
|
||||||
|
assert(type(command) == "table","command object expected")
|
||||||
|
local purified_name = purify.purify_escapes(command.name)
|
||||||
|
self.command_pool[purified_name] = command
|
||||||
|
if not self.command_meta.plugins[command.parent.name] then
|
||||||
|
self.command_meta.plugins[command.parent.name] = {}
|
||||||
|
end
|
||||||
|
table.insert(self.command_meta.plugins[command.parent.name],command.name)
|
||||||
|
return command
|
||||||
|
end
|
||||||
|
function command_handler:remove_command(command)
|
||||||
|
assert(type(command) == "table","command object expected")
|
||||||
|
local purified_name = purify.purify_escapes(command.name)
|
||||||
|
if self.command_pool[purified_name] then
|
||||||
|
local command = self.command_pool[purified_name]
|
||||||
|
--not exactly optimal, but lists are lists. can't do much about them.
|
||||||
|
table_utils.remove_value(self.command_meta.plugins[command.parent.name],command.name)
|
||||||
|
if #self.command_meta.plugins[command.parent.name] == 0 then
|
||||||
|
self.command_meta.plugins[command.parent.name] = nil
|
||||||
|
end
|
||||||
|
self.command_pool[purified_name] = nil
|
||||||
|
return true
|
||||||
|
else
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
function command_handler:get_command(name)
|
||||||
|
local purified_name = purify.purify_escapes(assert(type(name) == "string") and name)
|
||||||
|
if self.command_pool[purified_name] then
|
||||||
|
return self.command_pool[purified_name]
|
||||||
|
else
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
function command_handler:get_commands(name)
|
||||||
|
local list = {}
|
||||||
|
for k,v in pairs(self.command_pool) do
|
||||||
|
table.insert(list,k)
|
||||||
|
end
|
||||||
|
return list
|
||||||
|
end
|
||||||
|
function command_handler:get_commands_metadata()
|
||||||
|
return table_utils.deepcopy(self.command_meta)
|
||||||
|
end
|
||||||
|
function command_handler:handle(message)
|
||||||
|
for name,command in pairs(self.command_pool) do
|
||||||
|
if command.options.regex then
|
||||||
|
if message.content:match(command.options.regex) then
|
||||||
|
command:exec(message)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
else
|
||||||
|
if command.options.prefix then
|
||||||
|
for _,prefix in pairs(self.prefixes) do
|
||||||
|
if message.content:find(prefix..name.."$") == 1 or message.content:find(prefix..name.."%s") then
|
||||||
|
command:exec(message)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
end
|
||||||
|
else
|
||||||
|
if message.content:find(name.."$") == 1 or message.content:find(name.."%s") then
|
||||||
|
command:exec(message)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return command_handler
|
|
@ -0,0 +1,163 @@
|
||||||
|
--[[
|
||||||
|
This class handles command management.
|
||||||
|
]]
|
||||||
|
local table_utils = import("table-utils")
|
||||||
|
local class = import("classes.baseclass")
|
||||||
|
local command = class("Command")
|
||||||
|
local acl = import("classes.command-acl")
|
||||||
|
function command:__init(name,callback)
|
||||||
|
self.rules = acl()
|
||||||
|
self.name = name
|
||||||
|
self.timer = os.time()
|
||||||
|
self.options = {
|
||||||
|
allow_bots = false, --allow bots to execute the command
|
||||||
|
typing_decorator = false, --set if the bot should be "typing" while the command executes
|
||||||
|
prefix = true, --if true and if regex isn't enabled, check for prefix at the start. if not, don't check for prefix
|
||||||
|
regex = false, --check if the message matches this regular expression (should be a string)
|
||||||
|
no_parsing = false, --check if you want to disable the message argument parsing process
|
||||||
|
timeout = 1000, --set the timeout for a command
|
||||||
|
}
|
||||||
|
if type(callback) == "table" then
|
||||||
|
for k,v in pairs(callback.options or {}) do
|
||||||
|
self.options[k] = v
|
||||||
|
end
|
||||||
|
self.callback = callback.exec
|
||||||
|
self.args = callback.args or self.args
|
||||||
|
if callback.users then
|
||||||
|
for k,v in pairs(callback.users) do
|
||||||
|
self.rules:set_user_rule(k,v)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if callback.roles then
|
||||||
|
for k,v in pairs(callback.roles) do
|
||||||
|
self.rules:set_group_rule(k,v)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if callback.perm then
|
||||||
|
self.rules:set_perm_rules(callback.perm)
|
||||||
|
end
|
||||||
|
if callback.help then
|
||||||
|
self:set_help(callback.help,callback.usage)
|
||||||
|
end
|
||||||
|
elseif type(callback) == "function" then
|
||||||
|
self.callback = callback
|
||||||
|
end
|
||||||
|
end
|
||||||
|
--set the callback to be called on comm:exec(msg)
|
||||||
|
function command:set_callback(fn)
|
||||||
|
assert(type(fn) == "function","function expected, got "..type(fn))
|
||||||
|
self.callback = fn
|
||||||
|
return self
|
||||||
|
end
|
||||||
|
--generate help using only description and usage, or nothing at all
|
||||||
|
function command:generate_help(description,usage)
|
||||||
|
assert(not description or (type(description) == "string"),"Description should be either string or nil, got "..type(description))
|
||||||
|
assert(not usage or (type(usage) == "string"),"Usage should be either string or nil, got "..type(usage))
|
||||||
|
local backup_usage_str
|
||||||
|
if self.args then
|
||||||
|
backup_usage_str = self.name.." <"..table.concat(self.args,"> <")..">"
|
||||||
|
else
|
||||||
|
backup_usage_str = "not defined"
|
||||||
|
end
|
||||||
|
local permissions = table.concat(self.rules:export_snapshot()["perms"] or {},"\n")
|
||||||
|
if permissions == "" then
|
||||||
|
permissions = "All"
|
||||||
|
end
|
||||||
|
self.help = {embed = {
|
||||||
|
title = "Help for ``"..self.name.."``",
|
||||||
|
description = description,
|
||||||
|
fields = {
|
||||||
|
{name = "Usage: ",value = usage or backup_usage_str},
|
||||||
|
{name = "Perms: ",value = permissions}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
return self
|
||||||
|
end
|
||||||
|
--set the help message to be sent
|
||||||
|
function command:set_help(obj,usage)
|
||||||
|
if type(obj) == "string" then
|
||||||
|
self:generate_help(obj,usage)
|
||||||
|
elseif type(obj) == "table" then
|
||||||
|
self.help = obj
|
||||||
|
else
|
||||||
|
error("Type "..type(obj).." cannot be set as a help message")
|
||||||
|
end
|
||||||
|
return self
|
||||||
|
end
|
||||||
|
--print the help message, or generate it if there is none
|
||||||
|
function command:get_help()
|
||||||
|
if not self.help then
|
||||||
|
self:generate_help("Description not defined")
|
||||||
|
end
|
||||||
|
return self.help
|
||||||
|
end
|
||||||
|
function command:set_timeout_callback(fn)
|
||||||
|
assert(type(fn) == "function","function expected, got "..type(fn))
|
||||||
|
self.timeout_callback = fn
|
||||||
|
return self
|
||||||
|
end
|
||||||
|
|
||||||
|
--check the permissions for command
|
||||||
|
function command:check_permissions(message)
|
||||||
|
if message.author.bot and (not self.options.allow_bots) then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
if discordia.Date():toMilliseconds()-self.options.timeout < self.timer then
|
||||||
|
if self.timeout_callback then
|
||||||
|
self.timeout_callback(fn)
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
self.timer = discordia.Date():toMilliseconds()
|
||||||
|
if self.rules:check_user(message.author.id) then
|
||||||
|
local found,allow = self.rules:check_user(message.author.id)
|
||||||
|
return allow
|
||||||
|
end
|
||||||
|
if self.rules:check_group(message.member.roles) then
|
||||||
|
local found,allow = self.rules:check_group(message.member.roles)
|
||||||
|
return allow
|
||||||
|
end
|
||||||
|
return self.rules:check_perm(message.member:getPermissions(message.channel))
|
||||||
|
end
|
||||||
|
--the main entry point for the command - execute the callback within after
|
||||||
|
--multiple checks
|
||||||
|
function command:exec(message,args,opts)
|
||||||
|
local exec = self.callback
|
||||||
|
if not self.callback then
|
||||||
|
error("Callback not set for command "..self.name)
|
||||||
|
end
|
||||||
|
if self.decorator then
|
||||||
|
self.callback = self.decorator(self.callback)
|
||||||
|
end
|
||||||
|
local content
|
||||||
|
if self.options.regex then
|
||||||
|
content = message.content
|
||||||
|
else
|
||||||
|
local strstart,strend = message.content:find(self.name,1,true)
|
||||||
|
content = message.content:sub(strend+1,-1)
|
||||||
|
end
|
||||||
|
if self:check_permissions(message) then
|
||||||
|
local status,args,opts,err = import("air").parse(content,self.args,message.client,message.guild.id)
|
||||||
|
if status then
|
||||||
|
self.callback(message,args,opts)
|
||||||
|
else
|
||||||
|
msg:reply(err)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
--add decorators for the callback
|
||||||
|
function command:set_decorator(fn)
|
||||||
|
assert(type(fn) == "function","a decorator function expected, got "..type(fn))
|
||||||
|
self.decorator = fn
|
||||||
|
return self
|
||||||
|
end
|
||||||
|
--get a list of all properties of the command
|
||||||
|
function command:get_properties()
|
||||||
|
return {
|
||||||
|
name = self.name,
|
||||||
|
args = table_utils.deepcopy(self.args),
|
||||||
|
help = table_utils.deepcopy(self.help),
|
||||||
|
prefix = self.prefix
|
||||||
|
}
|
||||||
|
end
|
||||||
|
return command
|
|
@ -0,0 +1,95 @@
|
||||||
|
local class = import("classes.baseclass")
|
||||||
|
local emitter_proxy = class("EmitterProxy")
|
||||||
|
|
||||||
|
function emitter_proxy:__init(emitter)
|
||||||
|
self.original = emitter
|
||||||
|
self.callback_pool = {}
|
||||||
|
end
|
||||||
|
|
||||||
|
function emitter_proxy:on(event,callback)
|
||||||
|
if not self.callback_pool[event] then
|
||||||
|
self.callback_pool[event] = {}
|
||||||
|
end
|
||||||
|
self.callback_pool[event][callback] = callback
|
||||||
|
self.original:on(event,callback)
|
||||||
|
return callback
|
||||||
|
end
|
||||||
|
|
||||||
|
function emitter_proxy:once(event,callback)
|
||||||
|
if not self.callback_pool[event] then
|
||||||
|
self.callback_pool[event] = {}
|
||||||
|
end
|
||||||
|
local wrapper = function(...)
|
||||||
|
callback(...)
|
||||||
|
self.callback_pool[event][callback] = nil
|
||||||
|
end
|
||||||
|
self.callback_pool[event][callback] = wrapper
|
||||||
|
self.callback_pool[event][wrapper] = wrapper
|
||||||
|
self.original:once(event,wrapper)
|
||||||
|
return callback
|
||||||
|
end
|
||||||
|
|
||||||
|
function emitter_proxy:removeListener(event,callback)
|
||||||
|
if self.callback_pool[event] and self.callback_pool[event][callback] then
|
||||||
|
self.callback_pool[event][callback] = nil
|
||||||
|
self.original:removeListener(event,callback)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function emitter_proxy:removeAllListeners(event,callback)
|
||||||
|
if self.callback_pool[event] then
|
||||||
|
for k,v in pairs(self.callback_pool[event]) do
|
||||||
|
self.original:removeListener(event,v)
|
||||||
|
end
|
||||||
|
self.callback_pool[event] = nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function emitter_proxy:listeners(event)
|
||||||
|
local copy = {}
|
||||||
|
if self.callback_pool[event] then
|
||||||
|
for k,v in pairs(self.callback_pool[event]) do
|
||||||
|
table.insert(copy,v)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return copy
|
||||||
|
end
|
||||||
|
|
||||||
|
function emitter_proxy:listenerCount(event)
|
||||||
|
local count = 0
|
||||||
|
if event then
|
||||||
|
if self.callback_pool[event] then
|
||||||
|
for k,v in pairs(self.callback_pool[event]) do
|
||||||
|
count = count + 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
else
|
||||||
|
for k,v in pairs(self.callback_pool) do
|
||||||
|
for k2,v2 in pairs(v) do
|
||||||
|
count = count + 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return count
|
||||||
|
end
|
||||||
|
|
||||||
|
function emitter_proxy:propogate(event,emitter)
|
||||||
|
if not self.callback_pool[event] then
|
||||||
|
self.callback_pool[event] = {}
|
||||||
|
end
|
||||||
|
local emitter_propogate_handler = function(...)
|
||||||
|
emitter:emit(event,...)
|
||||||
|
end
|
||||||
|
self.callback_pool[event][emitter_propogate_handler] = emitter_propogate_handler
|
||||||
|
self.original:on(event,emitter_propogate_handler)
|
||||||
|
return emitter_propogate_handler
|
||||||
|
end
|
||||||
|
|
||||||
|
function emitter_proxy:destroy()
|
||||||
|
for k,v in pairs(self.callback_pool) do
|
||||||
|
for k2,v2 in pairs(v) do
|
||||||
|
self.original:removeListener(k,v2)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return emitter_proxy
|
|
@ -0,0 +1,12 @@
|
||||||
|
local interface = {}
|
||||||
|
interface.wrapper = function(client,guild_id)
|
||||||
|
local new_i = {}
|
||||||
|
new_i.message = {}
|
||||||
|
new_i.message.get = function(channel,id)
|
||||||
|
local new_m = {}
|
||||||
|
local message = client.getMessage(id)
|
||||||
|
local new_m.content = message.content
|
||||||
|
local new_m.created_at = message.createdAt
|
||||||
|
local new_m.attachments = {}
|
||||||
|
for k,v in pairs(message.attachments) do
|
||||||
|
table.insert(new_m
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue