text: 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',
                                             'poll',
                                             'poll_answer',
                                             'my_chat_member',
                                             'chat_member'])
              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()
  