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//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//channels//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//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//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//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//channels//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//channels//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//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//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]})