├── .gitignore ├── .vscode └── settings.json ├── package.lua ├── .github └── workflows │ └── rebuild-wiki.yml ├── libs ├── enums.lua ├── client │ ├── API.lua │ ├── EventHandler.lua │ └── resolver.lua └── containers │ └── Interaction.lua ├── CHANGELOG.md ├── examples └── latency-example.lua ├── init.lua ├── README.md ├── LICENSE └── docgen.lua /.gitignore: -------------------------------------------------------------------------------- 1 | tests/ 2 | *.code-workspace 3 | docs -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Lua.diagnostics.disable": [ 3 | "undefined-doc-class", 4 | "undefined-doc-name" 5 | ], 6 | "Lua.runtime.path": [ 7 | "?.lua", 8 | "?/init.lua", 9 | "deps/?/init.lua", 10 | "deps/?.lua", 11 | "libs/?.lua" 12 | ], 13 | "Lua.workspace.library": [ 14 | "../deps", 15 | "../libs", 16 | "${addons}/luvit/module/library" 17 | ], 18 | "Lua.diagnostics.globals": [ 19 | "p", 20 | "args" 21 | ], 22 | "Lua.workspace.checkThirdParty": false 23 | } -------------------------------------------------------------------------------- /package.lua: -------------------------------------------------------------------------------- 1 | return { 2 | name = "Bilal2453/discordia-interactions", 3 | version = "2.0.1", 4 | description = "A Discordia library extension that enables receiving and responding to Discord's Interactions.", 5 | tags = { "discord", "discordia", "interactions" }, 6 | license = "Apache License 2.0", 7 | author = { name = "Bilal2453", email = "belal2453@gmail.com" }, 8 | homepage = "https://github.com/Bilal2453/discordia-interactions", 9 | dependencies = { "SinisterRectus/discordia", }, 10 | files = { 11 | "**.lua", 12 | "!test*", 13 | "!tests/*" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /.github/workflows/rebuild-wiki.yml: -------------------------------------------------------------------------------- 1 | name: Rebuild Wiki 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | workflow_dispatch: 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v4 15 | with: 16 | repository: '${{ github.repository }}.wiki' 17 | path: 'wiki' 18 | 19 | - uses: actions/checkout@v4 20 | with: 21 | path: 'repo' 22 | 23 | - name: Install Luvit 24 | run: curl -L https://github.com/luvit/lit/raw/master/get-lit.sh | sh 25 | 26 | - name: Generate the wiki pages 27 | run: | 28 | cd repo 29 | ../luvit docgen.lua 30 | 31 | - name: Set up bot 32 | run: | 33 | mv -f $GITHUB_WORKSPACE/repo/docs/* $GITHUB_WORKSPACE/wiki/ 34 | cd $GITHUB_WORKSPACE/wiki 35 | git config user.name "github-actions[bot]" 36 | git config user.email "41898282+github-actions[bot]@users.noreply.github.com" 37 | 38 | - name: Update the wiki repo 39 | run: | 40 | cd $GITHUB_WORKSPACE/wiki 41 | git add --all 42 | git commit -m "Rebuild for commit ${{ github.event.after }}" 43 | git push 44 | continue-on-error: true 45 | -------------------------------------------------------------------------------- /libs/enums.lua: -------------------------------------------------------------------------------- 1 | local enums = {} 2 | 3 | enums.interactionType = { 4 | ping = 1, 5 | applicationCommand = 2, 6 | messageComponent = 3, 7 | applicationCommandAutocomplete = 4, 8 | modalSubmit = 5, 9 | } 10 | 11 | enums.interactionContextType = { 12 | guild = 1, 13 | botDm = 2, 14 | privateChannel = 3, 15 | } 16 | 17 | enums.interactionCallbackType = { 18 | pong = 1, 19 | channelMessage = 4, 20 | deferredChannelMessage = 5, 21 | deferredUpdateMessage = 6, 22 | updateMessage = 7, 23 | applicationCommandAutocompleteResult = 8, 24 | modal = 9, 25 | premiumRequired = 10, -- deprecated 26 | launchActivity = 12, 27 | } 28 | 29 | enums.applicationIntegrationType = { 30 | guildInstall = 0, 31 | userInstall = 2, 32 | } 33 | 34 | enums.appCommandType = { 35 | chatInput = 1, 36 | user = 2, 37 | message = 3, 38 | } 39 | 40 | enums.appCommandOptionType = { 41 | subCommand = 1, 42 | subCommandGroup = 2, 43 | string = 3, 44 | integer = 4, 45 | boolean = 5, 46 | user = 6, 47 | channel = 7, 48 | role = 8, 49 | mentionable = 9, 50 | number = 10, 51 | attachment = 11, 52 | } 53 | 54 | enums.appCommandPermissionType = { 55 | role = 1, 56 | user = 2, 57 | } 58 | 59 | enums.componentType = { 60 | actionRow = 1, 61 | button = 2, 62 | selectMenu = 3, 63 | textInput = 4, 64 | } 65 | 66 | enums.messageFlag = { 67 | hasThread = 0x00000020, 68 | ephemeral = 0x00000040, 69 | loading = 0x00000080, 70 | } 71 | 72 | return enums 73 | -------------------------------------------------------------------------------- /libs/client/API.lua: -------------------------------------------------------------------------------- 1 | local API = {} -- API:request is defined in init.lua 2 | local f = string.format 3 | 4 | -- endpoints are never patched into Discordia 5 | -- therefor not defining them in their own file, although the actual requests are 6 | local endpoints = { 7 | INTERACTION_CALLBACK = "/interactions/%s/%s/callback", 8 | INTERACTION_WEBHOOK = "/webhooks/%s/%s", 9 | INTERACTION_MESSAGES = "/webhooks/%s/%s/messages/%s", 10 | CHANNEL_MESSAGE = "/channels/%s/messages/%s", 11 | } 12 | 13 | function API:createInteractionResponse(id, token, payload, files) 14 | local endpoint = f(endpoints.INTERACTION_CALLBACK, id, token) 15 | return self:request("POST", endpoint, payload, {with_response = true}, files) 16 | end 17 | 18 | function API:createWebhookMessage(id, token, payload, files) -- same as executeWebhook but allows files 19 | local endpoint = f(endpoints.INTERACTION_WEBHOOK, id, token) 20 | return self:request("POST", endpoint, payload, nil, files) 21 | end 22 | 23 | function API:getWebhookMessage(id, token, msg_id) 24 | local endpoint = f(endpoints.INTERACTION_MESSAGES, id, token, msg_id) 25 | return self:request("GET", endpoint) 26 | end 27 | 28 | function API:deleteWebhookMessage(id, token, msg_id) 29 | local endpoint = f(endpoints.INTERACTION_MESSAGES, id, token, msg_id) 30 | return self:request("DELETE", endpoint) 31 | end 32 | 33 | function API:editWebhookMessage(id, token, msg_id, payload, files) 34 | local endpoint = f(endpoints.INTERACTION_MESSAGES, id, token, msg_id) 35 | return self:request("PATCH", endpoint, payload, nil, files) 36 | end 37 | 38 | function API:editMessage(channel_id, message_id, payload, files) -- patch Discordia's to allow files field 39 | local endpoint = f(endpoints.CHANNEL_MESSAGE, channel_id, message_id) 40 | return self:request("PATCH", endpoint, payload, nil, files) 41 | end 42 | 43 | return API 44 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 2.0.0 4 | 5 | - More stable returns on `Interaction.reply` initial replies. (Commit 2b559fa) 6 | - Add `interactionContextType` and `applicationIntegrationType` enumerators. (Commit 2b559fa) 7 | - Add new getters: `appPermissions`, `entitlements`, `context` and `integrationOwners`. (Commits 2b559fa, 696b70d) 8 | - Add a post-listener for `interactionCreate`, at `discordia_interactions.EventHandler.interaction_create_postlisteners`. (Commit 048c30b) 9 | - **Breaking Change**: Partial objects are now used for `Guild`, `Channel` and `Member`, no fetching is done behind the scene anymore. (Commits 5a9180e, 696b70d) 10 | - **Breaking Change**: `Interaction.replyDeferred` may now return a Message when `self.channel` is available. (Commit 2b559fa) 11 | - **Breaking Change**: Remove the support for a non-array input to `Interaction.autocomplete`. (Commit 2b559fa) 12 | - Fix `Interaction.editReply` when ID is not provided. (Commit 2b559fa) 13 | - Fix the autocomplete resolver using the wrong wrapper table. 14 | 15 | ## 1.2.1 16 | 17 | - Add support for interactions invoked in `GuildVoiceChannel` instances. (Commit 8832674) 18 | - Fix fetching issue on Guild caches. (Commit 8832674) 19 | - Fix attempts to index nil `self._channel` property. (Commit a9be099) 20 | - Fix `self._message` being `false` instead of `nil` on fetching failure. (Commit d0f95f5) 21 | - Fix inserting a nil Guild instance into cache when fetching fails. (Commit bfd410e) 22 | - Fix message flags being ignored if it had some flags already set. (Commit af32efc) 23 | 24 | ## 1.1.0 25 | 26 | - Add pre-listeners to EventHandler, allowing other extension authors to interact with the library. 27 | - Add an assert instead of returning nil when resolving. 28 | - Add the ability to wrap resolvers by other extensions to allow further user input. 29 | - Add support for modal responses. 30 | - Expose our internal EventHandler instance. 31 | -------------------------------------------------------------------------------- /libs/client/EventHandler.lua: -------------------------------------------------------------------------------- 1 | local Interaction = require("containers/Interaction") 2 | local events = { 3 | interaction_create_prelisteners = {}, 4 | interaction_create_postlisteners = {}, 5 | } 6 | 7 | local function emitListeners(listeners, ...) 8 | for _, v in pairs(listeners) do 9 | v(...) 10 | end 11 | end 12 | 13 | function events.INTERACTION_CREATE(d, client) 14 | local interaction = Interaction(d, client) 15 | emitListeners(events.interaction_create_prelisteners, interaction, client) 16 | client:emit("interactionCreate", interaction) 17 | emitListeners(events.interaction_create_postlisteners, interaction, client) 18 | end 19 | 20 | -- This code is part of Discordia 21 | local function checkReady(shard) 22 | for _, v in pairs(shard._loading) do 23 | if next(v) then return end 24 | end 25 | shard._ready = true 26 | shard._loading = nil 27 | collectgarbage() 28 | local client = shard._client 29 | client:emit('shardReady', shard._id) 30 | for _, other in pairs(client._shards) do 31 | if not other._ready then return end 32 | end 33 | return client:emit('ready') 34 | end 35 | 36 | function events.GUILD_CREATE(d, client, shard) 37 | if client._options.syncGuilds and not d.unavailable and not client._user._bot then 38 | shard:syncGuilds({d.id}) 39 | end 40 | local guild = client._guilds:get(d.id) 41 | if guild then 42 | if guild._partial then 43 | guild._partial = nil 44 | guild:_load(d) 45 | guild:_makeAvailable(d) 46 | return client:emit('guildCreate', guild) 47 | elseif guild._unavailable and not d.unavailable then 48 | guild:_load(d) 49 | guild:_makeAvailable(d) 50 | client:emit('guildAvailable', guild) 51 | end 52 | if shard._loading then 53 | shard._loading.guilds[d.id] = nil 54 | return checkReady(shard) 55 | end 56 | else 57 | guild = client._guilds:_insert(d) 58 | return client:emit('guildCreate', guild) 59 | end 60 | end 61 | 62 | return events 63 | -------------------------------------------------------------------------------- /examples/latency-example.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Usage: send `!new-button` command to get a message with components. 3 | 4 | Pressing the buttons will do stuff! 5 | ]] 6 | 7 | local timer = require 'timer' 8 | local discordia = require 'discordia' 9 | require 'discordia-interactions' 10 | 11 | local client = discordia.Client() 12 | client:enableIntents('messageContent') 13 | 14 | local Stopwatch = discordia.Stopwatch 15 | 16 | client:on('messageCreate', function (msg) 17 | -- Use discordia-extensions instead of this! 18 | -- this is just to get the example working 19 | if msg.content == '!new-button' then 20 | client._api:createMessage(msg.channel.id, { 21 | content = 'Here is a button!', 22 | components = { 23 | { 24 | type = 1, 25 | components = { 26 | { 27 | type = 2, 28 | label = 'API Ping', 29 | style = 1, 30 | custom_id = 'api_ping', 31 | }, 32 | { 33 | type = 2, 34 | label = 'API Ping Message', 35 | style = 3, 36 | custom_id = 'api_ping_msg', 37 | }, 38 | }, 39 | }, 40 | } 41 | }) 42 | end 43 | end) 44 | 45 | ---@param intr Interaction 46 | client:on('interactionCreate', function (intr) 47 | if intr.data.custom_id == 'api_ping' then 48 | local sw = Stopwatch() 49 | local msg, err = intr:replyDeferred(true) 50 | sw:stop() 51 | if not msg then 52 | return print(err) 53 | end 54 | intr:reply('The API latency (one-way) is ' .. (sw:getTime() / 2):toString()) 55 | timer.sleep(3000) 56 | intr:deleteReply() 57 | elseif intr.data.custom_id == 'api_ping_msg' then 58 | local sw = Stopwatch() 59 | local msg, err = intr:reply('Calculating latency...') 60 | sw:stop() 61 | if not msg then 62 | return print(err) 63 | end 64 | intr:editReply('The API latency (one-way) is ' .. (sw:getTime() / 2):toString()) 65 | timer.sleep(3000) 66 | intr:deleteReply() 67 | end 68 | end) 69 | 70 | client:run('Bot TOKEN') 71 | -------------------------------------------------------------------------------- /init.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Apache License 2.0 3 | 4 | Copyright (c) 2022-2024 Bilal2453 5 | 6 | Licensed under the Apache License, Version 2.0 (the "License"); 7 | you may not use this file except in compliance with the License. 8 | You may obtain a copy of the License at 9 | 10 | http://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, software 13 | distributed under the License is distributed on an "AS IS" BASIS, 14 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | See the License for the specific language governing permissions and 16 | limitations under the License. 17 | ]] 18 | 19 | local discordia = require("discordia") 20 | local EventHandler = require("client/EventHandler") 21 | local enums = require("enums") 22 | local API = require("client/API") 23 | 24 | -- Patch Discordia's enums to include some of the missing fields 25 | do 26 | local discordia_enums = discordia.enums 27 | local enum = discordia_enums.enum 28 | for k, v in pairs(enums) do 29 | if not discordia_enums[k] then -- compatibility with other extensions provided enums 30 | discordia_enums[k] = enum(v) 31 | else 32 | local new_enum = v 33 | for n, m in pairs(discordia_enums[k]) do 34 | if not new_enum[n] then 35 | new_enum[n] = m 36 | end 37 | end 38 | discordia_enums[k] = enum(new_enum) 39 | end 40 | end 41 | end 42 | 43 | -- Patch Discordia's API wrapper to include some of the missing endpoints 44 | -- Do note, one major patch (that should affect nothing) is of API.editMessage, was patched to include files support. 45 | do 46 | local discordia_api = discordia.class.classes.API 47 | API.request = discordia_api.request 48 | for k, v in pairs(API) do 49 | rawset(discordia_api, k, v) 50 | end 51 | end 52 | 53 | -- Patch Discordia's event handler to add interactionCreate event 54 | do 55 | local client = discordia.Client { -- tmp client object to quickly patch events 56 | logFile = '', -- do not create a log file 57 | } 58 | local events = client._events 59 | for k, v in pairs(EventHandler) do 60 | if rawget(events, k) then -- compatibility with other libraries 61 | local old_event = events[k] 62 | events[k] = function(...) 63 | v(...) 64 | return old_event(...) 65 | end 66 | else 67 | events[k] = v 68 | end 69 | end 70 | end 71 | 72 | return { 73 | EventHandler = EventHandler, 74 | Interaction = require("containers/Interaction"), 75 | resolver = require("client/resolver"), 76 | } 77 | -------------------------------------------------------------------------------- /libs/client/resolver.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | The code in this file is not injected into Discordia. 3 | It is only used internally in this library. 4 | 5 | Some of the code below was taken from original Discordia source code and modified to work 6 | for our use-case. This file is licensed under the Apache License 2.0. 7 | --]] 8 | 9 | --[[ 10 | Apache License 2.0 11 | 12 | Copyright (c) 2016-2024 SinisterRectus 13 | Copyright (c) 2021-2024 Bilal2453 14 | 15 | Licensed under the Apache License, Version 2.0 (the "License"); 16 | you may not use this file except in compliance with the License. 17 | You may obtain a copy of the License at 18 | 19 | http://www.apache.org/licenses/LICENSE-2.0 20 | 21 | Unless required by applicable law or agreed to in writing, software 22 | distributed under the License is distributed on an "AS IS" BASIS, 23 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 24 | See the License for the specific language governing permissions and 25 | limitations under the License. 26 | ]] 27 | 28 | local fs = require("fs") 29 | local ffi = require("ffi") 30 | local splitPath = require("pathjoin").splitPath 31 | 32 | local istype = ffi.istype 33 | local int64_t = ffi.typeof('int64_t') 34 | local uint64_t = ffi.typeof('uint64_t') 35 | local resolver = {} 36 | 37 | local function int(obj) 38 | local t = type(obj) 39 | if t == 'string' then 40 | if tonumber(obj) then 41 | return obj 42 | end 43 | elseif t == 'cdata' then 44 | if istype(int64_t, obj) or istype(uint64_t, obj) then 45 | return tostring(obj):match('%d*') 46 | end 47 | elseif t == 'number' then 48 | return string.format('%i', obj) 49 | end 50 | end 51 | 52 | function resolver.messageId(obj) 53 | if type(obj) == "table" and obj.__name == "Message" then 54 | return obj.id 55 | end 56 | return int(obj) 57 | end 58 | 59 | function resolver.file(obj, files) 60 | local obj_type = type(obj) 61 | if obj_type == "string" then 62 | local data, err = fs.readFileSync(obj) 63 | if not data then 64 | return nil, err 65 | end 66 | files = files or {} 67 | table.insert(files, {table.remove(splitPath(obj)), data}) 68 | elseif obj_type == "table" and type(obj[1]) == "string" and type(obj[2]) == "string" then 69 | files = files or {} 70 | table.insert(files, obj) 71 | else 72 | return nil, "Invalid file object: " .. tostring(obj) 73 | end 74 | return files 75 | end 76 | 77 | function resolver.mention(obj, mentions) 78 | if type(obj) == "table" and obj.mentionString then 79 | mentions = mentions or {} 80 | table.insert(mentions, obj.mentionString) 81 | else 82 | return nil, "Unmentionable object: " .. tostring(obj) 83 | end 84 | return mentions 85 | end 86 | 87 | local message_blacklisted_fields = { 88 | content = true, code = true, mention = true, 89 | mentions = true, file = true, files = true, 90 | reference = true, payload_json = true, embed = true, 91 | } 92 | 93 | resolver.message_resolvers = {} 94 | resolver.message_wrappers = {} 95 | 96 | function resolver.message(content) 97 | local err 98 | for _, v in pairs(resolver.message_resolvers) do 99 | local c = v(content) 100 | if c then 101 | content = c 102 | break 103 | end 104 | end 105 | if type(content) == "table" then 106 | ---@type table 107 | local tbl = content 108 | content = tbl.content 109 | 110 | if type(tbl.code) == "string" then 111 | content = string.format("```%s\n%s\n```", tbl.code, content) 112 | elseif tbl.code == true then 113 | content = string.format("```\n%s\n```", content) 114 | end 115 | 116 | local mentions 117 | if tbl.mention then 118 | mentions, err = resolver.mention(tbl.mention) 119 | if err then 120 | return nil, err 121 | end 122 | end 123 | if type(tbl.mentions) == "table" then 124 | for _, mention in ipairs(tbl.mentions) do 125 | mentions, err = mention(mention, mentions) 126 | if err then 127 | return nil, err 128 | end 129 | end 130 | end 131 | 132 | if mentions then 133 | table.insert(mentions, content) 134 | content = table.concat(mentions, ' ') 135 | end 136 | 137 | if tbl.embed then 138 | if type(tbl.embeds) == 'table' then 139 | tbl.embeds[#tbl.embeds + 1] = tbl.embed 140 | elseif tbl.embeds == nil then 141 | tbl.embeds = {tbl.embed} 142 | end 143 | end 144 | 145 | local files 146 | if tbl.file then 147 | files, err = resolver.file(tbl.file) 148 | if err then 149 | return nil, err 150 | end 151 | end 152 | if type(tbl.files) == "table" then 153 | for _, file in ipairs(tbl.files) do 154 | files, err = resolver.file(file, files) 155 | if err then 156 | return nil, err 157 | end 158 | end 159 | end 160 | 161 | local refMessage, refMention 162 | if tbl.reference then 163 | refMessage = {message_id = resolver.messageId(tbl.reference.message)} 164 | refMention = { 165 | parse = {"users", "roles", "everyone"}, 166 | replied_user = not not tbl.reference.mention, 167 | } 168 | end 169 | 170 | local result = { 171 | content = content, 172 | message_reference = tbl.message_reference or refMessage, 173 | allowed_mentions = tbl.allowed_mentions or refMention, 174 | } 175 | for k, v in pairs(tbl) do 176 | if not message_blacklisted_fields[k] then 177 | result[k] = v 178 | end 179 | end 180 | 181 | for _, v in pairs(resolver.message_wrappers) do 182 | v(result, files) 183 | end 184 | 185 | return result, files 186 | else 187 | return {content = content} 188 | end 189 | end 190 | 191 | resolver.autocomplete_resolvers = {} 192 | resolver.autocomplete_wrappers = {} 193 | 194 | function resolver.autocomplete(choices) 195 | for _, v in pairs(resolver.autocomplete_resolvers) do 196 | local c = v(choices) 197 | if c then 198 | choices = c 199 | break 200 | end 201 | end 202 | if type(choices) ~= "table" then return end 203 | for _, v in pairs(resolver.autocomplete_wrappers) do 204 | v(choices) 205 | end 206 | return choices 207 | end 208 | 209 | resolver.modal_resolvers = {} 210 | resolver.modal_wrappers = {} 211 | 212 | function resolver.modal(content) 213 | for _, v in pairs(resolver.modal_resolvers) do 214 | local c = v(content) 215 | if c then 216 | content = c 217 | break 218 | end 219 | end 220 | assert(type(content) == "table") 221 | for _, v in pairs(resolver.modal_wrappers) do 222 | v(content) 223 | end 224 | return content 225 | end 226 | 227 | return resolver 228 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | ![Version](https://img.shields.io/github/v/release/Bilal2453/discordia-interactions) 4 | ![License](https://img.shields.io/github/license/Bilal2453/discordia-interactions) 5 | 6 | discordia-interactions is an extension library that enables [Discordia](https://github.com/SinisterRectus/discordia) to receive and respond to [Discord Interactions](https://discord.com/developers/docs/interactions/receiving-and-responding#interaction-response-object-interaction-callback-type). 7 | This is done over the gateway websocket connection that [Discordia](https://github.com/SinisterRectus/discordia) offers, and aims to be very extensible so other extensions and libraries can build upon without having to worry about compatibility with each other. 8 | 9 | This only implements the interactions portion, for App commands such as Message Components or Slash Commands you need to use a second extension library alongside this one, for example [discordia-components](https://github.com/Bilal2453/discordia-components/) adds support for Message Components such as buttons and select menus. 10 | 11 | ## Supported responses 12 | 13 | - [x] Text Replies. 14 | - [x] Deferred Text Replies. 15 | - [x] Update Replies. 16 | - [x] Autocomplete Replies. 17 | - [x] Modal Replies. 18 | - [ ] Premium Required Replies. (deprecated by Discord) 19 | - [ ] Launch Activity Replies. 20 | 21 | ## Installing 22 | 23 | You can install this extension with Lit using the following steps: 24 | 25 | 1. Install `lit` if not already installed, see [Luvit installation guide](https://luvit.io/install.html) and [Discordia installation Tutorial](https://github.com/SinisterRectus/Discordia/wiki/Installing-Discordia). 26 | 2. Open a terminal (PowerShell or CMD on Windows) and preferably `cd` into your bot's directory (e.x. `cd C:\Users\X\Desktop\MyBot` then enter). 27 | 3. In the terminal, execute `lit install Bilal2453/discordia-interactions`. 28 | (Note: if you have not set up your PATH, you might have to do `./lit` instead of just `lit`) 29 | 30 | Once that is done, you should see Lit print the message `done: success`, indicating you are now ready to require the extension from your Discordia project. 31 | 32 | Due to a bug in Lit, the Lit upstream version of this library won't install, you have to manually clone it. 33 | 34 | You may also install the latest main branch by replacing the command in step 3 with: 35 | ```sh 36 | git clone https://github.com/Bilal2453/discordia-interactions.git ./deps/discordia-interactions` 37 | ``` 38 | 39 | ## Documentation 40 | 41 | The library is fully documented over at the [Wiki](https://github.com/Bilal2453/discordia-interactions/wiki), for any questions and support feel free to contact me on our [Discordia server](https://discord.gg/sinisterware), especially if you are writing a library using this! 42 | 43 | ## Building other extensions 44 | 45 | This library offers multiple wrapping stages for other libraries to integrate with, allowing you to control the data flow and hook at different stages. For example I built a little Slash command library for the [Discordia Wiki bot](https://github.com/Bilal2453/discordia-wiki-bot), and [this is](https://github.com/Bilal2453/discordia-wiki-bot/blob/da42b124646bc718e17db89fac0c15456da4520f/libs/slash.lua#L140-L144) how it hooks into discordia-interactions to implement a new `slashCommand` event. 46 | You can hook an `interactionCreate` event pre-listener, resolve user inputs to Interaction methods with your own resolver, or wrap resolved values. 47 | 48 | ## Deprecation form Discordia 49 | 50 | When using a library, you make a contract with that library, it's that what it documents is what it will do. In Discordia for example when the wiki says 51 | that `Guild.memberCount` is never `nil`, then you can simply trust that and never check whether the property is nil or not, because Discordia guarantees that as part of the contract it made with you. 52 | 53 | By using any third-party extension, said contract is automatically broken. (Note: So is modifying the library internally, the same arguments can be made against any method of modifying the library! not just extensions.) 54 | 55 | One thing that Discord broke their promises with are Partial Objects. Partial objects are normal objects but with almost none of the required properties that Discordia promises you to always have, for example `Guild.memberCount` is `nil` because Discord does not send that on partial objects. It only sends the bare minimum that represents what a thing is, for example a partial Guild only has the guild ID and the supported features array. 56 | 57 | discordia-interactions pre 2.0.0 version tried to keep the oauth Discordia makes with the user by selling its soul to the devil and transparently request the full objects from the API gateway instead of using the partial objects Discord sends, breaking one of the most important design philosophies of Discordia (that of never making additional HTTP requests in the background) but keeping away the complexity Discord introduced with partial objects from the end user. 58 | 59 | Sadly, when Discord introduced User Installed Apps, this is no more possible to keep, because in that context *we cannot request objects from the API*, as they are pretty much kept a secret and only the minimum amount of information is provided, as such starting from version 2.0.0, the library breaks this contract and makes a new one with you: Any object obtained by an Interaction is partial and most likely *WILL NOT* have all of the properties set; if you want the full object request it from the API, or otherwise operate with those objects with caution. 60 | 61 | The partial objects more specifically are `Guild`, `Channel` and `Member`. They are cached by Discordia, so iterating the Discordia cache might get you one of them, when the full version is given by Discord (if at all) then cached version is updated with the full one. 62 | You may check if a Guild instance is partial by checking `Guild._partial` (you may only check Guild). 63 | 64 | This change sadly adds complexity to the user, but it was absolutely required in order to support User Installed Apps and continue with the development. 65 | 66 | ## Examples 67 | 68 | **Take a look at the [examples](https://github.com/Bilal2453/discordia-interactions/tree/main/examples) directory for the actual examples.** 69 | 70 | Here is a bit of a usage run down: 71 | 72 | ```lua 73 | local discordia = require("discordia") 74 | require("discordia-interactions") -- Adds the interactionCreate event and applies other patches 75 | 76 | local client = discordia.Client() 77 | local intrType = discordia.enums.interactionType 78 | 79 | client:on("interactionCreate", function(interaction) 80 | -- Ephemeral reply to an interaction 81 | interaction:reply("Hello There! This is ephemeral reply", true) 82 | -- Send a followup reply 83 | interaction:reply { 84 | -- send a file with interactions info 85 | file = { -- identical to the Discordia field 86 | "info.txt", 87 | "Bot received an interaction of the type " .. intrType(interaction.type) .. " from the user " .. interaction.user.name 88 | }, 89 | -- try to mention the user 90 | mention = interaction.member or interaction.user, 91 | -- suppress all mentions 92 | allowed_mentions = { -- if Discordia's send doesn't handle said field, library'll treat it as raw 93 | parse = {} 94 | } 95 | } 96 | if interaction.type == intrType.messageComponent then 97 | -- update the message the component was attached to 98 | interaction:update { 99 | embed = { 100 | title = "Wow! You have actually used the component!", 101 | color = 0x00ff00, 102 | } 103 | } 104 | end 105 | end) 106 | 107 | client:run("Bot TOKEN") 108 | ``` 109 | 110 | ## Developers Notes 111 | 112 | While it is not hard to merge this extension into Discordia, you are discouraged from doing that. From what I have noticed multiple people are merging this into the Discordia code instead of just using the extensions, and while they might have some reasons to do that, it will make life a lot harder for maintainability, for example when a new discordia-interactions release comes out it will become pretty much a manual job to hand pick the patches to apply, and it creates *more* incompatibility instead of solving any, this is *EXPLICITLY* an extension for good reasons, otherwise I could've simply PRed this into Discordia and called it a day, which is a lot easier to me than developing an extension. 113 | 114 | ### License 115 | 116 | This project is licensed under the Apache License 2.0, see [LICENSE] for more information. 117 | Make sure to include the original copyright notice when copying! 118 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /docgen.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Apache License 2.0 3 | 4 | Copyright (c) 2016-2021 SinisterRectus (Original author) 5 | Copyright (c) 2021-2022 Bilal2453 (Heavily modified to partially support EmmyLua) 6 | 7 | Licensed under the Apache License, Version 2.0 (the "License"); 8 | you may not use this file except in compliance with the License. 9 | You may obtain a copy of the License at 10 | 11 | http://www.apache.org/licenses/LICENSE-2.0 12 | 13 | Unless required by applicable law or agreed to in writing, software 14 | distributed under the License is distributed on an "AS IS" BASIS, 15 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | See the License for the specific language governing permissions and 17 | limitations under the License. 18 | ]] 19 | 20 | local fs = require('fs') 21 | local pathjoin = require('pathjoin') 22 | 23 | local insert, sort, concat = table.insert, table.sort, table.concat 24 | local format = string.format 25 | local pathJoin = pathjoin.pathJoin 26 | 27 | local function scan(dir) 28 | for fileName, fileType in fs.scandirSync(dir) do 29 | local path = pathJoin(dir, fileName) 30 | if fileType == 'file' then 31 | coroutine.yield(path) 32 | else 33 | scan(path) 34 | end 35 | end 36 | end 37 | 38 | local function match(s, pattern) -- only useful for one capture 39 | return assert(s:match(pattern), s) 40 | end 41 | 42 | local function gmatch(s, pattern, hash) -- only useful for one capture 43 | local tbl = {} 44 | if hash then 45 | for k in s:gmatch(pattern) do 46 | tbl[k] = true 47 | end 48 | else 49 | for v in s:gmatch(pattern) do 50 | insert(tbl, v) 51 | end 52 | end 53 | return tbl 54 | end 55 | 56 | local function matchType(c) 57 | local line 58 | for _, s in ipairs(c) do 59 | if s:find '' then return 'ignore' end 60 | end 61 | for _, s in ipairs(c) do 62 | local m = s:match('%-%-%-%s*@(%S+)') 63 | if m then line = m; break end 64 | end 65 | return line 66 | end 67 | 68 | local function matchComments(s) 69 | local lines = {} 70 | local last_line = {} 71 | for l in s:gmatch('[^\n]*\n?') do 72 | if l:match '^%-%-' then 73 | last_line[#last_line + 1] = l 74 | elseif #last_line > 0 then 75 | last_line[#last_line + 1] = l 76 | lines[#lines+1] = last_line 77 | last_line = {} 78 | end 79 | end 80 | return lines 81 | end 82 | 83 | local function matchClassName(s) 84 | return match(s, '@class ([^%s:]+)') 85 | end 86 | 87 | local function matchMethodName(s) 88 | local m = s:match 'function%s*.-[:.]%s*([_%w]+)' 89 | or s:match 'function%s*([_%w]+)' 90 | or s:match '([_%w]+)%s*=%s*function' 91 | if not m then error(s) end 92 | return m 93 | end 94 | 95 | local function matchDescription(c) 96 | local desc = {} 97 | for _, v in ipairs(c) do 98 | local n, m = v:match('%-%-%-*%s*@'), v:match('%-%-+%s*(.+)') 99 | if not n and m then 100 | m = m:gsub('', '') -- invisible custom tags 101 | desc[#desc+1] = m 102 | end 103 | end 104 | return table.concat(desc):gsub('^%s+', ''):gsub('%s+$', '') 105 | end 106 | 107 | local function matchParents(c) 108 | local line 109 | for _, s in ipairs(c) do 110 | local m = s:match('@class [%a_][%a_%-%.%*]+%s*:%s*([^\n#@%-]+)') 111 | if m then line = m; break end 112 | end 113 | if not line then return {} end 114 | local ret = {} 115 | for s in line:gmatch('[^,]+') do 116 | ret[#ret + 1] = s:match('%S+'):gsub('%s+', '') 117 | end 118 | return ret 119 | end 120 | 121 | local function matchReturns(s) 122 | return gmatch(s, '@return (%S+)') 123 | end 124 | 125 | local function matchTags(s) 126 | local ret = {} 127 | for m in s:gmatch '' do 128 | ret[m] = true 129 | end 130 | return ret 131 | end 132 | 133 | local function matchMethodTags(s) 134 | local ret = {} 135 | for m in s:gmatch '' do 136 | ret[m] = true 137 | end 138 | return ret 139 | end 140 | 141 | local function matchProperties(s) 142 | local ret = {} 143 | for n, t, d in s:gmatch '@field%s*(%S+)%s*(%S+)%s*([^\n]*)' do 144 | ret[#ret+1] = { 145 | name = n, 146 | type = t, 147 | desc = d or '', 148 | } 149 | end 150 | return ret 151 | end 152 | 153 | local function matchParameters(c) 154 | local ret = {} 155 | for _, s in ipairs(c) do 156 | local param_name, optional, param_type = s:match('@param%s*([^%s%?]+)%s*(%??)%s*(%S+)') 157 | if param_name then 158 | ret[#ret+1] = {param_name, param_type, optional == '?'} 159 | end 160 | end 161 | if #ret > 0 then return ret end 162 | 163 | for _, s in ipairs(c) do 164 | local params = s:match('@type%s*fun%s*%((.-)%)') 165 | if not params then goto continue end 166 | for pp in params:gmatch('[^,]+') do 167 | local param_name, optional = pp:match('([%w_%-]+)%s*(%??)') 168 | local param_type = pp:match(':%s*(.+)') 169 | if param_name then 170 | ret[#ret+1] = {param_name, param_type, optional == '?'} 171 | end 172 | end 173 | ::continue:: 174 | end 175 | return ret 176 | end 177 | 178 | local function matchMethod(s, c) 179 | return { 180 | name = matchMethodName(c[#c]), 181 | desc = matchDescription(c), 182 | parameters = matchParameters(c), 183 | returns = matchReturns(s), 184 | tags = matchTags(s), 185 | } 186 | end 187 | 188 | ---- 189 | 190 | local docs = {} 191 | 192 | local function newClass() 193 | 194 | local class = { 195 | methods = {}, 196 | statics = {}, 197 | } 198 | 199 | local function init(s, c) 200 | class.name = matchClassName(s) 201 | class.parents = matchParents(c) 202 | class.desc = matchDescription(c) 203 | class.parameters = matchParameters(c) 204 | class.tags = matchTags(s) 205 | class.methodTags = matchMethodTags(s) 206 | class.properties = matchProperties(s) 207 | assert(not docs[class.name], 'duplicate class: ' .. class.name) 208 | docs[class.name] = class 209 | end 210 | 211 | return class, init 212 | 213 | end 214 | 215 | for f in coroutine.wrap(scan), './libs' do 216 | local d = assert(fs.readFileSync(f)) 217 | 218 | local class, initClass = newClass() 219 | local comments = matchComments(d) 220 | for i = 1, #comments do 221 | local s = table.concat(comments[i], '\n') 222 | local t = matchType(comments[i]) 223 | if t == 'ignore' then 224 | goto continue 225 | elseif t == 'class' then 226 | initClass(s, comments[i]) 227 | elseif t == 'param' or t == 'return' then 228 | local method = matchMethod(s, comments[i]) 229 | for k, v in pairs(class.methodTags) do 230 | method.tags[k] = v 231 | end 232 | method.class = class 233 | insert(method.tags.static and class.statics or class.methods, method) 234 | end 235 | ::continue:: 236 | end 237 | end 238 | 239 | ---- 240 | 241 | local output = 'docs' 242 | 243 | local function link(str) 244 | if type(str) == 'table' then 245 | local ret = {} 246 | for i, v in ipairs(str) do 247 | ret[i] = link(v) 248 | end 249 | return concat(ret, ', ') 250 | else 251 | local ret, optional = {}, false 252 | if str:match('%?$') then 253 | str = str:gsub('%?$', '') 254 | optional = true 255 | end 256 | for t in str:gmatch('[^|]+') do 257 | insert(ret, docs[t] and format('[[%s]]', t) or t) 258 | end 259 | if optional then 260 | insert(ret, 'nil') 261 | end 262 | return concat(ret, '/') 263 | end 264 | end 265 | 266 | local function sorter(a, b) 267 | return a.name < b.name 268 | end 269 | 270 | local function writeHeading(f, heading) 271 | f:write('## ', heading, '\n\n') 272 | end 273 | 274 | local function writeProperties(f, properties) 275 | sort(properties, sorter) 276 | f:write('| Name | Type | Description |\n') 277 | f:write('|-|-|-|\n') 278 | for _, v in ipairs(properties) do 279 | f:write('| ', v.name, ' | ', link(v.type), ' | ', v.desc, ' |\n') 280 | end 281 | f:write('\n') 282 | end 283 | 284 | local function writeParameters(f, parameters) 285 | f:write('(') 286 | local optional 287 | if #parameters > 0 then 288 | for i, param in ipairs(parameters) do 289 | f:write(param[1]) 290 | if i < #parameters then 291 | f:write(', ') 292 | end 293 | optional = param[3] 294 | param[2] = param[2]:gsub('|', '/') 295 | end 296 | f:write(')\n\n') 297 | if optional then 298 | f:write('| Parameter | Type | Optional |\n') 299 | f:write('|-|-|:-:|\n') 300 | for _, param in ipairs(parameters) do 301 | local o = param[3] and '✔' or '' 302 | f:write('| ', param[1], ' | ', link(param[2]), ' | ', o, ' |\n') 303 | end 304 | f:write('\n') 305 | else 306 | f:write('| Parameter | Type |\n') 307 | f:write('|-|-|\n') 308 | for _, param in ipairs(parameters) do 309 | f:write('| ', param[1], ' | ', link(param[2]), ' |\n') 310 | end 311 | f:write('\n') 312 | end 313 | else 314 | f:write(')\n\n') 315 | end 316 | end 317 | 318 | local methodTags = {} 319 | 320 | methodTags['http'] = 'This method always makes an HTTP request.' 321 | methodTags['http?'] = 'This method may make an HTTP request.' 322 | methodTags['ws'] = 'This method always makes a WebSocket request.' 323 | methodTags['mem'] = 'This method only operates on data in memory.' 324 | 325 | local function checkTags(tbl, check) 326 | for i, v in ipairs(check) do 327 | if tbl[v] then 328 | for j, w in ipairs(check) do 329 | if i ~= j then 330 | if tbl[w] then 331 | return error(string.format('mutually exclusive tags encountered: %s and %s', v, w), 1) 332 | end 333 | end 334 | end 335 | end 336 | end 337 | end 338 | 339 | local function writeMethods(f, methods) 340 | 341 | sort(methods, sorter) 342 | for _, method in ipairs(methods) do 343 | 344 | f:write('### ', method.name) 345 | writeParameters(f, method.parameters) 346 | f:write(method.desc, '\n\n') 347 | 348 | local tags = method.tags 349 | checkTags(tags, {'http', 'http?', 'mem'}) 350 | checkTags(tags, {'ws', 'mem'}) 351 | 352 | for k in pairs(tags) do 353 | if k ~= 'static' then 354 | assert(methodTags[k], k) 355 | f:write('*', methodTags[k], '*\n\n') 356 | end 357 | end 358 | 359 | f:write('**Returns:** ', link(method.returns), '\n\n----\n\n') 360 | 361 | end 362 | 363 | end 364 | 365 | if not fs.existsSync(output) then 366 | fs.mkdirSync(output) 367 | end 368 | 369 | local function collectParents(parents, k, ret, seen) 370 | ret = ret or {} 371 | seen = seen or {} 372 | for _, parent in ipairs(parents) do 373 | parent = docs[parent] 374 | if parent then 375 | for _, v in ipairs(parent[k]) do 376 | if not seen[v] then 377 | seen[v] = true 378 | insert(ret, v) 379 | end 380 | end 381 | end 382 | if parent then 383 | collectParents(parent.parents, k, ret, seen) 384 | end 385 | end 386 | return ret 387 | end 388 | 389 | for _, class in pairs(docs) do 390 | 391 | local f = io.open(pathJoin(output, class.name .. '.md'), 'w') 392 | 393 | local parents = class.parents 394 | local parentLinks = link(parents) 395 | 396 | if next(parents) then 397 | f:write('#### *extends ', parentLinks, '*\n\n') 398 | end 399 | 400 | f:write(class.desc, '\n\n') 401 | 402 | checkTags(class.tags, {'interface', 'abstract', 'patch'}) 403 | if class.tags.interface then 404 | writeHeading(f, 'Constructor') 405 | f:write('### ', class.name) 406 | writeParameters(f, class.parameters) 407 | elseif class.tags.abstract then 408 | f:write('*This is an abstract base class. Direct instances should never exist.*\n\n') 409 | elseif class.tags.patch then 410 | f:write("*This is a patched class.\nFor full usage refer to the Discordia Wiki, only patched methods and properities are documented here.*\n\n") 411 | else 412 | f:write('*Instances of this class should not be constructed by users.*\n\n') 413 | end 414 | 415 | local properties = collectParents(parents, 'properties') 416 | if next(properties) then 417 | writeHeading(f, 'Properties Inherited From ' .. parentLinks) 418 | writeProperties(f, properties) 419 | end 420 | 421 | if next(class.properties) then 422 | writeHeading(f, 'Properties') 423 | writeProperties(f, class.properties) 424 | end 425 | 426 | local statics = collectParents(parents, 'statics') 427 | if next(statics) then 428 | writeHeading(f, 'Static Methods Inherited From ' .. parentLinks) 429 | writeMethods(f, statics) 430 | end 431 | 432 | local methods = collectParents(parents, 'methods') 433 | if next(methods) then 434 | writeHeading(f, 'Methods Inherited From ' .. parentLinks) 435 | writeMethods(f, methods) 436 | end 437 | 438 | if next(class.statics) then 439 | writeHeading(f, 'Static Methods') 440 | writeMethods(f, class.statics) 441 | end 442 | 443 | if next(class.methods) then 444 | writeHeading(f, 'Methods') 445 | writeMethods(f, class.methods) 446 | end 447 | 448 | f:close() 449 | 450 | end 451 | -------------------------------------------------------------------------------- /libs/containers/Interaction.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Copyright 2021-2024 Bilal2453 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | ]] 16 | 17 | local discordia = require("discordia") 18 | local resolver = require("client/resolver") 19 | local enums = require("enums") 20 | local bit = require("bit") 21 | 22 | local bor = bit.bor 23 | local class = discordia.class 24 | local classes = class.classes 25 | local intrType = enums.interactionType 26 | local messageFlag = assert(enums.messageFlag) 27 | local resolveMessage = resolver.message 28 | local callbackType = enums.interactionCallbackType 29 | local channelType = discordia.enums.channelType 30 | 31 | local Snowflake = classes.Snowflake 32 | local Permissions = discordia.Permissions 33 | 34 | ---Represents a [Discord Interaction](https://discord.com/developers/docs/interactions/receiving-and-responding#interactions) 35 | ---allowing you to receive and respond to user interactions. 36 | --- 37 | ---Note that on `interactionCreate` event Discord sends *partial* Guild/Channel/Member objects, 38 | ---that means, any object obtained with the Interaction may or may not have specific properties set. 39 | ---@class Interaction: Snowflake 40 | ---@field applicationId string The application's unique snowflake ID. 41 | ---@field type number The Interaction's type, see `enums.interactionType`. 42 | ---@field guildId string? The Snowflake ID of the guild the interaction was sent from, if any. 43 | ---@field guild Guild? The Guild object the interaction was sent from. Equivalent to `Client:getGuild(Interaction.guildId)`. 44 | ---@field channelId string The Snowflake ID of the channel the interaction was sent from. Should always be provided, but keep in mind Discord flags it as optional for future-proofing. 45 | ---@field channel Channel? The Channel object the interaction was sent from. Equivalent to `Client:getChannel(Interaction.channelId)`. Can be `GuildTextChannel`, `GuildVoiceChannel` or `PrivateChannel`. 46 | ---@field message Message? The message the components that invoked this interaction are attached to. Only provided for components-based interactions. 47 | ---@field member Member? The member who invoked this interaction, if it was invoked in the context of a guild. 48 | ---@field user User? The user that invoked this interaction, should be always available but always check. 49 | ---@field data table The raw data of the interaction. See [Interaction Data Structure](https://discord.com/developers/docs/interactions/receiving-and-responding#interaction-object-resolved-data-structure). Should be always available but Discord may not provide it in the future for some scenarios. 50 | ---@field token string The interaction token. This is a secret and shouldn't be exposed, if leaked anyone can send messages on behalf of your bot. 51 | ---@field version string The interaction version, currently this is always set to `1`. 52 | ---@field appPermissions Permissions The permissions the app has in the source location of the interaction. 53 | ---@field locale string? The locale settings of the user who executed this interaction, see [languages](https://discord.com/developers/docs/reference#locales) for list of possible values. Always available except on PING interactions. 54 | ---@field guildLocale string The guild's preferred locale, if the interaction was executed in a guild, see [languages](https://discord.com/developers/docs/reference#locales) for list of possible values. 55 | ---@field entitlements table An array of raw [Entitlement](https://discord.com/developers/docs/resources/entitlement#entitlement-object) objects for monetized apps the user that invoked this interaction has. 56 | ---@field integrationOwners table Mapping of installation contexts that the interaction was authorized for to related user or guild IDs. See [Authorizing Integration Owners Object](https://discord.com/developers/docs/interactions/receiving-and-responding#interaction-object-authorizing-integration-owners-object) for details 57 | ---@field context number? The context in which this interaction was invoked, see `enums.interactionContextType`. 58 | --- 59 | ---@type Interaction | fun(data: table, parent: Client): Interaction 60 | local Interaction, get = class("Interaction", Snowflake) 61 | 62 | ---@type table 63 | local getter = get 64 | 65 | ---@param data table 66 | ---@param parent Client 67 | ---@protected 68 | --- 69 | function Interaction:__init(data, parent) 70 | Snowflake.__init(self, data, parent) 71 | self._data = data.data 72 | self._initialRes = false -- have we sent a response yet? 73 | self._deferred = false -- is the response we sent (if we have) deferred? 74 | self:_loadMore(data) 75 | end 76 | 77 | ---@protected 78 | function Interaction:_load(data) 79 | Snowflake._load(self, data) 80 | return self:_loadMore(data) 81 | end 82 | 83 | ---@protected 84 | function Interaction:_loadMore(data) 85 | self:_loadGuild(data) 86 | self:_loadChannel(data) 87 | self:_loadMember(data) 88 | self:_loadMessage(data) 89 | self._entitlements = data.entitlements 90 | self._authorizing_integration_owners = data.authorizing_integration_owners 91 | end 92 | 93 | ---@protected 94 | function Interaction:_loadGuild(data) 95 | if not data.guild_id then 96 | return 97 | end 98 | -- retrieve guild from cache if possible 99 | self._guild = self.parent:getGuild(data.guild_id) 100 | if self._guild then 101 | return 102 | end 103 | -- use the partial object 104 | if data.guild then 105 | local guild = data.guild 106 | -- required fields for initialization 107 | guild.stickers = guild.stickers or {} 108 | guild.emojis = guild.emojis or {} 109 | guild.roles = guild.roles or {} 110 | -- create and cache the partial guild 111 | self._guild = self.parent._guilds:_insert(guild) 112 | self._guild._partial = true 113 | end 114 | end 115 | 116 | local function insertChannel(client, data, parent) 117 | if data.guild_id and parent then 118 | if data.type == channelType.text or data.type == channelType.news then 119 | return parent._text_channels:_insert(data) 120 | elseif data.type == channelType.voice then 121 | return parent._voice_channels:_insert(data) 122 | elseif data.type == channelType.category then 123 | return parent._categories:_insert(data) 124 | end 125 | elseif data.type == channelType.private then 126 | return client._private_channels:_insert(data) 127 | elseif data.type == channelType.group then 128 | return client._group_channels:_insert(data) 129 | end 130 | end 131 | 132 | ---@protected 133 | function Interaction:_loadChannel(data) 134 | local channelId = data.channel_id 135 | if not channelId then 136 | return 137 | end 138 | -- first try retrieving it from cache 139 | self._channel = self.parent:getChannel(channelId) 140 | if self._channel then 141 | return 142 | end 143 | -- otherwise, use the partial channel object 144 | if data.channel then 145 | data.channel.permission_overwrites = {} 146 | self._channel = insertChannel(self.parent, data.channel, self._guild) 147 | end 148 | end 149 | 150 | ---@protected 151 | function Interaction:_loadMember(data) 152 | if data.member and self._guild then 153 | self._member = self._guild._members:_insert(data.member) 154 | self._user = self.parent._users:_insert(data.member.user) 155 | elseif data.user then 156 | self._user = self.parent._users:_insert(data.user) 157 | end 158 | end 159 | 160 | ---@protected 161 | function Interaction:_loadMessage(data) 162 | if not data.message or not self._channel then 163 | return 164 | end 165 | self._message = self._channel._messages:_insert(data.message) 166 | end 167 | 168 | ---@protected 169 | ---@return (Message|boolean)?, string? err 170 | function Interaction:_sendMessage(payload, files, deferred) 171 | local data, err = self.parent._api:createInteractionResponse(self.id, self._token, { 172 | type = deferred and callbackType.deferredChannelMessage or callbackType.channelMessage, 173 | data = payload, 174 | }, files) 175 | if data then 176 | self._initialRes = true 177 | self._deferred = deferred or false 178 | if self._channel and self._channel._messages then 179 | return self._channel._messages:_insert(data.resource.message) 180 | else 181 | return true 182 | end 183 | else 184 | return nil, err 185 | end 186 | end 187 | 188 | ---@protected 189 | ---@return (Message|boolean)?, string? err 190 | function Interaction:_sendFollowup(payload, files) 191 | local data, err = self.parent._api:createWebhookMessage(self._application_id, self._token, payload, files) 192 | if data then 193 | if self._channel then 194 | return self._channel._messages:_insert(data) 195 | else 196 | return true 197 | end 198 | else 199 | return nil, err 200 | end 201 | end 202 | 203 | ---Sends an interaction reply. An initial response is sent on the first call, 204 | ---if an initial response has already been sent a followup message is sent instead. 205 | ---If the initial response was a deferred response, calling this will edit the deferred message. 206 | --- 207 | ---Returns Message on success, otherwise `nil, err`. 208 | ---If `Interaction.channel` was not available, `true` will be returned instead of Message. 209 | ---@param content string|table 210 | ---@param isEphemeral? boolean 211 | ---@return Message|boolean 212 | function Interaction:reply(content, isEphemeral) 213 | isEphemeral = isEphemeral and true or type(content) == "table" and content.ephemeral 214 | local msg, files = resolveMessage(content) 215 | if not msg then 216 | return nil, files 217 | end 218 | -- handle flag masking 219 | if isEphemeral then 220 | msg.flags = bor(type(msg.flags) == "number" and msg.flags or 0, messageFlag.ephemeral) 221 | end 222 | -- choose desired method depending on the context 223 | local method 224 | if self._initialRes or self._deferred then 225 | method = self._sendFollowup 226 | else 227 | method = self._sendMessage 228 | end 229 | return method(self, msg, files) 230 | end 231 | 232 | ---Sends a deferred interaction reply. 233 | ---Deferred replies can only be sent as initial responses. 234 | ---A deferred reply displays "Bot is thinking..." to users, and once `:reply` is called again, the deferred message will be edited. 235 | --- 236 | ---Returns Message on success, otherwise `nil, err`. 237 | ---If `Interaction.channel` was not available, `true` will be returned instead of Message. 238 | ---@param isEphemeral? boolean 239 | ---@return Message|boolean 240 | function Interaction:replyDeferred(isEphemeral) 241 | assert(not self._initialRes, "only the initial response can be deferred") 242 | local msg = isEphemeral and {flags = messageFlag.ephemeral} or nil 243 | return self:_sendMessage(msg, nil, true) 244 | end 245 | 246 | ---Fetches a previously sent interaction response. 247 | ---If `id` was not provided, the original interaction response is fetched instead. 248 | ---@param id? Message-ID-Resolvable 249 | ---@return Message 250 | function Interaction:getReply(id) 251 | id = resolver.messageId(id) or "@original" 252 | local data, err = self.parent._api:getWebhookMessage(self._application_id, self._token, id) 253 | if data then 254 | return self._channel._messages:_insert(data) 255 | else 256 | return nil, err 257 | end 258 | end 259 | 260 | ---Modifies a previously sent interaction response. 261 | ---If `id` was not provided, the initial interaction response is edited instead. 262 | ---@param content table|string 263 | ---@param id? Message-ID-Resolvable 264 | ---@return boolean 265 | function Interaction:editReply(content, id) 266 | id = resolver.messageId(id) or "@original" 267 | local msg, files = resolveMessage(content) 268 | local data, err = self.parent._api:editWebhookMessage(self._application_id, self._token, id, msg, files) 269 | if data then 270 | return true 271 | else 272 | return false, err 273 | end 274 | end 275 | 276 | ---Deletes a previously sent response. If response `id` was not provided, original interaction response is deleted instead. 277 | ---If `id` was not provided, the initial interaction response is deleted instead. 278 | --- 279 | ---Returns `true` on success, otherwise `false, err`. 280 | ---@param id? Message-ID-Resolvable 281 | ---@return boolean 282 | function Interaction:deleteReply(id) 283 | id = resolver.messageId(id) or "@original" 284 | local data, err = self.parent._api:deleteWebhookMessage(self._application_id, self._token, id) 285 | if data then 286 | return true 287 | else 288 | return false, err 289 | end 290 | end 291 | 292 | function Interaction:_sendUpdate(payload, files) 293 | local data, err = self.parent._api:createInteractionResponse(self.id, self._token, { 294 | type = payload and callbackType.updateMessage or callbackType.deferredUpdateMessage, 295 | data = payload, 296 | }, files) 297 | if data then 298 | self._initialRes = true 299 | self._deferred = not payload and true 300 | return true 301 | else 302 | return false, err 303 | end 304 | end 305 | 306 | ---Responds to a component-based interaction by editing the message that the component is attached to. 307 | --- 308 | ---Returns `true` on success, otherwise `false, err`. 309 | ---@param content table|string 310 | ---@return boolean 311 | function Interaction:update(content) 312 | local t = type(content) 313 | assert(self._message, "UPDATE_MESSAGE is only supported by components-based interactions!") 314 | assert(t == "string" or t == "table", "bad argument #2 to update (expected table|string, got " .. t .. ')') 315 | local msg, files = resolveMessage(content) 316 | if not self._initialRes then 317 | return self:_sendUpdate(msg, files) 318 | end 319 | local data, err = self.parent._api:editMessage(self._message._parent._id, self._message._id, msg, files) 320 | if data then 321 | self._message:_setOldContent(data) 322 | self._message:_load(data) 323 | return true 324 | else 325 | return false, err 326 | end 327 | end 328 | 329 | ---Responds to a component-based interaction by acknowledging the interaction. 330 | ---Once `update` is called, the components message will be edited. 331 | --- 332 | ---Returns `true` on success, otherwise `false, err`. 333 | ---@return boolean 334 | function Interaction:updateDeferred() 335 | assert(self._message, "DEFERRED_UPDATE_MESSAGE is only supported by components-based interactions!") 336 | assert(not self._initialRes, "only the initial response can be deferred") 337 | return self:_sendUpdate() 338 | end 339 | 340 | ---Responds to an autocomplete interaction. 341 | ---`choices` is an array of tables with the fields `name` and `value`. 342 | ---For example: `{{name = "choice#1", value = "val1"}, {name = "choice#2", value = "val2"}}`. 343 | --- 344 | ---See [option choice structure](https://discord.com/developers/docs/interactions/application-commands#application-command-object-application-command-option-choice-structure) for more information on how to set a locale. 345 | --- 346 | ---Returns `true` on success, otherwise `false, err`. 347 | ---@param choices table 348 | ---@return boolean 349 | function Interaction:autocomplete(choices) 350 | assert(self._type == intrType.applicationCommandAutocomplete, "APPLICATION_COMMAND_AUTOCOMPLETE is only supported by application-based commands!") 351 | choices = resolver.autocomplete(choices) ---@diagnostic disable-line: cast-local-type 352 | assert(choices, "bad argument #1 to autocomplete (expected table, got " .. type(choices) .. ')') 353 | assert(#choices <= 25, "choices must not exceed 25") 354 | local data, err = self.parent._api:createInteractionResponse(self.id, self._token, { 355 | type = callbackType.applicationCommandAutocompleteResult, 356 | data = { 357 | choices = choices, 358 | }, 359 | }) 360 | if data then 361 | return true 362 | else 363 | return false, err 364 | end 365 | end 366 | 367 | function Interaction:_sendModal(payload) 368 | local data, err = self.parent._api:createInteractionResponse(self.id, self._token, { 369 | type = callbackType.modal, 370 | data = payload, 371 | }) 372 | if data then 373 | return true 374 | else 375 | return false, err 376 | end 377 | end 378 | 379 | ---Responds to an interaction by opening a Modal, also known as Text Inputs. 380 | ---By default this method takes the [raw structure](https://discord.com/developers/docs/interactions/message-components#text-inputs) defined by Discord 381 | ---but other extensions may also provide their own abstraction, see for example [discordia-modals](https://github.com/Bilal2453/discordia-modals/wiki/Modal). 382 | --- 383 | ---Returns `true` on success, otherwise `false, err`. 384 | ---@param modal table 385 | ---@return boolean 386 | function Interaction:modal(modal) 387 | modal = resolver.modal(modal) 388 | return self:_sendModal(modal) 389 | end 390 | 391 | function getter:applicationId() 392 | return self._application_id 393 | end 394 | 395 | function getter:type() 396 | return self._type 397 | end 398 | 399 | function getter:guildId() 400 | return self._guild_id 401 | end 402 | 403 | function getter:guild() 404 | return self._guild 405 | end 406 | 407 | function getter:channelId() 408 | return self._channel_id 409 | end 410 | 411 | function getter:channel() 412 | return self._channel 413 | end 414 | 415 | function getter:message() 416 | return self._message 417 | end 418 | 419 | function getter:member() 420 | return self._member 421 | end 422 | 423 | function getter:user() 424 | return self._user 425 | end 426 | 427 | function getter:data() 428 | return self._data 429 | end 430 | 431 | function getter:token() 432 | return self._token 433 | end 434 | 435 | function getter:version() 436 | return self._version 437 | end 438 | 439 | function getter:locale() 440 | return self._locale 441 | end 442 | 443 | function getter:guildLocale() 444 | return self._guild_locale 445 | end 446 | 447 | function getter:appPermissions() 448 | return Permissions(self._app_permissions) 449 | end 450 | 451 | function getter:entitlements() 452 | return self._entitlements 453 | end 454 | 455 | function getter:integrationOwners() 456 | return self._authorizing_integration_owners 457 | end 458 | 459 | function getter:context() 460 | return self._context 461 | end 462 | 463 | return Interaction 464 | --------------------------------------------------------------------------------