Live video on the AT Protocol
1package spxrpc
2
3import (
4 "context"
5 "fmt"
6 "net/http"
7 "time"
8
9 comatprototypes "github.com/bluesky-social/indigo/api/atproto"
10 "github.com/bluesky-social/indigo/atproto/syntax"
11 "github.com/bluesky-social/indigo/xrpc"
12 "github.com/google/uuid"
13 "github.com/labstack/echo/v4"
14 "github.com/streamplace/oatproxy/pkg/oatproxy"
15 "stream.place/streamplace/pkg/config"
16 "stream.place/streamplace/pkg/log"
17 "stream.place/streamplace/pkg/media"
18 "stream.place/streamplace/pkg/model"
19)
20
21func (s *Server) handleComAtprotoModerationCreateReport(ctx context.Context, body *comatprototypes.ModerationCreateReport_Input) (*comatprototypes.ModerationCreateReport_Output, error) {
22 c, ok := ctx.Value(echoContextKey).(echo.Context)
23 if !ok {
24 return nil, echo.NewHTTPError(http.StatusInternalServerError, "echo context not found")
25 }
26
27 atprotoProxy := c.Request().Header.Get("Atproto-Proxy")
28 if atprotoProxy == "" {
29 if len(s.cli.Labelers) > 0 {
30 atprotoProxy = fmt.Sprintf("%s#atproto_labeler", s.cli.Labelers[0])
31 } else {
32 return nil, echo.NewHTTPError(http.StatusBadRequest, "Atproto-Proxy header is required (where are you sending this report?)")
33 }
34 }
35
36 log.Log(ctx, "handleComAtprotoModerationCreateReport", "body", body)
37
38 session, client := oatproxy.GetOAuthSession(ctx)
39 if session == nil {
40 return nil, echo.NewHTTPError(http.StatusUnauthorized, "oauth session not found")
41 }
42
43 if body.Reason == nil {
44 empty := ""
45 body.Reason = &empty
46 }
47
48 if body.Subject == nil {
49 return nil, echo.NewHTTPError(http.StatusBadRequest, "subject is required")
50 }
51
52 var did string
53
54 if body.Subject.AdminDefs_RepoRef != nil {
55 d, err := syntax.ParseDID(body.Subject.AdminDefs_RepoRef.Did)
56 if err != nil {
57 return nil, echo.NewHTTPError(http.StatusBadRequest, "invalid subject did")
58 }
59 did = d.String()
60 } else if body.Subject.RepoStrongRef != nil {
61 aturi, err := syntax.ParseATURI(body.Subject.RepoStrongRef.Uri)
62 if err != nil {
63 return nil, echo.NewHTTPError(http.StatusBadRequest, "invalid subject uri")
64 }
65 did = aturi.Authority().String()
66 // if it's chat, we want the clip from the streamer, not from the chatter
67 if aturi.Collection() == "place.stream.chat.message" {
68 msg, err := s.model.GetChatMessage(body.Subject.RepoStrongRef.Uri)
69 if err != nil {
70 log.Error(ctx, "failed to get chat message for chat report", "error", err)
71 } else {
72 did = msg.StreamerRepoDID
73 }
74 }
75 } else {
76 return nil, echo.NewHTTPError(http.StatusBadRequest, "invalid subject")
77 }
78
79 clipID, err := makeClip(ctx, s.cli, s.model, did)
80 if err != nil {
81 // we still want the report to go through!
82 log.Error(ctx, "failed to make clip for report", "error", err)
83 } else {
84 clipURL := fmt.Sprintf("https://%s/api/clip/%s/%s.mp4", s.cli.PublicHost, did, clipID)
85 newReason := fmt.Sprintf("%s\n\nClip: %s", *body.Reason, clipURL)
86 body.Reason = &newReason
87 }
88
89 client.SetHeaders(map[string]string{
90 "Atproto-Proxy": atprotoProxy,
91 })
92
93 var output comatprototypes.ModerationCreateReport_Output
94 err = client.Do(ctx, xrpc.Procedure, "application/json", "com.atproto.moderation.createReport", nil, body, &output)
95 if err != nil {
96 return nil, echo.NewHTTPError(http.StatusInternalServerError, err.Error())
97 }
98
99 return &output, nil
100}
101
102func makeClip(ctx context.Context, cli *config.CLI, mod model.Model, did string) (string, error) {
103 after := time.Now().Add(-time.Duration(60) * time.Second)
104
105 uu, err := uuid.NewV7()
106 if err != nil {
107 return "", echo.NewHTTPError(http.StatusInternalServerError, "failed to generate uuid")
108 }
109
110 fd, err := cli.DataFileCreate([]string{did, "clips", fmt.Sprintf("%s.mp4", uu.String())}, false)
111 if err != nil {
112 return "", echo.NewHTTPError(http.StatusInternalServerError, "failed to create data file")
113 }
114 defer fd.Close()
115
116 err = media.ClipUser(ctx, mod, cli, did, fd, nil, &after)
117 if err != nil {
118 return "", echo.NewHTTPError(http.StatusInternalServerError, "failed to clip user")
119 }
120 return uu.String(), nil
121}