Live video on the AT Protocol
1package config
2
3import (
4 "context"
5 "crypto/rsa"
6 "crypto/x509"
7 "encoding/json"
8 "encoding/pem"
9 "errors"
10 "flag"
11 "fmt"
12 "io"
13 "net"
14 "os"
15 "path/filepath"
16 "runtime"
17 "strconv"
18 "strings"
19 "time"
20
21 "math/rand/v2"
22
23 "github.com/lestrrat-go/jwx/v2/jwk"
24 "github.com/livepeer/go-livepeer/cmd/livepeer/starter"
25 "github.com/lmittmann/tint"
26 slogGorm "github.com/orandin/slog-gorm"
27 "github.com/peterbourgon/ff/v3"
28 "stream.place/streamplace/pkg/aqtime"
29 "stream.place/streamplace/pkg/constants"
30 "stream.place/streamplace/pkg/crypto/aqpub"
31 "stream.place/streamplace/pkg/integrations/discord/discordtypes"
32 "stream.place/streamplace/pkg/log"
33)
34
35const SPDataDir = "$SP_DATA_DIR"
36const SegmentsDir = "segments"
37
38type BuildFlags struct {
39 Version string
40 BuildTime int64
41 UUID string
42}
43
44func (b BuildFlags) BuildTimeStr() string {
45 ts := time.Unix(b.BuildTime, 0)
46 return ts.UTC().Format(time.RFC3339)
47}
48
49func (b BuildFlags) BuildTimeStrExpo() string {
50 ts := time.Unix(b.BuildTime, 0)
51 return ts.UTC().Format("2006-01-02T15:04:05.000Z")
52}
53
54type CLI struct {
55 AdminAccount string
56 Build *BuildFlags
57 DataDir string
58 DBURL string
59 EthAccountAddr string
60 EthKeystorePath string
61 EthPassword string
62 FirebaseServiceAccount string
63 FirebaseServiceAccountFile string
64 GitLabURL string
65 HTTPAddr string
66 HTTPInternalAddr string
67 HTTPSAddr string
68 RtmpsAddr string
69 Secure bool
70 NoMist bool
71 MistAdminPort int
72 MistHTTPPort int
73 MistRTMPPort int
74 SigningKeyPath string
75 TAURL string
76 TLSCertPath string
77 TLSKeyPath string
78 PKCS11ModulePath string
79 PKCS11Pin string
80 PKCS11TokenSlot string
81 PKCS11TokenLabel string
82 PKCS11TokenSerial string
83 PKCS11KeypairLabel string
84 PKCS11KeypairID string
85 StreamerName string
86 RelayHost string
87 Debug map[string]map[string]int
88 AllowedStreams []string
89 WideOpen bool
90 Peers []string
91 Redirects []string
92 TestStream bool
93 FrontendProxy string
94 PublicOAuth bool
95 AppBundleID string
96 NoFirehose bool
97 PrintChat bool
98 Color string
99 LivepeerGatewayURL string
100 LivepeerGateway bool
101 WHIPTest string
102 Thumbnail bool
103 SmearAudio bool
104 ExternalSigning bool
105 RTMPServerAddon string
106 TracingEndpoint string
107 BroadcasterHost string
108 XXDeprecatedPublicHost string
109 ServerHost string
110 RateLimitPerSecond int
111 RateLimitBurst int
112 RateLimitWebsocket int
113 JWK jwk.Key
114 AccessJWK jwk.Key
115 dataDirFlags []*string
116 DiscordWebhooks []*discordtypes.Webhook
117 NewWebRTCPlayback bool
118 AppleTeamID string
119 AndroidCertFingerprint string
120 Labelers []string
121 AtprotoDID string
122 LivepeerHelp bool
123 PLCURL string
124 ContentFilters *ContentFilters
125 SQLLogging bool
126 SentryDSN string
127 LivepeerDebug bool
128 Tickets []string
129 IrohTopic string
130 DID string
131}
132
133// ContentFilters represents the content filtering configuration
134type ContentFilters struct {
135 ContentWarnings struct {
136 Enabled bool `json:"enabled"`
137 BlockedWarnings []string `json:"blocked_warnings"`
138 } `json:"content_warnings"`
139 DistributionPolicy struct {
140 Enabled bool `json:"enabled"`
141 } `json:"distribution_policy"`
142}
143
144func (cli *CLI) NewFlagSet(name string) *flag.FlagSet {
145 fs := flag.NewFlagSet("streamplace", flag.ExitOnError)
146 fs.StringVar(&cli.DataDir, "data-dir", DefaultDataDir(), "directory for keeping all streamplace data")
147 fs.StringVar(&cli.HTTPAddr, "http-addr", ":38080", "Public HTTP address")
148 fs.StringVar(&cli.HTTPInternalAddr, "http-internal-addr", "127.0.0.1:39090", "Private, admin-only HTTP address")
149 fs.StringVar(&cli.HTTPSAddr, "https-addr", ":38443", "Public HTTPS address")
150 fs.BoolVar(&cli.Secure, "secure", false, "Run with HTTPS. Required for WebRTC output")
151 cli.DataDirFlag(fs, &cli.TLSCertPath, "tls-cert", filepath.Join("tls", "tls.crt"), "Path to TLS certificate")
152 cli.DataDirFlag(fs, &cli.TLSKeyPath, "tls-key", filepath.Join("tls", "tls.key"), "Path to TLS key")
153 fs.StringVar(&cli.SigningKeyPath, "signing-key", "", "Path to signing key for pushing OTA updates to the app")
154 fs.StringVar(&cli.DBURL, "db-url", "sqlite://$SP_DATA_DIR/state.sqlite", "URL of the database to use for storing private streamplace state")
155 cli.dataDirFlags = append(cli.dataDirFlags, &cli.DBURL)
156 fs.StringVar(&cli.AdminAccount, "admin-account", "", "ethereum account that administrates this streamplace node")
157 fs.StringVar(&cli.FirebaseServiceAccount, "firebase-service-account", "", "Base64-encoded JSON string of a firebase service account key")
158 fs.StringVar(&cli.FirebaseServiceAccountFile, "firebase-service-account-file", "", "Path to a JSON file containing a firebase service account key")
159 fs.StringVar(&cli.GitLabURL, "gitlab-url", "https://git.stream.place/api/v4/projects/1", "gitlab url for generating download links")
160 cli.DataDirFlag(fs, &cli.EthKeystorePath, "eth-keystore-path", "keystore", "path to ethereum keystore")
161 fs.StringVar(&cli.EthAccountAddr, "eth-account-addr", "", "ethereum account address to use (if keystore contains more than one)")
162 fs.StringVar(&cli.EthPassword, "eth-password", "", "password for encrypting keystore")
163 fs.StringVar(&cli.TAURL, "ta-url", "http://timestamp.digicert.com", "timestamp authority server for signing")
164 fs.StringVar(&cli.PKCS11ModulePath, "pkcs11-module-path", "", "path to a PKCS11 module for HSM signing, for example /usr/lib/x86_64-linux-gnu/opensc-pkcs11.so")
165 fs.StringVar(&cli.PKCS11Pin, "pkcs11-pin", "", "PIN for logging into PKCS11 token. if not provided, will be prompted interactively")
166 fs.StringVar(&cli.PKCS11TokenSlot, "pkcs11-token-slot", "", "slot number of PKCS11 token (only use one of slot, label, or serial)")
167 fs.StringVar(&cli.PKCS11TokenLabel, "pkcs11-token-label", "", "label of PKCS11 token (only use one of slot, label, or serial)")
168 fs.StringVar(&cli.PKCS11TokenSerial, "pkcs11-token-serial", "", "serial number of PKCS11 token (only use one of slot, label, or serial)")
169 fs.StringVar(&cli.PKCS11KeypairLabel, "pkcs11-keypair-label", "", "label of signing keypair on PKCS11 token")
170 fs.StringVar(&cli.PKCS11KeypairID, "pkcs11-keypair-id", "", "id of signing keypair on PKCS11 token")
171 fs.StringVar(&cli.AppBundleID, "app-bundle-id", "", "bundle id of an app that we facilitate oauth login for")
172 fs.StringVar(&cli.StreamerName, "streamer-name", "", "name of the person streaming from this streamplace node")
173 fs.StringVar(&cli.FrontendProxy, "dev-frontend-proxy", "", "(FOR DEVELOPMENT ONLY) proxy frontend requests to this address instead of using the bundled frontend")
174 fs.BoolVar(&cli.PublicOAuth, "dev-public-oauth", false, "(FOR DEVELOPMENT ONLY) enable public oauth login for http://127.0.0.1 development")
175 fs.StringVar(&cli.LivepeerGatewayURL, "livepeer-gateway-url", "", "URL of the Livepeer Gateway to use for transcoding")
176 fs.BoolVar(&cli.LivepeerGateway, "livepeer-gateway", false, "enable embedded Livepeer Gateway")
177 fs.BoolVar(&cli.WideOpen, "wide-open", false, "allow ALL streams to be uploaded to this node (not recommended for production)")
178 cli.StringSliceFlag(fs, &cli.AllowedStreams, "allowed-streams", "", "if set, only allow these addresses or atproto DIDs to upload to this node")
179 cli.StringSliceFlag(fs, &cli.Peers, "peers", "", "other streamplace nodes to replicate to")
180 cli.StringSliceFlag(fs, &cli.Redirects, "redirects", "", "http 302s /path/one:/path/two,/path/three:/path/four")
181 cli.DebugFlag(fs, &cli.Debug, "debug", "", "modified log verbosity for specific functions or files in form func=ToHLS:3,file=gstreamer.go:4")
182 fs.BoolVar(&cli.TestStream, "test-stream", false, "run a built-in test stream on boot")
183 fs.BoolVar(&cli.NoFirehose, "no-firehose", false, "disable the bluesky firehose")
184 fs.BoolVar(&cli.PrintChat, "print-chat", false, "print chat messages to stdout")
185 fs.StringVar(&cli.WHIPTest, "whip-test", "", "run a WHIP self-test with the given parameters")
186 fs.StringVar(&cli.RelayHost, "relay-host", "wss://bsky.network", "websocket url for relay firehose")
187 fs.Bool("insecure", false, "DEPRECATED, does nothing.")
188 fs.StringVar(&cli.Color, "color", "", "'true' to enable colorized logging, 'false' to disable")
189 fs.StringVar(&cli.BroadcasterHost, "broadcaster-host", "", "public host for the broadcaster group that this node is a part of (excluding https:// e.g. stream.place)")
190 fs.StringVar(&cli.XXDeprecatedPublicHost, "public-host", "", "deprecated, use broadcaster-host or server-host instead as appropriate")
191 fs.StringVar(&cli.ServerHost, "server-host", "", "public host for this particular physical streamplace node. defaults to broadcaster-host and only must be set for multi-node broadcasters")
192 fs.BoolVar(&cli.Thumbnail, "thumbnail", true, "enable thumbnail generation")
193 fs.BoolVar(&cli.SmearAudio, "smear-audio", false, "enable audio smearing to create 'perfect' segment timestamps")
194 fs.BoolVar(&cli.ExternalSigning, "external-signing", false, "enable external signing via exec (prevents potential memory leak)")
195 fs.StringVar(&cli.TracingEndpoint, "tracing-endpoint", "", "gRPC endpoint to send traces to")
196 fs.IntVar(&cli.RateLimitPerSecond, "rate-limit-per-second", 0, "rate limit for requests per second per ip")
197 fs.IntVar(&cli.RateLimitBurst, "rate-limit-burst", 0, "rate limit burst for requests per ip")
198 fs.IntVar(&cli.RateLimitWebsocket, "rate-limit-websocket", 10, "number of concurrent websocket connections allowed per ip")
199 fs.StringVar(&cli.RTMPServerAddon, "rtmp-server-addon", "", "address of external RTMP server to forward streams to")
200 fs.StringVar(&cli.RtmpsAddr, "rtmps-addr", ":1935", "address to listen for RTMPS connections")
201 cli.JSONFlag(fs, &cli.DiscordWebhooks, "discord-webhooks", "[]", "JSON array of Discord webhooks to send notifications to")
202 fs.BoolVar(&cli.NewWebRTCPlayback, "new-webrtc-playback", true, "enable new webrtc playback")
203 fs.StringVar(&cli.AppleTeamID, "apple-team-id", "", "apple team id for deep linking")
204 fs.StringVar(&cli.AndroidCertFingerprint, "android-cert-fingerprint", "", "android cert fingerprint for deep linking")
205 cli.StringSliceFlag(fs, &cli.Labelers, "labelers", "", "did of labelers that this instance should subscribe to")
206 fs.StringVar(&cli.AtprotoDID, "atproto-did", "", "atproto did to respond to on /.well-known/atproto-did (default did:web:PUBLIC_HOST)")
207 cli.JSONFlag(fs, &cli.ContentFilters, "content-filters", "{}", "JSON content filtering rules")
208 fs.BoolVar(&cli.LivepeerHelp, "livepeer-help", false, "print help for livepeer flags and exit")
209 fs.StringVar(&cli.PLCURL, "plc-url", "https://plc.directory", "url of the plc directory")
210 fs.BoolVar(&cli.SQLLogging, "sql-logging", false, "enable sql logging")
211 fs.StringVar(&cli.SentryDSN, "sentry-dsn", "", "sentry dsn for error reporting")
212 fs.BoolVar(&cli.LivepeerDebug, "livepeer-debug", false, "log livepeer segments to $SP_DATA_DIR/livepeer-debug")
213 cli.StringSliceFlag(fs, &cli.Tickets, "tickets", "[]", "tickets to join the swarm with")
214 fs.StringVar(&cli.IrohTopic, "iroh-topic", "", "topic to use for the iroh swarm (must be 32 bytes in hex)")
215
216 lpFlags := flag.NewFlagSet("livepeer", flag.ContinueOnError)
217 _ = starter.NewLivepeerConfig(lpFlags)
218 lpFlags.VisitAll(func(f *flag.Flag) {
219 adapted := LivepeerFlags.CamelToSnake[f.Name]
220 fs.Var(f.Value, fmt.Sprintf("livepeer.%s", adapted), f.Usage)
221 })
222
223 if runtime.GOOS == "linux" {
224 fs.BoolVar(&cli.NoMist, "no-mist", true, "Disable MistServer")
225 fs.IntVar(&cli.MistAdminPort, "mist-admin-port", 14242, "MistServer admin port (internal use only)")
226 fs.IntVar(&cli.MistRTMPPort, "mist-rtmp-port", 11935, "MistServer RTMP port (internal use only)")
227 fs.IntVar(&cli.MistHTTPPort, "mist-http-port", 18080, "MistServer HTTP port (internal use only)")
228 }
229 return fs
230}
231
232var StreamplaceSchemePrefix = "streamplace://"
233
234func (cli *CLI) OwnPublicURL() string {
235 // No errors because we know it's valid from AddrFlag
236 host, port, _ := net.SplitHostPort(cli.HTTPAddr)
237
238 ip := net.ParseIP(host)
239 if host == "" || ip.IsUnspecified() {
240 host = "127.0.0.1"
241 }
242 addr := net.JoinHostPort(host, port)
243 return fmt.Sprintf("http://%s", addr)
244}
245
246func (cli *CLI) OwnInternalURL() string {
247 // No errors because we know it's valid from AddrFlag
248 host, port, _ := net.SplitHostPort(cli.HTTPInternalAddr)
249
250 ip := net.ParseIP(host)
251 if ip.IsUnspecified() {
252 host = "127.0.0.1"
253 }
254 addr := net.JoinHostPort(host, port)
255 return fmt.Sprintf("http://%s", addr)
256}
257
258func (cli *CLI) ParseSigningKey() (*rsa.PrivateKey, error) {
259 bs, err := os.ReadFile(cli.SigningKeyPath)
260 if err != nil {
261 return nil, err
262 }
263 block, _ := pem.Decode(bs)
264 if block == nil {
265 return nil, fmt.Errorf("no RSA key found in signing key")
266 }
267 key, err := x509.ParsePKCS1PrivateKey(block.Bytes)
268 if err != nil {
269 return nil, err
270 }
271 return key, nil
272}
273
274func RandomTrailer(length int) string {
275 const charset = "abcdefghijklmnopqrstuvwxyz0123456789"
276
277 res := make([]byte, length)
278 for i := 0; i < length; i++ {
279 res[i] = charset[rand.IntN(len(charset))]
280 }
281 return string(res)
282}
283
284func DefaultDataDir() string {
285 home, err := os.UserHomeDir()
286 if err != nil {
287 // not fatal unless the user doesn't set one later
288 return ""
289 }
290 return filepath.Join(home, ".streamplace")
291}
292
293var GormLogger = slogGorm.New(
294 slogGorm.WithHandler(tint.NewHandler(os.Stderr, &tint.Options{
295 TimeFormat: time.RFC3339,
296 })),
297 slogGorm.WithTraceAll(),
298)
299
300func DisableSQLLogging() {
301 GormLogger = slogGorm.New(
302 slogGorm.WithHandler(tint.NewHandler(os.Stderr, &tint.Options{
303 TimeFormat: time.RFC3339,
304 })),
305 )
306}
307
308func EnableSQLLogging() {
309 GormLogger = slogGorm.New(
310 slogGorm.WithHandler(tint.NewHandler(os.Stderr, &tint.Options{
311 TimeFormat: time.RFC3339,
312 })),
313 slogGorm.WithTraceAll(),
314 )
315}
316
317func (cli *CLI) Parse(fs *flag.FlagSet, args []string) error {
318 err := ff.Parse(
319 fs, args,
320 ff.WithEnvVarPrefix("SP"),
321 )
322 if err != nil {
323 return err
324 }
325 if cli.DataDir == "" {
326 return fmt.Errorf("could not determine default data dir (no $HOME) and none provided, please set --data-dir")
327 }
328 if cli.LivepeerGateway && cli.LivepeerGatewayURL != "" {
329 return fmt.Errorf("defining both livepeer-gateway and livepeer-gateway-url doesn't make sense. do you want an embedded gateway or an external one?")
330 }
331 if cli.LivepeerGateway {
332 log.MonkeypatchStderr()
333 gatewayPath := cli.DataFilePath([]string{"livepeer", "gateway"})
334 err = fs.Set("livepeer.rtmp-addr", "127.0.0.1:0")
335 if err != nil {
336 return err
337 }
338 err = fs.Set("livepeer.data-dir", gatewayPath)
339 if err != nil {
340 return err
341 }
342 err = fs.Set("livepeer.gateway", "true")
343 if err != nil {
344 return err
345 }
346 httpAddrFlag := fs.Lookup("livepeer.http-addr")
347 if httpAddrFlag == nil {
348 return fmt.Errorf("livepeer.http-addr not found")
349 }
350 httpAddr := httpAddrFlag.Value.String()
351 if httpAddr == "" {
352 httpAddr = "127.0.0.1:8935"
353 err = fs.Set("livepeer.http-addr", httpAddr)
354 if err != nil {
355 return err
356 }
357 }
358 cli.LivepeerGatewayURL = fmt.Sprintf("http://%s", httpAddr)
359 }
360 for _, dest := range cli.dataDirFlags {
361 *dest = strings.Replace(*dest, SPDataDir, cli.DataDir, 1)
362 }
363 if !cli.SQLLogging {
364 DisableSQLLogging()
365 } else {
366 EnableSQLLogging()
367 }
368 if cli.XXDeprecatedPublicHost != "" && cli.BroadcasterHost == "" {
369 log.Warn(context.Background(), "public-host is deprecated, use broadcaster-host or server-host instead as appropriate")
370 cli.BroadcasterHost = cli.XXDeprecatedPublicHost
371 }
372 if cli.ServerHost == "" && cli.BroadcasterHost != "" {
373 cli.ServerHost = cli.BroadcasterHost
374 }
375 if cli.PublicOAuth {
376 log.Warn(context.Background(), "--dev-public-oauth is set, this is not recommended for production")
377 }
378 if cli.FirebaseServiceAccount != "" && cli.FirebaseServiceAccountFile != "" {
379 return fmt.Errorf("defining both firebase-service-account and firebase-service-account-file doesn't make sense. do you want a base64-encoded string or a file?")
380 }
381 if cli.FirebaseServiceAccountFile != "" {
382 bs, err := os.ReadFile(cli.FirebaseServiceAccountFile)
383 if err != nil {
384 return err
385 }
386 cli.FirebaseServiceAccount = string(bs)
387 }
388 return nil
389}
390
391func (cli *CLI) DataFilePath(fpath []string) string {
392 if cli.DataDir == "" {
393 panic("no data dir configured")
394 }
395 // windows does not like colons
396 safe := []string{}
397 for _, p := range fpath {
398 safe = append(safe, strings.ReplaceAll(p, ":", "-"))
399 }
400 fpath = append([]string{cli.DataDir}, safe...)
401 fdpath := filepath.Join(fpath...)
402 return fdpath
403}
404
405// does a file exist in our data dir?
406func (cli *CLI) DataFileExists(fpath []string) (bool, error) {
407 ddpath := cli.DataFilePath(fpath)
408 _, err := os.Stat(ddpath)
409 if err == nil {
410 return true, nil
411 }
412 if errors.Is(err, os.ErrNotExist) {
413 return false, nil
414 }
415 return false, err
416}
417
418// write a file to our data dir
419func (cli *CLI) DataFileWrite(fpath []string, r io.Reader, overwrite bool) error {
420 fd, err := cli.DataFileCreate(fpath, overwrite)
421 if err != nil {
422 return err
423 }
424 defer fd.Close()
425 _, err = io.Copy(fd, r)
426 if err != nil {
427 return err
428 }
429
430 return nil
431}
432
433// create a file in our data dir. don't forget to close it!
434func (cli *CLI) DataFileCreate(fpath []string, overwrite bool) (*os.File, error) {
435 ddpath := cli.DataFilePath(fpath)
436 if !overwrite {
437 exists, err := cli.DataFileExists(fpath)
438 if err != nil {
439 return nil, err
440 }
441 if exists {
442 return nil, fmt.Errorf("refusing to overwrite file that exists: %s", ddpath)
443 }
444 }
445 if len(fpath) > 1 {
446 dirs, _ := filepath.Split(ddpath)
447 err := os.MkdirAll(dirs, os.ModePerm)
448 if err != nil {
449 return nil, fmt.Errorf("error creating subdirectories for %s: %w", ddpath, err)
450 }
451 }
452 return os.Create(ddpath)
453}
454
455// get a path to a segment file in our database
456func (cli *CLI) SegmentFilePath(user string, file string) (string, error) {
457 ext := filepath.Ext(file)
458 base := strings.TrimSuffix(file, ext)
459 aqt, err := aqtime.FromString(base)
460 if err != nil {
461 return "", err
462 }
463 fname := fmt.Sprintf("%s%s", aqt.FileSafeString(), ext)
464 yr, mon, day, hr, min, _, _ := aqt.Parts()
465 return cli.DataFilePath([]string{SegmentsDir, user, yr, mon, day, hr, min, fname}), nil
466}
467
468// get a path to a segment file in our database
469func (cli *CLI) HLSDir(user string) (string, error) {
470 return cli.DataFilePath([]string{SegmentsDir, "hls", user}), nil
471}
472
473// create a segment file in our database
474func (cli *CLI) SegmentFileCreate(user string, aqt aqtime.AQTime, ext string) (*os.File, error) {
475 fname := fmt.Sprintf("%s.%s", aqt.FileSafeString(), ext)
476 yr, mon, day, hr, min, _, _ := aqt.Parts()
477 return cli.DataFileCreate([]string{SegmentsDir, user, yr, mon, day, hr, min, fname}, false)
478}
479
480// read a file from our data dir
481func (cli *CLI) DataFileRead(fpath []string, w io.Writer) error {
482 ddpath := cli.DataFilePath(fpath)
483
484 fd, err := os.Open(ddpath)
485 if err != nil {
486 return err
487 }
488 _, err = io.Copy(w, fd)
489 if err != nil {
490 return err
491 }
492
493 return nil
494}
495
496func (cli *CLI) DataDirFlag(fs *flag.FlagSet, dest *string, name, defaultValue, usage string) {
497 cli.dataDirFlags = append(cli.dataDirFlags, dest)
498 *dest = filepath.Join(SPDataDir, defaultValue)
499 usage = fmt.Sprintf(`%s (default: "%s")`, usage, *dest)
500 fs.Func(name, usage, func(s string) error {
501 *dest = s
502 return nil
503 })
504}
505
506func (cli *CLI) HasMist() bool {
507 return runtime.GOOS == "linux"
508}
509
510// type for comma-separated ethereum addresses
511func (cli *CLI) AddressSliceFlag(fs *flag.FlagSet, dest *[]aqpub.Pub, name, defaultValue, usage string) {
512 *dest = []aqpub.Pub{}
513 usage = fmt.Sprintf(`%s (default: "%s")`, usage, *dest)
514 fs.Func(name, usage, func(s string) error {
515 if s == "" {
516 return nil
517 }
518 strs := strings.Split(s, ",")
519 for _, str := range strs {
520 pub, err := aqpub.FromHexString(str)
521 if err != nil {
522 return err
523 }
524 *dest = append(*dest, pub)
525 }
526 return nil
527 })
528}
529
530func (cli *CLI) StringSliceFlag(fs *flag.FlagSet, dest *[]string, name, defaultValue, usage string) {
531 *dest = []string{}
532 usage = fmt.Sprintf(`%s (default: "%s")`, usage, *dest)
533 fs.Func(name, usage, func(s string) error {
534 if s == "" {
535 return nil
536 }
537 strs := strings.Split(s, ",")
538 *dest = append(*dest, strs...)
539 return nil
540 })
541}
542
543func (cli *CLI) JSONFlag(fs *flag.FlagSet, dest any, name, defaultValue, usage string) {
544 usage = fmt.Sprintf(`%s (default: "%s")`, usage, defaultValue)
545 fs.Func(name, usage, func(s string) error {
546 if s == "" {
547 return nil
548 }
549 return json.Unmarshal([]byte(s), dest)
550 })
551}
552
553// debug flag for turning func=ToHLS:3,file=gstreamer.go:4 into {"func": {"ToHLS": 3}, "file": {"gstreamer.go": 4}}
554func (cli *CLI) DebugFlag(fs *flag.FlagSet, dest *map[string]map[string]int, name, defaultValue, usage string) {
555 *dest = map[string]map[string]int{}
556 fs.Func(name, usage, func(s string) error {
557 if s == "" {
558 return nil
559 }
560 pairs := strings.Split(s, ",")
561 for _, pair := range pairs {
562 scoreSplit := strings.Split(pair, ":")
563 if len(scoreSplit) != 2 {
564 return fmt.Errorf("invalid debug flag: %s", pair)
565 }
566 score, err := strconv.Atoi(scoreSplit[1])
567 if err != nil {
568 return fmt.Errorf("invalid debug flag: %s", pair)
569 }
570 selectorSplit := strings.Split(scoreSplit[0], "=")
571 if len(selectorSplit) != 2 {
572 return fmt.Errorf("invalid debug flag: %s", pair)
573 }
574 _, ok := (*dest)[selectorSplit[0]]
575 if !ok {
576 (*dest)[selectorSplit[0]] = map[string]int{}
577 }
578 (*dest)[selectorSplit[0]][selectorSplit[1]] = score
579 }
580
581 return nil
582 })
583}
584
585func (cli *CLI) StreamIsAllowed(did string) error {
586 if cli.WideOpen {
587 return nil
588 }
589 // if the user set no test streams, anyone can stream
590 openServer := len(cli.AllowedStreams) == 0 || (cli.TestStream && len(cli.AllowedStreams) == 1)
591 // but only valid atproto accounts! did:key is only allowed for our local test stream
592 isDIDKey := strings.HasPrefix(did, constants.DID_KEY_PREFIX)
593 if openServer && !isDIDKey {
594 return nil
595 }
596 for _, a := range cli.AllowedStreams {
597 if a == did {
598 return nil
599 }
600 }
601 return fmt.Errorf("user is not allowed to stream")
602}
603
604func (cli *CLI) MyDID() string {
605 return fmt.Sprintf("did:web:%s", cli.BroadcasterHost)
606}