cordo 2.5.1 → 2.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -84
- package/package.json +1 -1
- package/src/components/builtin/row.ts +12 -4
- package/src/core/files/config.ts +1 -1
- package/src/core/files/route.ts +9 -0
- package/src/core/gateway.ts +20 -3
- package/src/core/routing/resolve.ts +5 -3
- package/src/core/routing/respond.ts +5 -0
package/README.md
CHANGED
|
@@ -1,97 +1,23 @@
|
|
|
1
1
|
# Cordo
|
|
2
2
|
|
|
3
|
-
Cordo is a
|
|
3
|
+
Cordo is a developer-experience-first Discord App and UI library for TypeScript.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Cordo is built for and used in production with over 700k discord servers at https://freestuffbot.xyz/.
|
|
6
6
|
|
|
7
|
-
Or if you prefer real-life examples check out [FreeStuff Bot](https://github.com/FreeStuffBot/discord/tree/master/src/bot) or [Tudebot](https://github.com/Maanex/tudebot4/tree/master/src/cordo).
|
|
8
7
|
|
|
9
|
-
|
|
8
|
+
## Concepts
|
|
10
9
|
|
|
10
|
+
Cordo is taking inspiration from modern web-frameworks with concepts like **File Based Routing**, **Composables**, and **Error Boundaries**.
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
Cordo is also completely stateless allowing for incredibly easy load balancing, super light ram usage, and more. (This doesn't mean your bot cannot have state. It very well can.)
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
Cordo also abstracts away a lot of constraints/burdens on Discord's interactions api like completely hiding and managing *custom_id*s and automatically laying out components into rows or labels.
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
Cordo can be run as a standalone web-server (recommended) or integrated into existing Discord api libraries like discord.js.
|
|
17
17
|
|
|
18
|
-
### Components
|
|
19
18
|
|
|
20
|
-
|
|
21
|
-
You can create folders to build hierarchy. Each folder prefixes the resulting component id with the foldername and a _
|
|
22
|
-
For instance the handler in `components/foo/bar/test.ts` gets triggered for a component with the custom_id of `foo_bar_test`
|
|
19
|
+
## Quick Start
|
|
23
20
|
|
|
24
|
-
|
|
21
|
+
That's to do lol.
|
|
25
22
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
## Interaction flow
|
|
29
|
-
|
|
30
|
-
User presses button -> Check if this interaction has any overrides on timeout -> Check if there are global Component handlers -> Check if there is a state with the same name to take -> Error
|
|
31
|
-
|
|
32
|
-
User runs command -> Check if command handler exists -> Check if there is a state with the command name and _main -> Error
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
## API Coverage
|
|
36
|
-
|
|
37
|
-
✔️ Slash Commands
|
|
38
|
-
|
|
39
|
-
✔️ Command Groups
|
|
40
|
-
|
|
41
|
-
✔️ Command Autocomplete
|
|
42
|
-
|
|
43
|
-
✔️ Button Components
|
|
44
|
-
|
|
45
|
-
✔️ Dropdown Components
|
|
46
|
-
|
|
47
|
-
❌ Modals
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
## Naming Convensions
|
|
51
|
-
|
|
52
|
-
Commands may only have one word -> no CamelCase or snake_case needed
|
|
53
|
-
|
|
54
|
-
Components and States must be prefixed with the command they originated from -> A button on the settings command must start with settings_ (=> be placed inside a folder called settings)
|
|
55
|
-
|
|
56
|
-
Components must be named by their desired state and not how to get there. Example: Settings command has a page with general settings (settings_main), which has a button for advanced settings (settings_advanced) which has a button to destroy the world.
|
|
57
|
-
Incorrect naming: settings_advanced_destroy_world
|
|
58
|
-
Correct naming: settings_destroy_world
|
|
59
|
-
|
|
60
|
-
States also follow this principle to not inherit the path into the name, only the destination as shown above.
|
|
61
|
-
|
|
62
|
-
Components that just change state (like "open another page") or have state changing behaviour must be named like states. Example: config_page2, config_name
|
|
63
|
-
Components that have side effects (like changing settings or alike) must be named with a verb. Example: config_name_change, friends_request_send
|
|
64
|
-
For those verbs preferably pick:
|
|
65
|
-
* _change for Select/Dropdowns
|
|
66
|
-
* _toggle for Toggle Buttons
|
|
67
|
-
* _enable/_disable for Single Use Buttons
|
|
68
|
-
|
|
69
|
-
### Examples
|
|
70
|
-
|
|
71
|
-
Component and State names correct and incorrect examples:
|
|
72
|
-
|
|
73
|
-
❌ back
|
|
74
|
-
|
|
75
|
-
❌ back_button
|
|
76
|
-
|
|
77
|
-
❌ state_back
|
|
78
|
-
|
|
79
|
-
❌ settings_back
|
|
80
|
-
|
|
81
|
-
❌ settings_advanced_more
|
|
82
|
-
|
|
83
|
-
❌ command_free_button_one
|
|
84
|
-
|
|
85
|
-
✔️ settings_main
|
|
86
|
-
|
|
87
|
-
✔️ settings_description
|
|
88
|
-
|
|
89
|
-
✔️ free_show_details
|
|
90
|
-
|
|
91
|
-
✔️ settings_more
|
|
92
|
-
|
|
93
|
-
## Best practices
|
|
94
|
-
|
|
95
|
-
While theoretically the entire system could be built on soly using states, it is recomended to use interaction.reply and interaction.edit over states in non-interactive environments to save resources. States have a larger overhead than a simply interaction reply.
|
|
96
|
-
|
|
97
|
-
Cordo is best when it's used stateless. Try to reduce interactive responses to a minimum and always put as much information as possible in the custom ids. This allows for easy scaling as well as a great user experience. It might take some time to get used to but once you understand how to build on this, cordo naturally organizes your code.
|
|
23
|
+
If you wanna see how it looks like using cordo, check out the `./test` folder for some almost up to date demo project.
|
package/package.json
CHANGED
|
@@ -12,8 +12,16 @@ type AllowedComponentArray
|
|
|
12
12
|
export function row(...components: AllowedComponentArray | [ AllowedComponentArray ]) {
|
|
13
13
|
if (Array.isArray(components[0]))
|
|
14
14
|
components = components[0]
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
15
|
+
|
|
16
|
+
return createComponent('ActionRow', ({ hirarchy, attributes }) => {
|
|
17
|
+
const rendered = renderComponentList(components as AllowedComponentArray, 'ActionRow', hirarchy, attributes)
|
|
18
|
+
.filter(Boolean)
|
|
19
|
+
if (!rendered.length)
|
|
20
|
+
return null
|
|
21
|
+
|
|
22
|
+
return {
|
|
23
|
+
type: ComponentType.ActionRow,
|
|
24
|
+
components: rendered
|
|
25
|
+
}
|
|
26
|
+
})
|
|
19
27
|
}
|
package/src/core/files/config.ts
CHANGED
|
@@ -44,7 +44,7 @@ export type CordoConfig = {
|
|
|
44
44
|
onNetworkError: HookFor<any>,
|
|
45
45
|
|
|
46
46
|
/** transforms the name of the invoked command to a file or route name */
|
|
47
|
-
transformCommandName: TransformHookFor<string>,
|
|
47
|
+
transformCommandName: TransformHookFor<string, { type: 'slash' | 'message' | 'user' }>,
|
|
48
48
|
/** gets called by all cordo builtin components that render user facing text. e.g. buttons, text components, selects, etc */
|
|
49
49
|
transformUserFacingText: TransformHookFor<string, { component: StringComponentType, position: null | string, interaction?: CordoInteraction }>,
|
|
50
50
|
}
|
package/src/core/files/route.ts
CHANGED
|
@@ -89,6 +89,13 @@ type RouteRequestFromModal = {
|
|
|
89
89
|
selected: Map<string, string>
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
+
type InstallContext = {
|
|
93
|
+
/** true if the bot is installed in the current guild, non-exclusive */
|
|
94
|
+
isGuildInstalled: boolean
|
|
95
|
+
/** true if the bot is installed for the current user, non-exclusive */
|
|
96
|
+
isUserInstalled: boolean
|
|
97
|
+
}
|
|
98
|
+
|
|
92
99
|
export type RouteRequest = {
|
|
93
100
|
params: Record<string, string>
|
|
94
101
|
/** route path is the path to this route you are currently in */
|
|
@@ -97,6 +104,8 @@ export type RouteRequest = {
|
|
|
97
104
|
currentPath: string
|
|
98
105
|
rawInteraction: CordoInteraction
|
|
99
106
|
|
|
107
|
+
installContext: InstallContext
|
|
108
|
+
|
|
100
109
|
locals: {
|
|
101
110
|
get<T = any>(key: string): T | undefined
|
|
102
111
|
set(key: string, value: any): void
|
package/src/core/gateway.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ApplicationCommandType, ComponentType, InteractionResponseType, InteractionType, type APIInteraction } from "discord-api-types/v10"
|
|
1
|
+
import { ApplicationCommandOptionType, ApplicationCommandType, ComponentType, InteractionResponseType, InteractionType, type APIInteraction } from "discord-api-types/v10"
|
|
2
2
|
import type { Method } from "axios"
|
|
3
3
|
import axios from "axios"
|
|
4
4
|
import { FunctCompiler } from "../functions/compiler"
|
|
@@ -133,9 +133,26 @@ export namespace CordoGateway {
|
|
|
133
133
|
}
|
|
134
134
|
|
|
135
135
|
function handleCommandInteraction(i: CordoInteraction & { type: InteractionType.ApplicationCommand }) {
|
|
136
|
+
// slash commands: need to check for subcommands
|
|
136
137
|
if (i.data.type === ApplicationCommandType.ChatInput) {
|
|
137
|
-
|
|
138
|
-
|
|
138
|
+
let name = i.data.name
|
|
139
|
+
let option = i.data.options?.[0]
|
|
140
|
+
while (option?.type === ApplicationCommandOptionType.Subcommand || option?.type === ApplicationCommandOptionType.SubcommandGroup) {
|
|
141
|
+
name += ` ${option.name}`
|
|
142
|
+
option = option.options?.[0]
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const { route, path } = RoutingResolve.getRouteForCommand(name, 'slash')
|
|
146
|
+
CordoMagic.setCwd(path)
|
|
147
|
+
return RoutingRespond.callRoute(route.routeId, route.args, i)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// message/user commands: go ahead
|
|
151
|
+
if (i.data.type === ApplicationCommandType.Message || i.data.type === ApplicationCommandType.User) {
|
|
152
|
+
const { route, path } = RoutingResolve.getRouteForCommand(
|
|
153
|
+
i.data.name,
|
|
154
|
+
i.data.type === ApplicationCommandType.Message ? 'message' : 'user'
|
|
155
|
+
)
|
|
139
156
|
CordoMagic.setCwd(path)
|
|
140
157
|
return RoutingRespond.callRoute(route.routeId, route.args, i)
|
|
141
158
|
}
|
|
@@ -133,10 +133,12 @@ export namespace RoutingResolve {
|
|
|
133
133
|
}
|
|
134
134
|
}
|
|
135
135
|
|
|
136
|
-
export function getRouteForCommand(command: string) {
|
|
136
|
+
export function getRouteForCommand(command: string, type: 'slash' | 'message' | 'user') {
|
|
137
137
|
const fileName = Hooks.isDefined('transformCommandName')
|
|
138
|
-
? Hooks.callHook('transformCommandName', command)
|
|
139
|
-
:
|
|
138
|
+
? Hooks.callHook('transformCommandName', command, { type })
|
|
139
|
+
: type === 'slash'
|
|
140
|
+
? command.replaceAll(' ', '/').toLowerCase() // slash commands: subcommands become subfolders
|
|
141
|
+
: command.replaceAll(' ', '-').replace(/[^\w-]/g, '').toLowerCase() // message/user commands: just sanitize
|
|
140
142
|
const routePath = `command/${fileName}`
|
|
141
143
|
return {
|
|
142
144
|
route: getRouteFromPath(routePath, false),
|
|
@@ -138,6 +138,11 @@ export namespace RoutingRespond {
|
|
|
138
138
|
rawInteraction: interaction,
|
|
139
139
|
rawEntitlements: interaction.entitlements,
|
|
140
140
|
|
|
141
|
+
installContext: {
|
|
142
|
+
isGuildInstalled: Boolean(interaction.authorizing_integration_owners[0]),
|
|
143
|
+
isUserInstalled: Boolean(interaction.authorizing_integration_owners[1]),
|
|
144
|
+
},
|
|
145
|
+
|
|
141
146
|
locals: {
|
|
142
147
|
get: <T = any>(key: string) => interaction.locals[key] as T,
|
|
143
148
|
set: (key: string, value: any) => void (interaction.locals[key] = value),
|