@zaflun/lumio-agent-skill 0.2.0 → 0.4.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/dist/bin/lumio-skills.js
CHANGED
|
@@ -13,7 +13,7 @@ Lumio Extensions are sandboxed React apps that run inside a 3-layer iframe sandb
|
|
|
13
13
|
|
|
14
14
|
\`\`\`
|
|
15
15
|
3-Layer Sandbox:
|
|
16
|
-
Host (lumio.vision /
|
|
16
|
+
Host (lumio.vision / browser source)
|
|
17
17
|
\u2514\u2500\u2500 Supervisor iframe (supervisor.extension.lumio.vision)
|
|
18
18
|
\u2514\u2500\u2500 Extension Runtime iframe (ext.lumio.vision)
|
|
19
19
|
\u2514\u2500\u2500 Web Worker (server function execution)
|
|
@@ -23,7 +23,7 @@ Lumio Extensions are sandboxed React apps that run inside a 3-layer iframe sandb
|
|
|
23
23
|
Extension Project:
|
|
24
24
|
lumio.config.json <- Extension manifest (ID, name, category, permissions, targets, config_schema)
|
|
25
25
|
src/editor.tsx <- Dashboard editor panel (sidebar in overlay editor)
|
|
26
|
-
src/layer.tsx <-
|
|
26
|
+
src/layer.tsx <- Stream overlay surface (transparent, read-only inputs)
|
|
27
27
|
src/interactive.tsx <- Audience interaction page (optional)
|
|
28
28
|
server/schema.ts <- DB schema (defineTable + validators)
|
|
29
29
|
server/functions.ts <- Server functions (queries, mutations, actions)
|
|
@@ -52,7 +52,7 @@ Extension Project:
|
|
|
52
52
|
| Target | Where | Purpose |
|
|
53
53
|
|--------|-------|---------|
|
|
54
54
|
| \`editor\` | Dashboard overlay editor sidebar | Config UI, settings, content management |
|
|
55
|
-
| \`layer\` |
|
|
55
|
+
| \`layer\` | Browser Source (transparent) | Visual overlay in stream |
|
|
56
56
|
| \`interactive\` | Standalone page (\`/ext/{slug}\`) | Viewer interaction (votes, games) |
|
|
57
57
|
|
|
58
58
|
All three surfaces share storage via \`useExtensionStorage()\` \u2014 changes in editor propagate to layer in real-time.
|
|
@@ -144,7 +144,7 @@ lumio deploy
|
|
|
144
144
|
|
|
145
145
|
**VisionStyle** is a typed CSS subset (allowed: flexbox, grid, \`position: relative\` / \`absolute\`, sizing, spacing, colors, border, typography, transitions; blocked: \`fixed\`, \`sticky\`, CSS expressions).
|
|
146
146
|
|
|
147
|
-
On
|
|
147
|
+
On the layer surface: input components become read-only (buttons hidden, toggles show on/off text).
|
|
148
148
|
|
|
149
149
|
See \`components.md\` for full props and examples.
|
|
150
150
|
|
|
@@ -218,6 +218,101 @@ const { execute: spotifyQueueAdd } = useLumioAction("spotify:queue_add");
|
|
|
218
218
|
|
|
219
219
|
See \`server-functions.md\` for schema definition, storage scoping, and advanced patterns.
|
|
220
220
|
|
|
221
|
+
## Bot Modules
|
|
222
|
+
|
|
223
|
+
Extensions with category \`bot_module\` register handler-based server functions that respond to chat messages and platform events. Bot module handlers use six trigger types.
|
|
224
|
+
|
|
225
|
+
### Handler Types
|
|
226
|
+
|
|
227
|
+
| Handler | Trigger | Description |
|
|
228
|
+
|---------|---------|-------------|
|
|
229
|
+
| \`command(prefix, handler)\` | Chat command (e.g. \`!rank\`) | Fires when a message starts with the prefix |
|
|
230
|
+
| \`keyword(words, handler)\` | Keyword match | Fires when a message contains any listed keyword |
|
|
231
|
+
| \`pattern(regex, handler)\` | Regex match | Fires when a message matches the regex |
|
|
232
|
+
| \`event(type, handler)\` | Platform event | Fires on a specific event (e.g. \`twitch:follower\`) |
|
|
233
|
+
| \`timer(name, intervalSecs, handler)\` | Recurring timer | Fires every N seconds. Name must be unique per extension. |
|
|
234
|
+
| \`moderate(handler)\` | Every message | Runs on every chat message for moderation decisions |
|
|
235
|
+
|
|
236
|
+
### Handler Context
|
|
237
|
+
|
|
238
|
+
Bot module handlers receive an extended \`ctx\` with chat-specific properties:
|
|
239
|
+
|
|
240
|
+
\`\`\`typescript
|
|
241
|
+
import { command } from "@zaflun/lumio-sdk/server";
|
|
242
|
+
|
|
243
|
+
export const rankCmd = command("!rank", {
|
|
244
|
+
handler: async (ctx, message) => {
|
|
245
|
+
// ctx.user \u2014 chat user who sent the message
|
|
246
|
+
// { platform, platform_user_id, display_name, roles }
|
|
247
|
+
// roles: { moderator, vip, subscriber, broadcaster }
|
|
248
|
+
|
|
249
|
+
// ctx.auth \u2014 extension identity
|
|
250
|
+
// { userId, accountId, extensionId, installId }
|
|
251
|
+
|
|
252
|
+
// ctx.cache \u2014 in-memory KV cache scoped to install (TTL-based)
|
|
253
|
+
await ctx.cache.set("last_rank_user", ctx.user.display_name, { ttl: 300 });
|
|
254
|
+
const cached = await ctx.cache.get("last_rank_user");
|
|
255
|
+
|
|
256
|
+
// ctx.defer() \u2014 schedule a follow-up after handler returns
|
|
257
|
+
ctx.defer(async () => {
|
|
258
|
+
await ctx.db.insert("rank_log", { user: ctx.user.display_name });
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
return { action: "send_message", message: \`@\${ctx.user.display_name} your rank is Gold!\` };
|
|
262
|
+
},
|
|
263
|
+
});
|
|
264
|
+
\`\`\`
|
|
265
|
+
|
|
266
|
+
### \`ctx.user\` vs \`ctx.auth\`
|
|
267
|
+
|
|
268
|
+
- \`ctx.user\` is the **chat user** who triggered the handler (their platform identity, display name, and role flags). Only available in bot module handlers.
|
|
269
|
+
- \`ctx.auth\` is the **extension identity** (which extension, which install, which account). Available in all server functions.
|
|
270
|
+
|
|
271
|
+
### Timer Naming
|
|
272
|
+
|
|
273
|
+
Timer names must be unique within the extension. The name is used as the timer's identity for scheduling and deduplication. Use descriptive names:
|
|
274
|
+
|
|
275
|
+
\`\`\`typescript
|
|
276
|
+
timer("ad_reminder", 900, { handler: async (ctx) => { ... } }); // Good
|
|
277
|
+
timer("t1", 900, { handler: async (ctx) => { ... } }); // Bad \u2014 unclear
|
|
278
|
+
\`\`\`
|
|
279
|
+
|
|
280
|
+
### \`ctx.cache\`
|
|
281
|
+
|
|
282
|
+
The cache is an in-memory key-value store scoped to the extension install. Use it for rate limiting, cooldowns, and short-lived state that does not need to survive Worker restarts.
|
|
283
|
+
|
|
284
|
+
\`\`\`typescript
|
|
285
|
+
// Set with TTL (seconds)
|
|
286
|
+
await ctx.cache.set("cooldown:ban:" + userId, "1", { ttl: 60 });
|
|
287
|
+
|
|
288
|
+
// Get (returns null if expired or missing)
|
|
289
|
+
const val = await ctx.cache.get("cooldown:ban:" + userId);
|
|
290
|
+
|
|
291
|
+
// Delete
|
|
292
|
+
await ctx.cache.delete("cooldown:ban:" + userId);
|
|
293
|
+
\`\`\`
|
|
294
|
+
|
|
295
|
+
### \`ctx.defer()\`
|
|
296
|
+
|
|
297
|
+
Schedule work to run after the handler returns its response. Useful for logging, analytics, or secondary actions that should not delay the primary response.
|
|
298
|
+
|
|
299
|
+
\`\`\`typescript
|
|
300
|
+
export const modCheck = moderate({
|
|
301
|
+
handler: async (ctx, message) => {
|
|
302
|
+
if (isToxic(message.text)) {
|
|
303
|
+
ctx.defer(async () => {
|
|
304
|
+
await ctx.db.insert("mod_log", {
|
|
305
|
+
user: ctx.user.platform_user_id,
|
|
306
|
+
action: "timeout",
|
|
307
|
+
reason: "toxic message",
|
|
308
|
+
});
|
|
309
|
+
});
|
|
310
|
+
return { action: "timeout", duration: 300, message: "Please be respectful." };
|
|
311
|
+
}
|
|
312
|
+
return { action: "none" };
|
|
313
|
+
},
|
|
314
|
+
});\`\`\`
|
|
315
|
+
|
|
221
316
|
## CLI Commands
|
|
222
317
|
|
|
223
318
|
12 commands provided by \`@zaflun/lumio-cli\`:
|
|
@@ -472,7 +567,7 @@ import { Image } from "@zaflun/lumio-sdk";
|
|
|
472
567
|
|
|
473
568
|
## Input Components
|
|
474
569
|
|
|
475
|
-
**Important:** On
|
|
570
|
+
**Important:** On the layer surface, all input components become read-only. Buttons are hidden, toggles show "On"/"Off" text, dropdowns show the selected value.
|
|
476
571
|
|
|
477
572
|
### Button
|
|
478
573
|
|
|
@@ -652,7 +747,7 @@ const exitAnim = useAnimation("fade-out", { duration: 200 });
|
|
|
652
747
|
|
|
653
748
|
**Best practices:**
|
|
654
749
|
- Always use SDK presets instead of raw CSS animations for entrance/exit effects \u2014 the reconciler maps them to allowed VisionStyle \`animation\` properties
|
|
655
|
-
- Keep animation durations short on the
|
|
750
|
+
- Keep animation durations short on the layer surface (< 500ms) to avoid frame drops
|
|
656
751
|
- Avoid animating layout-affecting properties (\`width\`, \`height\`) on the layer; prefer \`opacity\` and \`transform\`
|
|
657
752
|
|
|
658
753
|
## VisionStyle
|
|
@@ -893,6 +988,129 @@ updateLayer({ layerId: "uuid", visible: true });
|
|
|
893
988
|
| \`spotify:queue_add\` | \`spotify:control\` | Add track to Spotify queue |
|
|
894
989
|
|
|
895
990
|
Actions are proxied through the Lumio API \u2014 the extension never accesses OAuth tokens or platform credentials directly.
|
|
991
|
+
|
|
992
|
+
## Bot Module Handlers
|
|
993
|
+
|
|
994
|
+
Extensions with category \`bot_module\` export handler wrappers that register triggers. Each wrapper defines when the handler fires and what context it receives.
|
|
995
|
+
|
|
996
|
+
### \`command(prefix, options)\`
|
|
997
|
+
|
|
998
|
+
Fires when a chat message starts with the given prefix.
|
|
999
|
+
|
|
1000
|
+
\`\`\`typescript
|
|
1001
|
+
import { command } from "@zaflun/lumio-sdk/server";
|
|
1002
|
+
|
|
1003
|
+
export const hello = command("!hello", {
|
|
1004
|
+
handler: async (ctx, message) => {
|
|
1005
|
+
return { action: "send_message", message: \`Hello @\${ctx.user.display_name}!\` };
|
|
1006
|
+
},
|
|
1007
|
+
});
|
|
1008
|
+
\`\`\`
|
|
1009
|
+
|
|
1010
|
+
### \`keyword(words, options)\`
|
|
1011
|
+
|
|
1012
|
+
Fires when a chat message contains any of the listed keywords (case-insensitive).
|
|
1013
|
+
|
|
1014
|
+
\`\`\`typescript
|
|
1015
|
+
import { keyword } from "@zaflun/lumio-sdk/server";
|
|
1016
|
+
|
|
1017
|
+
export const giveaway = keyword(["giveaway", "enter", "join"], {
|
|
1018
|
+
handler: async (ctx, message) => {
|
|
1019
|
+
await ctx.db.insert("entries", { user_id: ctx.user.platform_user_id });
|
|
1020
|
+
return { action: "send_message", message: \`@\${ctx.user.display_name} you are entered!\` };
|
|
1021
|
+
},
|
|
1022
|
+
});
|
|
1023
|
+
\`\`\`
|
|
1024
|
+
|
|
1025
|
+
### \`pattern(regex, options)\`
|
|
1026
|
+
|
|
1027
|
+
Fires when a chat message matches the given regex.
|
|
1028
|
+
|
|
1029
|
+
\`\`\`typescript
|
|
1030
|
+
import { pattern } from "@zaflun/lumio-sdk/server";
|
|
1031
|
+
|
|
1032
|
+
export const scoreParser = pattern(/^!score\\s+(\\w+)\\s+(\\d+)$/, {
|
|
1033
|
+
handler: async (ctx, message, matches) => {
|
|
1034
|
+
const [, game, score] = matches;
|
|
1035
|
+
await ctx.db.insert("scores", { game, score: parseInt(score, 10), user: ctx.user.display_name });
|
|
1036
|
+
return { action: "send_message", message: \`Score recorded: \${game} = \${score}\` };
|
|
1037
|
+
},
|
|
1038
|
+
});
|
|
1039
|
+
\`\`\`
|
|
1040
|
+
|
|
1041
|
+
### \`event(eventType, options)\`
|
|
1042
|
+
|
|
1043
|
+
Fires on a platform event. The \`eventType\` is a \`LumioEventType\` string.
|
|
1044
|
+
|
|
1045
|
+
\`\`\`typescript
|
|
1046
|
+
import { event } from "@zaflun/lumio-sdk/server";
|
|
1047
|
+
|
|
1048
|
+
export const welcomeFollower = event("twitch:follower", {
|
|
1049
|
+
handler: async (ctx, eventData) => {
|
|
1050
|
+
return { action: "send_message", message: \`Welcome @\${eventData.user_name}! Thanks for the follow!\` };
|
|
1051
|
+
},
|
|
1052
|
+
});
|
|
1053
|
+
\`\`\`
|
|
1054
|
+
|
|
1055
|
+
### \`timer(name, intervalSecs, options)\`
|
|
1056
|
+
|
|
1057
|
+
Fires on a recurring interval. The name must be unique within the extension.
|
|
1058
|
+
|
|
1059
|
+
\`\`\`typescript
|
|
1060
|
+
import { timer } from "@zaflun/lumio-sdk/server";
|
|
1061
|
+
|
|
1062
|
+
export const reminder = timer("social_reminder", 900, {
|
|
1063
|
+
handler: async (ctx) => {
|
|
1064
|
+
return { action: "send_message", message: "Follow us on Twitter! twitter.com/example" };
|
|
1065
|
+
},
|
|
1066
|
+
});
|
|
1067
|
+
\`\`\`
|
|
1068
|
+
|
|
1069
|
+
### \`moderate(options)\`
|
|
1070
|
+
|
|
1071
|
+
Runs on every incoming chat message. Used for moderation decisions.
|
|
1072
|
+
|
|
1073
|
+
\`\`\`typescript
|
|
1074
|
+
import { moderate } from "@zaflun/lumio-sdk/server";
|
|
1075
|
+
|
|
1076
|
+
export const toxicFilter = moderate({
|
|
1077
|
+
handler: async (ctx, message) => {
|
|
1078
|
+
const toxicScore = await analyzeToxicity(message.text);
|
|
1079
|
+
if (toxicScore > 0.9) {
|
|
1080
|
+
return { action: "timeout", duration: 600, message: "Please keep chat respectful." };
|
|
1081
|
+
}
|
|
1082
|
+
if (toxicScore > 0.7) {
|
|
1083
|
+
return { action: "delete", message: "Message removed by auto-mod." };
|
|
1084
|
+
}
|
|
1085
|
+
return { action: "none" };
|
|
1086
|
+
},
|
|
1087
|
+
});
|
|
1088
|
+
\`\`\`
|
|
1089
|
+
|
|
1090
|
+
### Handler Return Actions
|
|
1091
|
+
|
|
1092
|
+
All bot module handlers return an action object:
|
|
1093
|
+
|
|
1094
|
+
| Action | Fields | Description | Permission required |
|
|
1095
|
+
|--------|--------|-------------|---------------------|
|
|
1096
|
+
| \`send_message\` | \`message\` | Send a chat message | \`chat:send\` |
|
|
1097
|
+
| \`delete\` | \`message?\` | Delete the triggering message | \`chat:delete\` |
|
|
1098
|
+
| \`timeout\` | \`duration\`, \`message?\` | Timeout the user (seconds) | \`chat:ban\` |
|
|
1099
|
+
| \`ban\` | \`message?\` | Ban the user | \`chat:ban\` |
|
|
1100
|
+
| \`none\` | \u2014 | Take no action | \u2014 |
|
|
1101
|
+
|
|
1102
|
+
The extension must declare the matching permission in \`lumio.config.json\`. For example, a moderation module that bans users needs \`"permissions": ["chat:ban"]\`.
|
|
1103
|
+
|
|
1104
|
+
### Bot Module Permission Table
|
|
1105
|
+
|
|
1106
|
+
| Permission | Grants | Review level |
|
|
1107
|
+
|-----------|--------|--------------|
|
|
1108
|
+
| \`chat:send\` | Send chat messages | Standard |
|
|
1109
|
+
| \`chat:read\` | Read incoming chat messages | Standard |
|
|
1110
|
+
| \`chat:delete\` | Delete chat messages | Enhanced |
|
|
1111
|
+
| \`chat:ban\` | Ban, unban, and timeout users | Enhanced |
|
|
1112
|
+
| \`events:read\` | Subscribe to platform events | Standard |
|
|
1113
|
+
| \`events:write\` | Emit custom events | Standard |
|
|
896
1114
|
`;
|
|
897
1115
|
|
|
898
1116
|
// src/content/cli-workflows.ts
|
|
@@ -1175,7 +1393,7 @@ The Lumio auth system has 9 token types:
|
|
|
1175
1393
|
| 2 | \`lm_usr_\` | User API Key | Permanent | External API access for users |
|
|
1176
1394
|
| 3 | _(JWT)_ | JWT | Session | OAuth login session |
|
|
1177
1395
|
| 4 | \`lm_pop_\` | Popout Token | Non-expiring | Overlay access for non-session viewers |
|
|
1178
|
-
| 5 | \`lm_overlay_\` | Overlay Token | Permanent | Per-overlay
|
|
1396
|
+
| 5 | \`lm_overlay_\` | Overlay Token | Permanent | Per-overlay browser source access |
|
|
1179
1397
|
| 6 | \`lm_share_\` | Shared Overlay Token | Time-limited | Temporary shared overlay links |
|
|
1180
1398
|
| 7 | \`lm_ext_\` | Extension Token | 15 min | Scoped to one extension installation |
|
|
1181
1399
|
| 8 | \`lm_widget_\` | Widget Token | Permanent | Widget Gallery standalone embeds |
|
|
@@ -1285,7 +1503,7 @@ var runtimeScopesContent = `# Runtime Scopes
|
|
|
1285
1503
|
Extensions run inside a 3-layer iframe sandbox. Understanding the layers is important for debugging messaging and storage issues:
|
|
1286
1504
|
|
|
1287
1505
|
\`\`\`
|
|
1288
|
-
Host (lumio.vision /
|
|
1506
|
+
Host (lumio.vision / browser source)
|
|
1289
1507
|
\u2514\u2500\u2500 Supervisor iframe (supervisor.extension.lumio.vision)
|
|
1290
1508
|
Purpose: validates postMessage traffic, enforces egress allowlist,
|
|
1291
1509
|
mediates token refresh, proxies LumioComponentNode[] to host
|
|
@@ -1418,7 +1636,7 @@ var bestPracticesContent = `# Best Practices
|
|
|
1418
1636
|
|
|
1419
1637
|
### Layer surface
|
|
1420
1638
|
- Use CSS animations via \`VisionStyle\` instead of JS-driven animations
|
|
1421
|
-
- Keep the layer surface minimal \u2014 it runs in
|
|
1639
|
+
- Keep the layer surface minimal \u2014 it runs in an Browser Source at 30/60 fps
|
|
1422
1640
|
- Return \`null\` from components when they should be invisible (do not use \`display: none\`)
|
|
1423
1641
|
|
|
1424
1642
|
## UX Patterns
|
|
@@ -1469,7 +1687,7 @@ Wrap editor settings in \`CompactView\` for consistent styling with the Lumio da
|
|
|
1469
1687
|
\`\`\`
|
|
1470
1688
|
|
|
1471
1689
|
### Transparent backgrounds on layer
|
|
1472
|
-
The layer surface should have a transparent background so it composites correctly over the stream
|
|
1690
|
+
The layer surface should have a transparent background so it composites correctly over the stream:
|
|
1473
1691
|
|
|
1474
1692
|
\`\`\`tsx
|
|
1475
1693
|
// layer.tsx \u2014 no background, content floats over stream
|
|
@@ -1515,7 +1733,7 @@ const maxItems = config.maxItems ?? 10;
|
|
|
1515
1733
|
|
|
1516
1734
|
- Always use SDK presets via \`useAnimation()\` + \`<Animated>\` for entrance/exit effects \u2014 never write raw CSS keyframe \`@keyframes\` rules
|
|
1517
1735
|
- The reconciler maps animation preset names to allowed \`VisionStyle\` \`animation\` properties; raw keyframe strings are stripped by the Supervisor
|
|
1518
|
-
- Keep durations short on
|
|
1736
|
+
- Keep durations short on the layer (< 400ms). Longer animations cause visible frame drops in the Browser Source at 30/60 fps
|
|
1519
1737
|
- Prefer animating \`opacity\` and \`transform\` (GPU-composited) over \`width\`, \`height\`, \`margin\`, \`padding\` (layout-triggering)
|
|
1520
1738
|
|
|
1521
1739
|
\`\`\`tsx
|
|
@@ -1554,6 +1772,120 @@ For beta testing before public launch:
|
|
|
1554
1772
|
| Hardcoded strings | Use \`config_schema\` fields for user-customizable text |
|
|
1555
1773
|
| Using localStorage directly | Use \`useExtensionStorage()\` (localStorage blocked at ext.lumio.vision) |
|
|
1556
1774
|
| Raw CSS \`@keyframes\` animations | Use \`useAnimation()\` + \`<Animated>\` SDK presets |
|
|
1775
|
+
|
|
1776
|
+
## Bot Module Security
|
|
1777
|
+
|
|
1778
|
+
### Rate Limiting Awareness
|
|
1779
|
+
|
|
1780
|
+
Bot module handlers are subject to the same rate limits as other server functions. Plan for these limits in moderation logic:
|
|
1781
|
+
|
|
1782
|
+
- Actions (with fetch): 20/min per installation
|
|
1783
|
+
- Mutations: 50/min per installation
|
|
1784
|
+
- Storage writes: 200/min per installation
|
|
1785
|
+
|
|
1786
|
+
If your module processes high-volume chat, avoid issuing a moderation action on every message. Use \`ctx.cache\` for cooldowns:
|
|
1787
|
+
|
|
1788
|
+
\`\`\`tsx
|
|
1789
|
+
export const spamGuard = moderate({
|
|
1790
|
+
handler: async (ctx, message) => {
|
|
1791
|
+
const key = \`cooldown:\${ctx.user.platform_user_id}\`;
|
|
1792
|
+
if (await ctx.cache.get(key)) return { action: "none" };
|
|
1793
|
+
|
|
1794
|
+
if (isSpam(message.text)) {
|
|
1795
|
+
await ctx.cache.set(key, "1", { ttl: 30 });
|
|
1796
|
+
return { action: "timeout", duration: 60, message: "Spam detected." };
|
|
1797
|
+
}
|
|
1798
|
+
return { action: "none" };
|
|
1799
|
+
},
|
|
1800
|
+
});
|
|
1801
|
+
\`\`\`
|
|
1802
|
+
|
|
1803
|
+
### Moderation Escalation Pattern
|
|
1804
|
+
|
|
1805
|
+
For moderation modules, escalate actions progressively rather than jumping to bans. Track offense counts in the extension database:
|
|
1806
|
+
|
|
1807
|
+
\`\`\`tsx
|
|
1808
|
+
export const escalatingMod = moderate({
|
|
1809
|
+
handler: async (ctx, message) => {
|
|
1810
|
+
if (!isViolation(message.text)) return { action: "none" };
|
|
1811
|
+
|
|
1812
|
+
const offenses = await ctx.db.get("offenses", ctx.user.platform_user_id);
|
|
1813
|
+
const count = (offenses?.count ?? 0) + 1;
|
|
1814
|
+
await ctx.db.patch("offenses", ctx.user.platform_user_id, { count });
|
|
1815
|
+
|
|
1816
|
+
if (count >= 3) return { action: "ban", message: "Repeated violations." };
|
|
1817
|
+
if (count === 2) return { action: "timeout", duration: 600, message: "Second warning." };
|
|
1818
|
+
return { action: "delete", message: "First warning: message removed." };
|
|
1819
|
+
},
|
|
1820
|
+
});
|
|
1821
|
+
\`\`\`
|
|
1822
|
+
|
|
1823
|
+
### User Override Preservation
|
|
1824
|
+
|
|
1825
|
+
Account owners can override trigger configuration (command prefixes, keywords, regex patterns) via the dashboard. Always read trigger config from the handler context rather than hardcoding values:
|
|
1826
|
+
|
|
1827
|
+
\`\`\`tsx
|
|
1828
|
+
// Good: reads overridable config
|
|
1829
|
+
export const customCmd = command("!points", {
|
|
1830
|
+
handler: async (ctx, message) => {
|
|
1831
|
+
// The prefix "!points" is the default \u2014 users can override it in dashboard
|
|
1832
|
+
// Handler logic stays the same regardless of the configured prefix
|
|
1833
|
+
return { action: "send_message", message: "You have 100 points!" };
|
|
1834
|
+
},
|
|
1835
|
+
});
|
|
1836
|
+
\`\`\`
|
|
1837
|
+
|
|
1838
|
+
Extension developers should document which triggers are overridable in the extension's description so account owners know what they can customize.
|
|
1839
|
+
|
|
1840
|
+
### Cache-First + Defer Pattern
|
|
1841
|
+
|
|
1842
|
+
For handlers that need fast responses but also need to persist data, use \`ctx.cache\` for the immediate check and \`ctx.defer()\` to write to the database asynchronously:
|
|
1843
|
+
|
|
1844
|
+
\`\`\`tsx
|
|
1845
|
+
export const rateLimitedGreet = command("!greet", {
|
|
1846
|
+
handler: async (ctx, message) => {
|
|
1847
|
+
const cacheKey = \`greeted:\${ctx.user.platform_user_id}\`;
|
|
1848
|
+
|
|
1849
|
+
// Fast path: check cache
|
|
1850
|
+
if (await ctx.cache.get(cacheKey)) {
|
|
1851
|
+
return { action: "send_message", message: "You already greeted today!" };
|
|
1852
|
+
}
|
|
1853
|
+
|
|
1854
|
+
// Set cache immediately (fast)
|
|
1855
|
+
await ctx.cache.set(cacheKey, "1", { ttl: 86400 });
|
|
1856
|
+
|
|
1857
|
+
// Defer the DB write (does not block response)
|
|
1858
|
+
ctx.defer(async () => {
|
|
1859
|
+
await ctx.db.insert("greet_log", {
|
|
1860
|
+
user_id: ctx.user.platform_user_id,
|
|
1861
|
+
greeted_at: new Date().toISOString(),
|
|
1862
|
+
});
|
|
1863
|
+
});
|
|
1864
|
+
|
|
1865
|
+
return { action: "send_message", message: \`Hello @\${ctx.user.display_name}!\` };
|
|
1866
|
+
},
|
|
1867
|
+
});
|
|
1868
|
+
\`\`\`
|
|
1869
|
+
|
|
1870
|
+
### Exempt Role Support
|
|
1871
|
+
|
|
1872
|
+
Moderation modules should respect the account's exempt role configuration. The exempt roles (moderator, VIP, subscriber) are passed to the handler via \`ctx.user.roles\`. Check them before taking action:
|
|
1873
|
+
|
|
1874
|
+
\`\`\`tsx
|
|
1875
|
+
export const linkFilter = moderate({
|
|
1876
|
+
handler: async (ctx, message) => {
|
|
1877
|
+
// Respect exempt roles \u2014 do not moderate mods/VIPs
|
|
1878
|
+
if (ctx.user.roles.moderator || ctx.user.roles.vip) {
|
|
1879
|
+
return { action: "none" };
|
|
1880
|
+
}
|
|
1881
|
+
|
|
1882
|
+
if (containsLink(message.text)) {
|
|
1883
|
+
return { action: "delete", message: "Links are not allowed." };
|
|
1884
|
+
}
|
|
1885
|
+
return { action: "none" };
|
|
1886
|
+
},
|
|
1887
|
+
});
|
|
1888
|
+
\`\`\`
|
|
1557
1889
|
`;
|
|
1558
1890
|
|
|
1559
1891
|
// src/content/troubleshooting.ts
|
package/dist/index.js
CHANGED