refactor(geoip): reconcile geoip system (#31)
This commit is contained in:
@@ -18,8 +18,6 @@ FLUXER_GATEWAY_RPC_PORT=8081
|
||||
FLUXER_MEDIA_PROXY_PORT=8080
|
||||
FLUXER_ADMIN_PORT=8080
|
||||
FLUXER_MARKETING_PORT=8080
|
||||
FLUXER_GEOIP_PORT=8080
|
||||
GEOIP_HOST=geoip:8080
|
||||
|
||||
FLUXER_PATH_GATEWAY=/gateway
|
||||
FLUXER_PATH_ADMIN=/admin
|
||||
@@ -152,7 +150,6 @@ CLAMAV_PORT=3310
|
||||
|
||||
TENOR_API_KEY=
|
||||
YOUTUBE_API_KEY=
|
||||
IPINFO_TOKEN=
|
||||
|
||||
SECRET_KEY_BASE=
|
||||
GATEWAY_RPC_SECRET=
|
||||
|
||||
@@ -28,13 +28,6 @@
|
||||
reverse_proxy admin:8080
|
||||
}
|
||||
|
||||
@geoip path /geoip/*
|
||||
handle @geoip {
|
||||
handle_path /geoip/* {
|
||||
reverse_proxy geoip:8080
|
||||
}
|
||||
}
|
||||
|
||||
@marketing path /marketing /marketing/*
|
||||
handle @marketing {
|
||||
uri strip_prefix /marketing
|
||||
|
||||
@@ -145,19 +145,6 @@ services:
|
||||
- fluxer-shared
|
||||
restart: on-failure
|
||||
|
||||
geoip:
|
||||
image: golang:1.25.5
|
||||
working_dir: /workspace
|
||||
command: bash -c "mkdir -p /data && if [ ! -f /data/ipinfo_lite.mmdb ] && [ -n \"$$IPINFO_TOKEN\" ]; then echo 'Downloading GeoIP database...'; curl -fsSL -o /data/ipinfo_lite.mmdb \"https://ipinfo.io/data/ipinfo_lite.mmdb?token=$$IPINFO_TOKEN\" && echo 'GeoIP database downloaded'; fi && go run ."
|
||||
env_file:
|
||||
- ./.env
|
||||
volumes:
|
||||
- ../fluxer_geoip:/workspace
|
||||
- ./geoip_data:/data
|
||||
networks:
|
||||
- fluxer-shared
|
||||
restart: on-failure
|
||||
|
||||
gateway:
|
||||
image: erlang:28-slim
|
||||
working_dir: /workspace
|
||||
|
||||
34
dev/main.go
34
dev/main.go
@@ -1,34 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"fluxer.dev/dev/pkg/commands"
|
||||
)
|
||||
|
||||
func main() {
|
||||
if err := commands.NewRootCmd().Execute(); err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
@@ -1,305 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"fluxer.dev/dev/pkg/integrations"
|
||||
"fluxer.dev/dev/pkg/utils"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultComposeFile = "dev/compose.yaml"
|
||||
defaultEnvFile = "dev/.env"
|
||||
)
|
||||
|
||||
// NewRootCmd creates the root command
|
||||
func NewRootCmd() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "devctl",
|
||||
Short: "Fluxer development control tool",
|
||||
Long: "Docker Compose wrapper and development utilities for Fluxer.",
|
||||
}
|
||||
|
||||
cmd.AddCommand(
|
||||
NewUpCmd(),
|
||||
NewDownCmd(),
|
||||
NewRestartCmd(),
|
||||
NewLogsCmd(),
|
||||
NewPsCmd(),
|
||||
NewExecCmd(),
|
||||
NewShellCmd(),
|
||||
|
||||
NewLivekitSyncCmd(),
|
||||
NewGeoIPDownloadCmd(),
|
||||
NewEnsureNetworkCmd(),
|
||||
)
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
// NewUpCmd starts services
|
||||
func NewUpCmd() *cobra.Command {
|
||||
var detach bool
|
||||
var build bool
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "up [services...]",
|
||||
Short: "Start services",
|
||||
Long: "Start all or specific services using docker compose",
|
||||
RunE: func(cmd *cobra.Command, services []string) error {
|
||||
args := []string{"--env-file", defaultEnvFile, "-f", defaultComposeFile, "up"}
|
||||
if detach {
|
||||
args = append(args, "-d")
|
||||
}
|
||||
if build {
|
||||
args = append(args, "--build")
|
||||
}
|
||||
args = append(args, services...)
|
||||
return runDockerCompose(args...)
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().BoolVarP(&detach, "detach", "d", true, "Run in background")
|
||||
cmd.Flags().BoolVar(&build, "build", false, "Build images before starting")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
// NewDownCmd stops and removes containers
|
||||
func NewDownCmd() *cobra.Command {
|
||||
var volumes bool
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "down",
|
||||
Short: "Stop and remove containers",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
dcArgs := []string{"--env-file", defaultEnvFile, "-f", defaultComposeFile, "down"}
|
||||
if volumes {
|
||||
dcArgs = append(dcArgs, "-v")
|
||||
}
|
||||
return runDockerCompose(dcArgs...)
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().BoolVarP(&volumes, "volumes", "v", false, "Remove volumes")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
// NewRestartCmd restarts services
|
||||
func NewRestartCmd() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "restart [services...]",
|
||||
Short: "Restart services",
|
||||
RunE: func(cmd *cobra.Command, services []string) error {
|
||||
args := []string{"--env-file", defaultEnvFile, "-f", defaultComposeFile, "restart"}
|
||||
args = append(args, services...)
|
||||
return runDockerCompose(args...)
|
||||
},
|
||||
}
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
// NewLogsCmd shows service logs
|
||||
func NewLogsCmd() *cobra.Command {
|
||||
var follow bool
|
||||
var tail string
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "logs [services...]",
|
||||
Short: "Show service logs",
|
||||
RunE: func(cmd *cobra.Command, services []string) error {
|
||||
args := []string{"--env-file", defaultEnvFile, "-f", defaultComposeFile, "logs"}
|
||||
if follow {
|
||||
args = append(args, "-f")
|
||||
}
|
||||
if tail != "" {
|
||||
args = append(args, "--tail", tail)
|
||||
}
|
||||
args = append(args, services...)
|
||||
return runDockerCompose(args...)
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().BoolVarP(&follow, "follow", "f", true, "Follow log output")
|
||||
cmd.Flags().StringVarP(&tail, "tail", "n", "100", "Number of lines to show from the end")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
// NewPsCmd lists containers
|
||||
func NewPsCmd() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "ps",
|
||||
Short: "List containers",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return runDockerCompose("--env-file", defaultEnvFile, "-f", defaultComposeFile, "ps")
|
||||
},
|
||||
}
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
// NewExecCmd executes a command in a running container
|
||||
func NewExecCmd() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "exec SERVICE COMMAND...",
|
||||
Short: "Execute a command in a running container",
|
||||
Args: cobra.MinimumNArgs(2),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
dcArgs := []string{"--env-file", defaultEnvFile, "-f", defaultComposeFile, "exec"}
|
||||
dcArgs = append(dcArgs, args...)
|
||||
return runDockerCompose(dcArgs...)
|
||||
},
|
||||
}
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
// NewShellCmd opens a shell in a container
|
||||
func NewShellCmd() *cobra.Command {
|
||||
var shell string
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "sh SERVICE",
|
||||
Short: "Open a shell in a container",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
service := args[0]
|
||||
return runDockerCompose("--env-file", defaultEnvFile, "-f", defaultComposeFile, "exec", service, shell)
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().StringVar(&shell, "shell", "sh", "Shell to use (sh, bash, etc.)")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
// NewLivekitSyncCmd syncs LiveKit configuration
|
||||
func NewLivekitSyncCmd() *cobra.Command {
|
||||
var envPath string
|
||||
var outputPath string
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "livekit-sync",
|
||||
Short: "Generate LiveKit configuration from environment variables",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
env, err := utils.ParseEnvFile(envPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read env file: %w", err)
|
||||
}
|
||||
|
||||
written, err := integrations.WriteLivekitFileFromEnv(outputPath, env)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !written {
|
||||
fmt.Println("⚠️ Voice/LiveKit is disabled - no config generated")
|
||||
return nil
|
||||
}
|
||||
|
||||
fmt.Printf("✅ LiveKit config written to %s\n", outputPath)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().StringVarP(&envPath, "env", "e", defaultEnvFile, "Environment file path")
|
||||
cmd.Flags().StringVarP(&outputPath, "output", "o", "dev/livekit.yaml", "Output path")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
// NewGeoIPDownloadCmd downloads GeoIP database
|
||||
func NewGeoIPDownloadCmd() *cobra.Command {
|
||||
var token string
|
||||
var envPath string
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "geoip-download",
|
||||
Short: "Download GeoIP database from IPInfo",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return integrations.DownloadGeoIP(token, envPath)
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().StringVar(&token, "token", "", "IPInfo API token")
|
||||
cmd.Flags().StringVarP(&envPath, "env", "e", defaultEnvFile, "Env file to read token from")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
// NewEnsureNetworkCmd ensures the Docker network exists
|
||||
func NewEnsureNetworkCmd() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "ensure-network",
|
||||
Short: "Ensure the fluxer-shared Docker network exists",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return ensureNetwork()
|
||||
},
|
||||
}
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
// runDockerCompose runs a docker compose command
|
||||
func runDockerCompose(args ...string) error {
|
||||
cmd := exec.Command("docker", append([]string{"compose"}, args...)...)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
cmd.Stdin = os.Stdin
|
||||
return cmd.Run()
|
||||
}
|
||||
|
||||
// ensureNetwork ensures the fluxer-shared network exists
|
||||
func ensureNetwork() error {
|
||||
checkCmd := exec.Command("docker", "network", "ls", "--format", "{{.Name}}")
|
||||
output, err := checkCmd.Output()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to list networks: %w", err)
|
||||
}
|
||||
|
||||
networks := strings.Split(strings.TrimSpace(string(output)), "\n")
|
||||
for _, net := range networks {
|
||||
if net == "fluxer-shared" {
|
||||
fmt.Println("✅ fluxer-shared network already exists")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Println("Creating fluxer-shared network...")
|
||||
createCmd := exec.Command("docker", "network", "create", "fluxer-shared")
|
||||
createCmd.Stdout = os.Stdout
|
||||
createCmd.Stderr = os.Stderr
|
||||
if err := createCmd.Run(); err != nil {
|
||||
return fmt.Errorf("failed to create network: %w", err)
|
||||
}
|
||||
|
||||
fmt.Println("✅ fluxer-shared network created")
|
||||
return nil
|
||||
}
|
||||
@@ -1,95 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package integrations
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"fluxer.dev/dev/pkg/utils"
|
||||
)
|
||||
|
||||
const (
|
||||
DefaultGeoIPDir = "dev/geoip"
|
||||
DefaultGeoIPFile = "country_asn.mmdb"
|
||||
)
|
||||
|
||||
// DownloadGeoIP downloads the GeoIP database from IPInfo
|
||||
func DownloadGeoIP(tokenFlag, envPath string) error {
|
||||
token := strings.TrimSpace(tokenFlag)
|
||||
if token == "" {
|
||||
token = strings.TrimSpace(os.Getenv("IPINFO_TOKEN"))
|
||||
}
|
||||
|
||||
if token == "" && envPath != "" {
|
||||
env, err := utils.ParseEnvFile(envPath)
|
||||
if err == nil {
|
||||
token = strings.TrimSpace(env["IPINFO_TOKEN"])
|
||||
}
|
||||
}
|
||||
|
||||
if token == "" {
|
||||
return errors.New("IPInfo token required; provide via --token, IPINFO_TOKEN env var, or the config/env")
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(DefaultGeoIPDir, 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
outPath := filepath.Join(DefaultGeoIPDir, DefaultGeoIPFile)
|
||||
u := fmt.Sprintf("https://ipinfo.io/data/free/country_asn.mmdb?token=%s", url.QueryEscape(token))
|
||||
|
||||
fmt.Printf("Downloading GeoIP database to %s...\n", outPath)
|
||||
|
||||
resp, err := http.Get(u)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to download GeoIP db: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
body, _ := io.ReadAll(io.LimitReader(resp.Body, 512))
|
||||
return fmt.Errorf("unexpected response (%d): %s", resp.StatusCode, strings.TrimSpace(string(body)))
|
||||
}
|
||||
|
||||
f, err := os.Create(outPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
n, err := io.Copy(f, resp.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if n == 0 {
|
||||
return errors.New("downloaded GeoIP file is empty; check your IPInfo token")
|
||||
}
|
||||
|
||||
fmt.Printf("✅ GeoIP database downloaded (%d bytes).\n", n)
|
||||
fmt.Println()
|
||||
fmt.Println("If you're running a GeoIP service container, restart it so it picks up the new database.")
|
||||
return nil
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package integrations
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// WriteLivekitFileFromEnv writes LiveKit configuration from environment variables
|
||||
func WriteLivekitFileFromEnv(path string, env map[string]string) (bool, error) {
|
||||
voiceEnabled := strings.ToLower(strings.TrimSpace(env["VOICE_ENABLED"])) == "true"
|
||||
if !voiceEnabled {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
apiKey := strings.TrimSpace(env["LIVEKIT_API_KEY"])
|
||||
apiSecret := strings.TrimSpace(env["LIVEKIT_API_SECRET"])
|
||||
webhookURL := strings.TrimSpace(env["LIVEKIT_WEBHOOK_URL"])
|
||||
|
||||
if apiKey == "" || apiSecret == "" || webhookURL == "" {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
redisURL := strings.TrimSpace(env["REDIS_URL"])
|
||||
redisAddr := strings.TrimPrefix(redisURL, "redis://")
|
||||
if redisAddr == "" {
|
||||
redisAddr = "redis:6379"
|
||||
}
|
||||
|
||||
yaml := fmt.Sprintf(`port: 7880
|
||||
|
||||
redis:
|
||||
address: "%s"
|
||||
db: 0
|
||||
|
||||
keys:
|
||||
"%s": "%s"
|
||||
|
||||
rtc:
|
||||
tcp_port: 7881
|
||||
|
||||
webhook:
|
||||
api_key: "%s"
|
||||
urls:
|
||||
- "%s"
|
||||
|
||||
room:
|
||||
auto_create: true
|
||||
max_participants: 100
|
||||
empty_timeout: 300
|
||||
|
||||
development: true
|
||||
`, redisAddr, apiKey, apiSecret, apiKey, webhookURL)
|
||||
|
||||
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
||||
return false, err
|
||||
}
|
||||
if err := os.WriteFile(path, []byte(yaml), 0o600); err != nil {
|
||||
return false, err
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
@@ -1,154 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package utils
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"crypto/rand"
|
||||
"encoding/base32"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// FileExists checks if a file exists at the given path
|
||||
func FileExists(path string) bool {
|
||||
_, err := os.Stat(path)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// BoolString converts a boolean to a string ("true" or "false")
|
||||
func BoolString(b bool) string {
|
||||
if b {
|
||||
return "true"
|
||||
}
|
||||
return "false"
|
||||
}
|
||||
|
||||
// FirstNonZeroInt returns the first non-zero integer from the provided values,
|
||||
// or the default value if all are zero
|
||||
func FirstNonZeroInt(values ...int) int {
|
||||
for _, v := range values {
|
||||
if v != 0 {
|
||||
return v
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// DefaultString returns the value if non-empty, otherwise returns the default
|
||||
func DefaultString(value, defaultValue string) string {
|
||||
if strings.TrimSpace(value) == "" {
|
||||
return defaultValue
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
// RandomString generates a random alphanumeric string of the given length
|
||||
func RandomString(length int) string {
|
||||
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||
b := make([]byte, length)
|
||||
if _, err := rand.Read(b); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
for i := range b {
|
||||
b[i] = charset[int(b[i])%len(charset)]
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
|
||||
// RandomBase32 generates a random base32-encoded string (without padding)
|
||||
func RandomBase32(byteLength int) string {
|
||||
b := make([]byte, byteLength)
|
||||
if _, err := rand.Read(b); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return strings.TrimRight(base32.StdEncoding.EncodeToString(b), "=")
|
||||
}
|
||||
|
||||
// GenerateSnowflake generates a snowflake ID
|
||||
// Format: timestamp (42 bits) + worker ID (10 bits) + sequence (12 bits)
|
||||
func GenerateSnowflake() string {
|
||||
const fluxerEpoch = 1420070400000
|
||||
timestamp := time.Now().UnixMilli() - fluxerEpoch
|
||||
workerID := int64(0)
|
||||
sequence := int64(0)
|
||||
snowflake := (timestamp << 22) | (workerID << 12) | sequence
|
||||
return fmt.Sprintf("%d", snowflake)
|
||||
}
|
||||
|
||||
// ValidateURL validates that a string is a valid URL
|
||||
func ValidateURL(urlStr string) error {
|
||||
if urlStr == "" {
|
||||
return fmt.Errorf("URL cannot be empty")
|
||||
}
|
||||
parsedURL, err := url.Parse(urlStr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid URL: %w", err)
|
||||
}
|
||||
if parsedURL.Scheme == "" {
|
||||
return fmt.Errorf("URL must have a scheme (http:// or https://)")
|
||||
}
|
||||
if parsedURL.Host == "" {
|
||||
return fmt.Errorf("URL must have a host")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ParseEnvFile parses a .env file and returns a map of key-value pairs
|
||||
func ParseEnvFile(path string) (map[string]string, error) {
|
||||
file, err := os.Open(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
env := make(map[string]string)
|
||||
scanner := bufio.NewScanner(file)
|
||||
|
||||
for scanner.Scan() {
|
||||
line := strings.TrimSpace(scanner.Text())
|
||||
|
||||
if line == "" || strings.HasPrefix(line, "#") {
|
||||
continue
|
||||
}
|
||||
|
||||
parts := strings.SplitN(line, "=", 2)
|
||||
if len(parts) != 2 {
|
||||
continue
|
||||
}
|
||||
|
||||
key := strings.TrimSpace(parts[0])
|
||||
value := strings.TrimSpace(parts[1])
|
||||
|
||||
if len(value) >= 2 {
|
||||
if (value[0] == '"' && value[len(value)-1] == '"') ||
|
||||
(value[0] == '\'' && value[len(value)-1] == '\'') {
|
||||
value = value[1 : len(value)-1]
|
||||
}
|
||||
}
|
||||
|
||||
env[key] = value
|
||||
}
|
||||
|
||||
return env, scanner.Err()
|
||||
}
|
||||
154
dev/setup.sh
154
dev/setup.sh
@@ -1,154 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Copyright (C) 2026 Fluxer Contributors
|
||||
#
|
||||
# This file is part of Fluxer.
|
||||
#
|
||||
# Fluxer is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Fluxer is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m'
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
check_command() {
|
||||
if command -v "$1" &> /dev/null; then
|
||||
echo -e "${GREEN}[OK]${NC} $1 is installed"
|
||||
return 0
|
||||
else
|
||||
echo -e "${RED}[MISSING]${NC} $1 is not installed"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
check_node_version() {
|
||||
if command -v node &> /dev/null; then
|
||||
NODE_VERSION=$(node -v | sed 's/v//' | cut -d. -f1)
|
||||
if [ "$NODE_VERSION" -ge 20 ]; then
|
||||
echo -e "${GREEN}[OK]${NC} Node.js $(node -v) is installed"
|
||||
return 0
|
||||
else
|
||||
echo -e "${YELLOW}[WARN]${NC} Node.js $(node -v) is installed, but v20+ is recommended"
|
||||
return 0
|
||||
fi
|
||||
else
|
||||
echo -e "${RED}[MISSING]${NC} Node.js is not installed"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
echo "=== Fluxer Development Setup ==="
|
||||
echo ""
|
||||
|
||||
echo "Checking prerequisites..."
|
||||
echo ""
|
||||
|
||||
MISSING=0
|
||||
|
||||
check_node_version || MISSING=1
|
||||
check_command pnpm || MISSING=1
|
||||
check_command docker || MISSING=1
|
||||
check_command rustc || echo -e "${YELLOW}[OPTIONAL]${NC} Rust is not installed (needed for fluxer_app WASM modules)"
|
||||
check_command wasm-pack || echo -e "${YELLOW}[OPTIONAL]${NC} wasm-pack is not installed (needed for fluxer_app WASM modules)"
|
||||
check_command go || echo -e "${YELLOW}[OPTIONAL]${NC} Go is not installed (needed for fluxer_geoip)"
|
||||
|
||||
echo ""
|
||||
|
||||
if [ "$MISSING" -eq 1 ]; then
|
||||
echo -e "${RED}Some required dependencies are missing. Please install them before continuing.${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Creating Docker network if needed..."
|
||||
if docker network inspect fluxer-shared &> /dev/null; then
|
||||
echo -e "${GREEN}[OK]${NC} Docker network 'fluxer-shared' already exists"
|
||||
else
|
||||
docker network create fluxer-shared
|
||||
echo -e "${GREEN}[OK]${NC} Created Docker network 'fluxer-shared'"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
|
||||
if [ ! -f "$SCRIPT_DIR/.env" ]; then
|
||||
echo "Creating .env from .env.example..."
|
||||
cp "$SCRIPT_DIR/.env.example" "$SCRIPT_DIR/.env"
|
||||
echo -e "${GREEN}[OK]${NC} Created .env file"
|
||||
else
|
||||
echo -e "${GREEN}[OK]${NC} .env file already exists"
|
||||
fi
|
||||
|
||||
mkdir -p "$SCRIPT_DIR/geoip_data"
|
||||
if [ ! -f "$SCRIPT_DIR/geoip_data/ipinfo_lite.mmdb" ]; then
|
||||
echo -e "${YELLOW}[INFO]${NC} GeoIP database not found."
|
||||
echo " Set IPINFO_TOKEN in .env and run the geoip service to download it,"
|
||||
echo " or manually download ipinfo_lite.mmdb to dev/geoip_data/"
|
||||
else
|
||||
echo -e "${GREEN}[OK]${NC} GeoIP database exists"
|
||||
fi
|
||||
|
||||
if [ ! -f "$SCRIPT_DIR/livekit.yaml" ]; then
|
||||
echo "Creating default livekit.yaml..."
|
||||
cat > "$SCRIPT_DIR/livekit.yaml" << 'EOF'
|
||||
port: 7880
|
||||
|
||||
redis:
|
||||
address: 'redis:6379'
|
||||
db: 0
|
||||
|
||||
keys:
|
||||
'e1dG953yAoJPIsK1dzfTWAKMNE9gmnPL': 'rCtIICXHtAwSAJ4glb11jARcXCCgMTGvvTKLIlpD0pEoANLgjCNPD1Ysm8uWhQTB'
|
||||
|
||||
rtc:
|
||||
tcp_port: 7881
|
||||
|
||||
webhook:
|
||||
api_key: 'e1dG953yAoJPIsK1dzfTWAKMNE9gmnPL'
|
||||
urls:
|
||||
- 'http://api:8080/webhooks/livekit'
|
||||
|
||||
room:
|
||||
auto_create: true
|
||||
max_participants: 100
|
||||
empty_timeout: 300
|
||||
|
||||
development: true
|
||||
EOF
|
||||
echo -e "${GREEN}[OK]${NC} Created livekit.yaml"
|
||||
else
|
||||
echo -e "${GREEN}[OK]${NC} livekit.yaml already exists"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "=== Setup Complete ==="
|
||||
echo ""
|
||||
echo "Next steps:"
|
||||
echo ""
|
||||
echo "1. Start data stores:"
|
||||
echo " docker compose -f compose.data.yaml up -d"
|
||||
echo ""
|
||||
echo "2. Start app services:"
|
||||
echo " docker compose up -d api worker media gateway admin marketing docs geoip metrics caddy"
|
||||
echo ""
|
||||
echo "3. Run the frontend on your host machine:"
|
||||
echo " cd ../fluxer_app && pnpm install && pnpm dev"
|
||||
echo ""
|
||||
echo "4. Access the app at: http://localhost:8088"
|
||||
echo ""
|
||||
echo "Optional: Start Cloudflare tunnel:"
|
||||
echo " docker compose up -d cloudflared"
|
||||
echo ""
|
||||
Reference in New Issue
Block a user