Skip to content

Commit 3dab970

Browse files
committed
feat(plugins)!: rename the command handler to message
1 parent 5cdf54e commit 3dab970

5 files changed

Lines changed: 58 additions & 52 deletions

File tree

docs/content/plugins.mdx

Lines changed: 28 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ import { definePlugin } from 'zaileys'
3232
export default definePlugin({
3333
name: 'greet',
3434
description: 'Say hello',
35-
command: async (ctx) => {
35+
message: async (ctx) => {
3636
await ctx.reply('hi there 👋')
3737
},
3838
})
@@ -90,12 +90,12 @@ export default definePlugin({
9090
description: 'Say hello',
9191
cooldown: 3,
9292

93-
command: async (ctx) => {
93+
message: async (ctx) => {
9494
await ctx.reply('hi there 👋')
9595
},
9696

97-
message: (m) => {
98-
console.log('saw a message:', m.text)
97+
image: (ctx) => {
98+
console.log('someone sent a picture:', ctx.senderId)
9999
},
100100
})
101101
```
@@ -142,23 +142,23 @@ import { definePlugin } from 'zaileys'
142142
export default definePlugin({
143143
name: 'my-plugin',
144144
description: 'What it does',
145-
command: async (ctx) => { /* handles !my-plugin */ },
146-
message: (m) => { /* every inbound message */ },
145+
message: async (ctx) => { /* handles !my-plugin */ },
146+
image: (ctx) => { /* every inbound image */ },
147147
})
148148
```
149149

150150
### The `Plugin` shape
151151

152152
| Field | Type | Description |
153153
| ----- | ---- | ----------- |
154-
| `name` | `string` | Required. Identifies the plugin **and** names the command `command` handles. |
155-
| `command` | `(ctx, plugin) => void \| Promise<void>` | Handles the command named after this plugin. Omit if the plugin only listens to events. |
154+
| `name` | `string` | Required. Identifies the plugin **and** names the command it handles. `name: 'sticker'` answers `!sticker`. |
155+
| `message` | `(ctx, plugin) => void \| Promise<void>` | Handles that command. Omit if the plugin only listens to events. |
156156
| `setup` | `(ctx) => void \| (() => void) \| Promise<…>` | Optional escape hatch — see [below](#going-beyond-the-shorthands). |
157157
| `onUnload` | `() => void \| Promise<void>` | Optional cleanup, called on unload. |
158158

159159
Beside those, a plugin carries **every field a command spec carries**`aliases`, `description`,
160160
`usage`, `category`, `hidden`, `metadata`, and the guards `group`, `private`, `admin`, `cooldown`.
161-
They describe the command that `command` handles. See
161+
They describe the command that `message` handles. See
162162
[Commands](/commands#describing-a-command) for the full table.
163163

164164
```typescript
@@ -170,7 +170,7 @@ export default definePlugin({
170170
group: true,
171171
admin: true,
172172
cooldown: 3,
173-
command: async (ctx) => { /* ... */ },
173+
message: async (ctx) => { /* ... */ },
174174
})
175175
```
176176

@@ -180,32 +180,37 @@ without saying so.
180180
</Callout>
181181

182182
<Callout type="warning">
183-
Guards apply to **`command` only**. An event method like `message` receives the raw stream — check
184-
`m.isGroup` yourself if you need to narrow it. This keeps `admin` from triggering a group-metadata
183+
Guards apply to **`message` only**. An event method like `image` receives the raw stream — check
184+
`ctx.isGroup` yourself if you need to narrow it. This keeps `admin` from triggering a group-metadata
185185
lookup on every message that passes through your bot.
186186
</Callout>
187187

188188
## Event methods
189189

190-
Every inbound event has a matching optional method. Hyphenated event names become camelCase, so
191-
`poll-vote` is `pollVote`.
190+
Beside `message`, every inbound event has a matching optional method. Hyphenated event names become
191+
camelCase, so `poll-vote` is `pollVote`.
192192

193193
```typescript
194194
export default definePlugin({
195195
name: 'watcher',
196-
message: (m) => console.log(m.senderId, m.text),
197-
image: async (m) => { await m.reply('nice picture') },
198-
pollVote: async (v) => console.log(await v.options()),
199-
groupJoin: (e) => console.log('joined:', e.participants.length),
200-
callIncoming: (c) => console.log('call from', c.from),
196+
text: (ctx) => console.log(ctx.senderId, ctx.text),
197+
image: async (ctx) => { await ctx.reply('nice picture') },
198+
pollVote: async (vote) => console.log(await vote.options()),
199+
groupJoin: (event) => console.log('joined:', event.participants.length),
200+
callIncoming: (call) => console.log('call from', call.from),
201201
})
202202
```
203203

204-
Available: `message` · `text` · `image` · `video` · `audio` · `document` · `sticker` · `reaction` ·
204+
Available: `text` · `image` · `video` · `audio` · `document` · `sticker` · `reaction` ·
205205
`edit` · `delete` · `pollVote` · `buttonClick` · `listSelect` · `mention` · `mentionAll` ·
206206
`groupUpdate` · `groupJoin` · `groupLeave` · `memberTag` · `callIncoming` · `callEnded` ·
207207
`historySync` · `limited` · `presence` · `newsletter`.
208208

209+
<Callout type="info">
210+
There is no `message` event method — on a plugin that name is the **command handler**. To watch every
211+
inbound message regardless of type, use `setup(ctx)` and `ctx.on('message', …)`.
212+
</Callout>
213+
209214
Each is typed from [the event map](/events), so the payload needs no annotation. Listeners are
210215
removed automatically when the plugin unloads.
211216

@@ -214,7 +219,7 @@ removed automatically when the plugin unloads.
214219
A command handler gets the client straight off its context:
215220

216221
```typescript
217-
command: async (ctx) => {
222+
message: async (ctx) => {
218223
await ctx.client.group.removeMember(ctx.roomId!, ctx.mentions)
219224
},
220225
```
@@ -223,8 +228,8 @@ Event payloads are plain message contexts and carry no client, so every handler
223228
**plugin context** as a second argument:
224229

225230
```typescript
226-
message: async (msg, plugin) => {
227-
if (msg.text === 'ping') await plugin.client.send(msg.roomId!).text('pong')
231+
image: async (ctx, plugin) => {
232+
await plugin.client.send(ctx.roomId!).text('gambar diterima')
228233
},
229234
```
230235

src/plugin/registry.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,9 +53,9 @@ const specOf = (plugin: Plugin): CommandSpec => {
5353
return spec
5454
}
5555

56-
/** Turns the plugin's `command()` and per-event methods into real registrations. */
56+
/** Turns the plugin's `message` handler and per-event methods into real registrations. */
5757
const wireHandlers = (plugin: Plugin, ctx: PluginContext): void => {
58-
const run = plugin.command
58+
const run = plugin.message
5959
if (typeof run === 'function') {
6060
ctx.command(specOf(plugin), (commandCtx) => run(commandCtx, ctx))
6161
}

src/plugin/types.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,17 +32,19 @@ type Camel<S extends string> = S extends `${infer Head}-${infer Tail}`
3232
? `${Head}${Capitalize<Camel<Tail>>}`
3333
: S
3434

35-
/** One optional method per inbound event, derived so a new event is available here automatically. */
35+
/**
36+
* One optional method per inbound event, derived so a new event is available here automatically.
37+
* `message` is excluded — on a plugin that name belongs to the command handler.
38+
*/
3639
export type PluginEventHandlers = {
37-
[E in keyof InboundEventMap as Camel<E>]?: (
40+
[E in Exclude<keyof InboundEventMap, 'message'> as Camel<E>]?: (
3841
payload: InboundEventMap[E],
3942
ctx: PluginContext,
4043
) => void | Promise<void>
4144
}
4245

4346
/** Every inbound event name, in the order their methods are wired up. */
4447
export const INBOUND_EVENTS = [
45-
'message',
4648
'text',
4749
'image',
4850
'video',
@@ -67,7 +69,7 @@ export const INBOUND_EVENTS = [
6769
'limited',
6870
'presence',
6971
'newsletter',
70-
] as const satisfies ReadonlyArray<keyof InboundEventMap>
72+
] as const satisfies ReadonlyArray<Exclude<keyof InboundEventMap, 'message'>>
7173

7274
export type Plugin = CommandMeta &
7375
CommandGuards &
@@ -76,7 +78,7 @@ export type Plugin = CommandMeta &
7678
name: string
7779
aliases?: string[]
7880
/** Handles the command named after this plugin. The metadata above describes it. */
79-
command?: (ctx: Parameters<CommandHandler>[0], plugin: PluginContext) => void | Promise<void>
81+
message?: (ctx: Parameters<CommandHandler>[0], plugin: PluginContext) => void | Promise<void>
8082
/** Escape hatch for what the methods above cannot express: extra commands, middleware, cleanup. */
8183
setup?(ctx: PluginContext): void | (() => void) | Promise<void | (() => void)>
8284
onUnload?(): void | Promise<void>

tests/plugin/end-to-end.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ describe('plugin file to dispatched command', () => {
5353
`export default {
5454
name: 'ping',
5555
description: 'Cek bot',
56-
command: async (ctx) => { globalThis.__hit = ctx.command },
56+
message: async (ctx) => { globalThis.__hit = ctx.command },
5757
}`,
5858
)
5959
await boot()
@@ -70,11 +70,11 @@ describe('plugin file to dispatched command', () => {
7070
it('wires a declarative event method', async () => {
7171
await fs.writeFile(
7272
path.join(dir, 'info', 'watch.js'),
73-
`export default { name: 'watch', message: (m) => { globalThis.__seen = m.text } }`,
73+
`export default { name: 'watch', text: (ctx) => { globalThis.__seen = ctx.text } }`,
7474
)
7575
await boot()
7676

77-
client.emit('message', msg('halo'))
77+
client.emit('text', msg('halo'))
7878
expect((globalThis as Record<string, unknown>)['__seen']).toBe('halo')
7979
})
8080

tests/plugin/handlers.test.ts

Lines changed: 18 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,8 @@ const load = async (plugin: ReturnType<typeof definePlugin>, file = '/bot/plugin
2828
return { host: h, reg }
2929
}
3030

31-
describe('plugin metadata becomes the command spec', () => {
32-
it('registers command() using the plugin name and metadata, with no repetition', async () => {
31+
describe('the plugin is the command', () => {
32+
it('registers the command from the plugin name and metadata, with no repetition', async () => {
3333
const run = vi.fn()
3434
const { host: h } = await load(
3535
definePlugin({
@@ -40,7 +40,7 @@ describe('plugin metadata becomes the command spec', () => {
4040
group: true,
4141
admin: true,
4242
cooldown: 3,
43-
command: run,
43+
message: run,
4444
}),
4545
'/bot/plugins/group/kick.ts',
4646
)
@@ -57,18 +57,18 @@ describe('plugin metadata becomes the command spec', () => {
5757
})
5858
})
5959

60-
it('hands the plugin context to command() as a second argument', async () => {
60+
it('hands the plugin context to the handler as a second argument', async () => {
6161
const run = vi.fn()
62-
const { host: h } = await load(definePlugin({ name: 'x', command: run }))
62+
const { host: h } = await load(definePlugin({ name: 'x', message: run }))
6363
;(h.commands[0]!.handler as (c: unknown) => void)({ command: 'x' })
6464
expect(run).toHaveBeenCalledTimes(1)
6565
const [commandCtx, pluginCtx] = run.mock.calls[0] as [unknown, { category?: string }]
6666
expect(commandCtx).toEqual({ command: 'x' })
6767
expect(pluginCtx.category).toBe('tool')
6868
})
6969

70-
it('registers nothing when the plugin has no command handler', async () => {
71-
const { host: h } = await load(definePlugin({ name: 'quiet', description: 'no command' }))
70+
it('registers no command when the plugin has no message handler', async () => {
71+
const { host: h } = await load(definePlugin({ name: 'quiet', description: 'listener only' }))
7272
expect(h.commands).toHaveLength(0)
7373
})
7474
})
@@ -78,7 +78,7 @@ describe('event methods', () => {
7878
const { host: h } = await load(
7979
definePlugin({
8080
name: 'watcher',
81-
message: vi.fn(),
81+
text: vi.fn(),
8282
image: vi.fn(),
8383
pollVote: vi.fn(),
8484
callIncoming: vi.fn(),
@@ -87,23 +87,24 @@ describe('event methods', () => {
8787
expect(h.listeners.map((l) => l.event).sort()).toEqual([
8888
'call-incoming',
8989
'image',
90-
'message',
9190
'poll-vote',
91+
'text',
9292
])
9393
})
9494

9595
it('passes the payload and the plugin context to the method', async () => {
96-
const message = vi.fn()
97-
const { host: h } = await load(definePlugin({ name: 'watcher', message }))
96+
const text = vi.fn()
97+
const { host: h } = await load(definePlugin({ name: 'watcher', text }))
9898
h.listeners[0]!.handler({ text: 'halo' })
99-
const [payload, pluginCtx] = message.mock.calls[0] as [unknown, { category?: string }]
99+
const [payload, pluginCtx] = text.mock.calls[0] as [unknown, { category?: string }]
100100
expect(payload).toEqual({ text: 'halo' })
101101
expect(pluginCtx.category).toBe('tool')
102102
})
103103

104-
it('subscribes to nothing when no event method is present', async () => {
105-
const { host: h } = await load(definePlugin({ name: 'bare', command: vi.fn() }))
104+
it('never wires `message` as an event — that name is the command handler', async () => {
105+
const { host: h } = await load(definePlugin({ name: 'bare', message: vi.fn() }))
106106
expect(h.listeners).toHaveLength(0)
107+
expect(h.commands).toHaveLength(1)
107108
})
108109

109110
it('does not mistake plain metadata for a handler', async () => {
@@ -114,23 +115,21 @@ describe('event methods', () => {
114115
})
115116

116117
it('combines a command with event methods in one plugin', async () => {
117-
const { host: h } = await load(
118-
definePlugin({ name: 'both', command: vi.fn(), message: vi.fn() }),
119-
)
118+
const { host: h } = await load(definePlugin({ name: 'both', message: vi.fn(), image: vi.fn() }))
120119
expect(h.commands).toHaveLength(1)
121120
expect(h.listeners).toHaveLength(1)
122121
})
123122
})
124123

125124
describe('setup stays available', () => {
126125
it('is optional now', async () => {
127-
const { reg } = await load(definePlugin({ name: 'nosetup', command: vi.fn() }))
126+
const { reg } = await load(definePlugin({ name: 'nosetup', message: vi.fn() }))
128127
expect(reg.list()).toEqual(['nosetup'])
129128
})
130129

131130
it('still runs, alongside the declarative handlers', async () => {
132131
const setup = vi.fn()
133-
const { host: h } = await load(definePlugin({ name: 'mixed', command: vi.fn(), setup }))
132+
const { host: h } = await load(definePlugin({ name: 'mixed', message: vi.fn(), setup }))
134133
expect(setup).toHaveBeenCalledTimes(1)
135134
expect(h.commands).toHaveLength(1)
136135
})

0 commit comments

Comments
 (0)