telegram_send

telegram-send

Version pyversions Downloads License Debian package Ubuntu Package Version

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.

1"""
2.. include:: ../README.md
3"""
4from .version import __version__
5from .telegram_send import configure, delete, send
6
7
8__all__ = ["configure", "delete", "send", "__version__"]
async def configure(conf=None, channel=False, group=False, fm_integration=False):
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.
async def delete(message_ids, conf=None, timeout=30):
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.
async def send( *, messages=None, files=None, images=None, stickers=None, animations=None, videos=None, audios=None, captions=None, locations=None, conf=None, parse_mode=None, pre=False, silent=False, disable_web_page_preview=False, timeout=30):
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.
__version__ = '0.39.2'