From d4f3bdde817c58037261d16a7ef9c99575fc9bf3 Mon Sep 17 00:00:00 2001 From: Pietro Albini Date: Tue, 6 Oct 2015 20:04:40 +0200 Subject: [PATCH] Implement markdown support for botogram This also detects automatically if you're using markdown in your messages, so you don't have to specify it everytime you use it. Please note that markdown support isn't implemented yet in every Telegram client, but (currently) only on the Android and web clients. --- botogram/frozenbot.py | 5 +++-- botogram/objects/mixins.py | 17 ++++++++++++++--- botogram/utils.py | 7 +++++++ docs/api/bot.rst | 9 ++++++++- docs/api/telegram.rst | 18 ++++++++++++++++-- tests/test_utils.py | 10 ++++++++++ 6 files changed, 58 insertions(+), 8 deletions(-) diff --git a/botogram/frozenbot.py b/botogram/frozenbot.py index 78072ca..67afabc 100644 --- a/botogram/frozenbot.py +++ b/botogram/frozenbot.py @@ -118,10 +118,11 @@ def command(self, name): # Those are shortcuts to send messages directly to someone - def send(self, chat, message, preview=True, reply_to=None, extra=None): + def send(self, chat, message, preview=True, reply_to=None, syntax=None, + extra=None): """Send a message in a chat""" obj = objects.GenericChat({"id": chat}, self.api) - obj.send(message, preview, reply_to, extra) + obj.send(message, preview, reply_to, syntax, extra) def send_photo(self, chat, path, caption="", reply_to=None, extra=None): """Send a photo in a chat""" diff --git a/botogram/objects/mixins.py b/botogram/objects/mixins.py index b2a9fd5..fc2bc1f 100644 --- a/botogram/objects/mixins.py +++ b/botogram/objects/mixins.py @@ -8,6 +8,8 @@ import functools +from .. import utils + def _require_api(func): """Decorator which forces to have the api on an object""" @@ -23,12 +25,19 @@ class ChatMixin: """Add some methods for chats""" @_require_api - def send(self, message, preview=True, reply_to=None, extra=None): + def send(self, message, preview=True, reply_to=None, syntax=None, + extra=None): """Send a message""" # Convert instance of Message to ids in reply_to if hasattr(reply_to, "message_id"): reply_to = reply_to.message_id + # Use the correct syntax + if syntax is None: + syntax = "markdown" if utils.is_markdown(message) else "plain" + elif syntax not in ("plain", "markdown"): + raise ValueError("Invalid syntax type: %s") + # Build API call arguments args = {"chat_id": self.id, "text": message, "disable_web_page_preview": not preview} @@ -36,6 +45,8 @@ def send(self, message, preview=True, reply_to=None, extra=None): args["reply_to_message_id"] = reply_to if extra is not None: args["reply_markup"] = extra.serialize() + if syntax == "markdown": + args["parse_mode"] = "Markdown" self._api.call("sendMessage", args) @@ -76,9 +87,9 @@ def forward_to(self, to): }) @_require_api - def reply(self, message, preview=True, extra=None): + def reply(self, message, preview=True, syntax=None, extra=None): """Reply to the current message""" - self.chat.send(message, preview, self.message_id, extra) + self.chat.send(message, preview, self.message_id, syntax, extra) def reply_with_photo(self, path, caption, extra): """Reply with a photo to the current message""" diff --git a/botogram/utils.py b/botogram/utils.py index a7a43e0..224d2b1 100644 --- a/botogram/utils.py +++ b/botogram/utils.py @@ -18,6 +18,8 @@ _command_re = re.compile(r"^\/[a-zA-Z0-9_]+(\@[a-zA-Z0-9_]{5}[a-zA-Z0-9_]*)?$") _email_re = re.compile(r"[a-zA-Z0-9_\.\+\-]+\@[a-zA-Z0-9_\.\-]+\.[a-zA-Z]+") +_markdown_re = re.compile(r"(\*(.*)\*|_(.*)_|\[(.*)\]\((.*)\)|`(.*)`|" + r"```(.*)```)") # This small piece of global state will track if logbook was configured _logger_configured = False @@ -82,6 +84,11 @@ def usernames_in(message): return results +def is_markdown(string): + """Check if a string is actually markdown""" + return bool(_markdown_re.match(string)) + + def get_language(lang): """Get the GNUTranslations instance of a specific language""" path = pkg_resources.resource_filename("botogram", "i18n/%s.mo" % lang) diff --git a/docs/api/bot.rst b/docs/api/bot.rst index d8cb4ec..d8a6fe0 100644 --- a/docs/api/bot.rst +++ b/docs/api/bot.rst @@ -235,7 +235,7 @@ components. :return: A frozen instance of the current bot. - .. py:method:: send(chat, message[, preview=True, reply_to=None, extra=None]) + .. py:method:: send(chat, message[, preview=True, reply_to=None, syntax=None, extra=None]) This method sends a message to a specific chat. The chat must be identified by its ID, and Telegram applies some restrictions on the chats @@ -251,10 +251,17 @@ components. * :py:class:`botogram.ReplyKeyboardHide` * :py:class:`botogram.ForceReply` + The syntax parameter contains how the message should be processed by + Telegram, and it can be either ``plain`` (no syntax) or ``markdown``. If + you don't provide it, botogram will try to guess which syntax to use by + parsing the message you want to send. This feature is not supported by + all the Telegram clients. + :param int chat: The ID of the chat which should receive the message. :param str messgae: The message you want to send. :param bool preview: If you want to show the link preview. :param int reply_to: The ID of the message this one is replying to. + :param string syntax: The name of the syntax you used for the message. :param object extra: An extra object you want to attach (see above). .. py:method:: send_photo(chat, path[, caption="", reply_to=None, extra=None]) diff --git a/docs/api/telegram.rst b/docs/api/telegram.rst index 96276f9..b925f4c 100644 --- a/docs/api/telegram.rst +++ b/docs/api/telegram.rst @@ -36,7 +36,7 @@ Here you can see all the available classes and objects. *This attribute can be None if it's not provided by Telegram.* - .. py:method:: send(message, [preview=True, reply_to=None, extra=None]) + .. py:method:: send(message, [preview=True, reply_to=None, syntax=None, extra=None]) Send a message to the user. You can also define if a preview for links should be showed (yes by default), the message ID of the message this one @@ -47,9 +47,16 @@ Here you can see all the available classes and objects. * :py:class:`botogram.ReplyKeyboardHide` * :py:class:`botogram.ForceReply` + The syntax parameter contains how the message should be processed by + Telegram, and it can be either ``plain`` (no syntax) or ``markdown``. If + you don't provide it, botogram will try to guess which syntax to use by + parsing the message you want to send. This feature is not supported by + all the Telegram clients. + :param str message: The message you want to send :param bool preview: Show the link preview :param int reply_to: The ID of the message this one is replying to + :param string syntax: The name of the syntax you used for the message. :param object extra: An extra object you want to attach (see above) .. py:class:: botogram.GroupChat @@ -64,7 +71,7 @@ Here you can see all the available classes and objects. The title of the group chat - .. py:method:: send(message, [preview=True, reply_to=None, extra=None]) + .. py:method:: send(message, [preview=True, reply_to=None, syntax=None, extra=None]) Send a message to the group chat. You can also define if a preview for links should be showed (yes by default), the message ID of the message @@ -75,7 +82,14 @@ Here you can see all the available classes and objects. * :py:class:`botogram.ReplyKeyboardHide` * :py:class:`botogram.ForceReply` + The syntax parameter contains how the message should be processed by + Telegram, and it can be either ``plain`` (no syntax) or ``markdown``. If + you don't provide it, botogram will try to guess which syntax to use by + parsing the message you want to send. This feature is not supported by + all the Telegram clients. + :param str message: The message you want to send :param bool preview: Show the link preview :param int reply_to: The ID of the message this one is replying to + :param string syntax: The name of the syntax you used for the message. :param object extra: An extra object you want to attach (see above) diff --git a/tests/test_utils.py b/tests/test_utils.py index 3443048..16b1c1e 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -63,3 +63,13 @@ def test_usernames_in(): username_url = botogram.utils.usernames_in("http://pwd:john@example.com") assert username_url == [] + + +def test_is_markdown(): + assert not botogram.utils.is_markdown("not markdown, sorry!") + assert not botogram.utils.is_markdown("*all [wonderfully](broken syntax`") + assert botogram.utils.is_markdown("*a*") + assert botogram.utils.is_markdown("_a_") + assert botogram.utils.is_markdown("[a](b)") + assert botogram.utils.is_markdown("`a`") + assert botogram.utils.is_markdown("```a```")