User Forums Blocking Behavior + Misc Fixes

Make some adjustments to blocking behavior regarding the forums:

* Pre-existing bug: on a forum's home page (threads list), if a thread was
  created by a blocked user, the thread still appeared with the user's name and
  picture visible. Now: their picture and name will be "[unavailable]" but the
  thread title/message and link to the thread will remain. Note: in the thread
  view itself, posts by the blocked user will be missing as normal.
* Make some tweaks to allow forum moderators (and owners of user-owned forums)
  able to see messages from blocked users on their forum:
  * In threads: a blocked user's picture and name are "[unavailable]" but the
    content of their message is still shown, and can be deleted by moderators.

Misc fixes:

* Private photos: when viewing your granted/grantee lists, hide users whose
  accounts are inactive or who are blocked.
* CertifiedSince: in case a user was manually certified but their cert photo
  status is not correct, return their user CreatedAt time instead.
This commit is contained in:
Noah Petherbridge 2024-08-28 18:42:49 -07:00
parent 617cd48308
commit c37d0298b0
8 changed files with 84 additions and 33 deletions

View File

@ -74,7 +74,7 @@ func Thread() http.HandlerFunc {
} }
pager.ParsePage(r) pager.ParsePage(r)
comments, err := models.PaginateComments(currentUser, "threads", thread.ID, pager) comments, err := models.PaginateComments(currentUser, "threads", thread.ID, canModerate, pager)
if err != nil { if err != nil {
session.FlashError(w, r, "Couldn't paginate comments: %s", err) session.FlashError(w, r, "Couldn't paginate comments: %s", err)
templates.Redirect(w, "/") templates.Redirect(w, "/")

View File

@ -2,7 +2,6 @@ package models
import ( import (
"errors" "errors"
"fmt"
"time" "time"
"gorm.io/gorm" "gorm.io/gorm"
@ -66,7 +65,9 @@ func (u *User) CertifiedSince() (time.Time, error) {
} }
if cert.Status != CertificationPhotoApproved { if cert.Status != CertificationPhotoApproved {
return time.Time{}, fmt.Errorf("cert photo status is: %s (expected 'approved')", cert.Status) // The edge case can come up if a user was manually certified but didn't have an approved picture.
// Return their CreatedAt instead.
return u.CreatedAt, nil
} }
return cert.UpdatedAt, nil return cert.UpdatedAt, nil

View File

@ -104,11 +104,14 @@ func CountCommentsReceived(user *User) int64 {
} }
// PaginateComments provides a page of comments on something. // PaginateComments provides a page of comments on something.
func PaginateComments(user *User, tableName string, tableID uint64, pager *Pagination) ([]*Comment, error) { //
// Note: noBlockLists is to facilitate user-owned forums, where forum owners/moderators should override the block lists
// and retain full visibility into all user comments on their forum. Default/recommended is to leave it false, where
// the user's block list filters the view.
func PaginateComments(user *User, tableName string, tableID uint64, noBlockLists bool, pager *Pagination) ([]*Comment, error) {
var ( var (
cs = []*Comment{} cs = []*Comment{}
query = (&Comment{}).Preload() query = (&Comment{}).Preload()
blockedUserIDs = BlockedUserIDs(user)
wheres = []string{} wheres = []string{}
placeholders = []interface{}{} placeholders = []interface{}{}
) )
@ -116,10 +119,13 @@ func PaginateComments(user *User, tableName string, tableID uint64, pager *Pagin
wheres = append(wheres, "table_name = ? AND table_id = ?") wheres = append(wheres, "table_name = ? AND table_id = ?")
placeholders = append(placeholders, tableName, tableID) placeholders = append(placeholders, tableName, tableID)
if !noBlockLists {
blockedUserIDs := BlockedUserIDs(user)
if len(blockedUserIDs) > 0 { if len(blockedUserIDs) > 0 {
wheres = append(wheres, "user_id NOT IN ?") wheres = append(wheres, "user_id NOT IN ?")
placeholders = append(placeholders, blockedUserIDs) placeholders = append(placeholders, blockedUserIDs)
} }
}
// Don't show comments from banned or disabled accounts. // Don't show comments from banned or disabled accounts.
wheres = append(wheres, ` wheres = append(wheres, `

View File

@ -140,8 +140,9 @@ func PaginateForums(user *User, categories []string, search *Search, subscribed
WHERE user_id = ? WHERE user_id = ?
AND forum_id = forums.id AND forum_id = forums.id
) )
OR forums.owner_id = ?
`) `)
placeholders = append(placeholders, user.ID) placeholders = append(placeholders, user.ID, user.ID)
} }
// Apply their search terms. // Apply their search terms.

View File

@ -226,6 +226,10 @@ func PaginatePrivatePhotoList(user *User, grantee bool, pager *Pagination) ([]*U
query *gorm.DB query *gorm.DB
wheres = []string{} wheres = []string{}
placeholders = []interface{}{} placeholders = []interface{}{}
blocklist = BlockedUserIDs(user)
// Column name of "other user" depending on direction
otherUserColumn string
) )
// Which direction are we going? // Which direction are we going?
@ -233,22 +237,33 @@ func PaginatePrivatePhotoList(user *User, grantee bool, pager *Pagination) ([]*U
// Return the private photo grants for whom YOU are the recipient. // Return the private photo grants for whom YOU are the recipient.
wheres = append(wheres, "target_user_id = ?") wheres = append(wheres, "target_user_id = ?")
placeholders = append(placeholders, user.ID) placeholders = append(placeholders, user.ID)
otherUserColumn = "source_user_id"
} else { } else {
// Return the users that YOU have granted access to YOUR private pictures. // Return the users that YOU have granted access to YOUR private pictures.
wheres = append(wheres, "source_user_id = ?") wheres = append(wheres, "source_user_id = ?")
placeholders = append(placeholders, user.ID) placeholders = append(placeholders, user.ID)
otherUserColumn = "target_user_id"
} }
// Users that actually exist. // Filter out users who are banned/disabled.
wheres = append(wheres, ` wheres = append(wheres,
fmt.Sprintf(`
EXISTS ( EXISTS (
SELECT 1 SELECT 1
FROM users FROM users
WHERE users.id = private_photos.target_user_id WHERE private_photos.%s = users.id
OR users.id = private_photos.source_user_id AND users.status = 'active'
)`, )`,
otherUserColumn,
),
) )
// Filter blocked users.
if len(blocklist) > 0 {
wheres = append(wheres, fmt.Sprintf("%s NOT IN ?", otherUserColumn))
placeholders = append(placeholders, blocklist)
}
query = DB.Where( query = DB.Where(
strings.Join(wheres, " AND "), strings.Join(wheres, " AND "),
placeholders..., placeholders...,

View File

@ -8,6 +8,7 @@ type UserRelationship struct {
Computed bool // check whether the SetUserRelationships function has been run Computed bool // check whether the SetUserRelationships function has been run
IsFriend bool // if true, a friends-only profile pic can show IsFriend bool // if true, a friends-only profile pic can show
IsPrivateGranted bool // if true, a private profile pic can show IsPrivateGranted bool // if true, a private profile pic can show
IsBlocked bool // if true, the users are blocking each other
} }
// SetUserRelationships updates a set of User objects to populate their UserRelationships in // SetUserRelationships updates a set of User objects to populate their UserRelationships in
@ -17,12 +18,14 @@ func SetUserRelationships(currentUser *User, users []*User) error {
var ( var (
friendIDs = FriendIDs(currentUser.ID) friendIDs = FriendIDs(currentUser.ID)
privateGrants = PrivateGrantedUserIDs(currentUser.ID) privateGrants = PrivateGrantedUserIDs(currentUser.ID)
blockedIDs = BlockedUserIDs(currentUser)
) )
// Map them for easier lookup. // Map them for easier lookup.
var ( var (
friendMap = map[uint64]interface{}{} friendMap = map[uint64]interface{}{}
privateMap = map[uint64]interface{}{} privateMap = map[uint64]interface{}{}
blockedMap = map[uint64]interface{}{}
) )
for _, id := range friendIDs { for _, id := range friendIDs {
@ -33,6 +36,10 @@ func SetUserRelationships(currentUser *User, users []*User) error {
privateMap[id] = nil privateMap[id] = nil
} }
for _, id := range blockedIDs {
blockedMap[id] = nil
}
// Inject the UserRelationships. // Inject the UserRelationships.
for _, u := range users { for _, u := range users {
if u.ID == currentUser.ID { if u.ID == currentUser.ID {
@ -49,6 +56,10 @@ func SetUserRelationships(currentUser *User, users []*User) error {
if _, ok := privateMap[u.ID]; ok { if _, ok := privateMap[u.ID]; ok {
u.UserRelationship.IsPrivateGranted = true u.UserRelationship.IsPrivateGranted = true
} }
if _, ok := blockedMap[u.ID]; ok {
u.UserRelationship.IsBlocked = true
}
} }
return nil return nil
} }

View File

@ -111,12 +111,20 @@
<div class="box has-background-success-light has-text-dark"> <div class="box has-background-success-light has-text-dark">
<div class="columns"> <div class="columns">
<div class="column is-2 has-text-centered pt-0 pb-1"> <div class="column is-2 has-text-centered pt-0 pb-1">
<!-- Thread starter is blocked? -->
{{if .Comment.User.UserRelationship.IsBlocked}}
<div>
{{template "avatar-64x64"}}
</div>
[unavailable]
{{else}}
<div> <div>
<a href="/u/{{.Comment.User.Username}}"> <a href="/u/{{.Comment.User.Username}}">
{{template "avatar-64x64" .Comment.User}} {{template "avatar-64x64" .Comment.User}}
</a> </a>
</div> </div>
<a href="/u/{{.Comment.User.Username}}">{{.Comment.User.NameOrUsername}}</a> <a href="/u/{{.Comment.User.Username}}">{{.Comment.User.NameOrUsername}}</a>
{{end}}
</div> </div>
<div class="column content pt-1 pb-0"> <div class="column content pt-1 pb-0">
<a href="/forum/thread/{{.ID}}"> <a href="/forum/thread/{{.ID}}">

View File

@ -133,12 +133,21 @@
<div class="box has-background-link-light has-text-dark" id="p{{.ID}}"> <div class="box has-background-link-light has-text-dark" id="p{{.ID}}">
<div class="columns"> <div class="columns">
<div class="column is-2 has-text-centered"> <div class="column is-2 has-text-centered">
<!-- Thread starter is blocked? -->
{{if .User.UserRelationship.IsBlocked}}
<div>
{{template "avatar-64x64"}}
</div>
[unavailable]
{{else}}
<div> <div>
<a href="/u/{{$c.User.Username}}"> <a href="/u/{{$c.User.Username}}">
{{template "avatar-96x96" $c.User}} {{template "avatar-96x96" $c.User}}
</a> </a>
</div> </div>
<a href="/u/{{$c.User.Username}}">{{$c.User.NameOrUsername}}</a> <a href="/u/{{$c.User.Username}}">{{$c.User.NameOrUsername}}</a>
{{end}}
{{if $c.User.IsAdmin}} {{if $c.User.IsAdmin}}
<div class="is-size-7 mt-1"> <div class="is-size-7 mt-1">
<span class="tag is-danger is-light"> <span class="tag is-danger is-light">