Sprint 11: Freunde & Chat + Phosphor-Icon-Vollmigration
- Freundschaften (pending/accepted), Nutzersuche, Anfragen per Push - Direktnachrichten mit Polling, iMessage-Stil, Deep-Links aus Push - Alle Seiten (map, places, diary, health, dog-profile, sitting, knigge, forum, wiki, walks) vollständig auf Phosphor-Icons migriert - Wikidata-Rassen-Scraper (~833 neue Rassen, lokal gespiegelte Fotos) - TheDogAPI lokal gespiegelt (169 Rassen + Fotos) - Quiz-Result-Cards horizontal (korrekte Bildproportionen) - SW by-v89
This commit is contained in:
parent
96bd57f0ad
commit
097295c628
44 changed files with 9980 additions and 300 deletions
|
|
@ -4,12 +4,13 @@ Täglich: Gesundheits-Erinnerungen per Push versenden.
|
|||
"""
|
||||
|
||||
import logging
|
||||
from datetime import date, timedelta
|
||||
from datetime import date, datetime, timedelta
|
||||
from apscheduler.schedulers.asyncio import AsyncIOScheduler
|
||||
from apscheduler.triggers.cron import CronTrigger
|
||||
|
||||
from database import db
|
||||
from routes.push import send_push_to_user
|
||||
from routes.push import send_push_to_user, send_push_to_all
|
||||
import weather
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
|
@ -31,8 +32,53 @@ def start():
|
|||
replace_existing=True,
|
||||
misfire_grace_time=3600,
|
||||
)
|
||||
_scheduler.add_job(
|
||||
_job_weather_alert,
|
||||
CronTrigger(hour=7, minute=30), # täglich 07:30 Uhr
|
||||
id="weather_alert",
|
||||
replace_existing=True,
|
||||
misfire_grace_time=3600,
|
||||
)
|
||||
_scheduler.add_job(
|
||||
_job_milestone_check,
|
||||
CronTrigger(hour=0, minute=5), # täglich 00:05 Uhr
|
||||
id="milestone_check",
|
||||
replace_existing=True,
|
||||
misfire_grace_time=3600,
|
||||
)
|
||||
_scheduler.add_job(
|
||||
_job_import_events,
|
||||
CronTrigger(day_of_week='sun', hour=2), # jeden Sonntag 02:00 Uhr
|
||||
id="import_events",
|
||||
replace_existing=True,
|
||||
misfire_grace_time=7200,
|
||||
)
|
||||
# Einmalig beim Start (nach 10s Verzögerung) für sofortige Befüllung
|
||||
_scheduler.add_job(
|
||||
_job_import_events,
|
||||
'date',
|
||||
run_date=datetime.now() + timedelta(seconds=10),
|
||||
id="import_events_startup",
|
||||
replace_existing=True,
|
||||
)
|
||||
# Einmalig beim Start (nach 15s Verzögerung) — Rassen aus TheDogAPI befüllen
|
||||
_scheduler.add_job(
|
||||
_job_seed_breeds,
|
||||
'date',
|
||||
run_date=datetime.now() + timedelta(seconds=15),
|
||||
id="seed_breeds_startup",
|
||||
replace_existing=True,
|
||||
)
|
||||
# Einmalig beim Start (nach 45s Verzögerung) — fehlende Rassen aus Wikidata ergänzen
|
||||
_scheduler.add_job(
|
||||
_job_seed_wikidata_breeds,
|
||||
'date',
|
||||
run_date=datetime.now() + timedelta(seconds=45),
|
||||
id="seed_wikidata_startup",
|
||||
replace_existing=True,
|
||||
)
|
||||
_scheduler.start()
|
||||
logger.info("Scheduler gestartet — Health-Reminder 08:00, Giftköder-Archiv 03:00.")
|
||||
logger.info("Scheduler gestartet — Health-Reminder 08:00, Giftköder-Archiv 03:00, Wetter-Alert 07:30, Meilenstein-Check 00:05, Event-Import So 02:00, Rassen-Seed beim Start.")
|
||||
|
||||
|
||||
def stop():
|
||||
|
|
@ -122,3 +168,254 @@ async def _job_poison_archive():
|
|||
count = result.rowcount
|
||||
if count:
|
||||
logger.info(f"Giftköder-Archiv: {count} abgelaufene Meldungen archiviert.")
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# JOB: Wetter-Alarm (Hitzepfoten / Gewitter)
|
||||
# ------------------------------------------------------------------
|
||||
async def _job_weather_alert():
|
||||
"""
|
||||
Holt Tagesprognose für mehrere deutsche Städte.
|
||||
Sendet Push-Notification wenn:
|
||||
- Temperatur >= 28°C (Asphalt-Warnung für Pfoten)
|
||||
- Gewitter wahrscheinlich
|
||||
Hitze hat Vorrang: Bei Hitze wird kein Gewitter-Push mehr gesendet.
|
||||
"""
|
||||
logger.info("Wetter-Alert Job läuft")
|
||||
try:
|
||||
summary = await weather.get_weather_summary()
|
||||
except Exception as e:
|
||||
logger.error(f"Wetter-Alert: Fehler beim Abruf: {e}")
|
||||
return
|
||||
|
||||
max_temp = summary["max_temp_c"]
|
||||
thunderstorm = summary["thunderstorm"]
|
||||
|
||||
if max_temp >= 28:
|
||||
sent = send_push_to_all({
|
||||
"type": "weather_heat",
|
||||
"title": "☀️ Heißer Asphalt heute",
|
||||
"body": f"Bis {max_temp:.0f}°C heute — Asphalt kann über 50°C heiß werden. Frühmorgens oder abends gassi gehen!",
|
||||
"data": {"tag": "weather-heat"},
|
||||
})
|
||||
logger.info(f"Wetter-Alert Hitze: {max_temp:.1f}°C — {sent} Push gesendet.")
|
||||
return # Kein Gewitter-Push mehr nötig wenn Hitze bereits gemeldet
|
||||
|
||||
if thunderstorm:
|
||||
sent = send_push_to_all({
|
||||
"type": "weather_thunder",
|
||||
"title": "⛈️ Gewitter möglich",
|
||||
"body": "Heute Gewitter wahrscheinlich. Gassi-Tour früh einplanen und Hund beruhigen.",
|
||||
"data": {"tag": "weather-thunder"},
|
||||
})
|
||||
logger.info(f"Wetter-Alert Gewitter — {sent} Push gesendet.")
|
||||
return
|
||||
|
||||
logger.info("Wetter-Alert: Keine Warnung nötig heute.")
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# JOB: Geburtstags- und Monats-Meilensteine
|
||||
# Läuft täglich um 00:05 Uhr (Europe/Berlin).
|
||||
# Prüft alle Hunde mit gesetztem Geburtstag und erstellt bei Treffern
|
||||
# einen Tagebucheintrag (is_milestone=1) + Push-Notification.
|
||||
# ------------------------------------------------------------------
|
||||
async def _job_milestone_check():
|
||||
"""
|
||||
Prüft für jeden Hund mit bekanntem Geburtstag ob heute ein
|
||||
Meilenstein-Tag ist:
|
||||
- Jahrestag (1. Geburtstag, 2. Geburtstag, …)
|
||||
- Monatsjubiläum in den ersten 12 Monaten (1 Monat, 2 Monate, …, 11 Monate)
|
||||
Doppelt-Schutz: Wenn bereits ein Meilenstein-Eintrag mit demselben
|
||||
Titel für heute existiert, wird kein zweiter erstellt.
|
||||
"""
|
||||
today = date.today()
|
||||
logger.info(f"Meilenstein-Check läuft für {today}")
|
||||
|
||||
with db() as conn:
|
||||
dogs = conn.execute("""
|
||||
SELECT d.id, d.name, d.user_id, d.geburtstag
|
||||
FROM dogs d
|
||||
WHERE d.geburtstag IS NOT NULL
|
||||
AND d.geburtstag != ''
|
||||
""").fetchall()
|
||||
|
||||
created_total = 0
|
||||
|
||||
for dog in dogs:
|
||||
try:
|
||||
bday = date.fromisoformat(dog["geburtstag"])
|
||||
except ValueError:
|
||||
logger.warning(f"Meilenstein: ungültiges Geburtstag für Hund {dog['id']}: {dog['geburtstag']!r}")
|
||||
continue
|
||||
|
||||
milestone = _compute_milestone(today, bday, dog["name"])
|
||||
if milestone is None:
|
||||
continue
|
||||
|
||||
titel, text = milestone
|
||||
|
||||
with db() as conn:
|
||||
# Doppelt-Schutz: kein zweiter Eintrag am selben Tag mit gleichem Titel
|
||||
exists = conn.execute("""
|
||||
SELECT id FROM diary
|
||||
WHERE dog_id = ? AND datum = ? AND titel = ? AND is_milestone = 1
|
||||
""", (dog["id"], str(today), titel)).fetchone()
|
||||
|
||||
if exists:
|
||||
logger.info(f"Meilenstein bereits vorhanden: Hund {dog['id']} '{titel}'")
|
||||
continue
|
||||
|
||||
# Tagebucheintrag anlegen
|
||||
cur = conn.execute("""
|
||||
INSERT INTO diary (dog_id, datum, typ, titel, text, is_milestone)
|
||||
VALUES (?, ?, 'milestone', ?, ?, 1)
|
||||
""", (dog["id"], str(today), titel, text))
|
||||
entry_id = cur.lastrowid
|
||||
|
||||
# Junction-Tabelle befüllen
|
||||
conn.execute("""
|
||||
INSERT OR IGNORE INTO diary_dogs (diary_id, dog_id) VALUES (?, ?)
|
||||
""", (entry_id, dog["id"]))
|
||||
|
||||
# Push an Besitzer
|
||||
send_push_to_user(dog["user_id"], {
|
||||
"type": "milestone",
|
||||
"title": titel,
|
||||
"body": text,
|
||||
"data": {"page": "diary"},
|
||||
"tag": f"milestone-{dog['id']}-{today}",
|
||||
})
|
||||
|
||||
logger.info(f"Meilenstein erstellt: Hund {dog['id']} '{titel}' → diary_id={entry_id}")
|
||||
created_total += 1
|
||||
|
||||
logger.info(f"Meilenstein-Check fertig — {created_total} Einträge erstellt.")
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# JOB: VDH-Events importieren
|
||||
# ------------------------------------------------------------------
|
||||
async def _job_import_events():
|
||||
"""
|
||||
Scrapt Veranstaltungen von vdh.de und importiert neue Events in die DB.
|
||||
Bereits vorhandene external_ids werden übersprungen (Upsert-Logik).
|
||||
"""
|
||||
try:
|
||||
from scraper.events_vdh import fetch_vdh_events
|
||||
except ImportError as e:
|
||||
logger.error(f"Event-Import: Scraper konnte nicht geladen werden: {e}")
|
||||
return
|
||||
|
||||
try:
|
||||
events = await fetch_vdh_events()
|
||||
except Exception as e:
|
||||
logger.error(f"Event-Import: Fehler beim Scrapen: {e}")
|
||||
return
|
||||
|
||||
imported = 0
|
||||
with db() as conn:
|
||||
for ev in events:
|
||||
try:
|
||||
exists = conn.execute(
|
||||
"SELECT id FROM events WHERE external_id = ?",
|
||||
(ev['external_id'],)
|
||||
).fetchone()
|
||||
if not exists:
|
||||
conn.execute("""
|
||||
INSERT INTO events (user_id, titel, datum, ort_name, typ, link, quelle, external_id, status)
|
||||
VALUES (0, ?, ?, ?, ?, ?, 'vdh', ?, 'aktiv')
|
||||
""", (
|
||||
ev['titel'],
|
||||
ev['datum'],
|
||||
ev.get('ort_name'),
|
||||
ev['typ'],
|
||||
ev.get('link'),
|
||||
ev['external_id'],
|
||||
))
|
||||
imported += 1
|
||||
except Exception as e:
|
||||
logger.warning(f"Event-Import: Fehler beim Speichern von '{ev.get('titel')}': {e}")
|
||||
|
||||
logger.info(f"Event-Import: {imported} neue Events importiert (von {len(events)} geparsten).")
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# JOB: Rassen aus TheDogAPI seeden
|
||||
# ------------------------------------------------------------------
|
||||
async def _job_seed_breeds():
|
||||
"""Lädt alle Hunderassen von TheDogAPI und speichert sie in wiki_rassen."""
|
||||
try:
|
||||
from scraper.breeds import fetch_and_seed_breeds, mirror_breed_photos
|
||||
except ImportError as e:
|
||||
logger.error(f"Breed-Seed: Scraper konnte nicht geladen werden: {e}")
|
||||
return
|
||||
|
||||
try:
|
||||
count = await fetch_and_seed_breeds()
|
||||
logger.info(f"Breed seed job done: {count} breeds")
|
||||
mirrored = await mirror_breed_photos()
|
||||
logger.info(f"Breed photo mirror done: {mirrored} photos")
|
||||
except Exception as e:
|
||||
logger.error(f"Breed-Seed: Fehler: {e}")
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# JOB: Fehlende Rassen aus Wikidata ergänzen
|
||||
# ------------------------------------------------------------------
|
||||
async def _job_seed_wikidata_breeds():
|
||||
"""Lädt fehlende Hunderassen von Wikidata und spiegelt Fotos lokal."""
|
||||
try:
|
||||
from scraper.wikidata_breeds import fetch_and_seed_wikidata_breeds, mirror_wikidata_photos
|
||||
except ImportError as e:
|
||||
logger.error(f"Wikidata-Seed: Scraper konnte nicht geladen werden: {e}")
|
||||
return
|
||||
|
||||
try:
|
||||
count = await fetch_and_seed_wikidata_breeds()
|
||||
logger.info(f"Wikidata breed seed done: {count} neue Rassen")
|
||||
mirrored = await mirror_wikidata_photos()
|
||||
logger.info(f"Wikidata photo mirror done: {mirrored} Fotos")
|
||||
except Exception as e:
|
||||
logger.error(f"Wikidata-Seed: Fehler: {e}")
|
||||
|
||||
|
||||
def _compute_milestone(today: date, bday: date, dog_name: str):
|
||||
"""
|
||||
Gibt (titel, text) zurück wenn heute ein Meilenstein-Tag ist,
|
||||
sonst None.
|
||||
|
||||
Regeln:
|
||||
- Jahrestag (Monat + Tag stimmen überein, Jahrgang ≥ 1):
|
||||
"🎂 <Name> ist X Jahr(e) alt!"
|
||||
- Monatsjubiläum in den ersten 11 Monaten (Geburtsmonats-Tag):
|
||||
"🐾 <Name> ist heute X Monat(e) alt!"
|
||||
"""
|
||||
# Jahrestag?
|
||||
if today.month == bday.month and today.day == bday.day:
|
||||
years = today.year - bday.year
|
||||
if years <= 0:
|
||||
return None # Geburtstag selbst (Tag 0) → kein Eintrag
|
||||
years_label = f"{years} Jahr" if years == 1 else f"{years} Jahre"
|
||||
titel = f"🎂 {dog_name} ist {years_label} alt!"
|
||||
text = (
|
||||
f"Heute feiern wir {dog_name}s {years}. Geburtstag! 🐾🎉 "
|
||||
f"Herzlichen Glückwunsch zum {years_label}!"
|
||||
)
|
||||
return titel, text
|
||||
|
||||
# Monatsjubiläum (nur innerhalb des ersten Lebensjahres)?
|
||||
# today liegt im selben Monatstag wie der Geburtstag aber in einem anderen Monat.
|
||||
if today.day == bday.day:
|
||||
# Vollständige Monate seit Geburt berechnen
|
||||
months = (today.year - bday.year) * 12 + (today.month - bday.month)
|
||||
if 1 <= months <= 11:
|
||||
months_label = f"{months} Monat" if months == 1 else f"{months} Monate"
|
||||
titel = f"🐾 {dog_name} ist heute {months_label} alt!"
|
||||
text = (
|
||||
f"{dog_name} wird heute {months_label} alt — "
|
||||
f"was für ein tolles kleines Hundeleben! 🥳"
|
||||
)
|
||||
return titel, text
|
||||
|
||||
return None
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue