diff --git a/docs/plugin-api-additions/contexts.md b/docs/plugin-api-additions/contexts.md new file mode 100644 index 00000000..a523ece9 --- /dev/null +++ b/docs/plugin-api-additions/contexts.md @@ -0,0 +1,24 @@ +# 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') +``` diff --git a/plugins/python/_zoitechat_contexts.py b/plugins/python/_zoitechat_contexts.py new file mode 100644 index 00000000..cfd32722 --- /dev/null +++ b/plugins/python/_zoitechat_contexts.py @@ -0,0 +1,75 @@ +"""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