feat: add reply/forward/pin message flow across backend and web
Some checks failed
CI / test (push) Failing after 24s

- add reply_to/forwarded_from message fields and chat pinned_message field

- add forward and pin APIs plus reply support in message create

- wire web actions: Reply, Fwd, Pin and reply composer state

- fix spam policy bug: allow repeated identical messages, keep rate limiting
This commit is contained in:
2026-03-08 00:28:43 +03:00
parent 4d704fc279
commit e1d0375392
18 changed files with 287 additions and 29 deletions

View File

@@ -30,6 +30,7 @@ class Chat(Base):
id: Mapped[int] = mapped_column(primary_key=True, index=True)
type: Mapped[ChatType] = mapped_column(SAEnum(ChatType), nullable=False, index=True)
title: Mapped[str | None] = mapped_column(String(255), nullable=True)
pinned_message_id: Mapped[int | None] = mapped_column(ForeignKey("messages.id", ondelete="SET NULL"), nullable=True, index=True)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), nullable=False)
members: Mapped[list["ChatMember"]] = relationship(back_populates="chat", cascade="all, delete-orphan")

View File

@@ -8,6 +8,7 @@ from app.chats.schemas import (
ChatMemberAddRequest,
ChatMemberRead,
ChatMemberRoleUpdateRequest,
ChatPinMessageRequest,
ChatRead,
ChatTitleUpdateRequest,
)
@@ -17,6 +18,7 @@ from app.chats.service import (
get_chat_for_user,
get_chats_for_user,
leave_chat_for_user,
pin_chat_message_for_user,
remove_chat_member_for_user,
update_chat_member_role_for_user,
update_chat_title_for_user,
@@ -58,6 +60,7 @@ async def get_chat(
id=chat.id,
type=chat.type,
title=chat.title,
pinned_message_id=chat.pinned_message_id,
created_at=chat.created_at,
members=members,
)
@@ -127,3 +130,13 @@ async def leave_chat(
current_user: User = Depends(get_current_user),
) -> None:
await leave_chat_for_user(db, chat_id=chat_id, user_id=current_user.id)
@router.post("/{chat_id}/pin", response_model=ChatRead)
async def pin_chat_message(
chat_id: int,
payload: ChatPinMessageRequest,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user),
) -> ChatRead:
return await pin_chat_message_for_user(db, chat_id=chat_id, user_id=current_user.id, payload=payload)

View File

@@ -11,6 +11,7 @@ class ChatRead(BaseModel):
id: int
type: ChatType
title: str | None = None
pinned_message_id: int | None = None
created_at: datetime
@@ -43,3 +44,7 @@ class ChatMemberRoleUpdateRequest(BaseModel):
class ChatTitleUpdateRequest(BaseModel):
title: str = Field(min_length=1, max_length=255)
class ChatPinMessageRequest(BaseModel):
message_id: int | None = None

View File

@@ -4,7 +4,8 @@ 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 ChatCreateRequest, ChatTitleUpdateRequest
from app.chats.schemas import ChatCreateRequest, ChatPinMessageRequest, ChatTitleUpdateRequest
from app.messages.repository import get_message_by_id
from app.users.repository import get_user_by_id
@@ -211,3 +212,28 @@ async def leave_chat_for_user(db: AsyncSession, *, chat_id: int, user_id: int) -
)
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)
_ensure_group_or_channel(chat.type)
_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