import secrets from fastapi import HTTPException, status from sqlalchemy.exc import IntegrityError from sqlalchemy.ext.asyncio import AsyncSession from app.chats import repository from app.chats.models import Chat, ChatMember, ChatMemberRole, ChatType from app.chats.schemas import ( ChatBanRead, ChatCreateRequest, ChatDeleteRequest, ChatDiscoverRead, ChatMemberRead, ChatJoinByInviteRequest, ChatNotificationSettingsRead, ChatNotificationSettingsUpdate, ChatInviteLinkRead, ChatPinMessageRequest, ChatProfileUpdateRequest, ChatRead, ChatTitleUpdateRequest, ) from app.config.settings import settings from app.messages.repository import ( delete_messages_in_chat, get_hidden_message, get_message_by_id, hide_message_for_user, list_chat_message_ids, ) from app.realtime.presence import get_users_online_map from app.users.repository import get_user_by_id, has_block_relation_between_users, is_user_in_contacts from app.users.service import can_user_receive_private_messages async def _can_view_last_seen(*, db: AsyncSession, target_user, viewer_user_id: int) -> bool: if target_user.id == viewer_user_id: return True if target_user.privacy_last_seen == "everyone": return True if target_user.privacy_last_seen == "nobody": return False return await is_user_in_contacts(db, owner_user_id=target_user.id, candidate_user_id=viewer_user_id) async def _can_view_avatar(*, db: AsyncSession, target_user, viewer_user_id: int) -> bool: if target_user.id == viewer_user_id: return True if target_user.privacy_avatar == "everyone": return True if target_user.privacy_avatar == "nobody": return False return await is_user_in_contacts(db, owner_user_id=target_user.id, candidate_user_id=viewer_user_id) async def _can_invite_to_group(*, db: AsyncSession, target_user, actor_user_id: int) -> bool: if target_user.id == actor_user_id: return False if target_user.privacy_group_invites == "everyone": return True return await is_user_in_contacts(db, owner_user_id=target_user.id, candidate_user_id=actor_user_id) async def serialize_chat_for_user( db: AsyncSession, *, user_id: int, chat: Chat, current_username: str | None = None, ) -> ChatRead: display_title = chat.title my_role = None membership = await repository.get_chat_member(db, chat_id=chat.id, user_id=user_id) if membership: my_role = membership.role members_count: int | None = None online_count: int | None = None subscribers_count: int | None = None counterpart_user_id: int | None = None counterpart_name: str | None = None counterpart_username: str | None = None counterpart_avatar_url: str | None = None counterpart_is_online: bool | None = None counterpart_last_seen_at = None if chat.is_saved: display_title = "Saved Messages" elif chat.type == ChatType.PRIVATE: counterpart_id = await repository.get_private_counterpart_user_id(db, chat_id=chat.id, user_id=user_id) if counterpart_id: counterpart_user_id = counterpart_id counterpart = await get_user_by_id(db, counterpart_id) if counterpart: display_title = counterpart.name or counterpart.username counterpart_name = counterpart.name counterpart_username = counterpart.username presence_allowed = await _can_view_last_seen(db=db, target_user=counterpart, viewer_user_id=user_id) counterpart_last_seen_at = counterpart.last_seen_at if presence_allowed else None avatar_allowed = await _can_view_avatar(db=db, target_user=counterpart, viewer_user_id=user_id) counterpart_avatar_url = counterpart.avatar_url if avatar_allowed else None presence = await get_users_online_map([counterpart_id]) if counterpart: presence_allowed = await _can_view_last_seen(db=db, target_user=counterpart, viewer_user_id=user_id) counterpart_is_online = presence.get(counterpart_id, False) if presence_allowed else None else: member_ids = await repository.list_chat_member_user_ids(db, chat_id=chat.id) members_count = len(member_ids) online_presence = await get_users_online_map(member_ids) online_count = sum(1 for is_online in online_presence.values() if is_online) if chat.type == ChatType.CHANNEL: subscribers_count = members_count if not current_username: current_user = await get_user_by_id(db, user_id) current_username = current_user.username if current_user else None unread_count = await repository.get_unread_count_for_chat(db, chat_id=chat.id, user_id=user_id) unread_mentions_count = await repository.get_unread_mentions_count_for_chat( db, chat_id=chat.id, user_id=user_id, username=current_username, ) last_message = await repository.get_last_visible_message_for_user(db, chat_id=chat.id, user_id=user_id) user_setting = await repository.get_chat_user_setting(db, chat_id=chat.id, user_id=user_id) notification_setting = await repository.get_chat_notification_setting(db, chat_id=chat.id, user_id=user_id) archived = bool(user_setting and user_setting.archived) pinned = bool(user_setting and user_setting.pinned) muted = bool(notification_setting and notification_setting.muted) return ChatRead.model_validate( { "id": chat.id, "public_id": chat.public_id, "type": chat.type, "title": chat.title, "avatar_url": chat.avatar_url, "display_title": display_title, "handle": chat.handle, "description": chat.description, "is_public": chat.is_public, "is_saved": chat.is_saved, "archived": archived, "pinned": pinned, "muted": muted, "unread_count": unread_count, "unread_mentions_count": unread_mentions_count, "pinned_message_id": chat.pinned_message_id, "members_count": members_count, "online_count": online_count, "subscribers_count": subscribers_count, "counterpart_user_id": counterpart_user_id, "counterpart_name": counterpart_name, "counterpart_username": counterpart_username, "counterpart_avatar_url": counterpart_avatar_url, "counterpart_is_online": counterpart_is_online, "counterpart_last_seen_at": counterpart_last_seen_at, "last_message_text": last_message.text if last_message else None, "last_message_type": last_message.type if last_message else None, "last_message_created_at": last_message.created_at if last_message else None, "my_role": my_role, "created_at": chat.created_at, } ) async def serialize_chats_for_user(db: AsyncSession, *, user_id: int, chats: list[Chat]) -> list[ChatRead]: current_user = await get_user_by_id(db, user_id) current_username = current_user.username if current_user else None return [ await serialize_chat_for_user(db, user_id=user_id, chat=chat, current_username=current_username) for chat in chats ] async def create_chat_for_user(db: AsyncSession, *, creator_id: int, payload: ChatCreateRequest) -> Chat: member_ids = list(dict.fromkeys(payload.member_ids)) member_ids = [member_id for member_id in member_ids if member_id != creator_id] if payload.type == ChatType.PRIVATE and len(member_ids) != 1: raise HTTPException( status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail="Private chat requires exactly one target user.", ) if payload.type == ChatType.PRIVATE: target_user = await get_user_by_id(db, member_ids[0]) if target_user and not await can_user_receive_private_messages(db, target_user=target_user, actor_user_id=creator_id): raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="User does not accept private messages", ) if await has_block_relation_between_users(db, user_a_id=creator_id, user_b_id=member_ids[0]): raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Cannot create private chat due to block settings", ) existing_chat = await repository.find_private_chat_between_users( db, user_a_id=creator_id, user_b_id=member_ids[0], ) if existing_chat: return existing_chat if payload.type in {ChatType.GROUP, ChatType.CHANNEL} and not payload.title: raise HTTPException( status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail="Group and channel chats require title.", ) if payload.type == ChatType.PRIVATE and payload.is_public: raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail="Private chat cannot be public") if payload.is_public and payload.type in {ChatType.GROUP, ChatType.CHANNEL} and not payload.handle: raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail="Public chat requires handle") if payload.handle: existing = await repository.get_chat_by_handle(db, payload.handle.strip().lower()) if existing: raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="Handle is already taken") for member_id in member_ids: user = await get_user_by_id(db, member_id) if not user: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"User {member_id} not found") if payload.type in {ChatType.GROUP, ChatType.CHANNEL}: can_invite = await _can_invite_to_group(db=db, target_user=user, actor_user_id=creator_id) if not can_invite: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail=f"User {member_id} does not allow group invites from you", ) chat = await repository.create_chat_with_meta( db, chat_type=payload.type, title=payload.title, handle=payload.handle.strip().lower() if payload.handle else None, description=payload.description, is_public=payload.is_public, is_saved=False, ) await repository.add_chat_member(db, chat_id=chat.id, user_id=creator_id, role=ChatMemberRole.OWNER) default_role = ChatMemberRole.MEMBER for member_id in member_ids: await repository.add_chat_member(db, chat_id=chat.id, user_id=member_id, role=default_role) await db.commit() return chat async def get_chats_for_user( db: AsyncSession, *, user_id: int, limit: int = 50, before_id: int | None = None, query: str | None = None, archived: bool = False, ) -> list[Chat]: safe_limit = max(1, min(limit, 100)) if archived: chats = await repository.list_archived_user_chats(db, user_id=user_id, limit=safe_limit, before_id=before_id) else: chats = await repository.list_user_chats(db, user_id=user_id, limit=safe_limit, before_id=before_id, query=query) saved = await ensure_saved_messages_chat(db, user_id=user_id) if saved.id not in [c.id for c in chats]: chats = [saved, *chats] else: chats = [saved, *[c for c in chats if c.id != saved.id]] return chats async def get_chat_for_user(db: AsyncSession, *, chat_id: int, user_id: int) -> tuple[Chat, list]: chat = await repository.get_chat_by_id(db, chat_id) if not chat: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Chat not found") membership = await repository.get_chat_member(db, chat_id=chat_id, user_id=user_id) if not membership: raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="You are not a member of this chat") members = await repository.list_chat_members(db, chat_id=chat_id) enriched_members: list[ChatMemberRead] = [] for member in members: user = await get_user_by_id(db, member.user_id) enriched_members.append( ChatMemberRead( id=member.id, user_id=member.user_id, username=user.username if user else None, name=user.name if user else None, avatar_url=user.avatar_url if user else None, role=member.role, joined_at=member.joined_at, ) ) return chat, enriched_members async def ensure_chat_membership(db: AsyncSession, *, chat_id: int, user_id: int) -> None: membership = await repository.get_chat_member(db, chat_id=chat_id, user_id=user_id) if not membership: raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="You are not a member of this chat") async def _get_chat_and_membership(db: AsyncSession, *, chat_id: int, user_id: int) -> tuple[Chat, ChatMember]: chat = await repository.get_chat_by_id(db, chat_id) if not chat: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Chat not found") membership = await repository.get_chat_member(db, chat_id=chat_id, user_id=user_id) if not membership: raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="You are not a member of this chat") return chat, membership def _ensure_manage_permission(role: ChatMemberRole) -> None: if role not in {ChatMemberRole.OWNER, ChatMemberRole.ADMIN}: raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Insufficient permissions") def _ensure_group_or_channel(chat_type: ChatType) -> None: if chat_type == ChatType.PRIVATE: raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail="Private chat cannot be managed") async def update_chat_title_for_user( db: AsyncSession, *, chat_id: int, user_id: int, payload: ChatTitleUpdateRequest, ) -> Chat: chat, membership = await _get_chat_and_membership(db, chat_id=chat_id, user_id=user_id) _ensure_group_or_channel(chat.type) _ensure_manage_permission(membership.role) chat.title = payload.title await db.commit() await db.refresh(chat) return chat async def update_chat_profile_for_user( db: AsyncSession, *, chat_id: int, user_id: int, payload: ChatProfileUpdateRequest, ) -> Chat: chat, membership = await _get_chat_and_membership(db, chat_id=chat_id, user_id=user_id) _ensure_group_or_channel(chat.type) _ensure_manage_permission(membership.role) if payload.title is not None: chat.title = payload.title.strip() or chat.title if payload.description is not None: chat.description = payload.description.strip() or None if payload.avatar_url is not None: chat.avatar_url = payload.avatar_url.strip() or None await db.commit() await db.refresh(chat) return chat async def add_chat_member_for_user( db: AsyncSession, *, chat_id: int, actor_user_id: int, target_user_id: int, ) -> ChatMember: chat, membership = await _get_chat_and_membership(db, chat_id=chat_id, user_id=actor_user_id) _ensure_group_or_channel(chat.type) _ensure_manage_permission(membership.role) if await repository.is_user_banned_in_chat(db, chat_id=chat_id, user_id=target_user_id): raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="User is banned from this chat") if target_user_id == actor_user_id: raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail="User is already in chat") target_user = await get_user_by_id(db, target_user_id) if not target_user: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="User not found") if not await _can_invite_to_group(db=db, target_user=target_user, actor_user_id=actor_user_id): raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="User does not allow group invites from you") existing = await repository.get_chat_member(db, chat_id=chat_id, user_id=target_user_id) if existing: return existing try: member = await repository.add_chat_member( db, chat_id=chat_id, user_id=target_user_id, role=ChatMemberRole.MEMBER, ) await db.commit() await db.refresh(member) return member except IntegrityError: await db.rollback() existing = await repository.get_chat_member(db, chat_id=chat_id, user_id=target_user_id) if existing: return existing raise async def update_chat_member_role_for_user( db: AsyncSession, *, chat_id: int, actor_user_id: int, target_user_id: int, role: ChatMemberRole, ) -> ChatMember: chat, actor_membership = await _get_chat_and_membership(db, chat_id=chat_id, user_id=actor_user_id) _ensure_group_or_channel(chat.type) if actor_membership.role != ChatMemberRole.OWNER: raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Only owner can change roles") target_membership = await repository.get_chat_member(db, chat_id=chat_id, user_id=target_user_id) if not target_membership: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Target member not found") if target_membership.role == ChatMemberRole.OWNER and target_user_id != actor_user_id: raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail="Cannot change owner role") if target_user_id == actor_user_id and role != ChatMemberRole.OWNER: raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail="Owner cannot demote self") target_membership.role = role await db.commit() await db.refresh(target_membership) return target_membership async def remove_chat_member_for_user( db: AsyncSession, *, chat_id: int, actor_user_id: int, target_user_id: int, ) -> None: chat, actor_membership = await _get_chat_and_membership(db, chat_id=chat_id, user_id=actor_user_id) _ensure_group_or_channel(chat.type) target_membership = await repository.get_chat_member(db, chat_id=chat_id, user_id=target_user_id) if not target_membership: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Target member not found") if target_membership.role == ChatMemberRole.OWNER: raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail="Owner cannot be removed") if actor_user_id == target_user_id: raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail="Use leave endpoint") if actor_membership.role == ChatMemberRole.ADMIN and target_membership.role != ChatMemberRole.MEMBER: raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Admin can remove only members") if actor_membership.role not in {ChatMemberRole.OWNER, ChatMemberRole.ADMIN}: raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Insufficient permissions") await repository.delete_chat_member(db, target_membership) await db.commit() async def ban_chat_member_for_user( db: AsyncSession, *, chat_id: int, actor_user_id: int, target_user_id: int, ) -> None: chat, actor_membership = await _get_chat_and_membership(db, chat_id=chat_id, user_id=actor_user_id) _ensure_group_or_channel(chat.type) if actor_membership.role not in {ChatMemberRole.OWNER, ChatMemberRole.ADMIN}: raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Insufficient permissions") if target_user_id == actor_user_id: raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail="Cannot ban yourself") target_membership = await repository.get_chat_member(db, chat_id=chat_id, user_id=target_user_id) if target_membership: if target_membership.role == ChatMemberRole.OWNER: raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail="Owner cannot be banned") if actor_membership.role == ChatMemberRole.ADMIN and target_membership.role != ChatMemberRole.MEMBER: raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Admin can ban only members") await repository.delete_chat_member(db, target_membership) await repository.upsert_chat_ban(db, chat_id=chat_id, user_id=target_user_id, banned_by_user_id=actor_user_id) await db.commit() async def unban_chat_member_for_user( db: AsyncSession, *, chat_id: int, actor_user_id: int, target_user_id: int, ) -> None: chat, actor_membership = await _get_chat_and_membership(db, chat_id=chat_id, user_id=actor_user_id) _ensure_group_or_channel(chat.type) if actor_membership.role not in {ChatMemberRole.OWNER, ChatMemberRole.ADMIN}: raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Insufficient permissions") await repository.remove_chat_ban(db, chat_id=chat_id, user_id=target_user_id) await db.commit() async def list_chat_bans_for_user( db: AsyncSession, *, chat_id: int, actor_user_id: int, ) -> list[ChatBanRead]: chat, actor_membership = await _get_chat_and_membership(db, chat_id=chat_id, user_id=actor_user_id) _ensure_group_or_channel(chat.type) if actor_membership.role not in {ChatMemberRole.OWNER, ChatMemberRole.ADMIN}: raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Insufficient permissions") bans = await repository.list_chat_bans(db, chat_id=chat_id) return [ ChatBanRead( chat_id=ban.chat_id, user_id=ban.user_id, banned_by_user_id=ban.banned_by_user_id, created_at=ban.created_at, ) for ban in bans ] async def leave_chat_for_user(db: AsyncSession, *, chat_id: int, user_id: int) -> None: chat, membership = await _get_chat_and_membership(db, chat_id=chat_id, user_id=user_id) _ensure_group_or_channel(chat.type) if membership.role == ChatMemberRole.OWNER: members_count = await repository.count_chat_members(db, chat_id=chat_id) if members_count > 1: raise HTTPException( status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail="Owner cannot leave while chat has other members", ) await repository.delete_chat_member(db, membership) await db.commit() async def pin_chat_message_for_user( db: AsyncSession, *, chat_id: int, user_id: int, payload: ChatPinMessageRequest, ) -> Chat: chat, membership = await _get_chat_and_membership(db, chat_id=chat_id, user_id=user_id) if chat.type in {ChatType.GROUP, ChatType.CHANNEL}: _ensure_manage_permission(membership.role) if payload.message_id is None: chat.pinned_message_id = None await db.commit() await db.refresh(chat) return chat message = await get_message_by_id(db, payload.message_id) if not message or message.chat_id != chat_id: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Message not found in chat") chat.pinned_message_id = message.id await db.commit() await db.refresh(chat) return chat async def ensure_saved_messages_chat(db: AsyncSession, *, user_id: int) -> Chat: saved = await repository.find_saved_chat_for_user(db, user_id=user_id) if saved: return saved chat = await repository.create_chat_with_meta( db, chat_type=ChatType.PRIVATE, title="Saved Messages", handle=None, description="Personal cloud chat", is_public=False, is_saved=True, ) await repository.add_chat_member(db, chat_id=chat.id, user_id=user_id, role=ChatMemberRole.OWNER) await db.commit() await db.refresh(chat) return chat async def discover_public_chats_for_user(db: AsyncSession, *, user_id: int, query: str | None, limit: int = 30) -> list[ChatDiscoverRead]: rows = await repository.discover_public_chats(db, user_id=user_id, query=query, limit=max(1, min(limit, 50))) return [ ChatDiscoverRead.model_validate( { "id": chat.id, "public_id": chat.public_id, "type": chat.type, "title": chat.title, "avatar_url": chat.avatar_url, "handle": chat.handle, "description": chat.description, "is_public": chat.is_public, "is_saved": chat.is_saved, "pinned_message_id": chat.pinned_message_id, "created_at": chat.created_at, "is_member": is_member, } ) for chat, is_member in rows ] async def join_public_chat_for_user(db: AsyncSession, *, chat_id: int, user_id: int) -> Chat: chat = await repository.get_chat_by_id(db, chat_id) if not chat: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Chat not found") if not chat.is_public or chat.type not in {ChatType.GROUP, ChatType.CHANNEL}: raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Chat is not joinable") if await repository.is_user_banned_in_chat(db, chat_id=chat_id, user_id=user_id): raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="You are banned from this chat") membership = await repository.get_chat_member(db, chat_id=chat_id, user_id=user_id) if membership: return chat await repository.add_chat_member(db, chat_id=chat_id, user_id=user_id, role=ChatMemberRole.MEMBER) await db.commit() await db.refresh(chat) return chat async def delete_chat_for_user(db: AsyncSession, *, chat_id: int, user_id: int, payload: ChatDeleteRequest) -> None: chat, membership = await _get_chat_and_membership(db, chat_id=chat_id, user_id=user_id) if chat.is_saved: await clear_chat_for_user(db, chat_id=chat_id, user_id=user_id) return if chat.type == ChatType.CHANNEL and membership.role == ChatMemberRole.MEMBER: if payload.for_all: raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Insufficient permissions") await repository.delete_chat_member(db, membership) await db.commit() return delete_for_all = (payload.for_all and not chat.is_saved) or chat.type == ChatType.CHANNEL if delete_for_all: if chat.type in {ChatType.GROUP, ChatType.CHANNEL} and membership.role not in {ChatMemberRole.OWNER, ChatMemberRole.ADMIN}: raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Insufficient permissions") await db.delete(chat) await db.commit() return await repository.delete_chat_member(db, membership) await db.commit() async def clear_chat_for_user(db: AsyncSession, *, chat_id: int, user_id: int) -> None: chat, _membership = await _get_chat_and_membership(db, chat_id=chat_id, user_id=user_id) if chat.is_saved: await delete_messages_in_chat(db, chat_id=chat_id) await db.commit() return message_ids = await list_chat_message_ids(db, chat_id=chat_id) for message_id in message_ids: already_hidden = await get_hidden_message(db, message_id=message_id, user_id=user_id) if already_hidden: continue await hide_message_for_user(db, message_id=message_id, user_id=user_id) await db.commit() async def get_chat_notification_settings_for_user( db: AsyncSession, *, chat_id: int, user_id: int, ) -> ChatNotificationSettingsRead: await ensure_chat_membership(db, chat_id=chat_id, user_id=user_id) setting = await repository.get_chat_notification_setting(db, chat_id=chat_id, user_id=user_id) return ChatNotificationSettingsRead( chat_id=chat_id, user_id=user_id, muted=bool(setting and setting.muted), ) async def update_chat_notification_settings_for_user( db: AsyncSession, *, chat_id: int, user_id: int, payload: ChatNotificationSettingsUpdate, ) -> ChatNotificationSettingsRead: await ensure_chat_membership(db, chat_id=chat_id, user_id=user_id) setting = await repository.upsert_chat_notification_setting( db, chat_id=chat_id, user_id=user_id, muted=payload.muted, ) await db.commit() await db.refresh(setting) return ChatNotificationSettingsRead( chat_id=chat_id, user_id=user_id, muted=setting.muted, ) async def set_chat_archived_for_user( db: AsyncSession, *, chat_id: int, user_id: int, archived: bool, ) -> Chat: chat, _membership = await _get_chat_and_membership(db, chat_id=chat_id, user_id=user_id) await repository.upsert_chat_archived_setting(db, chat_id=chat_id, user_id=user_id, archived=archived) await db.commit() await db.refresh(chat) return chat async def set_chat_pinned_for_user( db: AsyncSession, *, chat_id: int, user_id: int, pinned: bool, ) -> Chat: chat, _membership = await _get_chat_and_membership(db, chat_id=chat_id, user_id=user_id) await repository.upsert_chat_pinned_setting(db, chat_id=chat_id, user_id=user_id, pinned=pinned) await db.commit() await db.refresh(chat) return chat async def create_chat_invite_link_for_user( db: AsyncSession, *, chat_id: int, user_id: int, ) -> ChatInviteLinkRead: chat, membership = await _get_chat_and_membership(db, chat_id=chat_id, user_id=user_id) _ensure_group_or_channel(chat.type) _ensure_manage_permission(membership.role) token = secrets.token_urlsafe(18) await repository.create_chat_invite_link(db, chat_id=chat_id, creator_user_id=user_id, token=token) await db.commit() invite_url = f"{settings.frontend_base_url.rstrip('/')}/join?token={token}" return ChatInviteLinkRead(chat_id=chat_id, token=token, invite_url=invite_url) async def join_chat_by_invite_for_user( db: AsyncSession, *, user_id: int, payload: ChatJoinByInviteRequest, ) -> Chat: link = await repository.get_active_chat_invite_by_token(db, token=payload.token.strip()) if not link: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Invite link not found") chat = await repository.get_chat_by_id(db, link.chat_id) if not chat: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Chat not found") if chat.type not in {ChatType.GROUP, ChatType.CHANNEL}: raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Invite link is not valid for this chat") if await repository.is_user_banned_in_chat(db, chat_id=chat.id, user_id=user_id): raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="You are banned from this chat") membership = await repository.get_chat_member(db, chat_id=chat.id, user_id=user_id) if membership: return chat await repository.add_chat_member(db, chat_id=chat.id, user_id=user_id, role=ChatMemberRole.MEMBER) await db.commit() await db.refresh(chat) return chat