feat: entrypoint for safe config + permissions; optional counter seeding via USE_INITIAL_COUNTERS

This commit is contained in:
Andre Beging
2025-09-30 09:44:58 +02:00
parent 6d60fd813c
commit 5b2e652682
3 changed files with 395 additions and 369 deletions

680
bot.py
View File

@@ -1,336 +1,344 @@
import os
import json
import yaml
import logging
import signal
import sys
from pathlib import Path
from typing import Dict, Any, Optional
from telegram import Update
from telegram.constants import ParseMode
from telegram.ext import (
ApplicationBuilder,
CommandHandler,
ContextTypes,
MessageHandler,
filters,
ChatMemberHandler,
)
DATA_DIR = Path(os.environ.get("DATA_DIR", "/data"))
COUNTERS_FILE = DATA_DIR / "counters.json"
CONFIG_FILE = Path(os.environ.get("CONFIG_FILE", "config.yaml"))
logger = logging.getLogger("counter_bot")
# ---------------------- Persistence Layer ----------------------
def load_config() -> Dict[str, Any]:
if not CONFIG_FILE.exists():
logger.warning("Konfigurationsdatei %s nicht gefunden. Nutze Umgebungsvariablen / Defaults.", CONFIG_FILE)
return {}
with CONFIG_FILE.open("r", encoding="utf-8") as f:
try:
return yaml.safe_load(f) or {}
except yaml.YAMLError as e:
logger.error("Fehler beim Lesen der Konfigurationsdatei: %s", e)
return {}
def load_counters() -> Dict[str, int]:
if not COUNTERS_FILE.exists():
return {}
try:
with COUNTERS_FILE.open("r", encoding="utf-8") as f:
data = json.load(f)
if isinstance(data, dict):
# ensure int values
fixed = {}
for k, v in data.items():
try:
fixed[k] = int(v)
except (ValueError, TypeError):
logger.warning("Ungültiger Wert für Counter %s -> %s, setze 0", k, v)
fixed[k] = 0
return fixed
return {}
except json.JSONDecodeError:
logger.error("Konnte %s nicht lesen (JSON Fehler). Starte mit leerem Satz.", COUNTERS_FILE)
return {}
def atomic_write(path: Path, content: str) -> None:
tmp = path.with_suffix(path.suffix + ".tmp")
with tmp.open("w", encoding="utf-8") as f:
f.write(content)
tmp.replace(path)
def save_counters(counters: Dict[str, int]) -> None:
DATA_DIR.mkdir(parents=True, exist_ok=True)
atomic_write(COUNTERS_FILE, json.dumps(counters, ensure_ascii=False, sort_keys=True, indent=2))
# ---------------------- Access Control ----------------------
def is_allowed(chat_id: int, allowed_list: Optional[list]) -> bool:
if not allowed_list: # None oder leere Liste => alle erlaubt
return True
return chat_id in allowed_list
# ---------------------- Command Handlers ----------------------
HELP_TEXT = (
"Verfügbare Befehle:\n"
"/add <name> - Legt einen neuen Counter mit Wert 0 an\n"
"/remove <name> - Löscht einen Counter\n"
"/increment <name> - Erhöht den Counter um 1 und zeigt neuen Wert\n"
"/decrement <name> - Verringert den Counter um 1 und zeigt neuen Wert\n"
"/list - Listet alle Counter\n"
"/help - Zeigt diese Hilfe an"
)
async def help_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
await update.message.reply_text(HELP_TEXT)
def norm_key(raw: str) -> str:
return raw.strip().lower()
def html_escape(text: str) -> str:
return (text
.replace('&', '&amp;')
.replace('<', '&lt;')
.replace('>', '&gt;')
)
async def add_counter(update: Update, context: ContextTypes.DEFAULT_TYPE):
chat_id = update.effective_chat.id
if not context.bot_data['access'](chat_id):
return
if not context.args:
await update.message.reply_text("Bitte einen Counternamen angeben: /add <name>")
return
key = norm_key(context.args[0])
counters = context.bot_data['counters']
if key in counters:
await update.message.reply_text(f"Counter <b>{html_escape(key)}</b> existiert bereits.", parse_mode=ParseMode.HTML)
return
counters[key] = 0
save_counters(counters)
await update.message.reply_text(f"Counter <b>{html_escape(key)}</b> wurde angelegt (Wert 0).", parse_mode=ParseMode.HTML)
async def remove_counter(update: Update, context: ContextTypes.DEFAULT_TYPE):
chat_id = update.effective_chat.id
if not context.bot_data['access'](chat_id):
return
if not context.args:
await update.message.reply_text("Bitte einen Counternamen angeben: /remove <name>")
return
key = norm_key(context.args[0])
counters = context.bot_data['counters']
if key not in counters:
await update.message.reply_text(f"Counter <b>{html_escape(key)}</b> existiert nicht.", parse_mode=ParseMode.HTML)
return
del counters[key]
save_counters(counters)
await update.message.reply_text(f"Counter <b>{html_escape(key)}</b> wurde gelöscht.", parse_mode=ParseMode.HTML)
async def increment_counter(update: Update, context: ContextTypes.DEFAULT_TYPE):
chat_id = update.effective_chat.id
if not context.bot_data['access'](chat_id):
return
if not context.args:
await update.message.reply_text("Bitte einen Counternamen angeben: /increment <name>")
return
key = norm_key(context.args[0])
counters = context.bot_data['counters']
if key not in counters:
esc = html_escape(key)
await update.message.reply_text(f"Counter <b>{esc}</b> existiert nicht. Lege ihn mit /add {esc} an.", parse_mode=ParseMode.HTML)
return
counters[key] += 1
save_counters(counters)
await update.message.reply_text(f"Counter <b>{html_escape(key)}</b> steht jetzt auf <b>{counters[key]}</b>.", parse_mode=ParseMode.HTML)
async def decrement_counter(update: Update, context: ContextTypes.DEFAULT_TYPE):
chat_id = update.effective_chat.id
if not context.bot_data['access'](chat_id):
return
if not context.args:
await update.message.reply_text("Bitte einen Counternamen angeben: /decrement <name>")
return
key = norm_key(context.args[0])
counters = context.bot_data['counters']
if key not in counters:
esc = html_escape(key)
await update.message.reply_text(f"Counter <b>{esc}</b> existiert nicht. Lege ihn mit /add {esc} an.", parse_mode=ParseMode.HTML)
return
counters[key] -= 1
save_counters(counters)
await update.message.reply_text(f"Counter <b>{html_escape(key)}</b> steht jetzt auf <b>{counters[key]}</b>.", parse_mode=ParseMode.HTML)
async def list_counters(update: Update, context: ContextTypes.DEFAULT_TYPE):
chat_id = update.effective_chat.id
if not context.bot_data['access'](chat_id):
return
counters = context.bot_data['counters']
if not counters:
await update.message.reply_text("Keine Counter vorhanden. Lege einen an mit /add <name>.")
return
lines = ["<b>Aktuelle Counter:</b>"]
for k in sorted(counters.keys()):
lines.append(f"- <b>{html_escape(k)}</b>: {counters[k]}")
await update.message.reply_text("\n".join(lines), parse_mode=ParseMode.HTML)
async def unknown(update: Update, context: ContextTypes.DEFAULT_TYPE):
await update.message.reply_text("Unbekannter Befehl. Nutze /help für eine Übersicht.")
async def error_handler(update: object, context: ContextTypes.DEFAULT_TYPE):
logger.error("Fehler während Update Verarbeitung", exc_info=context.error)
# Optionale kurze Rückmeldung nur bei klassischen Nachrichten
try:
if isinstance(update, Update) and update.effective_chat:
await context.bot.send_message(update.effective_chat.id, "⚠️ Interner Fehler beim Verarbeiten der Anfrage.")
except Exception:
pass
# ---------------------- Group Events ----------------------
async def new_members(update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Send a ready message when the bot itself is added to a group or any new member joins (configurable)."""
if update.message is None or not update.message.new_chat_members:
return
bot_user = (await context.bot.get_me())
announce_all = os.environ.get("ANNOUNCE_ALL_JOINS", "false").lower() in {"1", "true", "yes"}
for member in update.message.new_chat_members:
if member.id == bot_user.id:
await update.message.reply_text("🤖 Bot ist bereit! Verwende /help für Befehle.")
elif announce_all:
await update.message.reply_text(f"Willkommen {member.full_name}! Tippe /help für Befehle.")
async def debug_all_messages(update: Update, context: ContextTypes.DEFAULT_TYPE):
if update.message:
logger.debug("Empfangene Nachricht chat=%s user=%s text=%r", update.effective_chat.id, update.effective_user.id if update.effective_user else None, update.message.text)
async def my_chat_member(update: Update, context: ContextTypes.DEFAULT_TYPE):
# Fired when bot's status in a chat changes (e.g., added, removed, promoted)
chat = update.effective_chat
diff = update.my_chat_member
if diff:
logger.info("my_chat_member update chat=%s old=%s new=%s", chat.id if chat else None, diff.old_chat_member.status, diff.new_chat_member.status)
# If bot was just added / became member
try:
if diff.new_chat_member.status in {"member", "administrator"}:
await context.bot.send_message(chat.id, "🤖 Bot ist jetzt aktiv in diesem Chat. /help für Befehle.")
except Exception as e:
logger.warning("Fehler beim Senden der Aktiv-Nachricht: %s", e)
# ---------------------- Setup & Main ----------------------
def setup_logging(level: str):
numeric = getattr(logging, level.upper(), logging.INFO)
logging.basicConfig(stream=sys.stdout, level=numeric, format='%(asctime)s %(levelname)s %(name)s: %(message)s')
def init_counters(existing: Dict[str, int], config: Dict[str, Any]) -> Dict[str, int]:
if existing:
return existing
initial = config.get('initial_counters') or {}
normalized = {norm_key(k): int(v) for k, v in initial.items()}
if normalized:
save_counters(normalized)
return normalized
async def on_startup(app):
logger.info("Bot gestartet und bereit.")
announce_ids = os.environ.get("STARTUP_ANNOUNCE_CHAT_IDS")
if announce_ids:
ids = []
for raw in announce_ids.split(','):
raw = raw.strip()
if not raw:
continue
try:
ids.append(int(raw))
except ValueError:
logger.warning("Kann Chat ID %s nicht in int umwandeln", raw)
if ids:
me = await app.bot.get_me()
for cid in ids:
try:
await app.bot.send_message(cid, f"🤖 {me.first_name} ist bereit. Nutze /help für Befehle.")
except Exception as e:
logger.warning("Konnte Startup-Nachricht an %s nicht senden: %s", cid, e)
def main():
config = load_config()
token = os.environ.get('BOT_TOKEN') or config.get('bot_token')
if not token:
print("Fehlendes Bot Token: Setze BOT_TOKEN env oder bot_token in config.yaml", file=sys.stderr)
sys.exit(1)
setup_logging(config.get('log_level', 'INFO'))
counters = load_counters()
counters = init_counters(counters, config)
allowed_chat_ids = config.get('allowed_chat_ids')
if allowed_chat_ids is not None:
try:
allowed_chat_ids = [int(x) for x in allowed_chat_ids]
except Exception:
logger.error("allowed_chat_ids in config.yaml müssen Ganzzahlen sein")
allowed_chat_ids = None
def access(chat_id: int) -> bool:
if not is_allowed(chat_id, allowed_chat_ids):
logger.info("Verweigerter Zugriff von Chat %s", chat_id)
return False
return True
application = ApplicationBuilder().token(token).build()
# bot_data shared state
application.bot_data['counters'] = counters
application.bot_data['access'] = access
application.add_handler(CommandHandler("help", help_command))
application.add_handler(CommandHandler("add", add_counter))
application.add_handler(CommandHandler("remove", remove_counter))
application.add_handler(CommandHandler("increment", increment_counter))
application.add_handler(CommandHandler("decrement", decrement_counter))
application.add_handler(CommandHandler("list", list_counters))
# Group / membership events
application.add_handler(MessageHandler(filters.StatusUpdate.NEW_CHAT_MEMBERS, new_members))
application.add_handler(ChatMemberHandler(my_chat_member, ChatMemberHandler.MY_CHAT_MEMBER))
# Debug raw messages (placed last with low priority)
application.add_handler(MessageHandler(filters.ALL, debug_all_messages), group=100)
# Error handler
application.add_error_handler(error_handler)
# Startup callback
application.post_init = on_startup
application.run_polling(
stop_signals=(signal.SIGINT, signal.SIGTERM),
allowed_updates=["message", "chat_member", "my_chat_member"],
)
if __name__ == '__main__':
main()
import os
import json
import yaml
import logging
import signal
import sys
from pathlib import Path
from typing import Dict, Any, Optional
from telegram import Update
from telegram.constants import ParseMode
from telegram.ext import (
ApplicationBuilder,
CommandHandler,
ContextTypes,
MessageHandler,
filters,
ChatMemberHandler,
)
DATA_DIR = Path(os.environ.get("DATA_DIR", "/data"))
COUNTERS_FILE = DATA_DIR / "counters.json"
CONFIG_FILE = Path(os.environ.get("CONFIG_FILE", "config.yaml"))
logger = logging.getLogger("counter_bot")
# ---------------------- Persistence Layer ----------------------
def load_config() -> Dict[str, Any]:
if not CONFIG_FILE.exists():
logger.warning("Konfigurationsdatei %s nicht gefunden. Nutze Umgebungsvariablen / Defaults.", CONFIG_FILE)
return {}
with CONFIG_FILE.open("r", encoding="utf-8") as f:
try:
return yaml.safe_load(f) or {}
except yaml.YAMLError as e:
logger.error("Fehler beim Lesen der Konfigurationsdatei: %s", e)
return {}
def load_counters() -> Dict[str, int]:
if not COUNTERS_FILE.exists():
return {}
try:
with COUNTERS_FILE.open("r", encoding="utf-8") as f:
data = json.load(f)
if isinstance(data, dict):
# ensure int values
fixed = {}
for k, v in data.items():
try:
fixed[k] = int(v)
except (ValueError, TypeError):
logger.warning("Ungültiger Wert für Counter %s -> %s, setze 0", k, v)
fixed[k] = 0
return fixed
return {}
except json.JSONDecodeError:
logger.error("Konnte %s nicht lesen (JSON Fehler). Starte mit leerem Satz.", COUNTERS_FILE)
return {}
def atomic_write(path: Path, content: str) -> None:
tmp = path.with_suffix(path.suffix + ".tmp")
with tmp.open("w", encoding="utf-8") as f:
f.write(content)
tmp.replace(path)
def save_counters(counters: Dict[str, int]) -> None:
DATA_DIR.mkdir(parents=True, exist_ok=True)
atomic_write(COUNTERS_FILE, json.dumps(counters, ensure_ascii=False, sort_keys=True, indent=2))
# ---------------------- Access Control ----------------------
def is_allowed(chat_id: int, allowed_list: Optional[list]) -> bool:
if not allowed_list: # None oder leere Liste => alle erlaubt
return True
return chat_id in allowed_list
# ---------------------- Command Handlers ----------------------
HELP_TEXT = (
"Verfügbare Befehle:\n"
"/add <name> - Legt einen neuen Counter mit Wert 0 an\n"
"/remove <name> - Löscht einen Counter\n"
"/increment <name> - Erhöht den Counter um 1 und zeigt neuen Wert\n"
"/decrement <name> - Verringert den Counter um 1 und zeigt neuen Wert\n"
"/list - Listet alle Counter\n"
"/help - Zeigt diese Hilfe an"
)
async def help_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
await update.message.reply_text(HELP_TEXT)
def norm_key(raw: str) -> str:
return raw.strip().lower()
def html_escape(text: str) -> str:
return (text
.replace('&', '&amp;')
.replace('<', '&lt;')
.replace('>', '&gt;')
)
async def add_counter(update: Update, context: ContextTypes.DEFAULT_TYPE):
chat_id = update.effective_chat.id
if not context.bot_data['access'](chat_id):
return
if not context.args:
await update.message.reply_text("Bitte einen Counternamen angeben: /add <name>")
return
key = norm_key(context.args[0])
counters = context.bot_data['counters']
if key in counters:
await update.message.reply_text(f"Counter <b>{html_escape(key)}</b> existiert bereits.", parse_mode=ParseMode.HTML)
return
counters[key] = 0
save_counters(counters)
await update.message.reply_text(f"Counter <b>{html_escape(key)}</b> wurde angelegt (Wert 0).", parse_mode=ParseMode.HTML)
async def remove_counter(update: Update, context: ContextTypes.DEFAULT_TYPE):
chat_id = update.effective_chat.id
if not context.bot_data['access'](chat_id):
return
if not context.args:
await update.message.reply_text("Bitte einen Counternamen angeben: /remove <name>")
return
key = norm_key(context.args[0])
counters = context.bot_data['counters']
if key not in counters:
await update.message.reply_text(f"Counter <b>{html_escape(key)}</b> existiert nicht.", parse_mode=ParseMode.HTML)
return
del counters[key]
save_counters(counters)
await update.message.reply_text(f"Counter <b>{html_escape(key)}</b> wurde gelöscht.", parse_mode=ParseMode.HTML)
async def increment_counter(update: Update, context: ContextTypes.DEFAULT_TYPE):
chat_id = update.effective_chat.id
if not context.bot_data['access'](chat_id):
return
if not context.args:
await update.message.reply_text("Bitte einen Counternamen angeben: /increment <name>")
return
key = norm_key(context.args[0])
counters = context.bot_data['counters']
if key not in counters:
esc = html_escape(key)
await update.message.reply_text(f"Counter <b>{esc}</b> existiert nicht. Lege ihn mit /add {esc} an.", parse_mode=ParseMode.HTML)
return
counters[key] += 1
save_counters(counters)
await update.message.reply_text(f"Counter <b>{html_escape(key)}</b> steht jetzt auf <b>{counters[key]}</b>.", parse_mode=ParseMode.HTML)
async def decrement_counter(update: Update, context: ContextTypes.DEFAULT_TYPE):
chat_id = update.effective_chat.id
if not context.bot_data['access'](chat_id):
return
if not context.args:
await update.message.reply_text("Bitte einen Counternamen angeben: /decrement <name>")
return
key = norm_key(context.args[0])
counters = context.bot_data['counters']
if key not in counters:
esc = html_escape(key)
await update.message.reply_text(f"Counter <b>{esc}</b> existiert nicht. Lege ihn mit /add {esc} an.", parse_mode=ParseMode.HTML)
return
counters[key] -= 1
save_counters(counters)
await update.message.reply_text(f"Counter <b>{html_escape(key)}</b> steht jetzt auf <b>{counters[key]}</b>.", parse_mode=ParseMode.HTML)
async def list_counters(update: Update, context: ContextTypes.DEFAULT_TYPE):
chat_id = update.effective_chat.id
if not context.bot_data['access'](chat_id):
return
counters = context.bot_data['counters']
if not counters:
await update.message.reply_text("Keine Counter vorhanden. Lege einen an mit /add <name>.")
return
lines = ["<b>Aktuelle Counter:</b>"]
for k in sorted(counters.keys()):
lines.append(f"- <b>{html_escape(k)}</b>: {counters[k]}")
await update.message.reply_text("\n".join(lines), parse_mode=ParseMode.HTML)
async def unknown(update: Update, context: ContextTypes.DEFAULT_TYPE):
await update.message.reply_text("Unbekannter Befehl. Nutze /help für eine Übersicht.")
async def error_handler(update: object, context: ContextTypes.DEFAULT_TYPE):
logger.error("Fehler während Update Verarbeitung", exc_info=context.error)
# Optionale kurze Rückmeldung nur bei klassischen Nachrichten
try:
if isinstance(update, Update) and update.effective_chat:
await context.bot.send_message(update.effective_chat.id, "⚠️ Interner Fehler beim Verarbeiten der Anfrage.")
except Exception:
pass
# ---------------------- Group Events ----------------------
async def new_members(update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Send a ready message when the bot itself is added to a group or any new member joins (configurable)."""
if update.message is None or not update.message.new_chat_members:
return
bot_user = (await context.bot.get_me())
announce_all = os.environ.get("ANNOUNCE_ALL_JOINS", "false").lower() in {"1", "true", "yes"}
for member in update.message.new_chat_members:
if member.id == bot_user.id:
await update.message.reply_text("🤖 Bot ist bereit! Verwende /help für Befehle.")
elif announce_all:
await update.message.reply_text(f"Willkommen {member.full_name}! Tippe /help für Befehle.")
async def debug_all_messages(update: Update, context: ContextTypes.DEFAULT_TYPE):
if update.message:
logger.debug("Empfangene Nachricht chat=%s user=%s text=%r", update.effective_chat.id, update.effective_user.id if update.effective_user else None, update.message.text)
async def my_chat_member(update: Update, context: ContextTypes.DEFAULT_TYPE):
# Fired when bot's status in a chat changes (e.g., added, removed, promoted)
chat = update.effective_chat
diff = update.my_chat_member
if diff:
logger.info("my_chat_member update chat=%s old=%s new=%s", chat.id if chat else None, diff.old_chat_member.status, diff.new_chat_member.status)
# If bot was just added / became member
try:
if diff.new_chat_member.status in {"member", "administrator"}:
await context.bot.send_message(chat.id, "🤖 Bot ist jetzt aktiv in diesem Chat. /help für Befehle.")
except Exception as e:
logger.warning("Fehler beim Senden der Aktiv-Nachricht: %s", e)
# ---------------------- Setup & Main ----------------------
def setup_logging(level: str):
numeric = getattr(logging, level.upper(), logging.INFO)
logging.basicConfig(stream=sys.stdout, level=numeric, format='%(asctime)s %(levelname)s %(name)s: %(message)s')
def init_counters(existing: Dict[str, int], config: Dict[str, Any]) -> Dict[str, int]:
"""Return existing counters or (optionally) seed initial ones.
Seeding now only happens if BOTH conditions apply:
1) No existing counters file/content
2) Env USE_INITIAL_COUNTERS is truthy (1/true/yes)
"""
if existing:
return existing
if os.environ.get("USE_INITIAL_COUNTERS", "false").lower() not in {"1", "true", "yes"}:
return {}
initial = config.get('initial_counters') or {}
normalized = {norm_key(k): int(v) for k, v in initial.items()}
if normalized:
save_counters(normalized)
return normalized
async def on_startup(app):
logger.info("Bot gestartet und bereit.")
announce_ids = os.environ.get("STARTUP_ANNOUNCE_CHAT_IDS")
if announce_ids:
ids = []
for raw in announce_ids.split(','):
raw = raw.strip()
if not raw:
continue
try:
ids.append(int(raw))
except ValueError:
logger.warning("Kann Chat ID %s nicht in int umwandeln", raw)
if ids:
me = await app.bot.get_me()
for cid in ids:
try:
await app.bot.send_message(cid, f"🤖 {me.first_name} ist bereit. Nutze /help für Befehle.")
except Exception as e:
logger.warning("Konnte Startup-Nachricht an %s nicht senden: %s", cid, e)
def main():
config = load_config()
token = os.environ.get('BOT_TOKEN') or config.get('bot_token')
if not token:
print("Fehlendes Bot Token: Setze BOT_TOKEN env oder bot_token in config.yaml", file=sys.stderr)
sys.exit(1)
setup_logging(config.get('log_level', 'INFO'))
counters = load_counters()
counters = init_counters(counters, config)
allowed_chat_ids = config.get('allowed_chat_ids')
if allowed_chat_ids is not None:
try:
allowed_chat_ids = [int(x) for x in allowed_chat_ids]
except Exception:
logger.error("allowed_chat_ids in config.yaml müssen Ganzzahlen sein")
allowed_chat_ids = None
def access(chat_id: int) -> bool:
if not is_allowed(chat_id, allowed_chat_ids):
logger.info("Verweigerter Zugriff von Chat %s", chat_id)
return False
return True
application = ApplicationBuilder().token(token).build()
# bot_data shared state
application.bot_data['counters'] = counters
application.bot_data['access'] = access
application.add_handler(CommandHandler("help", help_command))
application.add_handler(CommandHandler("add", add_counter))
application.add_handler(CommandHandler("remove", remove_counter))
application.add_handler(CommandHandler("increment", increment_counter))
application.add_handler(CommandHandler("decrement", decrement_counter))
application.add_handler(CommandHandler("list", list_counters))
# Group / membership events
application.add_handler(MessageHandler(filters.StatusUpdate.NEW_CHAT_MEMBERS, new_members))
application.add_handler(ChatMemberHandler(my_chat_member, ChatMemberHandler.MY_CHAT_MEMBER))
# Debug raw messages (placed last with low priority)
application.add_handler(MessageHandler(filters.ALL, debug_all_messages), group=100)
# Error handler
application.add_error_handler(error_handler)
# Startup callback
application.post_init = on_startup
application.run_polling(
stop_signals=(signal.SIGINT, signal.SIGTERM),
allowed_updates=["message", "chat_member", "my_chat_member"],
)
if __name__ == '__main__':
main()