package services import ( "errors" "fmt" "time" "code.smartsheep.studio/hydrogen/identity/pkg/grpc/proto" "code.smartsheep.studio/hydrogen/interactive/pkg/database" "code.smartsheep.studio/hydrogen/interactive/pkg/models" "github.com/rs/zerolog/log" "github.com/samber/lo" "github.com/spf13/viper" "gorm.io/gorm" ) type PostTypeContext struct { Tx *gorm.DB TableName string ColumnName string CanReply bool CanRepost bool } func (v *PostTypeContext) FilterWithCategory(alias string) *PostTypeContext { name := v.ColumnName v.Tx.Joins(fmt.Sprintf("JOIN %s_categories ON %s.id = %s_categories.%s_id", name, v.TableName, name, name)). Joins(fmt.Sprintf("JOIN %s_categories ON %s_categories.id = %s_categories.category_id", name, name, name)). Where(name+"_categories.alias = ?", alias) return v } func (v *PostTypeContext) FilterWithTag(alias string) *PostTypeContext { name := v.ColumnName v.Tx.Joins(fmt.Sprintf("JOIN %s_tags ON %s.id = %s_tags.%s_id", name, v.TableName, name, name)). Joins(fmt.Sprintf("JOIN %s_tags ON %s_tags.id = %s_tags.category_id", name, name, name)). Where(name+"_tags.alias = ?", alias) return v } func (v *PostTypeContext) FilterPublishedAt(date time.Time) *PostTypeContext { v.Tx.Where("published_at <= ? AND published_at IS NULL", date) return v } func (v *PostTypeContext) FilterRealm(id uint) *PostTypeContext { if id > 0 { v.Tx = v.Tx.Where("realm_id = ?", id) } else { v.Tx = v.Tx.Where("realm_id IS NULL") } return v } func (v *PostTypeContext) FilterAuthor(id uint) *PostTypeContext { v.Tx = v.Tx.Where("author_id = ?", id) return v } func (v *PostTypeContext) FilterReply(condition bool) *PostTypeContext { if condition { v.Tx = v.Tx.Where("reply_id IS NOT NULL") } else { v.Tx = v.Tx.Where("reply_id IS NULL") } return v } func (v *PostTypeContext) SortCreatedAt(order string) *PostTypeContext { v.Tx.Order(fmt.Sprintf("created_at %s", order)) return v } func (v *PostTypeContext) GetViaAlias(alias string) (models.Feed, error) { var item models.Feed table := viper.GetString("database.prefix") + v.TableName userTable := viper.GetString("database.prefix") + "accounts" if err := v.Tx. Table(table). Select("*, ? as model_type", v.ColumnName). Joins(fmt.Sprintf("INNER JOIN %s AS author ON author_id = author.id", userTable)). Where("alias = ?", alias). First(&item).Error; err != nil { return item, err } var attachments []models.Attachment if err := database.C. Model(&models.Attachment{}). Where(v.ColumnName+"_id = ?", item.ID). Scan(&attachments).Error; err != nil { return item, err } else { item.Attachments = attachments } return item, nil } func (v *PostTypeContext) Get(id uint, noComments ...bool) (models.Feed, error) { var item models.Feed table := viper.GetString("database.prefix") + v.TableName userTable := viper.GetString("database.prefix") + "accounts" if err := v.Tx. Table(table). Select("*, ? as model_type", v.ColumnName). Joins(fmt.Sprintf("INNER JOIN %s AS author ON author_id = author.id", userTable)). Where("id = ?", id).First(&item).Error; err != nil { return item, err } var attachments []models.Attachment if err := database.C. Model(&models.Attachment{}). Where(v.ColumnName+"_id = ?", id). Scan(&attachments).Error; err != nil { return item, err } else { item.Attachments = attachments } return item, nil } func (v *PostTypeContext) Count() (int64, error) { var count int64 table := viper.GetString("database.prefix") + v.TableName if err := v.Tx.Table(table).Count(&count).Error; err != nil { return count, err } return count, nil } func (v *PostTypeContext) CountReactions(id uint) (map[string]int64, error) { var reactions []struct { Symbol string Count int64 } if err := database.C.Model(&models.Reaction{}). Select("symbol, COUNT(id) as count"). Where(v.ColumnName+"_id = ?", id). Group("symbol"). Scan(&reactions).Error; err != nil { return map[string]int64{}, err } return lo.SliceToMap(reactions, func(item struct { Symbol string Count int64 }, ) (string, int64) { return item.Symbol, item.Count }), nil } func (v *PostTypeContext) List(take int, offset int, noReact ...bool) ([]*models.Feed, error) { if take > 20 { take = 20 } var items []*models.Feed table := viper.GetString("database.prefix") + v.TableName if err := v.Tx. Table(table). Select("*, ? as model_type", v.ColumnName). Limit(take).Offset(offset).Find(&items).Error; err != nil { return items, err } idx := lo.Map(items, func(item *models.Feed, index int) uint { return item.ID }) if len(noReact) <= 0 || !noReact[0] { var reactions []struct { PostID uint Symbol string Count int64 } if err := database.C.Model(&models.Reaction{}). Select(v.ColumnName+"_id as post_id, symbol, COUNT(id) as count"). Where(v.ColumnName+"_id IN (?)", idx). Group("post_id, symbol"). Scan(&reactions).Error; err != nil { return items, err } itemMap := lo.SliceToMap(items, func(item *models.Feed) (uint, *models.Feed) { return item.ID, item }) list := map[uint]map[string]int64{} for _, info := range reactions { if _, ok := list[info.PostID]; !ok { list[info.PostID] = make(map[string]int64) } list[info.PostID][info.Symbol] = info.Count } for k, v := range list { if post, ok := itemMap[k]; ok { post.ReactionList = v } } } { var attachments []struct { models.Attachment PostID uint `json:"post_id"` } itemMap := lo.SliceToMap(items, func(item *models.Feed) (uint, *models.Feed) { return item.ID, item }) idx := lo.Map(items, func(item *models.Feed, index int) uint { return item.ID }) if err := database.C. Model(&models.Attachment{}). Select(v.ColumnName+"_id as post_id, *"). Where(v.ColumnName+"_id IN (?)", idx). Scan(&attachments).Error; err != nil { return items, err } list := map[uint][]models.Attachment{} for _, info := range attachments { list[info.PostID] = append(list[info.PostID], info.Attachment) } for k, v := range list { if post, ok := itemMap[k]; ok { post.Attachments = v } } } return items, nil } func MapCategoriesAndTags[T models.PostInterface](item T) (T, error) { var err error categories := item.GetCategories() for idx, category := range categories { categories[idx], err = GetCategory(category.Alias) if err != nil { return item, err } } item.SetCategories(categories) tags := item.GetHashtags() for idx, tag := range tags { tags[idx], err = GetTagOrCreate(tag.Alias, tag.Name) if err != nil { return item, err } } item.SetHashtags(tags) return item, nil } func NewPost[T models.PostInterface](item T) (T, error) { item, err := MapCategoriesAndTags(item) if err != nil { return item, err } if item.GetRealm() != nil { if item.GetRealm().RealmType != models.RealmTypePublic { var member models.RealmMember if err := database.C.Where(&models.RealmMember{ RealmID: item.GetRealm().ID, AccountID: item.GetAuthor().ID, }).First(&member).Error; err != nil { return item, fmt.Errorf("you aren't a part of that realm") } } } if err := database.C.Save(&item).Error; err != nil { return item, err } if item.GetReplyTo() != nil { go func() { var op models.Moment if err := database.C.Where("id = ?", item.GetReplyTo()).Preload("Author").First(&op).Error; err == nil { if op.Author.ID != item.GetAuthor().ID { postUrl := fmt.Sprintf("https://%s/posts/%d", viper.GetString("domain"), item.GetID()) err := NotifyAccount( op.Author, fmt.Sprintf("%s replied you", item.GetAuthor().Name), fmt.Sprintf("%s replied your post. Check it out!", item.GetAuthor().Name), &proto.NotifyLink{Label: "Related post", Url: postUrl}, ) if err != nil { log.Error().Err(err).Msg("An error occurred when notifying user...") } } } }() } var subscribers []models.AccountMembership if err := database.C.Where(&models.AccountMembership{ FollowingID: item.GetAuthor().ID, }).Preload("Follower").Find(&subscribers).Error; err == nil && len(subscribers) > 0 { go func() { accounts := lo.Map(subscribers, func(item models.AccountMembership, index int) models.Account { return item.Follower }) for _, account := range accounts { postUrl := fmt.Sprintf("https://%s/posts/%d", viper.GetString("domain"), item.GetID()) err := NotifyAccount( account, fmt.Sprintf("%s just posted a post", item.GetAuthor().Name), "Account you followed post a brand new post. Check it out!", &proto.NotifyLink{Label: "Related post", Url: postUrl}, ) if err != nil { log.Error().Err(err).Msg("An error occurred when notifying user...") } } }() } return item, nil } func EditPost[T models.PostInterface](item T) (T, error) { item, err := MapCategoriesAndTags(item) if err != nil { return item, err } err = database.C.Save(&item).Error return item, err } func DeletePost[T models.PostInterface](item T) error { return database.C.Delete(&item).Error } func (v *PostTypeContext) React(reaction models.Reaction) (bool, models.Reaction, error) { if err := database.C.Where(reaction).First(&reaction).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return true, reaction, database.C.Save(&reaction).Error } else { return true, reaction, err } } else { return false, reaction, database.C.Delete(&reaction).Error } }