add Initial Bot files
parent
10e479a001
commit
26e9f89150
@ -0,0 +1,38 @@
|
||||
local discordia = require('discordia')
|
||||
local client = discordia.Client()
|
||||
seed = os.time()
|
||||
math.randomseed(seed)
|
||||
|
||||
client:on('ready', function()
|
||||
print('Logged in as '.. client.user.username)
|
||||
end)
|
||||
|
||||
client:on('messageCreate', function(message)
|
||||
if message.content == '!ping' then
|
||||
message.channel:send('Pong!')
|
||||
end
|
||||
end)
|
||||
|
||||
client:on('messageCreate', function(message)
|
||||
if message.content == '!roll' then
|
||||
dice = math.random(20)
|
||||
message.channel:send('You rolled a ' .. dice .. ' out of 20')
|
||||
if dice==20 then
|
||||
message.channel:send('Nat 20! Crititcal Hit')
|
||||
print('nat20check')
|
||||
else if dice==1 then
|
||||
message.channel:send('Nat 1! Critical Miss!')
|
||||
print('nat1check')
|
||||
end
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
client:on('messageCreate', function(message)
|
||||
if message.content == '!time' then
|
||||
message.channel:send('The current time in military time is ' .. os.date() .. ', atleast in Chicago!')
|
||||
end
|
||||
end)
|
||||
|
||||
|
||||
client:run('Bot ODczMjU1Mjk2MDI0MzIyMDU5.YQ1wXg.BbXq1fu-4nlG95EkLkHujVEObG4')
|
@ -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,190 @@
|
||||
--[[lit-meta
|
||||
name = "creationix/coro-channel"
|
||||
version = "3.0.3"
|
||||
homepage = "https://github.com/luvit/lit/blob/master/deps/coro-channel.lua"
|
||||
description = "An adapter for wrapping uv streams as coro-streams."
|
||||
tags = {"coro", "adapter"}
|
||||
license = "MIT"
|
||||
author = { name = "Tim Caswell" }
|
||||
]]
|
||||
|
||||
-- local p = require('pretty-print').prettyPrint
|
||||
|
||||
local function assertResume(thread, ...)
|
||||
local success, err = coroutine.resume(thread, ...)
|
||||
if not success then
|
||||
error(debug.traceback(thread, err), 0)
|
||||
end
|
||||
end
|
||||
|
||||
local function makeCloser(socket)
|
||||
local closer = {
|
||||
read = false,
|
||||
written = false,
|
||||
errored = false,
|
||||
}
|
||||
|
||||
local closed = false
|
||||
|
||||
local function close()
|
||||
if closed then return end
|
||||
closed = true
|
||||
if not closer.readClosed then
|
||||
closer.readClosed = true
|
||||
if closer.onClose then
|
||||
closer.onClose()
|
||||
end
|
||||
end
|
||||
if not socket:is_closing() then
|
||||
socket:close()
|
||||
end
|
||||
end
|
||||
|
||||
closer.close = close
|
||||
|
||||
function closer.check()
|
||||
if closer.errored or (closer.read and closer.written) then
|
||||
return close()
|
||||
end
|
||||
end
|
||||
|
||||
return closer
|
||||
end
|
||||
|
||||
local function makeRead(socket, closer)
|
||||
local paused = true
|
||||
|
||||
local queue = {}
|
||||
local tindex = 0
|
||||
local dindex = 0
|
||||
|
||||
local function dispatch(data)
|
||||
|
||||
-- p("<-", data[1])
|
||||
|
||||
if tindex > dindex then
|
||||
local thread = queue[dindex]
|
||||
queue[dindex] = nil
|
||||
dindex = dindex + 1
|
||||
assertResume(thread, unpack(data))
|
||||
else
|
||||
queue[dindex] = data
|
||||
dindex = dindex + 1
|
||||
if not paused then
|
||||
paused = true
|
||||
assert(socket:read_stop())
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
closer.onClose = function ()
|
||||
if not closer.read then
|
||||
closer.read = true
|
||||
return dispatch {nil, closer.errored}
|
||||
end
|
||||
end
|
||||
|
||||
local function onRead(err, chunk)
|
||||
if err then
|
||||
closer.errored = err
|
||||
return closer.check()
|
||||
end
|
||||
if not chunk then
|
||||
if closer.read then return end
|
||||
closer.read = true
|
||||
dispatch {}
|
||||
return closer.check()
|
||||
end
|
||||
return dispatch {chunk}
|
||||
end
|
||||
|
||||
local function read()
|
||||
if dindex > tindex then
|
||||
local data = queue[tindex]
|
||||
queue[tindex] = nil
|
||||
tindex = tindex + 1
|
||||
return unpack(data)
|
||||
end
|
||||
if paused then
|
||||
paused = false
|
||||
assert(socket:read_start(onRead))
|
||||
end
|
||||
queue[tindex] = coroutine.running()
|
||||
tindex = tindex + 1
|
||||
return coroutine.yield()
|
||||
end
|
||||
|
||||
-- Auto use wrapper library for backwards compat
|
||||
return read
|
||||
end
|
||||
|
||||
local function makeWrite(socket, closer)
|
||||
|
||||
local function wait()
|
||||
local thread = coroutine.running()
|
||||
return function (err)
|
||||
assertResume(thread, err)
|
||||
end
|
||||
end
|
||||
|
||||
local function write(chunk)
|
||||
if closer.written then
|
||||
return nil, "already shutdown"
|
||||
end
|
||||
|
||||
-- p("->", chunk)
|
||||
|
||||
if chunk == nil then
|
||||
closer.written = true
|
||||
closer.check()
|
||||
local success, err = socket:shutdown(wait())
|
||||
if not success then
|
||||
return nil, err
|
||||
end
|
||||
err = coroutine.yield()
|
||||
return not err, err
|
||||
end
|
||||
|
||||
local success, err = socket:write(chunk, wait())
|
||||
if not success then
|
||||
closer.errored = err
|
||||
closer.check()
|
||||
return nil, err
|
||||
end
|
||||
err = coroutine.yield()
|
||||
return not err, err
|
||||
end
|
||||
|
||||
return write
|
||||
end
|
||||
|
||||
local function wrapRead(socket)
|
||||
local closer = makeCloser(socket)
|
||||
closer.written = true
|
||||
return makeRead(socket, closer), closer.close
|
||||
end
|
||||
|
||||
local function wrapWrite(socket)
|
||||
local closer = makeCloser(socket)
|
||||
closer.read = true
|
||||
return makeWrite(socket, closer), closer.close
|
||||
end
|
||||
|
||||
local function wrapStream(socket)
|
||||
assert(socket
|
||||
and socket.write
|
||||
and socket.shutdown
|
||||
and socket.read_start
|
||||
and socket.read_stop
|
||||
and socket.is_closing
|
||||
and socket.close, "socket does not appear to be a socket/uv_stream_t")
|
||||
|
||||
local closer = makeCloser(socket)
|
||||
return makeRead(socket, closer), makeWrite(socket, closer), closer.close
|
||||
end
|
||||
|
||||
return {
|
||||
wrapRead = wrapRead,
|
||||
wrapWrite = wrapWrite,
|
||||
wrapStream = wrapStream,
|
||||
}
|
@ -0,0 +1,206 @@
|
||||
--[[lit-meta
|
||||
name = "creationix/coro-http"
|
||||
version = "3.2.2"
|
||||
dependencies = {
|
||||
"creationix/coro-net@3.3.0",
|
||||
"luvit/http-codec@3.0.0"
|
||||
}
|
||||
homepage = "https://github.com/luvit/lit/blob/master/deps/coro-http.lua"
|
||||
description = "An coro style http(s) client and server helper."
|
||||
tags = {"coro", "http"}
|
||||
license = "MIT"
|
||||
author = { name = "Tim Caswell" }
|
||||
]]
|
||||
|
||||
local httpCodec = require('http-codec')
|
||||
local net = require('coro-net')
|
||||
|
||||
local function createServer(host, port, onConnect)
|
||||
return net.createServer({
|
||||
host = host,
|
||||
port = port,
|
||||
encoder = httpCodec.encoder,
|
||||
decoder = httpCodec.decoder,
|
||||
}, function (read, write, socket)
|
||||
for head in read do
|
||||
local parts = {}
|
||||
for part in read do
|
||||
if #part > 0 then
|
||||
parts[#parts + 1] = part
|
||||
else
|
||||
break
|
||||
end
|
||||
end
|
||||
local body = table.concat(parts)
|
||||
head, body = onConnect(head, body, socket)
|
||||
write(head)
|
||||
if body then write(body) end
|
||||
write("")
|
||||
if not head.keepAlive then break end
|
||||
end
|
||||
write()
|
||||
end)
|
||||
end
|
||||
|
||||
local function parseUrl(url)
|
||||
local protocol, host, hostname, port, path = url:match("^(https?:)//(([^/:]+):?([0-9]*))(/?.*)$")
|
||||
if not protocol then error("Not a valid http url: " .. url) end
|
||||
local tls = protocol == "https:"
|
||||
port = port and tonumber(port) or (tls and 443 or 80)
|
||||
if path == "" then path = "/" end
|
||||
return {
|
||||
tls = tls,
|
||||
host = host,
|
||||
hostname = hostname,
|
||||
port = port,
|
||||
path = path
|
||||
}
|
||||
end
|
||||
|
||||
local connections = {}
|
||||
|
||||
local function getConnection(host, port, tls, timeout)
|
||||
for i = #connections, 1, -1 do
|
||||
local connection = connections[i]
|
||||
if connection.host == host and connection.port == port and connection.tls == tls then
|
||||
table.remove(connections, i)
|
||||
-- Make sure the connection is still alive before reusing it.
|
||||
if not connection.socket:is_closing() then
|
||||
connection.reused = true
|
||||
connection.socket:ref()
|
||||
return connection
|
||||
end
|
||||
end
|
||||
end
|
||||
local read, write, socket, updateDecoder, updateEncoder = assert(net.connect {
|
||||
host = host,
|
||||
port = port,
|
||||
tls = tls,
|
||||
timeout = timeout,
|
||||
encoder = httpCodec.encoder,
|
||||
decoder = httpCodec.decoder
|
||||
})
|
||||
return {
|
||||
socket = socket,
|
||||
host = host,
|
||||
port = port,
|
||||
tls = tls,
|
||||
read = read,
|
||||
write = write,
|
||||
updateEncoder = updateEncoder,
|
||||
updateDecoder = updateDecoder,
|
||||
reset = function ()
|
||||
-- This is called after parsing the response head from a HEAD request.
|
||||
-- If you forget, the codec might hang waiting for a body that doesn't exist.
|
||||
updateDecoder(httpCodec.decoder())
|
||||
end
|
||||
}
|
||||
end
|
||||
|
||||
local function saveConnection(connection)
|
||||
if connection.socket:is_closing() then return end
|
||||
connections[#connections + 1] = connection
|
||||
connection.socket:unref()
|
||||
end
|
||||
|
||||
local function request(method, url, headers, body, customOptions)
|
||||
-- customOptions = { timeout = number, followRedirects = boolean }
|
||||
local options = {}
|
||||
if type(customOptions) == "number" then
|
||||
-- Ensure backwards compatibility, where customOptions used to just be timeout
|
||||
options.timeout = customOptions
|
||||
else
|
||||
options = customOptions or {}
|
||||
end
|
||||
options.followRedirects = options.followRedirects == nil and true or options.followRedirects -- Follow any redirects, Default: true
|
||||
|
||||
local uri = parseUrl(url)
|
||||
local connection = getConnection(uri.hostname, uri.port, uri.tls, options.timeout)
|
||||
local read = connection.read
|
||||
local write = connection.write
|
||||
|
||||
local req = {
|
||||
method = method,
|
||||
path = uri.path,
|
||||
{"Host", uri.host}
|
||||
}
|
||||
local contentLength
|
||||
local chunked
|
||||
if headers then
|
||||
for i = 1, #headers do
|
||||
local key, value = unpack(headers[i])
|
||||
key = key:lower()
|
||||
if key == "content-length" then
|
||||
contentLength = value
|
||||
elseif key == "content-encoding" and value:lower() == "chunked" then
|
||||
chunked = true
|
||||
end
|
||||
req[#req + 1] = headers[i]
|
||||
end
|
||||
end
|
||||
|
||||
if type(body) == "string" then
|
||||
if not chunked and not contentLength then
|
||||
req[#req + 1] = {"Content-Length", #body}
|
||||
end
|
||||
end
|
||||
|
||||
write(req)
|
||||
if body then write(body) end
|
||||
local res = read()
|
||||
if not res then
|
||||
if not connection.socket:is_closing() then
|
||||
connection.socket:close()
|
||||
end
|
||||
-- If we get an immediate close on a reused socket, try again with a new socket.
|
||||
-- TODO: think about if this could resend requests with side effects and cause
|
||||
-- them to double execute in the remote server.
|
||||
if connection.reused then
|
||||
return request(method, url, headers, body)
|
||||
end
|
||||
error("Connection closed")
|
||||
end
|
||||
|
||||
body = {}
|
||||
if req.method == "HEAD" then
|
||||
connection.reset()
|
||||
else
|
||||
while true do
|
||||
local item = read()
|
||||
if not item then
|
||||
res.keepAlive = false
|
||||
break
|
||||
end
|
||||
if #item == 0 then
|
||||
break
|
||||
end
|
||||
body[#body + 1] = item
|
||||
end
|
||||
end
|
||||
|
||||
if res.keepAlive then
|
||||
saveConnection(connection)
|
||||
else
|
||||
write()
|
||||
end
|
||||
|
||||
-- Follow redirects
|
||||
if method == "GET" and (res.code == 302 or res.code == 307) and options.followRedirects then
|
||||
for i = 1, #res do
|
||||
local key, location = unpack(res[i])
|
||||
if key:lower() == "location" then
|
||||
return request(method, location, headers)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return res, table.concat(body)
|
||||
end
|
||||
|
||||
return {
|
||||
createServer = createServer,
|
||||
parseUrl = parseUrl,
|
||||
getConnection = getConnection,
|
||||
saveConnection = saveConnection,
|
||||
request = request,
|
||||
}
|
@ -0,0 +1,196 @@
|
||||
--[[lit-meta
|
||||
name = "creationix/coro-net"
|
||||
version = "3.3.0"
|
||||
dependencies = {
|
||||
"creationix/coro-channel@3.0.0",
|
||||
"creationix/coro-wrapper@3.0.0",
|
||||
}
|
||||
optionalDependencies = {
|
||||
"luvit/secure-socket@1.0.0"
|
||||
}
|
||||
homepage = "https://github.com/luvit/lit/blob/master/deps/coro-net.lua"
|
||||
description = "An coro style client and server helper for tcp and pipes."
|
||||
tags = {"coro", "tcp", "pipe", "net"}
|
||||
license = "MIT"
|
||||
author = { name = "Tim Caswell" }
|
||||
]]
|
||||
|
||||
local uv = require('uv')
|
||||
local wrapStream = require('coro-channel').wrapStream
|
||||
local wrapper = require('coro-wrapper')
|
||||
local merger = wrapper.merger
|
||||
local decoder = wrapper.decoder
|
||||
local encoder = wrapper.encoder
|
||||
local secureSocket -- Lazy required from "secure-socket" on first use.
|
||||
|
||||
local function assertResume(thread, ...)
|
||||
local success, err = coroutine.resume(thread, ...)
|
||||
if not success then
|
||||
error(debug.traceback(thread, err), 0)
|
||||
end
|
||||
end
|
||||
|
||||
local function makeCallback(timeout)
|
||||
local thread = coroutine.running()
|
||||
local timer, done
|
||||
if timeout then
|
||||
timer = uv.new_timer()
|
||||
timer:start(timeout, 0, function ()
|
||||
if done then return end
|
||||
done = true
|
||||
timer:close()
|
||||
return assertResume(thread, nil, "timeout")
|
||||
end)
|
||||
end
|
||||
return function (err, data)
|
||||
if done then return end
|
||||
done = true
|
||||
if timer then timer:close() end
|
||||
if err then
|
||||
return assertResume(thread, nil, err)
|
||||
end
|
||||
return assertResume(thread, data or true)
|
||||
end
|
||||
end
|
||||
|
||||
local function normalize(options, server)
|
||||
local t = type(options)
|
||||
if t == "string" then
|
||||
options = {path=options}
|
||||
elseif t == "number" then
|
||||
options = {port=options}
|
||||
elseif t ~= "table" then
|
||||
assert("Net options must be table, string, or number")
|
||||
end
|
||||
if options.port or options.host then
|
||||
options.isTcp = true
|
||||
options.host = options.host or "127.0.0.1"
|
||||
assert(options.port, "options.port is required for tcp connections")
|
||||
elseif options.path then
|
||||
options.isTcp = false
|
||||
else
|
||||
error("Must set either options.path or options.port")
|
||||
end
|
||||
if options.tls == true then
|
||||
options.tls = {}
|
||||
end
|
||||
if options.tls then
|
||||
if server then
|
||||
options.tls.server = true
|
||||
assert(options.tls.cert, "TLS servers require a certificate")
|
||||
assert(options.tls.key, "TLS servers require a key")
|
||||
else
|
||||
options.tls.server = false
|
||||
options.tls.servername = options.host
|
||||
end
|
||||
end
|
||||
return options
|
||||
end
|
||||
|
||||
local function connect(options)
|
||||
local socket, success, err
|
||||
options = normalize(options)
|
||||
if options.isTcp then
|
||||
success, err = uv.getaddrinfo(options.host, options.port, {
|
||||
socktype = options.socktype or "stream",
|
||||
family = options.family or "inet",
|
||||
}, makeCallback(options.timeout))
|
||||
if not success then return nil, err end
|
||||
local res
|
||||
res, err = coroutine.yield()
|
||||
if not res then return nil, err end
|
||||
socket = uv.new_tcp()
|
||||
socket:connect(res[1].addr, res[1].port, makeCallback(options.timeout))
|
||||
else
|
||||
socket = uv.new_pipe(false)
|
||||
socket:connect(options.path, makeCallback(options.timeout))
|
||||
end
|
||||
success, err = coroutine.yield()
|
||||
if not success then return nil, err end
|
||||
local dsocket
|
||||
if options.tls then
|
||||
if not secureSocket then secureSocket = require('secure-socket') end
|
||||
dsocket, err = secureSocket(socket, options.tls)
|
||||
if not dsocket then
|
||||
return nil, err
|
||||
end
|
||||
else
|
||||
dsocket = socket
|
||||
end
|
||||
|
||||
local read, write, close = wrapStream(dsocket)
|
||||
local updateDecoder, updateEncoder
|
||||
if options.scan then
|
||||
-- TODO: Should we expose updateScan somehow?
|
||||
read = merger(read, options.scan)
|
||||
end
|
||||
if options.decoder then
|
||||
read, updateDecoder = decoder(read, options.decoder())
|
||||
elseif options.decode then
|
||||
read, updateDecoder = decoder(read, options.decode)
|
||||
end
|
||||
if options.encoder then
|
||||
write, updateEncoder = encoder(write, options.encoder())
|
||||
elseif options.encode then
|
||||
write, updateEncoder = encoder(write, options.encode)
|
||||
end
|
||||
return read, write, dsocket, updateDecoder, updateEncoder, close
|
||||
end
|
||||
|
||||
local function createServer(options, onConnect)
|
||||
local server
|
||||
options = normalize(options, true)
|
||||
if options.isTcp then
|
||||
server = uv.new_tcp()
|
||||
assert(server:bind(options.host, options.port))
|
||||
else
|
||||
server = uv.new_pipe(false)
|
||||
assert(server:bind(options.path))
|
||||
end
|
||||
assert(server:listen(256, function (err)
|
||||
assert(not err, err)
|
||||
local socket = options.isTcp and uv.new_tcp() or uv.new_pipe(false)
|
||||
server:accept(socket)
|
||||
coroutine.wrap(function ()
|
||||
local success, failure = xpcall(function ()
|
||||
local dsocket
|
||||
if options.tls then
|
||||
if not secureSocket then secureSocket = require('secure-socket') end
|
||||
dsocket = assert(secureSocket(socket, options.tls))
|
||||
dsocket.socket = socket
|
||||
else
|
||||
dsocket = socket
|
||||
end
|
||||
|
||||
local read, write = wrapStream(dsocket)
|
||||
local updateDecoder, updateEncoder
|
||||
if options.scan then
|
||||
-- TODO: should we expose updateScan somehow?
|
||||
read = merger(read, options.scan)
|
||||
end
|
||||
if options.decoder then
|
||||
read, updateDecoder = decoder(read, options.decoder())
|
||||
elseif options.decode then
|
||||
read, updateDecoder = decoder(read, options.decode)
|
||||
end
|
||||
if options.encoder then
|
||||
write, updateEncoder = encoder(write, options.encoder())
|
||||
elseif options.encode then
|
||||
write, updateEncoder = encoder(write, options.encode)
|
||||
end
|
||||
|
||||
return onConnect(read, write, dsocket, updateDecoder, updateEncoder)
|
||||
end, debug.traceback)
|
||||
if not success then
|
||||
print(failure)
|
||||
end
|
||||
end)()
|
||||
end))
|
||||
return server
|
||||
end
|
||||
|
||||
return {
|
||||
makeCallback = makeCallback,
|
||||
connect = connect,
|
||||
createServer = createServer,
|
||||
}
|
@ -0,0 +1,196 @@
|
||||
--[[lit-meta
|
||||
name = "creationix/coro-websocket"
|
||||
version = "3.1.1"
|
||||
dependencies = {
|
||||
"luvit/http-codec@3.0.0",
|
||||
"creationix/websocket-codec@3.0.0",
|
||||
"creationix/coro-net@3.3.0",
|
||||
}
|
||||
homepage = "https://github.com/luvit/lit/blob/master/deps/coro-websocket.lua"
|
||||
description = "Websocket helpers assuming coro style I/O."
|
||||
tags = {"coro", "websocket"}
|
||||
license = "MIT"
|
||||
author = { name = "Tim Caswell" }
|
||||
]]
|
||||
|
||||
local uv = require('uv')
|
||||
local httpCodec = require('http-codec')
|
||||
local websocketCodec = require('websocket-codec')
|
||||
local net = require('coro-net')
|
||||
|
||||
local function parseUrl(url)
|
||||
local protocol, host, port, pathname = string.match(url, "^(wss?)://([^:/]+):?(%d*)(/?[^#?]*)")
|
||||
local tls
|
||||
if protocol == "ws" then
|
||||
port = tonumber(port) or 80
|
||||
tls = false
|
||||
elseif protocol == "wss" then
|
||||
port = tonumber(port) or 443
|
||||
tls = true
|
||||
else
|
||||
return nil, "Sorry, only ws:// or wss:// protocols supported"
|
||||
end
|
||||
return {
|
||||
host = host,
|
||||
port = port,
|
||||
tls = tls,
|
||||
pathname = pathname
|
||||
}
|
||||
end
|
||||
|
||||
local function wrapIo(rawRead, rawWrite, options)
|
||||
|
||||
local closeSent = false
|
||||
|
||||
local timer
|
||||
|
||||
local function cleanup()
|
||||
if timer then
|
||||
if not timer:is_closing() then
|
||||
timer:close()
|
||||
end
|
||||
timer = nil
|
||||
end
|
||||
end
|
||||
|
||||
local function write(message)
|
||||
if message then
|
||||
message.mask = options.mask
|
||||
if message.opcode == 8 then
|
||||
closeSent = true
|
||||
rawWrite(message)
|
||||
cleanup()
|
||||
return rawWrite()
|
||||
end
|
||||
else
|
||||
if not closeSent then
|
||||
return write({
|
||||
opcode = 8,
|
||||
payload = ""
|
||||
})
|
||||
end
|
||||
end
|
||||
return rawWrite(message)
|
||||
end
|
||||
|
||||
|
||||
local function read()
|
||||
while true do
|
||||
local message = rawRead()
|
||||
if not message then
|
||||
return cleanup()
|
||||
end
|
||||
if message.opcode < 8 then
|
||||
return message
|
||||
end
|
||||
if not closeSent then
|
||||
if message.opcode == 8 then
|
||||
write {
|
||||
opcode = 8,
|
||||
payload = message.payload
|
||||
}
|
||||
elseif message.opcode == 9 then
|
||||
write {
|
||||
opcode = 10,
|
||||
payload = message.payload
|
||||
}
|
||||
end
|
||||
return message
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if options.heartbeat then
|
||||
local interval = options.heartbeat
|
||||
timer = uv.new_timer()
|
||||
timer:unref()
|
||||
timer:start(interval, interval, function ()
|
||||
coroutine.wrap(function ()
|
||||
local success, err = write {
|
||||
opcode = 10,
|
||||
payload = ""
|
||||
}
|
||||
if not success then
|
||||
timer:close()
|
||||
print(err)
|
||||
end
|
||||
end)()
|
||||
end)
|
||||
end
|
||||
|
||||
return read, write
|
||||
end
|
||||
|
||||
-- options table to configure connection
|
||||
-- options.path
|
||||
-- options.host
|
||||
-- options.port
|
||||
-- options.tls
|
||||
-- options.pathname
|
||||
-- options.subprotocol
|
||||
-- options.headers (as list of header/value pairs)
|
||||
-- options.timeout
|
||||
-- options.heartbeat
|
||||
-- returns res, read, write (res.socket has socket)
|
||||
local function connect(options)
|
||||
options = options or {}
|
||||
local config = {
|
||||
path = options.path,
|
||||
host = options.host,
|
||||
port = options.port,
|
||||
tls = options.tls,
|
||||
encoder = httpCodec.encoder,
|
||||
decoder = httpCodec.decoder,
|
||||
}
|
||||
local read, write, socket, updateDecoder, updateEncoder
|
||||
= net.connect(config, options.timeout or 10000)
|
||||
if not read then
|
||||
return nil, write
|
||||
end
|
||||
|
||||
local res
|
||||
|
||||
local success, err = websocketCodec.handshake({
|
||||
host = options.host,
|
||||
path = options.pathname,
|
||||
protocol = options.subprotocol
|
||||
}, function (req)
|
||||
local headers = options.headers
|
||||
if headers then
|
||||
for i = 1, #headers do
|
||||
req[#req + 1] = headers[i]
|
||||
end
|
||||
end
|
||||
write(req)
|
||||
res = read()
|
||||
if not res then error("Missing server response") end
|
||||
if res.code == 400 then
|
||||
-- p { req = req, res = res }
|
||||
local reason = read() or res.reason
|
||||
error("Invalid request: " .. reason)
|
||||
end
|
||||
return res
|
||||
end)
|
||||
if not success then
|
||||
return nil, err
|
||||
end
|
||||
|
||||
-- Upgrade the protocol to websocket
|
||||
updateDecoder(websocketCodec.decode)
|
||||
updateEncoder(websocketCodec.encode)
|
||||
|
||||
read, write = wrapIo(read, write, {
|
||||
mask = true,
|
||||
heartbeat = options.heartbeat
|
||||
})
|
||||
|
||||
res.socket = socket
|
||||
return res, read, write
|
||||
|
||||
end
|
||||
|
||||
return {
|
||||
parseUrl = parseUrl,
|
||||
wrapIo = wrapIo,
|
||||
connect = connect,
|
||||
}
|
@ -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 stops their loops. This does not
|
||||
empty any data that the client may have cached.
|
||||
]=]
|
||||
function Client:stop()
|
||||
for _, shard in pairs(self._shards) do
|
||||
shard:disconnect()
|
||||
end
|
||||
end
|
||||
|
||||
function Client:_modify(payload)
|
||||
local data, err = self._api:modifyCurrentUser(payload)
|
||||
if data then
|
||||
data.token = nil
|
||||
self._user:_load(data)
|
||||
return true
|
||||
else
|
||||
return false, err
|
||||
end
|
||||
end
|
||||
|
||||
--[=[
|
||||
@m setUsername
|
||||
@t http
|
||||
@p username string
|
||||
@r boolean
|
||||
@d Sets the client's username. This must be between 2 and 32 characters in
|
||||
length. This does not change the application name.
|
||||
]=]
|
||||
function Client:setUsername(username)
|
||||
return self:_modify({username = username or null})
|
||||
end
|
||||
|
||||
--[=[
|
||||
@m setAvatar
|
||||
@t http
|
||||
@p avatar Base64-Resolvable
|
||||
@r boolean
|
||||
@d Sets the client's avatar. To remove the avatar, pass an empty string or nil.
|
||||
This does not change the application image.
|
||||
]=]
|
||||
function Client:setAvatar(avatar)
|
||||
avatar = avatar and Resolver.base64(avatar)
|
||||
return self:_modify({avatar = avatar or null})
|
||||
end
|
||||
|
||||
--[=[
|
||||
@m createGuild
|
||||
@t http
|
||||
@p name string
|
||||
@r boolean
|
||||
@d Creates a new guild. The name must be between 2 and 100 characters in length.
|
||||
This method may not work if the current user is in too many guilds. Note that
|
||||
this does not return the created guild object; wait for the corresponding
|
||||
`guildCreate` event if you need the object.
|
||||
]=]
|
||||
function Client:createGuild(name)
|
||||
local data, err = self._api:createGuild({name = name})
|
||||
if data then
|
||||
return true
|
||||
else
|
||||
return false, err
|
||||
end
|
||||
end
|
||||
|
||||
--[=[
|
||||
@m createGroupChannel
|
||||
@t http
|
||||
@r GroupChannel
|
||||
@d Creates a new group channel. This method is only available for user accounts.
|
||||
]=]
|
||||
function Client:createGroupChannel()
|
||||
local data, err = self._api:createGroupDM()
|
||||
if data then
|
||||
return self._group_channels:_insert(data)
|
||||
else
|
||||
return nil, err
|
||||
end
|
||||
end
|
||||
|
||||
--[=[
|
||||
@m getWebhook
|
||||
@t http
|
||||
@p id string
|
||||
@r Webhook
|
||||
@d Gets a webhook object by ID. This always makes an HTTP request to obtain a
|
||||
static object that is not cached and is not updated by gateway events.
|
||||
]=]
|
||||
function Client:getWebhook(id)
|
||||
local data, err = self._api:getWebhook(id)
|
||||
if data then
|
||||
return Webhook(data, self)
|
||||
else
|
||||
return nil, err
|
||||
end
|
||||
end
|
||||
|
||||
--[=[
|
||||
@m getInvite
|
||||
@t http
|
||||
@p code string
|
||||
@op counts boolean
|
||||
@r Invite
|
||||
@d Gets an invite object by code. This always makes an HTTP request to obtain a
|
||||
static object that is not cached and is not updated by gateway events.
|
||||
]=]
|
||||
function Client:getInvite(code, counts)
|
||||
local data, err = self._api:getInvite(code, counts and {with_counts = true})
|
||||
if data then
|
||||
return Invite(data, self)
|
||||
else
|
||||
return nil, err
|
||||
end
|
||||
end
|
||||
|
||||
--[=[
|
||||
@m getUser
|
||||
@t http?
|
||||
@p id User-ID-Resolvable
|
||||
@r User
|
||||
@d Gets a user object by ID. If the object is already cached, then the cached
|
||||
object will be returned; otherwise, an HTTP request is made. Under circumstances
|
||||
which should be rare, the user object may be an old version, not updated by
|
||||
gateway events.
|
||||
]=]
|
||||
function Client:getUser(id)
|
||||
id = Resolver.userId(id)
|
||||
local user = self._users:get(id)
|
||||
if user then
|
||||
return user
|
||||
else
|
||||
local data, err = self._api:getUser(id)
|
||||
if data then
|
||||
return self._users:_insert(data)
|
||||
else
|
||||
return nil, err
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--[=[
|
||||
@m getGuild
|
||||
@t mem
|
||||
@p id Guild-ID-Resolvable
|
||||
@r Guild
|
||||
@d Gets a guild object by ID. The current user must be in the guild and the client
|
||||
must be running the appropriate shard that serves this guild. This method never
|
||||
makes an HTTP request to obtain a guild.
|
||||
]=]
|
||||
function Client:getGuild(id)
|
||||
id = Resolver.guildId(id)
|
||||
return self._guilds:get(id)
|
||||
end
|
||||
|
||||
--[=[
|
||||
@m getChannel
|
||||
@t mem
|
||||
@p id Channel-ID-Resolvable
|
||||
@r Channel
|
||||
@d Gets a channel object by ID. For guild channels, the current user must be in
|
||||
the channel's guild and the client must be running the appropriate shard that
|
||||
serves the channel's guild.
|
||||
|
||||
For private channels, the channel must have been previously opened and cached.
|
||||
If the channel is not cached, `User:getPrivateChannel` should be used instead.
|
||||
]=]
|
||||
function Client:getChannel(id)
|
||||
id = Resolver.channelId(id)
|
||||
local guild = self._channel_map[id]
|
||||
if guild then
|
||||
return guild._text_channels:get(id) or guild._voice_channels:get(id) or guild._categories:get(id)
|
||||
else
|
||||
return self._private_channels:get(id) or self._group_channels:get(id)
|
||||
end
|
||||
end
|
||||
|
||||
--[=[
|
||||
@m getRole
|
||||
@t mem
|
||||
@p id Role-ID-Resolvable
|
||||
@r Role
|
||||
@d Gets a role object by ID. The current user must be in the role's guild and
|
||||
the client must be running the appropriate shard that serves the role's guild.
|
||||
]=]
|
||||
function Client:getRole(id)
|
||||
id = Resolver.roleId(id)
|
||||
local guild = self._role_map[id]
|
||||
return guild and guild._roles:get(id)
|
||||
end
|
||||
|
||||
--[=[
|
||||
@m getEmoji
|
||||
@t mem
|
||||
@p id Emoji-ID-Resolvable
|
||||
@r Emoji
|
||||
@d Gets an emoji object by ID. The current user must be in the emoji's guild and
|
||||
the client must be running the appropriate shard that serves the emoji's guild.
|
||||
]=]
|
||||
function Client:getEmoji(id)
|
||||
id = Resolver.emojiId(id)
|
||||
local guild = self._emoji_map[id]
|
||||
return guild and guild._emojis:get(id)
|
||||
end
|
||||
|
||||
--[=[
|
||||
@m listVoiceRegions
|
||||
@t http
|
||||
@r table
|
||||
@d Returns a raw data table that contains a list of voice regions as provided by
|
||||
Discord, with no formatting beyond what is provided by the Discord API.
|
||||
]=]
|
||||
function Client:listVoiceRegions()
|
||||
return self._api:listVoiceRegions()
|
||||
end
|
||||
|
||||
--[=[
|
||||
@m getConnections
|
||||
@t http
|
||||
@r table
|
||||
@d Returns a raw data table that contains a list of connections as provided by
|
||||
Discord, with no formatting beyond what is provided by the Discord API.
|
||||
This is unrelated to voice connections.
|
||||
]=]
|
||||
function Client:getConnections()
|
||||
return self._api:getUsersConnections()
|
||||
end
|
||||
|
||||
--[=[
|
||||
@m getApplicationInformation
|
||||
@t http
|
||||
@r table
|
||||
@d Returns a raw data table that contains information about the current OAuth2
|
||||
application, with no formatting beyond what is provided by the Discord API.
|
||||
]=]
|
||||
function Client:getApplicationInformation()
|
||||
return self._api:getCurrentApplicationInformation()
|
||||
end
|
||||
|
||||
local function updateStatus(self)
|
||||
local presence = self._presence
|
||||
presence.afk = presence.afk or null
|
||||
presence.game = presence.game or null
|
||||
presence.since = presence.since or null
|
||||
presence.status = presence.status or null
|
||||
for _, shard in pairs(self._shards) do
|
||||
shard:updateStatus(presence)
|
||||
end
|
||||
end
|
||||
|
||||
--[=[
|
||||
@m setStatus
|
||||
@t ws
|
||||
@p status string
|
||||
@r nil
|
||||
@d Sets the current user's status on all shards that are managed by this client.
|
||||
See the `status` enumeration for acceptable status values.
|
||||
]=]
|
||||
function Client:setStatus(status)
|
||||
if type(status) == 'string' then
|
||||
self._presence.status = status
|
||||
if status == 'idle' then
|
||||
self._presence.since = 1000 * time()
|
||||
else
|
||||
self._presence.since = null
|
||||
end
|
||||
else
|
||||
self._presence.status = null
|
||||
self._presence.since = null
|
||||
end
|
||||
return updateStatus(self)
|
||||
end
|
||||
|
||||
--[=[
|
||||
@m setGame
|
||||
@t ws
|
||||
@p game string/table
|
||||
@r nil
|
||||
@d Sets the current user's game on all shards that are managed by this client.
|
||||
If a string is passed, it is treated as the game name. If a table is passed, it
|
||||
must have a `name` field and may optionally have a `url` or `type` field. Pass `nil` to
|
||||
remove the game status.
|
||||
]=]
|
||||
function Client:setGame(game)
|
||||
if type(game) == 'string' then
|
||||
game = {name = game, type = gameType.default}
|
||||
elseif type(game) == 'table' then
|
||||
if type(game.name) == 'string' then
|
||||
if type(game.type) ~= 'number' then
|
||||
if type(game.url) == 'string' then
|
||||
game.type = gameType.streaming
|
||||
else
|
||||
game.type = gameType.default
|
||||
end
|
||||
end
|
||||
else
|
||||
game = null
|
||||
end
|
||||
else
|
||||
game = null
|
||||
end
|
||||
self._presence.game = game
|
||||
return updateStatus(self)
|
||||
end
|
||||
|
||||
--[=[
|
||||
@m setAFK
|
||||
@t ws
|
||||
@p afk boolean
|
||||
@r nil
|
||||
@d Set the current user's AFK status on all shards that are managed by this client.
|
||||
This generally applies to user accounts and their push notifications.
|
||||
]=]
|
||||
function Client:setAFK(afk)
|
||||
if type(afk) == 'boolean' then
|
||||
self._presence.afk = afk
|
||||
else
|
||||
self._presence.afk = null
|
||||
end
|
||||
return updateStatus(self)
|
||||
end
|
||||
|
||||
--[=[@p shardCount number/nil The number of shards that this client is managing.]=]
|
||||
function get.shardCount(self)
|
||||
return self._shard_count
|
||||
end
|
||||
|
||||
--[=[@p totalShardCount number/nil The total number of shards that the current user is on.]=]
|
||||
function get.totalShardCount(self)
|
||||
return self._total_shard_count
|
||||
end
|
||||
|
||||
--[=[@p user User/nil User object representing the current user.]=]
|
||||
function get.user(self)
|
||||
return self._user
|
||||
end
|
||||
|
||||
--[=[@p owner User/nil User object representing the current user's owner.]=]
|
||||
function get.owner(self)
|
||||
return self._owner
|
||||
end
|
||||
|
||||
--[=[@p verified boolean/nil Whether the current user's owner's account is verified.]=]
|
||||
function get.verified(self)
|
||||
return self._user and self._user._verified
|
||||
end
|
||||
|
||||
--[=[@p mfaEnabled boolean/nil Whether the current user's owner's account has multi-factor (or two-factor)
|
||||
authentication enabled. This is equivalent to `verified`]=]
|
||||
function get.mfaEnabled(self)
|
||||
return self._user and self._user._verified
|
||||
end
|
||||
|
||||
--[=[@p email string/nil The current user's owner's account's email address (user-accounts only).]=]
|
||||
function get.email(self)
|
||||
return self._user and self._user._email
|
||||
end
|
||||
|
||||
--[=[@p guilds Cache An iterable cache of all guilds that are visible to the client. Note that the
|
||||
guilds present here correspond to which shards the client is managing. If all
|
||||
shards are managed by one client, then all guilds will be present.]=]
|
||||
function get.guilds(self)
|
||||
return self._guilds
|
||||
end
|
||||
|
||||
--[=[@p users Cache An iterable cache of all users that are visible to the client.
|
||||
To access a user that may exist but is not cached, use `Client:getUser`.]=]
|
||||
function get.users(self)
|
||||
return self._users
|
||||
end
|
||||
|
||||
--[=[@p privateChannels Cache An iterable cache of all private channels that are visible to the client. The
|
||||
channel must exist and must be open for it to be cached here. To access a
|
||||
private channel that may exist but is not cached, `User:getPrivateChannel`.]=]
|
||||
function get.privateChannels(self)
|
||||
return self._private_channels
|
||||
end
|
||||
|
||||
--[=[@p groupChannels Cache An iterable cache of all group channels that are visible to the client. Only
|
||||
user-accounts should have these.]=]
|
||||
function get.groupChannels(self)
|
||||
return self._group_channels
|
||||
end
|
||||
|
||||
--[=[@p relationships Cache An iterable cache of all relationships that are visible to the client. Only
|
||||
user-accounts should have these.]=]
|
||||
function get.relationships(self)
|
||||
return self._relationships
|
||||
end
|
||||
|
||||
return Client
|
@ -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,257 @@
|
||||
local json = require('json')
|
||||
local timer = require('timer')
|
||||
|
||||
local EventHandler = require('client/EventHandler')
|
||||
local WebSocket = require('client/WebSocket')
|
||||
|
||||
local constants = require('constants')
|
||||
local enums = require('enums')
|
||||
|
||||
local logLevel = enums.logLevel
|
||||
local min, max, random = math.min, math.max, math.random
|
||||
local null = json.null
|
||||
local format = string.format
|
||||
local sleep = timer.sleep
|
||||
local setInterval, clearInterval = timer.setInterval, timer.clearInterval
|
||||
local wrap = coroutine.wrap
|
||||
|
||||
local ID_DELAY = constants.ID_DELAY
|
||||
|
||||
local DISPATCH = 0
|
||||
local HEARTBEAT = 1
|
||||
local IDENTIFY = 2
|
||||
local STATUS_UPDATE = 3
|
||||
local VOICE_STATE_UPDATE = 4
|
||||
-- local VOICE_SERVER_PING = 5 -- TODO
|
||||
local RESUME = 6
|
||||
local RECONNECT = 7
|
||||
local REQUEST_GUILD_MEMBERS = 8
|
||||
local INVALID_SESSION = 9
|
||||
local HELLO = 10
|
||||
local HEARTBEAT_ACK = 11
|
||||
local GUILD_SYNC = 12
|
||||
|
||||
local ignore = {
|
||||
['CALL_DELETE'] = true,
|
||||
['CHANNEL_PINS_ACK'] = true,
|
||||
['GUILD_INTEGRATIONS_UPDATE'] = true,
|
||||
['MESSAGE_ACK'] = true,
|
||||
['PRESENCES_REPLACE'] = true,
|
||||
['USER_SETTINGS_UPDATE'] = true,
|
||||
['USER_GUILD_SETTINGS_UPDATE'] = true,
|
||||
['SESSIONS_REPLACE'] = true,
|
||||
['INVITE_CREATE'] = true,
|
||||
['INVITE_DELETE'] = true,
|
||||
['INTEGRATION_CREATE'] = true,
|
||||
['INTEGRATION_UPDATE'] = true,
|
||||
['INTEGRATION_DELETE'] = true,
|
||||
}
|
||||
|
||||
local Shard = require('class')('Shard', WebSocket)
|
||||
|
||||
function Shard:__init(id, client)
|
||||
WebSocket.__init(self, client)
|
||||
self._id = id
|
||||
self._client = client
|
||||
self._backoff = 1000
|
||||
end
|
||||
|
||||
for name in pairs(logLevel) do
|
||||
Shard[name] = function(self, fmt, ...)
|
||||
local client = self._client
|
||||
return client[name](client, format('Shard %i : %s', self._id, fmt), ...)
|
||||
end
|
||||
end
|
||||
|
||||
function Shard:__tostring()
|
||||
return format('Shard: %i', self._id)
|
||||
end
|
||||
|
||||
local function getReconnectTime(self, n, m)
|
||||
return self._backoff * (n + random() * (m - n))
|
||||
end
|
||||
|
||||
local function incrementReconnectTime(self)
|
||||
self._backoff = min(self._backoff * 2, 60000)
|
||||
end
|
||||
|
||||
local function decrementReconnectTime(self)
|
||||
self._backoff = max(self._backoff / 2, 1000)
|
||||
end
|
||||
|
||||
function Shard:handleDisconnect(url, path)
|
||||
self._client:emit('shardDisconnect', self._id)
|
||||
if self._reconnect then
|
||||
self:info('Reconnecting...')
|
||||
return self:connect(url, path)
|
||||
elseif self._reconnect == nil and self._client._options.autoReconnect then
|
||||
local backoff = getReconnectTime(self, 0.9, 1.1)
|
||||
incrementReconnectTime(self)
|
||||
self:info('Reconnecting after %i ms...', backoff)
|
||||
sleep(backoff)
|
||||
return self:connect(url, path)
|
||||
end
|
||||
end
|
||||
|
||||
function Shard:handlePayload(payload)
|
||||
|
||||
local client = self._client
|
||||
|
||||
local s = payload.s
|
||||
local t = payload.t
|
||||
local d = payload.d
|
||||
local op = payload.op
|
||||
|
||||
if t ~= null then
|
||||
self:debug('WebSocket OP %s : %s : %s', op, t, s)
|
||||
else
|
||||
self:debug('WebSocket OP %s', op)
|
||||
end
|
||||
|
||||
if op == DISPATCH then
|
||||
|
||||
self._seq = s
|
||||
if not ignore[t] then
|
||||
EventHandler[t](d, client, self)
|
||||
end
|
||||
|
||||
elseif op == HEARTBEAT then
|
||||
|
||||
self:heartbeat()
|
||||
|
||||
elseif op == RECONNECT then
|
||||
|
||||
self:info('Discord has requested a reconnection')
|
||||
self:disconnect(true)
|
||||
|
||||
elseif op == INVALID_SESSION then
|
||||
|
||||
if payload.d and self._session_id then
|
||||
self:info('Session invalidated, resuming...')
|
||||
self:resume()
|
||||
else
|
||||
self:info('Session invalidated, re-identifying...')
|
||||
sleep(random(1000, 5000))
|
||||
self:identify()
|
||||
end
|
||||
|
||||
elseif op == HELLO then
|
||||
|
||||
self:info('Received HELLO')
|
||||
self:startHeartbeat(d.heartbeat_interval)
|
||||
if self._session_id then
|
||||
self:resume()
|
||||
else
|
||||
self:identify()
|
||||
end
|
||||
|
||||
elseif op == HEARTBEAT_ACK then
|
||||
|
||||
client:emit('heartbeat', self._id, self._sw.milliseconds)
|
||||
|
||||
elseif op then
|
||||
|
||||
self:warning('Unhandled WebSocket payload OP %i', op)
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
local function loop(self)
|
||||
decrementReconnectTime(self)
|
||||
return wrap(self.heartbeat)(self)
|
||||
end
|
||||
|
||||
function Shard:startHeartbeat(interval)
|
||||
if self._heartbeat then
|
||||
clearInterval(self._heartbeat)
|
||||
end
|
||||
self._heartbeat = setInterval(interval, loop, self)
|
||||
end
|
||||
|
||||
function Shard:stopHeartbeat()
|
||||
if self._heartbeat then
|
||||
clearInterval(self._heartbeat)
|
||||
end
|
||||
self._heartbeat = nil
|
||||
end
|
||||
|
||||
function Shard:identifyWait()
|
||||
if self:waitFor('READY', 1.5 * ID_DELAY) then
|
||||
return sleep(ID_DELAY)
|
||||
end
|
||||
end
|
||||
|
||||
function Shard:heartbeat()
|
||||
self._sw:reset()
|
||||
return self:_send(HEARTBEAT, self._seq or json.null)
|
||||
end
|
||||
|
||||
function Shard:identify()
|
||||
|
||||
local client = self._client
|
||||
local mutex = client._mutex
|
||||
local options = client._options
|
||||
|
||||
mutex:lock()
|
||||
wrap(function()
|
||||
self:identifyWait()
|
||||
mutex:unlock()
|
||||
end)()
|
||||
|
||||
self._seq = nil
|
||||
self._session_id = nil
|
||||
self._ready = false
|
||||
self._loading = {guilds = {}, chunks = {}, syncs = {}}
|
||||
|
||||
return self:_send(IDENTIFY, {
|
||||
token = client._token,
|
||||
properties = {
|
||||
['$os'] = jit.os,
|
||||
['$browser'] = 'Discordia',
|
||||
['$device'] = 'Discordia',
|
||||
['$referrer'] = '',
|
||||
['$referring_domain'] = '',
|
||||
},
|
||||
compress = options.compress,
|
||||
large_threshold = options.largeThreshold,
|
||||
shard = {self._id, client._total_shard_count},
|
||||
presence = next(client._presence) and client._presence,
|
||||
}, true)
|
||||
|
||||
end
|
||||
|
||||
function Shard:resume()
|
||||
return self:_send(RESUME, {
|
||||
token = self._client._token,
|
||||
session_id = self._session_id,
|
||||
seq = self._seq
|
||||
})
|
||||
end
|
||||
|
||||
function Shard:requestGuildMembers(id)
|
||||
return self:_send(REQUEST_GUILD_MEMBERS, {
|
||||
guild_id = id,
|
||||
query = '',
|
||||
limit = 0,
|
||||
})
|
||||
end
|
||||
|
||||
function Shard:updateStatus(presence)
|
||||
return self:_send(STATUS_UPDATE, presence)
|
||||
end
|
||||
|
||||
function Shard:updateVoice(guild_id, channel_id, self_mute, self_deaf)
|
||||
return self:_send(VOICE_STATE_UPDATE, {
|
||||
guild_id = guild_id,
|
||||
channel_id = channel_id or null,
|
||||
self_mute = self_mute or false,
|
||||
self_deaf = self_deaf or false,
|
||||
})
|
||||
end
|
||||
|
||||
function Shard:syncGuilds(ids)
|
||||
return self:_send(GUILD_SYNC, ids)
|
||||
end
|
||||
|
||||
return Shard
|
@ -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,157 @@
|
||||
--[=[
|
||||
@c Activity
|
||||
@d Represents a Discord user's presence data, either plain game or streaming presence or a rich presence.
|
||||
Most if not all properties may be nil.
|
||||
]=]
|
||||
|
||||
local Container = require('containers/abstract/Container')
|
||||
|
||||
local format = string.format
|
||||
|
||||
local Activity, get = require('class')('Activity', Container)
|
||||
|
||||
function Activity:__init(data, parent)
|
||||
Container.__init(self, data, parent)
|
||||
return self:_loadMore(data)
|
||||
end
|
||||
|
||||
function Activity:_load(data)
|
||||
Container._load(self, data)
|
||||
return self:_loadMore(data)
|
||||
end
|
||||
|
||||
function Activity:_loadMore(data)
|
||||
local timestamps = data.timestamps
|
||||
self._start = timestamps and timestamps.start
|
||||
self._stop = timestamps and timestamps['end'] -- thanks discord
|
||||
local assets = data.assets
|
||||
self._small_text = assets and assets.small_text
|
||||
self._large_text = assets and assets.large_text
|
||||
self._small_image = assets and assets.small_image
|
||||
self._large_image = assets and assets.large_image
|
||||
local party = data.party
|
||||
self._party_id = party and party.id
|
||||
self._party_size = party and party.size and party.size[1]
|
||||
self._party_max = party and party.size and party.size[2]
|
||||
local emoji = data.emoji
|
||||
self._emoji_name = emoji and emoji.name
|
||||
self._emoji_id = emoji and emoji.id
|
||||
self._emoji_animated = emoji and emoji.animated
|
||||
end
|
||||
|
||||
--[=[
|
||||
@m __hash
|
||||
@r string
|
||||
@d Returns `Activity.parent:__hash()`
|
||||
]=]
|
||||
function Activity:__hash()
|
||||
return self._parent:__hash()
|
||||
end
|
||||
|
||||
--[=[@p start number/nil The Unix timestamp for when this Rich Presence activity was started.]=]
|
||||
function get.start(self)
|
||||
return self._start
|
||||
end
|
||||
|
||||
--[=[@p stop number/nil The Unix timestamp for when this Rich Presence activity was stopped.]=]
|
||||
function get.stop(self)
|
||||
return self._stop
|
||||
end
|
||||
|
||||
--[=[@p name string/nil The game that the user is currently playing.]=]
|
||||
function get.name(self)
|
||||
return self._name
|
||||
end
|
||||
|
||||
--[=[@p type number/nil The type of user's game status. See the `activityType`
|
||||
enumeration for a human-readable representation.]=]
|
||||
function get.type(self)
|
||||
return self._type
|
||||
end
|
||||
|
||||
--[=[@p url string/nil The URL that is set for a user's streaming game status.]=]
|
||||
function get.url(self)
|
||||
return self._url
|
||||
end
|
||||
|
||||
--[=[@p applicationId string/nil The application id controlling this Rich Presence activity.]=]
|
||||
function get.applicationId(self)
|
||||
return self._application_id
|
||||
end
|
||||
|
||||
--[=[@p state string/nil string for the Rich Presence state section.]=]
|
||||
function get.state(self)
|
||||
return self._state
|
||||
end
|
||||
|
||||
--[=[@p details string/nil string for the Rich Presence details section.]=]
|
||||
function get.details(self)
|
||||
return self._details
|
||||
end
|
||||
|
||||
--[=[@p textSmall string/nil string for the Rich Presence small image text.]=]
|
||||
function get.textSmall(self)
|
||||
return self._small_text
|
||||
end
|
||||
|
||||
--[=[@p textLarge string/nil string for the Rich Presence large image text.]=]
|
||||
function get.textLarge(self)
|
||||
return self._large_text
|
||||
end
|
||||
|
||||
--[=[@p imageSmall string/nil URL for the Rich Presence small image.]=]
|
||||
function get.imageSmall(self)
|
||||
return self._small_image
|
||||
end
|
||||
|
||||
--[=[@p imageLarge string/nil URL for the Rich Presence large image.]=]
|
||||
function get.imageLarge(self)
|
||||
return self._large_image
|
||||
end
|
||||
|
||||
--[=[@p partyId string/nil Party id for this Rich Presence.]=]
|
||||
function get.partyId(self)
|
||||
return self._party_id
|
||||
end
|
||||
|
||||
--[=[@p partySize number/nil Size of the Rich Presence party.]=]
|
||||
function get.partySize(self)
|
||||
return self._party_size
|
||||
end
|
||||
|
||||
--[=[@p partyMax number/nil Max size for the Rich Presence party.]=]
|
||||
function get.partyMax(self)
|
||||
return self._party_max
|
||||
end
|
||||
|
||||
--[=[@p emojiId string/nil The ID of the emoji used in this presence if one is
|
||||
set and if it is a custom emoji.]=]
|
||||
function get.emojiId(self)
|
||||
return self._emoji_id
|
||||
end
|
||||
|
||||
--[=[@p emojiName string/nil The name of the emoji used in this presence if one
|
||||
is set and if it has a custom emoji. This will be the raw string for a standard emoji.]=]
|
||||
function get.emojiName(self)
|
||||
return self._emoji_name
|
||||
end
|
||||
|
||||
--[=[@p emojiHash string/nil The discord hash for the emoji used in this presence if one is
|
||||
set. This will be the raw string for a standard emoji.]=]
|
||||
function get.emojiHash(self)
|
||||
if self._emoji_id then
|
||||
return self._emoji_name .. ':' .. self._emoji_id
|
||||
else
|
||||
return self._emoji_name
|
||||
end
|
||||
end
|
||||
|
||||
--[=[@p emojiURL string/nil string The URL that can be used to view a full
|
||||
version of the emoji used in this activity if one is set and if it is a custom emoji.]=]
|
||||
function get.emojiURL(self)
|
||||
local id = self._emoji_id
|
||||
local ext = self._emoji_animated and 'gif' or 'png'
|
||||
return id and format('https://cdn.discordapp.com/emojis/%s.%s', id, ext) or nil
|
||||
end
|
||||
|
||||
return Activity
|
@ -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,540 @@
|
||||
--[=[
|
||||
@c Member x UserPresence
|
||||
@d Represents a Discord guild member. Though one user may be a member in more than
|
||||
one guild, each presence is represented by a different member object associated
|
||||
with that guild. Note that any method or property that exists for the User class is
|
||||
also available in the Member class.
|
||||
]=]
|
||||
|
||||
local json = require('json')
|
||||
local enums = require('enums')
|
||||
local class = require('class')
|
||||
local UserPresence = require('containers/abstract/UserPresence')
|
||||
local ArrayIterable = require('iterables/ArrayIterable')
|
||||
local Color = require('utils/Color')
|
||||
local Resolver = require('client/Resolver')
|
||||
local GuildChannel = require('containers/abstract/GuildChannel')
|
||||
local Permissions = require('utils/Permissions')
|
||||
|
||||
local insert, remove, sort = table.insert, table.remove, table.sort
|
||||
local band, bor, bnot = bit.band, bit.bor, bit.bnot
|
||||
local isInstance = class.isInstance
|
||||
local permission = enums.permission
|
||||
|
||||
local Member, get = class('Member', UserPresence)
|
||||
|
||||
function Member:__init(data, parent)
|
||||
UserPresence.__init(self, data, parent)
|
||||
return self:_loadMore(data)
|
||||
end
|
||||
|
||||
function Member:_load(data)
|
||||
UserPresence._load(self, data)
|
||||
return self:_loadMore(data)
|
||||
end
|
||||
|
||||
function Member:_loadMore(data)
|
||||
if data.roles then
|
||||
local roles = #data.roles > 0 and data.roles or nil
|
||||
if self._roles then
|
||||
self._roles._array = roles
|
||||
else
|
||||
self._roles_raw = roles
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function sorter(a, b)
|
||||
if a._position == b._position then
|
||||
return tonumber(a._id) < tonumber(b._id)
|
||||
else
|
||||
return a._position > b._position
|
||||
end
|
||||
end
|
||||
|
||||
local function predicate(role)
|
||||
return role._color > 0
|
||||
end
|
||||
|
||||
--[=[
|
||||
@m getColor
|
||||
@t mem
|
||||
@r Color
|
||||
@d Returns a color object that represents the member's color as determined by
|
||||
its highest colored role. If the member has no colored roles, then the default
|
||||
color with a value of 0 is returned.
|
||||
]=]
|
||||
function Member:getColor()
|
||||
local roles = {}
|
||||
for role in self.roles:findAll(predicate) do
|
||||
insert(roles, role)
|
||||
end
|
||||
sort(roles, sorter)
|
||||
return roles[1] and roles[1]:getColor() or Color()
|
||||
end
|
||||
|
||||
local function has(a, b)
|
||||
return band(a, b) > 0
|
||||
end
|
||||
|
||||
--[=[
|
||||
@m hasPermission
|
||||
@t mem
|
||||
@op channel GuildChannel
|
||||
@p perm Permissions-Resolvable
|
||||
@r boolean
|
||||
@d Checks whether the member has a specific permission. If `channel` is omitted,
|
||||
then only guild-level permissions are checked. This is a relatively expensive
|
||||
operation. If you need to check multiple permissions at once, use the
|
||||
`getPermissions` method and check the resulting object.
|
||||
]=]
|
||||
function Member:hasPermission(channel, perm)
|
||||
|
||||
if not perm then
|
||||
perm = channel
|
||||
channel = nil
|
||||
end
|
||||
|
||||
local guild = self.guild
|
||||
if channel then
|
||||
if not isInstance(channel, GuildChannel) or channel.guild ~= guild then
|
||||
return error('Invalid GuildChannel: ' .. tostring(channel), 2)
|
||||
end
|
||||
end
|
||||
|
||||
local n = Resolver.permission(perm)
|
||||
if not n then
|
||||
return error('Invalid permission: ' .. tostring(perm), 2)
|
||||
end
|
||||
|
||||
if self.id == guild.ownerId then
|
||||
return true
|
||||
end
|
||||
|
||||
local rolePermissions = guild.defaultRole.permissions
|
||||
|
||||
for role in self.roles:iter() do
|
||||
if role.id ~= guild.id then -- just in case
|
||||
rolePermissions = bor(rolePermissions, role.permissions)
|
||||
end
|
||||
end
|
||||
|
||||
if has(rolePermissions, permission.administrator) then
|
||||
return true
|
||||
end
|
||||
|
||||
if channel then
|
||||
|
||||
local overwrites = channel.permissionOverwrites
|
||||
|
||||
local overwrite = overwrites:get(self.id)
|
||||
if overwrite then
|
||||
if has(overwrite.allowedPermissions, n) then
|
||||
return true
|
||||
end
|
||||
if has(overwrite.deniedPermissions, n) then
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
local allow, deny = 0, 0
|
||||
for role in self.roles:iter() do
|
||||
if role.id ~= guild.id then -- just in case
|
||||
overwrite = overwrites:get(role.id)
|
||||
if overwrite then
|
||||
allow = bor(allow, overwrite.allowedPermissions)
|
||||
deny = bor(deny, overwrite.deniedPermissions)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if has(allow, n) then
|
||||
return true
|
||||
end
|
||||
if has(deny, n) then
|
||||
return false
|
||||
end
|
||||
|
||||
local everyone = overwrites:get(guild.id)
|
||||
if everyone then
|
||||
if has(everyone.allowedPermissions, n) then
|
||||
return true
|
||||
end
|
||||
if has(everyone.deniedPermissions, n) then
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
return has(rolePermissions, n)
|
||||
|
||||
end
|
||||
|
||||
--[=[
|
||||
@m getPermissions
|
||||
@t mem
|
||||
@op channel GuildChannel
|
||||
@r Permissions
|
||||
@d Returns a permissions object that represents the member's total permissions for
|
||||
the guild, or for a specific channel if one is provided. If you just need to
|
||||
check one permission, use the `hasPermission` method.
|
||||
]=]
|
||||
function Member:getPermissions(channel)
|
||||
|
||||
local guild = self.guild
|
||||
if channel then
|
||||
if not isInstance(channel, GuildChannel) or channel.guild ~= guild then
|
||||
return error('Invalid GuildChannel: ' .. tostring(channel), 2)
|
||||
end
|
||||
end
|
||||
|
||||
if self.id == guild.ownerId then
|
||||
return Permissions.all()
|
||||
end
|
||||
|
||||
local ret = guild.defaultRole.permissions
|
||||
|
||||
for role in self.roles:iter() do
|
||||
if role.id ~= guild.id then -- just in case
|
||||
ret = bor(ret, role.permissions)
|
||||
end
|
||||
end
|
||||
|
||||
if band(ret, permission.administrator) > 0 then
|
||||
return Permissions.all()
|
||||
end
|
||||
|
||||
if channel then
|
||||
|
||||
local overwrites = channel.permissionOverwrites
|
||||
|
||||
local everyone = overwrites:get(guild.id)
|
||||
if everyone then
|
||||
ret = band(ret, bnot(everyone.deniedPermissions))
|
||||
ret = bor(ret, everyone.allowedPermissions)
|
||||
end
|
||||
|
||||
local allow, deny = 0, 0
|
||||
for role in self.roles:iter() do
|
||||
if role.id ~= guild.id then -- just in case
|
||||
local overwrite = overwrites:get(role.id)
|
||||
if overwrite then
|
||||
deny = bor(deny, overwrite.deniedPermissions)
|
||||
allow = bor(allow, overwrite.allowedPermissions)
|
||||
end
|
||||
end
|
||||
end
|
||||
ret = band(ret, bnot(deny))
|
||||
ret = bor(ret, allow)
|
||||
|
||||
local overwrite = overwrites:get(self.id)
|
||||
if overwrite then
|
||||
ret = band(ret, bnot(overwrite.deniedPermissions))
|
||||
ret = bor(ret, overwrite.allowedPermissions)
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
return Permissions(ret)
|
||||
|
||||
end
|
||||
|
||||
--[=[
|
||||
@m addRole
|
||||
@t http?
|
||||
@p id Role-ID-Resolvable
|
||||
@r boolean
|
||||
@d Adds a role to the member. If the member already has the role, then no action is
|
||||
taken. Note that the everyone role cannot be explicitly added.
|
||||
]=]
|
||||
function Member:addRole(id)
|
||||
if self:hasRole(id) then return true end
|
||||
id = Resolver.roleId(id)
|
||||
local data, err = self.client._api:addGuildMemberRole(self._parent._id, self.id, id)
|
||||
if data then
|
||||
local roles = self._roles and self._roles._array or self._roles_raw
|
||||
if roles then
|
||||
insert(roles, id)
|
||||
else
|
||||
self._roles_raw = {id}
|
||||
end
|
||||
return true
|
||||
else
|
||||
return false, err
|
||||
end
|
||||
end
|
||||
|
||||
--[=[
|
||||
@m removeRole
|
||||
@t http?
|
||||
@p id Role-ID-Resolvable
|
||||
@r boolean
|
||||
@d Removes a role from the member. If the member does not have the role, then no
|
||||
action is taken. Note that the everyone role cannot be removed.
|
||||
]=]
|
||||
function Member:removeRole(id)
|
||||
if not self:hasRole(id) then return true end
|
||||
id = Resolver.roleId(id)
|
||||
local data, err = self.client._api:removeGuildMemberRole(self._parent._id, self.id, id)
|
||||
if data then
|
||||
local roles = self._roles and self._roles._array or self._roles_raw
|
||||
if roles then
|
||||
for i, v in ipairs(roles) do
|
||||
if v == id then
|
||||
remove(roles, i)
|
||||
break
|
||||
end
|
||||
end
|
||||
if #roles == 0 then
|
||||
if self._roles then
|
||||
self._roles._array = nil
|
||||
else
|
||||
self._roles_raw = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
return true
|
||||
else
|
||||
return false, err
|
||||
end
|
||||
end
|
||||
|
||||
--[=[
|
||||
@m hasRole
|
||||
@t mem
|
||||
@p id Role-ID-Resolvable
|
||||
@r boolean
|
||||
@d Checks whether the member has a specific role. This will return true for the
|
||||
guild's default role in addition to any explicitly assigned roles.
|
||||
]=]
|
||||
function Member:hasRole(id)
|
||||
id = Resolver.roleId(id)
|
||||
if id == self._parent._id then return true end -- @everyone
|
||||
local roles = self._roles and self._roles._array or self._roles_raw
|
||||
if roles then
|
||||
for _, v in ipairs(roles) do
|
||||
if v == id then
|
||||
return true
|
||||
end
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
--[=[
|
||||
@m setNickname
|
||||
@t http
|
||||
@p nick string
|
||||
@r boolean
|
||||
@d Sets the member's nickname. This must be between 1 and 32 characters in length.
|
||||
Pass `nil` to remove the nickname.
|
||||
]=]
|
||||
function Member:setNickname(nick)
|
||||
nick = nick or ''
|
||||
local data, err
|
||||
if self.id == self.client._user._id then
|
||||
data, err = self.client._api:modifyCurrentUsersNick(self._parent._id, {nick = nick})
|
||||
else
|
||||
data, err = self.client._api:modifyGuildMember(self._parent._id, self.id, {nick = nick})
|
||||
end
|
||||
if data then
|
||||
self._nick = nick ~= '' and nick or nil
|
||||
return true
|
||||
else
|
||||
return false, err
|
||||
end
|
||||
end
|
||||
|
||||
--[=[
|
||||
@m setVoiceChannel
|
||||
@t http
|
||||
@p id Channel-ID-Resolvable
|
||||
@r boolean
|
||||
@d Moves the member to a new voice channel, but only if the member has an active
|
||||
voice connection in the current guild. Due to complexities in voice state
|
||||
handling, the member's `voiceChannel` property will update asynchronously via
|
||||
WebSocket; not as a result of the HTTP request.
|
||||
]=]
|
||||
function Member:setVoiceChannel(id)
|
||||
id = id and Resolver.channelId(id)
|
||||
local data, err = self.client._api:modifyGuildMember(self._parent._id, self.id, {channel_id = id or json.null})
|
||||
if data then
|
||||
return true
|
||||
else
|
||||
return false, err
|
||||
end
|
||||
end
|
||||
|
||||
--[=[
|
||||
@m mute
|
||||
@t http
|
||||
@r boolean
|
||||
@d Mutes the member in its guild.
|
||||
]=]
|
||||
function Member:mute()
|
||||
local data, err = self.client._api:modifyGuildMember(self._parent._id, self.id, {mute = true})
|
||||
if data then
|
||||
self._mute = true
|
||||
return true
|
||||
else
|
||||
return false, err
|
||||
end
|
||||
end
|
||||
|
||||
--[=[
|
||||
@m unmute
|
||||
@t http
|
||||
@r boolean
|
||||
@d Unmutes the member in its guild.
|
||||
]=]
|
||||
function Member:unmute()
|
||||
local data, err = self.client._api:modifyGuildMember(self._parent._id, self.id, {mute = false})
|
||||
if data then
|
||||
self._mute = false
|
||||
return true
|
||||
else
|
||||
return false, err
|
||||
end
|
||||
end
|
||||
|
||||
--[=[
|
||||
@m deafen
|
||||
@t http
|
||||
@r boolean
|
||||
@d Deafens the member in its guild.
|
||||
]=]
|
||||
function Member:deafen()
|
||||
local data, err = self.client._api:modifyGuildMember(self._parent._id, self.id, {deaf = true})
|
||||
if data then
|
||||
self._deaf = true
|
||||
return true
|
||||
else
|
||||
return false, err
|
||||
end
|
||||
end
|
||||
|
||||
--[=[
|
||||
@m undeafen
|
||||
@t http
|
||||
@r boolean
|
||||
@d Undeafens the member in its guild.
|
||||
]=]
|
||||
function Member:undeafen()
|
||||
local data, err = self.client._api:modifyGuildMember(self._parent._id, self.id, {deaf = false})
|
||||
if data then
|
||||
self._deaf = false
|
||||
return true
|
||||
else
|
||||
return false, err
|
||||
end
|
||||
end
|
||||
|
||||
--[=[
|
||||
@m kick
|
||||
@t http
|
||||
@p reason string
|
||||
@r boolean
|
||||
@d Equivalent to `Member.guild:kickUser(Member.user, reason)`
|
||||
]=]
|
||||
function Member:kick(reason)
|
||||
return self._parent:kickUser(self._user, reason)
|
||||
end
|
||||
|
||||
--[=[
|
||||
@m ban
|
||||
@t http
|
||||
@p reason string
|
||||
@p days number
|
||||
@r boolean
|
||||
@d Equivalent to `Member.guild:banUser(Member.user, reason, days)`
|
||||
]=]
|
||||
function Member:ban(reason, days)
|
||||
return self._parent:banUser(self._user, reason, days)
|
||||
end
|
||||
|
||||
--[=[
|
||||
@m unban
|
||||
@t http
|
||||
@p reason string
|
||||
@r boolean
|
||||
@d Equivalent to `Member.guild:unbanUser(Member.user, reason)`
|
||||
]=]
|
||||
function Member:unban(reason)
|
||||
return self._parent:unbanUser(self._user, reason)
|
||||
end
|
||||
|
||||
--[=[@p roles ArrayIterable An iterable array of guild roles that the member has. This does not explicitly
|
||||
include the default everyone role. Object order is not guaranteed.]=]
|
||||
function get.roles(self)
|
||||
if not self._roles then
|
||||
local roles = self._parent._roles
|
||||
self._roles = ArrayIterable(self._roles_raw, function(id)
|
||||
return roles:get(id)
|
||||
end)
|
||||
self._roles_raw = nil
|
||||
end
|
||||
return self._roles
|
||||
end
|
||||
|
||||
--[=[@p name string If the member has a nickname, then this will be equivalent to that nickname.
|
||||
Otherwise, this is equivalent to `Member.user.username`.]=]
|
||||
function get.name(self)
|
||||
return self._nick or self._user._username
|
||||
end
|
||||
|
||||
--[=[@p nickname string/nil The member's nickname, if one is set.]=]
|
||||
function get.nickname(self)
|
||||
return self._nick
|
||||
end
|
||||
|
||||
--[=[@p joinedAt string/nil The date and time at which the current member joined the guild, represented as
|
||||
an ISO 8601 string plus microseconds when available. Member objects generated
|
||||
via presence updates lack this property.]=]
|
||||
function get.joinedAt(self)
|
||||
return self._joined_at
|
||||
end
|
||||
|
||||
--[=[@p premiumSince string/nil The date and time at which the current member boosted the guild, represented as
|
||||
an ISO 8601 string plus microseconds when available.]=]
|
||||
function get.premiumSince(self)
|
||||
return self._premium_since
|
||||
end
|
||||
|
||||
--[=[@p voiceChannel GuildVoiceChannel/nil The voice channel to which this member is connected in the current guild.]=]
|
||||
function get.voiceChannel(self)
|
||||
local guild = self._parent
|
||||
local state = guild._voice_states[self:__hash()]
|
||||
return state and guild._voice_channels:get(state.channel_id)
|
||||
end
|
||||
|
||||
--[=[@p muted boolean Whether the member is voice muted in its guild.]=]
|
||||
function get.muted(self)
|
||||
local state = self._parent._voice_states[self:__hash()]
|
||||
return state and (state.mute or state.self_mute) or self._mute
|
||||
end
|
||||
|
||||
--[=[@p deafened boolean Whether the member is voice deafened in its guild.]=]
|
||||
function get.deafened(self)
|
||||
local state = self._parent._voice_states[self:__hash()]
|
||||
return state and (state.deaf or state.self_deaf) or self._deaf
|
||||
end
|
||||
|
||||
--[=[@p guild Guild The guild in which this member exists.]=]
|
||||
function get.guild(self)
|
||||
return self._parent
|
||||
end
|
||||
|
||||
--[=[@p highestRole Role The highest positioned role that the member has. If the member has no
|
||||
explicit roles, then this is equivalent to `Member.guild.defaultRole`.]=]
|
||||
function get.highestRole(self)
|
||||
local ret
|
||||
for role in self.roles:iter() do
|
||||
if not ret or sorter(role, ret) then
|
||||
ret = role
|
||||
end
|
||||
end
|
||||
return ret or self.guild.defaultRole
|
||||
end
|
||||
|
||||
return Member
|
@ -0,0 +1,601 @@
|
||||
--[=[
|
||||
@c Message x Snowflake
|
||||
@d Represents a text message sent in a Discord text channel. Messages can contain
|
||||
simple content strings, rich embeds, attachments, or reactions.
|
||||
]=]
|
||||
|
||||
local json = require('json')
|
||||
local enums = require('enums')
|
||||
local constants = require('constants')
|
||||
local Cache = require('iterables/Cache')
|
||||
local ArrayIterable = require('iterables/ArrayIterable')
|
||||
local Snowflake = require('containers/abstract/Snowflake')
|
||||
local Reaction = require('containers/Reaction')
|
||||
local Resolver = require('client/Resolver')
|
||||
|
||||
local insert = table.insert
|
||||
local null = json.null
|
||||
local format = string.format
|
||||
local messageFlag = enums.messageFlag
|
||||
local band, bor, bnot = bit.band, bit.bor, bit.bnot
|
||||
|
||||
local Message, get = require('class')('Message', Snowflake)
|
||||
|
||||
function Message:__init(data, parent)
|
||||
Snowflake.__init(self, data, parent)
|
||||
self._author = self.client._users:_insert(data.author)
|
||||
if data.member then
|
||||
data.member.user = data.author
|
||||
self._parent._parent._members:_insert(data.member)
|
||||
end
|
||||
self._timestamp = nil -- waste of space; can be calculated from Snowflake ID
|
||||
if data.reactions and #data.reactions > 0 then
|
||||
self._reactions = Cache(data.reactions, Reaction, self)
|
||||
end
|
||||
return self:_loadMore(data)
|
||||
end
|
||||
|
||||
function Message:_load(data)
|
||||
Snowflake._load(self, data)
|
||||
return self:_loadMore(data)
|
||||
end
|
||||
|
||||
local function parseMentions(content, pattern)
|
||||
if not content:find('%b<>') then return end
|
||||
local mentions, seen = {}, {}
|
||||
for id in content:gmatch(pattern) do
|
||||
if not seen[id] then
|
||||
insert(mentions, id)
|
||||
seen[id] = true
|
||||
end
|
||||
end
|
||||
return mentions
|
||||
end
|
||||
|
||||
function Message:_loadMore(data)
|
||||
|
||||
local mentions = {}
|
||||
if data.mentions then
|
||||
for _, user in ipairs(data.mentions) do
|
||||
mentions[user.id] = true
|
||||
if user.member then
|
||||
user.member.user = user
|
||||
self._parent._parent._members:_insert(user.member)
|
||||
else
|
||||
self.client._users:_insert(user)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if data.referenced_message and data.referenced_message ~= null then
|
||||
if mentions[data.referenced_message.author.id] then
|
||||
self._reply_target = data.referenced_message.author.id
|
||||
end
|
||||
self._referencedMessage = self._parent._messages:_insert(data.referenced_message)
|
||||
end
|
||||
|
||||
local content = data.content
|
||||
if content then
|
||||
if self._mentioned_users then
|
||||
self._mentioned_users._array = parseMentions(content, '<@!?(%d+)>')
|
||||
if self._reply_target then
|
||||
insert(self._mentioned_users._array, 1, self._reply_target)
|
||||
end
|
||||
end
|
||||
if self._mentioned_roles then
|
||||
self._mentioned_roles._array = parseMentions(content, '<@&(%d+)>')
|
||||
end
|
||||
if self._mentioned_channels then
|
||||
self._mentioned_channels._array = parseMentions(content, '<#(%d+)>')
|
||||
end
|
||||
if self._mentioned_emojis then
|
||||
self._mentioned_emojis._array = parseMentions(content, '<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
|
||||
if not reactions then return nil end
|
||||
|
||||
local emoji = d.emoji
|
||||
local k = emoji.id ~= null and emoji.id or emoji.name
|
||||
local reaction = reactions:get(k) or nil
|
||||
|
||||
if not reaction then return nil end -- uncached reaction?
|
||||
|
||||
reaction._count = reaction._count - 1
|
||||
if d.user_id == self.client._user._id then
|
||||
reaction._me = false
|
||||
end
|
||||
|
||||
if reaction._count == 0 then
|
||||
reactions:_delete(k)
|
||||
end
|
||||
|
||||
return reaction
|
||||
|
||||
end
|
||||
|
||||
function Message:_setOldContent(d)
|
||||
local ts = d.edited_timestamp
|
||||
if not ts then return end
|
||||
local old = self._old
|
||||
if old then
|
||||
old[ts] = old[ts] or self._content
|
||||
else
|
||||
self._old = {[ts] = self._content}
|
||||
end
|
||||
end
|
||||
|
||||
function Message:_modify(payload)
|
||||
local data, err = self.client._api:editMessage(self._parent._id, self._id, payload)
|
||||
if data then
|
||||
self:_setOldContent(data)
|
||||
self:_load(data)
|
||||
return true
|
||||
else
|
||||
return false, err
|
||||
end
|
||||
end
|
||||
|
||||
--[=[
|
||||
@m setContent
|
||||
@t http
|
||||
@p content string
|
||||
@r boolean
|
||||
@d Sets the message's content. The message must be authored by the current user
|
||||
(ie: you cannot change the content of messages sent by other users). The content
|
||||
must be from 1 to 2000 characters in length.
|
||||
]=]
|
||||
function Message:setContent(content)
|
||||
return self:_modify({content = content or null})
|
||||
end
|
||||
|
||||
--[=[
|
||||
@m setEmbed
|
||||
@t http
|
||||
@p embed table
|
||||
@r boolean
|
||||
@d Sets the message's embed. The message must be authored by the current user.
|
||||
(ie: you cannot change the embed of messages sent by other users).
|
||||
]=]
|
||||
function Message:setEmbed(embed)
|
||||
return self:_modify({embed = embed or null})
|
||||
end
|
||||
|
||||
--[=[
|
||||
@m hideEmbeds
|
||||
@t http
|
||||
@r boolean
|
||||
@d Hides all embeds for this message.
|
||||
]=]
|
||||
function Message:hideEmbeds()
|
||||
local flags = bor(self._flags or 0, messageFlag.suppressEmbeds)
|
||||
return self:_modify({flags = flags})
|
||||
end
|
||||
|
||||
--[=[
|
||||
@m showEmbeds
|
||||
@t http
|
||||
@r boolean
|
||||
@d Shows all embeds for this message.
|
||||
]=]
|
||||
function Message:showEmbeds()
|
||||
local flags = band(self._flags or 0, bnot(messageFlag.suppressEmbeds))
|
||||
return self:_modify({flags = flags})
|
||||
end
|
||||
|
||||
--[=[
|
||||
@m hasFlag
|
||||
@t mem
|
||||
@p flag Message-Flag-Resolvable
|
||||
@r boolean
|
||||
@d Indicates whether the message has a particular flag set.
|
||||
]=]
|
||||
function Message:hasFlag(flag)
|
||||
flag = Resolver.messageFlag(flag)
|
||||
return band(self._flags or 0, flag) > 0
|
||||
end
|
||||
|
||||
--[=[
|
||||
@m update
|
||||
@t http
|
||||
@p data table
|
||||
@r boolean
|
||||
@d Sets multiple properties of the message at the same time using a table similar
|
||||
to the one supported by `TextChannel.send`, except only `content` and `embed`
|
||||
are valid fields; `mention(s)`, `file(s)`, etc are not supported. The message
|
||||
must be authored by the current user. (ie: you cannot change the embed of messages
|
||||
sent by other users).
|
||||
]=]
|
||||
function Message:update(data)
|
||||
return self:_modify({
|
||||
content = data.content or null,
|
||||
embed = data.embed or null,
|
||||
})
|
||||
end
|
||||
|
||||
--[=[
|
||||
@m pin
|
||||
@t http
|
||||
@r boolean
|
||||
@d Pins the message in the channel.
|
||||
]=]
|
||||
function Message:pin()
|
||||
local data, err = self.client._api:addPinnedChannelMessage(self._parent._id, self._id)
|
||||
if data then
|
||||
self._pinned = true
|
||||
return true
|
||||
else
|
||||
return false, err
|
||||
end
|
||||
end
|
||||
|
||||
--[=[
|
||||
@m unpin
|
||||
@t http
|
||||
@r boolean
|
||||
@d Unpins the message in the channel.
|
||||
]=]
|
||||
function Message:unpin()
|
||||
local data, err = self.client._api:deletePinnedChannelMessage(self._parent._id, self._id)
|
||||
if data then
|
||||
self._pinned = false
|
||||
return true
|
||||
else
|
||||
return false, err
|
||||
end
|
||||
end
|
||||
|
||||
--[=[
|
||||
@m addReaction
|
||||
@t http
|
||||
@p emoji Emoji-Resolvable
|
||||
@r boolean
|
||||
@d Adds a reaction to the message. Note that this does not return the new reaction
|
||||
object; wait for the `reactionAdd` event instead.
|
||||
]=]
|
||||
function Message:addReaction(emoji)
|
||||
emoji = Resolver.emoji(emoji)
|
||||
local data, err = self.client._api:createReaction(self._parent._id, self._id, emoji)
|
||||
if data then
|
||||
return true
|
||||
else
|
||||
return false, err
|
||||
end
|
||||
end
|
||||
|
||||
--[=[
|
||||
@m removeReaction
|
||||
@t http
|
||||
@p emoji Emoji-Resolvable
|
||||
@op id User-ID-Resolvable
|
||||
@r boolean
|
||||
@d Removes a reaction from the message. Note that this does not return the old
|
||||
reaction object; wait for the `reactionRemove` event instead. If no user is
|
||||
indicated, then this will remove the current user's reaction.
|
||||
]=]
|
||||
function Message:removeReaction(emoji, id)
|
||||
emoji = Resolver.emoji(emoji)
|
||||
local data, err
|
||||
if id then
|
||||
id = Resolver.userId(id)
|
||||
data, err = self.client._api:deleteUserReaction(self._parent._id, self._id, emoji, id)
|
||||
else
|
||||
data, err = self.client._api:deleteOwnReaction(self._parent._id, self._id, emoji)
|
||||
end
|
||||
if data then
|
||||
return true
|
||||
else
|
||||
return false, err
|
||||
end
|
||||
end
|
||||
|
||||
--[=[
|
||||
@m clearReactions
|
||||
@t http
|
||||
@r boolean
|
||||
@d Removes all reactions from the message.
|
||||
]=]
|
||||
function Message:clearReactions()
|
||||
local data, err = self.client._api:deleteAllReactions(self._parent._id, self._id)
|
||||
if data then
|
||||
return true
|
||||
else
|
||||
return false, err
|
||||
end
|
||||
end
|
||||
|
||||
--[=[
|
||||
@m delete
|
||||
@t http
|
||||
@r boolean
|
||||
@d Permanently deletes the message. This cannot be undone!
|
||||
]=]
|
||||
function Message:delete()
|
||||
local data, err = self.client._api:deleteMessage(self._parent._id, self._id)
|
||||
if data then
|
||||
local cache = self._parent._messages
|
||||
if cache then
|
||||
cache:_delete(self._id)
|
||||
end
|
||||
return true
|
||||
else
|
||||
return false, err
|
||||
end
|
||||
end
|
||||
|
||||
--[=[
|
||||
@m reply
|
||||
@t http
|
||||
@p content string/table
|
||||
@r Message
|
||||
@d Equivalent to `Message.channel:send(content)`.
|
||||
]=]
|
||||
function Message:reply(content)
|
||||
return self._parent:send(content)
|
||||
end
|
||||
|
||||
--[=[@p reactions Cache An iterable cache of all reactions that exist for this message.]=]
|
||||
function get.reactions(self)
|
||||
if not self._reactions then
|
||||
self._reactions = Cache({}, Reaction, self)
|
||||
end
|
||||
return self._reactions
|
||||
end
|
||||
|
||||
--[=[@p mentionedUsers ArrayIterable An iterable array of all users that are mentioned in this message.]=]
|
||||
function get.mentionedUsers(self)
|
||||
if not self._mentioned_users then
|
||||
local users = self.client._users
|
||||
local mentions = parseMentions(self._content, '<@!?(%d+)>')
|
||||
if self._reply_target then
|
||||
insert(mentions, 1, self._reply_target)
|
||||
end
|
||||
self._mentioned_users = ArrayIterable(mentions, function(id)
|
||||
return users:get(id)
|
||||
end)
|
||||
end
|
||||
return self._mentioned_users
|
||||
end
|
||||
|
||||
--[=[@p mentionedRoles ArrayIterable An iterable array of known roles that are mentioned in this message, excluding
|
||||
the default everyone role. The message must be in a guild text channel and the
|
||||
roles must be cached in that channel's guild for them to appear here.]=]
|
||||
function get.mentionedRoles(self)
|
||||
if not self._mentioned_roles then
|
||||
local client = self.client
|
||||
local mentions = parseMentions(self._content, '<@&(%d+)>')
|
||||
self._mentioned_roles = ArrayIterable(mentions, function(id)
|
||||
local guild = client._role_map[id]
|
||||
return guild and guild._roles:get(id) or nil
|
||||
end)
|
||||
end
|
||||
return self._mentioned_roles
|
||||
end
|
||||
|
||||
--[=[@p mentionedEmojis ArrayIterable An iterable array of all known emojis that are mentioned in this message. If
|
||||
the client does not have the emoji cached, then it will not appear here.]=]
|
||||
function get.mentionedEmojis(self)
|
||||
if not self._mentioned_emojis then
|
||||
local client = self.client
|
||||
local mentions = parseMentions(self._content, '<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 referencedMessage Message/nil If available, the previous message that
|
||||
this current message references as seen in replies.]=]
|
||||
function get.referencedMessage(self)
|
||||
return self._referencedMessage
|
||||
end
|
||||
|
||||
--[=[@p link string URL that can be used to jump-to the message in the Discord client.]=]
|
||||
function get.link(self)
|
||||
local guild = self.guild
|
||||
return format('https://discord.com/channels/%s/%s/%s', guild and guild._id or '@me', self._parent._id, self._id)
|
||||
end
|
||||
|
||||
--[=[@p webhookId string/nil The ID of the webhook that generated this message, if applicable.]=]
|
||||
function get.webhookId(self)
|
||||
return self._webhook_id
|
||||
end
|
||||
|
||||
return Message
|
@ -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,337 @@
|
||||
--[=[
|
||||
@c TextChannel x Channel
|
||||
@t abc
|
||||
@d Defines the base methods and properties for all Discord text channels.
|
||||
]=]
|
||||
|
||||
local pathjoin = require('pathjoin')
|
||||
local Channel = require('containers/abstract/Channel')
|
||||
local Message = require('containers/Message')
|
||||
local WeakCache = require('iterables/WeakCache')
|
||||
local SecondaryCache = require('iterables/SecondaryCache')
|
||||
local Resolver = require('client/Resolver')
|
||||
local fs = require('fs')
|
||||
|
||||
local splitPath = pathjoin.splitPath
|
||||
local insert, remove, concat = table.insert, table.remove, table.concat
|
||||
local format = string.format
|
||||
local readFileSync = fs.readFileSync
|
||||
|
||||
local TextChannel, get = require('class')('TextChannel', Channel)
|
||||
|
||||
function TextChannel:__init(data, parent)
|
||||
Channel.__init(self, data, parent)
|
||||
self._messages = WeakCache({}, Message, self)
|
||||
end
|
||||
|
||||
--[=[
|
||||
@m getMessage
|
||||
@t http
|
||||
@p id Message-ID-Resolvable
|
||||
@r Message
|
||||
@d Gets a message object by ID. If the object is already cached, then the cached
|
||||
object will be returned; otherwise, an HTTP request is made.
|
||||
]=]
|
||||
function TextChannel:getMessage(id)
|
||||
id = Resolver.messageId(id)
|
||||
local message = self._messages:get(id)
|
||||
if message then
|
||||
return message
|
||||
else
|
||||
local data, err = self.client._api:getChannelMessage(self._id, id)
|
||||
if data then
|
||||
return self._messages:_insert(data)
|
||||
else
|
||||
return nil, err
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--[=[
|
||||
@m getFirstMessage
|
||||
@t http
|
||||
@r Message
|
||||
@d Returns the first message found in the channel, if any exist. This is not a
|
||||
cache shortcut; an HTTP request is made each time this method is called.
|
||||
]=]
|
||||
function TextChannel:getFirstMessage()
|
||||
local data, err = self.client._api:getChannelMessages(self._id, {after = self._id, limit = 1})
|
||||
if data then
|
||||
if data[1] then
|
||||
return self._messages:_insert(data[1])
|
||||
else
|
||||
return nil, 'Channel has no messages'
|
||||
end
|
||||
else
|
||||
return nil, err
|
||||
end
|
||||
end
|
||||
|
||||
--[=[
|
||||
@m getLastMessage
|
||||
@t http
|
||||
@r Message
|
||||
@d Returns the last message found in the channel, if any exist. This is not a
|
||||
cache shortcut; an HTTP request is made each time this method is called.
|
||||
]=]
|
||||
function TextChannel:getLastMessage()
|
||||
local data, err = self.client._api:getChannelMessages(self._id, {limit = 1})
|
||||
if data then
|
||||
if data[1] then
|
||||
return self._messages:_insert(data[1])
|
||||
else
|
||||
return nil, 'Channel has no messages'
|
||||
end
|
||||
else
|
||||
return nil, err
|
||||
end
|
||||
end
|
||||
|
||||
local function getMessages(self, query)
|
||||
local data, err = self.client._api:getChannelMessages(self._id, query)
|
||||
if data then
|
||||
return SecondaryCache(data, self._messages)
|
||||
else
|
||||
return nil, err
|
||||
end
|
||||
end
|
||||
|
||||
--[=[
|
||||
@m getMessages
|
||||
@t http
|
||||
@op limit number
|
||||
@r SecondaryCache
|
||||
@d Returns a newly constructed cache of between 1 and 100 (default = 50) message
|
||||
objects found in the channel. While the cache will never automatically gain or
|
||||
lose objects, the objects that it contains may be updated by gateway events.
|
||||
]=]
|
||||
function TextChannel:getMessages(limit)
|
||||
return getMessages(self, limit and {limit = limit})
|
||||
end
|
||||
|
||||
--[=[
|
||||
@m getMessagesAfter
|
||||
@t http
|
||||
@p id Message-ID-Resolvable
|
||||
@op limit number
|
||||
@r SecondaryCache
|
||||
@d Returns a newly constructed cache of between 1 and 100 (default = 50) message
|
||||
objects found in the channel after a specific id. While the cache will never
|
||||
automatically gain or lose objects, the objects that it contains may be updated
|
||||
by gateway events.
|
||||
]=]
|
||||
function TextChannel:getMessagesAfter(id, limit)
|
||||
id = Resolver.messageId(id)
|
||||
return getMessages(self, {after = id, limit = limit})
|
||||
end
|
||||
|
||||
--[=[
|
||||
@m getMessagesBefore
|
||||
@t http
|
||||
@p id Message-ID-Resolvable
|
||||
@op limit number
|
||||
@r SecondaryCache
|
||||
@d Returns a newly constructed cache of between 1 and 100 (default = 50) message
|
||||
objects found in the channel before a specific id. While the cache will never
|
||||
automatically gain or lose objects, the objects that it contains may be updated
|
||||
by gateway events.
|
||||
]=]
|
||||
function TextChannel:getMessagesBefore(id, limit)
|
||||
id = Resolver.messageId(id)
|
||||
return getMessages(self, {before = id, limit = limit})
|
||||
end
|
||||
|
||||
--[=[
|
||||
@m getMessagesAround
|
||||
@t http
|
||||
@p id Message-ID-Resolvable
|
||||
@op limit number
|
||||
@r SecondaryCache
|
||||
@d Returns a newly constructed cache of between 1 and 100 (default = 50) message
|
||||
objects found in the channel around a specific point. While the cache will never
|
||||
automatically gain or lose objects, the objects that it contains may be updated
|
||||
by gateway events.
|
||||
]=]
|
||||
function TextChannel:getMessagesAround(id, limit)
|
||||
id = Resolver.messageId(id)
|
||||
return getMessages(self, {around = id, limit = limit})
|
||||
end
|
||||
|
||||
--[=[
|
||||
@m getPinnedMessages
|
||||
@t http
|
||||
@r SecondaryCache
|
||||
@d Returns a newly constructed cache of up to 50 messages that are pinned in the
|
||||
channel. While the cache will never automatically gain or lose objects, the
|
||||
objects that it contains may be updated by gateway events.
|
||||
]=]
|
||||
function TextChannel:getPinnedMessages()
|
||||
local data, err = self.client._api:getPinnedMessages(self._id)
|
||||
if data then
|
||||
return SecondaryCache(data, self._messages)
|
||||
else
|
||||
return nil, err
|
||||
end
|
||||
end
|
||||
|
||||
--[=[
|
||||
@m broadcastTyping
|
||||
@t http
|
||||
@r boolean
|
||||
@d Indicates in the channel that the client's user "is typing".
|
||||
]=]
|
||||
function TextChannel:broadcastTyping()
|
||||
local data, err = self.client._api:triggerTypingIndicator(self._id)
|
||||
if data then
|
||||
return true
|
||||
else
|
||||
return false, err
|
||||
end
|
||||
end
|
||||
|
||||
local function parseFile(obj, files)
|
||||
if type(obj) == 'string' then
|
||||
local data, err = readFileSync(obj)
|
||||
if not data then
|
||||
return nil, err
|
||||
end
|
||||
files = files or {}
|
||||
insert(files, {remove(splitPath(obj)), data})
|
||||
elseif type(obj) == 'table' and type(obj[1]) == 'string' and type(obj[2]) == 'string' then
|
||||
files = files or {}
|
||||
insert(files, obj)
|
||||
else
|
||||
return nil, 'Invalid file object: ' .. tostring(obj)
|
||||
end
|
||||
return files
|
||||
end
|
||||
|
||||
local function parseMention(obj, mentions)
|
||||
if type(obj) == 'table' and obj.mentionString then
|
||||
mentions = mentions or {}
|
||||
insert(mentions, obj.mentionString)
|
||||
else
|
||||
return nil, 'Unmentionable object: ' .. tostring(obj)
|
||||
end
|
||||
return mentions
|
||||
end
|
||||
|
||||
--[=[
|
||||
@m send
|
||||
@t http
|
||||
@p content string/table
|
||||
@r Message
|
||||
@d Sends a message to the channel. If `content` is a string, then this is simply
|
||||
sent as the message content. If it is a table, more advanced formatting is
|
||||
allowed. See [[managing messages]] for more information.
|
||||
]=]
|
||||
function TextChannel:send(content)
|
||||
|
||||
local data, err
|
||||
|
||||
if type(content) == 'table' then
|
||||
|
||||
local tbl = content
|
||||
content = tbl.content
|
||||
|
||||
if type(tbl.code) == 'string' then
|
||||
content = format('```%s\n%s\n```', tbl.code, content)
|
||||
elseif tbl.code == true then
|
||||
content = format('```\n%s\n```', content)
|
||||
end
|
||||
|
||||
local mentions
|
||||
if tbl.mention then
|
||||
mentions, err = parseMention(tbl.mention)
|
||||
if err then
|
||||
return nil, err
|
||||
end
|
||||
end
|
||||
if type(tbl.mentions) == 'table' then
|
||||
for _, mention in ipairs(tbl.mentions) do
|
||||
mentions, err = parseMention(mention, mentions)
|
||||
if err then
|
||||
return nil, err
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if mentions then
|
||||
insert(mentions, content)
|
||||
content = concat(mentions, ' ')
|
||||
end
|
||||
|
||||
local files
|
||||
if tbl.file then
|
||||
files, err = parseFile(tbl.file)
|
||||
if err then
|
||||
return nil, err
|
||||
end
|
||||
end
|
||||
if type(tbl.files) == 'table' then
|
||||
for _, file in ipairs(tbl.files) do
|
||||
files, err = parseFile(file, files)
|
||||
if err then
|
||||
return nil, err
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local refMessage, refMention
|
||||
if tbl.reference then
|
||||
refMessage = {message_id = Resolver.messageId(tbl.reference.message)}
|
||||
refMention = {
|
||||
parse = {'users', 'roles', 'everyone'},
|
||||
replied_user = not not tbl.reference.mention,
|
||||
}
|
||||
end
|
||||
|
||||
data, err = self.client._api:createMessage(self._id, {
|
||||
content = content,
|
||||
tts = tbl.tts,
|
||||
nonce = tbl.nonce,
|
||||
embed = tbl.embed,
|
||||
message_reference = refMessage,
|
||||
allowed_mentions = refMention,
|
||||
}, files)
|
||||
|
||||
else
|
||||
|
||||
data, err = self.client._api:createMessage(self._id, {content = content})
|
||||
|
||||
end
|
||||
|
||||
if data then
|
||||
return self._messages:_insert(data)
|
||||
else
|
||||
return nil, err
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
--[=[
|
||||
@m sendf
|
||||
@t http
|
||||
@p content string
|
||||
@p ... *
|
||||
@r Message
|
||||
@d Sends a message to the channel with content formatted with `...` via `string.format`
|
||||
]=]
|
||||
function TextChannel:sendf(content, ...)
|
||||
local data, err = self.client._api:createMessage(self._id, {content = format(content, ...)})
|
||||
if data then
|
||||
return self._messages:_insert(data)
|
||||
else
|
||||
return nil, err
|
||||
end
|
||||
end
|
||||
|
||||
--[=[@p messages WeakCache An iterable weak cache of all messages that are
|
||||
visible to the client. Messages that are not referenced elsewhere are eventually
|
||||
garbage collected. To access a message that may exist but is not cached,
|
||||
use `TextChannel:getMessage`.]=]
|
||||
function get.messages(self)
|
||||
return self._messages
|
||||
end
|
||||
|
||||
return TextChannel
|
@ -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 satisfy the predicate; otherwise, the total number of objects.
|
||||
]=]
|
||||
function Iterable:count(fn)
|
||||
if not fn then
|
||||
return self:__len()
|
||||
end
|
||||
local n = 0
|
||||
for _ in self:findAll(fn) do
|
||||
n = n + 1
|
||||
end
|
||||
return n
|
||||
end
|
||||
|
||||
local function sorter(a, b)
|
||||
local t1, t2 = type(a), type(b)
|
||||
if t1 == 'string' then
|
||||
if t2 == 'string' then
|
||||
local n1 = tonumber(a)
|
||||
if n1 then
|
||||
local n2 = tonumber(b)
|
||||
if n2 then
|
||||
return n1 < n2
|
||||
end
|
||||
end
|
||||
return a:lower() < b:lower()
|
||||
elseif t2 == 'number' then
|
||||
local n1 = tonumber(a)
|
||||
if n1 then
|
||||
return n1 < b
|
||||
end
|
||||
return a:lower() < tostring(b)
|
||||
end
|
||||
elseif t1 == 'number' then
|
||||
if t2 == 'number' then
|
||||
return a < b
|
||||
elseif t2 == 'string' then
|
||||
local n2 = tonumber(b)
|
||||
if n2 then
|
||||
return a < n2
|
||||
end
|
||||
return tostring(a) < b:lower()
|
||||
end
|
||||
end
|
||||
local m1 = getmetatable(a)
|
||||
if m1 and m1.__lt then
|
||||
local m2 = getmetatable(b)
|
||||
if m2 and m2.__lt then
|
||||
return a < b
|
||||
end
|
||||
end
|
||||
return tostring(a) < tostring(b)
|
||||
end
|
||||
|
||||
--[=[
|
||||
@m toArray
|
||||
@op sortBy string
|
||||
@op fn function
|
||||
@r table
|
||||
@d Returns a sequentially-indexed table that contains references to all objects.
|
||||
If a `sortBy` string is provided, then the table is sorted by that particular
|
||||
property. If a predicate is provided, then only objects that satisfy it will
|
||||
be included.
|
||||
]=]
|
||||
function Iterable:toArray(sortBy, fn)
|
||||
local t1 = type(sortBy)
|
||||
if t1 == 'string' then
|
||||
fn = type(fn) == 'function' and fn
|
||||
elseif t1 == 'function' then
|
||||
fn = sortBy
|
||||
sortBy = nil
|
||||
end
|
||||
local ret = {}
|
||||
for obj in self:iter() do
|
||||
if not fn or fn(obj) then
|
||||
insert(ret, obj)
|
||||
end
|
||||
end
|
||||
if sortBy then
|
||||
sort(ret, function(a, b)
|
||||
return sorter(a[sortBy], b[sortBy])
|
||||
end)
|
||||
end
|
||||
return ret
|
||||
end
|
||||
|
||||
--[=[
|
||||
@m select
|
||||
@p ... string
|
||||
@r table
|
||||
@d Similarly to an SQL query, this returns a sorted Lua table of rows where each
|
||||
row corresponds to each object in the iterable, and each value in the row is
|
||||
selected from the objects according to the keys provided.
|
||||
]=]
|
||||
function Iterable:select(...)
|
||||
local rows = {}
|
||||
local keys = pack(...)
|
||||
for obj in self:iter() do
|
||||
local row = {}
|
||||
for i = 1, keys.n do
|
||||
row[i] = obj[keys[i]]
|
||||
end
|
||||
insert(rows, row)
|
||||
end
|
||||
sort(rows, function(a, b)
|
||||
for i = 1, keys.n do
|
||||
if a[i] ~= b[i] then
|
||||
return sorter(a[i], b[i])
|
||||
end
|
||||
end
|
||||
end)
|
||||
return rows
|
||||
end
|
||||
|
||||
--[=[
|
||||
@m pick
|
||||
@p ... string/function
|
||||
@r function
|
||||
@d This returns an iterator that, when called, returns the values from each
|
||||
encountered object, picked by the provided keys. If a key is a string, the objects
|
||||
are indexed with the string. If a key is a function, the function is called with
|
||||
the object passed as its first argument.
|
||||
]=]
|
||||
function Iterable:pick(...)
|
||||
local keys = pack(...)
|
||||
local values = {}
|
||||
local n = keys.n
|
||||
local gen = self:iter()
|
||||
return function()
|
||||
local obj = gen()
|
||||
if not obj then
|
||||
return nil
|
||||
end
|
||||
for i = 1, n do
|
||||
local k = keys[i]
|
||||
if type(k) == 'function' then
|
||||
values[i] = k(obj)
|
||||
else
|
||||
values[i] = obj[k]
|
||||
end
|
||||
end
|
||||
return unpack(values, 1, n)
|
||||
end
|
||||
end
|
||||
|
||||
return Iterable
|
@ -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 an arbitrary interval.
|
||||
]=]
|
||||
|
||||
local timer = require('timer')
|
||||
local Emitter = require('utils/Emitter')
|
||||
|
||||
local date = os.date
|
||||
local setInterval, clearInterval = timer.setInterval, timer.clearInterval
|
||||
|
||||
local Clock = require('class')('Clock', Emitter)
|
||||
|
||||
function Clock:__init()
|
||||
Emitter.__init(self)
|
||||
end
|
||||
|
||||
--[=[
|
||||
@m start
|
||||
@op utc boolean
|
||||
@r nil
|
||||
@d Starts the main loop for the clock. If a truthy argument is passed, then UTC
|
||||
time is used; otherwise, local time is used. As the clock ticks, an event is
|
||||
emitted for every `os.date` value change. The event name is the key of the value
|
||||
that changed and the event argument is the corresponding date table.
|
||||
]=]
|
||||
function Clock:start(utc)
|
||||
if self._interval then return end
|
||||
local fmt = utc and '!*t' or '*t'
|
||||
local prev = date(fmt)
|
||||
self._interval = setInterval(1000, function()
|
||||
local now = date(fmt)
|
||||
for k, v in pairs(now) do
|
||||
if v ~= prev[k] then
|
||||
self:emit(k, now)
|
||||
end
|
||||
end
|
||||
prev = now
|
||||
end)
|
||||
end
|
||||
|
||||
--[=[
|
||||
@m stop
|
||||
@r nil
|
||||
@d Stops the main loop for the clock.
|
||||
]=]
|
||||
function Clock:stop()
|
||||
if self._interval then
|
||||
clearInterval(self._interval)
|
||||
self._interval = nil
|
||||
end
|
||||
end
|
||||
|
||||
return Clock
|
@ -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 object represents.
|
||||
]=]
|
||||
function Permissions:toArray()
|
||||
local ret = {}
|
||||
local value = self._value
|
||||
for k, v in pairs(permission) do
|
||||
if band(value, v) > 0 then
|
||||
insert(ret, k)
|
||||
end
|
||||
end
|
||||
return ret
|
||||
end
|
||||
|
||||
--[=[
|
||||
@m union
|
||||
@p other Permissions
|
||||
@r Permissions
|
||||
@d Returns a new Permissions object that contains the permissions that are in
|
||||
either `self` or `other` (bitwise OR).
|
||||
]=]
|
||||
function Permissions:union(other)
|
||||
return Permissions(bor(self._value, other._value))
|
||||
end
|
||||
|
||||
--[=[
|
||||
@m intersection
|
||||
@p other Permissions
|
||||
@r Permissions
|
||||
@d Returns a new Permissions object that contains the permissions that are in
|
||||
both `self` and `other` (bitwise AND).
|
||||
]=]
|
||||
function Permissions:intersection(other) -- in both
|
||||
return Permissions(band(self._value, other._value))
|
||||
end
|
||||
|
||||
--[=[
|
||||
@m difference
|
||||
@p other Permissions
|
||||
@r Permissions
|
||||
@d Returns a new Permissions object that contains the permissions that are not
|
||||
in `self` or `other` (bitwise XOR).
|
||||
]=]
|
||||
function Permissions:difference(other) -- not in both
|
||||
return Permissions(bxor(self._value, other._value))
|
||||
end
|
||||
|
||||
--[=[
|
||||
@m complement
|
||||
@p other Permissions
|
||||
@r Permissions
|
||||
@d Returns a new Permissions object that contains the permissions that are not
|
||||
in `self`, but are in `other` (or the set of all permissions if omitted).
|
||||
]=]
|
||||
function Permissions:complement(other) -- in other not in self
|
||||
local value = other and other._value or ALL
|
||||
return Permissions(band(bnot(self._value), value))
|
||||
end
|
||||
|
||||
--[=[
|
||||
@m copy
|
||||
@r Permissions
|
||||
@d Returns a new copy of the original permissions object.
|
||||
]=]
|
||||
function Permissions:copy()
|
||||
return Permissions(self._value)
|
||||
end
|
||||
|
||||
--[=[@p value number The raw decimal value that represents the permissions value.]=]
|
||||
function get.value(self)
|
||||
return self._value
|
||||
end
|
||||
|
||||
return Permissions
|
@ -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 applications
|
||||
|
||||
local err = int_ptr_t()
|
||||
local state = lib.opus_encoder_create(sample_rate, channels, app, err)
|
||||
check(err[0])
|
||||
|
||||
check(lib.opus_encoder_init(state, sample_rate, channels, app))
|
||||
|
||||
return gc(state, lib.opus_encoder_destroy)
|
||||
|
||||
end
|
||||
|
||||
function Encoder:encode(input, input_len, frame_size, max_data_bytes)
|
||||
|
||||
local pcm = new('opus_int16[?]', input_len, input)
|
||||
local data = new('unsigned char[?]', max_data_bytes)
|
||||
|
||||
local ret = lib.opus_encode(self, pcm, frame_size, data, max_data_bytes)
|
||||
|
||||
return data, check(ret)
|
||||
|
||||
end
|
||||
|
||||
function Encoder:get(id)
|
||||
local ret = opus_int32_ptr_t()
|
||||
lib.opus_encoder_ctl(self, id, ret)
|
||||
return check(ret[0])
|
||||
end
|
||||
|
||||
function Encoder:set(id, value)
|
||||
if type(value) ~= 'number' then return throw(opus.BAD_ARG) end
|
||||
local ret = lib.opus_encoder_ctl(self, id, opus_int32_t(value))
|
||||
return check(ret)
|
||||
end
|
||||
|
||||
opus.Encoder = ffi.metatype('OpusEncoder', Encoder)
|
||||
|
||||
local Decoder = {}
|
||||
Decoder.__index = Decoder
|
||||
|
||||
function Decoder:__new(sample_rate, channels) -- luacheck: ignore self
|
||||
|
||||
local err = int_ptr_t()
|
||||
local state = lib.opus_decoder_create(sample_rate, channels, err)
|
||||
check(err[0])
|
||||
|
||||
check(lib.opus_decoder_init(state, sample_rate, channels))
|
||||
|
||||
return gc(state, lib.opus_decoder_destroy)
|
||||
|
||||
end
|
||||
|
||||
function Decoder:decode(data, len, frame_size, output_len)
|
||||
|
||||
local pcm = new('opus_int16[?]', output_len)
|
||||
|
||||
local ret = lib.opus_decode(self, data, len, pcm, frame_size, 0)
|
||||
|
||||
return pcm, check(ret)
|
||||
|
||||
end
|
||||
|
||||
function Decoder:get(id)
|
||||
local ret = opus_int32_ptr_t()
|
||||
lib.opus_decoder_ctl(self, id, ret)
|
||||
return check(ret[0])
|
||||
end
|
||||
|
||||
function Decoder:set(id, value)
|
||||
if type(value) ~= 'number' then return throw(opus.BAD_ARG) end
|
||||
local ret = lib.opus_decoder_ctl(self, id, opus_int32_t(value))
|
||||
return check(ret)
|
||||
end
|
||||
|
||||
opus.Decoder = ffi.metatype('OpusDecoder', Decoder)
|
||||
|
||||
return opus
|
@ -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.9.1',
|
||||
homepage = 'https://github.com/SinisterRectus/Discordia',
|
||||
dependencies = {
|
||||
'creationix/coro-http@3.1.0',
|
||||
'creationix/coro-websocket@3.1.0',
|
||||
'luvit/secure-socket@1.2.2',
|
||||
},
|
||||
tags = {'discord', 'api'},
|
||||
license = 'MIT',
|
||||
author = 'Sinister Rectus',
|
||||
files = {'**.lua'},
|
||||
}
|
@ -0,0 +1,301 @@
|
||||
--[[
|
||||
|
||||
Copyright 2014-2015 The Luvit Authors. All Rights Reserved.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS-IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
--]]
|
||||
|
||||
--[[lit-meta
|
||||
name = "luvit/http-codec"
|
||||
version = "3.0.6"
|
||||
homepage = "https://github.com/luvit/luvit/blob/master/deps/http-codec.lua"
|
||||
description = "A simple pair of functions for converting between hex and raw strings."
|
||||
tags = {"codec", "http"}
|
||||
license = "Apache 2"
|
||||
author = { name = "Tim Caswell" }
|
||||
]]
|
||||
|
||||
local sub = string.sub
|
||||
local gsub = string.gsub
|
||||
local lower = string.lower
|
||||
local find = string.find
|
||||
local format = string.format
|
||||
local concat = table.concat
|
||||
local match = string.match
|
||||
|
||||
local STATUS_CODES = {
|
||||
[100] = 'Continue',
|
||||
[101] = 'Switching Protocols',
|
||||
[102] = 'Processing', -- RFC 2518, obsoleted by RFC 4918
|
||||
[200] = 'OK',
|
||||
[201] = 'Created',
|
||||
[202] = 'Accepted',
|
||||
[203] = 'Non-Authoritative Information',
|
||||
[204] = 'No Content',
|
||||
[205] = 'Reset Content',
|
||||
[206] = 'Partial Content',
|
||||
[207] = 'Multi-Status', -- RFC 4918
|
||||
[300] = 'Multiple Choices',
|
||||
[301] = 'Moved Permanently',
|
||||
[302] = 'Moved Temporarily',
|
||||
[303] = 'See Other',
|
||||
[304] = 'Not Modified',
|
||||
[305] = 'Use Proxy',
|
||||
[307] = 'Temporary Redirect',
|
||||
[400] = 'Bad Request',
|
||||
[401] = 'Unauthorized',
|
||||
[402] = 'Payment Required',
|
||||
[403] = 'Forbidden',
|
||||
[404] = 'Not Found',
|
||||
[405] = 'Method Not Allowed',
|
||||
[406] = 'Not Acceptable',
|
||||
[407] = 'Proxy Authentication Required',
|
||||
[408] = 'Request Time-out',
|
||||
[409] = 'Conflict',
|
||||
[410] = 'Gone',
|
||||
[411] = 'Length Required',
|
||||
[412] = 'Precondition Failed',
|
||||
[413] = 'Request Entity Too Large',
|
||||
[414] = 'Request-URI Too Large',
|
||||
[415] = 'Unsupported Media Type',
|
||||
[416] = 'Requested Range Not Satisfiable',
|
||||
[417] = 'Expectation Failed',
|
||||
[418] = "I'm a teapot", -- RFC 2324
|
||||
[422] = 'Unprocessable Entity', -- RFC 4918
|
||||
[423] = 'Locked', -- RFC 4918
|
||||
[424] = 'Failed Dependency', -- RFC 4918
|
||||
[425] = 'Unordered Collection', -- RFC 4918
|
||||
[426] = 'Upgrade Required', -- RFC 2817
|
||||
[428] = 'Precondition Required', -- RFC 6585
|
||||
[429] = 'Too Many Requests', -- RFC 6585
|
||||
[431] = 'Request Header Fields Too Large', -- RFC 6585
|
||||
[500] = 'Internal Server Error',
|
||||
[501] = 'Not Implemented',
|
||||
[502] = 'Bad Gateway',
|
||||
[503] = 'Service Unavailable',
|
||||
[504] = 'Gateway Time-out',
|
||||
[505] = 'HTTP Version not supported',
|
||||
[506] = 'Variant Also Negotiates', -- RFC 2295
|
||||
[507] = 'Insufficient Storage', -- RFC 4918
|
||||
[509] = 'Bandwidth Limit Exceeded',
|
||||
[510] = 'Not Extended', -- RFC 2774
|
||||
[511] = 'Network Authentication Required' -- RFC 6585
|
||||
}
|
||||
|
||||
local function encoder()
|
||||
|
||||
local mode
|
||||
local encodeHead, encodeRaw, encodeChunked
|
||||
|
||||
function encodeHead(item)
|
||||
if not item or item == "" then
|
||||
return item
|
||||
elseif not (type(item) == "table") then
|
||||
error("expected a table but got a " .. type(item) .. " when encoding data")
|
||||
end
|
||||
local head, chunkedEncoding
|
||||
local version = item.version or 1.1
|
||||
if item.method then
|
||||
local path = item.path
|
||||
assert(path and #path > 0, "expected non-empty path")
|
||||
head = { item.method .. ' ' .. item.path .. ' HTTP/' .. version .. '\r\n' }
|
||||
else
|
||||
local reason = item.reason or STATUS_CODES[item.code]
|
||||
head = { 'HTTP/' .. version .. ' ' .. item.code .. ' ' .. reason .. '\r\n' }
|
||||
end
|
||||
for i = 1, #item do
|
||||
local key, value = unpack(item[i])
|
||||
local lowerKey = lower(key)
|
||||
if lowerKey == "transfer-encoding" then
|
||||
chunkedEncoding = lower(value) == "chunked"
|
||||
end
|
||||
value = gsub(tostring(value), "[\r\n]+", " ")
|
||||
head[#head + 1] = key .. ': ' .. tostring(value) .. '\r\n'
|
||||
end
|
||||
head[#head + 1] = '\r\n'
|
||||
|
||||
mode = chunkedEncoding and encodeChunked or encodeRaw
|
||||
return concat(head)
|
||||
end
|
||||
|
||||
function encodeRaw(item)
|
||||
if type(item) ~= "string" then
|
||||
mode = encodeHead
|
||||
return encodeHead(item)
|
||||
end
|
||||
return item
|
||||
end
|
||||
|
||||
function encodeChunked(item)
|
||||
if type(item) ~= "string" then
|
||||
mode = encodeHead
|
||||
local extra = encodeHead(item)
|
||||
if extra then
|
||||
return "0\r\n\r\n" .. extra
|
||||
else
|
||||
return "0\r\n\r\n"
|
||||
end
|
||||
end
|
||||
if #item == 0 then
|
||||
mode = encodeHead
|
||||
end
|
||||
return format("%x", #item) .. "\r\n" .. item .. "\r\n"
|
||||
end
|
||||
|
||||
mode = encodeHead
|
||||
return function (item)
|
||||
return mode(item)
|
||||
end
|
||||
end
|
||||
|
||||
local function decoder()
|
||||
|
||||
-- This decoder is somewhat stateful with 5 different parsing states.
|
||||
local decodeHead, decodeEmpty, decodeRaw, decodeChunked, decodeCounted
|
||||
local mode -- state variable that points to various decoders
|
||||
local bytesLeft -- For counted decoder
|
||||
|
||||
-- This state is for decoding the status line and headers.
|
||||
function decodeHead(chunk, index)
|
||||
if not chunk or index > #chunk then return end
|
||||
|
||||
local _, last = find(chunk, "\r?\n\r?\n", index)
|
||||
-- First make sure we have all the head before continuing
|
||||
if not last then
|
||||
if (#chunk - index) <= 8 * 1024 then return end
|
||||
-- But protect against evil clients by refusing heads over 8K long.
|
||||
error("entity too large")
|
||||
end
|
||||
|
||||
-- Parse the status/request line
|
||||
local head = {}
|
||||
local _, offset
|
||||
local version
|
||||
_, offset, version, head.code, head.reason =
|
||||
find(chunk, "^HTTP/(%d%.%d) (%d+) ([^\r\n]*)\r?\n", index)
|
||||
if offset then
|
||||
head.code = tonumber(head.code)
|
||||
else
|
||||
_, offset, head.method, head.path, version =
|
||||
find(chunk, "^(%u+) ([^ ]+) HTTP/(%d%.%d)\r?\n", index)
|
||||
if not offset then
|
||||
error("expected HTTP data")
|
||||
end
|
||||
end
|
||||
version = tonumber(version)
|
||||
head.version = version
|
||||
head.keepAlive = version > 1.0
|
||||
|
||||
-- We need to inspect some headers to know how to parse the body.
|
||||
local contentLength
|
||||
local chunkedEncoding
|
||||
|
||||
-- Parse the header lines
|
||||
while true do
|
||||
local key, value
|
||||
_, offset, key, value = find(chunk, "^([^:\r\n]+): *([^\r\n]*)\r?\n", offset + 1)
|
||||
if not offset then break end
|
||||
local lowerKey = lower(key)
|
||||
|
||||
-- Inspect a few headers and remember the values
|
||||
if lowerKey == "content-length" then
|
||||
contentLength = tonumber(value)
|
||||
elseif lowerKey == "transfer-encoding" then
|
||||
chunkedEncoding = lower(value) == "chunked"
|
||||
elseif lowerKey == "connection" then
|
||||
head.keepAlive = lower(value) == "keep-alive"
|
||||
end
|
||||
head[#head + 1] = {key, value}
|
||||
end
|
||||
|
||||
if head.keepAlive and (not (chunkedEncoding or (contentLength and contentLength > 0)))
|
||||
or (head.method == "GET" or head.method == "HEAD") then
|
||||
mode = decodeEmpty
|
||||
elseif chunkedEncoding then
|
||||
mode = decodeChunked
|
||||
elseif contentLength then
|
||||
bytesLeft = contentLength
|
||||
mode = decodeCounted
|
||||
elseif not head.keepAlive then
|
||||
mode = decodeRaw
|
||||
end
|
||||
return head, last + 1
|
||||
|
||||
end
|
||||
|
||||
-- This is used for inserting a single empty string into the output string for known empty bodies
|
||||
function decodeEmpty(chunk, index)
|
||||
mode = decodeHead
|
||||
return "", index
|
||||
end
|
||||
|
||||
function decodeRaw(chunk, index)
|
||||
if #chunk < index then return end
|
||||
return sub(chunk, index)
|
||||
end
|
||||
|
||||
function decodeChunked(chunk, index)
|
||||
local len, term
|
||||
len, term = match(chunk, "^(%x+)(..)", index)
|
||||
if not len then return end
|
||||
if term ~= "\r\n" then
|
||||
-- Wait for full chunk-size\r\n header
|
||||
if #chunk < 18 then return end
|
||||
-- But protect against evil clients by refusing chunk-sizes longer than 16 hex digits.
|
||||
error("chunk-size field too large")
|
||||
end
|
||||
index = index + #len + 2
|
||||
local offset = index - 1
|
||||
local length = tonumber(len, 16)
|
||||
if #chunk < offset + length + 2 then return end
|
||||
if length == 0 then
|
||||
mode = decodeHead
|
||||
end
|
||||
assert(sub(chunk, index + length, index + length + 1) == "\r\n")
|
||||
local piece = sub(chunk, index, index + length - 1)
|
||||
return piece, index + length + 2
|
||||
end
|
||||
|
||||
function decodeCounted(chunk, index)
|
||||
if bytesLeft == 0 then
|
||||
mode = decodeEmpty
|
||||
return mode(chunk, index)
|
||||
end
|
||||
local offset = index - 1
|
||||
local length = #chunk - offset
|
||||
-- Make sure we have at least one byte to process
|
||||
if length == 0 then return end
|
||||
|
||||
-- If there isn't enough data left, emit what we got so far
|
||||
if length < bytesLeft then
|
||||
bytesLeft = bytesLeft - length
|
||||
return sub(chunk, index)
|
||||
end
|
||||
|
||||
mode = decodeEmpty
|
||||
return sub(chunk, index, offset + bytesLeft), index + bytesLeft
|
||||
end
|
||||
|
||||
-- Switch between states by changing which decoder mode points to
|
||||
mode = decodeHead
|
||||
return function (chunk, index)
|
||||
return mode(chunk, index)
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
return {
|
||||
encoder = encoder,
|
||||
decoder = decoder,
|
||||
}
|
@ -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 >= 0x10101000
|
||||
|
||||
if isTLSv1_3 then
|
||||
DEFAULT_SECUREPROTOCOL = 'TLS'
|
||||
else
|
||||
DEFAULT_SECUREPROTOCOL = 'SSLv23'
|
||||
end
|
||||
end
|
||||
local DEFAULT_CIPHERS = 'TLS_AES_128_GCM_SHA256:TLS_AES_128_CCM_SHA256:' .. --TLS 1.3
|
||||
'ECDHE-RSA-AES128-SHA256:AES128-GCM-SHA256:' .. --TLS 1.2
|
||||
'RC4:HIGH:!MD5:!aNULL:!EDH' --TLS 1.0
|
||||
local DEFAULT_CA_STORE
|
||||
do
|
||||
local data = assert(loadResource("./root_ca.dat"))
|
||||
DEFAULT_CA_STORE = openssl.x509.store:new()
|
||||
local index = 1
|
||||
local dataLength = #data
|
||||
while index < dataLength do
|
||||
local len = bit.bor(bit.lshift(data:byte(index), 8), data:byte(index + 1))
|
||||
index = index + 2
|
||||
local cert = assert(openssl.x509.read(data:sub(index, index + len)))
|
||||
index = index + len
|
||||
assert(DEFAULT_CA_STORE:add(cert))
|
||||
end
|
||||
end
|
||||
|
||||
local function returnOne()
|
||||
return 1
|
||||
end
|
||||
|
||||
return function (options)
|
||||
local ctx = openssl.ssl.ctx_new(
|
||||
options.protocol or DEFAULT_SECUREPROTOCOL,
|
||||
options.ciphers or DEFAULT_CIPHERS)
|
||||
|
||||
local key, cert, ca
|
||||
if options.key then
|
||||
key = assert(openssl.pkey.read(options.key, true, 'pem'))
|
||||
end
|
||||
if options.cert then
|
||||
cert = {}
|
||||
for chunk in options.cert:gmatch("%-+BEGIN[^-]+%-+[^-]+%-+END[^-]+%-+") do
|
||||
cert[#cert + 1] = assert(openssl.x509.read(chunk))
|
||||
end
|
||||
end
|
||||
if options.ca then
|
||||
if type(options.ca) == "string" then
|
||||
ca = { assert(openssl.x509.read(options.ca)) }
|
||||
elseif type(options.ca) == "table" then
|
||||
ca = {}
|
||||
for i = 1, #options.ca do
|
||||
ca[i] = assert(openssl.x509.read(options.ca[i]))
|
||||
end
|
||||
else
|
||||
error("options.ca must be string or table of strings")
|
||||
end
|
||||
end
|
||||
if key and cert then
|
||||
local first = table.remove(cert, 1)
|
||||
assert(ctx:use(key, first))
|
||||
if #cert > 0 then
|
||||
-- TODO: find out if there is a way to not need to duplicate the last cert here
|
||||
-- as a dummy fill for the root CA cert
|
||||
assert(ctx:add(cert[#cert], cert))
|
||||
end
|
||||
end
|
||||
if ca then
|
||||
local store = openssl.x509.store:new()
|
||||
for i = 1, #ca do
|
||||
assert(store:add(ca[i]))
|
||||
end
|
||||
ctx:cert_store(store)
|
||||
elseif DEFAULT_CA_STORE then
|
||||
ctx:cert_store(DEFAULT_CA_STORE)
|
||||
end
|
||||
if not (options.insecure or options.key) then
|
||||
ctx:verify_mode(openssl.ssl.peer, returnOne)
|
||||
end
|
||||
|
||||
ctx:options(bit.bor(
|
||||
openssl.ssl.no_sslv2,
|
||||
openssl.ssl.no_sslv3,
|
||||
openssl.ssl.no_compression))
|
||||
|
||||
return ctx
|
||||
end
|
@ -0,0 +1,41 @@
|
||||
--[[
|
||||
|
||||
Copyright 2016 The Luvit Authors. All Rights Reserved.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS-IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
--]]
|
||||
local getContext = require('./context')
|
||||
local bioWrap = require('./biowrap')
|
||||
|
||||
local function assertResume(thread, ...)
|
||||
local success, err = coroutine.resume(thread, ...)
|
||||
if not success then
|
||||
error(debug.traceback(thread, err), 0)
|
||||
end
|
||||
end
|
||||
|
||||
return function (socket, options, callback)
|
||||
if options == true then options = {} end
|
||||
local ctx = getContext(options)
|
||||
local thread
|
||||
if not callback then
|
||||
thread = coroutine.running()
|
||||
end
|
||||
bioWrap(ctx, options.server, socket, callback or function (err, ssocket)
|
||||
return assertResume(thread, ssocket, err)
|
||||
end, options.servername)
|
||||
if not callback then
|
||||
return coroutine.yield()
|
||||
end
|
||||
end
|
@ -0,0 +1,12 @@
|
||||
return {
|
||||
name = "luvit/secure-socket",
|
||||
version = "1.2.3",
|
||||
homepage = "https://github.com/luvit/luvit/blob/master/deps/secure-socket",
|
||||
description = "Wrapper for luv streams to apply ssl/tls",
|
||||
dependencies = {
|
||||
"luvit/resource@2.1.0"
|
||||
},
|
||||
tags = {"ssl", "socket","tls"},
|
||||
license = "Apache 2",
|
||||
author = { name = "Tim Caswell" }
|
||||
}
|
Binary file not shown.
@ -0,0 +1,194 @@
|
||||
--[[lit-meta
|
||||
name = "creationix/sha1"
|
||||
version = "1.0.4"
|
||||
homepage = "https://github.com/luvit/lit/blob/master/deps/sha1.lua"
|
||||
description = "Pure Lua implementation of SHA1 using bitop"
|
||||
authors = {
|
||||
"Tim Caswell"
|
||||
}
|
||||
]]
|
||||
|
||||
-- http://csrc.nist.gov/groups/ST/toolkit/documents/Examples/SHA_All.pdf
|
||||
|
||||
local bit = require('bit')
|
||||
local band = bit.band
|
||||
local bor = bit.bor
|
||||
local bxor = bit.bxor
|
||||
local lshift = bit.lshift
|
||||
local rshift = bit.rshift
|
||||
local rol = bit.rol
|
||||
local tobit = bit.tobit
|
||||
local tohex = bit.tohex
|
||||
|
||||
local byte = string.byte
|
||||
local concat = table.concat
|
||||
local floor = table.floor
|
||||
|
||||
local hasFFi, ffi = pcall(require, "ffi")
|
||||
local newBlock = hasFFi and function ()
|
||||
return ffi.new("uint32_t[80]")
|
||||
end or function ()
|
||||
local t = {}
|
||||
for i = 0, 79 do
|
||||
t[i] = 0
|
||||
end
|
||||
return t
|
||||
end
|
||||
|
||||
local shared = newBlock()
|
||||
|
||||
local function unsigned(n)
|
||||
return n < 0 and (n + 0x100000000) or n
|
||||
end
|
||||
|
||||
local function create(sync)
|
||||
local h0 = 0x67452301
|
||||
local h1 = 0xEFCDAB89
|
||||
local h2 = 0x98BADCFE
|
||||
local h3 = 0x10325476
|
||||
local h4 = 0xC3D2E1F0
|
||||
-- The first 64 bytes (16 words) is the data chunk
|
||||
local W = sync and shared or newBlock()
|
||||
local offset = 0
|
||||
local shift = 24
|
||||
local totalLength = 0
|
||||
|
||||
local update, write, processBlock, digest
|
||||
|
||||
-- The user gave us more data. Store it!
|
||||
function update(chunk)
|
||||
local length = #chunk
|
||||
totalLength = totalLength + length * 8
|
||||
for i = 1, length do
|
||||
write(byte(chunk, i))
|
||||
end
|
||||
end
|
||||
|
||||
function write(data)
|
||||
W[offset] = bor(W[offset], lshift(band(data, 0xff), shift))
|
||||
if shift > 0 then
|
||||
shift = shift - 8
|
||||
else
|
||||
offset = offset + 1
|
||||
shift = 24
|
||||
end
|
||||
if offset == 16 then
|
||||
return processBlock()
|
||||
end
|
||||
end
|
||||
|
||||
-- No more data will come, pad the block, process and return the result.
|
||||
function digest()
|
||||
-- Pad
|
||||
write(0x80)
|
||||
if offset > 14 or (offset == 14 and shift < 24) then
|
||||
processBlock()
|
||||
end
|
||||
offset = 14
|
||||
shift = 24
|
||||
|
||||
-- 64-bit length big-endian
|
||||
write(0x00) -- numbers this big aren't accurate in lua anyway
|
||||
write(0x00) -- ..So just hard-code to zero.
|
||||
write(totalLength > 0xffffffffff and floor(totalLength / 0x10000000000) or 0x00)
|
||||
write(totalLength > 0xffffffff and floor(totalLength / 0x100000000) or 0x00)
|
||||
for s = 24, 0, -8 do
|
||||
write(rshift(totalLength, s))
|
||||
end
|
||||
|
||||
-- At this point one last processBlock() should trigger and we can pull out the result.
|
||||
return concat {
|
||||
tohex(h0),
|
||||
tohex(h1),
|
||||
tohex(h2),
|
||||
tohex(h3),
|
||||
tohex(h4)
|
||||
}
|
||||
end
|
||||
|
||||
-- We have a full block to process. Let's do it!
|
||||
function processBlock()
|
||||
|
||||
-- Extend the sixteen 32-bit words into eighty 32-bit words:
|
||||
for i = 16, 79, 1 do
|
||||
W[i] =
|
||||
rol(bxor(W[i - 3], W[i - 8], W[i - 14], W[i - 16]), 1)
|
||||
end
|
||||
|
||||
-- print("Block Contents:")
|
||||
-- for i = 0, 15 do
|
||||
-- print(string.format(" W[%d] = %s", i, tohex(W[i])))
|
||||
-- end
|
||||
-- print()
|
||||
|
||||
-- Initialize hash value for this chunk:
|
||||
local a = h0
|
||||
local b = h1
|
||||
local c = h2
|
||||
local d = h3
|
||||
local e = h4
|
||||
local f, k
|
||||
|
||||
-- print(" A B C D E")
|
||||
-- local format =
|
||||
-- "t=%02d: %s %s %s %s %s"
|
||||
-- Main loop:
|
||||
for t = 0, 79 do
|
||||
if t < 20 then
|
||||
f = bxor(d, band(b, bxor(c, d)))
|
||||
k = 0x5A827999
|
||||
elseif t < 40 then
|
||||
f = bxor(b, c, d)
|
||||
k = 0x6ED9EBA1
|
||||
elseif t < 60 then
|
||||
f = bor(band(b, c), (band(d, bor(b, c))))
|
||||
k = 0x8F1BBCDC
|
||||
else
|
||||
f = bxor(b, c, d)
|
||||
k = 0xCA62C1D6
|
||||
end
|
||||
e, d, c, b, a =
|
||||
d,
|
||||
c,
|
||||
rol(b, 30),
|
||||
a,
|
||||
tobit(
|
||||
unsigned(rol(a, 5)) +
|
||||
unsigned(f) +
|
||||
unsigned(e) +
|
||||
unsigned(k) +
|
||||
W[t]
|
||||
)
|
||||
-- print(string.format(format, t, tohex(a), tohex(b), tohex(c), tohex(d), tohex(e)))
|
||||
end
|
||||
|
||||
-- Add this chunk's hash to result so far:
|
||||
h0 = tobit(unsigned(h0) + a)
|
||||
h1 = tobit(unsigned(h1) + b)
|
||||
h2 = tobit(unsigned(h2) + c)
|
||||
h3 = tobit(unsigned(h3) + d)
|
||||
h4 = tobit(unsigned(h4) + e)
|
||||
|
||||
-- The block is now reusable.
|
||||
offset = 0
|
||||
for i = 0, 15 do
|
||||
W[i] = 0
|
||||
end
|
||||
end
|
||||
|
||||
return {
|
||||
update = update,
|
||||
digest = digest
|
||||
}
|
||||
|
||||
end
|
||||
|
||||
return function (buffer)
|
||||
-- Pass in false or nil to get a streaming interface.
|
||||
if not buffer then
|
||||
return create(false)
|
||||
end
|
||||
local shasum = create(true)
|
||||
shasum.update(buffer)
|
||||
return shasum.digest()
|
||||
end
|
@ -0,0 +1,22 @@
|
||||
|
||||
local sha1 = require('./init')
|
||||
assert(sha1("") == "da39a3ee5e6b4b0d3255bfef95601890afd80709")
|
||||
assert(sha1("abc") == "a9993e364706816aba3e25717850c26c9cd0d89d")
|
||||
assert(sha1("abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq")
|
||||
== "84983e441c3bd26ebaae4aa1f95129e5e54670f1")
|
||||
assert(sha1("abcdefghbcdefghicdefghijdefghijkefghijklfghijklmghijklmnhijklmnoijklmnopjklmnopqklmnopqrlmnopqrsmnopqrstnopqrstu")
|
||||
== "a49b2446a02c645bf419f995b67091253a04a259")
|
||||
assert(sha1(string.rep("a", 1000000))
|
||||
== "34aa973cd4c4daa4f61eeb2bdbad27316534016f")
|
||||
local sum = sha1()
|
||||
sum.update("a")
|
||||
sum.update("bc")
|
||||
assert(sum.digest() == "a9993e364706816aba3e25717850c26c9cd0d89d")
|
||||
sum = sha1()
|
||||
local aa = string.rep("a", 1000)
|
||||
for i = 1, 1000 do
|
||||
sum.update(aa)
|
||||
end
|
||||
assert(sum.digest() == "34aa973cd4c4daa4f61eeb2bdbad27316534016f")
|
||||
|
||||
print("All tests pass")
|
@ -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,
|
||||
}
|
@ -0,0 +1 @@
|
||||
{"url":"wss://gateway.discord.gg","873255296024322059":{"shards":1,"timestamp":1628272516,"owner":{"avatar":null,"discriminator":"0000","id":"872299874857676820","public_flags":1024,"username":"team872299874857676820","flags":1024}}}
|
Loading…
Reference in New Issue