add Initial Bot files

pull/1/head
Astoria Floyd 3 years ago
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')

114
deps/base64.lua vendored

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

206
deps/coro-http.lua vendored

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

196
deps/coro-net.lua vendored

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

301
deps/http-codec.lua vendored

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

124
deps/pathjoin.lua vendored

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

88
deps/resource.lua vendored

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

194
deps/sha1/init.lua vendored

@ -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…
Cancel
Save