2018-03-14 12:28:21 +03:00
|
|
|
import itertools
|
2017-09-30 11:12:01 +03:00
|
|
|
import logging
|
2017-09-07 21:17:40 +03:00
|
|
|
from datetime import datetime
|
2018-03-14 12:28:21 +03:00
|
|
|
from queue import Queue, Empty
|
2017-11-30 23:09:34 +03:00
|
|
|
from threading import RLock, Thread
|
2017-09-07 21:17:40 +03:00
|
|
|
|
2018-03-14 12:28:21 +03:00
|
|
|
from . import utils
|
2017-09-07 21:17:40 +03:00
|
|
|
from .tl import types as tl
|
2017-09-07 19:49:08 +03:00
|
|
|
|
2017-12-20 14:47:10 +03:00
|
|
|
__log__ = logging.getLogger(__name__)
|
|
|
|
|
2017-09-07 19:49:08 +03:00
|
|
|
|
|
|
|
class UpdateState:
|
2018-03-23 23:40:24 +03:00
|
|
|
"""
|
|
|
|
Used to hold the current state of processed updates.
|
|
|
|
To retrieve an update, :meth:`poll` should be called.
|
2017-09-07 19:49:08 +03:00
|
|
|
"""
|
2017-09-30 12:21:07 +03:00
|
|
|
WORKER_POLL_TIMEOUT = 5.0 # Avoid waiting forever on the workers
|
|
|
|
|
2017-09-30 12:17:31 +03:00
|
|
|
def __init__(self, workers=None):
|
|
|
|
"""
|
|
|
|
:param workers: This integer parameter has three possible cases:
|
|
|
|
workers is None: Updates will *not* be stored on self.
|
|
|
|
workers = 0: Another thread is responsible for calling self.poll()
|
|
|
|
workers > 0: 'workers' background threads will be spawned, any
|
2018-02-18 15:29:05 +03:00
|
|
|
any of them will invoke the self.handler.
|
2017-09-30 12:17:31 +03:00
|
|
|
"""
|
|
|
|
self._workers = workers
|
2017-09-30 11:12:01 +03:00
|
|
|
self._worker_threads = []
|
|
|
|
|
2018-02-18 15:29:05 +03:00
|
|
|
self.handler = None
|
2017-09-07 21:17:40 +03:00
|
|
|
self._updates_lock = RLock()
|
2017-11-30 23:09:34 +03:00
|
|
|
self._updates = Queue()
|
2017-09-07 19:49:08 +03:00
|
|
|
|
2017-09-07 21:17:40 +03:00
|
|
|
# https://core.telegram.org/api/updates
|
|
|
|
self._state = tl.updates.State(0, 0, datetime.now(), 0, 0)
|
2017-09-30 11:12:01 +03:00
|
|
|
|
2017-09-08 13:54:38 +03:00
|
|
|
def can_poll(self):
|
|
|
|
"""Returns True if a call to .poll() won't lock"""
|
2017-11-30 23:09:34 +03:00
|
|
|
return not self._updates.empty()
|
2017-09-07 19:49:08 +03:00
|
|
|
|
2017-09-30 12:21:07 +03:00
|
|
|
def poll(self, timeout=None):
|
2018-03-01 22:13:21 +03:00
|
|
|
"""
|
|
|
|
Polls an update or blocks until an update object is available.
|
|
|
|
If 'timeout is not None', it should be a floating point value,
|
|
|
|
and the method will 'return None' if waiting times out.
|
2017-09-30 12:21:07 +03:00
|
|
|
"""
|
2017-11-30 23:09:34 +03:00
|
|
|
try:
|
2018-03-01 22:13:21 +03:00
|
|
|
return self._updates.get(timeout=timeout)
|
2017-11-30 23:09:34 +03:00
|
|
|
except Empty:
|
2018-03-01 22:13:21 +03:00
|
|
|
return None
|
2017-09-07 19:49:08 +03:00
|
|
|
|
2017-09-30 11:12:01 +03:00
|
|
|
def get_workers(self):
|
|
|
|
return self._workers
|
|
|
|
|
|
|
|
def set_workers(self, n):
|
2017-09-30 12:17:31 +03:00
|
|
|
"""Changes the number of workers running.
|
|
|
|
If 'n is None', clears all pending updates from memory.
|
|
|
|
"""
|
2018-03-01 22:13:21 +03:00
|
|
|
if n is None:
|
|
|
|
self.stop_workers()
|
|
|
|
else:
|
|
|
|
self._workers = n
|
2017-10-01 20:56:24 +03:00
|
|
|
self.setup_workers()
|
2017-09-30 11:12:01 +03:00
|
|
|
|
|
|
|
workers = property(fget=get_workers, fset=set_workers)
|
|
|
|
|
2017-10-01 20:56:24 +03:00
|
|
|
def stop_workers(self):
|
2018-03-01 15:31:39 +03:00
|
|
|
"""
|
2018-03-01 22:13:21 +03:00
|
|
|
Waits for all the worker threads to stop.
|
2017-09-30 11:12:01 +03:00
|
|
|
"""
|
2018-03-01 22:13:21 +03:00
|
|
|
# Put dummy ``None`` objects so that they don't need to timeout.
|
|
|
|
n = self._workers
|
|
|
|
self._workers = None
|
2018-03-02 12:10:59 +03:00
|
|
|
if n:
|
|
|
|
with self._updates_lock:
|
|
|
|
for _ in range(n):
|
|
|
|
self._updates.put(None)
|
2017-09-30 19:39:31 +03:00
|
|
|
|
2017-09-30 11:12:01 +03:00
|
|
|
for t in self._worker_threads:
|
|
|
|
t.join()
|
|
|
|
|
|
|
|
self._worker_threads.clear()
|
2018-03-15 12:29:12 +03:00
|
|
|
self._workers = n
|
2017-09-30 11:12:01 +03:00
|
|
|
|
2017-10-01 20:56:24 +03:00
|
|
|
def setup_workers(self):
|
2017-09-30 12:17:31 +03:00
|
|
|
if self._worker_threads or not self._workers:
|
|
|
|
# There already are workers, or workers is None or 0. Do nothing.
|
2017-09-30 11:12:01 +03:00
|
|
|
return
|
|
|
|
|
|
|
|
for i in range(self._workers):
|
|
|
|
thread = Thread(
|
|
|
|
target=UpdateState._worker_loop,
|
|
|
|
name='UpdateWorker{}'.format(i),
|
|
|
|
daemon=True,
|
|
|
|
args=(self, i)
|
|
|
|
)
|
|
|
|
self._worker_threads.append(thread)
|
|
|
|
thread.start()
|
|
|
|
|
|
|
|
def _worker_loop(self, wid):
|
2018-03-01 22:13:21 +03:00
|
|
|
while self._workers is not None:
|
2017-09-30 11:12:01 +03:00
|
|
|
try:
|
2017-09-30 12:21:07 +03:00
|
|
|
update = self.poll(timeout=UpdateState.WORKER_POLL_TIMEOUT)
|
2018-02-18 15:29:05 +03:00
|
|
|
if update and self.handler:
|
|
|
|
self.handler(update)
|
2017-09-30 11:12:01 +03:00
|
|
|
except StopIteration:
|
|
|
|
break
|
2017-10-25 14:04:12 +03:00
|
|
|
except:
|
2017-09-30 11:12:01 +03:00
|
|
|
# We don't want to crash a worker thread due to any reason
|
2017-12-20 14:47:10 +03:00
|
|
|
__log__.exception('Unhandled exception on worker %d', wid)
|
2017-09-30 11:12:01 +03:00
|
|
|
|
2018-04-25 14:37:29 +03:00
|
|
|
def get_update_state(self, entity_id):
|
|
|
|
"""Gets the updates.State corresponding to the given entity or 0."""
|
|
|
|
return self._state
|
|
|
|
|
2017-09-07 19:49:08 +03:00
|
|
|
def process(self, update):
|
|
|
|
"""Processes an update object. This method is normally called by
|
|
|
|
the library itself.
|
|
|
|
"""
|
2017-09-30 12:17:31 +03:00
|
|
|
if self._workers is None:
|
|
|
|
return # No processing needs to be done if nobody's working
|
2017-09-07 21:17:40 +03:00
|
|
|
|
|
|
|
with self._updates_lock:
|
|
|
|
if isinstance(update, tl.updates.State):
|
2017-12-20 14:47:10 +03:00
|
|
|
__log__.debug('Saved new updates state')
|
2017-09-07 21:17:40 +03:00
|
|
|
self._state = update
|
2017-09-19 14:17:40 +03:00
|
|
|
return # Nothing else to be done
|
2017-09-07 19:58:54 +03:00
|
|
|
|
2018-02-03 17:42:43 +03:00
|
|
|
if hasattr(update, 'pts'):
|
|
|
|
self._state.pts = update.pts
|
2017-10-14 12:37:47 +03:00
|
|
|
|
2018-02-03 17:42:43 +03:00
|
|
|
# After running the script for over an hour and receiving over
|
|
|
|
# 1000 updates, the only duplicates received were users going
|
|
|
|
# online or offline. We can trust the server until new reports.
|
2018-03-14 12:28:21 +03:00
|
|
|
# This should only be used as read-only.
|
2017-11-30 22:40:35 +03:00
|
|
|
if isinstance(update, tl.UpdateShort):
|
2018-03-29 01:56:05 +03:00
|
|
|
update.update._entities = {}
|
2017-11-30 23:09:34 +03:00
|
|
|
self._updates.put(update.update)
|
2017-11-30 22:40:35 +03:00
|
|
|
# Expand "Updates" into "Update", and pass these to callbacks.
|
|
|
|
# Since .users and .chats have already been processed, we
|
|
|
|
# don't need to care about those either.
|
|
|
|
elif isinstance(update, (tl.Updates, tl.UpdatesCombined)):
|
2018-03-14 12:28:21 +03:00
|
|
|
entities = {utils.get_peer_id(x): x for x in
|
|
|
|
itertools.chain(update.users, update.chats)}
|
2017-11-30 23:09:34 +03:00
|
|
|
for u in update.updates:
|
2018-03-29 01:56:05 +03:00
|
|
|
u._entities = entities
|
2017-11-30 23:09:34 +03:00
|
|
|
self._updates.put(u)
|
2017-11-30 23:10:02 +03:00
|
|
|
# TODO Handle "tl.UpdatesTooLong"
|
2017-10-01 17:30:27 +03:00
|
|
|
else:
|
2018-03-29 01:56:05 +03:00
|
|
|
update._entities = {}
|
2017-11-30 23:09:34 +03:00
|
|
|
self._updates.put(update)
|