initial commit

This commit is contained in:
Thies Mueller
2026-06-21 16:39:16 +02:00
commit 1deb65b2fd
4 changed files with 376 additions and 0 deletions
+362
View File
@@ -0,0 +1,362 @@
import asyncio
import json
import os
from pathlib import Path
import httpx
import uvicorn
from fastapi import FastAPI, HTTPException, Header
from pydantic import BaseModel
from telegram import (
InlineKeyboardButton,
InlineKeyboardMarkup,
Update
)
from telegram.ext import (
Application,
CommandHandler,
CallbackQueryHandler,
ContextTypes
)
CONFIG_FILE = "config.json"
SUBSCRIBERS_FILE = "subscribers.json"
def load_config():
with open(CONFIG_FILE, "r", encoding="utf-8") as f:
return json.load(f)
config = load_config()
TELEGRAM_TOKEN = config["telegram_token"]
RENNPLAN_API = config["rennplan_api"]
PUSH_API_KEY = config["push_api_key"]
def load_subscribers():
if not Path(SUBSCRIBERS_FILE).exists():
return []
with open(SUBSCRIBERS_FILE, "r", encoding="utf-8") as f:
return json.load(f)
def save_subscribers(subscribers):
with open(SUBSCRIBERS_FILE, "w", encoding="utf-8") as f:
json.dump(subscribers, f, indent=2)
def add_subscriber(chat_id):
subscribers = load_subscribers()
if chat_id not in subscribers:
subscribers.append(chat_id)
save_subscribers(subscribers)
def remove_subscriber(chat_id):
subscribers = load_subscribers()
if chat_id in subscribers:
subscribers.remove(chat_id)
save_subscribers(subscribers)
async def lauf_selected(update: Update, context: ContextTypes.DEFAULT_TYPE):
query = update.callback_query
await query.answer()
lauf_name = query.data.split(":")[1]
data = await fetch_rennplan()
lauf = data["rennplan"][lauf_name]
keyboard = []
for race_id in sorted(
lauf.keys(),
key=lambda x: int(x)
):
race = lauf[race_id]
keyboard.append([
InlineKeyboardButton(
f"{race_id} - {race['name']} ({race['zeit']})",
callback_data=f"race:{lauf_name}:{race_id}"
)
])
keyboard.append([
InlineKeyboardButton(
"⬅ Zurück",
callback_data="back_laeufe"
)
])
await query.edit_message_text(
text=f"🏁 Rennen in {lauf_name.capitalize()}:",
reply_markup=InlineKeyboardMarkup(keyboard)
)
async def race_selected(update: Update, context: ContextTypes.DEFAULT_TYPE):
query = update.callback_query
await query.answer()
_, lauf_name, race_id = query.data.split(":")
data = await fetch_rennplan()
race = data["rennplan"][lauf_name][race_id]
keyboard = [[
InlineKeyboardButton(
"⬅ Zurück",
callback_data=f"lauf:{lauf_name}"
)
]]
await query.edit_message_text(
text=format_race(race),
parse_mode="HTML",
reply_markup=InlineKeyboardMarkup(keyboard)
)
async def back_to_laeufe(update: Update, context: ContextTypes.DEFAULT_TYPE):
query = update.callback_query
await query.answer()
data = await fetch_rennplan()
keyboard = []
for lauf_name in sorted(
data["rennplan"].keys(),
key=lambda x: int(x.replace("lauf", ""))
):
keyboard.append([
InlineKeyboardButton(
lauf_name.capitalize(),
callback_data=f"lauf:{lauf_name}"
)
])
await query.edit_message_text(
text="🏁 Bitte einen Lauf auswählen:",
reply_markup=InlineKeyboardMarkup(keyboard)
)
def get_latest_race(data):
rennplan = data["rennplan"]
latest_lauf_key = sorted(
rennplan.keys(),
key=lambda x: int(x.replace("lauf", ""))
)[-1]
lauf = rennplan[latest_lauf_key]
latest_race_key = sorted(
lauf.keys(),
key=lambda x: int(x)
)[-1]
return lauf[latest_race_key]
def format_race(race):
return (
f"🏁 <b>{race['name']}</b>\n"
f"📋 {race['art']}\n"
f"🕒 {race['zeit']}\n\n"
f"❶ <b>Bahn 1</b>\n{race['bahn1']}\n\n"
f"❷ <b>Bahn 2</b>\n{race['bahn2']}\n\n"
f"❸ <b>Bahn 3</b>\n{race['bahn3']}"
)
async def fetch_rennplan():
async with httpx.AsyncClient(timeout=30) as client:
response = await client.get(RENNPLAN_API)
response.raise_for_status()
return response.json()
async def fetch_latest_race():
async with httpx.AsyncClient(timeout=30) as client:
response = await client.get(RENNPLAN_API)
response.raise_for_status()
data = response.json()
return get_latest_race(data)
async def start_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
add_subscriber(update.effective_chat.id)
await update.message.reply_text(
"Moin! Du bekommst ab sofort Push Benachrichtigungen über Telegram. Vielen Dank!"
)
async def stop_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
remove_subscriber(update.effective_chat.id)
await update.message.reply_text(
"Auf wiedersehen!"
)
async def rennplan_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
try:
data = await fetch_rennplan()
rennplan = data["rennplan"]
keyboard = []
for lauf_name in sorted(
rennplan.keys(),
key=lambda x: int(x.replace("lauf", ""))
):
keyboard.append([
InlineKeyboardButton(
lauf_name.capitalize(),
callback_data=f"lauf:{lauf_name}"
)
])
await update.message.reply_text(
"🏁 Bitte wähle einen Lauf aus:",
reply_markup=InlineKeyboardMarkup(keyboard)
)
except Exception as e:
await update.message.reply_text(
f"Fehler beim Laden des Rennplans:\n{e}"
)
async def app_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
keyboard = [
[
InlineKeyboardButton(
text="Apps herunterladen",
url="https://apps.sport-am-tankumsee.de?source=telegram"
)
]
]
await update.message.reply_text(
"Lade dir hier unsere nativen Apps für iOS & Android herunter:",
reply_markup=InlineKeyboardMarkup(keyboard)
)
telegram_app = Application.builder().token(
TELEGRAM_TOKEN
).build()
telegram_app.add_handler(CommandHandler("start", start_command))
telegram_app.add_handler(CommandHandler("stop", stop_command))
telegram_app.add_handler(CommandHandler("rennplan", rennplan_command))
telegram_app.add_handler(CommandHandler("app", app_command))
telegram_app.add_handler(
CallbackQueryHandler(
lauf_selected,
pattern=r"^lauf:"
)
)
telegram_app.add_handler(
CallbackQueryHandler(
race_selected,
pattern=r"^race:"
)
)
telegram_app.add_handler(
CallbackQueryHandler(
back_to_laeufe,
pattern=r"^back_laeufe$"
)
)
api = FastAPI()
class PushMessage(BaseModel):
message: str
@api.post("/push")
async def push_message(
payload: PushMessage,
x_api_key: str = Header(None)
):
if x_api_key != PUSH_API_KEY:
raise HTTPException(
status_code=401,
detail="Invalid API Key"
)
subscribers = load_subscribers()
sent = 0
for chat_id in subscribers:
try:
await telegram_app.bot.send_message(
chat_id=chat_id,
text=payload.message,
parse_mode="HTML"
)
sent += 1
except Exception:
pass
return {
"success": True,
"sent": sent
}
async def start_api():
server = uvicorn.Server(
uvicorn.Config(
app=api,
host=config["listen_host"],
port=config["listen_port"],
log_level="info"
)
)
await server.serve()
async def main():
await telegram_app.initialize()
await telegram_app.start()
asyncio.create_task(start_api())
await telegram_app.updater.start_polling()
while True:
await asyncio.sleep(3600)
if __name__ == "__main__":
asyncio.run(main())