import html import json import re import time from typing import Optional import markdown from sanitizer import sanitize_html_fragment from db import get_db DEFAULT_ROLE_NAME_COLORS = { "admin": "#ff7b7b", "moderator": "#8ab4ff", "user": "#f5f5f7", } PROFILE_BIO_MODES = {"txt", "md", "html"} THEME_MODES = {"dark", "light"} PAGE_VISIBILITIES = {"team", "public"} PAGE_TYPES = {"project", "document", "update", "resource"} TEAM_MEMBER_ROLES = {"owner", "admin", "member"} PROFILE_VISIBILITIES = {"public", "members", "private"} LAST_ACTIVE_VISIBILITIES = {"everyone", "members", "nobody"} PRESENCE_MODES = {"online", "silent", "focus", "dnd"} DM_LAYOUT_MODES = {"cards", "compact"} REACTION_EMOJI_RE = re.compile(r"^.{1,16}$") HEX_COLOR_RE = re.compile(r"^#[0-9a-fA-F]{6}$") PAGE_LINK_RE = re.compile(r"\]\((?:/teams/[^/]+/pages/)?([a-z0-9-]{1,48})\)", re.IGNORECASE) def valid_hex_color(value: object) -> bool: return bool(isinstance(value, str) and HEX_COLOR_RE.fullmatch(value.strip())) def normalized_hex_color(value: object) -> Optional[str]: if not valid_hex_color(value): return None return str(value).strip().lower() def role_default_name_color(role: Optional[str]) -> str: return DEFAULT_ROLE_NAME_COLORS.get(str(role or "user"), DEFAULT_ROLE_NAME_COLORS["user"]) def resolve_name_color(user_role: Optional[str], explicit_color: Optional[str]) -> str: return normalized_hex_color(explicit_color) or role_default_name_color(user_role) def render_markdown_to_safe_html(text: str) -> str: rendered = markdown.markdown( text or "", extensions=[ "extra", "sane_lists", "nl2br", "tables", "fenced_code", ], output_format="html5", ) return sanitize_html_fragment(rendered) def safe_profile_bio_html(bio: str, bio_mode: str) -> str: mode = bio_mode if bio_mode in PROFILE_BIO_MODES else "txt" if mode == "html": return sanitize_html_fragment(bio or "") if mode == "md": return render_markdown_to_safe_html(bio or "") return "

" + html.escape(bio or "").replace("\n", "
") + "

" def extract_page_reference_slugs(markdown_text: str) -> list[str]: text = str(markdown_text or "") matches = PAGE_LINK_RE.findall(text) seen = set() slugs = [] for slug in matches: normalized = slug.lower().strip() if not normalized or normalized in seen: continue seen.add(normalized) slugs.append(normalized) return slugs def detect_website_brand(website: str) -> dict: value = str(website or "").strip().lower() if not value: return {"host": "", "brand": "web", "label": "website"} host = value host = host.replace("https://", "").replace("http://", "") host = host.split("/", 1)[0] host = host.removeprefix("www.") mappings = [ ("github.com", "github", "GitHub"), ("youtube.com", "youtube", "YouTube"), ("youtu.be", "youtube", "YouTube"), ("spotify.com", "spotify", "Spotify"), ("soundcloud.com", "soundcloud", "SoundCloud"), ("twitter.com", "x", "X"), ("x.com", "x", "X"), ] for suffix, brand, label in mappings: if host.endswith(suffix): return {"host": host, "brand": brand, "label": label} return {"host": host, "brand": "web", "label": "website"} def sqlite_table_exists(table_name: str) -> bool: with get_db() as db: row = db.execute( """ SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ? """, (table_name,), ).fetchone() return bool(row) def sqlite_column_exists(table_name: str, column_name: str) -> bool: with get_db() as db: rows = db.execute(f"PRAGMA table_info({table_name})").fetchall() return any(row["name"] == column_name for row in rows) def load_page_children(page_id: int) -> list[dict]: if not sqlite_column_exists("pages", "parent_page_id"): return [] with get_db() as db: rows = db.execute( """ SELECT id, slug, title, updated_at FROM pages WHERE parent_page_id = ? AND is_active = 1 ORDER BY title COLLATE NOCASE, id ASC """, (page_id,), ).fetchall() return [dict(row) for row in rows] def load_page_backlinks(page_id: int, page_slug: str, team_id: int) -> list[dict]: pattern_1 = f"%]({page_slug})%" pattern_2 = f"%/pages/{page_slug})%" with get_db() as db: rows = db.execute( """ SELECT id, slug, title, updated_at FROM pages WHERE team_id = ? AND is_active = 1 AND id != ? AND (body LIKE ? OR body LIKE ?) ORDER BY updated_at DESC, id DESC """, (team_id, page_id, pattern_1, pattern_2), ).fetchall() return [dict(row) for row in rows] def load_page_comments(page_id: int) -> list[dict]: if not sqlite_table_exists("page_comments"): return [] with get_db() as db: rows = db.execute( """ SELECT pc.id, pc.page_id, pc.user_id, pc.username, pc.body, pc.created_at, pc.updated_at, u.role AS user_role, us.name_color AS user_name_color FROM page_comments AS pc JOIN users AS u ON u.id = pc.user_id LEFT JOIN user_settings AS us ON us.user_id = u.id WHERE pc.page_id = ? AND pc.deleted_at IS NULL ORDER BY pc.created_at ASC, pc.id ASC """, (page_id,), ).fetchall() return [ { "id": row["id"], "page_id": row["page_id"], "user_id": row["user_id"], "username": row["username"], "body": row["body"], "created_at": row["created_at"], "updated_at": row["updated_at"], "user_role": row["user_role"], "user_name_color": resolve_name_color(row["user_role"], row["user_name_color"]), } for row in rows ] def reaction_summary_for_message(message_id: int, user: Optional[dict]) -> list[dict]: with get_db() as db: rows = db.execute( """ SELECT emoji, COUNT(*) AS count, MAX(CASE WHEN user_id = ? THEN 1 ELSE 0 END) AS reacted FROM message_reactions WHERE message_id = ? GROUP BY emoji ORDER BY count DESC, emoji ASC """, (user["id"] if user else -1, message_id), ).fetchall() return [ { "emoji": row["emoji"], "count": row["count"], "reacted": bool(row["reacted"]), } for row in rows ] def build_profile_privacy_context(viewer: Optional[dict], target_user_id: int) -> dict: is_self = bool(viewer and viewer["id"] == target_user_id) is_staff = bool(viewer and viewer["role"] in {"moderator", "admin"}) is_authenticated = bool(viewer) return { "is_self": is_self, "is_staff": is_staff, "is_authenticated": is_authenticated, } def can_view_profile_record(settings_row, viewer: Optional[dict], target_user_id: int) -> bool: visibility = "public" if settings_row and "profile_visibility" in settings_row.keys(): visibility = (settings_row["profile_visibility"] or "public").lower() if visibility not in PROFILE_VISIBILITIES: visibility = "public" ctx = build_profile_privacy_context(viewer, target_user_id) if visibility == "public": return True if visibility == "members": return ctx["is_authenticated"] or ctx["is_staff"] or ctx["is_self"] if visibility == "private": return ctx["is_self"] or ctx["is_staff"] return True def can_view_last_active(settings_row, viewer: Optional[dict], target_user_id: int) -> bool: visibility = "everyone" if settings_row and "last_active_visibility" in settings_row.keys(): visibility = (settings_row["last_active_visibility"] or "everyone").lower() if visibility not in LAST_ACTIVE_VISIBILITIES: visibility = "everyone" ctx = build_profile_privacy_context(viewer, target_user_id) if visibility == "everyone": return True if visibility == "members": return ctx["is_authenticated"] or ctx["is_staff"] or ctx["is_self"] if visibility == "nobody": return ctx["is_self"] or ctx["is_staff"] return False def profile_presence_badge_visible(settings_row, viewer: Optional[dict], target_user_id: int) -> bool: ctx = build_profile_privacy_context(viewer, target_user_id) if ctx["is_self"] or ctx["is_staff"]: return True if settings_row and "show_presence_badge" in settings_row.keys(): return bool(settings_row["show_presence_badge"]) return True def profile_payload(user_payload_func, user_row, profile_row, settings_row, viewer: Optional[dict]) -> dict: is_self = bool(viewer and viewer["id"] == user_row["id"]) status_payload = user_payload_func(user_row, settings_row) bio = profile_row["bio"] or "" bio_mode = profile_row["bio_mode"] or "txt" explicit_name_color = settings_row["name_color"] if settings_row else None resolved = resolve_name_color(user_row["role"], explicit_name_color) profile_visibility = settings_row["profile_visibility"] if settings_row and "profile_visibility" in settings_row.keys() else "public" if profile_visibility not in PROFILE_VISIBILITIES: profile_visibility = "public" last_active_visibility = settings_row["last_active_visibility"] if settings_row and "last_active_visibility" in settings_row.keys() else "everyone" if last_active_visibility not in LAST_ACTIVE_VISIBILITIES: last_active_visibility = "everyone" presence_mode = settings_row["presence_mode"] if settings_row and "presence_mode" in settings_row.keys() else "online" if presence_mode not in PRESENCE_MODES: presence_mode = "online" dm_layout_mode = settings_row["dm_layout_mode"] if settings_row and "dm_layout_mode" in settings_row.keys() else "cards" if dm_layout_mode not in DM_LAYOUT_MODES: dm_layout_mode = "cards" show_presence_badge = profile_presence_badge_visible(settings_row, viewer, user_row["id"]) last_active_at = user_row["last_login_at"] if "last_login_at" in user_row.keys() else None if not can_view_last_active(settings_row, viewer, user_row["id"]): last_active_at = None website = profile_row["website"] or "" website_meta = detect_website_brand(website) if website else {"host": "", "brand": "web", "label": "website"} return { "id": user_row["id"], "username": user_row["username"], "role": user_row["role"], "status": status_payload["status"], "presence_mode": presence_mode, "created_at": user_row["created_at"], "last_active_at": last_active_at, "display_name": profile_row["display_name"] if profile_row["display_name"] else user_row["username"], "badge_text": (profile_row["badge_text"] or "").strip() if "badge_text" in profile_row.keys() else "", "bio": bio, "bio_mode": bio_mode, "bio_html": safe_profile_bio_html(bio, bio_mode), "website": website, "website_meta": website_meta, "location": profile_row["location"] or "", "status_message": profile_row["status_message"] if "status_message" in profile_row.keys() else "", "banner_attachment_id": profile_row["banner_attachment_id"] if "banner_attachment_id" in profile_row.keys() else None, "banner_image_url": f"/api/attachments/{profile_row['banner_attachment_id']}/download" if ("banner_attachment_id" in profile_row.keys() and profile_row["banner_attachment_id"]) else "", "updated_at": profile_row["updated_at"], "is_self": is_self, "name_color": explicit_name_color, "resolved_name_color": resolved, "profile_background_color": settings_row["profile_background_color"] if settings_row and "profile_background_color" in settings_row.keys() else None, "profile_banner_color": settings_row["profile_banner_color"] if settings_row and "profile_banner_color" in settings_row.keys() else None, "profile_visibility": profile_visibility, "last_active_visibility": last_active_visibility, "show_presence_badge": show_presence_badge, "dm_layout_mode": dm_layout_mode, } def page_payload(page, user: Optional[dict], is_team_member_func, is_team_admin_func) -> dict: can_read = bool( page["visibility"] == "public" or ( user and ( user["role"] in {"moderator", "admin"} or is_team_member_func(page["team_id"], user["id"]) ) ) ) rendered_html = render_markdown_to_safe_html(page["body"] or "") parent_page_id = page["parent_page_id"] if "parent_page_id" in page.keys() else None parent_page_slug = page["parent_page_slug"] if "parent_page_slug" in page.keys() else None parent_page_title = page["parent_page_title"] if "parent_page_title" in page.keys() else None return { "id": page["id"], "team_id": page["team_id"], "team_slug": page["team_slug"], "team_name": page["team_name"], "channel_id": page["channel_id"], "channel_slug": page["channel_slug"], "channel_name": page["channel_name"], "parent_page_id": parent_page_id, "parent_page_slug": parent_page_slug, "parent_page_title": parent_page_title, "slug": page["slug"], "title": page["title"], "body": page["body"], "body_html": rendered_html, "visibility": page["visibility"], "page_type": page["page_type"], "created_by_user_id": page["created_by_user_id"], "updated_by_user_id": page["updated_by_user_id"], "created_at": page["created_at"], "updated_at": page["updated_at"], "can_read": can_read, "can_edit": bool( user and ( user["role"] in {"moderator", "admin"} or is_team_admin_func(page["team_id"], user) ) ), "children": load_page_children(page["id"]), "backlinks": load_page_backlinks( page["id"], page["slug"], page["team_id"], ), "references": extract_page_reference_slugs(page["body"] or ""), "comments": load_page_comments(page["id"]), } def list_dms_for_user(user_id: int) -> list[dict]: with get_db() as db: rows = db.execute( """ SELECT dc.id, dc.created_at, other.id AS other_user_id, other.username AS other_username, other.role AS other_role, us.name_color AS other_name_color, up.display_name AS other_display_name, up.status_message AS other_status_message, us.presence_mode AS other_presence_mode, ( SELECT m.text FROM messages AS m WHERE m.direct_conversation_id = dc.id ORDER BY m.id DESC LIMIT 1 ) AS last_message_text, ( SELECT m.created_at FROM messages AS m WHERE m.direct_conversation_id = dc.id ORDER BY m.id DESC LIMIT 1 ) AS last_message_created_at, ( SELECT m.id FROM messages AS m WHERE m.direct_conversation_id = dc.id ORDER BY m.id DESC LIMIT 1 ) AS last_message_id, ( SELECT COUNT(*) FROM messages AS m2 WHERE m2.direct_conversation_id = dc.id AND m2.user_id != ? AND NOT EXISTS ( SELECT 1 FROM message_receipts AS mr WHERE mr.message_id = m2.id AND mr.user_id = ? ) ) AS unread_count FROM direct_conversations AS dc JOIN direct_conversation_members AS self_member ON self_member.direct_conversation_id = dc.id JOIN direct_conversation_members AS other_member ON other_member.direct_conversation_id = dc.id AND other_member.user_id != self_member.user_id JOIN users AS other ON other.id = other_member.user_id LEFT JOIN user_profiles AS up ON up.user_id = other.id LEFT JOIN user_settings AS us ON us.user_id = other.id WHERE self_member.user_id = ? ORDER BY COALESCE(last_message_id, 0) DESC, dc.id DESC """, (user_id, user_id, user_id), ).fetchall() items = [] for row in rows: items.append( { "id": row["id"], "created_at": row["created_at"], "other_user": { "id": row["other_user_id"], "username": row["other_username"], "display_name": row["other_display_name"] or row["other_username"], "role": row["other_role"], "name_color": row["other_name_color"], "resolved_name_color": resolve_name_color(row["other_role"], row["other_name_color"]), "status_message": row["other_status_message"] or "", "presence_mode": row["other_presence_mode"] or "online", }, "last_message_text": row["last_message_text"], "last_message_created_at": row["last_message_created_at"], "last_message_id": row["last_message_id"], "unread_count": int(row["unread_count"] or 0), } ) return items def richer_message_payload(row, user: Optional[dict], attachment_payload_legacy_from_message_select_row, attachment_list_for_message) -> dict: with get_db() as db: edited = db.execute( """ SELECT created_at FROM message_edits WHERE message_id = ? ORDER BY id DESC LIMIT 1 """, (row["id"],), ).fetchone() receipt_count = db.execute( """ SELECT COUNT(*) AS count FROM message_receipts WHERE message_id = ? """, (row["id"],), ).fetchone()["count"] seen_by_user = False is_bookmarked = False is_pinned = False pin_row = None bookmark_table_exists = bool( db.execute( """ SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'message_bookmarks' """ ).fetchone() ) pin_table_exists = bool( db.execute( """ SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'message_pins' """ ).fetchone() ) if user: seen_by_user = bool( db.execute( """ SELECT 1 FROM message_receipts WHERE message_id = ? AND user_id = ? """, (row["id"], user["id"]), ).fetchone() ) if bookmark_table_exists: is_bookmarked = bool( db.execute( """ SELECT 1 FROM message_bookmarks WHERE message_id = ? AND user_id = ? """, (row["id"], user["id"]), ).fetchone() ) if pin_table_exists: pin_row = db.execute( """ SELECT pinned_at, pinned_by_user_id FROM message_pins WHERE message_id = ? """, (row["id"],), ).fetchone() is_pinned = bool(pin_row) primary_attachment = attachment_payload_legacy_from_message_select_row(row) attachments = attachment_list_for_message(row["id"]) if primary_attachment and not any(item["id"] == primary_attachment["id"] for item in attachments): attachments.insert(0, primary_attachment) user_role = row["user_role"] if "user_role" in row.keys() else "user" user_name_color = row["user_name_color"] if "user_name_color" in row.keys() else None resolved_color = resolve_name_color(user_role, user_name_color) return { "id": row["id"], "room_id": row["room_id"], "channel_id": row["channel_id"], "direct_conversation_id": row["direct_conversation_id"] if "direct_conversation_id" in row.keys() else None, "user_id": row["user_id"], "username": row["username"], "user_role": user_role, "user_name_color": resolved_color, "text": row["text"], "created_at": row["created_at"], "updated_at": edited["created_at"] if edited else None, "is_edited": bool(edited), "parent_message_id": row["parent_message_id"], "thread_reply_count": row["thread_reply_count"], "attachment": primary_attachment, "attachments": attachments, "read_receipt_count": receipt_count, "seen_by_current_user": seen_by_user, "reactions": reaction_summary_for_message(row["id"], user), "is_dm": bool(("direct_conversation_id" in row.keys()) and row["direct_conversation_id"]), "is_pinned": is_pinned, "pinned_at": pin_row["pinned_at"] if pin_row else None, "pinned_by_user_id": pin_row["pinned_by_user_id"] if pin_row else None, "is_bookmarked": is_bookmarked, } def feed_message_payload(row, user: Optional[dict], attachment_payload_legacy_from_message_select_row, attachment_list_for_message) -> dict: payload = richer_message_payload( row, user, attachment_payload_legacy_from_message_select_row, attachment_list_for_message, ) payload.update( { "room_slug": row["room_slug"] if "room_slug" in row.keys() else None, "room_name": row["room_name"] if "room_name" in row.keys() else None, "room_visibility": row["room_visibility"] if "room_visibility" in row.keys() else None, "team_slug": row["team_slug"] if "team_slug" in row.keys() else None, "team_name": row["team_name"] if "team_name" in row.keys() else None, "channel_slug": row["channel_slug"] if "channel_slug" in row.keys() else None, "channel_name": row["channel_name"] if "channel_name" in row.keys() else None, "dm_id": row["direct_conversation_id"] if "direct_conversation_id" in row.keys() else None, "is_mention": bool( user and f"@{user['username'].lower()}" in payload["text"].lower() ), } ) return payload