# retoor <retoor@molodetz.nl>
import logging
import secrets
from datetime import datetime, timezone
from fastapi import APIRouter, Request
from devplacepy.database import get_table
from devplacepy.utils import XP_POST
from devplacepy.content import (
create_content_item,
delete_content_item,
apply_vote,
create_comment_record,
set_bookmark,
is_owner,
is_admin,
)
from devplacepy.services.audit import record as audit
from devplacepy.services.correction import schedule_correction
from devplacepy.services.ai_modifier import schedule_modification
from devplacepy.services.devrant.params import merge_params
from devplacepy.services.devrant.tokens import resolve_user
from devplacepy.services.devrant.ids import as_int, post_by_id
from devplacepy.services.devrant.feed import list_rants, search_rants, load_rant_detail
from devplacepy.services.devrant.serializers import encode_tags
from devplacepy.routers.devrant._shared import dr_ok, dr_error, unauthorized
logger = logging.getLogger(__name__)
router = APIRouter()
DEFAULT_LIMIT = 20
MAX_LIMIT = 50
def _parse_tags(raw: object) -> list:
if not raw:
return []
if isinstance(raw, list):
return [str(tag).strip() for tag in raw if str(tag).strip()]
return [tag.strip() for tag in str(raw).split(",") if tag.strip()]
@router.get("/devrant/rants")
async def rant_feed(request: Request):
params = await merge_params(request)
viewer = resolve_user(params)
sort = params.get("sort") or "recent"
limit = min(MAX_LIMIT, max(1, as_int(params.get("limit"), DEFAULT_LIMIT)))
skip = max(0, as_int(params.get("skip"), 0))
rants = list_rants(sort, limit, skip, viewer)
num_notifs = 0
if viewer:
num_notifs = get_table("notifications").count(
user_uid=viewer["uid"], read=False
)
return dr_ok(
rants=rants,
settings={"notif_state": -1, "notif_token": ""},
set=secrets.token_hex(6),
wrw=0,
dpp=0,
num_notifs=num_notifs,
unread={"total": num_notifs},
news=None,
)
@router.get("/devrant/search")
async def search(request: Request):
params = await merge_params(request)
viewer = resolve_user(params)
term = (params.get("term") or "").strip()
return dr_ok(results=search_rants(term, viewer) if term else [])
@router.post("/devrant/rants")
async def create_rant(request: Request):
params = await merge_params(request)
user = resolve_user(params)
if not user:
return unauthorized()
text = (params.get("rant") or "").strip()
if len(text) < 1:
return dr_error("Your rant is too short.")
if len(text) > 125000:
return dr_error("Your rant is too long.")
tags = _parse_tags(params.get("tags"))
project_uid = params.get("project_uid") or None
uid, slug = create_content_item(
"posts",
"post",
user,
{
"title": None,
"content": text,
"topic": "rant",
"project_uid": project_uid,
"image": None,
"tags": encode_tags(tags),
},
text[:50],
XP_POST,
"First Post",
text,
None,
request,
)
post = get_table("posts").find_one(uid=uid)
return dr_ok(rant_id=int(post["id"]))
@router.get("/devrant/rants/{rant_id}")
async def get_rant(request: Request, rant_id: str):
params = await merge_params(request)
viewer = resolve_user(params)
post = post_by_id(rant_id)
if not post:
return dr_error("This rant does not exist.")
detail = load_rant_detail(post, viewer)
return dr_ok(rant=detail["rant"], comments=detail["comments"], subscribed=0)
@router.post("/devrant/rants/{rant_id}")
async def edit_rant(request: Request, rant_id: str):
params = await merge_params(request)
user = resolve_user(params)
if not user:
return unauthorized()
post = post_by_id(rant_id)
if not post:
return dr_error("This rant does not exist.", fail_reason="not_found")
if not is_owner(post, user):
return dr_error("You can only edit your own rants.", fail_reason="not_owner")
text = (params.get("rant") or "").strip()
if len(text) < 1 or len(text) > 125000:
return dr_error("Invalid rant length.", fail_reason="length")
tags = _parse_tags(params.get("tags"))
get_table("posts").update(
{
"uid": post["uid"],
"content": text,
"tags": encode_tags(tags),
"updated_at": datetime.now(timezone.utc).isoformat(),
},
["uid"],
)
schedule_correction(user, "posts", post["uid"], request)
schedule_modification(user, "posts", post["uid"], request)
audit.record(
request,
"post.edit",
user=user,
target_type="post",
target_uid=post["uid"],
origin="devrant",
summary=f"{user['username']} edited rant {post['id']}",
links=[audit.target("post", post["uid"])],
)
return dr_ok()
@router.delete("/devrant/rants/{rant_id}")
async def delete_rant(request: Request, rant_id: str):
params = await merge_params(request)
user = resolve_user(params)
if not user:
return unauthorized()
post = post_by_id(rant_id)
if not post:
return dr_error("This rant does not exist.")
if not (is_owner(post, user) or is_admin(user)):
return dr_error("You can only delete your own rants.")
delete_content_item(request, "posts", "post", user, post["slug"], "/feed")
return dr_ok()
@router.post("/devrant/rants/{rant_id}/vote")
async def vote_rant(request: Request, rant_id: str):
params = await merge_params(request)
user = resolve_user(params)
if not user:
return unauthorized()
post = post_by_id(rant_id)
if not post:
return dr_error("This rant does not exist.")
value = as_int(params.get("vote"))
if value not in (-1, 0, 1):
return dr_error("Invalid vote.")
apply_vote(request, user, "post", post["uid"], value)
fresh = get_table("posts").find_one(uid=post["uid"])
detail = load_rant_detail(fresh, user)
return dr_ok(rant=detail["rant"])
@router.post("/devrant/rants/{rant_id}/favorite")
async def favorite_rant(request: Request, rant_id: str):
return await _set_favorite(request, rant_id, True)
@router.post("/devrant/rants/{rant_id}/unfavorite")
async def unfavorite_rant(request: Request, rant_id: str):
return await _set_favorite(request, rant_id, False)
async def _set_favorite(request: Request, rant_id: str, saved: bool):
params = await merge_params(request)
user = resolve_user(params)
if not user:
return unauthorized()
post = post_by_id(rant_id)
if not post:
return dr_error("This rant does not exist.")
set_bookmark(request, user, "post", post["uid"], saved)
return dr_ok()
@router.post("/devrant/rants/{rant_id}/comments")
async def comment_rant(request: Request, rant_id: str):
params = await merge_params(request)
user = resolve_user(params)
if not user:
return unauthorized()
post = post_by_id(rant_id)
if not post:
return dr_error("This rant does not exist.")
text = (params.get("comment") or "").strip()
if len(text) < 1:
return dr_error("Your comment is too short.")
if len(text) > 1000:
return dr_error("Your comment is too long.")
create_comment_record(request, user, "post", post["uid"], text)
return dr_ok()