Compare commits

..
Author SHA1 Message Date
deepend 668c3cc0d7 filter hooks by connection and context in core
(cherry picked from commit 9126ce9202)
2026-11-08 06:20:05 -06:00
deepend 2355ffc5d0 reserve additive API slots and load optional Python modules
(cherry picked from commit 1cdee8ea6b)
2026-11-08 06:00:01 -06:00
12 changed files with 212 additions and 4 deletions

No files matched your search

@@ -0,0 +1,24 @@
# Optional plugin API additions (discussion #144)
Existing hooks, callbacks, return values, context lookup, strings and menu behaviour
remain unchanged. The C plugin function table gains four fixed append-only slots;
unused slots are NULL. Older binaries use the unchanged prefix. A plugin must check
the documented get_info() feature key before accessing a new slot on an older host.
New Python modules are installed by Meson and the existing Windows copy/installer
pipeline. Only modules present in the installation are exported; imports of
zoitechat, hexchat and xchat retain their compatibility aliases.
All APIs run on the client main thread unless explicitly documented otherwise.
New reserved flags must be zero. These changes do not make old API calls thread-safe.
Nested hook dispatch defers deleted-hook reclamation until its outermost return,
so new observers can safely emit events and unhook themselves or other hooks.
The optional asyncio module requires Python 3.7 or newer; older supported
interpreters skip that module and retain the existing scripting API.
Python reload drops the script/API modules while retaining the embedding module
so callback and lib.ph pointer types keep the same CFFI identity.
Server-hook insertion matches both raw and attrs hooks so documented priorities
are honoured for mixed registrations, including new filtered attrs callbacks.
+24
View File
@@ -0,0 +1,24 @@
# Core-filtered hooks
Python: `hook_server_filtered(name, callback, userdata=None, priority=PRI_NORM,
connection_id=None, channel=None)` and `hook_print_filtered(...)`.
Callbacks match the existing attrs variants: `(word, word_eol, userdata, attrs)`.
The connection ID distinguishes duplicate names; None/-1 means any connection
(including connection ID zero). A channel restriction uses that connection's
IRC comparator. None means any context; an empty string is an exact restriction.
C: the corresponding zoitechat_hook_*_filtered functions accept
`(ph, name, pri, flags, connection_id, channel, callback, userdata)`;
flags=0, ID=-1 for any and NULL channel for any. Check
`get_info("api_context_filters")` for "1" before accessing the new Windows slots.
Filtering takes place before Python/interpreter entry. Existing unrestricted
hooks, priority, EAT semantics and unload/unhook handling are unchanged.
Filters refer to the core-selected context, not an IRC target parsed from the
line: unknown queries/channels can still have a server-tab context, and QUIT
has its usual single server-hook context. Connection IDs last only this process.
```python
zoitechat.hook_print_filtered('Channel Message', callback,
connection_id=zoitechat.get_prefs('id'), channel='#zoitechat')
```
+21
View File
@@ -383,3 +383,24 @@ def find_context(server=None, channel=None):
return None
return Context(ctx)
# Optional additive APIs live in separate modules so builds can select features.
def __load_additive_features():
import importlib
import os
for suffix in ('fd', 'events', 'async', 'names', 'contexts',
'post', 'filters', 'caps'):
if suffix == 'async' and sys.version_info < (3, 7):
continue
name = '_zoitechat_' + suffix
if not os.path.isfile(os.path.join(os.path.dirname(__file__), name + '.py')):
continue
module = importlib.import_module(name)
for symbol in module.__all__:
globals()[symbol] = getattr(module, symbol)
__all__.append(symbol)
__load_additive_features()
+36
View File
@@ -0,0 +1,36 @@
"""Context restrictions evaluated by core before entering Python."""
import operator
import _zoitechat as api
from _zoitechat_embedded import ffi, lib
__all__ = ['hook_server_filtered', 'hook_print_filtered']
def _register(name, callback, userdata, priority, connection_id, channel, server):
if not callable(callback):
raise TypeError('callback must be callable')
connection_id = -1 if connection_id is None else operator.index(connection_id)
if connection_id < -1 or connection_id > 2147483647:
raise ValueError('connection_id must fit the C API; -1 means any')
plugin = api.__get_current_plugin()
hook = plugin.add_hook(callback, userdata)
register = lib.zoitechat_hook_server_filtered if server else lib.zoitechat_hook_print_filtered
dispatch = lib._on_server_attrs_hook if server else lib._on_print_attrs_hook
handle = register(lib.ph, name.encode(), priority, 0, connection_id,
ffi.NULL if channel is None else channel.encode(), dispatch, hook.handle)
if handle == ffi.NULL:
hook.is_unload = True
plugin.remove_hook(id(hook))
raise RuntimeError('unable to install filtered hook')
hook.zoitechat_hook = handle
return id(hook)
def hook_server_filtered(name, callback, userdata=None, priority=api.PRI_NORM,
connection_id=None, channel=None):
return _register(name, callback, userdata, priority, connection_id, channel, True)
def hook_print_filtered(name, callback, userdata=None, priority=api.PRI_NORM,
connection_id=None, channel=None):
return _register(name, callback, userdata, priority, connection_id, channel, False)
+5 -1
View File
@@ -27,7 +27,11 @@ python3_source = custom_target('python-bindings',
command: [find_program('generate_plugin.py'), '@INPUT@', '@OUTPUT@']
)
install_data(['_zoitechat.py', 'zoitechat.py', 'xchat.py', 'hexchat.py'],
# Include any selected additive API modules alongside the compatibility modules.
python_api_files = run_command(python3, '-c',
'import glob,sys; print("\\n".join(sorted(glob.glob(sys.argv[1] + "/_zoitechat*.py"))))',
meson.current_source_dir(), check: true).stdout().strip().split('\n')
install_data(python_api_files + ['zoitechat.py', 'xchat.py', 'hexchat.py'],
install_dir: join_paths(get_option('libdir'), 'zoitechat/python')
)
+6 -1
View File
@@ -590,7 +590,12 @@ def _on_plugin_deinit():
sys.stderr = sys.__stderr__
pydoc.help = pydoc.Helper()
for mod in ('_zoitechat', 'zoitechat', 'xchat', '_zoitechat_embedded'):
# CFFI callbacks keep the embedding module's ffi/lib identity for the
# process lifetime; deleting it creates incompatible types on reload.
for mod in ('_zoitechat', 'zoitechat', 'hexchat', 'xchat',
'_zoitechat_fd', '_zoitechat_events', '_zoitechat_async',
'_zoitechat_names', '_zoitechat_contexts', '_zoitechat_post',
'_zoitechat_filters', '_zoitechat_caps'):
try:
del sys.modules[mod]
+1
View File
@@ -52,6 +52,7 @@
<None Include="xchat.py" />
<None Include="hexchat.py" />
<None Include="_zoitechat.py" />
<None Include="_zoitechat_*.py" />
</ItemGroup>
<ItemGroup>
<ClCompile Include="$(IntDir)python.c" />
+1
View File
@@ -52,6 +52,7 @@
<None Include="xchat.py" />
<None Include="hexchat.py" />
<None Include="_zoitechat.py" />
<None Include="_zoitechat_*.py" />
</ItemGroup>
<ItemGroup>
<ClCompile Include="$(IntDir)python.c" />
+60 -2
View File
@@ -99,6 +99,9 @@ struct _zoitechat_hook
int tag; /* for timers & FDs only */
int type; /* HOOK_* */
int pri; /* fd */ /* priority / fd for HOOK_FD only */
int connection_filter;
char *channel_filter;
gboolean context_filtered;
};
struct _zoitechat_list
@@ -176,6 +179,7 @@ enum
GSList *plugin_list = NULL; /* export for plugingui.c */
static GSList *hook_list = NULL;
static unsigned int plugin_dispatch_depth;
extern const struct prefs vars[]; /* cfgfiles.c */
@@ -243,7 +247,7 @@ plugin_list_add (zoitechat_context *ctx, char *filename, const char *name,
{
zoitechat_plugin *pl;
pl = g_new (zoitechat_plugin, 1);
pl = g_new0 (zoitechat_plugin, 1);
pl->handle = handle;
pl->filename = filename;
pl->context = ctx;
@@ -350,6 +354,8 @@ plugin_add (session *sess, char *filename, void *handle, void *init_func,
pl->zoitechat_emit_print_attrs = zoitechat_emit_print_attrs;
pl->zoitechat_event_attrs_create = zoitechat_event_attrs_create;
pl->zoitechat_event_attrs_free = zoitechat_event_attrs_free;
pl->zoitechat_hook_server_filtered = zoitechat_hook_server_filtered;
pl->zoitechat_hook_print_filtered = zoitechat_hook_print_filtered;
/* 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)
@@ -610,6 +616,7 @@ plugin_hook_run (session *sess, char *name, char *word[], char *word_eol[],
zoitechat_hook *hook;
int ret, eat = 0;
plugin_dispatch_depth++;
list = hook_list;
while (1)
{
@@ -619,6 +626,15 @@ plugin_hook_run (session *sess, char *name, char *word[], char *word_eol[],
hook = list->data;
next = list->next;
if (hook->context_filtered &&
(!is_session (sess) ||
(hook->connection_filter != -1 && hook->connection_filter != sess->server->id) ||
(hook->channel_filter && sess->server->p_cmp (hook->channel_filter, sess->channel))))
{
list = next;
continue; /* filter in C before entering an interpreter */
}
hook->pl->context = sess;
/* run the plugin's callback function */
@@ -655,6 +671,10 @@ plugin_hook_run (session *sess, char *name, char *word[], char *word_eol[],
}
xit:
/* Nested callbacks can unhook entries still used by an outer dispatch.
* Keep both hook objects and list links alive until the outermost return. */
if (--plugin_dispatch_depth != 0)
return eat;
/* really remove deleted hooks now */
list = hook_list;
while (list)
@@ -806,7 +826,7 @@ plugin_insert_hook (zoitechat_hook *new_hook)
break;
case HOOK_SERVER:
case HOOK_SERVER_ATTRS:
new_hook_type = HOOK_SERVER | HOOK_PRINT_ATTRS;
new_hook_type = HOOK_SERVER | HOOK_SERVER_ATTRS;
break;
default:
new_hook_type = new_hook->type;
@@ -1018,10 +1038,45 @@ zoitechat_unhook (zoitechat_plugin *ph, zoitechat_hook *hook)
g_free (hook->name); /* NULL for timers & fds */
g_free (hook->help_text); /* NULL for non-commands */
g_free (hook->channel_filter);
hook->channel_filter = NULL;
return hook->userdata;
}
static zoitechat_hook *
plugin_add_filtered_hook (zoitechat_plugin *ph, const char *name, int pri,
int flags, int connection_id, const char *channel,
int type, void *callback, void *userdata)
{
zoitechat_hook *hook;
if (flags != 0 || connection_id < -1 || !name || !callback)
return NULL;
hook = plugin_add_hook (ph, type, pri, name, NULL, callback, 0, userdata);
hook->context_filtered = TRUE;
hook->connection_filter = connection_id;
hook->channel_filter = g_strdup (channel);
return hook;
}
zoitechat_hook *
zoitechat_hook_server_filtered (zoitechat_plugin *ph, const char *name, int pri,
int flags, int connection_id, const char *channel,
zoitechat_serv_attrs_cb *callback, void *userdata)
{
return plugin_add_filtered_hook (ph, name, pri, flags, connection_id, channel,
HOOK_SERVER_ATTRS, callback, userdata);
}
zoitechat_hook *
zoitechat_hook_print_filtered (zoitechat_plugin *ph, const char *name, int pri,
int flags, int connection_id, const char *channel,
zoitechat_print_attrs_cb *callback, void *userdata)
{
return plugin_add_filtered_hook (ph, name, pri, flags, connection_id, channel,
HOOK_PRINT_ATTRS, callback, userdata);
}
zoitechat_hook *
zoitechat_hook_command (zoitechat_plugin *ph, const char *name, int pri,
zoitechat_cmd_cb *callb, const char *help_text, void *userdata)
@@ -1181,6 +1236,9 @@ zoitechat_get_info (zoitechat_plugin *ph, const char *id)
return text_find_format_string (e);
}
if (strcmp (id, "api_context_filters") == 0)
return "1";
hash = str_hash (id);
/* do the session independant ones first */
switch (hash)
+9
View File
@@ -150,6 +150,15 @@ struct _zoitechat_plugin
void (*zoitechat_event_attrs_free) (zoitechat_plugin *ph,
zoitechat_event_attrs *attrs);
/* Additive API slots: append only; old binary offsets are unchanged. */
zoitechat_hook *(*zoitechat_hook_print_after) (zoitechat_plugin *, const char *, int,
int, int (*)(char *[], zoitechat_event_attrs *, void *), void *);
zoitechat_hook *(*zoitechat_hook_server_filtered) (zoitechat_plugin *, const char *, int,
int, int, const char *, int (*)(char *[], char *[], zoitechat_event_attrs *, void *), void *);
zoitechat_hook *(*zoitechat_hook_print_filtered) (zoitechat_plugin *, const char *, int,
int, int, const char *, int (*)(char *[], zoitechat_event_attrs *, void *), void *);
zoitechat_hook *(*zoitechat_register_capability) (zoitechat_plugin *, const char *, int, int, void *);
/* PRIVATE FIELDS! */
void *handle; /* from dlopen */
char *filename; /* loaded from */
+23
View File
@@ -193,10 +193,31 @@ struct _zoitechat_plugin
zoitechat_event_attrs *(*zoitechat_event_attrs_create) (zoitechat_plugin *ph);
void (*zoitechat_event_attrs_free) (zoitechat_plugin *ph,
zoitechat_event_attrs *attrs);
/* Additive API slots: append only; old binary offsets are unchanged. */
zoitechat_hook *(*zoitechat_hook_print_after) (zoitechat_plugin *, const char *, int,
int, int (*)(char *[], zoitechat_event_attrs *, void *), void *);
zoitechat_hook *(*zoitechat_hook_server_filtered) (zoitechat_plugin *, const char *, int,
int, int, const char *, int (*)(char *[], char *[], zoitechat_event_attrs *, void *), void *);
zoitechat_hook *(*zoitechat_hook_print_filtered) (zoitechat_plugin *, const char *, int,
int, int, const char *, int (*)(char *[], zoitechat_event_attrs *, void *), void *);
zoitechat_hook *(*zoitechat_register_capability) (zoitechat_plugin *, const char *, int, int, void *);
};
#endif
/* api_context_filters=1: connection_id -1 is any; NULL channel is any. */
zoitechat_hook *zoitechat_hook_server_filtered (zoitechat_plugin *ph,
const char *name, int pri, int flags, int connection_id, const char *channel,
int (*callback) (char *word[], char *word_eol[],
zoitechat_event_attrs *attrs, void *userdata),
void *userdata);
zoitechat_hook *zoitechat_hook_print_filtered (zoitechat_plugin *ph,
const char *name, int pri, int flags, int connection_id, const char *channel,
int (*callback) (char *word[], zoitechat_event_attrs *attrs, void *userdata),
void *userdata);
zoitechat_hook *
zoitechat_hook_command (zoitechat_plugin *ph,
const char *name,
@@ -416,6 +437,8 @@ zoitechat_pluginpref_list (zoitechat_plugin *ph,
#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_print ((ZOITECHAT_PLUGIN_HANDLE)->zoitechat_hook_print)
#define zoitechat_hook_server_filtered ((ZOITECHAT_PLUGIN_HANDLE)->zoitechat_hook_server_filtered)
#define zoitechat_hook_print_filtered ((ZOITECHAT_PLUGIN_HANDLE)->zoitechat_hook_print_filtered)
#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_fd ((ZOITECHAT_PLUGIN_HANDLE)->zoitechat_hook_fd)
+2
View File
@@ -90,6 +90,7 @@
<MSWindowsTheme Include="$(DepsRoot)\share\themes\MS-Windows\**\*" />
<HicolorIcons Include="$(DepsRoot)\share\icons\hicolor\index.theme" />
<HicolorIcons Include="$(DepsRoot)\share\icons\hicolor\**\*.*" />
<PythonAdditiveApis Include="..\..\plugins\python\_zoitechat_*.py" />
</ItemGroup>
<Copy SourceFiles="@(None)" DestinationFolder="$(ZoiteChatRel)" />
@@ -115,6 +116,7 @@
<Copy SourceFiles="..\..\plugins\python\hexchat.py" DestinationFolder="$(ZoiteChatRel)\python" Condition="'$(Python3Enabled)'=='true' or '$(Python38Enabled)'=='true'" />
<Copy SourceFiles="..\..\plugins\python\zoitechat.py" DestinationFolder="$(ZoiteChatRel)\python" Condition="'$(Python3Enabled)'=='true' or '$(Python38Enabled)'=='true'" />
<Copy SourceFiles="..\..\plugins\python\_zoitechat.py" DestinationFolder="$(ZoiteChatRel)\python" Condition="'$(Python3Enabled)'=='true' or '$(Python38Enabled)'=='true'" />
<Copy SourceFiles="@(PythonAdditiveApis)" DestinationFolder="$(ZoiteChatRel)\python" Condition="('$(Python3Enabled)'=='true' or '$(Python38Enabled)'=='true') and '@(PythonAdditiveApis)' != ''" />
<WriteLinesToFile File="$(ZoiteChatRel)portable-mode" Lines="2" Overwrite="true" />