mirror of
https://github.com/fromchat-messenger/web.git
synced 2026-09-22 19:15:08 +03:00
Add member count API for public chat
This commit is contained in:
@@ -0,0 +1,10 @@
|
|||||||
|
---
|
||||||
|
description: Do not filter cache, API, or DB fields by matching fixed UI/placeholder English strings
|
||||||
|
alwaysApply: true
|
||||||
|
---
|
||||||
|
|
||||||
|
# No magic-string “sanitization” of user or message data
|
||||||
|
|
||||||
|
- **Never** strip, null out, or rewrite stored or displayed values by comparing them to hard-coded UI strings (e.g. `"Direct messages"`, `"Direct message"`, `"User 123"`, etc.). Those strings can be legitimate **usernames, display names, or message text**.
|
||||||
|
- **Prefer**: fix the source (don’t persist placeholders; use `null`/absent fields; fix the writer). If legacy bad rows exist, use an explicit **schema/version/migration** or a **documented sentinel** agreed with the backend—not substring or equality checks on natural language.
|
||||||
|
- Applies especially to: local storage/cache layers, list previews, and any code that “cleans” strings before show or read.
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from collections import defaultdict, deque
|
from collections import defaultdict, deque
|
||||||
import time
|
import time
|
||||||
from fastapi import APIRouter, Depends, HTTPException, status, Request
|
from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException, status, Request
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
from sqlalchemy import inspect, text
|
from sqlalchemy import inspect, text
|
||||||
import uuid
|
import uuid
|
||||||
@@ -25,6 +25,17 @@ _FAILED_ATTEMPT_THRESHOLD = 5
|
|||||||
_failed_login_attempts: dict[str, deque[float]] = defaultdict(deque)
|
_failed_login_attempts: dict[str, deque[float]] = defaultdict(deque)
|
||||||
|
|
||||||
|
|
||||||
|
async def _broadcast_registered_user_count_task():
|
||||||
|
from ..db import SessionLocal
|
||||||
|
from .messaging import messagingManager
|
||||||
|
|
||||||
|
db = SessionLocal()
|
||||||
|
try:
|
||||||
|
await messagingManager.broadcast_registered_user_count(db)
|
||||||
|
finally:
|
||||||
|
db.close()
|
||||||
|
|
||||||
|
|
||||||
def _record_failed_login(identifier: str) -> bool:
|
def _record_failed_login(identifier: str) -> bool:
|
||||||
now = time.time()
|
now = time.time()
|
||||||
attempts = _failed_login_attempts[identifier]
|
attempts = _failed_login_attempts[identifier]
|
||||||
@@ -173,7 +184,12 @@ def login(request: Request, login_request: LoginRequest, db: Session = Depends(g
|
|||||||
|
|
||||||
@router.post("/register")
|
@router.post("/register")
|
||||||
@rate_limit_per_ip("3/hour")
|
@rate_limit_per_ip("3/hour")
|
||||||
def register(request: Request, register_request: RegisterRequest, db: Session = Depends(get_db)):
|
def register(
|
||||||
|
request: Request,
|
||||||
|
register_request: RegisterRequest,
|
||||||
|
background_tasks: BackgroundTasks,
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
):
|
||||||
username = register_request.username.strip()
|
username = register_request.username.strip()
|
||||||
display_name = register_request.display_name.strip()
|
display_name = register_request.display_name.strip()
|
||||||
password = register_request.password.strip()
|
password = register_request.password.strip()
|
||||||
@@ -288,6 +304,8 @@ def register(request: Request, register_request: RegisterRequest, db: Session =
|
|||||||
owner=is_owner,
|
owner=is_owner,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
background_tasks.add_task(_broadcast_registered_user_count_task)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"status": "success",
|
"status": "success",
|
||||||
"message": "Регистрация прошла успешно",
|
"message": "Регистрация прошла успешно",
|
||||||
@@ -550,6 +568,12 @@ async def _delete_user_data(user: User, db: Session):
|
|||||||
# Log error but don't fail the request
|
# Log error but don't fail the request
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
from .messaging import messagingManager
|
||||||
|
await messagingManager.broadcast_registered_user_count(db)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
@router.post("/delete")
|
@router.post("/delete")
|
||||||
async def delete_account(
|
async def delete_account(
|
||||||
|
|||||||
@@ -1045,6 +1045,8 @@ class MessaggingSocketManager:
|
|||||||
elif update_type == "statusUpdate":
|
elif update_type == "statusUpdate":
|
||||||
# Deduplicate by user ID
|
# Deduplicate by user ID
|
||||||
sig_data = {"type": update_type, "userId": data.get("userId")}
|
sig_data = {"type": update_type, "userId": data.get("userId")}
|
||||||
|
elif update_type == "registeredUserCount":
|
||||||
|
sig_data = {"type": update_type, "count": data.get("count")}
|
||||||
else:
|
else:
|
||||||
# For unknown types, use full data (less efficient but safe)
|
# For unknown types, use full data (less efficient but safe)
|
||||||
sig_data = {"type": update_type, "data": data}
|
sig_data = {"type": update_type, "data": data}
|
||||||
@@ -1297,6 +1299,17 @@ class MessaggingSocketManager:
|
|||||||
if websocket in self.user_by_ws:
|
if websocket in self.user_by_ws:
|
||||||
await self._send_update(websocket, message_type, update_data, db)
|
await self._send_update(websocket, message_type, update_data, db)
|
||||||
|
|
||||||
|
async def broadcast_registered_user_count(self, db: Session):
|
||||||
|
"""Notify all clients of the current non-deleted user count (public chat member count)."""
|
||||||
|
try:
|
||||||
|
n = db.query(User).filter(User.deleted.is_(False)).count()
|
||||||
|
except Exception:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
await self.broadcast({"type": "registeredUserCount", "data": {"count": n}}, db)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
async def send_update_to_user(self, user_id: int, update_type: str, update_data: dict, db: Session | None = None):
|
async def send_update_to_user(self, user_id: int, update_type: str, update_data: dict, db: Session | None = None):
|
||||||
"""Send an update to a specific user (batched)"""
|
"""Send an update to a specific user (batched)"""
|
||||||
for websocket in self.connections:
|
for websocket in self.connections:
|
||||||
|
|||||||
@@ -276,6 +276,16 @@ async def update_user_bio(
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/user/stats/registered-count")
|
||||||
|
def get_registered_user_count(
|
||||||
|
current_user: User = Depends(get_current_user),
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
):
|
||||||
|
"""Number of registered accounts (non-deleted users)."""
|
||||||
|
n = db.query(User).filter(User.deleted.is_(False)).count()
|
||||||
|
return {"count": n}
|
||||||
|
|
||||||
|
|
||||||
@router.get("/user/{username}")
|
@router.get("/user/{username}")
|
||||||
async def get_user_by_username(
|
async def get_user_by_username(
|
||||||
username: str,
|
username: str,
|
||||||
|
|||||||
Reference in New Issue
Block a user