Skip to content

Commit 09820e3

Browse files
author
Pietro Albini
committed
Add support for the HTML syntax
Now you can send messages with a subset of HTML in it, as you would do with Markdown. Automatic detection is also present. This commit also restructures the underlying code for the syntaxes.
1 parent df1aff0 commit 09820e3

File tree

8 files changed

+159
-52
lines changed

8 files changed

+159
-52
lines changed

botogram/objects/mixins.py

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
"""
88

99
from .. import utils
10+
from .. import syntaxes
1011

1112

1213
def _require_api(func):
@@ -30,12 +31,6 @@ def send(self, message, preview=True, reply_to=None, syntax=None,
3031
if hasattr(reply_to, "message_id"):
3132
reply_to = reply_to.message_id
3233

33-
# Use the correct syntax
34-
if syntax is None:
35-
syntax = "markdown" if utils.is_markdown(message) else "plain"
36-
elif syntax not in ("plain", "markdown"):
37-
raise ValueError("Invalid syntax type: %s")
38-
3934
# Get the correct chat_id
4035
chat_id = self.username if self.type == "channel" else self.id
4136

@@ -46,8 +41,10 @@ def send(self, message, preview=True, reply_to=None, syntax=None,
4641
args["reply_to_message_id"] = reply_to
4742
if extra is not None:
4843
args["reply_markup"] = extra.serialize()
49-
if syntax == "markdown":
50-
args["parse_mode"] = "Markdown"
44+
45+
syntax = syntaxes.guess_syntax(message, syntax)
46+
if syntax is not None:
47+
args["parse_mode"] = syntax
5148

5249
self._api.call("sendMessage", args)
5350

botogram/syntaxes.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
"""
2+
botogram.syntaxes
3+
Definition of the different message syntaxes
4+
5+
Copyright (c) 2016 Pietro Albini <[email protected]>
6+
Released under the MIT license
7+
"""
8+
9+
import re
10+
11+
12+
_markdown_re = re.compile(r"("
13+
r"\*(.*)\*|"
14+
r"_(.*)_|"
15+
r"\[(.*)\]\((.*)\)|"
16+
r"`(.*)`|"
17+
r"```(.*)```"
18+
r")")
19+
20+
_html_re = re.compile(r"("
21+
r"<b>(.*)<\/b>|"
22+
r"<strong>(.*)<\/strong>|"
23+
r"<i>(.*)<\/i>|"
24+
r"<em>(.*)<\/em>|"
25+
r"<a\shref=\"(.*)\">(.*)<\/a>|"
26+
r"<code>(.*)<\/code>|"
27+
r"<pre>(.*)<\/pre>"
28+
r")")
29+
30+
31+
def is_markdown(message):
32+
"""Check if a string is actually markdown"""
33+
return bool(_markdown_re.match(message))
34+
35+
36+
def is_html(message):
37+
"""Check if a string is actually HTML"""
38+
return bool(_html_re.match(message))
39+
40+
41+
def guess_syntax(message, provided):
42+
"""Guess the right syntax for a message"""
43+
if provided is None:
44+
if is_markdown(message):
45+
return "Markdown"
46+
elif is_html(message):
47+
return "HTML"
48+
else:
49+
return None
50+
51+
if provided in ("md", "markdown", "Markdown"):
52+
return "Markdown"
53+
elif provided in ("html", "HTML"):
54+
return "HTML"
55+
else:
56+
raise ValueError("Invalid syntax: %s" % provided)

botogram/utils.py

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,6 @@
2121
_command_re = re.compile(r"^\/[a-zA-Z0-9_]+(\@[a-zA-Z0-9_]{5}[a-zA-Z0-9_]*)?$")
2222
_email_re = re.compile(r"[a-zA-Z0-9_\.\+\-]+\@[a-zA-Z0-9_\.\-]+\.[a-zA-Z]+")
2323

24-
_markdown_re = re.compile(r"(\*(.*)\*|_(.*)_|\[(.*)\]\((.*)\)|`(.*)`|"
25-
r"```(.*)```)")
26-
2724
# This small piece of global state will track if logbook was configured
2825
_logger_configured = False
2926

@@ -133,11 +130,6 @@ def usernames_in(message):
133130
return results
134131

135132

136-
def is_markdown(string):
137-
"""Check if a string is actually markdown"""
138-
return bool(_markdown_re.match(string))
139-
140-
141133
def get_language(lang):
142134
"""Get the GNUTranslations instance of a specific language"""
143135
path = pkg_resources.resource_filename("botogram", "i18n/%s.mo" % lang)

docs/api/bot.rst

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -296,11 +296,9 @@ components.
296296
* :py:class:`botogram.ReplyKeyboardHide`
297297
* :py:class:`botogram.ForceReply`
298298

299-
The syntax parameter contains how the message should be processed by
300-
Telegram, and it can be either ``plain`` (no syntax) or ``markdown``. If
301-
you don't provide it, botogram will try to guess which syntax to use by
302-
parsing the message you want to send. This feature is not supported by
303-
all the Telegram clients.
299+
The *syntax* parameter is for defining how the message text should be
300+
processed by Telegram (:ref:`learn more about rich formatting
301+
<tricks-messages-syntax>`).
304302

305303
:param int chat: The ID of the chat which should receive the message.
306304
:param str messgae: The message you want to send.

docs/api/telegram.rst

Lines changed: 12 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -68,13 +68,10 @@ Available Classes
6868
from generating a *preview* for any link included in the message. If the
6969
message you are sending is in reply to another, set *reply_to* to the ID
7070
of the other :py:class:`~botogram.Message`. The *syntax* parameter is for
71-
defining how the message text should be processed by Telegram. Set it to
72-
either ``plain`` (no syntax) or ``markdown`` (see Telegram's current
73-
`Markdown support`_). By default botogram will try to guess which syntax
74-
to use by parsing the message. This feature is not supported by all app
75-
clients. *extra* is an optional object which specifies additional reply
76-
interface options on the recipient's end, and can be one of the following
77-
types:
71+
defining how the message text should be processed by Telegram
72+
(:ref:`learn more about rich formatting <tricks-messages-syntax>`).
73+
*extra* is an optional object which specifies additional reply interface
74+
options on the recipient's end, and can be one of the following types:
7875

7976
* :py:class:`botogram.ReplyKeyboardMarkup`
8077
* :py:class:`botogram.ReplyKeyboardHide`
@@ -204,13 +201,10 @@ Available Classes
204201
from generating a *preview* for any link included in the message. If the
205202
message you are sending is in reply to another, set *reply_to* to the ID
206203
of the other :py:class:`~botogram.Message`. The *syntax* parameter is for
207-
defining how the message text should be processed by Telegram. Set it to
208-
either ``plain`` (no syntax) or ``markdown`` (see Telegram's current
209-
`Markdown support`_). By default botogram will try to guess which syntax
210-
to use by parsing the message. This feature is not supported by all app
211-
clients. *extra* is an optional object which specifies additional reply
212-
interface options on the recipient's end, and can be one of the following
213-
types:
204+
defining how the message text should be processed by Telegram
205+
(:ref:`learn more about rich formatting <tricks-messages-syntax>`).
206+
*extra* is an optional object which specifies additional reply interface
207+
options on the recipient's end, and can be one of the following types:
214208

215209
* :py:class:`botogram.ReplyKeyboardMarkup`
216210
* :py:class:`botogram.ReplyKeyboardHide`
@@ -492,12 +486,10 @@ Available Classes
492486
Reply with the textual *message* in regards to this message. You may
493487
optionally stop clients from generating a *preview* for any link included
494488
in the reply. The *syntax* parameter is for defining how the message text
495-
should be processed by Telegram. Set it to either ``plain`` (no syntax) or
496-
``markdown`` (see Telegram's current `Markdown support`_). By default
497-
botogram will try to guess which syntax to use by parsing the message.
498-
This feature is not supported by all app clients. *extra* is an optional
499-
object which specifies additional reply interface options on the
500-
recipient's end, and can be one of the following types:
489+
should be processed by Telegram (:ref:`learn more about rich formatting
490+
<tricks-messages-syntax>`). *extra* is an optional object which
491+
specifies additional reply interface options on the recipient's end, and
492+
can be one of the following types:
501493

502494
* :py:class:`botogram.ReplyKeyboardMarkup`
503495
* :py:class:`botogram.ReplyKeyboardHide`
@@ -1048,4 +1040,3 @@ Available Classes
10481040
.. _Telegram's Bot API: https://core.telegram.org/bots/api
10491041
.. _API methods: https://core.telegram.org/bots/api#available-methods
10501042
.. _API types: https://core.telegram.org/bots/api#available-types
1051-
.. _Markdown support: https://core.telegram.org/bots/api#using-markdown

docs/tricks.rst

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,3 +44,37 @@ botogram, without being directly provided by the decorator:
4444

4545
* **shared**, which is an instance of the bot's
4646
:ref:`shared memory <shared-memory>`.
47+
48+
.. _tricks-messages-syntax:
49+
50+
=====================================
51+
Rich formatting with message syntaxes
52+
=====================================
53+
54+
Plain text messages can be boring for your users, and also hard to read if
55+
those messages are full with information. Because of that, Telegram allows bots
56+
to use rich formatting in their messages. Currently Telegram only supports `a
57+
subset of`_ Markdown and HTML.
58+
59+
In order to use rich formatting in your messages you don't need to do anything:
60+
botogram is smart enough to detect when a message uses rich formatting, and the
61+
used syntax. If for whatever reason that detection fails, you can specify the
62+
syntax you're using by providing it to the ``syntax`` parameter of the
63+
:py:meth:`~botogram.Chat.send` method:
64+
65+
.. code-block:: python
66+
67+
chat.send("*This is Markdown!*", syntax="markdown")
68+
69+
That parameter accepts the following values:
70+
71+
* ``markdown``, or its aliases ``md`` and ``Markdown``
72+
* ``html``, or its alias ``HTML``
73+
74+
.. note::
75+
76+
Support for rich formatting depends on your users' Telegram client. If
77+
they're using the official ones there are no problems, but that might work
78+
on unofficial clients.
79+
80+
.. _a subset of: https://core.telegram.org/bots/api#formatting-options

tests/test_syntaxes.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
"""
2+
Tests for botogram/syntaxes.py
3+
4+
Copyright (c) 2016 Pietro Albini <[email protected]>
5+
Released under the MIT license
6+
"""
7+
8+
import pytest
9+
10+
import botogram.syntaxes
11+
12+
13+
def test_is_markdown():
14+
assert not botogram.syntaxes.is_markdown("not markdown, sorry!")
15+
assert not botogram.syntaxes.is_markdown("*a [wonderfully](broken syntax`")
16+
17+
for delimiter in "*", "_", "`", "```":
18+
assert botogram.syntaxes.is_markdown(delimiter+"a"+delimiter)
19+
20+
assert botogram.syntaxes.is_markdown("[a](b)")
21+
22+
23+
def test_is_html():
24+
assert not botogram.syntaxes.is_html("not HTML, sorry!")
25+
assert not botogram.syntaxes.is_html("<a some </really> <b>roken html</i>")
26+
27+
for tag in "b", "strong", "i", "em", "pre", "code":
28+
assert botogram.syntaxes.is_html("<"+tag+">a</"+tag+">")
29+
30+
assert not botogram.syntaxes.is_html("<a>a</a>")
31+
assert not botogram.syntaxes.is_html("<a test=\"b\">a</a>")
32+
assert botogram.syntaxes.is_html("<a href=\"b\">a</a>")
33+
34+
35+
def test_guess_syntax():
36+
# Provided syntax name
37+
for name in ("md", "markdown", "Markdown"):
38+
assert botogram.syntaxes.guess_syntax("", name) == "Markdown"
39+
40+
for name in ("html", "HTML"):
41+
assert botogram.syntaxes.guess_syntax("", name) == "HTML"
42+
43+
with pytest.raises(ValueError):
44+
botogram.syntaxes.guess_syntax("", "invalid")
45+
46+
# Let's guess it
47+
assert botogram.syntaxes.guess_syntax("no syntax, sorry!", None) is None
48+
assert botogram.syntaxes.guess_syntax("*markdown*", None) == "Markdown"
49+
assert botogram.syntaxes.guess_syntax("<b>html</b>", None) == "HTML"

tests/test_utils.py

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -63,13 +63,3 @@ def test_usernames_in():
6363

6464
username_url = botogram.utils.usernames_in("http://pwd:[email protected]")
6565
assert username_url == []
66-
67-
68-
def test_is_markdown():
69-
assert not botogram.utils.is_markdown("not markdown, sorry!")
70-
assert not botogram.utils.is_markdown("*all [wonderfully](broken syntax`")
71-
assert botogram.utils.is_markdown("*a*")
72-
assert botogram.utils.is_markdown("_a_")
73-
assert botogram.utils.is_markdown("[a](b)")
74-
assert botogram.utils.is_markdown("`a`")
75-
assert botogram.utils.is_markdown("```a```")

0 commit comments

Comments
 (0)