glo/models.py

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