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

View File

@@ -1,33 +1,35 @@
# Multi-stage für kleinere finale Imagegröße # Multi-stage für kleinere finale Imagegröße
FROM python:3.12-slim AS base FROM python:3.12-slim AS base
ENV PYTHONDONTWRITEBYTECODE=1 \ ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \ PYTHONUNBUFFERED=1 \
PIP_NO_CACHE_DIR=1 PIP_NO_CACHE_DIR=1
# System deps (tzdata optional falls benötigt) # System deps (tzdata optional falls benötigt)
RUN apt-get update && apt-get install -y --no-install-recommends \ RUN apt-get update && apt-get install -y --no-install-recommends \
ca-certificates \ ca-certificates \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*
# Non-root user # Non-root user
RUN useradd -u 10001 -m appuser RUN useradd -u 10001 -m appuser
WORKDIR /app WORKDIR /app
COPY requirements.txt ./ COPY requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt RUN pip install --no-cache-dir -r requirements.txt
COPY bot.py ./ COPY bot.py ./
COPY config.example.yaml ./config.yaml COPY config.example.yaml ./config.example.yaml
COPY docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh
# Datenverzeichnis RUN chmod +x /usr/local/bin/docker-entrypoint.sh
RUN mkdir -p /data && chown -R appuser:appuser /data && chown appuser:appuser /app
VOLUME ["/data"] # Datenverzeichnis
# Pre-create data dir (ownership may be adjusted again at runtime by entrypoint)
USER appuser RUN mkdir -p /data
VOLUME ["/data"]
ENV DATA_DIR=/data \
CONFIG_FILE=/app/config.yaml ENV DATA_DIR=/data \
CONFIG_FILE=/app/config.yaml
CMD ["python", "bot.py"]
ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"]
CMD ["python", "bot.py"]

680
bot.py
View File

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

16
docker-entrypoint.sh Normal file
View File

@@ -0,0 +1,16 @@
#!/bin/sh
set -e
# If running as root, fix ownership of /data, then drop privileges
if [ "$(id -u)" = "0" ]; then
mkdir -p /data
chown -R appuser:appuser /data || echo "Warn: could not chown /data"
# Copy example config only if missing target
if [ ! -f /app/config.yaml ] && [ -f /app/config.example.yaml ]; then
cp /app/config.example.yaml /app/config.yaml
chown appuser:appuser /app/config.yaml || true
fi
exec su -s /bin/sh appuser -c "$*"
else
exec "$@"
fi