mirror of
https://github.com/ZoiteChat/zoitechat.git
synced 2026-11-08 17:05:38 +00:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f33ea41573 |
No files matched your search
@@ -0,0 +1,30 @@
|
|||||||
|
# Optional Python asyncio integration
|
||||||
|
|
||||||
|
`run_async(coroutine, context=None, interval=10)` returns an AsyncTask with
|
||||||
|
`done()`, `cancel()` and `result()`. Each invocation owns a separate event loop
|
||||||
|
pumped by an existing main-loop timer. A ready stop callback ensures selector
|
||||||
|
polling never blocks the UI. The captured context is restored after every pump;
|
||||||
|
a closed context cancels the work. No process-wide event-loop policy is changed.
|
||||||
|
|
||||||
|
Existing synchronous scripts are unaffected. Use `asyncio.get_running_loop()`
|
||||||
|
inside the coroutine. Child tasks use the same loop/context and are cancelled
|
||||||
|
when the main coroutine finishes or the script unloads. Cancellation cleanup is
|
||||||
|
bounded; tasks must not suppress cancellation indefinitely. Cleanup uses the
|
||||||
|
captured context while it remains open; after it closes, cancellation cleanup uses
|
||||||
|
the current valid context. Use explicit Context methods for context-specific
|
||||||
|
output in finally blocks, and do not issue context-dependent commands there. This provides normal
|
||||||
|
asyncio sockets/timers, not Windows subprocess support or a new HTTP library.
|
||||||
|
Asyncio hostname resolution can use its executor; use a resolver appropriate to
|
||||||
|
your library if you require no worker threads. Blocking work inside a coroutine
|
||||||
|
still blocks the client and must be avoided.
|
||||||
|
|
||||||
|
```python
|
||||||
|
import asyncio
|
||||||
|
import zoitechat
|
||||||
|
|
||||||
|
async def later():
|
||||||
|
await asyncio.sleep(1)
|
||||||
|
zoitechat.prnt('Still in the context where this task was started')
|
||||||
|
|
||||||
|
task = zoitechat.run_async(later())
|
||||||
|
```
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
# 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)
|
|
||||||
```
|
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
"""Opt-in asyncio tasks pumped without blocking the client main loop."""
|
||||||
|
import asyncio
|
||||||
|
import operator
|
||||||
|
import traceback
|
||||||
|
import _zoitechat as api
|
||||||
|
|
||||||
|
__all__ = ['AsyncTask', 'run_async']
|
||||||
|
|
||||||
|
|
||||||
|
class AsyncTask:
|
||||||
|
"""A script-owned task and isolated event loop; cancel() is cooperative."""
|
||||||
|
def __init__(self, coroutine, plugin, context, interval):
|
||||||
|
self._plugin = plugin
|
||||||
|
self._context = context
|
||||||
|
self._loop = asyncio.new_event_loop()
|
||||||
|
self._task = self._loop.create_task(coroutine)
|
||||||
|
self._closed = False
|
||||||
|
self._unload_hook = api.hook_unload(lambda data: self._close())
|
||||||
|
self._timer_hook = api.hook_timer(interval, self._tick)
|
||||||
|
|
||||||
|
def done(self):
|
||||||
|
return self._task.done()
|
||||||
|
|
||||||
|
def cancel(self):
|
||||||
|
if not self._closed:
|
||||||
|
return self._task.cancel()
|
||||||
|
return False
|
||||||
|
|
||||||
|
def result(self):
|
||||||
|
return self._task.result()
|
||||||
|
|
||||||
|
def _pump(self):
|
||||||
|
# A ready stop callback forces the selector's timeout to zero.
|
||||||
|
self._loop.call_soon(self._loop.stop)
|
||||||
|
self._loop.run_forever()
|
||||||
|
|
||||||
|
def _close(self):
|
||||||
|
if self._closed:
|
||||||
|
return
|
||||||
|
self._closed = True
|
||||||
|
previous = api.get_context()
|
||||||
|
self._context.set()
|
||||||
|
try:
|
||||||
|
pending = asyncio.all_tasks(self._loop)
|
||||||
|
for task in pending:
|
||||||
|
task.cancel()
|
||||||
|
# Bound cleanup work; tasks must cooperate with cancellation.
|
||||||
|
for unused in range(3):
|
||||||
|
if not any(not task.done() for task in pending):
|
||||||
|
break
|
||||||
|
self._pump()
|
||||||
|
for task in pending:
|
||||||
|
if not task.done():
|
||||||
|
self._loop.call_exception_handler({
|
||||||
|
'message': 'Script task ignored cancellation during unload',
|
||||||
|
'task': task,
|
||||||
|
})
|
||||||
|
finally:
|
||||||
|
self._loop.close()
|
||||||
|
previous.set()
|
||||||
|
|
||||||
|
def _tick(self, userdata):
|
||||||
|
if self._closed:
|
||||||
|
return False
|
||||||
|
previous = api.get_context()
|
||||||
|
if not self._context.set():
|
||||||
|
self._close()
|
||||||
|
self._plugin.remove_hook(self._unload_hook)
|
||||||
|
return False
|
||||||
|
try:
|
||||||
|
self._pump()
|
||||||
|
if not self._task.done():
|
||||||
|
return True
|
||||||
|
try:
|
||||||
|
self._task.result()
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
pass
|
||||||
|
except Exception:
|
||||||
|
traceback.print_exc()
|
||||||
|
self._close()
|
||||||
|
self._plugin.remove_hook(self._unload_hook)
|
||||||
|
return False
|
||||||
|
except Exception:
|
||||||
|
traceback.print_exc()
|
||||||
|
self._close()
|
||||||
|
self._plugin.remove_hook(self._unload_hook)
|
||||||
|
return False
|
||||||
|
finally:
|
||||||
|
previous.set()
|
||||||
|
|
||||||
|
|
||||||
|
def run_async(coroutine, context=None, interval=10):
|
||||||
|
"""Schedule a coroutine in a captured context; return an AsyncTask handle."""
|
||||||
|
interval = operator.index(interval)
|
||||||
|
if interval < 1 or interval > 2147483647:
|
||||||
|
raise ValueError('interval must be a positive C int in milliseconds')
|
||||||
|
if not asyncio.iscoroutine(coroutine):
|
||||||
|
raise TypeError('run_async expects a coroutine object')
|
||||||
|
plugin = api.__get_current_plugin()
|
||||||
|
return AsyncTask(coroutine, plugin, context or api.get_context(), interval)
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
"""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)
|
|
||||||
Reference in new issue
Block a user