Browse API 1 documentation
grove.filters.set()
Replace inbound and outbound message-filter callbacks.
Replace inbound and outbound message-filter callbacks.
Signature
grove.filters.set(handlers: {
inbound?: (message: GroveFilterMessage) => FilterResult | Promise<FilterResult>
outbound?: (message: GroveFilterMessage) => FilterResult | Promise<FilterResult>
conversations?: ('channel' | 'query')[]
}): void
Availability
IRC-bound runtimes with messages.filter.inbound and/or messages.filter.outbound. Grants are checked independently by direction. Scope limits which conversations participate. Composer utilities, application Roots, and settings pages cannot filter.
Parameters
| Parameter | Required | Description |
|---|---|---|
handlers.inbound |
No | Callback before personal display; omitted/non-function removes this direction. |
handlers.outbound |
No | Callback before sending; omitted/non-function removes this direction. |
handlers.conversations |
No | Limit to channel, query, or both. [] matches none. Default is both for ordinary filters, but query only when messages.protect is granted. Explicitly include channel for a protected channel protocol. |
Return value
Registration returns undefined. Each callback may return a result synchronously or through a Promise. Return values are described below.
Behavior and errors
Every call replaces both handlers and the conversation selection. Pass {} to unregister. Missing per-direction permission omits that direction. Changes to the accepted registration reset filter health; repeating an identical registration is not a guaranteed circuit-breaker reset.
Ordinary filters have a 150 ms per-message deadline; protected-message extensions have two seconds. Five consecutive timeouts remove a filter from the active chain. A thrown/rejected handler, timeout, absent callback, or unusable result normally allows the message. Log failures inside callbacks if diagnostics are needed; the facade’s filter catch converts a thrown exception to allow. Persistent protection policy can instead block when its owner fails to provide a valid protect result.
Callback input
interface GroveFilterMessage {
direction: 'inbound' | 'outbound'
networkID: string
target: string; targetKey: string
from: string; source: string; sourceKey: string
localNickname?: string; localNicknameKey?: string
account?: string; host?: string
kind: 'message' | 'action' | 'notice'
conversation: 'channel' | 'query'
delivery: 'live' | 'history' | 'replay'
supportsRelayMessage: boolean
relayedBy?: string
text: string; wireText: string; displayText: string
displaySource?: string
}
networkID is opaque session identity. target names the conversation (counterpart for a query); targetKey is its canonical key. source/from name the sender (the local sender for outbound), and sourceKey is canonical. localNickname and localNicknameKey identify the local user in either direction when known. account and host are optional server-supplied sender identity. relayedBy identifies the server-reported relay bot when present; supportsRelayMessage reports native relay support. All optional fields are omitted when unknown.
kind, conversation, direction, and delivery are host classifications. Use them directly; do not guess conversation kind from channel-prefix characters. Canonical keys are opaque and must not be lowercased again. wireText is the current protocol form after earlier filters; displayText is the current presentation; text is its compatibility alias. displaySource is a presentation-only sender override, never the authoritative identity.
Advance protocol sessions and send protocol responses only for live delivery. history is server backlog; replay re-evaluates existing transcript data. Ordinary message events use a different payload and do not substitute for this input.
Callback results
| Result | Meaning |
|---|---|
| ‘allow’, true, undefined, null | Continue unchanged. |
| false or ‘deny’ | Drop the message and stop this chain. |
| Other nonempty string | Legacy replacement; changes both forms until a prior transform separated them, then changes display only. |
| { action: ‘deny’, reason?: string } | Drop; optional safe outbound failure explanation, capped at 160 characters. |
| { action: ‘replace’, text: string } | Legacy replacement; newContent and new_content aliases also work. |
| { action: ‘transform’, wireText?, displayText?, displaySource?, decorations? } | Change supplied nonempty forms; inbound displaySource changes presentation only. A text field alone is not a transform shorthand. |
| { action: ‘confirm’, text?, wireText?, displayText?, prompt?, decorations? } | Outbound-only review request. text changes both forms unless a more specific field overrides it. |
| { action: ‘protect’, outboundMessages?, displayText?, hidden?, decorations? } | Terminal separately granted protocol result; see protection details below. |
Text fields are strings; hidden is boolean; outboundMessages is an array of strings. Replacement/transform text is bounded to 4,096 characters, displaySource to 64, and confirmation prompts to 240. Empty replacement fields do not erase a message; use deny. Decorations are objects { id: string, text: string, icon?: string }; up to four are retained, with 64-character bounds on each field. text remains required for fallback/accessibility when an SF Symbol icon is supplied.
Confirmation runs the rest of the outbound chain before the host presents its own review card. A later deny still blocks. Approval sends the captured final result without rerunning filters; extensions cannot approve their own requests. Inbound confirm is ignored.
Protected results
Requires messages.protect as well as the matching filter grant. protect applies only to ordinary messages, not ACTION or NOTICE. It terminates the chain. Outbound output has at most 32 ordered message bodies; Mango validates all frames before initiating the send. This is validation atomicity, not a guarantee that a disconnected network delivers every frame. Ordinary protected user messages need nonempty displayText; hidden protocol controls can omit it. Live inbound queries may emit response frames to the same peer. Inbound channels, history, and replay never emit those response frames.
A protect result alone does not install persistent policy. Use grove.protection.set to prevent a later crash/stop from allowing plaintext. See its page before implementing a security protocol.
Example
grove.filters.set({
conversations: ['channel'],
inbound: message => ({
action: 'transform',
displayText: message.displayText.replace(/^\[relay\] /u, ''),
decorations: [{ id: 'relay', text: 'RELAY' }]
})
})
See grove.protection.set() and Message filters.