├── .gitattributes ├── .gitignore ├── .gitmodules ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── docs ├── api.md ├── cdn.md ├── cli.md ├── const.md ├── outgoing-webhook-server.md ├── shard.md ├── util.intents.md ├── util.json.md ├── util.logger.md ├── util.md ├── util.mutex.md ├── util.session-limit.md └── util.uint.md ├── lacord-scm-4.rockspec ├── licenses ├── discordia └── luajit ├── lua └── lacord │ ├── api │ ├── init.lua │ ├── methods.lua │ ├── payload.lua │ ├── ratelimiting.lua │ └── webhooks.lua │ ├── bot.lua │ ├── cdn.lua │ ├── command.lua │ ├── const.lua │ ├── models │ ├── channel.lua │ ├── common │ │ └── send-interaction.lua │ ├── context.lua │ ├── guild.lua │ ├── impl │ │ ├── channel.lua │ │ ├── constructors.lua │ │ ├── guild.lua │ │ ├── init.lua │ │ ├── interaction.lua │ │ ├── message.lua │ │ ├── role.lua │ │ └── user.lua │ ├── interaction.lua │ ├── magic-numbers.lua │ ├── message.lua │ ├── methods.lua │ └── user.lua │ ├── outgoing-webhook-server-1.lua │ ├── outgoing-webhook-server-2.lua │ ├── shard.lua │ ├── ui │ ├── button.lua │ ├── common.lua │ ├── init.lua │ ├── select-menu.lua │ └── text-box.lua │ ├── util │ ├── cli_auto.lua │ ├── cli_default.lua │ ├── init.lua │ ├── intents.lua │ ├── json.lua │ ├── locales.lua │ ├── logger.lua │ ├── mime.lua │ ├── models │ │ ├── init.lua │ │ └── magic-numbers.lua │ ├── mutex.lua │ ├── session-limit.lua │ ├── translator.lua │ └── uint.lua │ └── wrapper │ └── outgoing-webhook-server.lua └── src └── archp.c /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | 3 | *.lua filter=untab -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /test/** 2 | /generated/** 3 | /.circleci/** 4 | /.busted 5 | *.src.rock 6 | /rockspecs/** 7 | /manual/** 8 | *.o 9 | *.so 10 | .vscode/** 11 | /docgen/** 12 | /site/** 13 | /compiles/** 14 | make-lacord.lua 15 | /lua/lacord/models/webhook.lua 16 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "ext/shs"] 2 | path = ext/shs 3 | url = https://github.com/Mehgugs/shs 4 | [submodule "ext/internationalize"] 5 | path = ext/internationalize 6 | url = https://github.com/Mehgugs/internationalize 7 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## changelog 2 | 3 | Here changes from versions `1590965828` onward are listed. 4 | 5 | ### 1629838555 -> 1637789515 6 | 7 | #### [Documentation](docs) 8 | 9 | - Documentation for `lacord.cli` was added. 10 | - Documentation for `lacord.util.session-limit` was added. 11 | - Wording was tweaked in `api.md`. 12 | 13 | #### NEW lacord.cli 14 | 15 | This new module has been introduced to facilitate command line configuration. 16 | Most users will interact with it via the `-l` module like so: 17 | `$ lua -lacord myscript.lua --unstable --deprecated --token XXXXX` 18 | 19 | Please refer to [the associated documentation](docs/cli.md) for usage information. 20 | 21 | The new `--unstable` flag added in this release will now control the usage of bleeding edge parts of 22 | the discord api. Setting this flag may cause discord to return errors or unexpected data. 23 | 24 | The new `--deprecated` flag added in this release will now be used to control availability of 25 | some deprecated features. Everything made available by this flag **will be removed in the next major version**. 26 | 27 | In `1637789515` the following features were marked as deprecated: 28 | 29 | - function `lacord.api.init` (was renamed) 30 | - function `lacord.shard.init` (was renamed) 31 | 32 | #### [lacord.api](lua/lacord/api.lua) 33 | 34 | - `api.init(options)` was renamed to `api.new`. 35 | - Providing lacord with the `--unstable` flag will change how attachments are sent, following the new? 36 | guidelines. This will be enforced at some unspecified point in the future, and this soft change has 37 | been marked as unstable as it may change considerably on discord's whim. This does not impact lacord's API. 38 | - Added a missing method, `modify_current_member`. 39 | - Fixed the `get_token` method. 40 | - Added support for guild scheduled events. 41 | - Fixed some endpoint formatting issues. 42 | - `edit_original_interaction_response` now accepts files. 43 | 44 | #### [lacord.shard](lua/lacord/shard.lua) 45 | - Add a session limit object to implement the `max_concurrency` 46 | rules mandated by discord. This object is automatically used 47 | by shards to orchestrate them. 48 | - !!`shard.new` now takes a session-limit object as its second argument. 49 | - !!`shard:send` now returns a boolean. 50 | - `shard.init(options, session_limit)` was renamed to `new`. 51 | 52 | #### [lacord.util](lua/lacord/util/init.lua) 53 | 54 | - Added command line processing functions. 55 | - Fixed `util.urlencoded_t` being incorrectly serialized. 56 | 57 | 58 | #### NEW [lacord.util.session-limit](lua/lacord/util/session-limit.lua) 59 | 60 | Added a new module for shard session limiting. Please see the [associated documentation](docs/util.session-limit.md) for more information. 61 | 62 | 63 | ### 1627995481.88199 -> 1629838555 64 | 65 | #### NEW [Documentation](docs) 66 | 67 | Markdown documentation was added in between releases, this now covers the public 68 | facing areas of the project. There may be methods that have been left out of the 69 | documentation: this is intentional. 70 | 71 | #### [lacord.api](lua/lacord/api.lua) 72 | 73 | - Added a missing method, `get_guild_application_commands`. 74 | - Fixed an incorrect HTTP verb in the `edit_original_interaction_response` request. 75 | - Added payload debugging. 76 | 77 | #### [lacord.cdn](lua/lacord/cdn.lua) 78 | 79 | - Fixed sticker image extensions not being added to the sticker url. 80 | - One can now use the format type as the value of `ext` for stickers. 81 | 82 | #### [lacord.util](lua/lacord/util/init.lua) 83 | 84 | - Fixed some content-typed objects not producing filenames correctly. 85 | 86 | #### [lacord.util.json](lua/lacord/util/json.lua) 87 | 88 | - Changed `content_type` to re-use metatables if 89 | the argument does not have a metatable. 90 | 91 | 92 | 93 | ### [Hotfix release] 1627995481 -> 1627995481.88199 94 | 95 | #### [lacord.api](lua/lacord/api.lua) 96 | 97 | - Fix a ratelimiting unlock I wasn't 100% on. 98 | - Add webhook token to the major parameters. 99 | - Support sending files through interactions. 100 | 101 | ### 1622157568 -> 1627995481 102 | 103 | - The dependencies list now contains `luatweetnacl` and `inspect`. 104 | - Removed the crude websocket patch. 105 | 106 | #### NEW [lacord.util.archp](src/archp.c) 107 | 108 | - Added a util module for platform fingerprinting, this is now what `util.platform` is initialized from. 109 | 110 | #### NEW [lacord.cdn](lua/lacord/cdn.lua) 111 | 112 | - Added a CDN client for obtaining urls and resources from the discord CDN. 113 | - This module provides a client at `cdn.new{options...}`. 114 | - This module provides `cdn.resouce_url(parameters..., ext, size)` see discord's reference section 115 | of the documentation for a list of cdn resources. 116 | 117 | #### [lacord.const](lua/lacord/const.lua) 118 | 119 | - Removed cdn URLS. see [lacord.cdn](lua/lacord/cdn.lua). 120 | - Using api version 9. 121 | 122 | #### NEW [lacord.outgoing-webhook-server](lua/lacord/outgoing-webhook-server.lua) 123 | 124 | - Added a new module for hosting an outgoing webhook for slash commands. 125 | - This module provides `outgoing_webhook_server.new(options, crtpath, keypath)`. 126 | This returns a cqueues server object suitable for accepting slash commands. 127 | Refer to the [README](README.md#slash-commands). 128 | 129 | 130 | 131 | #### [lacord.util](lua/lacord/util/init.lua) 132 | 133 | - Added support for content typed file objects. 134 | These are lightweight containers which associate a content type with data. 135 | These file containers are used to send files to discord in attachments. 136 | - Improved `util.platform`. 137 | - Added `util.version` for detecting lua version more easily. 138 | - Added `util.a_blob_of` for constructing content typed containers for raw data with 139 | an associated mime type. 140 | - Added `util.blob_for_file` for preparing a content typed file for serializing as a file with name. 141 | - Added `util.content_typed` to resolve an object with respect to content type tags. 142 | This returns `payload, cotnent_type` and is suitable for serialization or sending if the content type is present, if not content type is returned then it may have ignored incompatible objects. 143 | - Added `util.the_content_type` to get the content type of a content typed object. 144 | - Added `util.plaintext` `util.binary` `util.urlencoded` `util.png` `util.json_string` 145 | as common content type constructors. 146 | - Added `util.form` for marking POST bodies as form data, this is only respected when attaching files. 147 | 148 | #### [lacord.api](lua/lacord/api.lua) 149 | 150 | - Added global ratelimit support. (needs testing) 151 | - Removed `api.static` 152 | - Added sticker methods. 153 | - Added stage instance methods. 154 | - Added misc. methods like GET `oauth2/token` 155 | - Added contextual audit log reasons: 156 | ```lua 157 | api.with_reason "The reason I'm doing the action" 158 | api_client:auditloggable(...) 159 | ``` 160 | - Added / fixed thread methods. 161 | - Added some more api client options mostly for debugging. 162 | - Changed how file uploads are structured: 163 | - Pass in an array of content typed files instead of `{name, data}` pairs. 164 | - Set file names with `util.set_file_name(content_typed)`. 165 | 166 | 167 | ### 1619975269 -> 1622157568 168 | 169 | - The dependencies list now contains an incompatibility with the rock `lua-cjson`. 170 | you may need to do a fresh install of `lacord` after removing it if there's an issue. 171 | 172 | #### NEW [lacord.util.json](lua/lacord/util/json.lua) 173 | 174 | - Added a util module which controls the json encoder / decoder used by lacord. 175 | - This module provides the usual `encode()`, `decode()`, and `null` features. 176 | - Empty tables are configured to be parsed as empty json arrays. 177 | - A set of extra utilities are also provided: `jarray` will construct a new table 178 | which will always be parsed as a json array; `empty_array` is an opaque table 179 | that represents an empty json array. 180 | 181 | #### [lacord.const](lua/lacord/const.lua) 182 | 183 | - Added a flag `use_cjson` which governs what json library `lacord.json` becomes. 184 | - This flag is not intended for end users, and is used to modify the library's behaviour 185 | should the need arise. 186 | 187 | ### 1618833413 -> 1619975269 188 | 189 | #### [lacord.api](lua/lacord/api.lua) 190 | 191 | - Added requests for threads and interactions. This is **not stable**. 192 | - Deprecated `api.static` in favour of a webhook client (it can only use "static" methods as before but ratelimit caches are by webhook token). 193 | - Introduced dkjson as the encoder/decoder, this means lua's empty table `{}` is now treated as `[]` by the json encoder. 194 | - This is **temporary** as cjson needs to have its latest version published. If that is held up I will fork and publish my own rock. 195 | 196 | #### [lacord.util](lua/lacord/util/init.lua) 197 | 198 | - Added some string helpers. 199 | 200 | #### [lacord.util.date]() 201 | 202 | - REMOVED module. 203 | 204 | #### [lacord.util.plcompat]() 205 | 206 | - REMOVED module. 207 | 208 | #### [lacord.shard](lua/lacord/shard.lua) 209 | 210 | - Introduced dkjson as the encoder/decoder, this means lua's empty table `{}` is now treated as `[]` by the json encoder. 211 | - This is **temporary** as cjson needs to have its latest version published. 212 | 213 | - Fixed shards stalling due to a typo in messages. 214 | 215 | ### 1617477179 -> 1618833413 216 | 217 | #### global changes 218 | 219 | - The api and shard metatables are now explicit. 220 | 221 | #### [lacord.api](lua/lacord/api.lua) 222 | 223 | - Added api request methods, these cover everything *except* templates and slash commands right now. 224 | - Added `api.static` which is an instance of an api client with no token. 225 | This can be used to perform requests which do not require authentication, like executing webhooks. 226 | - Added `api:capture` for sequencing calls. 227 | - You can now attach text files with utf-8 encoding using `api:create_message_with_txt`. 228 | - You can now send other payload types via `api:request`, 229 | - Objects whose metatable has a `__lacord_content_type` will now be encoded using that 230 | same metatable's `__lacord_payload` function. 231 | - Bot, bearer, and client credientials are now supported authorization headers. 232 | 233 | #### [lacord.const](lua/lacord/const.lua) 234 | 235 | - Tweaked cdn links. 236 | 237 | #### [lacord.util](lua/lacord/util/init.lua) 238 | 239 | - REMOVED `util.interposable` 240 | - REMOVED `util.capturable` 241 | 242 | 243 | ### 1590965828 -> 1617477179 244 | 245 | #### global changes 246 | 247 | - Moved to version 8 of gateway. 248 | - Removed `lpeglabel` dependancy. 249 | - This removes `util.string` and `util.relabel`. 250 | - `lpeglabel` and `lpeg` are not compatible and cause the `lpeg` C library to fail (this is a hard crash). 251 | - Other dependencies (namely `lpegpatterns` from `http`) use `lpeg` so we are required to use it too. 252 | 253 | 254 | 255 | #### [lacord.api](lua/lacord/api.lua) 256 | 257 | - Fixed ratelimiting mostly. 258 | - Still needs another pass in future. 259 | - REMOVED error parsing for now, it will return in a future release. 260 | - REMOVED *most* api methods for now, they will return in a future release. 261 | 262 | #### [lacord.const](lua/lacord/const.lua) 263 | 264 | - Moved to `discord.com`. 265 | - Moved to `api_version` `8`. 266 | 267 | #### [lacord.util](lua/lacord/util/init.lua) 268 | 269 | - REMOVED all lpeg utilities. 270 | - made `util.capturable` more sensible. 271 | 272 | #### [lacord.util.plcompat](lua/lacord/util/plcompat.lua) 273 | 274 | - License now provided. 275 | 276 | #### [lacord.util.date](lua/lacord/util/plcompat.lua) 277 | 278 | - License now provided. -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ### Contribution Guidelines 2 | 3 | #### Documentation Guidelines 4 | 5 | When editing the documentation please make sure you follow the style outlined. 6 | If you want to add a markdown file that is not directly mapped to a file in the code, 7 | please open an issue first: the generating script for HTML has some options 8 | regarding their location. 9 | 10 | - Name files the way the lua module will be named with a lowercase `.md` extension. 11 | - Page titles must use h2 / `##`. 12 | - There should be an initial paragraph describing the module. 13 | - Please include context information in parentheses after broad types: 14 | ```md 15 | *string* not this! 16 | *string (uri)* this! 17 | ``` 18 | - The format for exported object documentation is: 19 | ```md 20 | #### *type* `name` 21 | 22 | Description of the export. 23 | ``` 24 | - The format for exported function documentation is: 25 | ```md 26 | #### *return type* `functionname(argument, ...)` 27 | 28 | Description of function. 29 | - *argument type* `argument` 30 | Optional description. 31 | 32 | ``` 33 | Please make sure vararg parameters `...` are documented with a bullets too. 34 | 35 | - You may inline documentation for tables in function arguments/members of exported tables: 36 | ```md 37 | *string (plaintext message)* `options.content` 38 | ``` 39 | 40 | - For exported types (realized in code as metatables with a constructor) 41 | document them using a h3 / `###` as follows: 42 | ```md 43 | ### *metatable* 44 | 45 | Description of the type. 46 | 47 | #### *metatable* `constructor(argument, ...)` 48 | ... 49 | 50 | 51 | #### *return type* `metatable:method(argument, ...)` 52 | ... 53 | 54 | ``` 55 | 56 | #### Code guidelines 57 | 58 | - All modules must use `local _ENV = {}`, this is to restrict globals. 59 | - **ALL** globals used must be localized at the top of the file. 60 | - Standard library methods must be localized individually, do not localize the whole module. 61 | - No tabs, use 4 spaces. 62 | - Use `_ENV` as the returned module table (as appropriate). 63 | - Use free variables in function declaration statements to export to `_ENV`. 64 | - **ALL** metatables must have a `__name`. 65 | - **ALL** `__name` fields must be scoped to lacord: `lacord.module.component`. 66 | - Do not use `_ENV` as a metatable. 67 | - Attach LDoc compatible comments if you wish to provide comments for functions. 68 | - Complex code / scattered code paths must have a documentation trail to explain. (See shard.lua's session limit explainers) 69 | - Please use `logger.throw` / `logger.fatal` to trigger a lua error. 70 | - If an object can/should be serialized to a file or sent over the internet, 71 | implement the content typed protocol for that object. 72 | 73 | #### Git Guidelines 74 | 75 | - PRs will be squashed, so please keep that in mind. 76 | - Always use long form commit messages. 77 | - Always make sure they have a descriptive summary line. 78 | - Always describe the changes you have made in a file, for each file changed. 79 | - Try to keep commits minimal, consider amending locally instead of pushing multiple commits. 80 | - Try to keep lines in your commit message body ~70 characters maximum. 81 | - Don't put emojis in summary messages. 82 | - When you fork please create a feature branch for a PR, do not use master. 83 | 84 | ##### Priority contributions 85 | 86 | - Bug fixes. 87 | - Improvements to existing code. 88 | - Coverage for discord api methods. 89 | - Documentation translations (open issue first). 90 | 91 | This list is not exhaustive, this is just some of the things I would be primarily interested in receiving PRs for. 92 | 93 | ##### What not to contribute 94 | 95 | - Clients (see [lacord-client](https://github.com/Mehgugs/lacord-client)). 96 | - Please check the above repo to see if either already provides what you want to add, or would 97 | be a better destination for your contribution. 98 | - Revisions to the rockspec (please open an issue and I will commit those if necessary). 99 | - Voice support. 100 | - Lua\[jit\] version compatibility. 101 | - CI. 102 | - All contributions to the `site` branch will be rejected, that branch is managed by a script. 103 | - Config files. 104 | - Coverage / Luacheck configuration. 105 | - .gitattribute changes. 106 | 107 | This list is not exhaustive. 108 | 109 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019-2021 Magicks 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## lacord 2 | 3 | lacord is a small discord library providing low level clients for the discord rest and gateway API. 4 | All data is given to the user as raw JSON. 5 | 6 | Documentation is sparsely provided in the form of LDoc comments which can be processed into a document using LDoc. 7 | There's hand written documentation in markdown format [here](docs) which can be viewed online [here](https://mehgugs.github.io/lacord/index.html). 8 | 9 | ## Example 10 | 11 | This example sends lines inputed at the terminal to discord over a supplied webhook. 12 | 13 | ```lua 14 | local api = require"lacord.api" 15 | local cqs = require"cqueues" 16 | local errno = require"cqueues.errno" 17 | local thread = require"cqueues.thread" 18 | local logger = require"lacord.util.logger" 19 | local webhook = require"lacord.cli".webhook 20 | 21 | local webhook_id, webhook_token = webhook:match"^(.+):(.+)$" 22 | 23 | local loop = cqs.new() 24 | 25 | local discord = api.new_webhook(webhook_id,webhook_token) 26 | 27 | local function starts(s, prefix) 28 | return s:sub(1, #prefix) == prefix 29 | end 30 | 31 | local function suffix(s, pre) 32 | local len = #pre 33 | return s:sub(1, len) == pre and s:sub(len + 1) or s 34 | end 35 | 36 | local thr, con = thread.start(function(con) 37 | print"Write messages to send over the webhook here!" 38 | for input in io.stdin:lines() do 39 | if input == ":quit" then break end 40 | con:write(input, "\n") 41 | end 42 | end) 43 | 44 | loop:wrap(function() 45 | local username = "lacord webhook example" 46 | for line in con:lines() do 47 | if starts(line, ":") then 48 | if starts(line, ":username ") then 49 | username = suffix(line, ":username ") 50 | end 51 | else 52 | local success = discord:execute_webhook{ 53 | content = line, 54 | username = username, 55 | } 56 | if not success then io.stdin:write":quit" break end 57 | end 58 | end 59 | 60 | local ok, why = thr:join() 61 | 62 | if not ok then logger.error("error in reader thread (%s, %q)", why, errno.strerror(why)) end 63 | end) 64 | 65 | assert(loop:loop()) 66 | ``` 67 | 68 | ## CDN Client Example 69 | 70 | ```lua 71 | local cqs = require"cqueues" 72 | local api = require"lacord.api" 73 | local cdn = require"lacord.cdn" 74 | local util = require"lacord.util" 75 | 76 | local loop = cqs.new() 77 | 78 | local discord_api = api.init{ 79 | token = "Bot "..require"lacord.cli".token 80 | ,accept_encoding = true 81 | ,track_ratelimits = false 82 | ,route_delay = 0 83 | } 84 | 85 | local a_cdn = cdn.new{ 86 | accept_encoding = true 87 | } 88 | 89 | loop:wrap(function() 90 | local success, data = discord_api:get_current_user() 91 | if success then 92 | local avatar = a_cdn:get_user_avatar(data.id, data.avatar, 'png') 93 | local fname, content = util.blob_for_file(avatar, "avatar") 94 | local fd = io.open(fname, "wb") 95 | fd:write(content) 96 | end 97 | end) 98 | 99 | assert(loop:loop()) 100 | ``` 101 | 102 | ## Installation 103 | 104 | This project depends on [`lua-http`](https://github.com/daurnimator/lua-http) and thus [`cqueues`](https://25thandclement.com/~william/projects/cqueues.html). This means that you must 105 | be able to install `cqueues` on your platform. 106 | 107 | You can consult the respective projects for 108 | detailed instructions but as a general guide the following tools/libraries should be installed and available on your system: 109 | 110 | - m4 111 | - awk 112 | - zlib-dev 113 | - libssl-dev (or equiv.)[¹](#note-1) 114 | 115 | Once you have the pre-requisites in order you can install this library with luarocks: 116 | 117 | - Directly `luarocks install lacord` 118 | - Via this repository 119 | - `git clone https://github.com/Mehgugs/lacord.git && cd lacord` 120 | - optionally checkout a specific commit 121 | - `luarocks make` 122 | 123 | ## Slash Commands 124 | 125 | This library provides support for slash commands naturally over the gateway and 126 | also provides a https server module under `lacord.outoing-webhook-server` for interfacing 127 | with discord over outgoing webhook. When using this method there are a couple of things to keep in mind: 128 | 129 | - You must use TLS. By default this module accepts two file paths after the server options table. 130 | The first one should be your full certificate chain in pem format and the second should be your private key in pem format. 131 | Should you wish to do more advanced TLS configuration, you can attach a ctx object to the options under `.ctx`. 132 | If you are using an external service to provide TLS upstream (e.g an nginx reverse proxy), you can forcefully disable TLS 133 | by setting `.tls` to `false`. 134 | 135 | - The first argument, the options table, is passed to `http.server.listen`. So please refer to the http library docs 136 | for a full list of network options. 137 | In addition to the `http` library's fields, the following are expected: 138 | - The string field `route` is the path component of the URL you configure your application to use. 139 | In the URL `https://example.com/interactions` this would be `/interactions`. Once again if you're 140 | redirecting traffic to lacord from an external service make sure the path is adjusted if necessary. 141 | - The function field `interact` is called when a discord interaction event is received by the webhook. 142 | The first argument is the json object payload discord sent, the next argument is the https response object. 143 | Return a valid json object from the function to send it to discord; if you do not it will respond with 500. 144 | Any error in this function is caught and will respond with 503, logging the message internally. 145 | You can also manipulate the response object to set the body directly, but this should be avoided unless necessary. 146 | - The function field `fallthrough` receives a response object, and is called with any other request (i.e requests to paths other than the `route`). 147 | - The string field `public_key` is your application's public key, necessary for signature verification. 148 | 149 | Here is a minimal example of configuration: 150 | 151 | ```lua 152 | local server = require"lacord.outgoing-webhook-server" 153 | 154 | local function interact(event, resp) 155 | if event.data.command == "hello" then 156 | return { 157 | type = 4, 158 | data = { 159 | content = "Hello, world!" 160 | } 161 | } 162 | else 163 | resp:set_code_and_reply(404, "Command not found.", "text/plain; charset=UTF-8") 164 | end 165 | end 166 | 167 | local loop = server.new({ 168 | public_key = os.getenv"PUBLIC_KEY", 169 | fallthrough = function(resp) resp:set_code_and_reply(404, "Page not found.", "text/plain; charset=UTF-8") end, 170 | interact = interact, 171 | host = "localhost", 172 | port = 8888, 173 | route = "/interactions" 174 | }) 175 | 176 | 177 | assert(loop:loop()) 178 | ``` 179 | 180 | The `loop` object has `.cq` field which can be used to `:wrap` asynchronous code. 181 | 182 | ## Notes 183 | 184 | #### Note 1 185 | I would recommend manually installing openssl with a version in the current stable series. 186 | At the time of writing this is the **1.1.1** series. -------------------------------------------------------------------------------- /docs/api.md: -------------------------------------------------------------------------------- 1 | ## lacord.api 2 | 3 | This module is used to connect to discord's restful api over http. 4 | All api methods must be called inside a cqueues managed coroutine. 5 | 6 | #### Ratelimits 7 | 8 | The api client will automatically handle ratelimits for you. 429s may still be encountered and will be handled as well. When system 9 | threads are involved – via cqueues threads – there are no provided mechanisms to serialize ratelimit state and sync up independant lua states, but this should not be a major issue. 10 | 11 | What follows is a short description of the ratelimiting algorithm used by lacord: 12 | 13 | 1. Calculate major parameters (via internal function `resolve_majors`) 14 | 2. Check if we already know what the ratelimit bucket is: 15 | - If yes, then continue. 16 | - If no, go to 4. 17 | 3. Get an instance of the ratelimiter and `:enter` it. 18 | 4. Make the request and resolve the 50/sec global ratelimit. 19 | 5. If it was the first time the ratelimit information is saved. 20 | 6. `:exit` from the bucket. 21 | 22 | 23 | 24 | #### *string* `USER_AGENT` 25 | 26 | The lacord user-agent used by all clients. 27 | 28 | #### *string* `URL` 29 | 30 | The api url used by the client. 31 | 32 | ### *api* 33 | 34 | This type has methods for interacting with the discord rest api. 35 | 36 | #### *api* `new(options)` 37 | 38 | This initializes the api client. 39 | 40 | - *{string, string}* `options.client_credentials` 41 | Set this field to to use Basic authentication for the client credentials grant. It should be a table of 42 | your id and client secret, for example: `{"92271879783469056", "3hPAIZAm7eb5vAhJSLSZiNhB7kANOOUp"}`. 43 | - *string* `options.token` 44 | Set this field to use either a Bearer or Bot token for authentication. Mutually exclusive with `client_credentials`. 45 | - *number (seconds)* `options.route_delay` 46 | Set this field to set a lower bound on the delay calculated when making requests. This is used to make the requests have more even availability but will reduce the throughput of the client. 47 | - *number (seconds)* `options.api_timeout` 48 | Set this field to control the request timeout. 49 | - *boolean* `options.accept_encoding` 50 | Set this flag to control whether the client should 51 | accept compressed data from discord. 52 | 53 | #### *boolean, applicaion/json|true, string, table* `api:method_name(...)` 54 | 55 | All requests you can make via this client are methods of the form: 56 | `api:`. For example "Create message" would be 57 | found at `api:create_message`. Consult discord's official documentation for a list of available api methods. All these methods return: a success 58 | boolean; response data decoded as json; an error message if success was false; and a table of errors if discord sent that in the error response. 59 | All of these functions accept arguments in the following way: first the route parameters as strings in the order they appear in the uri; then the payload if the route accepts a body; then the query if the route accepts a query; and finally a list of content typed objects to attach as files in a multipart request. 60 | Note that some endpoints may accept a query without a payload, in which case the arguments will look like: `route-parameters..., query`. 61 | In the case of `204` the second return value will simply be true. This client will only attempt to retry these requests on timeout, or if a ratelimit is hit. In the latter case the client will wait an appropriate amount of time before continuing to make the request. 62 | 63 | 64 | ##### NOTE 65 | 66 | The ["Guild scheduled events"](https://discord.com/developers/docs/resources/guild-scheduled-event) methods break the naming convention used by lacord: "List scheduled events for guild" would be found at `list_scheduled_guild_events`, and "Get Guild scheduled events" at `get_scheduled_guild_events`. This was done to improve readability because the names are rather long. 67 | 68 | #### *api.capture* `api:capture()` 69 | 70 | This function will create a capture object which can be used to safely sequence multiple requests together. 71 | 72 | ```lua 73 | -- somewhere in a cqueues coroutine... 74 | local api = require"lacord.api" 75 | local discord_api = api.init{blah} 76 | local R = discord_api 77 | :capture() 78 | :get_gateway_bot() 79 | :get_current_application_information() 80 | if R.success then -- ALL methods succeeded 81 | local results_list = R.result 82 | local A, B, C = R:results() 83 | else 84 | local why = R.error 85 | local partial = R.result 86 | -- There may be partial results collected before the error, you can use this to debug. 87 | R:some_method() -- If there's been a failure, calls like this are noop'd. 88 | end 89 | ``` 90 | 91 | #### *api.webhook* `new_webhook(webhook_id, webhook_token)` 92 | 93 | Create a client suitable for executing webhooks. 94 | The only methods this client has access to are 95 | the webhook methods, but the token and id will be inserted into the argument list for you internally. 96 | 97 | ```lua 98 | local hook = api.webhook_init(webhook_id, webhook_token) 99 | 100 | hook:execute_webhook{ 101 | content = line, 102 | username = username, 103 | } 104 | ``` 105 | 106 | #### *string* `with_reason(text)` 107 | 108 | Adds a new contextual audit log reason to the currently running cqueues coroutine. 109 | When requests are made in this coroutine which accept an audit log reason they 110 | will pull a reason from the thread's reason if one is available: 111 | 112 | ```lua 113 | -- somewhere in a cqueues coroutine... 114 | local api = require"lacord.api" 115 | local discord_api = api.init{blah} 116 | api.with_reason "I'm doing this because I can!" 117 | discord_api:create_guild_ban(...) -- this will have the reason defined above. 118 | ``` -------------------------------------------------------------------------------- /docs/cdn.md: -------------------------------------------------------------------------------- 1 | ## lacord.cdn 2 | 3 | This module is used to get discord CDN urls and fetch CDN resources. 4 | 5 | #### *string* `cdn_asset_url(...)` 6 | 7 | For each CDN endpoint listed [here][cdn_endpoints] there is 8 | a function in this module for creating a url. For example the "Custom Emoji" url function 9 | is found at `custom_emoji`. As arguments these functions take any parameters present in the documentation's url followed by the extension and size. 10 | 11 | ```lua 12 | local cdn = require"lacord.cdn" 13 | local the_url = cdn.custom_emoji_url(emoji_id, "png") 14 | ``` 15 | 16 | ### *cdn* 17 | 18 | This type has methods for retrieving assets from the discord CDN. 19 | 20 | #### *cdn* `new(options)` 21 | 22 | Constructs a new cdn client. 23 | 24 | - *number (http version)* `options.http_version` 25 | Set this field to control the http version used when making requests. 26 | - *boolean* `options.accept_encoding` 27 | Set this flag to control whether the client should 28 | accept compressed data from discord. 29 | - *number (seconds)* `options.api_timeout` 30 | Set this field to control the request timeout. 31 | 32 | #### *content_typed, string, table* `cdn:get_endpoint(...)` 33 | 34 | Fetches the an asset from the CDN. Similarly to the url functions, for every CDN endpoint listed [here][cdn_endpoints] there is a cdn client method for fetching 35 | an asset from the endpoint. For example the "Custom Emoji" method would be found at `cdn:get_custom_emoji`. All of these methods accept the same arguments as their associated url function. These functions return a content typed table, 36 | with the inner blob of data located at index 1. Read more about content types [here](util.html#content_typed). If these functions fail the first return will be nil, followed by an error message and an errors table if discord sent one. 37 | 38 | 39 | 40 | [cdn_endpoints]: 41 | https://discord.com/developers/docs/reference#image-formatting-cdn-endpoints -------------------------------------------------------------------------------- /docs/cli.md: -------------------------------------------------------------------------------- 1 | ## lacord.cli 2 | 3 | This module is used to set/get feature flags from the command line. 4 | 5 | ### Setting configuration values from the commandline 6 | 7 | You may provide configuration from the commandline to lacord, this can be loaded 8 | using [`module()`](#module). The presence of a flag on the commandline is enough to 9 | enable it, but for convenience an assortment of values are also supported. 10 | To set a flag simply append it as a commandline argument with the appropriate hypen prefix: 11 | `lua main.lua --debug`. To use and load this configuration, call this module inside your `main.lua` 12 | -- or equivalent entrypoint script -- and pass the vararg argument for the main chunk to the call like so: 13 | ```lua 14 | require"lacord.cli"(...) 15 | ``` 16 | 17 | #### *boolean|string* `option` 18 | 19 | Indexing this module will look up the key in the configuration. 20 | 21 | Currently the only recognized keys are: 22 | 23 | - *boolean* `debug` 24 | This field will enable debug logs. This is passed to the commandline as `--debug` 25 | - *boolean* `unstable` 26 | This field will enable unstable features. These are features of the library that 27 | are partially supported or discord has said should not be considered available 28 | for widespread use. Things may become gated behind this flag on discord's whim 29 | so please read the changelogs to see which features are behind this flag. 30 | This is passed to the commandline as `--unstable-features`. 31 | - *boolean* `deprecated` 32 | This field will enable deprecated features. These are features of the library that 33 | will be removed at a specified point in time. This is passed to the commandline as `--deprecated`. 34 | - *string* `log_file` 35 | This field will open a file in write mode and pass it to the logger to write into. This is passed to the commandline as `--log-file PATH` 36 | - *one of "0", "3", "8"* `log_mode` 37 | This field will set the logger's colour mode. This is passed to the commandline as 38 | `--log-mode n` 39 | - *boolean* `accept` 40 | This field will instruct the cli parser to admit arbitrary parameters. Currently only 41 | values are supported. This enables a user defined parameters to be captured in the cli module table. This **does not** affect the environment variables read. 42 | This is passed to the commandline as `--accept-everything`, but the shorthand mnemonic `-a` may be preferable. 43 | 44 | Additionally the following keys are loaded by this module **but are never used by lacord internally**. 45 | You must manually use them yourself where appropriate. 46 | 47 | - *string* `client_id` 48 | This field corresponds to the application's client id. This is passed to the commandline as `--client-id XXXXX`. 49 | 50 | - *string* `client_secret` 51 | This field corresponds to the application's client secret. This is passed to the commandline as `--client-secret XXXXX`. 52 | 53 | - *string* `token` 54 | This field corresponds to the application's bot token. This is passed to the commandline as `--token XXXXX`. 55 | 56 | 57 | All keys may also be set by environment variables by uppercasing the key and prepending `LACORD_`. 58 | For example, the `debug` key can be configured by the `LACORD_DEBUG` environment variable. 59 | 60 | In the case that an environment variable has been set **and** a commandline flag provided, 61 | this module will set the key if **either of them were positive**. 62 | 63 | #### *table (argv)* `module()` 64 | 65 | Calling the module like a function will load the function arguments as commandline arguments, 66 | and also resolve environment variables that are set. This returns the argv array. 67 | If the arguments do not match the scheme, argument resolving stops. The returned table contains 68 | all unprocessed arguments. 69 | 70 | Example with arguments as literals so you can see how it works: 71 | 72 | ```lua 73 | local arguments = require"lacord.cli"('--debug', '--unstable-features', 'foo') 74 | -- In this example the cli module now has the parameters `debug` and `unstable` set. 75 | -- `arguments` contains remaining commandline arguments that were not processed which in this case is 'foo'. 76 | ``` 77 | 78 | ### Shorthand parameters 79 | 80 | You may also specify parameters using a single `-`, in which case each character of the parameter is 81 | expanded to the corresponding long form and they are all set. You cannot use two different value flags in the same `-` string, because there will only be one value available for them to be assigned. In this scenario the module will set the first flag and stop expanding characters. 82 | 83 | Example invocation: 84 | ``` 85 | $ lua -lacord main.lua -dDuL 8 86 | ``` 87 | 88 | This would set `debug`, `deprecated`, `unstable` and set the `log_mode` to `8`. 89 | 90 | Characters and their corresponding key in cli: 91 | 92 | |Character|Field | 93 | |---------|---------------| 94 | |`d `|`debug `| 95 | |`D `|`deprecated `| 96 | |`u `|`unstable `| 97 | |`a `|`accept `| 98 | |`l `|`log_file `| 99 | |`L `|`log_mode `| 100 | |`i `|`client_id `| 101 | |`s `|`client_secret`| 102 | |`t `|`token `| 103 | 104 | ### Automatically loading flags in lua standalone 105 | 106 | The virtual module `acord` is designed to be loaded by the lua standalone interpreter option `l` (i.e `lua -lacord ...`). 107 | This module will read from `_G.arg` -- and also read environment variables -- and populate the cli table with options. 108 | 109 | On lua versions >= 5.4 this will emit lua warnings if there are malformed parameters. 110 | 111 | Example invocation: 112 | 113 | ```bash 114 | $ LACORD_TOKEN=XXXXXXXXXXXXXXXXXXXXXXXXXXXX lua -lacord main.lua --unstable-features 115 | ``` 116 | 117 | main.lua: 118 | ```lua 119 | .... 120 | local discord_api = api.init{ 121 | token = "Bot "..require"lacord.cli".token 122 | } 123 | .... 124 | ``` -------------------------------------------------------------------------------- /docs/const.md: -------------------------------------------------------------------------------- 1 | ## lacord.const 2 | 3 | This module contains static configuration. 4 | 5 | #### *string* `version` 6 | 7 | The version of lacord. 8 | 9 | #### *string* `homepage` 10 | 11 | The lacord project homepage. 12 | 13 | #### *integer* `api_version` 14 | 15 | The version of the discord api being used. -------------------------------------------------------------------------------- /docs/outgoing-webhook-server.md: -------------------------------------------------------------------------------- 1 | ## lacord.outgoing-webhook-server 2 | 3 | This module contains a low level https server for hosting an outgoing webhook for receiving discord interactions. 4 | 5 | ### *server* 6 | 7 | This type is a http server configured for use as an outgoing webhook. 8 | 9 | #### *server* `new(options, crtfile, keyfile)` 10 | 11 | Constructs a new https server. 12 | Providing TLS configuration (either by filepaths or context object) is optional, 13 | but by default a warning is printed because discord will not accept plain `http`. 14 | The callback `options.interact` can return a json object, and this will be set as 15 | the interaction response. You can also use the [response object](#response_object) to set the body manually. 16 | 17 | - *string* `options.public_key` 18 | Your applications public key, used for signature verification. 19 | - *function* `options.interact` 20 | The callback to handle interactions from discord. This is called with the interaction payload and a [response object](#response_object). 21 | - *function* `options.fallthrough` 22 | The callback to handle requests that are not on the route discord is configured to use. 23 | - *string* `options.route` 24 | The path discord will send interactions to, defaults to `/`. 25 | - *function* `options.onerror` 26 | The lua-http error handler, defaults to [`logger.error`](util.logger.html#error) 27 | - *openssl.ssl.context* `options.ctx` 28 | An openssl context for TLS. Mutually exclusive with `crtfile`. 29 | - *boolean* `options.ikwid` 30 | Set this flag to silence the warning message printing when TLS is not configured. 31 | - *string (filepath)* `crtfile` 32 | The path to your certificate chain file. 33 | - *string (filepath)* `keyfile` 34 | The path to your private key file. 35 | 36 | ### *response_object* 37 | 38 | This type has methods for setting the server response to an interaction. 39 | 40 | #### *headers* `response_object.request_headers` 41 | 42 | The request headers. 43 | 44 | #### *headers* `response_object.headers` 45 | 46 | The response headers. 47 | 48 | #### *string* `response_object.peername` 49 | 50 | The address and port at the other end of the connection. 51 | 52 | #### *string (http verb)* `response_object.method` 53 | 54 | The http verb. 55 | 56 | #### *string* `response_object.path` 57 | 58 | The path. 59 | 60 | 61 | #### *string* `response_object:set_body(body)` 62 | 63 | Sets the body to the specified content typed object. 64 | 65 | - *content_typed|string* `body` 66 | A content typed object will be resolved and the content-type header set along with the body, a string will set the body to that raw value. 67 | 68 | 69 | #### *nothing* `response_object:set_503()` 70 | 71 | Sets the status code to 503 and attaches a suitable body. 72 | 73 | #### *nothing* `response_object:set_500()` 74 | 75 | Sets the status code to 500 and attaches a suitable body. 76 | 77 | #### *nothing* `response_object:set_401()` 78 | 79 | Sets the status code to 401 and attaches a suitable body. 80 | 81 | #### *nothing* `response_object:set_ok()` 82 | 83 | Sets the status code to 204. 84 | 85 | #### *nothing* `response_object:set_ok_and_reply(body, content_type)` 86 | 87 | Sets the status code to 200 and attaches the given body. 88 | 89 | - *content_typed|string* `body` 90 | A content typed object will be resolved and the content-type header set along with the body, a string will set the body to that raw value. 91 | - *string (content_type)* `content_type` 92 | This will force the content-type set to be this argument if it is provided. 93 | 94 | #### *nothing* `response_object:set_code_and_reply(code, body, content_type)` 95 | 96 | Sets the status code to the given code and attaches the given body. 97 | 98 | - *number (http status code)* `code` 99 | - *content_typed|string* `body` 100 | A content typed object will be resolved and the content-type header set along with the body, a string will set the body to that raw value. 101 | - *string (content_type)* `content_type` 102 | This will force the content-type set to be this argument if it is provided. 103 | 104 | -------------------------------------------------------------------------------- /docs/shard.md: -------------------------------------------------------------------------------- 1 | ## lacord.shard 2 | 3 | This module is used to start a websocket session connected to discord's gateway. 4 | 5 | ### *shard* 6 | 7 | This type has methods for connecting and interaction with the discord gateway. 8 | 9 | #### *shard* `init(options, session_limiter)` 10 | 11 | Construct a new shard object using the given options and identify mutex. 12 | 13 | - *integer* `options.id` 14 | The ID of the shard. 15 | - *boolean* `options.transport_compression` 16 | Sets the transport compression gateway option, defaults to true. 17 | - *cqueue* `options.loop` 18 | Sets the cqueues controller object associated with this shard. 19 | - *function* `options.output` 20 | Sets the output callback to dispatch events to. 21 | - *string (url)|function* `options.gateway` 22 | Sets gateway url to connect to. This can be discovered by using `api:get_gateway_bot`. If this is a function it will be called with the shard object and must return the gateway url to use. 23 | - *boolean* `options.auto_reconnect` 24 | Set this flag to cause the shard to attempt to reconnect after a non-fatal disconnect. 25 | - *number (seconds)* `options.receive_timeout` 26 | Sets the timeout to use when reading from the websocket. 27 | - *integer (bitfield)* `options.intents` 28 | Sets the gateway intents to declare when identifying with discord. 29 | - *string* `options.token` 30 | Sets the token to use when identifying. 31 | - *integer* `options.large_threshold` 32 | Sets the large threshold value to declare when identifying. 33 | - *integer* `options.total_shard_count` 34 | The total amount of shards to declare when identiying. 35 | - *application/json* `options.presence` 36 | An initial presence object to declare when identiying. 37 | - *session limit* `session_limiter` 38 | A [session limit](util.session-limit.md) object created to handle sequencing the shards. 39 | 40 | #### *shard* `shard:connect()` 41 | 42 | Connects the shard to discord. This function is asynchronous and should be run inside a cqueues coroutine. (usually from state.loop) 43 | 44 | #### *shard* `shard:disconnect(why, code)` 45 | 46 | Disconnects the shard. This will not permanently stop an automatically connecting shard, please see [`shard:shutdown`](#shardshutdown). 47 | 48 | - *string* `why` 49 | The disconnect reason, defaults to `"requested"`. 50 | - *integer (websocket close code)* `code` 51 | Defaults to `4009` 52 | 53 | 54 | #### *shard* `shard:shutdown(...)` 55 | 56 | This will terminate the shard's connection, clearing any reconnection flags and then disconnecting. 57 | 58 | - ... 59 | Arguments to pass to disconnect. 60 | 61 | #### *boolean* `shard:request_guild_members(id)` 62 | 63 | Sends a REQUEST_GUILD_MEMBERS request. This will return true if the request was successfully sent. If this function fails it will return false followed by an error message. 64 | 65 | - *string (snowflake)* `id` 66 | The guild id to request members from. 67 | 68 | 69 | #### *boolean, application/json|string* `shard:update_status(presence)` 70 | 71 | Sends a STATUS_UPDATE request. This will return true if the request was successfully sent. If this function fails it will return false followed by an error message. 72 | 73 | - *application/json* `presence` 74 | The new presence for the bot. See the [discord documentation](https://discord.com/developers/docs/topics/gateway#update-presence). 75 | 76 | #### *boolean, application/json|string* `shard:update_voice(guild_id, channel_id, self_mute, self_deaf)` 77 | 78 | Sends a VOICE_STATE_UPDATE request. This will return true if the request was successfully sent. If this function fails it will return false followed by an error message. 79 | 80 | - *string (snowflake)* `guild_id` 81 | The guild id of the guild the voice channel is in. 82 | - *string (snowflake)* `channel_id` 83 | The voice channel id. 84 | - *boolean* `self_mute` 85 | Whether the bot is muted. 86 | - *boolean* `self_deaf` 87 | Whether the bot is deafened. 88 | -------------------------------------------------------------------------------- /docs/util.intents.md: -------------------------------------------------------------------------------- 1 | ## lacord.util.intents 2 | 3 | This module provides a thin convenience layer around the intents values used by discord. 4 | See the [intents documentation](https://discord.com/developers/docs/topics/gateway#gateway-intents) for more detailed information. 5 | 6 | #### *integer (intent)* `intent_name` 7 | 8 | For every individual intent there exists a field in the module with its value. "GUILD_MEMBERS" would be found at `guild_members`. 9 | 10 | ### Extra values 11 | 12 | #### *integer (intent)* `everything` 13 | 14 | This intent contains all other intents. 15 | 16 | #### *integer (intent)* `message` 17 | 18 | This intent contains all message related intents. 19 | 20 | #### *integer (intent)* `guild` 21 | 22 | This intent contains all guild related intents. 23 | 24 | #### *integer (intent)* `direct` 25 | 26 | This intent contains all direct message related intents. 27 | 28 | ## Defaults 29 | 30 | #### *integer (intent)* `normal` 31 | 32 | This intent contains everything except presences and voice states, because those usually clog up shards are not needed by most bots. 33 | 34 | #### *integer (intent)* `unprivileged` 35 | 36 | This intent contains everything except privilaged gateway intents. -------------------------------------------------------------------------------- /docs/util.json.md: -------------------------------------------------------------------------------- 1 | ## lacord.util.json 2 | 3 | This module re-exports the json encode and decode functions so that the library used may be configured. 4 | 5 | #### *string (application/json)* `encode(x)` 6 | 7 | Encodes the lua object `x` as a json object. 8 | 9 | #### *application/json* `decode(x)` 10 | 11 | Decodes a json string into a content_typed object. 12 | 13 | #### *application/json* `null` 14 | 15 | A value representing the json `null` value. 16 | 17 | #### *application/json* `empty_array` 18 | 19 | A value representing an empty json array. 20 | 21 | #### *application/json* `jarray(...)` 22 | 23 | Packs `...` into a json array object. -------------------------------------------------------------------------------- /docs/util.logger.md: -------------------------------------------------------------------------------- 1 | ## lacord.util.logger 2 | 3 | This module provides a set of functions for logging to standard output and error stream in a readable format. 4 | 5 | #### *file* `fd` 6 | 7 | Set this to a lua file object opened in a write mode to also write any output to the file. 8 | 9 | #### *integer* `mode(x)` 10 | 11 | Sets the colour mode the logger uses. To highlight text enclose it in `$ ;`. 12 | 13 | - *integer* `x` 14 | The colour mode, valid values are: 15 | - `0` no colouring. 16 | - `3` colouring using standard color codes. 17 | - `8` colouring using 8 bit ansi codes. 18 | 19 | #### *nothing* `info(fmt, ...)` 20 | 21 | Writes using the info prefix to standard output. 22 | 23 | - *string* `fmt` 24 | A lua format string. 25 | - `...` 26 | Format values. 27 | 28 | #### *nothing* `warn(fmt, ...)` 29 | 30 | Writes using the warning prefix to standard output. 31 | 32 | - *string* `fmt` 33 | A lua format string. 34 | - `...` 35 | Format values. 36 | 37 | 38 | #### *nothing* `error(fmt, ...)` 39 | 40 | Writes using the error prefix to standard error. 41 | 42 | - *string* `fmt` 43 | A lua format string. 44 | - `...` 45 | Format values. 46 | 47 | 48 | #### *nothing* `throw(fmt, ...)` 49 | 50 | Writes using the error prefix to standard error, 51 | and then raises the message as a lua error. 52 | 53 | - *string* `fmt` 54 | A lua format string. 55 | - `...` 56 | Format values. 57 | 58 | 59 | #### *nothing* `fatal(fmt, ...)` 60 | 61 | Writes using the error prefix to standard error, 62 | and then terminates the program with a non-zero exit code. 63 | 64 | - *string* `fmt` 65 | A lua format string. 66 | - `...` 67 | Format values. 68 | 69 | 70 | #### *anything* `assert(x, ...)` 71 | 72 | Similar to lua's assert but uses logger.throw when an assertion fails. 73 | 74 | - *string* `x` 75 | A value to assert is truthy. 76 | - `...` 77 | Arguments to [`throw`](#throw) 78 | 79 | -------------------------------------------------------------------------------- /docs/util.md: -------------------------------------------------------------------------------- 1 | ## lacord.util 2 | 3 | This module contains a miscellaneous collection of functions which provide some utility to other lacord modules. 4 | 5 | 6 | #### *string* `hash(str)` 7 | 8 | Computes the FNV-1a 32bit hash of the given string. 9 | 10 | - *string* `str` 11 | 12 | 13 | 14 | #### *number* `rand(A, B)` 15 | 16 | Produces a random double between `A` and `B`. 17 | 18 | - *number* `A` 19 | - *number* `B` 20 | 21 | 22 | #### *string* `platform` 23 | 24 | The operating system platform. 25 | 26 | 27 | #### *number* `version` 28 | 29 | The lua version as a number in `MAJOR.MINOR` form. 30 | 31 | 32 | #### *number* `version_major` 33 | 34 | The major version of the lua version. 35 | 36 | 37 | #### *number* `version_minor` 38 | 39 | The minor version of the lua version. 40 | 41 | 42 | #### *number* `version_release` 43 | 44 | The release/patch version of the lua version. 45 | 46 | 47 | #### *boolean* `startswith(s, prefix)` 48 | 49 | Tests whether the string `s` starts with the string `prefix`. 50 | 51 | - *string* `s` 52 | - *string* `prefix` 53 | 54 | 55 | #### *boolean* `endswith(s, suffix)` 56 | 57 | Tests whether the string `s` ends with the string `suffix`. 58 | 59 | - *string* `s` 60 | - *string* `suffix` 61 | 62 | 63 | #### *boolean* `suffix(s, pre)` 64 | 65 | Returns the suffix of `pre` in `s` 66 | 67 | - *string* `s` 68 | - *string* `pre` 69 | 70 | 71 | #### *boolean* `prefix(s, pre)` 72 | 73 | Returns the prefix of `suf` in `s` 74 | 75 | - *string* `s` 76 | - *string* `suf` 77 | 78 | 79 | ### content_typed 80 | 81 | The following utilities construct and manipulate objects which represent content typed data. These can be transparently passed to api methods as bodies and will have the correct content type attached to the request. This is also true when sending files in multipart requests. 82 | 83 | #### *string, string? (content_type)* `content_typed(payload)` 84 | 85 | Resolve a prospective payload with respect to lacord content types. The 2nd argument will be present if a content type was resolved. The processing is occurs when `payload` has a metatable with a `__lacord_content_type` function and a `__lacord_payload` function defined. 86 | 87 | - *table (implements \_\_lacord_content_type)* `payload` 88 | 89 | 90 | #### *string? (content_type)* `the_content_type(payload)` 91 | 92 | Resolve a prospective payload with respect to lacord content types. This only returns the content type or nil if one can not be resolved. 93 | 94 | - *table (implements \_\_lacord_content_type)* `payload` 95 | 96 | 97 | #### *table* `content_types` 98 | 99 | A table of commonly used content types. 100 | 101 | - *string* `JSON` 102 | - *string* `TEXT` 103 | - *string* `URLENCODED` 104 | - *string* `BYTES` 105 | - *string* `PNG` 106 | 107 | #### *content_typed* `plaintext(str, name)` 108 | 109 | Creates a content typed object containing plaintext. 110 | 111 | - *string* `str` 112 | The plaintext content. 113 | - *string* `name` 114 | A file name, optional. 115 | 116 | #### *content_typed* `binary(str, name)` 117 | 118 | Creates a content typed object containing raw bytes. 119 | 120 | - *string* `str` 121 | The content. 122 | - *string* `name` 123 | A file name, optional. 124 | 125 | #### *content_typed* `urlencoded(t)` 126 | 127 | Creates a content typed object containing url encoded key-value pairs. 128 | 129 | - *table* `t` 130 | The table of key-value pairs. 131 | 132 | 133 | #### *content_typed* `png(str, name)` 134 | 135 | Creates a content typed object containing png image data. 136 | 137 | - *string* `str` 138 | The png image data. 139 | - *string* `name` 140 | A file name, optional. 141 | 142 | #### *content_typed* `json_string(data, name)` 143 | 144 | Creates a content typed object containing encoded json. 145 | 146 | - *string* `data` 147 | The encoded json content. 148 | - *string* `name` 149 | A file name, optional. 150 | 151 | #### *string* `file_name(cted)` 152 | 153 | Gets the file name for the associated content typed object. 154 | 155 | - *content_typed* `cted` 156 | 157 | #### *nothing* `set_file_name(cted, name)` 158 | 159 | Sets the file name for the associated content typed object. 160 | 161 | - *content_typed* `cted` 162 | - *string* `name` 163 | 164 | 165 | #### *content_typed* `a_blob_of(content_type, data, name)` 166 | 167 | Creates a content typed object containing the specified content type. 168 | 169 | - *string (content type)* `content_type` 170 | - *string* `data` 171 | The data. 172 | - *string* `name` 173 | A file name, optional. 174 | 175 | #### *string, string (file name)* `blob_for_file` 176 | 177 | Resolves a content typed object and gets a file name suitable for writing to a file. This function will try to resolve file name extensions with respect to the content type's appropriate extension, if one exists. 178 | 179 | - *content_typed* `blob` 180 | - *string* `name` 181 | A name to use instead of the set name. 182 | 183 | -------------------------------------------------------------------------------- /docs/util.mutex.md: -------------------------------------------------------------------------------- 1 | ## lacord.util.mutex 2 | 3 | This module provides a simple mutex implementation for synchronizing cqueues coroutines. 4 | 5 | ### *mutex* 6 | 7 | This type has methods for locking and releasing the mutex and for delaying unlocks in various ways. 8 | 9 | #### *mutex* `new()` 10 | 11 | Creates a new mutex. 12 | 13 | 14 | #### *nothing* `mutex:lock(timeout)` 15 | 16 | Locks the mutex. 17 | 18 | - *number (seconds)* `timeout` 19 | An optional timeout to wait before automatically releasing the lock. 20 | 21 | 22 | #### *nothing* `mutex:unlock()` 23 | 24 | Unlocks the mutex. 25 | 26 | #### *nothing* `mutex:unlock_at(deadline)` 27 | 28 | Unlocks the mutex at the given time in the future. This deadline should be based on the cqueues `monotime` clock. 29 | 30 | - *number (seconds)* `deadline` 31 | 32 | #### *nothing* `mutex:unlock_after(time)` 33 | 34 | Unlocks the mutex after the given amount of seconds have elapsed. 35 | 36 | - *number (seconds)* `time` 37 | 38 | #### *nothing* `mutex:defer_unlock()` 39 | 40 | Unlocks the mutex at some point in the near future. 41 | 42 | - *number (seconds)* `time` 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /docs/util.session-limit.md: -------------------------------------------------------------------------------- 1 | ## lacord.util.session-limit 2 | 3 | This module provides an object to control concurrent shards identifying. 4 | 5 | ##### If you're wanting to run shards across multiple processes/threads please open an issue/ticket. 6 | 7 | ### *session-limit* 8 | 9 | This type is a semaphore-like object which can be used to orchestrate shard connection. 10 | The `:enter()` method is called when you prepare a shard. When the limit is used up 11 | -- and we've started `availability` requests concurrently -- `:enter()` will block until we're allowed to connect. Every call to `:enter()` is met with a call to `:exit()` when the shard receives a `READY` event from discord. Note that the shard implementation will call these methods as appropriate, you need only create this object. 12 | 13 | ### Example taken from `lacord-client` 14 | 15 | In this example `self` is the client (which is a table of shards and other bot related components). 16 | When the client connects we pass its `.session_limit`. This object is initialized with the `max_concurrency` field of `get_gateway_bot`. 17 | 18 | ```lua 19 | 20 | local R = self.api 21 | :capture() 22 | :get_current_application_information() 23 | :get_gateway_bot() 24 | 25 | if R.success then 26 | 27 | local gatewayinfo 28 | self.app, gatewayinfo = R:results() 29 | 30 | self.session_limit = session_limit.new(gatewayinfo.session_start_limit.max_concurrency) 31 | 32 | for i = 0 , gatewayinfo.shards - 1 do 33 | local s = shard.init({ 34 | token = self.api.token 35 | ,id = i 36 | ,gateway = gatewayinfo.url 37 | ,compress = false 38 | ,transport_compression = true 39 | ,total_shard_count = gatewayinfo.shards 40 | ,large_threshold = 100 41 | ,auto_reconnect = true 42 | ,loop = cqs.running() 43 | ,output = output, 44 | intents = self.intents 45 | }, self.session_limit) -- we pass in the client's session limit. 46 | self.shards[i] = s 47 | s:connect() 48 | end 49 | else.... 50 | ``` 51 | 52 | #### *session-limit* `new(availability)` 53 | 54 | Constructs *session-limit* object which will allow for up to `availability` concurrent 55 | requests before blocking. 56 | -------------------------------------------------------------------------------- /docs/util.uint.md: -------------------------------------------------------------------------------- 1 | ## lacord.util.uint 2 | 3 | This module provides functions for handling and manipulating discord snowflake IDs. In this module "encoded uint64" simply means that a uint64 value is being stored in a `lua_Integer` which is an int64. 4 | 5 | #### *integer (snowflake)* `touint(s)` 6 | 7 | Converts a number or string into an encoded uint64. 8 | 9 | - *string|number* `s` 10 | 11 | 12 | #### *integer (seconds)* `timestamp(s)` 13 | 14 | Computes the UNIX timestamp of a given snowflake. 15 | 16 | - *string|number (snowflake)* `s` 17 | 18 | #### *integer (snowflake)* `fromtime(s)` 19 | 20 | Creates an artificial snowflake from a given UNIX timestamp. 21 | 22 | - *integer (seconds)* `s` 23 | 24 | #### *table* `decompose(s)` 25 | 26 | Gets the timestamp, worker ID, process ID and increment from a snowflake. These are set as fields in the returned table at `timestamp`, `worker`, `pid` and `increment` respectively. 27 | 28 | - *string|number (snowflake)* `s` 29 | 30 | 31 | #### *integer (snowflake)* `synthesize(s, worker, pid, incr)` 32 | 33 | Creates an artifical snowflake from the given timestamp, worker and pid. 34 | 35 | - *integer (seconds)* `s` 36 | The timestamp. 37 | - *integer* `worker` 38 | The worker ID. 39 | - *integer* `pid` 40 | The process ID. 41 | - *integer* `incr` 42 | The increment. An internal incremented value is used if one is not provided. 43 | 44 | #### *boolean* `snowflake_sort(i, j)` 45 | 46 | A table sorter that will sort by the `id` field of the elements as snowflakes. 47 | 48 | 49 | #### *boolean* `id_sort(i, j)` 50 | 51 | A table sorter that will sort the elements as snowflake ids. -------------------------------------------------------------------------------- /lacord-scm-4.rockspec: -------------------------------------------------------------------------------- 1 | package = 'lacord' 2 | version = 'scm-4' 3 | 4 | source = { 5 | url = "git+https://github.com/Mehgugs/lacord.git" 6 | } 7 | 8 | local details = 9 | [[lacord is a small discord library providing low level clients for the discord rest and gateway API. 10 | Check out https://github.com/Mehgugs/lacord-client for a higher level wrapper over this project.]] 11 | 12 | description = { 13 | summary = 'A low level, lightweight discord API library.' 14 | ,homepage = "https://github.com/Mehgugs/lacord" 15 | ,license = 'MIT' 16 | ,maintainer = 'Magicks ' 17 | ,detailed = details 18 | } 19 | 20 | dependencies = { 21 | 'lua >= 5.3' 22 | ,'cqueues' 23 | ,'http' 24 | ,'lua-zlib' 25 | ,'lua-cjson-219' 26 | ,'inspect' 27 | } 28 | 29 | build = { 30 | type = "builtin" 31 | ,modules = { 32 | ["lacord.util.archp"] = "src/archp.c", 33 | ["lacord.cli"] = "lua/lacord/util/cli_default.lua", 34 | ["acord"] = "lua/lacord/util/cli_auto.lua", 35 | ["lacord.ext.shs"] = "ext/shs/shs.lua", 36 | ["lacord.outgoing-webhook-server"] = "lua/lacord/wrapper/outgoing-webhook-server.lua", 37 | 38 | ["internationalize"] = "ext/internationalize/internationalize/init.lua", 39 | ["internationalize.interpolation"] = "ext/internationalize/internationalize/interpolation.lua", 40 | ["internationalize.plural"] = "ext/internationalize/internationalize/plural.lua" 41 | } 42 | } -------------------------------------------------------------------------------- /licenses/discordia: -------------------------------------------------------------------------------- 1 | The ratelimiting implementation and multipart processing are inspired by discordia's implementations of the same things. 2 | 3 | =============================================================================== 4 | The MIT License (MIT) 5 | 6 | Copyright (c) 2016-2021 SinisterRectus 7 | 8 | Permission is hereby granted, free of charge, to any person obtaining a copy 9 | of this software and associated documentation files (the "Software"), to deal 10 | in the Software without restriction, including without limitation the rights 11 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | copies of the Software, and to permit persons to whom the Software is 13 | furnished to do so, subject to the following conditions: 14 | 15 | The above copyright notice and this permission notice shall be included in all 16 | copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | SOFTWARE. 25 | =============================================================================== -------------------------------------------------------------------------------- /licenses/luajit: -------------------------------------------------------------------------------- 1 | The code for `src/archp.c' is a crude recreation of luaJIT's architecture fingerprinting. 2 | 3 | =============================================================================== 4 | LuaJIT -- a Just-In-Time Compiler for Lua. https://luajit.org/ 5 | 6 | Copyright (C) 2005-2021 Mike Pall. All rights reserved. 7 | 8 | Permission is hereby granted, free of charge, to any person obtaining a copy 9 | of this software and associated documentation files (the "Software"), to deal 10 | in the Software without restriction, including without limitation the rights 11 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | copies of the Software, and to permit persons to whom the Software is 13 | furnished to do so, subject to the following conditions: 14 | 15 | The above copyright notice and this permission notice shall be included in 16 | all copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | THE SOFTWARE. 25 | 26 | [ MIT license: https://www.opensource.org/licenses/mit-license.php ] 27 | =============================================================================== -------------------------------------------------------------------------------- /lua/lacord/api/payload.lua: -------------------------------------------------------------------------------- 1 | local next = next 2 | local tostring = tostring 3 | 4 | local openf = io.open 5 | local time = os.time 6 | 7 | local concat = table.concat 8 | local insert = table.insert 9 | 10 | local cli = require"lacord.cli" 11 | local cqueues = require"cqueues" 12 | local encode = require"lacord.util.json".encode 13 | local logger = require"lacord.util.logger" 14 | local util = require"lacord.util" 15 | 16 | 17 | local content_typed = util.content_typed 18 | local file_name = util.file_name 19 | local JSON = util.content_types.JSON 20 | local LACORD_DEBUG = cli.debug 21 | local LACORD_INSPECT = cli.inspect_payload 22 | local monotime = cqueues.monotime 23 | 24 | local BOUNDARY1 = "lacord" .. ("%x"):format(util.hash(tostring(time()))) 25 | local BOUNDARY2 = "--" .. BOUNDARY1 26 | local BOUNDARY3 = BOUNDARY2 .. "--" 27 | 28 | local MULTIPART = ("multipart/form-data;boundary=%s"):format(BOUNDARY1) 29 | 30 | local with_payload = { 31 | PUT = true, 32 | PATCH = true, 33 | POST = true, 34 | } 35 | 36 | 37 | local function add_a_file(ret, default_ct, f, i) 38 | local name = file_name(f) 39 | local fstr, resolved_ct = content_typed(f) 40 | insert(ret, BOUNDARY2) 41 | insert(ret, ("Content-Disposition:form-data;name=\"files[%i]\";filename=%q"):format(i and i-1 or 0, name)) 42 | insert(ret, ("Content-Type:%s\r\n"):format(resolved_ct or default_ct)) 43 | insert(ret, fstr) 44 | end 45 | 46 | 47 | local empty_file_array = { } 48 | 49 | local function attach(payload, files, ct, default_ct) 50 | local ret 51 | if ct ~= "form" then 52 | if payload ~= '{}' then 53 | ret = { 54 | BOUNDARY2, 55 | "Content-Disposition:form-data;name=\"payload_json\"", 56 | ("Content-Type:%s\r\n"):format(ct), 57 | payload, 58 | } 59 | else 60 | logger.debug("Not adding empty payload.") 61 | ret = {} 62 | end 63 | else 64 | ret = {} 65 | for k, v in pairs(payload) do 66 | insert(ret, BOUNDARY2) 67 | local v_, vct = content_typed(v) 68 | if vct then 69 | insert(ret, ("Content-Disposition:form-data;name=%q"):format(k)) 70 | insert(ret, ("Content-Type:%s\r\n"):format(vct)) 71 | else 72 | insert(ret, ("Content-Disposition:form-data;name=%q\r\n"):format(k)) 73 | end 74 | insert(ret, v_ or tostring(v)) 75 | end 76 | end 77 | if #files == 1 then 78 | add_a_file(ret, default_ct, files[1]) 79 | else 80 | for i, v in ipairs(files) do 81 | add_a_file(ret, default_ct, v, i) 82 | end 83 | end 84 | insert(ret, BOUNDARY3) 85 | return concat(ret, "\r\n") 86 | end 87 | 88 | local function attach_files(payload, files, ct) 89 | return attach(payload, files, ct, util.content_types.BYTES) 90 | end 91 | 92 | return function(req, method, payload, files) 93 | if with_payload[method] then 94 | local content_type 95 | payload,content_type = content_typed(payload) 96 | if not content_type then 97 | payload = payload and encode(payload) or '{}' 98 | content_type = JSON 99 | end 100 | if files and next(files) or content_type == "form" then 101 | payload = attach_files(payload, files or empty_file_array, content_type) 102 | req.headers:append('content-type', MULTIPART) 103 | else 104 | req.headers:append('content-type', content_type) 105 | end 106 | if LACORD_DEBUG and LACORD_INSPECT then 107 | local file = openf("test/payload" .. util.hash(tostring(monotime())), "wb") 108 | file:write(payload) 109 | file:close() 110 | end 111 | req:set_body(payload) 112 | end 113 | end -------------------------------------------------------------------------------- /lua/lacord/api/ratelimiting.lua: -------------------------------------------------------------------------------- 1 | local min = math.min 2 | local setm = setmetatable 3 | 4 | local limiter = require"lacord.util.session-limit".new 5 | local logger = require"lacord.util.logger" 6 | local mutex = require"lacord.util.mutex".new 7 | 8 | local _ENV = {} 9 | 10 | local WEAK_CACHE = {__mode = "v"} 11 | 12 | 13 | function initialize_ratelimit_properties(state, options) 14 | state.bucket_names = {} 15 | state.ratelimit_data = {} 16 | state.routex = setm({}, {__index = function(t, k) local m = mutex(); t[k] = m return m end, __mode = "v"}) 17 | state.buckets = {} 18 | state.global = limiter(50) 19 | state.global.name = "global" 20 | state.route_delay = options.route_delay and min(options.route_delay, 0) or 1 21 | end 22 | 23 | local function get_bucket(self, id, major_params) 24 | local buckets = self.buckets[id] 25 | if major_params == "" then 26 | if buckets then return buckets 27 | else 28 | buckets = limiter(self.ratelimit_data[id].limit) 29 | self.buckets[id] = buckets 30 | return buckets 31 | end 32 | else 33 | if buckets and buckets[major_params] then 34 | return buckets[major_params] 35 | else 36 | if buckets then 37 | local obj = limiter(self.ratelimit_data[id].limit) 38 | obj.name = "bucket-"..id 39 | buckets[major_params] = obj 40 | return obj 41 | else 42 | return logger.throw("lacord.api.request: Bucket id missing from cache.") 43 | end 44 | end 45 | end 46 | end 47 | 48 | _ENV.get_bucket = get_bucket 49 | 50 | function handle_delay(self, delay, name, major_params, bucket, first_time, from_routex) 51 | if delay then 52 | local delay_s, delay_id, delay_limit = delay[1], delay[2], delay[3] 53 | 54 | if first_time then 55 | logger.debug("Creating new ratelimit: %s %s %s", delay_s, delay_id, delay_limit) 56 | if delay_id and delay_limit then 57 | self.bucket_names[name] = delay_id 58 | --local identifier = delay_id 59 | 60 | self.ratelimit_data[delay_id] = self.ratelimit_data[delay_id] or {limit = delay_limit, id = delay_id, delay = delay_s} 61 | 62 | if not self.buckets[delay_id] and major_params ~= "" then self.buckets[delay_id] = setm({}, WEAK_CACHE) end 63 | 64 | bucket = get_bucket(self, delay_id, major_params) 65 | 66 | bucket:enter() 67 | end 68 | elseif bucket.total ~= delay_limit then 69 | logger.warn("lacord.api: Ratelimit for %s (%s) has changed: %s -> %s", delay_id, name, bucket.total, delay_limit) 70 | local diff = bucket.total - delay_limit 71 | bucket.v = bucket.v - diff 72 | self.ratelimit_data[delay_id].limit = delay_limit 73 | end 74 | if bucket then bucket:exit_after(delay_s) end 75 | else 76 | logger.debug("lacord.api: There wasn't any ratelimit information when performing %s.", name) 77 | if bucket then bucket:exit_after(0) end 78 | end 79 | 80 | if from_routex then self.routex[name]:unlock() end 81 | end 82 | 83 | return _ENV -------------------------------------------------------------------------------- /lua/lacord/api/webhooks.lua: -------------------------------------------------------------------------------- 1 | local iter = pairs 2 | local setm = setmetatable 3 | 4 | local prefix = require"lacord.util".prefix 5 | 6 | 7 | return function(module, api, auth) 8 | local webhook_client = {} 9 | webhook_client.__index = webhook_client 10 | 11 | for method in iter(auth.map.webhook) do 12 | local fn = api[method] 13 | webhook_client[prefix(method, '_with_token')] = function(self, ...) 14 | local the_api = self[1] 15 | return fn(the_api, self.id, self.token, ...) 16 | end 17 | end 18 | 19 | for method in iter(auth.map.none) do 20 | local fn = api[method] 21 | webhook_client[method] = function(self, ...) self = self[1] return fn(self, ...) end 22 | end 23 | 24 | module.webhook_mt = webhook_client 25 | 26 | function module.new_webhook(id, token, options) 27 | options = options or {} 28 | options.webhook = true 29 | local client = module.new(options) 30 | return setm({client, id = id, token = token}, webhook_client) 31 | end 32 | end -------------------------------------------------------------------------------- /lua/lacord/const.lua: -------------------------------------------------------------------------------- 1 | 2 | local _ENV = {} 3 | version = "1637789515" 4 | homepage = "https://github.com/Mehgugs/lacord" 5 | time_unit = "seconds" 6 | discord_epoch = 1420070400 7 | api_version = 10 8 | 9 | api = { 10 | base_endpoint = "https://discord.com" 11 | ,cdn_endpoint = "https://cdn.discordapp.com" 12 | ,version = api_version 13 | ,max_retries = 6 14 | } 15 | 16 | gateway = { 17 | delay = .5 18 | ,identify_delay = 5 19 | ,ratelimit = {120, 60} 20 | ,allowance = 2 21 | ,version = api_version 22 | ,encoding = "json" 23 | ,compress = "zlib-stream" 24 | } 25 | 26 | models = { 27 | remove_unused_keys = true, 28 | timeouts = { 29 | remove_reaction = 30.0 30 | } 31 | } 32 | 33 | api.endpoint = ("%s/api/v%s"):format(api.base_endpoint, api.version) 34 | 35 | default_avatars = 5 36 | 37 | json_provider = "cjson" 38 | 39 | supported_cli_options = { 40 | debug = "flag", 41 | unstable = "flag", 42 | deprecated = "flag", 43 | client_id = "value", 44 | client_secret = "value", 45 | token = "value", 46 | log_file = "value", 47 | log_mode = {"0","3","8"}, 48 | accept = "flag", 49 | inspect_payload = "flag", 50 | ['unstable-features'] = "unstable", 51 | ['client-id'] = "client_id", 52 | ['client-secret'] = "client_secret", 53 | ['log-file'] = "log_file", 54 | ['log-mode'] = "log_mode", 55 | ['accept-everything'] = "accept", 56 | ['inspect-payload'] = "inspect_payload", 57 | ['quiet'] = "flag", 58 | ['quieter'] = "quiet", 59 | ['file'] = "value", 60 | 61 | --shorthand 62 | d = "debug", 63 | u = "unstable", 64 | D = "deprecated", 65 | i = "client_id", 66 | s = "client_secret", 67 | t = "token", 68 | l = "log_file", 69 | L = "log_mode", 70 | a = "accept", 71 | q = "quiet", 72 | f = "file" 73 | } 74 | 75 | supported_environment_variables = { 76 | LACORD_DEBUG = "debug", 77 | LACORD_UNSTABLE = "unstable", 78 | LACORD_DEPRECATED = "deprecated", 79 | LACORD_ID = "client_id", 80 | LACORD_SECRET = "client_secret", 81 | LACORD_TOKEN = "token", 82 | LACORD_LOG_MODE = "log_mode", 83 | LACORD_LOG_FILE = "log_file", 84 | LACORD_QUIET = "quiet" 85 | } 86 | 87 | return _ENV -------------------------------------------------------------------------------- /lua/lacord/models/channel.lua: -------------------------------------------------------------------------------- 1 | local err = error 2 | local to_s = tostring 3 | local typ = type 4 | 5 | local context = require"lacord.models.context" 6 | local methods = require"lacord.models.methods" 7 | local map_bang = require"lacord.util".map_bang 8 | local map = require"lacord.util".map 9 | local numbers = require"lacord.models.magic-numbers" 10 | local null = require"lacord.util.json".null 11 | 12 | local getapi = context.api 13 | local create = context.create 14 | local request = context.request 15 | local property = context.property 16 | 17 | local modify = methods.update 18 | local model_id = methods.model_id 19 | local resolve = methods.resolve 20 | 21 | local GUILD_PRIVATE_THREAD, GUILD_PUBLIC_THREAD = numbers.PRIVATE_THREAD, numbers.PUBLIC_THREAD 22 | 23 | --luacheck: ignore 111 24 | 25 | local _ENV = {} 26 | 27 | function fetch(c) 28 | c = model_id(c, 'channel') 29 | local chl = request('channel', c) 30 | if not chl then 31 | local api = getapi() 32 | local success, data, e = api:get_channel(c) 33 | if success then 34 | chl = create('channel', data) 35 | else 36 | return err( 37 | "lacord.models.channel: Unable to resolve channel id to channel." 38 | .."\n "..e 39 | ) 40 | end 41 | end 42 | return chl or err"lacord.models.channel: Unable to resolve channel id to channel." 43 | end 44 | 45 | 46 | function edit(c, changes) 47 | c = model_id(c, 'channel') 48 | 49 | local api = getapi() 50 | local success, data, e = api:modify_channel(c, changes) 51 | 52 | if success then 53 | local chl = create('channel', data) 54 | if chl.recipient_id then 55 | property('dms', chl.recipient_id, chl.id) 56 | end 57 | return chl 58 | else 59 | return nil, e 60 | end 61 | end 62 | 63 | function message(c, id) 64 | c = model_id(c, 'channel') 65 | id = model_id(id, 'message') 66 | 67 | local api = getapi() 68 | local success, data, e = api:get_message(c, id) 69 | if success then 70 | return create('message', data) 71 | else 72 | return nil, e 73 | end 74 | end 75 | 76 | function first_message(c) 77 | c = model_id(c, 'channel') 78 | local api = getapi() 79 | local success, data, e = api:get_channel_messages(c, {after = c, limit = 1}) 80 | if success and data[1] then 81 | return create('message', data[1]) 82 | elseif success and not data[1] then 83 | return false, "Channel has no messages." 84 | else 85 | return nil, e 86 | end 87 | end 88 | 89 | function last_message(c) 90 | c = model_id(c, 'channel') 91 | local api = getapi() 92 | local success, data, e = api:get_channel_messages(c, {limit = 1}) 93 | if success and data[1] then 94 | return create('message', data[1]) 95 | elseif success and not data[1] then 96 | return false, "Channel has no messages." 97 | else 98 | return nil, e 99 | end 100 | end 101 | 102 | local function swap(a, b, f) 103 | return f(b, a) 104 | end 105 | 106 | function messages(c, query) 107 | c = model_id(c, 'channel') 108 | local api = getapi() 109 | local success, data, e = api:get_channel_messages(c, query) 110 | if success and data[1] then 111 | return map_bang(swap, data, 'message', create) 112 | elseif success and not data[1] then 113 | return false, "Channel has no messages." 114 | else 115 | return nil, e 116 | end 117 | end 118 | 119 | function pinned_messages(c) 120 | c = model_id(c, 'channel') 121 | local api = getapi() 122 | local success, data, e = api:get_pinned_messages(c) 123 | 124 | if success then 125 | return data 126 | else 127 | return nil, e 128 | end 129 | end 130 | 131 | function broadcast_typing(c) 132 | c = model_id(c, 'channel') 133 | local api = getapi() 134 | local success, data, e = api:trigger_typing_indicator(c) 135 | 136 | if success and data then return true 137 | else return nil, e 138 | end 139 | end 140 | 141 | function rename(c, new_name) 142 | new_name = to_s(new_name) 143 | return modify(c, {name = new_name}) 144 | end 145 | 146 | function change_category(c, new_parent) 147 | local id = new_parent and model_id(new_parent, 'channel') or null 148 | return modify(c, {parent_id = id}) 149 | end 150 | 151 | function change_topic(c, the_topic) 152 | return modify(c, {topic = the_topic or null}) 153 | end 154 | 155 | function enable_slowmode(c, rl_per_user) 156 | return modify(c, { 157 | rate_limit_per_user = rl_per_user or null, 158 | }) 159 | end 160 | 161 | function disable_slowmode(c) 162 | return modify(c, {rate_limit_per_user = null}) 163 | end 164 | 165 | function enable_NSFW(c) 166 | return modify(c, {nsfw = true}) 167 | end 168 | 169 | function disable_NSFW(c) 170 | return modify(c, {nsfw = false}) 171 | end 172 | 173 | enable_SFW = disable_NSFW 174 | 175 | function fetch_invites(c) 176 | local the_id = model_id(c, 'channel') 177 | local api = getapi() 178 | local success, data, e = api:get_channel_invites(the_id) 179 | 180 | if success then return data 181 | else return nil, e 182 | end 183 | end 184 | 185 | function create_invite(c, payload) 186 | c = model_id(c, 'channel') 187 | local api = getapi() 188 | local success, data, e = api:create_channel_invite(c, payload) 189 | 190 | if success then return data 191 | else return nil, e 192 | end 193 | end 194 | 195 | function check_overwrite(c, user_role) 196 | c = model_id(c, 'channel') 197 | local id, kind = model_id(user_role, 'user', 'role') 198 | 199 | if c.permission_overwrites[id] then 200 | return c.permission_overwrites[id] 201 | else 202 | return { 203 | id = id, 204 | type = kind, 205 | allow = 0, 206 | deny = 0 207 | } 208 | end 209 | end 210 | 211 | function update_overwrite(c, ow) 212 | c = model_id(c, 'channel') 213 | 214 | if ow.id and ow.type and ow.allow and ow.deny then 215 | local api = getapi() 216 | local success, data, e = api:edit_channel_permissions(c.id, ow.id, { 217 | type = ow.type, 218 | id = ow.id, 219 | allow = ow.allow, 220 | deny = ow.deny 221 | }) 222 | if success and data then 223 | return ow 224 | else return nil, e 225 | end 226 | else 227 | return nil, "Overwrite object invalid" 228 | end 229 | end 230 | 231 | function crosspost(c, m_id) 232 | c = model_id(c, 'channel') 233 | m_id = model_id(m_id, 'message') 234 | local api = getapi() 235 | local success, data, e = api:crosspost_message(c, m_id) 236 | if success then 237 | return create('message', data) 238 | else 239 | return nil, e 240 | end 241 | end 242 | 243 | function delete_messages(c, input) 244 | c = model_id(c, 'channel') 245 | 246 | local messages = resolve(input, 'message') 247 | 248 | messages = messages and {messages.id} or map(model_id, input, 'message') 249 | 250 | local api = getapi() 251 | 252 | if #messages == 1 then 253 | local success, data , e = api:delete_message(c, messages[1]) 254 | return success and data or nil, e 255 | else 256 | local success, data , e = api:bulk_delete_messages(c, messages) 257 | return success and data or nil, e 258 | end 259 | end 260 | 261 | function delete_overwrite(c, user_role) 262 | c = model_id(c, 'channel') 263 | local id = model_id(user_role, 'user', 'role') 264 | local api = getapi() 265 | 266 | local success, data, e = api:delete_channel_permission(c, id) 267 | return success and data or nil, e 268 | end 269 | 270 | function pin(c, id) 271 | c = model_id(c, 'channel') 272 | id = model_id(id, 'message') 273 | 274 | local api = getapi() 275 | local success, data, e = api:pin_message(c, id) 276 | return success and data or nil, e 277 | end 278 | 279 | function unpin(c, id) 280 | c = model_id(c, 'channel') 281 | id = model_id(id, 'message') 282 | 283 | local api = getapi() 284 | local success, data, e = api:unpin_message(c, id) 285 | return success and data or nil, e 286 | end 287 | 288 | function message_to_thread(c, id, name, auto_archive_duration, rl_per_user) 289 | c = model_id(c, 'channel') 290 | id = model_id(id, 'message') 291 | 292 | local api = getapi() 293 | 294 | local success, data, e = api:start_thread_with_message(c, id, { 295 | name = name, 296 | auto_archive_duration = auto_archive_duration, 297 | rate_limit_per_user = rl_per_user 298 | }) 299 | 300 | if success then 301 | return create('channel', data) 302 | else 303 | return nil, e 304 | end 305 | end 306 | 307 | 308 | function private_thread(c, name, auto_archive_duration, rl_per_user, invitable) 309 | c = model_id(c, 'channel') 310 | 311 | local api = getapi() 312 | 313 | local success, data, e = api:start_thread_without_message(c, { 314 | name = name, 315 | auto_archive_duration = auto_archive_duration, 316 | rate_limit_per_user = rl_per_user, 317 | invitable = invitable, 318 | type = GUILD_PRIVATE_THREAD 319 | }) 320 | 321 | if success then 322 | return create('channel', data) 323 | else 324 | return nil, e 325 | end 326 | end 327 | 328 | --- Open a new thread in a channel (defaults to a public thread) 329 | function thread(c, name, auto_archive_duration, rl_per_user, type) 330 | c = model_id(c, 'channel') 331 | 332 | local api = getapi() 333 | 334 | local success, data, e = api:start_thread_without_message(c, { 335 | name = name, 336 | auto_archive_duration = auto_archive_duration, 337 | rate_limit_per_user = rl_per_user, 338 | type = type or GUILD_PUBLIC_THREAD 339 | }) 340 | 341 | if success then 342 | return create('channel', data) 343 | else 344 | return nil, e 345 | end 346 | end 347 | 348 | function forum_post(c, name, auto_archive_duration, rl_per_user, msg, files) 349 | c = model_id(c, 'channel') 350 | 351 | local api = getapi() 352 | 353 | local success, data, e = api:start_thread_in_forum(c, { 354 | name = name, 355 | auto_archive_duration = auto_archive_duration, 356 | rate_limit_per_user = rl_per_user, 357 | type = type or GUILD_PUBLIC_THREAD, 358 | message = msg 359 | }, files) 360 | 361 | if success then 362 | return create('channel', data) 363 | else 364 | return nil, e 365 | end 366 | end 367 | 368 | function join(c) 369 | c = model_id(c, 'channel') 370 | 371 | local api = getapi() 372 | local success, data, e = api:join_thread(c) 373 | return success and data or nil, e 374 | end 375 | 376 | function leave(c) 377 | c = model_id(c, 'channel') 378 | 379 | local api = getapi() 380 | local success, data, e = api:leave_thread(c) 381 | return success and data or nil, e 382 | end 383 | 384 | function add_to_thread(c, id) 385 | c = model_id(c, 'channel') 386 | id = model_id(id, 'user') 387 | 388 | local api = getapi() 389 | local success, data, e = api:add_thread_member(c, id) 390 | return success and data or nil, e 391 | end 392 | 393 | function remove_from_thread(c, id) 394 | c = model_id(c, 'channel') 395 | id = model_id(id, 'user') 396 | 397 | local api = getapi() 398 | local success, data, e = api:remove_thread_member(c, id) 399 | return success and data or nil, e 400 | end 401 | 402 | function membership_of(c, id) 403 | c = model_id(c, 'channel') 404 | id = model_id(id, 'user') 405 | 406 | local api = getapi() 407 | local success, data, e = api:get_thread_member(c, id) 408 | if success then 409 | return data 410 | else 411 | return nil, e 412 | end 413 | end 414 | 415 | function membership(c) 416 | c = model_id(c, 'channel') 417 | 418 | local api = getapi() 419 | local success, data, e = api:list_thread_members(c) 420 | if success then 421 | return data 422 | else 423 | return nil, e 424 | end 425 | end 426 | 427 | function _ENV.send(c, msg, files) 428 | c = model_id(c, 'channel') 429 | local api = getapi() 430 | if typ(msg) == 'string' then msg = {content = msg} end 431 | 432 | local success, data, e = api:create_message(c, msg, files) 433 | if success then 434 | return create('message', data) 435 | else 436 | return nil, e 437 | end 438 | end 439 | 440 | function _ENV.guild(c) 441 | c = resolve(c, 'channel') 442 | if c.guild_id then 443 | return request('guild', c.guild_id) 444 | end 445 | end 446 | 447 | return _ENV -------------------------------------------------------------------------------- /lua/lacord/models/common/send-interaction.lua: -------------------------------------------------------------------------------- 1 | local responses = require"lacord.models.magic-numbers".interaction_response 2 | 3 | local function send_inner(self, api, msg, files) 4 | if not self._state then 5 | local success, data, e = api:create_interaction_response(self.id, self.token, { 6 | type = responses.MESSAGE, 7 | data = msg 8 | }, files) 9 | if success and data then 10 | self._state = 'message' 11 | --self._empty = (not msg.content or files) 12 | return true 13 | else 14 | return nil, e 15 | end 16 | elseif self._state == 'message' then 17 | if self._ephemeral and msg then msg.flags = (msg.flags or 0) | 64 end 18 | 19 | local success, data, e = api:create_followup_message(self.application_id, self.token, msg, files) 20 | if success then return data else return nil, e end 21 | elseif self._state == 'loading' then 22 | if self._ephemeral and msg then 23 | msg.flags = (msg.flags or 0) | 64 24 | end 25 | local success, data, e = api:edit_original_interaction_response(self.application_id, self.token, msg, files) 26 | if success then 27 | self._state = 'message' 28 | --self._empty = (not msg.content or files) 29 | return data 30 | else return nil, e 31 | end 32 | end 33 | end 34 | 35 | return send_inner -------------------------------------------------------------------------------- /lua/lacord/models/context.lua: -------------------------------------------------------------------------------- 1 | local getm = getmetatable 2 | local setm = setmetatable 3 | local err = error 4 | local to_s = tostring 5 | 6 | local running = require"cqueues".running 7 | 8 | local _ENV = {} 9 | 10 | local cache = setm({}, {__mode = 'k'}) 11 | 12 | function attach(loop, ctx) 13 | cache[loop] = ctx 14 | return ctx 15 | end 16 | 17 | local function get() 18 | local loop = running() 19 | return cache[loop], loop 20 | end 21 | 22 | local function getfrom(loop) 23 | return cache[loop] 24 | end 25 | 26 | _ENV.get = get 27 | _ENV.getfrom = getfrom 28 | 29 | -- function checkset(ctx) 30 | -- local ctx_, loop = get() 31 | -- if ctx_ ~= ctx then 32 | -- cache[loop] = ctx 33 | -- end 34 | -- return ctx, loop 35 | -- end 36 | 37 | function api(ctx, loop) 38 | loop = loop or running() 39 | ctx = ctx or cache[loop] or err("lacord.models.context: no model ctx available for "..to_s(loop)..".") 40 | 41 | local mt = getm(ctx) 42 | if mt and mt.__lacord_model_context then 43 | local handle = mt.__lacord_model_context(ctx) 44 | local hmt = getm(handle) 45 | if hmt and hmt.__lacord_is_api then return handle end 46 | end 47 | return err("lacord.models.context: ctx for " ..to_s(loop).." did not satisfy __lacord_model_context or __lacord_is_api.") 48 | end 49 | 50 | function create(ctx_or_type, type_or_data, data_, ...) 51 | local mt = getm(ctx_or_type) 52 | local type, data, ctx 53 | local onlydots 54 | 55 | if not (mt and mt.__lacord_model_context) then 56 | local loop = running() 57 | type = ctx_or_type 58 | data = type_or_data 59 | ctx = cache[loop] or err("lacord.models.context: no model ctx available for "..to_s(loop)..".") 60 | mt = getm(ctx) 61 | else 62 | type = type_or_data 63 | data = data_ 64 | onlydots = true 65 | end 66 | 67 | if mt.__lacord_model_context_create then 68 | if onlydots then 69 | return mt.__lacord_model_context_create(ctx, type, data, ...) 70 | else 71 | return mt.__lacord_model_context_create(ctx, type, data, data_, ...) 72 | end 73 | end 74 | end 75 | 76 | function request(ctx, ...) -- table, primary_id 77 | local loop = running() 78 | local mt = getm(ctx) 79 | local old 80 | 81 | if not (mt and mt.__lacord_model_context) then 82 | old = ctx 83 | ctx = cache[loop] or err("lacord.models.context: no model ctx available for "..to_s(loop)..".") 84 | mt = getm(ctx) 85 | end 86 | 87 | if mt and mt.__lacord_model_context_request then 88 | if old then 89 | return mt.__lacord_model_context_request(ctx, old, ...) 90 | else 91 | return mt.__lacord_model_context_request(ctx, ...) 92 | end 93 | else 94 | return nil 95 | end 96 | end 97 | 98 | function store(ctx, ...) 99 | local loop = running() 100 | local mt = getm(ctx) 101 | local old 102 | 103 | if mt.__lacord_is_api then return nil end 104 | 105 | if not (mt and mt.__lacord_model_context) then 106 | old = ctx 107 | ctx = cache[loop] or err("lacord.models.context: no model ctx available for "..to_s(loop)..".") 108 | mt = getm(ctx) 109 | end 110 | 111 | if mt and mt.__lacord_model_context_store then 112 | if old then 113 | return mt.__lacord_model_context_store(ctx, old, ...) 114 | else 115 | return mt.__lacord_model_context_store(ctx, ...) 116 | end 117 | else 118 | return nil 119 | end 120 | end 121 | 122 | function property(ctx, ...) -- secondary_table, secondary_id, value 123 | local loop = running() 124 | local mt = getm(ctx) 125 | local old 126 | 127 | if mt.__lacord_is_api then return nil end 128 | 129 | if not (mt and mt.__lacord_model_context) then 130 | old = ctx 131 | ctx = cache[loop] or err("lacord.models.context: no model ctx available for "..to_s(loop)..".") 132 | mt = getm(ctx) 133 | end 134 | 135 | if mt and mt.__lacord_model_context_prop then 136 | if old then 137 | return mt.__lacord_model_context_prop(ctx, old, ...) 138 | else 139 | return mt.__lacord_model_context_prop(ctx, ...) 140 | end 141 | else 142 | return nil 143 | end 144 | end 145 | 146 | function upsert(ctx, ...) -- secondary_table, secondary_id, value 147 | local loop = running() 148 | local mt = getm(ctx) 149 | local old 150 | 151 | if mt.__lacord_is_api then return nil end 152 | 153 | if not (mt and mt.__lacord_model_context) then 154 | old = ctx 155 | ctx = cache[loop] or err("lacord.models.context: no model ctx available for "..to_s(loop)..".") 156 | mt = getm(ctx) 157 | end 158 | 159 | if mt and mt.__lacord_model_context_upsert then 160 | if old then 161 | return mt.__lacord_model_context_upsert(ctx, old, ...) 162 | else 163 | return mt.__lacord_model_context_upsert(ctx, ...) 164 | end 165 | else 166 | return nil 167 | end 168 | end 169 | 170 | local property_clear = {} 171 | 172 | _ENV.DEL = property_clear 173 | 174 | local upserters = { } 175 | 176 | function upserters.TABLE() return { } end 177 | function upserters.COUNT() return 0 end 178 | function upserters.VALUE(k) return function() return k end end 179 | 180 | _ENV.upserters = upserters 181 | 182 | function unstore(ctx, ...) 183 | local loop = running() 184 | local mt = getm(ctx) 185 | local old 186 | 187 | if mt.__lacord_is_api then return nil end 188 | 189 | if not (mt and mt.__lacord_model_context) then 190 | old = ctx 191 | ctx = cache[loop] or err("lacord.models.context: no model ctx available for "..to_s(loop)..".") 192 | mt = getm(ctx) 193 | end 194 | 195 | if mt and mt.__lacord_model_context_unstore then 196 | if old then 197 | return mt.__lacord_model_context_unstore(ctx, old, ...) 198 | else 199 | return mt.__lacord_model_context_unstore(ctx, ...) 200 | end 201 | else 202 | return nil 203 | end 204 | end 205 | 206 | local simple_mt = {} 207 | 208 | function simple_mt:__lacord_model_context() 209 | return self[1] 210 | end 211 | 212 | function simple_mt:__lacord_model_context_create(type, data) 213 | local ctor = self[2][type] 214 | if ctor then 215 | local obj = ctor(data) 216 | simple_mt.__lacord_model_context_store(self, obj, type) 217 | return obj 218 | else 219 | return data 220 | end 221 | end 222 | 223 | function simple_mt:__lacord_model_context_request(table, primary_id) 224 | local mcache = self[table] 225 | if mcache then 226 | return primary_id and mcache[primary_id] 227 | end 228 | end 229 | 230 | function simple_mt:__lacord_model_context_store(table, object) 231 | local mcache = self[table] 232 | if mcache then 233 | mcache[object.id] = object 234 | return object.id 235 | end 236 | end 237 | 238 | function simple_mt:__lacord_model_context_unstore(table, primary_id) 239 | local mcache = self[table] 240 | if mcache then 241 | local obj = mcache[primary_id] 242 | mcache[primary_id] = nil 243 | return obj 244 | end 245 | end 246 | 247 | function simple_mt:__lacord_model_context_prop(table, k, v) 248 | local pcache = self.props[table] 249 | if pcache then 250 | if v ~= nil then 251 | goto set 252 | else 253 | if k == "*" or k == nil then return pcache else return pcache[k] end 254 | end 255 | elseif (not pcache) and v ~= nil then 256 | if k == '*' then 257 | self.props[table] = v 258 | return 259 | else 260 | pcache = {} 261 | self.props[table] = pcache 262 | goto set 263 | end 264 | else 265 | return nil 266 | end 267 | 268 | ::set:: 269 | if v == property_clear then v = nil end 270 | if k == "*" then 271 | if v then 272 | self.props[table] = v 273 | end 274 | return pcache 275 | else 276 | local old = pcache[k] 277 | pcache[k] = v 278 | return old 279 | end 280 | end 281 | 282 | function simple_mt:__lacord_model_context_upsert(table, k, default) 283 | local pcache = self.props[table] 284 | if k == nil or k == "*" then err("lacord.models.context: Cannot upsert using key=*", 2) end 285 | if pcache then 286 | if pcache[k] then 287 | return pcache[k] 288 | else 289 | local new = default() 290 | pcache[k] = new 291 | return new 292 | end 293 | elseif not pcache then 294 | local new = default() 295 | pcache = {[k] = new} 296 | self.props[table] = pcache 297 | return new 298 | end 299 | end 300 | 301 | function simple_context(api, ctors) 302 | return setm({api, ctors; channel = {}, guild = {}, command = {}, props = {}}, simple_mt) 303 | end 304 | 305 | return _ENV -------------------------------------------------------------------------------- /lua/lacord/models/guild.lua: -------------------------------------------------------------------------------- 1 | local err = error 2 | local iter = pairs 3 | 4 | local insert = table.insert 5 | 6 | local context = require"lacord.models.context" 7 | local methods = require"lacord.models.methods" 8 | local map_bang = require"lacord.util".map_bang 9 | local map = require"lacord.util".map 10 | 11 | local fetch_chl = require"lacord.models.channel".fetch 12 | 13 | local getapi = context.api 14 | local getctx = context.get 15 | local create = context.create 16 | local request = context.request 17 | local property = context.property 18 | local upsert = context.upsert 19 | local TABLE = context.upserters.TABLE 20 | 21 | local model_id = methods.model_id 22 | 23 | --luacheck: ignore 111 24 | 25 | local _ENV = {} 26 | 27 | local function guild_property(type, guild_id, obj_id) 28 | local set = upsert('guild->'..type, guild_id, TABLE) 29 | set[obj_id] = true 30 | end 31 | 32 | _ENV.set_guild_property = guild_property 33 | 34 | function fetch(g) 35 | g = model_id(g, 'guild') 36 | local gld = request('guild', g) 37 | if not gld then 38 | local api = getapi() 39 | local success, data, e = api:get_guild(g) 40 | if success then 41 | gld = create('guild', data) 42 | else 43 | return err( 44 | "lacord.models.guild: Unable to resolve guild id to guild." 45 | .."\n "..e 46 | ) 47 | end 48 | end 49 | return gld or err"lacord.models.guild: Unable to resolve guild id to guild." 50 | end 51 | 52 | local function swap3(a, b, c, f) 53 | return f(c, b, a) 54 | end 55 | 56 | local function build_channels(data, ctx, g) 57 | data.guild_id = g 58 | local chl = create(ctx, 'channel', data) 59 | guild_property('channel', g, chl.id) 60 | return chl 61 | end 62 | 63 | function channels(g) 64 | g = model_id(g, 'guild') 65 | local ctx = getctx() 66 | 67 | local cached = property(ctx, 'guild->channel', g) 68 | 69 | if cached then 70 | local out = { } 71 | for k in iter(cached) do 72 | insert(out, fetch_chl(k)) 73 | end 74 | return out 75 | else 76 | local api = getapi(ctx) 77 | local success, data, e = api:get_guild_channels(g) 78 | if success then 79 | local res = map_bang(build_channels, data, ctx, g) 80 | return res 81 | else 82 | return nil, e 83 | end 84 | 85 | end 86 | end 87 | 88 | function new_channel(g, payload) 89 | g = model_id(g, 'guild') 90 | local ctx = getctx() 91 | local api = getapi(ctx) 92 | local success, data, e = api:create_guild_channel(g, payload) 93 | 94 | if success then 95 | data.guild_id = g 96 | local chl = create(ctx, 'channel', data) 97 | 98 | guild_property('channel', g, chl.id) 99 | 100 | return chl 101 | else 102 | return nil, e 103 | end 104 | end 105 | 106 | function active_threads(g) 107 | g = model_id(g, 'guild') 108 | local ctx = getctx() 109 | local api = getapi(ctx) 110 | local success, data, e = api:list_active_guild_threads(g) 111 | if success then 112 | local chls = map(swap3, data[1], 'channel', ctx, create) 113 | return chls, data[2] 114 | else 115 | return nil, e 116 | end 117 | end 118 | 119 | function membership(g, u) 120 | g = model_id(g, 'guild') 121 | u = model_id(u, 'user') 122 | 123 | local api = getapi() 124 | local success, data, e = api:get_guild_member(g, u) 125 | 126 | if success then 127 | return data 128 | else 129 | return nil, e 130 | end 131 | end 132 | 133 | function list_members(g, limit, after) 134 | g = model_id(g, 'guild') 135 | 136 | local api = getapi() 137 | local success, data, e = api:list_guild_members(g, {limit = limit, after = after}) 138 | 139 | if success then 140 | return data 141 | else 142 | return nil, e 143 | end 144 | end 145 | 146 | function search_members(g, limit, after) 147 | g = model_id(g, 'guild') 148 | 149 | local api = getapi() 150 | local success, data, e = api:search_guild_members(g, {limit = limit, after = after}) 151 | 152 | if success then 153 | return data 154 | else 155 | return nil, e 156 | end 157 | end 158 | 159 | function edit_membership(g, u, payload) 160 | g = model_id(g, 'guild') 161 | u = model_id(u, 'user') 162 | 163 | local api = getapi() 164 | local success, data, e = api:modify_guild_member(g, u, payload) 165 | 166 | if success then 167 | return data 168 | else 169 | return nil, e 170 | end 171 | end 172 | 173 | function edit_my_membership(g, payload) 174 | g = model_id(g, 'guild') 175 | 176 | local api = getapi() 177 | local success, data, e = api:modify_current_member(g, payload) 178 | 179 | if success then 180 | return data 181 | else 182 | return nil, e 183 | end 184 | end 185 | 186 | function add_role_to(g, u, r) 187 | g = model_id(g, 'guild') 188 | u = model_id(u, 'user') 189 | r = model_id(r, 'role') 190 | 191 | local api = getapi() 192 | local success, data, e = api:add_guild_member_role(g, u, r) 193 | 194 | if success then 195 | return data 196 | else 197 | return nil, e 198 | end 199 | end 200 | 201 | function remove_roles_from(g, u, r) 202 | g = model_id(g, 'guild') 203 | u = model_id(u, 'user') 204 | r = model_id(r, 'role') 205 | 206 | local api = getapi() 207 | local success, data, e = api:remove_guild_member_role(g, u, r) 208 | 209 | if success then 210 | return data 211 | else 212 | return nil, e 213 | end 214 | end 215 | 216 | function kick(g, u) 217 | g = model_id(g, 'guild') 218 | u = model_id(u, 'user') 219 | 220 | local api = getapi() 221 | local success, data, e = api:remove_guild_member(g, u) 222 | 223 | if success then 224 | return data 225 | else 226 | return nil, e 227 | end 228 | end 229 | 230 | function banlist(g, limit, before, after) 231 | g = model_id(g, 'guild') 232 | 233 | local api = getapi() 234 | local success, data, e = api:get_guild_bans(g, {limit = limit, before = before, after = after}) 235 | 236 | if success then 237 | return data 238 | else 239 | return nil, e 240 | end 241 | end 242 | 243 | function get_ban(g, b) 244 | g = model_id(g, 'guild') 245 | b = model_id(b, 'ban') 246 | 247 | local api = getapi() 248 | local success, data, e = api:get_guild_ban(g, b) 249 | 250 | if success then 251 | return data 252 | else 253 | return nil, e 254 | end 255 | end 256 | 257 | function ban(g, u, days) 258 | g = model_id(g, 'guild') 259 | u = model_id(u, 'user') 260 | 261 | local api = getapi() 262 | local success, data, e = api:create_guild_ban(g, u, days and {delete_message_days = days}) 263 | 264 | if success then 265 | return data 266 | else 267 | return nil, e 268 | end 269 | end 270 | 271 | function unban(g, u) 272 | g = model_id(g, 'guild') 273 | u = model_id(u, 'user') 274 | 275 | local api = getapi() 276 | local success, data, e = api:remove_guild_ban(g, u) 277 | 278 | if success then 279 | return data 280 | else 281 | return nil, e 282 | end 283 | end 284 | 285 | function load_roles(g) 286 | g = model_id(g, 'guild') 287 | 288 | local ctx = getctx() 289 | local api = getapi(ctx) 290 | local success, data, e = api:get_guild_roles(g) 291 | 292 | if success then 293 | local n = #data 294 | local item 295 | for i = 1, n do 296 | item = data[i] 297 | item.guild_id = g 298 | data[i] = create(ctx, 'role', item) 299 | guild_property('role', g, item.id) 300 | end 301 | return data 302 | else 303 | return nil, e 304 | end 305 | end 306 | 307 | function new_role(g, payload) 308 | g = model_id(g, 'guild') 309 | 310 | local ctx = getctx() 311 | local api = getapi(ctx) 312 | local success, data, e = api:create_guild_role(g, payload) 313 | 314 | if success then 315 | data.guild_id = g 316 | local rol = create(ctx, 'role', data) 317 | guild_property('role', g, rol.id) 318 | return rol 319 | else 320 | return nil, e 321 | end 322 | end 323 | 324 | function iterate(g, type) 325 | return iter, upsert('guild->'..type, g, TABLE) 326 | end 327 | 328 | 329 | 330 | return _ENV 331 | 332 | 333 | 334 | 335 | 336 | 337 | -------------------------------------------------------------------------------- /lua/lacord/models/impl/channel.lua: -------------------------------------------------------------------------------- 1 | local iter = pairs 2 | local setm = setmetatable 3 | 4 | local context = require"lacord.models.context" 5 | local mtostring = require"lacord.util.models".tostring 6 | local methodify = require"lacord.util.models".methodify 7 | local mod = require"lacord.models.channel" 8 | local fetch_chl = mod.fetch 9 | 10 | local unstore = context.unstore 11 | local create = context.create 12 | local property = context.property 13 | local DEL = context.DEL 14 | local upsert = context.upsert 15 | local TABLE = context.upserters.TABLE 16 | 17 | local _ENV = {} 18 | 19 | local channel_mt = { 20 | __lacord_model_id = function(obj) return obj.id end, 21 | __lacord_model = 'channel', 22 | __lacord_model_mention = function(obj) return "<#" .. obj.id .. ">" end, 23 | __tostring = mtostring, 24 | } 25 | 26 | local function as_channel(tbl) 27 | return setm(tbl, channel_mt) 28 | end 29 | 30 | local channel_id_wrapper = { 31 | __lacord_model_id = function(obj) return obj[1] end, 32 | __lacord_model = 'channel', 33 | __lacord_model_partial = true, 34 | __lacord_channel = function(obj) return fetch_chl(obj[1]) end 35 | } 36 | 37 | local function as_id(str) 38 | return setm({str}, channel_id_wrapper) 39 | end 40 | 41 | --- Operations on channels --- 42 | 43 | function channel_mt:__lacord_model_send(api, msg, files) 44 | local success, data, e = api:create_message(self.id, msg, files) 45 | if success then 46 | return create('message', data) 47 | else 48 | return nil, e 49 | end 50 | end 51 | 52 | function channel_id_wrapper:__lacord_model_send(api, msg, files) 53 | local success, data, e = api:create_message(self[1], msg, files) 54 | if success then 55 | return create('message', data) 56 | else 57 | return nil, e 58 | end 59 | end 60 | 61 | function channel_mt:__lacord_model_delete(api) 62 | local success, data, e = api:delete_channel(self.id) 63 | if success and data then 64 | unstore('channel', self.id) 65 | if self.recipient_id then 66 | property('dms', self.recipient_id, DEL) 67 | elseif self.guild_id then 68 | upsert('guild->role', self.guild_id, TABLE)[self.id] = nil 69 | end 70 | 71 | return true 72 | else 73 | return nil, e 74 | end 75 | end 76 | 77 | function channel_id_wrapper:__lacord_model_delete(api) 78 | local success, data, e = api:delete_channel(self[1]) 79 | if success and data then 80 | unstore('channel', self[1]) 81 | local dms = property('dms', "*") 82 | local rem 83 | for k , v in iter(dms) do 84 | if v == self[1] then 85 | rem = k break 86 | end 87 | end 88 | if rem then property('dms', rem, DEL) end 89 | return true 90 | else 91 | return nil, e 92 | end 93 | end 94 | 95 | function channel_mt:__lacord_model_update(api, edit) 96 | local success, data, e = api:modify_channel(self.id, edit) 97 | 98 | if success then 99 | data.guild_id = self.guild_id 100 | local chl = create('channel', data) 101 | return chl 102 | else 103 | return nil, e 104 | end 105 | end 106 | 107 | methodify(channel_mt, mod, 'guild') 108 | 109 | return { 110 | from = as_channel, 111 | as_id = as_id, 112 | 113 | mt = channel_mt, 114 | id_wrapper = channel_id_wrapper, 115 | } -------------------------------------------------------------------------------- /lua/lacord/models/impl/constructors.lua: -------------------------------------------------------------------------------- 1 | local impl = { 2 | user = require"lacord.models.impl.user", 3 | channel = require"lacord.models.impl.channel", 4 | guild = require"lacord.models.impl.guild", 5 | message = require"lacord.models.impl.message", 6 | role = require"lacord.models.impl.role", 7 | interaction = require"lacord.models.impl.interaction" 8 | } 9 | 10 | local ctors = { } 11 | 12 | for type, mod in pairs(impl) do 13 | ctors[type] = mod.from 14 | end 15 | 16 | return ctors -------------------------------------------------------------------------------- /lua/lacord/models/impl/guild.lua: -------------------------------------------------------------------------------- 1 | local setm = setmetatable 2 | 3 | local methodify = require"lacord.util.models".methodify 4 | local mod = require"lacord.models.guild" 5 | local mtostring = require"lacord.util.models".tostring 6 | local context = require"lacord.models.context" 7 | 8 | local create = context.create 9 | 10 | local fetch_gld = mod.fetch 11 | 12 | local THROW_OUT = require"lacord.const".models.remove_unused_keys 13 | 14 | local guild_mt = { 15 | __lacord_model_id = function(obj) return obj.id end, 16 | __lacord_model = 'guild', 17 | __tostring = mtostring, 18 | } 19 | 20 | local as_guild if THROW_OUT then 21 | function as_guild(tbl, _) 22 | 23 | if not tbl.unavailable then 24 | tbl.roles = nil 25 | tbl.emojis = nil 26 | tbl.members = nil 27 | tbl.voice_states = nil 28 | tbl.presences = nil 29 | tbl.channels = nil 30 | tbl.threads = nil 31 | end 32 | 33 | return setm(tbl, guild_mt) 34 | end 35 | else 36 | function as_guild(tbl) 37 | return setm(tbl, guild_mt) 38 | end 39 | end 40 | 41 | 42 | 43 | local guild_id_wrapper = { 44 | __lacord_model_id = function(obj) return obj[1] end, 45 | __lacord_model = 'guild', 46 | __lacord_model_partial = true, 47 | __lacord_guild = function(obj) return fetch_gld(obj[1]) end, 48 | } 49 | 50 | local function as_id(str) 51 | return setm({str}, guild_id_wrapper) 52 | end 53 | 54 | function guild_mt:__lacord_model_update(api, edit) 55 | local success, data, e = api:modify_guild(self.id, edit) 56 | 57 | if success then 58 | return create('guild', data, false, true) 59 | else 60 | return nil, e 61 | end 62 | end 63 | 64 | methodify(guild_mt, mod) 65 | 66 | return { 67 | from = as_guild, 68 | as_id = as_id, 69 | 70 | mt = guild_mt, 71 | id_wrapper = guild_id_wrapper, 72 | } -------------------------------------------------------------------------------- /lua/lacord/models/impl/init.lua: -------------------------------------------------------------------------------- 1 | local running = require"cqueues".running 2 | 3 | local context = require"lacord.models.context" 4 | local ctors = require"lacord.models.impl.constructors" 5 | 6 | return function(api, loop) 7 | local ctx = context.simple_context(api, ctors) 8 | loop = loop or running() 9 | if loop then 10 | context.attach(loop, ctx) 11 | end 12 | return ctx 13 | end -------------------------------------------------------------------------------- /lua/lacord/models/impl/interaction.lua: -------------------------------------------------------------------------------- 1 | local setm = setmetatable 2 | local to_n = tonumber 3 | 4 | local insert = table.insert 5 | 6 | local context = require"lacord.models.context" 7 | local mtostring = require"lacord.util.models".tostring 8 | local methodify = require"lacord.util.models".methodify 9 | local mod = require"lacord.models.interaction" 10 | local numbers = require"lacord.models.magic-numbers" 11 | local send_int = require"lacord.models.common.send-interaction" 12 | 13 | 14 | local store = context.store 15 | local unstore = context.unstore 16 | local create = context.create 17 | local property = context.property 18 | local DEL = context.DEL 19 | 20 | local types = numbers.interaction_type 21 | 22 | 23 | local _ENV = {} 24 | 25 | local interaction_mt = { 26 | __lacord_model_id = function(obj) return obj.id end, 27 | __lacord_model = 'interaction', 28 | __lacord_model_mention = function(obj) return "<#" .. obj.id .. ">" end, 29 | __tostring = mtostring, 30 | } 31 | 32 | local function as_interaction(self) 33 | -- normalize and hydrate -- 34 | if self.type == types.COMMAND then 35 | local opts = { } 36 | local options = self.data.options 37 | if self.data.options then 38 | for i = 1, #options do 39 | local opt = options[i] 40 | opts[opt.name] = opt.value 41 | insert(opts, opt.value) 42 | end 43 | end 44 | self.options = opts 45 | end 46 | 47 | if self.member then 48 | self.member.user = create('user', self.member.user) 49 | else 50 | self.user = create('user', self.user) 51 | end 52 | 53 | if self.message then 54 | self.message = create('message', self.message) 55 | end 56 | 57 | self.app_permissions = to_n(self.app_permissions) 58 | 59 | return setm(self, interaction_mt) 60 | end 61 | 62 | --- Operations on channels --- 63 | 64 | function interaction_mt:__lacord_model_send(...) 65 | return send_int(self, ...) 66 | end 67 | 68 | function interaction_mt:__lacord_channel() 69 | return mod.channel(self) 70 | end 71 | 72 | function interaction_mt:__lacord_guild() 73 | return mod.guild(self) 74 | end 75 | 76 | function interaction_mt:__lacord_user() 77 | if self.member then 78 | return self.member.user 79 | else return self.user 80 | end 81 | end 82 | 83 | methodify(interaction_mt, mod, 84 | 'channel', 'guild', 'state', 85 | 'custom_id', 86 | 'command', 'command_id', 'command_name', 'subcommand', 'root', 'group', 'command_type', 'args', 'target') 87 | 88 | return { 89 | from = as_interaction, 90 | 91 | mt = interaction_mt, 92 | } -------------------------------------------------------------------------------- /lua/lacord/models/impl/message.lua: -------------------------------------------------------------------------------- 1 | local setm = setmetatable 2 | 3 | local context = require"lacord.models.context" 4 | local mtostring = require"lacord.util.models".tostring 5 | local methodify = require"lacord.util.models".methodify 6 | local mod = require"lacord.models.message" 7 | 8 | local getapi = context.api 9 | local getctx = context.get 10 | local create = context.create 11 | local request = context.request 12 | local unstore = context.unstore 13 | 14 | local _ENV = {} 15 | 16 | local message_mt = { 17 | __lacord_model_id = function(obj) return obj.id end, 18 | __lacord_model = 'message', 19 | __tostring = mtostring, 20 | } 21 | 22 | local function as_channel(tbl) 23 | return setm(tbl, message_mt) 24 | end 25 | 26 | --- Operations on messages --- 27 | 28 | function message_mt:__lacord_model_send(api, msg, files) 29 | if not msg.message_reference then 30 | msg.message_reference = { 31 | message_id = self.id, 32 | channel_id = self.channel_id, 33 | fail_if_not_exists = false, 34 | } 35 | if self.guild_id then msg.message_reference.guild_id = self.guild_id end 36 | end 37 | local success, data, e = api:create_message(self.channel_id, msg, files) 38 | if success then 39 | return create('message', data) 40 | else 41 | return nil, e 42 | end 43 | end 44 | 45 | function message_mt:__lacord_model_delete(api) 46 | local success, data, e = api:delete_message(self.channel_id, self.id) 47 | if success and data then 48 | unstore('message', self.id) 49 | return true 50 | else 51 | return nil, e 52 | end 53 | end 54 | 55 | function message_mt:__lacord_model_update(api, edit, files) 56 | local success, data, e = api:edit_message(self.channel_id, self.id, edit, files) 57 | 58 | if success then 59 | return create('message', data) 60 | else 61 | return nil, e 62 | end 63 | end 64 | 65 | function message_mt:__lacord_channel() 66 | local ctx, loop = getctx() 67 | 68 | local chl = request(ctx, 'channel', self.channel_id) 69 | 70 | if not chl then 71 | local api = getapi(ctx, loop) 72 | local success, data, e = api:get_channel(self.channel_id) 73 | if success then 74 | chl = create('channel', data) 75 | else 76 | return nil, e 77 | end 78 | end 79 | 80 | return chl 81 | end 82 | 83 | function message_mt:__lacord_guild() 84 | if not self.guild_id then return nil 85 | else 86 | local ctx, loop = getctx() 87 | 88 | local gld = request(ctx, 'guild', self.guild_id) 89 | 90 | if not gld then 91 | local api = getapi(ctx, loop) 92 | local success, data, e = api:get_guild(self.guild_id) 93 | if success then 94 | gld = create('guild', data) 95 | else 96 | return nil, e 97 | end 98 | end 99 | 100 | return gld 101 | end 102 | end 103 | 104 | function message_mt:__lacord_user() 105 | if not (self.author and self.author.id) then return nil 106 | else 107 | local usr = request('user', self.author.id) 108 | if not usr then 109 | local api = getapi() 110 | local success, data, e = api:get_user(self.author.id) 111 | if success then 112 | usr = create('user', data) 113 | else 114 | return nil, e 115 | end 116 | end 117 | return usr 118 | end 119 | end 120 | 121 | methodify(message_mt, mod, 'link', 'channel', 'guild') 122 | 123 | return { 124 | from = as_channel, 125 | mt = message_mt, 126 | } -------------------------------------------------------------------------------- /lua/lacord/models/impl/role.lua: -------------------------------------------------------------------------------- 1 | local setm = setmetatable 2 | 3 | 4 | local context = require"lacord.models.context" 5 | local mtostring = require"lacord.util.models".tostring 6 | 7 | local create = context.create 8 | local unstore = context.unstore 9 | local upsert = context.upsert 10 | local TABLE = context.upserters.TABLE 11 | 12 | local role_mt = { 13 | __lacord_model_id = function(obj) return obj.id end, 14 | __lacord_model = 'role', 15 | __tostring = mtostring, 16 | __lacord_model_mention = function(obj) return "<@&" .. obj.id .. ">" end 17 | } 18 | 19 | function role_mt:__lacord_model_update(api, edit) 20 | 21 | local g = self.guild_id 22 | 23 | local success, data, e = api:modify_guild_role(g, self.id, edit) 24 | 25 | if success then 26 | data.guild_id = g 27 | local rol = create('role', data) 28 | return rol 29 | else 30 | return nil, e 31 | end 32 | end 33 | 34 | function role_mt:__lacord_model_delete(api) 35 | local g = self.guild_id 36 | local success, data, e = api:delete_guild_role(g, self.id) 37 | 38 | if success and data then 39 | unstore('role', self.id) 40 | upsert('guild->role', g, TABLE)[self.id] = nil 41 | return true 42 | else 43 | return nil, e 44 | end 45 | end 46 | 47 | local function as_role(tbl) return setm(tbl, role_mt) end 48 | 49 | return { 50 | from = as_role, 51 | 52 | mt = role_mt, 53 | } -------------------------------------------------------------------------------- /lua/lacord/models/impl/user.lua: -------------------------------------------------------------------------------- 1 | local setm = setmetatable 2 | 3 | local context = require"lacord.models.context" 4 | local mtostring = require"lacord.util.models".tostring 5 | local methodify = require"lacord.util.models".methodify 6 | local mod = require"lacord.models.user" 7 | local fetch_usr = mod.fetch 8 | 9 | local getapi = context.api 10 | local getctx = context.get 11 | local request = context.request 12 | local store = context.store 13 | local create = context.create 14 | local property = context.property 15 | 16 | local user_mt = { 17 | __tostring = mtostring, 18 | __lacord_model_id = function(obj) return obj.id end, 19 | __lacord_model = 'user', 20 | __lacord_model_mention = function(obj) return "<@" .. obj.id .. ">" end, 21 | __lacord_model_defer = { 22 | send = '__lacord_channel' 23 | } 24 | } 25 | 26 | --- Wrap a lua table in a metatable which designates it a user. 27 | local function as_user(tbl) 28 | return setm(tbl, user_mt) 29 | end 30 | 31 | local user_id_mt = { 32 | __lacord_model_id = function(obj) return obj[1] end, 33 | __lacord_model = 'user', 34 | __lacord_model_partial = true, 35 | __lacord_user = fetch_usr, 36 | } 37 | 38 | local function as_id(str) 39 | return setm({str}, user_id_mt) 40 | end 41 | 42 | function user_mt:__lacord_channel() 43 | local the_id = self.id 44 | local ctx, loop = getctx() 45 | 46 | local chl_id = property(ctx, 'dms', the_id) 47 | 48 | local chl = chl_id and request(ctx, 'channel', chl_id) 49 | 50 | if not chl then 51 | local api = getapi(ctx, loop) 52 | local success, data, e = api:create_dm{recipient_id = the_id} 53 | if success then 54 | chl = create('channel', data) 55 | property(ctx, 'dms', the_id, chl.id) 56 | else 57 | return nil, e 58 | end 59 | end 60 | 61 | return chl 62 | end 63 | 64 | methodify(user_mt, mod, 'tag') 65 | 66 | return { 67 | from = as_user, 68 | as_id = as_id, 69 | 70 | mt = user_mt, 71 | id_wrapper = user_id_mt, 72 | } -------------------------------------------------------------------------------- /lua/lacord/models/interaction.lua: -------------------------------------------------------------------------------- 1 | local err = error 2 | local iter = pairs 3 | local iiter = ipairs 4 | local to_s = tostring 5 | local typ = type 6 | 7 | local command = require"lacord.command" 8 | local context = require"lacord.models.context" 9 | local copy = require"lacord.util".copy 10 | local map_bang = require"lacord.util".map_bang 11 | local methods = require"lacord.models.methods" 12 | local numbers = require"lacord.models.magic-numbers" 13 | local send_int = require"lacord.models.common.send-interaction" 14 | 15 | 16 | 17 | local getapi = context.api 18 | local create = context.create 19 | local request = context.request 20 | local property = context.property 21 | 22 | local modify = methods.update 23 | local model_id = methods.model_id 24 | local resolve = methods.resolve 25 | 26 | local responses = numbers.interaction_response 27 | local types = numbers.interaction_type 28 | 29 | local _ENV = {} 30 | 31 | -- luacheck: ignore 111 32 | 33 | --- General interaction functionality --- 34 | 35 | function _ENV.channel(i) 36 | i = resolve(i, 'interaction') 37 | return i.channel_id and request('channel', i.channel_id) 38 | end 39 | 40 | 41 | function _ENV.guild(i) 42 | i = resolve(i, 'interaction') 43 | return i.guild_id and request('guild', i.guild_id) 44 | end 45 | 46 | function _ENV.invoker(i) 47 | i = resolve(i, 'interaction') 48 | return i.member and i.member.user or i.user 49 | end 50 | 51 | function state(i) i = resolve(i, 'interaction') return i._state end 52 | 53 | 54 | function _ENV.send(i, msg, files) 55 | i = resolve(i, 'interaction') 56 | if typ(msg) == 'string' then msg = {content = msg} end 57 | return send_int(i, getapi(), msg, files) 58 | end 59 | 60 | _ENV.reply = _ENV.send 61 | 62 | 63 | function _ENV.whisper(i, msg, files) 64 | i = resolve(i, 'interaction') 65 | i._ephemeral = true 66 | if typ(msg) == 'string' then msg = {content = msg, flags = 64} 67 | elseif not msg then msg = {flags = 64} 68 | end 69 | return send_int(i, getapi(), msg, files) 70 | end 71 | 72 | 73 | local ephemeral_data = {flags = 64} 74 | 75 | function defer(i, ephemeral) 76 | i = resolve(i, 'interaction') 77 | if not i._state then 78 | if ephemeral then i._ephemeral = true end 79 | local api = getapi() 80 | local success, data, e = api:create_interaction_response(i.id, i.token, { 81 | type = responses.LOADING, 82 | data = ephemeral and ephemeral_data or nil, 83 | }) 84 | if success and data then 85 | i._state = 'loading' 86 | return true 87 | else 88 | return nil, e 89 | end 90 | end 91 | end 92 | 93 | 94 | --- Component specific methods --- 95 | function ack(i, ephemeral) 96 | i = resolve(i, 'interaction') 97 | if not i._state then 98 | if ephemeral then i._ephemeral = true end 99 | local api = getapi() 100 | local success, data, e = api:create_interaction_response(i.id, i.token, { 101 | type = responses.ACKNOWLEDGE, 102 | data = ephemeral and ephemeral_data or nil, 103 | }) 104 | if success and data then 105 | i._state = 'message' 106 | return true 107 | else 108 | return nil, e 109 | end 110 | end 111 | end 112 | 113 | _ENV.acknowledge = _ENV.ack 114 | 115 | 116 | function update_message(i, msg, files) 117 | i = resolve(i, 'interaction') 118 | if i.type == types.COMPONENT then 119 | if typ(msg) == 'string' then msg = {content = msg} end 120 | local api = getapi() 121 | if not i._state then 122 | local success, data, e = api:create_interaction_response(i.id, i.token, { 123 | type = responses.UPDATE_MESSAGE, 124 | data = msg, 125 | }, files) 126 | if success and data then 127 | i._state = 'message' 128 | i._empty = (not msg.content or files) 129 | return true 130 | else 131 | return nil, e 132 | end 133 | else 134 | local success, data, e = api:edit_message(i.message.channel_id, i.message.id, msg, files) 135 | if success and data then 136 | return true 137 | else 138 | return nil, e 139 | end 140 | end 141 | end 142 | end 143 | 144 | 145 | function custom_id(i) 146 | i = resolve(i, 'interaction') 147 | return i.data.custom_id 148 | end 149 | 150 | 151 | local function values_(I, i) 152 | if I.data.values[i] then 153 | return I.data.values[i].value, values_(I, i + 1) 154 | end 155 | end 156 | 157 | function values(i) 158 | i = resolve(i, 'interaction') 159 | if i.data.values then 160 | return values_(i, 1) 161 | end 162 | end 163 | 164 | 165 | function clear(i) 166 | i = resolve(i, 'interaction') 167 | if i._empty then 168 | local api = getapi() 169 | local success, data, e = api:delete_original_interaction_response(i.application_id, i.token) 170 | if success and data then 171 | return true 172 | else 173 | return nil, e 174 | end 175 | else 176 | if (i.message.content ~= "") or i.message.attachments[1] or i.message.embeds[1] then 177 | return _ENV.update_message(i, {components = {}}) 178 | else 179 | local success, data, e = getapi():delete_message(i.message.channel_id, i.message.id) 180 | if success and data then 181 | return true 182 | else 183 | return nil, e 184 | end 185 | end 186 | end 187 | end 188 | 189 | function delete(i) 190 | i = resolve(i, 'interaction') 191 | if i._empty ~= nil then 192 | local api = getapi() 193 | local success, data, e = api:delete_original_interaction_response(i.application_id, i.token) 194 | if success and data then 195 | return true 196 | else 197 | return nil, e 198 | end 199 | else 200 | local success, data, e = getapi():delete_message(i.message.channel_id, i.message.id) 201 | if success and data then 202 | return true 203 | else 204 | return nil, e 205 | end 206 | end 207 | end 208 | 209 | 210 | local disable_ = function(cmp) cmp.disabled = true end 211 | 212 | function disable(i) 213 | i = resolve(i, 'interaction') 214 | local components = copy(i.message.components) 215 | for j , row in iiter(components) do 216 | components[j] = map_bang(disable_, row) 217 | end 218 | return _ENV.update_message(i, {components = components}) 219 | end 220 | 221 | 222 | function replace_components(i, msg, files) 223 | i = resolve(i, 'interaction') 224 | if typ(msg) == 'string' then msg = {content = msg} end 225 | msg.components = {} 226 | return _ENV.update_message(i, msg, files) 227 | end 228 | 229 | 230 | --- App command specific methods --- 231 | 232 | local function resolve_type(i, ty) 233 | i = resolve(i, 'interaction') 234 | if i.type == ty then return i end 235 | end 236 | 237 | local function load_name(i) 238 | i._full_name, i._inner_name, i._middle_name = command.full_name(i) 239 | return i._full_name 240 | end 241 | 242 | 243 | function command_name(i) 244 | if i._full_name then return i._full_name end 245 | i = resolve_type(i, types.COMMAND) 246 | if i then return load_name(i) end 247 | end 248 | 249 | 250 | function subcommand(i) 251 | if i._inner_name then return i._inner_name end 252 | i = resolve_type(i, types.COMMAND) 253 | if i then 254 | load_name(i) 255 | return i._inner_name 256 | end 257 | end 258 | 259 | 260 | function group(i) 261 | if i._inner_name then return i._inner_name end 262 | i = resolve_type(i, types.COMMAND) 263 | if i then 264 | load_name(i) 265 | return i._middle_name 266 | end 267 | end 268 | 269 | 270 | function root(i) 271 | i = resolve_type(i, types.COMMAND) 272 | if i then 273 | return i.data.name 274 | end 275 | end 276 | 277 | 278 | function command_id(i) 279 | i = resolve_type(i, types.COMMAND) 280 | if i then 281 | return i.data.id 282 | end 283 | end 284 | 285 | 286 | function command_type(i) 287 | i = resolve_type(i, types.COMMAND) 288 | if i then 289 | return i.data.type 290 | end 291 | end 292 | 293 | 294 | function _ENV.command(i) 295 | i = resolve_type(i, types.COMMAND) 296 | if i then 297 | return context.request('command', i.data.id) 298 | end 299 | end 300 | 301 | 302 | function args(i) 303 | i = resolve_type(i, types.COMMAND) 304 | if i then 305 | return i.options 306 | end 307 | end 308 | 309 | 310 | local targets = { 311 | [numbers.command_type.USER_CONTEXT] = 'users', 312 | [numbers.command_type.MESSAGE_CONTEXT] = 'messages', 313 | } 314 | 315 | function target(i) 316 | i = resolve_type(i, types.COMMAND) 317 | if i then 318 | local resolved = targets[i.data.type] 319 | local obj = resolved and i.data.resolved[resolved][i.data.target_id] 320 | if obj then 321 | return create(resolved:sub(1, -2), obj) 322 | end 323 | end 324 | end 325 | 326 | 327 | local function get_resolved(i, type, id) 328 | i = resolve(i, 'interaction') 329 | if i.data and i.data.resolved and i.data.resolved[type] then 330 | local map = i.data.resolved[type] 331 | if map[id] then return create(type:sub(1, -2), map[id]) end 332 | end 333 | end 334 | 335 | 336 | local can_resolve = { 337 | users = true, 338 | members = true, 339 | roles = true, 340 | channels = true, 341 | messages = true, 342 | attachments = true 343 | } 344 | 345 | for name in iter(can_resolve) do 346 | _ENV['resolved_'..name:sub(1, -2)] = function(i, ...) 347 | return get_resolved(i, name, ...) 348 | end 349 | end 350 | 351 | 352 | return _ENV -------------------------------------------------------------------------------- /lua/lacord/models/magic-numbers.lua: -------------------------------------------------------------------------------- 1 | -- luacheck: ignore 111 113 2 | 3 | local _ENV, iota, powers_of_two, iota1, iotaN, boundary = require"lacord.util.models.magic-numbers"() 4 | 5 | channel_type = iota{ 6 | TEXT, 7 | DM, 8 | VOICE, 9 | GROUP_DM, 10 | CATEGORY, 11 | NEWS, 12 | NEWS_THREAD, 13 | PUBLIC_THREAD, 14 | PRIVATE_THREAD, 15 | STAGE_VOICE, 16 | DIRECTORY, 17 | FORUM, 18 | } 19 | 20 | 21 | message_flag = powers_of_two{ 22 | CROSSPOSTED, 23 | IS_CROSSPOST, 24 | SUPPRESS_EMBEDS, 25 | SOURCE_MESSAGE_DELETED, 26 | URGENT, 27 | HAS_THREAD, 28 | EPHEMERAL, 29 | LOADING, 30 | MENTIONS_FAILED, 31 | } 32 | 33 | permission = powers_of_two{ 34 | CREATE_INSTANT_INVITE, 35 | KICK_MEMBERS, 36 | BAN_MEMBERS, 37 | ADMINISTRATOR, 38 | MANAGE_CHANNELS, 39 | MANAGE_GUILD, 40 | ADD_REACTIONS, 41 | VIEW_AUDIT_LOG, 42 | PRIORITY_SPEAKER, 43 | STREAM, 44 | VIEW_CHANNEL, 45 | SEND_MESSAGES, 46 | SEND_TTS_MESSAGES, 47 | MANAGE_MESSAGES, 48 | EMBED_LINKS, 49 | ATTACH_FILES, 50 | READ_MESSAGE_HISTORY, 51 | MENTION_EVERYONE, 52 | USE_EXTERNAL_EMOJIS, 53 | VIEW_GUILD_INSIGHTS, 54 | CONNECT, 55 | SPEAK, 56 | MUTE_MEMBERS, 57 | DEAFEN_MEMBERS, 58 | MOVE_MEMBERS, 59 | USE_VAD, 60 | CHANGE_NICKNAME, 61 | MANAGE_NICKNAMES, 62 | MANAGE_ROLES, 63 | MANAGE_WEBHOOKS, 64 | MANAGE_EMOJIS_AND_STICKERS, 65 | USE_APPLICATION_COMMANDS, 66 | REQUEST_TO_SPEAK, 67 | MANAGE_EVENTS, 68 | MANAGE_THREADS, 69 | CREATE_PUBLIC_THREADS, 70 | CREATE_PRIVATE_THREADS, 71 | USE_EXTERNAL_STICKERS, 72 | SEND_MESSAGES_IN_THREADS, 73 | USE_EMBEDDED_ACTIVITIES, 74 | MODERATE_MEMBERS, 75 | } 76 | 77 | interaction_type = iota1{ 78 | PING, 79 | COMMAND, 80 | COMPONENT, 81 | AUTOCOMPLETE, 82 | MODAL_RESPONSE, 83 | } 84 | 85 | interaction_response = iotaN{ 86 | PONG = 1, 87 | MESSAGE = 4, 88 | LOADING, 89 | ACKNOWLEDGE, 90 | UPDATE_MESSAGE, 91 | AUTOCOMPLETE_RESULT, 92 | CREATE_MODAL 93 | } 94 | 95 | command_type = iota1{ 96 | boundary(APP_COMMAND, CHAT), 97 | USER_CONTEXT, 98 | boundary(CONTEXT_COMMAND, MESSAGE_CONTEXT), 99 | } 100 | 101 | command_option_type = iota1{ 102 | SUB_COMMAND, 103 | boundary(SUB_COMMANDS, SUB_COMMAND_GROUP), 104 | STRING, 105 | INTEGER, 106 | BOOLEAN, 107 | USER, 108 | CHANNEL, 109 | ROLE, 110 | MENTIONABLE, 111 | NUMBER, 112 | ATTACHMENT, 113 | } 114 | 115 | component_type = iota1{ 116 | ACTION_ROW, 117 | BUTTON, 118 | SELECT_MENU, 119 | TEXT_BOX 120 | } 121 | 122 | button_style = iota1{ 123 | PRIMARY, 124 | SECONDARY, 125 | SUCCESS, 126 | boundary(INTERACTIVE, DANGER), 127 | LINK, 128 | } 129 | 130 | textbox_style = iota1{ 131 | SHORT, 132 | PARAGRAPH 133 | } 134 | 135 | return _ENV 136 | 137 | -------------------------------------------------------------------------------- /lua/lacord/models/message.lua: -------------------------------------------------------------------------------- 1 | local err = error 2 | local getm = getmetatable 3 | local iter = pairs 4 | local iiter = ipairs 5 | local to_s = tostring 6 | local typ = type 7 | 8 | local constants = require"lacord.const" 9 | local encodeURI = require"http.util".encodeURIComponent 10 | local endswith = require"lacord.util".endswith 11 | local map_bang = require"lacord.util".map_bang 12 | local new_promise = require"cqueues.promise".new 13 | local prefix = require"lacord.util".prefix 14 | 15 | local context = require"lacord.models.context" 16 | local methods = require"lacord.models.methods" 17 | local numbers = require"lacord.models.magic-numbers" 18 | 19 | 20 | local channel = require"lacord.models.channel" 21 | 22 | local getapi = context.api 23 | local create = context.create 24 | local request = context.request 25 | 26 | local model_id = methods.model_id 27 | local resolve = methods.resolve 28 | 29 | local link_endpoint = constants.api.base_endpoint .. "/channels" 30 | 31 | local SUPPRESS_EMBEDS = numbers.message_flags.SUPPRESS_EMBEDS 32 | local TIMEOUTS = constants.models.timeouts 33 | 34 | --luacheck: ignore 111 35 | 36 | local function is_emoji(emoji) 37 | local mt = getm(emoji) 38 | return typ(emoji) == "string" or (mt and mt.__lacord_emoji) 39 | end 40 | 41 | local _ENV = {} 42 | 43 | function fetch(c, m) 44 | return channel.message(c, m) 45 | end 46 | 47 | local function edit_by_id_inner(c, m, edit, files) 48 | local api = getapi() 49 | 50 | local success, data, e = api:edit_message(c, m, edit, files) 51 | 52 | if success then 53 | return create('message', data) 54 | else 55 | return nil, e 56 | end 57 | end 58 | 59 | function edit(m, ...) 60 | local msg = resolve(m, 'message') 61 | 62 | return edit_by_id_inner(msg.channel_id, msg.id, ...) 63 | end 64 | 65 | function edit_by_id(c, m, ...) 66 | c = model_id(c, 'message') 67 | m = model_id(m, 'message') 68 | 69 | return edit_by_id_inner(c, m, ...) 70 | end 71 | 72 | 73 | function react_by_id(c, m, reaction) 74 | c = model_id(c, 'channel') 75 | m = model_id(m, 'message') 76 | reaction = to_s(reaction) 77 | 78 | local api = getapi() 79 | 80 | local success, s, e = api:create_reaction(c, m, encodeURI(reaction)) 81 | 82 | if success then 83 | return s 84 | else 85 | return nil, e 86 | end 87 | end 88 | 89 | function unreact_by_id(c, m, reaction) 90 | c = model_id(c, 'channel') 91 | m = model_id(m, 'message') 92 | reaction = to_s(reaction) 93 | 94 | local api = getapi() 95 | 96 | local success, s, e = api:delete_own_reaction(c, m, encodeURI(reaction)) 97 | 98 | if success then 99 | return s 100 | else 101 | return nil, e 102 | end 103 | end 104 | 105 | function remove_reaction_by_id(c, m, reaction, u) 106 | c = model_id(c, 'channel') 107 | m = model_id(m, 'message') 108 | u = model_id(u, 'user') 109 | reaction = to_s(reaction) 110 | 111 | local api = getapi() 112 | 113 | local success, s, e = api:delete_user_reaction(c, m, encodeURI(reaction), u) 114 | 115 | if success then 116 | return s 117 | else 118 | return nil, e 119 | end 120 | end 121 | 122 | local function swap(a, b, f) 123 | return f(b, a) 124 | end 125 | 126 | function reacting_users_by_id(c, m, reaction) 127 | c = model_id(c, 'channel') 128 | m = model_id(m, 'message') 129 | reaction = to_s(reaction) 130 | 131 | local api = getapi() 132 | 133 | local success, users, e = api:get_reactions(c, m, encodeURI(reaction)) 134 | 135 | if success then 136 | return map_bang(swap, users, 'user', create) 137 | else 138 | return nil, e 139 | end 140 | end 141 | 142 | local function remove_single_reaction_by_id(c, m, reaction, errr) 143 | local success, s, e = getapi():delete_reactions(c, m, reaction) 144 | if success then 145 | return s 146 | else 147 | return errr(e) 148 | end 149 | end 150 | 151 | local function soft_err(e) return nil, e end 152 | 153 | function remove_reactions_by_id(c, m, reactions) 154 | c = model_id(c, 'channel') 155 | m = model_id(m, 'message') 156 | if not reactions then 157 | local api = getapi() 158 | local success, s, e = api:delete_all_reactions(c, m) 159 | 160 | if success then 161 | return s 162 | else 163 | return nil, e 164 | end 165 | elseif is_emoji(reactions) then 166 | return remove_single_reaction_by_id(c, m, encodeURI(to_s(reactions)), soft_err) 167 | elseif typ(reactions) == "table" then 168 | if #reactions == 1 then 169 | return remove_single_reaction_by_id(c, m, encodeURI(to_s(reactions[1])), soft_err) 170 | else 171 | local promises = {} 172 | for i , reaction in iiter(reactions) do 173 | promises[i] = new_promise(remove_single_reaction_by_id, c, m, encodeURI(to_s(reaction)), err) 174 | end 175 | local timeo = TIMEOUTS.remove_reactions 176 | if timeo then 177 | for i, p in iiter(promises) do 178 | if p:wait(timeo) then 179 | promises[i] = p:status() == "fulfilled" 180 | end 181 | end 182 | else 183 | for i, p in iiter(promises) do 184 | if p:wait() then 185 | promises[i] = p:status() == "fulfilled" 186 | end 187 | end 188 | end 189 | return promises 190 | end 191 | end 192 | end 193 | 194 | function remove_embeds_by_id(c, m, flgs) 195 | return _ENV.edit_by_id(c, m, { 196 | flags = flgs | SUPPRESS_EMBEDS 197 | }) 198 | end 199 | 200 | function show_embeds_by_id(c, m, flgs) 201 | return _ENV.edit_by_id(c, m, { 202 | flags = flgs & ~SUPPRESS_EMBEDS 203 | }) 204 | end 205 | 206 | function remove_embeds(m) 207 | m = resolve(m, 'message') 208 | return _ENV.remove_embeds_by_id(m.channel_id, m.id, m.flags or 0) 209 | end 210 | 211 | function show_embeds(m) 212 | m = resolve(m, 'message') 213 | return _ENV.show_embeds_by_id(m.channel_id, m.id, m.flags or 0) 214 | end 215 | 216 | function pin_by_id(c, m) 217 | local api = getapi() 218 | 219 | local success, data, e = api:add_pinned_channel_message(c, m) 220 | 221 | if success then 222 | return data 223 | else 224 | return nil, e 225 | end 226 | end 227 | 228 | function unpin_by_id(c, m) 229 | local api = getapi() 230 | 231 | local success, data, e = api:delete_pinned_channel_message(c, m) 232 | 233 | if success then 234 | return data 235 | else 236 | return nil, e 237 | end 238 | end 239 | 240 | local new = {} 241 | for name, definition in iter(_ENV) do 242 | if endswith(name, '_by_id') then 243 | local pfx = prefix(name, '_by_id') 244 | if not _ENV[pfx] then 245 | new[pfx] = function(m, ...) 246 | m = resolve(m, 'message') 247 | return definition(m.channel_id, m.id, ...) 248 | end 249 | end 250 | end 251 | end 252 | 253 | for k , v in iter(new) do _ENV[k] = v end 254 | 255 | function link(m) 256 | m = resolve(m, 'message') 257 | local container = m.guild_id 258 | 259 | if not container then 260 | local c = channel.fetch(m.channel_id) 261 | if c and c.type == 1 then container = "@me" end 262 | end 263 | if container then 264 | return link_endpoint.."/"..container.."/"..m.channel_id.."/"..m.id 265 | else 266 | return link_endpoint.."/"..m.channel_id.."/"..m.id 267 | end 268 | end 269 | 270 | function _ENV.channel(m) 271 | m = resolve(m, 'message') 272 | return request('channel', m.channel_id) 273 | end 274 | 275 | function _ENV.guild(m) 276 | m = resolve(m, 'message') 277 | if m.guild_id then 278 | return request('guild', m.guild_id) 279 | end 280 | end 281 | 282 | _ENV.send = methods.send 283 | 284 | return _ENV -------------------------------------------------------------------------------- /lua/lacord/models/methods.lua: -------------------------------------------------------------------------------- 1 | local err = error 2 | local getm = getmetatable 3 | local to_s = tostring 4 | local typ = type 5 | 6 | local getapi = require"lacord.models.context".api 7 | 8 | 9 | local _ENV = {} 10 | 11 | function mention(object) 12 | local mt = getm(object) 13 | 14 | if mt and mt.__lacord_model_mention then 15 | return mt.__lacord_model_mention(object) 16 | elseif mt then 17 | return mt.__lacord_model .. ": " .. mt.__lacord_model_id(object) 18 | else 19 | return to_s(object) 20 | end 21 | end 22 | 23 | local function send(object, msg, files, ctx, loop) 24 | local mt = getm(object) 25 | if mt then 26 | local def = mt.__lacord_model_defer 27 | if mt.__lacord_model_send then 28 | local api = getapi(ctx, loop) 29 | if typ(msg) == 'string' then msg = {content = msg} end 30 | return mt.__lacord_model_send(object, api, msg, files) 31 | elseif typ(def) == 'string' then 32 | return send(mt[def](object), msg, files, ctx, loop) 33 | elseif def and def.send then 34 | return send(mt[def.send](object), msg, files, ctx, loop) 35 | end 36 | end 37 | return err("Cannot send messages to a "..to_s(object)..".") 38 | end 39 | 40 | _ENV.send = send 41 | 42 | local function model_id_rec(mt, object, t, ...) 43 | if mt.__lacord_model == t then 44 | return mt.__lacord_model_id(object), t 45 | elseif ... then 46 | return model_id_rec(mt, object, ...) 47 | else 48 | return false, t 49 | end 50 | end 51 | 52 | local function model_id(object, ...) 53 | local t = typ(object) 54 | if t == 'string' then 55 | return object 56 | elseif t == 'number' then 57 | return ("%u"):format(object) 58 | end 59 | 60 | local mt = getm(object) 61 | if ... then 62 | if mt and mt.__lacord_model then 63 | local def = mt.__lacord_model_defer 64 | local tyid, what = model_id_rec(mt, object, ...) 65 | if tyid then return tyid 66 | elseif typ(def) == 'string' then 67 | return model_id(mt[def](object), ...) 68 | elseif def and def.id then 69 | return model_id(mt[def.id](object), ...) 70 | else 71 | return err("lacord.models: Object "..to_s(object).." was not the right kind of model. Expecting: "..what..".") 72 | end 73 | end 74 | elseif mt and mt.__lacord_model_id then 75 | return mt.__lacord_model_id(object) 76 | end 77 | return err("lacord.models: Object "..to_s(object).." does not implement the model protocol.") 78 | end 79 | 80 | _ENV.model_id = model_id 81 | 82 | local function update(object, edit, ctx, loop) 83 | local mt = getm(object) 84 | if mt then 85 | local def = mt.__lacord_model_defer 86 | if mt.__lacord_model_update then 87 | local api = getapi(ctx, loop) 88 | return mt.__lacord_model_update(object, api, edit) 89 | elseif typ(def) == 'string' then 90 | return update(mt[def](object), edit, ctx, loop) 91 | elseif def and def.update then 92 | return update(mt[def.update](object), edit, ctx, loop) 93 | end 94 | end 95 | return err("Cannot update "..to_s(object)..".") 96 | end 97 | 98 | local function delete(object, ctx, loop) 99 | local mt = getm(object) 100 | if mt and mt.__lacord_model_delete then 101 | local api = getapi(ctx, loop) 102 | return mt.__lacord_model_delete(object, api) 103 | end 104 | return err("Cannot delete "..to_s(object)..".") 105 | end 106 | 107 | local function resolve_rec(object, mt, t, ...) 108 | 109 | if mt.__lacord_model == t and not mt.__lacord_model_partial then 110 | return object, t 111 | else 112 | local creator = '__lacord_'..t 113 | if mt[creator] then return mt[creator](object), t 114 | elseif ... then return resolve_rec(object, mt, ...) 115 | end 116 | end 117 | end 118 | 119 | function resolve(object, ...) 120 | if ... then 121 | local mt = getm(object) 122 | if mt then 123 | return resolve_rec(object, mt, ...) 124 | end 125 | else 126 | local mt = getm(object) 127 | return mt and not mt.__lacord_model_partial and mt.__lacord_model and object 128 | end 129 | end 130 | 131 | _ENV.update = update 132 | _ENV.delete = delete 133 | 134 | return _ENV -------------------------------------------------------------------------------- /lua/lacord/models/user.lua: -------------------------------------------------------------------------------- 1 | local err = error 2 | local to_n = tostring 3 | 4 | local user_avatar_url = require"lacord.cdn" .user_avatar_url 5 | local default_user_avatar_url = require"lacord.cdn" .default_user_avatar_url 6 | local default_avatars = require"lacord.const".default_avatars 7 | 8 | local context = require"lacord.models.context" 9 | local methods = require"lacord.models.methods" 10 | local null = require"lacord.util.json".null 11 | 12 | local getapi = context.api 13 | local getctx = context.get 14 | local request = context.request 15 | local store = context.store 16 | local create = context.create 17 | local property = context.property 18 | 19 | local send = methods.send 20 | local resolve = methods.resolve 21 | local model_id = methods.model_id 22 | 23 | --luacheck: ignore 111 24 | 25 | local _ENV = {} 26 | 27 | function fetch(u) 28 | u = model_id(u, 'user') 29 | local usr 30 | local api = getapi() 31 | local success, data, e = api:get_user(u) 32 | if success then 33 | usr = create('user', data) 34 | else 35 | return err( 36 | "lacord.models.user: Unable to resolve user id to user." 37 | .."\n "..e 38 | ) 39 | end 40 | return usr or err"lacord.models.user: Unable to resolve user id to user." 41 | end 42 | 43 | function avatar_url(u, ...) 44 | u = resolve(u, 'user') 45 | if u.avatar ~= null then 46 | return user_avatar_url(u.id, u.avatar, ...) 47 | else 48 | return default_user_avatar_url(u.id, to_n(u.discriminator) % default_avatars, ...) 49 | end 50 | end 51 | 52 | local function private_channel(u) 53 | local the_id = model_id(u, 'user') 54 | local ctx, loop = getctx() 55 | 56 | local chl_id = property(ctx, 'dms', the_id) 57 | 58 | local chl = chl_id and request(ctx, 'channel', chl_id) 59 | 60 | if not chl then 61 | local api = getapi(ctx, loop) 62 | local success, data, e = api:create_dm{recipient_id = the_id} 63 | if success then 64 | chl = create('channel', data) 65 | property(ctx, 'dms', the_id, chl.id) 66 | else 67 | return nil, e 68 | end 69 | end 70 | 71 | return chl 72 | end 73 | 74 | _ENV.private_channel = private_channel 75 | 76 | function dm(u, ...) 77 | return send(private_channel(u), ...) 78 | end 79 | 80 | function tag(u) 81 | u = resolve(u, 'user') 82 | return u.username .. "#" .. u.discriminator 83 | end 84 | 85 | return _ENV 86 | -------------------------------------------------------------------------------- /lua/lacord/outgoing-webhook-server-2.lua: -------------------------------------------------------------------------------- 1 | local asserts = assert 2 | local iiter = ipairs 3 | local iter = pairs 4 | local try = pcall 5 | local setm = setmetatable 6 | local to_n = tonumber 7 | local to_s = tostring 8 | local typ = type 9 | 10 | local insert = table.insert 11 | local unpak = table.unpack 12 | 13 | local char = string.char 14 | 15 | local const = require"lacord.const" 16 | local shs = require"lacord.ext.shs" 17 | local util = require"lacord.util" 18 | local json = require"lacord.util.json" 19 | 20 | local sign_open = require"luatweetnacl".sign_open 21 | 22 | local default_server = "lacord " .. const.version 23 | 24 | local decode = json.decode 25 | 26 | local content_typed = util.content_typed 27 | local JSON = util.content_types.JSON 28 | local TEXT = util.content_types.TEXT 29 | 30 | --luacheck: ignore 111 31 | local _ENV = {} 32 | 33 | -- decode a hexadecimal string into bytes 34 | local function decode_hex(str) 35 | local bytes = {} 36 | for i = 1, #str, 2 do 37 | insert(bytes, to_n(str:sub(i, i + 1), 16)) 38 | end 39 | return char(unpak(bytes)) 40 | end 41 | 42 | local PING_ACK = json.encode{ 43 | type = 1 44 | } 45 | 46 | local function generic_handler(R) 47 | if R.method == "POST" then 48 | local verified = false 49 | local raw = R:get_body() 50 | 51 | local sig, timestamp = 52 | R.request_headers:get"x-signature-ed25519", 53 | R.request_headers:get"x-signature-timestamp" 54 | 55 | if sig ~= "" and timestamp ~= "" then 56 | verified = sign_open(decode_hex(sig) .. timestamp .. raw, R.data.key) ~= nil 57 | end 58 | 59 | if verified then 60 | local payload = decode(raw) 61 | if not payload then return R:set_code_and_reply(400, "Payload was not json.") 62 | else 63 | if payload.type == 1 then 64 | return R:set_ok_and_reply(PING_ACK, JSON) 65 | else 66 | return R.data.inner(R, payload) 67 | end 68 | end 69 | else 70 | return R:set_401() 71 | end 72 | 73 | else 74 | return R:set_code_and_reply(404, "Not found!", TEXT) 75 | end 76 | end 77 | 78 | local mt = {} for k, v in iter(shs.response_mt) do mt[k] = v end 79 | 80 | mt.__index = mt 81 | 82 | do 83 | local inner = shs.response_mt.set_body 84 | function mt:set_body(content) 85 | local data,ct = content_typed(content) 86 | if ct then self.headers:upsert('content-type', ct) end 87 | return inner(data) 88 | end 89 | end 90 | 91 | function new(options, ...) 92 | local data = {key = asserts(options.public_key, 93 | "Please provide your application's public key for signature verfication.")} 94 | 95 | local routes = options.routes or {} 96 | 97 | local discordpath = options.route or '/' 98 | 99 | data.inner = routes[discordpath] or assert(options.interact, 100 | "Please provide an event handler to receive interactions from.") 101 | 102 | routes['*'] = routes['*'] or options.fallthrough 103 | 104 | routes[discordpath] = generic_handler 105 | 106 | 107 | return shs.new({ 108 | routes = routes, 109 | host = options.host, 110 | port = options.port, 111 | server = options.server or default_server, 112 | ctx = options.ctx, 113 | data = data, 114 | response_mt = mt, 115 | }, ...) 116 | end 117 | 118 | return _ENV -------------------------------------------------------------------------------- /lua/lacord/ui/button.lua: -------------------------------------------------------------------------------- 1 | local setm = setmetatable 2 | local type = type 3 | 4 | local numbers = require"lacord.models.magic-numbers" 5 | local model_id = require"lacord.models.methods".model_id 6 | local run_methods = require"lacord.util".run_methods 7 | local common_methods = require"lacord.ui.common" 8 | 9 | local types = numbers.component_type 10 | local styles = numbers.button_style 11 | local style_names = numbers.button_style_names 12 | 13 | local _ENV = {} 14 | 15 | 16 | local button = {__name = "lacord.ui.button"} 17 | button.__index = button 18 | 19 | 20 | function new(t) 21 | local self = setm({_style = styles.SECONDARY}, button) 22 | if t then run_methods(self, t) end 23 | return self 24 | end 25 | 26 | 27 | function button:style(s) 28 | if type(s) == 'function' then 29 | self._style = s 30 | else 31 | if self._style == styles.LINK then return self end 32 | s = style_names[s] and s or styles[s] 33 | if s < styles.INTERACTIVE then 34 | self._style = s 35 | end 36 | end 37 | return self 38 | end 39 | 40 | 41 | function button:label(l) 42 | self._label = l 43 | return self 44 | end 45 | 46 | 47 | function button:emoji(name, id, animated) 48 | if type(name) == 'function' then 49 | self._emoji = name 50 | else 51 | self._emoji = { 52 | name = name, 53 | id = id and model_id(id, 'emoji'), 54 | animated = (not not animated) or nil 55 | } 56 | end 57 | return self 58 | end 59 | 60 | 61 | function button:__lacord_ui(...) 62 | return { 63 | type = types.BUTTON, 64 | custom_id = self:run('_custom_id', ...), 65 | style = self:run('_style', ...), 66 | label = self:run('_label', ...), 67 | emoji = self:run('_emoji', ...), 68 | disabled = self:run('_disabled', ...), 69 | url = self:run('_url', ...) 70 | } 71 | end 72 | 73 | 74 | common_methods(button, {'_style', '_label', '_emoji'}) 75 | 76 | 77 | function hyperlink(url, t) 78 | local self = setm({_url = url, _style = styles.LINK}, button) 79 | 80 | if t then run_methods(self, t) end 81 | 82 | return self 83 | end 84 | 85 | 86 | return _ENV -------------------------------------------------------------------------------- /lua/lacord/ui/common.lua: -------------------------------------------------------------------------------- 1 | local iiter = ipairs 2 | local setm = setmetatable 3 | 4 | local run_methods = require"lacord.util".run_methods 5 | 6 | 7 | return function (component, fields, f, ignore) 8 | 9 | function component:id(id) 10 | self._custom_id = id 11 | return self 12 | end 13 | 14 | function component:upsert_id(id) 15 | if not self._custom_id then 16 | self._custom_id = id 17 | end 18 | return self 19 | end 20 | 21 | function component:run(field, ...) 22 | if type(self[field]) == 'function' then 23 | return self[field](self, ...) 24 | end 25 | return self[field] 26 | end 27 | 28 | if not ignore then 29 | function component:disable() 30 | self._disabled = true 31 | return self 32 | end 33 | 34 | 35 | function component:enable() 36 | self._disabled = nil 37 | return self 38 | end 39 | end 40 | 41 | function component:clone(tt) 42 | local t = {} 43 | for key in iiter(fields) do 44 | t[key] = self[key] 45 | end 46 | t._custom_id = self._custom_id 47 | t._disabled = self._disabled 48 | t._name = self._name 49 | 50 | local new = setm(t, component) 51 | if tt then run_methods(new, tt) end 52 | return f and f(new, self) or new 53 | end 54 | end -------------------------------------------------------------------------------- /lua/lacord/ui/init.lua: -------------------------------------------------------------------------------- 1 | local getm = getmetatable 2 | local iter = pairs 3 | local iiter = ipairs 4 | local set = rawset 5 | local setm = setmetatable 6 | local type = type 7 | 8 | local running_coro = coroutine.running 9 | 10 | local insert = table.insert 11 | 12 | local dflt_env = _ENV 13 | 14 | local cqueues = require"cqueues" 15 | local context = require"lacord.models.context" 16 | local interaction = require"lacord.models.interaction" 17 | local numbers = require"lacord.models.magic-numbers" 18 | local resolve = require"lacord.models.methods".resolve 19 | local run_methods = require"lacord.util".run_methods 20 | 21 | local button = require"lacord.ui.button" 22 | local selects = require"lacord.ui.select-menu" 23 | local textbox = require"lacord.ui.text-box" 24 | 25 | local monotime = cqueues.monotime 26 | local running = cqueues.running 27 | local sleep = cqueues.sleep 28 | 29 | local types = numbers.component_type 30 | 31 | 32 | local _ENV = {} 33 | 34 | 35 | local interface = {__name = "lacord.ui"} 36 | interface.__index = interface 37 | 38 | 39 | function new(t) 40 | local self = setm({_rows = {}, _actions = {}}, interface) 41 | 42 | if t then run_methods(self, t) end 43 | 44 | return self 45 | end 46 | 47 | 48 | local function interface_cid(self, instance) 49 | return (instance._id .. "." ..(self._name or ("%p"):format(self):sub(2))):sub(1, 100) 50 | end 51 | 52 | function interface:add(item, ...) 53 | if getm(item).__lacord_ui then ::top:: 54 | if not self._current_row then 55 | local len = #self._rows 56 | if len < 5 then 57 | self._current_row = #self._rows + 1 58 | insert(self._rows, { item:upsert_id(interface_cid) }) 59 | end 60 | else 61 | if #self._rows[self._current_row] < 25 then 62 | insert(self._rows[self._current_row], item:upsert_id(interface_cid)) 63 | else 64 | self._current_row = nil 65 | goto top 66 | end 67 | end 68 | end 69 | if ... then return self:add(...) end 70 | return self 71 | end 72 | 73 | 74 | function interface:id(i) self._id = i return self end 75 | 76 | 77 | function interface:add_row(...) 78 | local len = #self._rows 79 | if len < 5 then 80 | self._current_row = #self._rows + 1 81 | insert(self._rows, {}) 82 | if ... then 83 | return self:add(...) 84 | end 85 | end 86 | end 87 | 88 | 89 | function interface:using_row(i, ...) 90 | local len = #self._rows 91 | if len < i then 92 | return self:add_row(...) 93 | else 94 | self._current_row = i 95 | if ... then 96 | return self:add(...) 97 | end 98 | end 99 | end 100 | 101 | 102 | function interface:add_action(name, fn) 103 | self._actions[name] = fn 104 | end 105 | 106 | 107 | local function interface_timeout(id, age) ::top:: 108 | sleep(age - monotime()) 109 | local ui = context.property('ui', id) 110 | age = ui and ui._age 111 | if ui and age > monotime() then 112 | goto top 113 | end 114 | 115 | ui = context.property('ui', id, context.DEL) or ui 116 | 117 | if ui._interface._actions.timeout then 118 | _ENV.in_environment(ui) 119 | ui._interface._actions.timeout(ui) 120 | end 121 | end 122 | 123 | 124 | local instance = {__name = "lacord.ui.live"} 125 | instance.__index = instance 126 | 127 | 128 | function instance:__tostring() 129 | return ("%s: %s"):format(getm(self).__name, self._id) 130 | end 131 | 132 | 133 | function instance:restore(I) 134 | return interaction.send(I, {components = self._components}) 135 | end 136 | 137 | 138 | function instance:store(K, V) 139 | self._state[K] = V 140 | end 141 | 142 | 143 | function instance:close(I) 144 | if I then I:clear() end 145 | return context.property('ui', self._id, context.DEL) 146 | end 147 | 148 | instance.stop = instance.close 149 | 150 | 151 | function interface:new_state() 152 | local out = { } 153 | for k, v in iter(self._initial_state) do 154 | out[k] = v 155 | end 156 | return out 157 | end 158 | 159 | 160 | function interface:instance(payload, timeout, target_id, ...) 161 | local this = { 162 | _age = timeout or self._age or false, 163 | _interface = self, 164 | _actions={}, 165 | _state=self:new_state(), 166 | _id = "", 167 | _target = type(target_id) ~= 'function' and target_id, 168 | _filter = type(target_id) == 'function' and target_id, 169 | } 170 | 171 | local id = self._id or ("%p"):format(this):sub(2) 172 | this._id = id 173 | 174 | setm(this, instance) 175 | if payload then 176 | local components = {} 177 | for i = 1, #self._rows do 178 | local row = self._rows[i] 179 | local row_out = {} 180 | for j = 1, #row do 181 | local json = getm(row[j]).__lacord_ui(row[j], this, ...) 182 | if self._actions[row[j]._name] then 183 | this._actions[json.custom_id] 184 | = row[j]._name 185 | end 186 | row_out[j] = json 187 | end 188 | components[i] = {type = types.ACTION_ROW, components = row_out} 189 | end 190 | 191 | self._components = components 192 | 193 | if payload.data then 194 | payload.data.components = components 195 | else 196 | payload.components = components 197 | end 198 | end 199 | 200 | context.property('ui', id, this) 201 | 202 | if this._age then 203 | running():wrap(interface_timeout, this._id, this._age) 204 | end 205 | 206 | return this 207 | end 208 | 209 | 210 | function interface:attach(I, timeout, msg, files, ephemeral) 211 | local payload = (type(msg) == 'string' and {content = msg}) or msg or {} 212 | 213 | if self._actions.init then 214 | local out = self._actions.init(timeout, payload, files, ephemeral) 215 | if out then 216 | timeout = out.timeout or timeout 217 | payload = out.payload or payload 218 | files = out.files or files 219 | ephemeral = out.ephemeral or ephemeral 220 | end 221 | end 222 | 223 | local target = resolve(I, 'user') 224 | self:instance(payload, timeout, target.id, I, target) 225 | 226 | return interaction[ephemeral and 'whisper' or 'reply'](I, payload, files) 227 | end 228 | 229 | 230 | function interface:attach_filter(I, timeout, filter, msg, files, ephemeral) 231 | local payload = (type(msg) == 'string' and {content = msg}) or msg or {} 232 | 233 | if self._actions.init then 234 | local out = self._actions.init(timeout, payload, files, ephemeral) 235 | timeout = out.timeout or timeout 236 | payload = out.payload or payload 237 | files = out.files or files 238 | ephemeral = out.ephemeral or ephemeral 239 | end 240 | 241 | local target = resolve(I, 'user') 242 | self:instance(payload, timeout, filter, I, target) 243 | 244 | return interaction[ephemeral and 'whisper' or 'reply'](I, payload, files) 245 | end 246 | 247 | 248 | function interface:attach_quietly(I, timeout, msg, files) 249 | return self:attach(I, timeout, msg, files, true) 250 | end 251 | 252 | 253 | function interface:attach_filter_quietly(I, timeout, msg, files) 254 | return self:attach_filter(I, timeout, msg, files, true) 255 | end 256 | 257 | 258 | local INTERFACE = {} 259 | local INSTANCE = {} 260 | local KEEP = {} 261 | 262 | 263 | local ctors = { 264 | button = button.new, 265 | select = selects.new, 266 | textbox = textbox.new 267 | } 268 | 269 | 270 | local env2 = {__name = "lacord.ui.state"} 271 | 272 | function env2:__index(K) 273 | return self[INSTANCE][running_coro()][K] or dflt_env[K] 274 | end 275 | 276 | function env2:__newindex(K, V) 277 | self[INSTANCE][running_coro()][K] = V 278 | end 279 | 280 | 281 | local view_helpers = {} 282 | 283 | function view_helpers.row(self, _,_, ccomps) return function(i) 284 | if i then 285 | self[INTERFACE]:using_row(i) 286 | else 287 | self[INTERFACE]:add_row() 288 | end 289 | return ccomps 290 | end end 291 | 292 | function view_helpers.using(self, get_stack, set_stack) return function(i) 293 | local stack = get_stack() 294 | if stack then 295 | for _,comp in iiter(stack) do 296 | comp._name = i 297 | self[INTERFACE]:add(comp) 298 | end 299 | set_stack(nil) 300 | end 301 | end end 302 | 303 | 304 | local function view_ctor(interface_id) 305 | local env = {__name = "lacord.ui.environment"} 306 | local stack 307 | local function set_stack(s) stack = s end 308 | local function get_stack() return stack end 309 | local comps, ccomps = {}, {} 310 | 311 | for name, ctor in iter(ctors) do 312 | local function the_ctor(t) 313 | if stack then insert(stack, ctor(t)) 314 | else 315 | stack = {ctor(t)} 316 | end 317 | end 318 | comps[name] = the_ctor 319 | ccomps[name] = function(self, t) 320 | if stack then 321 | for _,comp in iiter(stack) do 322 | self[INTERFACE]:add(comp) 323 | end 324 | stack = nil 325 | end 326 | stack = {ctor(t)} 327 | end 328 | end 329 | 330 | 331 | function env:__newindex(key, value) 332 | if type(value) == 'function' then 333 | if stack then 334 | self[INTERFACE]:add_action(key, value) 335 | if key ~= 'init' then 336 | local last 337 | for _,comp in iiter(stack) do 338 | comp._name = key 339 | last = comp 340 | self[INTERFACE]:add(comp) 341 | end 342 | set(self, key, last) 343 | stack = nil 344 | end 345 | else 346 | self[INTERFACE]:add_action(key, value) 347 | end 348 | else 349 | local mt = getm(value) 350 | if mt and mt.__lacord_state_wrap then 351 | value = value[1] 352 | self[KEEP][key] = true 353 | end 354 | set(self, key, value) 355 | end 356 | end 357 | 358 | 359 | function env:__index(k) 360 | if k == 'interface' then 361 | local out = self[INTERFACE] 362 | self[INTERFACE] = nil 363 | 364 | local keep = self[KEEP] 365 | self[KEEP] = nil 366 | 367 | local copy = {} 368 | for k_ , v in iter(self) do 369 | self[k_] = nil 370 | local mt = getm(v) 371 | if mt and mt.__lacord_ui and not keep[k_] then goto continue end 372 | copy[k_] = v 373 | ::continue:: 374 | end 375 | 376 | out._initial_state = copy 377 | 378 | self[INSTANCE] = setm({}, {__mode = 'k'}) 379 | 380 | out._environment = self 381 | 382 | setm(self, env2) 383 | 384 | return out 385 | elseif view_helpers[k] then 386 | return view_helpers[k](self, get_stack, set_stack, ccomps) 387 | else 388 | return self[INTERFACE]._actions[k] or comps[k] or dflt_env[k] 389 | end 390 | end 391 | 392 | 393 | local out = setm({[KEEP] = {}}, env) 394 | 395 | out[INTERFACE] = new{id = interface_id} 396 | 397 | return out 398 | end 399 | 400 | 401 | local viewf = {ignore_t = {__lacord_state_wrap = true}} 402 | 403 | function viewf:__call(...) return view_ctor(...) end 404 | 405 | function viewf.ignore(v) 406 | return setm({v}, viewf.ignore_t) 407 | end 408 | 409 | _ENV.view = setm(viewf, viewf) 410 | 411 | 412 | function in_environment(inst) 413 | if inst._interface._environment then 414 | inst._interface._environment[INSTANCE][running_coro()] = inst._state 415 | end 416 | end 417 | 418 | return _ENV -------------------------------------------------------------------------------- /lua/lacord/ui/select-menu.lua: -------------------------------------------------------------------------------- 1 | local setm = setmetatable 2 | local to_s = tostring 3 | local type = type 4 | 5 | local min = math.min 6 | local max = math.max 7 | 8 | local insert = table.insert 9 | 10 | local numbers = require"lacord.models.magic-numbers" 11 | local map = require"lacord.util".map 12 | local model_id = require"lacord.models.methods".model_id 13 | local run_methods = require"lacord.util".run_methods 14 | local common_methods = require"lacord.ui.common" 15 | 16 | local types = numbers.component_type 17 | 18 | 19 | local _ENV = {} 20 | 21 | 22 | local select = {__name = "lacord.ui.select-menu"} 23 | select.__index = select 24 | 25 | 26 | function new(t) 27 | local self = setm({_selections = {}, _n = 0}, select) 28 | if t then run_methods(self, t) end 29 | return self 30 | end 31 | 32 | 33 | function select:placeholder(str) 34 | self._placeholder = str 35 | return self 36 | end 37 | 38 | 39 | function select:min(n) 40 | self._min = min(max(n, 0), 25) 41 | return self 42 | end 43 | 44 | 45 | function select:max(n) 46 | self._max = min(max(n, 1), 25) 47 | return self 48 | end 49 | 50 | 51 | local selection = {__name = "lacord.ui.selection"} 52 | selection.__index = selection 53 | 54 | 55 | function select:selection(t) 56 | if self._n < 25 then 57 | local sel = setm({}, selection) 58 | if t then run_methods(sel, t) end 59 | self._n = self._n + 1 60 | insert(self._selections, sel) 61 | return sel 62 | end 63 | end 64 | 65 | 66 | function selection:label(l) 67 | self._label = l 68 | return self 69 | end 70 | 71 | 72 | function selection:value(v) 73 | self._value = to_s(v) 74 | return self 75 | end 76 | 77 | 78 | function selection:description(s) 79 | self._description = s 80 | return self 81 | end 82 | 83 | 84 | function selection:emoji(name, id, animated) 85 | self._emoji = { 86 | name = name, 87 | id = model_id(id, 'emoji'), 88 | animated = not not animated 89 | } 90 | return self 91 | end 92 | 93 | 94 | function selection:default(v) 95 | self._default = not not v 96 | return self 97 | end 98 | 99 | 100 | function selection:payload(...) 101 | return { 102 | label = self:run('_label', ...), 103 | value = self:run('_value', ...), 104 | description = self:run('_description', ...), 105 | emoji = self:run('_emoji', ...), 106 | default = self:run('_default', ...) 107 | } 108 | end 109 | 110 | common_methods(select, {'_placeholder', '_min', '_max'}, 111 | function(new, old) 112 | for i = 1, #old._selections do 113 | local olds = old._selections[i] 114 | local news = new:selection{ 115 | label = olds._label, 116 | value = olds._value, 117 | description = olds._description, 118 | default = olds._default, 119 | } 120 | if olds._emoji then 121 | if type(olds._emoji) == 'function' then 122 | news:emoji(olds._emoji) 123 | else 124 | news:emoji( 125 | olds._emoji.name, 126 | olds._emoji.id, 127 | olds._emoji.animated) 128 | end 129 | end 130 | end 131 | return new 132 | end) 133 | 134 | 135 | selection.run = select.run 136 | 137 | 138 | function select:__lacord_ui(...) 139 | return { 140 | type = types.SELECT_MENU, 141 | custom_id = self:run('_custom_id', ...), 142 | placeholder = self:run('_placeholder', ...), 143 | min_values = self:run('_min', ...), 144 | max_values = self:run('_max', ...), 145 | disabled = self:run('_disabled', ...), 146 | options = map(selection.payload, self._selections, ...) 147 | } 148 | end 149 | 150 | return _ENV 151 | 152 | -------------------------------------------------------------------------------- /lua/lacord/ui/text-box.lua: -------------------------------------------------------------------------------- 1 | local setm = setmetatable 2 | 3 | local numbers = require"lacord.models.magic-numbers" 4 | local run_methods = require"lacord.util".run_methods 5 | local common_methods = require"lacord.ui.common" 6 | 7 | local types = numbers.component_type 8 | local styles = numbers.textbox_style 9 | local style_names = numbers.textbox_style_names 10 | 11 | 12 | local _ENV = {} 13 | 14 | 15 | local textbox = {__name = "lacord.ui.text-box"} 16 | 17 | function new(t) 18 | local self = setm({}, textbox) 19 | if t then run_methods(self, t) end 20 | return self 21 | end 22 | 23 | 24 | function textbox:style(s) s = style_names[s] and s or styles[s] 25 | self._style = s 26 | return self 27 | end 28 | 29 | 30 | function textbox:label(l) 31 | self._label = l 32 | return self 33 | end 34 | 35 | 36 | function textbox:min(n) 37 | self._min = n 38 | return self 39 | end 40 | 41 | 42 | function textbox:max(n) 43 | self._max = n 44 | return self 45 | end 46 | 47 | 48 | function textbox:required(v) 49 | self._required = not not v 50 | return self 51 | end 52 | 53 | 54 | function textbox:value(v) 55 | self._value = v 56 | return self 57 | end 58 | 59 | 60 | function textbox:placeholder(v) 61 | self._placeholder = v 62 | return self 63 | end 64 | 65 | 66 | function textbox:__lacord_ui(...) 67 | return { 68 | type = types.TEXT_BOX, 69 | custom_id = self:run('_custom_id', ...), 70 | style = self:run('_style', ...), 71 | label = self:run('_label', ...), 72 | min_length = self:run('_min', ...), 73 | max_length = self:run('_max', ...), 74 | required = self:run('_required', ...), 75 | value = self:run('_value', ...), 76 | placeholder = self:run('_placeholder', ...) 77 | } 78 | end 79 | 80 | 81 | function textbox:__lacord_ui_name(name) 82 | self._name = name 83 | return self 84 | end 85 | 86 | common_methods(textbox, {'_style', '_label', '_min', '_max', '_required', '_value', '_placeholder'}, nil, true) 87 | 88 | 89 | return _ENV -------------------------------------------------------------------------------- /lua/lacord/util/cli_auto.lua: -------------------------------------------------------------------------------- 1 | local warn = warn or function()end 2 | 3 | for k in pairs(_G.arg) do 4 | if k < 0 then goto okay end 5 | end 6 | 7 | error"The -lacord module was loaded without being in script mode, you must provide a script file to use this option." 8 | 9 | ::okay:: 10 | 11 | warn"@on" 12 | 13 | require"lacord.cli"(table.unpack(_G.arg, 1)) 14 | 15 | warn"@off" -------------------------------------------------------------------------------- /lua/lacord/util/cli_default.lua: -------------------------------------------------------------------------------- 1 | local iiter = ipairs 2 | local iter = pairs 3 | local loads = load 4 | local setm = setmetatable 5 | local to_n = tonumber 6 | local try = pcall 7 | local typ = type 8 | local warn = warn or function()end 9 | 10 | local openf = io.open 11 | 12 | local getenv = os.getenv 13 | 14 | local pkgloaded= package.loaded 15 | 16 | local char = string.char 17 | 18 | local move = table.move 19 | local pak = table.pack 20 | local unpak = table.unpack 21 | 22 | local expected_args = require"lacord.const".supported_cli_options 23 | local expected_env = require"lacord.const".supported_environment_variables 24 | 25 | local climt = {__name = "lacord.cli"} 26 | 27 | local positives = { 28 | yes = true, 29 | no = false, 30 | y = true, 31 | n = false, 32 | on = true, 33 | off = false, 34 | ['true'] = true, 35 | [true] = true, 36 | [false] = false, 37 | ['false'] = false, 38 | ['1'] = true, 39 | ['0'] = false, 40 | [1] = true, 41 | [0] = false, 42 | } 43 | 44 | local function boolean_environment_variable(value) 45 | if value ~= nil then 46 | if typ(value) == "string" then value = value:lower() end 47 | local flag = positives[value] 48 | return flag, flag ~= nil 49 | end 50 | return nil, false 51 | end 52 | 53 | local function value_environment_variable(value) 54 | return value, value ~= nil 55 | end 56 | 57 | local function enum_environment_variable(enum, value) 58 | if value ~= nil then 59 | if typ(value) == "string" then value = value:lower() end 60 | 61 | for _, v in iiter(enum) do 62 | if v == value then return v, true end 63 | end 64 | end 65 | return nil, false 66 | end 67 | 68 | local function read_environment_variable(cfg, flagname, ...) 69 | if typ(cfg[flagname]) == "table" then return enum_environment_variable(cfg[flagname], ...) 70 | elseif cfg[flagname] == "flag" then return boolean_environment_variable(...) 71 | else return value_environment_variable(...) 72 | end 73 | end 74 | 75 | local function commandline_item(out, rest) 76 | local eval = false ::evaluate:: 77 | local key, expecting 78 | if expected_args[rest] == "flag" then 79 | out[rest] = true 80 | elseif expected_args[rest] == "value" then 81 | key = rest 82 | expecting = true 83 | elseif typ(expected_args[rest]) == "table" then 84 | key = rest 85 | expecting = 1 86 | elseif expected_args[expected_args[rest]] and not eval then 87 | rest = expected_args[rest] 88 | eval = true 89 | goto evaluate 90 | elseif out.accept then 91 | key = rest 92 | expecting = true 93 | end 94 | return key, expecting 95 | end 96 | 97 | local function cli_options_from_table(tbl) 98 | local out = {} 99 | local expecting = false 100 | local key 101 | 102 | for _ , item in iiter(tbl) do 103 | if expecting == 1 then 104 | expecting = false 105 | for _ , option in iiter(expected_args[key]) do 106 | if option == item then 107 | out[key] = item 108 | goto continue 109 | end 110 | end 111 | warn("lacord.util.commandline_args: Unrecognized option passed to --"..key) 112 | elseif expecting then 113 | out[key] = item 114 | expecting = false 115 | else 116 | key, expecting = commandline_item(out, item) 117 | end 118 | ::continue:: 119 | end 120 | 121 | for k, v in iter(tbl) do 122 | if not to_n(k) then 123 | k, expecting = commandline_item(out, k) 124 | if not k then goto continue end 125 | 126 | if expecting == 1 then 127 | out[k] = enum_environment_variable(expected_args[k], v) 128 | elseif expecting then 129 | out[k] = value_environment_variable(v) 130 | else 131 | out[k] = boolean_environment_variable(v) 132 | end 133 | ::continue:: 134 | end 135 | end 136 | return out 137 | end 138 | 139 | 140 | local file_env_mt = {} 141 | 142 | function file_env_mt.__index(_, k) 143 | return k 144 | end 145 | 146 | function file_env_mt.__newindex() end 147 | 148 | local file_env = setm({}, file_env_mt) 149 | 150 | 151 | local function commandline_args(...) 152 | local list = pak(...) 153 | local out = {} 154 | local expecting = false 155 | local key 156 | for i = 1, list.n do 157 | local item = list[i] 158 | local f,s = item:byte(1, 2) -- check for double `-` 159 | if expecting == 1 then 160 | expecting = false 161 | list[i] = nil 162 | for _ , option in iiter(expected_args[key]) do 163 | if option == item then 164 | out[key] = item 165 | goto continue 166 | end 167 | end 168 | warn("lacord.util.commandline_args: Unrecognized option passed to --"..key) 169 | elseif expecting then 170 | -- if we're expecting an argument then set the current argument as the value 171 | if key == "file" then 172 | local file = openf(item, "r") 173 | if file then 174 | local content = file:read"a" 175 | file:close() 176 | if not content then goto file_fail end 177 | 178 | local success, loader, t = try(loads, "return "..content, "lacord.cli.options", "t", file_env) 179 | if not success then goto file_fail end 180 | 181 | success,t = try(loader) 182 | 183 | if not success then goto file_fail end 184 | list[i] = nil 185 | move(list, i+1, list.n, 1) 186 | return cli_options_from_table(t), list 187 | end 188 | ::file_fail:: 189 | warn("lacord.util.commandline_args: Error loading arguments from file "..item) 190 | expecting = false 191 | list[i] = nil 192 | else 193 | out[key] = item 194 | expecting = false 195 | list[i] = nil 196 | end 197 | else -- if this is a new key 198 | if f == 45 and s == 45 then -- if at least a `-` is found 199 | list[i] = nil 200 | key, expecting = commandline_item(out, char(item:byte(3, -1))) -- cut off the -- prefix 201 | elseif f == 45 then 202 | list[i] = nil 203 | local chrs = {item:byte(2, -1)} 204 | for j, c in iiter(chrs) do 205 | key, expecting = commandline_item(out, char(c)) 206 | if expecting then 207 | if j ~= #chrs then warn("lacord.util.commandline_args: shorthands which both admit an argument were used, dropping: ".. char(unpak(chrs, j+1))) end 208 | break 209 | end 210 | end 211 | else 212 | move(list, i, list.n, 1) 213 | return out, list 214 | end 215 | end 216 | ::continue:: 217 | end 218 | return out, list 219 | end 220 | 221 | local function cli_options(...) 222 | local flags, remaining = commandline_args(...) 223 | 224 | for envname, flagname in iter(expected_env) do 225 | local value, was_set = read_environment_variable(expected_args, flagname, getenv(envname)) 226 | if was_set then flags[flagname] = flags[flagname] or value end 227 | end 228 | 229 | 230 | pkgloaded['lacord.cli'] = setm(flags, climt) 231 | 232 | return remaining, flags 233 | end 234 | 235 | 236 | 237 | function climt.__call(_,...) 238 | return cli_options(...) 239 | end 240 | 241 | return setmetatable({}, climt) -------------------------------------------------------------------------------- /lua/lacord/util/intents.lua: -------------------------------------------------------------------------------- 1 | local pairs = pairs 2 | local _ENV = {} 3 | 4 | local intents = { 5 | guilds = 0x0001, 6 | guild_members = 0x0002, 7 | guild_bans = 0x0004, 8 | guild_emojis = 0x0008, 9 | guild_integrations = 0x0010, 10 | guild_webhooks = 0x0020, 11 | guild_invites = 0x0040, 12 | guild_voice_states = 0x0080, 13 | guild_presences = 0x0100, 14 | guild_messages = 0x0200, 15 | guild_message_reactions = 0x0400, 16 | guild_message_typing = 0x0800, 17 | direct_messages = 0x1000, 18 | direct_message_reactions = 0x2000, 19 | direct_message_typing = 0x4000, 20 | message_content = 0x8000, 21 | } 22 | 23 | intents.everything = 0 24 | for _, value in pairs(intents) do 25 | intents.everything = intents.everything | value 26 | end 27 | 28 | intents.message = 0 29 | for name, value in pairs(intents) do 30 | if name:find'message' then 31 | intents.message = intents.message | value 32 | end 33 | end 34 | 35 | intents.guild = 0 36 | for name, value in pairs(intents) do 37 | if name:find'guild' then 38 | intents.guild = intents.guild | value 39 | end 40 | end 41 | 42 | intents.direct = 0 43 | for name, value in pairs(intents) do 44 | if name:find'direct' then 45 | intents.direct = intents.direct | value 46 | end 47 | end 48 | 49 | intents.normal = intents.everything & ~intents.guild_presences & ~intents.guild_voice_states 50 | intents.unprivileged = intents.everything & ~intents.guild_members & ~intents.guild_presences & ~intents.message_content 51 | 52 | return intents -------------------------------------------------------------------------------- /lua/lacord/util/json.lua: -------------------------------------------------------------------------------- 1 | local const = require"lacord.const" 2 | local setm = setmetatable 3 | local req = require 4 | local err = error 5 | local getm = getmetatable 6 | 7 | local REG = debug.getregistry() 8 | 9 | local _ENV = {} 10 | 11 | --luacheck: ignore 111 12 | 13 | local virtual_filenames = setm({}, {__mode = "k"}) 14 | local virtual_descriptions = setm({}, {__mode = "k"}) 15 | 16 | local function virtualname(self) return virtual_filenames[self] end 17 | local function set_virtualname(self, value) virtual_filenames[self] = value end 18 | 19 | local function virtualdescription(self) return virtual_descriptions[self] end 20 | local function set_virtualdescription(self, value) virtual_descriptions[self] = value end 21 | 22 | local function initialize_metatables(ja_mt, jo_mt, enc) 23 | ja_mt.__lacord_content_type = "application/json" 24 | ja_mt.__lacord_payload = enc 25 | ja_mt.__lacord_file_name = virtualname 26 | ja_mt.__lacord_set_file_name = set_virtualname 27 | ja_mt.__lacord_file_description = virtualdescription 28 | ja_mt.__lacord_set_file_description = set_virtualdescription 29 | 30 | jo_mt.__lacord_content_type = "application/json" 31 | jo_mt.__lacord_payload = enc 32 | jo_mt.__lacord_file_name = virtualname 33 | jo_mt.__lacord_set_file_name = set_virtualname 34 | jo_mt.__lacord_file_description = virtualdescription 35 | jo_mt.__lacord_set_file_description = set_virtualdescription 36 | end 37 | 38 | local jo_mt 39 | local ja_mt 40 | 41 | 42 | if const.json_provider == "cjson" then 43 | local cjson = req"cjson".new() 44 | cjson.encode_empty_table_as_object(false) 45 | cjson.decode_array_with_array_mt(true) 46 | 47 | local cjson_obj_encoder = req"cjson".new() 48 | cjson_obj_encoder.decode_array_with_array_mt(true) 49 | cjson_obj_encoder.encode_empty_table_as_object(true) 50 | 51 | encode = cjson.encode 52 | decode = cjson.decode 53 | null = cjson.null 54 | 55 | 56 | ja_mt = cjson.array_mt 57 | jo_mt = {} 58 | 59 | function jarray(t, ...) return setm(... and {t, ...} or t, cjson.array_mt) end 60 | function jobject(x) 61 | return x 62 | end 63 | 64 | initialize_metatables(ja_mt, jo_mt, encode) 65 | 66 | 67 | empty_array = cjson.empty_array 68 | 69 | function with_empty_as_object(v) 70 | return cjson_obj_encoder.encode(v) 71 | end 72 | elseif const.json_provider == "rapidjson" then 73 | local rapidjson = req"rapidjson" 74 | 75 | local empty_table_as_array = {empty_table_as_array = true} 76 | 77 | function encode(v) 78 | return rapidjson.encode(v, empty_table_as_array) 79 | end 80 | 81 | decode = rapidjson.decode 82 | 83 | null = rapidjson.null 84 | 85 | 86 | ja_mt = REG['json.array'] 87 | jo_mt = REG['json.object'] 88 | 89 | function jarray(t, ...) return setm(... and {t, ...} or t, ja_mt) end 90 | function jobject(x) return setm(x, jo_mt) end 91 | 92 | initialize_metatables(ja_mt, jo_mt, encode) 93 | 94 | 95 | empty_array = setm({}, ja_mt) 96 | 97 | function with_empty_as_object(v) 98 | return rapidjson.encode(v) 99 | end 100 | elseif const.json_provider == "dkjson" then 101 | local dkjson = req"dkjson" 102 | 103 | encode = dkjson.encode 104 | decode = dkjson.decode 105 | null = dkjson.null 106 | 107 | 108 | jo_mt = {__jsontype = "object"} 109 | ja_mt = {__jsontype = "array"} 110 | 111 | function jarray(t, ...) return setm(... and {t, ...} or t, ja_mt) end 112 | function jobject(x) return setm(x, jo_mt) end 113 | 114 | initialize_metatables(ja_mt, jo_mt, encode) 115 | 116 | 117 | empty_array = setm({}, {__tojson = function() return "[]" end}) 118 | 119 | function with_empty_as_object(_) 120 | return err("dkjson does not support switching empty tables to objects, please use json.jobject and then json.encode.") 121 | end 122 | end 123 | 124 | local newmt = { 125 | __lacord_content_type = "application/json", 126 | __lacord_payload = _ENV.encode, 127 | __lacord_file_name = virtualname, 128 | __lacord_set_file_name = set_virtualname, 129 | __lacord_file_description = virtualdescription, 130 | __lacord_set_file_description = set_virtualdescription, 131 | 132 | } 133 | 134 | function content_type(obj) 135 | local mt = getm(obj) 136 | 137 | if mt == jo_mt or mt == ja_mt then return obj, mt end 138 | 139 | if mt and mt.__lacord_content_type then 140 | return obj, mt 141 | elseif mt then 142 | mt.__lacord_content_type = "application/json" 143 | mt.__lacord_payload = _ENV.encode 144 | mt.__lacord_file_name = virtualname 145 | mt.__lacord_set_file_name = set_virtualname 146 | mt.__lacord_file_description = virtualdescription 147 | mt.__lacord_set_file_description = set_virtualdescription 148 | return obj, mt 149 | else 150 | setm(obj, newmt) 151 | return obj, newmt 152 | end 153 | end 154 | 155 | return _ENV -------------------------------------------------------------------------------- /lua/lacord/util/locales.lua: -------------------------------------------------------------------------------- 1 | return { 2 | 'da', 3 | 'de', 4 | 'en-GB', 5 | 'en-US', 6 | 'es-ES', 7 | 'fr', 8 | 'hr', 9 | 'it', 10 | 'lt', 11 | 'hu', 12 | 'nl', 13 | 'no', 14 | 'pl', 15 | 'pt-BR', 16 | 'ro', 17 | 'fi', 18 | 'sv-SE', 19 | 'vi', 20 | 'tr', 21 | 'cs', 22 | 'el', 23 | 'bg', 24 | 'ru', 25 | 'uk', 26 | 'hi', 27 | 'th', 28 | 'zh-CN', 29 | 'ja', 30 | 'zh-TW', 31 | 'ko', 32 | } -------------------------------------------------------------------------------- /lua/lacord/util/logger.lua: -------------------------------------------------------------------------------- 1 | local insert, unpack = table.insert, table.unpack 2 | local f = string.format 3 | local date, exit = os.date, os.exit 4 | local _stdout, _stderr = io.stdout, io.stderr 5 | local openf = io.open 6 | local to_n = tonumber 7 | local err = error 8 | local iter = pairs 9 | local iiter = ipairs 10 | local setm = setmetatable 11 | 12 | local cli = require"lacord.cli" 13 | local LACORD_DEBUG = cli.debug 14 | local LACORD_LOG_MODE = cli.log_mode 15 | local LACORD_LOG_FILE = cli.log_file 16 | 17 | local _ENV = {} 18 | 19 | --luacheck: ignore 111 20 | 21 | stdout = _stdout 22 | stderr = _stderr 23 | 24 | local _mode = 0 25 | 26 | --- An optional lua file object to write output to, must be opened in a write mode. 27 | fd = nil 28 | 29 | local colors = {} 30 | 31 | colors[0] = { 32 | info = "" 33 | ,warn = "" 34 | ,error = "" 35 | ,white = "" 36 | ,debug = "" 37 | ,info_highlight = "" 38 | ,warn_highlight = "" 39 | ,error_highlight = "" 40 | ,debug_highlight = "" 41 | } 42 | 43 | colors[3] = { 44 | info = "\27[0m\27[32m" 45 | ,warn = "\27[0m\27[33m" 46 | ,error = "\27[0m\27[31m" 47 | ,white = "\27[0m\27[1;37m" 48 | ,debug = "\27[0m\27[1;36m" 49 | ,info_highlight = "\27[0m\27[1;92m" 50 | ,warn_highlight = "\27[0m\27[1;93m" 51 | ,error_highlight = "\27[0m\27[1;91m" 52 | ,debug_highlight = "\27[0m\27[1;96m" 53 | } 54 | 55 | colors[8] = { 56 | info = "\27[0m\27[38;5;36m" 57 | ,warn = "\27[0m\27[38;5;220m" 58 | ,error = "\27[0m\27[38;5;196m" 59 | ,white = "\27[0m\27[38;5;231m" 60 | ,info_highlight = "\27[0m\27[38;5;48m" 61 | ,warn_highlight = "\27[0m\27[38;5;11m" 62 | ,error_highlight = "\27[0m\27[38;5;9m" 63 | ,debug = "\27[0m\27[38;5;105m" 64 | ,debug_highlight = "\27[0m\27[38;5;123m" 65 | } 66 | 67 | local function quick_highlight(c, body, last, level) 68 | if level and c[level .. '_highlight'] then return c[level .. '_highlight'] .. body .. last 69 | else return c.white .. body .. last 70 | end 71 | end 72 | 73 | local highlighters = { } 74 | local function paint(str, level) 75 | if not highlighters[level] then 76 | local function highlighter(body) 77 | return quick_highlight(colors[_mode], body, "\27[0m", level) 78 | end 79 | highlighters[level] = highlighter 80 | return str:gsub("$([^;]+);", highlighter) 81 | else 82 | return str:gsub("$([^;]+);", highlighters[level]) 83 | end 84 | end 85 | 86 | local function unpaint(str) return (str:gsub("$([^;]+);", "%1")) end 87 | 88 | local function check_fd(s, msg, code) 89 | if not s then 90 | fd = nil 91 | _ENV.warn("removed $logger.fd; because $(%q, %#x);.", msg , code) 92 | return false 93 | end 94 | return true 95 | end 96 | 97 | local fmts = { } 98 | 99 | local function writef(ifd, level, content) 100 | local timestamp = date"!%c" 101 | if fd then 102 | if check_fd(fd:write(timestamp, " ")) then 103 | if level then 104 | if not check_fd(fd:write(fmts[level], " ")) then goto finished end 105 | end 106 | check_fd(fd:write(content, "\n")) 107 | ::finished:: 108 | end 109 | end 110 | if ifd then 111 | local str = paint(content, level) 112 | ifd:write( 113 | colors[_mode][level or 'white'], 114 | timestamp, " " 115 | ) 116 | if level then ifd:write(colors[_mode][level .. "_highlight"], fmts[level], " ") 117 | else ifd:write("LOG", " ") end 118 | if _mode > 0 then 119 | ifd:write("\27[0m", str, "\27[0m\n") 120 | else 121 | ifd:write(str, "\n") 122 | end 123 | end 124 | end 125 | 126 | 127 | 128 | --- Logs to stdout, and the output file if set, using the INF info channel. 129 | -- @string str A format string 130 | -- @param[opt] ... Values passed into `string.format`. 131 | fmts.info = "INF" 132 | function info(...) 133 | return writef(_ENV.stdout, 'info', f(...)) 134 | end 135 | 136 | fmts.debug = "DBG" 137 | 138 | if LACORD_DEBUG then 139 | function _ENV.debug(...) 140 | return writef(_ENV.stdout, 'debug', f(...)) 141 | end 142 | else 143 | function _ENV.debug() 144 | end 145 | end 146 | 147 | 148 | --- Logs to stdout, and the output file if set, using the WRN warning channel. 149 | -- @string str A format string 150 | -- @param[opt] ... Values passed into `string.format`. 151 | fmts.warn = "WRN" 152 | function warn(...) 153 | return writef(_ENV.stdout, 'warn', f(...)) 154 | end 155 | 156 | --- Logs to stderr, and the output file if set, using the ERR error channel. 157 | -- @string str A format string 158 | -- @param[opt] ... Values passed into `string.format`. 159 | fmts.error = "ERR" 160 | function error(...) 161 | return writef(_ENV.stderr, 'error', f(...)) 162 | end 163 | 164 | --- Logs an error using `logger.error` and then throws a lua error with the same message. 165 | -- @string str A format string 166 | -- @param[opt] ... Values passed into `string.format`. 167 | function throw(...) 168 | local content = f(...) 169 | writef(_ENV.stderr, 'error', content) 170 | return err(unpaint(content), 2) 171 | end 172 | 173 | --- Logs an error using `logger.error` and then exits with a non-zero exit code. 174 | -- @string str A format string. 175 | -- @param[opt] ... Values passed into `string.format`. 176 | function fatal(...) 177 | error(...) 178 | error"Fatal error: quitting!" 179 | return exit(1, true) 180 | end 181 | 182 | --- Similar to lua's assert but uses logger.throw when an assertion fails. 183 | function _ENV.assert(v, ...) 184 | if v then return v 185 | else return _ENV.throw(...) 186 | end 187 | end 188 | 189 | function ferror(...) return err(f(...), 2) end 190 | 191 | --- Logs to stdout, and the output file if set. 192 | -- @string str A format string. 193 | -- @param[opt] ... Values passed into `string.format`. 194 | function printf(...) return writef(_ENV.stdout, nil, f(...)) end 195 | 196 | local modes = { 197 | [0] = true, 198 | [3] = true, 199 | [8] = true 200 | } 201 | 202 | --- Change color mode for writing to stdout. 203 | -- You can use: 204 | -- - `3` for `3/4 bit color` 205 | -- - `8` for `8 bit color` 206 | -- - `24` for `24 bit true color`. 207 | -- Setting it to `0` disables coloured output. 208 | -- @tparam number m The mode. 209 | function mode(m) 210 | m = m or 0 211 | _mode = modes[m] and m or 0 212 | return _mode 213 | end 214 | 215 | if LACORD_LOG_FILE then 216 | fd = openf(LACORD_LOG_FILE, "a") 217 | end 218 | 219 | if LACORD_LOG_MODE then 220 | mode(to_n(LACORD_LOG_MODE)) 221 | end 222 | 223 | return _ENV -------------------------------------------------------------------------------- /lua/lacord/util/mime.lua: -------------------------------------------------------------------------------- 1 | local mimes = { 2 | [".3g2"] = "video/3gpp2", 3 | [".3gp"] = "video/3gpp", 4 | [".7z"] = "application/x-7z-compressed", 5 | [".aac"] = "audio/aac", 6 | [".abw"] = "application/x-abiword", 7 | [".arc"] = "application/x-freearc", 8 | [".avi"] = "video/x-msvideo", 9 | [".azw"] = "application/vnd.amazon.ebook", 10 | [".bin"] = "application/octet-stream", 11 | [".bmp"] = "image/bmp", 12 | [".bz"] = "application/x-bzip", 13 | [".bz2"] = "application/x-bzip2", 14 | [".cda"] = "application/x-cdf", 15 | [".csh"] = "application/x-csh", 16 | [".css"] = "text/css", 17 | [".csv"] = "text/csv", 18 | [".doc"] = "application/msword", 19 | [".docx"] = "application/vnd.openxmlformats-officedocument.wordprocessingml.document", 20 | [".eot"] = "application/vnd.ms-fontobject", 21 | [".epub"] = "application/epub+zip", 22 | [".gif"] = "image/gif", 23 | [".gz"] = "application/gzip", 24 | [".htm"] = "text/html", 25 | [".html"] = "text/html", 26 | [".ico"] = "image/vnd.microsoft.icon", 27 | [".ics"] = "text/calendar", 28 | [".jar"] = "application/java-archive", 29 | [".jpeg"] = ".jpg", 30 | [".jpg"] = "image/jpeg", 31 | [".js"] = "text/javascript", 32 | [".json"] = "application/json", 33 | [".jsonld"] = "application/ld+json", 34 | [".mid"] = ".midi", 35 | [".midi"] = "audio/midi", 36 | [".mjs"] = "text/javascript", 37 | [".mp3"] = "audio/mpeg", 38 | [".mp4"] = "video/mp4", 39 | [".mpeg"] = "video/mpeg", 40 | [".mpkg"] = "application/vnd.apple.installer+xml", 41 | [".odp"] = "application/vnd.oasis.opendocument.presentation", 42 | [".ods"] = "application/vnd.oasis.opendocument.spreadsheet", 43 | [".odt"] = "application/vnd.oasis.opendocument.text", 44 | [".oga"] = "audio/ogg", 45 | [".ogv"] = "video/ogg", 46 | [".ogx"] = "application/ogg", 47 | [".opus"] = "audio/opus", 48 | [".otf"] = "font/otf", 49 | [".pdf"] = "application/pdf", 50 | [".php"] = "application/x-httpd-php", 51 | [".png"] = "image/png", 52 | [".ppt"] = "application/vnd.ms-powerpoint", 53 | [".pptx"] = "application/vnd.openxmlformats-officedocument.presentationml.presentation", 54 | [".rar"] = "application/vnd.rar", 55 | [".rtf"] = "application/rtf", 56 | [".sh"] = "application/x-sh", 57 | [".svg"] = "image/svg+xml", 58 | [".swf"] = "application/x-shockwave-flash", 59 | [".tar"] = "application/x-tar", 60 | [".tif"] = "image/tiff", 61 | [".tiff"] = "image/tiff", 62 | [".ts"] = "video/mp2t", 63 | [".ttf"] = "font/ttf", 64 | [".txt"] = "text/plain", 65 | [".vsd"] = "application/vnd.visio", 66 | [".wav"] = "audio/wav", 67 | [".weba"] = "audio/webm", 68 | [".webm"] = "video/webm", 69 | [".webp"] = "image/webp", 70 | [".woff"] = "font/woff", 71 | [".woff2"] = "font/woff2", 72 | [".xhtml"] = "application/xhtml+xml", 73 | [".xls"] = "application/vnd.ms-excel", 74 | [".xlsx"] = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", 75 | [".xml"] = "XML", 76 | [".xul"] = "application/vnd.mozilla.xul+xml", 77 | [".zip"] = "application/zip" 78 | } 79 | 80 | local mime_exts = {} 81 | 82 | for k, v in pairs(mimes) do 83 | mime_exts[v] = k 84 | end 85 | 86 | return { 87 | exts = mime_exts, 88 | types = mimes 89 | } -------------------------------------------------------------------------------- /lua/lacord/util/models/init.lua: -------------------------------------------------------------------------------- 1 | local err = error 2 | local getm = getmetatable 3 | local to_s = tostring 4 | 5 | local upper = string.upper 6 | 7 | local set = require"lacord.util".set 8 | 9 | local resolve = require"lacord.models.methods".resolve 10 | local model_id = require"lacord.models.methods".model_id 11 | 12 | local _ENV = {} 13 | 14 | function methodify(metatable, methods, exclude, ...) 15 | local props = exclude and set(exclude, ...) 16 | if props then 17 | function metatable:__index(key) 18 | if props[key] then 19 | return methods[key](self) 20 | else 21 | return methods[key] 22 | end 23 | end 24 | else 25 | metatable.__index = methods 26 | end 27 | end 28 | 29 | 30 | function tostring(self) 31 | local mt = getm(self) 32 | local header = (mt.__lacord_model or 'model'):gsub("^.", upper, 1) 33 | 34 | if mt.__lacord_model_mention then 35 | header = header .. " " .. mt.__lacord_model_mention(self) 36 | elseif mt.__lacord_model_id then 37 | header = header .. " <" .. mt.__lacord_model_id(self) .. ">" 38 | end 39 | return header 40 | end 41 | 42 | function model_selectors(name, mt) 43 | local function the_model(obj) 44 | if getm(obj) == mt then return obj end 45 | return resolve(obj, name) or err("Object " .. to_s(obj) .. " does not implement the " .. name .. " protocol!") 46 | end 47 | 48 | local function the_id(obj) 49 | if getm(obj) == mt then return obj.id end 50 | return model_id(obj, name) 51 | end 52 | 53 | return the_model, the_id 54 | end 55 | 56 | return _ENV 57 | -------------------------------------------------------------------------------- /lua/lacord/util/models/magic-numbers.lua: -------------------------------------------------------------------------------- 1 | local iter = pairs 2 | local iiter = ipairs 3 | local getm = getmetatable 4 | local set = rawset 5 | local setm = setmetatable 6 | 7 | local max = math.max 8 | 9 | local bound = {} 10 | local enum = {__name = "lacord.models.enum"} 11 | 12 | local function check_bound(v, out) 13 | if getm(v) == bound then 14 | out.__boundary = out.__boundary or {} 15 | out.__boundary[v[2]] = v[1] 16 | return v[1] 17 | else return v 18 | end 19 | end 20 | 21 | local function resolve_bound(t) 22 | if t.__boundary then 23 | for name, field in iter(t.__boundary) do 24 | t[name] = t[field] + .5 25 | end 26 | t.__boundary = nil 27 | end 28 | return setm(t, enum) 29 | end 30 | 31 | local function powers_of_two(t) 32 | local out = {} 33 | for i , v in iiter(t) do 34 | v = check_bound(v, out) 35 | out[v] = 1 << (i-1) 36 | end 37 | return resolve_bound(out) 38 | end 39 | 40 | local function iota(t) 41 | local out = {} 42 | for i , v in iiter(t) do 43 | v = check_bound(v, out) 44 | out[v] = i - 1 45 | end 46 | return resolve_bound(out) 47 | end 48 | 49 | local function iota1(t) 50 | local out = {} 51 | for i , v in iiter(t) do 52 | v = check_bound(v, out) 53 | out[v] = i 54 | end 55 | return resolve_bound(out) 56 | end 57 | 58 | local function iotaN(t) 59 | local out = {} 60 | local M = 0 61 | for k, v in iter(t) do 62 | if type(k) == 'number' then goto continue end 63 | out[k] = v 64 | M = max(v, M) 65 | ::continue:: 66 | end 67 | for i , v in iiter(t) do 68 | v = check_bound(v, out) 69 | out[v] = i + M 70 | end 71 | return resolve_bound(out) 72 | end 73 | 74 | local function boundary(name, field) 75 | return setm({field, name}, bound) 76 | end 77 | 78 | local function magic_index(_, k) 79 | return k 80 | end 81 | 82 | local function magic_newindex(t, k, v) 83 | set(t, k, v) 84 | set(t, k..'s', v) 85 | if getm(v) == enum then 86 | local out = {} for k_ , v_ in iter(v) do 87 | if k_ == '__boundary' then goto continue end 88 | out[v_] = k_ 89 | ::continue:: 90 | end 91 | set(t, k..'_names', out) 92 | end 93 | end 94 | 95 | return function() 96 | return 97 | setmetatable({}, {__index = magic_index, __newindex = magic_newindex}), 98 | iota, 99 | powers_of_two, 100 | iota1, 101 | iotaN, 102 | boundary 103 | end 104 | -------------------------------------------------------------------------------- /lua/lacord/util/mutex.lua: -------------------------------------------------------------------------------- 1 | --- A minimal mutex implementation. 2 | -- @module util.mutex 3 | 4 | local cqueues = require"cqueues" 5 | local sleep = cqueues.sleep 6 | local monotime = cqueues.monotime 7 | local me = cqueues.running 8 | local cond = require"cqueues.condition" 9 | local setmetatable = setmetatable 10 | local max = math.max 11 | 12 | local _ENV = {} 13 | 14 | local mutex = {} 15 | 16 | mutex.__index = mutex 17 | mutex.__name = 'lacord.mutex' 18 | 19 | --- Locks the mutex. 20 | -- @tparam mutex self 21 | -- @tparam[opt] number timeout an optional timeout to wait. 22 | function mutex:lock(timeout) 23 | self:check_hangover() 24 | if self.inuse then 25 | self.inuse = self.pollfd:wait(timeout) 26 | self:check_hangover() 27 | else 28 | self.inuse = true 29 | end 30 | end 31 | 32 | --- Unlocks the mutex. 33 | -- @tparam mutex self 34 | function mutex:unlock() 35 | if self.inuse then 36 | self.inuse = false 37 | self.pollfd:signal(1) 38 | end 39 | end 40 | 41 | local function unlockAfter(self, time) 42 | sleep(time) 43 | self:unlock() 44 | end 45 | 46 | --- Unlocks the mutex after the specified time in seconds. 47 | -- @tparam mutex self 48 | -- @tparam number time The time to unlock after, in seconds. 49 | function mutex:unlock_after(time) 50 | me():wrap(unlockAfter, self, time) 51 | end 52 | 53 | local function unlockAt(self, deadline) 54 | sleep(max(0, deadline - monotime())) 55 | self:unlock() 56 | end 57 | 58 | --- Unlocks the mutex at the specified point in time in seconds. 59 | -- @tparam mutex self 60 | -- @tparam number time The time to unlock at, in seconds. 61 | function mutex:unlock_at(deadline) 62 | me():wrap(unlockAt, self, deadline) 63 | end 64 | 65 | local function defered(self) 66 | sleep() 67 | self:unlock() 68 | end 69 | 70 | --- Unlocks the mutex on the next schedule. 71 | -- @tparam mutex self 72 | function mutex:defer_unlock() 73 | me():wrap(defered, self) 74 | end 75 | 76 | function mutex:set_hangover(delay) 77 | if self.hangover then 78 | self.hangover = max(self.hangover, monotime() + delay) 79 | else 80 | self.hangover = monotime() + delay 81 | end 82 | end 83 | 84 | function mutex:check_hangover() 85 | local the_hangover = self.hangover 86 | if the_hangover and the_hangover > monotime() then 87 | sleep(the_hangover - monotime()) 88 | if the_hangover ~= self.hangover then return self:check_hangover() end 89 | end 90 | end 91 | 92 | --- Creates a new mutex 93 | -- @treturn mutex 94 | function new() 95 | return setmetatable({ 96 | pollfd = cond.new() 97 | ,inuse= false 98 | }, mutex) 99 | end 100 | 101 | --- Mutex Object. 102 | -- @table mutex 103 | -- @within Objects 104 | -- @bool inuse 105 | -- @field cond The condition variable. 106 | 107 | return _ENV -------------------------------------------------------------------------------- /lua/lacord/util/session-limit.lua: -------------------------------------------------------------------------------- 1 | local cond = require"cqueues.condition" 2 | local me = require"cqueues".running 3 | local sleep = require"cqueues".sleep 4 | local setm = setmetatable 5 | 6 | local _ENV = {} 7 | 8 | --luacheck: ignore 111 9 | 10 | local session_limit = {__name = "lacord.session-limit"} 11 | 12 | session_limit.__index = session_limit 13 | 14 | function new(availability) 15 | return setm({ 16 | v = availability, 17 | total = availability, 18 | cv = cond.new(false), 19 | }, session_limit) 20 | end 21 | 22 | function session_limit:exit() 23 | self.v = self.v + 1 24 | self.cv:signal(1) 25 | end 26 | 27 | function session_limit:enter() 28 | while self.v <= 0 do self.cv:wait() end 29 | self.v = self.v - 1 30 | end 31 | 32 | local waiter = function(s, sc) sleep(sc) s:exit() end 33 | function session_limit:exit_after(secs) 34 | if secs > 0.0 then 35 | me():wrap(waiter, self, secs) 36 | else 37 | me():wrap(session_limit.exit, self) 38 | end 39 | end 40 | 41 | return _ENV 42 | -------------------------------------------------------------------------------- /lua/lacord/util/translator.lua: -------------------------------------------------------------------------------- 1 | local i18n = require"internationalize" 2 | local locales = require"lacord.util.locales" 3 | 4 | local translator = {__name = "lacord.translator"} 5 | 6 | function translator:__index(k) 7 | if translator[k] then return translator[k] 8 | elseif self.instance[k] then 9 | local function method(this, ...) 10 | return this.instance[k](this.instance, ...) 11 | end 12 | self[k] = method 13 | return method 14 | end 15 | end 16 | 17 | function translator:__call(str) 18 | return setmetatable({str}, self) 19 | end 20 | 21 | function translator:add(t) 22 | for k, v in pairs(t) do 23 | self.ctx[k] = v 24 | end 25 | end 26 | 27 | local function __lacord_localize(T, locale_str, info) 28 | local ctx = {} 29 | 30 | for k ,v in pairs(T.ctx) do 31 | ctx[k] = v 32 | end 33 | for k ,v in pairs(info) do 34 | ctx[k] = v 35 | end 36 | 37 | local out = T:translations_from(locale_str[1], ctx, locales) 38 | 39 | local default = T.instance.locale 40 | local rest, primary, set = {} 41 | 42 | for _ , result in ipairs(out) do 43 | local loc, value, used = result[1], result[2], result[3] 44 | 45 | if loc == default or used == default then 46 | primary = value 47 | elseif value ~= primary then 48 | rest[loc] = value 49 | set = true 50 | end 51 | end 52 | 53 | return primary, set and rest or nil 54 | end 55 | 56 | local function new(default_locale) 57 | local instance = i18n(default_locale) 58 | 59 | return setmetatable({instance = instance, ctx = {}, __lacord_localize = __lacord_localize}, translator) 60 | end 61 | 62 | return { 63 | new = new 64 | } 65 | 66 | -------------------------------------------------------------------------------- /lua/lacord/util/uint.lua: -------------------------------------------------------------------------------- 1 | --- Unsigned integer encoding and utilities. 2 | -- @module util.uint 3 | 4 | local ult, mtoint, mntype, max_int = math.ult, math.tointeger, math.type, math.maxinteger 5 | local setmetatable = setmetatable 6 | local tonumber = tonumber 7 | local type = type 8 | local time = os.time 9 | local constants = require"lacord.const" 10 | 11 | local _ENV = setmetatable({}, {__call = function(self,s) return self.touint(s) end}) 12 | 13 | local function to_integer_worker(s) -- string -> uint64 14 | local n = 0 15 | local l = #s 16 | local place = 1 17 | for i = l -1,0, -1 do 18 | n = n + tonumber(s:sub(i+1,i+1)) * place 19 | place = place * 10 20 | end 21 | return n 22 | end 23 | 24 | local two_63 = 2^63 25 | local two_64 = 2^64 26 | 27 | local function float_to_uint(f) 28 | if f > max_int then 29 | return mtoint(((f + two_63) % two_64) - two_63) 30 | else 31 | return mtoint(f) 32 | end 33 | end 34 | 35 | local function lnum_to_uint(l) 36 | if mntype(l) == 'integer' then 37 | return l 38 | else 39 | return float_to_uint(l) 40 | end 41 | end 42 | 43 | local function numeral(str) 44 | local s, e = str:find('%d+', 1) 45 | return s == 1 and e == #str 46 | end 47 | 48 | --- Converts a number or string into an encoded uint64. 49 | -- @tparam number|string s 50 | -- @treturn[1] integer The encoded uint64. 51 | -- @treturn[2] nil 52 | function touint(s) 53 | if type(s) == 'number' then return lnum_to_uint(s) 54 | elseif type(s) == 'string' and numeral(s) then 55 | return to_integer_worker(s) 56 | end 57 | end 58 | 59 | --- uint64 tostring 60 | -- @int i An encoded uint64. 61 | function tostring(i) return type(i) == 'string' and i or ("%u"):format(i) end 62 | 63 | local function udiv (n, d) 64 | if d < 0 then 65 | if ult(n, d) then return 0 66 | else return 1 67 | end 68 | end 69 | local q = ((n >> 1) // d) << 1 70 | local r = n - q * d 71 | if not ult(r, d) then q = q + 1 end 72 | return q 73 | end 74 | 75 | do 76 | local fnv_basis = -3750763034362895579 77 | local fnv_prime = 1099511628211 78 | --- Computes the FNV-1a 64bit hash of the given string. 79 | -- @str str The input string. 80 | -- @treturn integer The hash. 81 | function hash(str) 82 | local hash = fnv_basis 83 | for i = 1, #str do 84 | hash = hash ~ str:byte(i) 85 | hash = (hash * fnv_prime) 86 | end 87 | return hash 88 | end 89 | end 90 | 91 | local epoch = constants.discord_epoch * 1000 92 | 93 | --- Computes the UNIX timestamp of a given uint64, using discord's bitfield format. 94 | -- @tparam string|number s The snowflake. 95 | -- @treturn integer The timestamp. 96 | function timestamp(s) 97 | return udiv((touint(s) >> 22) + epoch , 1000) 98 | end 99 | 100 | --- Creates an artificial snowflake from a given UNIX timestamp. 101 | -- @tparam[opt=current time] integer s The timestamp. 102 | -- @treturn integer The resulting snowflake. 103 | function fromtime(s) 104 | s = by10(s or time(), 3) 105 | return (s - epoch) << 22 106 | end 107 | 108 | --- Gets the timestamp, worker ID, process ID and increment from a snowflake. 109 | -- @tparam number|string s The snowflake. 110 | -- @treturn table 111 | function decompose(s) 112 | s = touint(s) 113 | return { 114 | timestamp = timestamp(s) 115 | ,worker = (s & 0x3E0000) >> 17 116 | ,pid = (s & 0x1F000) >> 12 117 | ,increment = s & 0xFFF 118 | } 119 | end 120 | 121 | local inc = -1 122 | 123 | --- Creates an artifical snowflake from the given timestamp, worker and pid. 124 | -- @int s The timestamp. 125 | -- @int worker The worker ID. 126 | -- @int pid The process ID. 127 | -- @int[opt] incr The increment. An internal incremented value is used if one is not provided. 128 | -- @treturn integer The snowflake. 129 | function synthesize(s, worker, pid, incr) 130 | inc = (inc + 1) & 0xFFF 131 | incr = (incr or inc) & 0xFFF 132 | worker = ((worker or 0) & 63) << 17 133 | pid = ((pid or 0) & 63) << 12 134 | return fromtime(s) | worker | pid | incr 135 | end 136 | 137 | ---sort two snowflake objects. 138 | function snowflake_sort(i,j) return ult(touint(i.id) , touint(j.id)) end 139 | 140 | ---sort two snowflake ids. 141 | function id_sort(i,j) return ult(touint(i) , touint(j)) end 142 | 143 | return _ENV -------------------------------------------------------------------------------- /lua/lacord/wrapper/outgoing-webhook-server.lua: -------------------------------------------------------------------------------- 1 | local cli = require"lacord.cli" 2 | 3 | if not pcall(require, "luatweetnacl") then 4 | require"lacord.util.logger".fatal( 5 | "You do not have $luatweetnacl; installed. \z 6 | This is a necessary dependency for using the slash command webserver, \z 7 | but due to issues provisioning the module on all cqueues compatible systems \z 8 | it has been removed from the rockspec. Please run $luarocks install luatweetnacl; \z 9 | to install the module.") 10 | end 11 | 12 | if cli.unstable then return require"lacord.outgoing-webhook-server-2" 13 | else return require"lacord.outgoing-webhook-server-1" 14 | end -------------------------------------------------------------------------------- /src/archp.c: -------------------------------------------------------------------------------- 1 | 2 | #include "lua.h" 3 | 4 | #if defined(__i386) || defined(__i386__) || defined(_M_IX86) 5 | 6 | #define ARCHP_ARCH_NAME "x86" 7 | #elif defined(__x86_64__) || defined(__x86_64) || defined(_M_X64) || defined(_M_AMD64) 8 | 9 | #define ARCHP_ARCH_NAME "x64" 10 | #elif defined(__arm__) || defined(__arm) || defined(__ARM__) || defined(__ARM) 11 | 12 | #define ARCHP_ARCH_NAME "ARM" 13 | #elif defined(__aarch64__) 14 | 15 | #define ARCHP_ARCH_NAME "ARM64" 16 | #elif defined(__ppc__) || defined(__ppc) || defined(__PPC__) || defined(__PPC) || defined(__powerpc__) || defined(__powerpc) || defined(__POWERPC__) || defined(__POWERPC) || defined(_M_PPC) 17 | 18 | #define ARCHP_ARCH_NAME "PPC" 19 | #elif defined(__mips64__) || defined(__mips64) || defined(__MIPS64__) || defined(__MIPS64) 20 | 21 | #define ARCHP_ARCH_NAME "MIPS64" 22 | #elif defined(__mips__) || defined(__mips) || defined(__MIPS__) || defined(__MIPS) 23 | 24 | #define ARCHP_ARCH_NAME "MIPS32" 25 | #else 26 | #define ARCHP_ARCH_NAME "Unknown" 27 | #endif 28 | 29 | 30 | #if defined(_WIN32) && !defined(_XBOX_VER) 31 | #define ARCHP_OS "Windows" 32 | #elif defined(__linux__) 33 | #define ARCHP_OS "Linux" 34 | #elif defined(__MACH__) && defined(__APPLE__) 35 | #define ARCHP_OS "OSX" 36 | #elif (defined(__FreeBSD__) || defined(__FreeBSD_kernel__) || \ 37 | defined(__NetBSD__) || defined(__OpenBSD__) || \ 38 | defined(__DragonFly__)) && !defined(__ORBIS__) 39 | #define ARCHP_OS "BSD" 40 | #elif (defined(__sun__) && defined(__svr4__)) 41 | #define ARCHP_EXTRA "Solaris" 42 | #define ARCHP_OS "POSIX" 43 | #elif defined(__HAIKU__) 44 | #define ARCHP_OS "POSIX" 45 | #elif defined(__CYGWIN__) 46 | #define ARCHP_EXTRA "Cygwin" 47 | #define ARCHP_OS "POSIX" 48 | #else 49 | #define ARCHP_OS "Other" 50 | #endif 51 | 52 | LUALIB_API int luaopen_lacord_util_archp(lua_State* L) { 53 | lua_createtable(L, 0, 0); 54 | lua_pushstring(L, "os"); 55 | lua_pushstring(L, ARCHP_OS); 56 | lua_settable(L, -3); 57 | #if defined(ARCHP_EXTRA) 58 | lua_pushstring(L, "extra"); 59 | lua_pushstring(L, ARCHP_EXTRA); 60 | lua_settable(L, -3); 61 | #endif 62 | 63 | lua_pushstring(L, "arch"); 64 | lua_pushstring(L, ARCHP_ARCH_NAME); 65 | lua_settable(L, -3); 66 | 67 | lua_pushstring(L, "lua"); 68 | lua_createtable(L, 0, 0); 69 | 70 | lua_pushstring(L, "release"); 71 | lua_pushstring(L, LUA_RELEASE); 72 | lua_settable(L, -3); 73 | 74 | lua_pushstring(L, "vm"); 75 | lua_pushinteger(L, LUA_VERSION_NUM); 76 | lua_settable(L, -3); 77 | #if (LUA_VERSION_NUM == 501) 78 | lua_pushstring(L, "major"); 79 | lua_pushstring(L, "5"); 80 | lua_settable(L, -3); 81 | 82 | lua_pushstring(L, "minor"); 83 | lua_pushstring(L, "1"); 84 | lua_settable(L, -3); 85 | 86 | lua_pushstring(L, "release_num"); 87 | lua_pushstring(L, LUA_RELEASE+8); 88 | lua_settable(L, -3); 89 | #else 90 | lua_pushstring(L, "major"); 91 | lua_pushstring(L, LUA_VERSION_MAJOR); 92 | lua_settable(L, -3); 93 | 94 | lua_pushstring(L, "minor"); 95 | lua_pushstring(L, LUA_VERSION_MINOR); 96 | lua_settable(L, -3); 97 | 98 | lua_pushstring(L, "release_num"); 99 | lua_pushstring(L, LUA_VERSION_RELEASE); 100 | lua_settable(L, -3); 101 | #endif 102 | 103 | lua_settable(L, -3); 104 | return 1; 105 | } --------------------------------------------------------------------------------