Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions home/home.go
Original file line number Diff line number Diff line change
Expand Up @@ -445,16 +445,24 @@ const statusCardScript = `<script>
}
}

function refresh() {
function refresh(scrollToTop) {
if (inflight) return;
inflight = true;
fetch('/user/status/stream', { credentials: 'same-origin', cache: 'no-store' })
.then(function(r){ return r.ok ? r.text() : null; })
.then(function(html){
if (html == null) return;
var saved = currentInput();
// Preserve scroll position unless we explicitly want to
// scroll to top (e.g. after posting a new status).
var feed = document.getElementById('home-statuses');
var scrollPos = feed ? feed.scrollTop : 0;
wrap.innerHTML = html;
restoreInput(saved);
var newFeed = document.getElementById('home-statuses');
if (newFeed) {
newFeed.scrollTop = scrollToTop ? 0 : scrollPos;
}
bindForm();
})
.catch(function(){})
Expand Down Expand Up @@ -483,7 +491,7 @@ const statusCardScript = `<script>
body: body.toString()
}).then(function(){
input.value = '';
refresh();
refresh(true);
}).catch(function(){
// Fall back to a native form submit on network error.
form.submit();
Expand Down
13 changes: 10 additions & 3 deletions internal/auth/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"net/http"
"strings"
"sync"
Expand Down Expand Up @@ -477,6 +478,11 @@ func CanPost(accountID string) bool {
return true
}

// Must be at least 24 hours old.
if time.Since(acc.Created) < 24*time.Hour {
return false
}

if VerificationRequired == nil || !VerificationRequired() {
return true
}
Expand All @@ -497,10 +503,11 @@ func PostBlockReason(accountID string) string {
if acc.Admin || acc.Approved {
return ""
}
if VerificationRequired == nil || !VerificationRequired() {
return ""
if time.Since(acc.Created) < 24*time.Hour {
remaining := (24*time.Hour - time.Since(acc.Created)).Round(time.Minute)
return fmt.Sprintf("New accounts must wait 24 hours before posting. %s remaining.", remaining)
}
if !acc.EmailVerified {
if VerificationRequired != nil && VerificationRequired() && !acc.EmailVerified {
return "Verify your email at /account before posting."
}
return ""
Expand Down
19 changes: 7 additions & 12 deletions internal/flag/flag.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,21 +80,16 @@ func CheckContent(contentType, itemID, title, content string) {
return
}

prompt := `You are a content moderator for a community that values purposeful, respectful discussion. Every post should be meaningful — this is not a place to waste time.
prompt := `You are a strict content moderator for a family-friendly community. Every post should be meaningful and respectful. This is not a place to waste time, troll, or post crude content.

Classify the content with ONLY ONE WORD:
- SPAM (promotional spam, advertising, repetitive junk)
- TEST (test posts like "test", "hello world", etc.)
- LOW_QUALITY (low-effort content, memes, nonsensical, no substance)
- HARMFUL (gossip, backbiting, slander, personal attacks, mocking others, trolling)
- OK (meaningful, on-topic, respectful content)

Community principles:
- Stay on topic and contribute something meaningful
- Be respectful — disagree with ideas, not people
- No gossip or backbiting (speaking ill of someone behind their back)
- No personal attacks, mockery, or belittling
- Religious and political discussion is welcome when done with sincerity and good manners
- TEST (test posts like "test", "hello world", meaningless typing)
- LOW_QUALITY (low-effort, memes, nonsensical, no substance, gibberish, single words)
- HARMFUL (vulgar, crude, sexual, obscene, gossip, slander, personal attacks, mocking, trolling, shock content)
- OK (meaningful, on-topic, respectful content that adds value)

When in doubt, flag it. Better to flag something borderline than let inappropriate content through.

Respond with just the single word.`

Expand Down
58 changes: 42 additions & 16 deletions user/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,17 @@ func GetProfile(userID string) *Profile {
return profile
}

// UpdateStatus is the safe way to change a user's status. It passes a
// fresh Profile to UpdateProfile so the existing map entry isn't
// aliased — UpdateProfile reads the old status from the map correctly
// and pushes it to history before storing the new one.
func UpdateStatus(userID, newStatus string) error {
return UpdateProfile(&Profile{
UserID: userID,
Status: newStatus,
})
}

// UpdateProfile saves a user's profile. Every non-empty previous
// status is pushed onto the history so the full timeline of what a
// user has said is preserved. Empty updates (clearing a status) are
Expand Down Expand Up @@ -439,9 +450,7 @@ func StatusHandler(w http.ResponseWriter, r *http.Request) {
}
}

profile := GetProfile(sess.Account)
profile.Status = status
UpdateProfile(profile)
UpdateStatus(sess.Account, status)

// Async content moderation — flags spam/test/harmful automatically
// and auto-bans the user if it's bad. Fire-and-forget.
Expand Down Expand Up @@ -483,9 +492,7 @@ func PostSystemStatus(text string) error {
if len(text) > MaxStatusLength {
text = text[:MaxStatusLength-1] + "…"
}
profile := GetProfile(app.SystemUserID)
profile.Status = text
return UpdateProfile(profile)
return UpdateStatus(app.SystemUserID, text)
}

// moderateStatus runs async content moderation on a status post. If the
Expand Down Expand Up @@ -596,15 +603,14 @@ func Handler(w http.ResponseWriter, r *http.Request) {
return
}

// Handle POST request for status update (legacy, profile page form)
// Handle POST request for status update (legacy, profile page form).
// Same gates as StatusHandler — CanPost, rate limit, wallet charge.
if r.Method == "POST" {
sess, _, err := auth.RequireSession(r)
sess, acc, err := auth.RequireSession(r)
if err != nil {
app.Unauthorized(w, r)
return
}

// Only allow updating own status
if sess.Account != username {
app.Forbidden(w, r, "")
return
Expand All @@ -615,15 +621,35 @@ func Handler(w http.ResponseWriter, r *http.Request) {
status = status[:MaxStatusLength]
}

profile := GetProfile(sess.Account)
profile.Status = status
UpdateProfile(profile)
if status != "" {
if !auth.CanPost(acc.ID) {
app.Forbidden(w, r, auth.PostBlockReason(acc.ID))
return
}
if err := auth.CheckPostRate(acc.ID); err != nil {
app.Forbidden(w, r, err.Error())
return
}
canProceed, _, cost, _ := wallet.CheckQuota(acc.ID, wallet.OpSocialPost)
if !canProceed {
app.Forbidden(w, r, fmt.Sprintf("Status updates cost %d credit. Top up at /wallet", cost))
return
}
if err := wallet.ConsumeQuota(acc.ID, wallet.OpSocialPost); err != nil {
app.Forbidden(w, r, err.Error())
return
}
}

if status != "" && sess.Account != app.SystemUserID && AIReplyHook != nil && containsMention(status, MicroMention) {
go AIReplyHook(sess.Account, status)
UpdateStatus(sess.Account, status)

if status != "" {
go moderateStatus(sess.Account, status)
if sess.Account != app.SystemUserID && AIReplyHook != nil && containsMention(status, MicroMention) {
go AIReplyHook(sess.Account, status)
}
}

// Redirect back to profile
http.Redirect(w, r, "/@"+sess.Account, http.StatusSeeOther)
return
}
Expand Down
Loading