Implement verification checkmark

This commit is contained in:
2025-10-21 22:49:26 +03:00
Unverified
parent 3293d91368
commit 9e19342998
23 changed files with 736 additions and 52 deletions
+27 -3
View File
@@ -4,8 +4,15 @@ from contextlib import asynccontextmanager
import subprocess
import sys
import os
from constants import DATABASE_URL
from routes import account, messaging, profile, push, webrtc
import logging
from models import User
from constants import OWNER_USERNAME
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
logger = logging.getLogger("uvicorn.error")
@asynccontextmanager
async def lifespan(app: FastAPI):
@@ -20,13 +27,30 @@ async def lifespan(app: FastAPI):
"import sys; sys.path.append('.'); from migration import run_migrations; run_migrations()"
],
cwd=os.path.dirname(os.path.abspath(__file__))
# No capture_output - let it stream to terminal in real-time
# No text=True - let it use the terminal's encoding
)
except Exception as e:
print(f"Failed to run database migrations: {e}")
raise
try:
engine = create_engine(DATABASE_URL)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
with SessionLocal() as db:
# Find the owner user
owner = db.query(User).filter(User.username == OWNER_USERNAME).first()
if owner and not owner.verified:
owner.verified = True
db.commit()
logger.info(f"Owner user '{OWNER_USERNAME}' has been verified")
elif owner and owner.verified:
logger.info(f"Owner user '{OWNER_USERNAME}' is already verified")
else:
logger.warning(f"Owner user '{OWNER_USERNAME}' not found")
except Exception as e:
logger.error(f"Failed to ensure owner verification: {e}")
yield
# Shutdown (if needed in the future)
+2
View File
@@ -20,6 +20,7 @@ class User(Base):
online = Column(Boolean, default=False)
last_seen = Column(DateTime, default=datetime.now)
created_at = Column(DateTime, default=datetime.now)
verified = Column(Boolean, default=False)
messages = relationship("Message", back_populates="author", lazy="select")
@@ -186,6 +187,7 @@ class UserProfileResponse(BaseModel):
online: bool
last_seen: datetime
created_at: datetime
verified: bool
class Config:
from_attributes = True
+8 -2
View File
@@ -20,7 +20,8 @@ def convert_user(user: User) -> dict:
"display_name": user.display_name,
"profile_picture": user.profile_picture,
"bio": user.bio,
"admin": user.username == OWNER_USERNAME
"admin": user.username == OWNER_USERNAME,
"verified": user.verified
}
@router.get("/check_auth")
@@ -113,12 +114,17 @@ def register(request: RegisterRequest, db: Session = Depends(get_db)):
)
hashed_password = get_password_hash(password)
# Set verified=True for the owner (first user to register)
is_owner = not owner_exists and username == OWNER_USERNAME
new_user = User(
username=username,
display_name=display_name,
password_hash=hashed_password,
online=True,
last_seen=datetime.now()
last_seen=datetime.now(),
verified=is_owner
)
db.add(new_user)
+9
View File
@@ -60,6 +60,7 @@ def convert_message(msg: Message) -> dict:
"is_edited": msg.is_edited,
"username": msg.author.display_name,
"profile_picture": msg.author.profile_picture,
"verified": msg.author.verified,
"reply_to": convert_message(msg.reply_to) if msg.reply_to else None,
"reactions": list(reactions_dict.values()),
"files": [
@@ -92,6 +93,13 @@ def convert_dm_envelope(envelope: DMEnvelope) -> dict:
"username": reaction.user.display_name
})
# Get sender info for verified status
from models import User
from dependencies import get_db
db = next(get_db())
sender = db.query(User).filter(User.id == envelope.sender_id).first()
sender_verified = sender.verified if sender else False
return {
"id": envelope.id,
"senderId": envelope.sender_id,
@@ -102,6 +110,7 @@ def convert_dm_envelope(envelope: DMEnvelope) -> dict:
"iv2": envelope.iv2_b64,
"wrappedMk": envelope.wrapped_mk_b64,
"timestamp": envelope.timestamp.isoformat(),
"verified": sender_verified,
"reactions": list(reactions_dict.values()),
"files": [
{
+63 -1
View File
@@ -12,6 +12,7 @@ from dependencies import get_db, get_current_user
from models import User, UpdateBioRequest, UserProfileResponse
from pydantic import BaseModel
from validation import is_valid_username, is_valid_display_name
from similarity import is_user_similar_to_verified
router = APIRouter()
@@ -244,5 +245,66 @@ async def get_user_by_id(
bio=user.bio,
online=user.online,
last_seen=user.last_seen,
created_at=user.created_at
created_at=user.created_at,
verified=user.verified
)
@router.post("/user/{user_id}/verify")
async def verify_user(
user_id: int,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db)
):
"""
Toggle verification status for a user (owner only)
"""
# Only user with ID 1 (owner) can verify users
if current_user.id != 1:
raise HTTPException(status_code=403, detail="Only owner can verify users")
target_user = db.query(User).filter(User.id == user_id).first()
if not target_user:
raise HTTPException(status_code=404, detail="User not found")
# Toggle verification status
target_user.verified = not target_user.verified
db.commit()
return {
"verified": target_user.verified,
"message": f"User verification {'enabled' if target_user.verified else 'disabled'}"
}
@router.get("/user/check-similarity/{user_id}")
async def check_user_similarity(
user_id: int,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db)
):
"""
Check if a user is similar to any verified user
"""
target_user = db.query(User).filter(User.id == user_id).first()
if not target_user:
raise HTTPException(status_code=404, detail="User not found")
# Get all verified users
verified_users = db.query(User).filter(User.verified == True).all()
verified_users_data = [
{"username": user.username, "display_name": user.display_name}
for user in verified_users
]
# Check similarity
is_similar, similar_to = is_user_similar_to_verified(
target_user.username,
target_user.display_name,
verified_users_data
)
return {
"isSimilar": is_similar,
"similarTo": similar_to if is_similar else None
}
+147
View File
@@ -0,0 +1,147 @@
"""
Similarity detection utilities for username and display name comparison.
Implements both edit distance and visual similarity detection.
"""
def levenshtein_distance(s1: str, s2: str) -> int:
"""Calculate Levenshtein distance between two strings."""
if len(s1) < len(s2):
return levenshtein_distance(s2, s1)
if len(s2) == 0:
return len(s1)
previous_row = list(range(len(s2) + 1))
for i, c1 in enumerate(s1):
current_row = [i + 1]
for j, c2 in enumerate(s2):
insertions = previous_row[j + 1] + 1
deletions = current_row[j] + 1
substitutions = previous_row[j] + (c1 != c2)
current_row.append(min(insertions, deletions, substitutions))
previous_row = current_row
return previous_row[-1]
def check_visual_similarity(s1: str, s2: str) -> bool:
"""
Check if two strings are visually similar using common homoglyphs.
Returns True if strings are visually similar.
"""
if len(s1) != len(s2):
return False
# Common homoglyph mappings
homoglyphs = {
'0': ['O', 'o', 'Q'],
'O': ['0', 'o', 'Q'],
'o': ['0', 'O', 'Q'],
'1': ['l', 'I', '|'],
'l': ['1', 'I', '|'],
'I': ['1', 'l', '|'],
'5': ['S', 's'],
'S': ['5', 's'],
's': ['5', 'S'],
'6': ['G', 'g'],
'G': ['6', 'g'],
'g': ['6', 'G'],
'8': ['B', 'b'],
'B': ['8', 'b'],
'b': ['8', 'B'],
'9': ['g', 'q'],
'g': ['9', 'q'],
'q': ['9', 'g'],
'2': ['Z', 'z'],
'Z': ['2', 'z'],
'z': ['2', 'Z'],
'3': ['E'],
'E': ['3'],
'4': ['A'],
'A': ['4'],
'7': ['T', 't'],
'T': ['7', 't'],
't': ['7', 'T'],
}
for i in range(len(s1)):
c1, c2 = s1[i], s2[i]
if c1 == c2:
continue
# Check if characters are homoglyphs
if (c1 in homoglyphs and c2 in homoglyphs[c1]) or \
(c2 in homoglyphs and c1 in homoglyphs[c2]):
continue
return False
return True
def check_username_similarity(username1: str, username2: str) -> bool:
"""
Check if two usernames are similar using both edit distance and visual similarity.
Returns True if usernames are considered similar.
"""
if username1 == username2:
return False
# Check edit distance (Levenshtein distance <= 2)
edit_distance = levenshtein_distance(username1.lower(), username2.lower())
if edit_distance <= 2:
return True
# Check visual similarity
if check_visual_similarity(username1, username2):
return True
return False
def check_display_name_similarity(display_name1: str, display_name2: str) -> bool:
"""
Check if two display names are similar using both edit distance and visual similarity.
Returns True if display names are considered similar.
"""
if display_name1 == display_name2:
return False
# Check edit distance (Levenshtein distance <= 2)
edit_distance = levenshtein_distance(display_name1.lower(), display_name2.lower())
if edit_distance <= 2:
return True
# Check visual similarity
if check_visual_similarity(display_name1, display_name2):
return True
return False
def is_user_similar_to_verified(user_username: str, user_display_name: str,
verified_users: list[dict]) -> tuple[bool, str]:
"""
Check if a user is similar to any verified user.
Args:
user_username: Username to check
user_display_name: Display name to check
verified_users: List of verified user dictionaries with 'username' and 'display_name' keys
Returns:
Tuple of (is_similar, similar_to_username)
"""
for verified_user in verified_users:
verified_username = verified_user.get('username', '')
verified_display_name = verified_user.get('display_name', '')
# Check username similarity
if check_username_similarity(user_username, verified_username):
return True, verified_username
# Check display name similarity
if check_display_name_similarity(user_display_name, verified_display_name):
return True, verified_username
return False, ""