forked from retoor/devplacepy
feat: add per-user avatar seed regeneration with irreversible random avatar replacement
Implement a new `avatar_seed` column on the users table that overrides the username-based seed for Multiavatar generation. Introduce a null-safe `avatar_seed(user)` choke point in `avatar.py` that resolves `user.get("avatar_seed") or user.get("username")`, registered as a Jinja global so every render site (`_avatar_link.html`, `avatar_url(...)` calls, SEO `og_image`, issues ad-hoc dicts, devRant payload/PNG) propagates a regenerated seed. Add `POST /profile/{username}/regenerate-avatar` endpoint (owner-or-admin only) that writes a fresh `generate_uid()` to `avatar_seed`, invalidates the target's user cache, and audits `profile.avatar.regenerate`. The previous seed is overwritten and never stored, making regeneration irreversible. Document the feature in `AGENTS.md` and `README.md`, add the API endpoint to `docs_api.py`, and include the `regenerate_avatar` Devii tool in `CONFIRM_REQUIRED`.
This commit is contained in:
@@ -501,3 +501,27 @@
|
||||
.game-perks {
|
||||
margin-top: var(--space-xl);
|
||||
}
|
||||
|
||||
.game-legacy {
|
||||
margin-top: var(--space-xl);
|
||||
}
|
||||
|
||||
.game-legacy-note {
|
||||
color: var(--text-muted);
|
||||
font-size: 0.85rem;
|
||||
margin: 0 0 var(--space-md);
|
||||
}
|
||||
|
||||
.legacy-card {
|
||||
border-color: #f5c518;
|
||||
}
|
||||
|
||||
.game-plot-golden {
|
||||
border-color: #f5c518;
|
||||
box-shadow: 0 0 0 1px #f5c518 inset, 0 0 12px rgba(245, 197, 24, 0.4);
|
||||
}
|
||||
|
||||
.plot-steal-locked {
|
||||
color: var(--warning);
|
||||
font-family: var(--font-mono);
|
||||
}
|
||||
|
||||
@@ -28,6 +28,11 @@
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.avatar-regen-btn {
|
||||
display: block;
|
||||
margin: 0.5rem auto 0;
|
||||
}
|
||||
|
||||
.profile-name {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 700;
|
||||
|
||||
@@ -18,6 +18,7 @@ import { ReactionBar } from "./ReactionBar.js";
|
||||
import { BookmarkManager } from "./BookmarkManager.js";
|
||||
import { PollManager } from "./PollManager.js";
|
||||
import { ApiKeyManager } from "./ApiKeyManager.js";
|
||||
import { AvatarRegenerator } from "./AvatarRegenerator.js";
|
||||
import { CustomizationToggle } from "./CustomizationToggle.js";
|
||||
import { NotificationPrefs } from "./NotificationPrefs.js";
|
||||
import { AiCorrection } from "./AiCorrection.js";
|
||||
@@ -67,6 +68,7 @@ class Application {
|
||||
this.bookmarks = new BookmarkManager();
|
||||
this.polls = new PollManager();
|
||||
this.apiKey = new ApiKeyManager();
|
||||
this.avatarRegenerator = new AvatarRegenerator();
|
||||
this.customizationToggle = new CustomizationToggle();
|
||||
this.notificationPrefs = new NotificationPrefs(this.pubsub);
|
||||
this.aiCorrection = new AiCorrection();
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
// retoor <retoor@molodetz.nl>
|
||||
|
||||
import { Http } from "./Http.js";
|
||||
import { Toast } from "./Toast.js";
|
||||
|
||||
let confirmedThisSession = false;
|
||||
|
||||
class AvatarRegenerator {
|
||||
constructor() {
|
||||
this.btn = document.querySelector("[data-regenerate-avatar]");
|
||||
if (!this.btn) return;
|
||||
this.preview = document.getElementById("profile-avatar-preview");
|
||||
this.btn.addEventListener("click", () => this.regenerate());
|
||||
}
|
||||
|
||||
async regenerate() {
|
||||
if (!confirmedThisSession) {
|
||||
const ok = await window.app.dialog.confirm({
|
||||
title: "Regenerate avatar",
|
||||
message: "A new random avatar will be generated. Your current avatar is gone for good and can never be brought back.",
|
||||
confirmLabel: "Regenerate",
|
||||
danger: true,
|
||||
});
|
||||
if (!ok) return;
|
||||
confirmedThisSession = true;
|
||||
}
|
||||
const username = this.btn.dataset.username;
|
||||
this.btn.disabled = true;
|
||||
try {
|
||||
const result = await Http.postJson(`/profile/${username}/regenerate-avatar`, {});
|
||||
const url = result && result.data ? result.data.avatar_url : "";
|
||||
if (!url) {
|
||||
Toast.flash(this.btn, "Error", 2000, "Regenerate avatar");
|
||||
return;
|
||||
}
|
||||
if (this.preview) this.preview.src = url;
|
||||
Toast.flash(this.btn, "Regenerated", 2000, "Regenerate avatar");
|
||||
} catch {
|
||||
Toast.flash(this.btn, "Error", 2000, "Regenerate avatar");
|
||||
} finally {
|
||||
this.btn.disabled = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
window.AvatarRegenerator = AvatarRegenerator;
|
||||
export { AvatarRegenerator };
|
||||
@@ -183,8 +183,10 @@ export class CommentManager {
|
||||
actions.insertAdjacentElement("afterend", form);
|
||||
|
||||
this.enhanceForm(form);
|
||||
const textarea = form.querySelector("textarea");
|
||||
if (textarea) textarea.focus();
|
||||
setTimeout(() => {
|
||||
const textarea = form.querySelector("textarea");
|
||||
if (textarea) textarea.focus();
|
||||
}, 20);
|
||||
}
|
||||
|
||||
enhanceForm(form) {
|
||||
@@ -192,7 +194,6 @@ export class CommentManager {
|
||||
if (enhancer) {
|
||||
enhancer.initEmojiPickers();
|
||||
enhancer.initMentionInputs();
|
||||
enhancer.initAttachmentManagers();
|
||||
}
|
||||
const textarea = form.querySelector("textarea");
|
||||
if (textarea) {
|
||||
|
||||
@@ -69,6 +69,7 @@ export class GameFarm {
|
||||
if (this.mode === "own") {
|
||||
this._setHost("[data-shop-host]", this._shopHtml(farm));
|
||||
this._setHost("[data-perk-host]", this._perksHtml(farm));
|
||||
this._setHost("[data-legacy-host]", this._legacyHtml(farm));
|
||||
this._setHost("[data-daily-host]", this._dailyHtml(farm));
|
||||
this._setHost("[data-quest-host]", this._questsHtml(farm));
|
||||
}
|
||||
@@ -92,6 +93,7 @@ export class GameFarm {
|
||||
set("[data-hud-harvests]", farm.total_harvests);
|
||||
set("[data-hud-prestige]", farm.prestige);
|
||||
set("[data-hud-prestige-mult]", `+${Math.round(farm.prestige_multiplier * 100 - 100)}% coins`);
|
||||
set("[data-hud-stars]", farm.stars);
|
||||
const fill = this.root.querySelector("[data-hud-xp-fill]");
|
||||
if (fill) {
|
||||
const pct = farm.level_is_max || !farm.level_span ? 100 : Math.floor((100 * farm.level_into) / farm.level_span);
|
||||
@@ -130,6 +132,17 @@ export class GameFarm {
|
||||
.join("");
|
||||
}
|
||||
|
||||
_legacyHtml(farm) {
|
||||
return (farm.legacy || [])
|
||||
.map((up) => {
|
||||
const action = up.maxed
|
||||
? `<span class="perk-maxed">Maxed</span>`
|
||||
: `<form method="post" action="/game/legacy" data-game-action="legacy"><input type="hidden" name="key" value="${up.key}"><button type="submit" class="btn btn-sm">Buy (${up.cost} ★)</button></form>`;
|
||||
return `<div class="perk-card legacy-card"><span class="perk-icon" aria-hidden="true">${up.icon}</span><strong class="perk-name">${up.name}</strong><span class="perk-level">Lv ${up.level}/${up.max_level}</span><span class="perk-effect">${up.effect || up.description}</span>${action}</div>`;
|
||||
})
|
||||
.join("");
|
||||
}
|
||||
|
||||
_dailyHtml(farm) {
|
||||
const action = farm.daily_available
|
||||
? `<form method="post" action="/game/daily" data-game-action="daily"><button type="submit" class="btn btn-sm btn-primary">Claim +${farm.daily_reward}c</button></form>`
|
||||
@@ -164,7 +177,8 @@ export class GameFarm {
|
||||
}
|
||||
|
||||
_plotHtml(plot, farm) {
|
||||
const head = `<div class="game-plot game-plot-${plot.state}" data-slot="${plot.slot}" data-state="${plot.state}" data-ready-at="${plot.ready_at}">`;
|
||||
const golden = plot.is_golden ? " game-plot-golden" : "";
|
||||
const head = `<div class="game-plot game-plot-${plot.state}${golden}" data-slot="${plot.slot}" data-state="${plot.state}" data-ready-at="${plot.ready_at}">`;
|
||||
let body = "";
|
||||
if (plot.state === "empty") {
|
||||
if (farm.is_owner) {
|
||||
@@ -188,11 +202,15 @@ export class GameFarm {
|
||||
body += `<form method="post" action="/game/fertilize" data-game-action="fertilize"><input type="hidden" name="slot" value="${plot.slot}"><button type="submit" class="btn btn-sm">⚡ Fertilize (${plot.fertilize_cost}c)</button></form>`;
|
||||
}
|
||||
} else {
|
||||
body = `<span class="plot-icon plot-ready-icon" aria-hidden="true">${plot.crop_icon}</span><span class="plot-crop-name">${plot.crop_name}</span>`;
|
||||
const goldenMark = plot.is_golden ? " ✨" : "";
|
||||
body = `<span class="plot-icon plot-ready-icon" aria-hidden="true">${plot.crop_icon}</span><span class="plot-crop-name">${plot.crop_name}${goldenMark}</span>`;
|
||||
if (farm.is_owner) {
|
||||
body += `<form method="post" action="/game/harvest" data-game-action="harvest"><input type="hidden" name="slot" value="${plot.slot}"><button type="submit" class="btn btn-sm btn-primary">Harvest +${plot.reward_coins}c</button></form>`;
|
||||
const label = plot.is_golden ? "Harvest golden" : "Harvest";
|
||||
body += `<form method="post" action="/game/harvest" data-game-action="harvest"><input type="hidden" name="slot" value="${plot.slot}"><button type="submit" class="btn btn-sm btn-primary">${label}</button></form>`;
|
||||
} else if (plot.can_steal) {
|
||||
body += `<form method="post" action="/game/farm/${farm.owner_username}/steal" data-game-action="steal"><input type="hidden" name="slot" value="${plot.slot}"><button type="submit" class="btn btn-sm btn-danger" data-confirm="Steal this ready build?">Steal +${plot.steal_coins}c</button></form>`;
|
||||
} else if (plot.steal_reason === "cooldown") {
|
||||
body += `<span class="plot-ready-label plot-steal-locked">Raid again in ${this._format(plot.steal_cooldown_seconds)}</span>`;
|
||||
} else {
|
||||
body += `<span class="plot-ready-label">Ready</span>`;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user