From 44e677a973f00a21a47e0ac31813d8dcd0ff2155 Mon Sep 17 00:00:00 2001 From: deepend-tildeclub Date: Fri, 2 Oct 2026 14:12:37 -0600 Subject: [PATCH] add structured IRC event hooks --- docs/plugin-api-additions/events.md | 26 ++++++++++ plugins/python/_zoitechat_events.py | 77 +++++++++++++++++++++++++++++ 2 files changed, 103 insertions(+) create mode 100644 docs/plugin-api-additions/events.md create mode 100644 plugins/python/_zoitechat_events.py diff --git a/docs/plugin-api-additions/events.md b/docs/plugin-api-additions/events.md new file mode 100644 index 00000000..0a713889 --- /dev/null +++ b/docs/plugin-api-additions/events.md @@ -0,0 +1,26 @@ +# Semantic IRC hooks + +`on_join`, `on_part`, `on_quit`, `on_nick`, `on_kick`, `on_message`, and +`on_notice` accept `(callback, userdata=None, priority=PRI_NORM)`. The general +form is `hook_event(command, callback, userdata=None, priority=PRI_NORM)`. +Callbacks receive `(event, userdata)`, where the immutable IRCEvent has +command, prefix, nick, user, host, params, context, connection_id and time. +`params` preserves the trailing parameter, including spaces; JOIN parameters +also retain extended-join account/realname data when supplied by the server. + +These wrappers use existing server hooks, before core command processing; +return the ordinary EAT_* constants. EAT_ZOITECHAT suppresses the entire IRC +command (including client state updates), not just its text. Use EAT_NONE for +observation. Other plugins' priority and eating rules remain unchanged. +The context is the core-selected context, which can be the server tab when a +query/channel does not yet exist. QUIT is one event per incoming command, +not one callback per affected channel. `event.time` is the existing server-time +attribute (zero when unavailable); other message tags are not exposed here. + +```python +def joined(event, userdata): + event.context.prnt('{} joined {}'.format(event.nick, event.params[0])) + return zoitechat.EAT_NONE + +zoitechat.on_join(joined) +``` diff --git a/plugins/python/_zoitechat_events.py b/plugins/python/_zoitechat_events.py new file mode 100644 index 00000000..9341c16b --- /dev/null +++ b/plugins/python/_zoitechat_events.py @@ -0,0 +1,77 @@ +"""Structured, opt-in hooks before the IRC command's core processing.""" +from collections import namedtuple +import _zoitechat as api + +__all__ = ['IRCEvent', 'hook_event', 'on_join', 'on_part', 'on_quit', + 'on_nick', 'on_kick', 'on_message', 'on_notice'] +IRCEvent = namedtuple('IRCEvent', 'command prefix nick user host params context connection_id time') +_SUPPORTED = frozenset(('JOIN', 'PART', 'QUIT', 'NICK', 'KICK', 'PRIVMSG', 'NOTICE')) + + +def _parse(line): + prefix = '' + if line.startswith(':'): + prefix, separator, line = line[1:].partition(' ') + if not separator: + return None + command, separator, rest = line.partition(' ') + params = [] + while rest: + rest = rest.lstrip(' ') + if rest.startswith(':'): + params.append(rest[1:]) + break + param, separator, rest = rest.partition(' ') + if param: + params.append(param) + nick, marker, userhost = prefix.partition('!') + user, marker, host = userhost.partition('@') + return command.upper(), prefix, nick, user, host, tuple(params) + + +def hook_event(command, callback, userdata=None, priority=api.PRI_NORM): + """callback(event, userdata) returns existing EAT_* constants.""" + command = command.upper() + if command not in _SUPPORTED: + raise ValueError('unsupported semantic event: ' + command) + if not callable(callback): + raise TypeError('callback must be callable') + + def dispatch(word, word_eol, data, attrs): + if not word_eol: + return api.EAT_NONE + parsed = _parse(word_eol[0]) + if parsed is None or parsed[0] != command: + return api.EAT_NONE + event = IRCEvent(*(parsed + (api.get_context(), api.get_prefs('id'), attrs.time))) + return callback(event, data) + + return api.hook_server_attrs(command, dispatch, userdata, priority) + + +def on_join(callback, userdata=None, priority=api.PRI_NORM): + return hook_event('JOIN', callback, userdata, priority) + + +def on_part(callback, userdata=None, priority=api.PRI_NORM): + return hook_event('PART', callback, userdata, priority) + + +def on_quit(callback, userdata=None, priority=api.PRI_NORM): + return hook_event('QUIT', callback, userdata, priority) + + +def on_nick(callback, userdata=None, priority=api.PRI_NORM): + return hook_event('NICK', callback, userdata, priority) + + +def on_kick(callback, userdata=None, priority=api.PRI_NORM): + return hook_event('KICK', callback, userdata, priority) + + +def on_message(callback, userdata=None, priority=api.PRI_NORM): + return hook_event('PRIVMSG', callback, userdata, priority) + + +def on_notice(callback, userdata=None, priority=api.PRI_NORM): + return hook_event('NOTICE', callback, userdata, priority)