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}