-
Notifications
You must be signed in to change notification settings - Fork 1.6k
Sending more than just messages
The current version of the library, v1, does not yet implement some features in the friendly methods, such as spoilers in client.send_message. And it never might, as it increases the maintenance burden, but are not so commonly used. However, that doesn't mean the library can't do those things.
Stickers are nothing else than files, and when you successfully retrieve the stickers for a certain sticker set, all you will have are handles to these files. Remember, the files Telegram holds on their servers can be referenced through this pair of ID/hash (unique per user), and you need to use this handle when sending a "document" message. This working example will send yourself the very first sticker you have:
# Get all the sticker sets this user has
from telethon.tl.functions.messages import GetAllStickersRequest
sticker_sets = await client(GetAllStickersRequest(0))
# Choose a sticker set
from telethon.tl.functions.messages import GetStickerSetRequest
from telethon.tl.types import InputStickerSetID
sticker_set = sticker_sets.sets[0]
# Get the stickers for this sticker set
stickers = await client(GetStickerSetRequest(
stickerset=InputStickerSetID(
id=sticker_set.id, access_hash=sticker_set.access_hash
),
hash=0
))
# Stickers are nothing more than files, so send that
await client.send_file('me', stickers.documents[0])It works very similar to replying to a message. You need to specify the chat, message ID you wish to react to, and reaction, using :tl:SendReaction:
from telethon.tl.functions.messages import SendReactionRequest
await client(SendReactionRequest(
peer=chat,
msg_id=42,
reaction=[types.ReactionEmoji(
emoticon='❤️'
)]
))Note that you cannot use strings like :heart: for the reaction. You must use the desired emoji directly. You can most easily achieve this by copy-pasting the emoji from an official application such as Telegram Desktop.
If for some reason you cannot embed emoji directly into the code, you can also use its unicode escape (which you can find using websites like symbl.cc), or install a different package, like emoji:
# All of these work exactly the same (you only need one):
import emoji
reaction = emoji.emojize(':red_heart:')
reaction = '❤️'
reaction = '\u2764'
from telethon.tl.functions.messages import SendReactionRequest
await client(SendReactionRequest(
peer=chat,
msg_id=42,
reaction=[types.ReactionEmoji(
emoticon=reaction
)]
))Please make sure to check the help pages of the respective websites you use if you need a more in-depth explanation on how they work. Telethon only needs you to provide the emoji in some form. Some packages or websites can make this easier.
Telethon's v1 markdown parser does not offer a way to send spoiler (hidden text) or custom emoji. However, it's easy to add support for them.
Telethon's parse_mode supports using a custom object with parse and unparse functions to work. This means it's possible to leverage the current markdown implementation and extend it with custom functionality.
Copy the following code into your own:
from telethon.extensions import markdown
from telethon import types
class CustomMarkdown:
@staticmethod
def parse(text):
text, entities = markdown.parse(text)
for i, e in enumerate(entities):
if isinstance(e, types.MessageEntityTextUrl):
if e.url == 'spoiler':
entities[i] = types.MessageEntitySpoiler(e.offset, e.length)
elif e.url.startswith('emoji/'):
entities[i] = types.MessageEntityCustomEmoji(e.offset, e.length, int(e.url.split('/')[1]))
return text, entities
@staticmethod
def unparse(text, entities):
for i, e in enumerate(entities or []):
if isinstance(e, types.MessageEntityCustomEmoji):
entities[i] = types.MessageEntityTextUrl(e.offset, e.length, f'emoji/{e.document_id}')
if isinstance(e, types.MessageEntitySpoiler):
entities[i] = types.MessageEntityTextUrl(e.offset, e.length, 'spoiler')
return markdown.unparse(text, entities)This creates a custom class with parse and unparse. CustomMarkdown.parse uses markdown.parse (so it works just like the default markdown), but before returning, it scans the parsed text for the following inline URLs:
message = 'this is a [link text](spoiler) and [❤️](emoji/10002345) too'Here, the message contains a link text with URL spoiler. The above code will replace the URL with MessageEntitySpoiler. It also contains a URL with emoji/10002345, which will be replaced with MessageEntityCustomEmoji. Effectively sending those instead of the URL.
To use the class, you must change your client.parse_mode to it (be sure to use an instance, because the type is callable and the library would attempt to create an instance to parse it):
client.parse_mode = CustomMarkdown()Now, in your message text, you can use inline links which become spoilers and custom emoji! (Note that for custom emoji to work, the inline link text must be a normal emoji):
client.send_message('me', 'hello this is a [hidden text](spoiler), with custom emoji [❤️](emoji/10002345) !')You may have noticed the emoji URL is followed by a number. This number is a document_id. To find it, the easiest way is to send a message to your own chat with the premium emoji you want to use using an official client, and then use Telethon to print the message.entities. It will contain the document_id you need to use.
Full html parser/unparser example
A full example of html parser and unparser that have both spoiler and custom emoji.
you can use this code exactly like how would use the custom markdown parser mentioned above, just set parse_mode to CustomHtmlParser() and you are done.
"""
Simple HTML -> Telegram entity parser.
"""
from collections import deque
from html import escape
from html.parser import HTMLParser
from typing import Iterable, Tuple, List
from telethon.helpers import add_surrogate, del_surrogate, within_surrogate, strip_text
from telethon.tl import TLObject
from telethon.types import (
MessageEntityBold, MessageEntityItalic, MessageEntityCode,
MessageEntityPre, MessageEntityEmail, MessageEntityUrl,
MessageEntityTextUrl, MessageEntityMentionName,
MessageEntityUnderline, MessageEntityStrike, MessageEntityBlockquote,
MessageEntityCustomEmoji, MessageEntitySpoiler, TypeMessageEntity
)
class HTMLToTelegramParser(HTMLParser):
def __init__(self):
super().__init__()
self.text = ''
self.entities = []
self._building_entities = {}
self._open_tags = deque()
self._open_tags_meta = deque()
def handle_starttag(self, tag, attrs):
self._open_tags.appendleft(tag)
self._open_tags_meta.appendleft(None)
attrs = dict(attrs)
EntityType = None
args = {}
match tag:
case 'strong' | 'b':
EntityType = MessageEntityBold
case 'em' | 'i':
EntityType = MessageEntityItalic
case 'u':
EntityType = MessageEntityUnderline
case 'del' | 's':
EntityType = MessageEntityStrike
case 'blockquote':
EntityType = MessageEntityBlockquote
case 'tg-spoiler':
EntityType = MessageEntitySpoiler
case 'code':
try:
# If we're in the middle of a <pre> tag, this <code> tag is
# probably intended for syntax highlighting.
#
# Syntax highlighting is set with
# <code class='language-...'>codeblock</code>
# inside <pre> tags
pre = self._building_entities['pre']
try:
pre.language = attrs['class'][len('language-'):] # type: ignore
except KeyError:
pass
except KeyError:
EntityType = MessageEntityCode
case 'pre':
EntityType = MessageEntityPre
args['language'] = ''
case 'a':
try:
url = attrs['href']
if not url:
raise KeyError
except KeyError:
return
if url.startswith('mailto:'):
url = url[len('mailto:'):]
EntityType = MessageEntityEmail
else:
if self.get_starttag_text() == url:
EntityType = MessageEntityUrl
else:
EntityType = MessageEntityTextUrl
args['url'] = del_surrogate(url)
url = None
self._open_tags_meta.popleft()
self._open_tags_meta.appendleft(url)
case 'tg-emoji':
try:
emoji_id = attrs['emoji-id']
if not emoji_id:
raise ValueError
emoji_id = int(emoji_id)
except (KeyError, ValueError):
return
EntityType = MessageEntityCustomEmoji
args['document_id'] = emoji_id
if EntityType and tag not in self._building_entities:
self._building_entities[tag] = EntityType(
offset=len(self.text),
# The length will be determined when closing the tag.
length=0,
**args)
def handle_data(self, data):
previous_tag = self._open_tags[0] if len(self._open_tags) > 0 else ''
if previous_tag == 'a':
url = self._open_tags_meta[0]
if url:
data = url
for tag, entity in self._building_entities.items():
entity.length += len(data)
self.text += data
def handle_endtag(self, tag):
try:
self._open_tags.popleft()
self._open_tags_meta.popleft()
except IndexError:
pass
entity = self._building_entities.pop(tag, None)
if entity:
self.entities.append(entity)
ENTITY_TO_FORMATTER = {
MessageEntityBold: ('<strong>', '</strong>'),
MessageEntityItalic: ('<em>', '</em>'),
MessageEntityCode: ('<code>', '</code>'),
MessageEntityUnderline: ('<u>', '</u>'),
MessageEntityStrike: ('<del>', '</del>'),
MessageEntityBlockquote: ('<blockquote>', '</blockquote>'),
MessageEntitySpoiler: ('<tg-spoiler>', '</tg-spoiler>'),
MessageEntityPre: lambda e, _: (
"<pre>\n"
" <code class='language-{}'>\n"
" ".format(e.language), "{}\n"
" </code>\n"
"</pre>"
),
MessageEntityEmail: lambda _, t: ('<a href="mailto:{}">'.format(t), '</a>'),
MessageEntityUrl: lambda _, t: ('<a href="{}">'.format(t), '</a>'),
MessageEntityTextUrl: lambda e, _: ('<a href="{}">'.format(escape(e.url)), '</a>'),
MessageEntityMentionName: lambda e, _: ('<a href="tg://user?id={}">'.format(e.user_id), '</a>'),
MessageEntityCustomEmoji: lambda e, _: ('<tg-emoji emoji-id="{}">'.format(e.document_id), '</tg-emoji>')
}
class CustomHtmlParser:
@staticmethod
def parse(html: str) -> Tuple[str, List[TypeMessageEntity]]:
"""
Parses the given HTML message and returns its stripped representation
plus a list of the MessageEntity's that were found.
:param html: the message with HTML to be parsed.
:return: a tuple consisting of (clean message, [message entities]).
"""
if not html:
return html, []
parser = HTMLToTelegramParser()
parser.feed(add_surrogate(html))
text = strip_text(parser.text, parser.entities)
parser.entities.reverse()
parser.entities.sort(key=lambda entity: entity.offset)
return del_surrogate(text), parser.entities
@staticmethod
def unparse(text: str, entities: Iterable[TypeMessageEntity]) -> str:
"""
Performs the reverse operation to .parse(), effectively returning HTML
given a normal text and its MessageEntity's.
:param text: the text to be reconverted into HTML.
:param entities: the MessageEntity's applied to the text.
:return: a HTML representation of the combination of both inputs.
"""
if not text:
return text
elif not entities:
return escape(text)
if isinstance(entities, TLObject):
entities = (entities,) # type: ignore
text = add_surrogate(text)
insert_at = []
for i, entity in enumerate(entities):
s = entity.offset
e = entity.offset + entity.length
delimiter = ENTITY_TO_FORMATTER.get(type(entity), None) # type: ignore
if delimiter:
if callable(delimiter):
delimiter = delimiter(entity, text[s:e])
insert_at.append((s, i, delimiter[0]))
insert_at.append((e, -i, delimiter[1]))
insert_at.sort(key=lambda t: (t[0], t[1]))
next_escape_bound = len(text)
while insert_at:
# Same logic as markdown.py
at, _, what = insert_at.pop()
while within_surrogate(text, at):
at += 1
text = text[:at] + what + escape(text[at:next_escape_bound]) + text[next_escape_bound:]
next_escape_bound = at
text = escape(text[:next_escape_bound]) + text[next_escape_bound:]
return del_surrogate(text)example of sending message using custom html parser
client.send_message('me', 'hello this is a <tg-spoiler>hidden text</tg-spoiler>, with custom emoji <tg-emoji emoji-id="10002345">❤️</tg-emoji> !')