# retoor <retoor@molodetz.nl>
from devplacepy.database import (
get_table,
get_user_stars,
record_activity,
record_unique_activity,
)
from devplacepy.services.background import background
from devplacepy.utils.authcache import clear_user_cache
from devplacepy.utils.badges import award_badge, notify_badge, LEVEL_BADGES
from devplacepy.utils.notifications import create_notification
LEVEL_XP = 100
XP_POST = 10
XP_COMMENT = 2
XP_PROJECT = 15
XP_GIST = 5
XP_UPVOTE = 5
XP_FOLLOW = 5
def level_for_xp(xp: int) -> int:
return 1 + max(0, xp) // LEVEL_XP
def award_xp(user_uid: str, amount: int) -> dict:
users = get_table("users")
user = users.find_one(uid=user_uid)
if not user or amount <= 0:
return {
"xp": user.get("xp", 0) if user else 0,
"level": user.get("level", 1) if user else 1,
"leveled_up": False,
}
current_xp = user.get("xp", 0) or 0
current_level = user.get("level", 1) or 1
new_xp = max(0, current_xp + amount)
new_level = level_for_xp(new_xp)
users.update({"uid": user_uid, "xp": new_xp, "level": new_level}, ["uid"])
clear_user_cache(user_uid)
leveled_up = new_level > current_level
from devplacepy.services.audit import record as audit
audit.record_system(
"reward.xp.grant",
actor_kind="user",
actor_uid=user_uid,
new_value=amount,
metadata={"amount": amount, "total_xp": new_xp},
summary=f"user earned {amount} XP",
links=[audit.actor(user_uid)],
)
if leveled_up:
audit.record_system(
"reward.level.up",
actor_kind="user",
actor_uid=user_uid,
old_value=current_level,
new_value=new_level,
metadata={"level": new_level},
summary=f"user reached level {new_level}",
links=[audit.actor(user_uid)],
)
create_notification(
user_uid,
"level",
f"You reached level {new_level}",
user_uid,
f"/profile/{user['username']}",
)
for level in range(current_level + 1, new_level + 1):
badge_name = LEVEL_BADGES.get(level)
if badge_name and award_badge(user_uid, badge_name):
notify_badge(user_uid, badge_name)
return {"xp": new_xp, "level": new_level, "leveled_up": leveled_up}
_COUNT_MILESTONES = [
("Prolific", "posts", 10),
("Wordsmith", "posts", 50),
("Veteran", "posts", 100),
("Conversationalist", "comments", 25),
("Debater", "comments", 100),
("Builder", "projects", 5),
("Architect", "projects", 10),
("Snippet Collector", "gists", 5),
("Rising Star", "stars", 25),
("Star Author", "stars", 100),
("Superstar", "stars", 500),
("Popular", "followers", 10),
("Influencer", "followers", 50),
("Celebrity", "followers", 100),
("Connector", "following", 10),
("On Fire", "streak", 7),
("Dedicated", "streak", 30),
("Unstoppable", "streak", 100),
]
def _milestone_metrics(user_uid: str):
cache: dict[str, int] = {}
def resolve(key: str) -> int:
if key in cache:
return cache[key]
if key == "posts":
value = get_table("posts").count(user_uid=user_uid, deleted_at=None)
elif key == "comments":
value = get_table("comments").count(user_uid=user_uid, deleted_at=None)
elif key == "projects":
value = get_table("projects").count(user_uid=user_uid, deleted_at=None)
elif key == "gists":
value = get_table("gists").count(user_uid=user_uid, deleted_at=None)
elif key == "stars":
value = get_user_stars(user_uid)
elif key == "followers":
value = get_table("follows").count(following_uid=user_uid, deleted_at=None)
elif key == "following":
value = get_table("follows").count(follower_uid=user_uid, deleted_at=None)
elif key == "streak":
from devplacepy.database import get_streaks
value = get_streaks(user_uid)["current"]
else:
value = 0
cache[key] = value
return value
return resolve
def check_milestone_badges(user_uid: str) -> list:
held = {row["badge_name"] for row in get_table("badges").find(user_uid=user_uid)}
pending = [m for m in _COUNT_MILESTONES if m[0] not in held]
if not pending:
return []
metric = _milestone_metrics(user_uid)
awarded = []
for badge_name, metric_key, threshold in pending:
if metric(metric_key) >= threshold and award_badge(user_uid, badge_name):
awarded.append(badge_name)
if metric_key == "streak":
from devplacepy.services.audit import record as audit
audit.record_system(
"reward.streak.milestone",
actor_kind="user",
actor_uid=user_uid,
metadata={"streak": metric("streak")},
summary=f"user reached a {metric('streak')}-day streak",
links=[audit.actor(user_uid)],
)
for badge_name in awarded:
notify_badge(user_uid, badge_name)
return awarded
def award_rewards(user_uid: str, amount: int, first_badge: str | None = None) -> None:
background.submit(_apply_rewards, user_uid, amount, first_badge)
def _apply_rewards(user_uid: str, amount: int, first_badge: str | None = None) -> None:
if first_badge:
award_badge(user_uid, first_badge)
award_xp(user_uid, amount)
check_milestone_badges(user_uid)
ACHIEVEMENTS = {
"docs.read": [(1, "Curious"), (5, "Studious"), (15, "Scholar")],
"devii": [(1, "AI Curious"), (25, "AI Whisperer")],
"fork": [(1, "First Fork"), (5, "Forker")],
"zip": [(1, "Archivist")],
"seo": [(1, "SEO Auditor")],
"deepsearch": [(1, "Researcher"), (10, "Deep Diver")],
"container": [(1, "Container Captain")],
"message": [(1, "Messenger"), (100, "Chatterbox")],
"bookmark": [(1, "Bookmarker"), (25, "Curator")],
"reaction": [(1, "First Reaction"), (50, "Cheerleader")],
"vote": [(1, "First Star Given"), (50, "Supporter"), (250, "Patron")],
"follow": [(1, "Friendly")],
"upload": [(1, "First Upload")],
"project_file": [(1, "File Creator")],
"issue": [(1, "Bug Reporter")],
"poll": [(1, "Pollster")],
"profile": [(1, "Profiled")],
"harvest": [(1, "Green Thumb"), (50, "Master Farmer")],
"water": [(1, "Good Neighbor")],
"harvest_stolen": [(1, "Cat Burglar")],
"got_stolen_from": [(1, "Robbed")],
}
UNIQUE_ACTIONS = {"docs.read"}
def track_action(user_uid: str, action: str, target: str | None = None) -> None:
if not user_uid or action not in ACHIEVEMENTS:
return
background.submit(_apply_achievements, user_uid, action, target)
def _apply_achievements(user_uid: str, action: str, target: str | None) -> None:
if action in UNIQUE_ACTIONS:
if target is None:
return
count = record_unique_activity(user_uid, action, target)
else:
count = record_activity(user_uid, action)
if not count:
return
for threshold, badge_name in ACHIEVEMENTS.get(action, []):
if count >= threshold and award_badge(user_uid, badge_name):
notify_badge(user_uid, badge_name)