Compare commits

..
Author SHA1 Message Date
deepend 37c93bb0f0 add structured IRC event hooks
(cherry picked from commit 44e677a973)
2026-11-08 06:05:59 -06:00
8 changed files with 104 additions and 92 deletions

No files matched your search

+26
View File
@@ -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)
```
-21
View File
@@ -1,21 +0,0 @@
# Post-display print observers
Python: `hook_print_after(name, callback, userdata=None, priority=PRI_NORM)`.
The callback is `(word, word_eol, userdata, attrs)`, matching hook_print_attrs().
It runs after display_event() has handled the text, so prnt() follows the original
text. Return values are ignored. Events eaten by a pre-hook or hidden by client
preferences do not invoke these observers. This is post-display notification,
not a notification that the entire incoming IRC command has finished updating state.
C: `zoitechat_hook_print_after(ph, name, priority, flags, callback, userdata)`;
flags must be zero. Check `get_info("api_hook_print_after")` for "1" before
accessing the appended Windows function-table slot. Existing pre-hook timing is
unchanged. Unhook/unload use existing lifecycle functions. Recursive emission
skips an already-running observer to prevent it immediately invoking itself.
```python
def after(word, word_eol, userdata, attrs):
zoitechat.prnt('This follows the original channel message')
zoitechat.hook_print_after('Channel Message', after)
```
+77
View File
@@ -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)
-21
View File
@@ -1,21 +0,0 @@
"""Opt-in post-display print observers."""
import _zoitechat as api
from _zoitechat_embedded import ffi, lib
__all__ = ['hook_print_after']
def hook_print_after(name, callback, userdata=None, priority=api.PRI_NORM):
"""callback(word, word_eol, userdata, attrs); its return value is ignored."""
if not callable(callback):
raise TypeError('callback must be callable')
plugin = api.__get_current_plugin()
hook = plugin.add_hook(callback, userdata)
handle = lib.zoitechat_hook_print_after(lib.ph, name.encode(), priority, 0,
lib._on_print_attrs_hook, hook.handle)
if handle == ffi.NULL:
hook.is_unload = True
plugin.remove_hook(id(hook))
raise RuntimeError('unable to install post-display hook')
hook.zoitechat_hook = handle
return id(hook)
+1 -41
View File
@@ -143,8 +143,7 @@ enum
HOOK_PRINT_ATTRS = 1 << 4, /* same as above, with attributes */ HOOK_PRINT_ATTRS = 1 << 4, /* same as above, with attributes */
HOOK_TIMER = 1 << 5, /* timeouts */ HOOK_TIMER = 1 << 5, /* timeouts */
HOOK_FD = 1 << 6, /* sockets & fds */ HOOK_FD = 1 << 6, /* sockets & fds */
HOOK_DELETED = 1 << 7, /* marked for deletion */ HOOK_DELETED = 1 << 7 /* marked for deletion */
HOOK_PRINT_AFTER = 1 << 8 /* post-display observers */
}; };
enum enum
@@ -352,7 +351,6 @@ plugin_add (session *sess, char *filename, void *handle, void *init_func,
pl->zoitechat_emit_print_attrs = zoitechat_emit_print_attrs; pl->zoitechat_emit_print_attrs = zoitechat_emit_print_attrs;
pl->zoitechat_event_attrs_create = zoitechat_event_attrs_create; pl->zoitechat_event_attrs_create = zoitechat_event_attrs_create;
pl->zoitechat_event_attrs_free = zoitechat_event_attrs_free; pl->zoitechat_event_attrs_free = zoitechat_event_attrs_free;
pl->zoitechat_hook_print_after = zoitechat_hook_print_after;
/* run zoitechat_plugin_init, if it returns 0, close the plugin */ /* run zoitechat_plugin_init, if it returns 0, close the plugin */
if (((zoitechat_init_func *)init_func) (pl, &pl->name, &pl->desc, &pl->version, arg) == 0) if (((zoitechat_init_func *)init_func) (pl, &pl->name, &pl->desc, &pl->version, arg) == 0)
@@ -623,13 +621,6 @@ plugin_hook_run (session *sess, char *name, char *word[], char *word_eol[],
hook = list->data; hook = list->data;
next = list->next; next = list->next;
if (type == HOOK_PRINT_AFTER && hook->tag)
{
list = next;
continue;
}
if (type == HOOK_PRINT_AFTER)
hook->tag = 1;
hook->pl->context = sess; hook->pl->context = sess;
/* run the plugin's callback function */ /* run the plugin's callback function */
@@ -638,7 +629,6 @@ plugin_hook_run (session *sess, char *name, char *word[], char *word_eol[],
case HOOK_COMMAND: case HOOK_COMMAND:
ret = ((zoitechat_cmd_cb *)hook->callback) (word, word_eol, hook->userdata); ret = ((zoitechat_cmd_cb *)hook->callback) (word, word_eol, hook->userdata);
break; break;
case HOOK_PRINT_AFTER:
case HOOK_PRINT_ATTRS: case HOOK_PRINT_ATTRS:
ret = ((zoitechat_print_attrs_cb *)hook->callback) (word, attrs, hook->userdata); ret = ((zoitechat_print_attrs_cb *)hook->callback) (word, attrs, hook->userdata);
break; break;
@@ -653,15 +643,6 @@ plugin_hook_run (session *sess, char *name, char *word[], char *word_eol[],
break; break;
} }
if (type == HOOK_PRINT_AFTER)
{
hook->tag = 0;
if (!is_session (sess))
goto xit;
list = next;
continue;
}
if ((ret & ZOITECHAT_EAT_ZOITECHAT) && (ret & ZOITECHAT_EAT_PLUGIN)) if ((ret & ZOITECHAT_EAT_ZOITECHAT) && (ret & ZOITECHAT_EAT_PLUGIN))
{ {
eat = 1; eat = 1;
@@ -744,15 +725,6 @@ plugin_emit_print (session *sess, char *word[], time_t server_time)
HOOK_PRINT | HOOK_PRINT_ATTRS); HOOK_PRINT | HOOK_PRINT_ATTRS);
} }
void
plugin_emit_print_after (session *sess, char *word[], time_t server_time)
{
zoitechat_event_attrs attrs;
attrs.server_time_utc = server_time;
if (is_session (sess))
plugin_hook_run (sess, word[0], word, NULL, &attrs, HOOK_PRINT_AFTER);
}
int int
plugin_emit_dummy_print (session *sess, char *name) plugin_emit_dummy_print (session *sess, char *name)
{ {
@@ -1056,15 +1028,6 @@ zoitechat_unhook (zoitechat_plugin *ph, zoitechat_hook *hook)
return hook->userdata; return hook->userdata;
} }
zoitechat_hook *
zoitechat_hook_print_after (zoitechat_plugin *ph, const char *name, int pri,
int flags, zoitechat_print_attrs_cb *callback, void *userdata)
{
if (flags != 0 || !name || !callback)
return NULL;
return plugin_add_hook (ph, HOOK_PRINT_AFTER, pri, name, NULL, callback, 0, userdata);
}
zoitechat_hook * zoitechat_hook *
zoitechat_hook_command (zoitechat_plugin *ph, const char *name, int pri, zoitechat_hook_command (zoitechat_plugin *ph, const char *name, int pri,
zoitechat_cmd_cb *callb, const char *help_text, void *userdata) zoitechat_cmd_cb *callb, const char *help_text, void *userdata)
@@ -1224,9 +1187,6 @@ zoitechat_get_info (zoitechat_plugin *ph, const char *id)
return text_find_format_string (e); return text_find_format_string (e);
} }
if (strcmp (id, "api_hook_print_after") == 0)
return "1";
hash = str_hash (id); hash = str_hash (id);
/* do the session independant ones first */ /* do the session independant ones first */
switch (hash) switch (hash)
-1
View File
@@ -183,7 +183,6 @@ int plugin_emit_command (session *sess, char *name, char *word[], char *word_eol
int plugin_emit_server (session *sess, char *name, char *word[], char *word_eol[], int plugin_emit_server (session *sess, char *name, char *word[], char *word_eol[],
time_t server_time); time_t server_time);
int plugin_emit_print (session *sess, char *word[], time_t server_time); int plugin_emit_print (session *sess, char *word[], time_t server_time);
void plugin_emit_print_after (session *sess, char *word[], time_t server_time);
int plugin_emit_dummy_print (session *sess, char *name); int plugin_emit_dummy_print (session *sess, char *name);
int plugin_emit_keypress (session *sess, unsigned int state, unsigned int keyval, gunichar key); int plugin_emit_keypress (session *sess, unsigned int state, unsigned int keyval, gunichar key);
GList* plugin_command_list(GList *tmp_list); GList* plugin_command_list(GList *tmp_list);
-1
View File
@@ -2320,7 +2320,6 @@ text_emit_with_message (int index, session *sess, char *a, char *b, char *c, cha
if (!prefs.hex_away_omit_alerts || !sess->server->is_away) if (!prefs.hex_away_omit_alerts || !sess->server->is_away)
sound_play_event (index); sound_play_event (index);
display_event (sess, index, word, stripcolor_args, timestamp, message); display_event (sess, index, word, stripcolor_args, timestamp, message);
plugin_emit_print_after (sess, word, timestamp);
} }
char * char *
-7
View File
@@ -206,12 +206,6 @@ struct _zoitechat_plugin
#endif #endif
/* api_hook_print_after=1: observer returns are ignored; flags must be zero. */
zoitechat_hook *zoitechat_hook_print_after (zoitechat_plugin *ph,
const char *name, int pri, int flags,
int (*callback) (char *word[], zoitechat_event_attrs *attrs, void *userdata),
void *userdata);
zoitechat_hook * zoitechat_hook *
zoitechat_hook_command (zoitechat_plugin *ph, zoitechat_hook_command (zoitechat_plugin *ph,
const char *name, const char *name,
@@ -431,7 +425,6 @@ zoitechat_pluginpref_list (zoitechat_plugin *ph,
#define zoitechat_hook_server ((ZOITECHAT_PLUGIN_HANDLE)->zoitechat_hook_server) #define zoitechat_hook_server ((ZOITECHAT_PLUGIN_HANDLE)->zoitechat_hook_server)
#define zoitechat_hook_server_attrs ((ZOITECHAT_PLUGIN_HANDLE)->zoitechat_hook_server_attrs) #define zoitechat_hook_server_attrs ((ZOITECHAT_PLUGIN_HANDLE)->zoitechat_hook_server_attrs)
#define zoitechat_hook_print ((ZOITECHAT_PLUGIN_HANDLE)->zoitechat_hook_print) #define zoitechat_hook_print ((ZOITECHAT_PLUGIN_HANDLE)->zoitechat_hook_print)
#define zoitechat_hook_print_after ((ZOITECHAT_PLUGIN_HANDLE)->zoitechat_hook_print_after)
#define zoitechat_hook_print_attrs ((ZOITECHAT_PLUGIN_HANDLE)->zoitechat_hook_print_attrs) #define zoitechat_hook_print_attrs ((ZOITECHAT_PLUGIN_HANDLE)->zoitechat_hook_print_attrs)
#define zoitechat_hook_timer ((ZOITECHAT_PLUGIN_HANDLE)->zoitechat_hook_timer) #define zoitechat_hook_timer ((ZOITECHAT_PLUGIN_HANDLE)->zoitechat_hook_timer)
#define zoitechat_hook_fd ((ZOITECHAT_PLUGIN_HANDLE)->zoitechat_hook_fd) #define zoitechat_hook_fd ((ZOITECHAT_PLUGIN_HANDLE)->zoitechat_hook_fd)