Live video on the AT Protocol
79
fork

Configure Feed

Select the types of activity you want to include in your feed.

at eli/too-many-keyframes 119 lines 3.2 kB view raw
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}