Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
9ecc5b8
preparation of routes
Aug 20, 2025
b10ba62
feat: implement message creation and conversation handling in message…
Aug 20, 2025
67b9e20
feat: add message handling with CRUD operations and update message sc…
Aug 21, 2025
339d889
feat: enhance conversation retrieval with DTO response model
Aug 21, 2025
64e4626
feat: add updated_at column to Message table and update related DTO
Aug 21, 2025
5b92d0a
feat(#30): implement WebSocket support for chat and enhance message D…
Aug 22, 2025
f903c83
update app version to 2.2.0
Aug 22, 2025
f7d6fec
update app version to 2.2.0
Aug 22, 2025
0b39460
feat: refactor comment and like endpoints to use DTOs for improved re…
evanhgs Aug 23, 2025
cf5d3ce
feat: upgrade PostDetailResponse to use DTOs for likes and comments
evanhgs Aug 26, 2025
d13eeeb
feat: enhance comment handling by integrating UserLightDTO for user d…
evanhgs Aug 27, 2025
0b6e384
feat: enhance conversation handling by integrating UserLightDTO and o…
evanhgs Sep 7, 2025
42f3e12
feat: add self-message prevention and optimize notification sending i…
evanhgs Sep 8, 2025
22abde3
chore: version
evanhgs Sep 9, 2025
9c02241
feat: refactor message handling to use UserLightDTO for sender detail…
evanhgs Sep 9, 2025
a351b98
removing websocket features
evanhgs Sep 25, 2025
98de64b
add mqtt broker and configuration
evanhgs Sep 27, 2025
ee190a2
feat: add MQTT broker configuration and entrypoint script, docker con…
evanhgs Sep 28, 2025
2596004
chore: rm detail from ConversationOut dto
evanhgs Sep 28, 2025
aeb3808
chore: update PostgreSQL port and requirements, bump version to v2.3
Oct 3, 2025
9f3ab11
fix: update PostgreSQL port in docker-compose, refactor message publi…
Oct 4, 2025
61fd30d
feat: add MQTT service configuration to docker-compose
Oct 4, 2025
49d5254
Merge branch 'master' into dev
evanhgs Oct 4, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,8 @@
# POSTGRES_DB=val_local
# POSTGRES_USER=val_local
# POSTGRES_PASSWORD=val_local
#
# === mosquitto broker credentials ===
# MQTT_HOST=localhost
# MQTT_USER=mqttuser
# MQTT_PASSWORD=mqttpassword
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,5 @@ htmlcov/
# Docker
.docker

# Mqtt
mqtt/passwordfile
16 changes: 16 additions & 0 deletions app/core/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from pydantic_settings import BaseSettings
from pydantic import Field
from fastapi_mqtt import MQTTConfig, FastMQTT

class Settings(BaseSettings):
DATABASE_URL: str = Field(..., alias="DATABASE_URL")
Expand All @@ -12,8 +13,23 @@ class Settings(BaseSettings):
POSTGRES_USER: Optional[str] = Field(None, alias="POSTGRES_USER")
POSTGRES_PASSWORD: Optional[str] = Field(None, alias="POSTGRES_PASSWORD")

MQTT_HOST: str = Field(..., alias="MQTT_HOST")
MQTT_USER: str = Field(..., alias="MQTT_USER")
MQTT_PASSWORD: str = Field(..., alias="MQTT_PASSWORD")

class Config:
env_file = ".env"
populate_by_name = True

settings = Settings()

# mqtt broker config
fast_mqtt = FastMQTT(
config=MQTTConfig(
host=settings.MQTT_HOST,
port=1883,
username=settings.MQTT_USER,
password=settings.MQTT_PASSWORD,
keepalive=60
)
)
3 changes: 1 addition & 2 deletions app/core/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@

from app.core.config import settings


UPLOAD_FOLDER = "public/uploads/"
ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif'}

Expand All @@ -37,7 +36,7 @@ def jwt_user_id(request: Request) -> int:
raise HTTPException(status_code=400, detail="Invalid or missing token")
if isinstance(data['id'], int):
return data['id']
raise HTTPException(status_code=400, detail="An error occurred with the token")
raise HTTPException(status_code=400, detail="An error occurred with the token, please login to refresh it")


def allowed_file(filename: str) -> bool:
Expand Down
27 changes: 26 additions & 1 deletion app/main.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
from contextlib import asynccontextmanager

from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware

from app.core.config import fast_mqtt
from app.core.utils import get_version
from app.routes import auth, user, follow, post, like, comment, message
from app.core.database import engine
Expand All @@ -9,10 +12,18 @@
# create DB
Base.metadata.create_all(bind=engine)



@asynccontextmanager
async def _lifespan(_app: FastAPI):
await fast_mqtt.mqtt_startup()
yield
await fast_mqtt.mqtt_shutdown()
app = FastAPI(
title="Valenstagram API v 2",
title="Valenstagram API v2",
docs_url="/api/docs",
openapi_url="/api/openapi.json",
lifespan=_lifespan
)

app.add_middleware(
Expand All @@ -22,6 +33,20 @@
allow_headers=["*"],
)

@fast_mqtt.on_connect()
def connect(client, flags, rc, properties):
print("Connected to MQTT Broker")
client.subscribe("chat/+/messages")

@fast_mqtt.on_message()
async def messages(client, topic, payload, qos, properties):
print(f"Message reçu: {payload} sur {topic}")

@app.post("/ssage/send-me")
async def send_message(topic: str, message: str):
fast_mqtt.publish(topic, message)
return {"status": "message sent"}

# Routers
app.include_router(auth.router)
app.include_router(user.router)
Expand Down
5 changes: 5 additions & 0 deletions app/models/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,9 @@ class Conversation(Base):
user2_id = Column(Integer, ForeignKey('utilisateur.id'), nullable=False)
created_at = Column(DateTime, default=utc_now)

user1 = relationship('User', foreign_keys=[user1_id])
user2 = relationship('User', foreign_keys=[user2_id])

messages = relationship('Message', backref='conversation', lazy='joined')

__table_args__ = (UniqueConstraint('user1_id', 'user2_id', name='unique_conversation'),)
Expand All @@ -98,3 +101,5 @@ class Message(Base):
created_at = Column(DateTime, default=utc_now)
updated_at = Column(DateTime, default=utc_now)
is_read = Column(Boolean, default=False)

sender = relationship('User', foreign_keys=[sender_id])
136 changes: 79 additions & 57 deletions app/routes/message.py
Original file line number Diff line number Diff line change
@@ -1,27 +1,30 @@
import asyncio
import datetime
from typing import List

from fastapi import APIRouter, Depends, HTTPException, WebSocket
from sqlalchemy.orm import Session
from fastapi import APIRouter, Depends, HTTPException
from fastapi.encoders import jsonable_encoder
from sqlalchemy.orm import Session, joinedload

from app.core.config import fast_mqtt
from app.core.database import get_db
from app.core.utils import jwt_user_id, ConnectionManager
from app.core.utils import jwt_user_id
from app.models.models import Conversation, User, Message
from app.schemas.message import MessageSent, MessageOut, MessageUpdate, ConversationOut, ConversationDTO, MessageDTO
from app.schemas.user import UserLightDTO

router = APIRouter(prefix="/message", tags=["messages"])
manager = ConnectionManager()

@router.post("/send/{username}", response_model=MessageOut)
def send_message(
@router.post("/send/{user_id}", response_model=MessageDTO)
async def send_message(
payload: MessageSent,
username: str,
user_id: int,
db: Session = Depends(get_db),
current_user: int = Depends(jwt_user_id)
):
"""send message to user and check if conv exist else create a new conversation"""
other_user = db.query(User).filter_by(username=username).first()
other_user = db.query(User).filter_by(id=user_id).first()
if other_user.id == current_user:
raise HTTPException(status_code=400, detail="You cannot talk to yourself")

conversation = db.query(Conversation).filter(
((Conversation.user1_id == current_user) & (Conversation.user2_id == other_user.id)) |
Expand All @@ -42,27 +45,25 @@ def send_message(
db.commit()
db.refresh(new_message)

# send notification to the other user if connected
asyncio.create_task(manager.send_personal_message(
{
"event": "new_message",
"conversation_id": conversation.id,
"message": {
"id": new_message.id,
"sender_id": new_message.sender_id,
"content": new_message.content,
"created_at": new_message.created_at.isoformat(),
"is_read": new_message.is_read
}
},
other_user.id
))

return MessageOut(
detail="success",
message=MessageDTO.model_validate(new_message, from_attributes=True) # avoid warning from IDE & prevent error from SQLAlchemy orm
message_dto = MessageDTO(
id=new_message.id,
conversation_id=new_message.conversation_id,
sender=UserLightDTO(
id=new_message.sender_id,
username=new_message.sender.username,
profile_picture=new_message.sender.profile_picture
),
content=new_message.content,
created_at=new_message.created_at,
updated_at=new_message.updated_at,
is_read=new_message.is_read
)

topic = f"chat/{conversation.id}/messages"
fast_mqtt.publish(topic, jsonable_encoder(message_dto))

return message_dto



@router.delete("/{message_id}")
Expand Down Expand Up @@ -123,15 +124,31 @@ def get_conversation_content(

# display read message
for msg in conversation.messages:
if msg.sender_id != current_user and not msg.is_read:
if msg.sender != current_user and not msg.is_read:
msg.is_read = True
db.commit()
db.refresh(conversation)

message_sorted = sorted(conversation.messages, key=lambda m: m.created_at, reverse=False)

return ConversationOut(
conversation=ConversationDTO.model_validate(conversation, from_attributes=True),
messages=[MessageDTO.model_validate(m, from_attributes=True) for m in conversation.messages],
detail="success",
messages=[
MessageDTO(
id=m.id,
conversation_id=m.conversation_id,
sender=UserLightDTO(
id=m.sender.id,
username=m.sender.username,
profile_picture=m.sender.profile_picture
),
content=m.content,
created_at=m.created_at,
updated_at=m.updated_at,
is_read=m.is_read
)
for m in message_sorted
]
)

@router.get("/conversations", response_model=List[ConversationDTO])
Expand All @@ -140,32 +157,37 @@ def get_user_conversations(
current_user: int = Depends(jwt_user_id)
):
"""display all the conversations of the current user"""
conversations = db.query(Conversation).filter(
(Conversation.user1_id == current_user) | (Conversation.user2_id == current_user)
).all()
conversations = (
db.query(Conversation)
.options(
joinedload(Conversation.user1),
joinedload(Conversation.user2)
)
.filter(
(Conversation.user1_id == current_user) |
(Conversation.user2_id == current_user)
).all()
)

if not conversations:
return []

return conversations


""" Maybe a good features ?
@router.delete("/{conversation_id}")
def delete_conversation(conversation_id):
# delete conversation and all this message
"""

@router.websocket("/ws/{user_id}")
async def websocket_endpoint_chat(
user_id: int,
websocket: WebSocket
):
"""websocket endpoint for chat"""
await manager.connect(websocket, user_id)
try:
while True:
data = await websocket.receive_text()
await manager.send_personal_message({"message: ": data}, user_id)
except Exception as e:
manager.disconnect(websocket, user_id)
print(f"WebSocket disconnected for user {user_id}: {e}")
conversations_dto = []
for conv in conversations:
conversations_dto.append(
ConversationDTO(
id=conv.id,
user1=UserLightDTO(
id=conv.user1.id,
username=conv.user1.username,
profile_picture=conv.user1.profile_picture,
),
user2=UserLightDTO(
id=conv.user2.id,
username=conv.user2.username,
profile_picture=conv.user2.profile_picture,
),
created_at=conv.created_at,
)
)
return conversations_dto
9 changes: 5 additions & 4 deletions app/schemas/message.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
from pydantic import BaseModel
from datetime import datetime

from app.schemas.user import UserLightDTO


class MessageSent(BaseModel):
content: str
Expand All @@ -11,7 +13,7 @@ class MessageUpdate(BaseModel):
class MessageDTO(BaseModel):
id: int
conversation_id: int
sender_id: int
sender: UserLightDTO
content: str
created_at: datetime
updated_at: datetime
Expand All @@ -29,14 +31,13 @@ class Config:

class ConversationDTO(BaseModel):
id: int
user1_id: int
user2_id: int
user1: UserLightDTO
user2: UserLightDTO
created_at: datetime

class ConversationOut(BaseModel):
conversation: ConversationDTO
messages: list[MessageDTO]
detail: str

class Config:
orm_mode = True
17 changes: 17 additions & 0 deletions docker-compose.prd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,23 @@ services:
- frontend
- backend

mqtt:
image: eclipse-mosquitto
container_name: mqtt
restart: unless-stopped
env_file:
- .env
volumes:
- ./mqtt/mosquitto.conf:/mosquitto/config/mosquitto.conf
- ./mqtt/entrypoint.sh:/mosquitto/entrypoint.sh
- ./mqtt/data:/mosquitto/data
- ./mqtt/log:/mosquitto/log
- ./mqtt/config:/mosquitto/config
ports:
- "1883:1883"
- "9001:9001"
entrypoint: [ "/mosquitto/entrypoint.sh" ]

volumes:
postgres_data:
uploads_data:
Expand Down
Loading