237 lines
7.7 KiB
Python
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
|