2022-08-24 05:55:19 +00:00
|
|
|
package models
|
|
|
|
|
|
|
|
import (
|
2024-01-06 06:14:42 +00:00
|
|
|
"errors"
|
|
|
|
"math"
|
2023-09-17 06:20:12 +00:00
|
|
|
"strings"
|
2022-08-24 05:55:19 +00:00
|
|
|
"time"
|
|
|
|
|
2022-08-27 02:50:33 +00:00
|
|
|
"code.nonshy.com/nonshy/website/pkg/log"
|
2022-08-24 05:55:19 +00:00
|
|
|
"gorm.io/gorm"
|
|
|
|
)
|
|
|
|
|
|
|
|
// Comment table - in forum threads, on profiles or photos, etc.
|
|
|
|
type Comment struct {
|
|
|
|
ID uint64 `gorm:"primaryKey"`
|
|
|
|
TableName string `gorm:"index"`
|
|
|
|
TableID uint64 `gorm:"index"`
|
|
|
|
UserID uint64 `gorm:"index"`
|
Change Logs
* Add a ChangeLog table to collect historic updates to various database tables.
* Created, Updated (with field diffs) and Deleted actions are logged, as well
as certification photo approves/denies.
* Specific items added to the change log:
* When a user photo is marked Explicit by an admin
* When users block/unblock each other
* When photo comments are posted, edited, and deleted
* When forums are created, edited, and deleted
* When forum comments are created, edited and deleted
* When a new forum thread is created
* When a user uploads or removes their own certification photo
* When an admin approves or rejects a certification photo
* When a user uploads, modifies or deletes their gallery photos
* When a friend request is sent
* When a friend request is accepted, ignored, or rejected
* When a friendship is removed
2024-02-26 01:03:36 +00:00
|
|
|
User User `json:"-"`
|
2022-08-24 05:55:19 +00:00
|
|
|
Message string
|
2024-03-04 01:58:18 +00:00
|
|
|
CreatedAt time.Time `gorm:"index"`
|
2022-08-24 05:55:19 +00:00
|
|
|
UpdatedAt time.Time
|
|
|
|
}
|
|
|
|
|
2022-08-27 02:50:33 +00:00
|
|
|
// CommentableTables are the set of table names that allow comments (via the
|
|
|
|
// generic "/comments" URI which accepts a table_name param)
|
|
|
|
var CommentableTables = map[string]interface{}{
|
2022-08-27 18:42:48 +00:00
|
|
|
"photos": nil,
|
|
|
|
"threads": nil,
|
2022-08-27 02:50:33 +00:00
|
|
|
}
|
|
|
|
|
2024-08-08 06:05:23 +00:00
|
|
|
// SubscribableTables are the set of table names that allow notification subscriptions.
|
|
|
|
var SubscribableTables = map[string]interface{}{
|
|
|
|
"photos": nil,
|
|
|
|
"threads": nil,
|
|
|
|
|
|
|
|
// Special case: new photo uploads from your friends. You can't comment on this,
|
|
|
|
// but you can (un)subscribe from it all the same.
|
|
|
|
"friend.photos": nil,
|
|
|
|
}
|
|
|
|
|
2022-08-24 05:55:19 +00:00
|
|
|
// Preload related tables for the forum (classmethod).
|
|
|
|
func (c *Comment) Preload() *gorm.DB {
|
|
|
|
return DB.Preload("User.ProfilePhoto")
|
|
|
|
}
|
|
|
|
|
|
|
|
// GetComment by ID.
|
|
|
|
func GetComment(id uint64) (*Comment, error) {
|
|
|
|
c := &Comment{}
|
|
|
|
result := c.Preload().First(&c, id)
|
|
|
|
return c, result.Error
|
|
|
|
}
|
|
|
|
|
2022-08-26 02:58:43 +00:00
|
|
|
// GetComments queries a set of comment IDs and returns them mapped.
|
|
|
|
func GetComments(IDs []uint64) (map[uint64]*Comment, error) {
|
|
|
|
var (
|
|
|
|
mt = map[uint64]*Comment{}
|
|
|
|
ts = []*Comment{}
|
|
|
|
)
|
|
|
|
|
|
|
|
result := (&Comment{}).Preload().Where("id IN ?", IDs).Find(&ts)
|
|
|
|
for _, row := range ts {
|
|
|
|
mt[row.ID] = row
|
|
|
|
}
|
|
|
|
|
|
|
|
return mt, result.Error
|
|
|
|
}
|
|
|
|
|
2022-08-24 05:55:19 +00:00
|
|
|
// AddComment about anything.
|
|
|
|
func AddComment(user *User, tableName string, tableID uint64, message string) (*Comment, error) {
|
|
|
|
c := &Comment{
|
|
|
|
TableName: tableName,
|
|
|
|
TableID: tableID,
|
|
|
|
User: *user,
|
|
|
|
Message: message,
|
|
|
|
}
|
|
|
|
result := DB.Create(c)
|
|
|
|
return c, result.Error
|
|
|
|
}
|
|
|
|
|
2024-01-07 00:44:05 +00:00
|
|
|
// CountCommentsByUser returns the total number of comments written by a user.
|
|
|
|
func CountCommentsByUser(user *User, tableName string) int64 {
|
|
|
|
var count int64
|
|
|
|
result := DB.Where(
|
|
|
|
"table_name = ? AND user_id = ?",
|
|
|
|
tableName, user.ID,
|
|
|
|
).Model(&Comment{}).Count(&count)
|
|
|
|
if result.Error != nil {
|
|
|
|
log.Error("CountCommentsByUser(%d): %s", user.ID, result.Error)
|
|
|
|
}
|
|
|
|
return count
|
|
|
|
}
|
|
|
|
|
|
|
|
// CountCommentsReceived returns the total number of comments received on a user's photos.
|
|
|
|
func CountCommentsReceived(user *User) int64 {
|
|
|
|
var count int64
|
|
|
|
DB.Model(&Comment{}).Joins(
|
|
|
|
"LEFT OUTER JOIN photos ON (comments.table_name = 'photos' AND comments.table_id = photos.id)",
|
|
|
|
).Where(
|
|
|
|
"comments.table_name = 'photos' AND photos.user_id = ?",
|
|
|
|
user.ID,
|
|
|
|
).Count(&count)
|
|
|
|
return count
|
|
|
|
}
|
|
|
|
|
2022-08-24 05:55:19 +00:00
|
|
|
// PaginateComments provides a page of comments on something.
|
2024-08-29 01:42:49 +00:00
|
|
|
//
|
|
|
|
// 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) {
|
2022-08-24 05:55:19 +00:00
|
|
|
var (
|
2024-08-29 01:42:49 +00:00
|
|
|
cs = []*Comment{}
|
|
|
|
query = (&Comment{}).Preload()
|
|
|
|
wheres = []string{}
|
|
|
|
placeholders = []interface{}{}
|
2022-08-24 05:55:19 +00:00
|
|
|
)
|
|
|
|
|
2023-09-17 06:20:12 +00:00
|
|
|
wheres = append(wheres, "table_name = ? AND table_id = ?")
|
|
|
|
placeholders = append(placeholders, tableName, tableID)
|
|
|
|
|
2024-08-29 01:42:49 +00:00
|
|
|
if !noBlockLists {
|
|
|
|
blockedUserIDs := BlockedUserIDs(user)
|
|
|
|
if len(blockedUserIDs) > 0 {
|
|
|
|
wheres = append(wheres, "user_id NOT IN ?")
|
|
|
|
placeholders = append(placeholders, blockedUserIDs)
|
|
|
|
}
|
2023-09-17 06:20:12 +00:00
|
|
|
}
|
|
|
|
|
2023-10-22 22:02:24 +00:00
|
|
|
// Don't show comments from banned or disabled accounts.
|
|
|
|
wheres = append(wheres, `
|
|
|
|
EXISTS (
|
|
|
|
SELECT 1
|
|
|
|
FROM users
|
|
|
|
WHERE users.id = comments.user_id
|
|
|
|
AND users.status = 'active'
|
|
|
|
)
|
|
|
|
`)
|
|
|
|
|
2022-08-24 05:55:19 +00:00
|
|
|
query = query.Where(
|
2023-09-17 06:20:12 +00:00
|
|
|
strings.Join(wheres, " AND "),
|
|
|
|
placeholders...,
|
2022-08-24 05:55:19 +00:00
|
|
|
).Order(pager.Sort)
|
|
|
|
|
|
|
|
query.Model(&Comment{}).Count(&pager.Total)
|
|
|
|
result := query.Offset(pager.GetOffset()).Limit(pager.PerPage).Find(&cs)
|
2022-09-09 04:42:20 +00:00
|
|
|
|
|
|
|
// Inject user relationships into these comments' authors.
|
|
|
|
SetUserRelationshipsInComments(user, cs)
|
|
|
|
|
2022-08-24 05:55:19 +00:00
|
|
|
return cs, result.Error
|
|
|
|
}
|
|
|
|
|
2024-01-06 06:14:42 +00:00
|
|
|
// FindPageByComment finds out what page a comment ID exists on for the current user, taking into
|
|
|
|
// account their block lists and comment visibility.
|
|
|
|
//
|
|
|
|
// Note: the comments are assumed ordered by created_at asc.
|
|
|
|
func FindPageByComment(user *User, comment *Comment, pageSize int) (int, error) {
|
|
|
|
var (
|
|
|
|
allCommentIDs []uint64
|
|
|
|
blockedUserIDs = BlockedUserIDs(user)
|
|
|
|
wheres = []string{}
|
|
|
|
placeholders = []interface{}{}
|
|
|
|
)
|
|
|
|
|
|
|
|
// Get the complete set of comment IDs that this comment is on a thread of.
|
|
|
|
wheres = append(wheres, "table_name = ? AND table_id = ?")
|
|
|
|
placeholders = append(placeholders, comment.TableName, comment.TableID)
|
|
|
|
|
|
|
|
if len(blockedUserIDs) > 0 {
|
|
|
|
wheres = append(wheres, "user_id NOT IN ?")
|
|
|
|
placeholders = append(placeholders, blockedUserIDs)
|
|
|
|
}
|
|
|
|
|
|
|
|
result := DB.Table(
|
|
|
|
"comments",
|
|
|
|
).Select(
|
|
|
|
"id",
|
|
|
|
).Where(
|
|
|
|
strings.Join(wheres, " AND "),
|
|
|
|
placeholders...,
|
|
|
|
).Order("created_at asc").Scan(&allCommentIDs)
|
|
|
|
if result.Error != nil {
|
|
|
|
return 0, result.Error
|
|
|
|
}
|
|
|
|
|
|
|
|
// Scan the comment thread to find it.
|
|
|
|
for i, cid := range allCommentIDs {
|
|
|
|
if cid == comment.ID {
|
|
|
|
var page = int(math.Ceil(float64(i) / float64(pageSize)))
|
2024-08-04 04:00:51 +00:00
|
|
|
|
|
|
|
// If the comment index is an equal multiple of the page size
|
|
|
|
// (e.g. comment #20 is the 1st comment on page 2, since 0-19 is page 1),
|
|
|
|
// account for an off-by-one error.
|
|
|
|
if i%pageSize == 0 {
|
|
|
|
page++
|
|
|
|
}
|
|
|
|
|
2024-01-06 06:25:05 +00:00
|
|
|
if page == 0 {
|
|
|
|
page = 1
|
|
|
|
}
|
2024-01-06 06:14:42 +00:00
|
|
|
return page, nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return -1, errors.New("comment not visible to current user")
|
|
|
|
}
|
|
|
|
|
2022-08-27 02:50:33 +00:00
|
|
|
// ListComments returns a complete set of comments without paging.
|
2023-09-17 06:07:32 +00:00
|
|
|
func ListComments(user *User, tableName string, tableID uint64) ([]*Comment, error) {
|
|
|
|
var (
|
|
|
|
cs []*Comment
|
2023-10-22 22:02:24 +00:00
|
|
|
blockedUserIDs = BlockedUserIDs(user)
|
2023-09-17 06:20:12 +00:00
|
|
|
wheres = []string{}
|
|
|
|
placeholders = []interface{}{}
|
2023-09-17 06:07:32 +00:00
|
|
|
)
|
2023-09-17 06:20:12 +00:00
|
|
|
|
|
|
|
wheres = append(wheres, "table_name = ? AND table_id = ?")
|
|
|
|
placeholders = append(placeholders, tableName, tableID)
|
|
|
|
|
|
|
|
if len(blockedUserIDs) > 0 {
|
|
|
|
wheres = append(wheres, "user_id NOT IN ?")
|
|
|
|
placeholders = append(placeholders, blockedUserIDs)
|
|
|
|
}
|
|
|
|
|
2023-10-22 22:02:24 +00:00
|
|
|
// Don't show comments from banned or disabled accounts.
|
|
|
|
wheres = append(wheres, `
|
|
|
|
EXISTS (
|
|
|
|
SELECT 1
|
|
|
|
FROM users
|
|
|
|
WHERE users.id = comments.user_id
|
|
|
|
AND users.status = 'active'
|
|
|
|
)
|
|
|
|
`)
|
|
|
|
|
2022-08-27 02:50:33 +00:00
|
|
|
result := (&Comment{}).Preload().Where(
|
2023-09-17 06:20:12 +00:00
|
|
|
strings.Join(wheres, " AND "),
|
|
|
|
placeholders...,
|
2022-08-27 02:50:33 +00:00
|
|
|
).Order("created_at asc").Find(&cs)
|
|
|
|
return cs, result.Error
|
|
|
|
}
|
|
|
|
|
2022-08-24 05:55:19 +00:00
|
|
|
// Save a comment.
|
|
|
|
func (c *Comment) Save() error {
|
|
|
|
return DB.Save(c).Error
|
|
|
|
}
|
|
|
|
|
|
|
|
// Delete a comment.
|
|
|
|
func (c *Comment) Delete() error {
|
|
|
|
return DB.Delete(c).Error
|
|
|
|
}
|
2022-08-27 02:50:33 +00:00
|
|
|
|
2022-08-29 03:56:26 +00:00
|
|
|
// IsEdited returns if a comment was reasonably edited after it was created.
|
|
|
|
func (c *Comment) IsEdited() bool {
|
|
|
|
return c.UpdatedAt.Sub(c.CreatedAt) > 5*time.Second
|
|
|
|
}
|
|
|
|
|
2022-08-27 02:50:33 +00:00
|
|
|
type CommentCountMap map[uint64]int64
|
|
|
|
|
|
|
|
// MapCommentCounts collects total numbers of comments over a set of table IDs. Returns a
|
|
|
|
// map of table ID (uint64) to comment counts for each (int64).
|
|
|
|
func MapCommentCounts(tableName string, tableIDs []uint64) CommentCountMap {
|
|
|
|
var result = CommentCountMap{}
|
|
|
|
|
|
|
|
// Initialize the result set.
|
|
|
|
for _, id := range tableIDs {
|
|
|
|
result[id] = 0
|
|
|
|
}
|
|
|
|
|
|
|
|
// Hold the result of the grouped count query.
|
|
|
|
type group struct {
|
|
|
|
ID uint64
|
|
|
|
Comments int64
|
|
|
|
}
|
|
|
|
var groups = []group{}
|
|
|
|
|
|
|
|
// Map the counts of comments to each of these IDs.
|
|
|
|
if res := DB.Table(
|
|
|
|
"comments",
|
|
|
|
).Select(
|
|
|
|
"table_id AS id, count(id) AS comments",
|
|
|
|
).Where(
|
|
|
|
"table_name = ? AND table_id IN ?",
|
|
|
|
tableName, tableIDs,
|
|
|
|
).Group("table_id").Scan(&groups); res.Error != nil {
|
|
|
|
log.Error("MapCommentCounts: count query: %s", res.Error)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Map the counts back in.
|
|
|
|
for _, row := range groups {
|
|
|
|
result[row.ID] = row.Comments
|
|
|
|
}
|
|
|
|
|
|
|
|
return result
|
|
|
|
}
|
|
|
|
|
|
|
|
// Get a comment count for the given table ID from the map.
|
|
|
|
func (cc CommentCountMap) Get(id uint64) int64 {
|
|
|
|
if value, ok := cc[id]; ok {
|
|
|
|
return value
|
|
|
|
}
|
|
|
|
return 0
|
|
|
|
}
|