mirror of
https://github.com/ZoiteChat/zoitechat.git
synced 2026-11-08 17:05:38 +00:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
66c93fe2e1 |
No files matched your search
@@ -1,24 +0,0 @@
|
|||||||
# Connection and context navigation
|
|
||||||
|
|
||||||
`get_connections()` returns one Connection snapshot per live connection:
|
|
||||||
`id`, `network`, `server`, `context`. Connection IDs distinguish simultaneous
|
|
||||||
connections with identical names. Prefer its server-tab context where present.
|
|
||||||
`get_connection(context=None)` finds the owner of a live context.
|
|
||||||
|
|
||||||
`get_contexts(connection_id=None, types=None)` lists live contexts, optionally
|
|
||||||
filtered by connection ID (or a Connection object) and session types: server=1,
|
|
||||||
channel=2, query=3. `connection.contexts(types=None)` is a convenience method.
|
|
||||||
`find_context_by_id(connection_id, channel=None)` / `connection.find(channel)`
|
|
||||||
resolve using that connection's IRC comparator; no channel selects the server tab.
|
|
||||||
Missing/closed contexts return None. Existing find_context() remains unchanged.
|
|
||||||
|
|
||||||
These helpers use the existing channels list's ID/context fields, so no new C
|
|
||||||
context ABI is needed. Snapshots are not persistent IDs across client restarts;
|
|
||||||
refresh them after closing/recreating connections. Always check Context.set()
|
|
||||||
when explicitly selecting a context that might have closed.
|
|
||||||
|
|
||||||
```python
|
|
||||||
connection = zoitechat.get_connection()
|
|
||||||
for context in connection.contexts(types=(2,)):
|
|
||||||
context.prnt('A channel on this exact connection')
|
|
||||||
```
|
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
# Python descriptor hooks
|
||||||
|
|
||||||
|
`hook_fd(fd, callback, userdata=None, flags=FD_READ)` returns an ordinary hook
|
||||||
|
handle accepted by `unhook()`. Callback arguments are `(fd, ready_flags, userdata)`;
|
||||||
|
return True to retain the watch and False/None to remove it. Exceptions print a
|
||||||
|
traceback and remove the watch. Unloading the script removes its watches.
|
||||||
|
The callback runs in the context captured during registration, and that context
|
||||||
|
is restored afterwards. A closed context removes the watch.
|
||||||
|
|
||||||
|
Use nonblocking IO and bounded reads/writes. The descriptor belongs to the script;
|
||||||
|
unhook before closing it. On Windows socket descriptors use the default flags;
|
||||||
|
CRT descriptors need FD_NOTSOCKET. The existing C API uses int descriptors: values
|
||||||
|
outside its range are rejected rather than truncated. This does not add threads.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
```python
|
||||||
|
watch = zoitechat.hook_fd(sock.fileno(), on_readable, flags=zoitechat.FD_READ)
|
||||||
|
# def on_readable(fd, flags, userdata): ...; return True
|
||||||
|
```
|
||||||
@@ -1,75 +0,0 @@
|
|||||||
"""Navigate live connections by ID instead of ambiguous server/network names."""
|
|
||||||
from collections import namedtuple
|
|
||||||
import operator
|
|
||||||
import _zoitechat as api
|
|
||||||
|
|
||||||
__all__ = ['Connection', 'get_connections', 'get_connection', 'get_contexts',
|
|
||||||
'find_context_by_id']
|
|
||||||
|
|
||||||
|
|
||||||
class Connection(namedtuple('_Connection', 'id network server context')):
|
|
||||||
__slots__ = ()
|
|
||||||
|
|
||||||
def contexts(self, types=None):
|
|
||||||
return get_contexts(self.id, types)
|
|
||||||
|
|
||||||
def find(self, channel=None):
|
|
||||||
return find_context_by_id(self.id, channel)
|
|
||||||
|
|
||||||
|
|
||||||
def _id(connection):
|
|
||||||
return operator.index(connection.id if isinstance(connection, Connection) else connection)
|
|
||||||
|
|
||||||
|
|
||||||
def get_connections():
|
|
||||||
"""Return one snapshot per live connection, including disconnected ones."""
|
|
||||||
entries = api.get_list('channels')
|
|
||||||
by_id = {}
|
|
||||||
order = []
|
|
||||||
for entry in entries:
|
|
||||||
if entry.id not in by_id:
|
|
||||||
order.append(entry.id)
|
|
||||||
if entry.id not in by_id or entry.type == 1:
|
|
||||||
by_id[entry.id] = Connection(entry.id, entry.network, entry.server,
|
|
||||||
entry.context)
|
|
||||||
return [by_id[connection_id] for connection_id in order]
|
|
||||||
|
|
||||||
|
|
||||||
def get_connection(context=None):
|
|
||||||
"""Find the owning connection of a Context; return None when it has closed."""
|
|
||||||
context = context or api.get_context()
|
|
||||||
for entry in api.get_list('channels'):
|
|
||||||
if entry.context == context:
|
|
||||||
for connection in get_connections():
|
|
||||||
if connection.id == entry.id:
|
|
||||||
return connection
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def get_contexts(connection_id=None, types=None):
|
|
||||||
"""Return contexts, optionally restricted to a connection and session types."""
|
|
||||||
if connection_id is not None:
|
|
||||||
connection_id = _id(connection_id)
|
|
||||||
if types is not None:
|
|
||||||
types = frozenset(types)
|
|
||||||
return [entry.context for entry in api.get_list('channels')
|
|
||||||
if (connection_id is None or entry.id == connection_id)
|
|
||||||
and (types is None or entry.type in types)]
|
|
||||||
|
|
||||||
|
|
||||||
def find_context_by_id(connection_id, channel=None):
|
|
||||||
"""Resolve a live context on exactly one connection; None selects its server tab."""
|
|
||||||
connection_id = _id(connection_id)
|
|
||||||
previous = api.get_context()
|
|
||||||
try:
|
|
||||||
for entry in api.get_list('channels'):
|
|
||||||
if entry.id != connection_id:
|
|
||||||
continue
|
|
||||||
if channel is None:
|
|
||||||
if entry.type == 1:
|
|
||||||
return entry.context
|
|
||||||
elif entry.context.set() and api.nickcmp(entry.channel, channel) == 0:
|
|
||||||
return entry.context
|
|
||||||
finally:
|
|
||||||
previous.set()
|
|
||||||
return None
|
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
"""File descriptor readiness hooks; callbacks execute on the main thread."""
|
||||||
|
import operator
|
||||||
|
import _zoitechat as api
|
||||||
|
from _zoitechat_embedded import ffi, lib
|
||||||
|
|
||||||
|
__all__ = ['FD_READ', 'FD_WRITE', 'FD_EXCEPTION', 'FD_NOTSOCKET', 'hook_fd']
|
||||||
|
FD_READ, FD_WRITE, FD_EXCEPTION, FD_NOTSOCKET = 1, 2, 4, 8
|
||||||
|
|
||||||
|
|
||||||
|
def hook_fd(fd, callback, userdata=None, flags=FD_READ):
|
||||||
|
"""callback(fd, ready_flags, userdata): True keeps the watch, False removes it."""
|
||||||
|
fd = operator.index(fd)
|
||||||
|
flags = operator.index(flags)
|
||||||
|
if fd < 0 or fd > 2147483647:
|
||||||
|
raise ValueError('fd must fit the C API nonnegative int descriptor')
|
||||||
|
if flags & ~15 or not flags & 7:
|
||||||
|
raise ValueError('flags must select READ, WRITE or EXCEPTION')
|
||||||
|
if not callable(callback):
|
||||||
|
raise TypeError('callback must be callable')
|
||||||
|
plugin = api.__get_current_plugin()
|
||||||
|
context = api.get_context()
|
||||||
|
|
||||||
|
def dispatch(descriptor, ready, data):
|
||||||
|
previous = api.get_context()
|
||||||
|
if not context.set():
|
||||||
|
return False
|
||||||
|
try:
|
||||||
|
return callback(descriptor, ready, data)
|
||||||
|
finally:
|
||||||
|
previous.set()
|
||||||
|
|
||||||
|
hook = plugin.add_hook(dispatch, userdata)
|
||||||
|
handle = lib.zoitechat_hook_fd(lib.ph, fd, flags, lib._on_fd_hook, hook.handle)
|
||||||
|
if handle == ffi.NULL:
|
||||||
|
hook.is_unload = True
|
||||||
|
plugin.remove_hook(id(hook))
|
||||||
|
raise RuntimeError('unable to watch descriptor')
|
||||||
|
hook.zoitechat_hook = handle
|
||||||
|
return id(hook)
|
||||||
@@ -38,6 +38,7 @@ extern "Python" int _on_print_attrs_hook(char **, zoitechat_event_attrs *, void
|
|||||||
extern "Python" int _on_server_hook(char **, char **, void *);
|
extern "Python" int _on_server_hook(char **, char **, void *);
|
||||||
extern "Python" int _on_server_attrs_hook(char **, char **, zoitechat_event_attrs *, void *);
|
extern "Python" int _on_server_attrs_hook(char **, char **, zoitechat_event_attrs *, void *);
|
||||||
extern "Python" int _on_timer_hook(void *);
|
extern "Python" int _on_timer_hook(void *);
|
||||||
|
extern "Python" int _on_fd_hook(int, int, void *);
|
||||||
|
|
||||||
extern "Python" int _on_plugin_init(char **, char **, char **, char *, char *);
|
extern "Python" int _on_plugin_init(char **, char **, char **, char *, char *);
|
||||||
extern "Python" int _on_plugin_deinit(void);
|
extern "Python" int _on_plugin_deinit(void);
|
||||||
|
|||||||
@@ -304,6 +304,28 @@ def _on_timer_hook(userdata):
|
|||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
@ffi.def_extern()
|
||||||
|
def _on_fd_hook(fd, flags, userdata):
|
||||||
|
hook = ffi.from_handle(userdata)
|
||||||
|
keep = False
|
||||||
|
try:
|
||||||
|
keep = bool(hook.callback(fd, flags, hook.userdata))
|
||||||
|
except Exception:
|
||||||
|
traceback.print_exc()
|
||||||
|
if not keep:
|
||||||
|
# The C watch removes itself on return; do not unhook it a second time.
|
||||||
|
try:
|
||||||
|
hook.is_unload = True
|
||||||
|
# from_handle() yields a weak proxy: id(proxy) is not id(Hook).
|
||||||
|
for existing in hook.plugin.hooks:
|
||||||
|
if existing == hook:
|
||||||
|
hook.plugin.remove_hook(id(existing))
|
||||||
|
break
|
||||||
|
except ReferenceError:
|
||||||
|
pass # the callback already unhooked itself
|
||||||
|
return int(keep)
|
||||||
|
|
||||||
|
|
||||||
@ffi.def_extern()
|
@ffi.def_extern()
|
||||||
def _on_say_command(word, word_eol, userdata):
|
def _on_say_command(word, word_eol, userdata):
|
||||||
"""Handle input in the special >>python<< tab.
|
"""Handle input in the special >>python<< tab.
|
||||||
|
|||||||
Reference in new issue
Block a user