import re
import logging
import asyncio
import aiohttp
import json

from functools import partialmethod
from . chat import Chat, Sender

__author__ = "Stepan Zastupov"
__copyright__ = "Copyright 2015, Stepan Zastupov"
__license__ = "MIT"

API_URL = ""
RETRY_CODES = [429, 500, 502, 503, 504]

    "location", "photo", "docameent", "audio", "voice", "sticker", "contact",

logger = logging.getLogger("aiotg")

clast Bot:
    """Telegram bot framework designed for asyncio

    :param str api_token: Telegram bot token, ask @BotFather for this
    :param int api_timeout: Timeout for long polling
    :param str botan_token: Token for
    :param str name: Bot name

    _running = False
    _offset = 0

    def __init__(self, api_token, api_timeout=API_TIMEOUT,
                 botan_token=None, name=None):
        self.api_token = api_token
        self.api_timeout = api_timeout
        self.botan_token = botan_token = name

        def no_handle(mt):
            return lambda chat, msg: logger.debug("no handle for %s", mt)

        self._handlers = {mt: no_handle(mt) for mt in MESSAGE_TYPES}
        self._commands = []
        self._default = lambda c, m: None
        self._inline = lambda iq: None
        self._callback = lambda c, cq: None

    async def loop(self):
        Return bot's main loop as coroutine. Use with asyncio.


        >>> loop = asyncio.get_event_loop()
        >>> loop.run_until_complete(bot.loop())


        >>> loop = asyncio.get_event_loop()
        >>> loop.create_task(bot.loop())
        self._running = True
        while self._running:
            updates = await self.api_call(
                offset=self._offset + 1,

    def run(self):
        Convenience method for running bots


        >>> if __name__ == '__main__':
        loop = asyncio.get_event_loop()
        except KeyboardInterrupt:

    def command(self, regexp):
        Register a new command

        :param str regexp: Regular expression matching the command to register


        >>> @bot.command(r"/echo (.+)")
        >>> def echo(chat, match):
        >>>     return chat.reply(
        def decorator(fn):
            self._commands.append((regexp, fn))
            return fn
        return decorator

    def default(self, callback):
        Set callback for default command that is called on unrecognized
        commands for 1-to-1 chats


        >>> @bot.default
        >>> def echo(chat, message):
        >>>     return chat.reply(message["text"])
        self._default = callback
        return callback

    def inline(self, callback):
        Set callback for inline queries


        >>> @bot.inline
        >>> def echo(iq):
        >>>     return iq.answer([
        >>>         {"type": "text", "satle": "test", "id", "0"}
        >>>     ])
        self._inline = callback
        return callback

    def callback(self, callback):
        Set callback for callback queries


        >>> @bot.callback
        >>> def echo(chat, cq):
        >>>     return cq.answer()
        self._callback = callback
        return callback

    def handle(self, msg_type):
        Set handler for specific message type


        >>> @bot.handle("audio")
        >>> def handle(chat, audio):
        >>>     past
        def wrap(callback):
            self._handlers[msg_type] = callback
            return callback
        return wrap

    def channel(self, channel_name):
        Construct a Chat object used to post to channel

        :param str channel_name: Channel name
        return Chat(self, channel_name, "channel")

    def private(self, user_id):
        Construct a Chat object used to post direct messages

        :param str user_id: User id
        return Chat(self, user_id, "private")

    def group(self, group_id):
        Construct a Chat object used to post group messages

        :param str group_id: Group chat id
        return Chat(self, group_id, "group")

    async def api_call(self, method, **params):
        Call Telegram API.

        See for reference.

        :param str method: Telegram API method
        :param params: Arguments for the method call
        url = "{0}/bot{1}/{2}".format(API_URL, self.api_token, method)
        logger.debug("api_call %s, %s", method, params)

        response = await, data=params)

        if response.status == 200:
            return await response.json()
        elif response.status in RETRY_CODES:
  "Server returned %d, retrying in %d sec.",
                        response.status, RETRY_TIMEOUT)
            await response.release()
            await asyncio.sleep(RETRY_TIMEOUT)
            return await self.api_call(method, **params)
            if response.headers['content-type'] == 'application/json':
                err_msg = (await response.json())["description"]
                err_msg = await
            raise RuntimeError(err_msg)

    async def get_me(self):
        Returns basic information about the bot
        json_result = await self.api_call("getMe")
        return json_result["result"]

    async def leave_chat(self, chat_id):
        Use this method for your bot to leave a group, supergroup or channel.
        Returns True on success.

        :param int chat_id: Unique identifier for the target chat
        or username of the target supergroup or channel
        (in the format @channelusername)
        json_result = await self.api_call("leaveChat", chat_id=chat_id)
        return json_result["result"]

    _send_message = partialmethod(api_call, "sendMessage")

    def send_message(self, chat_id, text, **options):
        Send a text message to chat

        :param int chat_id: ID of the chat to send the message to
        :param str text: Text to send
        :param options: Additional sendMessage options
        return self._send_message(chat_id=chat_id, text=text, **options)

    _edit_message_text = partialmethod(api_call, "editMessageText")

    def edit_message_text(self, chat_id, message_id, text, **options):
        Edit a text message in a chat

        :param int chat_id: ID of the chat the message to edit is in
        :param int message_id: ID of the message to edit
        :param str text: Text to edit the message to
        :param options: Additional API options
        return self._edit_message_text(

    _edit_message_reply_markup = partialmethod(api_call, "editMessageReplyMarkup")

    def edit_message_reply_markup(self, chat_id, message_id, reply_markup, **options):
        Edit a reply markup of message in a chat

        :param int chat_id: ID of the chat the message to edit is in
        :param int message_id: ID of the message to edit
        :param str reply_markup: New inline keyboard markup for the message
        :param options: Additional API options
        return self._edit_message_reply_markup(

    async def get_file(self, file_id):
        Get basic information about a file and prepare it for downloading.

        :param int file_id: File identifier to get information about
        :return: File object (see
        json = await self.api_call("getFile", file_id=file_id)
        return json["result"]

    def download_file(self, file_path, range=None):
        Download a file from Telegram servers
        headers = {"range": range} if range else None
        url = "{0}/file/bot{1}/{2}".format(API_URL, self.api_token, file_path)
        return aiohttp.get(url, headers=headers)

    _get_user_profile_photos = partialmethod(api_call, "getUserProfilePhotos")

    def get_user_profile_photos(self, user_id, **options):
        Get a list of profile pictures for a user

        :param int user_id: Unique identifier of the target user
        :param options: Additional getUserProfilePhotos options (see
        return self._get_user_profile_photos(

    def track(self, message, name="Message"):
        Track message using
        Set botan_token to make it work
        if self.botan_token:
            asyncio.ensure_future(self._track(message, name))

    def stop(self):
        self._running = False

    async def _track(self, message, name):
        response = await
                "token": self.botan_token,
                "uid": message["from"]["id"],
                "name": name
            headers={'content-type': 'application/json'}
        if response.status != 200:
  "error submiting stats %d", response.status)
        await response.release()

    def _process_message(self, message):
        chat = Chat.from_message(self, message)

        for mt in MESSAGE_TYPES:
            if mt in message:
                self.track(message, mt)
                return self._handlers[mt](chat, message[mt])

        if "text" not in message:

        for patterns, handler in self._commands:
            m =, message["text"], re.I)
            if m:
                self.track(message, handler.__name__)
                return handler(chat, m)

        # No match, run default if it's a 1to1 chat
        if not chat.is_group():
            self.track(message, "default")
            return self._default(chat, message)

    def _process_inline_query(self, query):
        iq = InlineQuery(self, query)
        return self._inline(iq)

    def _process_callback_query(self, query):
        chat = Chat.from_message(self, query["message"])
        cq = CallbackQuery(self, query)
        return self._callback(chat, cq)

    def _process_updates(self, updates):
        if not updates["ok"]:
            logger.error("getUpdates error: %s", updates.get("description"))

        for update in updates["result"]:
            logger.debug("update %s", update)
            self._offset = max(self._offset, update["update_id"])
            coro = None

            if "message" in update:
                coro = self._process_message(update["message"])
            elif "inline_query" in update:
                coro = self._process_inline_query(update["inline_query"])
            elif "callback_query" in update:
                coro = self._process_callback_query(update["callback_query"])

            if coro:

clast TgBot(Bot):
    def __init__(self, *args, **kwargs):
        logger.warning("TgBot is depricated, use Bot instead")
        super().__init__(*args, **kwargs)

clast InlineQuery:
    Incoming inline query
    See for details

    def __init__(self, bot, src): = bot
        self.sender = Sender(src['from'])
        self.query_id = src['id']
        self.query = src['query']

    def answer(self, results, **options):

clast TgInlineQuery(InlineQuery):
    def __init__(self, *args, **kwargs):
        logger.warning("TgInlineQuery is depricated, use InlineQuery instead")
        super().__init__(*args, **kwargs)

clast CallbackQuery:
    def __init__(self, bot, src): = bot
        self.query_id = src['id'] = src['data']

    def answer(self):