242333d8b7
* Show follower counts on forums * Sort by popularity (follow count)
292 lines
7.1 KiB
Go
292 lines
7.1 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
|
|
}
|
|
|
|
// CountForumMemberships counts how many subscribers a forum has.
|
|
func CountForumMemberships(forum *Forum) int64 {
|
|
var count int64
|
|
DB.Model(&ForumMembership{}).Where(
|
|
"forum_id = ?",
|
|
forum.ID,
|
|
).Count(&count)
|
|
return count
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
// ForumFollowerMap maps table IDs to counts of memberships.
|
|
type ForumFollowerMap map[uint64]int64
|
|
|
|
// Get like stats from the map.
|
|
func (fm ForumFollowerMap) Get(id uint64) int64 {
|
|
return fm[id]
|
|
}
|
|
|
|
// MapForumFollowers maps out the count of followers for a set of forums.
|
|
func MapForumFollowers(forums []*Forum) ForumFollowerMap {
|
|
var (
|
|
result = ForumFollowerMap{}
|
|
forumIDs = []uint64{}
|
|
)
|
|
|
|
// Initialize the result set.
|
|
for _, forum := range forums {
|
|
forumIDs = append(forumIDs, forum.ID)
|
|
}
|
|
|
|
// Hold the result of the grouped count query.
|
|
type group struct {
|
|
ID uint64
|
|
Followers int64
|
|
}
|
|
var groups = []group{}
|
|
|
|
// Map the counts of likes to each of these IDs.
|
|
if res := DB.Model(
|
|
&ForumMembership{},
|
|
).Select(
|
|
"forum_id AS id, count(id) AS followers",
|
|
).Where(
|
|
"forum_id IN ?",
|
|
forumIDs,
|
|
).Group("forum_id").Scan(&groups); res.Error != nil {
|
|
log.Error("MapLikes: count query: %s", res.Error)
|
|
}
|
|
|
|
for _, row := range groups {
|
|
result[row.ID] = row.Followers
|
|
}
|
|
|
|
return result
|
|
}
|