glo/routes/dms.py
2026-11-03 00:22:48 +00:00

237 lines
7.7 KiB
Python

from .common import (
MAX_HISTORY_MESSAGES,
MAX_INCREMENTAL_MESSAGES,
MAX_MESSAGE_LENGTH,
now_iso,
csrf_valid,
rate_limited,
require_user,
require_writable_user,
audit,
safe_json,
jsonify,
request,
get_db,
build_message_payload,
resolve_name_color,
validate_dm_access,
dm_payload,
find_or_create_dm,
get_dm_other_user,
MESSAGE_SELECT,
resolve_owned_attachment,
resolve_owned_attachment_ids,
normalize_username,
list_dms_for_user,
)
def register_dm_routes(app):
@app.route("/api/dms")
def api_list_dms():
user, error = require_user()
if error:
return error
return jsonify({"dms": list_dms_for_user(user["id"])})
@app.route("/api/dms", methods=["POST"])
def api_create_or_open_dm():
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
other_user_id = data.get("user_id")
username = normalize_username(data.get("username"))
with get_db() as db:
target = None
if other_user_id is not None:
try:
parsed_id = int(other_user_id)
except (TypeError, ValueError):
return jsonify({"error": "Invalid user_id."}), 400
target = db.execute(
"""
SELECT id, username, role, status
FROM users
WHERE id = ?
""",
(parsed_id,),
).fetchone()
elif username:
target = db.execute(
"""
SELECT id, username, role, status
FROM users
WHERE username = ?
""",
(username,),
).fetchone()
if not target:
return jsonify({"error": "User not found."}), 404
if int(target["id"]) == int(user["id"]):
return jsonify({"error": "You cannot DM yourself."}), 400
if target["status"] in {"disabled", "blacklisted"}:
return jsonify({"error": "User not available."}), 400
dm_id = find_or_create_dm(user["id"], target["id"], now_iso)
audit("dm_opened", user["id"], "dm", str(dm_id), str(target["id"]))
return jsonify({"dm": dm_payload(dm_id, user["id"], resolve_name_color)}), 201
@app.route("/api/dms/<int:dm_id>/messages")
def api_get_dm_messages(dm_id: int):
user, error = require_user()
if error:
return error
if not validate_dm_access(dm_id, user):
return jsonify({"error": "DM not found."}), 404
after_raw = request.args.get("after_id", "").strip()
after_id = 0
if after_raw:
try:
after_id = max(0, int(after_raw))
except ValueError:
return jsonify({"error": "after_id must be a non-negative integer."}), 400
with get_db() as db:
if after_raw:
rows = db.execute(
f"""
{MESSAGE_SELECT}
WHERE m.direct_conversation_id = ?
AND m.parent_message_id IS NULL
AND m.id > ?
ORDER BY m.id ASC
LIMIT ?
""",
(dm_id, after_id, MAX_INCREMENTAL_MESSAGES),
).fetchall()
else:
rows = db.execute(
f"""
SELECT *
FROM (
{MESSAGE_SELECT}
WHERE m.direct_conversation_id = ?
AND m.parent_message_id IS NULL
ORDER BY m.id DESC
LIMIT ?
)
ORDER BY id ASC
""",
(dm_id, MAX_HISTORY_MESSAGES),
).fetchall()
messages = [build_message_payload(row, user) for row in rows]
latest_id = messages[-1]["id"] if messages else after_id
return jsonify(
{
"dm": dm_payload(dm_id, user["id"], resolve_name_color),
"messages": messages,
"latest_id": latest_id,
"has_more": len(messages) == MAX_INCREMENTAL_MESSAGES,
}
)
@app.route("/api/dms/<int:dm_id>/messages", methods=["POST"])
def api_send_dm_message(dm_id: int):
limited = rate_limited("send")
if limited:
return limited
if not csrf_valid():
return jsonify({"error": "Security token expired.", "code": "csrf"}), 403
user, error = require_writable_user()
if error:
return error
if not validate_dm_access(dm_id, user):
return jsonify({"error": "DM not found."}), 404
try:
data = safe_json()
except ValueError as error:
return jsonify({"error": str(error)}), 400
text = data.get("text", "")
if not isinstance(text, str):
return jsonify({"error": "Message must be text."}), 400
text = text.strip()
if len(text) > MAX_MESSAGE_LENGTH:
return jsonify({"error": "Message is too long."}), 400
attachment_ids, attachment_error = resolve_owned_attachment_ids(
data.get("attachment_ids", []),
user["id"],
)
if attachment_error:
return attachment_error
if not attachment_ids and data.get("attachment_id") is not None:
single_attachment_id, single_attachment_error = resolve_owned_attachment(
data.get("attachment_id"),
user["id"],
)
if single_attachment_error:
return single_attachment_error
attachment_ids = [single_attachment_id] if single_attachment_id else []
attachment_id = attachment_ids[0] if attachment_ids else None
if not text and not attachment_id:
return jsonify({"error": "Message cannot be empty."}), 400
with get_db() as db:
db.execute("BEGIN IMMEDIATE")
cursor = db.execute(
"""
INSERT INTO messages (
room_id,
channel_id,
direct_conversation_id,
user_id,
username,
text,
attachment_id,
parent_message_id,
created_at
) VALUES (NULL, NULL, ?, ?, ?, ?, ?, NULL, ?)
""",
(dm_id, user["id"], user["username"], text, attachment_id, now_iso()),
)
message_id = cursor.lastrowid
for position, extra_attachment_id in enumerate(attachment_ids):
db.execute(
"""
INSERT OR IGNORE INTO message_attachments (
message_id,
attachment_id,
position
) VALUES (?, ?, ?)
""",
(message_id, extra_attachment_id, position),
)
db.commit()
with get_db() as db:
row = db.execute(f"{MESSAGE_SELECT} WHERE m.id = ?", (message_id,)).fetchone()
audit("dm_message_sent", user["id"], "dm", str(dm_id), str(message_id))
return jsonify({"message": build_message_payload(row, user)}), 201