Live video on the AT Protocol
1package discord
2
3import (
4 "bytes"
5 "context"
6 "encoding/json"
7 "fmt"
8 "io"
9 "net/http"
10 "net/url"
11 "strconv"
12 "strings"
13
14 "github.com/bluesky-social/indigo/api/bsky"
15 "stream.place/streamplace/pkg/aqhttp"
16 "stream.place/streamplace/pkg/integrations/discord/discordtypes"
17 "stream.place/streamplace/pkg/log"
18 "stream.place/streamplace/pkg/model"
19 "stream.place/streamplace/pkg/streamplace"
20)
21
22func SendLivestream(ctx context.Context, w *discordtypes.Webhook, pdsURL string, lsv *streamplace.Livestream_LivestreamView, postView *bsky.FeedDefs_PostView, spcp *streamplace.ChatProfile) error {
23
24 ctx = log.WithLogValues(ctx, "func", "SendLivestream")
25 ls, ok := lsv.Record.Val.(*streamplace.Livestream)
26 if !ok {
27 return fmt.Errorf("failed to cast livestream view to livestream")
28 }
29 content := fmt.Sprintf("🔴 LIVE %s", ls.Title)
30
31 for _, rewrite := range w.Rewrite {
32 content = strings.ReplaceAll(content, rewrite.From, rewrite.To)
33 }
34
35 payload := discordtypes.Payload{
36 Username: fmt.Sprintf("@%s", lsv.Author.Handle),
37 Content: fmt.Sprintf("%s%s%s", w.Prefix, content, w.Suffix),
38 }
39
40 avatarURL, err := GetAvatarURL(ctx, lsv.Author.Did)
41 if err != nil {
42 log.Warn(ctx, "failed to get avatar URL", "err", err)
43 }
44 if avatarURL != "" {
45 payload.AvatarURL = avatarURL
46 }
47
48 color := "f8baca"
49 if spcp != nil && spcp.Color != nil {
50 color = strings.TrimPrefix(model.ColorToHex(spcp.Color), "#")
51 }
52
53 colorInt, err := strconv.ParseInt(color, 16, 64)
54 if err != nil {
55 log.Warn(ctx, "failed to parse color", "err", err)
56 }
57 payload.Embeds = []discordtypes.Embed{
58 {
59 Color: int(colorInt),
60 },
61 }
62
63 suffix := "!"
64 if ls.Url != nil {
65 u, err := url.Parse(*ls.Url)
66 if err != nil {
67 log.Warn(ctx, "failed to parse URL", "err", err)
68 } else {
69 suffix = fmt.Sprintf(" on %s!", u.Host)
70 payload.Embeds[0].URL = fmt.Sprintf("%s/%s", *ls.Url, lsv.Author.Handle)
71 }
72 }
73
74 payload.Embeds[0].Title = fmt.Sprintf("@%s is LIVE%s", lsv.Author.Handle, suffix)
75
76 if ls.Thumb != nil {
77 u, err := url.Parse(fmt.Sprintf("%s/xrpc/com.atproto.sync.getBlob", pdsURL))
78 if err != nil {
79 return fmt.Errorf("failed to parse base URL: %w", err)
80 }
81 q := u.Query()
82 q.Set("did", lsv.Author.Did)
83 q.Set("cid", ls.Thumb.Ref.String())
84 u.RawQuery = q.Encode()
85 imageURL := u.String()
86 payload.Embeds[0].Image = &discordtypes.Image{
87 URL: imageURL,
88 }
89 }
90
91 jsonPayload, err := json.Marshal(payload)
92 if err != nil {
93 return fmt.Errorf("failed to marshal payload: %w", err)
94 }
95
96 log.Warn(ctx, "sending livestream to discord", "payload", string(jsonPayload))
97
98 req, err := http.NewRequestWithContext(ctx, "POST", w.URL, bytes.NewReader(jsonPayload))
99 if err != nil {
100 return fmt.Errorf("failed to create request: %w", err)
101 }
102 req.Header.Set("Content-Type", "application/json")
103
104 resp, err := aqhttp.Do(ctx, req)
105 if err != nil {
106 return fmt.Errorf("failed to send request: %w", err)
107 }
108 defer resp.Body.Close()
109
110 if resp.StatusCode != http.StatusNoContent {
111 body, err := io.ReadAll(resp.Body)
112 if err != nil {
113 return fmt.Errorf("failed to read response body: %w", err)
114 }
115 return fmt.Errorf("failed to send request (http %d): %s", resp.StatusCode, string(body))
116 }
117
118 return nil
119}