├── .gitignore ├── .gitattributes ├── bin ├── libopus │ ├── libopus.x64.dll │ ├── libopus.x86.dll │ └── LICENSE ├── libsodium │ ├── libsodium.x64.dll │ ├── libsodium.x86.dll │ └── LICENSE └── README.md ├── libs ├── voice │ ├── streams │ │ ├── PCMGenerator.lua │ │ ├── PCMStream.lua │ │ ├── PCMString.lua │ │ └── FFmpegProcess.lua │ ├── VoiceManager.lua │ ├── VoiceSocket.lua │ └── opus.lua ├── constants.lua ├── iterables │ ├── FilteredIterable.lua │ ├── WeakCache.lua │ ├── TableIterable.lua │ ├── SecondaryCache.lua │ ├── ArrayIterable.lua │ ├── Cache.lua │ └── Iterable.lua ├── containers │ ├── Relationship.lua │ ├── PrivateChannel.lua │ ├── Ban.lua │ ├── abstract │ │ ├── Snowflake.lua │ │ ├── Channel.lua │ │ ├── Container.lua │ │ ├── UserPresence.lua │ │ └── GuildChannel.lua │ ├── GuildCategoryChannel.lua │ ├── Sticker.lua │ ├── GroupChannel.lua │ ├── GuildVoiceChannel.lua │ ├── Webhook.lua │ ├── Emoji.lua │ ├── Reaction.lua │ ├── Activity.lua │ ├── GuildTextChannel.lua │ ├── User.lua │ ├── AuditLogEntry.lua │ ├── Invite.lua │ └── PermissionOverwrite.lua ├── utils │ ├── Clock.lua │ ├── Mutex.lua │ ├── Logger.lua │ ├── Deque.lua │ ├── Stopwatch.lua │ ├── Emitter.lua │ ├── Permissions.lua │ ├── Time.lua │ └── Color.lua ├── endpoints.lua ├── client │ ├── WebSocket.lua │ ├── Resolver.lua │ └── Shard.lua ├── class.lua └── extensions.lua ├── init.lua ├── examples ├── pingPong.lua ├── basicCommands.lua ├── appender.lua ├── embed.lua └── helpCommandExample.lua ├── LICENSE ├── package.lua ├── README.md └── docgen.lua /.gitignore: -------------------------------------------------------------------------------- 1 | docs 2 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.lua eol=lf 2 | -------------------------------------------------------------------------------- /bin/libopus/libopus.x64.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SinisterRectus/Discordia/HEAD/bin/libopus/libopus.x64.dll -------------------------------------------------------------------------------- /bin/libopus/libopus.x86.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SinisterRectus/Discordia/HEAD/bin/libopus/libopus.x86.dll -------------------------------------------------------------------------------- /bin/libsodium/libsodium.x64.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SinisterRectus/Discordia/HEAD/bin/libsodium/libsodium.x64.dll -------------------------------------------------------------------------------- /bin/libsodium/libsodium.x86.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SinisterRectus/Discordia/HEAD/bin/libsodium/libsodium.x86.dll -------------------------------------------------------------------------------- /libs/voice/streams/PCMGenerator.lua: -------------------------------------------------------------------------------- 1 | local PCMGenerator = require('class')('PCMGenerator') 2 | 3 | function PCMGenerator:__init(fn) 4 | self._fn = fn 5 | end 6 | 7 | function PCMGenerator:read(n) 8 | local pcm = {} 9 | local fn = self._fn 10 | for i = 1, n, 2 do 11 | local left, right = fn() 12 | pcm[i] = tonumber(left) or 0 13 | pcm[i + 1] = tonumber(right) or pcm[i] 14 | end 15 | return pcm 16 | end 17 | 18 | return PCMGenerator 19 | -------------------------------------------------------------------------------- /libs/constants.lua: -------------------------------------------------------------------------------- 1 | return { 2 | CACHE_AGE = 3600, -- seconds 3 | ID_DELAY = 5000, -- milliseconds 4 | GATEWAY_DELAY = 500, -- milliseconds, 5 | DISCORD_EPOCH = 1420070400000, -- milliseconds 6 | API_VERSION = 8, 7 | DEFAULT_AVATARS = 5, 8 | ZWSP = '\226\128\139', 9 | NS_PER_US = 1000, 10 | US_PER_MS = 1000, 11 | MS_PER_S = 1000, 12 | S_PER_MIN = 60, 13 | MIN_PER_HOUR = 60, 14 | HOUR_PER_DAY = 24, 15 | DAY_PER_WEEK = 7, 16 | GATEWAY_VERSION_VOICE = 8, 17 | } 18 | -------------------------------------------------------------------------------- /init.lua: -------------------------------------------------------------------------------- 1 | return { 2 | class = require('class'), 3 | enums = require('enums'), 4 | extensions = require('extensions'), 5 | package = require('./package.lua'), 6 | Client = require('client/Client'), 7 | Clock = require('utils/Clock'), 8 | Color = require('utils/Color'), 9 | Date = require('utils/Date'), 10 | Deque = require('utils/Deque'), 11 | Emitter = require('utils/Emitter'), 12 | Logger = require('utils/Logger'), 13 | Mutex = require('utils/Mutex'), 14 | Permissions = require('utils/Permissions'), 15 | Stopwatch = require('utils/Stopwatch'), 16 | Time = require('utils/Time'), 17 | storage = {}, 18 | } 19 | -------------------------------------------------------------------------------- /libs/voice/streams/PCMStream.lua: -------------------------------------------------------------------------------- 1 | local remove = table.remove 2 | local unpack = string.unpack -- luacheck: ignore 3 | local rep = string.rep 4 | 5 | local fmt = setmetatable({}, { 6 | __index = function(self, n) 7 | self[n] = '<' .. rep('i2', n) 8 | return self[n] 9 | end 10 | }) 11 | 12 | local PCMStream = require('class')('PCMStream') 13 | 14 | function PCMStream:__init(stream) 15 | self._stream = stream 16 | end 17 | 18 | function PCMStream:read(n) 19 | local m = n * 2 20 | local str = self._stream:read(m) 21 | if str and #str == m then 22 | local pcm = {unpack(fmt[n], str)} 23 | remove(pcm) 24 | return pcm 25 | end 26 | end 27 | 28 | return PCMStream 29 | -------------------------------------------------------------------------------- /libs/voice/streams/PCMString.lua: -------------------------------------------------------------------------------- 1 | local remove = table.remove 2 | local unpack = string.unpack -- luacheck: ignore 3 | local rep = string.rep 4 | 5 | local fmt = setmetatable({}, { 6 | __index = function(self, n) 7 | self[n] = '<' .. rep('i2', n) 8 | return self[n] 9 | end 10 | }) 11 | 12 | local PCMString = require('class')('PCMString') 13 | 14 | function PCMString:__init(str) 15 | self._len = #str 16 | self._str = str 17 | end 18 | 19 | function PCMString:read(n) 20 | local i = self._i or 1 21 | if i + n * 2 < self._len then 22 | local pcm = {unpack(fmt[n], self._str, i)} 23 | self._i = remove(pcm) 24 | return pcm 25 | end 26 | end 27 | 28 | return PCMString 29 | -------------------------------------------------------------------------------- /libs/iterables/FilteredIterable.lua: -------------------------------------------------------------------------------- 1 | --[=[ 2 | @c FilteredIterable x Iterable 3 | @mt mem 4 | @d Iterable class that wraps another iterable and serves a subset of the objects 5 | that the original iterable contains. 6 | ]=] 7 | 8 | local Iterable = require('iterables/Iterable') 9 | 10 | local FilteredIterable = require('class')('FilteredIterable', Iterable) 11 | 12 | function FilteredIterable:__init(base, predicate) 13 | self._base = base 14 | self._predicate = predicate 15 | end 16 | 17 | --[=[ 18 | @m iter 19 | @r function 20 | @d Returns an iterator that returns all contained objects. The order of the objects 21 | is not guaranteed. 22 | ]=] 23 | function FilteredIterable:iter() 24 | return self._base:findAll(self._predicate) 25 | end 26 | 27 | return FilteredIterable 28 | -------------------------------------------------------------------------------- /examples/pingPong.lua: -------------------------------------------------------------------------------- 1 | local discordia = require("discordia") 2 | local client = discordia.Client() 3 | 4 | -- enables receiving message.content so it is not empty 5 | -- make sure you also enable it in Developer Portal 6 | -- see https://github.com/SinisterRectus/Discordia/discussions/369 7 | client:enableIntents(discordia.enums.gatewayIntent.messageContent) 8 | 9 | client:on("ready", function() -- bot is ready 10 | print("Logged in as " .. client.user.username) 11 | end) 12 | 13 | client:on("messageCreate", function(message) 14 | 15 | local content = message.content 16 | 17 | if content == "!ping" then 18 | message:reply("Pong!") 19 | elseif content == "!pong" then 20 | message:reply("Ping!") 21 | end 22 | 23 | end) 24 | 25 | client:run("Bot BOT_TOKEN") -- replace BOT_TOKEN with your bot token 26 | -------------------------------------------------------------------------------- /libs/iterables/WeakCache.lua: -------------------------------------------------------------------------------- 1 | --[=[ 2 | @c WeakCache x Cache 3 | @mt mem 4 | @d Extends the functionality of a regular cache by making use of weak references 5 | to the objects that are cached. If all references to an object are weak, as they 6 | are here, then the object will be deleted on the next garbage collection cycle. 7 | ]=] 8 | 9 | local Cache = require('iterables/Cache') 10 | local Iterable = require('iterables/Iterable') 11 | 12 | local WeakCache = require('class')('WeakCache', Cache) 13 | 14 | local meta = {__mode = 'v'} 15 | 16 | function WeakCache:__init(array, constructor, parent) 17 | Cache.__init(self, array, constructor, parent) 18 | setmetatable(self._objects, meta) 19 | end 20 | 21 | function WeakCache:__len() -- NOTE: _count is not accurate for weak caches 22 | return Iterable.__len(self) 23 | end 24 | 25 | return WeakCache 26 | -------------------------------------------------------------------------------- /bin/libsodium/LICENSE: -------------------------------------------------------------------------------- 1 | /* 2 | * ISC License 3 | * 4 | * Copyright (c) 2013-2024 5 | * Frank Denis 6 | * 7 | * Permission to use, copy, modify, and/or distribute this software for any 8 | * purpose with or without fee is hereby granted, provided that the above 9 | * copyright notice and this permission notice appear in all copies. 10 | * 11 | * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 12 | * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 13 | * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 14 | * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 15 | * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 16 | * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 17 | * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 18 | */ -------------------------------------------------------------------------------- /libs/containers/Relationship.lua: -------------------------------------------------------------------------------- 1 | --[=[ 2 | @c Relationship x UserPresence 3 | @d Represents a relationship between the current user and another Discord user. 4 | This is generally either a friend or a blocked user. This class should only be 5 | relevant to user-accounts; bots cannot normally have relationships. 6 | ]=] 7 | 8 | local UserPresence = require('containers/abstract/UserPresence') 9 | 10 | local Relationship, get = require('class')('Relationship', UserPresence) 11 | 12 | function Relationship:__init(data, parent) 13 | UserPresence.__init(self, data, parent) 14 | end 15 | 16 | --[=[@p name string Equivalent to `Relationship.user.username`.]=] 17 | function get.name(self) 18 | return self._user._username 19 | end 20 | 21 | --[=[@p type number The relationship type. See the `relationshipType` enumeration for a 22 | human-readable representation.]=] 23 | function get.type(self) 24 | return self._type 25 | end 26 | 27 | return Relationship 28 | -------------------------------------------------------------------------------- /examples/basicCommands.lua: -------------------------------------------------------------------------------- 1 | local discordia = require("discordia") 2 | local client = discordia.Client() 3 | 4 | discordia.extensions() -- load all helpful extensions 5 | 6 | -- enables receiving message.content so it is not empty 7 | -- make sure you also enable it in Developer Portal 8 | -- see https://github.com/SinisterRectus/Discordia/discussions/369 9 | client:enableIntents(discordia.enums.gatewayIntent.messageContent) 10 | 11 | client:on("ready", function() -- bot is ready 12 | print("Logged in as " .. client.user.username) 13 | end) 14 | 15 | client:on("messageCreate", function(message) 16 | 17 | local content = message.content 18 | local args = content:split(" ") -- split all arguments into a table 19 | 20 | if args[1] == "!ping" then 21 | message:reply("Pong!") 22 | elseif args[1] == "!echo" then 23 | table.remove(args, 1) -- remove the first argument (!echo) from the table 24 | message:reply(table.concat(args, " ")) -- concatenate the arguments into a string, then reply with it 25 | end 26 | 27 | end) 28 | 29 | 30 | client:run("Bot BOT_TOKEN") -- replace BOT_TOKEN with your bot token 31 | -------------------------------------------------------------------------------- /libs/voice/VoiceManager.lua: -------------------------------------------------------------------------------- 1 | local VoiceSocket = require('voice/VoiceSocket') 2 | local Emitter = require('utils/Emitter') 3 | 4 | local opus = require('voice/opus') or {} 5 | local sodium = require('voice/sodium') or {} 6 | local constants = require('constants') 7 | 8 | local wrap = coroutine.wrap 9 | local format = string.format 10 | 11 | local GATEWAY_VERSION_VOICE = constants.GATEWAY_VERSION_VOICE 12 | 13 | local VoiceManager = require('class')('VoiceManager', Emitter) 14 | 15 | function VoiceManager:__init(client) 16 | Emitter.__init(self) 17 | self._client = client 18 | end 19 | 20 | function VoiceManager:_prepareConnection(state, connection) 21 | if not next(opus) then 22 | return self._client:error('Cannot prepare voice connection: libopus not found') 23 | end 24 | if not next(sodium) then 25 | return self._client:error('Cannot prepare voice connection: libsodium not found') 26 | end 27 | local socket = VoiceSocket(state, connection, self) 28 | local url = 'wss://' .. state.endpoint 29 | local path = format('/?v=%i', GATEWAY_VERSION_VOICE) 30 | return wrap(socket.connect)(socket, url, path) 31 | end 32 | 33 | return VoiceManager 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016-2025 SinisterRectus 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /libs/containers/PrivateChannel.lua: -------------------------------------------------------------------------------- 1 | --[=[ 2 | @c PrivateChannel x TextChannel 3 | @d Represents a private Discord text channel used to track correspondences between 4 | the current user and one other recipient. 5 | ]=] 6 | 7 | local TextChannel = require('containers/abstract/TextChannel') 8 | 9 | local PrivateChannel, get = require('class')('PrivateChannel', TextChannel) 10 | 11 | function PrivateChannel:__init(data, parent) 12 | TextChannel.__init(self, data, parent) 13 | self._recipient = self.client._users:_insert(data.recipients[1]) 14 | end 15 | 16 | --[=[ 17 | @m close 18 | @t http 19 | @r boolean 20 | @d Closes the channel. This does not delete the channel. To re-open the channel, 21 | use `User:getPrivateChannel`. 22 | ]=] 23 | function PrivateChannel:close() 24 | return self:_delete() 25 | end 26 | 27 | --[=[@p name string Equivalent to `PrivateChannel.recipient.username`.]=] 28 | function get.name(self) 29 | return self._recipient._username 30 | end 31 | 32 | --[=[@p recipient User The recipient of this channel's messages, other than the current user.]=] 33 | function get.recipient(self) 34 | return self._recipient 35 | end 36 | 37 | return PrivateChannel 38 | -------------------------------------------------------------------------------- /libs/iterables/TableIterable.lua: -------------------------------------------------------------------------------- 1 | --[=[ 2 | @c TableIterable x Iterable 3 | @mt mem 4 | @d Iterable class that wraps a basic Lua table, where order is not guaranteed. 5 | Some versions may use a map function to shape the objects before they are accessed. 6 | ]=] 7 | 8 | local Iterable = require('iterables/Iterable') 9 | 10 | local TableIterable = require('class')('TableIterable', Iterable) 11 | 12 | function TableIterable:__init(tbl, map) 13 | self._tbl = tbl 14 | self._map = map 15 | end 16 | 17 | --[=[ 18 | @m iter 19 | @r function 20 | @d Returns an iterator that returns all contained objects. The order of the objects is not guaranteed. 21 | ]=] 22 | function TableIterable:iter() 23 | local tbl = self._tbl 24 | if not tbl then 25 | return function() 26 | return nil 27 | end 28 | end 29 | local map = self._map 30 | if map then 31 | local k, v 32 | return function() 33 | while true do 34 | k, v = next(tbl, k) 35 | if not v then 36 | return nil 37 | end 38 | v = map(v) 39 | if v then 40 | return v 41 | end 42 | end 43 | end 44 | else 45 | local k, v 46 | return function() 47 | k, v = next(tbl, k) 48 | return v 49 | end 50 | end 51 | end 52 | 53 | return TableIterable 54 | -------------------------------------------------------------------------------- /examples/appender.lua: -------------------------------------------------------------------------------- 1 | local discordia = require("discordia") 2 | local client = discordia.Client() 3 | 4 | -- enables receiving message.content so it is not empty 5 | -- make sure you also enable it in Developer Portal 6 | -- see https://github.com/SinisterRectus/Discordia/discussions/369 7 | client:enableIntents(discordia.enums.gatewayIntent.messageContent) 8 | 9 | local lines = {} -- blank table of messages 10 | 11 | client:on("ready", function() -- bot is ready 12 | print("Logged in as " .. client.user.username) 13 | end) 14 | 15 | client:on("messageCreate", function(message) 16 | 17 | local content = message.content 18 | local author = message.author 19 | 20 | if author == client.user then return end -- the bot should not append its own messages 21 | 22 | if content == "!lines" then -- if the lines command is activated 23 | message.channel:send { 24 | file = {"lines.txt", table.concat(lines, "\n")} -- concatenate and send the collected lines in a file 25 | } 26 | lines = {} -- empty the lines table 27 | else -- if the lines command is NOT activated 28 | table.insert(lines, content) -- append the message as a new line 29 | end 30 | 31 | end) 32 | 33 | client:run("Bot BOT_TOKEN") -- replace BOT_TOKEN with your bot token 34 | -------------------------------------------------------------------------------- /libs/containers/Ban.lua: -------------------------------------------------------------------------------- 1 | --[=[ 2 | @c Ban x Container 3 | @d Represents a Discord guild ban. Essentially a combination of the banned user and 4 | a reason explaining the ban, if one was provided. 5 | ]=] 6 | 7 | local Container = require('containers/abstract/Container') 8 | 9 | local Ban, get = require('class')('Ban', Container) 10 | 11 | function Ban:__init(data, parent) 12 | Container.__init(self, data, parent) 13 | self._user = self.client._users:_insert(data.user) 14 | end 15 | 16 | --[=[ 17 | @m __hash 18 | @r string 19 | @d Returns `Ban.user.id` 20 | ]=] 21 | function Ban:__hash() 22 | return self._user._id 23 | end 24 | 25 | --[=[ 26 | @m delete 27 | @t http 28 | @r boolean 29 | @d Deletes the ban object, unbanning the corresponding user. 30 | Equivalent to `Ban.guild:unbanUser(Ban.user)`. 31 | ]=] 32 | function Ban:delete() 33 | return self._parent:unbanUser(self._user) 34 | end 35 | 36 | --[=[@p reason string/nil The reason for the ban, if one was set. This should be from 1 to 512 characters 37 | in length.]=] 38 | function get.reason(self) 39 | return self._reason 40 | end 41 | 42 | --[=[@p guild Guild The guild in which this ban object exists.]=] 43 | function get.guild(self) 44 | return self._parent 45 | end 46 | 47 | --[=[@p user User The user that this ban object represents.]=] 48 | function get.user(self) 49 | return self._user 50 | end 51 | 52 | return Ban 53 | -------------------------------------------------------------------------------- /examples/embed.lua: -------------------------------------------------------------------------------- 1 | local discordia = require("discordia") 2 | local client = discordia.Client() 3 | 4 | -- enables receiving message.content so it is not empty 5 | -- make sure you also enable it in Developer Portal 6 | -- see https://github.com/SinisterRectus/Discordia/discussions/369 7 | client:enableIntents(discordia.enums.gatewayIntent.messageContent) 8 | 9 | client:on("ready", function() -- bot is ready 10 | print("Logged in as " .. client.user.username) 11 | end) 12 | 13 | client:on("messageCreate", function(message) 14 | 15 | local content = message.content 16 | local author = message.author 17 | 18 | if content == "!embed" then 19 | message:reply { 20 | embed = { 21 | title = "Embed Title", 22 | description = "Here is my fancy description!", 23 | author = { 24 | name = author.username, 25 | icon_url = author.avatarURL 26 | }, 27 | fields = { -- array of fields 28 | { 29 | name = "Field 1", 30 | value = "This is some information", 31 | inline = true 32 | }, 33 | { 34 | name = "Field 2", 35 | value = "This is some more information", 36 | inline = false 37 | } 38 | }, 39 | footer = { 40 | text = "Created with Discordia" 41 | }, 42 | color = 0x000000 -- hex color code 43 | } 44 | } 45 | end 46 | 47 | end) 48 | 49 | client:run("Bot BOT_TOKEN") -- replace BOT_TOKEN with your bot token 50 | -------------------------------------------------------------------------------- /bin/README.md: -------------------------------------------------------------------------------- 1 | Discordia loads dynamic libraries using LuaJIT's `ffi.load` function. On Windows, you must rename your libopus file to `opus.dll` and your libsodium file to `sodium.dll`. They must both be placed in a proper directory. Use your main application directory if you are unsure of which to use. Also be sure to use the appropriate file for your architecture. This can be checked at `jit.arch`. 2 | 3 | From http://luajit.org/ext_ffi_api.html: 4 | 5 | `clib = ffi.load(name [,global])` 6 | 7 | This loads the dynamic library given by name and returns a new C library namespace which binds to its symbols. On POSIX systems, if global is true, the library symbols are loaded into the global namespace, too. 8 | 9 | If name is a path, the library is loaded from this path. Otherwise name is canonicalized in a system-dependent way and searched in the default search path for dynamic libraries: 10 | 11 | On POSIX systems, if the name contains no dot, the extension .so is appended. Also, the lib prefix is prepended if necessary. So ffi.load("z") looks for "libz.so" in the default shared library search path. 12 | 13 | On Windows systems, if the name contains no dot, the extension .dll is appended. So ffi.load("ws2_32") looks for "ws2_32.dll" in the default DLL search path. 14 | 15 | From http://luajit.org/ext_jit.html: 16 | 17 | `jit.arch` 18 | 19 | Contains the target architecture name: "x86", "x64", "arm", "ppc", "ppcspe", or "mips". 20 | -------------------------------------------------------------------------------- /libs/utils/Clock.lua: -------------------------------------------------------------------------------- 1 | --[=[ 2 | @c Clock x Emitter 3 | @t ui 4 | @mt mem 5 | @d Used to periodically execute code according to the ticking of the system clock instead of an arbitrary interval. 6 | ]=] 7 | 8 | local timer = require('timer') 9 | local Emitter = require('utils/Emitter') 10 | 11 | local date = os.date 12 | local setInterval, clearInterval = timer.setInterval, timer.clearInterval 13 | 14 | local Clock = require('class')('Clock', Emitter) 15 | 16 | function Clock:__init() 17 | Emitter.__init(self) 18 | end 19 | 20 | --[=[ 21 | @m start 22 | @op utc boolean 23 | @r nil 24 | @d Starts the main loop for the clock. If a truthy argument is passed, then UTC 25 | time is used; otherwise, local time is used. As the clock ticks, an event is 26 | emitted for every `os.date` value change. The event name is the key of the value 27 | that changed and the event argument is the corresponding date table. 28 | ]=] 29 | function Clock:start(utc) 30 | if self._interval then return end 31 | local fmt = utc and '!*t' or '*t' 32 | local prev = date(fmt) 33 | self._interval = setInterval(1000, function() 34 | local now = date(fmt) 35 | for k, v in pairs(now) do ---@diagnostic disable-line: param-type-mismatch 36 | if v ~= prev[k] then 37 | self:emit(k, now) 38 | end 39 | end 40 | prev = now 41 | end) 42 | end 43 | 44 | --[=[ 45 | @m stop 46 | @r nil 47 | @d Stops the main loop for the clock. 48 | ]=] 49 | function Clock:stop() 50 | if self._interval then 51 | clearInterval(self._interval) 52 | self._interval = nil 53 | end 54 | end 55 | 56 | return Clock 57 | -------------------------------------------------------------------------------- /package.lua: -------------------------------------------------------------------------------- 1 | --[[The MIT License (MIT) 2 | 3 | Copyright (c) 2016-2025 SinisterRectus 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE.]] 22 | 23 | return { 24 | name = 'SinisterRectus/discordia', 25 | version = '2.13.1', 26 | homepage = 'https://github.com/SinisterRectus/Discordia', 27 | dependencies = { 28 | 'luvit/coro-http@3.2.4', 29 | 'luvit/coro-websocket@3.1.1', 30 | 'luvit/secure-socket@1.2.4', 31 | }, 32 | tags = {'discord', 'api'}, 33 | license = 'MIT', 34 | author = 'Sinister Rectus', 35 | files = {'**.lua'}, 36 | } 37 | -------------------------------------------------------------------------------- /examples/helpCommandExample.lua: -------------------------------------------------------------------------------- 1 | local discordia = require('discordia') 2 | local client = discordia.Client() 3 | discordia.extensions() -- load all helpful extensions 4 | 5 | -- enables receiving message.content so it is not empty 6 | -- make sure you also enable it in Developer Portal 7 | -- see https://github.com/SinisterRectus/Discordia/discussions/369 8 | client:enableIntents(discordia.enums.gatewayIntent.messageContent) 9 | 10 | local prefix = "." 11 | local commands = { 12 | [prefix .. "ping"] = { 13 | description = "Answers with pong.", 14 | exec = function(message) 15 | message.channel:send("Pong!") 16 | end 17 | }, 18 | [prefix .. "hello"] = { 19 | description = "Answers with world.", 20 | exec = function(message) 21 | message.channel:send("world!") 22 | end 23 | } 24 | } 25 | 26 | client:on('ready', function() 27 | print(string.format('Logged in as %s', client.user.username)) 28 | end) 29 | 30 | client:on("messageCreate", function(message) 31 | local args = message.content:split(" ") -- split all arguments into a table 32 | 33 | local command = commands[args[1]] 34 | if command then -- ping or hello 35 | command.exec(message) -- execute the command 36 | end 37 | 38 | if args[1] == prefix.."help" then -- display all the commands 39 | local output = {} 40 | for word, tbl in pairs(commands) do 41 | table.insert(output, string.format("Command: %s\nDescription: %s", word, tbl.description)) 42 | end 43 | 44 | message:reply(table.concat(output, "\n\n")) 45 | end 46 | end) 47 | 48 | 49 | client:run("Bot BOT_TOKEN") -- replace BOT_TOKEN with your bot token 50 | -------------------------------------------------------------------------------- /libs/utils/Mutex.lua: -------------------------------------------------------------------------------- 1 | --[=[ 2 | @c Mutex 3 | @t ui 4 | @mt mem 5 | @d Mutual exclusion class used to control Lua coroutine execution order. 6 | ]=] 7 | 8 | local Deque = require('utils/Deque') 9 | local timer = require('timer') 10 | 11 | local yield = coroutine.yield 12 | local resume = coroutine.resume 13 | local running = coroutine.running 14 | local setTimeout = timer.setTimeout 15 | 16 | local Mutex = require('class')('Mutex', Deque) 17 | 18 | function Mutex:__init() 19 | Deque.__init(self) 20 | self._active = false 21 | end 22 | 23 | --[=[ 24 | @m lock 25 | @op prepend boolean 26 | @r nil 27 | @d If the mutex is not active (if a coroutine is not queued), this will activate 28 | the mutex; otherwise, this will yield and queue the current coroutine. 29 | ]=] 30 | function Mutex:lock(prepend) 31 | if self._active then 32 | if prepend then 33 | return yield(self:pushLeft(running())) 34 | else 35 | return yield(self:pushRight(running())) 36 | end 37 | else 38 | self._active = true 39 | end 40 | end 41 | 42 | --[=[ 43 | @m unlock 44 | @r nil 45 | @d If the mutex is active (if a coroutine is queued), this will dequeue and 46 | resume the next available coroutine; otherwise, this will deactivate the mutex. 47 | ]=] 48 | function Mutex:unlock() 49 | if self:getCount() > 0 then 50 | return assert(resume(self:popLeft())) 51 | else 52 | self._active = false 53 | end 54 | end 55 | 56 | --[=[ 57 | @m unlockAfter 58 | @p delay number 59 | @r uv_timer 60 | @d Asynchronously unlocks the mutex after a specified time in milliseconds. 61 | The relevant `uv_timer` object is returned. 62 | ]=] 63 | local unlock = Mutex.unlock 64 | function Mutex:unlockAfter(delay) 65 | return setTimeout(delay, unlock, self) 66 | end 67 | 68 | return Mutex 69 | -------------------------------------------------------------------------------- /libs/containers/abstract/Snowflake.lua: -------------------------------------------------------------------------------- 1 | --[=[ 2 | @c Snowflake x Container 3 | @t abc 4 | @d Defines the base methods and/or properties for all Discord objects that have 5 | a Snowflake ID. 6 | ]=] 7 | 8 | local Date = require('utils/Date') 9 | local Container = require('containers/abstract/Container') 10 | 11 | local Snowflake, get = require('class')('Snowflake', Container) 12 | 13 | function Snowflake:__init(data, parent) 14 | Container.__init(self, data, parent) 15 | end 16 | 17 | --[=[ 18 | @m __hash 19 | @r string 20 | @d Returns `Snowflake.id` 21 | ]=] 22 | function Snowflake:__hash() 23 | return self._id 24 | end 25 | 26 | --[=[ 27 | @m getDate 28 | @t mem 29 | @r Date 30 | @d Returns a unique Date object that represents when the object was created by Discord. 31 | 32 | Equivalent to `Date.fromSnowflake(Snowflake.id)` 33 | ]=] 34 | function Snowflake:getDate() 35 | return Date.fromSnowflake(self._id) 36 | end 37 | 38 | --[=[@p id string The Snowflake ID that can be used to identify the object. This is guaranteed to 39 | be unique except in cases where an object shares the ID of its parent.]=] 40 | function get.id(self) 41 | return self._id 42 | end 43 | 44 | --[=[@p createdAt number The Unix time in seconds at which this object was created by Discord. Additional 45 | decimal points may be present, though only the first 3 (milliseconds) should be 46 | considered accurate. 47 | 48 | Equivalent to `Date.parseSnowflake(Snowflake.id)`. 49 | ]=] 50 | function get.createdAt(self) 51 | return Date.parseSnowflake(self._id) 52 | end 53 | 54 | --[=[@p timestamp string The date and time at which this object was created by Discord, represented as 55 | an ISO 8601 string plus microseconds when available. 56 | 57 | Equivalent to `Date.fromSnowflake(Snowflake.id):toISO()`. 58 | ]=] 59 | function get.timestamp(self) 60 | return Date.fromSnowflake(self._id):toISO() 61 | end 62 | 63 | return Snowflake 64 | -------------------------------------------------------------------------------- /libs/containers/abstract/Channel.lua: -------------------------------------------------------------------------------- 1 | --[=[ 2 | @c Channel x Snowflake 3 | @t abc 4 | @d Defines the base methods and properties for all Discord channel types. 5 | ]=] 6 | 7 | local Snowflake = require('containers/abstract/Snowflake') 8 | local enums = require('enums') 9 | 10 | local format = string.format 11 | local channelType = assert(enums.channelType) 12 | 13 | local Channel, get = require('class')('Channel', Snowflake) 14 | 15 | function Channel:__init(data, parent) 16 | Snowflake.__init(self, data, parent) 17 | end 18 | 19 | function Channel:_modify(payload) 20 | local data, err = self.client._api:modifyChannel(self._id, payload) 21 | if data then 22 | self:_load(data) 23 | return true 24 | else 25 | return false, err 26 | end 27 | end 28 | 29 | function Channel:_delete() 30 | local data, err = self.client._api:deleteChannel(self._id) 31 | if data then 32 | local cache 33 | local t = self._type 34 | if t == channelType.text or t == channelType.news then 35 | cache = self._parent._text_channels 36 | elseif t == channelType.private then 37 | cache = self._parent._private_channels 38 | elseif t == channelType.group then 39 | cache = self._parent._group_channels 40 | elseif t == channelType.voice then 41 | cache = self._parent._voice_channels 42 | elseif t == channelType.category then 43 | cache = self._parent._categories 44 | end 45 | if cache then 46 | cache:_delete(self._id) 47 | end 48 | return true 49 | else 50 | return false, err 51 | end 52 | end 53 | 54 | --[=[@p type number The channel type. See the `channelType` enumeration for a 55 | human-readable representation.]=] 56 | function get.type(self) 57 | return self._type 58 | end 59 | 60 | --[=[@p mentionString string A string that, when included in a message content, 61 | may resolve as a link to a channel in the official Discord client.]=] 62 | function get.mentionString(self) 63 | return format('<#%s>', self._id) 64 | end 65 | 66 | return Channel 67 | -------------------------------------------------------------------------------- /libs/containers/abstract/Container.lua: -------------------------------------------------------------------------------- 1 | --[=[ 2 | @c Container 3 | @t abc 4 | @d Defines the base methods and properties for all Discord objects and 5 | structures. Container classes are constructed internally with information 6 | received from Discord and should never be manually constructed. 7 | ]=] 8 | 9 | local json = require('json') 10 | 11 | local null = json.null 12 | local format = string.format 13 | 14 | local Container, get = require('class')('Container') 15 | 16 | local types = {['string'] = true, ['number'] = true, ['boolean'] = true} 17 | 18 | local function load(self, data) 19 | -- assert(type(data) == 'table') -- debug 20 | for k, v in pairs(data) do 21 | if types[type(v)] then 22 | self['_' .. k] = v 23 | elseif v == null then 24 | self['_' .. k] = nil 25 | end 26 | end 27 | end 28 | 29 | function Container:__init(data, parent) 30 | -- assert(type(parent) == 'table') -- debug 31 | self._parent = parent 32 | return load(self, data) 33 | end 34 | 35 | --[=[ 36 | @m __eq 37 | @r boolean 38 | @d Defines the behavior of the `==` operator. Allows containers to be directly 39 | compared according to their type and `__hash` return values. 40 | ]=] 41 | function Container:__eq(other) 42 | return self.__class == other.__class and self:__hash() == other:__hash() 43 | end 44 | 45 | --[=[ 46 | @m __tostring 47 | @r string 48 | @d Defines the behavior of the `tostring` function. All containers follow the format 49 | `ClassName: hash`. 50 | ]=] 51 | function Container:__tostring() 52 | return format('%s: %s', self.__name, self:__hash()) 53 | end 54 | 55 | Container._load = load 56 | 57 | --[=[@p client Client A shortcut to the client object to which this container is visible.]=] 58 | function get.client(self) 59 | return self._parent.client or self._parent 60 | end 61 | 62 | --[=[@p parent Container/Client The parent object of to which this container is 63 | a child. For example, the parent of a role is the guild in which the role exists.]=] 64 | function get.parent(self) 65 | return self._parent 66 | end 67 | 68 | return Container 69 | -------------------------------------------------------------------------------- /libs/iterables/SecondaryCache.lua: -------------------------------------------------------------------------------- 1 | --[=[ 2 | @c SecondaryCache x Iterable 3 | @mt mem 4 | @d Iterable class that wraps another cache. Objects added to or removed from a 5 | secondary cache are also automatically added to or removed from the primary 6 | cache that it wraps. 7 | ]=] 8 | 9 | local Iterable = require('iterables/Iterable') 10 | 11 | local SecondaryCache = require('class')('SecondaryCache', Iterable) 12 | 13 | function SecondaryCache:__init(array, primary) 14 | local objects = {} 15 | for _, data in ipairs(array) do 16 | local obj = primary:_insert(data) 17 | objects[obj:__hash()] = obj 18 | end 19 | self._count = #array 20 | self._objects = objects 21 | self._primary = primary 22 | end 23 | 24 | function SecondaryCache:__pairs() 25 | return next, self._objects 26 | end 27 | 28 | function SecondaryCache:__len() 29 | return self._count 30 | end 31 | 32 | function SecondaryCache:_insert(data) 33 | local obj = self._primary:_insert(data) 34 | local k = obj:__hash() 35 | if not self._objects[k] then 36 | self._objects[k] = obj 37 | self._count = self._count + 1 38 | end 39 | return obj 40 | end 41 | 42 | function SecondaryCache:_remove(data) 43 | local obj = self._primary:_insert(data) -- yes, this is correct 44 | local k = obj:__hash() 45 | if self._objects[k] then 46 | self._objects[k] = nil 47 | self._count = self._count - 1 48 | end 49 | return obj 50 | end 51 | 52 | --[=[ 53 | @m get 54 | @p k * 55 | @r * 56 | @d Returns an individual object by key, where the key should match the result of 57 | calling `__hash` on the contained objects. Unlike the default version, this 58 | method operates with O(1) complexity. 59 | ]=] 60 | function SecondaryCache:get(k) 61 | return self._objects[k] 62 | end 63 | 64 | --[=[ 65 | @m iter 66 | @r function 67 | @d Returns an iterator that returns all contained objects. The order of the objects 68 | is not guaranteed. 69 | ]=] 70 | function SecondaryCache:iter() 71 | local objects, k, obj = self._objects, nil, nil 72 | return function() 73 | k, obj = next(objects, k) 74 | return obj 75 | end 76 | end 77 | 78 | return SecondaryCache 79 | -------------------------------------------------------------------------------- /bin/libopus/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2001-2011 Xiph.Org, Skype Limited, Octasic, 2 | Jean-Marc Valin, Timothy B. Terriberry, 3 | CSIRO, Gregory Maxwell, Mark Borgerding, 4 | Erik de Castro Lopo 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions 8 | are met: 9 | 10 | - Redistributions of source code must retain the above copyright 11 | notice, this list of conditions and the following disclaimer. 12 | 13 | - Redistributions in binary form must reproduce the above copyright 14 | notice, this list of conditions and the following disclaimer in the 15 | documentation and/or other materials provided with the distribution. 16 | 17 | - Neither the name of Internet Society, IETF or IETF Trust, nor the 18 | names of specific contributors, may be used to endorse or promote 19 | products derived from this software without specific prior written 20 | permission. 21 | 22 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 23 | ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 24 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 25 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER 26 | OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 27 | EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 28 | PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 29 | PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 30 | LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 31 | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 32 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 33 | 34 | Opus is subject to the royalty-free patent licenses which are 35 | specified at: 36 | 37 | Xiph.Org Foundation: 38 | https://datatracker.ietf.org/ipr/1524/ 39 | 40 | Microsoft Corporation: 41 | https://datatracker.ietf.org/ipr/1914/ 42 | 43 | Broadcom Corporation: 44 | https://datatracker.ietf.org/ipr/1526/ 45 | 46 | -------------------------------------------------------------------------------- /libs/voice/streams/FFmpegProcess.lua: -------------------------------------------------------------------------------- 1 | local uv = require('uv') 2 | 3 | local remove = table.remove 4 | local unpack = string.unpack -- luacheck: ignore 5 | local rep = string.rep 6 | local yield, resume, running = coroutine.yield, coroutine.resume, coroutine.running 7 | 8 | local function onExit() end 9 | 10 | local fmt = setmetatable({}, { 11 | __index = function(self, n) 12 | self[n] = '<' .. rep('i2', n) 13 | return self[n] 14 | end 15 | }) 16 | 17 | local FFmpegProcess = require('class')('FFmpegProcess') 18 | 19 | function FFmpegProcess:__init(path, rate, channels) 20 | 21 | local stdout = uv.new_pipe(false) 22 | 23 | self._child = assert(uv.spawn('ffmpeg', { 24 | args = {'-i', path, '-ar', rate, '-ac', channels, '-f', 's16le', 'pipe:1', '-loglevel', 'warning'}, 25 | stdio = {0, stdout, 2}, 26 | }, onExit), 'ffmpeg could not be started, is it installed and on your executable path?') 27 | 28 | local buffer 29 | local thread = running() 30 | stdout:read_start(function(err, chunk) 31 | if err or not chunk then 32 | self:close() 33 | else 34 | buffer = chunk 35 | end 36 | stdout:read_stop() 37 | return assert(resume(thread)) 38 | end) 39 | 40 | self._buffer = buffer or '' 41 | self._stdout = stdout 42 | 43 | yield() 44 | 45 | end 46 | 47 | function FFmpegProcess:read(n) 48 | 49 | local buffer = self._buffer 50 | local stdout = self._stdout 51 | local bytes = n * 2 52 | 53 | if not self._closed and #buffer < bytes then 54 | 55 | local thread = running() 56 | stdout:read_start(function(err, chunk) 57 | if err or not chunk then 58 | self:close() 59 | elseif #chunk > 0 then 60 | buffer = buffer .. chunk 61 | end 62 | if #buffer >= bytes or self._closed then 63 | stdout:read_stop() 64 | return assert(resume(thread)) 65 | end 66 | end) 67 | yield() 68 | 69 | end 70 | 71 | if #buffer >= bytes then 72 | self._buffer = buffer:sub(bytes + 1) 73 | local pcm = {unpack(fmt[n], buffer)} 74 | remove(pcm) 75 | return pcm 76 | end 77 | 78 | end 79 | 80 | function FFmpegProcess:close() 81 | self._closed = true 82 | self._child:kill() 83 | if not self._stdout:is_closing() then 84 | self._stdout:close() 85 | end 86 | end 87 | 88 | return FFmpegProcess 89 | -------------------------------------------------------------------------------- /libs/utils/Logger.lua: -------------------------------------------------------------------------------- 1 | --[=[ 2 | @c Logger 3 | @t ui 4 | @mt mem 5 | @p level number 6 | @p dateTime string 7 | @op file string 8 | @d Used to log formatted messages to stdout (the console) or to a file. 9 | The `dateTime` argument should be a format string that is accepted by `os.date`. 10 | The file argument should be a relative or absolute file path or `nil` if no log 11 | file is desired. See the `logLevel` enumeration for acceptable log level values. 12 | ]=] 13 | 14 | local fs = require('fs') 15 | 16 | local date = os.date 17 | local format = string.format 18 | local stdout = _G.process.stdout.handle ---@diagnostic disable-line: undefined-field 19 | local openSync, writeSync = fs.openSync, fs.writeSync 20 | 21 | -- local BLACK = 30 22 | local RED = 31 23 | local GREEN = 32 24 | local YELLOW = 33 25 | -- local BLUE = 34 26 | -- local MAGENTA = 35 27 | local CYAN = 36 28 | -- local WHITE = 37 29 | 30 | local config = { 31 | {'[ERROR] ', RED}, 32 | {'[WARNING]', YELLOW}, 33 | {'[INFO] ', GREEN}, 34 | {'[DEBUG] ', CYAN}, 35 | } 36 | 37 | do -- parse config 38 | local bold = 1 39 | for _, v in ipairs(config) do 40 | v[3] = format('\27[%i;%im%s\27[0m', bold, v[2], v[1]) 41 | end 42 | end 43 | 44 | local Logger = require('class')('Logger') 45 | 46 | function Logger:__init(level, dateTime, file) 47 | self._level = level 48 | self._dateTime = dateTime 49 | self._file = file and openSync(file, 'a') 50 | end 51 | 52 | --[=[ 53 | @m log 54 | @p level number 55 | @p msg string 56 | @p ... * 57 | @r string 58 | @d If the provided level is less than or equal to the log level set on 59 | initialization, this logs a message to stdout as defined by Luvit's `process` 60 | module and to a file if one was provided on initialization. The `msg, ...` pair 61 | is formatted according to `string.format` and returned if the message is logged. 62 | ]=] 63 | function Logger:log(level, msg, ...) 64 | 65 | if self._level < level then return end 66 | 67 | local tag = config[level] 68 | if not tag then return end 69 | 70 | msg = format(msg, ...) 71 | 72 | local d = date(self._dateTime) 73 | if self._file then 74 | writeSync(self._file, -1, format('%s | %s | %s\n', d, tag[1], msg)) 75 | end 76 | stdout:write(format('%s | %s | %s\n', d, tag[3], msg)) 77 | 78 | return msg 79 | 80 | end 81 | 82 | return Logger 83 | -------------------------------------------------------------------------------- /libs/utils/Deque.lua: -------------------------------------------------------------------------------- 1 | --[=[ 2 | @c Deque 3 | @t ui 4 | @mt mem 5 | @d An implementation of a double-ended queue. 6 | ]=] 7 | 8 | local Deque = require('class')('Deque') 9 | 10 | function Deque:__init() 11 | self._objects = {} 12 | self._first = 0 13 | self._last = -1 14 | end 15 | 16 | --[=[ 17 | @m getCount 18 | @r number 19 | @d Returns the total number of values stored. 20 | ]=] 21 | function Deque:getCount() 22 | return self._last - self._first + 1 23 | end 24 | 25 | --[=[ 26 | @m pushLeft 27 | @p obj * 28 | @r nil 29 | @d Adds a value of any type to the left side of the deque. 30 | ]=] 31 | function Deque:pushLeft(obj) 32 | self._first = self._first - 1 33 | self._objects[self._first] = obj 34 | end 35 | 36 | --[=[ 37 | @m pushRight 38 | @p obj * 39 | @r nil 40 | @d Adds a value of any type to the right side of the deque. 41 | ]=] 42 | function Deque:pushRight(obj) 43 | self._last = self._last + 1 44 | self._objects[self._last] = obj 45 | end 46 | 47 | --[=[ 48 | @m popLeft 49 | @r * 50 | @d Removes and returns a value from the left side of the deque. 51 | ]=] 52 | function Deque:popLeft() 53 | if self._first > self._last then return nil end 54 | local obj = self._objects[self._first] 55 | self._objects[self._first] = nil 56 | self._first = self._first + 1 57 | return obj 58 | end 59 | 60 | --[=[ 61 | @m popRight 62 | @r * 63 | @d Removes and returns a value from the right side of the deque. 64 | ]=] 65 | function Deque:popRight() 66 | if self._first > self._last then return nil end 67 | local obj = self._objects[self._last] 68 | self._objects[self._last] = nil 69 | self._last = self._last - 1 70 | return obj 71 | end 72 | 73 | --[=[ 74 | @m peekLeft 75 | @r * 76 | @d Returns the value at the left side of the deque without removing it. 77 | ]=] 78 | function Deque:peekLeft() 79 | return self._objects[self._first] 80 | end 81 | 82 | --[=[ 83 | @m peekRight 84 | @r * 85 | @d Returns the value at the right side of the deque without removing it. 86 | ]=] 87 | function Deque:peekRight() 88 | return self._objects[self._last] 89 | end 90 | 91 | --[=[ 92 | @m iter 93 | @r function 94 | @d Iterates over the deque from left to right. 95 | ]=] 96 | function Deque:iter() 97 | local t = self._objects 98 | local i = self._first - 1 99 | return function() 100 | i = i + 1 101 | return t[i] 102 | end 103 | end 104 | 105 | return Deque 106 | -------------------------------------------------------------------------------- /libs/utils/Stopwatch.lua: -------------------------------------------------------------------------------- 1 | --[=[ 2 | @c Stopwatch 3 | @t ui 4 | @mt mem 5 | @op stopped boolean 6 | @d Used to measure an elapsed period of time. If a truthy value is passed as an 7 | argument, then the stopwatch will initialize in an idle state; otherwise, it will 8 | initialize in an active state. Although nanosecond precision is available, Lua 9 | can only reliably provide microsecond accuracy due to the lack of native 64-bit 10 | integer support. Generally, milliseconds should be sufficient here. 11 | ]=] 12 | 13 | local hrtime = require('uv').hrtime 14 | local constants = require('constants') 15 | local Time = require('utils/Time') 16 | 17 | local format = string.format 18 | 19 | local MS_PER_NS = 1 / (constants.NS_PER_US * constants.US_PER_MS) 20 | 21 | local Stopwatch, get = require('class')('Stopwatch') 22 | 23 | function Stopwatch:__init(stopped) 24 | local t = hrtime() 25 | self._initial = t 26 | self._final = stopped and t or nil 27 | end 28 | 29 | --[=[ 30 | @m __tostring 31 | @r string 32 | @d Defines the behavior of the `tostring` function. Returns a string that 33 | represents the elapsed milliseconds for convenience of introspection. 34 | ]=] 35 | function Stopwatch:__tostring() 36 | return format('Stopwatch: %s ms', self.milliseconds) 37 | end 38 | 39 | --[=[ 40 | @m stop 41 | @r nil 42 | @d Effectively stops the stopwatch. 43 | ]=] 44 | function Stopwatch:stop() 45 | if self._final then return end 46 | self._final = hrtime() 47 | end 48 | 49 | --[=[ 50 | @m start 51 | @r nil 52 | @d Effectively starts the stopwatch. 53 | ]=] 54 | function Stopwatch:start() 55 | if not self._final then return end 56 | self._initial = self._initial + hrtime() - self._final 57 | self._final = nil 58 | end 59 | 60 | --[=[ 61 | @m reset 62 | @r nil 63 | @d Effectively resets the stopwatch. 64 | ]=] 65 | function Stopwatch:reset() 66 | self._initial = self._final or hrtime() 67 | end 68 | 69 | --[=[ 70 | @m getTime 71 | @r Time 72 | @d Returns a new Time object that represents the currently elapsed time. This is 73 | useful for "catching" the current time and comparing its many forms as required. 74 | ]=] 75 | function Stopwatch:getTime() 76 | return Time(self.milliseconds) 77 | end 78 | 79 | --[=[@p milliseconds number The total number of elapsed milliseconds. If the 80 | stopwatch is running, this will naturally be different each time that it is accessed.]=] 81 | function get.milliseconds(self) 82 | local ns = (self._final or hrtime()) - self._initial 83 | return ns * MS_PER_NS 84 | end 85 | 86 | return Stopwatch 87 | -------------------------------------------------------------------------------- /libs/iterables/ArrayIterable.lua: -------------------------------------------------------------------------------- 1 | --[=[ 2 | @c ArrayIterable x Iterable 3 | @mt mem 4 | @d Iterable class that contains objects in a constant, ordered fashion, although 5 | the order may change if the internal array is modified. Some versions may use a 6 | map function to shape the objects before they are accessed. 7 | ]=] 8 | 9 | local Iterable = require('iterables/Iterable') 10 | 11 | local ArrayIterable, get = require('class')('ArrayIterable', Iterable) 12 | 13 | function ArrayIterable:__init(array, map) 14 | self._array = array 15 | self._map = map 16 | end 17 | 18 | function ArrayIterable:__len() 19 | local array = self._array 20 | if not array or #array == 0 then 21 | return 0 22 | end 23 | local map = self._map 24 | if map then -- map can return nil 25 | return Iterable.__len(self) 26 | else 27 | return #array 28 | end 29 | end 30 | 31 | --[=[@p first * The first object in the array]=] 32 | function get.first(self) 33 | local array = self._array 34 | if not array or #array == 0 then 35 | return nil 36 | end 37 | local map = self._map 38 | if map then 39 | for i = 1, #array, 1 do 40 | local v = array[i] 41 | local obj = v and map(v) 42 | if obj then 43 | return obj 44 | end 45 | end 46 | else 47 | return array[1] 48 | end 49 | end 50 | 51 | --[=[@p last * The last object in the array]=] 52 | function get.last(self) 53 | local array = self._array 54 | if not array or #array == 0 then 55 | return nil 56 | end 57 | local map = self._map 58 | if map then 59 | for i = #array, 1, -1 do 60 | local v = array[i] 61 | local obj = v and map(v) 62 | if obj then 63 | return obj 64 | end 65 | end 66 | else 67 | return array[#array] 68 | end 69 | end 70 | 71 | --[=[ 72 | @m iter 73 | @r function 74 | @d Returns an iterator for all contained objects in a consistent order. 75 | ]=] 76 | function ArrayIterable:iter() 77 | local array = self._array 78 | if not array or #array == 0 then 79 | return function() -- new closure for consistency 80 | return nil 81 | end 82 | end 83 | local map = self._map 84 | if map then 85 | local i = 0 86 | return function() 87 | while true do 88 | i = i + 1 89 | local v = array[i] 90 | if not v then 91 | return nil 92 | end 93 | v = map(v) 94 | if v then 95 | return v 96 | end 97 | end 98 | end 99 | else 100 | local i = 0 101 | return function() 102 | i = i + 1 103 | return array[i] 104 | end 105 | end 106 | end 107 | 108 | return ArrayIterable 109 | -------------------------------------------------------------------------------- /libs/containers/GuildCategoryChannel.lua: -------------------------------------------------------------------------------- 1 | --[=[ 2 | @c GuildCategoryChannel x GuildChannel 3 | @d Represents a channel category in a Discord guild, used to organize individual 4 | text or voice channels in that guild. 5 | ]=] 6 | 7 | local GuildChannel = require('containers/abstract/GuildChannel') 8 | local FilteredIterable = require('iterables/FilteredIterable') 9 | local enums = require('enums') 10 | 11 | local channelType = assert(enums.channelType) 12 | 13 | local GuildCategoryChannel, get = require('class')('GuildCategoryChannel', GuildChannel) 14 | 15 | function GuildCategoryChannel:__init(data, parent) 16 | GuildChannel.__init(self, data, parent) 17 | end 18 | 19 | --[=[ 20 | @m createTextChannel 21 | @t http 22 | @p name string 23 | @r GuildTextChannel 24 | @d Creates a new GuildTextChannel with this category as it's parent. Similar to `Guild:createTextChannel(name)` 25 | ]=] 26 | function GuildCategoryChannel:createTextChannel(name) 27 | local guild = self._parent 28 | local data, err = guild.client._api:createGuildChannel(guild._id, { 29 | name = name, 30 | type = channelType.text, 31 | parent_id = self._id 32 | }) 33 | if data then 34 | return guild._text_channels:_insert(data) 35 | else 36 | return nil, err 37 | end 38 | end 39 | 40 | --[=[ 41 | @m createVoiceChannel 42 | @t http 43 | @p name string 44 | @r GuildVoiceChannel 45 | @d Creates a new GuildVoiceChannel with this category as it's parent. Similar to `Guild:createVoiceChannel(name)` 46 | ]=] 47 | function GuildCategoryChannel:createVoiceChannel(name) 48 | local guild = self._parent 49 | local data, err = guild.client._api:createGuildChannel(guild._id, { 50 | name = name, 51 | type = channelType.voice, 52 | parent_id = self._id 53 | }) 54 | if data then 55 | return guild._voice_channels:_insert(data) 56 | else 57 | return nil, err 58 | end 59 | end 60 | 61 | --[=[@p textChannels FilteredIterable Iterable of all textChannels in the Category.]=] 62 | function get.textChannels(self) 63 | if not self._text_channels then 64 | local id = self._id 65 | self._text_channels = FilteredIterable(self._parent._text_channels, function(c) 66 | return c._parent_id == id 67 | end) 68 | end 69 | return self._text_channels 70 | end 71 | 72 | --[=[@p voiceChannels FilteredIterable Iterable of all voiceChannels in the Category.]=] 73 | function get.voiceChannels(self) 74 | if not self._voice_channels then 75 | local id = self._id 76 | self._voice_channels = FilteredIterable(self._parent._voice_channels, function(c) 77 | return c._parent_id == id 78 | end) 79 | end 80 | return self._voice_channels 81 | end 82 | 83 | return GuildCategoryChannel 84 | -------------------------------------------------------------------------------- /libs/containers/Sticker.lua: -------------------------------------------------------------------------------- 1 | --[=[ 2 | @c Sticker x Snowflake 3 | @d Represents a sticker object. 4 | ]=] 5 | 6 | local Snowflake = require('containers/abstract/Snowflake') 7 | local json = require('json') 8 | 9 | local format = string.format 10 | 11 | local Sticker, get = require('class')('Sticker', Snowflake) 12 | 13 | function Sticker:__init(data, parent) 14 | Snowflake.__init(self, data, parent) 15 | self.client._sticker_map[self._id] = parent 16 | end 17 | 18 | function Sticker:_load(data) 19 | Snowflake._load(self, data) 20 | end 21 | 22 | function Sticker:_modify(payload) 23 | local data, err = self.client._api:modifyGuildSticker(self._parent._id, self._id, payload) 24 | if data then 25 | self:_load(data) 26 | return true 27 | else 28 | return false, err 29 | end 30 | end 31 | 32 | --[=[ 33 | @m setName 34 | @t http 35 | @p name string 36 | @r boolean 37 | @d Sets the stickers's name. The name must be between 2 and 30 characters in length. 38 | ]=] 39 | function Sticker:setName(name) 40 | return self:_modify({name = name or json.null}) 41 | end 42 | 43 | --[=[ 44 | @m setDescription 45 | @t http 46 | @p description string 47 | @r boolean 48 | @d Sets the stickers's description. The description must be between 2 and 30 characters in length. 49 | ]=] 50 | function Sticker:setDescription(description) 51 | return self:_modify({description = description or json.null}) 52 | end 53 | 54 | --[=[ 55 | @m setTags 56 | @t http 57 | @p tags string 58 | @r boolean 59 | @d Sets the stickers's tags. The tags can only be up to 200 characters long. 60 | ]=] 61 | function Sticker:setTags(tags) 62 | return self:_modify({tags = tags or json.null}) 63 | end 64 | 65 | --[=[ 66 | @m delete 67 | @t http 68 | @r boolean 69 | @d Permanently deletes the sticker. This cannot be undone! 70 | ]=] 71 | function Sticker:delete() 72 | local data, err = self.client._api:deleteGuildSticker(self._parent._id, self._id) 73 | if data then 74 | self._parent._stickers:_delete(self._id) 75 | return true 76 | else 77 | return false, err 78 | end 79 | end 80 | 81 | --[=[@p name string The name of the sticker.]=] 82 | function get.name(self) 83 | return self._name 84 | end 85 | 86 | --[=[@p description string The description of the sticker.]=] 87 | function get.description(self) 88 | return self._description 89 | end 90 | 91 | --[=[@p tags string The tags of the sticker.]=] 92 | function get.tags(self) 93 | return self._tags 94 | end 95 | 96 | --[=[@p type number The sticker format type.]=] 97 | function get.type(self) 98 | return self._format_type 99 | end 100 | 101 | --[=[@p guild Guild The guild in which the sticker exists.]=] 102 | function get.guild(self) 103 | return self._parent 104 | end 105 | 106 | --[=[@p url string The URL that can be used to view a full version of the sticker.]=] 107 | function get.url(self) 108 | return format('https://cdn.discordapp.com/stickers/%s.png', self._id) 109 | end 110 | 111 | return Sticker -------------------------------------------------------------------------------- /libs/endpoints.lua: -------------------------------------------------------------------------------- 1 | return { 2 | CHANNEL = "/channels/%s", 3 | CHANNEL_FOLLOWERS = "/channels/%s/followers", 4 | CHANNEL_INVITES = "/channels/%s/invites", 5 | CHANNEL_MESSAGE = "/channels/%s/messages/%s", 6 | CHANNEL_MESSAGES = "/channels/%s/messages", 7 | CHANNEL_MESSAGES_BULK_DELETE = "/channels/%s/messages/bulk-delete", 8 | CHANNEL_MESSAGE_CROSSPOST = "/channels/%s/messages/%s/crosspost", 9 | CHANNEL_MESSAGE_REACTION = "/channels/%s/messages/%s/reactions/%s", 10 | CHANNEL_MESSAGE_REACTIONS = "/channels/%s/messages/%s/reactions", 11 | CHANNEL_MESSAGE_REACTION_ME = "/channels/%s/messages/%s/reactions/%s/@me", 12 | CHANNEL_MESSAGE_REACTION_USER = "/channels/%s/messages/%s/reactions/%s/%s", 13 | CHANNEL_PERMISSION = "/channels/%s/permissions/%s", 14 | CHANNEL_PIN = "/channels/%s/pins/%s", 15 | CHANNEL_PINS = "/channels/%s/pins", 16 | CHANNEL_RECIPIENT = "/channels/%s/recipients/%s", 17 | CHANNEL_TYPING = "/channels/%s/typing", 18 | CHANNEL_WEBHOOKS = "/channels/%s/webhooks", 19 | GATEWAY = "/gateway", 20 | GATEWAY_BOT = "/gateway/bot", 21 | GUILD = "/guilds/%s", 22 | GUILDS = "/guilds", 23 | GUILD_AUDIT_LOGS = "/guilds/%s/audit-logs", 24 | GUILD_BAN = "/guilds/%s/bans/%s", 25 | GUILD_BANS = "/guilds/%s/bans", 26 | GUILD_CHANNELS = "/guilds/%s/channels", 27 | GUILD_EMBED = "/guilds/%s/embed", 28 | GUILD_EMOJI = "/guilds/%s/emojis/%s", 29 | GUILD_EMOJIS = "/guilds/%s/emojis", 30 | GUILD_STICKER = "/guilds/%s/stickers/%s", 31 | GUILD_STICKERS = "/guilds/%s/stickers", 32 | GUILD_INTEGRATION = "/guilds/%s/integrations/%s", 33 | GUILD_INTEGRATIONS = "/guilds/%s/integrations", 34 | GUILD_INTEGRATION_SYNC = "/guilds/%s/integrations/%s/sync", 35 | GUILD_INVITES = "/guilds/%s/invites", 36 | GUILD_MEMBER = "/guilds/%s/members/%s", 37 | GUILD_MEMBERS = "/guilds/%s/members", 38 | GUILD_MEMBER_ME = "/guilds/%s/members/@me", 39 | GUILD_MEMBER_ROLE = "/guilds/%s/members/%s/roles/%s", 40 | GUILD_PRUNE = "/guilds/%s/prune", 41 | GUILD_REGIONS = "/guilds/%s/regions", 42 | GUILD_ROLE = "/guilds/%s/roles/%s", 43 | GUILD_ROLES = "/guilds/%s/roles", 44 | GUILD_WEBHOOKS = "/guilds/%s/webhooks", 45 | INVITE = "/invites/%s", 46 | OAUTH2_APPLICATION_ME = "/oauth2/applications/@me", 47 | USER = "/users/%s", 48 | USER_ME = "/users/@me", 49 | USER_ME_CHANNELS = "/users/@me/channels", 50 | USER_ME_CONNECTIONS = "/users/@me/connections", 51 | USER_ME_GUILD = "/users/@me/guilds/%s", 52 | USER_ME_GUILDS = "/users/@me/guilds", 53 | VOICE_REGIONS = "/voice/regions", 54 | WEBHOOK = "/webhooks/%s", 55 | WEBHOOK_TOKEN = "/webhooks/%s/%s", 56 | WEBHOOK_TOKEN_GITHUB = "/webhooks/%s/%s/github", 57 | WEBHOOK_TOKEN_SLACK = "/webhooks/%s/%s/slack", 58 | } 59 | -------------------------------------------------------------------------------- /libs/client/WebSocket.lua: -------------------------------------------------------------------------------- 1 | local json = require('json') 2 | local miniz = require('miniz') 3 | local Mutex = require('utils/Mutex') 4 | local Emitter = require('utils/Emitter') 5 | local Stopwatch = require('utils/Stopwatch') 6 | 7 | local websocket = require('coro-websocket') 8 | local constants = require('constants') 9 | 10 | local inflate = miniz.inflate 11 | local encode, decode, null = json.encode, json.decode, json.null 12 | local ws_parseUrl, ws_connect = websocket.parseUrl, websocket.connect 13 | 14 | local GATEWAY_DELAY = constants.GATEWAY_DELAY 15 | 16 | local TEXT = 1 17 | local BINARY = 2 18 | local CLOSE = 8 19 | 20 | local CLOSE_CODE = '\003\232' -- code: 1000 21 | local RECONNECT_CODE = '\015\160' -- code: 4000 22 | 23 | local function connect(url, path) 24 | local options = assert(ws_parseUrl(url)) 25 | options.pathname = path 26 | return assert(ws_connect(options)) 27 | end 28 | 29 | local WebSocket = require('class')('WebSocket', Emitter) 30 | 31 | function WebSocket:__init(parent) 32 | Emitter.__init(self) 33 | self._parent = parent 34 | self._mutex = Mutex() 35 | self._sw = Stopwatch() 36 | end 37 | 38 | function WebSocket:connect(url, path) 39 | 40 | local success, res, read, write = pcall(connect, url, path) 41 | 42 | if success then 43 | self._read = read 44 | self._write = write 45 | self._reconnect = nil 46 | self:info('Connected to %s', url) 47 | local parent = self._parent 48 | for message in self._read do 49 | local payload, str = self:parseMessage(message) 50 | if not payload then break end 51 | parent:emit('raw', str) 52 | if self.handlePayload then -- virtual method 53 | self:handlePayload(payload) 54 | end 55 | end 56 | self:info('Disconnected') 57 | else 58 | self:error('Could not connect to %s (%s)', url, res) -- TODO: get new url? 59 | end 60 | 61 | self._read = nil 62 | self._write = nil 63 | self._identified = nil 64 | 65 | if self.stopHeartbeat then -- virtual method 66 | self:stopHeartbeat() 67 | end 68 | 69 | if self.handleDisconnect then -- virtual method 70 | return self:handleDisconnect(url, path) 71 | end 72 | 73 | end 74 | 75 | function WebSocket:parseMessage(message) 76 | 77 | local opcode = message.opcode 78 | local payload = message.payload 79 | 80 | if opcode == TEXT then 81 | 82 | return decode(payload, 1, null), payload 83 | 84 | elseif opcode == BINARY then 85 | 86 | payload = inflate(payload, 1) 87 | return decode(payload, 1, null), payload 88 | 89 | elseif opcode == CLOSE then 90 | 91 | local code, i = ('>H'):unpack(payload) 92 | local msg = #payload > i and payload:sub(i) or 'Connection closed' 93 | self:warning('%i - %s', code, msg) 94 | return nil 95 | 96 | end 97 | 98 | end 99 | 100 | function WebSocket:_send(op, d, identify) 101 | self._mutex:lock() 102 | local success, err 103 | if identify or self._session_id then 104 | if self._write then 105 | success, err = self._write {opcode = TEXT, payload = encode {op = op, d = d}} 106 | else 107 | success, err = false, 'Not connected to gateway' 108 | end 109 | else 110 | success, err = false, 'Invalid session' 111 | end 112 | self._mutex:unlockAfter(GATEWAY_DELAY) 113 | return success, err 114 | end 115 | 116 | function WebSocket:disconnect(reconnect) 117 | if not self._write then return end 118 | self._reconnect = not not reconnect 119 | self._write {opcode = CLOSE, payload = reconnect and RECONNECT_CODE or CLOSE_CODE} 120 | self._read = nil 121 | self._write = nil 122 | end 123 | 124 | return WebSocket 125 | -------------------------------------------------------------------------------- /libs/containers/GroupChannel.lua: -------------------------------------------------------------------------------- 1 | --[=[ 2 | @c GroupChannel x TextChannel 3 | @d Represents a Discord group channel. Essentially a private channel that may have 4 | more than one and up to ten recipients. This class should only be relevant to 5 | user-accounts; bots cannot normally join group channels. 6 | ]=] 7 | 8 | local json = require('json') 9 | 10 | local TextChannel = require('containers/abstract/TextChannel') 11 | local SecondaryCache = require('iterables/SecondaryCache') 12 | local Resolver = require('client/Resolver') 13 | 14 | local format = string.format 15 | 16 | local GroupChannel, get = require('class')('GroupChannel', TextChannel) 17 | 18 | function GroupChannel:__init(data, parent) 19 | TextChannel.__init(self, data, parent) 20 | self._recipients = SecondaryCache(data.recipients, self.client._users) 21 | end 22 | 23 | --[=[ 24 | @m setName 25 | @t http 26 | @p name string 27 | @r boolean 28 | @d Sets the channel's name. This must be between 1 and 100 characters in length. 29 | ]=] 30 | function GroupChannel:setName(name) 31 | return self:_modify({name = name or json.null}) 32 | end 33 | 34 | --[=[ 35 | @m setIcon 36 | @t http 37 | @p icon Base64-Resolvable 38 | @r boolean 39 | @d Sets the channel's icon. To remove the icon, pass `nil`. 40 | ]=] 41 | function GroupChannel:setIcon(icon) 42 | icon = icon and Resolver.base64(icon) 43 | return self:_modify({icon = icon or json.null}) 44 | end 45 | 46 | --[=[ 47 | @m addRecipient 48 | @t http 49 | @p id User-ID-Resolvable 50 | @r boolean 51 | @d Adds a user to the channel. 52 | ]=] 53 | function GroupChannel:addRecipient(id) 54 | id = Resolver.userId(id) 55 | local data, err = self.client._api:groupDMAddRecipient(self._id, id) 56 | if data then 57 | return true 58 | else 59 | return false, err 60 | end 61 | end 62 | 63 | --[=[ 64 | @m removeRecipient 65 | @t http 66 | @p id User-ID-Resolvable 67 | @r boolean 68 | @d Removes a user from the channel. 69 | ]=] 70 | function GroupChannel:removeRecipient(id) 71 | id = Resolver.userId(id) 72 | local data, err = self.client._api:groupDMRemoveRecipient(self._id, id) 73 | if data then 74 | return true 75 | else 76 | return false, err 77 | end 78 | end 79 | 80 | --[=[ 81 | @m leave 82 | @t http 83 | @r boolean 84 | @d Removes the client's user from the channel. If no users remain, the channel 85 | is destroyed. 86 | ]=] 87 | function GroupChannel:leave() 88 | return self:_delete() 89 | end 90 | 91 | --[=[@p recipients SecondaryCache A secondary cache of users that are present in the channel.]=] 92 | function get.recipients(self) 93 | return self._recipients 94 | end 95 | 96 | --[=[@p name string The name of the channel.]=] 97 | function get.name(self) 98 | return self._name 99 | end 100 | 101 | --[=[@p ownerId string The Snowflake ID of the user that owns (created) the channel.]=] 102 | function get.ownerId(self) 103 | return self._owner_id 104 | end 105 | 106 | --[=[@p owner User/nil Equivalent to `GroupChannel.recipients:get(GroupChannel.ownerId)`.]=] 107 | function get.owner(self) 108 | return self._recipients:get(self._owner_id) 109 | end 110 | 111 | --[=[@p icon string/nil The hash for the channel's custom icon, if one is set.]=] 112 | function get.icon(self) 113 | return self._icon 114 | end 115 | 116 | --[=[@p iconURL string/nil The URL that can be used to view the channel's icon, if one is set.]=] 117 | function get.iconURL(self) 118 | local icon = self._icon 119 | return icon and format('https://cdn.discordapp.com/channel-icons/%s/%s.png', self._id, icon) 120 | end 121 | 122 | return GroupChannel 123 | -------------------------------------------------------------------------------- /libs/containers/abstract/UserPresence.lua: -------------------------------------------------------------------------------- 1 | --[=[ 2 | @c UserPresence x Container 3 | @t abc 4 | @d Defines the base methods and/or properties for classes that represent a 5 | user's current presence information. Note that any method or property that 6 | exists for the User class is also available in the UserPresence class and its 7 | subclasses. 8 | ]=] 9 | 10 | local null = require('json').null 11 | local User = require('containers/User') 12 | local Activity = require('containers/Activity') 13 | local Container = require('containers/abstract/Container') 14 | 15 | local UserPresence, get = require('class')('UserPresence', Container) 16 | 17 | function UserPresence:__init(data, parent) 18 | Container.__init(self, data, parent) 19 | self._user = self.client._users:_insert(data.user) 20 | end 21 | 22 | --[=[ 23 | @m __hash 24 | @r string 25 | @d Returns `UserPresence.user.id` 26 | ]=] 27 | function UserPresence:__hash() 28 | return self._user._id 29 | end 30 | 31 | local activities = setmetatable({}, {__mode = 'v'}) 32 | 33 | function UserPresence:_loadPresence(presence) 34 | self._status = presence.status 35 | local status = presence.client_status 36 | if status then 37 | self._web_status = status.web 38 | self._mobile_status = status.mobile 39 | self._desktop_status = status.desktop 40 | end 41 | local activity = presence.activities[1] 42 | if activity == null then 43 | self._activity = nil 44 | elseif activity then 45 | local arr = presence.activities 46 | if arr and arr[2] then 47 | for i = 2, #arr do 48 | for k, v in pairs(arr[i]) do 49 | activity[k] = v 50 | end 51 | end 52 | end 53 | if self._activity then 54 | self._activity:_load(activity) 55 | else 56 | local cached = activities[self:__hash()] 57 | if cached then 58 | cached:_load(activity) 59 | else 60 | cached = Activity(activity, self) 61 | activities[self:__hash()] = cached 62 | end 63 | self._activity = cached 64 | end 65 | end 66 | end 67 | 68 | function get.gameName(self) 69 | self.client:_deprecated(self.__name, 'gameName', 'activity.name') 70 | return self._activity and self._activity._name 71 | end 72 | 73 | function get.gameType(self) 74 | self.client:_deprecated(self.__name, 'gameType', 'activity.type') 75 | return self._activity and self._activity._type 76 | end 77 | 78 | function get.gameURL(self) 79 | self.client:_deprecated(self.__name, 'gameURL', 'activity.url') 80 | return self._activity and self._activity._url 81 | end 82 | 83 | --[=[@p status string The user's overall status (online, dnd, idle, offline).]=] 84 | function get.status(self) 85 | return self._status or 'offline' 86 | end 87 | 88 | --[=[@p webStatus string The user's web status (online, dnd, idle, offline).]=] 89 | function get.webStatus(self) 90 | return self._web_status or 'offline' 91 | end 92 | 93 | --[=[@p mobileStatus string The user's mobile status (online, dnd, idle, offline).]=] 94 | function get.mobileStatus(self) 95 | return self._mobile_status or 'offline' 96 | end 97 | 98 | --[=[@p desktopStatus string The user's desktop status (online, dnd, idle, offline).]=] 99 | function get.desktopStatus(self) 100 | return self._desktop_status or 'offline' 101 | end 102 | 103 | --[=[@p user User The user that this presence represents.]=] 104 | function get.user(self) 105 | return self._user 106 | end 107 | 108 | --[=[@p activity Activity/nil The Activity that this presence represents.]=] 109 | function get.activity(self) 110 | return self._activity 111 | end 112 | 113 | -- user shortcuts 114 | 115 | for k, v in pairs(User) do 116 | UserPresence[k] = UserPresence[k] or function(self, ...) 117 | return v(self._user, ...) 118 | end 119 | end 120 | 121 | for k, v in pairs(User.__getters) do 122 | get[k] = get[k] or function(self) 123 | return v(self._user) 124 | end 125 | end 126 | 127 | return UserPresence 128 | -------------------------------------------------------------------------------- /libs/iterables/Cache.lua: -------------------------------------------------------------------------------- 1 | --[=[ 2 | @c Cache x Iterable 3 | @mt mem 4 | @d Iterable class that holds references to Discordia Class objects in no particular order. 5 | ]=] 6 | 7 | local json = require('json') 8 | local Iterable = require('iterables/Iterable') 9 | 10 | local null = json.null 11 | 12 | local Cache = require('class')('Cache', Iterable) 13 | 14 | local meta = {__mode = 'v'} 15 | 16 | function Cache:__init(array, constructor, parent) 17 | local objects = {} 18 | for _, data in ipairs(array) do 19 | local obj = constructor(data, parent) 20 | objects[obj:__hash()] = obj 21 | end 22 | self._count = #array 23 | self._objects = objects 24 | self._constructor = constructor 25 | self._parent = parent 26 | self._deleted = setmetatable({}, meta) 27 | end 28 | 29 | function Cache:__pairs() 30 | return next, self._objects 31 | end 32 | 33 | function Cache:__len() 34 | return self._count 35 | end 36 | 37 | local function insert(self, k, obj) 38 | self._objects[k] = obj 39 | self._count = self._count + 1 40 | return obj 41 | end 42 | 43 | local function remove(self, k, obj) 44 | self._objects[k] = nil 45 | self._deleted[k] = obj 46 | self._count = self._count - 1 47 | return obj 48 | end 49 | 50 | local function hash(data) 51 | -- local meta = getmetatable(data) -- debug 52 | -- assert(meta and meta.__jsontype == 'object') -- debug 53 | if data.id then -- snowflakes 54 | return data.id 55 | elseif data.user then -- members 56 | return data.user.id 57 | elseif data.emoji then -- reactions 58 | return data.emoji.id ~= null and data.emoji.id or data.emoji.name 59 | elseif data.code then -- invites 60 | return data.code 61 | else 62 | return nil, 'json data could not be hashed' 63 | end 64 | end 65 | 66 | function Cache:_insert(data) 67 | local k = assert(hash(data)) 68 | local old = self._objects[k] 69 | if old then 70 | old:_load(data) 71 | return old 72 | elseif self._deleted[k] then 73 | local deleted = insert(self, k, self._deleted[k]) 74 | deleted:_load(data) 75 | return deleted 76 | else 77 | local obj = self._constructor(data, self._parent) 78 | return insert(self, k, obj) 79 | end 80 | end 81 | 82 | function Cache:_remove(data) 83 | local k = assert(hash(data)) 84 | local old = self._objects[k] 85 | if old then 86 | old:_load(data) 87 | return remove(self, k, old) 88 | elseif self._deleted[k] then 89 | return self._deleted[k] 90 | else 91 | return self._constructor(data, self._parent) 92 | end 93 | end 94 | 95 | function Cache:_delete(k) 96 | local old = self._objects[k] 97 | if old then 98 | return remove(self, k, old) 99 | elseif self._deleted[k] then 100 | return self._deleted[k] 101 | else 102 | return nil 103 | end 104 | end 105 | 106 | function Cache:_load(array, update) 107 | if update then 108 | local updated = {} 109 | for _, data in ipairs(array) do 110 | local obj = self:_insert(data) 111 | updated[obj:__hash()] = true 112 | end 113 | for obj in self:iter() do 114 | local k = obj:__hash() 115 | if not updated[k] then 116 | self:_delete(k) 117 | end 118 | end 119 | else 120 | for _, data in ipairs(array) do 121 | self:_insert(data) 122 | end 123 | end 124 | end 125 | 126 | --[=[ 127 | @m get 128 | @p k * 129 | @r * 130 | @d Returns an individual object by key, where the key should match the result of 131 | calling `__hash` on the contained objects. Unlike Iterable:get, this 132 | method operates with O(1) complexity. 133 | ]=] 134 | function Cache:get(k) 135 | return self._objects[k] 136 | end 137 | 138 | --[=[ 139 | @m iter 140 | @r function 141 | @d Returns an iterator that returns all contained objects. The order of the objects 142 | is not guaranteed. 143 | ]=] 144 | function Cache:iter() 145 | local objects, k, obj = self._objects, nil, nil 146 | return function() 147 | k, obj = next(objects, k) 148 | return obj 149 | end 150 | end 151 | 152 | return Cache 153 | -------------------------------------------------------------------------------- /libs/class.lua: -------------------------------------------------------------------------------- 1 | local format = string.format 2 | 3 | local meta = {} 4 | local names = {} 5 | local classes = {} 6 | local objects = setmetatable({}, {__mode = 'k'}) 7 | 8 | function meta:__call(...) 9 | local obj = setmetatable({}, self) 10 | objects[obj] = true 11 | obj:__init(...) 12 | return obj 13 | end 14 | 15 | function meta:__tostring() 16 | return 'class ' .. self.__name 17 | end 18 | 19 | local default = {} 20 | 21 | function default:__tostring() 22 | return self.__name 23 | end 24 | 25 | function default:__hash() 26 | return self 27 | end 28 | 29 | local function isClass(cls) 30 | return not not classes[cls] 31 | end 32 | 33 | local function isObject(obj) 34 | return not not objects[obj] 35 | end 36 | 37 | local function isSubclass(sub, cls) 38 | if isClass(sub) and isClass(cls) then 39 | if sub == cls then 40 | return true 41 | else 42 | for _, base in ipairs(sub.__bases) do 43 | if isSubclass(base, cls) then 44 | return true 45 | end 46 | end 47 | end 48 | end 49 | return false 50 | end 51 | 52 | local function isInstance(obj, cls) 53 | return isObject(obj) and isSubclass(obj.__class, cls) 54 | end 55 | 56 | local function profile() 57 | local ret = setmetatable({}, {__index = function() return 0 end}) 58 | for obj in pairs(objects) do 59 | local name = obj.__name 60 | ret[name] = ret[name] + 1 61 | end 62 | return ret 63 | end 64 | 65 | local types = {['string'] = true, ['number'] = true, ['boolean'] = true} 66 | 67 | local function _getPrimitive(v) 68 | return types[type(v)] and v or v ~= nil and tostring(v) or nil 69 | end 70 | 71 | local function serialize(obj) 72 | if isObject(obj) then 73 | local ret = {} 74 | for k, v in pairs(obj.__getters) do 75 | ret[k] = _getPrimitive(v(obj)) 76 | end 77 | return ret 78 | else 79 | return _getPrimitive(obj) 80 | end 81 | end 82 | 83 | local rawtype = type 84 | local function type(obj) 85 | return isObject(obj) and obj.__name or rawtype(obj) 86 | end 87 | 88 | return setmetatable({ 89 | 90 | classes = names, 91 | isClass = isClass, 92 | isObject = isObject, 93 | isSubclass = isSubclass, 94 | isInstance = isInstance, 95 | type = type, 96 | profile = profile, 97 | serialize = serialize, 98 | 99 | }, {__call = function(_, name, ...) 100 | 101 | if names[name] then return error(format('Class %q already defined', name)) end 102 | 103 | local class = setmetatable({}, meta) 104 | classes[class] = true 105 | 106 | for k, v in pairs(default) do 107 | class[k] = v 108 | end 109 | 110 | local bases = {...} 111 | local getters = {} 112 | local setters = {} 113 | 114 | for _, base in ipairs(bases) do 115 | for k1, v1 in pairs(base) do 116 | class[k1] = v1 117 | for k2, v2 in pairs(base.__getters) do 118 | getters[k2] = v2 119 | end 120 | for k2, v2 in pairs(base.__setters) do 121 | setters[k2] = v2 122 | end 123 | end 124 | end 125 | 126 | class.__name = name 127 | class.__class = class 128 | class.__bases = bases 129 | class.__getters = getters 130 | class.__setters = setters 131 | 132 | local pool = {} 133 | local n = #pool 134 | 135 | function class:__index(k) 136 | if getters[k] then 137 | return getters[k](self) 138 | elseif pool[k] then 139 | return rawget(self, pool[k]) 140 | else 141 | return class[k] 142 | end 143 | end 144 | 145 | function class:__newindex(k, v) 146 | if setters[k] then 147 | return setters[k](self, v) 148 | elseif class[k] or getters[k] then 149 | return error(format('Cannot overwrite protected property: %s.%s', name, k)) 150 | elseif k:find('_', 1, true) ~= 1 then 151 | return error(format('Cannot write property to object without leading underscore: %s.%s', name, k)) 152 | else 153 | if not pool[k] then 154 | n = n + 1 155 | pool[k] = n 156 | end 157 | return rawset(self, pool[k], v) 158 | end 159 | end 160 | 161 | names[name] = class 162 | 163 | return class, getters, setters 164 | 165 | end}) 166 | -------------------------------------------------------------------------------- /libs/containers/GuildVoiceChannel.lua: -------------------------------------------------------------------------------- 1 | --[=[ 2 | @c GuildVoiceChannel x GuildChannel 3 | @d Represents a voice channel in a Discord guild, where guild members can connect 4 | and communicate via voice chat. 5 | ]=] 6 | 7 | local json = require('json') 8 | 9 | local GuildChannel = require('containers/abstract/GuildChannel') 10 | local VoiceConnection = require('voice/VoiceConnection') 11 | local TableIterable = require('iterables/TableIterable') 12 | 13 | local GuildVoiceChannel, get = require('class')('GuildVoiceChannel', GuildChannel) 14 | 15 | function GuildVoiceChannel:__init(data, parent) 16 | GuildChannel.__init(self, data, parent) 17 | end 18 | 19 | --[=[ 20 | @m setBitrate 21 | @t http 22 | @p bitrate number 23 | @r boolean 24 | @d Sets the channel's audio bitrate in bits per second (bps). This must be between 25 | 8000 and 96000 (or 128000 for partnered servers). If `nil` is passed, the 26 | default is set, which is 64000. 27 | ]=] 28 | function GuildVoiceChannel:setBitrate(bitrate) 29 | return self:_modify({bitrate = bitrate or json.null}) 30 | end 31 | 32 | --[=[ 33 | @m setUserLimit 34 | @t http 35 | @p user_limit number 36 | @r boolean 37 | @d Sets the channel's user limit. This must be between 0 and 99 (where 0 is 38 | unlimited). If `nil` is passed, the default is set, which is 0. 39 | ]=] 40 | function GuildVoiceChannel:setUserLimit(user_limit) 41 | return self:_modify({user_limit = user_limit or json.null}) 42 | end 43 | 44 | --[=[ 45 | @m join 46 | @t ws 47 | @r VoiceConnection 48 | @d Join this channel and form a connection to the Voice Gateway. 49 | ]=] 50 | function GuildVoiceChannel:join() 51 | 52 | local success, err 53 | 54 | local connection = self._connection 55 | 56 | if connection then 57 | 58 | if connection._ready then 59 | return connection 60 | end 61 | 62 | else 63 | 64 | local guild = self._parent 65 | local client = guild._parent 66 | 67 | success, err = client._shards[guild.shardId]:updateVoice(guild._id, self._id) 68 | 69 | if not success then 70 | return nil, err 71 | end 72 | 73 | connection = guild._connection 74 | 75 | if not connection then 76 | connection = VoiceConnection(self) 77 | guild._connection = connection 78 | end 79 | 80 | self._connection = connection 81 | 82 | end 83 | 84 | success, err = connection:_await() 85 | 86 | if success then 87 | return connection 88 | else 89 | return nil, err 90 | end 91 | 92 | end 93 | 94 | --[=[ 95 | @m leave 96 | @t http 97 | @r boolean 98 | @d Leave this channel if there is an existing voice connection to it. 99 | Equivalent to GuildVoiceChannel.connection:close() 100 | ]=] 101 | function GuildVoiceChannel:leave() 102 | if self._connection then 103 | return self._connection:close() 104 | else 105 | return false, 'No voice connection exists for this channel' 106 | end 107 | end 108 | 109 | --[=[@p bitrate number The channel's bitrate in bits per second (bps). This should be between 8000 and 110 | 96000 (or 128000 for partnered servers).]=] 111 | function get.bitrate(self) 112 | return self._bitrate 113 | end 114 | 115 | --[=[@p userLimit number The amount of users allowed to be in this channel. 116 | Users with `moveMembers` permission ignore this limit.]=] 117 | function get.userLimit(self) 118 | return self._user_limit 119 | end 120 | 121 | --[=[@p connectedMembers TableIterable An iterable of all users connected to the channel.]=] 122 | function get.connectedMembers(self) 123 | if not self._connected_members then 124 | local id = self._id 125 | local members = self._parent._members 126 | self._connected_members = TableIterable(self._parent._voice_states, function(state) 127 | return state.channel_id == id and members:get(state.user_id) 128 | end) 129 | end 130 | return self._connected_members 131 | end 132 | 133 | --[=[@p connection VoiceConnection/nil The VoiceConnection for this channel if one exists.]=] 134 | function get.connection(self) 135 | return self._connection 136 | end 137 | 138 | return GuildVoiceChannel 139 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Discordia 2 | 3 | **Discord API library written in Lua for the Luvit runtime environment** 4 | 5 | ### Introduction 6 | 7 | **[Discord](https://discord.com/)** is a freeware, multi-platform, voice and text client. It has a [documented RESTful API](https://discord.com/developers/docs/intro) that allows developers to make Discord bots for use on their servers. 8 | 9 | **[Luvit](https://luvit.io)** is an open-source, asynchronous I/O Lua runtime environment. It is a combination of [LuaJIT](http://luajit.com/) and [libuv](http://libuv.org/), layered with various libraries to provide server-side functionality similar to that of [Node.js](https://nodejs.org/en/), but with Lua instead of JavaScript. Luvit's companion package manager, lit, makes it easy to set up the Luvit runtime and its published libraries. 10 | 11 | Discordia is a Lua wrapper for the official Discord API, and provides a high-level, object-oriented, event-driven interface for developing Discord bots. By using Lua's native coroutines, asynchronous HTTP and WebSocket communication is internally abstracted in a way that allows end-users to write blocking-style code without blocking I/O operations. 12 | 13 | Join the [Discord API](https://discord.gg/NKM3XmF) server to discuss Discordia and other Discord libraries! 14 | 15 | Join the independent [Discordia](https://discord.gg/EzRYYDW) server for more! 16 | 17 | ### Installation 18 | 19 | - To install Luvit, visit https://luvit.io and follow the instructions provided for your platform. 20 | - To install Discordia, run `lit install SinisterRectus/discordia` 21 | - Run your bot script using, for example, `luvit bot.lua` 22 | 23 | ### Example 24 | 25 | ```lua 26 | local discordia = require('discordia') 27 | local client = discordia.Client() 28 | 29 | client:on('ready', function() 30 | print('Logged in as '.. client.user.username) 31 | end) 32 | 33 | client:on('messageCreate', function(message) 34 | if message.content == '!ping' then 35 | message.channel:send('Pong!') 36 | end 37 | end) 38 | 39 | client:run('Bot INSERT_TOKEN_HERE') 40 | ``` 41 | 42 | ### Documentation 43 | 44 | Please visit this project's [Wiki](https://github.com/SinisterRectus/Discordia/wiki) for documentation and tutorials. 45 | 46 | ### History 47 | 48 | The earliest version of Discordia, before it even had that name, was released as a [Just Cause 2 Multiplayer module](https://www.jc-mp.com/forums/index.php/topic,5936.0.html) on 7 March 2016. It utilized LuaSocket, LuaSec, and (eventually) Copas to provide basic REST functionality in a sandboxed Lua 5.2 environment. The goal was to bridge the game chat with a Discord client. Due to a lack of WSS support (at the time), the project was put on hold in favor of a general-purpose Lua library for Discord. After finishing a relatively stable version of Discordia, the JC2MP bridge was re-designed to connect with Discordia via inter-process communication. 49 | 50 | ### FAQs 51 | 52 | Why Lua? 53 | - Lua is a lightweight scripting language that tends to be beginner-friendly, but powerful in the hands of an advanced user at the same time. Although Lua might not have the same popularity as that of other scripting languages such as Python or JavaScript, Lua's expandability makes it equally as capable as the others, while remaining easy-to-use and often more resource efficient. 54 | 55 | Why Luvit? 56 | - Luvit makes Lua web development an easy task on multiple platforms. Its [installation](https://luvit.io/install.html) process is (optionally) automated and uses pre-built [luvi cores](https://github.com/luvit/luvi/releases) when available. It also comes with many libraries essential to async I/O programming and networking. Compared to Node.js, Luvit [advertises](https://luvit.io/blog/luvit-reborn.html) similar speed, but reduced memory consumption. Compared to other Discord libraries, Discordia is expected to perform well due Luvit's use of LuaJIT, although it has not been benchmarked. 57 | 58 | Can I run this on a different Lua distribution? 59 | - The development and deployment of Discordia relies on the Luvit framework and its package manager. Porting Discordia and its dependencies to classic Lua or LuaJIT may be possible, but this is not a current project goal. 60 | 61 | How can I contribute? 62 | - Pull requests are welcomed, but please check with the library author before starting a major implementation. Contributions to the Wiki are helpful, too. 63 | 64 | Are there other Discord libraries? 65 | - Absolutely. Check the official [libraries](https://discord.com/developers/docs/topics/community-resources) page of the Discord API documentation or the unofficial Discord API server linked above. 66 | -------------------------------------------------------------------------------- /libs/containers/Webhook.lua: -------------------------------------------------------------------------------- 1 | --[=[ 2 | @c Webhook x Snowflake 3 | @d Represents a handle used to send webhook messages to a guild text channel in a 4 | one-way fashion. This class defines methods and properties for managing the 5 | webhook, not for sending messages. 6 | ]=] 7 | 8 | local json = require('json') 9 | local enums = require('enums') 10 | local Snowflake = require('containers/abstract/Snowflake') 11 | local User = require('containers/User') 12 | local Resolver = require('client/Resolver') 13 | 14 | local defaultAvatar = assert(enums.defaultAvatar) 15 | 16 | local Webhook, get = require('class')('Webhook', Snowflake) 17 | 18 | function Webhook:__init(data, parent) 19 | Snowflake.__init(self, data, parent) 20 | self._user = data.user and self.client._users:_insert(data.user) -- DNE if getting by token 21 | end 22 | 23 | function Webhook:_modify(payload) 24 | local data, err = self.client._api:modifyWebhook(self._id, payload) 25 | if data then 26 | self:_load(data) 27 | return true 28 | else 29 | return false, err 30 | end 31 | end 32 | 33 | --[=[ 34 | @m getAvatarURL 35 | @t mem 36 | @op size number 37 | @op ext string 38 | @r string 39 | @d Returns a URL that can be used to view the webhooks's full avatar. If provided, 40 | the size must be a power of 2 while the extension must be a valid image format. 41 | If the webhook does not have a custom avatar, the default URL is returned. 42 | ]=] 43 | function Webhook:getAvatarURL(size, ext) 44 | return User.getAvatarURL(self, size, ext) 45 | end 46 | 47 | --[=[ 48 | @m getDefaultAvatarURL 49 | @t mem 50 | @op size number 51 | @r string 52 | @d Returns a URL that can be used to view the webhooks's default avatar. 53 | ]=] 54 | function Webhook:getDefaultAvatarURL(size) 55 | return User.getDefaultAvatarURL(self, size) 56 | end 57 | 58 | --[=[ 59 | @m setName 60 | @t http 61 | @p name string 62 | @r boolean 63 | @d Sets the webhook's name. This must be between 2 and 32 characters in length. 64 | ]=] 65 | function Webhook:setName(name) 66 | return self:_modify({name = name or json.null}) 67 | end 68 | 69 | --[=[ 70 | @m setAvatar 71 | @t http 72 | @p avatar Base64-Resolvable 73 | @r boolean 74 | @d Sets the webhook's avatar. If `nil` is passed, the avatar is removed. 75 | ]=] 76 | function Webhook:setAvatar(avatar) 77 | avatar = avatar and Resolver.base64(avatar) 78 | return self:_modify({avatar = avatar or json.null}) 79 | end 80 | 81 | --[=[ 82 | @m delete 83 | @t http 84 | @r boolean 85 | @d Permanently deletes the webhook. This cannot be undone! 86 | ]=] 87 | function Webhook:delete() 88 | local data, err = self.client._api:deleteWebhook(self._id) 89 | if data then 90 | return true 91 | else 92 | return false, err 93 | end 94 | end 95 | 96 | --[=[@p guildId string The ID of the guild in which this webhook exists.]=] 97 | function get.guildId(self) 98 | return self._guild_id 99 | end 100 | 101 | --[=[@p channelId string The ID of the channel in which this webhook exists.]=] 102 | function get.channelId(self) 103 | return self._channel_id 104 | end 105 | 106 | --[=[@p user User/nil The user that created this webhook.]=] 107 | function get.user(self) 108 | return self._user 109 | end 110 | 111 | --[=[@p token string The token that can be used to access this webhook.]=] 112 | function get.token(self) 113 | return self._token 114 | end 115 | 116 | --[=[@p name string The name of the webhook. This should be between 2 and 32 characters in length.]=] 117 | function get.name(self) 118 | return self._name 119 | end 120 | 121 | --[=[@p type number The type of the webhook. See the `webhookType` enum for a human-readable representation.]=] 122 | function get.type(self) 123 | return self._type 124 | end 125 | 126 | --[=[@p avatar string/nil The hash for the webhook's custom avatar, if one is set.]=] 127 | function get.avatar(self) 128 | return self._avatar 129 | end 130 | 131 | --[=[@p avatarURL string Equivalent to the result of calling `Webhook:getAvatarURL()`.]=] 132 | function get.avatarURL(self) 133 | return self:getAvatarURL() 134 | end 135 | 136 | --[=[@p defaultAvatar number The default avatar for the webhook. See the `defaultAvatar` enumeration for 137 | a human-readable representation. This should always be `defaultAvatar.blurple`.]=] 138 | function get.defaultAvatar() 139 | return defaultAvatar.blurple 140 | end 141 | 142 | --[=[@p defaultAvatarURL string Equivalent to the result of calling `Webhook:getDefaultAvatarURL()`.]=] 143 | function get.defaultAvatarURL(self) 144 | return self:getDefaultAvatarURL() 145 | end 146 | 147 | return Webhook 148 | -------------------------------------------------------------------------------- /libs/containers/Emoji.lua: -------------------------------------------------------------------------------- 1 | --[=[ 2 | @c Emoji x Snowflake 3 | @d Represents a custom emoji object usable in message content and reactions. 4 | Standard unicode emojis do not have a class; they are just strings. 5 | ]=] 6 | 7 | local Snowflake = require('containers/abstract/Snowflake') 8 | local Resolver = require('client/Resolver') 9 | local ArrayIterable = require('iterables/ArrayIterable') 10 | local json = require('json') 11 | 12 | local format = string.format 13 | 14 | local Emoji, get = require('class')('Emoji', Snowflake) 15 | 16 | function Emoji:__init(data, parent) 17 | Snowflake.__init(self, data, parent) 18 | self.client._emoji_map[self._id] = parent 19 | return self:_loadMore(data) 20 | end 21 | 22 | function Emoji:_load(data) 23 | Snowflake._load(self, data) 24 | return self:_loadMore(data) 25 | end 26 | 27 | function Emoji:_loadMore(data) 28 | if data.roles then 29 | local roles = #data.roles > 0 and data.roles or nil 30 | if self._roles then 31 | self._roles._array = roles 32 | else 33 | self._roles_raw = roles 34 | end 35 | end 36 | end 37 | 38 | function Emoji:_modify(payload) 39 | local data, err = self.client._api:modifyGuildEmoji(self._parent._id, self._id, payload) 40 | if data then 41 | self:_load(data) 42 | return true 43 | else 44 | return false, err 45 | end 46 | end 47 | 48 | --[=[ 49 | @m setName 50 | @t http 51 | @p name string 52 | @r boolean 53 | @d Sets the emoji's name. The name must be between 2 and 32 characters in length. 54 | ]=] 55 | function Emoji:setName(name) 56 | return self:_modify({name = name or json.null}) 57 | end 58 | 59 | --[=[ 60 | @m setRoles 61 | @t http 62 | @p roles Role-ID-Resolvables 63 | @r boolean 64 | @d Sets the roles that can use the emoji. 65 | ]=] 66 | function Emoji:setRoles(roles) 67 | roles = Resolver.roleIds(roles) 68 | return self:_modify({roles = roles or json.null}) 69 | end 70 | 71 | --[=[ 72 | @m delete 73 | @t http 74 | @r boolean 75 | @d Permanently deletes the emoji. This cannot be undone! 76 | ]=] 77 | function Emoji:delete() 78 | local data, err = self.client._api:deleteGuildEmoji(self._parent._id, self._id) 79 | if data then 80 | local cache = self._parent._emojis 81 | if cache then 82 | cache:_delete(self._id) 83 | end 84 | return true 85 | else 86 | return false, err 87 | end 88 | end 89 | 90 | --[=[ 91 | @m hasRole 92 | @t mem 93 | @p id Role-ID-Resolvable 94 | @r boolean 95 | @d Returns whether or not the provided role is allowed to use the emoji. 96 | ]=] 97 | function Emoji:hasRole(id) 98 | id = Resolver.roleId(id) 99 | local roles = self._roles and self._roles._array or self._roles_raw 100 | if roles then 101 | for _, v in ipairs(roles) do 102 | if v == id then 103 | return true 104 | end 105 | end 106 | end 107 | return false 108 | end 109 | 110 | --[=[@p name string The name of the emoji.]=] 111 | function get.name(self) 112 | return self._name 113 | end 114 | 115 | --[=[@p guild Guild The guild in which the emoji exists.]=] 116 | function get.guild(self) 117 | return self._parent 118 | end 119 | 120 | --[=[@p mentionString string A string that, when included in a message content, may resolve as an emoji image 121 | in the official Discord client.]=] 122 | function get.mentionString(self) 123 | local fmt = self._animated and '' or '<:%s>' 124 | return format(fmt, self.hash) 125 | end 126 | 127 | --[=[@p url string The URL that can be used to view a full version of the emoji.]=] 128 | function get.url(self) 129 | local ext = self._animated and 'gif' or 'png' 130 | return format('https://cdn.discordapp.com/emojis/%s.%s', self._id, ext) 131 | end 132 | 133 | --[=[@p managed boolean Whether this emoji is managed by an integration such as Twitch or YouTube.]=] 134 | function get.managed(self) 135 | return self._managed 136 | end 137 | 138 | --[=[@p requireColons boolean Whether this emoji requires colons to be used in the official Discord client.]=] 139 | function get.requireColons(self) 140 | return self._require_colons 141 | end 142 | 143 | --[=[@p hash string String with the format `name:id`, used in HTTP requests. 144 | This is different from `Emoji:__hash`, which returns only the Snowflake ID. 145 | ]=] 146 | function get.hash(self) 147 | return self._name .. ':' .. self._id 148 | end 149 | 150 | --[=[@p animated boolean Whether this emoji is animated.]=] 151 | function get.animated(self) 152 | return self._animated 153 | end 154 | 155 | --[=[@p roles ArrayIterable An iterable array of roles that may be required to use this emoji, generally 156 | related to integration-managed emojis. Object order is not guaranteed.]=] 157 | function get.roles(self) 158 | if not self._roles then 159 | local roles = self._parent._roles 160 | self._roles = ArrayIterable(self._roles_raw, function(id) 161 | return roles:get(id) 162 | end) 163 | self._roles_raw = nil 164 | end 165 | return self._roles 166 | end 167 | 168 | return Emoji 169 | -------------------------------------------------------------------------------- /libs/containers/Reaction.lua: -------------------------------------------------------------------------------- 1 | --[=[ 2 | @c Reaction x Container 3 | @d Represents an emoji that has been used to react to a Discord text message. Both 4 | standard and custom emojis can be used. 5 | ]=] 6 | 7 | local json = require('json') 8 | local Container = require('containers/abstract/Container') 9 | local SecondaryCache = require('iterables/SecondaryCache') 10 | local Resolver = require('client/Resolver') 11 | 12 | local null = json.null 13 | local format = string.format 14 | 15 | local Reaction, get = require('class')('Reaction', Container) 16 | 17 | function Reaction:__init(data, parent) 18 | Container.__init(self, data, parent) 19 | local emoji = data.emoji 20 | self._emoji_id = emoji.id ~= null and emoji.id or nil 21 | self._emoji_name = emoji.name 22 | if emoji.animated ~= null and emoji.animated ~= nil then -- not always present 23 | self._emoji_animated = emoji.animated 24 | end 25 | end 26 | 27 | --[=[ 28 | @m __hash 29 | @r string 30 | @d Returns `Reaction.emojiId or Reaction.emojiName` 31 | ]=] 32 | function Reaction:__hash() 33 | return self._emoji_id or self._emoji_name 34 | end 35 | 36 | local function getUsers(self, query) 37 | local emoji = Resolver.emoji(self) 38 | local message = self._parent 39 | local channel = message._parent 40 | local data, err = self.client._api:getReactions(channel._id, message._id, emoji, query) 41 | if data then 42 | return SecondaryCache(data, self.client._users) 43 | else 44 | return nil, err 45 | end 46 | end 47 | 48 | --[=[ 49 | @m getUsers 50 | @t http 51 | @op limit number 52 | @r SecondaryCache 53 | @d Returns a newly constructed cache of all users that have used this reaction in 54 | its parent message. The cache is not automatically updated via gateway events, 55 | but the internally referenced user objects may be updated. You must call this 56 | method again to guarantee that the objects are update to date. 57 | ]=] 58 | function Reaction:getUsers(limit) 59 | return getUsers(self, limit and {limit = limit}) 60 | end 61 | 62 | --[=[ 63 | @m getUsersBefore 64 | @t http 65 | @p id User-ID-Resolvable 66 | @op limit number 67 | @r SecondaryCache 68 | @d Returns a newly constructed cache of all users that have used this reaction before the specified id in 69 | its parent message. The cache is not automatically updated via gateway events, 70 | but the internally referenced user objects may be updated. You must call this 71 | method again to guarantee that the objects are update to date. 72 | ]=] 73 | function Reaction:getUsersBefore(id, limit) 74 | id = Resolver.userId(id) 75 | return getUsers(self, {before = id, limit = limit}) 76 | end 77 | 78 | --[=[ 79 | @m getUsersAfter 80 | @t http 81 | @p id User-ID-Resolvable 82 | @op limit number 83 | @r SecondaryCache 84 | @d Returns a newly constructed cache of all users that have used this reaction 85 | after the specified id in its parent message. The cache is not automatically 86 | updated via gateway events, but the internally referenced user objects may be 87 | updated. You must call this method again to guarantee that the objects are update to date. 88 | ]=] 89 | function Reaction:getUsersAfter(id, limit) 90 | id = Resolver.userId(id) 91 | return getUsers(self, {after = id, limit = limit}) 92 | end 93 | 94 | --[=[ 95 | @m delete 96 | @t http 97 | @op id User-ID-Resolvable 98 | @r boolean 99 | @d Equivalent to `Reaction.message:removeReaction(Reaction)` 100 | ]=] 101 | function Reaction:delete(id) 102 | return self._parent:removeReaction(self, id) 103 | end 104 | 105 | --[=[@p emojiId string/nil The ID of the emoji used in this reaction if it is a custom emoji.]=] 106 | function get.emojiId(self) 107 | return self._emoji_id 108 | end 109 | 110 | --[=[@p emojiName string The name of the emoji used in this reaction. 111 | This will be the raw string for a standard emoji.]=] 112 | function get.emojiName(self) 113 | return self._emoji_name 114 | end 115 | 116 | --[=[@p emojiHash string The discord hash for the emoji used in this reaction. 117 | This will be the raw string for a standard emoji.]=] 118 | function get.emojiHash(self) 119 | if self._emoji_id then 120 | return self._emoji_name .. ':' .. self._emoji_id 121 | else 122 | return self._emoji_name 123 | end 124 | end 125 | 126 | --[=[@p emojiURL string/nil string The URL that can be used to view a full 127 | version of the emoji used in this reaction if it is a custom emoji.]=] 128 | function get.emojiURL(self) 129 | local id = self._emoji_id 130 | local ext = self._emoji_animated and 'gif' or 'png' 131 | return id and format('https://cdn.discordapp.com/emojis/%s.%s', id, ext) or nil 132 | end 133 | 134 | --[=[@p me boolean Whether the current user has used this reaction.]=] 135 | function get.me(self) 136 | return self._me 137 | end 138 | 139 | --[=[@p count number The total number of users that have used this reaction.]=] 140 | function get.count(self) 141 | return self._count 142 | end 143 | 144 | --[=[@p message Message The message on which this reaction exists.]=] 145 | function get.message(self) 146 | return self._parent 147 | end 148 | 149 | return Reaction 150 | -------------------------------------------------------------------------------- /libs/containers/Activity.lua: -------------------------------------------------------------------------------- 1 | --[=[ 2 | @c Activity 3 | @d Represents a Discord user's presence data, either an application or streaming 4 | presence or a rich presence. Most if not all properties may be nil. 5 | ]=] 6 | 7 | local Container = require('containers/abstract/Container') 8 | 9 | local format = string.format 10 | 11 | local Activity, get = require('class')('Activity', Container) 12 | 13 | function Activity:__init(data, parent) 14 | Container.__init(self, data, parent) 15 | return self:_loadMore(data) 16 | end 17 | 18 | function Activity:_load(data) 19 | Container._load(self, data) 20 | return self:_loadMore(data) 21 | end 22 | 23 | function Activity:_loadMore(data) 24 | local timestamps = data.timestamps 25 | self._start = timestamps and timestamps.start 26 | self._stop = timestamps and timestamps['end'] -- thanks discord 27 | local assets = data.assets 28 | self._small_text = assets and assets.small_text 29 | self._large_text = assets and assets.large_text 30 | self._small_image = assets and assets.small_image 31 | self._large_image = assets and assets.large_image 32 | local party = data.party 33 | self._party_id = party and party.id 34 | self._party_size = party and party.size and party.size[1] 35 | self._party_max = party and party.size and party.size[2] 36 | local emoji = data.emoji 37 | self._emoji_name = emoji and emoji.name 38 | self._emoji_id = emoji and emoji.id 39 | self._emoji_animated = emoji and emoji.animated 40 | end 41 | 42 | --[=[ 43 | @m __hash 44 | @r string 45 | @d Returns `Activity.parent:__hash()` 46 | ]=] 47 | function Activity:__hash() 48 | return self._parent:__hash() 49 | end 50 | 51 | --[=[@p start number/nil The Unix timestamp for when this Rich Presence activity was started.]=] 52 | function get.start(self) 53 | return self._start 54 | end 55 | 56 | --[=[@p stop number/nil The Unix timestamp for when this Rich Presence activity was stopped.]=] 57 | function get.stop(self) 58 | return self._stop 59 | end 60 | 61 | --[=[@p name string/nil The name of the activity in which the user is currently engaged.]=] 62 | function get.name(self) 63 | return self._name 64 | end 65 | 66 | --[=[@p type number/nil The user's activity type. See the `activityType` 67 | enumeration for a human-readable representation.]=] 68 | function get.type(self) 69 | return self._type 70 | end 71 | 72 | --[=[@p url string/nil The URL for a user's streaming activity.]=] 73 | function get.url(self) 74 | return self._url 75 | end 76 | 77 | --[=[@p applicationId string/nil The application id controlling this Rich Presence activity.]=] 78 | function get.applicationId(self) 79 | return self._application_id 80 | end 81 | 82 | --[=[@p state string/nil string for the Rich Presence state section.]=] 83 | function get.state(self) 84 | return self._state 85 | end 86 | 87 | --[=[@p details string/nil string for the Rich Presence details section.]=] 88 | function get.details(self) 89 | return self._details 90 | end 91 | 92 | --[=[@p textSmall string/nil string for the Rich Presence small image text.]=] 93 | function get.textSmall(self) 94 | return self._small_text 95 | end 96 | 97 | --[=[@p textLarge string/nil string for the Rich Presence large image text.]=] 98 | function get.textLarge(self) 99 | return self._large_text 100 | end 101 | 102 | --[=[@p imageSmall string/nil URL for the Rich Presence small image.]=] 103 | function get.imageSmall(self) 104 | return self._small_image 105 | end 106 | 107 | --[=[@p imageLarge string/nil URL for the Rich Presence large image.]=] 108 | function get.imageLarge(self) 109 | return self._large_image 110 | end 111 | 112 | --[=[@p partyId string/nil Party id for this Rich Presence.]=] 113 | function get.partyId(self) 114 | return self._party_id 115 | end 116 | 117 | --[=[@p partySize number/nil Size of the Rich Presence party.]=] 118 | function get.partySize(self) 119 | return self._party_size 120 | end 121 | 122 | --[=[@p partyMax number/nil Max size for the Rich Presence party.]=] 123 | function get.partyMax(self) 124 | return self._party_max 125 | end 126 | 127 | --[=[@p emojiId string/nil The ID of the emoji used in this presence if one is 128 | set and if it is a custom emoji.]=] 129 | function get.emojiId(self) 130 | return self._emoji_id 131 | end 132 | 133 | --[=[@p emojiName string/nil The name of the emoji used in this presence if one 134 | is set and if it has a custom emoji. This will be the raw string for a standard emoji.]=] 135 | function get.emojiName(self) 136 | return self._emoji_name 137 | end 138 | 139 | --[=[@p emojiHash string/nil The discord hash for the emoji used in this presence if one is 140 | set. This will be the raw string for a standard emoji.]=] 141 | function get.emojiHash(self) 142 | if self._emoji_id then 143 | return self._emoji_name .. ':' .. self._emoji_id 144 | else 145 | return self._emoji_name 146 | end 147 | end 148 | 149 | --[=[@p emojiURL string/nil string The URL that can be used to view a full 150 | version of the emoji used in this activity if one is set and if it is a custom emoji.]=] 151 | function get.emojiURL(self) 152 | local id = self._emoji_id 153 | local ext = self._emoji_animated and 'gif' or 'png' 154 | return id and format('https://cdn.discordapp.com/emojis/%s.%s', id, ext) or nil 155 | end 156 | 157 | return Activity 158 | -------------------------------------------------------------------------------- /libs/client/Resolver.lua: -------------------------------------------------------------------------------- 1 | local fs = require('fs') 2 | local ffi = require('ffi') 3 | local ssl = require('openssl') 4 | local class = require('class') 5 | local enums = require('enums') 6 | 7 | local permission = assert(enums.permission) 8 | local gatewayIntent = assert(enums.gatewayIntent) 9 | local actionType = assert(enums.actionType) 10 | local messageFlag = assert(enums.messageFlag) 11 | local base64 = ssl.base64 12 | local readFileSync = fs.readFileSync 13 | local classes = class.classes 14 | local isInstance = class.isInstance 15 | local isObject = class.isObject 16 | local insert = table.insert 17 | local format = string.format 18 | 19 | local Resolver = {} 20 | 21 | local istype = ffi.istype 22 | local int64_t = ffi.typeof('int64_t') 23 | local uint64_t = ffi.typeof('uint64_t') 24 | 25 | local function int(obj) 26 | local t = type(obj) 27 | if t == 'string' then 28 | if tonumber(obj) then 29 | return obj 30 | end 31 | elseif t == 'cdata' then 32 | if istype(int64_t, obj) or istype(uint64_t, obj) then 33 | return tostring(obj):match('%d*') 34 | end 35 | elseif t == 'number' then 36 | return format('%i', obj) 37 | elseif isInstance(obj, classes.Date) then 38 | return obj:toSnowflake() 39 | end 40 | end 41 | 42 | function Resolver.userId(obj) 43 | if isObject(obj) then 44 | if isInstance(obj, classes.User) then 45 | return obj.id 46 | elseif isInstance(obj, classes.Member) then 47 | return obj.user.id 48 | elseif isInstance(obj, classes.Message) then 49 | return obj.author.id 50 | elseif isInstance(obj, classes.Guild) then 51 | return obj.ownerId 52 | end 53 | end 54 | return int(obj) 55 | end 56 | 57 | function Resolver.messageId(obj) 58 | if isInstance(obj, classes.Message) then 59 | return obj.id 60 | end 61 | return int(obj) 62 | end 63 | 64 | function Resolver.channelId(obj) 65 | if isInstance(obj, classes.Channel) then 66 | return obj.id 67 | end 68 | return int(obj) 69 | end 70 | 71 | function Resolver.roleId(obj) 72 | if isInstance(obj, classes.Role) then 73 | return obj.id 74 | end 75 | return int(obj) 76 | end 77 | 78 | function Resolver.emojiId(obj) 79 | if isInstance(obj, classes.Emoji) then 80 | return obj.id 81 | elseif isInstance(obj, classes.Reaction) then 82 | return obj.emojiId 83 | elseif isInstance(obj, classes.Activity) then 84 | return obj.emojiId 85 | end 86 | return int(obj) 87 | end 88 | 89 | function Resolver.stickerId(obj) 90 | if isInstance(obj, classes.Sticker) then 91 | return obj.id 92 | end 93 | return int(obj) 94 | end 95 | 96 | function Resolver.guildId(obj) 97 | if isInstance(obj, classes.Guild) then 98 | return obj.id 99 | end 100 | return int(obj) 101 | end 102 | 103 | function Resolver.entryId(obj) 104 | if isInstance(obj, classes.AuditLogEntry) then 105 | return obj.id 106 | end 107 | return int(obj) 108 | end 109 | 110 | function Resolver.messageIds(objs) 111 | local ret = {} 112 | if isInstance(objs, classes.Iterable) then 113 | for obj in objs:iter() do 114 | insert(ret, Resolver.messageId(obj)) 115 | end 116 | elseif type(objs) == 'table' then 117 | for _, obj in pairs(objs) do 118 | insert(ret, Resolver.messageId(obj)) 119 | end 120 | end 121 | return ret 122 | end 123 | 124 | function Resolver.roleIds(objs) 125 | local ret = {} 126 | if isInstance(objs, classes.Iterable) then 127 | for obj in objs:iter() do 128 | insert(ret, Resolver.roleId(obj)) 129 | end 130 | elseif type(objs) == 'table' then 131 | for _, obj in pairs(objs) do 132 | insert(ret, Resolver.roleId(obj)) 133 | end 134 | end 135 | return ret 136 | end 137 | 138 | function Resolver.emoji(obj) 139 | if isInstance(obj, classes.Emoji) then 140 | return obj.hash 141 | elseif isInstance(obj, classes.Reaction) then 142 | return obj.emojiHash 143 | elseif isInstance(obj, classes.Activity) then 144 | return obj.emojiHash 145 | end 146 | return tostring(obj) 147 | end 148 | 149 | function Resolver.sticker(obj) 150 | if isInstance(obj, classes.Sticker) then 151 | return obj.hash 152 | end 153 | return tostring(obj) 154 | end 155 | 156 | function Resolver.color(obj) 157 | if isInstance(obj, classes.Color) then 158 | return obj.value 159 | end 160 | return tonumber(obj) 161 | end 162 | 163 | function Resolver.permissions(obj) 164 | if isInstance(obj, classes.Permissions) then 165 | return obj.value 166 | end 167 | return tonumber(obj) 168 | end 169 | 170 | function Resolver.permission(obj) 171 | local t = type(obj) 172 | local n = nil 173 | if t == 'string' then 174 | n = permission[obj] 175 | elseif t == 'number' then 176 | n = permission(obj) and obj 177 | end 178 | return n 179 | end 180 | 181 | function Resolver.gatewayIntent(obj) 182 | local t = type(obj) 183 | local n = nil 184 | if t == 'string' then 185 | n = gatewayIntent[obj] 186 | elseif t == 'number' then 187 | n = gatewayIntent(obj) and obj 188 | end 189 | return n 190 | end 191 | 192 | function Resolver.actionType(obj) 193 | local t = type(obj) 194 | local n = nil 195 | if t == 'string' then 196 | n = actionType[obj] 197 | elseif t == 'number' then 198 | n = actionType(obj) and obj 199 | end 200 | return n 201 | end 202 | 203 | function Resolver.messageFlag(obj) 204 | local t = type(obj) 205 | local n = nil 206 | if t == 'string' then 207 | n = messageFlag[obj] 208 | elseif t == 'number' then 209 | n = messageFlag(obj) and obj 210 | end 211 | return n 212 | end 213 | 214 | function Resolver.base64(obj) 215 | if type(obj) == 'string' then 216 | if obj:find('data:.*;base64,') == 1 then 217 | return obj 218 | end 219 | local data, err = readFileSync(obj) 220 | if not data then 221 | return nil, err 222 | end 223 | return 'data:;base64,' .. base64(data) 224 | end 225 | return nil 226 | end 227 | 228 | return Resolver 229 | -------------------------------------------------------------------------------- /libs/containers/GuildTextChannel.lua: -------------------------------------------------------------------------------- 1 | --[=[ 2 | @c GuildTextChannel x GuildChannel x TextChannel 3 | @d Represents a text channel in a Discord guild, where guild members and webhooks 4 | can send and receive messages. 5 | ]=] 6 | 7 | local json = require('json') 8 | 9 | local GuildChannel = require('containers/abstract/GuildChannel') 10 | local TextChannel = require('containers/abstract/TextChannel') 11 | local FilteredIterable = require('iterables/FilteredIterable') 12 | local Webhook = require('containers/Webhook') 13 | local Cache = require('iterables/Cache') 14 | local Resolver = require('client/Resolver') 15 | 16 | local GuildTextChannel, get = require('class')('GuildTextChannel', GuildChannel, TextChannel) 17 | 18 | function GuildTextChannel:__init(data, parent) 19 | GuildChannel.__init(self, data, parent) 20 | TextChannel.__init(self, data, parent) 21 | end 22 | 23 | function GuildTextChannel:_load(data) 24 | GuildChannel._load(self, data) 25 | TextChannel._load(self, data) 26 | end 27 | 28 | --[=[ 29 | @m createWebhook 30 | @t http 31 | @p name string 32 | @r Webhook 33 | @d Creates a webhook for this channel. The name must be between 2 and 32 characters 34 | in length. 35 | ]=] 36 | function GuildTextChannel:createWebhook(name) 37 | local data, err = self.client._api:createWebhook(self._id, {name = name}) 38 | if data then 39 | return Webhook(data, self.client) 40 | else 41 | return nil, err 42 | end 43 | end 44 | 45 | --[=[ 46 | @m getWebhooks 47 | @t http 48 | @r Cache 49 | @d Returns a newly constructed cache of all webhook objects for the channel. The 50 | cache and its objects are not automatically updated via gateway events. You must 51 | call this method again to get the updated objects. 52 | ]=] 53 | function GuildTextChannel:getWebhooks() 54 | local data, err = self.client._api:getChannelWebhooks(self._id) 55 | if data then 56 | return Cache(data, Webhook, self.client) 57 | else 58 | return nil, err 59 | end 60 | end 61 | 62 | --[=[ 63 | @m bulkDelete 64 | @t http 65 | @p messages Message-ID-Resolvables 66 | @r boolean 67 | @d Bulk deletes multiple messages, from 2 to 100, from the channel. Messages over 68 | 2 weeks old cannot be deleted and will return an error. 69 | ]=] 70 | function GuildTextChannel:bulkDelete(messages) 71 | messages = Resolver.messageIds(messages) 72 | local data, err 73 | if #messages == 1 then 74 | data, err = self.client._api:deleteMessage(self._id, messages[1]) 75 | else 76 | data, err = self.client._api:bulkDeleteMessages(self._id, {messages = messages}) 77 | end 78 | if data then 79 | return true 80 | else 81 | return false, err 82 | end 83 | end 84 | 85 | --[=[ 86 | @m setTopic 87 | @t http 88 | @p topic string 89 | @r boolean 90 | @d Sets the channel's topic. This must be between 1 and 1024 characters. Pass `nil` 91 | to remove the topic. 92 | ]=] 93 | function GuildTextChannel:setTopic(topic) 94 | return self:_modify({topic = topic or json.null}) 95 | end 96 | 97 | --[=[ 98 | @m setRateLimit 99 | @t http 100 | @p limit number 101 | @r boolean 102 | @d Sets the channel's slowmode rate limit in seconds. This must be between 0 and 120. 103 | Passing 0 or `nil` will clear the limit. 104 | ]=] 105 | function GuildTextChannel:setRateLimit(limit) 106 | return self:_modify({rate_limit_per_user = limit or json.null}) 107 | end 108 | 109 | --[=[ 110 | @m enableNSFW 111 | @t http 112 | @r boolean 113 | @d Enables the NSFW setting for the channel. NSFW channels are hidden from users 114 | until the user explicitly requests to view them. 115 | ]=] 116 | function GuildTextChannel:enableNSFW() 117 | return self:_modify({nsfw = true}) 118 | end 119 | 120 | --[=[ 121 | @m follow 122 | @t http 123 | @p targetId Channel-ID-Resolvable 124 | @r string 125 | @d Follow this News channel and publish announcements to `targetId`. 126 | Returns a 403 HTTP error if `GuildTextChannel.isNews` is false. 127 | ]=] 128 | function GuildTextChannel:follow(targetId) 129 | targetId = Resolver.channelId(targetId) 130 | local data, err = self.client._api:followNewsChannel(self._id, { 131 | webhook_channel_id = targetId, 132 | }) 133 | if data then 134 | return data.webhook_id 135 | else 136 | return nil, err 137 | end 138 | end 139 | 140 | --[=[ 141 | @m disableNSFW 142 | @t http 143 | @r boolean 144 | @d Disables the NSFW setting for the channel. NSFW channels are hidden from users 145 | until the user explicitly requests to view them. 146 | ]=] 147 | function GuildTextChannel:disableNSFW() 148 | return self:_modify({nsfw = false}) 149 | end 150 | 151 | --[=[@p topic string/nil The channel's topic. This should be between 1 and 1024 characters.]=] 152 | function get.topic(self) 153 | return self._topic 154 | end 155 | 156 | --[=[@p nsfw boolean Whether this channel is marked as NSFW (not safe for work).]=] 157 | function get.nsfw(self) 158 | return self._nsfw or false 159 | end 160 | 161 | --[=[@p rateLimit number Slowmode rate limit per guild member.]=] 162 | function get.rateLimit(self) 163 | return self._rate_limit_per_user or 0 164 | end 165 | 166 | --[=[@p isNews boolean Whether this channel is a news channel of type 5.]=] 167 | function get.isNews(self) 168 | return self._type == 5 169 | end 170 | 171 | --[=[@p members FilteredIterable A filtered iterable of guild members that have 172 | permission to read this channel. If you want to check whether a specific member 173 | has permission to read this channel, it would be better to get the member object 174 | elsewhere and use `Member:hasPermission` rather than check whether the member 175 | exists here.]=] 176 | function get.members(self) 177 | if not self._members then 178 | self._members = FilteredIterable(self._parent._members, function(m) 179 | return m:hasPermission(self, 'readMessages') 180 | end) 181 | end 182 | return self._members 183 | end 184 | 185 | return GuildTextChannel 186 | -------------------------------------------------------------------------------- /libs/voice/VoiceSocket.lua: -------------------------------------------------------------------------------- 1 | local uv = require('uv') 2 | local class = require('class') 3 | local timer = require('timer') 4 | local enums = require('enums') 5 | local sodium = require('voice/sodium') or {} 6 | 7 | local WebSocket = require('client/WebSocket') 8 | 9 | local logLevel = assert(enums.logLevel) 10 | local format = string.format 11 | local setInterval, clearInterval = timer.setInterval, timer.clearInterval 12 | local wrap = coroutine.wrap 13 | local time = os.time 14 | local unpack, pack = string.unpack, string.pack -- luacheck: ignore 15 | 16 | local SUPPORTED_ENCRYPTION_MODES = { 'aead_xchacha20_poly1305_rtpsize' } 17 | if sodium.aead_aes256_gcm then -- AEAD AES256-GCM is only available if the hardware supports it 18 | table.insert(SUPPORTED_ENCRYPTION_MODES, 1, 'aead_aes256_gcm_rtpsize') 19 | end 20 | 21 | local IDENTIFY = 0 22 | local SELECT_PROTOCOL = 1 23 | local READY = 2 24 | local HEARTBEAT = 3 25 | local DESCRIPTION = 4 26 | local SPEAKING = 5 27 | local HEARTBEAT_ACK = 6 28 | local RESUME = 7 29 | local HELLO = 8 30 | local RESUMED = 9 31 | 32 | local function checkMode(modes) 33 | for _, ENCRYPTION_MODE in ipairs(SUPPORTED_ENCRYPTION_MODES) do 34 | for _, mode in ipairs(modes) do 35 | if mode == ENCRYPTION_MODE then 36 | return mode 37 | end 38 | end 39 | end 40 | end 41 | 42 | local VoiceSocket = class('VoiceSocket', WebSocket) 43 | 44 | for name in pairs(logLevel) do 45 | VoiceSocket[name] = function(self, fmt, ...) 46 | local client = self._client 47 | return client[name](client, format('Voice : %s', fmt), ...) 48 | end 49 | end 50 | 51 | function VoiceSocket:__init(state, connection, manager) 52 | WebSocket.__init(self, manager) 53 | self._state = state 54 | self._manager = manager 55 | self._client = manager._client 56 | self._connection = connection 57 | self._session_id = state.session_id 58 | self._seq_ack = -1 59 | end 60 | 61 | function VoiceSocket:handleDisconnect() 62 | -- TODO: reconnecting and resuming 63 | self._connection:_cleanup() 64 | end 65 | 66 | function VoiceSocket:handlePayload(payload) 67 | 68 | local manager = self._manager 69 | 70 | local d = payload.d 71 | local op = payload.op 72 | 73 | if payload.seq then 74 | self._seq_ack = payload.seq 75 | end 76 | 77 | self:debug('WebSocket OP %s', op) 78 | 79 | if op == HELLO then 80 | 81 | self:info('Received HELLO') 82 | self:startHeartbeat(d.heartbeat_interval) 83 | self:identify() 84 | 85 | elseif op == READY then 86 | 87 | self:info('Received READY') 88 | local mode = checkMode(d.modes) 89 | if mode then 90 | self:debug('Selected encryption mode %q', mode) 91 | self._mode = mode 92 | self._ssrc = d.ssrc 93 | self:handshake(d.ip, d.port) 94 | else 95 | self:error('No supported encryption mode available') 96 | self:disconnect() 97 | end 98 | 99 | elseif op == RESUMED then 100 | 101 | self:info('Received RESUMED') 102 | 103 | elseif op == DESCRIPTION then 104 | 105 | if d.mode == self._mode then 106 | self._connection:_prepare(d.secret_key, self) 107 | else 108 | self:error('%q encryption mode not available', self._mode) 109 | self:disconnect() 110 | end 111 | 112 | elseif op == HEARTBEAT_ACK then 113 | 114 | manager:emit('heartbeat', nil, self._sw.milliseconds) -- TODO: id 115 | 116 | elseif op == SPEAKING then 117 | 118 | return -- TODO 119 | 120 | elseif op == 12 or op == 13 then 121 | 122 | return -- ignore 123 | 124 | elseif op then 125 | 126 | self:warning('Unhandled WebSocket payload OP %i', op) 127 | 128 | end 129 | 130 | end 131 | 132 | local function loop(self) 133 | return wrap(self.heartbeat)(self) 134 | end 135 | 136 | function VoiceSocket:startHeartbeat(interval) 137 | if self._heartbeat then 138 | clearInterval(self._heartbeat) 139 | end 140 | self._heartbeat = setInterval(interval, loop, self) 141 | end 142 | 143 | function VoiceSocket:stopHeartbeat() 144 | if self._heartbeat then 145 | clearInterval(self._heartbeat) 146 | end 147 | self._heartbeat = nil 148 | end 149 | 150 | function VoiceSocket:heartbeat() 151 | self._sw:reset() 152 | return self:_send(HEARTBEAT, { 153 | t = time(), 154 | seq_ack = self._seq_ack, 155 | }) 156 | end 157 | 158 | function VoiceSocket:identify() 159 | local state = self._state 160 | return self:_send(IDENTIFY, { 161 | server_id = state.guild_id, 162 | user_id = state.user_id, 163 | session_id = state.session_id, 164 | token = state.token, 165 | }, true) 166 | end 167 | 168 | function VoiceSocket:resume() 169 | local state = self._state 170 | return self:_send(RESUME, { 171 | server_id = state.guild_id, 172 | session_id = state.session_id, 173 | token = state.token, 174 | seq_ack = self._seq_ack, 175 | }) 176 | end 177 | 178 | function VoiceSocket:handshake(server_ip, server_port) 179 | local udp = uv.new_udp() 180 | self._udp = udp 181 | self._ip = server_ip 182 | self._port = server_port 183 | udp:recv_start(function(err, data) 184 | assert(not err, err) 185 | udp:recv_stop() 186 | local client_ip = unpack('xxxxxxxxz', data) 187 | local client_port = unpack('I2I2I4c64H', 0x1, 70, self._ssrc, self._ip, self._port) 191 | return udp:send(packet, server_ip, server_port) 192 | end 193 | 194 | function VoiceSocket:selectProtocol(address, port) 195 | return self:_send(SELECT_PROTOCOL, { 196 | protocol = 'udp', 197 | data = { 198 | address = address, 199 | port = port, 200 | mode = self._mode, 201 | } 202 | }) 203 | end 204 | 205 | function VoiceSocket:setSpeaking(speaking) 206 | return self:_send(SPEAKING, { 207 | speaking = speaking, 208 | delay = 0, 209 | ssrc = self._ssrc, 210 | }) 211 | end 212 | 213 | return VoiceSocket 214 | -------------------------------------------------------------------------------- /libs/extensions.lua: -------------------------------------------------------------------------------- 1 | --[[ NOTE: 2 | These standard library extensions are NOT used in Discordia. They are here as a 3 | convenience for those who wish to use them. 4 | 5 | There are multiple ways to implement some of these commonly used functions. 6 | Please pay attention to the implementations used here and make sure that they 7 | match your expectations. 8 | 9 | You may freely add to, remove, or edit any of the code here without any effect 10 | on the rest of the library. If you do make changes, do be careful when sharing 11 | your expectations with other users. 12 | 13 | You can inject these extensions into the standard Lua global tables by 14 | calling either the main module (ex: discordia.extensions()) or each sub-module 15 | (ex: discordia.extensions.string()) 16 | ]] 17 | 18 | local sort, concat = table.sort, table.concat 19 | local insert, remove = table.insert, table.remove 20 | local byte, char = string.byte, string.char 21 | local gmatch, match = string.gmatch, string.match 22 | local rep, find, sub = string.rep, string.find, string.sub 23 | local min, max, random = math.min, math.max, math.random 24 | local ceil, floor = math.ceil, math.floor 25 | 26 | local table = {} 27 | 28 | function table.count(tbl) 29 | local n = 0 30 | for _ in pairs(tbl) do 31 | n = n + 1 32 | end 33 | return n 34 | end 35 | 36 | function table.deepcount(tbl) 37 | local n = 0 38 | for _, v in pairs(tbl) do 39 | n = type(v) == 'table' and n + table.deepcount(v) or n + 1 40 | end 41 | return n 42 | end 43 | 44 | function table.copy(tbl) 45 | local ret = {} 46 | for k, v in pairs(tbl) do 47 | ret[k] = v 48 | end 49 | return ret 50 | end 51 | 52 | function table.deepcopy(tbl) 53 | local ret = {} 54 | for k, v in pairs(tbl) do 55 | ret[k] = type(v) == 'table' and table.deepcopy(v) or v 56 | end 57 | return ret 58 | end 59 | 60 | function table.reverse(tbl) 61 | for i = 1, #tbl do 62 | insert(tbl, i, remove(tbl)) 63 | end 64 | end 65 | 66 | function table.reversed(tbl) 67 | local ret = {} 68 | for i = #tbl, 1, -1 do 69 | insert(ret, tbl[i]) 70 | end 71 | return ret 72 | end 73 | 74 | function table.keys(tbl) 75 | local ret = {} 76 | for k in pairs(tbl) do 77 | insert(ret, k) 78 | end 79 | return ret 80 | end 81 | 82 | function table.values(tbl) 83 | local ret = {} 84 | for _, v in pairs(tbl) do 85 | insert(ret, v) 86 | end 87 | return ret 88 | end 89 | 90 | function table.randomipair(tbl) 91 | local i = random(#tbl) 92 | return i, tbl[i] 93 | end 94 | 95 | function table.randompair(tbl) 96 | local rand = random(table.count(tbl)) 97 | local n = 0 98 | for k, v in pairs(tbl) do 99 | n = n + 1 100 | if n == rand then 101 | return k, v 102 | end 103 | end 104 | end 105 | 106 | function table.sorted(tbl, fn) 107 | local ret = {} 108 | for i, v in ipairs(tbl) do 109 | ret[i] = v 110 | end 111 | sort(ret, fn) 112 | return ret 113 | end 114 | 115 | function table.search(tbl, value) 116 | for k, v in pairs(tbl) do 117 | if v == value then 118 | return k 119 | end 120 | end 121 | return nil 122 | end 123 | 124 | function table.slice(tbl, start, stop, step) 125 | local ret = {} 126 | for i = start or 1, stop or #tbl, step or 1 do 127 | insert(ret, tbl[i]) 128 | end 129 | return ret 130 | end 131 | 132 | local string = {} 133 | 134 | function string.split(str, delim) 135 | local ret = {} 136 | if not str then 137 | return ret 138 | end 139 | if not delim or delim == '' then 140 | for c in gmatch(str, '.') do 141 | insert(ret, c) 142 | end 143 | return ret 144 | end 145 | local n = 1 146 | while true do 147 | local i, j = find(str, delim, n) 148 | if not i then break end 149 | insert(ret, sub(str, n, i - 1)) 150 | n = j + 1 151 | end 152 | insert(ret, sub(str, n)) 153 | return ret 154 | end 155 | 156 | function string.trim(str) 157 | return match(str, '^%s*(.-)%s*$') 158 | end 159 | 160 | function string.pad(str, len, align, pattern) 161 | pattern = pattern or ' ' 162 | if align == 'right' then 163 | return rep(pattern, (len - #str) / #pattern) .. str 164 | elseif align == 'center' then 165 | local pad = 0.5 * (len - #str) / #pattern 166 | return rep(pattern, floor(pad)) .. str .. rep(pattern, ceil(pad)) 167 | else -- left 168 | return str .. rep(pattern, (len - #str) / #pattern) 169 | end 170 | end 171 | 172 | function string.startswith(str, pattern, plain) 173 | local start = 1 174 | return find(str, pattern, start, plain) == start 175 | end 176 | 177 | function string.endswith(str, pattern, plain) 178 | local start = #str - #pattern + 1 179 | return find(str, pattern, start, plain) == start 180 | end 181 | 182 | function string.levenshtein(str1, str2) 183 | 184 | if str1 == str2 then return 0 end 185 | 186 | local len1 = #str1 187 | local len2 = #str2 188 | 189 | if len1 == 0 then 190 | return len2 191 | elseif len2 == 0 then 192 | return len1 193 | end 194 | 195 | local matrix = {} 196 | for i = 0, len1 do 197 | matrix[i] = {[0] = i} 198 | end 199 | for j = 0, len2 do 200 | matrix[0][j] = j 201 | end 202 | 203 | for i = 1, len1 do 204 | for j = 1, len2 do 205 | local cost = byte(str1, i) == byte(str2, j) and 0 or 1 206 | matrix[i][j] = min(matrix[i-1][j] + 1, matrix[i][j-1] + 1, matrix[i-1][j-1] + cost) 207 | end 208 | end 209 | 210 | return matrix[len1][len2] 211 | 212 | end 213 | 214 | function string.random(len, mn, mx) 215 | local ret = {} 216 | mn = mn or 0 217 | mx = mx or 255 218 | for _ = 1, len do 219 | insert(ret, char(random(mn, mx))) 220 | end 221 | return concat(ret) 222 | end 223 | 224 | local math = {} 225 | 226 | function math.clamp(n, minValue, maxValue) 227 | return min(max(n, minValue), maxValue) 228 | end 229 | 230 | function math.round(n, i) 231 | local m = 10 ^ (i or 0) 232 | return floor(n * m + 0.5) / m 233 | end 234 | 235 | local ext = setmetatable({ 236 | table = table, 237 | string = string, 238 | math = math, 239 | }, {__call = function(self) 240 | for _, v in pairs(self) do 241 | v() 242 | end 243 | end}) 244 | 245 | for n, m in pairs(ext) do 246 | setmetatable(m, {__call = function(self) 247 | for k, v in pairs(self) do 248 | _G[n][k] = v 249 | end 250 | end}) 251 | end 252 | 253 | return ext 254 | -------------------------------------------------------------------------------- /libs/utils/Emitter.lua: -------------------------------------------------------------------------------- 1 | --[=[ 2 | @c Emitter 3 | @t ui 4 | @mt mem 5 | @d Implements an asynchronous event emitter where callbacks can be subscribed to 6 | specific named events. When events are emitted, the callbacks are called in the 7 | order that they were originally registered. 8 | ]=] 9 | 10 | local timer = require('timer') 11 | 12 | local wrap, yield = coroutine.wrap, coroutine.yield 13 | local resume, running = coroutine.resume, coroutine.running 14 | local insert, remove = table.insert, table.remove 15 | local setTimeout, clearTimeout = timer.setTimeout, timer.clearTimeout 16 | 17 | local Emitter = require('class')('Emitter') 18 | 19 | function Emitter:__init() 20 | self._listeners = {} 21 | end 22 | 23 | local function new(self, name, listener) 24 | local listeners = self._listeners[name] 25 | if not listeners then 26 | listeners = {} 27 | self._listeners[name] = listeners 28 | end 29 | insert(listeners, listener) 30 | return listener.fn 31 | end 32 | 33 | --[=[ 34 | @m on 35 | @p name string 36 | @p fn function 37 | @r function 38 | @d Subscribes a callback to be called every time the named event is emitted. 39 | Callbacks registered with this method will automatically be wrapped as a new 40 | coroutine when they are called. Returns the original callback for convenience. 41 | ]=] 42 | function Emitter:on(name, fn) 43 | return new(self, name, {fn = fn}) 44 | end 45 | 46 | --[=[ 47 | @m once 48 | @p name string 49 | @p fn function 50 | @r function 51 | @d Subscribes a callback to be called only the first time this event is emitted. 52 | Callbacks registered with this method will automatically be wrapped as a new 53 | coroutine when they are called. Returns the original callback for convenience. 54 | ]=] 55 | function Emitter:once(name, fn) 56 | return new(self, name, {fn = fn, once = true}) 57 | end 58 | 59 | --[=[ 60 | @m onSync 61 | @p name string 62 | @p fn function 63 | @r function 64 | @d Subscribes a callback to be called every time the named event is emitted. 65 | Callbacks registered with this method are not automatically wrapped as a 66 | coroutine. Returns the original callback for convenience. 67 | ]=] 68 | function Emitter:onSync(name, fn) 69 | return new(self, name, {fn = fn, sync = true}) 70 | end 71 | 72 | --[=[ 73 | @m onceSync 74 | @p name string 75 | @p fn function 76 | @r function 77 | @d Subscribes a callback to be called only the first time this event is emitted. 78 | Callbacks registered with this method are not automatically wrapped as a coroutine. 79 | Returns the original callback for convenience. 80 | ]=] 81 | function Emitter:onceSync(name, fn) 82 | return new(self, name, {fn = fn, once = true, sync = true}) 83 | end 84 | 85 | --[=[ 86 | @m emit 87 | @p name string 88 | @op ... * 89 | @r nil 90 | @d Emits the named event and a variable number of arguments to pass to the event callbacks. 91 | ]=] 92 | function Emitter:emit(name, ...) 93 | local listeners = self._listeners[name] 94 | if not listeners then return end 95 | for i = 1, #listeners do 96 | local listener = listeners[i] 97 | if listener then 98 | local fn = listener.fn 99 | if listener.once then 100 | listeners[i] = false 101 | end 102 | if listener.sync then 103 | fn(...) 104 | else 105 | wrap(fn)(...) 106 | end 107 | end 108 | end 109 | if listeners._removed then 110 | for i = #listeners, 1, -1 do 111 | if not listeners[i] then 112 | remove(listeners, i) 113 | end 114 | end 115 | if #listeners == 0 then 116 | self._listeners[name] = nil 117 | end 118 | listeners._removed = nil 119 | end 120 | end 121 | 122 | --[=[ 123 | @m getListeners 124 | @p name string 125 | @r function 126 | @d Returns an iterator for all callbacks registered to the named event. 127 | ]=] 128 | function Emitter:getListeners(name) 129 | local listeners = self._listeners[name] 130 | if not listeners then return function() end end 131 | local i = 0 132 | return function() 133 | while i < #listeners do 134 | i = i + 1 135 | if listeners[i] then 136 | return listeners[i].fn 137 | end 138 | end 139 | end 140 | end 141 | 142 | --[=[ 143 | @m getListenerCount 144 | @p name string 145 | @r number 146 | @d Returns the number of callbacks registered to the named event. 147 | ]=] 148 | function Emitter:getListenerCount(name) 149 | local listeners = self._listeners[name] 150 | if not listeners then return 0 end 151 | local n = 0 152 | for _, listener in ipairs(listeners) do 153 | if listener then 154 | n = n + 1 155 | end 156 | end 157 | return n 158 | end 159 | 160 | --[=[ 161 | @m removeListener 162 | @p name string 163 | @p fn function 164 | @r nil 165 | @d Unregisters all instances of the callback from the named event. 166 | ]=] 167 | function Emitter:removeListener(name, fn) 168 | local listeners = self._listeners[name] 169 | if not listeners then return end 170 | for i, listener in ipairs(listeners) do 171 | if listener and listener.fn == fn then 172 | listeners[i] = false 173 | end 174 | end 175 | listeners._removed = true 176 | end 177 | 178 | --[=[ 179 | @m removeAllListeners 180 | @p name string/nil 181 | @r nil 182 | @d Unregisters all callbacks for the emitter. If a name is passed, then only 183 | callbacks for that specific event are unregistered. 184 | ]=] 185 | function Emitter:removeAllListeners(name) 186 | if name then 187 | self._listeners[name] = nil 188 | else 189 | for k in pairs(self._listeners) do 190 | self._listeners[k] = nil 191 | end 192 | end 193 | end 194 | 195 | --[=[ 196 | @m waitFor 197 | @p name string 198 | @op timeout number 199 | @op predicate function 200 | @r boolean 201 | @r ... 202 | @d When called inside of a coroutine, this will yield the coroutine until the 203 | named event is emitted. If a timeout (in milliseconds) is provided, the function 204 | will return after the time expires, regardless of whether the event is emitted, 205 | and `false` will be returned; otherwise, `true` is returned. If a predicate is 206 | provided, events that do not pass the predicate will be ignored. 207 | ]=] 208 | function Emitter:waitFor(name, timeout, predicate) 209 | local thread = running() 210 | local fn 211 | fn = self:onSync(name, function(...) 212 | if predicate and not predicate(...) then return end 213 | if timeout then 214 | clearTimeout(timeout) 215 | end 216 | self:removeListener(name, fn) 217 | return assert(resume(thread, true, ...)) 218 | end) 219 | timeout = timeout and setTimeout(timeout, function() 220 | self:removeListener(name, fn) 221 | return assert(resume(thread, false)) 222 | end) 223 | return yield() 224 | end 225 | 226 | return Emitter 227 | -------------------------------------------------------------------------------- /libs/containers/User.lua: -------------------------------------------------------------------------------- 1 | --[=[ 2 | @c User x Snowflake 3 | @d Represents a single user of Discord, either a human or a bot, outside of any 4 | specific guild's context. 5 | ]=] 6 | 7 | local Snowflake = require('containers/abstract/Snowflake') 8 | local FilteredIterable = require('iterables/FilteredIterable') 9 | local constants = require('constants') 10 | 11 | local format = string.format 12 | local DEFAULT_AVATARS = constants.DEFAULT_AVATARS 13 | 14 | local User, get = require('class')('User', Snowflake) 15 | 16 | function User:__init(data, parent) 17 | Snowflake.__init(self, data, parent) 18 | end 19 | 20 | --[=[ 21 | @m getAvatarURL 22 | @t mem 23 | @op size number 24 | @op ext string 25 | @r string 26 | @d Returns a URL that can be used to view the user's full avatar. If provided, the 27 | size must be a power of 2 while the extension must be a valid image format. If 28 | the user does not have a custom avatar, the default URL is returned. 29 | ]=] 30 | function User:getAvatarURL(size, ext) 31 | local avatar = self._avatar 32 | if avatar then 33 | ext = ext or avatar:find('a_') == 1 and 'gif' or 'png' 34 | if size then 35 | return format('https://cdn.discordapp.com/avatars/%s/%s.%s?size=%s', self._id, avatar, ext, size) 36 | else 37 | return format('https://cdn.discordapp.com/avatars/%s/%s.%s', self._id, avatar, ext) 38 | end 39 | else 40 | return self:getDefaultAvatarURL(size) 41 | end 42 | end 43 | 44 | --[=[ 45 | @m getDefaultAvatarURL 46 | @t mem 47 | @op size number 48 | @r string 49 | @d Returns a URL that can be used to view the user's default avatar. 50 | ]=] 51 | function User:getDefaultAvatarURL(size) 52 | local avatar = self.defaultAvatar 53 | if size then 54 | return format('https://cdn.discordapp.com/embed/avatars/%s.png?size=%s', avatar, size) 55 | else 56 | return format('https://cdn.discordapp.com/embed/avatars/%s.png', avatar) 57 | end 58 | end 59 | 60 | --[=[ 61 | @m getPrivateChannel 62 | @t http 63 | @r PrivateChannel 64 | @d Returns a private channel that can be used to communicate with the user. If the 65 | channel is not cached an HTTP request is made to open one. 66 | ]=] 67 | function User:getPrivateChannel() 68 | local id = self._id 69 | local client = self.client 70 | local channel = client._private_channels:find(function(e) return e._recipient._id == id end) 71 | if channel then 72 | return channel 73 | else 74 | local data, err = client._api:createDM({recipient_id = id}) 75 | if data then 76 | return client._private_channels:_insert(data) 77 | else 78 | return nil, err 79 | end 80 | end 81 | end 82 | 83 | --[=[ 84 | @m send 85 | @t http 86 | @p content string/table 87 | @r Message 88 | @d Equivalent to `User:getPrivateChannel():send(content)` 89 | ]=] 90 | function User:send(content) 91 | local channel, err = self:getPrivateChannel() 92 | if channel then 93 | return channel:send(content) 94 | else 95 | return nil, err 96 | end 97 | end 98 | 99 | --[=[ 100 | @m sendf 101 | @t http 102 | @p content string 103 | @r Message 104 | @d Equivalent to `User:getPrivateChannel():sendf(content)` 105 | ]=] 106 | function User:sendf(content, ...) 107 | local channel, err = self:getPrivateChannel() 108 | if channel then 109 | return channel:sendf(content, ...) 110 | else 111 | return nil, err 112 | end 113 | end 114 | 115 | --[=[@p bot boolean Whether this user is a bot.]=] 116 | function get.bot(self) 117 | return self._bot or false 118 | end 119 | 120 | --[=[@p name string Equivalent to `User.globalName or User.username`.]=] 121 | function get.name(self) 122 | return self._global_name or self._username 123 | end 124 | 125 | --[=[@p username string The name of the user. This should be between 2 and 32 characters in length.]=] 126 | function get.username(self) 127 | return self._username 128 | end 129 | 130 | --[=[@p globalName string/nil The global display name of the user. 131 | If set, this has priority over the a username in displays, but not over a guild nickname.]=] 132 | function get.globalName(self) 133 | return self._global_name 134 | end 135 | 136 | --[=[@p discriminator number The discriminator of the user. This is a string that is used to 137 | discriminate the user from other users with the same username. Note that this will be "0" 138 | for users with unique usernames.]=] 139 | function get.discriminator(self) 140 | return self._discriminator 141 | end 142 | 143 | --[=[@p tag string The user's username if unique or username and discriminator concatenated by an `#`.]=] 144 | function get.tag(self) 145 | if self._discriminator == "0" then 146 | return self._username 147 | else 148 | return self._username .. '#' .. self._discriminator 149 | end 150 | end 151 | 152 | function get.fullname(self) 153 | self.client:_deprecated(self.__name, 'fullname', 'tag') 154 | if self._discriminator == "0" then 155 | return self._username 156 | else 157 | return self._username .. '#' .. self._discriminator 158 | end 159 | end 160 | 161 | --[=[@p avatar string/nil The hash for the user's custom avatar, if one is set.]=] 162 | function get.avatar(self) 163 | return self._avatar 164 | end 165 | 166 | --[=[@p defaultAvatar number The user's default avatar. See the `defaultAvatar` enumeration for a 167 | human-readable representation.]=] 168 | function get.defaultAvatar(self) 169 | if self._discriminator == '0' then 170 | return (self._id / 2^22) % 6 171 | else 172 | return self._discriminator % 5 173 | end 174 | end 175 | 176 | --[=[@p avatarURL string Equivalent to the result of calling `User:getAvatarURL()`.]=] 177 | function get.avatarURL(self) 178 | return self:getAvatarURL() 179 | end 180 | 181 | --[=[@p defaultAvatarURL string Equivalent to the result of calling `User:getDefaultAvatarURL()`.]=] 182 | function get.defaultAvatarURL(self) 183 | return self:getDefaultAvatarURL() 184 | end 185 | 186 | --[=[@p mentionString string A string that, when included in a message content, may resolve as user 187 | notification in the official Discord client.]=] 188 | function get.mentionString(self) 189 | return format('<@%s>', self._id) 190 | end 191 | 192 | --[=[@p mutualGuilds FilteredIterable A iterable cache of all guilds where this user shares a membership with the 193 | current user. The guild must be cached on the current client and the user's 194 | member object must be cached in that guild in order for it to appear here.]=] 195 | function get.mutualGuilds(self) 196 | if not self._mutual_guilds then 197 | local id = self._id 198 | self._mutual_guilds = FilteredIterable(self.client._guilds, function(g) 199 | return g._members:get(id) 200 | end) 201 | end 202 | return self._mutual_guilds 203 | end 204 | 205 | return User 206 | -------------------------------------------------------------------------------- /libs/containers/AuditLogEntry.lua: -------------------------------------------------------------------------------- 1 | --[=[ 2 | @c AuditLogEntry x Snowflake 3 | @d Represents an entry made into a guild's audit log. 4 | ]=] 5 | 6 | local Snowflake = require('containers/abstract/Snowflake') 7 | 8 | local enums = require('enums') 9 | local actionType = assert(enums.actionType) 10 | 11 | local AuditLogEntry, get = require('class')('AuditLogEntry', Snowflake) 12 | 13 | function AuditLogEntry:__init(data, parent) 14 | Snowflake.__init(self, data, parent) 15 | if data.changes then 16 | for i, change in ipairs(data.changes) do 17 | data.changes[change.key] = change 18 | data.changes[i] = nil 19 | change.key = nil 20 | change.old = change.old_value 21 | change.new = change.new_value 22 | change.old_value = nil 23 | change.new_value = nil 24 | end 25 | self._changes = data.changes 26 | end 27 | self._options = data.options 28 | end 29 | 30 | --[=[ 31 | @m getBeforeAfter 32 | @t mem 33 | @r table 34 | @r table 35 | @d Returns two tables of the target's properties before the change, and after the change. 36 | ]=] 37 | function AuditLogEntry:getBeforeAfter() 38 | local before, after = {}, {} 39 | for k, change in pairs(self._changes) do 40 | before[k], after[k] = change.old, change.new 41 | end 42 | return before, after 43 | end 44 | 45 | local function unknown(self) 46 | return nil, 'unknown audit log action type: ' .. self._action_type 47 | end 48 | 49 | local targets = setmetatable({ 50 | 51 | [actionType.guildUpdate] = function(self) 52 | return self._parent 53 | end, 54 | 55 | [actionType.channelCreate] = function(self) 56 | return self._parent:getChannel(self._target_id) 57 | end, 58 | 59 | [actionType.channelUpdate] = function(self) 60 | return self._parent:getChannel(self._target_id) 61 | end, 62 | 63 | [actionType.channelDelete] = function(self) 64 | return self._parent:getChannel(self._target_id) 65 | end, 66 | 67 | [actionType.channelOverwriteCreate] = function(self) 68 | return self._parent:getChannel(self._target_id) 69 | end, 70 | 71 | [actionType.channelOverwriteUpdate] = function(self) 72 | return self._parent:getChannel(self._target_id) 73 | end, 74 | 75 | [actionType.channelOverwriteDelete] = function(self) 76 | return self._parent:getChannel(self._target_id) 77 | end, 78 | 79 | [actionType.memberKick] = function(self) 80 | return self._parent._parent:getUser(self._target_id) 81 | end, 82 | 83 | [actionType.memberPrune] = function() 84 | return nil 85 | end, 86 | 87 | [actionType.memberBanAdd] = function(self) 88 | return self._parent._parent:getUser(self._target_id) 89 | end, 90 | 91 | [actionType.memberBanRemove] = function(self) 92 | return self._parent._parent:getUser(self._target_id) 93 | end, 94 | 95 | [actionType.memberUpdate] = function(self) 96 | return self._parent:getMember(self._target_id) 97 | end, 98 | 99 | [actionType.memberRoleUpdate] = function(self) 100 | return self._parent:getMember(self._target_id) 101 | end, 102 | 103 | [actionType.roleCreate] = function(self) 104 | return self._parent:getRole(self._target_id) 105 | end, 106 | 107 | [actionType.roleUpdate] = function(self) 108 | return self._parent:getRole(self._target_id) 109 | end, 110 | 111 | [actionType.roleDelete] = function(self) 112 | return self._parent:getRole(self._target_id) 113 | end, 114 | 115 | [actionType.inviteCreate] = function() 116 | return nil 117 | end, 118 | 119 | [actionType.inviteUpdate] = function() 120 | return nil 121 | end, 122 | 123 | [actionType.inviteDelete] = function() 124 | return nil 125 | end, 126 | 127 | [actionType.webhookCreate] = function(self) 128 | return self._parent._parent._webhooks:get(self._target_id) 129 | end, 130 | 131 | [actionType.webhookUpdate] = function(self) 132 | return self._parent._parent._webhooks:get(self._target_id) 133 | end, 134 | 135 | [actionType.webhookDelete] = function(self) 136 | return self._parent._parent._webhooks:get(self._target_id) 137 | end, 138 | 139 | [actionType.emojiCreate] = function(self) 140 | return self._parent:getEmoji(self._target_id) 141 | end, 142 | 143 | [actionType.emojiUpdate] = function(self) 144 | return self._parent:getEmoji(self._target_id) 145 | end, 146 | 147 | [actionType.emojiDelete] = function(self) 148 | return self._parent:getEmoji(self._target_id) 149 | end, 150 | 151 | [actionType.messageDelete] = function(self) 152 | return self._parent._parent:getUser(self._target_id) 153 | end, 154 | 155 | }, {__index = function() return unknown end}) 156 | 157 | --[=[ 158 | @m getTarget 159 | @t http? 160 | @r * 161 | @d Gets the target object of the affected entity. The returned object can be: [[Guild]], 162 | [[GuildChannel]], [[User]], [[Member]], [[Role]], [[Webhook]], [[Emoji]], nil 163 | ]=] 164 | function AuditLogEntry:getTarget() 165 | return targets[self._action_type](self) 166 | end 167 | 168 | --[=[ 169 | @m getUser 170 | @t http? 171 | @r User 172 | @d Gets the user who performed the changes. 173 | ]=] 174 | function AuditLogEntry:getUser() 175 | return self._parent._parent:getUser(self._user_id) 176 | end 177 | 178 | --[=[ 179 | @m getMember 180 | @t http? 181 | @r Member/nil 182 | @d Gets the member object of the user who performed the changes. 183 | ]=] 184 | function AuditLogEntry:getMember() 185 | return self._parent:getMember(self._user_id) 186 | end 187 | 188 | --[=[@p changes table/nil A table of audit log change objects. The key represents 189 | the property of the changed target and the value contains a table of `new` and 190 | possibly `old`, representing the property's new and old value.]=] 191 | function get.changes(self) 192 | return self._changes 193 | end 194 | 195 | --[=[@p options table/nil A table of optional audit log information.]=] 196 | function get.options(self) 197 | return self._options 198 | end 199 | 200 | --[=[@p actionType number The action type. Use the `actionType `enumeration 201 | for a human-readable representation.]=] 202 | function get.actionType(self) 203 | return self._action_type 204 | end 205 | 206 | --[=[@p targetId string/nil The Snowflake ID of the affected entity. Will 207 | be `nil` for certain targets.]=] 208 | function get.targetId(self) 209 | return self._target_id 210 | end 211 | 212 | --[=[@p userId string The Snowflake ID of the user who commited the action.]=] 213 | function get.userId(self) 214 | return self._user_id 215 | end 216 | 217 | --[=[@p reason string/nil The reason provided by the user for the change.]=] 218 | function get.reason(self) 219 | return self._reason 220 | end 221 | 222 | --[=[@p guild Guild The guild in which this audit log entry was found.]=] 223 | function get.guild(self) 224 | return self._parent 225 | end 226 | 227 | return AuditLogEntry 228 | -------------------------------------------------------------------------------- /libs/containers/Invite.lua: -------------------------------------------------------------------------------- 1 | --[=[ 2 | @c Invite x Container 3 | @d Represents an invitation to a Discord guild channel. Invites can be used to join 4 | a guild, though they are not always permanent. 5 | ]=] 6 | 7 | local Container = require('containers/abstract/Container') 8 | local json = require('json') 9 | 10 | local format = string.format 11 | local null = json.null 12 | 13 | local function load(v) 14 | return v ~= null and v or nil 15 | end 16 | 17 | local Invite, get = require('class')('Invite', Container) 18 | 19 | function Invite:__init(data, parent) 20 | Container.__init(self, data, parent) 21 | self._guild_id = load(data.guild.id) 22 | self._channel_id = load(data.channel.id) 23 | self._guild_name = load(data.guild.name) 24 | self._guild_icon = load(data.guild.icon) 25 | self._guild_splash = load(data.guild.splash) 26 | self._guild_banner = load(data.guild.banner) 27 | self._guild_description = load(data.guild.description) 28 | self._guild_verification_level = load(data.guild.verification_level) 29 | self._channel_name = load(data.channel.name) 30 | self._channel_type = load(data.channel.type) 31 | if data.inviter then 32 | self._inviter = self.client._users:_insert(data.inviter) 33 | end 34 | end 35 | 36 | --[=[ 37 | @m __hash 38 | @r string 39 | @d Returns `Invite.code` 40 | ]=] 41 | function Invite:__hash() 42 | return self._code 43 | end 44 | 45 | --[=[ 46 | @m delete 47 | @t http 48 | @r boolean 49 | @d Permanently deletes the invite. This cannot be undone! 50 | ]=] 51 | function Invite:delete() 52 | local data, err = self.client._api:deleteInvite(self._code) 53 | if data then 54 | return true 55 | else 56 | return false, err 57 | end 58 | end 59 | 60 | --[=[@p code string The invite's code which can be used to identify the invite.]=] 61 | function get.code(self) 62 | return self._code 63 | end 64 | 65 | --[=[@p guildId string The Snowflake ID of the guild to which this invite belongs.]=] 66 | function get.guildId(self) 67 | return self._guild_id 68 | end 69 | 70 | --[=[@p guildName string The name of the guild to which this invite belongs.]=] 71 | function get.guildName(self) 72 | return self._guild_name 73 | end 74 | 75 | --[=[@p channelId string The Snowflake ID of the channel to which this belongs.]=] 76 | function get.channelId(self) 77 | return self._channel_id 78 | end 79 | 80 | --[=[@p channelName string The name of the channel to which this invite belongs.]=] 81 | function get.channelName(self) 82 | return self._channel_name 83 | end 84 | 85 | --[=[@p channelType number The type of the channel to which this invite belongs. Use the `channelType` 86 | enumeration for a human-readable representation.]=] 87 | function get.channelType(self) 88 | return self._channel_type 89 | end 90 | 91 | --[=[@p guildIcon string/nil The hash for the guild's custom icon, if one is set.]=] 92 | function get.guildIcon(self) 93 | return self._guild_icon 94 | end 95 | 96 | --[=[@p guildBanner string/nil The hash for the guild's custom banner, if one is set.]=] 97 | function get.guildBanner(self) 98 | return self._guild_banner 99 | end 100 | 101 | --[=[@p guildSplash string/nil The hash for the guild's custom splash, if one is set.]=] 102 | function get.guildSplash(self) 103 | return self._guild_splash 104 | end 105 | 106 | --[=[@p guildIconURL string/nil The URL that can be used to view the guild's icon, if one is set.]=] 107 | function get.guildIconURL(self) 108 | local icon = self._guild_icon 109 | return icon and format('https://cdn.discordapp.com/icons/%s/%s.png', self._guild_id, icon) or nil 110 | end 111 | 112 | --[=[@p guildBannerURL string/nil The URL that can be used to view the guild's banner, if one is set.]=] 113 | function get.guildBannerURL(self) 114 | local banner = self._guild_banner 115 | return banner and format('https://cdn.discordapp.com/banners/%s/%s.png', self._guild_id, banner) or nil 116 | end 117 | 118 | --[=[@p guildSplashURL string/nil The URL that can be used to view the guild's splash, if one is set.]=] 119 | function get.guildSplashURL(self) 120 | local splash = self._guild_splash 121 | return splash and format('https://cdn.discordapp.com/splashs/%s/%s.png', self._guild_id, splash) or nil 122 | end 123 | 124 | --[=[@p guildDescription string/nil The guild's custom description, if one is set.]=] 125 | function get.guildDescription(self) 126 | return self._guild_description 127 | end 128 | 129 | --[=[@p guildVerificationLevel number/nil The guild's verification level, if available.]=] 130 | function get.guildVerificationLevel(self) 131 | return self._guild_verification_level 132 | end 133 | 134 | --[=[@p inviter User/nil The object of the user that created the invite. This will not exist if the 135 | invite is a guild widget or a vanity invite.]=] 136 | function get.inviter(self) 137 | return self._inviter 138 | end 139 | 140 | --[=[@p uses number/nil How many times this invite has been used. This will not exist if the invite is 141 | accessed via `Client:getInvite`.]=] 142 | function get.uses(self) 143 | return self._uses 144 | end 145 | 146 | --[=[@p maxUses number/nil The maximum amount of times this invite can be used. This will not exist if the 147 | invite is accessed via `Client:getInvite`.]=] 148 | function get.maxUses(self) 149 | return self._max_uses 150 | end 151 | 152 | --[=[@p maxAge number/nil How long, in seconds, this invite lasts before it expires. This will not exist 153 | if the invite is accessed via `Client:getInvite`.]=] 154 | function get.maxAge(self) 155 | return self._max_age 156 | end 157 | 158 | --[=[@p temporary boolean/nil Whether the invite grants temporary membership. This will not exist if the 159 | invite is accessed via `Client:getInvite`.]=] 160 | function get.temporary(self) 161 | return self._temporary 162 | end 163 | 164 | --[=[@p createdAt string/nil The date and time at which the invite was created, represented as an ISO 8601 165 | string plus microseconds when available. This will not exist if the invite is 166 | accessed via `Client:getInvite`.]=] 167 | function get.createdAt(self) 168 | return self._created_at 169 | end 170 | 171 | --[=[@p revoked boolean/nil Whether the invite has been revoked. This will not exist if the invite is 172 | accessed via `Client:getInvite`.]=] 173 | function get.revoked(self) 174 | return self._revoked 175 | end 176 | 177 | --[=[@p approximatePresenceCount number/nil The approximate count of online members.]=] 178 | function get.approximatePresenceCount(self) 179 | return self._approximate_presence_count 180 | end 181 | 182 | --[=[@p approximateMemberCount number/nil The approximate count of all members.]=] 183 | function get.approximateMemberCount(self) 184 | return self._approximate_member_count 185 | end 186 | 187 | return Invite 188 | -------------------------------------------------------------------------------- /libs/utils/Permissions.lua: -------------------------------------------------------------------------------- 1 | --[=[ 2 | @c Permissions 3 | @t ui 4 | @mt mem 5 | @op value number 6 | @d Wrapper for a bitfield that is more specifically used to represent Discord 7 | permissions. See the `permission` enumeration for acceptable permission values. 8 | ]=] 9 | 10 | local enums = require('enums') 11 | local Resolver = require('client/Resolver') 12 | 13 | local permission = assert(enums.permission) 14 | 15 | local format = string.format 16 | local band, bor, bnot, bxor = bit.band, bit.bor, bit.bnot, bit.bxor 17 | local sort, insert, concat = table.sort, table.insert, table.concat 18 | 19 | local ALL = 0ULL 20 | for _, value in pairs(permission) do 21 | ALL = bor(ALL, value) 22 | end 23 | 24 | local Permissions, get = require('class')('Permissions') 25 | 26 | function Permissions:__init(value) 27 | self._value = (tonumber(value) or 0) + 0ULL 28 | end 29 | 30 | --[=[ 31 | @m __tostring 32 | @r string 33 | @d Defines the behavior of the `tostring` function. Returns a readable list of 34 | permissions stored for convenience of introspection. 35 | ]=] 36 | function Permissions:__tostring() 37 | if self._value == 0 then 38 | return 'Permissions: 0 (none)' 39 | else 40 | local a = self:toArray() 41 | sort(a) 42 | return format('Permissions: %i (%s)', self.value, concat(a, ', ')) 43 | end 44 | end 45 | 46 | --[=[ 47 | @m fromMany 48 | @t static 49 | @p ... Permission-Resolvables 50 | @r Permissions 51 | @d Returns a Permissions object with all of the defined permissions. 52 | ]=] 53 | function Permissions.fromMany(...) 54 | local ret = Permissions() 55 | ret:enable(...) 56 | return ret 57 | end 58 | 59 | --[=[ 60 | @m all 61 | @t static 62 | @r Permissions 63 | @d Returns a Permissions object with all permissions. 64 | ]=] 65 | function Permissions.all() 66 | return Permissions(ALL) 67 | end 68 | 69 | --[=[ 70 | @m __eq 71 | @r boolean 72 | @d Defines the behavior of the `==` operator. Allows permissions to be directly 73 | compared according to their value. 74 | ]=] 75 | function Permissions:__eq(other) 76 | return self._value == other._value 77 | end 78 | 79 | local function getPerm(i, ...) 80 | local v = select(i, ...) 81 | local n = Resolver.permission(v) 82 | if not n then 83 | return error('Invalid permission: ' .. tostring(v), 2) 84 | end 85 | return n + 0ULL 86 | end 87 | 88 | --[=[ 89 | @m enable 90 | @p ... Permission-Resolvables 91 | @r nil 92 | @d Enables a specific permission or permissions. See the `permission` enumeration 93 | for acceptable permission values. 94 | ]=] 95 | function Permissions:enable(...) 96 | local value = self._value 97 | for i = 1, select('#', ...) do 98 | local perm = getPerm(i, ...) 99 | value = bor(value, perm) 100 | end 101 | self._value = value 102 | end 103 | 104 | --[=[ 105 | @m disable 106 | @p ... Permission-Resolvables 107 | @r nil 108 | @d Disables a specific permission or permissions. See the `permission` enumeration 109 | for acceptable permission values. 110 | ]=] 111 | function Permissions:disable(...) 112 | local value = self._value 113 | for i = 1, select('#', ...) do 114 | local perm = getPerm(i, ...) 115 | value = band(value, bnot(perm)) 116 | end 117 | self._value = value 118 | end 119 | 120 | --[=[ 121 | @m has 122 | @p ... Permission-Resolvables 123 | @r boolean 124 | @d Returns whether this set has a specific permission or permissions. See the 125 | `permission` enumeration for acceptable permission values. 126 | ]=] 127 | function Permissions:has(...) 128 | local value = self._value 129 | for i = 1, select('#', ...) do 130 | local perm = getPerm(i, ...) 131 | if band(value, perm) == 0 then 132 | return false 133 | end 134 | end 135 | return true 136 | end 137 | 138 | --[=[ 139 | @m enableAll 140 | @r nil 141 | @d Enables all permissions values. 142 | ]=] 143 | function Permissions:enableAll() 144 | self._value = ALL 145 | end 146 | 147 | --[=[ 148 | @m disableAll 149 | @r nil 150 | @d Disables all permissions values. 151 | ]=] 152 | function Permissions:disableAll() 153 | self._value = 0ULL 154 | end 155 | 156 | --[=[ 157 | @m toHex 158 | @r string 159 | @d Returns the hexadecimal string that represents the permissions value. 160 | ]=] 161 | function Permissions:toHex() 162 | return format('0x%08X', self._value) 163 | end 164 | 165 | --[=[ 166 | @m toTable 167 | @r table 168 | @d Returns a table that represents the permissions value, where the keys are the 169 | permission names and the values are `true` or `false`. 170 | ]=] 171 | function Permissions:toTable() 172 | local ret = {} 173 | local value = self._value 174 | for k, v in pairs(permission) do 175 | ret[k] = band(value, v) > 0 176 | end 177 | return ret 178 | end 179 | 180 | --[=[ 181 | @m toArray 182 | @r table 183 | @d Returns an array of the names of the permissions that this object represents. 184 | ]=] 185 | function Permissions:toArray() 186 | local ret = {} 187 | local value = self._value 188 | for k, v in pairs(permission) do 189 | if band(value, v) > 0 then 190 | insert(ret, k) 191 | end 192 | end 193 | return ret 194 | end 195 | 196 | --[=[ 197 | @m union 198 | @p other Permissions 199 | @r Permissions 200 | @d Returns a new Permissions object that contains the permissions that are in 201 | either `self` or `other` (bitwise OR). 202 | ]=] 203 | function Permissions:union(other) 204 | return Permissions(bor(self._value, other._value)) 205 | end 206 | 207 | --[=[ 208 | @m intersection 209 | @p other Permissions 210 | @r Permissions 211 | @d Returns a new Permissions object that contains the permissions that are in 212 | both `self` and `other` (bitwise AND). 213 | ]=] 214 | function Permissions:intersection(other) -- in both 215 | return Permissions(band(self._value, other._value)) 216 | end 217 | 218 | --[=[ 219 | @m difference 220 | @p other Permissions 221 | @r Permissions 222 | @d Returns a new Permissions object that contains the permissions that are not 223 | in `self` or `other` (bitwise XOR). 224 | ]=] 225 | function Permissions:difference(other) -- not in both 226 | return Permissions(bxor(self._value, other._value)) 227 | end 228 | 229 | --[=[ 230 | @m complement 231 | @op other Permissions 232 | @r Permissions 233 | @d Returns a new Permissions object that contains the permissions that are not 234 | in `self`, but are in `other` (or the set of all permissions if omitted). 235 | ]=] 236 | function Permissions:complement(other) -- in other not in self 237 | local value = other and other._value or ALL 238 | return Permissions(band(bnot(self._value), value)) 239 | end 240 | 241 | --[=[ 242 | @m copy 243 | @r Permissions 244 | @d Returns a new copy of the original permissions object. 245 | ]=] 246 | function Permissions:copy() 247 | return Permissions(self._value) 248 | end 249 | 250 | --[=[@p value number The raw decimal value that represents the permissions value.]=] 251 | function get.value(self) 252 | return tonumber(self._value) 253 | end 254 | 255 | return Permissions 256 | -------------------------------------------------------------------------------- /libs/iterables/Iterable.lua: -------------------------------------------------------------------------------- 1 | --[=[ 2 | @c Iterable 3 | @mt mem 4 | @d Abstract base class that defines the base methods and properties for a 5 | general purpose data structure with features that are better suited for an 6 | object-oriented environment. 7 | 8 | Note: All sub-classes should implement their own `__init` and `iter` methods and 9 | all stored objects should have a `__hash` method. 10 | ]=] 11 | 12 | local random = math.random 13 | local insert, sort, pack, unpack = table.insert, table.sort, table.pack, table.unpack 14 | 15 | local Iterable = require('class')('Iterable') 16 | 17 | --[=[ 18 | @m __pairs 19 | @r function 20 | @d Defines the behavior of the `pairs` function. Returns an iterator that returns 21 | a `key, value` pair, where `key` is the result of calling `__hash` on the `value`. 22 | ]=] 23 | function Iterable:__pairs() 24 | local gen = self:iter() 25 | return function() 26 | local obj = gen() 27 | if not obj then 28 | return nil 29 | end 30 | return obj:__hash(), obj 31 | end 32 | end 33 | 34 | --[=[ 35 | @m __len 36 | @r function 37 | @d Defines the behavior of the `#` operator. Returns the total number of objects 38 | stored in the iterable. 39 | ]=] 40 | function Iterable:__len() 41 | local n = 0 42 | for _ in self:iter() do 43 | n = n + 1 44 | end 45 | return n 46 | end 47 | 48 | --[=[ 49 | @m get 50 | @p k * 51 | @r * 52 | @d Returns an individual object by key, where the key should match the result of 53 | calling `__hash` on the contained objects. Operates with up to O(n) complexity. 54 | ]=] 55 | function Iterable:get(k) -- objects must be hashable 56 | for obj in self:iter() do 57 | if obj:__hash() == k then 58 | return obj 59 | end 60 | end 61 | return nil 62 | end 63 | 64 | --[=[ 65 | @m find 66 | @p fn function 67 | @r * 68 | @d Returns the first object that satisfies a predicate. 69 | ]=] 70 | function Iterable:find(fn) 71 | for obj in self:iter() do 72 | if fn(obj) then 73 | return obj 74 | end 75 | end 76 | return nil 77 | end 78 | 79 | --[=[ 80 | @m findAll 81 | @p fn function 82 | @r function 83 | @d Returns an iterator that returns all objects that satisfy a predicate. 84 | ]=] 85 | function Iterable:findAll(fn) 86 | local gen = self:iter() 87 | return function() 88 | while true do 89 | local obj = gen() 90 | if not obj then 91 | return nil 92 | end 93 | if fn(obj) then 94 | return obj 95 | end 96 | end 97 | end 98 | end 99 | 100 | --[=[ 101 | @m forEach 102 | @p fn function 103 | @r nil 104 | @d Iterates through all objects and calls a function `fn` that takes the 105 | objects as an argument. 106 | ]=] 107 | function Iterable:forEach(fn) 108 | for obj in self:iter() do 109 | fn(obj) 110 | end 111 | end 112 | 113 | --[=[ 114 | @m random 115 | @r * 116 | @d Returns a random object that is contained in the iterable. 117 | ]=] 118 | function Iterable:random() 119 | local n = 1 120 | local rand = random(#self) 121 | for obj in self:iter() do 122 | if n == rand then 123 | return obj 124 | end 125 | n = n + 1 126 | end 127 | end 128 | 129 | --[=[ 130 | @m count 131 | @op fn function 132 | @r number 133 | @d If a predicate is provided, this returns the number of objects in the iterable 134 | that satisfy the predicate; otherwise, the total number of objects. 135 | ]=] 136 | function Iterable:count(fn) 137 | if not fn then 138 | return self:__len() 139 | end 140 | local n = 0 141 | for _ in self:findAll(fn) do 142 | n = n + 1 143 | end 144 | return n 145 | end 146 | 147 | local function sorter(a, b) 148 | local t1, t2 = type(a), type(b) 149 | if t1 == 'string' then 150 | if t2 == 'string' then 151 | local n1 = tonumber(a) 152 | if n1 then 153 | local n2 = tonumber(b) 154 | if n2 then 155 | return n1 < n2 156 | end 157 | end 158 | return a:lower() < b:lower() 159 | elseif t2 == 'number' then 160 | local n1 = tonumber(a) 161 | if n1 then 162 | return n1 < b 163 | end 164 | return a:lower() < tostring(b) 165 | end 166 | elseif t1 == 'number' then 167 | if t2 == 'number' then 168 | return a < b 169 | elseif t2 == 'string' then 170 | local n2 = tonumber(b) 171 | if n2 then 172 | return a < n2 173 | end 174 | return tostring(a) < b:lower() 175 | end 176 | end 177 | local m1 = getmetatable(a) 178 | if m1 and m1.__lt then 179 | local m2 = getmetatable(b) 180 | if m2 and m2.__lt then 181 | return a < b 182 | end 183 | end 184 | return tostring(a) < tostring(b) 185 | end 186 | 187 | --[=[ 188 | @m toArray 189 | @op sortBy string 190 | @op fn function 191 | @r table 192 | @d Returns a sequentially-indexed table that contains references to all objects. 193 | If a `sortBy` string is provided, then the table is sorted by that particular 194 | property. If a predicate is provided, then only objects that satisfy it will 195 | be included. 196 | ]=] 197 | function Iterable:toArray(sortBy, fn) 198 | local t1 = type(sortBy) 199 | if t1 == 'string' then 200 | fn = type(fn) == 'function' and fn 201 | elseif t1 == 'function' then 202 | fn = sortBy 203 | sortBy = nil 204 | end 205 | local ret = {} 206 | for obj in self:iter() do 207 | if not fn or fn(obj) then 208 | insert(ret, obj) 209 | end 210 | end 211 | if sortBy then 212 | sort(ret, function(a, b) 213 | return sorter(a[sortBy], b[sortBy]) 214 | end) 215 | end 216 | return ret 217 | end 218 | 219 | --[=[ 220 | @m select 221 | @p ... string 222 | @r table 223 | @d Similarly to an SQL query, this returns a sorted Lua table of rows where each 224 | row corresponds to each object in the iterable, and each value in the row is 225 | selected from the objects according to the keys provided. 226 | ]=] 227 | function Iterable:select(...) 228 | local rows = {} 229 | local keys = pack(...) 230 | for obj in self:iter() do 231 | local row = {} 232 | for i = 1, keys.n do 233 | row[i] = obj[keys[i]] 234 | end 235 | insert(rows, row) 236 | end 237 | sort(rows, function(a, b) 238 | for i = 1, keys.n do 239 | if a[i] ~= b[i] then 240 | return sorter(a[i], b[i]) 241 | end 242 | end 243 | return false 244 | end) 245 | return rows 246 | end 247 | 248 | --[=[ 249 | @m pick 250 | @p ... string/function 251 | @r function 252 | @d This returns an iterator that, when called, returns the values from each 253 | encountered object, picked by the provided keys. If a key is a string, the objects 254 | are indexed with the string. If a key is a function, the function is called with 255 | the object passed as its first argument. 256 | ]=] 257 | function Iterable:pick(...) 258 | local keys = pack(...) 259 | local values = {} 260 | local n = keys.n 261 | local gen = self:iter() 262 | return function() 263 | local obj = gen() 264 | if not obj then 265 | return nil 266 | end 267 | for i = 1, n do 268 | local k = keys[i] 269 | if type(k) == 'function' then 270 | values[i] = k(obj) 271 | else 272 | values[i] = obj[k] 273 | end 274 | end 275 | return unpack(values, 1, n) 276 | end 277 | end 278 | 279 | return Iterable 280 | -------------------------------------------------------------------------------- /libs/utils/Time.lua: -------------------------------------------------------------------------------- 1 | --[=[ 2 | @c Time 3 | @t ui 4 | @mt mem 5 | @op value number 6 | @d Represents a length of time and provides utilities for converting to and from 7 | different formats. Supported units are: weeks, days, hours, minutes, seconds, 8 | and milliseconds. 9 | ]=] 10 | 11 | local class = require('class') 12 | local constants = require('constants') 13 | 14 | local MS_PER_S = constants.MS_PER_S 15 | local MS_PER_MIN = MS_PER_S * constants.S_PER_MIN 16 | local MS_PER_HOUR = MS_PER_MIN * constants.MIN_PER_HOUR 17 | local MS_PER_DAY = MS_PER_HOUR * constants.HOUR_PER_DAY 18 | local MS_PER_WEEK = MS_PER_DAY * constants.DAY_PER_WEEK 19 | 20 | local insert, concat = table.insert, table.concat 21 | local modf, fmod = math.modf, math.fmod 22 | local isInstance = class.isInstance 23 | 24 | local function decompose(value, mult) 25 | return modf(value / mult), fmod(value, mult) 26 | end 27 | 28 | local units = { 29 | {'weeks', MS_PER_WEEK}, 30 | {'days', MS_PER_DAY}, 31 | {'hours', MS_PER_HOUR}, 32 | {'minutes', MS_PER_MIN}, 33 | {'seconds', MS_PER_S}, 34 | {'milliseconds', 1}, 35 | } 36 | 37 | local Time = class('Time') 38 | 39 | local function check(self, other) 40 | if not isInstance(self, Time) or not isInstance(other, Time) then 41 | return error('Cannot perform operation with non-Time object', 2) 42 | end 43 | end 44 | 45 | function Time:__init(value) 46 | self._value = tonumber(value) or 0 47 | end 48 | 49 | function Time:__tostring() 50 | return 'Time: ' .. self:toString() 51 | end 52 | 53 | --[=[ 54 | @m toString 55 | @r string 56 | @d Returns a human-readable string built from the set of normalized time values 57 | that the object represents. 58 | ]=] 59 | function Time:toString() 60 | local ret = {} 61 | local ms = self:toMilliseconds() 62 | for _, unit in ipairs(units) do 63 | local n 64 | n, ms = decompose(ms, unit[2]) 65 | if n == 1 then 66 | insert(ret, n .. ' ' .. unit[1]:sub(1, -2)) 67 | elseif n > 0 then 68 | insert(ret, n .. ' ' .. unit[1]) 69 | end 70 | end 71 | return #ret > 0 and concat(ret, ', ') or '0 milliseconds' 72 | end 73 | 74 | function Time:__eq(other) check(self, other) 75 | return self._value == other._value 76 | end 77 | 78 | function Time:__lt(other) check(self, other) 79 | return self._value < other._value 80 | end 81 | 82 | function Time:__le(other) check(self, other) 83 | return self._value <= other._value 84 | end 85 | 86 | function Time:__add(other) check(self, other) 87 | return Time(self._value + other._value) 88 | end 89 | 90 | function Time:__sub(other) check(self, other) 91 | return Time(self._value - other._value) 92 | end 93 | 94 | function Time:__mul(other) 95 | if not isInstance(self, Time) then 96 | self, other = other, self 97 | end 98 | other = tonumber(other) 99 | if other then 100 | return Time(self._value * other) 101 | else 102 | return error('Cannot perform operation with non-numeric object') 103 | end 104 | end 105 | 106 | function Time:__div(other) 107 | if not isInstance(self, Time) then 108 | return error('Division with Time is not commutative') 109 | end 110 | other = tonumber(other) 111 | if other then 112 | return Time(self._value / other) 113 | else 114 | return error('Cannot perform operation with non-numeric object') 115 | end 116 | end 117 | 118 | --[=[ 119 | @m fromWeeks 120 | @t static 121 | @p t number 122 | @r Time 123 | @d Constructs a new Time object from a value interpreted as weeks, where a week 124 | is equal to 7 days. 125 | ]=] 126 | function Time.fromWeeks(t) 127 | return Time(t * MS_PER_WEEK) 128 | end 129 | 130 | --[=[ 131 | @m fromDays 132 | @t static 133 | @p t number 134 | @r Time 135 | @d Constructs a new Time object from a value interpreted as days, where a day is 136 | equal to 24 hours. 137 | ]=] 138 | function Time.fromDays(t) 139 | return Time(t * MS_PER_DAY) 140 | end 141 | 142 | --[=[ 143 | @m fromHours 144 | @t static 145 | @p t number 146 | @r Time 147 | @d Constructs a new Time object from a value interpreted as hours, where an hour is 148 | equal to 60 minutes. 149 | ]=] 150 | function Time.fromHours(t) 151 | return Time(t * MS_PER_HOUR) 152 | end 153 | 154 | --[=[ 155 | @m fromMinutes 156 | @t static 157 | @p t number 158 | @r Time 159 | @d Constructs a new Time object from a value interpreted as minutes, where a minute 160 | is equal to 60 seconds. 161 | ]=] 162 | function Time.fromMinutes(t) 163 | return Time(t * MS_PER_MIN) 164 | end 165 | 166 | --[=[ 167 | @m fromSeconds 168 | @t static 169 | @p t number 170 | @r Time 171 | @d Constructs a new Time object from a value interpreted as seconds, where a second 172 | is equal to 1000 milliseconds. 173 | ]=] 174 | function Time.fromSeconds(t) 175 | return Time(t * MS_PER_S) 176 | end 177 | 178 | --[=[ 179 | @m fromMilliseconds 180 | @t static 181 | @p t number 182 | @r Time 183 | @d Constructs a new Time object from a value interpreted as milliseconds, the base 184 | unit represented. 185 | ]=] 186 | function Time.fromMilliseconds(t) 187 | return Time(t) 188 | end 189 | 190 | --[=[ 191 | @m fromTable 192 | @t static 193 | @p t table 194 | @r Time 195 | @d Constructs a new Time object from a table of time values where the keys are 196 | defined in the constructors above (eg: `weeks`, `days`, `hours`). 197 | ]=] 198 | function Time.fromTable(t) 199 | local n = 0 200 | for _, v in ipairs(units) do 201 | local m = tonumber(t[v[1]]) 202 | if m then 203 | n = n + m * v[2] 204 | end 205 | end 206 | return Time(n) 207 | end 208 | 209 | --[=[ 210 | @m toWeeks 211 | @r number 212 | @d Returns the total number of weeks that the time object represents. 213 | ]=] 214 | function Time:toWeeks() 215 | return self:toMilliseconds() / MS_PER_WEEK 216 | end 217 | 218 | --[=[ 219 | @m toDays 220 | @r number 221 | @d Returns the total number of days that the time object represents. 222 | ]=] 223 | function Time:toDays() 224 | return self:toMilliseconds() / MS_PER_DAY 225 | end 226 | 227 | --[=[ 228 | @m toHours 229 | @r number 230 | @d Returns the total number of hours that the time object represents. 231 | ]=] 232 | function Time:toHours() 233 | return self:toMilliseconds() / MS_PER_HOUR 234 | end 235 | 236 | --[=[ 237 | @m toMinutes 238 | @r number 239 | @d Returns the total number of minutes that the time object represents. 240 | ]=] 241 | function Time:toMinutes() 242 | return self:toMilliseconds() / MS_PER_MIN 243 | end 244 | 245 | --[=[ 246 | @m toSeconds 247 | @r number 248 | @d Returns the total number of seconds that the time object represents. 249 | ]=] 250 | function Time:toSeconds() 251 | return self:toMilliseconds() / MS_PER_S 252 | end 253 | 254 | --[=[ 255 | @m toMilliseconds 256 | @r number 257 | @d Returns the total number of milliseconds that the time object represents. 258 | ]=] 259 | function Time:toMilliseconds() 260 | return self._value 261 | end 262 | 263 | --[=[ 264 | @m toTable 265 | @r number 266 | @d Returns a table of normalized time values that represent the time object in 267 | a more accessible form. 268 | ]=] 269 | function Time:toTable() 270 | local ret = {} 271 | local ms = self:toMilliseconds() 272 | for _, unit in ipairs(units) do 273 | ret[unit[1]], ms = decompose(ms, unit[2]) 274 | end 275 | return ret 276 | end 277 | 278 | return Time 279 | -------------------------------------------------------------------------------- /libs/client/Shard.lua: -------------------------------------------------------------------------------- 1 | local json = require('json') 2 | local timer = require('timer') 3 | 4 | local EventHandler = require('client/EventHandler') 5 | local WebSocket = require('client/WebSocket') 6 | 7 | local constants = require('constants') 8 | local enums = require('enums') 9 | 10 | local logLevel = assert(enums.logLevel) 11 | local min, max, random = math.min, math.max, math.random 12 | local null = json.null 13 | local format = string.format 14 | local sleep = timer.sleep 15 | local setInterval, clearInterval = timer.setInterval, timer.clearInterval 16 | local wrap = coroutine.wrap 17 | 18 | local ID_DELAY = constants.ID_DELAY 19 | 20 | local DISPATCH = 0 21 | local HEARTBEAT = 1 22 | local IDENTIFY = 2 23 | local STATUS_UPDATE = 3 24 | local VOICE_STATE_UPDATE = 4 25 | -- local VOICE_SERVER_PING = 5 -- TODO 26 | local RESUME = 6 27 | local RECONNECT = 7 28 | local REQUEST_GUILD_MEMBERS = 8 29 | local INVALID_SESSION = 9 30 | local HELLO = 10 31 | local HEARTBEAT_ACK = 11 32 | local GUILD_SYNC = 12 33 | 34 | local ignore = { 35 | ['CALL_DELETE'] = true, 36 | ['CHANNEL_PINS_ACK'] = true, 37 | ['GUILD_INTEGRATIONS_UPDATE'] = true, 38 | ['MESSAGE_ACK'] = true, 39 | ['PRESENCES_REPLACE'] = true, 40 | ['USER_SETTINGS_UPDATE'] = true, 41 | ['USER_GUILD_SETTINGS_UPDATE'] = true, 42 | ['SESSIONS_REPLACE'] = true, 43 | ['INVITE_CREATE'] = true, 44 | ['INVITE_DELETE'] = true, 45 | ['INTEGRATION_CREATE'] = true, 46 | ['INTEGRATION_UPDATE'] = true, 47 | ['INTEGRATION_DELETE'] = true, 48 | ['EMBEDDED_ACTIVITY_UPDATE'] = true, 49 | ['GIFT_CODE_UPDATE'] = true, 50 | ['GUILD_JOIN_REQUEST_UPDATE'] = true, 51 | ['GUILD_JOIN_REQUEST_DELETE'] = true, 52 | ['APPLICATION_COMMAND_PERMISSIONS_UPDATE'] = true, 53 | } 54 | 55 | local Shard = require('class')('Shard', WebSocket) 56 | 57 | function Shard:__init(id, client) 58 | WebSocket.__init(self, client) 59 | self._id = id 60 | self._client = client 61 | self._backoff = 1000 62 | end 63 | 64 | for name in pairs(logLevel) do 65 | Shard[name] = function(self, fmt, ...) 66 | local client = self._client 67 | return client[name](client, format('Shard %i : %s', self._id, fmt), ...) 68 | end 69 | end 70 | 71 | function Shard:__tostring() 72 | return format('Shard: %i', self._id) 73 | end 74 | 75 | local function getReconnectTime(self, n, m) 76 | return self._backoff * (n + random() * (m - n)) 77 | end 78 | 79 | local function incrementReconnectTime(self) 80 | self._backoff = min(self._backoff * 2, 60000) 81 | end 82 | 83 | local function decrementReconnectTime(self) 84 | self._backoff = max(self._backoff / 2, 1000) 85 | end 86 | 87 | function Shard:handleDisconnect(url, path) 88 | self._client:emit('shardDisconnect', self._id) 89 | if self._reconnect then 90 | self:info('Reconnecting...') 91 | return self:connect(url, path) 92 | elseif self._reconnect == nil and self._client._options.autoReconnect then 93 | local backoff = getReconnectTime(self, 0.9, 1.1) 94 | incrementReconnectTime(self) 95 | self:info('Reconnecting after %i ms...', backoff) 96 | sleep(backoff) 97 | return self:connect(url, path) 98 | end 99 | end 100 | 101 | function Shard:handlePayload(payload) 102 | 103 | local client = self._client 104 | 105 | local s = payload.s 106 | local t = payload.t 107 | local d = payload.d 108 | local op = payload.op 109 | 110 | if t ~= null then 111 | self:debug('WebSocket OP %s : %s : %s', op, t, s) 112 | else 113 | self:debug('WebSocket OP %s', op) 114 | end 115 | 116 | if op == DISPATCH then 117 | 118 | self._seq = s 119 | if not ignore[t] then 120 | EventHandler[t](d, client, self) 121 | end 122 | 123 | elseif op == HEARTBEAT then 124 | 125 | self:heartbeat() 126 | 127 | elseif op == RECONNECT then 128 | 129 | self:info('Discord has requested a reconnection') 130 | self:disconnect(true) 131 | 132 | elseif op == INVALID_SESSION then 133 | 134 | if payload.d and self._session_id then 135 | self:info('Session invalidated, resuming...') 136 | self:resume() 137 | else 138 | self:info('Session invalidated, re-identifying...') 139 | sleep(random(1000, 5000)) 140 | self:identify() 141 | end 142 | 143 | elseif op == HELLO then 144 | 145 | self:info('Received HELLO') 146 | self:startHeartbeat(d.heartbeat_interval) 147 | if self._session_id then 148 | self:resume() 149 | else 150 | self:identify() 151 | end 152 | 153 | elseif op == HEARTBEAT_ACK then 154 | 155 | client:emit('heartbeat', self._id, self._sw.milliseconds) 156 | 157 | elseif op then 158 | 159 | self:warning('Unhandled WebSocket payload OP %i', op) 160 | 161 | end 162 | 163 | end 164 | 165 | local function loop(self) 166 | decrementReconnectTime(self) 167 | return wrap(self.heartbeat)(self) 168 | end 169 | 170 | function Shard:startHeartbeat(interval) 171 | if self._heartbeat then 172 | clearInterval(self._heartbeat) 173 | end 174 | self._heartbeat = setInterval(interval, loop, self) 175 | end 176 | 177 | function Shard:stopHeartbeat() 178 | if self._heartbeat then 179 | clearInterval(self._heartbeat) 180 | end 181 | self._heartbeat = nil 182 | end 183 | 184 | function Shard:identifyWait() 185 | if self:waitFor('READY', 1.5 * ID_DELAY) then 186 | return sleep(ID_DELAY) 187 | end 188 | end 189 | 190 | function Shard:heartbeat() 191 | self._sw:reset() 192 | return self:_send(HEARTBEAT, self._seq or json.null) 193 | end 194 | 195 | function Shard:identify() 196 | 197 | local client = self._client 198 | local mutex = client._mutex 199 | local options = client._options 200 | 201 | mutex:lock() 202 | wrap(function() 203 | self:identifyWait() 204 | mutex:unlock() 205 | end)() 206 | 207 | self._seq = nil 208 | self._session_id = nil 209 | self._ready = false 210 | self._loading = {guilds = {}, chunks = {}, syncs = {}} 211 | 212 | return self:_send(IDENTIFY, { 213 | token = client._token, 214 | properties = { 215 | ['$os'] = jit.os, 216 | ['$browser'] = 'Discordia', 217 | ['$device'] = 'Discordia', 218 | ['$referrer'] = '', 219 | ['$referring_domain'] = '', 220 | }, 221 | compress = options.compress, 222 | large_threshold = options.largeThreshold, 223 | shard = {self._id, client._total_shard_count}, 224 | presence = next(client._presence) and client._presence, 225 | intents = client._intents, 226 | }, true) 227 | 228 | end 229 | 230 | function Shard:resume() 231 | return self:_send(RESUME, { 232 | token = self._client._token, 233 | session_id = self._session_id, 234 | seq = self._seq 235 | }) 236 | end 237 | 238 | function Shard:requestGuildMembers(id) 239 | return self:_send(REQUEST_GUILD_MEMBERS, { 240 | guild_id = id, 241 | query = '', 242 | limit = 0, 243 | }) 244 | end 245 | 246 | function Shard:updateStatus(presence) 247 | return self:_send(STATUS_UPDATE, presence) 248 | end 249 | 250 | function Shard:updateVoice(guild_id, channel_id, self_mute, self_deaf) 251 | return self:_send(VOICE_STATE_UPDATE, { 252 | guild_id = guild_id, 253 | channel_id = channel_id or null, 254 | self_mute = self_mute or false, 255 | self_deaf = self_deaf or false, 256 | }) 257 | end 258 | 259 | function Shard:syncGuilds(ids) 260 | return self:_send(GUILD_SYNC, ids) 261 | end 262 | 263 | return Shard 264 | -------------------------------------------------------------------------------- /libs/containers/PermissionOverwrite.lua: -------------------------------------------------------------------------------- 1 | --[=[ 2 | @c PermissionOverwrite x Snowflake 3 | @d Represents an object that is used to allow or deny specific permissions for a 4 | role or member in a Discord guild channel. 5 | ]=] 6 | 7 | local Snowflake = require('containers/abstract/Snowflake') 8 | local Permissions = require('utils/Permissions') 9 | local Resolver = require('client/Resolver') 10 | 11 | local PermissionOverwrite, get = require('class')('PermissionOverwrite', Snowflake) 12 | 13 | function PermissionOverwrite:__init(data, parent) 14 | Snowflake.__init(self, data, parent) 15 | end 16 | 17 | --[=[ 18 | @m delete 19 | @t http 20 | @r boolean 21 | @d Deletes the permission overwrite. This can be undone by creating a new version of 22 | the same overwrite. 23 | ]=] 24 | function PermissionOverwrite:delete() 25 | local data, err = self.client._api:deleteChannelPermission(self._parent._id, self._id) 26 | if data then 27 | local cache = self._parent._permission_overwrites 28 | if cache then 29 | cache:_delete(self._id) 30 | end 31 | return true 32 | else 33 | return false, err 34 | end 35 | end 36 | 37 | --[=[ 38 | @m getObject 39 | @t http? 40 | @r Role/Member 41 | @d Returns the object associated with this overwrite, either a role or member. 42 | This may make an HTTP request if the object is not cached. 43 | ]=] 44 | function PermissionOverwrite:getObject() 45 | local guild = self._parent._parent 46 | if self.type == 'role' then 47 | return guild:getRole(self._id) 48 | elseif self.type == 'member' then 49 | return guild:getMember(self._id) 50 | end 51 | end 52 | 53 | local function getPermissions(self) 54 | return Permissions(self._allow_new or self._allow), Permissions(self._deny_new or self._deny) 55 | end 56 | 57 | local function setPermissions(self, allow, deny) 58 | local data, err = self.client._api:editChannelPermissions(self._parent._id, self._id, { 59 | allow = allow, deny = deny, type = self._type 60 | }) 61 | if data then 62 | self._allow, self._deny = allow, deny 63 | return true 64 | else 65 | return false, err 66 | end 67 | end 68 | 69 | --[=[ 70 | @m getAllowedPermissions 71 | @t mem 72 | @r Permissions 73 | @d Returns a permissions object that represents the permissions that this overwrite 74 | explicitly allows. 75 | ]=] 76 | function PermissionOverwrite:getAllowedPermissions() 77 | return Permissions(self._allow_new or self._allow) 78 | end 79 | 80 | --[=[ 81 | @m getDeniedPermissions 82 | @t mem 83 | @r Permissions 84 | @d Returns a permissions object that represents the permissions that this overwrite 85 | explicitly denies. 86 | ]=] 87 | function PermissionOverwrite:getDeniedPermissions() 88 | return Permissions(self._deny_new or self._deny) 89 | end 90 | 91 | --[=[ 92 | @m setPermissions 93 | @t http 94 | @p allowed Permissions-Resolvables 95 | @p denied Permissions-Resolvables 96 | @r boolean 97 | @d Sets the permissions that this overwrite explicitly allows and denies. This 98 | method does NOT resolve conflicts. Please be sure to use the correct parameters. 99 | ]=] 100 | function PermissionOverwrite:setPermissions(allowed, denied) 101 | local allow = Resolver.permissions(allowed) 102 | local deny = Resolver.permissions(denied) 103 | return setPermissions(self, allow, deny) 104 | end 105 | 106 | --[=[ 107 | @m setAllowedPermissions 108 | @t http 109 | @p allowed Permissions-Resolvables 110 | @r boolean 111 | @d Sets the permissions that this overwrite explicitly allows. 112 | ]=] 113 | function PermissionOverwrite:setAllowedPermissions(allowed) 114 | local allow = Permissions(Resolver.permissions(allowed)) 115 | local deny = allow:complement(self:getDeniedPermissions()) -- un-deny the allowed permissions 116 | return setPermissions(self, allow.value, deny.value) 117 | end 118 | 119 | --[=[ 120 | @m setDeniedPermissions 121 | @t http 122 | @p denied Permissions-Resolvables 123 | @r boolean 124 | @d Sets the permissions that this overwrite explicitly denies. 125 | ]=] 126 | function PermissionOverwrite:setDeniedPermissions(denied) 127 | local deny = Permissions(Resolver.permissions(denied)) 128 | local allow = deny:complement(self:getAllowedPermissions()) -- un-allow the denied permissions 129 | return setPermissions(self, allow.value, deny.value) 130 | end 131 | 132 | --[=[ 133 | @m allowPermissions 134 | @t http 135 | @p ... Permission-Resolvables 136 | @r boolean 137 | @d Allows individual permissions in this overwrite. 138 | ]=] 139 | function PermissionOverwrite:allowPermissions(...) 140 | local allowed, denied = getPermissions(self) 141 | allowed:enable(...); denied:disable(...) 142 | return setPermissions(self, allowed.value, denied.value) 143 | end 144 | 145 | --[=[ 146 | @m denyPermissions 147 | @t http 148 | @p ... Permission-Resolvables 149 | @r boolean 150 | @d Denies individual permissions in this overwrite. 151 | ]=] 152 | function PermissionOverwrite:denyPermissions(...) 153 | local allowed, denied = getPermissions(self) 154 | allowed:disable(...); denied:enable(...) 155 | return setPermissions(self, allowed.value, denied.value) 156 | end 157 | 158 | --[=[ 159 | @m clearPermissions 160 | @t http 161 | @p ... Permission-Resolvables 162 | @r boolean 163 | @d Clears individual permissions in this overwrite. 164 | ]=] 165 | function PermissionOverwrite:clearPermissions(...) 166 | local allowed, denied = getPermissions(self) 167 | allowed:disable(...); denied:disable(...) 168 | return setPermissions(self, allowed.value, denied.value) 169 | end 170 | 171 | --[=[ 172 | @m allowAllPermissions 173 | @t http 174 | @r boolean 175 | @d Allows all permissions in this overwrite. 176 | ]=] 177 | function PermissionOverwrite:allowAllPermissions() 178 | local allowed, denied = getPermissions(self) 179 | allowed:enableAll(); denied:disableAll() 180 | return setPermissions(self, allowed.value, denied.value) 181 | end 182 | 183 | --[=[ 184 | @m denyAllPermissions 185 | @t http 186 | @r boolean 187 | @d Denies all permissions in this overwrite. 188 | ]=] 189 | function PermissionOverwrite:denyAllPermissions() 190 | local allowed, denied = getPermissions(self) 191 | allowed:disableAll(); denied:enableAll() 192 | return setPermissions(self, allowed.value, denied.value) 193 | end 194 | 195 | --[=[ 196 | @m clearAllPermissions 197 | @t http 198 | @r boolean 199 | @d Clears all permissions in this overwrite. 200 | ]=] 201 | function PermissionOverwrite:clearAllPermissions() 202 | local allowed, denied = getPermissions(self) 203 | allowed:disableAll(); denied:disableAll() 204 | return setPermissions(self, allowed.value, denied.value) 205 | end 206 | 207 | --[=[@p type string The overwrite type; either "role" or "member".]=] 208 | function get.type(self) 209 | if type(self._type) == 'string' then 210 | return self._type 211 | elseif self._type == 1 then 212 | return 'member' 213 | else -- 0 214 | return 'role' 215 | end 216 | end 217 | 218 | --[=[@p channel GuildChannel The channel in which this overwrite exists.]=] 219 | function get.channel(self) 220 | return self._parent 221 | end 222 | 223 | --[=[@p guild Guild The guild in which this overwrite exists. Equivalent to `PermissionOverwrite.channel.guild`.]=] 224 | function get.guild(self) 225 | return self._parent._parent 226 | end 227 | 228 | --[=[@p allowedPermissions number The number representing the total permissions allowed by this overwrite.]=] 229 | function get.allowedPermissions(self) 230 | return tonumber(self._allow_new) or tonumber(self._allow) 231 | end 232 | 233 | --[=[@p deniedPermissions number The number representing the total permissions denied by this overwrite.]=] 234 | function get.deniedPermissions(self) 235 | return tonumber(self._deny_new) or tonumber(self._deny) 236 | end 237 | 238 | return PermissionOverwrite 239 | -------------------------------------------------------------------------------- /libs/utils/Color.lua: -------------------------------------------------------------------------------- 1 | --[=[ 2 | @c Color 3 | @t ui 4 | @mt mem 5 | @p value number 6 | @d Wrapper for 24-bit colors packed as a decimal value. See the static constructors for more information. 7 | ]=] 8 | 9 | local class = require('class') 10 | 11 | local format = string.format 12 | local min, max, abs, floor = math.min, math.max, math.abs, math.floor 13 | local lshift, rshift = bit.lshift, bit.rshift 14 | local band, bor = bit.band, bit.bor 15 | local bnot = bit.bnot 16 | local isInstance = class.isInstance 17 | 18 | local Color, get = class('Color') 19 | 20 | local function check(self, other) 21 | if not isInstance(self, Color) or not isInstance(other, Color) then 22 | return error('Cannot perform operation with non-Color object', 2) 23 | end 24 | end 25 | 26 | local function clamp(n, mn, mx) 27 | return min(max(n, mn), mx) 28 | end 29 | 30 | function Color:__init(value) 31 | value = tonumber(value) 32 | self._value = value and band(value, 0xFFFFFF) or 0 33 | end 34 | 35 | function Color:__tostring() 36 | return format('Color: %s (%i, %i, %i)', self:toHex(), self:toRGB()) 37 | end 38 | 39 | function Color:__eq(other) check(self, other) 40 | return self._value == other._value 41 | end 42 | 43 | function Color:__add(other) check(self, other) 44 | local r = clamp(self.r + other.r, 0, 0xFF) 45 | local g = clamp(self.g + other.g, 0, 0xFF) 46 | local b = clamp(self.b + other.b, 0, 0xFF) 47 | return Color.fromRGB(r, g, b) 48 | end 49 | 50 | function Color:__sub(other) check(self, other) 51 | local r = clamp(self.r - other.r, 0, 0xFF) 52 | local g = clamp(self.g - other.g, 0, 0xFF) 53 | local b = clamp(self.b - other.b, 0, 0xFF) 54 | return Color.fromRGB(r, g, b) 55 | end 56 | 57 | function Color:__mul(other) 58 | if not isInstance(self, Color) then 59 | self, other = other, self 60 | end 61 | other = tonumber(other) 62 | if other then 63 | local r = clamp(self.r * other, 0, 0xFF) 64 | local g = clamp(self.g * other, 0, 0xFF) 65 | local b = clamp(self.b * other, 0, 0xFF) 66 | return Color.fromRGB(r, g, b) 67 | else 68 | return error('Cannot perform operation with non-numeric object') 69 | end 70 | end 71 | 72 | function Color:__div(other) 73 | if not isInstance(self, Color) then 74 | return error('Division with Color is not commutative') 75 | end 76 | other = tonumber(other) 77 | if other then 78 | local r = clamp(self.r / other, 0, 0xFF) 79 | local g = clamp(self.g / other, 0, 0xFF) 80 | local b = clamp(self.b / other, 0, 0xFF) 81 | return Color.fromRGB(r, g, b) 82 | else 83 | return error('Cannot perform operation with non-numeric object') 84 | end 85 | end 86 | 87 | --[=[ 88 | @m fromHex 89 | @t static 90 | @p hex string 91 | @r Color 92 | @d Constructs a new Color object from a hexadecimal string. The string may or may 93 | not be prefixed by `#`; all other characters are interpreted as a hex string. 94 | ]=] 95 | function Color.fromHex(hex) 96 | return Color(tonumber(hex:match('#?(.*)'), 16)) 97 | end 98 | 99 | --[=[ 100 | @m fromRGB 101 | @t static 102 | @p r number 103 | @p g number 104 | @p b number 105 | @r Color 106 | @d Constructs a new Color object from RGB values. Values are allowed to overflow 107 | though one component will not overflow to the next component. 108 | ]=] 109 | function Color.fromRGB(r, g, b) 110 | r = band(lshift(r, 16), 0xFF0000) 111 | g = band(lshift(g, 8), 0x00FF00) 112 | b = band(b, 0x0000FF) 113 | return Color(bor(bor(r, g), b)) 114 | end 115 | 116 | local function fromHue(h, c, m) 117 | local x = c * (1 - abs(h / 60 % 2 - 1)) 118 | local r, g, b 119 | if 0 <= h and h < 60 then 120 | r, g, b = c, x, 0 121 | elseif 60 <= h and h < 120 then 122 | r, g, b = x, c, 0 123 | elseif 120 <= h and h < 180 then 124 | r, g, b = 0, c, x 125 | elseif 180 <= h and h < 240 then 126 | r, g, b = 0, x, c 127 | elseif 240 <= h and h < 300 then 128 | r, g, b = x, 0, c 129 | elseif 300 <= h and h < 360 then 130 | r, g, b = c, 0, x 131 | end 132 | r = (r + m) * 0xFF 133 | g = (g + m) * 0xFF 134 | b = (b + m) * 0xFF 135 | return r, g, b 136 | end 137 | 138 | local function toHue(r, g, b) 139 | r = r / 0xFF 140 | g = g / 0xFF 141 | b = b / 0xFF 142 | local mn = min(r, g, b) 143 | local mx = max(r, g, b) 144 | local d = mx - mn 145 | local h 146 | if d == 0 then 147 | h = 0 148 | elseif mx == r then 149 | h = (g - b) / d % 6 150 | elseif mx == g then 151 | h = (b - r) / d + 2 152 | elseif mx == b then 153 | h = (r - g) / d + 4 154 | end 155 | h = floor(h * 60 + 0.5) 156 | return h, d, mx, mn 157 | end 158 | 159 | --[=[ 160 | @m fromHSV 161 | @t static 162 | @p h number 163 | @p s number 164 | @p v number 165 | @r Color 166 | @d Constructs a new Color object from HSV values. Hue is allowed to overflow 167 | while saturation and value are clamped to [0, 1]. 168 | ]=] 169 | function Color.fromHSV(h, s, v) 170 | h = h % 360 171 | s = clamp(s, 0, 1) 172 | v = clamp(v, 0, 1) 173 | local c = v * s 174 | local m = v - c 175 | local r, g, b = fromHue(h, c, m) 176 | return Color.fromRGB(r, g, b) 177 | end 178 | 179 | --[=[ 180 | @m fromHSL 181 | @t static 182 | @p h number 183 | @p s number 184 | @p l number 185 | @r Color 186 | @d Constructs a new Color object from HSL values. Hue is allowed to overflow 187 | while saturation and lightness are clamped to [0, 1]. 188 | ]=] 189 | function Color.fromHSL(h, s, l) 190 | h = h % 360 191 | s = clamp(s, 0, 1) 192 | l = clamp(l, 0, 1) 193 | local c = (1 - abs(2 * l - 1)) * s 194 | local m = l - c * 0.5 195 | local r, g, b = fromHue(h, c, m) 196 | return Color.fromRGB(r, g, b) 197 | end 198 | 199 | --[=[ 200 | @m toHex 201 | @r string 202 | @d Returns a 6-digit hexadecimal string that represents the color value. 203 | ]=] 204 | function Color:toHex() 205 | return format('#%06X', self._value) 206 | end 207 | 208 | --[=[ 209 | @m toRGB 210 | @r number 211 | @r number 212 | @r number 213 | @d Returns the red, green, and blue values that are packed into the color value. 214 | ]=] 215 | function Color:toRGB() 216 | return self.r, self.g, self.b 217 | end 218 | 219 | --[=[ 220 | @m toHSV 221 | @r number 222 | @r number 223 | @r number 224 | @d Returns the hue, saturation, and value that represents the color value. 225 | ]=] 226 | function Color:toHSV() 227 | local h, d, mx = toHue(self.r, self.g, self.b) 228 | local v = mx 229 | local s = mx == 0 and 0 or d / mx 230 | return h, s, v 231 | end 232 | 233 | --[=[ 234 | @m toHSL 235 | @r number 236 | @r number 237 | @r number 238 | @d Returns the hue, saturation, and lightness that represents the color value. 239 | ]=] 240 | function Color:toHSL() 241 | local h, d, mx, mn = toHue(self.r, self.g, self.b) 242 | local l = (mx + mn) * 0.5 243 | local s = d == 0 and 0 or d / (1 - abs(2 * l - 1)) 244 | return h, s, l 245 | end 246 | 247 | --[=[@p value number The raw decimal value that represents the color value.]=] 248 | function get.value(self) 249 | return self._value 250 | end 251 | 252 | local function getByte(value, offset) 253 | return band(rshift(value, offset), 0xFF) 254 | end 255 | 256 | --[=[@p r number The value that represents the color's red-level.]=] 257 | function get.r(self) 258 | return getByte(self._value, 16) 259 | end 260 | 261 | --[=[@p g number The value that represents the color's green-level.]=] 262 | function get.g(self) 263 | return getByte(self._value, 8) 264 | end 265 | 266 | --[=[@p b number The value that represents the color's blue-level.]=] 267 | function get.b(self) 268 | return getByte(self._value, 0) 269 | end 270 | 271 | local function setByte(value, offset, new) 272 | local byte = lshift(0xFF, offset) 273 | value = band(value, bnot(byte)) 274 | return bor(value, band(lshift(new, offset), byte)) 275 | end 276 | 277 | --[=[ 278 | @m setRed 279 | @r nil 280 | @d Sets the color's red-level. 281 | ]=] 282 | function Color:setRed(r) 283 | self._value = setByte(self._value, 16, r) 284 | end 285 | 286 | --[=[ 287 | @m setGreen 288 | @r nil 289 | @d Sets the color's green-level. 290 | ]=] 291 | function Color:setGreen(g) 292 | self._value = setByte(self._value, 8, g) 293 | end 294 | 295 | --[=[ 296 | @m setBlue 297 | @r nil 298 | @d Sets the color's blue-level. 299 | ]=] 300 | function Color:setBlue(b) 301 | self._value = setByte(self._value, 0, b) 302 | end 303 | 304 | --[=[ 305 | @m copy 306 | @r Color 307 | @d Returns a new copy of the original color object. 308 | ]=] 309 | function Color:copy() 310 | return Color(self._value) 311 | end 312 | 313 | return Color 314 | -------------------------------------------------------------------------------- /libs/voice/opus.lua: -------------------------------------------------------------------------------- 1 | local ffi = require('ffi') 2 | 3 | local loaded, lib = pcall(ffi.load, 'opus') 4 | if not loaded then 5 | return nil, lib 6 | end 7 | 8 | local new, typeof, gc = ffi.new, ffi.typeof, ffi.gc 9 | 10 | ffi.cdef[[ 11 | typedef int16_t opus_int16; 12 | typedef int32_t opus_int32; 13 | typedef uint16_t opus_uint16; 14 | typedef uint32_t opus_uint32; 15 | 16 | typedef struct OpusEncoder OpusEncoder; 17 | typedef struct OpusDecoder OpusDecoder; 18 | 19 | const char *opus_strerror(int error); 20 | const char *opus_get_version_string(void); 21 | 22 | OpusEncoder *opus_encoder_create(opus_int32 Fs, int channels, int application, int *error); 23 | int opus_encoder_init(OpusEncoder *st, opus_int32 Fs, int channels, int application); 24 | int opus_encoder_get_size(int channels); 25 | int opus_encoder_ctl(OpusEncoder *st, int request, ...); 26 | void opus_encoder_destroy(OpusEncoder *st); 27 | 28 | opus_int32 opus_encode( 29 | OpusEncoder *st, 30 | const opus_int16 *pcm, 31 | int frame_size, 32 | unsigned char *data, 33 | opus_int32 max_data_bytes 34 | ); 35 | 36 | opus_int32 opus_encode_float( 37 | OpusEncoder *st, 38 | const float *pcm, 39 | int frame_size, 40 | unsigned char *data, 41 | opus_int32 max_data_bytes 42 | ); 43 | 44 | OpusDecoder *opus_decoder_create(opus_int32 Fs, int channels, int *error); 45 | int opus_decoder_init(OpusDecoder *st, opus_int32 Fs, int channels); 46 | int opus_decoder_get_size(int channels); 47 | int opus_decoder_ctl(OpusDecoder *st, int request, ...); 48 | void opus_decoder_destroy(OpusDecoder *st); 49 | 50 | int opus_decode( 51 | OpusDecoder *st, 52 | const unsigned char *data, 53 | opus_int32 len, 54 | opus_int16 *pcm, 55 | int frame_size, 56 | int decode_fec 57 | ); 58 | 59 | int opus_decode_float( 60 | OpusDecoder *st, 61 | const unsigned char *data, 62 | opus_int32 len, 63 | float *pcm, 64 | int frame_size, 65 | int decode_fec 66 | ); 67 | ]] 68 | 69 | local opus = {} 70 | 71 | opus.OK = 0 72 | opus.BAD_ARG = -1 73 | opus.BUFFER_TOO_SMALL = -2 74 | opus.INTERNAL_ERROR = -3 75 | opus.INVALID_PACKET = -4 76 | opus.UNIMPLEMENTED = -5 77 | opus.INVALID_STATE = -6 78 | opus.ALLOC_FAIL = -7 79 | 80 | opus.APPLICATION_VOIP = 2048 81 | opus.APPLICATION_AUDIO = 2049 82 | opus.APPLICATION_RESTRICTED_LOWDELAY = 2051 83 | 84 | opus.AUTO = -1000 85 | opus.BITRATE_MAX = -1 86 | 87 | opus.SIGNAL_VOICE = 3001 88 | opus.SIGNAL_MUSIC = 3002 89 | opus.BANDWIDTH_NARROWBAND = 1101 90 | opus.BANDWIDTH_MEDIUMBAND = 1102 91 | opus.BANDWIDTH_WIDEBAND = 1103 92 | opus.BANDWIDTH_SUPERWIDEBAND = 1104 93 | opus.BANDWIDTH_FULLBAND = 1105 94 | 95 | opus.SET_APPLICATION_REQUEST = 4000 96 | opus.GET_APPLICATION_REQUEST = 4001 97 | opus.SET_BITRATE_REQUEST = 4002 98 | opus.GET_BITRATE_REQUEST = 4003 99 | opus.SET_MAX_BANDWIDTH_REQUEST = 4004 100 | opus.GET_MAX_BANDWIDTH_REQUEST = 4005 101 | opus.SET_VBR_REQUEST = 4006 102 | opus.GET_VBR_REQUEST = 4007 103 | opus.SET_BANDWIDTH_REQUEST = 4008 104 | opus.GET_BANDWIDTH_REQUEST = 4009 105 | opus.SET_COMPLEXITY_REQUEST = 4010 106 | opus.GET_COMPLEXITY_REQUEST = 4011 107 | opus.SET_INBAND_FEC_REQUEST = 4012 108 | opus.GET_INBAND_FEC_REQUEST = 4013 109 | opus.SET_PACKET_LOSS_PERC_REQUEST = 4014 110 | opus.GET_PACKET_LOSS_PERC_REQUEST = 4015 111 | opus.SET_DTX_REQUEST = 4016 112 | opus.GET_DTX_REQUEST = 4017 113 | opus.SET_VBR_CONSTRAINT_REQUEST = 4020 114 | opus.GET_VBR_CONSTRAINT_REQUEST = 4021 115 | opus.SET_FORCE_CHANNELS_REQUEST = 4022 116 | opus.GET_FORCE_CHANNELS_REQUEST = 4023 117 | opus.SET_SIGNAL_REQUEST = 4024 118 | opus.GET_SIGNAL_REQUEST = 4025 119 | opus.GET_LOOKAHEAD_REQUEST = 4027 120 | opus.GET_SAMPLE_RATE_REQUEST = 4029 121 | opus.GET_FINAL_RANGE_REQUEST = 4031 122 | opus.GET_PITCH_REQUEST = 4033 123 | opus.SET_GAIN_REQUEST = 4034 124 | opus.GET_GAIN_REQUEST = 4045 125 | opus.SET_LSB_DEPTH_REQUEST = 4036 126 | opus.GET_LSB_DEPTH_REQUEST = 4037 127 | opus.GET_LAST_PACKET_DURATION_REQUEST = 4039 128 | opus.SET_EXPERT_FRAME_DURATION_REQUEST = 4040 129 | opus.GET_EXPERT_FRAME_DURATION_REQUEST = 4041 130 | opus.SET_PREDICTION_DISABLED_REQUEST = 4042 131 | opus.GET_PREDICTION_DISABLED_REQUEST = 4043 132 | opus.SET_PHASE_INVERSION_DISABLED_REQUEST = 4046 133 | opus.GET_PHASE_INVERSION_DISABLED_REQUEST = 4047 134 | 135 | opus.FRAMESIZE_ARG = 5000 136 | opus.FRAMESIZE_2_5_MS = 5001 137 | opus.FRAMESIZE_5_MS = 5002 138 | opus.FRAMESIZE_10_MS = 5003 139 | opus.FRAMESIZE_20_MS = 5004 140 | opus.FRAMESIZE_40_MS = 5005 141 | opus.FRAMESIZE_60_MS = 5006 142 | opus.FRAMESIZE_80_MS = 5007 143 | opus.FRAMESIZE_100_MS = 5008 144 | opus.FRAMESIZE_120_MS = 5009 145 | 146 | local int_ptr_t = typeof('int[1]') 147 | local opus_int32_t = typeof('opus_int32') 148 | local opus_int32_ptr_t = typeof('opus_int32[1]') 149 | 150 | local function throw(code) 151 | local version = ffi.string(lib.opus_get_version_string()) 152 | local message = ffi.string(lib.opus_strerror(code)) 153 | return error(string.format('[%s] %s', version, message)) 154 | end 155 | 156 | local function check(value) 157 | return value >= opus.OK and value or throw(value) 158 | end 159 | 160 | local Encoder = {} 161 | Encoder.__index = Encoder 162 | 163 | function Encoder:__new(sample_rate, channels, app) -- luacheck: ignore self 164 | 165 | app = app or opus.APPLICATION_AUDIO -- TODO: test different applications 166 | 167 | local err = int_ptr_t() 168 | local state = lib.opus_encoder_create(sample_rate, channels, app, err) 169 | check(err[0]) 170 | 171 | check(lib.opus_encoder_init(state, sample_rate, channels, app)) 172 | 173 | return gc(state, lib.opus_encoder_destroy) 174 | 175 | end 176 | 177 | function Encoder:encode(input, input_len, frame_size, max_data_bytes) 178 | 179 | local pcm = new('opus_int16[?]', input_len, input) 180 | local data = new('unsigned char[?]', max_data_bytes) 181 | 182 | local ret = lib.opus_encode(self, pcm, frame_size, data, max_data_bytes) 183 | 184 | return data, check(ret) 185 | 186 | end 187 | 188 | function Encoder:get(id) 189 | local ret = opus_int32_ptr_t() 190 | lib.opus_encoder_ctl(self, id, ret) 191 | return check(ret[0]) 192 | end 193 | 194 | function Encoder:set(id, value) 195 | if type(value) ~= 'number' then return throw(opus.BAD_ARG) end 196 | local ret = lib.opus_encoder_ctl(self, id, opus_int32_t(value)) 197 | return check(ret) 198 | end 199 | 200 | opus.Encoder = ffi.metatype('OpusEncoder', Encoder) 201 | 202 | local Decoder = {} 203 | Decoder.__index = Decoder 204 | 205 | function Decoder:__new(sample_rate, channels) -- luacheck: ignore self 206 | 207 | local err = int_ptr_t() 208 | local state = lib.opus_decoder_create(sample_rate, channels, err) 209 | check(err[0]) 210 | 211 | check(lib.opus_decoder_init(state, sample_rate, channels)) 212 | 213 | return gc(state, lib.opus_decoder_destroy) 214 | 215 | end 216 | 217 | function Decoder:decode(data, len, frame_size, output_len) 218 | 219 | local pcm = new('opus_int16[?]', output_len) 220 | 221 | local ret = lib.opus_decode(self, data, len, pcm, frame_size, 0) 222 | 223 | return pcm, check(ret) 224 | 225 | end 226 | 227 | function Decoder:get(id) 228 | local ret = opus_int32_ptr_t() 229 | lib.opus_decoder_ctl(self, id, ret) 230 | return check(ret[0]) 231 | end 232 | 233 | function Decoder:set(id, value) 234 | if type(value) ~= 'number' then return throw(opus.BAD_ARG) end 235 | local ret = lib.opus_decoder_ctl(self, id, opus_int32_t(value)) 236 | return check(ret) 237 | end 238 | 239 | opus.Decoder = ffi.metatype('OpusDecoder', Decoder) 240 | 241 | return opus 242 | -------------------------------------------------------------------------------- /libs/containers/abstract/GuildChannel.lua: -------------------------------------------------------------------------------- 1 | --[=[ 2 | @c GuildChannel x Channel 3 | @t abc 4 | @d Defines the base methods and properties for all Discord guild channels. 5 | ]=] 6 | 7 | local json = require('json') 8 | local enums = require('enums') 9 | local class = require('class') 10 | local Channel = require('containers/abstract/Channel') 11 | local PermissionOverwrite = require('containers/PermissionOverwrite') 12 | local Invite = require('containers/Invite') 13 | local Cache = require('iterables/Cache') 14 | local Resolver = require('client/Resolver') 15 | 16 | local isInstance = class.isInstance 17 | local classes = class.classes 18 | local channelType = assert(enums.channelType) 19 | 20 | local insert, sort = table.insert, table.sort 21 | local min, max, floor = math.min, math.max, math.floor 22 | local huge = math.huge 23 | 24 | local GuildChannel, get = class('GuildChannel', Channel) 25 | 26 | function GuildChannel:__init(data, parent) 27 | Channel.__init(self, data, parent) 28 | self.client._channel_map[self._id] = parent 29 | self._permission_overwrites = Cache({}, PermissionOverwrite, self) 30 | return self:_loadMore(data) 31 | end 32 | 33 | function GuildChannel:_load(data) 34 | Channel._load(self, data) 35 | return self:_loadMore(data) 36 | end 37 | 38 | function GuildChannel:_loadMore(data) 39 | return self._permission_overwrites:_load(data.permission_overwrites, true) 40 | end 41 | 42 | --[=[ 43 | @m setName 44 | @t http 45 | @p name string 46 | @r boolean 47 | @d Sets the channel's name. This must be between 2 and 100 characters in length. 48 | ]=] 49 | function GuildChannel:setName(name) 50 | return self:_modify({name = name or json.null}) 51 | end 52 | 53 | --[=[ 54 | @m setCategory 55 | @t http 56 | @p id Channel-ID-Resolvable 57 | @r boolean 58 | @d Sets the channel's parent category. 59 | ]=] 60 | function GuildChannel:setCategory(id) 61 | id = Resolver.channelId(id) 62 | return self:_modify({parent_id = id or json.null}) 63 | end 64 | 65 | local function sorter(a, b) 66 | if a.position == b.position then 67 | return tonumber(a.id) < tonumber(b.id) 68 | else 69 | return a.position < b.position 70 | end 71 | end 72 | 73 | local function getSortedChannels(self) 74 | 75 | local channels 76 | local t = self._type 77 | if t == channelType.text or t == channelType.news then 78 | channels = self._parent._text_channels 79 | elseif t == channelType.voice then 80 | channels = self._parent._voice_channels 81 | elseif t == channelType.category then 82 | channels = self._parent._categories 83 | end 84 | 85 | local ret = {} 86 | for channel in channels:iter() do 87 | insert(ret, {id = channel._id, position = channel._position}) 88 | end 89 | sort(ret, sorter) 90 | 91 | return ret 92 | 93 | end 94 | 95 | local function setSortedChannels(self, channels) 96 | local data, err = self.client._api:modifyGuildChannelPositions(self._parent._id, channels) 97 | if data then 98 | return true 99 | else 100 | return false, err 101 | end 102 | end 103 | 104 | --[=[ 105 | @m moveUp 106 | @t http 107 | @p n number 108 | @r boolean 109 | @d Moves a channel up its list. The parameter `n` indicates how many spaces the 110 | channel should be moved, clamped to the highest position, with a default of 1 if 111 | it is omitted. This will also normalize the positions of all channels. 112 | ]=] 113 | function GuildChannel:moveUp(n) 114 | 115 | n = tonumber(n) or 1 116 | if n < 0 then 117 | return self:moveDown(-n) 118 | end 119 | 120 | local channels = getSortedChannels(self) 121 | 122 | local new = huge 123 | for i = #channels - 1, 0, -1 do 124 | local v = channels[i + 1] 125 | if v.id == self._id then 126 | new = max(0, i - floor(n)) 127 | v.position = new 128 | elseif i >= new then 129 | v.position = i + 1 130 | else 131 | v.position = i 132 | end 133 | end 134 | 135 | return setSortedChannels(self, channels) 136 | 137 | end 138 | 139 | --[=[ 140 | @m moveDown 141 | @t http 142 | @p n number 143 | @r boolean 144 | @d Moves a channel down its list. The parameter `n` indicates how many spaces the 145 | channel should be moved, clamped to the lowest position, with a default of 1 if 146 | it is omitted. This will also normalize the positions of all channels. 147 | ]=] 148 | function GuildChannel:moveDown(n) 149 | 150 | n = tonumber(n) or 1 151 | if n < 0 then 152 | return self:moveUp(-n) 153 | end 154 | 155 | local channels = getSortedChannels(self) 156 | 157 | local new = -huge 158 | for i = 0, #channels - 1 do 159 | local v = channels[i + 1] 160 | if v.id == self._id then 161 | new = min(i + floor(n), #channels - 1) 162 | v.position = new 163 | elseif i <= new then 164 | v.position = i - 1 165 | else 166 | v.position = i 167 | end 168 | end 169 | 170 | return setSortedChannels(self, channels) 171 | 172 | end 173 | 174 | --[=[ 175 | @m createInvite 176 | @t http 177 | @op payload table 178 | @r Invite 179 | @d Creates an invite to the channel. Optional payload fields are: max_age: number 180 | time in seconds until expiration, default = 86400 (24 hours), max_uses: number 181 | total number of uses allowed, default = 0 (unlimited), temporary: boolean whether 182 | the invite grants temporary membership, default = false, unique: boolean whether 183 | a unique code should be guaranteed, default = false 184 | ]=] 185 | function GuildChannel:createInvite(payload) 186 | local data, err = self.client._api:createChannelInvite(self._id, payload) 187 | if data then 188 | return Invite(data, self.client) 189 | else 190 | return nil, err 191 | end 192 | end 193 | 194 | --[=[ 195 | @m getInvites 196 | @t http 197 | @r Cache 198 | @d Returns a newly constructed cache of all invite objects for the channel. The 199 | cache and its objects are not automatically updated via gateway events. You must 200 | call this method again to get the updated objects. 201 | ]=] 202 | function GuildChannel:getInvites() 203 | local data, err = self.client._api:getChannelInvites(self._id) 204 | if data then 205 | return Cache(data, Invite, self.client) 206 | else 207 | return nil, err 208 | end 209 | end 210 | 211 | --[=[ 212 | @m getPermissionOverwriteFor 213 | @t mem 214 | @p obj Role/Member 215 | @r PermissionOverwrite 216 | @d Returns a permission overwrite object corresponding to the provided member or 217 | role object. If a cached overwrite is not found, an empty overwrite with 218 | zero-permissions is returned instead. Therefore, this can be used to create a 219 | new overwrite when one does not exist. Note that the member or role must exist 220 | in the same guild as the channel does. 221 | ]=] 222 | function GuildChannel:getPermissionOverwriteFor(obj) 223 | local id, type 224 | if isInstance(obj, classes.Role) and self._parent == obj._parent then 225 | id, type = obj._id, 'role' 226 | elseif isInstance(obj, classes.Member) and self._parent == obj._parent then 227 | id, type = obj._user._id, 'member' 228 | else 229 | return nil, 'Invalid Role or Member: ' .. tostring(obj) 230 | end 231 | local overwrites = self._permission_overwrites 232 | return overwrites:get(id) or overwrites:_insert(setmetatable({ 233 | id = id, type = type, allow = 0, deny = 0 234 | }, {__jsontype = 'object'})) 235 | end 236 | 237 | --[=[ 238 | @m delete 239 | @t http 240 | @r boolean 241 | @d Permanently deletes the channel. This cannot be undone! 242 | ]=] 243 | function GuildChannel:delete() 244 | return self:_delete() 245 | end 246 | 247 | --[=[@p permissionOverwrites Cache An iterable cache of all overwrites that exist in this channel. To access an 248 | overwrite that may exist, but is not cached, use `GuildChannel:getPermissionOverwriteFor`.]=] 249 | function get.permissionOverwrites(self) 250 | return self._permission_overwrites 251 | end 252 | 253 | --[=[@p name string The name of the channel. This should be between 2 and 100 characters in length.]=] 254 | function get.name(self) 255 | return self._name 256 | end 257 | 258 | --[=[@p position number The position of the channel, where 0 is the highest.]=] 259 | function get.position(self) 260 | return self._position 261 | end 262 | 263 | --[=[@p guild Guild The guild in which this channel exists.]=] 264 | function get.guild(self) 265 | return self._parent 266 | end 267 | 268 | --[=[@p category GuildCategoryChannel/nil The parent channel category that may contain this channel.]=] 269 | function get.category(self) 270 | return self._parent._categories:get(self._parent_id) 271 | end 272 | 273 | --[=[@p private boolean Whether the "everyone" role has permission to view this 274 | channel. In the Discord channel, private text channels are indicated with a lock 275 | icon and private voice channels are not visible.]=] 276 | function get.private(self) 277 | local overwrite = self._permission_overwrites:get(self._parent._id) 278 | return overwrite and overwrite:getDeniedPermissions():has('readMessages') 279 | end 280 | 281 | return GuildChannel 282 | -------------------------------------------------------------------------------- /docgen.lua: -------------------------------------------------------------------------------- 1 | --[=[ 2 | @c ClassName [x base_1 x base_2 ... x base_n] 3 | @t tag 4 | @mt methodTag (applies to all class methods) 5 | @p parameterName type 6 | @op optionalParameterName type 7 | @d description+ 8 | ]=] 9 | 10 | --[=[ 11 | @m methodName 12 | @t tag 13 | @p parameterName type 14 | @op optionalParameterName type 15 | @r return 16 | @d description+ 17 | ]=] 18 | 19 | --[=[ 20 | @p propertyName type description+ 21 | ]=] 22 | 23 | local fs = require('fs') 24 | local pathjoin = require('pathjoin') 25 | 26 | local insert, sort, concat = table.insert, table.sort, table.concat 27 | local format = string.format 28 | local pathJoin = pathjoin.pathJoin 29 | 30 | local function scan(dir) 31 | for fileName, fileType in fs.scandirSync(dir) do 32 | local path = pathJoin(dir, fileName) 33 | if fileType == 'file' then 34 | coroutine.yield(path) 35 | else 36 | scan(path) 37 | end 38 | end 39 | end 40 | 41 | local function match(s, pattern) -- only useful for one capture 42 | return assert(s:match(pattern), s) 43 | end 44 | 45 | local function gmatch(s, pattern, hash) -- only useful for one capture 46 | local tbl = {} 47 | if hash then 48 | for k in s:gmatch(pattern) do 49 | tbl[k] = true 50 | end 51 | else 52 | for v in s:gmatch(pattern) do 53 | insert(tbl, v) 54 | end 55 | end 56 | return tbl 57 | end 58 | 59 | local function matchType(s) 60 | return s:match('^@(%S+)') 61 | end 62 | 63 | local function matchComments(s) 64 | return s:gmatch('--%[=%[%s*(.-)%s*%]=%]') 65 | end 66 | 67 | local function matchClassName(s) 68 | return match(s, '@c (%S+)') 69 | end 70 | 71 | local function matchMethodName(s) 72 | return match(s, '@m (%S+)') 73 | end 74 | 75 | local function matchDescription(s) 76 | return match(s, '@d (.+)'):gsub('%s+', ' ') 77 | end 78 | 79 | local function matchParents(s) 80 | return gmatch(s, 'x (%S+)') 81 | end 82 | 83 | local function matchReturns(s) 84 | return gmatch(s, '@r (%S+)') 85 | end 86 | 87 | local function matchTags(s) 88 | return gmatch(s, '@t (%S+)', true) 89 | end 90 | 91 | local function matchMethodTags(s) 92 | return gmatch(s, '@mt (%S+)', true) 93 | end 94 | 95 | local function matchProperty(s) 96 | local a, b, c = s:match('@p (%S+) (%S+) (.+)') 97 | return { 98 | name = assert(a, s), 99 | type = assert(b, s), 100 | desc = assert(c, s):gsub('%s+', ' '), 101 | } 102 | end 103 | 104 | local function matchParameters(s) 105 | local ret = {} 106 | for optional, paramName, paramType in s:gmatch('@(o?)p (%S+) (%S+)') do 107 | insert(ret, {paramName, paramType, optional == 'o'}) 108 | end 109 | return ret 110 | end 111 | 112 | local function matchMethod(s) 113 | return { 114 | name = matchMethodName(s), 115 | desc = matchDescription(s), 116 | parameters = matchParameters(s), 117 | returns = matchReturns(s), 118 | tags = matchTags(s), 119 | } 120 | end 121 | 122 | ---- 123 | 124 | local docs = {} 125 | 126 | local function newClass() 127 | 128 | local class = { 129 | methods = {}, 130 | statics = {}, 131 | properties = {}, 132 | } 133 | 134 | local function init(s) 135 | class.name = matchClassName(s) 136 | class.parents = matchParents(s) 137 | class.desc = matchDescription(s) 138 | class.parameters = matchParameters(s) 139 | class.tags = matchTags(s) 140 | class.methodTags = matchMethodTags(s) 141 | assert(not docs[class.name], 'duplicate class: ' .. class.name) 142 | docs[class.name] = class 143 | end 144 | 145 | return class, init 146 | 147 | end 148 | 149 | for f in coroutine.wrap(scan), './libs' do 150 | 151 | local d = assert(fs.readFileSync(f)) 152 | 153 | local class, initClass = newClass() 154 | for s in matchComments(d) do 155 | local t = matchType(s) 156 | if t == 'c' then 157 | initClass(s) 158 | elseif t == 'm' then 159 | local method = matchMethod(s) 160 | for k, v in pairs(class.methodTags) do 161 | method.tags[k] = v 162 | end 163 | method.class = class 164 | insert(method.tags.static and class.statics or class.methods, method) 165 | elseif t == 'p' then 166 | insert(class.properties, matchProperty(s)) 167 | end 168 | end 169 | 170 | end 171 | 172 | ---- 173 | 174 | local output = 'docs' 175 | 176 | local function link(str) 177 | if type(str) == 'table' then 178 | local ret = {} 179 | for i, v in ipairs(str) do 180 | ret[i] = link(v) 181 | end 182 | return concat(ret, ', ') 183 | else 184 | local ret = {} 185 | for t in str:gmatch('[^/]+') do 186 | insert(ret, docs[t] and format('[[%s]]', t) or t) 187 | end 188 | return concat(ret, '/') 189 | end 190 | end 191 | 192 | local function sorter(a, b) 193 | return a.name < b.name 194 | end 195 | 196 | local function writeHeading(f, heading) 197 | f:write('## ', heading, '\n\n') 198 | end 199 | 200 | local function writeProperties(f, properties) 201 | sort(properties, sorter) 202 | f:write('| Name | Type | Description |\n') 203 | f:write('|-|-|-|\n') 204 | for _, v in ipairs(properties) do 205 | f:write('| ', v.name, ' | ', link(v.type), ' | ', v.desc, ' |\n') 206 | end 207 | f:write('\n') 208 | end 209 | 210 | local function writeParameters(f, parameters) 211 | f:write('(') 212 | local optional 213 | if #parameters > 0 then 214 | for i, param in ipairs(parameters) do 215 | f:write(param[1]) 216 | if i < #parameters then 217 | f:write(', ') 218 | end 219 | if param[3] then 220 | optional = true 221 | end 222 | end 223 | f:write(')\n\n') 224 | if optional then 225 | f:write('| Parameter | Type | Optional |\n') 226 | f:write('|-|-|:-:|\n') 227 | for _, param in ipairs(parameters) do 228 | local o = param[3] and '✔' or '' 229 | f:write('| ', param[1], ' | ', link(param[2]), ' | ', o, ' |\n') 230 | end 231 | f:write('\n') 232 | else 233 | f:write('| Parameter | Type |\n') 234 | f:write('|-|-|\n') 235 | for _, param in ipairs(parameters) do 236 | f:write('| ', param[1], ' | ', link(param[2]), ' |\n') 237 | end 238 | f:write('\n') 239 | end 240 | else 241 | f:write(')\n\n') 242 | end 243 | end 244 | 245 | local methodTags = {} 246 | 247 | methodTags['http'] = 'This method always makes an HTTP request.' 248 | methodTags['http?'] = 'This method may make an HTTP request.' 249 | methodTags['ws'] = 'This method always makes a WebSocket request.' 250 | methodTags['mem'] = 'This method only operates on data in memory.' 251 | 252 | local function checkTags(tbl, check) 253 | for i, v in ipairs(check) do 254 | if tbl[v] then 255 | for j, w in ipairs(check) do 256 | if i ~= j then 257 | if tbl[w] then 258 | return error(string.format('mutually exclusive tags encountered: %s and %s', v, w), 1) 259 | end 260 | end 261 | end 262 | end 263 | end 264 | end 265 | 266 | local function writeMethods(f, methods) 267 | 268 | sort(methods, sorter) 269 | for _, method in ipairs(methods) do 270 | 271 | f:write('### ', method.name) 272 | writeParameters(f, method.parameters) 273 | f:write(method.desc, '\n\n') 274 | 275 | local tags = method.tags 276 | checkTags(tags, {'http', 'http?', 'mem'}) 277 | checkTags(tags, {'ws', 'mem'}) 278 | 279 | for k in pairs(tags) do 280 | if k ~= 'static' then 281 | assert(methodTags[k], k) 282 | f:write('*', methodTags[k], '*\n\n') 283 | end 284 | end 285 | 286 | f:write('**Returns:** ', link(method.returns), '\n\n----\n\n') 287 | 288 | end 289 | 290 | end 291 | 292 | if not fs.existsSync(output) then 293 | fs.mkdirSync(output) 294 | end 295 | 296 | local function collectParents(parents, k, ret, seen) 297 | ret = ret or {} 298 | seen = seen or {} 299 | for _, parent in ipairs(parents) do 300 | parent = docs[parent] 301 | if parent then 302 | for _, v in ipairs(parent[k]) do 303 | if not seen[v] then 304 | seen[v] = true 305 | insert(ret, v) 306 | end 307 | end 308 | end 309 | collectParents(parent.parents, k, ret, seen) 310 | end 311 | return ret 312 | end 313 | 314 | for _, class in pairs(docs) do 315 | 316 | local f = io.open(pathJoin(output, class.name .. '.md'), 'w') 317 | 318 | local parents = class.parents 319 | local parentLinks = link(parents) 320 | 321 | if next(parents) then 322 | f:write('#### *extends ', parentLinks, '*\n\n') 323 | end 324 | 325 | f:write(class.desc, '\n\n') 326 | 327 | checkTags(class.tags, {'ui', 'abc'}) 328 | if class.tags.ui then 329 | writeHeading(f, 'Constructor') 330 | f:write('### ', class.name) 331 | writeParameters(f, class.parameters) 332 | elseif class.tags.abc then 333 | f:write('*This is an abstract base class. Direct instances should never exist.*\n\n') 334 | else 335 | f:write('*Instances of this class should not be constructed by users.*\n\n') 336 | end 337 | 338 | local properties = collectParents(parents, 'properties') 339 | if next(properties) then 340 | writeHeading(f, 'Properties Inherited From ' .. parentLinks) 341 | writeProperties(f, properties) 342 | end 343 | 344 | if next(class.properties) then 345 | writeHeading(f, 'Properties') 346 | writeProperties(f, class.properties) 347 | end 348 | 349 | local statics = collectParents(parents, 'statics') 350 | if next(statics) then 351 | writeHeading(f, 'Static Methods Inherited From ' .. parentLinks) 352 | writeMethods(f, statics) 353 | end 354 | 355 | local methods = collectParents(parents, 'methods') 356 | if next(methods) then 357 | writeHeading(f, 'Methods Inherited From ' .. parentLinks) 358 | writeMethods(f, methods) 359 | end 360 | 361 | if next(class.statics) then 362 | writeHeading(f, 'Static Methods') 363 | writeMethods(f, class.statics) 364 | end 365 | 366 | if next(class.methods) then 367 | writeHeading(f, 'Methods') 368 | writeMethods(f, class.methods) 369 | end 370 | 371 | f:close() 372 | 373 | end 374 | --------------------------------------------------------------------------------