Add activity push notifications and registration patches
This commit is contained in:
+50
-17
@@ -35,6 +35,8 @@ from fastapi.exception_handlers import request_validation_exception_handler
|
||||
from feature_schema import FEATURE_SCHEMA
|
||||
import push_devices
|
||||
import bug_reporter
|
||||
import notifications
|
||||
import community_badges
|
||||
from i18n import (
|
||||
LANGUAGE_COOKIE, current_language, current_page, gettext as _,
|
||||
language_url, safe_return_path, format_time, format_datetime,
|
||||
@@ -100,7 +102,9 @@ BADGE_DEFINITIONS = (
|
||||
("founder", "Gründer", "⚔️", None, "Von Anfang an dabei und MetalCircle aufgebaut", "special"),
|
||||
("admin", "Admin", "🏴☠️", None, "Verantwortung für MetalCircle", "special"),
|
||||
("captns_mate", "Captns Mate", "☠️", None, "Die treue Gefährtin des Captains", "special"),
|
||||
("alpha_tester", "Alpha Tester", "👑", None, "Schon in der Alpha dabei – unsere frühesten Tester", "beta"),
|
||||
("beta_tester", "Beta Tester", "🧪", None, "In der Beta dabei", "beta"),
|
||||
("early_bird", "Early Bird", "🐦", None, "Früh Teil der MetalCircle-Community geworden", "beta"),
|
||||
("first_gig", "Erster Gig", "🎸", 1, "Dein erstes besuchtes Konzert", "attendance"),
|
||||
("regular", "Stammgast", "🤘", 5, "5 Konzerte am selben Veranstaltungsort besucht", "venue"),
|
||||
("ten_gigs", "10 Gigs", "🔥", 10, "10 besuchte Konzerte", "attendance"),
|
||||
@@ -121,7 +125,6 @@ VENUE_BADGE_CODES = tuple(
|
||||
badge_code for badge_code, _name, _icon, _threshold, _description, category in BADGE_DEFINITIONS
|
||||
if category == "venue"
|
||||
)
|
||||
BETA_REGISTRATION_DEADLINE = datetime(2026, 9, 16)
|
||||
BADGE_BY_CODE = {
|
||||
badge_code: (name, icon, threshold, description, category)
|
||||
for badge_code, name, icon, threshold, description, category in BADGE_DEFINITIONS
|
||||
@@ -527,7 +530,14 @@ def ensure_schema():
|
||||
@asynccontextmanager
|
||||
async def lifespan(_app: FastAPI):
|
||||
ensure_schema()
|
||||
yield
|
||||
with get_db_connection() as connection:
|
||||
community_badges.reconcile(connection.cursor())
|
||||
worker = notifications.PushWorker(get_db_connection)
|
||||
worker.start()
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
worker.stop()
|
||||
|
||||
|
||||
app = FastAPI(title="MetalCircle", lifespan=lifespan)
|
||||
@@ -638,9 +648,14 @@ async def localize_request(request: Request, call_next):
|
||||
|
||||
|
||||
@app.get("/language/{language}")
|
||||
def change_language(language: str, next: str = "/"):
|
||||
def change_language(request: Request, language: str, next: str = "/"):
|
||||
if language not in {"de", "en"}:
|
||||
return HTMLResponse(_("Ungültige Sprache."), status_code=400)
|
||||
if request.cookies.get(SESSION_COOKIE):
|
||||
user = get_current_user(request)
|
||||
if user:
|
||||
with get_db_connection() as connection:
|
||||
notifications.save_language(connection, user['id'], language)
|
||||
response = RedirectResponse(safe_return_path(next), status_code=303)
|
||||
response.set_cookie(
|
||||
LANGUAGE_COOKIE, language, max_age=365 * 24 * 60 * 60,
|
||||
@@ -1494,15 +1509,7 @@ def grant_earned_badges(user_id: int, stats: dict, registered_at):
|
||||
|
||||
with get_db_connection() as connection:
|
||||
with connection.cursor() as cursor:
|
||||
if registered_at < BETA_REGISTRATION_DEADLINE:
|
||||
cursor.execute(
|
||||
"""
|
||||
INSERT INTO user_badges (user_id, badge_code)
|
||||
VALUES (%s, 'beta_tester')
|
||||
ON CONFLICT (user_id, badge_code) DO NOTHING
|
||||
""",
|
||||
(user_id,),
|
||||
)
|
||||
community_badges.reconcile(cursor, user_id)
|
||||
|
||||
for badge_code, _name, _icon, threshold, _description, category in BADGE_DEFINITIONS:
|
||||
if category == "attendance" and threshold is not None and stats["total"] >= threshold:
|
||||
@@ -1545,7 +1552,10 @@ def load_badge_assets():
|
||||
with get_db_connection() as connection:
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute("SELECT badge_code, path FROM badge_assets")
|
||||
return {row[0]: row[1] for row in cursor.fetchall()}
|
||||
assets = {'alpha_tester': '/static/images/patch-alpha-tester.svg',
|
||||
'early_bird': '/static/images/patch-early-bird.svg'}
|
||||
assets.update({row[0]: row[1] for row in cursor.fetchall()})
|
||||
return assets
|
||||
|
||||
|
||||
def load_profile(username: str):
|
||||
@@ -2399,6 +2409,8 @@ def register_user(
|
||||
user_id,
|
||||
invite_id
|
||||
))
|
||||
community_badges.reconcile(cursor, user_id)
|
||||
notifications.save_language(connection, user_id, current_language.get())
|
||||
|
||||
connection.commit()
|
||||
|
||||
@@ -2532,6 +2544,8 @@ def login(
|
||||
with get_db_connection() as connection:
|
||||
connection.execute('DELETE FROM sessions WHERE token_hash=%s', (hash_token(old_token),))
|
||||
connection.commit()
|
||||
with get_db_connection() as connection:
|
||||
notifications.save_language(connection, row[0], current_language.get())
|
||||
response = RedirectResponse(next_path, status_code=303)
|
||||
return attach_session(response, create_session(row[0]))
|
||||
|
||||
@@ -2735,6 +2749,8 @@ def render_profile(
|
||||
}
|
||||
badges.sort(key=lambda badge: badge["sort_key"])
|
||||
|
||||
with get_db_connection() as connection:
|
||||
push_preferences = notifications.preferences(connection, viewer['id'])
|
||||
template = templates.get_template("profile.html")
|
||||
return HTMLResponse(
|
||||
template.render(
|
||||
@@ -2750,6 +2766,7 @@ def render_profile(
|
||||
form_error=form_error,
|
||||
form_success=form_success,
|
||||
instagram_input=instagram_input,
|
||||
push_preferences=push_preferences,
|
||||
),
|
||||
status_code=status_code,
|
||||
)
|
||||
@@ -2766,6 +2783,8 @@ def own_profile(request: Request, saved: str = ""):
|
||||
messages.append(_("Profilbild aktualisiert"))
|
||||
if "instagram" in saved_items:
|
||||
messages.append(_("Instagram verknüpft"))
|
||||
if "notifications" in saved_items:
|
||||
messages.append(_("Benachrichtigungseinstellungen gespeichert"))
|
||||
return render_profile(
|
||||
request,
|
||||
user["username"],
|
||||
@@ -2901,11 +2920,14 @@ def export_profile_data(request: Request):
|
||||
)
|
||||
badges = cursor.fetchall()
|
||||
|
||||
push_preferences = notifications.preferences(connection, user_id)
|
||||
|
||||
def rows_to_dicts(rows, keys):
|
||||
return [dict(zip(keys, row)) for row in rows]
|
||||
|
||||
data = {
|
||||
"export_version": 1,
|
||||
"notification_preferences": push_preferences,
|
||||
"exported_at": datetime.now(),
|
||||
"account": dict(zip(
|
||||
("id", "username", "email", "display_name", "avatar_path",
|
||||
@@ -3062,10 +3084,13 @@ def send_friend_request(request: Request, username: str):
|
||||
"""
|
||||
INSERT INTO friendships (requester_id, addressee_id)
|
||||
VALUES (%s, %s)
|
||||
ON CONFLICT DO NOTHING
|
||||
ON CONFLICT DO NOTHING RETURNING id
|
||||
""",
|
||||
(user["id"], profile["id"]),
|
||||
)
|
||||
created = cursor.fetchone()
|
||||
if created:
|
||||
notifications.enqueue(cursor, 'friend_request', user['id'], profile['id'], created[0])
|
||||
connection.commit()
|
||||
return RedirectResponse(f"/users/{profile['username']}", status_code=303)
|
||||
|
||||
@@ -3319,9 +3344,10 @@ def send_message(request: Request, username: str, body: str = Form(...)):
|
||||
if not partner:
|
||||
return HTMLResponse(_("Chat nicht erlaubt. Er ist für Freunde und Unterhaltungen mit Admins verfügbar."), status_code=403)
|
||||
cursor.execute(
|
||||
"INSERT INTO direct_messages (sender_id, recipient_id, body) VALUES (%s, %s, %s)",
|
||||
"INSERT INTO direct_messages (sender_id, recipient_id, body) VALUES (%s, %s, %s) RETURNING id",
|
||||
(user["id"], partner["id"], body),
|
||||
)
|
||||
notifications.enqueue(cursor, 'direct_message', user['id'], partner['id'], cursor.fetchone()[0])
|
||||
connection.commit()
|
||||
return RedirectResponse(f"/messages/{partner['username']}#latest", status_code=303)
|
||||
|
||||
@@ -4026,10 +4052,13 @@ async def create_concert(
|
||||
INSERT INTO event_invitations (concert_id, user_id, invited_by)
|
||||
SELECT %s, id, %s FROM users
|
||||
WHERE id = ANY(%s) AND id <> %s
|
||||
ON CONFLICT (concert_id, user_id) DO NOTHING
|
||||
ON CONFLICT (concert_id, user_id) DO NOTHING RETURNING user_id
|
||||
""",
|
||||
(concert_id, user["id"], invited_user_ids, user["id"]),
|
||||
)
|
||||
for invited_id, in cursor.fetchall():
|
||||
notifications.enqueue(cursor, 'event_invitation', user['id'], invited_id, concert_id,
|
||||
'invitation:' + uuid.uuid4().hex)
|
||||
|
||||
connection.commit()
|
||||
|
||||
@@ -4257,10 +4286,13 @@ async def edit_concert(
|
||||
"""
|
||||
INSERT INTO event_invitations (concert_id, user_id, invited_by)
|
||||
SELECT %s, id, %s FROM users WHERE id = ANY(%s) AND id <> %s
|
||||
ON CONFLICT (concert_id, user_id) DO NOTHING
|
||||
ON CONFLICT (concert_id, user_id) DO NOTHING RETURNING user_id
|
||||
""",
|
||||
(concert_id, user["id"], invited_user_ids, user["id"]),
|
||||
)
|
||||
for invited_id, in cursor.fetchall():
|
||||
notifications.enqueue(cursor, 'event_invitation', user['id'], invited_id, concert_id,
|
||||
'invitation:' + uuid.uuid4().hex)
|
||||
elif can_manage_event_access(user, concert):
|
||||
cursor.execute("DELETE FROM event_invitations WHERE concert_id = %s", (concert_id,))
|
||||
if can_edit_title(user, concert):
|
||||
@@ -5115,3 +5147,4 @@ def search_venues(q: str):
|
||||
|
||||
push_devices.register_routes(app, get_db_connection, get_current_user, SESSION_COOKIE)
|
||||
bug_reporter.register_routes(app, templates, get_db_connection, get_current_user)
|
||||
notifications.register_routes(app, get_db_connection, get_current_user, SESSION_COOKIE)
|
||||
|
||||
Reference in New Issue
Block a user