moderation: add chat bans for groups/channels with web actions
All checks were successful
CI / test (push) Successful in 26s
All checks were successful
CI / test (push) Successful in 26s
This commit is contained in:
@@ -108,3 +108,14 @@ class ChatInviteLink(Base):
|
||||
token: Mapped[str] = mapped_column(String(64), nullable=False, index=True)
|
||||
is_active: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True, server_default="true")
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), nullable=False)
|
||||
|
||||
|
||||
class ChatBan(Base):
|
||||
__tablename__ = "chat_bans"
|
||||
__table_args__ = (UniqueConstraint("chat_id", "user_id", name="uq_chat_bans_chat_user"),)
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True, index=True)
|
||||
chat_id: Mapped[int] = mapped_column(ForeignKey("chats.id", ondelete="CASCADE"), nullable=False, index=True)
|
||||
user_id: Mapped[int] = mapped_column(ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True)
|
||||
banned_by_user_id: Mapped[int] = mapped_column(ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), nullable=False)
|
||||
|
||||
@@ -2,7 +2,7 @@ from sqlalchemy import Select, String, func, or_, select
|
||||
from sqlalchemy.orm import aliased
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.chats.models import Chat, ChatInviteLink, ChatMember, ChatMemberRole, ChatNotificationSetting, ChatType, ChatUserSetting
|
||||
from app.chats.models import Chat, ChatBan, ChatInviteLink, ChatMember, ChatMemberRole, ChatNotificationSetting, ChatType, ChatUserSetting
|
||||
from app.messages.models import Message, MessageHidden, MessageReceipt
|
||||
|
||||
|
||||
@@ -401,3 +401,31 @@ async def get_active_chat_invite_by_token(db: AsyncSession, *, token: str) -> Ch
|
||||
.limit(1)
|
||||
)
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
|
||||
async def get_chat_ban(db: AsyncSession, *, chat_id: int, user_id: int) -> ChatBan | None:
|
||||
result = await db.execute(select(ChatBan).where(ChatBan.chat_id == chat_id, ChatBan.user_id == user_id))
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
|
||||
async def is_user_banned_in_chat(db: AsyncSession, *, chat_id: int, user_id: int) -> bool:
|
||||
result = await db.execute(select(ChatBan.id).where(ChatBan.chat_id == chat_id, ChatBan.user_id == user_id).limit(1))
|
||||
return result.scalar_one_or_none() is not None
|
||||
|
||||
|
||||
async def upsert_chat_ban(db: AsyncSession, *, chat_id: int, user_id: int, banned_by_user_id: int) -> ChatBan:
|
||||
existing = await get_chat_ban(db, chat_id=chat_id, user_id=user_id)
|
||||
if existing:
|
||||
existing.banned_by_user_id = banned_by_user_id
|
||||
await db.flush()
|
||||
return existing
|
||||
ban = ChatBan(chat_id=chat_id, user_id=user_id, banned_by_user_id=banned_by_user_id)
|
||||
db.add(ban)
|
||||
await db.flush()
|
||||
return ban
|
||||
|
||||
|
||||
async def remove_chat_ban(db: AsyncSession, *, chat_id: int, user_id: int) -> None:
|
||||
ban = await get_chat_ban(db, chat_id=chat_id, user_id=user_id)
|
||||
if ban:
|
||||
await db.delete(ban)
|
||||
|
||||
@@ -21,6 +21,7 @@ from app.chats.schemas import (
|
||||
)
|
||||
from app.chats.service import (
|
||||
add_chat_member_for_user,
|
||||
ban_chat_member_for_user,
|
||||
create_chat_for_user,
|
||||
create_chat_invite_link_for_user,
|
||||
clear_chat_for_user,
|
||||
@@ -39,6 +40,7 @@ from app.chats.service import (
|
||||
serialize_chats_for_user,
|
||||
set_chat_archived_for_user,
|
||||
set_chat_pinned_for_user,
|
||||
unban_chat_member_for_user,
|
||||
update_chat_member_role_for_user,
|
||||
update_chat_notification_settings_for_user,
|
||||
update_chat_profile_for_user,
|
||||
@@ -207,6 +209,29 @@ async def remove_chat_member(
|
||||
await realtime_gateway.publish_chat_updated(chat_id=chat_id)
|
||||
|
||||
|
||||
@router.post("/{chat_id}/bans/{user_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def ban_chat_member(
|
||||
chat_id: int,
|
||||
user_id: int,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
) -> None:
|
||||
await ban_chat_member_for_user(db, chat_id=chat_id, actor_user_id=current_user.id, target_user_id=user_id)
|
||||
realtime_gateway.remove_chat_subscription(chat_id=chat_id, user_id=user_id)
|
||||
await realtime_gateway.publish_chat_updated(chat_id=chat_id)
|
||||
|
||||
|
||||
@router.delete("/{chat_id}/bans/{user_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def unban_chat_member(
|
||||
chat_id: int,
|
||||
user_id: int,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
) -> None:
|
||||
await unban_chat_member_for_user(db, chat_id=chat_id, actor_user_id=current_user.id, target_user_id=user_id)
|
||||
await realtime_gateway.publish_chat_updated(chat_id=chat_id)
|
||||
|
||||
|
||||
@router.post("/{chat_id}/leave", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def leave_chat(
|
||||
chat_id: int,
|
||||
|
||||
@@ -350,6 +350,8 @@ async def add_chat_member_for_user(
|
||||
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)
|
||||
@@ -427,6 +429,45 @@ async def remove_chat_member_for_user(
|
||||
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 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)
|
||||
@@ -514,6 +555,8 @@ async def join_public_chat_for_user(db: AsyncSession, *, chat_id: int, user_id:
|
||||
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
|
||||
@@ -650,6 +693,8 @@ async def join_chat_by_invite_for_user(
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user