643 lines
18 KiB
Python
643 lines
18 KiB
Python
import json
|
|
import re
|
|
import time
|
|
from typing import Optional
|
|
|
|
from config import utc_now
|
|
from db import get_db
|
|
from auth import user_payload
|
|
|
|
ROOM_SLUG_RE = re.compile(r"^[a-z0-9](?:[a-z0-9-]{0,46}[a-z0-9])?$")
|
|
|
|
|
|
MESSAGE_SELECT = """
|
|
SELECT
|
|
m.id,
|
|
m.room_id,
|
|
m.channel_id,
|
|
m.direct_conversation_id,
|
|
m.user_id,
|
|
m.username,
|
|
m.text,
|
|
m.created_at,
|
|
m.attachment_id,
|
|
m.parent_message_id,
|
|
(
|
|
SELECT COUNT(*)
|
|
FROM messages AS thread_reply
|
|
WHERE thread_reply.parent_message_id = m.id
|
|
) AS thread_reply_count,
|
|
a.original_name AS attachment_original_name,
|
|
a.mime_type AS attachment_mime_type,
|
|
a.size_bytes AS attachment_size_bytes,
|
|
u.role AS user_role,
|
|
us.name_color AS user_name_color
|
|
FROM messages AS m
|
|
JOIN users AS u
|
|
ON u.id = m.user_id
|
|
LEFT JOIN user_settings AS us
|
|
ON us.user_id = u.id
|
|
LEFT JOIN attachments AS a
|
|
ON a.id = m.attachment_id
|
|
"""
|
|
|
|
|
|
def team_page_select() -> str:
|
|
has_parent_page = False
|
|
try:
|
|
with get_db() as db:
|
|
rows = db.execute("PRAGMA table_info(pages)").fetchall()
|
|
has_parent_page = any(row["name"] == "parent_page_id" for row in rows)
|
|
except Exception:
|
|
has_parent_page = False
|
|
|
|
if has_parent_page:
|
|
return """
|
|
SELECT
|
|
p.id,
|
|
p.team_id,
|
|
p.channel_id,
|
|
p.parent_page_id,
|
|
parent.slug AS parent_page_slug,
|
|
parent.title AS parent_page_title,
|
|
p.slug,
|
|
p.title,
|
|
p.body,
|
|
p.visibility,
|
|
p.page_type,
|
|
p.created_by_user_id,
|
|
p.updated_by_user_id,
|
|
p.created_at,
|
|
p.updated_at,
|
|
p.is_active,
|
|
t.slug AS team_slug,
|
|
t.name AS team_name,
|
|
c.slug AS channel_slug,
|
|
c.name AS channel_name
|
|
FROM pages AS p
|
|
JOIN teams AS t
|
|
ON t.id = p.team_id
|
|
LEFT JOIN channels AS c
|
|
ON c.id = p.channel_id
|
|
LEFT JOIN pages AS parent
|
|
ON parent.id = p.parent_page_id
|
|
"""
|
|
|
|
return """
|
|
SELECT
|
|
p.id,
|
|
p.team_id,
|
|
p.channel_id,
|
|
NULL AS parent_page_id,
|
|
NULL AS parent_page_slug,
|
|
NULL AS parent_page_title,
|
|
p.slug,
|
|
p.title,
|
|
p.body,
|
|
p.visibility,
|
|
p.page_type,
|
|
p.created_by_user_id,
|
|
p.updated_by_user_id,
|
|
p.created_at,
|
|
p.updated_at,
|
|
p.is_active,
|
|
t.slug AS team_slug,
|
|
t.name AS team_name,
|
|
c.slug AS channel_slug,
|
|
c.name AS channel_name
|
|
FROM pages AS p
|
|
JOIN teams AS t
|
|
ON t.id = p.team_id
|
|
LEFT JOIN channels AS c
|
|
ON c.id = p.channel_id
|
|
"""
|
|
|
|
|
|
def team_slug_from_name(name: str) -> str:
|
|
return re.sub(r"[^a-z0-9-]+", "-", name.lower()).strip("-")[:48].strip("-")
|
|
|
|
|
|
def page_slug_from_title(title: str) -> str:
|
|
return re.sub(r"[^a-z0-9-]+", "-", title.lower()).strip("-")[:48].strip("-")
|
|
|
|
|
|
def room_by_id(room_id: int):
|
|
with get_db() as db:
|
|
return db.execute(
|
|
"""
|
|
SELECT
|
|
id,
|
|
slug,
|
|
name,
|
|
description,
|
|
visibility,
|
|
created_by_user_id,
|
|
is_active
|
|
FROM rooms
|
|
WHERE id = ?
|
|
AND is_active = 1
|
|
""",
|
|
(room_id,),
|
|
).fetchone()
|
|
|
|
|
|
def get_room_by_slug(slug: str):
|
|
with get_db() as db:
|
|
return db.execute(
|
|
"""
|
|
SELECT
|
|
id,
|
|
slug,
|
|
name,
|
|
description,
|
|
visibility,
|
|
created_by_user_id,
|
|
is_active
|
|
FROM rooms
|
|
WHERE slug = ?
|
|
AND is_active = 1
|
|
""",
|
|
(slug,),
|
|
).fetchone()
|
|
|
|
|
|
def room_member(room_id: int, user_id: int) -> bool:
|
|
with get_db() as db:
|
|
row = db.execute(
|
|
"""
|
|
SELECT 1
|
|
FROM room_members
|
|
WHERE room_id = ?
|
|
AND user_id = ?
|
|
""",
|
|
(room_id, user_id),
|
|
).fetchone()
|
|
return bool(row)
|
|
|
|
|
|
def room_owner(room_id: int, user_id: int) -> bool:
|
|
with get_db() as db:
|
|
row = db.execute(
|
|
"""
|
|
SELECT 1
|
|
FROM room_members
|
|
WHERE room_id = ?
|
|
AND user_id = ?
|
|
AND membership_role = 'owner'
|
|
""",
|
|
(room_id, user_id),
|
|
).fetchone()
|
|
return bool(row)
|
|
|
|
|
|
def can_access_room(room, user: Optional[dict]) -> bool:
|
|
if room["visibility"] == "public":
|
|
return True
|
|
if not user:
|
|
return False
|
|
if user["role"] in {"moderator", "admin"}:
|
|
return True
|
|
return room_member(room["id"], user["id"])
|
|
|
|
|
|
def can_write_room(room, user: Optional[dict]) -> bool:
|
|
if not user or user["status"] != "active":
|
|
return False
|
|
return can_access_room(room, user)
|
|
|
|
|
|
def can_manage_room(room, user: Optional[dict]) -> bool:
|
|
if not user:
|
|
return False
|
|
if user["role"] == "admin":
|
|
return True
|
|
return room_owner(room["id"], user["id"])
|
|
|
|
|
|
def room_payload(room, user: Optional[dict]) -> dict:
|
|
return {
|
|
"id": room["id"],
|
|
"slug": room["slug"],
|
|
"name": room["name"],
|
|
"description": room["description"],
|
|
"visibility": room["visibility"],
|
|
"can_read": can_access_room(room, user),
|
|
"can_write": can_write_room(room, user),
|
|
"can_manage": can_manage_room(room, user),
|
|
}
|
|
|
|
|
|
def get_team_by_slug(slug: str):
|
|
with get_db() as db:
|
|
return db.execute(
|
|
"""
|
|
SELECT
|
|
id,
|
|
slug,
|
|
name,
|
|
description,
|
|
created_by_user_id,
|
|
created_at,
|
|
is_active
|
|
FROM teams
|
|
WHERE slug = ?
|
|
AND is_active = 1
|
|
""",
|
|
(slug,),
|
|
).fetchone()
|
|
|
|
|
|
def get_channel_by_id(channel_id: int):
|
|
with get_db() as db:
|
|
return db.execute(
|
|
"""
|
|
SELECT
|
|
c.id,
|
|
c.team_id,
|
|
c.legacy_room_id,
|
|
c.slug,
|
|
c.name,
|
|
c.description,
|
|
c.created_by_user_id,
|
|
c.created_at,
|
|
c.is_active,
|
|
t.slug AS team_slug,
|
|
t.name AS team_name,
|
|
t.description AS team_description,
|
|
t.is_active AS team_is_active
|
|
FROM channels AS c
|
|
JOIN teams AS t
|
|
ON t.id = c.team_id
|
|
WHERE c.id = ?
|
|
AND c.is_active = 1
|
|
AND t.is_active = 1
|
|
""",
|
|
(channel_id,),
|
|
).fetchone()
|
|
|
|
|
|
def get_channel_by_slug(team_slug: str, channel_slug: str):
|
|
with get_db() as db:
|
|
return db.execute(
|
|
"""
|
|
SELECT
|
|
c.id,
|
|
c.team_id,
|
|
c.legacy_room_id,
|
|
c.slug,
|
|
c.name,
|
|
c.description,
|
|
c.created_by_user_id,
|
|
c.created_at,
|
|
c.is_active,
|
|
t.slug AS team_slug,
|
|
t.name AS team_name,
|
|
t.description AS team_description,
|
|
t.is_active AS team_is_active
|
|
FROM channels AS c
|
|
JOIN teams AS t
|
|
ON t.id = c.team_id
|
|
WHERE t.slug = ?
|
|
AND c.slug = ?
|
|
AND t.is_active = 1
|
|
AND c.is_active = 1
|
|
""",
|
|
(team_slug, channel_slug),
|
|
).fetchone()
|
|
|
|
|
|
def get_channel_by_legacy_room_id(room_id: int):
|
|
with get_db() as db:
|
|
return db.execute(
|
|
"""
|
|
SELECT
|
|
c.id,
|
|
c.team_id,
|
|
c.legacy_room_id,
|
|
c.slug,
|
|
c.name,
|
|
c.description,
|
|
c.created_by_user_id,
|
|
c.created_at,
|
|
c.is_active,
|
|
t.slug AS team_slug,
|
|
t.name AS team_name,
|
|
t.description AS team_description,
|
|
t.is_active AS team_is_active
|
|
FROM channels AS c
|
|
JOIN teams AS t
|
|
ON t.id = c.team_id
|
|
WHERE c.legacy_room_id = ?
|
|
AND c.is_active = 1
|
|
AND t.is_active = 1
|
|
""",
|
|
(room_id,),
|
|
).fetchone()
|
|
|
|
|
|
def team_member_role(team_id: int, user_id: int) -> Optional[str]:
|
|
with get_db() as db:
|
|
row = db.execute(
|
|
"""
|
|
SELECT membership_role
|
|
FROM team_members
|
|
WHERE team_id = ?
|
|
AND user_id = ?
|
|
""",
|
|
(team_id, user_id),
|
|
).fetchone()
|
|
return row["membership_role"] if row else None
|
|
|
|
|
|
def is_team_member(team_id: int, user_id: int) -> bool:
|
|
return team_member_role(team_id, user_id) is not None
|
|
|
|
|
|
def is_team_admin(team_id: int, user: Optional[dict]) -> bool:
|
|
if not user:
|
|
return False
|
|
if user["role"] == "admin":
|
|
return True
|
|
return team_member_role(team_id, user["id"]) in {"owner", "admin"}
|
|
|
|
|
|
def can_edit_team(team_id: int, user: Optional[dict]) -> bool:
|
|
return is_team_admin(team_id, user)
|
|
|
|
|
|
def can_access_team(team, user: Optional[dict]) -> bool:
|
|
if not team["is_active"]:
|
|
return False
|
|
if user and user["role"] in {"moderator", "admin"}:
|
|
return True
|
|
return bool(user and is_team_member(team["id"], user["id"]))
|
|
|
|
|
|
def legacy_room_for_channel(channel):
|
|
if not channel["legacy_room_id"]:
|
|
return None
|
|
return room_by_id(channel["legacy_room_id"])
|
|
|
|
|
|
def can_access_channel(channel, user: Optional[dict]) -> bool:
|
|
if user and user["role"] in {"moderator", "admin"}:
|
|
return True
|
|
legacy_room = legacy_room_for_channel(channel)
|
|
if legacy_room:
|
|
return can_access_room(legacy_room, user)
|
|
return bool(user and is_team_member(channel["team_id"], user["id"]))
|
|
|
|
|
|
def can_write_channel(channel, user: Optional[dict]) -> bool:
|
|
return bool(user and user["status"] == "active" and can_access_channel(channel, user))
|
|
|
|
|
|
def team_payload(team, user: Optional[dict]) -> dict:
|
|
membership_role = team_member_role(team["id"], user["id"]) if user else None
|
|
return {
|
|
"id": team["id"],
|
|
"slug": team["slug"],
|
|
"name": team["name"],
|
|
"description": team["description"],
|
|
"membership_role": membership_role,
|
|
"can_read": can_access_team(team, user),
|
|
"can_manage": is_team_admin(team["id"], user),
|
|
}
|
|
|
|
|
|
def channel_payload(channel, user: Optional[dict]) -> dict:
|
|
return {
|
|
"id": channel["id"],
|
|
"team_id": channel["team_id"],
|
|
"team_slug": channel["team_slug"],
|
|
"team_name": channel["team_name"],
|
|
"legacy_room_id": channel["legacy_room_id"],
|
|
"slug": channel["slug"],
|
|
"name": channel["name"],
|
|
"description": channel["description"],
|
|
"can_read": can_access_channel(channel, user),
|
|
"can_write": can_write_channel(channel, user),
|
|
"can_manage": is_team_admin(channel["team_id"], user),
|
|
}
|
|
|
|
|
|
def get_dm_for_user(dm_id: int, user_id: int):
|
|
with get_db() as db:
|
|
row = db.execute(
|
|
"""
|
|
SELECT
|
|
dc.id,
|
|
dc.created_at
|
|
FROM direct_conversations AS dc
|
|
JOIN direct_conversation_members AS dcm
|
|
ON dcm.direct_conversation_id = dc.id
|
|
WHERE dc.id = ?
|
|
AND dcm.user_id = ?
|
|
""",
|
|
(dm_id, user_id),
|
|
).fetchone()
|
|
return row
|
|
|
|
|
|
def get_dm_other_user(dm_id: int, user_id: int):
|
|
with get_db() as db:
|
|
row = db.execute(
|
|
"""
|
|
SELECT
|
|
u.id,
|
|
u.username,
|
|
u.role,
|
|
up.display_name,
|
|
up.status_message,
|
|
us.name_color,
|
|
us.presence_mode
|
|
FROM direct_conversation_members AS dcm
|
|
JOIN users AS u
|
|
ON u.id = dcm.user_id
|
|
LEFT JOIN user_profiles AS up
|
|
ON up.user_id = u.id
|
|
LEFT JOIN user_settings AS us
|
|
ON us.user_id = u.id
|
|
WHERE dcm.direct_conversation_id = ?
|
|
AND dcm.user_id != ?
|
|
LIMIT 1
|
|
""",
|
|
(dm_id, user_id),
|
|
).fetchone()
|
|
return row
|
|
|
|
|
|
def validate_dm_access(dm_id: int, user: Optional[dict]) -> bool:
|
|
if not user:
|
|
return False
|
|
return bool(get_dm_for_user(dm_id, user["id"]))
|
|
|
|
|
|
def dm_payload(dm_id: int, user_id: int, resolve_name_color) -> dict:
|
|
dm = get_dm_for_user(dm_id, user_id)
|
|
other = get_dm_other_user(dm_id, user_id)
|
|
return {
|
|
"id": dm["id"],
|
|
"created_at": dm["created_at"],
|
|
"other_user": {
|
|
"id": other["id"],
|
|
"username": other["username"],
|
|
"display_name": other["display_name"] or other["username"],
|
|
"role": other["role"],
|
|
"name_color": other["name_color"],
|
|
"resolved_name_color": resolve_name_color(other["role"], other["name_color"]),
|
|
"status_message": other["status_message"] or "",
|
|
"presence_mode": other["presence_mode"] or "online",
|
|
},
|
|
}
|
|
|
|
|
|
def find_or_create_dm(user_a_id: int, user_b_id: int, now_iso_func) -> int:
|
|
low_id, high_id = sorted([int(user_a_id), int(user_b_id)])
|
|
with get_db() as db:
|
|
existing = db.execute(
|
|
"""
|
|
SELECT dc.id
|
|
FROM direct_conversations AS dc
|
|
JOIN direct_conversation_members AS m1
|
|
ON m1.direct_conversation_id = dc.id AND m1.user_id = ?
|
|
JOIN direct_conversation_members AS m2
|
|
ON m2.direct_conversation_id = dc.id AND m2.user_id = ?
|
|
WHERE dc.member_pair_key = ?
|
|
LIMIT 1
|
|
""",
|
|
(low_id, high_id, f"{low_id}:{high_id}"),
|
|
).fetchone()
|
|
if existing:
|
|
return existing["id"]
|
|
|
|
with get_db() as db:
|
|
db.execute("BEGIN IMMEDIATE")
|
|
existing = db.execute(
|
|
"""
|
|
SELECT id
|
|
FROM direct_conversations
|
|
WHERE member_pair_key = ?
|
|
LIMIT 1
|
|
""",
|
|
(f"{low_id}:{high_id}",),
|
|
).fetchone()
|
|
if existing:
|
|
db.commit()
|
|
return existing["id"]
|
|
|
|
cursor = db.execute(
|
|
"""
|
|
INSERT INTO direct_conversations (
|
|
member_pair_key,
|
|
created_at
|
|
) VALUES (?, ?)
|
|
""",
|
|
(f"{low_id}:{high_id}", now_iso_func()),
|
|
)
|
|
dm_id = cursor.lastrowid
|
|
db.execute(
|
|
"""
|
|
INSERT INTO direct_conversation_members (
|
|
direct_conversation_id,
|
|
user_id,
|
|
added_at
|
|
) VALUES (?, ?, ?)
|
|
""",
|
|
(dm_id, low_id, now_iso_func()),
|
|
)
|
|
db.execute(
|
|
"""
|
|
INSERT INTO direct_conversation_members (
|
|
direct_conversation_id,
|
|
user_id,
|
|
added_at
|
|
) VALUES (?, ?, ?)
|
|
""",
|
|
(dm_id, high_id, now_iso_func()),
|
|
)
|
|
db.commit()
|
|
return dm_id
|
|
|
|
|
|
def update_presence(
|
|
db_write_func,
|
|
user_id: int,
|
|
view_name: Optional[str] = None,
|
|
room_id: Optional[int] = None,
|
|
channel_id: Optional[int] = None,
|
|
direct_conversation_id: Optional[int] = None,
|
|
) -> None:
|
|
db_write_func(
|
|
"""
|
|
INSERT INTO presence_events (
|
|
user_id,
|
|
updated_at,
|
|
view_name,
|
|
room_id,
|
|
channel_id,
|
|
direct_conversation_id
|
|
) VALUES (?, ?, ?, ?, ?, ?)
|
|
ON CONFLICT(user_id)
|
|
DO UPDATE SET
|
|
updated_at = excluded.updated_at,
|
|
view_name = excluded.view_name,
|
|
room_id = excluded.room_id,
|
|
channel_id = excluded.channel_id,
|
|
direct_conversation_id = excluded.direct_conversation_id
|
|
""",
|
|
(
|
|
user_id,
|
|
time.time(),
|
|
view_name,
|
|
room_id,
|
|
channel_id,
|
|
direct_conversation_id,
|
|
),
|
|
)
|
|
|
|
|
|
def room_presence_count(ttl_seconds: int, room_id: int) -> int:
|
|
cutoff = time.time() - ttl_seconds
|
|
with get_db() as db:
|
|
row = db.execute(
|
|
"""
|
|
SELECT COUNT(*) AS count
|
|
FROM presence_events
|
|
WHERE room_id = ?
|
|
AND updated_at >= ?
|
|
""",
|
|
(room_id, cutoff),
|
|
).fetchone()
|
|
return row["count"] if row else 0
|
|
|
|
|
|
def channel_presence_count(ttl_seconds: int, channel_id: int) -> int:
|
|
cutoff = time.time() - ttl_seconds
|
|
with get_db() as db:
|
|
row = db.execute(
|
|
"""
|
|
SELECT COUNT(*) AS count
|
|
FROM presence_events
|
|
WHERE channel_id = ?
|
|
AND updated_at >= ?
|
|
""",
|
|
(channel_id, cutoff),
|
|
).fetchone()
|
|
return row["count"] if row else 0
|
|
|
|
|
|
def dm_presence_count(ttl_seconds: int, dm_id: int) -> int:
|
|
cutoff = time.time() - ttl_seconds
|
|
with get_db() as db:
|
|
row = db.execute(
|
|
"""
|
|
SELECT COUNT(*) AS count
|
|
FROM presence_events
|
|
WHERE direct_conversation_id = ?
|
|
AND updated_at >= ?
|
|
""",
|
|
(dm_id, cutoff),
|
|
).fetchone()
|
|
return row["count"] if row else 0
|