mirror of
https://github.com/wgh136/nysoure.git
synced 2025-09-27 20:27:23 +00:00
282 lines
8.5 KiB
Go
282 lines
8.5 KiB
Go
package service
|
|
|
|
import (
|
|
"nysoure/server/dao"
|
|
"nysoure/server/model"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/gofiber/fiber/v3/log"
|
|
)
|
|
|
|
const (
|
|
maxCommentsPerIP = 512 // Maximum number of comments allowed per IP address per day
|
|
maxCommentLength = 2048 // Maximum length of a comment
|
|
maxCommentBriefLines = 16 // Maximum number of lines in a comment brief
|
|
maxCommentBriefLength = 256 // Maximum length of a comment brief
|
|
)
|
|
|
|
type CommentRequest struct {
|
|
Content string `json:"content"` // markdown
|
|
// Images []uint `json:"images"` // Unrequired after new design
|
|
}
|
|
|
|
func findImagesInContent(content string, host string) []uint {
|
|
// Handle both absolute and relative URLs
|
|
absolutePattern := `!\[.*?\]\((?:https?://` + host + `)?/api/image/(\d+)(?:\s+["'].*?["'])?\)`
|
|
relativePattern := `!\[.*?\]\(/api/image/(\d+)(?:\s+["'].*?["'])?\)`
|
|
|
|
// Combine patterns and compile regex
|
|
patterns := []string{absolutePattern, relativePattern}
|
|
|
|
// Store unique image IDs to avoid duplicates
|
|
imageIDs := make(map[uint]struct{})
|
|
|
|
for _, pattern := range patterns {
|
|
re := regexp.MustCompile(pattern)
|
|
matches := re.FindAllStringSubmatch(content, -1)
|
|
|
|
for _, match := range matches {
|
|
if len(match) >= 2 {
|
|
if id, err := strconv.ParseUint(match[1], 10, 32); err == nil {
|
|
imageIDs[uint(id)] = struct{}{}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Convert map keys to slice
|
|
result := make([]uint, 0, len(imageIDs))
|
|
for id := range imageIDs {
|
|
result = append(result, id)
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
func CreateComment(req CommentRequest, userID uint, refID uint, ip string, cType model.CommentType, host string) (*model.CommentView, error) {
|
|
if len(req.Content) == 0 {
|
|
return nil, model.NewRequestError("Content cannot be empty")
|
|
}
|
|
if len([]rune(req.Content)) > maxCommentLength {
|
|
return nil, model.NewRequestError("Comment content exceeds maximum length of 1024 characters")
|
|
}
|
|
|
|
switch cType {
|
|
case model.CommentTypeResource:
|
|
resourceExists, err := dao.ExistsResource(refID)
|
|
if err != nil {
|
|
log.Error("Error checking resource existence:", err)
|
|
return nil, model.NewInternalServerError("Error checking resource existence")
|
|
}
|
|
if !resourceExists {
|
|
return nil, model.NewNotFoundError("Resource not found")
|
|
}
|
|
case model.CommentTypeReply:
|
|
_, err := dao.GetCommentByID(refID)
|
|
if err != nil {
|
|
log.Error("Error getting reply comment:", err)
|
|
return nil, model.NewNotFoundError("Reply comment not found")
|
|
}
|
|
}
|
|
|
|
userExists, err := dao.ExistsUserByID(userID)
|
|
if err != nil {
|
|
log.Error("Error checking user existence:", err)
|
|
return nil, model.NewInternalServerError("Error checking user existence")
|
|
}
|
|
if !userExists {
|
|
return nil, model.NewNotFoundError("User not found")
|
|
}
|
|
|
|
images := findImagesInContent(req.Content, host)
|
|
|
|
c, err := dao.CreateComment(req.Content, userID, refID, images, cType)
|
|
if err != nil {
|
|
log.Error("Error creating comment:", err)
|
|
return nil, model.NewInternalServerError("Error creating comment")
|
|
}
|
|
err = dao.AddNewCommentActivity(userID, c.ID)
|
|
if err != nil {
|
|
log.Error("Error creating comment activity:", err)
|
|
}
|
|
return c.ToView(), nil
|
|
}
|
|
|
|
func restrictCommentLength(content string) (c string, truncated bool) {
|
|
lines := strings.Split(content, "\n")
|
|
lineCount := 0
|
|
for i, line := range lines {
|
|
reg := regexp.MustCompile(`!\[.*?\]\(.*?\)`)
|
|
if reg.MatchString(line) {
|
|
// Count the line with image as 5 lines
|
|
lineCount += 5
|
|
} else {
|
|
lineCount++
|
|
}
|
|
|
|
if lineCount > maxCommentBriefLines {
|
|
lines = lines[:i+1] // Keep the current line
|
|
content = strings.Join(lines, "\n")
|
|
truncated = true
|
|
break
|
|
}
|
|
}
|
|
|
|
if len([]rune(content)) > maxCommentBriefLength {
|
|
i := len(lines) - 1
|
|
for count := len([]rune(content)); count > maxCommentBriefLength && i > 0; i-- {
|
|
count -= len([]rune(lines[i]))
|
|
}
|
|
if i == 0 && len([]rune(lines[0])) > maxCommentBriefLength {
|
|
content = string([]rune(lines[0])[:maxCommentBriefLength])
|
|
} else {
|
|
content = strings.Join(lines[:i+1], "\n")
|
|
}
|
|
truncated = true
|
|
}
|
|
return content, truncated
|
|
}
|
|
|
|
func ListResourceComments(resourceID uint, page int) ([]model.CommentView, int, error) {
|
|
resourceExists, err := dao.ExistsResource(resourceID)
|
|
if err != nil {
|
|
log.Error("Error checking resource existence:", err)
|
|
return nil, 0, model.NewInternalServerError("Error checking resource existence")
|
|
}
|
|
if !resourceExists {
|
|
return nil, 0, model.NewNotFoundError("Resource not found")
|
|
}
|
|
comments, totalPages, err := dao.GetCommentByResourceID(resourceID, page, pageSize)
|
|
if err != nil {
|
|
log.Error("Error getting comments:", err)
|
|
return nil, 0, model.NewInternalServerError("Error getting comments")
|
|
}
|
|
res := make([]model.CommentView, 0, len(comments))
|
|
for _, c := range comments {
|
|
v := *c.ToView()
|
|
var truncated bool
|
|
v.Content, truncated = restrictCommentLength(v.Content)
|
|
v.ContentTruncated = truncated
|
|
res = append(res, v)
|
|
}
|
|
return res, totalPages, nil
|
|
}
|
|
|
|
func ListCommentReplies(commentID uint, page int) ([]model.CommentView, int, error) {
|
|
replies, totalPages, err := dao.GetCommentReplies(commentID, page, pageSize)
|
|
if err != nil {
|
|
log.Error("Error getting replies:", err)
|
|
return nil, 0, model.NewInternalServerError("Error getting replies")
|
|
}
|
|
res := make([]model.CommentView, 0, len(replies))
|
|
for _, r := range replies {
|
|
v := *r.ToView()
|
|
var truncated bool
|
|
v.Content, truncated = restrictCommentLength(v.Content)
|
|
v.ContentTruncated = truncated
|
|
res = append(res, v)
|
|
}
|
|
return res, totalPages, nil
|
|
}
|
|
|
|
func ListCommentsWithUser(username string, page int) ([]model.CommentWithResourceView, int, error) {
|
|
comments, totalPages, err := dao.GetCommentsWithUser(username, page, pageSize)
|
|
if err != nil {
|
|
log.Error("Error getting comments:", err)
|
|
return nil, 0, model.NewInternalServerError("Error getting comments")
|
|
}
|
|
res := make([]model.CommentWithResourceView, 0, len(comments))
|
|
for _, c := range comments {
|
|
r, err := dao.GetResourceByID(c.RefID)
|
|
if err != nil {
|
|
log.Error("Error getting resource for comment:", err)
|
|
return nil, 0, model.NewInternalServerError("Error getting resource for comment")
|
|
}
|
|
v := *c.ToViewWithResource(&r)
|
|
var truncated bool
|
|
v.Content, truncated = restrictCommentLength(v.Content)
|
|
v.ContentTruncated = truncated
|
|
res = append(res, v)
|
|
}
|
|
return res, totalPages, nil
|
|
}
|
|
|
|
func UpdateComment(commentID, userID uint, req CommentRequest, host string) (*model.CommentView, error) {
|
|
if len(req.Content) == 0 {
|
|
return nil, model.NewRequestError("Content cannot be empty")
|
|
}
|
|
if len([]rune(req.Content)) > maxCommentLength {
|
|
return nil, model.NewRequestError("Comment content exceeds maximum length of 1024 characters")
|
|
}
|
|
|
|
comment, err := dao.GetCommentByID(commentID)
|
|
if err != nil {
|
|
return nil, model.NewNotFoundError("Comment not found")
|
|
}
|
|
if comment.UserID != userID {
|
|
isAdmin, err := CheckUserIsAdmin(userID)
|
|
if err != nil {
|
|
log.Error("Error checking if user is admin:", err)
|
|
return nil, model.NewInternalServerError("Error checking user permissions")
|
|
}
|
|
if !isAdmin {
|
|
return nil, model.NewUnAuthorizedError("You can only update your own comments")
|
|
}
|
|
}
|
|
images := findImagesInContent(req.Content, host)
|
|
updated, err := dao.UpdateCommentContent(commentID, req.Content, images)
|
|
if err != nil {
|
|
return nil, model.NewInternalServerError("Error updating comment")
|
|
}
|
|
return updated.ToView(), nil
|
|
}
|
|
|
|
func DeleteComment(commentID, userID uint) error {
|
|
comment, err := dao.GetCommentByID(commentID)
|
|
if err != nil {
|
|
return model.NewNotFoundError("Comment not found")
|
|
}
|
|
if comment.UserID != userID {
|
|
return model.NewRequestError("You can only delete your own comments")
|
|
}
|
|
if err := dao.DeleteCommentByID(commentID); err != nil {
|
|
return model.NewInternalServerError("Error deleting comment")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func GetCommentByID(commentID uint) (*model.CommentWithRefView, error) {
|
|
comment, err := dao.GetCommentByID(commentID)
|
|
if err != nil {
|
|
return nil, model.NewNotFoundError("Comment not found")
|
|
}
|
|
|
|
var resource *model.Resource
|
|
var replyTo *model.Comment
|
|
if comment.Type == model.CommentTypeResource {
|
|
r, err := dao.GetResourceByID(comment.RefID)
|
|
if err != nil {
|
|
log.Error("Error getting resource for comment:", err)
|
|
return nil, model.NewInternalServerError("Error getting resource for comment")
|
|
}
|
|
resource = &r
|
|
} else {
|
|
reply, err := dao.GetCommentByID(comment.RefID)
|
|
if err != nil {
|
|
log.Error("Error getting reply for comment:", err)
|
|
return nil, model.NewInternalServerError("Error getting reply for comment")
|
|
}
|
|
replyTo = reply
|
|
}
|
|
|
|
v := comment.ToViewWithRef(resource, replyTo)
|
|
|
|
if v.ReplyTo != nil {
|
|
v.ReplyTo.Content, v.ReplyTo.ContentTruncated = restrictCommentLength(v.ReplyTo.Content)
|
|
}
|
|
|
|
return v, nil
|
|
}
|