2022-08-24 05:55:19 +00:00
|
|
|
package models
|
|
|
|
|
|
|
|
import (
|
|
|
|
"errors"
|
|
|
|
"strings"
|
|
|
|
"time"
|
|
|
|
|
2024-08-22 04:53:35 +00:00
|
|
|
"code.nonshy.com/nonshy/website/pkg/config"
|
2022-08-24 05:55:19 +00:00
|
|
|
"gorm.io/gorm"
|
|
|
|
)
|
|
|
|
|
|
|
|
// Forum table.
|
|
|
|
type Forum struct {
|
|
|
|
ID uint64 `gorm:"primaryKey"`
|
|
|
|
OwnerID uint64 `gorm:"index"`
|
|
|
|
Owner User `gorm:"foreignKey:owner_id"`
|
|
|
|
Category string `gorm:"index"`
|
|
|
|
Fragment string `gorm:"uniqueIndex"`
|
|
|
|
Title string
|
|
|
|
Description string
|
|
|
|
Explicit bool `gorm:"index"`
|
|
|
|
Privileged bool
|
|
|
|
PermitPhotos bool
|
2024-05-11 19:23:06 +00:00
|
|
|
Private bool `gorm:"index"`
|
2022-08-24 05:55:19 +00:00
|
|
|
CreatedAt time.Time
|
|
|
|
UpdatedAt time.Time
|
|
|
|
}
|
|
|
|
|
|
|
|
// Preload related tables for the forum (classmethod).
|
|
|
|
func (f *Forum) Preload() *gorm.DB {
|
2024-08-22 04:53:35 +00:00
|
|
|
return DB.Preload("Owner").Preload("Owner.ProfilePhoto")
|
2022-08-24 05:55:19 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// GetForum by ID.
|
|
|
|
func GetForum(id uint64) (*Forum, error) {
|
|
|
|
forum := &Forum{}
|
|
|
|
result := forum.Preload().First(&forum, id)
|
|
|
|
return forum, result.Error
|
|
|
|
}
|
|
|
|
|
2022-08-31 05:13:57 +00:00
|
|
|
// GetForums queries a set of thread IDs and returns them mapped.
|
|
|
|
func GetForums(IDs []uint64) (map[uint64]*Forum, error) {
|
|
|
|
var (
|
|
|
|
mt = map[uint64]*Forum{}
|
|
|
|
fs = []*Forum{}
|
|
|
|
)
|
|
|
|
|
|
|
|
result := (&Forum{}).Preload().Where("id IN ?", IDs).Find(&fs)
|
|
|
|
for _, row := range fs {
|
|
|
|
mt[row.ID] = row
|
|
|
|
}
|
|
|
|
|
|
|
|
return mt, result.Error
|
|
|
|
}
|
|
|
|
|
2022-08-24 05:55:19 +00:00
|
|
|
// ForumByFragment looks up a forum by its URL fragment.
|
|
|
|
func ForumByFragment(fragment string) (*Forum, error) {
|
|
|
|
if fragment == "" {
|
|
|
|
return nil, errors.New("the URL fragment is required")
|
|
|
|
}
|
|
|
|
|
|
|
|
var (
|
|
|
|
f = &Forum{}
|
|
|
|
result = f.Preload().Where(
|
|
|
|
"fragment = ?",
|
|
|
|
fragment,
|
|
|
|
).First(&f)
|
|
|
|
)
|
|
|
|
|
|
|
|
return f, result.Error
|
|
|
|
}
|
|
|
|
|
2024-08-22 04:53:35 +00:00
|
|
|
// CanEdit checks if the user has edit rights over this forum.
|
|
|
|
//
|
|
|
|
// That is, they are its Owner or they are an admin with Manage Forums permission.
|
|
|
|
func (f *Forum) CanEdit(user *User) bool {
|
|
|
|
return user.HasAdminScope(config.ScopeForumAdmin) || f.OwnerID == user.ID
|
|
|
|
}
|
|
|
|
|
2022-08-24 05:55:19 +00:00
|
|
|
/*
|
|
|
|
PaginateForums scans over the available forums for a user.
|
|
|
|
|
|
|
|
Parameters:
|
|
|
|
|
|
|
|
- userID: of who is looking
|
|
|
|
- categories: optional, filter within categories
|
|
|
|
- pager
|
2024-08-27 03:47:14 +00:00
|
|
|
|
|
|
|
The pager Sort accepts a couple of custom values for more advanced sorting:
|
|
|
|
|
|
|
|
- by_latest: recently updated posts
|
|
|
|
- by_threads: thread count
|
|
|
|
- by_posts: post count
|
|
|
|
- by_users: user count
|
2022-08-24 05:55:19 +00:00
|
|
|
*/
|
2024-08-21 04:26:53 +00:00
|
|
|
func PaginateForums(user *User, categories []string, search *Search, subscribed bool, pager *Pagination) ([]*Forum, error) {
|
2022-08-24 05:55:19 +00:00
|
|
|
var (
|
|
|
|
fs = []*Forum{}
|
|
|
|
query = (&Forum{}).Preload()
|
|
|
|
wheres = []string{}
|
|
|
|
placeholders = []interface{}{}
|
|
|
|
)
|
|
|
|
|
2023-05-25 01:40:27 +00:00
|
|
|
if len(categories) > 0 {
|
2022-08-24 05:55:19 +00:00
|
|
|
wheres = append(wheres, "category IN ?")
|
|
|
|
placeholders = append(placeholders, categories)
|
|
|
|
}
|
|
|
|
|
2022-08-25 04:17:34 +00:00
|
|
|
// Hide explicit forum if user hasn't opted into it.
|
|
|
|
if !user.Explicit && !user.IsAdmin {
|
|
|
|
wheres = append(wheres, "explicit = false")
|
|
|
|
}
|
|
|
|
|
2024-08-22 06:06:06 +00:00
|
|
|
// Hide private forums except for admins and approved users.
|
2024-05-11 19:23:06 +00:00
|
|
|
if !user.IsAdmin {
|
2024-08-22 06:06:06 +00:00
|
|
|
wheres = append(wheres, `
|
|
|
|
(
|
|
|
|
private IS NOT TRUE
|
|
|
|
OR EXISTS (
|
|
|
|
SELECT 1
|
|
|
|
FROM forum_memberships
|
|
|
|
WHERE forum_id = forums.id
|
|
|
|
AND user_id = ?
|
|
|
|
AND (
|
|
|
|
is_moderator IS TRUE
|
|
|
|
OR approved IS TRUE
|
|
|
|
)
|
|
|
|
)
|
|
|
|
)`,
|
|
|
|
)
|
|
|
|
placeholders = append(placeholders, user.ID)
|
2024-05-11 19:23:06 +00:00
|
|
|
}
|
|
|
|
|
2024-08-21 04:26:53 +00:00
|
|
|
// Followed forums only? (for the My List category on home page)
|
|
|
|
if subscribed {
|
|
|
|
wheres = append(wheres, `
|
|
|
|
EXISTS (
|
|
|
|
SELECT 1
|
|
|
|
FROM forum_memberships
|
|
|
|
WHERE user_id = ?
|
|
|
|
AND forum_id = forums.id
|
|
|
|
)
|
2024-08-29 01:42:49 +00:00
|
|
|
OR forums.owner_id = ?
|
2024-08-21 04:26:53 +00:00
|
|
|
`)
|
2024-08-29 01:42:49 +00:00
|
|
|
placeholders = append(placeholders, user.ID, user.ID)
|
2024-08-21 04:26:53 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Apply their search terms.
|
|
|
|
if search != nil {
|
|
|
|
for _, term := range search.Includes {
|
|
|
|
var ilike = "%" + strings.ToLower(term) + "%"
|
|
|
|
wheres = append(wheres, "(fragment ILIKE ? OR title ILIKE ? OR description ILIKE ?)")
|
|
|
|
placeholders = append(placeholders, ilike, ilike, ilike)
|
|
|
|
}
|
|
|
|
for _, term := range search.Excludes {
|
|
|
|
var ilike = "%" + strings.ToLower(term) + "%"
|
|
|
|
wheres = append(wheres, "(fragment NOT ILIKE ? AND title NOT ILIKE ? AND description NOT ILIKE ?)")
|
|
|
|
placeholders = append(placeholders, ilike, ilike, ilike)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-08-24 05:55:19 +00:00
|
|
|
// Filters?
|
|
|
|
if len(wheres) > 0 {
|
|
|
|
query = query.Where(
|
|
|
|
strings.Join(wheres, " AND "),
|
|
|
|
placeholders...,
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2024-08-27 03:47:14 +00:00
|
|
|
// Custom SORT parameters.
|
|
|
|
switch pager.Sort {
|
2024-08-27 04:36:48 +00:00
|
|
|
case "by_followers":
|
|
|
|
pager.Sort = `(
|
|
|
|
SELECT count(forum_memberships.id)
|
|
|
|
FROM forum_memberships
|
|
|
|
WHERE forum_memberships.forum_id = forums.id
|
|
|
|
) DESC`
|
2024-08-27 03:47:14 +00:00
|
|
|
case "by_latest":
|
|
|
|
pager.Sort = `(
|
|
|
|
SELECT MAX(threads.updated_at)
|
|
|
|
FROM threads
|
|
|
|
WHERE threads.forum_id = forums.id
|
|
|
|
) DESC NULLS LAST`
|
|
|
|
case "by_threads":
|
|
|
|
pager.Sort = `(
|
|
|
|
SELECT count(threads.id)
|
|
|
|
FROM threads
|
|
|
|
WHERE threads.forum_id = forums.id
|
|
|
|
) DESC`
|
|
|
|
case "by_posts":
|
|
|
|
pager.Sort = `(
|
|
|
|
SELECT count(comments.id)
|
|
|
|
FROM threads
|
|
|
|
JOIN comments ON comments.table_name='threads' AND comments.table_id=threads.id
|
|
|
|
WHERE threads.forum_id = forums.id
|
|
|
|
) DESC`
|
|
|
|
case "by_users":
|
|
|
|
pager.Sort = `(
|
|
|
|
SELECT count(distinct(users.id))
|
|
|
|
FROM threads
|
|
|
|
JOIN comments ON comments.table_name='threads' AND comments.table_id=threads.id
|
|
|
|
JOIN users ON comments.user_id=users.id
|
|
|
|
WHERE threads.forum_id = forums.id
|
|
|
|
) DESC`
|
|
|
|
}
|
|
|
|
|
2022-08-24 05:55:19 +00:00
|
|
|
query = query.Order(pager.Sort)
|
|
|
|
query.Model(&Forum{}).Count(&pager.Total)
|
|
|
|
result := query.Offset(pager.GetOffset()).Limit(pager.PerPage).Find(&fs)
|
|
|
|
return fs, result.Error
|
|
|
|
}
|
|
|
|
|
|
|
|
// PaginateOwnedForums returns forums the user owns (or all forums to admins).
|
2024-08-23 04:57:14 +00:00
|
|
|
func PaginateOwnedForums(userID uint64, isAdmin bool, categories []string, search *Search, pager *Pagination) ([]*Forum, error) {
|
2022-08-24 05:55:19 +00:00
|
|
|
var (
|
2024-08-21 02:31:56 +00:00
|
|
|
fs = []*Forum{}
|
|
|
|
query = (&Forum{}).Preload()
|
|
|
|
wheres = []string{}
|
|
|
|
placeholders = []interface{}{}
|
2022-08-24 05:55:19 +00:00
|
|
|
)
|
|
|
|
|
2024-08-21 02:31:56 +00:00
|
|
|
// Users see only their owned forums.
|
2022-08-24 05:55:19 +00:00
|
|
|
if !isAdmin {
|
2024-08-21 02:31:56 +00:00
|
|
|
wheres = append(wheres, "owner_id = ?")
|
|
|
|
placeholders = append(placeholders, userID)
|
2022-08-24 05:55:19 +00:00
|
|
|
}
|
|
|
|
|
2024-08-23 04:57:14 +00:00
|
|
|
if len(categories) > 0 {
|
|
|
|
wheres = append(wheres, "category IN ?")
|
|
|
|
placeholders = append(placeholders, categories)
|
|
|
|
}
|
|
|
|
|
2024-08-21 02:31:56 +00:00
|
|
|
// Apply their search terms.
|
|
|
|
if search != nil {
|
|
|
|
for _, term := range search.Includes {
|
|
|
|
var ilike = "%" + strings.ToLower(term) + "%"
|
|
|
|
wheres = append(wheres, "(fragment ILIKE ? OR title ILIKE ? OR description ILIKE ?)")
|
|
|
|
placeholders = append(placeholders, ilike, ilike, ilike)
|
|
|
|
}
|
|
|
|
for _, term := range search.Excludes {
|
|
|
|
var ilike = "%" + strings.ToLower(term) + "%"
|
|
|
|
wheres = append(wheres, "(fragment NOT ILIKE ? AND title NOT ILIKE ? AND description NOT ILIKE ?)")
|
|
|
|
placeholders = append(placeholders, ilike, ilike, ilike)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
query = query.Where(
|
|
|
|
strings.Join(wheres, " AND "),
|
|
|
|
placeholders...,
|
|
|
|
).Order(pager.Sort)
|
|
|
|
|
2022-08-24 05:55:19 +00:00
|
|
|
query.Model(&Forum{}).Count(&pager.Total)
|
|
|
|
result := query.Offset(pager.GetOffset()).Limit(pager.PerPage).Find(&fs)
|
|
|
|
return fs, result.Error
|
|
|
|
}
|
|
|
|
|
|
|
|
// CreateForum.
|
|
|
|
func CreateForum(f *Forum) error {
|
|
|
|
result := DB.Create(f)
|
|
|
|
return result.Error
|
|
|
|
}
|
|
|
|
|
|
|
|
// Save a forum.
|
|
|
|
func (f *Forum) Save() error {
|
|
|
|
return DB.Save(f).Error
|
|
|
|
}
|
|
|
|
|
|
|
|
// CategorizedForum supports the main index page with custom categories.
|
|
|
|
type CategorizedForum struct {
|
|
|
|
Category string
|
|
|
|
Forums []*Forum
|
|
|
|
}
|
|
|
|
|
|
|
|
// CategorizeForums buckets forums into categories for front-end.
|
|
|
|
func CategorizeForums(fs []*Forum, categories []string) []*CategorizedForum {
|
|
|
|
var (
|
|
|
|
result = []*CategorizedForum{}
|
|
|
|
idxMap = map[string]int{}
|
|
|
|
)
|
|
|
|
|
2024-08-21 04:26:53 +00:00
|
|
|
// Forum Browse page: we are not grouping by categories but still need at least one.
|
|
|
|
if len(categories) == 0 {
|
|
|
|
return []*CategorizedForum{
|
|
|
|
{
|
|
|
|
Forums: fs,
|
|
|
|
},
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-08-24 05:55:19 +00:00
|
|
|
// Initialize the result set.
|
|
|
|
for i, category := range categories {
|
|
|
|
result = append(result, &CategorizedForum{
|
|
|
|
Category: category,
|
|
|
|
Forums: []*Forum{},
|
|
|
|
})
|
|
|
|
idxMap[category] = i
|
|
|
|
}
|
|
|
|
|
|
|
|
// Bucket the forums into their categories.
|
|
|
|
for _, forum := range fs {
|
|
|
|
category := forum.Category
|
|
|
|
if category == "" {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
idx := idxMap[category]
|
|
|
|
result[idx].Forums = append(result[idx].Forums, forum)
|
|
|
|
}
|
|
|
|
|
2023-05-24 03:04:17 +00:00
|
|
|
// Remove any blank categories with no boards.
|
|
|
|
var filtered = []*CategorizedForum{}
|
|
|
|
for _, forum := range result {
|
|
|
|
if len(forum.Forums) == 0 {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
filtered = append(filtered, forum)
|
|
|
|
}
|
|
|
|
|
|
|
|
return filtered
|
2022-08-24 05:55:19 +00:00
|
|
|
}
|