website/pkg/models/forum_membership.go
Noah Petherbridge 8a321eb8d2 User-owned Forum Improvements
* Private forums: CanBeSeenBy moderators, approved followers, its owner and
  admin users.
  * Note: the endpoint to subscribe to the forum won't allow users to follow
    the private forum, so approved followers can not be created at this time,
    except by adding them as moderators.
* Admins: when creating a forum they can choose "no category" to create it as
  an unofficial community forum.
* Code cleanup
  * More feature flag checking
2024-08-23 22:56:40 -07:00

236 lines
5.9 KiB
Go

package models
import (
"errors"
"strings"
"time"
"code.nonshy.com/nonshy/website/pkg/config"
"code.nonshy.com/nonshy/website/pkg/log"
"gorm.io/gorm"
)
// ForumMembership table.
//
// Unique key constraint pairs user_id and forum_id.
type ForumMembership struct {
ID uint64 `gorm:"primaryKey"`
UserID uint64 `gorm:"uniqueIndex:idx_forum_membership"`
User User `gorm:"foreignKey:user_id"`
ForumID uint64 `gorm:"uniqueIndex:idx_forum_membership"`
Forum Forum `gorm:"foreignKey:forum_id"`
Approved bool `gorm:"index"`
IsModerator bool `gorm:"index"`
CreatedAt time.Time
UpdatedAt time.Time
}
// Preload related tables for the forum (classmethod).
func (f *ForumMembership) Preload() *gorm.DB {
return DB.Preload("User").Preload("Forum")
}
// CreateForumMembership subscribes the user to a forum.
func CreateForumMembership(user *User, forum *Forum) (*ForumMembership, error) {
var (
f = &ForumMembership{
User: *user,
Forum: *forum,
Approved: true,
}
result = DB.Create(f)
)
return f, result.Error
}
// GetForumMembership looks up a forum membership, returning an error if one is not found.
func GetForumMembership(user *User, forum *Forum) (*ForumMembership, error) {
var (
f = &ForumMembership{}
result = f.Preload().Where(
"user_id = ? AND forum_id = ?",
user.ID, forum.ID,
).First(&f)
)
return f, result.Error
}
// AddModerator appoints a moderator to the forum, returning that user's ForumMembership.
//
// If the target is not following the forum, a ForumMembership is created, marked as a moderator and returned.
func (f *Forum) AddModerator(user *User) (*ForumMembership, error) {
var fm *ForumMembership
if found, err := GetForumMembership(user, f); err != nil {
fm = &ForumMembership{
User: *user,
Forum: *f,
Approved: true,
}
} else {
fm = found
}
// They are already a moderator?
if fm.IsModerator {
return fm, errors.New("they are already a moderator of this forum")
}
fm.IsModerator = true
err := fm.Save()
return fm, err
}
// CanBeSeenBy checks whether the user can see a private forum.
//
// Admins, owners, moderators and approved followers can see it.
//
// Note: this may invoke a DB query to check for moderator.
func (f *Forum) CanBeSeenBy(user *User) bool {
if !f.Private || user.IsAdmin || user.ID == f.OwnerID {
return true
}
if fm, err := GetForumMembership(user, f); err == nil {
return fm.Approved || fm.IsModerator
}
return false
}
// CanBeModeratedBy checks whether the user can moderate this forum.
//
// Admins, owners and moderators can do so.
//
// Note: this may invoke a DB query to check for moderator.
func (f *Forum) CanBeModeratedBy(user *User) bool {
if user.HasAdminScope(config.ScopeForumModerator) || f.OwnerID == user.ID {
return true
}
if fm, err := GetForumMembership(user, f); err == nil {
return fm.IsModerator
}
return false
}
// RemoveModerator will unset a user's moderator flag on this forum.
func (f *Forum) RemoveModerator(user *User) (*ForumMembership, error) {
fm, err := GetForumMembership(user, f)
if err != nil {
return nil, err
}
fm.IsModerator = false
err = fm.Save()
return fm, err
}
// GetModerators loads all of the moderators of a forum, ordered alphabetically by username.
func (f *Forum) GetModerators() ([]*User, error) {
// Find all forum memberships that moderate us.
var (
fm = []*ForumMembership{}
result = (&ForumMembership{}).Preload().Where(
"forum_id = ? AND is_moderator IS TRUE",
f.ID,
).Find(&fm)
)
if result.Error != nil {
log.Error("Forum(%d).GetModerators(): %s", f.ID, result.Error)
return nil, nil
}
// Load these users.
var userIDs = []uint64{}
for _, row := range fm {
userIDs = append(userIDs, row.UserID)
}
return GetUsersAlphabetically(userIDs)
}
// IsForumSubscribed checks if the current user subscribes to this forum.
func IsForumSubscribed(user *User, forum *Forum) bool {
f, _ := GetForumMembership(user, forum)
return f.UserID == user.ID
}
// HasForumSubscriptions returns if the current user has at least one forum membership.
func (u *User) HasForumSubscriptions() bool {
var count int64
DB.Model(&ForumMembership{}).Where(
"user_id = ?",
u.ID,
).Count(&count)
return count > 0
}
// Save a forum membership.
func (f *ForumMembership) Save() error {
return DB.Save(f).Error
}
// Delete a forum membership.
func (f *ForumMembership) Delete() error {
return DB.Delete(f).Error
}
// PaginateForumMemberships paginates over a user's ForumMemberships.
func PaginateForumMemberships(user *User, pager *Pagination) ([]*ForumMembership, error) {
var (
fs = []*ForumMembership{}
query = (&ForumMembership{}).Preload()
wheres = []string{}
placeholders = []interface{}{}
)
query = query.Where(
strings.Join(wheres, " AND "),
placeholders...,
).Order(pager.Sort)
query.Model(&ForumMembership{}).Count(&pager.Total)
result := query.Offset(pager.GetOffset()).Limit(pager.PerPage).Find(&fs)
return fs, result.Error
}
// ForumMembershipMap maps table IDs to Likes metadata.
type ForumMembershipMap map[uint64]bool
// Get like stats from the map.
func (fm ForumMembershipMap) Get(id uint64) bool {
return fm[id]
}
// MapForumMemberships looks up a user's memberships in bulk.
func MapForumMemberships(user *User, forums []*Forum) ForumMembershipMap {
var (
result = ForumMembershipMap{}
forumIDs = []uint64{}
)
// Initialize the result set.
for _, forum := range forums {
result[forum.ID] = false
forumIDs = append(forumIDs, forum.ID)
}
// Map the forum IDs the user subscribes to.
var followIDs = []uint64{}
if res := DB.Model(&ForumMembership{}).Select(
"forum_id",
).Where(
"user_id = ? AND forum_id IN ?",
user.ID, forumIDs,
).Scan(&followIDs); res.Error != nil {
log.Error("MapForumMemberships: %s", res.Error)
}
for _, forumID := range followIDs {
result[forumID] = true
}
return result
}