Notification when admin users are blocked

This commit is contained in:
Noah Petherbridge 2024-09-09 21:17:22 -07:00
parent 79ea384d40
commit 8d9588b039
6 changed files with 97 additions and 7 deletions

View File

@ -117,11 +117,17 @@ func UserActions() http.HandlerFunc {
return return
} }
// Get their block lists.
insights, err := models.GetBlocklistInsights(user) insights, err := models.GetBlocklistInsights(user)
if err != nil { if err != nil {
session.FlashError(w, r, "Error getting blocklist insights: %s", err) session.FlashError(w, r, "Error getting blocklist insights: %s", err)
} }
vars["BlocklistInsights"] = insights vars["BlocklistInsights"] = insights
// Also surface counts of admin blocks.
count, total := models.CountBlockedAdminUsers(user)
vars["AdminBlockCount"] = count
vars["AdminBlockTotal"] = total
case "essays": case "essays":
// Edit their profile essays easily. // Edit their profile essays easily.
if !currentUser.HasAdminScope(config.ScopePhotoModerator) { if !currentUser.HasAdminScope(config.ScopePhotoModerator) {

View File

@ -112,17 +112,35 @@ func BlockUser() http.HandlerFunc {
return return
} }
// Can't block admins who have the unblockable scope. // If the target user is an admin, log this to the admin reports page.
if user.IsAdmin && user.HasAdminScope(config.ScopeUnblockable) { if user.IsAdmin {
// Is the target admin user unblockable?
var (
unblockable = user.HasAdminScope(config.ScopeUnblockable)
footer string // qualifier for the admin report body
)
// Add a footer to the report to indicate whether the block goes through.
if unblockable {
footer = "**Unblockable:** this admin can not be blocked, so the block was not added and the user was shown an error message."
} else {
footer = "**Notice:** This admin is not unblockable, so the block has been added successfully."
}
// Also, include this user's current count of blocked admin users.
count, total := models.CountBlockedAdminUsers(currentUser)
footer += fmt.Sprintf("\n\nThis user now blocks %d of %d admin user(s) on this site.", count+1, total)
// For curiosity's sake, log a report. // For curiosity's sake, log a report.
fb := &models.Feedback{ fb := &models.Feedback{
Intent: "report", Intent: "report",
Subject: "A user tried to block an admin", Subject: "A user tried to block an admin",
Message: fmt.Sprintf( Message: fmt.Sprintf(
"A user has tried to block an admin user account!\n\n"+ "A user has tried to block an admin user account!\n\n"+
"* Username: %s\n* Tried to block: %s", "* Username: %s\n* Tried to block: %s\n\n%s",
currentUser.Username, currentUser.Username,
user.Username, user.Username,
footer,
), ),
UserID: currentUser.ID, UserID: currentUser.ID,
TableName: "users", TableName: "users",
@ -132,9 +150,12 @@ func BlockUser() http.HandlerFunc {
log.Error("Could not log feedback for user %s trying to block admin %s: %s", currentUser.Username, user.Username, err) log.Error("Could not log feedback for user %s trying to block admin %s: %s", currentUser.Username, user.Username, err)
} }
session.FlashError(w, r, "You can not block site administrators.") // If the admin is unblockable, give the user an error message and return.
templates.Redirect(w, "/u/"+username) if unblockable {
return session.FlashError(w, r, "You can not block site administrators.")
templates.Redirect(w, "/u/"+username)
return
}
} }
// Block the target user. // Block the target user.

View File

@ -199,6 +199,7 @@ func GetBlocklistInsights(user *User) (*BlocklistInsight, error) {
reverse = []*Block{} // Users who block the target reverse = []*Block{} // Users who block the target
userIDs = []uint64{user.ID} userIDs = []uint64{user.ID}
usernames = map[uint64]string{} usernames = map[uint64]string{}
admins = map[uint64]bool{}
) )
// Get the complete blocklist and bucket them into forward and reverse. // Get the complete blocklist and bucket them into forward and reverse.
@ -218,6 +219,7 @@ func GetBlocklistInsights(user *User) (*BlocklistInsight, error) {
type scanItem struct { type scanItem struct {
ID uint64 ID uint64
Username string Username string
IsAdmin bool
} }
var scan = []scanItem{} var scan = []scanItem{}
if res := DB.Table( if res := DB.Table(
@ -225,6 +227,7 @@ func GetBlocklistInsights(user *User) (*BlocklistInsight, error) {
).Select( ).Select(
"id", "id",
"username", "username",
"is_admin",
).Where( ).Where(
"id IN ?", userIDs, "id IN ?", userIDs,
).Scan(&scan); res.Error != nil { ).Scan(&scan); res.Error != nil {
@ -233,6 +236,7 @@ func GetBlocklistInsights(user *User) (*BlocklistInsight, error) {
for _, row := range scan { for _, row := range scan {
usernames[row.ID] = row.Username usernames[row.ID] = row.Username
admins[row.ID] = row.IsAdmin
} }
} }
@ -245,6 +249,7 @@ func GetBlocklistInsights(user *User) (*BlocklistInsight, error) {
if username, ok := usernames[row.TargetUserID]; ok { if username, ok := usernames[row.TargetUserID]; ok {
result.Blocks = append(result.Blocks, BlocklistInsightUser{ result.Blocks = append(result.Blocks, BlocklistInsightUser{
Username: username, Username: username,
IsAdmin: admins[row.TargetUserID],
Date: row.CreatedAt, Date: row.CreatedAt,
}) })
} }
@ -253,6 +258,7 @@ func GetBlocklistInsights(user *User) (*BlocklistInsight, error) {
if username, ok := usernames[row.SourceUserID]; ok { if username, ok := usernames[row.SourceUserID]; ok {
result.BlockedBy = append(result.BlockedBy, BlocklistInsightUser{ result.BlockedBy = append(result.BlockedBy, BlocklistInsightUser{
Username: username, Username: username,
IsAdmin: admins[row.SourceUserID],
Date: row.CreatedAt, Date: row.CreatedAt,
}) })
} }
@ -268,6 +274,7 @@ type BlocklistInsight struct {
type BlocklistInsightUser struct { type BlocklistInsightUser struct {
Username string Username string
IsAdmin bool
Date time.Time Date time.Time
} }

View File

@ -595,6 +595,33 @@ func MapAdminUsers(user *User) (UserMap, error) {
return MapUsers(user, userIDs) return MapUsers(user, userIDs)
} }
// CountBlockedAdminUsers returns a count of how many admin users the current user has blocked, out of how many total.
func CountBlockedAdminUsers(user *User) (count, total int64) {
// Count the blocked admins.
DB.Model(&User{}).Select(
"count(users.id) AS cnt",
).Joins(
"JOIN blocks ON (blocks.target_user_id = users.id)",
).Where(
"blocks.source_user_id = ? AND users.is_admin IS TRUE",
user.ID,
).Count(&count)
// And the total number of available admins.
total = CountAdminUsers()
return
}
// CountAdminUsers returns a count of how many admin users exist on the site.
func CountAdminUsers() (count int64) {
DB.Model(&User{}).Select(
"count(id) AS cnt",
).Where(
"users.is_admin IS TRUE",
).Count(&count)
return
}
// Has a user ID in the map? // Has a user ID in the map?
func (um UserMap) Has(id uint64) bool { func (um UserMap) Has(id uint64) bool {
_, ok := um[id] _, ok := um[id]

View File

@ -100,11 +100,17 @@
<p> <p>
It is also against the <a href="/tos#child-exploitation">Terms of Service</a> of this website, and It is also against the <a href="/tos#child-exploitation">Terms of Service</a> of this website, and
members who violate this rule will be banned. This website is actively monitored to keep on top of this stuff, members who violate this rule will be banned. <strong>This website is actively monitored</strong> to keep on top of this stuff,
and we cooperate enthusiastically with and we cooperate enthusiastically with
<a href="https://www.missingkids.org/" title="National Center for Missing and Exploited Children">NCMEC</a> <a href="https://www.missingkids.org/" title="National Center for Missing and Exploited Children">NCMEC</a>
and relevant law enforcement agencies. and relevant law enforcement agencies.
</p> </p>
<p>
This incident has been reported to the website administrator. If you are surprised to see this message and
it was on accident, don't worry -- but <strong>repeated attempts to bypass this search filter</strong> by trying
other related keywords <strong>will be noticed</strong> and may attract extra admin attention to your account.
</p>
</div> </div>
{{end}} {{end}}

View File

@ -99,6 +99,23 @@
<h3>Block Lists</h3> <h3>Block Lists</h3>
<!-- Surface if admin users are blocked -->
{{if .AdminBlockCount}}
<h5 class="has-text-danger">
<i class="fa fa-peace"></i>
Blocked Admins <span class="tag is-danger">{{.AdminBlockCount}}</span>
</h5>
<p>
This user blocks <strong>{{.AdminBlockCount}}</strong> out of {{.AdminBlockTotal}} admin{{Pluralize64 .AdminBlockTotal}} of this website.
</p>
<p>
If this number is unusually high, it can indicate this user may be proactively blocking all the admins in order to be
sneaky or evade moderation.
</p>
{{end}}
<div class="columns"> <div class="columns">
<div class="column"> <div class="column">
<h5 class="has-text-warning">Forward List <span class="tag is-warning">{{len .BlocklistInsights.Blocks}}</span></h5> <h5 class="has-text-warning">Forward List <span class="tag is-warning">{{len .BlocklistInsights.Blocks}}</span></h5>
@ -118,6 +135,9 @@
{{range .BlocklistInsights.Blocks}} {{range .BlocklistInsights.Blocks}}
<li> <li>
<a href="/u/{{.Username}}">{{.Username}}</a> <a href="/u/{{.Username}}">{{.Username}}</a>
{{if .IsAdmin}}
<sup class="has-text-warning fa fa-peace" title="Admin user"></sup>
{{end}}
<small class="has-text-grey" title="{{.Date}}">{{.Date.Format "2006-01-02"}}</small> <small class="has-text-grey" title="{{.Date}}">{{.Date.Format "2006-01-02"}}</small>
</li> </li>
{{end}} {{end}}
@ -141,6 +161,9 @@
{{range .BlocklistInsights.BlockedBy}} {{range .BlocklistInsights.BlockedBy}}
<li> <li>
<a href="/u/{{.Username}}">{{.Username}}</a> <a href="/u/{{.Username}}">{{.Username}}</a>
{{if .IsAdmin}}
<sup class="has-text-warning fa fa-peace" title="Admin user"></sup>
{{end}}
<small class="has-text-grey" title="{{.Date}}">{{.Date.Format "2006-01-02"}}</small> <small class="has-text-grey" title="{{.Date}}">{{.Date.Format "2006-01-02"}}</small>
</li> </li>
{{end}} {{end}}