telegram_send
telegram-send
Telegram-send is a command-line tool to send messages and files over Telegram to your account, to a group or to a channel. It provides a simple interface that can be easily called from other programs.
Table of Contents
Usage
To send a message:
telegram-send "Hello, World!"
There is a maximum message length of 4096 characters, larger messages will be automatically split up into smaller ones and sent separately.
To send a message using Markdown or HTML formatting:
telegram-send --format markdown "Only the *bold* use _italics_"
telegram-send --format html "<pre>fixed-width messages</pre> are <i>also</i> supported"
telegram-send --format markdown "||Do good and find good\!||" # spoiler
Note that not all Markdown syntax or all HTML tags are supported. For more information on supported formatting, see the formatting options. We use the MarkdownV2 style for Markdown.
The --pre flag formats messages as fixed-width text:
telegram-send --pre "monospace"
To send a message without link previews:
telegram-send --disable-web-page-preview "https://github.com/rahiel/telegram-send"
To send a message from stdin:
printf 'With\nmultiple\nlines' | telegram-send --stdin
With this option you can send the output of any program.
To send a file (maximum file size of 50 MB) with an optional caption:
telegram-send --file quran.pdf --caption "The Noble Qur'an"
To send an image (maximum file size of 10 MB) with an optional caption:
telegram-send --image moon.jpg --caption "The Moon at Night"
To send a sticker:
telegram-send --sticker sticker.webp
To send a GIF or a soundless MP4 video (encoded as H.264/MPEG-4 AVC with a maximum file size of 50 MB) with an optional caption:
telegram-send --animation kitty.gif --caption "🐱"
To send an MP4 video (maximum file size of 50 MB) with an optional caption:
telegram-send --video birds.mp4 --caption "Singing Birds"
To send an audio file with an optional caption:
telegram-send --audio "Pachelbel's Canon.mp3" --caption "Johann Pachelbel - Canon in D"
To send a location via latitude and longitude:
telegram-send --location 35.5398033 -79.7488965
All captions can be optionally formatted with Markdown or html:
telegram-send --image moon.jpg --caption "The __Moon__ at *Night*" --format markdown
Telegram-send integrates into your file manager (Thunar, Nautilus and Nemo):

Installation
macOS
brew install pipx
pipx ensurepath
pipx install telegram-send
Linux
On Ubuntu/Debian:
sudo apt install telegram-send
On other Linux systems or if you need a newer version:
First Install the pipx package using your package manager.
Then run:
pipx ensurepath
pipx install telegram-send
And finally configure it with telegram-send --configure if you want to send to
your account, telegram-send --configure-group to send to a group or with
telegram-send --configure-channel to send to a channel.
Use the --config option to use multiple configurations. For example to set up
sending to a channel in a non-default configuration: telegram-send --config
channel.conf --configure-channel. Then always specify the config file to use
it: telegram-send --config channel.conf "Bismillah".
The -g option uses the global configuration at /etc/telegram-send.conf.
Configure it once: sudo telegram-send -g --configure and all users on the
system can send messages with this config: telegram-send -g "GNU". To use this
option you need to install telegram-send with sudo:
Install telegram-send system-wide with pip:
sudo pip3 install telegram-send
Examples
Here are some examples to get a taste of what is possible with telegram-send.
Alert on completion of shell commands
Receive an alert when long-running commands finish with the tg alias, based on
Ubuntu's built-in alert. Put the following in your ~/.bashrc:
alias tg='telegram-send "$([ $? = 0 ] && echo "" || echo "error: ") $(history|tail -n1|sed -e '\''s/^\s*[0-9]\+\s*//;s/[;&|]\s*tg$//'\'')"'
And then use it like sleep 10; tg. This will send you a message with the
completed command, in this case sleep 10.
What if you started a program and forgot to set the alert? Suspend the program
with Ctrl+Z and then enter fg; telegram-send "your message here".
To automatically receive notifications for long running commands, use ntfy with the Telegram backend.
Periodic messages with cron
We can combine telegram-send with cron to periodically send messages. Here we will set up a cron job to send the Astronomy Picture of the Day to the astropod channel.
Create a bot by talking to the BotFather, create a public channel and add
your bot as administrator to the channel. You will need to explicitly search for
your bot's username when adding it. Then run telegram-send --configure-channel
--config astropod.conf. We will use the apod.py script that gets the daily
picture and calls telegram-send to post it to the channel.
We create a cron job /etc/cron.d/astropod (as root) with the content:
SHELL=/bin/sh
PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin
# m h dom mon dow user command
0 1 * * * telegram ~/apod.py --config ~/astropod.conf
Make sure the file ends with a newline. Cron will then execute the script every
day at 1:00 as the user telegram. Join the astropod channel to see the
result.
Supervisor process state notifications
Supervisor controls and monitors processes. It can start processes at boot,
restart them if they fail and also report on their status. Supervisor-alert
is a simple plugin for Supervisor that sends messages on process state updates
to an arbitrary program. Using it with telegram-send (by using the --telegram
option), you can receive notifications whenever one of your processes exits.
Usage from Python
Because telegram-send is written in Python, you can use its functionality
directly from other Python programs: import telegram_send. Look at the
documentation.
Cron job output
Cron has a built-in feature to send the output of jobs via mail. In this example we'll send cron output over Telegram. Here is the example cron job:
SHELL=/bin/sh
PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin
# m h dom mon dow user command
0 * * * * rahiel chronic ~/script.bash 2>&1 | telegram-send -g --stdin
The command is chronic ~/script.bash 2>&1 | telegram-send -g --stdin. We run
the cron job with chronic, a tool from moreutils. Chronic makes sure that
a command produces no output unless it fails. No news is good news! If our
script fails, chronic passes the output through the pipe (|) to telegram-send.
We also send the output of stderr by redirecting stderr to stdout (2>&1).
Here we've installed telegram-send system-wide with sudo and use the global
configuration (-g) so telegram-send is usable in the cron job. Place the
cron job in /etc/cron.d/ and make sure the file ends with a newline. The
filename can't contain a . either.
ASCII pictures
Combining --stdin and --pre, we can send ASCII pictures:
ncal -bh | telegram-send --pre --stdin
apt-get moo | telegram-send --pre --stdin
Questions & Answers
How to use a proxy?
You can set a proxy with an environment variable:
HTTPS_PROXY=https://ip:port telegram-send "hello"
Within Python you can set the environment variable with:
os.environ["HTTPS_PROXY"] = "https://ip:port"
If you have a SOCKS proxy, you need to install support for it:
pip3 install pysocks
If you installed telegram-send with sudo, you also need to install pysocks
with sudo.
How to send the same message to multiple users?
First you configure telegram-send for every recipient you want to send messages to:
telegram-send --config user1.conf --configure
telegram-send --config group1.conf --configure-group
telegram-send --config group2.conf --configure-group
telegram-send --config channel1.conf --configure-channel
You will need all of the above config files. Now to send a message to all of the above configured recipients:
telegram-send --config user1.conf \
--config group1.conf \
--config group2.conf \
--config channel1.conf \
"Multicasting!"
How to get sticker files?
In Telegram Desktop you right click a sticker and choose "Save Image As...". You
can then send the saved webp file with telegram-send --sticker sticker.webp.
Other Questions
There are many answered questions and answers in the issue tracker: https://github.com/rahiel/telegram-send/issues?q=is%3Aissue%20state%3Aclosed%20label%3Aquestion
Uninstallation
telegram-send --clean
pipx uninstall telegram-send
Or uninstall with your package manager.
353async def configure(conf=None, channel=False, group=False, fm_integration=False): 354 """Guide user to set up the bot, saves configuration at `conf`. 355 356 # Arguments 357 358 - conf (str): Path where to save the configuration file. May contain `~` for 359 user's home. 360 - channel (bool, optional): Configure a channel. 361 - group (bool, optional): Configure a group. 362 - fm_integration (bool, optional): Setup file manager integration. 363 """ 364 conf = expanduser(conf) if conf else get_config_path() 365 prompt = "❯ " 366 contact_url = "https://telegram.me/" 367 root_topic_message = None 368 369 print(f"Talk with the {markup('BotFather', 'cyan')} on Telegram ({contact_url}BotFather), " 370 "create a bot and insert the token") 371 try: 372 token = input(markup(prompt, "magenta")).strip() 373 except UnicodeEncodeError: 374 # some users can only display ASCII 375 prompt = "> " 376 token = input(markup(prompt, "magenta")).strip() 377 378 try: 379 bot = telegram.Bot(token) 380 bot_details = await bot.get_me() 381 bot_name = bot_details.username 382 assert bot_name is not None 383 except Exception as e: 384 print(f"Error: {e}") 385 print(markup("Something went wrong, please try again.\n", "red")) 386 return await configure(conf, channel=channel, group=group, fm_integration=fm_integration) 387 388 print(f"Connected with {markup(bot_name, 'cyan')}.\n") 389 390 if channel: 391 print(f"Do you want to send to a {markup('public', 'bold')} or a {markup('private', 'bold')} channel? [pub/priv]") 392 channel_type = input(markup(prompt, "magenta")).strip() 393 if channel_type.startswith("pub"): 394 print( 395 "\nEnter your channel's public name or link: " 396 "\nExample: @username or https://t.me/username" 397 ) 398 chat_id = input(markup(prompt, "magenta")).strip() 399 if "/" in chat_id: 400 chat_id = "@" + chat_id.split("/")[-1] 401 elif chat_id.startswith("@"): 402 pass 403 else: 404 chat_id = "@" + chat_id 405 else: 406 print( 407 "\nOpen https://web.telegram.org/?legacy=1#/im in your browser, sign in and open your private channel." 408 "\nNow copy the URL in the address bar and enter it here:" 409 "\nExample: https://web.telegram.org/?legacy=1#/im?p=c1498081025_17886896740758033425" 410 ) 411 url = input(markup(prompt, "magenta")).strip() 412 match = re.match(r".+web\.(telegram|tlgr)\.org\/\?legacy=1#\/im\?p=c(?P<chat_id>\d+)_\d+", url) 413 if not match: 414 print(markup("Invalid URL.", "red")) 415 return await configure(conf, channel=channel, group=group, fm_integration=fm_integration) 416 chat_id = "-100" + match.group("chat_id") 417 418 authorized = False 419 while not authorized: 420 try: 421 await bot.send_chat_action(chat_id=chat_id, action="typing") 422 authorized = True 423 except (telegram.error.Forbidden, telegram.error.BadRequest): 424 # Telegram returns a BadRequest when a non-admin bot tries to send to a private channel 425 input(f"Please add {markup(bot_name, 'cyan')} as administrator to your channel and press Enter") 426 print(markup("\nCongratulations! telegram-send can now post to your channel!", "green")) 427 else: 428 password = "".join([str(randint(0, 9)) for _ in range(5)]) 429 bot_url = contact_url + bot_name 430 fancy_bot_name = markup(bot_name, "cyan") 431 if group: 432 password = f"/{password}@{bot_name}" 433 print(f"Please add {fancy_bot_name} to your group\nand send the following message to the group: " 434 f"{markup(password, 'bold')}\n") 435 else: 436 print(f"Please add {fancy_bot_name} on Telegram ({bot_url})\nand send it the password: " 437 f"{markup(password, 'bold')}\n") 438 439 update, update_id = None, None 440 441 async def get_user(): 442 updates = await bot.get_updates(offset=update_id, read_timeout=10) 443 for update in updates: 444 if update.message: 445 if update.message.text == password: 446 return update, None 447 if len(updates) > 0: 448 return None, updates[-1].update_id + 1 449 else: 450 return None, None 451 452 def get_root_topic_message(message: telegram.Message): 453 while message.reply_to_message is not None: 454 message = message.reply_to_message 455 456 if message.forum_topic_created is not None: 457 return message 458 return None 459 460 while update is None: 461 try: 462 update, update_id = await get_user() 463 except Exception as e: 464 print(f"Error! {e}") 465 466 chat_id = update.message.chat_id 467 user = update.message.from_user.username or update.message.from_user.first_name 468 469 if update.message.chat.is_forum: 470 root_topic_message = get_root_topic_message(update.message) 471 472 text = f"🎊 Congratulations {user}! 🎊\ntelegram-send is now ready for use!" 473 print(markup(text, "green")) 474 475 await bot.send_message( 476 chat_id=chat_id, 477 text=text, 478 reply_to_message_id=root_topic_message.message_id if isinstance(root_topic_message, telegram.Message) else None 479 ) 480 481 config = configparser.ConfigParser() 482 483 if root_topic_message is not None and isinstance(root_topic_message, telegram.Message): 484 config["telegram"] = {"TOKEN": token, "chat_id": chat_id, "reply_to_message_id": root_topic_message.message_id} 485 else: 486 config["telegram"] = {"TOKEN": token, "chat_id": chat_id} 487 conf_dir = dirname(conf) 488 if conf_dir: 489 makedirs(conf_dir, exist_ok=True) 490 with open(conf, "w") as f: 491 config.write(f) 492 if fm_integration: 493 if not sys.platform.startswith("win32"): 494 return integrate_file_manager()
Guide user to set up the bot, saves configuration at conf.
Arguments
- conf (str): Path where to save the configuration file. May contain
~for user's home. - channel (bool, optional): Configure a channel.
- group (bool, optional): Configure a group.
- fm_integration (bool, optional): Setup file manager integration.
327async def delete(message_ids, conf=None, timeout=30): 328 """Delete messages that have been sent before over Telegram. Restrictions given by Telegram API apply. 329 330 Note that Telegram restricts this to messages which have been sent during the last 48 hours. 331 https://python-telegram-bot.readthedocs.io/en/stable/telegram.bot.html#telegram.Bot.delete_message 332 333 # Arguments 334 335 - message_ids (list[str]): The messages ids of all messages to be deleted. 336 - conf (str): Path of configuration file to use. Will use the default config if not specified. 337 `~` expands to user's home directory. 338 - timeout (int|float): The read timeout for network connections in seconds. 339 """ 340 settings = get_config_settings(conf) 341 token = settings.token 342 chat_id = settings.chat_id 343 bot = telegram.Bot(token) 344 345 if message_ids: 346 for m in message_ids: 347 try: 348 await bot.delete_message(chat_id=chat_id, message_id=m, read_timeout=timeout) 349 except telegram.error.TelegramError as e: 350 warn(markup(f"Deleting message with id={m} failed: {e}", "red"))
Delete messages that have been sent before over Telegram. Restrictions given by Telegram API apply.
Note that Telegram restricts this to messages which have been sent during the last 48 hours. https://python-telegram-bot.readthedocs.io/en/stable/telegram.bot.html#telegram.Bot.delete_message
Arguments
- message_ids (list[str]): The messages ids of all messages to be deleted.
- conf (str): Path of configuration file to use. Will use the default config if not specified.
~expands to user's home directory. - timeout (int|float): The read timeout for network connections in seconds.
171async def send(*, 172 messages=None, files=None, images=None, stickers=None, animations=None, videos=None, audios=None, 173 captions=None, locations=None, conf=None, parse_mode=None, pre=False, silent=False, 174 disable_web_page_preview=False, timeout=30): 175 """Send data over Telegram. All arguments are optional. 176 177 Always use this function with explicit keyword arguments. So 178 `send(messages=["Hello!"])` instead of `send(["Hello!"])`. 179 180 The `file` type is the [file object][] returned by the `open()` function. 181 To send an image/file you open it in binary mode: 182 ``` python 183 import telegram_send 184 185 with open("image.jpg", "rb") as f: 186 telegram_send.send(images=[f]) 187 ``` 188 189 [file object]: https://docs.python.org/3/glossary.html#term-file-object 190 191 # Arguments 192 193 - conf (str): Path of configuration file to use. Will use the default config if not specified. 194 `~` expands to user's home directory. 195 - messages (list[str]): The messages to send. 196 - parse_mode (str): Specifies formatting of messages, one of `["text", "markdown", "html"]`. 197 - pre (bool): Send messages as preformatted fixed width (monospace) text. 198 - files (list[file]): The files to send. 199 - images (list[file]): The images to send. 200 - stickers (list[file]): The stickers to send. 201 - animations (list[file]): The animations to send. 202 - videos (list[file]): The videos to send. 203 - audios (list[file]): The audios to send. 204 - captions (list[str]): The captions to send with the images/files/animations/videos or audios. 205 - locations (list[str]): The locations to send. Locations are strings containing the latitude and longitude 206 separated by whitespace or a comma. 207 - silent (bool): Send silently without sound. 208 - disable_web_page_preview (bool): Disables web page previews for all links in the messages. 209 - timeout (int|float): The read timeout for network connections in seconds. 210 """ 211 settings = get_config_settings(conf) 212 token = settings.token 213 chat_id = settings.chat_id 214 reply_to_message_id = settings.reply_to_message_id 215 bot = telegram.Bot(token) 216 217 # We let the user specify "text" as a parse mode to be more explicit about 218 # the lack of formatting applied to the message, but "text" isn't a supported 219 # parse_mode in python-telegram-bot. Instead, set the parse_mode to None 220 # in this case. 221 if parse_mode == "text": 222 parse_mode = None 223 224 # collect all message ids sent during the current invocation 225 message_ids = [] 226 227 kwargs = { 228 "chat_id": chat_id, 229 "disable_notification": silent, 230 "read_timeout": timeout, 231 "reply_to_message_id": reply_to_message_id 232 } 233 234 if messages: 235 async def send_message(message, parse_mode): 236 if pre: 237 parse_mode = "html" 238 message = pre_format(message) 239 return await bot.send_message( 240 text=message, 241 parse_mode=parse_mode, 242 disable_web_page_preview=disable_web_page_preview, 243 **kwargs 244 ) 245 246 for m in messages: 247 if len(m) > MessageLimit.MAX_TEXT_LENGTH: 248 warn(markup( 249 f"Message longer than MAX_MESSAGE_LENGTH={MessageLimit.MAX_TEXT_LENGTH}, splitting into smaller messages.", 250 "red")) 251 ms = split_message(m, MessageLimit.MAX_TEXT_LENGTH) 252 for m in ms: 253 message_ids += [(await send_message(m, parse_mode))["message_id"]] 254 elif len(m) == 0: 255 continue 256 else: 257 message_ids += [(await send_message(m, parse_mode))["message_id"]] 258 259 def make_captions(items, captions): 260 # make captions equal length when not all images/files have captions 261 captions += [None] * (len(items) - len(captions)) 262 return zip(items, captions) 263 264 # kwargs for send methods with caption support 265 kwargs_caption = deepcopy(kwargs) 266 kwargs_caption["parse_mode"] = parse_mode 267 268 if files: 269 if captions: 270 for (f, c) in make_captions(files, captions): 271 message_ids += [await bot.send_document(document=f, caption=c, **kwargs_caption)] 272 else: 273 for f in files: 274 message_ids += [await bot.send_document(document=f, **kwargs)] 275 276 if images: 277 if captions: 278 for (i, c) in make_captions(images, captions): 279 message_ids += [await bot.send_photo(photo=i, caption=c, **kwargs_caption)] 280 else: 281 for i in images: 282 message_ids += [await bot.send_photo(photo=i, **kwargs)] 283 284 if stickers: 285 for i in stickers: 286 message_ids += [await bot.send_sticker(sticker=i, **kwargs)] 287 288 if animations: 289 if captions: 290 for (a, c) in make_captions(animations, captions): 291 message_ids += [await bot.send_animation(animation=a, caption=c, **kwargs_caption)] 292 else: 293 for a in animations: 294 message_ids += [await bot.send_animation(animation=a, **kwargs)] 295 296 if videos: 297 if captions: 298 for (v, c) in make_captions(videos, captions): 299 message_ids += [await bot.send_video(video=v, caption=c, supports_streaming=True, **kwargs_caption)] 300 else: 301 for v in videos: 302 message_ids += [await bot.send_video(video=v, supports_streaming=True, **kwargs)] 303 304 if audios: 305 if captions: 306 for (a, c) in make_captions(audios, captions): 307 message_ids += [await bot.send_audio(audio=a, caption=c, **kwargs_caption)] 308 else: 309 for a in audios: 310 message_ids += [await bot.send_audio(audio=a, **kwargs)] 311 312 if locations: 313 it = iter(locations) 314 for loc in it: 315 if "," in loc: 316 lat, lon = loc.split(",") 317 else: 318 lat = loc 319 lon = next(it) 320 message_ids += [await bot.send_location(latitude=float(lat), 321 longitude=float(lon), 322 **kwargs)] 323 324 return message_ids
Send data over Telegram. All arguments are optional.
Always use this function with explicit keyword arguments. So
send(messages=["Hello!"]) instead of send(["Hello!"]).
The file type is the file object returned by the open() function.
To send an image/file you open it in binary mode:
import telegram_send
with open("image.jpg", "rb") as f:
telegram_send.send(images=[f])
Arguments
- conf (str): Path of configuration file to use. Will use the default config if not specified.
~expands to user's home directory. - messages (list[str]): The messages to send.
- parse_mode (str): Specifies formatting of messages, one of
["text", "markdown", "html"]. - pre (bool): Send messages as preformatted fixed width (monospace) text.
- files (list[file]): The files to send.
- images (list[file]): The images to send.
- stickers (list[file]): The stickers to send.
- animations (list[file]): The animations to send.
- videos (list[file]): The videos to send.
- audios (list[file]): The audios to send.
- captions (list[str]): The captions to send with the images/files/animations/videos or audios.
- locations (list[str]): The locations to send. Locations are strings containing the latitude and longitude separated by whitespace or a comma.
- silent (bool): Send silently without sound.
- disable_web_page_preview (bool): Disables web page previews for all links in the messages.
- timeout (int|float): The read timeout for network connections in seconds.