Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,28 @@ The `--pre` flag formats messages as fixed-width text:
telegram-send --pre "monospace"
```

To send a message to a specific chat id without changing the configuration:
``` shell
telegram-send --chat-id -1001234567890 "Hello"
```

To send a message to a chat id by alias (the mapping should be in the configuration):
``` shell
telegram-send --alias channel "Hello"
```
To use an alias, you must define an `[aliases]` block in the configuration file (`~/.config/telegram-send.conf` by default, or `%LOCALAPPDATA%\telegram-send\telegram-send.conf` on Windows), mapping the alias to a specific chat ID:
```ini
[aliases]
channel = -1001234567890
group = -1009876543210
```

To configure and use the bot token securely without storing it in a plain-text configuration file, you can set the `TELEGRAM_BOT_TOKEN` environment variable. If this is present, telegram-send will use it during `telegram-send --configure` and skip prompting you for the token. The token will not be saved into the configuration file.
``` shell
export TELEGRAM_BOT_TOKEN="123456789:ABCDEFGHIJKLMNOPQRSTUVWXYZ123456789"
telegram-send --configure
```

To send a message without link previews:
``` shell
telegram-send --disable-web-page-preview "https://github.com/rahiel/telegram-send"
Expand Down
96 changes: 69 additions & 27 deletions telegram_send/telegram_send.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import argparse
import asyncio
import configparser
import os
import re
import sys
from copy import deepcopy
Expand Down Expand Up @@ -93,6 +94,8 @@ async def run():
parser.add_argument("--timeout", help="Set the read timeout for network operations. (in seconds)",
type=float, default=30., action="store")
parser.add_argument("--version", action="version", version=f"%(prog)s {__version__}")
parser.add_argument("--chat-id", help="Target chat id to send messages to, bypassing the configured chat id", dest="chat_id")
parser.add_argument("--alias", help="Target chat alias to send messages to, bypassing the configured chat id. The mapping is stored in the configuration.", dest="alias")
args = parser.parse_args()

conf : list[str | None]
Expand Down Expand Up @@ -130,7 +133,7 @@ async def run():
args.message = [message] + args.message

try:
await delete(args.delete, conf=conf[0])
await delete(args.delete, conf=conf[0], chat_id=args.chat_id, alias=args.alias)
message_ids = []
for c in conf:
message_ids += await send(
Expand All @@ -148,7 +151,9 @@ async def run():
audios=args.audio,
captions=args.caption,
locations=args.location,
timeout=args.timeout
timeout=args.timeout,
chat_id=args.chat_id,
alias=args.alias
)
if args.showids and message_ids:
smessage_ids = [str(m) for m in message_ids]
Expand All @@ -170,7 +175,7 @@ async def run():
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):
disable_web_page_preview=False, timeout=30, chat_id=None, alias=None):
"""Send data over Telegram. All arguments are optional.

Always use this function with explicit keyword arguments. So
Expand Down Expand Up @@ -206,8 +211,10 @@ async def send(*,
- 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.
- chat_id (str): Target chat id to send messages to, bypassing the configured chat id.
- alias (str): Target chat alias to send messages to, bypassing the configured chat id. The mapping is stored in the configuration.
"""
settings = get_config_settings(conf)
settings = get_config_settings(conf, override_chat_id=chat_id, override_alias=alias)
token = settings.token
chat_id = settings.chat_id
reply_to_message_id = settings.reply_to_message_id
Expand Down Expand Up @@ -323,7 +330,7 @@ def make_captions(items, captions):
return message_ids


async def delete(message_ids, conf=None, timeout=30):
async def delete(message_ids, conf=None, timeout=30, chat_id=None, alias=None):
"""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.
Expand All @@ -335,8 +342,10 @@ async def delete(message_ids, conf=None, timeout=30):
- 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.
- chat_id (str): Target chat id to delete messages from, bypassing the configured chat id.
- alias (str): Target chat alias to delete messages from.
"""
settings = get_config_settings(conf)
settings = get_config_settings(conf, override_chat_id=chat_id, override_alias=alias)
token = settings.token
chat_id = settings.chat_id
bot = telegram.Bot(token)
Expand Down Expand Up @@ -365,14 +374,16 @@ async def configure(conf=None, channel=False, group=False, fm_integration=False)
contact_url = "https://telegram.me/"
root_topic_message = None

print(f"Talk with the {markup('BotFather', 'cyan')} on Telegram ({contact_url}BotFather), "
"create a bot and insert the token")
try:
token = input(markup(prompt, "magenta")).strip()
except UnicodeEncodeError:
# some users can only display ASCII
prompt = "> "
token = input(markup(prompt, "magenta")).strip()
token = os.environ.get("TELEGRAM_BOT_TOKEN")
if not token:
print(f"Talk with the {markup('BotFather', 'cyan')} on Telegram ({contact_url}BotFather), "
"create a bot and insert the token")
try:
token = input(markup(prompt, "magenta")).strip()
except UnicodeEncodeError:
# some users can only display ASCII
prompt = "> "
token = input(markup(prompt, "magenta")).strip()

try:
bot = telegram.Bot(token)
Expand Down Expand Up @@ -479,10 +490,13 @@ def get_root_topic_message(message: telegram.Message):

config = configparser.ConfigParser()

config_data = {"chat_id": chat_id}
if not os.environ.get("TELEGRAM_BOT_TOKEN"):
config_data["TOKEN"] = token
if root_topic_message is not None and isinstance(root_topic_message, telegram.Message):
config["telegram"] = {"TOKEN": token, "chat_id": chat_id, "reply_to_message_id": root_topic_message.message_id}
else:
config["telegram"] = {"TOKEN": token, "chat_id": chat_id}
config_data["reply_to_message_id"] = root_topic_message.message_id

config["telegram"] = config_data
conf_dir = dirname(conf)
if conf_dir:
makedirs(conf_dir, exist_ok=True)
Expand Down Expand Up @@ -557,21 +571,49 @@ class Settings(NamedTuple):
reply_to_message_id: int | str | None


def get_config_settings(conf=None) -> Settings:
def get_config_settings(conf=None, override_chat_id=None, override_alias=None) -> Settings:
conf = expanduser(conf) if conf else get_config_path()
config = configparser.ConfigParser()
if not config.read(conf) or not config.has_section("telegram"):
raise ConfigError(f"Config not found: {conf}")

missing_options = set(["token", "chat_id"]) - set(config.options("telegram"))
if len(missing_options) > 0:
raise ConfigError(f"Missing options in config: {', '.join(missing_options)}")
# Allow reading token from env instead of config file
env_token = os.environ.get("TELEGRAM_BOT_TOKEN")

has_config = config.read(conf) and config.has_section("telegram")

if not has_config:
if not env_token:
raise ConfigError(f"Config not found: {conf}")

required_options = {"chat_id"}
if not env_token:
required_options.add("token")

if has_config:
missing_options = set(required_options) - set(config.options("telegram"))
if len(missing_options) > 0:
raise ConfigError(f"Missing options in config: {', '.join(missing_options)}")

if has_config and not env_token:
token = config.get("telegram", "token")
else:
token = env_token

if override_chat_id:
chat = override_chat_id
elif override_alias:
if has_config and config.has_section("aliases") and config.has_option("aliases", override_alias):
chat = config.get("aliases", override_alias)
else:
raise ConfigError(f"Alias '{override_alias}' not found in config: {conf}")
else:
if has_config and config.has_option("telegram", "chat_id"):
chat = config.get("telegram", "chat_id")
else:
raise ConfigError(f"Missing options in config: chat_id")

token = config.get("telegram", "token")
chat = config.get("telegram", "chat_id")
reply = config.get("telegram", "reply_to_message_id", fallback=None)
reply = config.get("telegram", "reply_to_message_id", fallback=None) if has_config else None

chat_id = int(chat) if chat.isdigit() else chat
chat_id = int(chat) if str(chat).lstrip('-').isdigit() else chat
reply_to_message_id = int(reply) if reply and reply.isdigit() else reply
return Settings(token=token, chat_id=chat_id, reply_to_message_id=reply_to_message_id)

Expand Down