2024-02-02 15:42:42 +00:00
package services
import (
2024-03-05 15:40:54 +00:00
"errors"
"fmt"
2024-08-17 14:25:11 +00:00
"regexp"
2024-07-29 16:06:58 +00:00
"strconv"
2024-05-25 09:43:26 +00:00
"time"
2024-06-22 09:29:53 +00:00
"git.solsynth.dev/hydrogen/interactive/pkg/internal/database"
"git.solsynth.dev/hydrogen/interactive/pkg/internal/models"
2024-03-02 17:23:11 +00:00
"github.com/rs/zerolog/log"
2024-02-03 07:20:32 +00:00
"github.com/samber/lo"
"github.com/spf13/viper"
2024-07-29 16:06:58 +00:00
"gorm.io/datatypes"
2024-02-02 16:50:23 +00:00
"gorm.io/gorm"
2024-03-02 17:23:11 +00:00
)
2024-07-27 17:49:16 +00:00
func FilterPostWithUserContext ( tx * gorm . DB , user * models . Account ) * gorm . DB {
if user == nil {
return tx . Where ( "visibility = ?" , models . PostVisibilityAll )
}
2024-07-29 16:06:58 +00:00
const (
FriendsVisibility = models . PostVisibilityFriends
SelectedVisibility = models . PostVisibilitySelected
2024-08-17 17:06:52 +00:00
FilteredVisibility = models . PostVisibilityFiltered
2024-07-29 16:06:58 +00:00
NoneVisibility = models . PostVisibilityNone
2024-07-29 15:33:55 +00:00
)
2024-07-27 17:49:16 +00:00
2024-08-17 17:06:52 +00:00
friends , _ := ListAccountFriends ( * user )
friendAllowList := lo . Map ( friends , func ( item models . Account , index int ) uint {
return item . ID
} )
2024-09-03 12:07:20 +00:00
blocked , _ := ListAccountBlockedUsers ( * user )
blockedDisallowList := lo . Map ( blocked , func ( item models . Account , index int ) uint {
return item . ID
} )
2024-08-17 17:06:52 +00:00
2024-08-10 09:18:55 +00:00
tx = tx . Where (
2024-09-03 12:07:20 +00:00
"(visibility != ? OR (visibility != ? AND author_id IN ? AND author_id NOT IN ?) OR (visibility = ? AND ?) OR (visibility = ? AND NOT ?) OR author_id = ?)" ,
2024-08-10 09:18:55 +00:00
NoneVisibility ,
2024-08-17 17:06:52 +00:00
FriendsVisibility ,
friendAllowList ,
2024-09-03 12:07:20 +00:00
blockedDisallowList ,
2024-08-10 09:18:55 +00:00
SelectedVisibility ,
datatypes . JSONQuery ( "visible_users" ) . HasKey ( strconv . Itoa ( int ( user . ID ) ) ) ,
2024-08-17 17:06:52 +00:00
FilteredVisibility ,
2024-08-10 09:18:55 +00:00
datatypes . JSONQuery ( "invisible_users" ) . HasKey ( strconv . Itoa ( int ( user . ID ) ) ) ,
user . ID ,
)
2024-07-29 16:06:58 +00:00
2024-07-27 17:49:16 +00:00
return tx
}
2024-05-15 11:45:49 +00:00
func FilterPostWithCategory ( tx * gorm . DB , alias string ) * gorm . DB {
2024-07-07 05:57:31 +00:00
prefix := viper . GetString ( "database.prefix" )
return tx . Joins ( fmt . Sprintf ( "JOIN %spost_categories ON %sposts.id = %spost_categories.post_id" , prefix , prefix , prefix ) ) .
2024-07-07 06:09:52 +00:00
Joins ( fmt . Sprintf ( "JOIN %scategories ON %scategories.id = %spost_categories.category_id" , prefix , prefix , prefix ) ) .
Where ( fmt . Sprintf ( "%scategories.alias = ?" , prefix ) , alias )
2024-02-07 08:41:35 +00:00
}
2024-05-15 11:45:49 +00:00
func FilterPostWithTag ( tx * gorm . DB , alias string ) * gorm . DB {
2024-07-07 05:57:31 +00:00
prefix := viper . GetString ( "database.prefix" )
return tx . Joins ( fmt . Sprintf ( "JOIN %spost_tags ON %sposts.id = %spost_tags.post_id" , prefix , prefix , prefix ) ) .
2024-07-07 06:09:52 +00:00
Joins ( fmt . Sprintf ( "JOIN %stags ON %stags.id = %spost_tags.tag_id" , prefix , prefix , prefix ) ) .
Where ( fmt . Sprintf ( "%stags.alias = ?" , prefix ) , alias )
2024-02-07 08:41:35 +00:00
}
2024-07-03 14:16:23 +00:00
func FilterPostWithRealm ( tx * gorm . DB , id uint ) * gorm . DB {
2024-03-02 17:23:11 +00:00
if id > 0 {
2024-05-15 11:45:49 +00:00
return tx . Where ( "realm_id = ?" , id )
2024-03-02 17:23:11 +00:00
} else {
2024-05-15 11:45:49 +00:00
return tx . Where ( "realm_id IS NULL" )
2024-02-06 02:57:05 +00:00
}
2024-03-02 17:23:11 +00:00
}
2024-02-06 02:57:05 +00:00
2024-05-15 11:45:49 +00:00
func FilterPostReply ( tx * gorm . DB , replyTo ... uint ) * gorm . DB {
if len ( replyTo ) > 0 && replyTo [ 0 ] > 0 {
return tx . Where ( "reply_id = ?" , replyTo [ 0 ] )
2024-03-02 17:23:11 +00:00
} else {
2024-05-15 11:45:49 +00:00
return tx . Where ( "reply_id IS NULL" )
2024-02-06 02:57:05 +00:00
}
2024-03-02 17:23:11 +00:00
}
2024-02-06 02:57:05 +00:00
2024-05-15 11:45:49 +00:00
func FilterPostWithPublishedAt ( tx * gorm . DB , date time . Time ) * gorm . DB {
2024-07-21 17:44:04 +00:00
return tx .
Where ( "published_at <= ? OR published_at IS NULL" , date ) .
Where ( "published_until > ? OR published_until IS NULL" , date )
2024-03-02 17:23:11 +00:00
}
2024-07-03 14:16:23 +00:00
func FilterPostWithAuthorDraft ( tx * gorm . DB , uid uint ) * gorm . DB {
return tx . Where ( "author_id = ? AND is_draft = ?" , uid , true )
}
func FilterPostDraft ( tx * gorm . DB ) * gorm . DB {
2024-07-05 12:56:25 +00:00
return tx . Where ( "is_draft = ? OR is_draft IS NULL" , false )
2024-07-03 14:16:23 +00:00
}
2024-07-23 08:12:19 +00:00
func PreloadGeneral ( tx * gorm . DB ) * gorm . DB {
return tx .
2024-07-07 03:34:37 +00:00
Preload ( "Tags" ) .
Preload ( "Categories" ) .
2024-06-22 17:18:12 +00:00
Preload ( "Realm" ) .
2024-05-15 11:45:49 +00:00
Preload ( "Author" ) .
2024-05-15 12:31:25 +00:00
Preload ( "ReplyTo" ) .
Preload ( "ReplyTo.Author" ) .
2024-07-07 04:31:05 +00:00
Preload ( "ReplyTo.Tags" ) .
Preload ( "ReplyTo.Categories" ) .
2024-05-15 12:31:25 +00:00
Preload ( "RepostTo" ) .
Preload ( "RepostTo.Author" ) .
2024-07-07 04:31:05 +00:00
Preload ( "RepostTo.Tags" ) .
2024-07-23 08:12:19 +00:00
Preload ( "RepostTo.Categories" )
}
func GetPost ( tx * gorm . DB , id uint , ignoreLimitation ... bool ) ( models . Post , error ) {
if len ( ignoreLimitation ) == 0 || ! ignoreLimitation [ 0 ] {
tx = FilterPostWithPublishedAt ( tx , time . Now ( ) )
}
var item models . Post
if err := PreloadGeneral ( tx ) .
Where ( "id = ?" , id ) .
2024-05-15 11:45:49 +00:00
First ( & item ) . Error ; err != nil {
2024-03-10 15:35:38 +00:00
return item , err
}
2024-03-02 17:23:11 +00:00
return item , nil
}
2024-08-17 07:40:07 +00:00
func GetPostByAlias ( tx * gorm . DB , alias , area string , ignoreLimitation ... bool ) ( models . Post , error ) {
if len ( ignoreLimitation ) == 0 || ! ignoreLimitation [ 0 ] {
tx = FilterPostWithPublishedAt ( tx , time . Now ( ) )
}
var item models . Post
if err := PreloadGeneral ( tx ) .
Where ( "alias = ?" , alias ) .
Where ( "area_alias = ?" , area ) .
First ( & item ) . Error ; err != nil {
return item , err
}
return item , nil
}
2024-05-15 11:45:49 +00:00
func CountPost ( tx * gorm . DB ) ( int64 , error ) {
2024-03-02 17:23:11 +00:00
var count int64
2024-05-15 11:45:49 +00:00
if err := tx . Model ( & models . Post { } ) . Count ( & count ) . Error ; err != nil {
2024-03-02 17:23:11 +00:00
return count , err
}
return count , nil
2024-02-06 02:57:05 +00:00
}
2024-05-15 11:45:49 +00:00
func CountPostReply ( id uint ) int64 {
2024-03-25 11:47:51 +00:00
var count int64
2024-05-15 11:45:49 +00:00
if err := database . C . Model ( & models . Post { } ) .
Where ( "reply_id = ?" , id ) .
2024-03-25 11:47:51 +00:00
Count ( & count ) . Error ; err != nil {
return 0
}
return count
}
2024-05-15 11:45:49 +00:00
func CountPostReactions ( id uint ) int64 {
2024-03-25 11:47:51 +00:00
var count int64
if err := database . C . Model ( & models . Reaction { } ) .
2024-05-15 11:45:49 +00:00
Where ( "post_id = ?" , id ) .
2024-03-25 11:47:51 +00:00
Count ( & count ) . Error ; err != nil {
return 0
}
return count
}
2024-07-23 08:12:19 +00:00
func ListPost ( tx * gorm . DB , take int , offset int , order any , noReact ... bool ) ( [ ] * models . Post , error ) {
2024-07-16 08:52:00 +00:00
if take > 100 {
take = 100
2024-02-12 04:32:37 +00:00
}
2024-05-19 12:15:28 +00:00
var items [ ] * models . Post
2024-07-23 08:12:19 +00:00
if err := PreloadGeneral ( tx ) .
2024-05-15 11:45:49 +00:00
Limit ( take ) . Offset ( offset ) .
2024-07-23 08:12:19 +00:00
Order ( order ) .
2024-05-15 11:45:49 +00:00
Find ( & items ) . Error ; err != nil {
2024-03-02 17:23:11 +00:00
return items , err
2024-02-03 07:20:32 +00:00
}
2024-05-19 12:15:28 +00:00
idx := lo . Map ( items , func ( item * models . Post , index int ) uint {
2024-03-05 15:40:54 +00:00
return item . ID
2024-03-03 14:57:17 +00:00
} )
2024-07-03 14:16:23 +00:00
// Load reactions
2024-03-03 14:57:17 +00:00
if len ( noReact ) <= 0 || ! noReact [ 0 ] {
2024-07-05 13:06:18 +00:00
if mapping , err := BatchListResourceReactions ( database . C . Where ( "post_id IN ?" , idx ) , "post_id" ) ; err != nil {
2024-03-03 14:57:17 +00:00
return items , err
2024-07-03 14:16:23 +00:00
} else {
itemMap := lo . SliceToMap ( items , func ( item * models . Post ) ( uint , * models . Post ) {
return item . ID , item
} )
for k , v := range mapping {
if post , ok := itemMap [ k ] ; ok {
2024-07-16 08:52:00 +00:00
post . Metric = models . PostMetric {
ReactionList : v ,
}
2024-07-03 14:16:23 +00:00
}
2024-03-03 14:57:17 +00:00
}
}
}
2024-07-03 14:16:23 +00:00
// Load replies
2024-05-25 09:43:26 +00:00
if len ( noReact ) <= 0 || ! noReact [ 0 ] {
var replies [ ] struct {
PostID uint
Count int64
}
if err := database . C . Model ( & models . Post { } ) .
2024-08-01 20:53:25 +00:00
Select ( "reply_id as post_id, COUNT(id) as count" ) .
2024-05-25 09:43:26 +00:00
Where ( "reply_id IN (?)" , idx ) .
Group ( "post_id" ) .
Scan ( & replies ) . Error ; err != nil {
return items , err
}
itemMap := lo . SliceToMap ( items , func ( item * models . Post ) ( uint , * models . Post ) {
return item . ID , item
} )
list := map [ uint ] int64 { }
for _ , info := range replies {
list [ info . PostID ] = info . Count
}
for k , v := range list {
if post , ok := itemMap [ k ] ; ok {
2024-07-16 08:52:00 +00:00
post . Metric = models . PostMetric {
ReactionList : post . Metric . ReactionList ,
ReplyCount : v ,
}
2024-05-25 09:43:26 +00:00
}
}
}
2024-03-02 17:23:11 +00:00
return items , nil
2024-02-03 07:20:32 +00:00
}
2024-08-10 13:11:55 +00:00
func ListPostMinimal ( tx * gorm . DB , take int , offset int , order any ) ( [ ] * models . Post , error ) {
if take > 500 {
take = 500
}
var items [ ] * models . Post
if err := tx .
Limit ( take ) . Offset ( offset ) .
Order ( order ) .
Find ( & items ) . Error ; err != nil {
return items , err
}
return items , nil
}
2024-07-03 14:16:23 +00:00
func EnsurePostCategoriesAndTags ( item models . Post ) ( models . Post , error ) {
2024-02-02 15:42:42 +00:00
var err error
2024-05-15 11:45:49 +00:00
for idx , category := range item . Categories {
item . Categories [ idx ] , err = GetCategory ( category . Alias )
2024-02-02 15:42:42 +00:00
if err != nil {
2024-03-02 17:23:11 +00:00
return item , err
2024-02-02 15:42:42 +00:00
}
}
2024-05-15 11:45:49 +00:00
for idx , tag := range item . Tags {
item . Tags [ idx ] , err = GetTagOrCreate ( tag . Alias , tag . Name )
2024-02-02 15:42:42 +00:00
if err != nil {
2024-03-02 17:23:11 +00:00
return item , err
2024-02-02 15:42:42 +00:00
}
}
2024-03-02 17:23:11 +00:00
return item , nil
}
2024-05-15 11:45:49 +00:00
func NewPost ( user models . Account , item models . Post ) ( models . Post , error ) {
2024-08-17 16:07:53 +00:00
if item . Alias != nil && len ( * item . Alias ) == 0 {
item . Alias = nil
}
2024-08-17 14:25:11 +00:00
if item . Alias != nil {
re := regexp . MustCompile ( ` ^[a-z0-9.-]+$ ` )
if ! re . MatchString ( * item . Alias ) {
return item , fmt . Errorf ( "invalid post alias, learn more about alias rule on our wiki" )
}
}
2024-08-17 07:40:07 +00:00
if item . Realm != nil {
item . AreaAlias = & item . Realm . Alias
} else {
item . AreaAlias = & user . Name
}
2024-07-28 13:36:47 +00:00
log . Debug ( ) . Any ( "body" , item . Body ) . Msg ( "Posting a post..." )
start := time . Now ( )
log . Debug ( ) . Any ( "tags" , item . Tags ) . Any ( "categories" , item . Categories ) . Msg ( "Preparing categories and tags..." )
2024-07-03 14:16:23 +00:00
item , err := EnsurePostCategoriesAndTags ( item )
2024-03-02 17:23:11 +00:00
if err != nil {
return item , err
}
2024-02-02 15:42:42 +00:00
2024-05-15 11:45:49 +00:00
if item . RealmID != nil {
2024-07-28 13:36:47 +00:00
log . Debug ( ) . Uint ( "id" , * item . RealmID ) . Msg ( "Looking for post author realm..." )
2024-09-17 11:31:54 +00:00
member , err := GetRealmMember ( * item . RealmID , user . ID )
2024-05-04 14:22:58 +00:00
if err != nil {
return item , fmt . Errorf ( "you aren't a part of that realm: %v" , err )
2024-09-17 11:31:54 +00:00
} else if ! item . Realm . IsCommunity && member . PowerLevel < 25 {
return item , fmt . Errorf ( "you need has power level above 25 of a realm or in a community realm to post" )
2024-02-09 04:36:39 +00:00
}
2024-02-02 15:42:42 +00:00
}
2024-07-28 13:36:47 +00:00
log . Debug ( ) . Msg ( "Saving post record into database..." )
2024-03-02 17:23:11 +00:00
if err := database . C . Save ( & item ) . Error ; err != nil {
return item , err
2024-02-03 17:08:31 +00:00
}
2024-05-15 11:45:49 +00:00
// Notify the original poster its post has been replied
if item . ReplyID != nil {
2024-06-08 04:33:10 +00:00
var op models . Post
if err := database . C .
Where ( "id = ?" , item . ReplyID ) .
Preload ( "Author" ) .
First ( & op ) . Error ; err == nil {
if op . Author . ID != user . ID {
2024-07-28 13:36:47 +00:00
log . Debug ( ) . Uint ( "user" , op . AuthorID ) . Msg ( "Notifying the original poster their post got replied..." )
2024-07-16 02:53:02 +00:00
err = NotifyPosterAccount (
2024-06-08 04:33:10 +00:00
op . Author ,
2024-07-16 02:53:02 +00:00
"Post got replied" ,
2024-07-27 17:49:16 +00:00
fmt . Sprintf ( "%s (%s) replied your post (#%d)." , user . Nick , user . Name , op . ID ) ,
2024-07-16 02:53:02 +00:00
lo . ToPtr ( fmt . Sprintf ( "%s replied you" , user . Nick ) ) ,
2024-06-08 04:33:10 +00:00
)
if err != nil {
log . Error ( ) . Err ( err ) . Msg ( "An error occurred when notifying user..." )
2024-03-02 17:23:11 +00:00
}
}
2024-06-08 04:33:10 +00:00
}
2024-02-02 15:42:42 +00:00
}
2024-09-16 16:35:42 +00:00
// Notify the subscriptions
if content , ok := item . Body [ "content" ] . ( string ) ; ok {
var title * string
title , _ = item . Body [ "title" ] . ( * string )
go func ( ) {
if err := NotifyUserSubscription ( user , content , title ) ; err != nil {
log . Error ( ) . Err ( err ) . Msg ( "An error occurred when notifying subscriptions user by user..." )
}
for _ , tag := range item . Tags {
if err := NotifyTagSubscription ( tag , user , content , title ) ; err != nil {
log . Error ( ) . Err ( err ) . Msg ( "An error occurred when notifying subscriptions user by tag..." )
}
}
for _ , category := range item . Categories {
if err := NotifyCategorySubscription ( category , user , content , title ) ; err != nil {
log . Error ( ) . Err ( err ) . Msg ( "An error occurred when notifying subscriptions user by category..." )
}
}
} ( )
}
2024-07-28 13:36:47 +00:00
log . Debug ( ) . Dur ( "elapsed" , time . Since ( start ) ) . Msg ( "The post is posted." )
2024-03-02 17:23:11 +00:00
return item , nil
2024-02-02 15:42:42 +00:00
}
2024-02-02 16:50:23 +00:00
2024-05-15 11:45:49 +00:00
func EditPost ( item models . Post ) ( models . Post , error ) {
2024-08-17 16:07:53 +00:00
if item . Alias != nil && len ( * item . Alias ) == 0 {
item . Alias = nil
}
2024-08-17 14:25:11 +00:00
if item . Alias != nil {
re := regexp . MustCompile ( ` ^[a-z0-9.-]+$ ` )
if ! re . MatchString ( * item . Alias ) {
return item , fmt . Errorf ( "invalid post alias, learn more about alias rule on our wiki" )
}
}
2024-08-17 07:40:07 +00:00
if item . Realm != nil {
item . AreaAlias = & item . Realm . Alias
} else {
item . AreaAlias = & item . Author . Name
}
2024-07-03 14:16:23 +00:00
item , err := EnsurePostCategoriesAndTags ( item )
2024-03-02 17:23:11 +00:00
if err != nil {
return item , err
2024-02-03 17:08:31 +00:00
}
2024-03-02 17:23:11 +00:00
err = database . C . Save ( & item ) . Error
2024-02-03 17:08:31 +00:00
2024-03-02 17:23:11 +00:00
return item , err
}
2024-02-03 17:08:31 +00:00
2024-05-15 11:45:49 +00:00
func DeletePost ( item models . Post ) error {
2024-03-02 17:23:11 +00:00
return database . C . Delete ( & item ) . Error
2024-02-03 17:08:31 +00:00
}
2024-06-08 04:33:10 +00:00
func ReactPost ( user models . Account , reaction models . Reaction ) ( bool , models . Reaction , error ) {
2024-07-23 08:12:19 +00:00
var op models . Post
if err := database . C .
Where ( "id = ?" , reaction . PostID ) .
Preload ( "Author" ) .
First ( & op ) . Error ; err != nil {
return true , reaction , err
}
2024-03-03 04:17:18 +00:00
if err := database . C . Where ( reaction ) . First ( & reaction ) . Error ; err != nil {
if errors . Is ( err , gorm . ErrRecordNotFound ) {
2024-07-23 08:12:19 +00:00
if op . Author . ID != user . ID {
err = NotifyPosterAccount (
op . Author ,
"Post got reacted" ,
fmt . Sprintf ( "%s (%s) reacted your post a %s." , user . Nick , user . Name , reaction . Symbol ) ,
lo . ToPtr ( fmt . Sprintf ( "%s reacted you" , user . Nick ) ) ,
)
if err != nil {
log . Error ( ) . Err ( err ) . Msg ( "An error occurred when notifying user..." )
2024-06-08 04:33:10 +00:00
}
}
2024-07-23 08:12:19 +00:00
err = database . C . Save ( & reaction ) . Error
2024-07-26 14:21:31 +00:00
if err == nil && reaction . Attitude != models . AttitudeNeutral {
2024-07-23 08:12:19 +00:00
_ = ModifyPosterVoteCount ( op . Author , reaction . Attitude == models . AttitudePositive , 1 )
2024-07-26 13:09:53 +00:00
if reaction . Attitude == models . AttitudePositive {
op . TotalUpvote ++
} else {
op . TotalDownvote ++
}
database . C . Save ( & op )
2024-07-23 08:12:19 +00:00
}
return true , reaction , err
2024-03-03 04:17:18 +00:00
} else {
return true , reaction , err
}
2024-02-02 16:50:23 +00:00
} else {
2024-07-23 08:12:19 +00:00
err = database . C . Delete ( & reaction ) . Error
2024-07-26 14:21:31 +00:00
if err == nil && reaction . Attitude != models . AttitudeNeutral {
2024-07-23 08:12:19 +00:00
_ = ModifyPosterVoteCount ( op . Author , reaction . Attitude == models . AttitudePositive , - 1 )
2024-07-26 13:09:53 +00:00
if reaction . Attitude == models . AttitudePositive {
op . TotalUpvote --
} else {
op . TotalDownvote --
}
database . C . Save ( & op )
2024-07-23 08:12:19 +00:00
}
return false , reaction , err
2024-02-02 16:50:23 +00:00
}
}
2024-07-25 14:58:47 +00:00
func PinPost ( post models . Post ) ( bool , error ) {
if post . PinnedAt != nil {
post . PinnedAt = nil
} else {
post . PinnedAt = lo . ToPtr ( time . Now ( ) )
}
if err := database . C . Save ( & post ) . Error ; err != nil {
return post . PinnedAt != nil , err
}
return post . PinnedAt != nil , nil
}