✨ Universal reading feed
♻️ Refactor post listing
This commit is contained in:
parent
26dfd25763
commit
d6fa3bb15d
@ -53,7 +53,7 @@ func apUserOutbox(c *fiber.Ctx) error {
|
|||||||
return fiber.NewError(fiber.StatusNotFound, err.Error())
|
return fiber.NewError(fiber.StatusNotFound, err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
tx, err := UniversalPostFilter(c, database.C)
|
tx, err := services.UniversalPostFilter(c, database.C)
|
||||||
tx.Where("publisher_id = ? AND reply_id IS NULL", publisher.ID)
|
tx.Where("publisher_id = ? AND reply_id IS NULL", publisher.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fiber.NewError(fiber.StatusBadRequest, err.Error())
|
return fiber.NewError(fiber.StatusBadRequest, err.Error())
|
||||||
|
@ -31,6 +31,7 @@ func MapControllers(app *fiber.App, baseURL string) {
|
|||||||
{
|
{
|
||||||
recommendations.Get("/", listRecommendation)
|
recommendations.Get("/", listRecommendation)
|
||||||
recommendations.Get("/shuffle", listRecommendationShuffle)
|
recommendations.Get("/shuffle", listRecommendationShuffle)
|
||||||
|
recommendations.Get("/feed", getRecommendationFeed)
|
||||||
}
|
}
|
||||||
|
|
||||||
stories := api.Group("/stories").Name("Story API")
|
stories := api.Group("/stories").Name("Story API")
|
||||||
|
@ -4,13 +4,11 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
|
||||||
|
|
||||||
"git.solsynth.dev/hypernet/nexus/pkg/nex/cruda"
|
"git.solsynth.dev/hypernet/nexus/pkg/nex/cruda"
|
||||||
"git.solsynth.dev/hypernet/nexus/pkg/nex/sec"
|
"git.solsynth.dev/hypernet/nexus/pkg/nex/sec"
|
||||||
"git.solsynth.dev/hypernet/passport/pkg/authkit"
|
"git.solsynth.dev/hypernet/passport/pkg/authkit"
|
||||||
authm "git.solsynth.dev/hypernet/passport/pkg/authkit/models"
|
authm "git.solsynth.dev/hypernet/passport/pkg/authkit/models"
|
||||||
"gorm.io/gorm"
|
|
||||||
|
|
||||||
"git.solsynth.dev/hypernet/interactive/pkg/internal/database"
|
"git.solsynth.dev/hypernet/interactive/pkg/internal/database"
|
||||||
"git.solsynth.dev/hypernet/interactive/pkg/internal/gap"
|
"git.solsynth.dev/hypernet/interactive/pkg/internal/gap"
|
||||||
@ -21,68 +19,6 @@ import (
|
|||||||
"github.com/samber/lo"
|
"github.com/samber/lo"
|
||||||
)
|
)
|
||||||
|
|
||||||
type UniversalPostFilterConfig struct {
|
|
||||||
ShowDraft bool
|
|
||||||
ShowReply bool
|
|
||||||
ShowCollapsed bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func UniversalPostFilter(c *fiber.Ctx, tx *gorm.DB, cfg ...UniversalPostFilterConfig) (*gorm.DB, error) {
|
|
||||||
var config UniversalPostFilterConfig
|
|
||||||
if len(cfg) > 0 {
|
|
||||||
config = cfg[0]
|
|
||||||
} else {
|
|
||||||
config = UniversalPostFilterConfig{}
|
|
||||||
}
|
|
||||||
|
|
||||||
if user, authenticated := c.Locals("user").(authm.Account); authenticated {
|
|
||||||
tx = services.FilterPostWithUserContext(c, tx, &user)
|
|
||||||
if c.QueryBool("noDraft", true) && !config.ShowDraft {
|
|
||||||
tx = services.FilterPostDraft(tx)
|
|
||||||
tx = services.FilterPostWithPublishedAt(tx, time.Now())
|
|
||||||
} else {
|
|
||||||
tx = services.FilterPostDraftWithAuthor(database.C, user.ID)
|
|
||||||
tx = services.FilterPostWithPublishedAt(tx, time.Now(), user.ID)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
tx = services.FilterPostWithUserContext(c, tx, nil)
|
|
||||||
tx = services.FilterPostDraft(tx)
|
|
||||||
tx = services.FilterPostWithPublishedAt(tx, time.Now())
|
|
||||||
}
|
|
||||||
|
|
||||||
if c.QueryBool("noReply", true) && !config.ShowReply {
|
|
||||||
tx = services.FilterPostReply(tx)
|
|
||||||
}
|
|
||||||
if c.QueryBool("noCollapse", true) && !config.ShowCollapsed {
|
|
||||||
tx = tx.Where("is_collapsed = ? OR is_collapsed IS NULL", false)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(c.Query("author")) > 0 {
|
|
||||||
var author models.Publisher
|
|
||||||
if err := database.C.Where("name = ?", c.Query("author")).First(&author).Error; err != nil {
|
|
||||||
return tx, fiber.NewError(fiber.StatusNotFound, err.Error())
|
|
||||||
}
|
|
||||||
tx = tx.Where("publisher_id = ?", author.ID)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(c.Query("categories")) > 0 {
|
|
||||||
tx = services.FilterPostWithCategory(tx, c.Query("categories"))
|
|
||||||
}
|
|
||||||
if len(c.Query("tags")) > 0 {
|
|
||||||
tx = services.FilterPostWithTag(tx, c.Query("tags"))
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(c.Query("type")) > 0 {
|
|
||||||
tx = services.FilterPostWithType(tx, c.Query("type"))
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(c.Query("realm")) > 0 {
|
|
||||||
tx = services.FilterPostWithRealm(tx, c.Query("realm"))
|
|
||||||
}
|
|
||||||
|
|
||||||
return tx, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func getPost(c *fiber.Ctx) error {
|
func getPost(c *fiber.Ctx) error {
|
||||||
id := c.Params("postId")
|
id := c.Params("postId")
|
||||||
|
|
||||||
@ -91,7 +27,7 @@ func getPost(c *fiber.Ctx) error {
|
|||||||
|
|
||||||
tx := database.C
|
tx := database.C
|
||||||
|
|
||||||
if tx, err = UniversalPostFilter(c, tx, UniversalPostFilterConfig{
|
if tx, err = services.UniversalPostFilter(c, tx, services.UniversalPostFilterConfig{
|
||||||
ShowReply: true,
|
ShowReply: true,
|
||||||
ShowDraft: true,
|
ShowDraft: true,
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
@ -140,7 +76,7 @@ func searchPost(c *fiber.Ctx) error {
|
|||||||
tx = services.FilterPostWithFuzzySearch(tx, probe)
|
tx = services.FilterPostWithFuzzySearch(tx, probe)
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
if tx, err = UniversalPostFilter(c, tx, UniversalPostFilterConfig{
|
if tx, err = services.UniversalPostFilter(c, tx, services.UniversalPostFilterConfig{
|
||||||
ShowReply: true,
|
ShowReply: true,
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
return err
|
return err
|
||||||
@ -183,7 +119,7 @@ func listPost(c *fiber.Ctx) error {
|
|||||||
tx := database.C
|
tx := database.C
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
if tx, err = UniversalPostFilter(c, tx); err != nil {
|
if tx, err = services.UniversalPostFilter(c, tx); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -224,7 +160,7 @@ func listPostMinimal(c *fiber.Ctx) error {
|
|||||||
tx := database.C
|
tx := database.C
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
if tx, err = UniversalPostFilter(c, tx); err != nil {
|
if tx, err = services.UniversalPostFilter(c, tx); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
"git.solsynth.dev/hypernet/interactive/pkg/internal/database"
|
"git.solsynth.dev/hypernet/interactive/pkg/internal/database"
|
||||||
"git.solsynth.dev/hypernet/interactive/pkg/internal/models"
|
"git.solsynth.dev/hypernet/interactive/pkg/internal/models"
|
||||||
"git.solsynth.dev/hypernet/interactive/pkg/internal/services"
|
"git.solsynth.dev/hypernet/interactive/pkg/internal/services"
|
||||||
@ -50,7 +52,7 @@ func listRecommendationShuffle(c *fiber.Ctx) error {
|
|||||||
tx := database.C
|
tx := database.C
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
if tx, err = UniversalPostFilter(c, tx); err != nil {
|
if tx, err = services.UniversalPostFilter(c, tx); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -83,3 +85,25 @@ func listRecommendationShuffle(c *fiber.Ctx) error {
|
|||||||
"data": items,
|
"data": items,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getRecommendationFeed(c *fiber.Ctx) error {
|
||||||
|
limit := c.QueryInt("limit", 20)
|
||||||
|
cursor := c.QueryInt("cursor", 0)
|
||||||
|
|
||||||
|
var cursorTime *time.Time
|
||||||
|
if cursor > 0 {
|
||||||
|
cursorTime = lo.ToPtr(time.Unix(int64(cursor), 0))
|
||||||
|
}
|
||||||
|
|
||||||
|
var userId *uint
|
||||||
|
if user, authenticated := c.Locals("user").(authm.Account); authenticated {
|
||||||
|
userId = &user.ID
|
||||||
|
}
|
||||||
|
|
||||||
|
entries, err := services.GetFeed(c, limit, userId, cursorTime)
|
||||||
|
if err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.JSON(entries)
|
||||||
|
}
|
||||||
|
@ -44,6 +44,8 @@ func FetchFediversePost(cfg FediverseFriendConfig) ([]models.FediversePost, erro
|
|||||||
})
|
})
|
||||||
return posts, nil
|
return posts, nil
|
||||||
default:
|
default:
|
||||||
|
// TODO Other platform fetching is still under development
|
||||||
|
// DO NOT USE THEM
|
||||||
return nil, fmt.Errorf("unsupported fediverse service: %s", cfg.Type)
|
return nil, fmt.Errorf("unsupported fediverse service: %s", cfg.Type)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
84
pkg/internal/services/feed.go
Normal file
84
pkg/internal/services/feed.go
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
package services
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.solsynth.dev/hypernet/interactive/pkg/internal/database"
|
||||||
|
"git.solsynth.dev/hypernet/interactive/pkg/internal/models"
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"github.com/samber/lo"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type FeedEntry struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Data any `json:"data"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetFeed(c *fiber.Ctx, limit int, user *uint, cursor *time.Time) ([]FeedEntry, error) {
|
||||||
|
// We got two types of data for now
|
||||||
|
// Plan to let each of them take 50% of the output
|
||||||
|
|
||||||
|
var feed []FeedEntry
|
||||||
|
|
||||||
|
interTx, err := UniversalPostFilter(c, database.C, UniversalPostFilterConfig{
|
||||||
|
TimeCursor: cursor,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to prepare load interactive posts: %v", err)
|
||||||
|
}
|
||||||
|
interPosts, err := ListPostForFeed(interTx, limit/2, user)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to load interactive posts: %v", err)
|
||||||
|
}
|
||||||
|
feed = append(feed, interPosts...)
|
||||||
|
|
||||||
|
fediTx := database.C
|
||||||
|
if cursor != nil {
|
||||||
|
fediTx = fediTx.Where("created_at < ?", *cursor)
|
||||||
|
}
|
||||||
|
fediPosts, err := ListFediversePostForFeed(fediTx, limit/2)
|
||||||
|
if err != nil {
|
||||||
|
return feed, fmt.Errorf("failed to load fediverse posts: %v", err)
|
||||||
|
}
|
||||||
|
feed = append(feed, fediPosts...)
|
||||||
|
|
||||||
|
return feed, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// We assume the database context already handled the filtering and pagination
|
||||||
|
// Only manage to pulling the content only
|
||||||
|
|
||||||
|
func ListPostForFeed(tx *gorm.DB, limit int, user *uint) ([]FeedEntry, error) {
|
||||||
|
posts, err := ListPost(tx, limit, -1, "published_at DESC", user)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
entries := lo.Map(posts, func(post *models.Post, _ int) FeedEntry {
|
||||||
|
return FeedEntry{
|
||||||
|
Type: "interactive.post",
|
||||||
|
Data: post,
|
||||||
|
CreatedAt: post.CreatedAt,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return entries, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ListFediversePostForFeed(tx *gorm.DB, limit int) ([]FeedEntry, error) {
|
||||||
|
var posts []models.FediversePost
|
||||||
|
if err := tx.
|
||||||
|
Preload("User").Limit(limit).
|
||||||
|
Find(&posts).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
entries := lo.Map(posts, func(post models.FediversePost, _ int) FeedEntry {
|
||||||
|
return FeedEntry{
|
||||||
|
Type: "fediverse.post",
|
||||||
|
Data: post,
|
||||||
|
CreatedAt: post.CreatedAt,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return entries, nil
|
||||||
|
}
|
@ -414,9 +414,15 @@ func ListPost(tx *gorm.DB, take int, offset int, order any, user *uint, noReact
|
|||||||
take = 100
|
take = 100
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if take >= 0 {
|
||||||
|
tx = tx.Limit(take)
|
||||||
|
}
|
||||||
|
if offset >= 0 {
|
||||||
|
tx = tx.Offset(offset)
|
||||||
|
}
|
||||||
|
|
||||||
var items []*models.Post
|
var items []*models.Post
|
||||||
if err := PreloadGeneral(tx).
|
if err := PreloadGeneral(tx).
|
||||||
Limit(take).Offset(offset).
|
|
||||||
Order(order).
|
Order(order).
|
||||||
Find(&items).Error; err != nil {
|
Find(&items).Error; err != nil {
|
||||||
return items, err
|
return items, err
|
||||||
|
79
pkg/internal/services/posts_getter.go
Normal file
79
pkg/internal/services/posts_getter.go
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
package services
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.solsynth.dev/hypernet/interactive/pkg/internal/database"
|
||||||
|
"git.solsynth.dev/hypernet/interactive/pkg/internal/models"
|
||||||
|
authm "git.solsynth.dev/hypernet/passport/pkg/authkit/models"
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type UniversalPostFilterConfig struct {
|
||||||
|
ShowDraft bool
|
||||||
|
ShowReply bool
|
||||||
|
ShowCollapsed bool
|
||||||
|
TimeCursor *time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
func UniversalPostFilter(c *fiber.Ctx, tx *gorm.DB, cfg ...UniversalPostFilterConfig) (*gorm.DB, error) {
|
||||||
|
var config UniversalPostFilterConfig
|
||||||
|
if len(cfg) > 0 {
|
||||||
|
config = cfg[0]
|
||||||
|
} else {
|
||||||
|
config = UniversalPostFilterConfig{}
|
||||||
|
}
|
||||||
|
|
||||||
|
timeCursor := time.Now()
|
||||||
|
if config.TimeCursor != nil {
|
||||||
|
timeCursor = *config.TimeCursor
|
||||||
|
}
|
||||||
|
|
||||||
|
if user, authenticated := c.Locals("user").(authm.Account); authenticated {
|
||||||
|
tx = FilterPostWithUserContext(c, tx, &user)
|
||||||
|
if c.QueryBool("noDraft", true) && !config.ShowDraft {
|
||||||
|
tx = FilterPostDraft(tx)
|
||||||
|
tx = FilterPostWithPublishedAt(tx, timeCursor)
|
||||||
|
} else {
|
||||||
|
tx = FilterPostDraftWithAuthor(database.C, user.ID)
|
||||||
|
tx = FilterPostWithPublishedAt(tx, timeCursor, user.ID)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
tx = FilterPostWithUserContext(c, tx, nil)
|
||||||
|
tx = FilterPostDraft(tx)
|
||||||
|
tx = FilterPostWithPublishedAt(tx, timeCursor)
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.QueryBool("noReply", true) && !config.ShowReply {
|
||||||
|
tx = FilterPostReply(tx)
|
||||||
|
}
|
||||||
|
if c.QueryBool("noCollapse", true) && !config.ShowCollapsed {
|
||||||
|
tx = tx.Where("is_collapsed = ? OR is_collapsed IS NULL", false)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(c.Query("author")) > 0 {
|
||||||
|
var author models.Publisher
|
||||||
|
if err := database.C.Where("name = ?", c.Query("author")).First(&author).Error; err != nil {
|
||||||
|
return tx, fiber.NewError(fiber.StatusNotFound, err.Error())
|
||||||
|
}
|
||||||
|
tx = tx.Where("publisher_id = ?", author.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(c.Query("categories")) > 0 {
|
||||||
|
tx = FilterPostWithCategory(tx, c.Query("categories"))
|
||||||
|
}
|
||||||
|
if len(c.Query("tags")) > 0 {
|
||||||
|
tx = FilterPostWithTag(tx, c.Query("tags"))
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(c.Query("type")) > 0 {
|
||||||
|
tx = FilterPostWithType(tx, c.Query("type"))
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(c.Query("realm")) > 0 {
|
||||||
|
tx = FilterPostWithRealm(tx, c.Query("realm"))
|
||||||
|
}
|
||||||
|
|
||||||
|
return tx, nil
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user