337 lines
11 KiB
Python
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]})
|