feat(chat): add random public_id and fix users blocked route
Some checks failed
CI / test (push) Failing after 20s
Some checks failed
CI / test (push) Failing after 20s
- add chats.public_id random identifier with migration 0011
- expose public_id in chat API payloads
- use chat public_id in message search UI label
- fix users router order so /users/blocked no longer conflicts with /users/{user_id}
This commit is contained in:
43
alembic/versions/0011_chat_public_id.py
Normal file
43
alembic/versions/0011_chat_public_id.py
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
"""add chats public_id
|
||||||
|
|
||||||
|
Revision ID: 0011_chat_public_id
|
||||||
|
Revises: 0010_blocked_users
|
||||||
|
Create Date: 2026-03-08 15:00:00.000000
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
from app.utils.id_generator import generate_public_id
|
||||||
|
|
||||||
|
|
||||||
|
revision: str = "0011_chat_public_id"
|
||||||
|
down_revision: Union[str, Sequence[str], None] = "0010_blocked_users"
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
op.add_column("chats", sa.Column("public_id", sa.String(length=24), nullable=True))
|
||||||
|
conn = op.get_bind()
|
||||||
|
rows = list(conn.execute(sa.text("SELECT id FROM chats")))
|
||||||
|
used: set[str] = set()
|
||||||
|
for row in rows:
|
||||||
|
chat_id = row[0]
|
||||||
|
public_id = generate_public_id()
|
||||||
|
while public_id in used:
|
||||||
|
public_id = generate_public_id()
|
||||||
|
used.add(public_id)
|
||||||
|
conn.execute(
|
||||||
|
sa.text("UPDATE chats SET public_id = :public_id WHERE id = :id"),
|
||||||
|
{"public_id": public_id, "id": chat_id},
|
||||||
|
)
|
||||||
|
op.alter_column("chats", "public_id", existing_type=sa.String(length=24), nullable=False)
|
||||||
|
op.create_index(op.f("ix_chats_public_id"), "chats", ["public_id"], unique=True)
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
op.drop_index(op.f("ix_chats_public_id"), table_name="chats")
|
||||||
|
op.drop_column("chats", "public_id")
|
||||||
@@ -6,6 +6,7 @@ from sqlalchemy import Boolean, DateTime, Enum as SAEnum, ForeignKey, String, Un
|
|||||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||||
|
|
||||||
from app.database.base import Base
|
from app.database.base import Base
|
||||||
|
from app.utils.id_generator import generate_public_id
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from app.messages.models import Message
|
from app.messages.models import Message
|
||||||
@@ -28,6 +29,7 @@ class Chat(Base):
|
|||||||
__tablename__ = "chats"
|
__tablename__ = "chats"
|
||||||
|
|
||||||
id: Mapped[int] = mapped_column(primary_key=True, index=True)
|
id: Mapped[int] = mapped_column(primary_key=True, index=True)
|
||||||
|
public_id: Mapped[str] = mapped_column(String(24), unique=True, index=True, nullable=False, default=generate_public_id)
|
||||||
type: Mapped[ChatType] = mapped_column(SAEnum(ChatType), nullable=False, index=True)
|
type: Mapped[ChatType] = mapped_column(SAEnum(ChatType), nullable=False, index=True)
|
||||||
title: Mapped[str | None] = mapped_column(String(255), nullable=True)
|
title: Mapped[str | None] = mapped_column(String(255), nullable=True)
|
||||||
handle: Mapped[str | None] = mapped_column(String(64), nullable=True, unique=True, index=True)
|
handle: Mapped[str | None] = mapped_column(String(64), nullable=True, unique=True, index=True)
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ class ChatRead(BaseModel):
|
|||||||
model_config = ConfigDict(from_attributes=True)
|
model_config = ConfigDict(from_attributes=True)
|
||||||
|
|
||||||
id: int
|
id: int
|
||||||
|
public_id: str
|
||||||
type: ChatType
|
type: ChatType
|
||||||
title: str | None = None
|
title: str | None = None
|
||||||
display_title: str | None = None
|
display_title: str | None = None
|
||||||
|
|||||||
@@ -66,6 +66,7 @@ async def serialize_chat_for_user(db: AsyncSession, *, user_id: int, chat: Chat)
|
|||||||
return ChatRead.model_validate(
|
return ChatRead.model_validate(
|
||||||
{
|
{
|
||||||
"id": chat.id,
|
"id": chat.id,
|
||||||
|
"public_id": chat.public_id,
|
||||||
"type": chat.type,
|
"type": chat.type,
|
||||||
"title": chat.title,
|
"title": chat.title,
|
||||||
"display_title": display_title,
|
"display_title": display_title,
|
||||||
@@ -375,6 +376,7 @@ async def discover_public_chats_for_user(db: AsyncSession, *, user_id: int, quer
|
|||||||
ChatDiscoverRead.model_validate(
|
ChatDiscoverRead.model_validate(
|
||||||
{
|
{
|
||||||
"id": chat.id,
|
"id": chat.id,
|
||||||
|
"public_id": chat.public_id,
|
||||||
"type": chat.type,
|
"type": chat.type,
|
||||||
"title": chat.title,
|
"title": chat.title,
|
||||||
"handle": chat.handle,
|
"handle": chat.handle,
|
||||||
|
|||||||
@@ -41,14 +41,6 @@ async def search_users(
|
|||||||
return users
|
return users
|
||||||
|
|
||||||
|
|
||||||
@router.get("/{user_id}", response_model=UserRead)
|
|
||||||
async def read_user(user_id: int, db: AsyncSession = Depends(get_db), _current_user: User = Depends(get_current_user)) -> UserRead:
|
|
||||||
user = await get_user_by_id(db, user_id)
|
|
||||||
if not user:
|
|
||||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="User not found")
|
|
||||||
return user
|
|
||||||
|
|
||||||
|
|
||||||
@router.put("/profile", response_model=UserRead)
|
@router.put("/profile", response_model=UserRead)
|
||||||
async def update_profile(
|
async def update_profile(
|
||||||
payload: UserProfileUpdate,
|
payload: UserProfileUpdate,
|
||||||
@@ -102,3 +94,11 @@ async def unblock_user_endpoint(
|
|||||||
if user_id == current_user.id:
|
if user_id == current_user.id:
|
||||||
return
|
return
|
||||||
await unblock_user(db, user_id=current_user.id, blocked_user_id=user_id)
|
await unblock_user(db, user_id=current_user.id, blocked_user_id=user_id)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/{user_id}", response_model=UserRead)
|
||||||
|
async def read_user(user_id: int, db: AsyncSession = Depends(get_db), _current_user: User = Depends(get_current_user)) -> UserRead:
|
||||||
|
user = await get_user_by_id(db, user_id)
|
||||||
|
if not user:
|
||||||
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="User not found")
|
||||||
|
return user
|
||||||
|
|||||||
9
app/utils/id_generator.py
Normal file
9
app/utils/id_generator.py
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import secrets
|
||||||
|
import string
|
||||||
|
|
||||||
|
|
||||||
|
_ALPHABET = string.ascii_letters + string.digits
|
||||||
|
|
||||||
|
|
||||||
|
def generate_public_id(length: int = 12) -> str:
|
||||||
|
return "".join(secrets.choice(_ALPHABET) for _ in range(length))
|
||||||
@@ -4,6 +4,7 @@ export type DeliveryStatus = "sending" | "sent" | "delivered" | "read";
|
|||||||
|
|
||||||
export interface Chat {
|
export interface Chat {
|
||||||
id: number;
|
id: number;
|
||||||
|
public_id: string;
|
||||||
type: ChatType;
|
type: ChatType;
|
||||||
title: string | null;
|
title: string | null;
|
||||||
display_title?: string | null;
|
display_title?: string | null;
|
||||||
|
|||||||
@@ -138,7 +138,10 @@ export function ChatsPage() {
|
|||||||
<p className="px-2 py-1 text-xs text-slate-400">Nothing found</p>
|
<p className="px-2 py-1 text-xs text-slate-400">Nothing found</p>
|
||||||
) : null}
|
) : null}
|
||||||
<div className="tg-scrollbar max-h-[50vh] space-y-1 overflow-auto">
|
<div className="tg-scrollbar max-h-[50vh] space-y-1 overflow-auto">
|
||||||
{searchResults.map((message) => (
|
{searchResults.map((message) => {
|
||||||
|
const chatMeta = chats.find((chat) => chat.id === message.chat_id);
|
||||||
|
const chatLabel = chatMeta?.public_id ?? String(message.chat_id);
|
||||||
|
return (
|
||||||
<button
|
<button
|
||||||
className="block w-full rounded-lg bg-slate-800/80 px-3 py-2 text-left hover:bg-slate-700/80"
|
className="block w-full rounded-lg bg-slate-800/80 px-3 py-2 text-left hover:bg-slate-700/80"
|
||||||
key={`search-msg-${message.id}`}
|
key={`search-msg-${message.id}`}
|
||||||
@@ -147,10 +150,11 @@ export function ChatsPage() {
|
|||||||
setSearchOpen(false);
|
setSearchOpen(false);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<p className="mb-1 text-[11px] text-slate-400">chat #{message.chat_id} · msg #{message.id}</p>
|
<p className="mb-1 text-[11px] text-slate-400">chat {chatLabel} · msg #{message.id}</p>
|
||||||
<p className="line-clamp-2 text-sm text-slate-100">{message.text || "[media]"}</p>
|
<p className="line-clamp-2 text-sm text-slate-100">{message.text || "[media]"}</p>
|
||||||
</button>
|
</button>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user