| """ |
| Drop-in replacement for telegram.ext.JobQueue / telegram.ext.Job. |
| |
| python-telegram-bot's own JobQueue is itself a thin wrapper around |
| APScheduler's AsyncIOScheduler, and APScheduler is already a direct |
| dependency of this project (see src/service/leaderboard_service.py's use of |
| CronTrigger), so this wrapper talks to it directly instead of going through |
| an intermediate Telegram-specific abstraction. |
| """ |
|
|
| import datetime |
| import logging |
| import traceback |
|
|
| from apscheduler.schedulers.asyncio import AsyncIOScheduler |
| from apscheduler.triggers.interval import IntervalTrigger |
| from apscheduler.triggers.date import DateTrigger |
|
|
| logger = logging.getLogger(__name__) |
|
|
|
|
| def _as_timedelta(value) -> datetime.timedelta: |
| if isinstance(value, datetime.timedelta): |
| return value |
| return datetime.timedelta(seconds=value) |
|
|
|
|
| class Job: |
| """Mirrors telegram.ext.Job.""" |
|
|
| def __init__(self, aps_job, name, data, callback, application): |
| self._aps_job = aps_job |
| self.name = name |
| self.data = data |
| self._callback = callback |
| self._application = application |
|
|
| @property |
| def next_t(self): |
| return getattr(self._aps_job, "next_run_time", None) if self._aps_job else None |
|
|
| def schedule_removal(self): |
| try: |
| self._aps_job.remove() |
| except Exception: |
| pass |
|
|
| async def run(self, application): |
| """Manually trigger this job's callback once, immediately. Mirrors |
| telegram.ext.Job.run(), used by timer_service.py's should_run_on_startup.""" |
| context = application.new_context(job=self) |
| try: |
| await self._callback(context) |
| except Exception: |
| logger.exception(f"Error running job {self.name}") |
| await context.bot.report_error(f"Error running job {self.name}:\n\n{traceback.format_exc()}") |
|
|
|
|
| class JobQueue: |
| """Mirrors telegram.ext.JobQueue.""" |
|
|
| def __init__(self, application, timezone=None): |
| self._application = application |
| self._scheduler = AsyncIOScheduler(timezone=timezone) |
|
|
| async def start(self): |
| if not self._scheduler.running: |
| self._scheduler.start() |
|
|
| async def stop(self): |
| if self._scheduler.running: |
| self._scheduler.shutdown(wait=False) |
|
|
| def _make_runner(self, callback, job_holder): |
| async def _runner(): |
| context = self._application.new_context(job=job_holder[0]) |
| try: |
| await callback(context) |
| except Exception: |
| name = job_holder[0].name if job_holder[0] else "<unknown>" |
| logger.exception(f"Error running job {name}") |
| await context.bot.report_error(f"Error running job {name}:\n\n{traceback.format_exc()}") |
|
|
| return _runner |
|
|
| def run_repeating(self, callback, interval, first=None, name=None, data=None): |
| job_holder = [None] |
| if first is None: |
| next_run_time = None |
| else: |
| next_run_time = datetime.datetime.now(self._scheduler.timezone) + _as_timedelta(first) |
| aps_job = self._scheduler.add_job( |
| self._make_runner(callback, job_holder), |
| trigger=IntervalTrigger(seconds=_as_timedelta(interval).total_seconds()), |
| next_run_time=next_run_time, |
| name=name, |
| ) |
| job = Job(aps_job, name=name, data=data, callback=callback, application=self._application) |
| job_holder[0] = job |
| return job |
|
|
| def run_once(self, callback, when, data=None, name=None): |
| job_holder = [None] |
| if isinstance(when, datetime.datetime): |
| run_date = when |
| else: |
| run_date = datetime.datetime.now(self._scheduler.timezone) + _as_timedelta(when) |
| aps_job = self._scheduler.add_job( |
| self._make_runner(callback, job_holder), |
| trigger=DateTrigger(run_date=run_date), |
| name=name, |
| ) |
| job = Job(aps_job, name=name, data=data, callback=callback, application=self._application) |
| job_holder[0] = job |
| return job |
|
|
| def run_custom(self, callback, job_kwargs, name=None, data=None): |
| job_holder = [None] |
| kwargs = dict(job_kwargs) |
| trigger = kwargs.pop("trigger") |
| aps_job = self._scheduler.add_job( |
| self._make_runner(callback, job_holder), |
| trigger=trigger, |
| name=name, |
| **kwargs, |
| ) |
| job = Job(aps_job, name=name, data=data, callback=callback, application=self._application) |
| job_holder[0] = job |
| return job |
|
|