Live video on the AT Protocol
1package spxrpc
2
3import (
4 "context"
5 "fmt"
6 "net/http"
7 "regexp"
8 "strconv"
9 "strings"
10
11 "github.com/bluesky-social/indigo/api/bsky"
12 "github.com/labstack/echo/v4"
13 "stream.place/streamplace/pkg/log"
14 "stream.place/streamplace/pkg/model"
15)
16
17var FeedSkeletonRE = regexp.MustCompile(`^at://did:(web|plc):([a-z0-9\.\-]+)/app.bsky.feed.generator/([a-z0-9\.\-]+)$`)
18
19func parseFeedSkeleton(did string) (string, string, error) {
20 matches := FeedSkeletonRE.FindStringSubmatch(did)
21 if len(matches) != 4 {
22 return "", "", echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("invalid feed parameter: %s", did))
23 }
24 return fmt.Sprintf("did:%s:%s", matches[1], matches[2]), matches[3], nil
25}
26
27const FeedLiveStreams = "live-streams"
28const FeedAllStreams = "all-streams"
29
30func (s *Server) handleAppBskyFeedGetFeedSkeleton(ctx context.Context, inCursor string, feed string, limit int) (*bsky.FeedGetFeedSkeleton_Output, error) {
31 _, name, err := parseFeedSkeleton(feed)
32 if err != nil {
33 return nil, err
34 }
35 var ts int64
36 if inCursor != "" {
37 parts := strings.Split(inCursor, "::")
38 if len(parts) != 2 {
39 return nil, echo.NewHTTPError(http.StatusBadRequest, "invalid cursor format")
40 }
41 ts, err = strconv.ParseInt(parts[0], 10, 64)
42 if err != nil {
43 return nil, echo.NewHTTPError(http.StatusBadRequest, "invalid cursor timestamp")
44 }
45 }
46 var posts []model.FeedPost
47 outCursor := ""
48 if name == FeedAllStreams {
49 posts, err = s.model.ListFeedPostsByType("livestream", limit, ts)
50 if err != nil {
51 return nil, echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("failed to list feed posts: %v", err))
52 }
53 if len(posts) > 0 {
54 last := posts[len(posts)-1]
55 ts := last.CreatedAt.UnixMilli()
56 outCursor = fmt.Sprintf("%d::%s", ts, last.CID)
57 }
58 } else if name == FeedLiveStreams {
59 segs, err := s.model.MostRecentSegments()
60 if err != nil {
61 return nil, echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("failed to get recent segments: %v", err))
62 }
63 for _, seg := range segs {
64 ls, err := s.model.GetLatestLivestreamForRepo(seg.RepoDID)
65 if err != nil {
66 log.Error(ctx, "failed to get latest livestream, skipping", "repoDID", seg.RepoDID, "error", err)
67 continue
68 }
69 posts = append(posts, model.FeedPost{
70 URI: ls.PostURI,
71 })
72 }
73 } else {
74 return nil, echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("invalid feed name: %s", name))
75 }
76 res := bsky.FeedGetFeedSkeleton_Output{
77 Feed: []*bsky.FeedDefs_SkeletonFeedPost{},
78 }
79 if outCursor != "" {
80 res.Cursor = &outCursor
81 }
82 for _, post := range posts {
83 res.Feed = append(res.Feed, &bsky.FeedDefs_SkeletonFeedPost{
84 Post: post.URI,
85 })
86 }
87 return &res, nil
88}