package services import ( "context" "fmt" "math" "sort" "time" "git.solsynth.dev/hypernet/interactive/pkg/internal/database" "git.solsynth.dev/hypernet/interactive/pkg/internal/gap" "git.solsynth.dev/hypernet/interactive/pkg/internal/models" "git.solsynth.dev/hypernet/interactive/pkg/proto" "git.solsynth.dev/hypernet/nexus/pkg/nex" "github.com/gofiber/fiber/v2" "github.com/rs/zerolog/log" "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 // Planing the feed limitF := float64(limit) interCount := int(math.Ceil(limitF * 0.5)) fediCount := int(math.Ceil(limitF * 0.25)) newsCount := int(math.Ceil(limitF * 0.25)) // Internal posts interTx, err := UniversalPostFilter(c, database.C) if err != nil { return nil, fmt.Errorf("failed to prepare load interactive posts: %v", err) } if cursor != nil { interTx = interTx.Where("published_at < ?", *cursor) } interPosts, err := ListPostForFeed(interTx, interCount, user) if err != nil { return nil, fmt.Errorf("failed to load interactive posts: %v", err) } feed = append(feed, interPosts...) // Fediverse posts fediTx := database.C if cursor != nil { fediTx = fediTx.Where("created_at < ?", *cursor) } fediPosts, err := ListFediversePostForFeed(fediTx, fediCount) if err != nil { return feed, fmt.Errorf("failed to load fediverse posts: %v", err) } feed = append(feed, fediPosts...) sort.Slice(feed, func(i, j int) bool { return feed[i].CreatedAt.After(feed[j].CreatedAt) }) // News today - from Reader if news, err := ListNewsForFeed(newsCount, cursor); err != nil { log.Error().Err(err).Msg("Failed to load news in getting feed...") } else { feed = append(feed, news) } 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: TruncatePostContent(*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 } func ListNewsForFeed(limit int, cursor *time.Time) (FeedEntry, error) { conn, err := gap.Nx.GetClientGrpcConn("re") if err != nil { return FeedEntry{}, fmt.Errorf("failed to get grpc connection with reader: %v", err) } client := proto.NewFeedServiceClient(conn) request := &proto.GetFeedRequest{ Limit: int64(limit), } if cursor != nil { request.Cursor = lo.ToPtr(uint64(cursor.UnixMilli())) } resp, err := client.GetFeed(context.Background(), request) if err != nil { return FeedEntry{}, fmt.Errorf("failed to get feed from reader: %v", err) } var createdAt time.Time return FeedEntry{ Type: "reader.news", CreatedAt: createdAt, Data: lo.Map(resp.Items, func(item *proto.FeedItem, _ int) map[string]any { cta := time.UnixMilli(int64(item.CreatedAt)) createdAt = lo.Ternary(createdAt.Before(cta), cta, createdAt) return nex.DecodeMap(item.Content) }), }, nil }