cordo 2.6.1 → 2.7.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 +277 -2
- package/package.json +1 -1
- package/src/components/component.ts +4 -4
- package/src/components/index.ts +1 -0
- package/src/components/modifier.ts +10 -2
- package/src/components/mods/debug-id-to-label.ts +1 -1
- package/src/components/mods/debug-print.ts +1 -1
- package/src/components/mods/debug-route.ts +1 -1
- package/src/components/mods/disable-all-components.ts +1 -1
- package/src/components/mods/enforce-private-response.ts +13 -0
- package/src/core/routing/respond.ts +16 -11
package/README.md
CHANGED
|
@@ -18,6 +18,281 @@ Cordo can be run as a standalone web-server (recommended) or integrated into exi
|
|
|
18
18
|
|
|
19
19
|
## Quick Start
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
### 1. Install via bun
|
|
22
|
+
|
|
23
|
+
`bun add cordo`
|
|
24
|
+
|
|
25
|
+
<details>
|
|
26
|
+
<summary>Other runtimes</summary>
|
|
27
|
+
|
|
28
|
+
Cordo should also be able to run on Deno.
|
|
29
|
+
|
|
30
|
+
Cordo should also be able to run on Node with experimental native TypeScript support enabled.
|
|
31
|
+
|
|
32
|
+
> [!NOTE]
|
|
33
|
+
> The `plugin/hono` package uses Bun specific apis. This should not be an issue since the package must be explicitly imported but keep it in mind if you use hono in a non-bun environment.
|
|
34
|
+
</details>
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
### 2. Add to your project
|
|
38
|
+
|
|
39
|
+
First you need to mount cordo by running `await Cordo.mountCordo()`. This step reads the cordo config and lock files and enables the file tree searching.
|
|
40
|
+
|
|
41
|
+
Second you need to pass incoming events to cordo, depending on the rest of your project this might look slightly different:
|
|
42
|
+
|
|
43
|
+
<details>
|
|
44
|
+
<summary>Using express (http)</summary>
|
|
45
|
+
|
|
46
|
+
```ts
|
|
47
|
+
import { useWithExpress } from 'cordo/plugin/express'
|
|
48
|
+
const app = express()
|
|
49
|
+
const clientPublicKey = 'your discord public key'
|
|
50
|
+
app.use(useWithExpress(clientPublicKey))
|
|
51
|
+
app.listen(5058, () => console.log('Cordo is running on port 5058'))
|
|
52
|
+
```
|
|
53
|
+
</details>
|
|
54
|
+
|
|
55
|
+
<details>
|
|
56
|
+
<summary>Using hono (http)</summary>
|
|
57
|
+
|
|
58
|
+
```ts
|
|
59
|
+
import { useWithHono } from 'cordo/plugin/hono'
|
|
60
|
+
const app = new Hono()
|
|
61
|
+
const clientPublicKey = 'your discord public key'
|
|
62
|
+
app.use('/cordo', useWithHono(clientPublicKey))
|
|
63
|
+
return app
|
|
64
|
+
```
|
|
65
|
+
</details>
|
|
66
|
+
|
|
67
|
+
<details>
|
|
68
|
+
<summary>Using discord.js</summary>
|
|
69
|
+
|
|
70
|
+
```ts
|
|
71
|
+
import { useWithDiscordJs } from 'cordo/plugin/djs'
|
|
72
|
+
const client = new Client({ ... })
|
|
73
|
+
useWithDiscordJs(client)
|
|
74
|
+
client.login()
|
|
75
|
+
```
|
|
76
|
+
</details>
|
|
77
|
+
|
|
78
|
+
<details>
|
|
79
|
+
<summary>Manually triggering (not recommended)</summary>
|
|
80
|
+
|
|
81
|
+
```ts
|
|
82
|
+
// Pass the raw body as a node buffer:
|
|
83
|
+
Cordo.triggerInteraction(req.body, {
|
|
84
|
+
// The httpCallback, if provided, is called on the first response to the interaction. Use this if you are receiving events via http, omit otherwise.
|
|
85
|
+
httpCallback: (payload: any) => res.status(200).json(payload)
|
|
86
|
+
})
|
|
87
|
+
```
|
|
88
|
+
</details>
|
|
89
|
+
|
|
90
|
+
### 3. Create cordo.config.ts
|
|
91
|
+
|
|
92
|
+
This is your primary configuration file. You can use the example from below as a starting point.
|
|
93
|
+
|
|
94
|
+
```ts
|
|
95
|
+
import { defineCordoConfig } from 'cordo/core'
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
export default defineCordoConfig({
|
|
99
|
+
// the root directory for cordo to search through
|
|
100
|
+
rootDir: './src',
|
|
101
|
+
// the file for cordo to store the generated types
|
|
102
|
+
typeDest: './src/types/cordo.ts',
|
|
103
|
+
client: {
|
|
104
|
+
// your app's id
|
|
105
|
+
id: '123456789'
|
|
106
|
+
}
|
|
107
|
+
})
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### 4. Create your first route
|
|
111
|
+
|
|
112
|
+
1. Create a folder called `routes` inside your rootDir.
|
|
113
|
+
2. Create a folder called `command` inside the routes folder.
|
|
114
|
+
3. Create a file named after your slash command inside the command folder.
|
|
115
|
+
4. Export a cordo route handler as the default export:
|
|
116
|
+
|
|
117
|
+
```ts
|
|
118
|
+
|
|
119
|
+
export default defineCordoRoute((i) => {
|
|
120
|
+
i.render(
|
|
121
|
+
text('My first command')
|
|
122
|
+
)
|
|
123
|
+
})
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## Concepts
|
|
127
|
+
|
|
128
|
+
### File based routing
|
|
129
|
+
|
|
130
|
+
For each interaction flow cordo is keeping track of a route, the webdev equivalent of a url. This means everything you do with cordo revolves around navigating to different routes.
|
|
131
|
+
|
|
132
|
+
As an entry point you will always have `command/...` routes.
|
|
133
|
+
- A slash command `/foo` will have the route `command/foo` (and it's handler at `routes/command/foo.ts`)
|
|
134
|
+
- A slash command with subcommand `/foo bar` will have the route `command/foo/bar`
|
|
135
|
+
- Context commands are converted to lowercase, spaces replaced with `-` and all other non-word characters removed. Example: `Add user :3` will have the route `command/add-user-3`
|
|
136
|
+
- Advanced: You can use the `transformCommandName` hook in your cordo.config to use custom logic
|
|
137
|
+
|
|
138
|
+
You will primarily use these two situations to switch routes:
|
|
139
|
+
|
|
140
|
+
#### 1. Route level redirects
|
|
141
|
+
|
|
142
|
+
You can redirect directly upon calling the route like so
|
|
143
|
+
|
|
144
|
+
```ts
|
|
145
|
+
export default defineCordoRoute((i) => {
|
|
146
|
+
i.goto('my/route')
|
|
147
|
+
})
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Which can be useful to route commands to their proper location. You can also use this to gate specific routes to specific conditions.
|
|
151
|
+
|
|
152
|
+
```ts
|
|
153
|
+
export default defineCordoRoute((i) => {
|
|
154
|
+
if (!isAdmin(i.user)) {
|
|
155
|
+
return i.goto('my/route')
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// or else...
|
|
159
|
+
})
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
#### 2. In response to user input
|
|
163
|
+
|
|
164
|
+
For all interactable components you can define a list of "functs" - which are special micro functions - to execute when the component is interacted with.
|
|
165
|
+
|
|
166
|
+
This includes buttons
|
|
167
|
+
|
|
168
|
+
```ts
|
|
169
|
+
i.render(
|
|
170
|
+
button()
|
|
171
|
+
.onClick(...)
|
|
172
|
+
)
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
Select menus
|
|
176
|
+
|
|
177
|
+
```ts
|
|
178
|
+
i.render(
|
|
179
|
+
selectString()
|
|
180
|
+
.addOption({
|
|
181
|
+
label: 'test',
|
|
182
|
+
onClick: [ ... ]
|
|
183
|
+
})
|
|
184
|
+
)
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
And modals:
|
|
188
|
+
|
|
189
|
+
```ts
|
|
190
|
+
i.prompt(
|
|
191
|
+
modal(...)
|
|
192
|
+
.onClick(...)
|
|
193
|
+
)
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
The primary functs you will be using are run and goto.
|
|
197
|
+
|
|
198
|
+
`goto` does a route change and will navigate the user to the specified route.
|
|
199
|
+
|
|
200
|
+
```ts
|
|
201
|
+
i.render(
|
|
202
|
+
button()
|
|
203
|
+
.label('See more')
|
|
204
|
+
.onClick(goto('./more'))
|
|
205
|
+
)
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
This is a good moment to mention that all routes support relative paths. You can use `absolute/routes`, `./relative`, and `../upwards` like you're used to from file systems.
|
|
209
|
+
|
|
210
|
+
`run` is similar to goto as it will execute the code in the location specified, yet it will not allow the route to render anything. You can use this paradigm to have purely functional routes and ones that serve ui.
|
|
211
|
+
|
|
212
|
+
```ts
|
|
213
|
+
i.render(
|
|
214
|
+
button()
|
|
215
|
+
.label('Save preferences')
|
|
216
|
+
.onClick(
|
|
217
|
+
run('utils/save-user-to-db', {
|
|
218
|
+
wait: true,
|
|
219
|
+
continueOnError: false
|
|
220
|
+
}),
|
|
221
|
+
goto('.')
|
|
222
|
+
)
|
|
223
|
+
)
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
In this case we're running the route at `routes/utils/save-user-to-db.ts` and waiting for it to finish (this route might be async). If anything goes wrong we will abort and let the nearest error boundary handle it (we will get to this). Once done we will navigate to `.` which is the current route, forcing cordo to re-render the one we are at right now.
|
|
227
|
+
|
|
228
|
+
The function at `utils/save-user-to-db` might use "locals" to attach information to the interaction such that the current route can display the successful database save.
|
|
229
|
+
|
|
230
|
+
```ts
|
|
231
|
+
// routes/utils/save-user-to-db.ts
|
|
232
|
+
export default defineCordoRoute(await (i) => {
|
|
233
|
+
try {
|
|
234
|
+
// ... save to database ...
|
|
235
|
+
|
|
236
|
+
// set a local variable
|
|
237
|
+
i.locals.set('message', 'Successfully saved to database!')
|
|
238
|
+
} catch (ex) {
|
|
239
|
+
// this will abort the interaction and trigger an error boundary
|
|
240
|
+
throw new CordoError('Database saving failed', ex.message)
|
|
241
|
+
}
|
|
242
|
+
})
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
```ts
|
|
246
|
+
// current route
|
|
247
|
+
export default defineCordoRoute((i) => {
|
|
248
|
+
i.render(
|
|
249
|
+
// Retreive the message from locals
|
|
250
|
+
text(`A message: ${i.locals.get('message')}`)
|
|
251
|
+
// Only show if there is a message in the locals
|
|
252
|
+
.visible(i.locals.has('message')),
|
|
253
|
+
|
|
254
|
+
// The very button to trigger it all
|
|
255
|
+
button()
|
|
256
|
+
.label('Save preferences')
|
|
257
|
+
.onClick(
|
|
258
|
+
run('utils/save-user-to-db', {
|
|
259
|
+
wait: true,
|
|
260
|
+
continueOnError: false
|
|
261
|
+
}),
|
|
262
|
+
goto('.')
|
|
263
|
+
)
|
|
264
|
+
)
|
|
265
|
+
})
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
This almost recursive approach might take a second to get used to but it allows for some very powerful composability and is highly scaleable.
|
|
269
|
+
|
|
270
|
+
Please note that cordo is stateless, this means the locals are only present for the single interaction they are written to and are not there once a follow up interaction happens. You can use route parameters to store dynamic information or use an external state management that works with your deployment.
|
|
271
|
+
|
|
272
|
+
#### Parameters in routes
|
|
273
|
+
|
|
274
|
+
You can use parameters in your routes like so:
|
|
275
|
+
|
|
276
|
+
* `routes/profile/[userid]/index.ts` -> matches `profile/12345678`
|
|
277
|
+
* `routes/profile/[userid]/friendlist.ts` -> matches `profile/12345678/friendlist`
|
|
278
|
+
* `routes/profile/[userid]/friendlist/[friend].ts` -> matches `profile/12345678/friendlist/481279874192`
|
|
279
|
+
|
|
280
|
+
You can access params using `i.params`.
|
|
281
|
+
|
|
282
|
+
A single `[parameter]` only captures a single string until the end of the route string or until the next forward slash.
|
|
283
|
+
|
|
284
|
+
Cordo's routing does not allow routes to have `..` for navigating up anywhere besides the beginning of the route. This means you can safely use template strings in your routes if you prefix them with something.
|
|
285
|
+
- Good: `shop/category/${...}`
|
|
286
|
+
- Bad: `${...}/info`
|
|
287
|
+
|
|
288
|
+
`.ts` files starting with _ are ignored and can be used for hosting utilities.
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
### Oh yeah, error boundaries
|
|
292
|
+
|
|
293
|
+
If an error is thrown cordo will first check the folder of the file for the current route and check for a file called `error.ts` exporting `export default defineCordoErrorBoundary((error, req) => ...)`. If none is found it will go up one folder and repeat until the routes folder is left at which point cordo will use it's built-in error handler.
|
|
294
|
+
|
|
295
|
+
- The search starts at the folder of the file for the current route. This means if you trigger a different route using `run` the cwd is not changed and the error handler stays the same. If you use `goto` the cwd is changed to the new route and such is the error handler.
|
|
296
|
+
- An error handler can at any point `throw` the error again to escalate it to a higher up error handler.
|
|
297
|
+
- An error handler can handle the interaction just like a normal route handler including: accessing locals and re-routing to non-error routes.
|
|
22
298
|
|
|
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
|
@@ -163,8 +163,8 @@ export function renderComponentList(
|
|
|
163
163
|
}
|
|
164
164
|
|
|
165
165
|
for (const mod of modifiers) {
|
|
166
|
-
if (mod.hooks?.
|
|
167
|
-
pipeline = mod.hooks.
|
|
166
|
+
if (mod.hooks?.beforeRender)
|
|
167
|
+
pipeline = mod.hooks.beforeRender(pipeline)
|
|
168
168
|
}
|
|
169
169
|
|
|
170
170
|
let output = pipeline
|
|
@@ -172,8 +172,8 @@ export function renderComponentList(
|
|
|
172
172
|
.filter(Boolean)
|
|
173
173
|
|
|
174
174
|
for (const mod of modifiers) {
|
|
175
|
-
if (mod.hooks?.
|
|
176
|
-
output = mod.hooks.
|
|
175
|
+
if (mod.hooks?.afterRender)
|
|
176
|
+
output = mod.hooks.afterRender(output)
|
|
177
177
|
}
|
|
178
178
|
|
|
179
179
|
return output
|
package/src/components/index.ts
CHANGED
|
@@ -29,6 +29,7 @@ export { debugIdToLabel } from './mods/debug-id-to-label'
|
|
|
29
29
|
export { debugPrint } from './mods/debug-print'
|
|
30
30
|
export { debugRoute } from './mods/debug-route'
|
|
31
31
|
export { disableAllComponents } from './mods/disable-all-components'
|
|
32
|
+
export { enforcePrivateResponse } from './mods/enforce-private-response'
|
|
32
33
|
|
|
33
34
|
export type { CordoComponent, StringComponentType } from './component'
|
|
34
35
|
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import type { CordoInteraction } from "../core"
|
|
2
|
+
import type { RouteResponse } from "../core/files/route"
|
|
3
|
+
import type { RoutingRespond } from "../core/routing/respond"
|
|
1
4
|
import type { CordoComponentPayload, renderComponentList, StringComponentType } from "./component"
|
|
2
5
|
|
|
3
6
|
|
|
@@ -8,8 +11,9 @@ export type CordoModifier = {
|
|
|
8
11
|
name: string
|
|
9
12
|
hooks?: {
|
|
10
13
|
onRender?: (c: CordoComponentPayload<StringComponentType>) => CordoComponentPayload<StringComponentType>
|
|
11
|
-
|
|
12
|
-
|
|
14
|
+
beforeRender?: (c: Array<CordoComponentPayload<StringComponentType>>) => Array<CordoComponentPayload<StringComponentType>>
|
|
15
|
+
afterRender?: (c: ReturnType<typeof renderComponentList>) => ReturnType<typeof renderComponentList>
|
|
16
|
+
beforeRouteResponse?: (response: RouteResponse, i: CordoInteraction, opts: RoutingRespond.RouteOpts) => void
|
|
13
17
|
}
|
|
14
18
|
}
|
|
15
19
|
}
|
|
@@ -18,6 +22,10 @@ export function createModifier(value: CordoModifier[typeof CordoModifierSymbol])
|
|
|
18
22
|
return { [CordoModifierSymbol]: value }
|
|
19
23
|
}
|
|
20
24
|
|
|
25
|
+
export function isModifier(t: Record<string, any>): t is CordoModifier {
|
|
26
|
+
return !!t && CordoModifierSymbol in t
|
|
27
|
+
}
|
|
28
|
+
|
|
21
29
|
export function readModifier(mod: CordoModifier): CordoModifier[typeof CordoModifierSymbol] {
|
|
22
30
|
return mod[CordoModifierSymbol]!
|
|
23
31
|
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { createModifier } from "../modifier"
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
export function enforcePrivateResponse() {
|
|
5
|
+
return createModifier({
|
|
6
|
+
name: 'enforce-private-response',
|
|
7
|
+
hooks: {
|
|
8
|
+
beforeRouteResponse: (_res, _i, opts) => {
|
|
9
|
+
opts.isPrivate = true
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
})
|
|
13
|
+
}
|
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
import { ApplicationCommandType, InteractionContextType, InteractionResponseType, InteractionType, MessageFlags, type APIUser } from "discord-api-types/v10"
|
|
1
|
+
import { ApplicationCommandOptionType, ApplicationCommandType, InteractionContextType, InteractionResponseType, InteractionType, MessageFlags, type APIUser } from "discord-api-types/v10"
|
|
2
2
|
import { disableAllComponents } from "../../components"
|
|
3
|
-
import { ComponentType,
|
|
3
|
+
import { ComponentType, renderComponent, renderComponentList, type CordoComponent } from "../../components/component"
|
|
4
4
|
import type { RouteInternals, RouteRequest, RouteResponse } from "../files/route"
|
|
5
5
|
import { InteractionInternals, type CordoInteraction } from "../interaction"
|
|
6
6
|
import { CordoMagic } from "../magic"
|
|
7
7
|
import { CordoGateway } from "../gateway"
|
|
8
8
|
import { FunctInternals } from "../../functions/funct"
|
|
9
9
|
import { goto, run } from "../../functions"
|
|
10
|
-
import
|
|
10
|
+
import { isModifier, readModifier } from "../../components/modifier"
|
|
11
11
|
import { FunctCompiler } from "../../functions/compiler"
|
|
12
12
|
import { RoutingResolve } from "./resolve"
|
|
13
13
|
|
|
@@ -27,16 +27,11 @@ export namespace RoutingRespond {
|
|
|
27
27
|
//
|
|
28
28
|
|
|
29
29
|
export function renderRouteResponse(response: RouteResponse, i: CordoInteraction, opts: RouteOpts = {}) {
|
|
30
|
-
const modifiers: CordoModifier[] = []
|
|
31
|
-
|
|
32
30
|
for (const item of response) {
|
|
33
|
-
if (
|
|
34
|
-
|
|
31
|
+
if (isModifier(item))
|
|
32
|
+
readModifier(item).hooks?.beforeRouteResponse?.(response, i, opts)
|
|
35
33
|
}
|
|
36
34
|
|
|
37
|
-
//
|
|
38
|
-
// TODO modifiers
|
|
39
|
-
|
|
40
35
|
if (opts.disableComponents)
|
|
41
36
|
response.push(disableAllComponents())
|
|
42
37
|
|
|
@@ -91,6 +86,16 @@ export namespace RoutingRespond {
|
|
|
91
86
|
return out
|
|
92
87
|
}
|
|
93
88
|
|
|
89
|
+
function parseCommandOptions(options: any) {
|
|
90
|
+
if (!options)
|
|
91
|
+
return {}
|
|
92
|
+
|
|
93
|
+
if (options.length === 1 && options[0].type === ApplicationCommandOptionType.SubcommandGroup)
|
|
94
|
+
return parseCommandOptions(options[0].options)
|
|
95
|
+
|
|
96
|
+
return (options as Record<string, any>[]).reduce((out, opt) => ({ [opt.name]: opt.value, ...out }), {})
|
|
97
|
+
}
|
|
98
|
+
|
|
94
99
|
export function buildRouteRequest(
|
|
95
100
|
route: RouteInternals.ParsedRoute,
|
|
96
101
|
args: string[],
|
|
@@ -199,7 +204,7 @@ export namespace RoutingRespond {
|
|
|
199
204
|
// @ts-ignore
|
|
200
205
|
id: interaction.data.id,
|
|
201
206
|
// @ts-ignore
|
|
202
|
-
options: interaction.data.options
|
|
207
|
+
options: parseCommandOptions(interaction.data.options),
|
|
203
208
|
// @ts-ignore
|
|
204
209
|
type: (interaction.data.type === ApplicationCommandType.ChatInput)
|
|
205
210
|
? 'chat'
|