mirror of
https://github.com/lorsanstand/Aether.git
synced 2026-06-19 12:05:16 +03:00
Add edit message
This commit is contained in:
@@ -1,53 +0,0 @@
|
||||
COMPANY_NAME=AETHER
|
||||
|
||||
MODE=DEV
|
||||
LOG_LEVEL=DEBUG
|
||||
|
||||
BACKEND_HOST=localhost
|
||||
BACKEND_PORT=8080
|
||||
WORKERS=4
|
||||
FRONTEND_URL=http://localhost:5173
|
||||
|
||||
VITE_API_URL=/api/v1
|
||||
FRONTEND_PORT=3056
|
||||
|
||||
FIRST_SUPER_USER_EMAIL=admin@example.com
|
||||
FIRST_SUPER_USER_PASS=admin
|
||||
FIRST_SUPER_USER_USERNAME=admin
|
||||
|
||||
DB_HOST=localhost
|
||||
DB_PORT=5432
|
||||
DB_PASS=postgres
|
||||
DB_USER=postgres
|
||||
DB_NAME=Aether
|
||||
|
||||
REDIS_HOST=localhost
|
||||
REDIS_PORT=6379
|
||||
# REDIS_PASS=
|
||||
# REDIS_DB=
|
||||
|
||||
#CORS_HEADERS=["Content-Type", "Set-Cookie", "Access-Control-Allow-Headers", "Access-Control-Allow-Origin", "Authorization"]
|
||||
#CORS_ORIGINS=["http://localhost:3000"]
|
||||
#CORS_METHODS=["GET", "POST", "OPTIONS", "DELETE", "PATCH", "PUT"]
|
||||
|
||||
CORS_HEADERS=["Content-Type", "Set-Cookie", "Access-Control-Allow-Headers", "Access-Control-Allow-Origin", "Authorization"]
|
||||
CORS_ORIGINS=["http://localhost:5500", "http://localhost:5173", "http://localhost:8080", "http://127.0.0.1:8080", "null"]
|
||||
CORS_METHODS=["GET", "POST", "OPTIONS", "DELETE", "PATCH", "PUT"]
|
||||
|
||||
SECRET_KEY=sercretKey
|
||||
ALGORITHM=HS256
|
||||
|
||||
SMTP_SERVER=localhost
|
||||
SMTP_PORT=1025
|
||||
SMTP_EMAIL=noreply@cityvibe.ru
|
||||
SMTP_PASS=test
|
||||
|
||||
RMQ_HOST=localhost
|
||||
RMQ_USER=guest
|
||||
RMQ_PASS=guest
|
||||
RMQ_PORT=5672
|
||||
|
||||
S3_URL=http://192.168.31.190:9002
|
||||
S3_ACCESS_KEY_ID=lorsan
|
||||
S3_SECRET_ACCESS_KEY=Lorser2009!
|
||||
S3_BUCKET_NAME=aether
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
__pycache__
|
||||
.env
|
||||
.env.prod
|
||||
test.py
|
||||
.env.old
|
||||
.env
|
||||
Generated
+1
-1
@@ -4,7 +4,7 @@
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<sourceFolder url="file://$MODULE_DIR$/backend" isTestSource="false" />
|
||||
</content>
|
||||
<orderEntry type="jdk" jdkName="Python 3.13 (aetherbackend-6Zf3gKAD-py3.13)" jdkType="Python SDK" />
|
||||
<orderEntry type="jdk" jdkName="Python 3.14 (aetherbackend-6Zf3gKAD-py3.14)" jdkType="Python SDK" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
<component name="PyDocumentationSettings">
|
||||
|
||||
Generated
+1
-1
@@ -3,5 +3,5 @@
|
||||
<component name="Black">
|
||||
<option name="sdkName" value="Python 3.13" />
|
||||
</component>
|
||||
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.13 (aetherbackend-6Zf3gKAD-py3.13)" project-jdk-type="Python SDK" />
|
||||
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.14 (aetherbackend-6Zf3gKAD-py3.14)" project-jdk-type="Python SDK" />
|
||||
</project>
|
||||
@@ -97,8 +97,8 @@ pip install -e .
|
||||
# Или используя poetry
|
||||
poetry install
|
||||
|
||||
# Создайте .env файл
|
||||
cat > .env << EOF
|
||||
# Создайте .env.prod файл
|
||||
cat > .env.prod << EOF
|
||||
DATABASE_URL=postgresql+asyncpg://user:password@localhost:5432/aether
|
||||
REDIS_URL=redis://localhost:6379
|
||||
SECRET_KEY=your-secret-key-here
|
||||
@@ -130,8 +130,8 @@ cd frontend
|
||||
# Установите зависимости
|
||||
npm install
|
||||
|
||||
# Создайте .env файл
|
||||
echo "VITE_API_URL=http://localhost:8000" > .env
|
||||
# Создайте .env.prod файл
|
||||
echo "VITE_API_URL=http://localhost:8000" > .env.prod
|
||||
|
||||
# Запустите dev сервер
|
||||
npm run dev
|
||||
@@ -279,7 +279,7 @@ pytest tests/ -v
|
||||
docker build -t aether-backend .
|
||||
|
||||
# Запуск контейнера
|
||||
docker run -p 8000:8000 --env-file .env aether-backend
|
||||
docker run -p 8000:8000 --env-file .env.prod aether-backend
|
||||
```
|
||||
|
||||
### Frontend
|
||||
|
||||
@@ -77,7 +77,7 @@ class MessageDAO(BaseDAO[MessageModel, MessageCreateDB, MessageUpdateDB]):
|
||||
model = MessageModel
|
||||
|
||||
@classmethod
|
||||
async def find_all_asc(
|
||||
async def find_all_desc(
|
||||
cls,
|
||||
session: AsyncSession,
|
||||
offset: Optional[int],
|
||||
@@ -85,7 +85,7 @@ class MessageDAO(BaseDAO[MessageModel, MessageCreateDB, MessageUpdateDB]):
|
||||
*filter,
|
||||
**filter_by
|
||||
) -> List[MessageModel]:
|
||||
stmt = select(MessageModel).filter(*filter).filter_by(**filter_by).order_by(MessageModel.created_at.asc())
|
||||
stmt = select(MessageModel).filter(*filter).filter_by(**filter_by).order_by(MessageModel.created_at.desc())
|
||||
|
||||
if offset is not None:
|
||||
stmt = stmt.offset(offset)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import uuid
|
||||
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
from sqlalchemy import ForeignKey, UUID, UniqueConstraint
|
||||
from sqlalchemy import ForeignKey, UUID, UniqueConstraint, text
|
||||
|
||||
from app.core.database import Base
|
||||
|
||||
@@ -14,6 +14,7 @@ class MessageModel(Base):
|
||||
chat_id: Mapped[uuid.UUID] = mapped_column(UUID, ForeignKey("chat.id", ondelete="CASCADE"), index=True)
|
||||
content: Mapped[str] = mapped_column()
|
||||
is_read: Mapped[bool] = mapped_column(default=False)
|
||||
is_edited: Mapped[bool] = mapped_column(default=False, server_default=text("false"))
|
||||
|
||||
|
||||
class ChatModel(Base):
|
||||
|
||||
@@ -6,7 +6,7 @@ from fastapi import APIRouter, Depends, WebSocket, WebSocketDisconnect
|
||||
from app.chats.service import ChatService
|
||||
from app.auth.dependencies import get_current_verified_user
|
||||
from app.users.models import UserModel
|
||||
from app.chats.schemas import Chat, MessageCreate, Message
|
||||
from app.chats.schemas import Chat, MessageCreate, Message, MessageUpdate
|
||||
|
||||
router = APIRouter(prefix="/chats", tags=["chats"])
|
||||
|
||||
@@ -19,7 +19,7 @@ async def get_chats(
|
||||
return await ChatService.get_chats(user, offset, limit)
|
||||
|
||||
@router.get("/{chat_id}")
|
||||
async def get_chat(
|
||||
async def get_messages(
|
||||
chat_id: uuid.UUID,
|
||||
offset: int = 0,
|
||||
limit: int = 10,
|
||||
@@ -31,6 +31,10 @@ async def get_chat(
|
||||
async def send_message(message: MessageCreate, user: UserModel = Depends(get_current_verified_user)) -> Message:
|
||||
return await ChatService.send_message(user, message)
|
||||
|
||||
@router.put("/message")
|
||||
async def edit_message(message_update: MessageUpdate, user: UserModel = Depends(get_current_verified_user)) -> Message:
|
||||
return await ChatService.update_message(user, message_update)
|
||||
|
||||
|
||||
@router.websocket("/ws")
|
||||
async def websocket_endpoint(ws: WebSocket, user: UserModel = Depends(get_current_verified_user)):
|
||||
|
||||
@@ -2,7 +2,7 @@ from datetime import datetime
|
||||
from typing import Optional
|
||||
import uuid
|
||||
|
||||
from pydantic import BaseModel
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
|
||||
|
||||
class MessageCreate(BaseModel):
|
||||
@@ -21,10 +21,13 @@ class MessageCreateDB(BaseModel):
|
||||
chat_id: Optional[uuid.UUID]
|
||||
content: Optional[str]
|
||||
is_read: Optional[bool] = False
|
||||
is_edited: Optional[bool] = False
|
||||
|
||||
|
||||
class MessageUpdateDB(BaseModel):
|
||||
content: Optional[str]
|
||||
is_edited: Optional[bool] = False
|
||||
|
||||
|
||||
|
||||
class Message(BaseModel):
|
||||
@@ -32,9 +35,12 @@ class Message(BaseModel):
|
||||
sender_id: int
|
||||
chat_id: uuid.UUID
|
||||
content: str
|
||||
is_edited: Optional[bool] = False
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
class ChatBase(BaseModel):
|
||||
is_group: Optional[bool] = False
|
||||
|
||||
@@ -9,7 +9,7 @@ from sqlalchemy import and_
|
||||
from app.core.database import async_session_maker
|
||||
from app.chats.dao import ChatDAO, MessageDAO, ParticipantDAO
|
||||
from app.chats.models import ChatModel, MessageModel, ParticipantModel
|
||||
from app.chats.schemas import Chat, MessageCreate, MessageCreateDB, ChatCreateDB, ParticipantCreateDB, Message
|
||||
from app.chats.schemas import Chat, MessageCreate, MessageCreateDB, ChatCreateDB, ParticipantCreateDB, Message, MessageUpdateDB, MessageUpdate
|
||||
from app.users.models import UserModel
|
||||
from app.core.redis import get_redis
|
||||
|
||||
@@ -92,14 +92,7 @@ class ChatService:
|
||||
)
|
||||
)
|
||||
|
||||
await cls._send_ws_message(members_ids, Message(
|
||||
id=message_db.id,
|
||||
sender_id=message_db.sender_id,
|
||||
chat_id=message_db.chat_id,
|
||||
content=message_db.content,
|
||||
created_at=message_db.created_at,
|
||||
updated_at=message_db.updated_at
|
||||
))
|
||||
await cls._send_ws_message(members_ids, Message.model_validate(message_db))
|
||||
|
||||
await ChatDAO.update(
|
||||
session,
|
||||
@@ -124,7 +117,7 @@ class ChatService:
|
||||
log.warning("Access denied to chat", extra={"user_id": user.id, "chat_id": chat_id})
|
||||
raise HTTPException(status.HTTP_403_FORBIDDEN, detail="Access denied")
|
||||
|
||||
messages = await MessageDAO.find_all_asc(
|
||||
messages = await MessageDAO.find_all_desc(
|
||||
session,
|
||||
offset,
|
||||
limit,
|
||||
@@ -181,3 +174,44 @@ class ChatService:
|
||||
}
|
||||
await redis_client.publish("messenger_updates", json.dumps(payload))
|
||||
log.debug(f"Published message for user_id: {user_id}")
|
||||
|
||||
|
||||
@classmethod
|
||||
async def update_message(cls, user: UserModel, message_update: MessageUpdate) -> Message:
|
||||
async with async_session_maker() as session:
|
||||
message_exist = await MessageDAO.find_one_or_none(
|
||||
session,
|
||||
and_(
|
||||
MessageModel.id==message_update.id,
|
||||
MessageModel.sender_id==user.id
|
||||
)
|
||||
)
|
||||
|
||||
if message_exist is None:
|
||||
log.warning("Message not found", extra={"user_id": user.id, "message_id": message_update.id})
|
||||
raise HTTPException(status.HTTP_404_NOT_FOUND, detail="Message not found")
|
||||
|
||||
|
||||
message_update_db = await MessageDAO.update(
|
||||
session,
|
||||
MessageModel.id==message_update.id,
|
||||
obj_in=MessageUpdateDB(
|
||||
content=message_update.content,
|
||||
is_edited=True
|
||||
)
|
||||
)
|
||||
|
||||
members = await ParticipantDAO.find_all(
|
||||
session,
|
||||
None,
|
||||
None,
|
||||
ParticipantModel.chat_id==message_exist.chat_id
|
||||
)
|
||||
|
||||
member_ids = [member.user_id for member in members]
|
||||
|
||||
await cls._send_ws_message(member_ids, Message.model_validate(message_update_db))
|
||||
|
||||
await session.commit()
|
||||
log.info("Message update successfully", extra={"user_id": user.id, "message_id": message_update.id})
|
||||
return message_update_db
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
"""Edit message table: adding is_edited column
|
||||
|
||||
Revision ID: 76159faa56c8
|
||||
Revises: 0d3f7039ba77
|
||||
Create Date: 2026-01-20 17:22:23.965106
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = '76159faa56c8'
|
||||
down_revision: Union[str, Sequence[str], None] = '0d3f7039ba77'
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
"""Upgrade schema."""
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.add_column('message', sa.Column('is_edited', sa.Boolean(), nullable=False, server_default=sa.text("false")))
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
"""Downgrade schema."""
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_column('message', 'is_edited')
|
||||
# ### end Alembic commands ###
|
||||
+3
-3
@@ -56,7 +56,7 @@ services:
|
||||
networks:
|
||||
- aether
|
||||
env_file:
|
||||
- .env
|
||||
- .env.prod
|
||||
|
||||
celery:
|
||||
build:
|
||||
@@ -70,7 +70,7 @@ services:
|
||||
networks:
|
||||
- aether
|
||||
env_file:
|
||||
- .env
|
||||
- .env.prod
|
||||
restart: unless-stopped
|
||||
|
||||
backend:
|
||||
@@ -95,7 +95,7 @@ services:
|
||||
networks:
|
||||
- aether
|
||||
env_file:
|
||||
- .env
|
||||
- .env.prod
|
||||
restart: unless-stopped
|
||||
|
||||
frontend:
|
||||
|
||||
@@ -66,6 +66,14 @@ function App() {
|
||||
</PrivateRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/chat/:chatId"
|
||||
element={
|
||||
<PrivateRoute>
|
||||
<ChatPage />
|
||||
</PrivateRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/profile"
|
||||
element={
|
||||
|
||||
+1003
-6
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,58 @@
|
||||
import apiClient from './api';
|
||||
|
||||
export type Chat = {
|
||||
chat_id: string;
|
||||
user_id: number;
|
||||
last_message: string | null;
|
||||
avatar_url: string | null;
|
||||
display_name: string;
|
||||
}
|
||||
|
||||
export type Message = {
|
||||
id: string;
|
||||
sender_id: number;
|
||||
chat_id: string;
|
||||
content: string;
|
||||
is_edited?: boolean;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export type MessageCreate = {
|
||||
content: string;
|
||||
chat_id?: string;
|
||||
recipient_id?: number;
|
||||
}
|
||||
|
||||
export type MessageUpdate = {
|
||||
id: string;
|
||||
content: string;
|
||||
}
|
||||
|
||||
const chatService = {
|
||||
async getChats(offset: number = 0, limit: number = 10): Promise<Chat[]> {
|
||||
const response = await apiClient.get('/chats/', {
|
||||
params: { offset, limit }
|
||||
});
|
||||
return response.data;
|
||||
},
|
||||
|
||||
async getChatMessages(chatId: string, offset: number = 0, limit: number = 50): Promise<Message[]> {
|
||||
const response = await apiClient.get(`/chats/${chatId}`, {
|
||||
params: { offset, limit }
|
||||
});
|
||||
return response.data;
|
||||
},
|
||||
|
||||
async sendMessage(data: MessageCreate): Promise<Message> {
|
||||
const response = await apiClient.post('/chats/message', data);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
async updateMessage(data: MessageUpdate): Promise<Message> {
|
||||
const response = await apiClient.put('/chats/message', data);
|
||||
return response.data;
|
||||
}
|
||||
};
|
||||
|
||||
export default chatService;
|
||||
@@ -26,6 +26,11 @@ export const userService = {
|
||||
return response.data;
|
||||
},
|
||||
|
||||
getUserById: async (userId: number): Promise<User> => {
|
||||
const response = await apiClient.get(`/users/${userId}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
updateProfile: async (data: UserUpdate): Promise<User> => {
|
||||
const response = await apiClient.put('/users/me', data);
|
||||
return response.data;
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import { create } from 'zustand';
|
||||
import { persist } from 'zustand/middleware';
|
||||
import type { Chat } from '../services/chatService';
|
||||
|
||||
interface ChatStore {
|
||||
chats: Chat[];
|
||||
setChats: (chats: Chat[]) => void;
|
||||
updateChat: (chatId: string, updates: Partial<Chat>) => void;
|
||||
clearChats: () => void;
|
||||
}
|
||||
|
||||
export const useChatStore = create<ChatStore>()(
|
||||
persist(
|
||||
(set) => ({
|
||||
chats: [],
|
||||
|
||||
setChats: (chats) => set({ chats }),
|
||||
|
||||
updateChat: (chatId, updates) =>
|
||||
set((state) => ({
|
||||
chats: state.chats.map(chat =>
|
||||
chat.chat_id === chatId ? { ...chat, ...updates } : chat
|
||||
)
|
||||
})),
|
||||
|
||||
clearChats: () => set({ chats: [] }),
|
||||
}),
|
||||
{
|
||||
name: 'aether-chats',
|
||||
partialize: (state) => ({ chats: state.chats }),
|
||||
}
|
||||
)
|
||||
);
|
||||
Reference in New Issue
Block a user