diff options
| author | Jan Tuomi <jan@jantuomi.fi> | 2025-07-19 10:42:15 +0300 |
|---|---|---|
| committer | Jan Tuomi <jan@jantuomi.fi> | 2025-07-19 10:42:15 +0300 |
| commit | 5a636abf7e98a5687811167917d0e07103599bc8 (patch) | |
| tree | 682f8d70d194c6bf02b750780f05689fcb83ba5a /telepot/__init__.py | |
| parent | e1ae18d1bc9e26f8ab817ca1c1e4e271badf02ad (diff) | |
Vendor the unmaintained telepot dependency
Diffstat (limited to 'telepot/__init__.py')
| -rw-r--r-- | telepot/__init__.py | 1405 |
1 files changed, 1405 insertions, 0 deletions
diff --git a/telepot/__init__.py b/telepot/__init__.py new file mode 100644 index 0000000..30cb9d4 --- /dev/null +++ b/telepot/__init__.py @@ -0,0 +1,1405 @@ +import sys +import io +import time +import json +import threading +import traceback +import collections +import bisect + +try: + import Queue as queue +except ImportError: + import queue + +# Patch urllib3 for sending unicode filename +from . import hack + +from . import exception + + +__version_info__ = (12, 7) +__version__ = '.'.join(map(str, __version_info__)) + + +def flavor(msg): + """ + Return flavor of message or event. + + A message's flavor may be one of these: + + - ``chat`` + - ``callback_query`` + - ``inline_query`` + - ``chosen_inline_result`` + - ``shipping_query`` + - ``pre_checkout_query`` + + An event's flavor is determined by the single top-level key. + """ + if 'message_id' in msg: + return 'chat' + elif 'id' in msg and 'chat_instance' in msg: + return 'callback_query' + elif 'id' in msg and 'query' in msg: + return 'inline_query' + elif 'result_id' in msg: + return 'chosen_inline_result' + elif 'id' in msg and 'shipping_address' in msg: + return 'shipping_query' + elif 'id' in msg and 'total_amount' in msg: + return 'pre_checkout_query' + else: + top_keys = list(msg.keys()) + if len(top_keys) == 1: + return top_keys[0] + + raise exception.BadFlavor(msg) + + +chat_flavors = ['chat'] +inline_flavors = ['inline_query', 'chosen_inline_result'] + + +def _find_first_key(d, keys): + for k in keys: + if k in d: + return k + raise KeyError('No suggested keys %s in %s' % (str(keys), str(d))) + + +all_content_types = [ + 'text', 'audio', 'document', 'game', 'photo', 'sticker', 'video', 'voice', 'video_note', + 'contact', 'location', 'venue', 'new_chat_member', 'left_chat_member', 'new_chat_title', + 'new_chat_photo', 'delete_chat_photo', 'group_chat_created', 'supergroup_chat_created', + 'channel_chat_created', 'migrate_to_chat_id', 'migrate_from_chat_id', 'pinned_message', + 'new_chat_members', 'invoice', 'successful_payment' +] + +def glance(msg, flavor='chat', long=False): + """ + Extract "headline" info about a message. + Use parameter ``long`` to control whether a short or long tuple is returned. + + When ``flavor`` is ``chat`` + (``msg`` being a `Message <https://core.telegram.org/bots/api#message>`_ object): + + - short: (content_type, ``msg['chat']['type']``, ``msg['chat']['id']``) + - long: (content_type, ``msg['chat']['type']``, ``msg['chat']['id']``, ``msg['date']``, ``msg['message_id']``) + + *content_type* can be: ``text``, ``audio``, ``document``, ``game``, ``photo``, ``sticker``, ``video``, ``voice``, + ``video_note``, ``contact``, ``location``, ``venue``, ``new_chat_member``, ``left_chat_member``, ``new_chat_title``, + ``new_chat_photo``, ``delete_chat_photo``, ``group_chat_created``, ``supergroup_chat_created``, + ``channel_chat_created``, ``migrate_to_chat_id``, ``migrate_from_chat_id``, ``pinned_message``, + ``new_chat_members``, ``invoice``, ``successful_payment``. + + When ``flavor`` is ``callback_query`` + (``msg`` being a `CallbackQuery <https://core.telegram.org/bots/api#callbackquery>`_ object): + + - regardless: (``msg['id']``, ``msg['from']['id']``, ``msg['data']``) + + When ``flavor`` is ``inline_query`` + (``msg`` being a `InlineQuery <https://core.telegram.org/bots/api#inlinequery>`_ object): + + - short: (``msg['id']``, ``msg['from']['id']``, ``msg['query']``) + - long: (``msg['id']``, ``msg['from']['id']``, ``msg['query']``, ``msg['offset']``) + + When ``flavor`` is ``chosen_inline_result`` + (``msg`` being a `ChosenInlineResult <https://core.telegram.org/bots/api#choseninlineresult>`_ object): + + - regardless: (``msg['result_id']``, ``msg['from']['id']``, ``msg['query']``) + + When ``flavor`` is ``shipping_query`` + (``msg`` being a `ShippingQuery <https://core.telegram.org/bots/api#shippingquery>`_ object): + + - regardless: (``msg['id']``, ``msg['from']['id']``, ``msg['invoice_payload']``) + + When ``flavor`` is ``pre_checkout_query`` + (``msg`` being a `PreCheckoutQuery <https://core.telegram.org/bots/api#precheckoutquery>`_ object): + + - short: (``msg['id']``, ``msg['from']['id']``, ``msg['invoice_payload']``) + - long: (``msg['id']``, ``msg['from']['id']``, ``msg['invoice_payload']``, ``msg['currency']``, ``msg['total_amount']``) + """ + def gl_chat(): + content_type = _find_first_key(msg, all_content_types) + + if long: + return content_type, msg['chat']['type'], msg['chat']['id'], msg['date'], msg['message_id'] + else: + return content_type, msg['chat']['type'], msg['chat']['id'] + + def gl_callback_query(): + return msg['id'], msg['from']['id'], msg['data'] + + def gl_inline_query(): + if long: + return msg['id'], msg['from']['id'], msg['query'], msg['offset'] + else: + return msg['id'], msg['from']['id'], msg['query'] + + def gl_chosen_inline_result(): + return msg['result_id'], msg['from']['id'], msg['query'] + + def gl_shipping_query(): + return msg['id'], msg['from']['id'], msg['invoice_payload'] + + def gl_pre_checkout_query(): + if long: + return msg['id'], msg['from']['id'], msg['invoice_payload'], msg['currency'], msg['total_amount'] + else: + return msg['id'], msg['from']['id'], msg['invoice_payload'] + + try: + fn = {'chat': gl_chat, + 'callback_query': gl_callback_query, + 'inline_query': gl_inline_query, + 'chosen_inline_result': gl_chosen_inline_result, + 'shipping_query': gl_shipping_query, + 'pre_checkout_query': gl_pre_checkout_query}[flavor] + except KeyError: + raise exception.BadFlavor(flavor) + + return fn() + + +def flance(msg, long=False): + """ + A combination of :meth:`telepot.flavor` and :meth:`telepot.glance`, + return a 2-tuple (flavor, headline_info), where *headline_info* is whatever extracted by + :meth:`telepot.glance` depending on the message flavor and the ``long`` parameter. + """ + f = flavor(msg) + g = glance(msg, flavor=f, long=long) + return f,g + + +def peel(event): + """ + Remove an event's top-level skin (where its flavor is determined), and return + the core content. + """ + return list(event.values())[0] + + +def fleece(event): + """ + A combination of :meth:`telepot.flavor` and :meth:`telepot.peel`, + return a 2-tuple (flavor, content) of an event. + """ + return flavor(event), peel(event) + + +def is_event(msg): + """ + Return whether the message looks like an event. That is, whether it has a flavor + that starts with an underscore. + """ + return flavor(msg).startswith('_') + + +def origin_identifier(msg): + """ + Extract the message identifier of a callback query's origin. Returned value + is guaranteed to be a tuple. + + ``msg`` is expected to be ``callback_query``. + """ + if 'message' in msg: + return msg['message']['chat']['id'], msg['message']['message_id'] + elif 'inline_message_id' in msg: + return msg['inline_message_id'], + else: + raise ValueError() + +def message_identifier(msg): + """ + Extract an identifier for message editing. Useful with :meth:`telepot.Bot.editMessageText` + and similar methods. Returned value is guaranteed to be a tuple. + + ``msg`` is expected to be ``chat`` or ``choson_inline_result``. + """ + if 'chat' in msg and 'message_id' in msg: + return msg['chat']['id'], msg['message_id'] + elif 'inline_message_id' in msg: + return msg['inline_message_id'], + else: + raise ValueError() + +def _dismantle_message_identifier(f): + if isinstance(f, tuple): + if len(f) == 2: + return {'chat_id': f[0], 'message_id': f[1]} + elif len(f) == 1: + return {'inline_message_id': f[0]} + else: + raise ValueError() + else: + return {'inline_message_id': f} + +def _split_input_media_array(media_array): + def ensure_dict(input_media): + if isinstance(input_media, tuple) and hasattr(input_media, '_asdict'): + return input_media._asdict() + elif isinstance(input_media, dict): + return input_media + else: + raise ValueError() + + def given_attach_name(input_media): + if isinstance(input_media['media'], tuple): + return input_media['media'][0] + else: + return None + + def attach_name_generator(used_names): + x = 0 + while 1: + x += 1 + name = 'media' + str(x) + if name in used_names: + continue; + yield name + + def split_media(input_media, name_generator): + file_spec = input_media['media'] + + # file_id, URL + if _isstring(file_spec): + return (input_media, None) + + # file-object + # (attach-name, file-object) + # (attach-name, (filename, file-object)) + if isinstance(file_spec, tuple): + name, f = file_spec + else: + name, f = next(name_generator), file_spec + + m = input_media.copy() + m['media'] = 'attach://' + name + + return (m, (name, f)) + + ms = [ensure_dict(m) for m in media_array] + + used_names = [given_attach_name(m) for m in ms if given_attach_name(m) is not None] + name_generator = attach_name_generator(used_names) + + splitted = [split_media(m, name_generator) for m in ms] + + legal_media, attachments = map(list, zip(*splitted)) + files_to_attach = dict([a for a in attachments if a is not None]) + + return (legal_media, files_to_attach) + + +PY_3 = sys.version_info.major >= 3 +_string_type = str if PY_3 else basestring +_file_type = io.IOBase if PY_3 else file + +def _isstring(s): + return isinstance(s, _string_type) + +def _isfile(f): + return isinstance(f, _file_type) + + +from . import helper + +def flavor_router(routing_table): + router = helper.Router(flavor, routing_table) + return router.route + + +class _BotBase(object): + def __init__(self, token): + self._token = token + self._file_chunk_size = 65536 + + +def _strip(params, more=[]): + return {key: value for key,value in params.items() if key not in ['self']+more} + +def _rectify(params): + def make_jsonable(value): + if isinstance(value, list): + return [make_jsonable(v) for v in value] + elif isinstance(value, dict): + return {k:make_jsonable(v) for k,v in value.items() if v is not None} + elif isinstance(value, tuple) and hasattr(value, '_asdict'): + return {k:make_jsonable(v) for k,v in value._asdict().items() if v is not None} + else: + return value + + def flatten(value): + v = make_jsonable(value) + + if isinstance(v, (dict, list)): + return json.dumps(v, separators=(',',':')) + else: + return v + + # remove None, then json-serialize if needed + return {k: flatten(v) for k,v in params.items() if v is not None} + + +from . import api + +class Bot(_BotBase): + class Scheduler(threading.Thread): + # A class that is sorted by timestamp. Use `bisect` module to ensure order in event queue. + Event = collections.namedtuple('Event', ['timestamp', 'data']) + Event.__eq__ = lambda self, other: self.timestamp == other.timestamp + Event.__ne__ = lambda self, other: self.timestamp != other.timestamp + Event.__gt__ = lambda self, other: self.timestamp > other.timestamp + Event.__ge__ = lambda self, other: self.timestamp >= other.timestamp + Event.__lt__ = lambda self, other: self.timestamp < other.timestamp + Event.__le__ = lambda self, other: self.timestamp <= other.timestamp + + def __init__(self): + super(Bot.Scheduler, self).__init__() + self._eventq = [] + self._lock = threading.RLock() # reentrant lock to allow locked method calling locked method + self._event_handler = None + + def _locked(fn): + def k(self, *args, **kwargs): + with self._lock: + return fn(self, *args, **kwargs) + return k + + @_locked + def _insert_event(self, data, when): + ev = self.Event(when, data) + bisect.insort(self._eventq, ev) + return ev + + @_locked + def _remove_event(self, event): + # Find event according to its timestamp. + # Index returned should be one behind. + i = bisect.bisect(self._eventq, event) + + # Having two events with identical timestamp is unlikely but possible. + # I am going to move forward and compare timestamp AND object address + # to make sure the correct object is found. + + while i > 0: + i -= 1 + e = self._eventq[i] + + if e.timestamp != event.timestamp: + raise exception.EventNotFound(event) + elif id(e) == id(event): + self._eventq.pop(i) + return + + raise exception.EventNotFound(event) + + @_locked + def _pop_expired_event(self): + if not self._eventq: + return None + + if self._eventq[0].timestamp <= time.time(): + return self._eventq.pop(0) + else: + return None + + def event_at(self, when, data): + """ + Schedule some data to emit at an absolute timestamp. + + :type when: int or float + :type data: dictionary + :return: an internal Event object + """ + return self._insert_event(data, when) + + def event_later(self, delay, data): + """ + Schedule some data to emit after a number of seconds. + + :type delay: int or float + :type data: dictionary + :return: an internal Event object + """ + return self._insert_event(data, time.time()+delay) + + def event_now(self, data): + """ + Emit some data as soon as possible. + + :type data: dictionary + :return: an internal Event object + """ + return self._insert_event(data, time.time()) + + def cancel(self, event): + """ + Cancel an event. + + :type event: an internal Event object + """ + self._remove_event(event) + + def run(self): + while 1: + e = self._pop_expired_event() + while e: + if callable(e.data): + d = e.data() # call the data-producing function + if d is not None: + self._event_handler(d) + else: + self._event_handler(e.data) + + e = self._pop_expired_event() + time.sleep(0.1) + + def run_as_thread(self): + self.daemon = True + self.start() + + def on_event(self, fn): + self._event_handler = fn + + def __init__(self, token): + super(Bot, self).__init__(token) + + self._scheduler = self.Scheduler() + + self._router = helper.Router(flavor, {'chat': lambda msg: self.on_chat_message(msg), + 'callback_query': lambda msg: self.on_callback_query(msg), + 'inline_query': lambda msg: self.on_inline_query(msg), + 'chosen_inline_result': lambda msg: self.on_chosen_inline_result(msg)}) + # use lambda to delay evaluation of self.on_ZZZ to runtime because + # I don't want to require defining all methods right here. + + @property + def scheduler(self): + return self._scheduler + + @property + def router(self): + return self._router + + def handle(self, msg): + self._router.route(msg) + + def _api_request(self, method, params=None, files=None, **kwargs): + return api.request((self._token, method, params, files), **kwargs) + + def _api_request_with_file(self, method, params, file_key, file_value, **kwargs): + if _isstring(file_value): + params[file_key] = file_value + return self._api_request(method, _rectify(params), **kwargs) + else: + files = {file_key: file_value} + return self._api_request(method, _rectify(params), files, **kwargs) + + def getMe(self): + """ See: https://core.telegram.org/bots/api#getme """ + return self._api_request('getMe') + + def sendMessage(self, chat_id, text, + parse_mode=None, + disable_web_page_preview=None, + disable_notification=None, + reply_to_message_id=None, + reply_markup=None): + """ See: https://core.telegram.org/bots/api#sendmessage """ + p = _strip(locals()) + return self._api_request('sendMessage', _rectify(p)) + + def forwardMessage(self, chat_id, from_chat_id, message_id, + disable_notification=None): + """ See: https://core.telegram.org/bots/api#forwardmessage """ + p = _strip(locals()) + return self._api_request('forwardMessage', _rectify(p)) + + def sendPhoto(self, chat_id, photo, + caption=None, + parse_mode=None, + disable_notification=None, + reply_to_message_id=None, + reply_markup=None): + """ + See: https://core.telegram.org/bots/api#sendphoto + + :param photo: + - string: ``file_id`` for a photo existing on Telegram servers + - string: HTTP URL of a photo from the Internet + - file-like object: obtained by ``open(path, 'rb')`` + - tuple: (filename, file-like object). If the filename contains + non-ASCII characters and you are using Python 2.7, make sure the + filename is a unicode string. + """ + p = _strip(locals(), more=['photo']) + return self._api_request_with_file('sendPhoto', _rectify(p), 'photo', photo) + + def sendAudio(self, chat_id, audio, + caption=None, + parse_mode=None, + duration=None, + performer=None, + title=None, + disable_notification=None, + reply_to_message_id=None, + reply_markup=None): + """ + See: https://core.telegram.org/bots/api#sendaudio + + :param audio: Same as ``photo`` in :meth:`telepot.Bot.sendPhoto` + """ + p = _strip(locals(), more=['audio']) + return self._api_request_with_file('sendAudio', _rectify(p), 'audio', audio) + + def sendDocument(self, chat_id, document, + caption=None, + parse_mode=None, + disable_notification=None, + reply_to_message_id=None, + reply_markup=None): + """ + See: https://core.telegram.org/bots/api#senddocument + + :param document: Same as ``photo`` in :meth:`telepot.Bot.sendPhoto` + """ + p = _strip(locals(), more=['document']) + return self._api_request_with_file('sendDocument', _rectify(p), 'document', document) + + def sendVideo(self, chat_id, video, + duration=None, + width=None, + height=None, + caption=None, + parse_mode=None, + supports_streaming=None, + disable_notification=None, + reply_to_message_id=None, + reply_markup=None): + """ + See: https://core.telegram.org/bots/api#sendvideo + + :param video: Same as ``photo`` in :meth:`telepot.Bot.sendPhoto` + """ + p = _strip(locals(), more=['video']) + return self._api_request_with_file('sendVideo', _rectify(p), 'video', video) + + def sendVoice(self, chat_id, voice, + caption=None, + parse_mode=None, + duration=None, + disable_notification=None, + reply_to_message_id=None, + reply_markup=None): + """ + See: https://core.telegram.org/bots/api#sendvoice + + :param voice: Same as ``photo`` in :meth:`telepot.Bot.sendPhoto` + """ + p = _strip(locals(), more=['voice']) + return self._api_request_with_file('sendVoice', _rectify(p), 'voice', voice) + + def sendVideoNote(self, chat_id, video_note, + duration=None, + length=None, + disable_notification=None, + reply_to_message_id=None, + reply_markup=None): + """ + See: https://core.telegram.org/bots/api#sendvideonote + + :param video_note: Same as ``photo`` in :meth:`telepot.Bot.sendPhoto` + + :param length: + Although marked as optional, this method does not seem to work without + it being specified. Supply any integer you want. It seems to have no effect + on the video note's display size. + """ + p = _strip(locals(), more=['video_note']) + return self._api_request_with_file('sendVideoNote', _rectify(p), 'video_note', video_note) + + def sendMediaGroup(self, chat_id, media, + disable_notification=None, + reply_to_message_id=None): + """ + See: https://core.telegram.org/bots/api#sendmediagroup + + :type media: array of `InputMedia <https://core.telegram.org/bots/api#inputmedia>`_ objects + :param media: + To indicate media locations, each InputMedia object's ``media`` field + should be one of these: + + - string: ``file_id`` for a file existing on Telegram servers + - string: HTTP URL of a file from the Internet + - file-like object: obtained by ``open(path, 'rb')`` + - tuple: (form-data name, file-like object) + - tuple: (form-data name, (filename, file-like object)) + + In case of uploading, you may supply customized multipart/form-data + names for each uploaded file (as in last 2 options above). Otherwise, + telepot assigns unique names to each uploaded file. Names assigned by + telepot will not collide with user-supplied names, if any. + """ + p = _strip(locals(), more=['media']) + legal_media, files_to_attach = _split_input_media_array(media) + + p['media'] = legal_media + return self._api_request('sendMediaGroup', _rectify(p), files_to_attach) + + def sendLocation(self, chat_id, latitude, longitude, + live_period=None, + disable_notification=None, + reply_to_message_id=None, + reply_markup=None): + """ See: https://core.telegram.org/bots/api#sendlocation """ + p = _strip(locals()) + return self._api_request('sendLocation', _rectify(p)) + + def editMessageLiveLocation(self, msg_identifier, latitude, longitude, + reply_markup=None): + """ + See: https://core.telegram.org/bots/api#editmessagelivelocation + + :param msg_identifier: Same as in :meth:`.Bot.editMessageText` + """ + p = _strip(locals(), more=['msg_identifier']) + p.update(_dismantle_message_identifier(msg_identifier)) + return self._api_request('editMessageLiveLocation', _rectify(p)) + + def stopMessageLiveLocation(self, msg_identifier, + reply_markup=None): + """ + See: https://core.telegram.org/bots/api#stopmessagelivelocation + + :param msg_identifier: Same as in :meth:`.Bot.editMessageText` + """ + p = _strip(locals(), more=['msg_identifier']) + p.update(_dismantle_message_identifier(msg_identifier)) + return self._api_request('stopMessageLiveLocation', _rectify(p)) + + def sendVenue(self, chat_id, latitude, longitude, title, address, + foursquare_id=None, + disable_notification=None, + reply_to_message_id=None, + reply_markup=None): + """ See: https://core.telegram.org/bots/api#sendvenue """ + p = _strip(locals()) + return self._api_request('sendVenue', _rectify(p)) + + def sendContact(self, chat_id, phone_number, first_name, + last_name=None, + disable_notification=None, + reply_to_message_id=None, + reply_markup=None): + """ See: https://core.telegram.org/bots/api#sendcontact """ + p = _strip(locals()) + return self._api_request('sendContact', _rectify(p)) + + def sendGame(self, chat_id, game_short_name, + disable_notification=None, + reply_to_message_id=None, + reply_markup=None): + """ See: https://core.telegram.org/bots/api#sendgame """ + p = _strip(locals()) + return self._api_request('sendGame', _rectify(p)) + + def sendInvoice(self, chat_id, title, description, payload, + provider_token, start_parameter, currency, prices, + provider_data=None, + photo_url=None, + photo_size=None, + photo_width=None, + photo_height=None, + need_name=None, + need_phone_number=None, + need_email=None, + need_shipping_address=None, + is_flexible=None, + disable_notification=None, + reply_to_message_id=None, + reply_markup=None): + """ See: https://core.telegram.org/bots/api#sendinvoice """ + p = _strip(locals()) + return self._api_request('sendInvoice', _rectify(p)) + + def sendChatAction(self, chat_id, action): + """ See: https://core.telegram.org/bots/api#sendchataction """ + p = _strip(locals()) + return self._api_request('sendChatAction', _rectify(p)) + + def getUserProfilePhotos(self, user_id, + offset=None, + limit=None): + """ See: https://core.telegram.org/bots/api#getuserprofilephotos """ + p = _strip(locals()) + return self._api_request('getUserProfilePhotos', _rectify(p)) + + def getFile(self, file_id): + """ See: https://core.telegram.org/bots/api#getfile """ + p = _strip(locals()) + return self._api_request('getFile', _rectify(p)) + + def kickChatMember(self, chat_id, user_id, + until_date=None): + """ See: https://core.telegram.org/bots/api#kickchatmember """ + p = _strip(locals()) + return self._api_request('kickChatMember', _rectify(p)) + + def unbanChatMember(self, chat_id, user_id): + """ See: https://core.telegram.org/bots/api#unbanchatmember """ + p = _strip(locals()) + return self._api_request('unbanChatMember', _rectify(p)) + + def restrictChatMember(self, chat_id, user_id, + until_date=None, + can_send_messages=None, + can_send_media_messages=None, + can_send_other_messages=None, + can_add_web_page_previews=None): + """ See: https://core.telegram.org/bots/api#restrictchatmember """ + p = _strip(locals()) + return self._api_request('restrictChatMember', _rectify(p)) + + def promoteChatMember(self, chat_id, user_id, + can_change_info=None, + can_post_messages=None, + can_edit_messages=None, + can_delete_messages=None, + can_invite_users=None, + can_restrict_members=None, + can_pin_messages=None, + can_promote_members=None): + """ See: https://core.telegram.org/bots/api#promotechatmember """ + p = _strip(locals()) + return self._api_request('promoteChatMember', _rectify(p)) + + def exportChatInviteLink(self, chat_id): + """ See: https://core.telegram.org/bots/api#exportchatinvitelink """ + p = _strip(locals()) + return self._api_request('exportChatInviteLink', _rectify(p)) + + def setChatPhoto(self, chat_id, photo): + """ See: https://core.telegram.org/bots/api#setchatphoto """ + p = _strip(locals(), more=['photo']) + return self._api_request_with_file('setChatPhoto', _rectify(p), 'photo', photo) + + def deleteChatPhoto(self, chat_id): + """ See: https://core.telegram.org/bots/api#deletechatphoto """ + p = _strip(locals()) + return self._api_request('deleteChatPhoto', _rectify(p)) + + def setChatTitle(self, chat_id, title): + """ See: https://core.telegram.org/bots/api#setchattitle """ + p = _strip(locals()) + return self._api_request('setChatTitle', _rectify(p)) + + def setChatDescription(self, chat_id, + description=None): + """ See: https://core.telegram.org/bots/api#setchatdescription """ + p = _strip(locals()) + return self._api_request('setChatDescription', _rectify(p)) + + def pinChatMessage(self, chat_id, message_id, + disable_notification=None): + """ See: https://core.telegram.org/bots/api#pinchatmessage """ + p = _strip(locals()) + return self._api_request('pinChatMessage', _rectify(p)) + + def unpinChatMessage(self, chat_id): + """ See: https://core.telegram.org/bots/api#unpinchatmessage """ + p = _strip(locals()) + return self._api_request('unpinChatMessage', _rectify(p)) + + def leaveChat(self, chat_id): + """ See: https://core.telegram.org/bots/api#leavechat """ + p = _strip(locals()) + return self._api_request('leaveChat', _rectify(p)) + + def getChat(self, chat_id): + """ See: https://core.telegram.org/bots/api#getchat """ + p = _strip(locals()) + return self._api_request('getChat', _rectify(p)) + + def getChatAdministrators(self, chat_id): + """ See: https://core.telegram.org/bots/api#getchatadministrators """ + p = _strip(locals()) + return self._api_request('getChatAdministrators', _rectify(p)) + + def getChatMembersCount(self, chat_id): + """ See: https://core.telegram.org/bots/api#getchatmemberscount """ + p = _strip(locals()) + return self._api_request('getChatMembersCount', _rectify(p)) + + def getChatMember(self, chat_id, user_id): + """ See: https://core.telegram.org/bots/api#getchatmember """ + p = _strip(locals()) + return self._api_request('getChatMember', _rectify(p)) + + def setChatStickerSet(self, chat_id, sticker_set_name): + """ See: https://core.telegram.org/bots/api#setchatstickerset """ + p = _strip(locals()) + return self._api_request('setChatStickerSet', _rectify(p)) + + def deleteChatStickerSet(self, chat_id): + """ See: https://core.telegram.org/bots/api#deletechatstickerset """ + p = _strip(locals()) + return self._api_request('deleteChatStickerSet', _rectify(p)) + + def answerCallbackQuery(self, callback_query_id, + text=None, + show_alert=None, + url=None, + cache_time=None): + """ See: https://core.telegram.org/bots/api#answercallbackquery """ + p = _strip(locals()) + return self._api_request('answerCallbackQuery', _rectify(p)) + + def answerShippingQuery(self, shipping_query_id, ok, + shipping_options=None, + error_message=None): + """ See: https://core.telegram.org/bots/api#answershippingquery """ + p = _strip(locals()) + return self._api_request('answerShippingQuery', _rectify(p)) + + def answerPreCheckoutQuery(self, pre_checkout_query_id, ok, + error_message=None): + """ See: https://core.telegram.org/bots/api#answerprecheckoutquery """ + p = _strip(locals()) + return self._api_request('answerPreCheckoutQuery', _rectify(p)) + + def editMessageText(self, msg_identifier, text, + parse_mode=None, + disable_web_page_preview=None, + reply_markup=None): + """ + See: https://core.telegram.org/bots/api#editmessagetext + + :param msg_identifier: + a 2-tuple (``chat_id``, ``message_id``), + a 1-tuple (``inline_message_id``), + or simply ``inline_message_id``. + You may extract this value easily with :meth:`telepot.message_identifier` + """ + p = _strip(locals(), more=['msg_identifier']) + p.update(_dismantle_message_identifier(msg_identifier)) + return self._api_request('editMessageText', _rectify(p)) + + def editMessageCaption(self, msg_identifier, + caption=None, + parse_mode=None, + reply_markup=None): + """ + See: https://core.telegram.org/bots/api#editmessagecaption + + :param msg_identifier: Same as ``msg_identifier`` in :meth:`telepot.Bot.editMessageText` + """ + p = _strip(locals(), more=['msg_identifier']) + p.update(_dismantle_message_identifier(msg_identifier)) + return self._api_request('editMessageCaption', _rectify(p)) + + def editMessageReplyMarkup(self, msg_identifier, + reply_markup=None): + """ + See: https://core.telegram.org/bots/api#editmessagereplymarkup + + :param msg_identifier: Same as ``msg_identifier`` in :meth:`telepot.Bot.editMessageText` + """ + p = _strip(locals(), more=['msg_identifier']) + p.update(_dismantle_message_identifier(msg_identifier)) + return self._api_request('editMessageReplyMarkup', _rectify(p)) + + def deleteMessage(self, msg_identifier): + """ + See: https://core.telegram.org/bots/api#deletemessage + + :param msg_identifier: + Same as ``msg_identifier`` in :meth:`telepot.Bot.editMessageText`, + except this method does not work on inline messages. + """ + p = _strip(locals(), more=['msg_identifier']) + p.update(_dismantle_message_identifier(msg_identifier)) + return self._api_request('deleteMessage', _rectify(p)) + + def sendSticker(self, chat_id, sticker, + disable_notification=None, + reply_to_message_id=None, + reply_markup=None): + """ + See: https://core.telegram.org/bots/api#sendsticker + + :param sticker: Same as ``photo`` in :meth:`telepot.Bot.sendPhoto` + """ + p = _strip(locals(), more=['sticker']) + return self._api_request_with_file('sendSticker', _rectify(p), 'sticker', sticker) + + def getStickerSet(self, name): + """ + See: https://core.telegram.org/bots/api#getstickerset + """ + p = _strip(locals()) + return self._api_request('getStickerSet', _rectify(p)) + + def uploadStickerFile(self, user_id, png_sticker): + """ + See: https://core.telegram.org/bots/api#uploadstickerfile + """ + p = _strip(locals(), more=['png_sticker']) + return self._api_request_with_file('uploadStickerFile', _rectify(p), 'png_sticker', png_sticker) + + def createNewStickerSet(self, user_id, name, title, png_sticker, emojis, + contains_masks=None, + mask_position=None): + """ + See: https://core.telegram.org/bots/api#createnewstickerset + """ + p = _strip(locals(), more=['png_sticker']) + return self._api_request_with_file('createNewStickerSet', _rectify(p), 'png_sticker', png_sticker) + + def addStickerToSet(self, user_id, name, png_sticker, emojis, + mask_position=None): + """ + See: https://core.telegram.org/bots/api#addstickertoset + """ + p = _strip(locals(), more=['png_sticker']) + return self._api_request_with_file('addStickerToSet', _rectify(p), 'png_sticker', png_sticker) + + def setStickerPositionInSet(self, sticker, position): + """ + See: https://core.telegram.org/bots/api#setstickerpositioninset + """ + p = _strip(locals()) + return self._api_request('setStickerPositionInSet', _rectify(p)) + + def deleteStickerFromSet(self, sticker): + """ + See: https://core.telegram.org/bots/api#deletestickerfromset + """ + p = _strip(locals()) + return self._api_request('deleteStickerFromSet', _rectify(p)) + + def answerInlineQuery(self, inline_query_id, results, + cache_time=None, + is_personal=None, + next_offset=None, + switch_pm_text=None, + switch_pm_parameter=None): + """ See: https://core.telegram.org/bots/api#answerinlinequery """ + p = _strip(locals()) + return self._api_request('answerInlineQuery', _rectify(p)) + + def getUpdates(self, + offset=None, + limit=None, + timeout=None, + allowed_updates=None): + """ See: https://core.telegram.org/bots/api#getupdates """ + p = _strip(locals()) + return self._api_request('getUpdates', _rectify(p)) + + def setWebhook(self, + url=None, + certificate=None, + max_connections=None, + allowed_updates=None): + """ See: https://core.telegram.org/bots/api#setwebhook """ + p = _strip(locals(), more=['certificate']) + + if certificate: + files = {'certificate': certificate} + return self._api_request('setWebhook', _rectify(p), files) + else: + return self._api_request('setWebhook', _rectify(p)) + + def deleteWebhook(self): + """ See: https://core.telegram.org/bots/api#deletewebhook """ + return self._api_request('deleteWebhook') + + def getWebhookInfo(self): + """ See: https://core.telegram.org/bots/api#getwebhookinfo """ + return self._api_request('getWebhookInfo') + + def setGameScore(self, user_id, score, game_message_identifier, + force=None, + disable_edit_message=None): + """ + See: https://core.telegram.org/bots/api#setgamescore + + :param game_message_identifier: Same as ``msg_identifier`` in :meth:`telepot.Bot.editMessageText` + """ + p = _strip(locals(), more=['game_message_identifier']) + p.update(_dismantle_message_identifier(game_message_identifier)) + return self._api_request('setGameScore', _rectify(p)) + + def getGameHighScores(self, user_id, game_message_identifier): + """ + See: https://core.telegram.org/bots/api#getgamehighscores + + :param game_message_identifier: Same as ``msg_identifier`` in :meth:`telepot.Bot.editMessageText` + """ + p = _strip(locals(), more=['game_message_identifier']) + p.update(_dismantle_message_identifier(game_message_identifier)) + return self._api_request('getGameHighScores', _rectify(p)) + + def download_file(self, file_id, dest): + """ + Download a file to local disk. + + :param dest: a path or a ``file`` object + """ + f = self.getFile(file_id) + try: + d = dest if _isfile(dest) else open(dest, 'wb') + + r = api.download((self._token, f['file_path']), preload_content=False) + + while 1: + data = r.read(self._file_chunk_size) + if not data: + break + d.write(data) + finally: + if not _isfile(dest) and 'd' in locals(): + d.close() + + if 'r' in locals(): + r.release_conn() + + def message_loop(self, callback=None, relax=0.1, + timeout=20, allowed_updates=None, + source=None, ordered=True, maxhold=3, + run_forever=False): + """ + :deprecated: will be removed in future. Use :class:`.MessageLoop` instead. + + Spawn a thread to constantly ``getUpdates`` or pull updates from a queue. + Apply ``callback`` to every message received. Also starts the scheduler thread + for internal events. + + :param callback: + a function that takes one argument (the message), or a routing table. + If ``None``, the bot's ``handle`` method is used. + + A *routing table* is a dictionary of ``{flavor: function}``, mapping messages to appropriate + handler functions according to their flavors. It allows you to define functions specifically + to handle one flavor of messages. It usually looks like this: ``{'chat': fn1, + 'callback_query': fn2, 'inline_query': fn3, ...}``. Each handler function should take + one argument (the message). + + :param source: + Source of updates. + If ``None``, ``getUpdates`` is used to obtain new messages from Telegram servers. + If it is a synchronized queue (``Queue.Queue`` in Python 2.7 or + ``queue.Queue`` in Python 3), new messages are pulled from the queue. + A web application implementing a webhook can dump updates into the queue, + while the bot pulls from it. This is how telepot can be integrated with webhooks. + + Acceptable contents in queue: + + - ``str``, ``unicode`` (Python 2.7), or ``bytes`` (Python 3, decoded using UTF-8) + representing a JSON-serialized `Update <https://core.telegram.org/bots/api#update>`_ object. + - a ``dict`` representing an Update object. + + When ``source`` is ``None``, these parameters are meaningful: + + :type relax: float + :param relax: seconds between each ``getUpdates`` + + :type timeout: int + :param timeout: + ``timeout`` parameter supplied to :meth:`telepot.Bot.getUpdates`, + controlling how long to poll. + + :type allowed_updates: array of string + :param allowed_updates: + ``allowed_updates`` parameter supplied to :meth:`telepot.Bot.getUpdates`, + controlling which types of updates to receive. + + When ``source`` is a queue, these parameters are meaningful: + + :type ordered: bool + :param ordered: + If ``True``, ensure in-order delivery of messages to ``callback`` + (i.e. updates with a smaller ``update_id`` always come before those with + a larger ``update_id``). + If ``False``, no re-ordering is done. ``callback`` is applied to messages + as soon as they are pulled from queue. + + :type maxhold: float + :param maxhold: + Applied only when ``ordered`` is ``True``. The maximum number of seconds + an update is held waiting for a not-yet-arrived smaller ``update_id``. + When this number of seconds is up, the update is delivered to ``callback`` + even if some smaller ``update_id``\s have not yet arrived. If those smaller + ``update_id``\s arrive at some later time, they are discarded. + + Finally, there is this parameter, meaningful always: + + :type run_forever: bool or str + :param run_forever: + If ``True`` or any non-empty string, append an infinite loop at the end of + this method, so it never returns. Useful as the very last line in a program. + A non-empty string will also be printed, useful as an indication that the + program is listening. + """ + if callback is None: + callback = self.handle + elif isinstance(callback, dict): + callback = flavor_router(callback) + + collect_queue = queue.Queue() + + def collector(): + while 1: + try: + item = collect_queue.get(block=True) + callback(item) + except: + # Localize error so thread can keep going. + traceback.print_exc() + + def relay_to_collector(update): + key = _find_first_key(update, ['message', + 'edited_message', + 'channel_post', + 'edited_channel_post', + 'callback_query', + 'inline_query', + 'chosen_inline_result', + 'shipping_query', + 'pre_checkout_query']) + collect_queue.put(update[key]) + return update['update_id'] + + def get_from_telegram_server(): + offset = None # running offset + allowed_upd = allowed_updates + while 1: + try: + result = self.getUpdates(offset=offset, + timeout=timeout, + allowed_updates=allowed_upd) + + # Once passed, this parameter is no longer needed. + allowed_upd = None + + if len(result) > 0: + # No sort. Trust server to give messages in correct order. + # Update offset to max(update_id) + 1 + offset = max([relay_to_collector(update) for update in result]) + 1 + + except exception.BadHTTPResponse as e: + traceback.print_exc() + + # Servers probably down. Wait longer. + if e.status == 502: + time.sleep(30) + except: + traceback.print_exc() + finally: + time.sleep(relax) + + def dictify3(data): + if type(data) is bytes: + return json.loads(data.decode('utf-8')) + elif type(data) is str: + return json.loads(data) + elif type(data) is dict: + return data + else: + raise ValueError() + + def dictify27(data): + if type(data) in [str, unicode]: + return json.loads(data) + elif type(data) is dict: + return data + else: + raise ValueError() + + def get_from_queue_unordered(qu): + dictify = dictify3 if sys.version_info >= (3,) else dictify27 + while 1: + try: + data = qu.get(block=True) + update = dictify(data) + relay_to_collector(update) + except: + traceback.print_exc() + + def get_from_queue(qu): + dictify = dictify3 if sys.version_info >= (3,) else dictify27 + + # Here is the re-ordering mechanism, ensuring in-order delivery of updates. + max_id = None # max update_id passed to callback + buffer = collections.deque() # keep those updates which skip some update_id + qwait = None # how long to wait for updates, + # because buffer's content has to be returned in time. + + while 1: + try: + data = qu.get(block=True, timeout=qwait) + update = dictify(data) + + if max_id is None: + # First message received, handle regardless. + max_id = relay_to_collector(update) + + elif update['update_id'] == max_id + 1: + # No update_id skipped, handle naturally. + max_id = relay_to_collector(update) + + # clear contagious updates in buffer + if len(buffer) > 0: + buffer.popleft() # first element belongs to update just received, useless now. + while 1: + try: + if type(buffer[0]) is dict: + max_id = relay_to_collector(buffer.popleft()) # updates that arrived earlier, handle them. + else: + break # gap, no more contagious updates + except IndexError: + break # buffer empty + + elif update['update_id'] > max_id + 1: + # Update arrives pre-maturely, insert to buffer. + nbuf = len(buffer) + if update['update_id'] <= max_id + nbuf: + # buffer long enough, put update at position + buffer[update['update_id'] - max_id - 1] = update + else: + # buffer too short, lengthen it + expire = time.time() + maxhold + for a in range(nbuf, update['update_id']-max_id-1): + buffer.append(expire) # put expiry time in gaps + buffer.append(update) + + else: + pass # discard + + except queue.Empty: + # debug message + # print('Timeout') + + # some buffer contents have to be handled + # flush buffer until a non-expired time is encountered + while 1: + try: + if type(buffer[0]) is dict: + max_id = relay_to_collector(buffer.popleft()) + else: + expire = buffer[0] + if expire <= time.time(): + max_id += 1 + buffer.popleft() + else: + break # non-expired + except IndexError: + break # buffer empty + except: + traceback.print_exc() + finally: + try: + # don't wait longer than next expiry time + qwait = buffer[0] - time.time() + if qwait < 0: + qwait = 0 + except IndexError: + # buffer empty, can wait forever + qwait = None + + # debug message + # print ('Buffer:', str(buffer), ', To Wait:', qwait, ', Max ID:', max_id) + + collector_thread = threading.Thread(target=collector) + collector_thread.daemon = True + collector_thread.start() + + if source is None: + message_thread = threading.Thread(target=get_from_telegram_server) + elif isinstance(source, queue.Queue): + if ordered: + message_thread = threading.Thread(target=get_from_queue, args=(source,)) + else: + message_thread = threading.Thread(target=get_from_queue_unordered, args=(source,)) + else: + raise ValueError('Invalid source') + + message_thread.daemon = True # need this for main thread to be killable by Ctrl-C + message_thread.start() + + self._scheduler.on_event(collect_queue.put) + self._scheduler.run_as_thread() + + if run_forever: + if _isstring(run_forever): + print(run_forever) + while 1: + time.sleep(10) + + +import inspect + +class SpeakerBot(Bot): + def __init__(self, token): + super(SpeakerBot, self).__init__(token) + self._mic = helper.Microphone() + + @property + def mic(self): + return self._mic + + def create_listener(self): + q = queue.Queue() + self._mic.add(q) + ln = helper.Listener(self._mic, q) + return ln + + +class DelegatorBot(SpeakerBot): + def __init__(self, token, delegation_patterns): + """ + :param delegation_patterns: a list of (seeder, delegator) tuples. + """ + super(DelegatorBot, self).__init__(token) + self._delegate_records = [p+({},) for p in delegation_patterns] + + def _startable(self, delegate): + return ((hasattr(delegate, 'start') and inspect.ismethod(delegate.start)) and + (hasattr(delegate, 'is_alive') and inspect.ismethod(delegate.is_alive))) + + def _tuple_is_valid(self, t): + return len(t) == 3 and callable(t[0]) and type(t[1]) in [list, tuple] and type(t[2]) is dict + + def _ensure_startable(self, delegate): + if self._startable(delegate): + return delegate + elif callable(delegate): + return threading.Thread(target=delegate) + elif type(delegate) is tuple and self._tuple_is_valid(delegate): + func, args, kwargs = delegate + return threading.Thread(target=func, args=args, kwargs=kwargs) + else: + raise RuntimeError('Delegate does not have the required methods, is not callable, and is not a valid tuple.') + + def handle(self, msg): + self._mic.send(msg) + + for calculate_seed, make_delegate, dict in self._delegate_records: + id = calculate_seed(msg) + + if id is None: + continue + elif isinstance(id, collections.Hashable): + if id not in dict or not dict[id].is_alive(): + d = make_delegate((self, msg, id)) + d = self._ensure_startable(d) + + dict[id] = d + dict[id].start() + else: + d = make_delegate((self, msg, id)) + d = self._ensure_startable(d) + d.start() |
