Add standalone GusVoice AWG bot and Linux installer
This commit is contained in:
commit
38c6a1a682
23 files changed
+2924
No files matched your search
Vendored
+103
@@ -0,0 +1,103 @@
|
||||
# SPDX-License-Identifier: MIT
|
||||
"""Long polling loop: `getUpdates` with `offset`, one update at a time, in order.
|
||||
|
||||
The offset moves past an update only AFTER the handler has returned for it, so an update in flight when the process
|
||||
dies is delivered again. A handler exception goes to `on_error(update, exc)` (default: print the traceback) and the
|
||||
update counts as handled — otherwise one bad update would come back forever; make `on_error` re-raise to stop the loop
|
||||
without confirming that update instead. Fatal answers (401 — token revoked or bot disabled, 403 — bots are off on the
|
||||
instance, 404, 409 — a webhook is set or another copy of the bot is polling) raise `GusVoiceApiError`; everything else
|
||||
(network, 5xx, 400) is retried after a pause of 1 s growing to 30 s. Same rules as the TypeScript SDK `runPolling`.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import random
|
||||
import sys
|
||||
import threading
|
||||
import traceback
|
||||
from typing import TYPE_CHECKING, Any, Callable, Dict, Iterable, Optional
|
||||
|
||||
from .errors import GusVoiceApiError
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .client import Client
|
||||
|
||||
FATAL_CODES = frozenset({401, 403, 404, 409})
|
||||
RECONNECT_MIN_S = 1.0
|
||||
RECONNECT_MAX_S = 30.0
|
||||
|
||||
|
||||
def _default_on_error(update: Dict[str, Any], exc: BaseException) -> None:
|
||||
print(f"gusvoice_bot: handler failed on update {update.get('update_id')}:", file=sys.stderr)
|
||||
traceback.print_exception(type(exc), exc, exc.__traceback__, file=sys.stderr)
|
||||
|
||||
|
||||
def _log(message: str) -> None:
|
||||
print(f"gusvoice_bot: {message}", file=sys.stderr)
|
||||
|
||||
|
||||
def run_polling(
|
||||
client: "Client",
|
||||
handler: Callable[[Dict[str, Any]], Any],
|
||||
*,
|
||||
timeout: int = 30,
|
||||
limit: int = 100,
|
||||
allowed_updates: Optional[Iterable[str]] = None,
|
||||
on_error: Optional[Callable[[Dict[str, Any], BaseException], Any]] = None,
|
||||
stop_event: Optional[threading.Event] = None,
|
||||
log: Callable[[str], Any] = _log,
|
||||
) -> None:
|
||||
"""Receive updates until `stop_event` is set (or Ctrl+C) and call `handler(update)` for each.
|
||||
|
||||
`timeout` — seconds the server may hold one request (0-50). `getMe` is called first: a wrong token fails right away.
|
||||
A long poll is not interrupted by `stop_event`: the loop stops when the current request returns (≤ `timeout` s).
|
||||
On exit the handled updates are confirmed (`offset` without fetching anything new).
|
||||
"""
|
||||
on_error = on_error or _default_on_error
|
||||
wait = stop_event.wait if stop_event is not None else client._sleep
|
||||
poll_timeout = min(max(int(timeout), 0), 50)
|
||||
allowed = list(allowed_updates) if allowed_updates is not None else None
|
||||
client.get_me()
|
||||
last = 0 # update_id of the last handled update
|
||||
backoff = RECONNECT_MIN_S
|
||||
try:
|
||||
while stop_event is None or not stop_event.is_set():
|
||||
try:
|
||||
updates = client.get_updates(
|
||||
offset=last + 1 if last > 0 else None, limit=limit, timeout=poll_timeout, allowed_updates=allowed
|
||||
)
|
||||
backoff = RECONNECT_MIN_S
|
||||
except GusVoiceApiError as e:
|
||||
if e.code in FATAL_CODES:
|
||||
if e.code == 409:
|
||||
log(f"stopped: {e.description}")
|
||||
raise
|
||||
log(f"getUpdates failed ({e}), retry in {round(backoff)} s")
|
||||
wait(backoff * (0.8 + random.random() * 0.4))
|
||||
backoff = min(backoff * 2, RECONNECT_MAX_S)
|
||||
continue
|
||||
except Exception as e: # network, timeout, garbage — try again later
|
||||
log(f"getUpdates failed ({e!r}), retry in {round(backoff)} s")
|
||||
wait(backoff * (0.8 + random.random() * 0.4))
|
||||
backoff = min(backoff * 2, RECONNECT_MAX_S)
|
||||
continue
|
||||
for update in updates or []:
|
||||
if stop_event is not None and stop_event.is_set():
|
||||
break
|
||||
uid = update.get("update_id") if isinstance(update, dict) else None
|
||||
if not isinstance(uid, (int, float)) or isinstance(uid, bool):
|
||||
continue
|
||||
uid = int(uid)
|
||||
if uid <= last:
|
||||
continue # repeat of an already handled update
|
||||
try:
|
||||
handler(update)
|
||||
except Exception as exc:
|
||||
on_error(update, exc) # re-raise here to stop without confirming this update
|
||||
last = uid
|
||||
finally:
|
||||
if last > 0:
|
||||
try:
|
||||
# Confirm what was handled: an empty `allowed_updates` returns nothing but moves the offset.
|
||||
client.get_updates(offset=last + 1, timeout=0, limit=1, allowed_updates=[])
|
||||
except Exception:
|
||||
pass
|
||||
Reference in new issue
Block a user