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

337 lines
11 KiB
Python

import time
from .common import (
TYPING_TTL_SECONDS,
PRESENCE_TTL_SECONDS,
csrf_valid,
rate_limited,
current_user,
require_user,
require_writable_user,
safe_json,
jsonify,
get_db,
db_write,
get_room_by_slug,
can_access_room,
can_write_room,
get_channel_by_slug,
can_access_channel,
can_write_channel,
validate_dm_access,
update_presence,
room_presence_count,
channel_presence_count,
dm_presence_count,
)
def register_presence_routes(app):
@app.route("/api/presence", methods=["POST"])
def api_presence_ping():
if not csrf_valid():
return jsonify({"error": "Security token expired.", "code": "csrf"}), 403
user, error = require_user()
if error:
return error
try:
data = safe_json()
except ValueError as error:
return jsonify({"error": str(error)}), 400
view_name = str(data.get("view", "")).strip()[:32] or None
room_id = data.get("room_id")
channel_id = data.get("channel_id")
dm_id = data.get("dm_id")
try:
room_id = int(room_id) if room_id is not None else None
except (TypeError, ValueError):
room_id = None
try:
channel_id = int(channel_id) if channel_id is not None else None
except (TypeError, ValueError):
channel_id = None
try:
dm_id = int(dm_id) if dm_id is not None else None
except (TypeError, ValueError):
dm_id = None
update_presence(db_write, user["id"], view_name, room_id, channel_id, dm_id)
return jsonify({"status": "ok"})
@app.route("/api/rooms/<string:slug>/presence")
def api_room_presence(slug: str):
room = get_room_by_slug(slug)
user = current_user()
if not room or not can_access_room(room, user):
return jsonify({"error": "Room not found."}), 404
return jsonify(
{
"room_id": room["id"],
"online_count": room_presence_count(PRESENCE_TTL_SECONDS, room["id"]),
}
)
@app.route("/api/teams/<string:team_slug>/channels/<string:channel_slug>/presence")
def api_channel_presence(team_slug: str, channel_slug: str):
channel = get_channel_by_slug(team_slug, channel_slug)
user = current_user()
if not channel or not can_access_channel(channel, user):
return jsonify({"error": "Channel not found."}), 404
return jsonify(
{
"channel_id": channel["id"],
"online_count": channel_presence_count(PRESENCE_TTL_SECONDS, channel["id"]),
}
)
@app.route("/api/dms/<int:dm_id>/presence")
def api_dm_presence(dm_id: int):
user = current_user()
if not validate_dm_access(dm_id, user):
return jsonify({"error": "DM not found."}), 404
return jsonify(
{
"dm_id": dm_id,
"online_count": dm_presence_count(PRESENCE_TTL_SECONDS, dm_id),
}
)
@app.route("/api/rooms/<string:slug>/typing", methods=["POST"])
def set_typing(slug: str):
if not csrf_valid():
return jsonify({"error": "Security token expired.", "code": "csrf"}), 403
user, error = require_writable_user()
if error:
return error
limited = rate_limited("typing")
if limited:
return limited
room = get_room_by_slug(slug)
if not room or not can_write_room(room, user):
return jsonify({"error": "You cannot use typing indicators here.", "code": "forbidden"}), 403
try:
data = safe_json()
except ValueError as error:
return jsonify({"error": str(error)}), 400
typing = bool(data.get("typing"))
if typing:
db_write(
"""
INSERT INTO typing_events (
room_id,
user_id,
updated_at
) VALUES (?, ?, ?)
ON CONFLICT(room_id, user_id)
DO UPDATE SET updated_at = excluded.updated_at
""",
(room["id"], user["id"], time.time()),
)
else:
db_write(
"DELETE FROM typing_events WHERE room_id = ? AND user_id = ?",
(room["id"], user["id"]),
)
return jsonify({"status": "ok"})
@app.route("/api/rooms/<string:slug>/typing")
def get_typing(slug: str):
room = get_room_by_slug(slug)
user = current_user()
if not room or not can_access_room(room, user):
return jsonify({"error": "Room not found."}), 404
cutoff = time.time() - TYPING_TTL_SECONDS
with get_db() as db:
db.execute("DELETE FROM typing_events WHERE updated_at < ?", (cutoff,))
rows = db.execute(
"""
SELECT
u.id,
u.username
FROM typing_events AS t
JOIN users AS u
ON u.id = t.user_id
WHERE t.room_id = ?
AND t.user_id != ?
AND t.updated_at >= ?
ORDER BY u.username COLLATE NOCASE
""",
(room["id"], user["id"] if user else -1, cutoff),
).fetchall()
db.commit()
return jsonify({"users": [{"id": row["id"], "username": row["username"]} for row in rows]})
@app.route("/api/teams/<string:team_slug>/channels/<string:channel_slug>/typing", methods=["POST"])
def api_set_channel_typing(team_slug: str, channel_slug: str):
if not csrf_valid():
return jsonify({"error": "Security token expired.", "code": "csrf"}), 403
user, error = require_writable_user()
if error:
return error
limited = rate_limited("typing")
if limited:
return limited
channel = get_channel_by_slug(team_slug, channel_slug)
if not channel:
return jsonify({"error": "Channel not found."}), 404
if not can_write_channel(channel, user):
return jsonify({"error": "You cannot use typing indicators here.", "code": "forbidden"}), 403
try:
data = safe_json()
except ValueError as error:
return jsonify({"error": str(error)}), 400
typing = bool(data.get("typing"))
if typing:
db_write(
"""
INSERT INTO channel_typing_events (
channel_id,
user_id,
updated_at
) VALUES (?, ?, ?)
ON CONFLICT(channel_id, user_id)
DO UPDATE SET updated_at = excluded.updated_at
""",
(channel["id"], user["id"], time.time()),
)
else:
db_write(
"DELETE FROM channel_typing_events WHERE channel_id = ? AND user_id = ?",
(channel["id"], user["id"]),
)
return jsonify({"status": "ok"})
@app.route("/api/teams/<string:team_slug>/channels/<string:channel_slug>/typing")
def api_get_channel_typing(team_slug: str, channel_slug: str):
channel = get_channel_by_slug(team_slug, channel_slug)
if not channel:
return jsonify({"error": "Channel not found."}), 404
user = current_user()
if not can_access_channel(channel, user):
if not user:
return jsonify({"error": "Login required.", "code": "login_required"}), 401
return jsonify({"error": "You do not have access to this channel.", "code": "forbidden"}), 403
cutoff = time.time() - TYPING_TTL_SECONDS
with get_db() as db:
db.execute("DELETE FROM channel_typing_events WHERE updated_at < ?", (cutoff,))
rows = db.execute(
"""
SELECT
u.id,
u.username
FROM channel_typing_events AS t
JOIN users AS u
ON u.id = t.user_id
WHERE t.channel_id = ?
AND t.user_id != ?
AND t.updated_at >= ?
ORDER BY u.username COLLATE NOCASE
""",
(channel["id"], user["id"] if user else -1, cutoff),
).fetchall()
db.commit()
return jsonify({"users": [{"id": row["id"], "username": row["username"]} for row in rows]})
@app.route("/api/dms/<int:dm_id>/typing", methods=["POST"])
def api_set_dm_typing(dm_id: int):
if not csrf_valid():
return jsonify({"error": "Security token expired.", "code": "csrf"}), 403
user, error = require_writable_user()
if error:
return error
limited = rate_limited("typing")
if limited:
return limited
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
typing = bool(data.get("typing"))
if typing:
db_write(
"""
INSERT INTO dm_typing_events (
direct_conversation_id,
user_id,
updated_at
) VALUES (?, ?, ?)
ON CONFLICT(direct_conversation_id, user_id)
DO UPDATE SET updated_at = excluded.updated_at
""",
(dm_id, user["id"], time.time()),
)
else:
db_write(
"""
DELETE FROM dm_typing_events
WHERE direct_conversation_id = ?
AND user_id = ?
""",
(dm_id, user["id"]),
)
return jsonify({"status": "ok"})
@app.route("/api/dms/<int:dm_id>/typing")
def api_get_dm_typing(dm_id: int):
user = current_user()
if not validate_dm_access(dm_id, user):
return jsonify({"error": "DM not found."}), 404
cutoff = time.time() - TYPING_TTL_SECONDS
with get_db() as db:
db.execute("DELETE FROM dm_typing_events WHERE updated_at < ?", (cutoff,))
rows = db.execute(
"""
SELECT
u.id,
u.username
FROM dm_typing_events AS t
JOIN users AS u
ON u.id = t.user_id
WHERE t.direct_conversation_id = ?
AND t.user_id != ?
AND t.updated_at >= ?
ORDER BY u.username COLLATE NOCASE
""",
(dm_id, user["id"], cutoff),
).fetchall()
db.commit()
return jsonify({"users": [{"id": row["id"], "username": row["username"]} for row in rows]})