コード例 #1
0
ファイル: mlfw.py プロジェクト: wa7err/pytgbot
def main():
    # get you bot instance.
    bot = Bot(API_KEY, return_python_objects=False)
    # Set `return_python_objects=False`
    # because we need to be really fast to answer inline queries in time,
    # and my computer is crap,
    # so any nanosecond this adds is too much,
    # resulting in the queries timing out.

    logging.add_colored_handler(logger_name=__name__, level=logging.DEBUG)

    my_info = bot.get_me()
    logger.info("Information about myself: {info}".format(info=my_info))
    last_update_id = 0
    mlfw = MLFW(bot)
    while True:
        # loop forever.
        for update in bot.get_updates(limit=100, offset=last_update_id+1, error_as_empty=True).result:
            last_update_id = update.update_id
            logger.debug(update)
            if "inline_query" not in update or not update.inline_query:
                continue
            inline_query_id = update.inline_query.id
            query = update.inline_query.query
            query_offset = update.inline_query.offset
            mlfw.search(query, inline_query_id, offset=query_offset)
コード例 #2
0
ファイル: native_objects.py プロジェクト: wa7err/pytgbot
def main():
    logging.add_colored_handler(level=logging.DEBUG)
    # get you bot instance.
    bot = Bot(API_KEY, return_python_objects=True)
    print(bot.get_me())

    # do the update loop.
    last_update_id = 0
    while True:  # loop forever.
        updates = bot.get_updates(limit=100, offset=last_update_id + 1)
        for update in updates:  # for every new update
            last_update_id = update.update_id
            print(update)
            result = update.to_array()
            assert isinstance(result, dict)
            print(result)
コード例 #3
0
ファイル: native_objects.py プロジェクト: luckydonald/pytgbot
def main():
    logging.add_colored_handler(level=logging.DEBUG)
    # get you bot instance.
    bot = Bot(API_KEY, return_python_objects=True)
    print(bot.get_me())

    # do the update loop.
    last_update_id = 0
    while True:  # loop forever.
        updates = bot.get_updates(limit=100, offset=last_update_id + 1)
        for update in updates:  # for every new update
            last_update_id = update.update_id
            print(update)
            result = update.to_array()
            assert isinstance(result, dict)
            print(result)
コード例 #4
0
ファイル: cli.py プロジェクト: luckydonald/pytgbot
def main(API_KEY):
    if API_KEY is None:
        API_KEY = answer("Input API key")
    # get your bot instance.
    bot = Bot(API_KEY, return_python_objects=True)
    my_info=bot.get_me()
    print("Information about myself: {info}".format(info=my_info))
    in_tread = Thread(target=get_updates, name="cli input thread", args=(bot,))
    in_tread.daemon = True
    in_tread.start()
    notice = "You can enter commands now.\n"
    while True:
        cmd = input(notice)
        notice = ""  # never display again.
        try:
            result_str = parse_input(bot, cmd)
            if result_str:
                print(result_str)
        except Exception as e:
            logger.exception("Error.")
            print("Error: " + str(e))
コード例 #5
0
def main(API_KEY):
    if API_KEY is None:
        API_KEY = answer("Input API key")
    # get your bot instance.
    bot = Bot(API_KEY, return_python_objects=True)
    my_info = bot.get_me()
    print("Information about myself: {info}".format(info=my_info))
    in_tread = Thread(target=get_updates,
                      name="cli input thread",
                      args=(bot, ))
    in_tread.daemon = True
    in_tread.start()
    notice = "You can enter commands now.\n"
    while True:
        cmd = input(notice)
        notice = ""  # never display again.
        try:
            result_str = parse_input(bot, cmd)
            if result_str:
                print(result_str)
        except Exception as e:
            logger.exception("Error.")
            print("Error: " + str(e))
コード例 #6
0
from pytgbot.exceptions import TgApiException

__author__ = 'luckydonald'

import logging
logger = logging.getLogger(__name__)

from somewhere import API_KEY  # so I don't upload them to github :D
# Just remove the line, and add API_KEY="..."

from pytgbot import Bot
from luckydonaldUtils.encoding import to_native as n
# get you bot instance.
bot = Bot(API_KEY)

my_info = bot.get_me()
print("Information about myself: {info}".format(info=my_info))
last_update_id = 0
while True:
    # loop forever.
    for update in bot.get_updates(limit=100,
                                  offset=last_update_id + 1,
                                  error_as_empty=True):
        assert isinstance(update, Update)
        last_update_id = update.update_id
        print(update)
        if not update.inline_query:
            continue
        query_obj = update.inline_query
        assert isinstance(query_obj, InlineQuery)
        inline_query_id = query_obj.id
コード例 #7
0
ファイル: base.py プロジェクト: TheRinger/teleflask
class TeleflaskBase(TeleflaskMixinBase):
    VERSION = VERSION
    __version__ = VERSION

    def __init__(self, api_key, app=None, blueprint=None,
                 # FlaskTgBot kwargs:
                 hostname=None, hostpath=None, hookpath="/income/{API_KEY}",
                 debug_routes=False, disable_setting_webhook=False,
                 # pytgbot kwargs:
                 return_python_objects=True):
        """
        A new Teleflask(Base) object.
        
        :param api_key: The key for the telegram bot api.
        :type  api_key: str

        :param app: The flask app if you don't like to call :meth:`init_app` yourself. 
        :type  app: flask.Flask | None
        
        :param blueprint: A blueprint, where the telegram webhook (and the debug endpoints, see `debug_routes`) will be registered in.
                          Use if you don't like to call :meth:`init_app` yourself.
                          If not set, but `app` is, it will register any routes to the `app` itself.
        :type  blueprint: flask.Blueprint | None

        :param hostname: The hostname or IP (and maybe a port) where this server is reachable in the internet.
                         Specify the path with :param hostpath:
                         Used to calculate the webhook url.
                         Also configurable via environment variables. See calculate_webhook_url()
        :param hostpath: The host url the base of where this bot is reachable.
                         Examples: None (for root of server) or "/bot2"
                         Note: The webhook will only be set on initialisation.
                         Also configurable via environment variables. See calculate_webhook_url()
        :param hookpath: The endpoint of the telegram webhook.
                        Defaults to "/income/<API_KEY>"
                        Note: The webhook will only be set on initialisation.
                        Also configurable via environment variables. See calculate_webhook_url()
        :param debug_routes: Add extra url endpoints usefull for debugging. See setup_routes(...)

        :param disable_setting_webhook: Disable updating the webhook when starting. Useful for unit tests.

        :param return_python_objects: Enable return_python_objects in pytgbot. See pytgbot.bot.Bot
        """
        self.__api_key = api_key
        self.bot = None  # will be set in self.init_bot()
        self.update_listener = list()
        self.commands = dict()
        self._return_python_objects = return_python_objects
        self._webhook_url = None  # will be filled out by self.calculate_webhook_url() in self.init_app(...)
        self.hostname = hostname  # e.g. "example.com:443"
        self.hostpath = hostpath
        self.hookpath = hookpath
        self.disable_setting_webhook=disable_setting_webhook
        if app or blueprint:
            self.init_app(app, blueprint=blueprint, debug_routes=debug_routes)
        # end if
    # end def

    def init_bot(self):
        """
        Creates the bot, and retrieves information about the bot itself (username, user_id) from telegram.
        
        :return: 
        """
        if not self.bot:  # so you can manually set it before calling `init_app(...)`,
            # e.g. a mocking bot class for unit tests
            self.bot = Bot(self.__api_key, return_python_objects=self._return_python_objects)
        elif self.bot.return_python_objects != self._return_python_objects:
            # we don't have the same setting as the given one
            raise ValueError("The already set bot has return_python_objects {given}, but we have {our}".format(
                given=self.bot.return_python_objects, our=self._return_python_objects
            ))
        # end def
        myself = self.bot.get_me()
        if self.bot.return_python_objects:
            self._user_id = myself.id
            self._username = myself.username
        else:
            self._user_id = myself["result"]["id"]
            self._username = myself["result"]["username"]
        # end if
    # end def

    def init_app(self, app, blueprint=None, debug_routes=False):
        """
        Gives us access to the flask app (and optionally provide a Blueprint),
        where we will add a routing endpoint for the telegram webhook.
        
        Calls `self.init_bot()`.

        :param app: the :class:`flask.Flask` app
        :type  app: flask.Flask
        
        :param blueprint: A blueprint, where the telegram webhook (and the debug endpoints, see `debug_routes`) will be registered in.
                          If `None` was provided, it will register any routes to the `app` itself.
        :type  blueprint: flask.Blueprint | None

        :param debug_routes: Add extra url endpoints, useful for debugging. See setup_routes(...)
        :type  debug_routes: bool

        :return: None
        :rtype: None
        """
        self.app = app
        self.blueprint = blueprint
        self.init_bot()
        hookpath, self._webhook_url = self.calculate_webhook_url(hostname=self.hostname, hostpath=self.hostpath, hookpath=self.hookpath)
        self.setup_routes(hookpath=hookpath, debug_routes=debug_routes)
        self.set_webhook()  # this will set the webhook in the bot api.

    def calculate_webhook_url(self, hostname=None, hostpath=None, hookpath="/income/{API_KEY}"):
        """
        Calculates the webhook url.
        Please note, this doesn't change any registered view function!
        Returns a tuple of the hook path (the url endpoint for your flask app) and the full webhook url (for telegram)
        Note: Both can include the full API key, as replacement for ``{API_KEY}`` in the hookpath.

        :Example:

        Your bot is at ``https://example.com:443/bot2/``,
        you want your flask to get the updates at ``/tg-webhook/{API_KEY}``.
        This means Telegram will have to send the updates to ``https://example.com:443/bot2/tg-webhook/{API_KEY}``.
    
        You now would set
            hostname = "example.com:443",
            hostpath = "/bot2",
            hookpath = "/tg-webhook/{API_KEY}"
        
        Note: Set ``hostpath`` if you are behind a reverse proxy, and/or your flask app root is *not* at the web server root.
        
    
        :param hostname: A hostname. Without the protocol.
                         Examples: "localhost", "example.com", "example.com:443"
                         If None (default), the hostname comes from the URL_HOSTNAME environment variable, or from http://ipinfo.io if that fails.
        :param hostpath: The path after the hostname. It must start with a slash.
                         Use this if you aren't at the root at the server, i.e. use url_rewrite.
                         Example: "/bot2"
                         If None (default), the path will be read from the URL_PATH environment variable, or "" if that fails.
        :param hookpath: Template for the route of incoming telegram webhook events. Must start with a slash.
                         The placeholder {API_KEY} will replaced with the telegram api key.
                         Note: This doesn't change any routing. You need to update any registered @app.route manually!
        :return: the tuple of calculated (hookpath, webhook_url).
        :rtype: tuple
        """
        import os, requests
        # #
        # #  try to fill out empty arguments
        # #
        if not hostname:
            hostname = os.getenv('URL_HOSTNAME', None)
        # end if
        if hostpath is None:
            hostpath = os.getenv('URL_PATH', "")
        # end if
        if not hookpath:
            hookpath = "/income/{API_KEY}"
        # end if
        # #
        # #  check if the path looks at least a bit valid
        # #
        logger.debug("hostname={hostn!r}, hostpath={hostp!r}, hookpath={hookp!r}".format(
            hostn=hostname, hostp=hostpath, hookp=hookpath
        ))
        if hostname:
            if hostname.endswith("/"):
                raise ValueError("hostname can't end with a slash: {value}".format(value=hostname))
            # end if
            if hostname.startswith("https://"):
                hostname = hostname[len("https://"):]
                logger.warning("Automatically removed \"https://\" from hostname. Don't include it.")
            # end if
            if hostname.startswith("http://"):
                raise ValueError("Don't include the protocol ('http://') in the hostname. "
                                 "Also telegram doesn't support http, only https.")
            # end if
        else:  # no hostname
            info = requests.get('http://ipinfo.io').json()
            hostname = str(info["ip"])
            logger.warning("URL_HOSTNAME env not set, falling back to ip address: {ip!r}".format(ip=hostname))
        # end if
        if not hostpath == "" and not hostpath.startswith("/"):
            logger.info("hostpath didn't start with a slash: {value!r} Will be added automatically".format(value=hostpath))
            hostpath = "/" + hostpath
        # end def
        if not hookpath.startswith("/"):
            raise ValueError("hookpath must start with a slash: {value!r}".format(value=hostpath))
        # end def
        hookpath = hookpath.format(API_KEY=self.__api_key)
        if not hostpath:
            logger.info("URL_PATH is not set.")
        webhook_url = "https://{hostname}{hostpath}{hookpath}".format(hostname=hostname, hostpath=hostpath, hookpath=hookpath)
        logger.debug("Tele={hostn!r}, hostpath={hostp!r}, hookpath={hookp!r}".format(
            hostn=hostname, hostp=hostpath, hookp=hookpath
        ))
        return hookpath, webhook_url
    # end def

    @property
    def username(self):
        return self._username
    # end def

    @property
    def user_id(self):
        return self._user_id
    # end def

    @property
    def webhook_url(self):
        return self._webhook_url
    # end def

    def set_webhook(self):
        """
        Sets the telegram webhook.
        Checks Telegram if there is a webhook set, and if it needs to be changed. 

        :return:
        """
        assert isinstance(self.bot, Bot)
        existing_webhook = self.bot.get_webhook_info()

        if self._return_python_objects:
            from pytgbot.api_types.receivable import WebhookInfo
            assert isinstance(existing_webhook, WebhookInfo)
            webhook_url = existing_webhook.url
            webhook_meta = existing_webhook.to_array()
        else:
            webhook_url = existing_webhook["result"]["url"]
            webhook_meta = existing_webhook["result"]
        # end def
        del existing_webhook
        logger.info("Last webhook pointed to {url!r}.\nMetadata: {hook}".format(
            url=self.hide_api_key(webhook_url), hook=self.hide_api_key("{!r}".format(webhook_meta))
            ))
        if webhook_url == self.webhook_url:
            logger.info("Webhook set correctly. No need to change.")
        else:
            if not self.app.config.get("DISABLE_SETTING_TELEGRAM_WEBHOOK", False):
                logger.info("Setting webhook to {url}".format(url=self.hide_api_key(self.webhook_url)))
                logger.debug(self.bot.set_webhook(url=self.webhook_url))
            else:
                logger.info("Would set webhook to {url!r}, but is disabled by DISABLE_SETTING_TELEGRAM_WEBHOOK config.".format(url=self.hide_api_key(self.webhook_url)))
            # end if
        # end if
    # end def

    def do_startup(self):
        """
        This code is executed after server boot.
        
        Sets the telegram webhook (see :meth:`set_webhook(self)`)
        and calls `super().do_setup()` for the superclass (e.g. other mixins)

        :return:
        """
        self.init_bot()  # retrieve username, user_id from telegram
        self.set_webhook()  # register telegram webhook
        super().do_startup()  # do more registered startup actions.
    # end def

    def hide_api_key(self, string):
        """
        Replaces the api key with "<API_KEY>" in a given string.
        
        Note: if the given object is no string, :meth:`str(object)` is called first.

        :param string: The str which can contain the api key.
        :return: string with the key replaced
        """
        if not isinstance(string, str):
            string = str(string)
        # end if
        return string.replace(self.__api_key, "<API_KEY>")
    # end def

    def jsonify(self, func):
        """
        Decorator.
        Converts the returned value of the function to json, and sets mimetype to "text/json".
        It will also automatically replace the api key where found in the output with "<API_KEY>".

        Usage:
            @app.route("/foobar")
            @app.jsonify
            def foobar():
               return {"foo": "bar"}
            # end def
            # app is a instance of this class


        There are some special cases to note:

        - :class:`tuple` is interpreted as (data, status).
            E.g.
                return {"error": "not found"}, 404
            would result in a 404 page, with json content {"error": "not found"}

        - :class:`flask.Response` will be returned directly, except it is in a :class:`tuple`
            In that case the status code of the returned response will be overwritten by the second tuple element.

        - :class:`TgBotApiObject` will be converted to json too. Status code 200.

        - An exception will be returned as `{"error": "exception raised"}` with status code 503.


        :param func: the function to wrap
        :return: the wrapped function returning json responses.
        """
        from functools import wraps
        from flask import Response
        import json
        logger.debug("func: {}".format(func))

        @wraps(func)
        def jsonify_inner(*args, **kwargs):
            try:
                result = func(*args, **kwargs)
            except:
                logger.exception("failed executing {name}.".format(name=func.__name__), exc_info=True)
                result = {"error": "exception raised"}, 503
            # end def
            status = None  # will be 200 if not otherwise changed
            if isinstance(result, tuple):
                response, status = result
            else:
                response = result
            # end if
            if isinstance(response, Response):
                if status:
                    response.status_code = status
                # end if
                return response
            # end if
            if isinstance(response, TgBotApiObject):
                response = response.to_array()
            # end if
            response = json.dumps(response)
            # end if
            assert isinstance(response, str)
            response_kwargs = {}
            response_kwargs.setdefault("mimetype", "text/json")
            if status:
                response_kwargs["status"] = status
            # end if
            res = Response(self.hide_api_key(response), **response_kwargs)
            logger.debug("returning: {}".format(res))
            return res
        # end def inner
        return jsonify_inner
    # end def

    @_self_jsonify
    def view_exec(self, api_key, command):
        """
        Issue commands. E.g. /exec/TELEGRAM_API_KEY/getMe

        :param api_key: gets checked, so you can't just execute commands.
        :param command: the actual command
        :return:
        """
        if api_key != self.__api_key:
            error_msg = "Wrong API key: {wrong_key!r}".format(wrong_key=api_key)
            logger.warning(error_msg)
            return {"status": "error", "message": error_msg, "error_code": 403}, 403
        # end if
        from flask import request
        from pytgbot.exceptions import TgApiServerException
        logger.debug("COMMAND: {cmd}, ARGS: {args}".format(cmd=command, args=request.args))
        try:
            res = self.bot.do(command, **request.args)
            if self._return_python_objects:
                return res.to_array()
            else:
                return res
            # end if
        except TgApiServerException as e:
            return {"status": "error", "message": e.description, "error_code": e.error_code}, e.error_code
        # end try
    # end def

    @_self_jsonify
    def view_status(self):
        """
        Returns the status about the bot's webhook.

        :return: webhook info
        """
        try:
            res = self.bot.get_webhook_info()  # TODO: fix to work with return_python_objects==False
            return res.to_array()
        except TgApiServerException as e:
            return {"status": "error", "message": e.description, "error_code": e.error_code}, e.error_code
        # end try

    @_self_jsonify
    def view_updates(self):
        """
        This processes incoming telegram updates.

        :return:
        """
        from pprint import pformat
        from flask import request
        from pytgbot.api_types.receivable.updates import Update

        logger.debug("INCOME:\n{}\n\nHEADER:\n{}".format(
            pformat(request.get_json()),
            request.headers if hasattr(request, "headers") else None
        ))
        update = Update.from_array(request.get_json())
        try:
            result = self.process_update(update)
        except Exception as e:
            logger.exception("process_update()")
            result = {"status": "error", "message": str(e)}
        result = result if result else {"status": "probably ok"}
        logger.info("returning result: {}".format(result))
        return result
    # end def

    @_self_jsonify
    def view_hostinfo(self):
        """
        Get infos about your host, like IP etc.
        :return:
        """
        import socket
        import requests
        info = requests.get('http://ipinfo.io').json()
        info["host"] = socket.gethostname()
        info["version"] = self.VERSION
        return info
    # end def

    def get_router(self):
        """
        Where to call `add_url_rule` (aka. `@route`) on.
        Returns either the blueprint if there is any, or the app.
        
        :raises ValueError: if neither blueprint nor app is set.
        
        :returns: either the blueprint if it is set, or the app.
        :rtype: flask.Blueprint | flask.Flask
        """
        if self.blueprint:
            return self.blueprint
        # end if
        if not self.app:
            raise ValueError("The app (self.app) is not set.")
        # end if
        return self.app

    def setup_routes(self, hookpath, debug_routes=False):
        """
        Sets the pathes to the registered blueprint/app:
            - "webhook" (self.view_updates) at hookpath
        Also, if `debug_routes` is `True`:
            - "exec" (self.view_exec) at "/exec/API_KEY/<command>"  (`API_KEY` is filled in, `<command>` is any Telegram API command.)
            - "status" (self.view_status) at "/status"
            - "hostinfo" (self.view_hostinfo) at "/hostinfo"
        
        :param hookpath: The path where it expects telegram updates to hit the flask app/blueprint.
        :type  hookpath: str
        
        :param debug_paths: Add several debug pathes.
        :type  debug_routes: bool
        """
        # Todo: Find out how to handle blueprints
        if not self.app and not self.blueprint:
            raise ValueError("No app (self.app) or Blueprint (self.blueprint) was set.")
        # end if
        router = self.get_router()
        logger.info("Adding webhook route: {url!r}".format(url=hookpath))
        router.add_url_rule(hookpath, endpoint="webhook", view_func=self.view_updates, methods=['POST'])
        if debug_routes:
            router.add_url_rule("/exec/{api_key}/<command>".format(api_key=self.__api_key), endpoint="exec" , view_func=self.view_exec)
            router.add_url_rule("/status", endpoint="status", view_func=self.view_status)
            router.add_url_rule("/hostinfo", endpoint="hostinfo", view_func=self.view_hostinfo)
        # end if
    # end def

    @abc.abstractmethod
    def process_update(self, update):
        return
    # end def

    def process_result(self, update, result):
        """
        Send the result.
        It may be a :class:`Message` or a list of :class:`Message`s
        Strings will be send as :class:`TextMessage`, encoded as raw text.
        
        :param manager:
        :param result:
        :return:
        """
        from ..messages import Message
        reply_to, reply_id = None, None
        if update.message and update.message.chat.id and update.message.message_id:
            reply_to, reply_id = update.message.chat.id, update.message.message_id
        # end if
        if update.channel_post and update.channel_post.chat.id and update.channel_post.message_id:
            reply_to, reply_id = update.channel_post.chat.id, update.channel_post.message_id
        # end if
        if update.edited_message and update.edited_message.chat.id and update.edited_message.message_id:
            reply_to, reply_id = update.edited_message.chat.id, update.edited_message.message_id
        # end if
        if update.edited_channel_post and update.edited_channel_post.chat.id and update.edited_channel_post.message_id:
            reply_to, reply_id = update.edited_channel_post.chat.id, update.edited_channel_post.message_id
        # end if
        if isinstance(result, (Message, str, list, tuple)):
            self.send_message(result, reply_to, reply_id)
        elif result is False or result is None:
            logger.debug("Ignored result {res!r}".format(res=result))
            # ignore it
        else:
            logger.warn("Unexpected plugin result: {type}".format(type=type(result)))
        # end if
    # end def

    def send_message(self, message, reply_to, reply_id):
        """
        Sends a Message.
        Plain strings will become an unformatted TextMessage.
        Supports to mass send lists, tuples, Iterable.

        :param message: A Message object.
        :type  message: Message | str | list | tuple |
        :param instant: Send without waiting for the plugin's function to be done. True to send as soon as possible.
        False or None to wait until the plugin's function is done and has returned, messages the answers in a bulk.
        :type  instant: bool or None
        """
        from pytgbot.exceptions import TgApiException
        from ..messages import Message, TextMessage

        logger.debug("Got {}".format(message))
        if not isinstance(message, (Message, str, list, tuple)):
            raise TypeError("Is not a Message type (or str or tuple/list).")
        # end if
        if isinstance(message, tuple):
            message = [x for x in message]
        # end if
        if not isinstance(message, list):
            message = [message]
        # end if
        assert isinstance(message, list)
        for msg in message:
            if isinstance(msg, str):
                assert not isinstance(message, str)  # because we would split a string to pieces.
                msg = TextMessage(msg, parse_mode="text")
            # end if
            if not isinstance(msg, Message):
                raise TypeError("Is not a Message type.")
            # end if
            # if msg._next_msg:  # TODO: Reply message?
            #     message.insert(message.index(msg) + 1, msg._next_msg)
            #     msg._next_msg = None
            from requests.exceptions import RequestException
            try:
                msg.send(self.bot, reply_to, reply_id)
            except (TgApiException, RequestException):
                logger.exception("Manager failed messages. Message was {msg!s}".format(msg=msg))
コード例 #8
0
ファイル: Run.py プロジェクト: WALR/Telegram2FB
reload(sys)
sys.setdefaultencoding('utf-8')

import logging
logger = logging.getLogger(__name__)

from pytgbot import Bot, InputFile
import facebook

API_KEY="..."
graph = facebook.GraphAPI(access_token='...')
TEST_CHAT = 12345

bot = Bot(API_KEY)

my_info=bot.get_me()
print("Informacion del Bot: {info}".format(info=my_info))
last_update_id = 0

while True:
	# loop forever.
	for update in bot.get_updates(limit=100, offset=last_update_id+1)["result"]:
		last_update_id = update["update_id"]
		print(update)
		if "message" in update and "text" in update["message"]:

			if "chat" in update["message"]:
				sender = update["message"]["chat"]["id"]
			else:
				sender = update["message"]["from"]["id"]
				
コード例 #9
0
ファイル: cli.py プロジェクト: luckydonald-forks/pytgbot
class CLI(object):
    METHOD_INCLUDES = {}  # added to in the __init__ method.
    METHOD_EXCLUDES = [
        "do",
    ]

    def __init__(self, API_KEY, debug=False):
        if API_KEY is None:
            API_KEY = self.ask_for_apikey()
        self._api_key = API_KEY

        self.bot = Bot(API_KEY, return_python_objects=True)

        self.me = self.bot.get_me()
        logger.info("Information about myself: {info}".format(info=self.me))

        self.METHOD_INCLUDES.update({
            "debug": self.cmd_debug,
            "help": self.cmd_help,
        })

        self.color = Color()

        self.update_thread = self.create_update_thread()

        self.functions = {k: v for k, v in self.get_functions()}
        self.current_candidates = []
        self.is_debug = debug

        self.query = "pytgbot> "  # in front of the input.
        self.register_tab_completion()

    # end def

    def print(self, text):
        current_input = readline.get_line_buffer()
        delete_chars = len(self.query) + len(current_input)

        # remove the input:
        sys.stdout.flush()
        for i in range(delete_chars):
            sys.stdout.write("\b \b")
        # end for
        sys.stdout.write("\033[K")
        sys.stdout.flush()

        # actual output
        sys.stdout.write(str(text))
        sys.stdout.write("\n")
        sys.stdout.flush()
        # load back the input
        sys.stdout.write(self.query)
        sys.stdout.flush()
        sys.stdout.write(current_input)
        sys.stdout.flush()
        readline.redisplay()

    # end def

    def ask_for_apikey(self):
        return answer("Input your bot API key.")

    # end def

    def register_tab_completion(self):
        # Register our completer function
        readline.parse_and_bind('tab: complete')
        readline.set_completer(self.complete)
        # Use the tab key for completion

    # end def

    def create_update_thread(self):
        tg_update_thread = Thread(target=self.get_updates,
                                  name="telegram update thread")
        tg_update_thread.daemon = True
        tg_update_thread.start()
        return tg_update_thread

    # end def

    def run(self):
        print("You can enter commands now.")
        while True:
            sys.stdout.write("\r")
            sys.stdout.flush()
            cmd = input(self.query)
            try:
                result_str = self.parse_input(cmd)
                if result_str:
                    self.print(result_str)
            except Exception as e:
                logger.exception("Error.")
                self.print("{color_red}{error}{all_off}".format(
                    error=e, **self.color.formatter))
            # end try
        # end while

    # end def

    def update_callback(self, string):
        try:
            self.print_update(string)
        except AttributeError:
            self.print(string)
            raise
        # end try

    # end def

    def get_updates(self):
        last_update_id = 0
        while True:  # loop forever.
            for update in self.bot.get_updates(
                    limit=100,
                    offset=last_update_id + 1,
                    poll_timeout=60,
                    error_as_empty=True):  # for every new update
                last_update_id = update.update_id
                if self.is_debug:
                    logger.info(repr(last_update_id))
                # end if
                try:
                    self.update_callback(update)
                except Exception:
                    logger.exception(
                        "update_callback failed\nIncomming update: {u}".format(
                            u=update))
                # end def
            # end for
        # end while

    # end def

    def print_update(self, update):
        self.print(self.process_update(update))

    # end def

    def process_update(self, update):
        assert_type_or_raise(update, Update,
                             parameter_name="update")  # is message
        if update.message:
            return self.process_message(update.message)
        elif update.channel_post:
            return self.process_message(update.channel_post)
        elif update.inline_query:
            qry = update.inline_query
            assert_type_or_raise(qry,
                                 InlineQuery,
                                 parameter_name="update.inline_query")
            qry_from_print = self.color.overwrite_color(
                self.print_peer(qry.from_peer, id_prefix=True),
                self.color.formatter.color_yellow,
                prefix=True,
                reset=self.color.formatter.color_white)
            return "[query {id}] {from_print}:{all_off} {text}".format(
                from_print=qry_from_print,
                id=qry.id,
                text=qry.query,
                **self.color.formatter)
        else:
            return str(update)
        # end if

    # end def

    def process_message(self, msg):
        # prepare prefix with chat infos:
        assert_type_or_raise(msg, Message, parameter_name="msg")
        user_print = None
        if "from_peer" in msg and msg.from_peer:  # 'channel' might have no `from_peer`.
            user_print = self.color.overwrite_color(
                self.print_peer(msg.from_peer, id_prefix="user"),
                self.color.formatter.color_yellow,
                prefix=True,
                reset=self.color.formatter.color_white)
        if msg.chat.type == 'private':
            prefix = "{background_grey}{color_white}[msg {message_id}]{color_off} {color_yellow}{user}{color_white}: ".format(
                message_id=msg.message_id,
                user=user_print,
                **self.color.formatter)
        elif msg.chat.type in ('group', 'supergroup'):
            group_print = self.color.overwrite_color(
                self.print_peer(msg.chat, id_prefix=True),
                self.color.formatter.color_yellow,
                prefix=True,
                reset=self.color.formatter.color_white)
            prefix = "{background_grey}{color_white}[msg {message_id}]{color_off} {color_yellow}{user}{color_white} in {chat}: ".format(
                message_id=msg.message_id,
                user=user_print,
                chat=group_print,
                **self.color.formatter)
        elif msg.chat.type == 'channel':
            group_print = self.color.overwrite_color(
                self.print_peer(msg.chat, id_prefix=True),
                self.color.formatter.color_yellow,
                prefix=True,
                reset=self.color.formatter.color_white)
            prefix = "{background_grey}{color_white}[msg {message_id}]{color_off} {color_white}In {chat}: ".format(
                message_id=msg.message_id,
                user=user_print,
                chat=group_print,
                **self.color.formatter)
        else:
            prefix = "{background_grey}{color_white}[msg {message_id}]{color_off} {color_red}UNKNOWN ORIGIN{color_white}: ".format(
                message_id=msg.message_id, **self.color.formatter)
        # end if

        # now the message types
        if "text" in msg:
            return prefix + self.color.formatter.color_red + msg.text + self.color.formatter.all_off
        if "photo" in msg:
            photo = msg.photo[0]
            for p in msg.photo[1:]:
                if p.file_size > photo.file_size:
                    photo = p
                # end if
            # end for
            return prefix + "\n" + self.process_file(
                photo, msg.caption, file_type="photo",
                height="10") + self.color.formatter.all_off
        if "sticker" in msg:
            return prefix + "\n" + self.process_file(
                msg.sticker,
                msg.caption,
                file_type="sticker",
                as_png=True,
                height="10") + self.color.formatter.all_off
        # end if

    # end def

    def process_file(self,
                     file,
                     caption,
                     file_type="file",
                     as_png=False,
                     inline=True,
                     height=None):
        file_object = self.bot.get_file(file.file_id)
        file_url = self.bot.get_download_url(file_object)
        file_content = get_file(file_url, as_png=as_png)
        file_name = file_url.split("/")[-1]
        if as_png:
            file_name = file_name + ".png"
        # end if
        save_file_name = str(file.file_id) + "__" + file_name
        return "[{type} {file_id}]\n{image}{color_red}{caption}{color_off}".format(
            file_id=file.file_id,
            caption=(" " + caption if caption else ""),
            image=iterm_show_file(save_file_name,
                                  data=file_content,
                                  inline=inline,
                                  height=height),
            type=file_type,
            file_name=save_file_name,
            **self.color.formatter)

    # end def

    def parse_input(self, cmd):
        if " " not in cmd:  # functions like get_me doesn't need params.
            command, args = cmd, None
        else:
            command, args = cmd.split(" ", maxsplit=1)
        if command == "msg":
            user, message = args.split(" ", maxsplit=1)
            try:
                user = cached_chats[user]
            except KeyError:
                return "{color_red}{inverse_on}[FAIL]{inverse_off} I don't have that peer cached.{all_off}".format(
                    **self.color.formatter)
                # TODO: accept anyway? So you can use a username or something?
            try:
                result = self.bot.send_msg(user, message)
                return "{color_green}{inverse_on}[ OK ]{inverse_off} {0}{all_off}".format(
                    result, **self.color.formatter)
            except TgApiException as e:
                return "{color_red}{inverse_on}[FAIL]{inverse_off} {0}{all_off}".format(
                    e, **self.color.formatter)
            # end try
        # end if
        if command not in self.functions:
            return '{color_red}{inverse_on}ERROR:{inverse_off} The function {0!r} was not found.{all_off}'.format(
                command, **self.color.formatter)
        # end if
        cmd_func = self.functions[command]
        try:
            if args:
                parsed_args = parse_args(args)
                if not isinstance(parsed_args, (list, dict)):
                    parsed_args = [str(parsed_args)]
                # end if not isinstance
                if isinstance(parsed_args, list):
                    call_result = cmd_func(*parsed_args)
                else:
                    assert isinstance(parsed_args, dict)
                    call_result = cmd_func(**parsed_args)
                    # end if isinstance
            else:
                call_result = cmd_func()
                # end if
            # end if
            return "{color_green}{inverse_on}[ OK ]{inverse_off} {result}{all_off}".format(
                result=call_result, **self.color.formatter)
        except TgApiException as e:
            return "{color_red}{inverse_on}[FAIL]{inverse_off} {exception}{all_off}".format(
                exception=e, **self.color.formatter)
        # end try

    # end def

    def cmd_debug(self):
        self.is_debug = not self.is_debug
        return "Turned {state} debug.".format(
            state="on" if self.is_debug else "off")

    # end if

    def cmd_help(self, *args, **kwargs):
        return self.get_help(*args, **kwargs)

    # end def

    def get_help(self, search="", return_string=True):
        strings = []
        for name, func in self.functions.items():
            if search and not name.startswith(search):
                continue
            func_str = "{func} - {doc}".format(
                func=self.get_func_str(func),
                doc=func.__doc__.strip().split("\n")[0])
            if search and search == name:  # perfect hit
                func_def = "{color_blue}def{color_off} ".format(
                    **self.color.formatter) + self.get_func_str(func) + ":"
                seperator = ("-" * (len(func_def) - 1))
                return func_def + "\n|>" + seperator + "\n|  " + ("\n| ".join(
                    func.__doc__.strip().split("\n")) + "\n'>" + seperator)
            strings.append(func_str)
            # end if
        # end for
        return "\n".join(strings)

    # end def

    def get_func_str(self, func):
        from inspect import signature
        has_self = hasattr(func, "__self__")
        args = []
        for i, param in enumerate(signature(func).parameters):
            if i == 0 and has_self:
                key = "{color_magenta}{text}{color_off}".format(
                    text=param.name, **self.color.formatter)
            else:
                key = param.name
            # end if
            if param.default:
                arg = "{key}={default!r}".format(key=key,
                                                 default=param.default)
            else:
                arg = arg
            # end if
            args.append(arg)
        # end for
        spec = "{name}({params})".format(name=func.__name__,
                                         params=", ".join(args))
        return spec

    def get_functions(self):
        for name, func in chain(self.METHOD_INCLUDES.items(),
                                getmembers(self.bot)):
            if name.startswith("_"):
                continue
            # end if
            if name in self.METHOD_EXCLUDES:
                continue
            # end if
            elif not ismethod(func):
                continue
            # end if
            yield name, func
        # end for

    # end def

    def complete(self, text, state):
        if state == 0:  # first of list, prepare the list
            self.current_candidates = [
                cmd for cmd in list(self.functions.keys())
                if cmd.startswith(text)
            ]
        # end if
        try:
            return self.current_candidates[state]
        except IndexError:
            return None
        # end try
        if True:
            pass
        else:
            return None
            # This is the first time for this text, so build a match list.
            origline = readline.get_line_buffer()
            begin = readline.get_begidx()
            end = readline.get_endidx()
            being_completed = origline[begin:end]
            words = origline.split()

            logger.debug('origline=%s', repr(origline))
            logger.debug('begin=%s', begin)
            logger.debug('end=%s', end)
            logger.debug('being_completed=%s', being_completed)
            logger.debug('words=%s', words)

            if not words:
                self.current_candidates = sorted(self.functions.keys())
            else:
                try:
                    if begin == 0:
                        # first word
                        candidates = self.functions.keys()
                    else:
                        # later word
                        first = words[0]
                        candidates = self.functions[first]

                    if being_completed:
                        # match options with portion of input
                        # being completed
                        self.current_candidates = [
                            w for w in candidates
                            if w.startswith(being_completed)
                        ]
                    else:
                        # matching empty string so use all candidates
                        self.current_candidates = candidates

                    logging.debug('candidates=%s', self.current_candidates)

                except (KeyError, IndexError) as err:
                    logging.error('completion error: %s', err)
                    self.current_candidates = []

        try:
            response = self.current_candidates[state]
        except IndexError:
            response = None
        logging.debug('complete(%s, %s) => %s', repr(text), state, response)
        return response

    # end def

    def print_peer(self, peer, show_id=True, id_prefix="", reply=True):
        """
        :param id_prefix: Prefix of the #id thing. Set a string, or true to have it generated.
        :type  id_prefix: str|bool
        """
        if isinstance(id_prefix, bool):
            if id_prefix:  # True
                if isinstance(peer, User):
                    id_prefix = "user"
                elif isinstance(peer, Chat):
                    id_prefix = peer.type
                else:
                    id_prefix = "unknown"
                # end if
            else:  # False
                id_prefix = ""
            # end if
        # end if
        peer_string = self.peer_to_string(peer)
        if show_id and "id" in peer:
            peer_string += " ({color_lightblue}{id_prefix}#{id}{color_off})".format(
                id_prefix=id_prefix, id=peer.id, **self.color.formatter)
        return peer_string

    def peer_to_string(self, peer):
        assert_type_or_raise(peer, Peer, parameter_name="peer")
        if isinstance(peer, Chat):  # chats
            return "{title}".format(title=peer.title)
        assert_type_or_raise(peer, User, parameter_name="peer")
        if peer.first_name:
            if peer.last_name:
                return "{first_name} {last_name}".format(
                    first_name=peer.first_name, last_name=peer.last_name)
            # end if last_name
            return "{first_name}".format(first_name=peer.first_name)
        # not first_name
        elif peer.last_name:
            return "{last_name}".format(last_name=peer.last_name)
        elif peer.username:
            return "{}@{username}".format(username=peer.username)
        elif peer.id:
            return "#{id}".format(id=peer.id)
        # end if
        return "<UNKNOWN>"
コード例 #10
0
VERSION = "1.3.1"
__version__ = VERSION

app = Flask(__name__)
app.register_blueprint(version_bp)

sentry = add_error_reporting(app)
bot = Bot(API_KEY, return_python_objects=False)
# Set `return_python_objects=False`
# because we need to be really fast to answer inline queries in time,
# and my computer is crap,
# so any nanosecond this adds is too much,
# resulting in the queries timing out.

username = bot.get_me().result.username
logging.add_colored_handler(level=logging.DEBUG)

mlfw = MLFW(bot)


def to_json_remove_api_key(func):
    """
    Jsonfify returned stuff, if dict or list
    Then set mimetype to "text/json"

    :param func: the function to wrap
    :return:
    """
    from functools import wraps
コード例 #11
0
class TeleflaskBase(TeleflaskMixinBase):
    VERSION = VERSION
    __version__ = VERSION

    def __init__(self, api_key, app=None, blueprint=None,
                 # FlaskTgBot kwargs:
                 hostname=None, hostpath=None, hookpath="/income/{API_KEY}",
                 debug_routes=False, disable_setting_webhook_route=None, disable_setting_webhook_telegram=None,
                 # pytgbot kwargs:
                 return_python_objects=True):
        """
        A new Teleflask(Base) object.

        :param api_key: The key for the telegram bot api.
        :type  api_key: str

        :param app: The flask app if you don't like to call :meth:`init_app` yourself.
        :type  app: flask.Flask | None

        :param blueprint: A blueprint, where the telegram webhook (and the debug endpoints, see `debug_routes`) will be registered in.
                          Use if you don't like to call :meth:`init_app` yourself.
                          If not set, but `app` is, it will register any routes to the `app` itself.
        :type  blueprint: flask.Blueprint | None

        :param hostname: The hostname or IP (and maybe a port) where this server is reachable in the internet.
                         Specify the path with `hostpath`
                         Used to calculate the webhook url.
                         Also configurable via environment variables. See calculate_webhook_url()
        :type  hostname: None|str

        :param hostpath: The host url the base of where this bot is reachable.
                         Examples: None (for root of server) or "/bot2"
                         Note: The webhook will only be set on initialisation.
                         Also configurable via environment variables. See calculate_webhook_url()
        :type  hostpath: None|str

        :param hookpath: The endpoint of the telegram webhook.
                        Defaults to "/income/<API_KEY>"
                        Note: The webhook will only be set on initialisation.
                        Also configurable via environment variables. See calculate_webhook_url()
        :type  hookpath: str

        :param debug_routes: Add extra url endpoints usefull for debugging. See setup_routes(...)
        :type  debug_routes: bool

        :param disable_setting_webhook_telegram: Disable updating the telegram webhook when starting.
                                                 Useful for unit tests. Defaults to the app's config
                                                 DISABLE_SETTING_ROUTE_WEBHOOK or False.
        :type  disable_setting_webhook_telegram: None|bool

        :param disable_setting_webhook_route: Disable creation of the webhook route.
                                              Usefull if you don't need to listen for incomming events.
        :type  disable_setting_webhook_route: None|bool

        :param return_python_objects: Enable return_python_objects in pytgbot. See pytgbot.bot.Bot
        """
        self.__api_key = api_key
        self._bot = None  # will be set in self.init_bot()
        self.app = None  # will be filled out by self.init_app(...)
        self.blueprint = None  # will be filled out by self.init_app(...)
        self._return_python_objects = return_python_objects
        self.__webhook_url = None  # will be filled out by self.calculate_webhook_url() in self.init_app(...)
        self.hostname = hostname  # e.g. "example.com:443"
        self.hostpath = hostpath
        self.hookpath = hookpath

        if disable_setting_webhook_route is None:
            try:
                self.disable_setting_webhook_route = self.app.config["DISABLE_SETTING_WEBHOOK_ROUTE"]
            except (AttributeError, KeyError):
                logger.debug(
                    'disable_setting_webhook_route is None and app is None or app has no DISABLE_SETTING_WEBHOOK_ROUTE'
                    ' config. Assuming False.'
                )
                self.disable_setting_webhook_route = False
            # end try
        else:
            self.disable_setting_webhook_route = disable_setting_webhook_route
        # end if

        if disable_setting_webhook_telegram is None:
            try:
                self.disable_setting_webhook_telegram = self.app.config["DISABLE_SETTING_WEBHOOK_TELEGRAM"]
            except (AttributeError, KeyError):
                logger.debug(
                    'disable_setting_webhook_telegram is None and app is None or app has no DISABLE_SETTING_WEBHOOK_TELEGRAM'
                    ' config. Assuming False.'
                )
                self.disable_setting_webhook_telegram = False
            # end try
        else:
            self.disable_setting_webhook_telegram = disable_setting_webhook_telegram
        # end if

        if app or blueprint:  # if we have an app or flask blueprint call init_app for adding the routes, which calls init_bot as well.
            self.init_app(app, blueprint=blueprint, debug_routes=debug_routes)
        elif api_key:  # otherwise if we have at least an api key, call init_bot.
            self.init_bot()
        # end if

        self.update_listener = list()
        self.commands = dict()
    # end def

    def init_bot(self):
        """
        Creates the bot, and retrieves information about the bot itself (username, user_id) from telegram.

        :return:
        """
        if not self._bot:  # so you can manually set it before calling `init_app(...)`,
            # e.g. a mocking bot class for unit tests
            self._bot = Bot(self._api_key, return_python_objects=self._return_python_objects)
        elif self._bot.return_python_objects != self._return_python_objects:
            # we don't have the same setting as the given one
            raise ValueError("The already set bot has return_python_objects {given}, but we have {our}".format(
                given=self._bot.return_python_objects, our=self._return_python_objects
            ))
        # end def
        myself = self._bot.get_me()
        if self._bot.return_python_objects:
            self._user_id = myself.id
            self._username = myself.username
        else:
            self._user_id = myself["result"]["id"]
            self._username = myself["result"]["username"]
        # end if
    # end def

    def init_app(self, app, blueprint=None, debug_routes=False):
        """
        Gives us access to the flask app (and optionally provide a Blueprint),
        where we will add a routing endpoint for the telegram webhook.

        Calls `self.init_bot()`, calculates and sets webhook routes, and finally runs `self.do_startup()`.

        :param app: the :class:`flask.Flask` app
        :type  app: flask.Flask

        :param blueprint: A blueprint, where the telegram webhook (and the debug endpoints, see `debug_routes`) will be registered in.
                          If `None` was provided, it will register any routes to the `app` itself.
                          Note: this is NOT a `TBlueprint`, but a regular `flask` one!
        :type  blueprint: flask.Blueprint | None

        :param debug_routes: Add extra url endpoints, useful for debugging. See setup_routes(...)
        :type  debug_routes: bool

        :return: None
        :rtype: None
        """
        self.app = app
        self.blueprint = blueprint
        self.init_bot()
        hookpath, self.__webhook_url = self.calculate_webhook_url(hostname=self.hostname, hostpath=self.hostpath, hookpath=self.hookpath)
        self.setup_routes(hookpath=hookpath, debug_routes=debug_routes)
        self.set_webhook_telegram()  # this will set the webhook in the bot api.
        self.do_startup()  # this calls the startup listeners of extending classes.
    # end def

    def calculate_webhook_url(self, hostname=None, hostpath=None, hookpath="/income/{API_KEY}"):
        """
        Calculates the webhook url.
        Please note, this doesn't change any registered view function!
        Returns a tuple of the hook path (the url endpoint for your flask app) and the full webhook url (for telegram)
        Note: Both can include the full API key, as replacement for ``{API_KEY}`` in the hookpath.

        :Example:

        Your bot is at ``https://example.com:443/bot2/``,
        you want your flask to get the updates at ``/tg-webhook/{API_KEY}``.
        This means Telegram will have to send the updates to ``https://example.com:443/bot2/tg-webhook/{API_KEY}``.

        You now would set
            hostname = "example.com:443",
            hostpath = "/bot2",
            hookpath = "/tg-webhook/{API_KEY}"

        Note: Set ``hostpath`` if you are behind a reverse proxy, and/or your flask app root is *not* at the web server root.


        :param hostname: A hostname. Without the protocol.
                         Examples: "localhost", "example.com", "example.com:443"
                         If None (default), the hostname comes from the URL_HOSTNAME environment variable, or from http://ipinfo.io if that fails.
        :param hostpath: The path after the hostname. It must start with a slash.
                         Use this if you aren't at the root at the server, i.e. use url_rewrite.
                         Example: "/bot2"
                         If None (default), the path will be read from the URL_PATH environment variable, or "" if that fails.
        :param hookpath: Template for the route of incoming telegram webhook events. Must start with a slash.
                         The placeholder {API_KEY} will replaced with the telegram api key.
                         Note: This doesn't change any routing. You need to update any registered @app.route manually!
        :return: the tuple of calculated (hookpath, webhook_url).
        :rtype: tuple
        """
        import os, requests
        # #
        # #  try to fill out empty arguments
        # #
        if not hostname:
            hostname = os.getenv('URL_HOSTNAME', None)
        # end if
        if hostpath is None:
            hostpath = os.getenv('URL_PATH', "")
        # end if
        if not hookpath:
            hookpath = "/income/{API_KEY}"
        # end if
        # #
        # #  check if the path looks at least a bit valid
        # #
        logger.debug("hostname={hostn!r}, hostpath={hostp!r}, hookpath={hookp!r}".format(
            hostn=hostname, hostp=hostpath, hookp=hookpath
        ))
        if hostname:
            if hostname.endswith("/"):
                raise ValueError("hostname can't end with a slash: {value}".format(value=hostname))
            # end if
            if hostname.startswith("https://"):
                hostname = hostname[len("https://"):]
                logger.warning("Automatically removed \"https://\" from hostname. Don't include it.")
            # end if
            if hostname.startswith("http://"):
                raise ValueError("Don't include the protocol ('http://') in the hostname. "
                                 "Also telegram doesn't support http, only https.")
            # end if
        else:  # no hostname
            info = requests.get('http://ipinfo.io').json()
            hostname = str(info["ip"])
            logger.warning("URL_HOSTNAME env not set, falling back to ip address: {ip!r}".format(ip=hostname))
        # end if
        if not hostpath == "" and not hostpath.startswith("/"):
            logger.info("hostpath didn't start with a slash: {value!r} Will be added automatically".format(value=hostpath))
            hostpath = "/" + hostpath
        # end def
        if not hookpath.startswith("/"):
            raise ValueError("hookpath must start with a slash: {value!r}".format(value=hostpath))
        # end def
        hookpath = hookpath.format(API_KEY=self._api_key)
        if not hostpath:
            logger.info("URL_PATH is not set.")
        webhook_url = "https://{hostname}{hostpath}{hookpath}".format(hostname=hostname, hostpath=hostpath, hookpath=hookpath)
        logger.debug("host={hostn!r}, hostpath={hostp!r}, hookpath={hookp!r}, hookurl={url!r}".format(
            hostn=hostname, hostp=hostpath, hookp=hookpath, url=webhook_url
        ))
        return hookpath, webhook_url
    # end def

    @property
    def bot(self):
        """
        :return: Returns the bot
        :rtype: Bot
        """
        return self._bot
    # end def
    @property
    def username(self):
        """
        Returns the name of the registerd bot
        :return:
        """
        return self._username
    # end def

    @property
    def user_id(self):
        return self._user_id
    # end def

    @property
    def _webhook_url(self):
        return self.__webhook_url
    # end def

    @property
    def _api_key(self):
        return self.__api_key
    # end def

    def set_webhook_telegram(self):
        """
        Sets the telegram webhook.
        Checks Telegram if there is a webhook set, and if it needs to be changed.

        :return:
        """
        assert isinstance(self.bot, Bot)
        existing_webhook = self.bot.get_webhook_info()

        if self._return_python_objects:
            from pytgbot.api_types.receivable import WebhookInfo
            assert isinstance(existing_webhook, WebhookInfo)
            webhook_url = existing_webhook.url
            webhook_meta = existing_webhook.to_array()
        else:
            webhook_url = existing_webhook["result"]["url"]
            webhook_meta = existing_webhook["result"]
        # end def
        del existing_webhook
        logger.info("Last webhook pointed to {url!r}.\nMetadata: {hook}".format(
            url=self.hide_api_key(webhook_url), hook=self.hide_api_key("{!r}".format(webhook_meta))
            ))
        if webhook_url == self._webhook_url:
            logger.info("Webhook set correctly. No need to change.")
        else:
            if not self.disable_setting_webhook_telegram:
                logger.info("Setting webhook to {url}".format(url=self.hide_api_key(self._webhook_url)))
                logger.debug(self.bot.set_webhook(url=self._webhook_url))
            else:
                logger.info(
                    "Would set webhook to {url!r}, but action is disabled by DISABLE_SETTING_TELEGRAM_WEBHOOK config "
                    "or disable_setting_webhook_telegram argument.".format(url=self.hide_api_key(self._webhook_url))
                )
            # end if
        # end if
    # end def

    def do_startup(self):
        """
        This code is executed after server boot.

        Sets the telegram webhook (see :meth:`set_webhook_telegram(self)`)
        and calls `super().do_setup()` for the superclass (e.g. other mixins)

        :return:
        """
        super().do_startup()  # do more registered startup actions.
    # end def

    def hide_api_key(self, string):
        """
        Replaces the api key with "<API_KEY>" in a given string.

        Note: if the given object is no string, :meth:`str(object)` is called first.

        :param string: The str which can contain the api key.
        :return: string with the key replaced
        """
        if not isinstance(string, str):
            string = str(string)
        # end if
        return string.replace(self._api_key, "<API_KEY>")
    # end def

    def jsonify(self, func):
        """
        Decorator.
        Converts the returned value of the function to json, and sets mimetype to "text/json".
        It will also automatically replace the api key where found in the output with "<API_KEY>".

        Usage:
            @app.route("/foobar")
            @app.jsonify
            def foobar():
               return {"foo": "bar"}
            # end def
            # app is a instance of this class


        There are some special cases to note:

        - :class:`tuple` is interpreted as (data, status).
            E.g.
                return {"error": "not found"}, 404
            would result in a 404 page, with json content {"error": "not found"}

        - :class:`flask.Response` will be returned directly, except it is in a :class:`tuple`
            In that case the status code of the returned response will be overwritten by the second tuple element.

        - :class:`TgBotApiObject` will be converted to json too. Status code 200.

        - An exception will be returned as `{"error": "exception raised"}` with status code 503.


        :param func: the function to wrap
        :return: the wrapped function returning json responses.
        """
        from functools import wraps
        from flask import Response
        import json
        logger.debug("func: {}".format(func))

        @wraps(func)
        def jsonify_inner(*args, **kwargs):
            try:
                result = func(*args, **kwargs)
            except:
                logger.exception("failed executing {name}.".format(name=func.__name__), exc_info=True)
                result = {"error": "exception raised"}, 503
            # end def
            status = None  # will be 200 if not otherwise changed
            if isinstance(result, tuple):
                response, status = result
            else:
                response = result
            # end if
            if isinstance(response, Response):
                if status:
                    response.status_code = status
                # end if
                return response
            # end if
            if isinstance(response, TgBotApiObject):
                response = response.to_array()
            # end if
            response = json.dumps(response)
            # end if
            assert isinstance(response, str)
            response_kwargs = {}
            response_kwargs.setdefault("mimetype", "text/json")
            if status:
                response_kwargs["status"] = status
            # end if
            res = Response(self.hide_api_key(response), **response_kwargs)
            logger.debug("returning: {}".format(res))
            return res
        # end def inner
        return jsonify_inner
    # end def

    @_self_jsonify
    def view_exec(self, api_key, command):
        """
        Issue commands. E.g. /exec/TELEGRAM_API_KEY/getMe

        :param api_key: gets checked, so you can't just execute commands.
        :param command: the actual command
        :return:
        """
        if api_key != self._api_key:
            error_msg = "Wrong API key: {wrong_key!r}".format(wrong_key=api_key)
            logger.warning(error_msg)
            return {"status": "error", "message": error_msg, "error_code": 403}, 403
        # end if
        from flask import request
        from pytgbot.exceptions import TgApiServerException
        logger.debug("COMMAND: {cmd}, ARGS: {args}".format(cmd=command, args=request.args))
        try:
            res = self.bot.do(command, **request.args)
            if self._return_python_objects:
                return res.to_array()
            else:
                return res
            # end if
        except TgApiServerException as e:
            return {"status": "error", "message": e.description, "error_code": e.error_code}, e.error_code
        # end try
    # end def

    @_self_jsonify
    def view_status(self):
        """
        Returns the status about the bot's webhook.

        :return: webhook info
        """
        try:
            res = self.bot.get_webhook_info()  # TODO: fix to work with return_python_objects==False
            return res.to_array()
        except TgApiServerException as e:
            return {"status": "error", "message": e.description, "error_code": e.error_code}, e.error_code
        # end try

    @_self_jsonify
    def view_updates(self):
        """
        This processes incoming telegram updates.

        :return:
        """
        from pprint import pformat
        from flask import request

        logger.debug("INCOME:\n{}\n\nHEADER:\n{}".format(
            pformat(request.get_json()),
            request.headers if hasattr(request, "headers") else None
        ))
        update = TGUpdate.from_array(request.get_json())
        try:
            result = self.process_update(update)
        except Exception as e:
            logger.exception("process_update()")
            result = {"status": "error", "message": str(e)}
        result = result if result else {"status": "probably ok"}
        logger.info("returning result: {}".format(result))
        return result
    # end def

    @_self_jsonify
    def view_host_info(self):
        """
        Get infos about your host, like IP etc.
        :return:
        """
        import socket
        import requests
        info = requests.get('http://ipinfo.io').json()
        info["host"] = socket.gethostname()
        info["version"] = self.VERSION
        return info
    # end def

    @_self_jsonify
    def view_routes_info(self):
        """
        Get infos about your host, like IP etc.
        :return:
        """
        from werkzeug.routing import Rule
        routes = []
        for rule in self.app.url_map.iter_rules():
            assert isinstance(rule, Rule)
            routes.append({
                'methods': list(rule.methods),
                'rule': rule.rule,
                'endpoint': rule.endpoint,
                'subdomain': rule.subdomain,
                'redirect_to': rule.redirect_to,
                'alias': rule.alias,
                'host': rule.host,
                'build_only': rule.build_only
            })
        # end for
        return routes
    # end def

    @_self_jsonify
    def view_request(self):
        """
        Get infos about your host, like IP etc.
        :return:
        """
        import json
        from flask import session
        j = json.loads(json.dumps(session)),
        # end for
        return j
    # end def

    def get_router(self):
        """
        Where to call `add_url_rule` (aka. `@route`) on.
        Returns either the blueprint if there is any, or the app.

        :raises ValueError: if neither blueprint nor app is set.

        :returns: either the blueprint if it is set, or the app.
        :rtype: flask.Blueprint | flask.Flask
        """
        if self.blueprint:
            return self.blueprint
        # end if
        if not self.app:
            raise ValueError("The app (self.app) is not set.")
        # end if
        return self.app

    def setup_routes(self, hookpath, debug_routes=False):
        """
        Sets the pathes to the registered blueprint/app:
            - "webhook"  (self.view_updates) at hookpath
        Also, if `debug_routes` is `True`:
            - "exec"     (self.view_exec)        at "/teleflask_debug/exec/API_KEY/<command>"  (`API_KEY` is replaced, `<command>` is any Telegram API command.)
            - "status"   (self.view_status)      at "/teleflask_debug/status"
            - "hostinfo" (self.view_host_info)   at "/teleflask_debug/hostinfo"
            - "routes"   (self.view_routes_info) at "/teleflask_debug/routes"

        :param hookpath: The path where it expects telegram updates to hit the flask app/blueprint.
        :type  hookpath: str

        :param debug_routes: Add several debug paths.
        :type  debug_routes: bool
        """
        # Todo: Find out how to handle blueprints
        if not self.app and not self.blueprint:
            raise ValueError("No app (self.app) or Blueprint (self.blueprint) was set.")
        # end if
        router = self.get_router()
        if not self.disable_setting_webhook_route:
            logger.info("Adding webhook route: {url!r}".format(url=hookpath))
            assert hookpath
            router.add_url_rule(hookpath, endpoint="webhook", view_func=self.view_updates, methods=['POST'])
        else:
            logger.info("Not adding webhook route, because disable_setting_webhook=True")
        # end if
        if debug_routes:
            logger.info("Adding debug routes.".format(url=hookpath))
            router.add_url_rule("/teleflask_debug/exec/{api_key}/<command>".format(api_key=self._api_key), endpoint="exec", view_func=self.view_exec)
            router.add_url_rule("/teleflask_debug/status", endpoint="status", view_func=self.view_status)
            router.add_url_rule("/teleflask_debug/routes", endpoint="routes", view_func=self.view_routes_info)
        # end if
    # end def

    @abc.abstractmethod
    def process_update(self, update):
        return
    # end def

    def process_result(self, update, result):
        """
        Send the result.
        It may be a :class:`Message` or a list of :class:`Message`s
        Strings will be send as :class:`TextMessage`, encoded as raw text.

        :param update: A telegram incoming update
        :type  update: TGUpdate

        :param result: Something to send.
        :type  result: Union[List[Union[Message, str]], Message, str]

        :return: List of telegram responses.
        :rtype: list
        """
        from ..messages import Message
        from ..new_messages import SendableMessageBase
        reply_chat, reply_msg = self.msg_get_reply_params(update)
        if isinstance(result, (SendableMessageBase, Message, str, list, tuple)):
            return list(self.send_messages(result, reply_chat, reply_msg))
        elif result is False or result is None:
            logger.debug("Ignored result {res!r}".format(res=result))
            # ignore it
        else:
            logger.warning("Unexpected plugin result: {type}".format(type=type(result)))
        # end if
    # end def

    @staticmethod
    def msg_get_reply_params(update):
        """
        Builds the `reply_chat` (chat id) and `reply_msg` (message id) values needed for `Message.send(...)` from an telegram `pytgbot` `Update` instance.

        :param update: pytgbot.api_types.receivable.updates.Update
        :return: reply_chat, reply_msg
        :rtype: tuple(int,int)
        """
        assert_type_or_raise(update, TGUpdate, parameter_name="update")
        assert isinstance(update, TGUpdate)

        if update.message and update.message.migrate_to_chat_id:
            return update.message.migrate_to_chat_id, update.message.message_id
        # end if
        if update.message and update.message.chat.id and update.message.message_id:
            return update.message.chat.id, update.message.message_id
        # end if
        if update.channel_post and update.channel_post.chat.id and update.channel_post.message_id:
            return update.channel_post.chat.id, update.channel_post.message_id
        # end if
        if update.edited_message and update.edited_message.chat.id and update.edited_message.message_id:
            return update.edited_message.chat.id, update.edited_message.message_id
        # end if
        if update.edited_channel_post and update.edited_channel_post.chat.id and update.edited_channel_post.message_id:
            return update.edited_channel_post.chat.id, update.edited_channel_post.message_id
        # end if
        if update.callback_query and update.callback_query.message:
            message_id = update.callback_query.message.message_id if update.callback_query.message.message_id else None
            if update.callback_query.message.chat and update.callback_query.message.chat.id:
                return update.callback_query.message.chat.id, message_id
            # end if
            if update.callback_query.message.from_peer and update.callback_query.message.from_peer.id:
                return update.callback_query.message.from_peer.id, message_id
            # end if
        # end if
        if update.inline_query and update.inline_query.from_peer and update.inline_query.from_peer.id:
            return update.inline_query.from_peer.id, None
        # end if
        return None, None
    # end def

    def send_messages(self, messages, reply_chat, reply_msg):
        """
        Sends a Message.
        Plain strings will become an unformatted TextMessage.
        Supports to mass send lists, tuples, Iterable.

        :param messages: A Message object.
        :type  messages: Message | str | list | tuple |
        :param reply_chat: chat id
        :type  reply_chat: int
        :param reply_msg: message id
        :type  reply_msg: int
        :param instant: Send without waiting for the plugin's function to be done. True to send as soon as possible.
        False or None to wait until the plugin's function is done and has returned, messages the answers in a bulk.
        :type  instant: bool or None
        """
        from pytgbot.exceptions import TgApiException
        from ..messages import Message, TextMessage
        from ..new_messages import SendableMessageBase

        logger.debug("Got {}".format(messages))
        if not isinstance(messages, (SendableMessageBase, Message, str, list, tuple)):
            raise TypeError("Is not a Message type (or str or tuple/list).")
        # end if
        if isinstance(messages, tuple):
            messages = [x for x in messages]
        # end if
        if not isinstance(messages, list):
            messages = [messages]
        # end if
        assert isinstance(messages, list)
        for msg in messages:
            if isinstance(msg, str):
                assert not isinstance(messages, str)  # because we would split a string to pieces.
                msg = TextMessage(msg, parse_mode="text")
            # end if
            if not isinstance(msg, (Message, SendableMessageBase)):
                raise TypeError("Is not a Message/SendableMessageBase type.")
            # end if
            # if msg._next_msg:  # TODO: Reply message?
            #     message.insert(message.index(msg) + 1, msg._next_msg)
            #     msg._next_msg = None
            from requests.exceptions import RequestException
            msg._apply_update_receiver(receiver=reply_chat, reply_id=reply_msg)
            try:
                yield msg.send(self.bot)
            except (TgApiException, RequestException):
                logger.exception("Manager failed messages. Message was {msg!s}".format(msg=msg))
            # end try
        # end for
    # end def

    def send_message(self, messages, reply_chat, reply_msg):
        """
        Backwards compatible version of send_messages.

        :param messages:
        :param reply_chat: chat id
        :type  reply_chat: int
        :param reply_msg: message id
        :type  reply_msg: int
        :return: None
        """
        list(self.send_messages(messages, reply_chat=reply_chat, reply_msg=reply_msg))
        return None