This commit is contained in:
2026-06-22 11:40:27 +02:00
parent e965775809
commit 01be0c523e
6 changed files with 195 additions and 7 deletions
+67 -2
View File
@@ -30,6 +30,8 @@ type Entry struct {
ReplyCount int64 `json:"reply_count"`
LastActivity int64 `json:"last_activity"`
Deleted int64 `json:"deleted"`
BumpCount int64 `json:"bump_count"`
LastBump int64 `json:"last_bump"`
Username string `json:"username"`
Avatar string `json:"avatar"`
}
@@ -39,6 +41,7 @@ type Entry struct {
const entrySelect = `
SELECT e.pid, e.uid, e.created_at, e.content, e.filepath,
e.reply_to, e.reply_count, e.last_activity, e.deleted,
e.bump_count, e.last_bump,
COALESCE(u.username, ''), COALESCE(u.avatar, '')
FROM entry e LEFT JOIN user u ON u.uid = e.uid`
@@ -49,7 +52,8 @@ func scanEntries(rows *sql.Rows) ([]Entry, error) {
for rows.Next() {
var e Entry
if err := rows.Scan(&e.PID, &e.UID, &e.CreatedAt, &e.Content, &e.Filepath,
&e.ReplyTo, &e.ReplyCount, &e.LastActivity, &e.Deleted, &e.Username, &e.Avatar); err != nil {
&e.ReplyTo, &e.ReplyCount, &e.LastActivity, &e.Deleted,
&e.BumpCount, &e.LastBump, &e.Username, &e.Avatar); err != nil {
return nil, err
}
entries = append(entries, e)
@@ -62,7 +66,8 @@ func entryByPID(pid int64) (Entry, error) {
var e Entry
err := db.QueryRow(entrySelect+` WHERE e.pid = ?`, pid).Scan(
&e.PID, &e.UID, &e.CreatedAt, &e.Content, &e.Filepath,
&e.ReplyTo, &e.ReplyCount, &e.LastActivity, &e.Deleted, &e.Username, &e.Avatar)
&e.ReplyTo, &e.ReplyCount, &e.LastActivity, &e.Deleted,
&e.BumpCount, &e.LastBump, &e.Username, &e.Avatar)
return e, err
}
@@ -341,6 +346,66 @@ func handleDeleteEntry(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusOK, map[string]string{"status": "deleted"})
}
// bumpStep ist der Cooldown-Zuwachs je bereits erfolgtem Bump: nach dem 1. Bump
// ein Tag Sperre, nach dem 10. Bump zehn Tage. So wird Hochspülen mit jedem Mal
// teurer.
const bumpStep = 24 * 60 * 60
// handleBump hebt last_activity eines Beitrags auf jetzt -> er steigt im Feed
// (Roots, sortiert nach last_activity) bzw. in der Antwortliste eines Threads
// wieder nach oben. Tritt an die Stelle der früheren "Bump"-Kommentare. Gegen
// Spam wächst der Cooldown linear mit der Zahl bisheriger Bumps (siehe bumpStep);
// er sitzt am Beitrag selbst (global), nicht am bumpenden Nutzer. Jeder
// Eingeloggte darf bumpen (Route in der requireAuth-Gruppe).
func handleBump(w http.ResponseWriter, r *http.Request) {
pid, err := strconv.ParseInt(chi.URLParam(r, "pid"), 10, 64)
if err != nil {
writeError(w, http.StatusBadRequest, "Ungültige pid")
return
}
var bumpCount, lastBump, deleted int64
if err := db.QueryRow(`SELECT bump_count, last_bump, deleted FROM entry WHERE pid = ?`, pid).
Scan(&bumpCount, &lastBump, &deleted); err != nil {
writeError(w, http.StatusNotFound, "Beitrag nicht gefunden")
return
}
if deleted != 0 {
writeError(w, http.StatusNotFound, "Beitrag nicht gefunden")
return
}
now := time.Now().Unix()
// Cooldown = bisherige Bumps in Tagen. Beim ersten Bump (bumpCount 0) ist die
// Sperre 0 -> sofort erlaubt; danach wächst sie mit jedem Bump.
if remaining := lastBump + bumpCount*bumpStep - now; remaining > 0 {
w.Header().Set("Retry-After", strconv.FormatInt(remaining, 10))
writeJSON(w, http.StatusTooManyRequests, map[string]any{
"error": "Dieser Beitrag wurde erst kürzlich repostet.",
"retry_after": remaining,
"bump_count": bumpCount,
})
return
}
if _, err := db.Exec(
`UPDATE entry SET last_activity = ?, bump_count = bump_count + 1, last_bump = ? WHERE pid = ?`,
now, now, pid,
); err != nil {
writeError(w, http.StatusInternalServerError, "Bumpen fehlgeschlagen")
return
}
bumpCount++
writeJSON(w, http.StatusOK, map[string]any{
"pid": pid,
"bump_count": bumpCount,
"last_bump": now,
"last_activity": now,
"retry_after": bumpCount * bumpStep, // bis zum nächsten erlaubten Bump
})
}
// voteTally liefert die Zähler und die eigene Auswahl (selected) für einen Beitrag.
// Ist uid == 0 (nicht eingeloggt), ist selected immer "none".
func voteTally(pid, uid int64) (left, right int, selected string) {