🗑️ Clean up code

This commit is contained in:
LittleSheep 2024-05-15 19:45:49 +08:00
parent da557fbe60
commit 4709760edc
25 changed files with 361 additions and 1349 deletions

12
.idea/dataSources.xml Normal file
View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DataSourceManagerImpl" format="xml" multifile-model="true">
<data-source source="LOCAL" name="hy_interactive@localhost" uuid="a2f70c83-03f8-4240-bb8b-ac697502cfe2">
<driver-ref>postgresql</driver-ref>
<synchronize>true</synchronize>
<jdbc-driver>org.postgresql.Driver</jdbc-driver>
<jdbc-url>jdbc:postgresql://localhost:5432/hy_interactive</jdbc-url>
<working-dir>$ProjectFileDir$</working-dir>
</data-source>
</component>
</project>

View File

@ -5,23 +5,19 @@ import (
"gorm.io/gorm" "gorm.io/gorm"
) )
var DatabaseAutoActionRange = []any{ var AutoMaintainRange = []any{
&models.Account{}, &models.Account{},
&models.Realm{}, &models.Realm{},
&models.Category{}, &models.Category{},
&models.Tag{}, &models.Tag{},
&models.Moment{}, &models.Post{},
&models.Article{},
&models.Comment{},
&models.Reaction{}, &models.Reaction{},
&models.Attachment{}, &models.Attachment{},
} }
func RunMigration(source *gorm.DB) error { func RunMigration(source *gorm.DB) error {
if err := source.AutoMigrate( if err := source.AutoMigrate(
append([]any{ AutoMaintainRange...,
&models.AccountMembership{},
}, DatabaseAutoActionRange...)...,
); err != nil { ); err != nil {
return err return err
} }

View File

@ -1,7 +1,5 @@
package models package models
import "time"
// Account profiles basically fetched from Hydrogen.Passport // Account profiles basically fetched from Hydrogen.Passport
// But cache at here for better usage // But cache at here for better usage
// At the same time this model can make relations between local models // At the same time this model can make relations between local models
@ -15,19 +13,8 @@ type Account struct {
Description string `json:"description"` Description string `json:"description"`
EmailAddress string `json:"email_address"` EmailAddress string `json:"email_address"`
PowerLevel int `json:"power_level"` PowerLevel int `json:"power_level"`
Moments []Moment `json:"moments" gorm:"foreignKey:AuthorID"` Posts []Post `json:"posts" gorm:"foreignKey:AuthorID"`
Articles []Article `json:"articles" gorm:"foreignKey:AuthorID"`
Attachments []Attachment `json:"attachments" gorm:"foreignKey:AuthorID"` Attachments []Attachment `json:"attachments" gorm:"foreignKey:AuthorID"`
Reactions []Reaction `json:"reactions"` Reactions []Reaction `json:"reactions"`
ExternalID uint `json:"external_id"` ExternalID uint `json:"external_id"`
} }
type AccountMembership struct {
ID uint `json:"id" gorm:"primaryKey"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
Follower Account `json:"follower"`
Following Account `json:"following"`
FollowerID uint
FollowingID uint
}

View File

@ -1,41 +0,0 @@
package models
type Article struct {
PostBase
Title string `json:"title"`
Hashtags []Tag `json:"tags" gorm:"many2many:article_tags"`
Categories []Category `json:"categories" gorm:"many2many:article_categories"`
Reactions []Reaction `json:"reactions"`
Attachments []Attachment `json:"attachments"`
Description string `json:"description"`
Content string `json:"content"`
RealmID *uint `json:"realm_id"`
Realm *Realm `json:"realm"`
Comments []Comment `json:"comments" gorm:"foreignKey:ArticleID"`
}
func (p *Article) GetReplyTo() PostInterface {
return nil
}
func (p *Article) GetRepostTo() PostInterface {
return nil
}
func (p *Article) GetHashtags() []Tag {
return p.Hashtags
}
func (p *Article) GetCategories() []Category {
return p.Categories
}
func (p *Article) SetHashtags(tags []Tag) {
p.Hashtags = tags
}
func (p *Article) SetCategories(categories []Category) {
p.Categories = categories
}

View File

@ -25,12 +25,10 @@ type Attachment struct {
Mimetype string `json:"mimetype"` Mimetype string `json:"mimetype"`
Hashcode string `json:"hashcode"` Hashcode string `json:"hashcode"`
Type AttachmentType `json:"type"` Type AttachmentType `json:"type"`
ExternalUrl string `json:"external_url"`
Author Account `json:"author"` Author Account `json:"author"`
ArticleID *uint `json:"article_id"`
MomentID *uint `json:"moment_id"`
CommentID *uint `json:"comment_id"`
AuthorID uint `json:"author_id"` AuthorID uint `json:"author_id"`
PostID *uint `json:"post_id"`
} }
func (v Attachment) GetStoragePath() string { func (v Attachment) GetStoragePath() string {

View File

@ -6,9 +6,7 @@ type Tag struct {
Alias string `json:"alias" gorm:"uniqueIndex" validate:"lowercase,alphanum,min=4,max=24"` Alias string `json:"alias" gorm:"uniqueIndex" validate:"lowercase,alphanum,min=4,max=24"`
Name string `json:"name"` Name string `json:"name"`
Description string `json:"description"` Description string `json:"description"`
Articles []Article `json:"articles" gorm:"many2many:article_tags"` Posts []Post `json:"posts" gorm:"many2many:post_tags"`
Moments []Moment `json:"moments" gorm:"many2many:moment_tags"`
Comments []Comment `json:"comments" gorm:"many2many:comment_tags"`
} }
type Category struct { type Category struct {
@ -17,7 +15,5 @@ type Category struct {
Alias string `json:"alias" gorm:"uniqueIndex" validate:"lowercase,alphanum,min=4,max=24"` Alias string `json:"alias" gorm:"uniqueIndex" validate:"lowercase,alphanum,min=4,max=24"`
Name string `json:"name"` Name string `json:"name"`
Description string `json:"description"` Description string `json:"description"`
Articles []Article `json:"articles" gorm:"many2many:article_categories"` Posts []Post `json:"posts" gorm:"many2many:post_categories"`
Moments []Moment `json:"moments" gorm:"many2many:moment_categories"`
Comments []Comment `json:"comments" gorm:"many2many:comment_categories"`
} }

View File

@ -1,38 +0,0 @@
package models
type Comment struct {
PostBase
Content string `json:"content"`
Hashtags []Tag `json:"tags" gorm:"many2many:comment_tags"`
Categories []Category `json:"categories" gorm:"many2many:comment_categories"`
Reactions []Reaction `json:"reactions"`
Attachments []Attachment `json:"attachments"`
ReplyID *uint `json:"reply_id"`
ReplyTo *Comment `json:"reply_to" gorm:"foreignKey:ReplyID"`
ArticleID *uint `json:"article_id"`
MomentID *uint `json:"moment_id"`
Article *Article `json:"article"`
Moment *Moment `json:"moment"`
}
func (p *Comment) GetReplyTo() PostInterface {
return p.ReplyTo
}
func (p *Comment) GetHashtags() []Tag {
return p.Hashtags
}
func (p *Comment) GetCategories() []Category {
return p.Categories
}
func (p *Comment) SetHashtags(tags []Tag) {
p.Hashtags = tags
}
func (p *Comment) SetCategories(categories []Category) {
p.Categories = categories
}

View File

@ -1,22 +0,0 @@
package models
type Feed struct {
BaseModel
Alias string `json:"alias"`
Title string `json:"title"`
Description string `json:"description"`
Content string `json:"content"`
ModelType string `json:"model_type"`
CommentCount int64 `json:"comment_count"`
ReactionCount int64 `json:"reaction_count"`
AuthorID uint `json:"author_id"`
RealmID *uint `json:"realm_id"`
Author Account `json:"author" gorm:"embedded"`
Attachments []Attachment `json:"attachments" gorm:"-"`
ReactionList map[string]int64 `json:"reaction_list" gorm:"-"`
}

View File

@ -1,41 +0,0 @@
package models
type Moment struct {
PostBase
Content string `json:"content"`
Hashtags []Tag `json:"tags" gorm:"many2many:moment_tags"`
Categories []Category `json:"categories" gorm:"many2many:moment_categories"`
Reactions []Reaction `json:"reactions"`
Attachments []Attachment `json:"attachments"`
RealmID *uint `json:"realm_id"`
RepostID *uint `json:"repost_id"`
Realm *Realm `json:"realm"`
RepostTo *Moment `json:"repost_to" gorm:"foreignKey:RepostID"`
Comments []Comment `json:"comments" gorm:"foreignKey:MomentID"`
}
func (p *Moment) GetRepostTo() PostInterface {
return p.RepostTo
}
func (p *Moment) GetRealm() *Realm {
return p.Realm
}
func (p *Moment) GetHashtags() []Tag {
return p.Hashtags
}
func (p *Moment) GetCategories() []Category {
return p.Categories
}
func (p *Moment) SetHashtags(tags []Tag) {
p.Hashtags = tags
}
func (p *Moment) SetCategories(categories []Category) {
p.Categories = categories
}

View File

@ -12,53 +12,30 @@ type PostReactInfo struct {
RepostCount int64 `json:"repost_count"` RepostCount int64 `json:"repost_count"`
} }
type PostBase struct { type Post struct {
BaseModel BaseModel
Alias string `json:"alias" gorm:"uniqueIndex"` Alias string `json:"alias" gorm:"uniqueIndex"`
Content string `json:"content"`
Tags []Tag `json:"tags" gorm:"many2many:post_tags"`
Categories []Category `json:"categories" gorm:"many2many:post_categories"`
Reactions []Reaction `json:"reactions"`
Attachments []Attachment `json:"attachments"`
Replies []Post `json:"replies" gorm:"foreignKey:ReplyID"`
ReplyID *uint `json:"reply_id"`
RepostID *uint `json:"repost_id"`
RealmID *uint `json:"realm_id"`
ReplyTo *Post `json:"reply_to" gorm:"foreignKey:ReplyID"`
RepostTo *Post `json:"repost_to" gorm:"foreignKey:RepostID"`
Realm *Realm `json:"realm"`
PublishedAt *time.Time `json:"published_at"` PublishedAt *time.Time `json:"published_at"`
AuthorID uint `json:"author_id"` AuthorID uint `json:"author_id"`
Author Account `json:"author"` Author Account `json:"author"`
// Dynamic Calculated Values // Dynamic Calculated Values
ReplyCount int64 `json:"comment_count"`
ReactionCount int64 `json:"reaction_count"`
ReactionList map[string]int64 `json:"reaction_list" gorm:"-"` ReactionList map[string]int64 `json:"reaction_list" gorm:"-"`
} }
func (p *PostBase) GetID() uint {
return p.ID
}
func (p *PostBase) GetReplyTo() PostInterface {
return nil
}
func (p *PostBase) GetRepostTo() PostInterface {
return nil
}
func (p *PostBase) GetAuthor() Account {
return p.Author
}
func (p *PostBase) GetRealm() *Realm {
return nil
}
func (p *PostBase) SetReactionList(list map[string]int64) {
p.ReactionList = list
}
type PostInterface interface {
GetID() uint
GetHashtags() []Tag
GetCategories() []Category
GetReplyTo() PostInterface
GetRepostTo() PostInterface
GetAuthor() Account
GetRealm() *Realm
SetHashtags([]Tag)
SetCategories([]Category)
SetReactionList(map[string]int64)
}

View File

@ -20,8 +20,6 @@ type Reaction struct {
Symbol string `json:"symbol"` Symbol string `json:"symbol"`
Attitude ReactionAttitude `json:"attitude"` Attitude ReactionAttitude `json:"attitude"`
ArticleID *uint `json:"article_id"` PostID *uint `json:"post_id"`
MomentID *uint `json:"moment_id"`
CommentID *uint `json:"comment_id"`
AccountID uint `json:"account_id"` AccountID uint `json:"account_id"`
} }

View File

@ -8,8 +8,7 @@ type Realm struct {
Alias string `json:"alias"` Alias string `json:"alias"`
Name string `json:"name"` Name string `json:"name"`
Description string `json:"description"` Description string `json:"description"`
Articles []Article `json:"article"` Posts []Post `json:"posts"`
Moments []Moment `json:"moments"`
IsPublic bool `json:"is_public"` IsPublic bool `json:"is_public"`
IsCommunity bool `json:"is_community"` IsCommunity bool `json:"is_community"`
ExternalID uint `json:"external_id"` ExternalID uint `json:"external_id"`

View File

@ -1,141 +0,0 @@
package server
import (
"fmt"
"strings"
"time"
"git.solsynth.dev/hydrogen/interactive/pkg/database"
"git.solsynth.dev/hydrogen/interactive/pkg/models"
"git.solsynth.dev/hydrogen/interactive/pkg/services"
"github.com/gofiber/fiber/v2"
"github.com/google/uuid"
)
func contextArticle() *services.PostTypeContext {
return &services.PostTypeContext{
Tx: database.C,
TableName: "articles",
ColumnName: "article",
CanReply: false,
CanRepost: false,
}
}
func createArticle(c *fiber.Ctx) error {
user := c.Locals("principal").(models.Account)
var data struct {
Alias string `json:"alias" form:"alias"`
Title string `json:"title" form:"title" validate:"required"`
Description string `json:"description" form:"description"`
Content string `json:"content" form:"content" validate:"required"`
Hashtags []models.Tag `json:"hashtags" form:"hashtags"`
Categories []models.Category `json:"categories" form:"categories"`
Attachments []models.Attachment `json:"attachments" form:"attachments"`
PublishedAt *time.Time `json:"published_at" form:"published_at"`
RealmAlias string `json:"realm" form:"realm"`
}
if err := BindAndValidate(c, &data); err != nil {
return err
} else if len(data.Alias) == 0 {
data.Alias = strings.ReplaceAll(uuid.NewString(), "-", "")
}
item := &models.Article{
PostBase: models.PostBase{
Alias: data.Alias,
PublishedAt: data.PublishedAt,
AuthorID: user.ID,
},
Hashtags: data.Hashtags,
Categories: data.Categories,
Attachments: data.Attachments,
Title: data.Title,
Description: data.Description,
Content: data.Content,
}
if len(data.RealmAlias) > 0 {
if realm, err := services.GetRealmWithAlias(data.RealmAlias); err != nil {
return fiber.NewError(fiber.StatusBadRequest, err.Error())
} else if _, err := services.GetRealmMember(realm.ExternalID, user.ExternalID); err != nil {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("you aren't a part of related realm: %v", err))
} else {
item.RealmID = &realm.ID
}
}
if item, err := services.NewPost(item); err != nil {
return fiber.NewError(fiber.StatusBadRequest, err.Error())
} else {
return c.JSON(item)
}
}
func editArticle(c *fiber.Ctx) error {
user := c.Locals("principal").(models.Account)
id, _ := c.ParamsInt("articleId", 0)
var data struct {
Alias string `json:"alias" form:"alias" validate:"required"`
Title string `json:"title" form:"title" validate:"required"`
Description string `json:"description" form:"description"`
Content string `json:"content" form:"content" validate:"required"`
PublishedAt *time.Time `json:"published_at" form:"published_at"`
Hashtags []models.Tag `json:"hashtags" form:"hashtags"`
Categories []models.Category `json:"categories" form:"categories"`
Attachments []models.Attachment `json:"attachments" form:"attachments"`
}
if err := BindAndValidate(c, &data); err != nil {
return err
}
var item *models.Article
if err := database.C.Where(models.Article{
PostBase: models.PostBase{
BaseModel: models.BaseModel{ID: uint(id)},
AuthorID: user.ID,
},
}).First(&item).Error; err != nil {
return fiber.NewError(fiber.StatusNotFound, err.Error())
}
item.Alias = data.Alias
item.Title = data.Title
item.Description = data.Description
item.Content = data.Content
item.PublishedAt = data.PublishedAt
item.Hashtags = data.Hashtags
item.Categories = data.Categories
item.Attachments = data.Attachments
if item, err := services.EditPost(item); err != nil {
return fiber.NewError(fiber.StatusBadRequest, err.Error())
} else {
return c.JSON(item)
}
}
func deleteArticle(c *fiber.Ctx) error {
user := c.Locals("principal").(models.Account)
id, _ := c.ParamsInt("articleId", 0)
var item *models.Article
if err := database.C.Where(models.Article{
PostBase: models.PostBase{
BaseModel: models.BaseModel{ID: uint(id)},
AuthorID: user.ID,
},
}).First(&item).Error; err != nil {
return fiber.NewError(fiber.StatusNotFound, err.Error())
}
if err := services.DeletePost(item); err != nil {
return fiber.NewError(fiber.StatusBadRequest, err.Error())
}
return c.SendStatus(fiber.StatusOK)
}

View File

@ -1,196 +0,0 @@
package server
import (
"fmt"
"github.com/spf13/viper"
"strings"
"time"
"git.solsynth.dev/hydrogen/interactive/pkg/database"
"git.solsynth.dev/hydrogen/interactive/pkg/models"
"git.solsynth.dev/hydrogen/interactive/pkg/services"
"github.com/gofiber/fiber/v2"
"github.com/google/uuid"
)
func contextComment() *services.PostTypeContext {
return &services.PostTypeContext{
Tx: database.C,
TableName: "comments",
ColumnName: "comment",
CanReply: false,
CanRepost: true,
}
}
func listComment(c *fiber.Ctx) error {
take := c.QueryInt("take", 0)
offset := c.QueryInt("offset", 0)
alias := c.Params("postId")
mx := c.Locals(postContextKey).(*services.PostTypeContext).
FilterPublishedAt(time.Now())
item, err := mx.GetViaAlias(alias)
if err != nil {
return fiber.NewError(fiber.StatusNotFound, err.Error())
}
data, err := mx.ListComment(item.ID, take, offset)
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
}
count, err := mx.CountComment(item.ID)
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
}
return c.JSON(fiber.Map{
"count": count,
"data": data,
})
}
func createComment(c *fiber.Ctx) error {
user := c.Locals("principal").(models.Account)
var data struct {
Content string `json:"content" form:"content" validate:"required"`
PublishedAt *time.Time `json:"published_at" form:"published_at"`
Hashtags []models.Tag `json:"hashtags" form:"hashtags"`
Categories []models.Category `json:"categories" form:"categories"`
Attachments []models.Attachment `json:"attachments" form:"attachments"`
ReplyTo uint `json:"reply_to" form:"reply_to"`
}
if err := BindAndValidate(c, &data); err != nil {
return err
}
item := &models.Comment{
PostBase: models.PostBase{
Alias: strings.ReplaceAll(uuid.NewString(), "-", ""),
PublishedAt: data.PublishedAt,
AuthorID: user.ID,
},
Hashtags: data.Hashtags,
Categories: data.Categories,
Attachments: data.Attachments,
Content: data.Content,
}
postType := c.Params("postType")
alias := c.Params("postId")
var err error
var res models.Feed
var columnName string
var tableName string
switch postType {
case "moments":
columnName = "moment"
tableName = viper.GetString("database.table_prefix") + "moments"
err = database.C.Model(&models.Moment{}).Where("alias = ?", alias).Select("id").First(&res).Error
case "articles":
columnName = "article"
tableName = viper.GetString("database.table_prefix") + "articles"
err = database.C.Model(&models.Article{}).Where("alias = ?", alias).Select("id").First(&res).Error
default:
return fiber.NewError(fiber.StatusBadRequest, "comment must belongs to a resource")
}
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("belongs to resource was not found: %v", err))
} else {
switch postType {
case "moments":
item.MomentID = &res.ID
case "articles":
item.ArticleID = &res.ID
}
}
var relatedCount int64
if data.ReplyTo > 0 {
if err := database.C.Where("id = ?", data.ReplyTo).
Model(&models.Comment{}).Count(&relatedCount).Error; err != nil {
return fiber.NewError(fiber.StatusBadRequest, err.Error())
} else if relatedCount <= 0 {
return fiber.NewError(fiber.StatusNotFound, "related post was not found")
} else {
item.ReplyID = &data.ReplyTo
}
}
item, err = services.NewPost(item)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, err.Error())
}
// Notify the original poster their post is commented by someone
go services.CommentNotify(item, res, columnName, tableName)
return c.JSON(item)
}
func editComment(c *fiber.Ctx) error {
user := c.Locals("principal").(models.Account)
id, _ := c.ParamsInt("commentId", 0)
var data struct {
Content string `json:"content" form:"content" validate:"required"`
PublishedAt *time.Time `json:"published_at" form:"published_at"`
Hashtags []models.Tag `json:"hashtags" form:"hashtags"`
Categories []models.Category `json:"categories" form:"categories"`
}
if err := BindAndValidate(c, &data); err != nil {
return err
}
var item *models.Comment
if err := database.C.Where(models.Comment{
PostBase: models.PostBase{
BaseModel: models.BaseModel{ID: uint(id)},
AuthorID: user.ID,
},
}).First(&item).Error; err != nil {
return fiber.NewError(fiber.StatusNotFound, err.Error())
}
item.Content = data.Content
item.PublishedAt = data.PublishedAt
item.Hashtags = data.Hashtags
item.Categories = data.Categories
if item, err := services.EditPost(item); err != nil {
return fiber.NewError(fiber.StatusBadRequest, err.Error())
} else {
return c.JSON(item)
}
}
func deleteComment(c *fiber.Ctx) error {
user := c.Locals("principal").(models.Account)
id, _ := c.ParamsInt("commentId", 0)
var item *models.Comment
if err := database.C.Where(models.Comment{
PostBase: models.PostBase{
BaseModel: models.BaseModel{ID: uint(id)},
AuthorID: user.ID,
},
}).First(&item).Error; err != nil {
return fiber.NewError(fiber.StatusNotFound, err.Error())
}
if err := services.DeletePost(item); err != nil {
return fiber.NewError(fiber.StatusBadRequest, err.Error())
}
return c.SendStatus(fiber.StatusOK)
}

View File

@ -1,200 +1,49 @@
package server package server
import ( import (
"fmt"
"git.solsynth.dev/hydrogen/interactive/pkg/services"
"strings"
"git.solsynth.dev/hydrogen/interactive/pkg/database" "git.solsynth.dev/hydrogen/interactive/pkg/database"
"git.solsynth.dev/hydrogen/interactive/pkg/models" "git.solsynth.dev/hydrogen/interactive/pkg/models"
"git.solsynth.dev/hydrogen/interactive/pkg/services"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
"github.com/samber/lo"
"github.com/spf13/viper"
)
const (
queryArticle = "id, created_at, updated_at, alias, title, NULL as content, description, realm_id, author_id, 'article' as model_type"
queryMoment = "id, created_at, updated_at, alias, NULL as title, content, NULL as description, realm_id, author_id, 'moment' as model_type"
) )
func listFeed(c *fiber.Ctx) error { func listFeed(c *fiber.Ctx) error {
take := c.QueryInt("take", 0) take := c.QueryInt("take", 0)
offset := c.QueryInt("offset", 0) offset := c.QueryInt("offset", 0)
realmAlias := c.Query("realm") realmId := c.QueryInt("realmId", 0)
if take > 20 { tx := database.C
take = 20 if realmId > 0 {
tx = services.FilterWithRealm(tx, uint(realmId))
} }
var whereConditions []string
if len(realmAlias) > 0 {
realm, err := services.GetRealmWithAlias(realmAlias)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("related realm was not found: %v", err))
}
whereConditions = append(whereConditions, fmt.Sprintf("feed.realm_id = %d", realm.ID))
}
var author models.Account
if len(c.Query("authorId")) > 0 { if len(c.Query("authorId")) > 0 {
var author models.Account
if err := database.C.Where(&models.Account{Name: c.Query("authorId")}).First(&author).Error; err != nil { if err := database.C.Where(&models.Account{Name: c.Query("authorId")}).First(&author).Error; err != nil {
return fiber.NewError(fiber.StatusNotFound, err.Error()) return fiber.NewError(fiber.StatusNotFound, err.Error())
} else {
whereConditions = append(whereConditions, fmt.Sprintf("feed.author_id = %d", author.ID))
} }
tx = tx.Where("author_id = ?", author.ID)
} }
var whereStatement string if len(c.Query("category")) > 0 {
if len(whereConditions) > 0 { tx = services.FilterPostWithCategory(tx, c.Query("category"))
whereStatement += "WHERE " + strings.Join(whereConditions, " AND ") }
if len(c.Query("tag")) > 0 {
tx = services.FilterPostWithTag(tx, c.Query("tag"))
} }
var result []*models.Feed count, err := services.CountPost(tx)
if err != nil {
userTable := viper.GetString("database.prefix") + "accounts"
commentTable := viper.GetString("database.prefix") + "comments"
reactionTable := viper.GetString("database.prefix") + "reactions"
database.C.Raw(
fmt.Sprintf(`SELECT feed.*, author.*,
COALESCE(comment_count, 0) AS comment_count,
COALESCE(reaction_count, 0) AS reaction_count
FROM (? UNION ALL ?) AS feed
INNER JOIN %s AS author ON author_id = author.id
LEFT JOIN (SELECT article_id, moment_id, COUNT(*) AS comment_count
FROM %s
GROUP BY article_id, moment_id) AS comments
ON (feed.model_type = 'article' AND feed.id = comments.article_id) OR
(feed.model_type = 'moment' AND feed.id = comments.moment_id)
LEFT JOIN (SELECT article_id, moment_id, COUNT(*) AS reaction_count
FROM %s
GROUP BY article_id, moment_id) AS reactions
ON (feed.model_type = 'article' AND feed.id = reactions.article_id) OR
(feed.model_type = 'moment' AND feed.id = reactions.moment_id)
%s ORDER BY feed.created_at desc LIMIT ? OFFSET ?`,
userTable,
commentTable,
reactionTable,
whereStatement,
),
database.C.Select(queryArticle).Model(&models.Article{}),
database.C.Select(queryMoment).Model(&models.Moment{}),
take,
offset,
).Scan(&result)
if !c.QueryBool("noReact", false) {
var reactions []struct {
PostID uint
Symbol string
Count int64
}
revertReaction := func(dataset string) error {
itemMap := lo.SliceToMap(lo.FilterMap(result, func(item *models.Feed, index int) (*models.Feed, bool) {
return item, item.ModelType == dataset
}), func(item *models.Feed) (uint, *models.Feed) {
return item.ID, item
})
idx := lo.Map(lo.Filter(result, func(item *models.Feed, index int) bool {
return item.ModelType == dataset
}), func(item *models.Feed, index int) uint {
return item.ID
})
if err := database.C.Model(&models.Reaction{}).
Select(dataset+"_id as post_id, symbol, COUNT(id) as count").
Where(dataset+"_id IN (?)", idx).
Group("post_id, symbol").
Scan(&reactions).Error; err != nil {
return fiber.NewError(fiber.StatusInternalServerError, err.Error()) return fiber.NewError(fiber.StatusInternalServerError, err.Error())
} }
list := map[uint]map[string]int64{} items, err := services.ListPost(tx, take, offset)
for _, info := range reactions { if err != nil {
if _, ok := list[info.PostID]; !ok { return fiber.NewError(fiber.StatusBadRequest, err.Error())
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
}
}
return nil
}
if err := revertReaction("article"); err != nil {
return err
}
if err := revertReaction("moment"); err != nil {
return err
}
}
if !c.QueryBool("noAttachment", false) {
revertAttachment := func(dataset string) error {
var attachments []struct {
models.Attachment
PostID uint `json:"post_id"`
}
itemMap := lo.SliceToMap(lo.FilterMap(result, func(item *models.Feed, index int) (*models.Feed, bool) {
return item, item.ModelType == dataset
}), func(item *models.Feed) (uint, *models.Feed) {
return item.ID, item
})
idx := lo.Map(lo.Filter(result, func(item *models.Feed, index int) bool {
return item.ModelType == dataset
}), func(item *models.Feed, index int) uint {
return item.ID
})
if err := database.C.
Model(&models.Attachment{}).
Select(dataset+"_id as post_id, *").
Where(dataset+"_id IN (?)", idx).
Scan(&attachments).Error; err != nil {
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
}
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 nil
}
if err := revertAttachment("article"); err != nil {
return err
}
if err := revertAttachment("moment"); err != nil {
return err
}
}
var count int64
database.C.Raw(`SELECT COUNT(*) FROM (? UNION ALL ?) as feed`,
database.C.Select(queryArticle).Model(&models.Article{}),
database.C.Select(queryMoment).Model(&models.Moment{}),
).Scan(&count)
return c.JSON(fiber.Map{ return c.JSON(fiber.Map{
"count": count, "count": count,
"data": result, "data": items,
}) })
} }

View File

@ -1,147 +0,0 @@
package server
import (
"fmt"
"strings"
"time"
"git.solsynth.dev/hydrogen/interactive/pkg/database"
"git.solsynth.dev/hydrogen/interactive/pkg/models"
"git.solsynth.dev/hydrogen/interactive/pkg/services"
"github.com/gofiber/fiber/v2"
"github.com/google/uuid"
)
func contextMoment() *services.PostTypeContext {
return &services.PostTypeContext{
Tx: database.C,
TableName: "moments",
ColumnName: "moment",
CanReply: false,
CanRepost: true,
}
}
func createMoment(c *fiber.Ctx) error {
user := c.Locals("principal").(models.Account)
var data struct {
Alias string `json:"alias" form:"alias"`
Content string `json:"content" form:"content" validate:"required,max=1024"`
Hashtags []models.Tag `json:"hashtags" form:"hashtags"`
Categories []models.Category `json:"categories" form:"categories"`
Attachments []models.Attachment `json:"attachments" form:"attachments"`
PublishedAt *time.Time `json:"published_at" form:"published_at"`
RealmAlias string `json:"realm" form:"realm"`
RepostTo uint `json:"repost_to" form:"repost_to"`
}
if err := BindAndValidate(c, &data); err != nil {
return err
} else if len(data.Alias) == 0 {
data.Alias = strings.ReplaceAll(uuid.NewString(), "-", "")
}
item := &models.Moment{
PostBase: models.PostBase{
Alias: data.Alias,
PublishedAt: data.PublishedAt,
AuthorID: user.ID,
},
Hashtags: data.Hashtags,
Categories: data.Categories,
Attachments: data.Attachments,
Content: data.Content,
}
var relatedCount int64
if data.RepostTo > 0 {
if err := database.C.Where("id = ?", data.RepostTo).
Model(&models.Moment{}).Count(&relatedCount).Error; err != nil {
return fiber.NewError(fiber.StatusBadRequest, err.Error())
} else if relatedCount <= 0 {
return fiber.NewError(fiber.StatusNotFound, "related post was not found")
} else {
item.RepostID = &data.RepostTo
}
}
if len(data.RealmAlias) > 0 {
if realm, err := services.GetRealmWithAlias(data.RealmAlias); err != nil {
return fiber.NewError(fiber.StatusBadRequest, err.Error())
} else if _, err := services.GetRealmMember(realm.ExternalID, user.ExternalID); err != nil {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("you aren't a part of related realm: %v", err))
} else {
item.RealmID = &realm.ID
}
}
item, err := services.NewPost(item)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, err.Error())
}
return c.JSON(item)
}
func editMoment(c *fiber.Ctx) error {
user := c.Locals("principal").(models.Account)
id, _ := c.ParamsInt("momentId", 0)
var data struct {
Alias string `json:"alias" form:"alias" validate:"required"`
Content string `json:"content" form:"content" validate:"required,max=1024"`
PublishedAt *time.Time `json:"published_at" form:"published_at"`
Hashtags []models.Tag `json:"hashtags" form:"hashtags"`
Categories []models.Category `json:"categories" form:"categories"`
Attachments []models.Attachment `json:"attachments" form:"attachments"`
}
if err := BindAndValidate(c, &data); err != nil {
return err
}
var item *models.Moment
if err := database.C.Where(models.Moment{
PostBase: models.PostBase{
BaseModel: models.BaseModel{ID: uint(id)},
AuthorID: user.ID,
},
}).First(&item).Error; err != nil {
return fiber.NewError(fiber.StatusNotFound, err.Error())
}
item.Alias = data.Alias
item.Content = data.Content
item.PublishedAt = data.PublishedAt
item.Hashtags = data.Hashtags
item.Categories = data.Categories
item.Attachments = data.Attachments
if item, err := services.EditPost(item); err != nil {
return fiber.NewError(fiber.StatusBadRequest, err.Error())
} else {
return c.JSON(item)
}
}
func deleteMoment(c *fiber.Ctx) error {
user := c.Locals("principal").(models.Account)
id, _ := c.ParamsInt("momentId", 0)
var item *models.Moment
if err := database.C.Where(models.Moment{
PostBase: models.PostBase{
BaseModel: models.BaseModel{ID: uint(id)},
AuthorID: user.ID,
},
}).First(&item).Error; err != nil {
return fiber.NewError(fiber.StatusNotFound, err.Error())
}
if err := services.DeletePost(item); err != nil {
return fiber.NewError(fiber.StatusBadRequest, err.Error())
}
return c.SendStatus(fiber.StatusOK)
}

View File

@ -2,6 +2,8 @@ package server
import ( import (
"fmt" "fmt"
"github.com/google/uuid"
"strings"
"time" "time"
"git.solsynth.dev/hydrogen/interactive/pkg/database" "git.solsynth.dev/hydrogen/interactive/pkg/database"
@ -11,38 +13,17 @@ import (
"github.com/samber/lo" "github.com/samber/lo"
) )
var postContextKey = "ptx"
func useDynamicContext(c *fiber.Ctx) error {
postType := c.Params("postType")
switch postType {
case "articles":
c.Locals(postContextKey, contextArticle())
case "moments":
c.Locals(postContextKey, contextMoment())
case "comments":
c.Locals(postContextKey, contextComment())
default:
return fiber.NewError(fiber.StatusBadRequest, "invalid dataset")
}
return c.Next()
}
func getPost(c *fiber.Ctx) error { func getPost(c *fiber.Ctx) error {
alias := c.Params("postId") alias := c.Params("postId")
mx := c.Locals(postContextKey).(*services.PostTypeContext). item, err := services.GetPostWithAlias(alias)
FilterPublishedAt(time.Now())
item, err := mx.GetViaAlias(alias)
if err != nil { if err != nil {
return fiber.NewError(fiber.StatusNotFound, err.Error()) return fiber.NewError(fiber.StatusNotFound, err.Error())
} }
item.CommentCount = mx.CountComments(item.ID) item.ReplyCount = services.CountPostReply(item.ID)
item.ReactionCount = mx.CountReactions(item.ID) item.ReactionCount = services.CountPostReactions(item.ID)
item.ReactionList, err = mx.ListReactions(item.ID) item.ReactionList, err = services.ListPostReactions(item.ID)
if err != nil { if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, err.Error()) return fiber.NewError(fiber.StatusInternalServerError, err.Error())
} }
@ -55,36 +36,32 @@ func listPost(c *fiber.Ctx) error {
offset := c.QueryInt("offset", 0) offset := c.QueryInt("offset", 0)
realmId := c.QueryInt("realmId", 0) realmId := c.QueryInt("realmId", 0)
mx := c.Locals(postContextKey).(*services.PostTypeContext). tx := database.C
FilterPublishedAt(time.Now()). if realmId > 0 {
FilterRealm(uint(realmId)). tx = services.FilterWithRealm(tx, uint(realmId))
SortCreatedAt("desc") }
var author models.Account
if len(c.Query("authorId")) > 0 { if len(c.Query("authorId")) > 0 {
var author models.Account
if err := database.C.Where(&models.Account{Name: c.Query("authorId")}).First(&author).Error; err != nil { if err := database.C.Where(&models.Account{Name: c.Query("authorId")}).First(&author).Error; err != nil {
return fiber.NewError(fiber.StatusNotFound, err.Error()) return fiber.NewError(fiber.StatusNotFound, err.Error())
} }
mx = mx.FilterAuthor(author.ID) tx = tx.Where("author_id = ?", author.ID)
} }
if len(c.Query("category")) > 0 { if len(c.Query("category")) > 0 {
mx = mx.FilterWithCategory(c.Query("category")) tx = services.FilterPostWithCategory(tx, c.Query("category"))
} }
if len(c.Query("tag")) > 0 { if len(c.Query("tag")) > 0 {
mx = mx.FilterWithTag(c.Query("tag")) tx = services.FilterPostWithTag(tx, c.Query("tag"))
} }
if !c.QueryBool("reply", true) { count, err := services.CountPost(tx)
mx = mx.FilterReply(true)
}
count, err := mx.Count()
if err != nil { if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, err.Error()) return fiber.NewError(fiber.StatusInternalServerError, err.Error())
} }
items, err := mx.List(take, offset) items, err := services.ListPost(tx, take, offset)
if err != nil { if err != nil {
return fiber.NewError(fiber.StatusBadRequest, err.Error()) return fiber.NewError(fiber.StatusBadRequest, err.Error())
} }
@ -95,6 +72,124 @@ func listPost(c *fiber.Ctx) error {
}) })
} }
func createPost(c *fiber.Ctx) error {
user := c.Locals("principal").(models.Account)
var data struct {
Alias string `json:"alias" form:"alias"`
Content string `json:"content" form:"content" validate:"required,max=4096"`
Tags []models.Tag `json:"tags" form:"tags"`
Categories []models.Category `json:"categories" form:"categories"`
Attachments []models.Attachment `json:"attachments" form:"attachments"`
PublishedAt *time.Time `json:"published_at" form:"published_at"`
RealmAlias string `json:"realm" form:"realm"`
RepostTo uint `json:"repost_to" form:"repost_to"`
}
if err := BindAndValidate(c, &data); err != nil {
return err
} else if len(data.Alias) == 0 {
data.Alias = strings.ReplaceAll(uuid.NewString(), "-", "")
}
item := models.Post{
Alias: data.Alias,
PublishedAt: data.PublishedAt,
AuthorID: user.ID,
Tags: data.Tags,
Categories: data.Categories,
Attachments: data.Attachments,
Content: data.Content,
}
var relatedCount int64
if data.RepostTo > 0 {
if err := database.C.Where("id = ?", data.RepostTo).
Model(&models.Post{}).Count(&relatedCount).Error; err != nil {
return fiber.NewError(fiber.StatusBadRequest, err.Error())
} else if relatedCount <= 0 {
return fiber.NewError(fiber.StatusNotFound, "related post was not found")
} else {
item.RepostID = &data.RepostTo
}
}
if len(data.RealmAlias) > 0 {
if realm, err := services.GetRealmWithAlias(data.RealmAlias); err != nil {
return fiber.NewError(fiber.StatusBadRequest, err.Error())
} else if _, err := services.GetRealmMember(realm.ExternalID, user.ExternalID); err != nil {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("you aren't a part of related realm: %v", err))
} else {
item.RealmID = &realm.ID
}
}
item, err := services.NewPost(user, item)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, err.Error())
}
return c.JSON(item)
}
func editPost(c *fiber.Ctx) error {
user := c.Locals("principal").(models.Account)
id, _ := c.ParamsInt("postId", 0)
var data struct {
Alias string `json:"alias" form:"alias" validate:"required"`
Content string `json:"content" form:"content" validate:"required,max=1024"`
PublishedAt *time.Time `json:"published_at" form:"published_at"`
Tags []models.Tag `json:"tags" form:"tags"`
Categories []models.Category `json:"categories" form:"categories"`
Attachments []models.Attachment `json:"attachments" form:"attachments"`
}
if err := BindAndValidate(c, &data); err != nil {
return err
}
var item models.Post
if err := database.C.Where(models.Post{
BaseModel: models.BaseModel{ID: uint(id)},
AuthorID: user.ID,
}).First(&item).Error; err != nil {
return fiber.NewError(fiber.StatusNotFound, err.Error())
}
item.Alias = data.Alias
item.Content = data.Content
item.PublishedAt = data.PublishedAt
item.Tags = data.Tags
item.Categories = data.Categories
item.Attachments = data.Attachments
if item, err := services.EditPost(item); err != nil {
return fiber.NewError(fiber.StatusBadRequest, err.Error())
} else {
return c.JSON(item)
}
}
func deletePost(c *fiber.Ctx) error {
user := c.Locals("principal").(models.Account)
id, _ := c.ParamsInt("postId", 0)
var item models.Post
if err := database.C.Where(models.Post{
BaseModel: models.BaseModel{ID: uint(id)},
AuthorID: user.ID,
}).First(&item).Error; err != nil {
return fiber.NewError(fiber.StatusNotFound, err.Error())
}
if err := services.DeletePost(item); err != nil {
return fiber.NewError(fiber.StatusBadRequest, err.Error())
}
return c.SendStatus(fiber.StatusOK)
}
func reactPost(c *fiber.Ctx) error { func reactPost(c *fiber.Ctx) error {
user := c.Locals("principal").(models.Account) user := c.Locals("principal").(models.Account)
@ -107,45 +202,23 @@ func reactPost(c *fiber.Ctx) error {
return err return err
} }
mx := c.Locals(postContextKey).(*services.PostTypeContext)
reaction := models.Reaction{ reaction := models.Reaction{
Symbol: data.Symbol, Symbol: data.Symbol,
Attitude: data.Attitude, Attitude: data.Attitude,
AccountID: user.ID, AccountID: user.ID,
} }
postType := c.Params("postType")
alias := c.Params("postId") alias := c.Params("postId")
var err error var res models.Post
var res models.Feed
switch postType { if err := database.C.Where("id = ?", alias).Select("id").First(&res).Error; err != nil {
case "moments": return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("unable to find post to react: %v", err))
err = database.C.Model(&models.Moment{}).Where("id = ?", alias).Select("id").First(&res).Error
case "articles":
err = database.C.Model(&models.Article{}).Where("id = ?", alias).Select("id").First(&res).Error
case "comments":
err = database.C.Model(&models.Comment{}).Where("id = ?", alias).Select("id").First(&res).Error
default:
return fiber.NewError(fiber.StatusBadRequest, "comment must belongs to a resource")
}
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("belongs to resource was not found: %v", err))
} else { } else {
switch postType { reaction.PostID = &res.ID
case "moments":
reaction.MomentID = &res.ID
case "articles":
reaction.ArticleID = &res.ID
case "comments":
reaction.CommentID = &res.ID
}
} }
if positive, reaction, err := mx.React(reaction); err != nil { if positive, reaction, err := services.ReactPost(reaction); err != nil {
return fiber.NewError(fiber.StatusBadRequest, err.Error()) return fiber.NewError(fiber.StatusBadRequest, err.Error())
} else { } else {
return c.Status(lo.Ternary(positive, fiber.StatusCreated, fiber.StatusNoContent)).JSON(reaction) return c.Status(lo.Ternary(positive, fiber.StatusCreated, fiber.StatusNoContent)).JSON(reaction)

52
pkg/server/replies_api.go Normal file
View File

@ -0,0 +1,52 @@
package server
import (
"fmt"
"git.solsynth.dev/hydrogen/interactive/pkg/database"
"git.solsynth.dev/hydrogen/interactive/pkg/models"
"git.solsynth.dev/hydrogen/interactive/pkg/services"
"github.com/gofiber/fiber/v2"
)
func listReplies(c *fiber.Ctx) error {
take := c.QueryInt("take", 0)
offset := c.QueryInt("offset", 0)
tx := database.C
var post models.Post
if err := database.C.Where("alias = ?", c.Params("postId")).First(&post).Error; err != nil {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("unable to find post: %v", err))
} else {
tx = services.FilterPostReply(tx, post.ID)
}
if len(c.Query("authorId")) > 0 {
var author models.Account
if err := database.C.Where(&models.Account{Name: c.Query("authorId")}).First(&author).Error; err != nil {
return fiber.NewError(fiber.StatusNotFound, err.Error())
}
tx = tx.Where("author_id = ?", author.ID)
}
if len(c.Query("category")) > 0 {
tx = services.FilterPostWithCategory(tx, c.Query("category"))
}
if len(c.Query("tag")) > 0 {
tx = services.FilterPostWithTag(tx, c.Query("tag"))
}
count, err := services.CountPost(tx)
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
}
items, err := services.ListPost(tx, take, offset)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, err.Error())
}
return c.JSON(fiber.Map{
"count": count,
"data": items,
})
}

View File

@ -62,8 +62,6 @@ func NewServer() {
{ {
api.Get("/users/me", authMiddleware, getUserinfo) api.Get("/users/me", authMiddleware, getUserinfo)
api.Get("/users/:accountId", getOthersInfo) api.Get("/users/:accountId", getOthersInfo)
api.Get("/users/:accountId/follow", authMiddleware, getAccountFollowed)
api.Post("/users/:accountId/follow", authMiddleware, doFollowAccount)
api.Get("/attachments/o/:fileId", readAttachment) api.Get("/attachments/o/:fileId", readAttachment)
api.Post("/attachments", authMiddleware, uploadAttachment) api.Post("/attachments", authMiddleware, uploadAttachment)
@ -71,33 +69,16 @@ func NewServer() {
api.Get("/feed", listFeed) api.Get("/feed", listFeed)
posts := api.Group("/p/:postType").Use(useDynamicContext).Name("Dataset Universal API") posts := api.Group("/posts").Name("Posts API")
{ {
posts.Get("/", listPost) posts.Get("/", listPost)
posts.Get("/:postId", getPost) posts.Get("/:postId", getPost)
posts.Post("/", authMiddleware, createPost)
posts.Post("/:postId/react", authMiddleware, reactPost) posts.Post("/:postId/react", authMiddleware, reactPost)
posts.Get("/:postId/comments", listComment) posts.Put("/:postId", authMiddleware, editPost)
posts.Post("/:postId/comments", authMiddleware, createComment) posts.Delete("/:postId", authMiddleware, deletePost)
}
moments := api.Group("/p/moments").Name("Moments API") posts.Get("/:postId/replies", listReplies)
{
moments.Post("/", authMiddleware, createMoment)
moments.Put("/:momentId", authMiddleware, editMoment)
moments.Delete("/:momentId", authMiddleware, deleteMoment)
}
articles := api.Group("/p/articles").Name("Articles API")
{
articles.Post("/", authMiddleware, createArticle)
articles.Put("/:articleId", authMiddleware, editArticle)
articles.Delete("/:articleId", authMiddleware, deleteArticle)
}
comments := api.Group("/p/comments").Name("Comments API")
{
comments.Put("/:commentId", authMiddleware, editComment)
comments.Delete("/:commentId", authMiddleware, deleteComment)
} }
api.Get("/categories", listCategories) api.Get("/categories", listCategories)

View File

@ -3,7 +3,6 @@ package server
import ( import (
"git.solsynth.dev/hydrogen/interactive/pkg/database" "git.solsynth.dev/hydrogen/interactive/pkg/database"
"git.solsynth.dev/hydrogen/interactive/pkg/models" "git.solsynth.dev/hydrogen/interactive/pkg/models"
"git.solsynth.dev/hydrogen/interactive/pkg/services"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
) )
@ -32,45 +31,3 @@ func getOthersInfo(c *fiber.Ctx) error {
return c.JSON(data) return c.JSON(data)
} }
func getAccountFollowed(c *fiber.Ctx) error {
user := c.Locals("principal").(models.Account)
accountId, _ := c.ParamsInt("accountId", 0)
var data models.Account
if err := database.C.
Where(&models.Account{BaseModel: models.BaseModel{ID: uint(accountId)}}).
First(&data).Error; err != nil {
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
}
_, status := services.GetAccountFollowed(user, data)
return c.JSON(fiber.Map{
"is_followed": status,
})
}
func doFollowAccount(c *fiber.Ctx) error {
user := c.Locals("principal").(models.Account)
id, _ := c.ParamsInt("accountId", 0)
var account models.Account
if err := database.C.Where(&models.Account{
BaseModel: models.BaseModel{ID: uint(id)},
}).First(&account).Error; err != nil {
return fiber.NewError(fiber.StatusNotFound, err.Error())
}
if _, ok := services.GetAccountFollowed(user, account); ok {
if err := services.UnfollowAccount(user.ID, account.ID); err != nil {
return fiber.NewError(fiber.StatusBadRequest, err.Error())
}
return c.SendStatus(fiber.StatusNoContent)
} else {
if err := services.FollowAccount(user.ID, account.ID); err != nil {
return fiber.NewError(fiber.StatusBadRequest, err.Error())
}
return c.SendStatus(fiber.StatusCreated)
}
}

View File

@ -11,30 +11,6 @@ import (
"time" "time"
) )
func FollowAccount(followerId, followingId uint) error {
relationship := models.AccountMembership{
FollowerID: followerId,
FollowingID: followingId,
}
return database.C.Create(&relationship).Error
}
func UnfollowAccount(followerId, followingId uint) error {
return database.C.Where(models.AccountMembership{
FollowerID: followerId,
FollowingID: followingId,
}).Delete(&models.AccountMembership{}).Error
}
func GetAccountFollowed(user models.Account, target models.Account) (models.AccountMembership, bool) {
var relationship models.AccountMembership
err := database.C.Model(&models.AccountMembership{}).
Where(&models.AccountMembership{FollowerID: user.ID, FollowingID: target.ID}).
First(&relationship).
Error
return relationship, err == nil
}
func GetAccountFriend(userId, relatedId uint, status int) (*proto.FriendshipResponse, error) { func GetAccountFriend(userId, relatedId uint, status int) (*proto.FriendshipResponse, error) {
var user models.Account var user models.Account
if err := database.C.Where("id = ?", userId).First(&user).Error; err != nil { if err := database.C.Where("id = ?", userId).First(&user).Error; err != nil {

View File

@ -11,7 +11,7 @@ func DoAutoDatabaseCleanup() {
log.Debug().Time("deadline", deadline).Msg("Now cleaning up entire database...") log.Debug().Time("deadline", deadline).Msg("Now cleaning up entire database...")
var count int64 var count int64
for _, model := range database.DatabaseAutoActionRange { for _, model := range database.AutoMaintainRange {
tx := database.C.Unscoped().Delete(model, "deleted_at >= ?", deadline) tx := database.C.Unscoped().Delete(model, "deleted_at >= ?", deadline)
if tx.Error != nil { if tx.Error != nil {
log.Error().Err(tx.Error).Msg("An error occurred when running auth context cleanup...") log.Error().Err(tx.Error).Msg("An error occurred when running auth context cleanup...")

View File

@ -1,107 +0,0 @@
package services
import (
"fmt"
"git.solsynth.dev/hydrogen/passport/pkg/grpc/proto"
"github.com/rs/zerolog/log"
"time"
"git.solsynth.dev/hydrogen/interactive/pkg/database"
"git.solsynth.dev/hydrogen/interactive/pkg/models"
"github.com/samber/lo"
"github.com/spf13/viper"
)
func (v *PostTypeContext) ListComment(id uint, take int, offset int, noReact ...bool) ([]*models.Feed, error) {
if take > 20 {
take = 20
}
var items []*models.Feed
table := viper.GetString("database.prefix") + "comments"
userTable := viper.GetString("database.prefix") + "accounts"
if err := v.Tx.
Table(table).
Select("*, ? as model_type", "comment").
Where(v.ColumnName+"_id = ?", id).
Joins(fmt.Sprintf("INNER JOIN %s as author ON author_id = author.id", userTable)).
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("comment_id as post_id, symbol, COUNT(id) as count").
Where("comment_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
}
}
}
return items, nil
}
func (v *PostTypeContext) CountComment(id uint) (int64, error) {
var count int64
if err := database.C.
Model(&models.Comment{}).
Where(v.ColumnName+"_id = ?", id).
Where("published_at <= ?", time.Now()).
Count(&count).Error; err != nil {
return count, err
}
return count, nil
}
func CommentNotify(this models.PostInterface, original models.Feed, columnName, tableName string) {
var op models.Feed
if err := database.C.
Where(columnName+"_id = ?", original.ID).
Preload("Author").
Table(tableName).
First(&op).Error; err == nil {
if op.Author.ID != this.GetAuthor().ID {
postUrl := fmt.Sprintf("https://%s/posts/%d", viper.GetString("domain"), this.GetID())
err := NotifyAccount(
op.Author,
fmt.Sprintf("%s commented you", this.GetAuthor().Name),
fmt.Sprintf("%s commented your post. Check it out!", this.GetAuthor().Name),
false,
&proto.NotifyLink{Label: "Related post", Url: postUrl},
)
if err != nil {
log.Error().Err(err).Msg("An error occurred when notifying user...")
}
}
}
}

View File

@ -1 +0,0 @@
package services

View File

@ -3,8 +3,6 @@ package services
import ( import (
"errors" "errors"
"fmt" "fmt"
"time"
"git.solsynth.dev/hydrogen/interactive/pkg/database" "git.solsynth.dev/hydrogen/interactive/pkg/database"
"git.solsynth.dev/hydrogen/interactive/pkg/models" "git.solsynth.dev/hydrogen/interactive/pkg/models"
"git.solsynth.dev/hydrogen/passport/pkg/grpc/proto" "git.solsynth.dev/hydrogen/passport/pkg/grpc/proto"
@ -12,131 +10,90 @@ import (
"github.com/samber/lo" "github.com/samber/lo"
"github.com/spf13/viper" "github.com/spf13/viper"
"gorm.io/gorm" "gorm.io/gorm"
"time"
) )
type PostTypeContext struct { func FilterPostWithCategory(tx *gorm.DB, alias string) *gorm.DB {
Tx *gorm.DB return tx.Joins("JOIN post_categories ON posts.id = post_categories.post_id").
Joins("JOIN post_categories ON post_categories.id = post_categories.category_id").
TableName string Where("post_categories.alias = ?", alias)
ColumnName string
CanReply bool
CanRepost bool
} }
func (v *PostTypeContext) FilterWithCategory(alias string) *PostTypeContext { func FilterPostWithTag(tx *gorm.DB, alias string) *gorm.DB {
name := v.ColumnName return tx.Joins("JOIN post_tags ON posts.id = post_tags.post_id").
v.Tx.Joins(fmt.Sprintf("JOIN %s_categories ON %s.id = %s_categories.%s_id", name, v.TableName, name, name)). Joins("JOIN post_tags ON post_tags.id = post_tags.category_id").
Joins(fmt.Sprintf("JOIN %s_categories ON %s_categories.id = %s_categories.category_id", name, name, name)). Where("post_tags.alias = ?", alias)
Where(name+"_categories.alias = ?", alias)
return v
} }
func (v *PostTypeContext) FilterWithTag(alias string) *PostTypeContext { func FilterWithRealm(tx *gorm.DB, id uint) *gorm.DB {
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 { if id > 0 {
v.Tx = v.Tx.Where("realm_id = ?", id) return tx.Where("realm_id = ?", id)
} else { } else {
v.Tx = v.Tx.Where("realm_id IS NULL") return tx.Where("realm_id IS NULL")
} }
return v
} }
func (v *PostTypeContext) FilterAuthor(id uint) *PostTypeContext { func FilterPostReply(tx *gorm.DB, replyTo ...uint) *gorm.DB {
v.Tx = v.Tx.Where("author_id = ?", id) if len(replyTo) > 0 && replyTo[0] > 0 {
return v return tx.Where("reply_id = ?", replyTo[0])
}
func (v *PostTypeContext) FilterReply(condition bool) *PostTypeContext {
if condition {
v.Tx = v.Tx.Where("reply_id IS NOT NULL")
} else { } else {
v.Tx = v.Tx.Where("reply_id IS NULL") return tx.Where("reply_id IS NULL")
} }
return v
} }
func (v *PostTypeContext) SortCreatedAt(order string) *PostTypeContext { func FilterPostWithPublishedAt(tx *gorm.DB, date time.Time) *gorm.DB {
v.Tx.Order(fmt.Sprintf("created_at %s", order)) return tx.Where("published_at <= ? AND published_at IS NULL", date)
return v
} }
func (v *PostTypeContext) GetViaAlias(alias string) (models.Feed, error) { func GetPostWithAlias(alias string, ignoreLimitation ...bool) (models.Post, error) {
var item models.Feed tx := database.C
table := viper.GetString("database.prefix") + v.TableName if len(ignoreLimitation) == 0 || !ignoreLimitation[0] {
userTable := viper.GetString("database.prefix") + "accounts" tx = FilterPostWithPublishedAt(tx, time.Now())
if err := v.Tx. }
Table(table).
Select("*, ? as model_type", v.ColumnName). var item models.Post
Joins(fmt.Sprintf("INNER JOIN %s AS author ON author_id = author.id", userTable)). if err := tx.
Where("alias = ?", alias). Where("alias = ?", alias).
Preload("Author").
Preload("Attachments").
First(&item).Error; err != nil { First(&item).Error; err != nil {
return item, err return item, err
} }
var attachments []models.Attachment return item, nil
if err := database.C. }
Model(&models.Attachment{}).
Where(v.ColumnName+"_id = ?", item.ID). func GetPost(id uint, ignoreLimitation ...bool) (models.Post, error) {
Scan(&attachments).Error; err != nil { tx := database.C
if len(ignoreLimitation) == 0 || !ignoreLimitation[0] {
tx = FilterPostWithPublishedAt(tx, time.Now())
}
var item models.Post
if err := tx.
Where("id = ?", id).
Preload("Author").
Preload("Attachments").
First(&item).Error; err != nil {
return item, err return item, err
} else {
item.Attachments = attachments
} }
return item, nil return item, nil
} }
func (v *PostTypeContext) Get(id uint, noComments ...bool) (models.Feed, error) { func CountPost(tx *gorm.DB) (int64, 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 var count int64
table := viper.GetString("database.prefix") + v.TableName if err := tx.Model(&models.Post{}).Count(&count).Error; err != nil {
if err := v.Tx.Table(table).Count(&count).Error; err != nil {
return count, err return count, err
} }
return count, nil return count, nil
} }
func (v *PostTypeContext) CountComments(id uint) int64 { func CountPostReply(id uint) int64 {
var count int64 var count int64
if err := database.C.Model(&models.Comment{}). if err := database.C.Model(&models.Post{}).
Where(v.ColumnName+"_id = ?", id). Where("reply_id = ?", id).
Count(&count).Error; err != nil { Count(&count).Error; err != nil {
return 0 return 0
} }
@ -144,10 +101,10 @@ func (v *PostTypeContext) CountComments(id uint) int64 {
return count return count
} }
func (v *PostTypeContext) CountReactions(id uint) int64 { func CountPostReactions(id uint) int64 {
var count int64 var count int64
if err := database.C.Model(&models.Reaction{}). if err := database.C.Model(&models.Reaction{}).
Where(v.ColumnName+"_id = ?", id). Where("post_id = ?", id).
Count(&count).Error; err != nil { Count(&count).Error; err != nil {
return 0 return 0
} }
@ -155,7 +112,7 @@ func (v *PostTypeContext) CountReactions(id uint) int64 {
return count return count
} }
func (v *PostTypeContext) ListReactions(id uint) (map[string]int64, error) { func ListPostReactions(id uint) (map[string]int64, error) {
var reactions []struct { var reactions []struct {
Symbol string Symbol string
Count int64 Count int64
@ -163,7 +120,7 @@ func (v *PostTypeContext) ListReactions(id uint) (map[string]int64, error) {
if err := database.C.Model(&models.Reaction{}). if err := database.C.Model(&models.Reaction{}).
Select("symbol, COUNT(id) as count"). Select("symbol, COUNT(id) as count").
Where(v.ColumnName+"_id = ?", id). Where("post_id = ?", id).
Group("symbol"). Group("symbol").
Scan(&reactions).Error; err != nil { Scan(&reactions).Error; err != nil {
return map[string]int64{}, err return map[string]int64{}, err
@ -178,21 +135,22 @@ func (v *PostTypeContext) ListReactions(id uint) (map[string]int64, error) {
}), nil }), nil
} }
func (v *PostTypeContext) List(take int, offset int, noReact ...bool) ([]*models.Feed, error) { func ListPost(tx *gorm.DB, take int, offset int, noReact ...bool) ([]models.Post, error) {
if take > 20 { if take > 20 {
take = 20 take = 20
} }
var items []*models.Feed var items []models.Post
table := viper.GetString("database.prefix") + v.TableName if err := tx.
if err := v.Tx. Limit(take).Offset(offset).
Table(table). Order("created_at DESC").
Select("*, ? as model_type", v.ColumnName). Preload("Author").
Limit(take).Offset(offset).Find(&items).Error; err != nil { Preload("Attachments").
Find(&items).Error; err != nil {
return items, err return items, err
} }
idx := lo.Map(items, func(item *models.Feed, index int) uint { idx := lo.Map(items, func(item models.Post, index int) uint {
return item.ID return item.ID
}) })
@ -204,14 +162,14 @@ func (v *PostTypeContext) List(take int, offset int, noReact ...bool) ([]*models
} }
if err := database.C.Model(&models.Reaction{}). if err := database.C.Model(&models.Reaction{}).
Select(v.ColumnName+"_id as post_id, symbol, COUNT(id) as count"). Select("post_id as post_id, symbol, COUNT(id) as count").
Where(v.ColumnName+"_id IN (?)", idx). Where("post_id IN (?)", idx).
Group("post_id, symbol"). Group("post_id, symbol").
Scan(&reactions).Error; err != nil { Scan(&reactions).Error; err != nil {
return items, err return items, err
} }
itemMap := lo.SliceToMap(items, func(item *models.Feed) (uint, *models.Feed) { itemMap := lo.SliceToMap(items, func(item models.Post) (uint, models.Post) {
return item.ID, item return item.ID, item
}) })
@ -230,73 +188,34 @@ func (v *PostTypeContext) List(take int, offset int, noReact ...bool) ([]*models
} }
} }
{
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 return items, nil
} }
func MapCategoriesAndTags[T models.PostInterface](item T) (T, error) { func InitPostCategoriesAndTags(item models.Post) (models.Post, error) {
var err error var err error
categories := item.GetCategories() for idx, category := range item.Categories {
for idx, category := range categories { item.Categories[idx], err = GetCategory(category.Alias)
categories[idx], err = GetCategory(category.Alias)
if err != nil { if err != nil {
return item, err return item, err
} }
} }
item.SetCategories(categories) for idx, tag := range item.Tags {
tags := item.GetHashtags() item.Tags[idx], err = GetTagOrCreate(tag.Alias, tag.Name)
for idx, tag := range tags {
tags[idx], err = GetTagOrCreate(tag.Alias, tag.Name)
if err != nil { if err != nil {
return item, err return item, err
} }
} }
item.SetHashtags(tags)
return item, nil return item, nil
} }
func NewPost[T models.PostInterface](item T) (T, error) { func NewPost(user models.Account, item models.Post) (models.Post, error) {
item, err := MapCategoriesAndTags(item) item, err := InitPostCategoriesAndTags(item)
if err != nil { if err != nil {
return item, err return item, err
} }
if item.GetRealm() != nil { if item.RealmID != nil {
_, err := GetRealmMember(item.GetRealm().ID, item.GetAuthor().ExternalID) _, err := GetRealmMember(*item.RealmID, user.ExternalID)
if err != nil { if err != nil {
return item, fmt.Errorf("you aren't a part of that realm: %v", err) return item, fmt.Errorf("you aren't a part of that realm: %v", err)
} }
@ -306,19 +225,20 @@ func NewPost[T models.PostInterface](item T) (T, error) {
return item, err return item, err
} }
if item.GetReplyTo() != nil { // Notify the original poster its post has been replied
if item.ReplyID != nil {
go func() { go func() {
var op models.Moment var op models.Post
if err := database.C. if err := database.C.
Where("id = ?", item.GetReplyTo()). Where("id = ?", item.ReplyID).
Preload("Author"). Preload("Author").
First(&op).Error; err == nil { First(&op).Error; err == nil {
if op.Author.ID != item.GetAuthor().ID { if op.Author.ID != user.ID {
postUrl := fmt.Sprintf("https://%s/posts/%d", viper.GetString("domain"), item.GetID()) postUrl := fmt.Sprintf("https://%s/posts/%s", viper.GetString("domain"), item.Alias)
err := NotifyAccount( err := NotifyAccount(
op.Author, op.Author,
fmt.Sprintf("%s replied you", item.GetAuthor().Name), fmt.Sprintf("%s replied you", user.Nick),
fmt.Sprintf("%s replied your post. Check it out!", item.GetAuthor().Name), fmt.Sprintf("%s (%s) replied your post #%s.", user.Nick, user.Name, op.Alias),
false, false,
&proto.NotifyLink{Label: "Related post", Url: postUrl}, &proto.NotifyLink{Label: "Related post", Url: postUrl},
) )
@ -330,36 +250,11 @@ func NewPost[T models.PostInterface](item T) (T, error) {
}() }()
} }
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),
"Someone you followed post a brand new post. Check it out!",
false,
&proto.NotifyLink{Label: "Related post", Url: postUrl},
)
if err != nil {
log.Error().Err(err).Msg("An error occurred when notifying user...")
}
}
}()
}
return item, nil return item, nil
} }
func EditPost[T models.PostInterface](item T) (T, error) { func EditPost(item models.Post) (models.Post, error) {
item, err := MapCategoriesAndTags(item) item, err := InitPostCategoriesAndTags(item)
if err != nil { if err != nil {
return item, err return item, err
} }
@ -369,11 +264,11 @@ func EditPost[T models.PostInterface](item T) (T, error) {
return item, err return item, err
} }
func DeletePost[T models.PostInterface](item T) error { func DeletePost(item models.Post) error {
return database.C.Delete(&item).Error return database.C.Delete(&item).Error
} }
func (v *PostTypeContext) React(reaction models.Reaction) (bool, models.Reaction, error) { func ReactPost(reaction models.Reaction) (bool, models.Reaction, error) {
if err := database.C.Where(reaction).First(&reaction).Error; err != nil { if err := database.C.Where(reaction).First(&reaction).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
return true, reaction, database.C.Save(&reaction).Error return true, reaction, database.C.Save(&reaction).Error