diff --git a/features.py b/features.py index 25dd75e..98dda3c 100644 --- a/features.py +++ b/features.py @@ -33,6 +33,7 @@ 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) @@ -86,6 +87,33 @@ def extract_page_reference_slugs(markdown_text: str) -> list[str]: 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 attachment_kind_from_name_and_mime( original_name: Optional[str], mime_type: Optional[str], @@ -104,6 +132,18 @@ def attachment_kind_from_name_and_mime( def load_page_children(get_db, page_id: int) -> list[dict]: with get_db() as db: + has_parent_page = bool( + db.execute( + """ + SELECT 1 + FROM pragma_table_info('pages') + WHERE name = 'parent_page_id' + """ + ).fetchone() + ) + if not has_parent_page: + return [] + rows = db.execute( """ SELECT @@ -146,6 +186,19 @@ def load_page_backlinks(get_db, page_id: int, page_slug: str, team_id: int) -> l def load_page_comments(get_db, page_id: int) -> list[dict]: with get_db() as db: + table_exists = bool( + db.execute( + """ + SELECT 1 + FROM sqlite_master + WHERE type = 'table' + AND name = 'page_comments' + """ + ).fetchone() + ) + if not table_exists: + return [] + rows = db.execute( """ SELECT @@ -187,6 +240,19 @@ def load_page_comments(get_db, page_id: int) -> list[dict]: def next_page_version_number(get_db, page_id: int) -> int: with get_db() as db: + table_exists = bool( + db.execute( + """ + SELECT 1 + FROM sqlite_master + WHERE type = 'table' + AND name = 'page_versions' + """ + ).fetchone() + ) + if not table_exists: + return 1 + row = db.execute( """ SELECT COALESCE(MAX(version_number), 0) AS value @@ -210,6 +276,20 @@ def snapshot_page_version( created_by_user_id: int, created_at: str, ) -> int: + with get_db() as db: + table_exists = bool( + db.execute( + """ + SELECT 1 + FROM sqlite_master + WHERE type = 'table' + AND name = 'page_versions' + """ + ).fetchone() + ) + if not table_exists: + return 0 + version_number = next_page_version_number(get_db, page_id) with get_db() as db: db.execute("BEGIN IMMEDIATE") @@ -247,6 +327,19 @@ def snapshot_page_version( def load_page_versions(get_db, page_id: int) -> list[dict]: with get_db() as db: + table_exists = bool( + db.execute( + """ + SELECT 1 + FROM sqlite_master + WHERE type = 'table' + AND name = 'page_versions' + """ + ).fetchone() + ) + if not table_exists: + return [] + rows = db.execute( """ SELECT @@ -274,6 +367,19 @@ def load_page_versions(get_db, page_id: int) -> list[dict]: def load_page_version_detail(get_db, version_id: int): with get_db() as db: + table_exists = bool( + db.execute( + """ + SELECT 1 + FROM sqlite_master + WHERE type = 'table' + AND name = 'page_versions' + """ + ).fetchone() + ) + if not table_exists: + return None + row = db.execute( """ SELECT @@ -330,6 +436,9 @@ def page_payload(app_module, page, user: Optional[dict]) -> dict: ) ) 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"], @@ -338,9 +447,9 @@ def page_payload(app_module, page, user: Optional[dict]) -> dict: "channel_id": page["channel_id"], "channel_slug": page["channel_slug"], "channel_name": page["channel_name"], - "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, + "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"], @@ -740,6 +849,30 @@ def richer_message_payload( 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 current_user_id: seen_by_user = bool( db.execute( @@ -752,26 +885,29 @@ def richer_message_payload( (row["id"], current_user_id), ).fetchone() ) - is_bookmarked = bool( - db.execute( - """ - SELECT 1 - FROM message_bookmarks - WHERE message_id = ? - AND user_id = ? - """, - (row["id"], current_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"], current_user_id), + ).fetchone() + ) - pin_row = db.execute( - """ - SELECT pinned_at, pinned_by_user_id - FROM message_pins - WHERE message_id = ? - """, - (row["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(get_db, row["id"]) @@ -803,7 +939,7 @@ def richer_message_payload( "seen_by_current_user": seen_by_user, "reactions": reaction_summary_for_message(get_db, row["id"], user), "is_dm": bool(("direct_conversation_id" in row.keys()) and row["direct_conversation_id"]), - "is_pinned": bool(pin_row), + "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, @@ -856,15 +992,23 @@ def profile_payload( profile_visibility = settings_row["profile_visibility"] if settings_row else "public" if profile_visibility not in PROFILE_VISIBILITIES: - profile_visibility = "public" + profile_visibility = "public" last_active_visibility = settings_row["last_active_visibility"] if settings_row else "everyone" if last_active_visibility not in LAST_ACTIVE_VISIBILITIES: - last_active_visibility = "everyone" + last_active_visibility = "everyone" presence_mode = settings_row["presence_mode"] if settings_row else "online" if presence_mode not in PRESENCE_MODES: - presence_mode = "online" + presence_mode = "online" + + dm_layout_mode = settings_row["dm_layout_mode"] if settings_row else "cards" + if dm_layout_mode not in DM_LAYOUT_MODES: + dm_layout_mode = "cards" + + show_presence_badge = bool( + settings_row["show_presence_badge"] if settings_row and "show_presence_badge" in settings_row.keys() else 1 + ) last_active_at = user_row["last_login_at"] if "last_login_at" in user_row.keys() else None @@ -876,6 +1020,9 @@ def profile_payload( elif last_active_visibility == "nobody": can_show_last_active = bool(viewer and viewer["id"] == user_row["id"]) + 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"], @@ -889,19 +1036,22 @@ def profile_payload( "bio": bio, "bio_mode": bio_mode, "bio_html": safe_profile_bio_html(html_module, bio, bio_mode), - "website": profile_row["website"] or "", + "website": website, + "website_meta": website_meta, "location": profile_row["location"] or "", - "status_message": profile_row["status_message"] or "", - "banner_attachment_id": profile_row["banner_attachment_id"], - "banner_image_url": f"/api/attachments/{profile_row['banner_attachment_id']}/download" if profile_row["banner_attachment_id"] else "", + "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 else None, - "profile_banner_color": settings_row["profile_banner_color"] if settings_row else None, + "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, } @@ -977,6 +1127,7 @@ def list_dms_for_user(get_db, user_id: int) -> list[dict]: "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": 0, } ) return items @@ -1338,8 +1489,21 @@ def call_payload(get_db, call_id: int) -> Optional[dict]: def dispatch_scheduled_messages(get_db, now_iso_func, app_logger) -> None: - now_value = now_iso_func() with get_db() as db: + table_exists = bool( + db.execute( + """ + SELECT 1 + FROM sqlite_master + WHERE type = 'table' + AND name = 'scheduled_messages' + """ + ).fetchone() + ) + if not table_exists: + return + + now_value = now_iso_func() rows = db.execute( """ SELECT