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)
comments, err := models.PaginateComments(currentUser, "threads", thread.ID, pager)
comments, err := models.PaginateComments(currentUser, "threads", thread.ID, canModerate, pager)
if err != nil {
session.FlashError(w, r, "Couldn't paginate comments: %s", err)
templates.Redirect(w, "/")

View File

@ -2,7 +2,6 @@ package models
import (
"errors"
"fmt"
"time"
"gorm.io/gorm"
@ -66,7 +65,9 @@ func (u *User) CertifiedSince() (time.Time, error) {
}
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

View File

@ -104,11 +104,14 @@ func CountCommentsReceived(user *User) int64 {
}
// 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 (
cs = []*Comment{}
query = (&Comment{}).Preload()
blockedUserIDs = BlockedUserIDs(user)
wheres = []string{}
placeholders = []interface{}{}
)
@ -116,10 +119,13 @@ func PaginateComments(user *User, tableName string, tableID uint64, pager *Pagin
wheres = append(wheres, "table_name = ? AND table_id = ?")
placeholders = append(placeholders, tableName, tableID)
if !noBlockLists {
blockedUserIDs := BlockedUserIDs(user)
if len(blockedUserIDs) > 0 {
wheres = append(wheres, "user_id NOT IN ?")
placeholders = append(placeholders, blockedUserIDs)
}
}
// Don't show comments from banned or disabled accounts.
wheres = append(wheres, `

View File

@ -140,8 +140,9 @@ func PaginateForums(user *User, categories []string, search *Search, subscribed
WHERE user_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.

View File

@ -226,6 +226,10 @@ func PaginatePrivatePhotoList(user *User, grantee bool, pager *Pagination) ([]*U
query *gorm.DB
wheres = []string{}
placeholders = []interface{}{}
blocklist = BlockedUserIDs(user)
// Column name of "other user" depending on direction
otherUserColumn string
)
// 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.
wheres = append(wheres, "target_user_id = ?")
placeholders = append(placeholders, user.ID)
otherUserColumn = "source_user_id"
} else {
// Return the users that YOU have granted access to YOUR private pictures.
wheres = append(wheres, "source_user_id = ?")
placeholders = append(placeholders, user.ID)
otherUserColumn = "target_user_id"
}
// Users that actually exist.
wheres = append(wheres, `
// Filter out users who are banned/disabled.
wheres = append(wheres,
fmt.Sprintf(`
EXISTS (
SELECT 1
FROM users
WHERE users.id = private_photos.target_user_id
OR users.id = private_photos.source_user_id
WHERE private_photos.%s = users.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(
strings.Join(wheres, " AND "),
placeholders...,

View File

@ -8,6 +8,7 @@ type UserRelationship struct {
Computed bool // check whether the SetUserRelationships function has been run
IsFriend bool // if true, a friends-only 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
@ -17,12 +18,14 @@ func SetUserRelationships(currentUser *User, users []*User) error {
var (
friendIDs = FriendIDs(currentUser.ID)
privateGrants = PrivateGrantedUserIDs(currentUser.ID)
blockedIDs = BlockedUserIDs(currentUser)
)
// Map them for easier lookup.
var (
friendMap = map[uint64]interface{}{}
privateMap = map[uint64]interface{}{}
blockedMap = map[uint64]interface{}{}
)
for _, id := range friendIDs {
@ -33,6 +36,10 @@ func SetUserRelationships(currentUser *User, users []*User) error {
privateMap[id] = nil
}
for _, id := range blockedIDs {
blockedMap[id] = nil
}
// Inject the UserRelationships.
for _, u := range users {
if u.ID == currentUser.ID {
@ -49,6 +56,10 @@ func SetUserRelationships(currentUser *User, users []*User) error {
if _, ok := privateMap[u.ID]; ok {
u.UserRelationship.IsPrivateGranted = true
}
if _, ok := blockedMap[u.ID]; ok {
u.UserRelationship.IsBlocked = true
}
}
return nil
}

View File

@ -111,12 +111,20 @@
<div class="box has-background-success-light has-text-dark">
<div class="columns">
<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>
<a href="/u/{{.Comment.User.Username}}">
{{template "avatar-64x64" .Comment.User}}
</a>
</div>
<a href="/u/{{.Comment.User.Username}}">{{.Comment.User.NameOrUsername}}</a>
{{end}}
</div>
<div class="column content pt-1 pb-0">
<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="columns">
<div class="column is-2 has-text-centered">
<!-- Thread starter is blocked? -->
{{if .User.UserRelationship.IsBlocked}}
<div>
{{template "avatar-64x64"}}
</div>
[unavailable]
{{else}}
<div>
<a href="/u/{{$c.User.Username}}">
{{template "avatar-96x96" $c.User}}
</a>
</div>
<a href="/u/{{$c.User.Username}}">{{$c.User.NameOrUsername}}</a>
{{end}}
{{if $c.User.IsAdmin}}
<div class="is-size-7 mt-1">
<span class="tag is-danger is-light">