Update routes.py

This commit is contained in:
dev 2026-10-31 09:36:13 +00:00
parent 49494e5399
commit 98997e26ca

366
routes.py
View File

@ -7,9 +7,13 @@ import time
from flask import jsonify, request from flask import jsonify, request
from features import ( from features import (
DM_LAYOUT_MODES,
LAST_ACTIVE_VISIBILITIES,
PAGE_TYPES, PAGE_TYPES,
PAGE_VISIBILITIES, PAGE_VISIBILITIES,
PRESENCE_MODES,
PROFILE_BIO_MODES, PROFILE_BIO_MODES,
PROFILE_VISIBILITIES,
REACTION_EMOJI_RE, REACTION_EMOJI_RE,
TEAM_MEMBER_ROLES, TEAM_MEMBER_ROLES,
THEME_MODES, THEME_MODES,
@ -135,6 +139,9 @@ def register_routes(app, ctx):
user["id"] if user else None, user["id"] if user else None,
) )
def build_local_stub_share_url(kind: str, object_id: int) -> str:
return f"/shared/{kind}/{object_id}"
def build_room_helper_call_id(room) -> str: def build_room_helper_call_id(room) -> str:
return f"room-{room['slug']}" return f"room-{room['slug']}"
@ -1973,6 +1980,86 @@ def register_routes(app, ctx):
} }
) )
@app.route("/api/messages/<int:message_id>/bookmark", methods=["POST"])
def api_bookmark_message(message_id: int):
if not csrf_valid():
return jsonify({"error": "Security token expired.", "code": "csrf"}), 403
user, error = require_user()
if error:
return error
return jsonify({"status": "stubbed", "message_id": message_id})
@app.route("/api/messages/<int:message_id>/bookmark", methods=["DELETE"])
def api_unbookmark_message(message_id: int):
if not csrf_valid():
return jsonify({"error": "Security token expired.", "code": "csrf"}), 403
user, error = require_user()
if error:
return error
return jsonify({"status": "stubbed", "message_id": message_id})
@app.route("/api/bookmarks")
def api_list_bookmarks():
user, error = require_user()
if error:
return error
return jsonify({"messages": []})
@app.route("/api/messages/<int:message_id>/pin", methods=["POST"])
def api_pin_message(message_id: int):
if not csrf_valid():
return jsonify({"error": "Security token expired.", "code": "csrf"}), 403
user, error = require_user()
if error:
return error
return jsonify({"status": "stubbed", "message_id": message_id})
@app.route("/api/messages/<int:message_id>/pin", methods=["DELETE"])
def api_unpin_message(message_id: int):
if not csrf_valid():
return jsonify({"error": "Security token expired.", "code": "csrf"}), 403
user, error = require_user()
if error:
return error
return jsonify({"status": "stubbed", "message_id": message_id})
@app.route("/api/pins")
def api_list_pins():
user, error = require_user()
if error:
return error
return jsonify({"messages": []})
@app.route("/api/messages/<int:message_id>/share", methods=["POST"])
def api_share_message(message_id: int):
if not csrf_valid():
return jsonify({"error": "Security token expired.", "code": "csrf"}), 403
user, error = require_user()
if error:
return error
return jsonify(
{
"share": {
"url": build_local_stub_share_url("message", message_id)
}
}
)
@app.route("/api/attachments/<int:attachment_id>/share", methods=["POST"])
def api_share_attachment(attachment_id: int):
if not csrf_valid():
return jsonify({"error": "Security token expired.", "code": "csrf"}), 403
user, error = require_user()
if error:
return error
return jsonify(
{
"share": {
"url": build_local_stub_share_url("attachment", attachment_id)
}
}
)
@app.route("/api/presence", methods=["POST"]) @app.route("/api/presence", methods=["POST"])
def api_presence_ping(): def api_presence_ping():
if not csrf_valid(): if not csrf_valid():
@ -4021,6 +4108,138 @@ def register_routes(app, ctx):
audit("page_deleted", user["id"], "page", str(page["id"]), f"{team_slug}/{page_slug}") audit("page_deleted", user["id"], "page", str(page["id"]), f"{team_slug}/{page_slug}")
return jsonify({"status": "deleted"}) return jsonify({"status": "deleted"})
@app.route("/api/teams/<string:team_slug>/pages/<string:page_slug>/versions")
def api_list_page_versions(team_slug: str, page_slug: str):
user = current_user()
with get_db() as db:
page = db.execute(
f"""
{team_page_select()}
WHERE t.slug = ?
AND p.slug = ?
AND p.is_active = 1
AND t.is_active = 1
""",
(team_slug, page_slug),
).fetchone()
if not page:
return jsonify({"error": "Page not found."}), 404
permitted = bool(
page["visibility"] == "public"
or (user and (user["role"] in {"moderator", "admin"} or is_team_member(page["team_id"], user["id"])))
)
if not permitted:
if not user:
return jsonify({"error": "Login required.", "code": "login_required"}), 401
return jsonify({"error": "Forbidden.", "code": "forbidden"}), 403
return jsonify({"versions": []})
@app.route("/api/page-versions/<int:version_id>")
def api_get_page_version(version_id: int):
return jsonify(
{
"version": {
"id": version_id,
"version_number": 1,
"title": "version unavailable",
"body_html": "<p>page version history is not enabled on this server.</p>",
}
}
)
@app.route("/api/page-versions/<int:version_id>/restore", methods=["POST"])
def api_restore_page_version(version_id: int):
if not csrf_valid():
return jsonify({"error": "Security token expired.", "code": "csrf"}), 403
return jsonify({"error": "Page version restore is not enabled on this server."}), 501
@app.route("/api/teams/<string:team_slug>/pages/<string:page_slug>/comments")
def api_list_page_comments(team_slug: str, page_slug: str):
user = current_user()
with get_db() as db:
page = db.execute(
f"""
{team_page_select()}
WHERE t.slug = ?
AND p.slug = ?
AND p.is_active = 1
AND t.is_active = 1
""",
(team_slug, page_slug),
).fetchone()
if not page:
return jsonify({"error": "Page not found."}), 404
permitted = bool(
page["visibility"] == "public"
or (user and (user["role"] in {"moderator", "admin"} or is_team_member(page["team_id"], user["id"])))
)
if not permitted:
if not user:
return jsonify({"error": "Login required.", "code": "login_required"}), 401
return jsonify({"error": "Forbidden.", "code": "forbidden"}), 403
return jsonify({"comments": []})
@app.route("/api/teams/<string:team_slug>/pages/<string:page_slug>/comments", methods=["POST"])
def api_create_page_comment(team_slug: str, page_slug: str):
if not csrf_valid():
return jsonify({"error": "Security token expired.", "code": "csrf"}), 403
user, error = require_writable_user()
if error:
return error
return jsonify({"error": "Page comments are not enabled on this server."}), 501
@app.route("/api/page-comments/<int:comment_id>", methods=["DELETE"])
def api_delete_page_comment(comment_id: int):
if not csrf_valid():
return jsonify({"error": "Security token expired.", "code": "csrf"}), 403
user, error = require_writable_user()
if error:
return error
return jsonify({"error": "Page comments are not enabled on this server."}), 501
@app.route("/api/teams/<string:team_slug>/pages/<string:page_slug>/share", methods=["POST"])
def api_create_page_share(team_slug: str, page_slug: str):
if not csrf_valid():
return jsonify({"error": "Security token expired.", "code": "csrf"}), 403
user, error = require_user()
if error:
return error
with get_db() as db:
page = db.execute(
"""
SELECT p.id
FROM pages AS p
JOIN teams AS t
ON t.id = p.team_id
WHERE t.slug = ?
AND p.slug = ?
AND p.is_active = 1
AND t.is_active = 1
""",
(team_slug, page_slug),
).fetchone()
if not page:
return jsonify({"error": "Page not found."}), 404
return jsonify(
{
"share": {
"url": build_local_stub_share_url("page", page["id"])
}
}
)
@app.route("/api/profile") @app.route("/api/profile")
def api_get_my_profile(): def api_get_my_profile():
user, error = require_user() user, error = require_user()
@ -4036,7 +4255,8 @@ def register_routes(app, ctx):
role, role,
status, status,
restricted_until, restricted_until,
created_at created_at,
last_login_at
FROM users FROM users
WHERE id = ? WHERE id = ?
""", """,
@ -4070,7 +4290,9 @@ def register_routes(app, ctx):
profile_background_color, profile_background_color,
profile_banner_color, profile_banner_color,
profile_visibility, profile_visibility,
last_active_visibility last_active_visibility,
dm_layout_mode,
show_presence_badge
FROM user_settings FROM user_settings
WHERE user_id = ? WHERE user_id = ?
""", """,
@ -4081,7 +4303,6 @@ def register_routes(app, ctx):
@app.route("/api/profile", methods=["PATCH"]) @app.route("/api/profile", methods=["PATCH"])
def api_update_my_profile(): def api_update_my_profile():
if not csrf_valid(): if not csrf_valid():
return jsonify({"error": "Security token expired.", "code": "csrf"}), 403 return jsonify({"error": "Security token expired.", "code": "csrf"}), 403
@ -4104,6 +4325,16 @@ def register_routes(app, ctx):
bio_mode = str(data.get("bio_mode", "txt")).strip().lower() bio_mode = str(data.get("bio_mode", "txt")).strip().lower()
website = str(data.get("website", "")).strip() website = str(data.get("website", "")).strip()
location = str(data.get("location", "")).strip() location = str(data.get("location", "")).strip()
status_message = str(data.get("status_message", "")).strip()
banner_attachment_id = data.get("banner_attachment_id")
if banner_attachment_id not in (None, ""):
try:
banner_attachment_id = int(banner_attachment_id)
except (TypeError, ValueError):
return jsonify({"error": "Invalid banner attachment."}), 400
else:
banner_attachment_id = None
if len(display_name) > MAX_DISPLAY_NAME_LENGTH: if len(display_name) > MAX_DISPLAY_NAME_LENGTH:
return jsonify({"error": "Display name is too long."}), 400 return jsonify({"error": "Display name is too long."}), 400
@ -4117,6 +4348,23 @@ def register_routes(app, ctx):
return jsonify({"error": "Website is too long."}), 400 return jsonify({"error": "Website is too long."}), 400
if len(location) > MAX_LOCATION_LENGTH: if len(location) > MAX_LOCATION_LENGTH:
return jsonify({"error": "Location is too long."}), 400 return jsonify({"error": "Location is too long."}), 400
if len(status_message) > 160:
return jsonify({"error": "Status message is too long."}), 400
if banner_attachment_id is not None:
with get_db() as db:
attachment = db.execute(
"""
SELECT id, uploader_user_id
FROM attachments
WHERE id = ?
""",
(banner_attachment_id,),
).fetchone()
if not attachment:
return jsonify({"error": "Banner attachment not found."}), 404
if attachment["uploader_user_id"] != user["id"]:
return jsonify({"error": "You can only use your own uploaded image."}), 403
db_write( db_write(
""" """
@ -4128,6 +4376,8 @@ def register_routes(app, ctx):
bio_mode = ?, bio_mode = ?,
website = ?, website = ?,
location = ?, location = ?,
status_message = ?,
banner_attachment_id = ?,
updated_at = ? updated_at = ?
WHERE user_id = ? WHERE user_id = ?
""", """,
@ -4138,6 +4388,8 @@ def register_routes(app, ctx):
bio_mode, bio_mode,
website, website,
location, location,
status_message,
banner_attachment_id,
now_iso(), now_iso(),
user["id"], user["id"],
), ),
@ -4152,7 +4404,8 @@ def register_routes(app, ctx):
role, role,
status, status,
restricted_until, restricted_until,
created_at created_at,
last_login_at
FROM users FROM users
WHERE id = ? WHERE id = ?
""", """,
@ -4178,7 +4431,6 @@ def register_routes(app, ctx):
(user["id"],), (user["id"],),
).fetchone() ).fetchone()
settings_row = db.execute( settings_row = db.execute(
""" """
SELECT SELECT
@ -4187,7 +4439,9 @@ def register_routes(app, ctx):
profile_background_color, profile_background_color,
profile_banner_color, profile_banner_color,
profile_visibility, profile_visibility,
last_active_visibility last_active_visibility,
dm_layout_mode,
show_presence_badge
FROM user_settings FROM user_settings
WHERE user_id = ? WHERE user_id = ?
""", """,
@ -4210,7 +4464,8 @@ def register_routes(app, ctx):
role, role,
status, status,
restricted_until, restricted_until,
created_at created_at,
last_login_at
FROM users FROM users
WHERE username = ? WHERE username = ?
""", """,
@ -4247,7 +4502,9 @@ def register_routes(app, ctx):
profile_background_color, profile_background_color,
profile_banner_color, profile_banner_color,
profile_visibility, profile_visibility,
last_active_visibility last_active_visibility,
dm_layout_mode,
show_presence_badge
FROM user_settings FROM user_settings
WHERE user_id = ? WHERE user_id = ?
""", """,
@ -4274,6 +4531,9 @@ def register_routes(app, ctx):
profile_banner_color, profile_banner_color,
profile_visibility, profile_visibility,
last_active_visibility, last_active_visibility,
presence_mode,
dm_layout_mode,
show_presence_badge,
updated_at updated_at
FROM user_settings FROM user_settings
WHERE user_id = ? WHERE user_id = ?
@ -4281,7 +4541,6 @@ def register_routes(app, ctx):
(user["id"],), (user["id"],),
).fetchone() ).fetchone()
return jsonify( return jsonify(
{ {
"settings": { "settings": {
@ -4293,6 +4552,9 @@ def register_routes(app, ctx):
"profile_banner_color": row["profile_banner_color"], "profile_banner_color": row["profile_banner_color"],
"profile_visibility": row["profile_visibility"], "profile_visibility": row["profile_visibility"],
"last_active_visibility": row["last_active_visibility"], "last_active_visibility": row["last_active_visibility"],
"presence_mode": row["presence_mode"],
"dm_layout_mode": row["dm_layout_mode"],
"show_presence_badge": bool(row["show_presence_badge"]),
"updated_at": row["updated_at"], "updated_at": row["updated_at"],
} }
} }
@ -4320,15 +4582,36 @@ def register_routes(app, ctx):
name_color = data.get("name_color") name_color = data.get("name_color")
custom_css = str(data.get("custom_css", "")) custom_css = str(data.get("custom_css", ""))
theme_mode = str(data.get("theme_mode", "dark")).strip().lower() theme_mode = str(data.get("theme_mode", "dark")).strip().lower()
profile_background_color = data.get("profile_background_color")
profile_banner_color = data.get("profile_banner_color")
profile_visibility = str(data.get("profile_visibility", "public")).strip().lower()
last_active_visibility = str(data.get("last_active_visibility", "everyone")).strip().lower()
presence_mode = str(data.get("presence_mode", "online")).strip().lower()
dm_layout_mode = str(data.get("dm_layout_mode", "cards")).strip().lower()
show_presence_badge_raw = data.get("show_presence_badge", True)
if not valid_hex_color(accent_color): if not valid_hex_color(accent_color):
return jsonify({"error": "Invalid accent color."}), 400 return jsonify({"error": "Invalid accent color."}), 400
if name_color not in (None, "") and not valid_hex_color(name_color): if name_color not in (None, "") and not valid_hex_color(name_color):
return jsonify({"error": "Invalid name color."}), 400 return jsonify({"error": "Invalid name color."}), 400
if profile_background_color not in (None, "") and not valid_hex_color(profile_background_color):
return jsonify({"error": "Invalid profile background color."}), 400
if profile_banner_color not in (None, "") and not valid_hex_color(profile_banner_color):
return jsonify({"error": "Invalid profile banner color."}), 400
if len(custom_css) > MAX_CUSTOM_CSS_LENGTH: if len(custom_css) > MAX_CUSTOM_CSS_LENGTH:
return jsonify({"error": "Custom CSS is too long."}), 400 return jsonify({"error": "Custom CSS is too long."}), 400
if theme_mode not in THEME_MODES: if theme_mode not in THEME_MODES:
return jsonify({"error": "Invalid theme mode."}), 400 return jsonify({"error": "Invalid theme mode."}), 400
if profile_visibility not in PROFILE_VISIBILITIES:
return jsonify({"error": "Invalid profile visibility."}), 400
if last_active_visibility not in LAST_ACTIVE_VISIBILITIES:
return jsonify({"error": "Invalid last active visibility."}), 400
if presence_mode not in PRESENCE_MODES:
return jsonify({"error": "Invalid presence mode."}), 400
if dm_layout_mode not in DM_LAYOUT_MODES:
return jsonify({"error": "Invalid dm layout mode."}), 400
show_presence_badge = 1 if bool(show_presence_badge_raw) else 0
db_write( db_write(
""" """
@ -4342,6 +4625,9 @@ def register_routes(app, ctx):
profile_banner_color = ?, profile_banner_color = ?,
profile_visibility = ?, profile_visibility = ?,
last_active_visibility = ?, last_active_visibility = ?,
presence_mode = ?,
dm_layout_mode = ?,
show_presence_badge = ?,
updated_at = ? updated_at = ?
WHERE user_id = ? WHERE user_id = ?
""", """,
@ -4350,10 +4636,13 @@ def register_routes(app, ctx):
normalized_hex_color(name_color) if name_color else None, normalized_hex_color(name_color) if name_color else None,
custom_css, custom_css,
theme_mode, theme_mode,
data.get("profile_background_color"), normalized_hex_color(profile_background_color) if profile_background_color else None,
data.get("profile_banner_color"), normalized_hex_color(profile_banner_color) if profile_banner_color else None,
str(data.get("profile_visibility", "public")).strip().lower(), profile_visibility,
str(data.get("last_active_visibility", "mutuals")).strip().lower(), last_active_visibility,
presence_mode,
dm_layout_mode,
show_presence_badge,
now_iso(), now_iso(),
user["id"], user["id"],
), ),
@ -4371,6 +4660,9 @@ def register_routes(app, ctx):
profile_banner_color, profile_banner_color,
profile_visibility, profile_visibility,
last_active_visibility, last_active_visibility,
presence_mode,
dm_layout_mode,
show_presence_badge,
updated_at updated_at
FROM user_settings FROM user_settings
WHERE user_id = ? WHERE user_id = ?
@ -4379,7 +4671,53 @@ def register_routes(app, ctx):
).fetchone() ).fetchone()
audit("settings_updated", user["id"], "user", str(user["id"])) audit("settings_updated", user["id"], "user", str(user["id"]))
return jsonify({"settings": dict(row)}) return jsonify(
{
"settings": {
**dict(row),
"show_presence_badge": bool(row["show_presence_badge"]),
}
}
)
@app.route("/api/scheduled-messages", methods=["POST"])
def api_create_scheduled_message():
if not csrf_valid():
return jsonify({"error": "Security token expired.", "code": "csrf"}), 403
user, error = require_writable_user()
if error:
return error
try:
data = safe_json()
except ValueError as error:
return jsonify({"error": str(error)}), 400
text = str(data.get("text", "")).strip()
deliver_at = str(data.get("deliver_at", "")).strip()
scope_kind = str(data.get("scope_kind", "")).strip()
scope_id = data.get("scope_id")
if not text:
return jsonify({"error": "Scheduled message text is required."}), 400
if len(text) > MAX_MESSAGE_LENGTH:
return jsonify({"error": "Scheduled message is too long."}), 400
if not deliver_at:
return jsonify({"error": "deliver_at is required."}), 400
return jsonify(
{
"status": "stubbed",
"scheduled_message": {
"id": 0,
"text": text,
"deliver_at": deliver_at,
"scope_kind": scope_kind,
"scope_id": scope_id,
}
}
), 201
@app.route("/api/dms") @app.route("/api/dms")
def api_list_dms(): def api_list_dms():