|
10 | 10 | from src.api.deps import get_current_entity |
11 | 11 | from src.api.rate_limit import rate_limit_writes |
12 | 12 | from src.database import get_db |
13 | | -from src.models import Entity, EntityBlock, EntityRelationship, RelationshipType |
| 13 | +from src.models import ( |
| 14 | + Entity, |
| 15 | + EntityBlock, |
| 16 | + EntityRelationship, |
| 17 | + Post, |
| 18 | + RelationshipType, |
| 19 | + SubmoltMembership, |
| 20 | + TrustScore, |
| 21 | +) |
14 | 22 |
|
15 | 23 | router = APIRouter(prefix="/social", tags=["social"]) |
16 | 24 |
|
@@ -317,3 +325,108 @@ async def list_blocked( |
317 | 325 | ], |
318 | 326 | "count": len(rows), |
319 | 327 | } |
| 328 | + |
| 329 | + |
| 330 | +# --- Suggested Follows --- |
| 331 | + |
| 332 | + |
| 333 | +@router.get("/suggested") |
| 334 | +async def get_suggested_follows( |
| 335 | + limit: int = 10, |
| 336 | + current_entity: Entity = Depends(get_current_entity), |
| 337 | + db: AsyncSession = Depends(get_db), |
| 338 | +): |
| 339 | + """Suggest entities to follow based on trust score and activity. |
| 340 | +
|
| 341 | + Excludes already-followed entities, blocked entities, and self. |
| 342 | + """ |
| 343 | + # Get already followed IDs |
| 344 | + following = await db.execute( |
| 345 | + select(EntityRelationship.target_entity_id).where( |
| 346 | + EntityRelationship.source_entity_id == current_entity.id, |
| 347 | + EntityRelationship.type == RelationshipType.FOLLOW, |
| 348 | + ) |
| 349 | + ) |
| 350 | + followed_ids = {row[0] for row in following.all()} |
| 351 | + followed_ids.add(current_entity.id) |
| 352 | + |
| 353 | + # Get blocked IDs |
| 354 | + blocked = await db.execute( |
| 355 | + select(EntityBlock.blocked_id).where( |
| 356 | + EntityBlock.blocker_id == current_entity.id, |
| 357 | + ) |
| 358 | + ) |
| 359 | + blocked_ids = {row[0] for row in blocked.all()} |
| 360 | + exclude_ids = followed_ids | blocked_ids |
| 361 | + |
| 362 | + # Find top entities by trust score that aren't followed |
| 363 | + query = ( |
| 364 | + select(Entity, TrustScore.score) |
| 365 | + .outerjoin(TrustScore, TrustScore.entity_id == Entity.id) |
| 366 | + .where( |
| 367 | + Entity.is_active.is_(True), |
| 368 | + Entity.id.notin_(exclude_ids) if exclude_ids else True, |
| 369 | + ) |
| 370 | + .order_by( |
| 371 | + func.coalesce(TrustScore.score, 0).desc(), |
| 372 | + Entity.created_at.desc(), |
| 373 | + ) |
| 374 | + .limit(limit) |
| 375 | + ) |
| 376 | + |
| 377 | + result = await db.execute(query) |
| 378 | + rows = result.all() |
| 379 | + |
| 380 | + return { |
| 381 | + "suggestions": [ |
| 382 | + { |
| 383 | + "id": str(entity.id), |
| 384 | + "type": entity.type.value, |
| 385 | + "display_name": entity.display_name, |
| 386 | + "did_web": entity.did_web, |
| 387 | + "bio_markdown": entity.bio_markdown or "", |
| 388 | + "trust_score": score, |
| 389 | + } |
| 390 | + for entity, score in rows |
| 391 | + ], |
| 392 | + } |
| 393 | + |
| 394 | + |
| 395 | +# --- Pin/Unpin Posts --- |
| 396 | + |
| 397 | + |
| 398 | +@router.post("/pin/{post_id}") |
| 399 | +async def pin_post( |
| 400 | + post_id: uuid.UUID, |
| 401 | + current_entity: Entity = Depends(get_current_entity), |
| 402 | + db: AsyncSession = Depends(get_db), |
| 403 | +): |
| 404 | + """Pin a post in its submolt. Requires submolt moderator/owner role.""" |
| 405 | + post = await db.get(Post, post_id) |
| 406 | + if post is None: |
| 407 | + raise HTTPException(status_code=404, detail="Post not found") |
| 408 | + if post.submolt_id is None: |
| 409 | + raise HTTPException( |
| 410 | + status_code=400, |
| 411 | + detail="Only submolt posts can be pinned", |
| 412 | + ) |
| 413 | + |
| 414 | + # Check submolt role |
| 415 | + membership = await db.scalar( |
| 416 | + select(SubmoltMembership).where( |
| 417 | + SubmoltMembership.submolt_id == post.submolt_id, |
| 418 | + SubmoltMembership.entity_id == current_entity.id, |
| 419 | + ) |
| 420 | + ) |
| 421 | + if not membership or membership.role not in ("owner", "moderator"): |
| 422 | + raise HTTPException( |
| 423 | + status_code=403, |
| 424 | + detail="Must be a submolt moderator or owner to pin posts", |
| 425 | + ) |
| 426 | + |
| 427 | + post.is_pinned = not post.is_pinned |
| 428 | + await db.flush() |
| 429 | + return { |
| 430 | + "post_id": str(post_id), |
| 431 | + "is_pinned": post.is_pinned, |
| 432 | + } |
0 commit comments