From 2e007b5076ae1807215b41156b3573fb318881f5 Mon Sep 17 00:00:00 2001 From: hampus-fluxer Date: Mon, 5 Jan 2026 23:19:05 +0100 Subject: [PATCH] refactor(geoip): reconcile geoip system (#31) --- .github/workflows/deploy-api.yaml | 2 - .github/workflows/deploy-geoip.yaml | 124 ------- .github/workflows/deploy-marketing.yaml | 1 - .github/workflows/update-geoip-db.yaml | 81 ----- dev/.env.example | 3 - dev/Caddyfile.dev | 7 - dev/compose.yaml | 13 - dev/main.go | 34 -- dev/pkg/commands/commands.go | 305 ------------------ dev/pkg/integrations/geoip.go | 95 ------ dev/pkg/integrations/livekit.go | 82 ----- dev/pkg/utils/helpers.go | 154 --------- dev/setup.sh | 154 --------- fluxer_admin/src/fluxer_admin/api/users.gleam | 4 +- .../pages/pending_verifications_page.gleam | 30 -- .../pages/user_detail/tabs/account.gleam | 5 +- fluxer_api/src/App.ts | 2 - fluxer_api/src/Config.ts | 9 - fluxer_api/src/admin/models/UserTypes.ts | 4 +- .../services/AdminUserSecurityService.ts | 31 +- fluxer_api/src/auth/AuthModel.ts | 56 ++-- .../src/auth/services/AuthLoginService.ts | 4 +- .../auth/services/AuthRegistrationService.ts | 45 ++- .../src/auth/services/AuthSessionService.ts | 55 +--- fluxer_api/src/database/types/AuthTypes.ts | 12 +- fluxer_api/src/debug/DebugController.ts | 63 ---- fluxer_api/src/models/AuthSession.ts | 18 +- fluxer_api/src/rpc/RpcModel.ts | 8 + fluxer_api/src/rpc/RpcService.ts | 28 +- fluxer_api/src/test/setup.ts | 2 - fluxer_api/src/utils/IpUtils.test.ts | 136 -------- fluxer_api/src/utils/IpUtils.ts | 185 +++++------ fluxer_api/src/utils/UserAgentUtils.ts | 61 ++++ .../src/worker/tasks/harvestUserData.ts | 24 +- .../scripts/cmd/set-build-channel/main.go | 66 ---- .../src/components/modals/tabs/DevicesTab.tsx | 19 +- fluxer_app/src/records/AuthSessionRecord.tsx | 4 +- ...74407_add_auth_session_user_agent_cols.cql | 2 + fluxer_geoip/Dockerfile | 21 -- fluxer_geoip/go.mod | 8 - fluxer_geoip/go.sum | 4 - fluxer_geoip/main.go | 190 ----------- fluxer_marketing/locales/ar/messages.po | 26 +- fluxer_marketing/locales/bg/messages.po | 26 +- fluxer_marketing/locales/cs/messages.po | 26 +- fluxer_marketing/locales/da/messages.po | 26 +- fluxer_marketing/locales/de/messages.po | 26 +- fluxer_marketing/locales/el/messages.po | 26 +- fluxer_marketing/locales/en-GB/messages.po | 26 +- fluxer_marketing/locales/es-419/messages.po | 26 +- fluxer_marketing/locales/es-ES/messages.po | 26 +- fluxer_marketing/locales/fi/messages.po | 26 +- fluxer_marketing/locales/fr/messages.po | 26 +- fluxer_marketing/locales/he/messages.po | 26 +- fluxer_marketing/locales/hi/messages.po | 26 +- fluxer_marketing/locales/hr/messages.po | 26 +- fluxer_marketing/locales/hu/messages.po | 26 +- fluxer_marketing/locales/id/messages.po | 26 +- fluxer_marketing/locales/it/messages.po | 26 +- fluxer_marketing/locales/ja/messages.po | 26 +- fluxer_marketing/locales/ko/messages.po | 26 +- fluxer_marketing/locales/lt/messages.po | 26 +- fluxer_marketing/locales/messages.pot | 8 +- fluxer_marketing/locales/nl/messages.po | 26 +- fluxer_marketing/locales/no/messages.po | 26 +- fluxer_marketing/locales/pl/messages.po | 26 +- fluxer_marketing/locales/pt-BR/messages.po | 26 +- fluxer_marketing/locales/ro/messages.po | 26 +- fluxer_marketing/locales/ru/messages.po | 26 +- fluxer_marketing/locales/sv-SE/messages.po | 26 +- fluxer_marketing/locales/th/messages.po | 26 +- fluxer_marketing/locales/tr/messages.po | 26 +- fluxer_marketing/locales/uk/messages.po | 26 +- fluxer_marketing/locales/vi/messages.po | 26 +- fluxer_marketing/locales/zh-CN/messages.po | 26 +- fluxer_marketing/locales/zh-TW/messages.po | 26 +- fluxer_marketing/src/fluxer_marketing.gleam | 7 +- .../fluxer_marketing/components/footer.gleam | 10 +- .../src/fluxer_marketing/config.gleam | 3 - .../src/fluxer_marketing/geoip.gleam | 256 +++++++-------- .../src/fluxer_marketing/router.gleam | 10 - .../src/fluxer_marketing/web.gleam | 1 - fluxer_marketing/test/geoip_test.gleam | 142 -------- justfile | 131 ++------ scripts/cassandra-migrate/Dockerfile | 2 +- scripts/just/livekit-sync.js | 51 +++ 86 files changed, 982 insertions(+), 2648 deletions(-) delete mode 100644 .github/workflows/deploy-geoip.yaml delete mode 100644 .github/workflows/update-geoip-db.yaml delete mode 100644 dev/main.go delete mode 100644 dev/pkg/commands/commands.go delete mode 100644 dev/pkg/integrations/geoip.go delete mode 100644 dev/pkg/integrations/livekit.go delete mode 100644 dev/pkg/utils/helpers.go delete mode 100755 dev/setup.sh delete mode 100644 fluxer_api/src/debug/DebugController.ts delete mode 100644 fluxer_api/src/utils/IpUtils.test.ts create mode 100644 fluxer_api/src/utils/UserAgentUtils.ts delete mode 100644 fluxer_app/scripts/cmd/set-build-channel/main.go create mode 100644 fluxer_devops/cassandra/migrations/20260105174407_add_auth_session_user_agent_cols.cql delete mode 100644 fluxer_geoip/Dockerfile delete mode 100644 fluxer_geoip/go.mod delete mode 100644 fluxer_geoip/go.sum delete mode 100644 fluxer_geoip/main.go delete mode 100644 fluxer_marketing/test/geoip_test.gleam create mode 100755 scripts/just/livekit-sync.js diff --git a/.github/workflows/deploy-api.yaml b/.github/workflows/deploy-api.yaml index fa361f0b..6cf0e321 100644 --- a/.github/workflows/deploy-api.yaml +++ b/.github/workflows/deploy-api.yaml @@ -231,8 +231,6 @@ jobs: CLAMAV_HOST=clamav CLAMAV_PORT=3310 - GEOIP_HOST=fluxer-geoip_app:8080 - GEOIP_PROVIDER=maxmind MAXMIND_DB_PATH=/data/GeoLite2-City.mmdb VAPID_PUBLIC_KEY=BEIwQxIwfj6m90tLYAR0AU_GJWU4kw8J_zJcHQG55NCUWSyRy-dzMOgvxk8yEDwdVyJZa6xUL4fmwngijq8T2pY diff --git a/.github/workflows/deploy-geoip.yaml b/.github/workflows/deploy-geoip.yaml deleted file mode 100644 index 46d89bac..00000000 --- a/.github/workflows/deploy-geoip.yaml +++ /dev/null @@ -1,124 +0,0 @@ -name: deploy geoip - -on: - push: - branches: - - main - paths: - - fluxer_geoip/** - - .github/workflows/deploy-geoip.yaml - workflow_dispatch: {} - -concurrency: - group: deploy-fluxer-geoip - cancel-in-progress: true - -permissions: - contents: read - -jobs: - deploy: - name: Deploy geoip - runs-on: blacksmith-2vcpu-ubuntu-2404 - timeout-minutes: 10 - steps: - - uses: actions/checkout@v6 - - - name: Record deploy commit - run: | - set -euo pipefail - sha=$(git rev-parse HEAD) - echo "Deploying commit ${sha}" - printf 'DEPLOY_SHA=%s\n' "$sha" >> "$GITHUB_ENV" - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Login to Docker Hub - uses: docker/login-action@v3 - with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_PASSWORD }} - - - name: Build image - uses: docker/build-push-action@v6 - with: - context: fluxer_geoip - file: fluxer_geoip/Dockerfile - tags: fluxer-geoip:${{ env.DEPLOY_SHA }} - load: true - platforms: linux/amd64 - cache-from: type=gha,scope=deploy-fluxer-geoip - cache-to: type=gha,mode=max,scope=deploy-fluxer-geoip - env: - DOCKER_BUILD_SUMMARY: false - DOCKER_BUILD_RECORD_UPLOAD: false - - - name: Install docker-pussh - run: | - set -euo pipefail - mkdir -p ~/.docker/cli-plugins - curl -fsSL https://raw.githubusercontent.com/psviderski/unregistry/v0.3.1/docker-pussh \ - -o ~/.docker/cli-plugins/docker-pussh - chmod +x ~/.docker/cli-plugins/docker-pussh - - - name: Set up SSH agent - uses: webfactory/ssh-agent@v0.9.1 - with: - ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY_SERVER }} - - - name: Add server to known hosts - run: | - set -euo pipefail - mkdir -p ~/.ssh - ssh-keyscan -H ${{ secrets.SERVER_IP }} >> ~/.ssh/known_hosts - - - name: Push image and deploy - env: - IMAGE_TAG: fluxer-geoip:${{ env.DEPLOY_SHA }} - SERVER: ${{ secrets.SERVER_USER }}@${{ secrets.SERVER_IP }} - run: | - set -euo pipefail - docker pussh "${IMAGE_TAG}" "${SERVER}" - - ssh "${SERVER}" "IMAGE_TAG=${IMAGE_TAG} bash" << 'EOF' - set -euo pipefail - - sudo mkdir -p /opt/fluxer-geoip - sudo chown -R "${USER}:${USER}" /opt/fluxer-geoip - cd /opt/fluxer-geoip - - cat > compose.yaml << 'COMPOSEEOF' - services: - app: - image: ${IMAGE_TAG} - volumes: - - /etc/fluxer/ipinfo_lite.mmdb:/data/ipinfo_lite.mmdb:ro - deploy: - replicas: 3 - restart_policy: - condition: on-failure - delay: 5s - max_attempts: 3 - update_config: - parallelism: 1 - delay: 10s - order: start-first - rollback_config: - parallelism: 1 - delay: 10s - environment: - - FLUXER_GEOIP_PORT=8080 - - GEOIP_DB_PATH=/data/ipinfo_lite.mmdb - - GEOIP_CACHE_TTL=10m - - GEOIP_CACHE_SIZE=20000 - networks: - - fluxer-shared - - networks: - fluxer-shared: - external: true - COMPOSEEOF - - docker stack deploy --with-registry-auth --detach=false --resolve-image never -c compose.yaml fluxer-geoip - EOF diff --git a/.github/workflows/deploy-marketing.yaml b/.github/workflows/deploy-marketing.yaml index b01b5492..42d2162f 100644 --- a/.github/workflows/deploy-marketing.yaml +++ b/.github/workflows/deploy-marketing.yaml @@ -160,7 +160,6 @@ jobs: - FLUXER_MARKETING_ENDPOINT=${MARKETING_ENDPOINT} - FLUXER_MARKETING_PORT=8080 - FLUXER_PATH_MARKETING=/ - - GEOIP_HOST=fluxer-geoip_app:8080 - RELEASE_CHANNEL=${RELEASE_CHANNEL} - FLUXER_METRICS_HOST=fluxer-metrics_app:8080 deploy: diff --git a/.github/workflows/update-geoip-db.yaml b/.github/workflows/update-geoip-db.yaml deleted file mode 100644 index 33fa90c8..00000000 --- a/.github/workflows/update-geoip-db.yaml +++ /dev/null @@ -1,81 +0,0 @@ -name: update geoip-db - -on: - schedule: - - cron: '0 0 * * *' - workflow_dispatch: - -permissions: - contents: read - -concurrency: - group: update-geoip-db - cancel-in-progress: false - -jobs: - refresh-db: - runs-on: blacksmith-2vcpu-ubuntu-2404 - timeout-minutes: 10 - - steps: - - name: Checkout - uses: actions/checkout@v6 - with: - fetch-depth: 0 - - - name: Set up SSH agent - uses: webfactory/ssh-agent@v0.9.1 - with: - ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY_SERVER }} - - - name: Add server to known hosts - run: | - set -euo pipefail - mkdir -p ~/.ssh - ssh-keyscan -H ${{ secrets.SERVER_IP }} >> ~/.ssh/known_hosts - - - name: Refresh MMDB on server & roll restart - env: - SERVER: ${{ secrets.SERVER_USER }}@${{ secrets.SERVER_IP }} - IPINFO_TOKEN: ${{ secrets.IPINFO_TOKEN }} - run: | - set -euo pipefail - - ssh "${SERVER}" bash << EOSSH - set -euo pipefail - - if ! command -v curl >/dev/null 2>&1; then - sudo apt-get update -y - sudo apt-get install -y curl - fi - - if ! command -v go >/dev/null 2>&1; then - sudo apt-get update -y - sudo apt-get install -y golang-go - fi - - export PATH="\$PATH:\$(go env GOPATH)/bin" - if ! command -v mmdbverify >/dev/null 2>&1; then - GOBIN="\$(go env GOPATH)/bin" go install github.com/maxmind/mmdbverify@latest - fi - - TMPDIR="\$(mktemp -d)" - trap 'rm -rf "\$TMPDIR"' EXIT - - DEST_DIR="/etc/fluxer" - DEST_DB="\${DEST_DIR}/ipinfo_lite.mmdb" - - mkdir -p "\$DEST_DIR" - - curl -fsSL -o "\$TMPDIR/ipinfo_lite.mmdb" \ - "https://ipinfo.io/data/ipinfo_lite.mmdb?token=${IPINFO_TOKEN}" - - [ -s "\$TMPDIR/ipinfo_lite.mmdb" ] - - mmdbverify -file "\$TMPDIR/ipinfo_lite.mmdb" - - install -m 0644 "\$TMPDIR/ipinfo_lite.mmdb" "\$DEST_DB.tmp" - mv -f "\$DEST_DB.tmp" "\$DEST_DB" - - docker service update --force fluxer-geoip_app - EOSSH diff --git a/dev/.env.example b/dev/.env.example index 977074c8..cd918411 100644 --- a/dev/.env.example +++ b/dev/.env.example @@ -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= diff --git a/dev/Caddyfile.dev b/dev/Caddyfile.dev index bef2fcf8..327e9b62 100644 --- a/dev/Caddyfile.dev +++ b/dev/Caddyfile.dev @@ -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 diff --git a/dev/compose.yaml b/dev/compose.yaml index d59f72f4..a6674672 100644 --- a/dev/compose.yaml +++ b/dev/compose.yaml @@ -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 diff --git a/dev/main.go b/dev/main.go deleted file mode 100644 index 9527e290..00000000 --- a/dev/main.go +++ /dev/null @@ -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 . - */ - -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) - } -} diff --git a/dev/pkg/commands/commands.go b/dev/pkg/commands/commands.go deleted file mode 100644 index 07d3ed20..00000000 --- a/dev/pkg/commands/commands.go +++ /dev/null @@ -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 . - */ - -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 -} diff --git a/dev/pkg/integrations/geoip.go b/dev/pkg/integrations/geoip.go deleted file mode 100644 index 1c819140..00000000 --- a/dev/pkg/integrations/geoip.go +++ /dev/null @@ -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 . - */ - -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 -} diff --git a/dev/pkg/integrations/livekit.go b/dev/pkg/integrations/livekit.go deleted file mode 100644 index bb745072..00000000 --- a/dev/pkg/integrations/livekit.go +++ /dev/null @@ -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 . - */ - -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 -} diff --git a/dev/pkg/utils/helpers.go b/dev/pkg/utils/helpers.go deleted file mode 100644 index 584400cd..00000000 --- a/dev/pkg/utils/helpers.go +++ /dev/null @@ -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 . - */ - -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() -} diff --git a/dev/setup.sh b/dev/setup.sh deleted file mode 100755 index d8829394..00000000 --- a/dev/setup.sh +++ /dev/null @@ -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 . - -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 "" \ No newline at end of file diff --git a/fluxer_admin/src/fluxer_admin/api/users.gleam b/fluxer_admin/src/fluxer_admin/api/users.gleam index 957e4b3e..0870c1e2 100644 --- a/fluxer_admin/src/fluxer_admin/api/users.gleam +++ b/fluxer_admin/src/fluxer_admin/api/users.gleam @@ -55,7 +55,7 @@ pub type UserSession { client_ip: String, client_os: String, client_platform: String, - client_location: String, + client_location: Option(String), ) } @@ -635,7 +635,7 @@ pub fn list_user_sessions( use client_ip <- decode.field("client_ip", decode.string) use client_os <- decode.field("client_os", decode.string) use client_platform <- decode.field("client_platform", decode.string) - use client_location <- decode.field("client_location", decode.string) + use client_location <- decode.field("client_location", decode.optional(decode.string)) decode.success(UserSession( session_id_hash: session_id_hash, created_at: created_at, diff --git a/fluxer_admin/src/fluxer_admin/pages/pending_verifications_page.gleam b/fluxer_admin/src/fluxer_admin/pages/pending_verifications_page.gleam index ef040ac5..949ee58c 100644 --- a/fluxer_admin/src/fluxer_admin/pages/pending_verifications_page.gleam +++ b/fluxer_admin/src/fluxer_admin/pages/pending_verifications_page.gleam @@ -429,7 +429,6 @@ fn render_pending_verification_card( can_review: Bool, ) -> element.Element(a) { let metadata_warning = user_agent_warning(pv.metadata) - let geoip_hint = geoip_reason_value(pv.metadata) h.div( [ @@ -493,10 +492,6 @@ fn render_pending_verification_card( option.Some(msg) -> ui.pill(msg, ui.PillWarning) option.None -> element.none() }, - case geoip_hint { - option.Some(hint) -> ui.pill("GeoIP: " <> hint, ui.PillInfo) - option.None -> element.none() - }, ]), ]), h.details( @@ -606,9 +601,6 @@ fn render_registration_metadata( False -> ip <> " (Normalized: " <> normalized_ip <> ")" } - let geoip_reason = - option_or_default("none", metadata_value(metadata, "geoip_reason")) - let os = option_or_default("Unknown", metadata_value(metadata, "os")) let browser = @@ -627,14 +619,6 @@ fn render_registration_metadata( let ip_reverse = metadata_value(metadata, "ip_address_reverse") - let geoip_note = case geoip_reason { - "none" -> element.none() - reason -> - h.div([a.class("text-xs text-neutral-500")], [ - element.text("GeoIP hint: " <> reason), - ]) - } - h.div([a.class("flex flex-col gap-0.5 text-xs text-neutral-600")], [ h.div([], [element.text("Display Name: " <> display_name)]), h.div([], [element.text("IP: " <> ip_display)]), @@ -644,7 +628,6 @@ fn render_registration_metadata( h.div([], [element.text("Reverse DNS: " <> reverse)]) option.None -> element.none() }, - geoip_note, h.div([], [element.text("OS: " <> os)]), h.div([], [element.text("Browser: " <> browser)]), h.div([], [element.text("Device: " <> device)]), @@ -705,19 +688,6 @@ fn metadata_value( }) } -fn geoip_reason_value( - metadata: List(verifications.PendingVerificationMetadata), -) -> option.Option(String) { - case metadata_value(metadata, "geoip_reason") { - option.Some(reason) -> - case reason { - "none" -> option.None - r -> option.Some(r) - } - option.None -> option.None - } -} - fn option_or_default(default: String, value: option.Option(String)) -> String { case value { option.Some(v) -> v diff --git a/fluxer_admin/src/fluxer_admin/pages/user_detail/tabs/account.gleam b/fluxer_admin/src/fluxer_admin/pages/user_detail/tabs/account.gleam index 699ceedb..1106640a 100644 --- a/fluxer_admin/src/fluxer_admin/pages/user_detail/tabs/account.gleam +++ b/fluxer_admin/src/fluxer_admin/pages/user_detail/tabs/account.gleam @@ -209,7 +209,10 @@ pub fn account_tab( ], ), h.div([a.class("text-neutral-900")], [ - element.text(session_item.client_location), + case session_item.client_location { + option.Some(location) -> element.text(location) + option.None -> element.text("Unknown") + }, ]), ]), h.div([], [ diff --git a/fluxer_api/src/App.ts b/fluxer_api/src/App.ts index 81a09365..fb99a286 100644 --- a/fluxer_api/src/App.ts +++ b/fluxer_api/src/App.ts @@ -29,7 +29,6 @@ import {AuthController} from '~/auth/AuthController'; import {Config} from '~/Config'; import {ChannelController} from '~/channel/ChannelController'; import type {StreamPreviewService} from '~/channel/services/StreamPreviewService'; -import {DebugController} from '~/debug/DebugController'; import {DownloadController} from '~/download/DownloadController'; import {AppErrorHandler, AppNotFoundHandler} from '~/Errors'; import {InvalidApiOriginError} from '~/errors/InvalidApiOriginError'; @@ -230,7 +229,6 @@ routes.get('/_health', async (ctx) => ctx.text('OK')); GatewayController(routes); -DebugController(routes); registerAdminControllers(routes); AuthController(routes); ChannelController(routes); diff --git a/fluxer_api/src/Config.ts b/fluxer_api/src/Config.ts index 0069d551..e4c6c395 100644 --- a/fluxer_api/src/Config.ts +++ b/fluxer_api/src/Config.ts @@ -114,8 +114,6 @@ const ConfigSchema = z.object({ }), geoip: z.object({ - provider: z.enum(['ipinfo', 'maxmind']), - host: z.string().optional(), maxmindDbPath: z.string().optional(), }), @@ -313,12 +311,7 @@ function loadConfig() { : Array.from(new Set([apiPublicEndpoint, webAppEndpoint, apiClientEndpoint])); const testModeEnabled = optionalBool('FLUXER_TEST_MODE'); - const geoipProviderRaw = optional('GEOIP_PROVIDER')?.trim().toLowerCase(); - const geoipProvider = geoipProviderRaw === 'maxmind' ? 'maxmind' : 'ipinfo'; const maxmindDbPath = optional('MAXMIND_DB_PATH'); - if (geoipProvider === 'maxmind' && !maxmindDbPath) { - throw new Error('Missing required environment variable: MAXMIND_DB_PATH'); - } return ConfigSchema.parse({ nodeEnv: optional('NODE_ENV') || 'development', @@ -353,8 +346,6 @@ function loadConfig() { }, geoip: { - provider: geoipProvider, - host: optional('GEOIP_HOST') || 'geoip', maxmindDbPath, }, diff --git a/fluxer_api/src/admin/models/UserTypes.ts b/fluxer_api/src/admin/models/UserTypes.ts index 3b332818..3ae219b9 100644 --- a/fluxer_api/src/admin/models/UserTypes.ts +++ b/fluxer_api/src/admin/models/UserTypes.ts @@ -55,9 +55,9 @@ export const mapUserToAdminResponse = async (user: User, cacheService?: ICacheSe let lastActiveLocation: string | null = null; if (user.lastActiveIp) { try { - const geoip = await IpUtils.getCountryCodeDetailed(user.lastActiveIp); + const geoip = await IpUtils.lookupGeoip(user.lastActiveIp); const formattedLocation = IpUtils.formatGeoipLocation(geoip); - lastActiveLocation = formattedLocation === IpUtils.UNKNOWN_LOCATION ? null : formattedLocation; + lastActiveLocation = formattedLocation; } catch { lastActiveLocation = null; } diff --git a/fluxer_api/src/admin/services/AdminUserSecurityService.ts b/fluxer_api/src/admin/services/AdminUserSecurityService.ts index f6e1534f..fef8387d 100644 --- a/fluxer_api/src/admin/services/AdminUserSecurityService.ts +++ b/fluxer_api/src/admin/services/AdminUserSecurityService.ts @@ -27,6 +27,8 @@ import type {IEmailService} from '~/infrastructure/IEmailService'; import type {BotMfaMirrorService} from '~/oauth/BotMfaMirrorService'; import type {IUserRepository} from '~/user/IUserRepository'; import type {UserContactChangeLogService} from '~/user/services/UserContactChangeLogService'; +import * as IpUtils from '~/utils/IpUtils'; +import {resolveSessionClientInfo} from '~/utils/UserAgentUtils'; import type { BulkUpdateUserFlagsRequest, DisableForSuspiciousActivityRequest, @@ -350,6 +352,9 @@ export class AdminUserSecurityService { } const sessions = await userRepository.listAuthSessions(userIdTyped); + const locationResults = await Promise.allSettled( + sessions.map((session) => IpUtils.getLocationLabelFromIp(session.clientIp)), + ); await auditService.createAuditLog({ adminUserId, @@ -361,15 +366,23 @@ export class AdminUserSecurityService { }); return { - sessions: sessions.map((session) => ({ - session_id_hash: session.sessionIdHash.toString('base64url'), - created_at: session.createdAt.toISOString(), - approx_last_used_at: session.approximateLastUsedAt.toISOString(), - client_ip: session.clientIp, - client_os: session.clientOs, - client_platform: session.clientPlatform, - client_location: session.clientLocation ?? 'Unknown Location', - })), + sessions: sessions.map((session, index) => { + const locationResult = locationResults[index]; + const clientLocation = locationResult?.status === 'fulfilled' ? locationResult.value : null; + const {clientOs, clientPlatform} = resolveSessionClientInfo({ + userAgent: session.clientUserAgent, + isDesktopClient: session.clientIsDesktop, + }); + return { + session_id_hash: session.sessionIdHash.toString('base64url'), + created_at: session.createdAt.toISOString(), + approx_last_used_at: session.approximateLastUsedAt.toISOString(), + client_ip: session.clientIp, + client_os: clientOs, + client_platform: clientPlatform, + client_location: clientLocation, + }; + }), }; } } diff --git a/fluxer_api/src/auth/AuthModel.ts b/fluxer_api/src/auth/AuthModel.ts index befa7159..dbdfee86 100644 --- a/fluxer_api/src/auth/AuthModel.ts +++ b/fluxer_api/src/auth/AuthModel.ts @@ -20,7 +20,8 @@ import {uint8ArrayToBase64} from 'uint8array-extras'; import type {AuthSession} from '~/Models'; import {createStringType, EmailType, GlobalNameType, PasswordType, UsernameType, z} from '~/Schema'; -import {UNKNOWN_LOCATION} from '~/utils/IpUtils'; +import {getLocationLabelFromIp} from '~/utils/IpUtils'; +import {resolveSessionClientInfo} from '~/utils/UserAgentUtils'; export const RegisterRequest = z.object({ email: EmailType.optional(), @@ -80,26 +81,45 @@ export const VerifyEmailRequest = z.object({ export type VerifyEmailRequest = z.infer; -export const mapAuthSessionsToResponse = ({ +async function resolveAuthSessionLocation(session: AuthSession): Promise { + try { + return await getLocationLabelFromIp(session.clientIp); + } catch { + return null; + } +} + +export const mapAuthSessionsToResponse = async ({ authSessions, }: { authSessions: Array; -}): Array => { - return authSessions - .sort((a, b) => { - const aTime = a.approximateLastUsedAt?.getTime() || 0; - const bTime = b.approximateLastUsedAt?.getTime() || 0; - return bTime - aTime; - }) - .map((authSession): AuthSessionResponse => { - return { - id: uint8ArrayToBase64(authSession.sessionIdHash, {urlSafe: true}), - approx_last_used_at: authSession.approximateLastUsedAt?.toISOString() || null, - client_os: authSession.clientOs, - client_platform: authSession.clientPlatform, - client_location: authSession.clientLocation ?? UNKNOWN_LOCATION, - }; +}): Promise> => { + const sortedSessions = [...authSessions].sort((a, b) => { + const aTime = a.approximateLastUsedAt?.getTime() || 0; + const bTime = b.approximateLastUsedAt?.getTime() || 0; + return bTime - aTime; + }); + + const locationResults = await Promise.allSettled( + sortedSessions.map((session) => resolveAuthSessionLocation(session)), + ); + + return sortedSessions.map((authSession, index): AuthSessionResponse => { + const locationResult = locationResults[index]; + const clientLocation = locationResult?.status === 'fulfilled' ? locationResult.value : null; + const {clientOs, clientPlatform} = resolveSessionClientInfo({ + userAgent: authSession.clientUserAgent, + isDesktopClient: authSession.clientIsDesktop, }); + + return { + id: uint8ArrayToBase64(authSession.sessionIdHash, {urlSafe: true}), + approx_last_used_at: authSession.approximateLastUsedAt?.toISOString() || null, + client_os: clientOs, + client_platform: clientPlatform, + client_location: clientLocation, + }; + }); }; export const AuthSessionResponse = z.object({ @@ -107,7 +127,7 @@ export const AuthSessionResponse = z.object({ approx_last_used_at: z.iso.datetime().nullish(), client_os: z.string(), client_platform: z.string(), - client_location: z.string(), + client_location: z.string().nullable(), }); export type AuthSessionResponse = z.infer; diff --git a/fluxer_api/src/auth/services/AuthLoginService.ts b/fluxer_api/src/auth/services/AuthLoginService.ts index d9b4f1b5..edf1ae6e 100644 --- a/fluxer_api/src/auth/services/AuthLoginService.ts +++ b/fluxer_api/src/auth/services/AuthLoginService.ts @@ -331,8 +331,8 @@ export class AuthLoginService { if (!isIpAuthorized) { const ticket = createIpAuthorizationTicket(await this.generateSecureToken()); const authToken = createIpAuthorizationToken(await this.generateSecureToken()); - const geoipResult = await IpUtils.getCountryCodeDetailed(clientIp); - const clientLocation = IpUtils.formatGeoipLocation(geoipResult); + const geoipResult = await IpUtils.lookupGeoip(clientIp); + const clientLocation = IpUtils.formatGeoipLocation(geoipResult) ?? IpUtils.UNKNOWN_LOCATION; const userAgent = request.headers.get('user-agent') || ''; const platform = request.headers.get('x-fluxer-platform'); diff --git a/fluxer_api/src/auth/services/AuthRegistrationService.ts b/fluxer_api/src/auth/services/AuthRegistrationService.ts index 81394591..838344cb 100644 --- a/fluxer_api/src/auth/services/AuthRegistrationService.ts +++ b/fluxer_api/src/auth/services/AuthRegistrationService.ts @@ -83,8 +83,6 @@ const MINIMUM_AGE_BY_COUNTRY: Record = { const DEFAULT_MINIMUM_AGE = 13; const USER_AGENT_TRUNCATE_LENGTH = 512; -type CountryResultDetailed = Awaited>; - interface RegistrationMetadataContext { metadata: Map; clientIp: string; @@ -120,11 +118,6 @@ function determineAgeGroup(age: number | null): string { return '65+'; } -function sanitizeEmail(email: string | null | undefined): {raw: string | null; key: string | null} { - const key = email ? email.toLowerCase() : null; - return {raw: email ?? null, key}; -} - function isIpv6(ip: string): boolean { return ip.includes(':'); } @@ -189,8 +182,8 @@ export class AuthRegistrationService { const metrics = getMetricsService(); const clientIp = IpUtils.requireClientIp(request); - const countryCode = await IpUtils.getCountryCodeFromReq(request); - const countryResultDetailed = await IpUtils.getCountryCodeDetailed(clientIp); + const geoipResult = await IpUtils.lookupGeoip(clientIp); + const countryCode = geoipResult.countryCode; const minAge = (countryCode && MINIMUM_AGE_BY_COUNTRY[countryCode]) || DEFAULT_MINIMUM_AGE; if (!this.validateAge({dateOfBirth: data.date_of_birth, minAge})) { @@ -204,7 +197,8 @@ export class AuthRegistrationService { throw InputValidationError.create('password', 'Password is too common'); } - const {raw: rawEmail, key: emailKey} = sanitizeEmail(data.email); + const rawEmail = data.email ?? null; + const emailKey = rawEmail ? rawEmail.toLowerCase() : null; const enforceRateLimits = !Config.dev.relaxRegistrationRateLimits; await this.enforceRegistrationRateLimits({enforceRateLimits, clientIp, emailKey}); @@ -300,7 +294,7 @@ export class AuthRegistrationService { name: 'user.registration', dimensions: { country: countryCode ?? 'unknown', - state: countryResultDetailed.region ?? 'unknown', + state: geoipResult.region ?? 'unknown', ip_version: isIpv6(clientIp) ? 'v6' : 'v4', }, }); @@ -310,7 +304,7 @@ export class AuthRegistrationService { name: 'user.age', dimensions: { country: countryCode ?? 'unknown', - state: countryResultDetailed.region ?? 'unknown', + state: geoipResult.region ?? 'unknown', age: age !== null ? age.toString() : 'unknown', age_group: determineAgeGroup(age), }, @@ -333,7 +327,7 @@ export class AuthRegistrationService { user, clientIp, request, - countryResultDetailed, + geoipResult, }); if (isPendingVerification) @@ -542,9 +536,9 @@ export class AuthRegistrationService { user: User; clientIp: string; request: Request; - countryResultDetailed: CountryResultDetailed; + geoipResult: IpUtils.GeoipResult; }): Promise { - const {user, clientIp, request, countryResultDetailed} = params; + const {user, clientIp, request, geoipResult} = params; const userAgentHeader = (request.headers.get('user-agent') ?? '').trim(); const fluxerTag = `${user.username}#${user.discriminator.toString().padStart(4, '0')}`; @@ -559,9 +553,9 @@ export class AuthRegistrationService { ? this.parseUserAgentSafe(userAgentHeader) : {osInfo: 'Unknown', browserInfo: 'Unknown', deviceInfo: 'Desktop/Unknown'}; - const normalizedIp = countryResultDetailed.normalizedIp ?? clientIp; - const geoipReason = countryResultDetailed.reason ?? 'none'; - const locationLabel = IpUtils.formatGeoipLocation(countryResultDetailed); + const normalizedIp = geoipResult.normalizedIp ?? clientIp; + const locationLabel = IpUtils.formatGeoipLocation(geoipResult) ?? IpUtils.UNKNOWN_LOCATION; + const safeCountryCode = geoipResult.countryCode ?? 'unknown'; const ipAddressReverse = await IpUtils.getIpAddressReverse(normalizedIp, this.cacheService); const metadataEntries: Array<[string, string]> = [ @@ -570,27 +564,26 @@ export class AuthRegistrationService { ['email', emailDisplay], ['ip_address', clientIp], ['normalized_ip', normalizedIp], - ['country_code', countryResultDetailed.countryCode], + ['country_code', safeCountryCode], ['location', locationLabel], - ['geoip_reason', geoipReason], ['os', uaInfo.osInfo], ['browser', uaInfo.browserInfo], ['device', uaInfo.deviceInfo], ['user_agent', truncatedUserAgent], ]; - if (countryResultDetailed.city) metadataEntries.push(['city', countryResultDetailed.city]); - if (countryResultDetailed.region) metadataEntries.push(['region', countryResultDetailed.region]); - if (countryResultDetailed.countryName) metadataEntries.push(['country_name', countryResultDetailed.countryName]); + if (geoipResult.city) metadataEntries.push(['city', geoipResult.city]); + if (geoipResult.region) metadataEntries.push(['region', geoipResult.region]); + if (geoipResult.countryName) metadataEntries.push(['country_name', geoipResult.countryName]); if (ipAddressReverse) metadataEntries.push(['ip_address_reverse', ipAddressReverse]); return { metadata: new Map(metadataEntries), clientIp, - countryCode: countryResultDetailed.countryCode, + countryCode: safeCountryCode, location: locationLabel, - city: countryResultDetailed.city, - region: countryResultDetailed.region, + city: geoipResult.city, + region: geoipResult.region, osInfo: uaInfo.osInfo, browserInfo: uaInfo.browserInfo, deviceInfo: uaInfo.deviceInfo, diff --git a/fluxer_api/src/auth/services/AuthSessionService.ts b/fluxer_api/src/auth/services/AuthSessionService.ts index a0120934..129b082f 100644 --- a/fluxer_api/src/auth/services/AuthSessionService.ts +++ b/fluxer_api/src/auth/services/AuthSessionService.ts @@ -17,12 +17,10 @@ * along with Fluxer. If not, see . */ -import Bowser from 'bowser'; import {type AuthSessionResponse, mapAuthSessionsToResponse} from '~/auth/AuthModel'; import type {UserID} from '~/BrandedTypes'; import {AccessDeniedError} from '~/Errors'; import type {IGatewayService} from '~/infrastructure/IGatewayService'; -import {Logger} from '~/Logger'; import type {AuthSession, User} from '~/Models'; import type {IUserRepository} from '~/user/IUserRepository'; import * as IpUtils from '~/utils/IpUtils'; @@ -41,45 +39,6 @@ interface UpdateUserActivityParams { userId: UserID; clientIp: string; } - -function formatNameVersion(name?: string | null, version?: string | null): string { - if (!name) return 'Unknown'; - if (!version) return name; - return `${name} ${version}`; -} - -function nullIfUnknown(value: string | null | undefined): string | null { - if (!value) return null; - return value === IpUtils.UNKNOWN_LOCATION ? null : value; -} - -function parseUserAgent(userAgentRaw: string): {clientOs: string; detectedPlatform: string} { - const ua = userAgentRaw.trim(); - if (!ua) return {clientOs: 'Unknown', detectedPlatform: 'Unknown'}; - - try { - const parser = Bowser.getParser(ua); - const osName = parser.getOSName() || 'Unknown'; - const osVersion = parser.getOSVersion() || null; - const browserName = parser.getBrowserName() || 'Unknown'; - const browserVersion = parser.getBrowserVersion() || null; - - return { - clientOs: formatNameVersion(osName, osVersion), - detectedPlatform: formatNameVersion(browserName, browserVersion), - }; - } catch (error) { - Logger.warn({error}, 'Failed to parse user agent'); - return {clientOs: 'Unknown', detectedPlatform: 'Unknown'}; - } -} - -function resolveClientPlatform(platformHeader: string | null, detectedPlatform: string): string { - if (!platformHeader) return detectedPlatform; - if (platformHeader === 'desktop') return 'Fluxer Desktop'; - return detectedPlatform; -} - export class AuthSessionService { constructor( private repository: IUserRepository, @@ -97,11 +56,7 @@ export class AuthSessionService { const platformHeader = request.headers.get('x-fluxer-platform')?.trim().toLowerCase() ?? null; const uaRaw = request.headers.get('user-agent') ?? ''; - const uaInfo = parseUserAgent(uaRaw); - - const geoip = await IpUtils.getCountryCodeDetailed(ip); - const locationLabel = nullIfUnknown(IpUtils.formatGeoipLocation(geoip)); - const countryLabel = nullIfUnknown(geoip.countryName ?? geoip.countryCode ?? null); + const isDesktopClient = platformHeader === 'desktop'; const authSession = await this.repository.createAuthSession({ user_id: user.id, @@ -109,10 +64,8 @@ export class AuthSessionService { created_at: now, approx_last_used_at: now, client_ip: ip, - client_os: uaInfo.clientOs, - client_platform: resolveClientPlatform(platformHeader, uaInfo.detectedPlatform), - client_country: countryLabel, - client_location: locationLabel, + client_user_agent: uaRaw || null, + client_is_desktop: isDesktopClient, version: 1, }); @@ -125,7 +78,7 @@ export class AuthSessionService { async getAuthSessions(userId: UserID): Promise> { const authSessions = await this.repository.listAuthSessions(userId); - return mapAuthSessionsToResponse({authSessions}); + return await mapAuthSessionsToResponse({authSessions}); } async updateAuthSessionLastUsed(tokenHash: Uint8Array): Promise { diff --git a/fluxer_api/src/database/types/AuthTypes.ts b/fluxer_api/src/database/types/AuthTypes.ts index 8269797f..ab91a38b 100644 --- a/fluxer_api/src/database/types/AuthTypes.ts +++ b/fluxer_api/src/database/types/AuthTypes.ts @@ -35,10 +35,8 @@ export interface AuthSessionRow { created_at: Date; approx_last_used_at: Date; client_ip: string; - client_os: string; - client_platform: string; - client_country: Nullish; - client_location: Nullish; + client_user_agent: Nullish; + client_is_desktop: Nullish; version: number; } @@ -142,10 +140,8 @@ export const AUTH_SESSION_COLUMNS = [ 'created_at', 'approx_last_used_at', 'client_ip', - 'client_os', - 'client_platform', - 'client_country', - 'client_location', + 'client_user_agent', + 'client_is_desktop', 'version', ] as const satisfies ReadonlyArray; diff --git a/fluxer_api/src/debug/DebugController.ts b/fluxer_api/src/debug/DebugController.ts deleted file mode 100644 index ba1cc959..00000000 --- a/fluxer_api/src/debug/DebugController.ts +++ /dev/null @@ -1,63 +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 . - */ - -import type {HonoApp} from '~/App'; -import {Config} from '~/Config'; -import {DEFAULT_CC, extractClientIp, formatGeoipLocation, getCountryCodeDetailed} from '~/utils/IpUtils'; - -export const DebugController = (app: HonoApp) => { - app.get('/_debug/geoip', async (ctx) => { - const manualIp = ctx.req.query('ip')?.trim() || null; - const headerIp = extractClientIp(ctx.req.raw); - const chosenIp = manualIp || headerIp; - - let countryCode = DEFAULT_CC; - let normalizedIp: string | null = null; - let error: string | null = null; - let reason: string | null = null; - let geoipLocation: string | null = null; - - if (chosenIp) { - try { - const result = await getCountryCodeDetailed(chosenIp); - countryCode = result.countryCode; - normalizedIp = result.normalizedIp; - reason = result.reason; - geoipLocation = formatGeoipLocation(result); - } catch (err) { - error = (err as Error).message; - } - } - - return ctx.json({ - x_forwarded_for: ctx.req.header('x-forwarded-for') || null, - ip: chosenIp, - manual_ip: manualIp, - extracted_ip: headerIp, - country_code: countryCode, - normalized_ip: normalizedIp, - reason, - geoip_host: Config.geoip.host || null, - geoip_provider: Config.geoip.provider, - geoip_location: geoipLocation, - default_cc: DEFAULT_CC, - error, - }); - }); -}; diff --git a/fluxer_api/src/models/AuthSession.ts b/fluxer_api/src/models/AuthSession.ts index ca17deac..0843c6d6 100644 --- a/fluxer_api/src/models/AuthSession.ts +++ b/fluxer_api/src/models/AuthSession.ts @@ -26,10 +26,8 @@ export class AuthSession { readonly createdAt: Date; readonly approximateLastUsedAt: Date; readonly clientIp: string; - readonly clientOs: string; - readonly clientPlatform: string; - readonly clientCountry: string | null; - readonly clientLocation: string | null; + readonly clientUserAgent: string | null; + readonly clientIsDesktop: boolean | null; readonly version: number; constructor(row: AuthSessionRow) { @@ -38,10 +36,8 @@ export class AuthSession { this.createdAt = row.created_at; this.approximateLastUsedAt = row.approx_last_used_at; this.clientIp = row.client_ip; - this.clientOs = row.client_os; - this.clientPlatform = row.client_platform; - this.clientCountry = row.client_country ?? null; - this.clientLocation = row.client_location ?? null; + this.clientUserAgent = row.client_user_agent ?? null; + this.clientIsDesktop = row.client_is_desktop ?? null; this.version = row.version; } @@ -52,10 +48,8 @@ export class AuthSession { created_at: this.createdAt, approx_last_used_at: this.approximateLastUsedAt, client_ip: this.clientIp, - client_os: this.clientOs, - client_platform: this.clientPlatform, - client_country: this.clientCountry, - client_location: this.clientLocation, + client_user_agent: this.clientUserAgent, + client_is_desktop: this.clientIsDesktop, version: this.version, }; } diff --git a/fluxer_api/src/rpc/RpcModel.ts b/fluxer_api/src/rpc/RpcModel.ts index c1ecf2bf..20434f94 100644 --- a/fluxer_api/src/rpc/RpcModel.ts +++ b/fluxer_api/src/rpc/RpcModel.ts @@ -68,6 +68,10 @@ export const RpcRequest = z.discriminatedUnion('type', [ type: z.literal('get_badge_counts'), user_ids: z.array(Int64Type), }), + z.object({ + type: z.literal('geoip_lookup'), + ip: createStringType(1, 45), + }), z.object({ type: z.literal('delete_push_subscriptions'), subscriptions: z.array( @@ -290,6 +294,10 @@ export const RpcResponse = z.discriminatedUnion('type', [ type: z.literal('validate_custom_status'), data: RpcResponseValidateCustomStatus, }), + z.object({ + type: z.literal('geoip_lookup'), + data: z.object({country_code: z.string()}), + }), z.object({ type: z.literal('get_dm_channel'), data: z.object({ diff --git a/fluxer_api/src/rpc/RpcService.ts b/fluxer_api/src/rpc/RpcService.ts index d394d03f..36a16dbf 100644 --- a/fluxer_api/src/rpc/RpcService.ts +++ b/fluxer_api/src/rpc/RpcService.ts @@ -97,7 +97,7 @@ import { import {isUserAdult} from '~/utils/AgeUtils'; import {deriveDominantAvatarColor} from '~/utils/AvatarColorUtils'; import {calculateDistance, parseCoordinate} from '~/utils/GeoUtils'; -import {formatGeoipLocation, getCountryCodeDetailed} from '~/utils/IpUtils'; +import {lookupGeoip} from '~/utils/IpUtils'; import type {VoiceAccessContext, VoiceAvailabilityService} from '~/voice/VoiceAvailabilityService'; import type {VoiceService} from '~/voice/VoiceService'; import type {IWebhookRepository} from '~/webhook/IWebhookRepository'; @@ -284,6 +284,15 @@ export class RpcService { }, }; } + case 'geoip_lookup': { + const geoip = await lookupGeoip(request.ip); + return { + type: 'geoip_lookup', + data: { + country_code: geoip.countryCode, + }, + }; + } case 'delete_push_subscriptions': return { type: 'delete_push_subscriptions', @@ -678,24 +687,9 @@ export class RpcService { }); let countryCode = 'US'; - let geoipReason: string | null = null; if (ip) { - const geoip = await getCountryCodeDetailed(ip); + const geoip = await lookupGeoip(ip); countryCode = geoip.countryCode; - geoipReason = geoip.reason; - if (geoipReason) { - Logger.warn( - { - ip, - normalized_ip: geoip.normalizedIp, - reason: geoipReason, - geoip_host: Config.geoip.host, - geoip_provider: Config.geoip.provider, - geoip_location: formatGeoipLocation(geoip), - }, - 'GeoIP lookup fell back to default country code', - ); - } } else { Logger.warn({context: 'rpc_geoip', reason: 'ip_missing'}, 'RPC session request missing IP for GeoIP'); } diff --git a/fluxer_api/src/test/setup.ts b/fluxer_api/src/test/setup.ts index 8f2a6793..b903a710 100644 --- a/fluxer_api/src/test/setup.ts +++ b/fluxer_api/src/test/setup.ts @@ -74,8 +74,6 @@ process.env.CLAMAV_ENABLED = 'false'; process.env.FLUXER_APP_HOST = 'localhost:3000'; process.env.FLUXER_APP_PROTOCOL = 'http'; -process.env.GEOIP_HOST = 'geoip.test'; -process.env.GEOIP_PROVIDER = 'ipinfo'; process.env.SUDO_MODE_SECRET = 'test-sudo-secret'; import {vi} from 'vitest'; diff --git a/fluxer_api/src/utils/IpUtils.test.ts b/fluxer_api/src/utils/IpUtils.test.ts deleted file mode 100644 index aa10cc2b..00000000 --- a/fluxer_api/src/utils/IpUtils.test.ts +++ /dev/null @@ -1,136 +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 . - */ - -import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest'; -import {__clearIpCache, DEFAULT_CC, FETCH_TIMEOUT_MS, formatGeoipLocation, getCountryCode} from './IpUtils'; - -vi.mock('~/Config', () => ({Config: {geoip: {provider: 'ipinfo', host: 'geoip:8080'}}})); - -describe('getCountryCode(ip)', () => { - const realFetch = globalThis.fetch; - - beforeEach(() => { - __clearIpCache(); - vi.useRealTimers(); - vi.restoreAllMocks(); - }); - - afterEach(() => { - globalThis.fetch = realFetch; - }); - - it('returns US if GEOIP_HOST is not set', async () => { - vi.doMock('~/Config', () => ({Config: {geoip: {provider: 'ipinfo', host: ''}}})); - const {getCountryCode: modFn, __clearIpCache: reset} = await import('./IpUtils'); - reset(); - const cc = await modFn('8.8.8.8'); - expect(cc).toBe(DEFAULT_CC); - }); - - it('returns US for invalid IP', async () => { - const cc = await getCountryCode('not_an_ip'); - expect(cc).toBe(DEFAULT_CC); - }); - - it('accepts bracketed IPv6', async () => { - globalThis.fetch = vi.fn().mockResolvedValue({ok: true, text: () => Promise.resolve('se')}); - const cc = await getCountryCode('[2001:db8::1]'); - expect(cc).toBe('SE'); - }); - - it('returns uppercase alpha-2 on success', async () => { - globalThis.fetch = vi.fn().mockResolvedValue({ok: true, text: () => Promise.resolve('se')}); - const cc = await getCountryCode('8.8.8.8'); - expect(cc).toBe('SE'); - expect(globalThis.fetch).toHaveBeenCalledTimes(1); - }); - - it('falls back on non-2xx', async () => { - globalThis.fetch = vi.fn().mockResolvedValue({ok: false, text: () => Promise.resolve('se')}); - const cc = await getCountryCode('8.8.8.8'); - expect(cc).toBe(DEFAULT_CC); - }); - - it('falls back on invalid body', async () => { - globalThis.fetch = vi.fn().mockResolvedValue({ok: true, text: () => Promise.resolve('USA')}); - const cc = await getCountryCode('8.8.8.8'); - expect(cc).toBe(DEFAULT_CC); - }); - - it('falls back on network error', async () => { - globalThis.fetch = vi.fn().mockRejectedValue(new Error('fail')); - const cc = await getCountryCode('8.8.8.8'); - expect(cc).toBe(DEFAULT_CC); - }); - - it('uses cache for repeated lookups within TTL', async () => { - const spyNow = vi.spyOn(Date, 'now'); - spyNow.mockReturnValueOnce(1_000); - globalThis.fetch = vi.fn().mockResolvedValue({ok: true, text: () => Promise.resolve('gb')}); - const r1 = await getCountryCode('1.1.1.1'); - expect(r1).toBe('GB'); - spyNow.mockReturnValueOnce(2_000); - const r2 = await getCountryCode('1.1.1.1'); - expect(r2).toBe('GB'); - expect(globalThis.fetch).toHaveBeenCalledTimes(1); - }); - - it('aborts after timeout', async () => { - vi.useFakeTimers(); - const abortingFetch = vi.fn( - (_url, opts) => - new Promise((_res, rej) => { - opts?.signal?.addEventListener('abort', () => - rej(Object.assign(new Error('AbortError'), {name: 'AbortError'})), - ); - }), - ); - globalThis.fetch = abortingFetch; - const p = getCountryCode('8.8.8.8'); - vi.advanceTimersByTime(FETCH_TIMEOUT_MS + 5); - await expect(p).resolves.toBe(DEFAULT_CC); - expect(abortingFetch).toHaveBeenCalledTimes(1); - }); -}); - -describe('formatGeoipLocation(result)', () => { - it('returns Unknown Location when no parts are available', () => { - const label = formatGeoipLocation({ - countryCode: DEFAULT_CC, - normalizedIp: '1.1.1.1', - reason: null, - city: null, - region: null, - countryName: null, - }); - expect(label).toBe('Unknown Location'); - }); - - it('concatenates available city, region, and country', () => { - const label = formatGeoipLocation({ - countryCode: 'US', - normalizedIp: '1.1.1.1', - reason: null, - city: 'San Francisco', - region: 'CA', - countryName: 'United States', - }); - expect(label).toBe('San Francisco, CA, United States'); - }); -}); diff --git a/fluxer_api/src/utils/IpUtils.ts b/fluxer_api/src/utils/IpUtils.ts index 64d98a7b..80e9d257 100644 --- a/fluxer_api/src/utils/IpUtils.ts +++ b/fluxer_api/src/utils/IpUtils.ts @@ -25,42 +25,43 @@ import type {ICacheService} from '~/infrastructure/ICacheService'; import {Logger} from '~/Logger'; const CACHE_TTL_MS = 10 * 60 * 1000; - -export const DEFAULT_CC = 'US'; -export const FETCH_TIMEOUT_MS = 1500; -export const UNKNOWN_LOCATION = 'Unknown Location'; const REVERSE_DNS_CACHE_TTL_SECONDS = 24 * 60 * 60; const REVERSE_DNS_CACHE_PREFIX = 'reverse-dns:'; +export const UNKNOWN_LOCATION = 'Unknown Location'; + export interface GeoipResult { - countryCode: string; + countryCode: string | null; normalizedIp: string | null; - reason: string | null; city: string | null; region: string | null; countryName: string | null; } -interface CacheVal { +type CacheEntry = { result: GeoipResult; - exp: number; -} + expiresAt: number; +}; -const cache = new Map(); +const geoipCache = new Map(); let maxmindReader: Reader | null = null; let maxmindReaderPromise: Promise> | null = null; export function __clearIpCache(): void { - cache.clear(); + geoipCache.clear(); +} + +export function __resetMaxmindReader(): void { + maxmindReader = null; + maxmindReaderPromise = null; } export function extractClientIp(req: Request): string | null { const xff = (req.headers.get('X-Forwarded-For') ?? '').trim(); - if (!xff) { - return null; - } - const first = xff.split(',')[0]?.trim() ?? ''; + if (!xff) return null; + const [first] = xff.split(','); + if (!first) return null; return normalizeIpString(first); } @@ -72,24 +73,33 @@ export function requireClientIp(req: Request): string { return ip; } -function buildFallbackResult(clean: string, reason: string | null): GeoipResult { +export async function lookupGeoip(req: Request): Promise; +export async function lookupGeoip(ip: string): Promise; +export async function lookupGeoip(input: string | Request): Promise { + const ip = typeof input === 'string' ? input : extractClientIp(input); + if (!ip) { + return buildFallbackResult(''); + } + return lookupGeoipFromString(ip); +} + +function buildFallbackResult(clean: string): GeoipResult { return { - countryCode: DEFAULT_CC, - normalizedIp: clean, - reason, + countryCode: null, + normalizedIp: clean || null, city: null, region: null, countryName: null, }; } -async function getMaxmindReader(): Promise> { +async function ensureMaxmindReader(): Promise> { if (maxmindReader) return maxmindReader; if (!maxmindReaderPromise) { const dbPath = Config.geoip.maxmindDbPath; if (!dbPath) { - return Promise.reject(new Error('Missing MaxMind DB path')); + throw new Error('Missing MaxMind DB path'); } maxmindReaderPromise = maxmind @@ -107,127 +117,79 @@ async function getMaxmindReader(): Promise> { return maxmindReaderPromise; } -function getSubdivisionLabel(record?: CityResponse): string | null { +function stateLabel(record?: CityResponse): string | null { const subdivision = record?.subdivisions?.[0]; if (!subdivision) return null; - return subdivision.iso_code || subdivision.names?.en || null; + return subdivision.names?.en || subdivision.iso_code || null; } async function lookupMaxmind(clean: string): Promise { const dbPath = Config.geoip.maxmindDbPath; if (!dbPath) { - return buildFallbackResult(clean, 'maxmind_db_missing'); + return buildFallbackResult(clean); } try { - const reader = await getMaxmindReader(); + const reader = await ensureMaxmindReader(); const record = reader.get(clean); - if (!record) { - return buildFallbackResult(clean, 'maxmind_not_found'); - } + if (!record) return buildFallbackResult(clean); + + const isoCode = record.country?.iso_code; + const countryCode = isoCode ? isoCode.toUpperCase() : null; - const countryCode = (record.country?.iso_code ?? DEFAULT_CC).toUpperCase(); return { countryCode, normalizedIp: clean, - reason: null, city: record.city?.names?.en ?? null, - region: getSubdivisionLabel(record), - countryName: record.country?.names?.en ?? countryDisplayName(countryCode) ?? null, + region: stateLabel(record), + countryName: record.country?.names?.en ?? (countryCode ? countryDisplayName(countryCode) : null) ?? null, }; } catch (error) { - const message = (error as Error)?.message ?? 'unknown'; - Logger.warn({error, maxmind_db_path: dbPath}, 'MaxMind lookup failed'); - return buildFallbackResult(clean, `maxmind_error:${message}`); + const message = (error as Error).message ?? 'unknown'; + Logger.warn({error, maxmind_db_path: dbPath, message}, 'MaxMind lookup failed'); + return buildFallbackResult(clean); } } -async function lookupIpinfo(clean: string): Promise { - const host = Config.geoip.host; - if (!host) { - return buildFallbackResult(clean, 'geoip_host_missing'); - } - - const url = `http://${host}/lookup?ip=${encodeURIComponent(clean)}`; - const controller = new AbortController(); - const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS); - - try { - const res = await globalThis.fetch!(url, { - signal: controller.signal, - headers: {Accept: 'text/plain'}, - }); - if (!res.ok) { - return buildFallbackResult(clean, `non_ok:${res.status}`); - } - - const text = (await res.text()).trim().toUpperCase(); - const countryCode = isAsciiUpperAlpha2(text) ? text : DEFAULT_CC; - const reason = isAsciiUpperAlpha2(text) ? null : 'invalid_response'; - return { - countryCode, - normalizedIp: clean, - reason, - city: null, - region: null, - countryName: countryDisplayName(countryCode), - }; - } catch (error) { - const message = (error as Error)?.message ?? 'unknown'; - return buildFallbackResult(clean, `error:${message}`); - } finally { - clearTimeout(timer); - } -} - -export async function getCountryCodeDetailed(ip: string): Promise { - const clean = normalizeIpString(ip); - if (!isIPv4(clean) && !isIPv6(clean)) { - return buildFallbackResult(clean, 'invalid_ip'); - } - - const cached = cache.get(clean); +async function resolveGeoip(clean: string): Promise { const now = Date.now(); - if (cached && now < cached.exp) { + const cached = geoipCache.get(clean); + if (cached && now < cached.expiresAt) { return cached.result; } - const provider = Config.geoip.provider; - const result = provider === 'maxmind' ? await lookupMaxmind(clean) : await lookupIpinfo(clean); - - cache.set(clean, {result, exp: now + CACHE_TTL_MS}); + const result = await lookupMaxmind(clean); + geoipCache.set(clean, {result, expiresAt: now + CACHE_TTL_MS}); return result; } -export async function getCountryCode(ip: string): Promise { - const result = await getCountryCodeDetailed(ip); - return result.countryCode; -} +async function lookupGeoipFromString(value: string): Promise { + const clean = normalizeIpString(value); + if (!isIPv4(clean) && !isIPv6(clean)) { + return buildFallbackResult(clean); + } -export async function getCountryCodeFromReq(req: Request): Promise { - const ip = extractClientIp(req); - if (!ip) return DEFAULT_CC; - return await getCountryCode(ip); + return resolveGeoip(clean); } function countryDisplayName(code: string, locale = 'en'): string | null { - const c = code.toUpperCase(); - if (!isAsciiUpperAlpha2(c)) return null; + const upper = code.toUpperCase(); + if (!isAsciiUpperAlpha2(upper)) return null; const dn = new Intl.DisplayNames([locale], {type: 'region', fallback: 'none'}); - return dn.of(c) ?? null; + return dn.of(upper) ?? null; } -export function formatGeoipLocation(result: GeoipResult): string { +export function formatGeoipLocation(result: GeoipResult): string | null { const parts: Array = []; if (result.city) parts.push(result.city); if (result.region) parts.push(result.region); const countryLabel = result.countryName ?? result.countryCode; if (countryLabel) parts.push(countryLabel); - return parts.length > 0 ? parts.join(', ') : UNKNOWN_LOCATION; + return parts.length > 0 ? parts.join(', ') : null; } -function stripBrackets(s: string): string { - return s.startsWith('[') && s.endsWith(']') ? s.slice(1, -1) : s; +function stripBrackets(value: string): string { + return value.startsWith('[') && value.endsWith(']') ? value.slice(1, -1) : value; } export function normalizeIpString(value: string): string { @@ -239,12 +201,9 @@ export function normalizeIpString(value: string): string { export async function getIpAddressReverse(ip: string, cacheService?: ICacheService): Promise { const cacheKey = `${REVERSE_DNS_CACHE_PREFIX}${ip}`; - if (cacheService) { const cached = await cacheService.get(cacheKey); - if (cached !== null) { - return cached === '' ? null : cached; - } + if (cached !== null) return cached === '' ? null : cached; } let result: string | null = null; @@ -262,9 +221,17 @@ export async function getIpAddressReverse(ip: string, cacheService?: ICacheServi return result; } -function isAsciiUpperAlpha2(s: string): boolean { - if (s.length !== 2) return false; - const a = s.charCodeAt(0); - const b = s.charCodeAt(1); - return a >= 65 && a <= 90 && b >= 65 && b <= 90; +export async function getLocationLabelFromIp(ip: string): Promise { + const result = await lookupGeoip(ip); + return formatGeoipLocation(result); +} + +function isAsciiUpperAlpha2(value: string): boolean { + return ( + value.length === 2 && + value.charCodeAt(0) >= 65 && + value.charCodeAt(0) <= 90 && + value.charCodeAt(1) >= 65 && + value.charCodeAt(1) <= 90 + ); } diff --git a/fluxer_api/src/utils/UserAgentUtils.ts b/fluxer_api/src/utils/UserAgentUtils.ts new file mode 100644 index 00000000..d2e3b1a2 --- /dev/null +++ b/fluxer_api/src/utils/UserAgentUtils.ts @@ -0,0 +1,61 @@ +/* + * 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 . + */ + +import Bowser from 'bowser'; +import {Logger} from '~/Logger'; + +export interface UserAgentInfo { + clientOs: string; + detectedPlatform: string; +} + +const UNKNOWN_LABEL = 'Unknown'; + +function formatName(name?: string | null): string { + const normalized = name?.trim(); + return normalized || UNKNOWN_LABEL; +} + +export function parseUserAgentSafe(userAgentRaw: string): UserAgentInfo { + const ua = userAgentRaw.trim(); + if (!ua) return {clientOs: UNKNOWN_LABEL, detectedPlatform: UNKNOWN_LABEL}; + + try { + const parser = Bowser.getParser(ua); + return { + clientOs: formatName(parser.getOSName()), + detectedPlatform: formatName(parser.getBrowserName()), + }; + } catch (error) { + Logger.warn({error}, 'Failed to parse user agent'); + return {clientOs: UNKNOWN_LABEL, detectedPlatform: UNKNOWN_LABEL}; + } +} + +export function resolveSessionClientInfo(args: {userAgent: string | null; isDesktopClient: boolean | null}): { + clientOs: string; + clientPlatform: string; +} { + const parsed = parseUserAgentSafe(args.userAgent ?? ''); + const clientPlatform = args.isDesktopClient ? 'Fluxer Desktop' : parsed.detectedPlatform; + return { + clientOs: parsed.clientOs, + clientPlatform, + }; +} diff --git a/fluxer_api/src/worker/tasks/harvestUserData.ts b/fluxer_api/src/worker/tasks/harvestUserData.ts index 14c09a18..4d1d90ff 100644 --- a/fluxer_api/src/worker/tasks/harvestUserData.ts +++ b/fluxer_api/src/worker/tasks/harvestUserData.ts @@ -27,6 +27,7 @@ import {Config} from '~/Config'; import {makeAttachmentCdnUrl} from '~/channel/services/message/MessageHelpers'; import {Logger} from '~/Logger'; import * as SnowflakeUtils from '~/utils/SnowflakeUtils'; +import {resolveSessionClientInfo} from '~/utils/UserAgentUtils'; import {appendAssetToArchive, buildHashedAssetKey, getAnimatedAssetExtension} from '../utils/AssetArchiveHelpers'; import {getWorkerDependencies} from '../WorkerContext'; @@ -324,15 +325,20 @@ const harvestUserData: Task = async (payload, helpers) => { mfa_enabled: user.authenticatorTypes.size > 0, authenticator_types: Array.from(user.authenticatorTypes), }, - auth_sessions: authSessions.map((session) => ({ - created_at: session.createdAt.toISOString(), - approx_last_used_at: session.approximateLastUsedAt?.toISOString() ?? null, - client_ip: session.clientIp, - client_os: session.clientOs, - client_platform: session.clientPlatform, - client_country: session.clientCountry, - client_location: session.clientLocation, - })), + auth_sessions: authSessions.map((session) => { + const {clientOs, clientPlatform} = resolveSessionClientInfo({ + userAgent: session.clientUserAgent, + isDesktopClient: session.clientIsDesktop, + }); + return { + created_at: session.createdAt.toISOString(), + approx_last_used_at: session.approximateLastUsedAt?.toISOString() ?? null, + client_ip: session.clientIp, + client_os: clientOs, + client_user_agent: session.clientUserAgent, + client_platform: clientPlatform, + }; + }), relationships: relationships.map((rel) => ({ target_user_id: rel.targetUserId.toString(), type: rel.type, diff --git a/fluxer_app/scripts/cmd/set-build-channel/main.go b/fluxer_app/scripts/cmd/set-build-channel/main.go deleted file mode 100644 index c503c333..00000000 --- a/fluxer_app/scripts/cmd/set-build-channel/main.go +++ /dev/null @@ -1,66 +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 . - */ - -package main - -import ( - "fmt" - "os" - "path/filepath" - "slices" -) - -var allowedChannels = []string{"stable", "canary"} - -func parseChannel() string { - raw := os.Getenv("BUILD_CHANNEL") - if raw != "" && slices.Contains(allowedChannels, raw) { - return raw - } - return "stable" -} - -func main() { - channel := parseChannel() - - cwd, _ := os.Getwd() - targetPath := filepath.Join(cwd, "..", "src-electron", "common", "build-channel.ts") - - fileContent := fmt.Sprintf(`/* - * This file is generated by scripts/cmd/set-build-channel. - * DO NOT EDIT MANUALLY. - */ - -export type BuildChannel = 'stable' | 'canary'; - -const DEFAULT_BUILD_CHANNEL = '%s' as BuildChannel; - -const envChannel = process.env.BUILD_CHANNEL?.toLowerCase(); -export const BUILD_CHANNEL = (envChannel === 'canary' ? 'canary' : DEFAULT_BUILD_CHANNEL) as BuildChannel; -export const IS_CANARY = BUILD_CHANNEL === 'canary'; -export const CHANNEL_DISPLAY_NAME = BUILD_CHANNEL; -`, channel) - - if err := os.WriteFile(targetPath, []byte(fileContent), 0644); err != nil { - fmt.Fprintf(os.Stderr, "Error writing file: %v\n", err) - os.Exit(1) - } - - fmt.Printf("Wrote %s with channel '%s'\n", targetPath, channel) -} diff --git a/fluxer_app/src/components/modals/tabs/DevicesTab.tsx b/fluxer_app/src/components/modals/tabs/DevicesTab.tsx index 8dc907d5..3ca4e7be 100644 --- a/fluxer_app/src/components/modals/tabs/DevicesTab.tsx +++ b/fluxer_app/src/components/modals/tabs/DevicesTab.tsx @@ -97,6 +97,9 @@ const AuthSession: React.FC = observer( const platformLabel = authSession.clientPlatform === 'Fluxer Desktop' ? t`Fluxer Desktop` : authSession.clientPlatform; + const hasLocation = Boolean(authSession.clientLocation); + const locationRowVisible = hasLocation || !isCurrent; + return (
= observer( {platformLabel} -
- {authSession.clientLocation} - {!isCurrent && ( - <> - + {locationRowVisible && ( +
+ {hasLocation && {authSession.clientLocation}} + {!isCurrent && hasLocation && } + {!isCurrent && ( {DateUtils.getShortRelativeDateString(authSession.approxLastUsedAt ?? new Date(0))} - - )} -
+ )} +
+ )}
diff --git a/fluxer_app/src/records/AuthSessionRecord.tsx b/fluxer_app/src/records/AuthSessionRecord.tsx index afbb5401..a3d4dfe5 100644 --- a/fluxer_app/src/records/AuthSessionRecord.tsx +++ b/fluxer_app/src/records/AuthSessionRecord.tsx @@ -22,7 +22,7 @@ export type AuthSession = Readonly<{ approx_last_used_at: string | null; client_os: string; client_platform: string; - client_location: string; + client_location: string | null; }>; export class AuthSessionRecord { @@ -30,7 +30,7 @@ export class AuthSessionRecord { readonly approxLastUsedAt: Date | null; readonly clientOs: string; readonly clientPlatform: string; - readonly clientLocation: string; + readonly clientLocation: string | null; constructor(data: AuthSession) { this.id = data.id; diff --git a/fluxer_devops/cassandra/migrations/20260105174407_add_auth_session_user_agent_cols.cql b/fluxer_devops/cassandra/migrations/20260105174407_add_auth_session_user_agent_cols.cql new file mode 100644 index 00000000..2267c286 --- /dev/null +++ b/fluxer_devops/cassandra/migrations/20260105174407_add_auth_session_user_agent_cols.cql @@ -0,0 +1,2 @@ +ALTER TABLE fluxer.auth_sessions ADD client_user_agent text; +ALTER TABLE fluxer.auth_sessions ADD client_is_desktop boolean; diff --git a/fluxer_geoip/Dockerfile b/fluxer_geoip/Dockerfile deleted file mode 100644 index d9cf4ed7..00000000 --- a/fluxer_geoip/Dockerfile +++ /dev/null @@ -1,21 +0,0 @@ -FROM golang:1.25.5 AS builder -WORKDIR /src - -ENV CGO_ENABLED=0 GOOS=linux - -COPY go.mod go.sum ./ -RUN go mod download - -COPY . . -RUN go build -trimpath -buildvcs=false -ldflags="-s -w" -o /out/fluxer_geoip ./... - -FROM gcr.io/distroless/static:nonroot -WORKDIR /app - -VOLUME ["/data"] - -EXPOSE 8080 - -COPY --from=builder /out/fluxer_geoip /app/fluxer_geoip -USER nonroot:nonroot -ENTRYPOINT ["/app/fluxer_geoip"] diff --git a/fluxer_geoip/go.mod b/fluxer_geoip/go.mod deleted file mode 100644 index d2d9709b..00000000 --- a/fluxer_geoip/go.mod +++ /dev/null @@ -1,8 +0,0 @@ -module fluxer_geoip - -go 1.25.5 - -require ( - github.com/oschwald/maxminddb-golang/v2 v2.0.0 // indirect - golang.org/x/sys v0.37.0 // indirect -) diff --git a/fluxer_geoip/go.sum b/fluxer_geoip/go.sum deleted file mode 100644 index 9af6012a..00000000 --- a/fluxer_geoip/go.sum +++ /dev/null @@ -1,4 +0,0 @@ -github.com/oschwald/maxminddb-golang/v2 v2.0.0 h1:Gyljxck1kHbBxDgLM++NfDWBqvu1pWWfT8XbosSo0bo= -github.com/oschwald/maxminddb-golang/v2 v2.0.0/go.mod h1:gG4V88LsawPEqtbL1Veh1WRh+nVSYwXzJ1P5Fcn77g0= -golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= -golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= diff --git a/fluxer_geoip/main.go b/fluxer_geoip/main.go deleted file mode 100644 index e178d35d..00000000 --- a/fluxer_geoip/main.go +++ /dev/null @@ -1,190 +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 . - */ - -package main - -import ( - "log" - "net/http" - "net/netip" - "os" - "strconv" - "strings" - "sync" - "time" - - maxminddb "github.com/oschwald/maxminddb-golang/v2" -) - -type cfg struct { - Port string - DBPath string - TTL time.Duration - Cap int -} - -type liteRecord struct { - CountryCode string `maxminddb:"country_code"` -} - -type cacheEntry struct { - val string - exp time.Time -} -type lru struct { - mu sync.Mutex - data map[string]cacheEntry - order []string - ttl time.Duration - cap int -} - -func newLRU(ttl time.Duration, cap int) *lru { - return &lru{data: make(map[string]cacheEntry, cap), order: make([]string, 0, cap), ttl: ttl, cap: cap} -} -func (c *lru) get(k string) (string, bool) { - c.mu.Lock() - defer c.mu.Unlock() - ent, ok := c.data[k] - if !ok || time.Now().After(ent.exp) { - if ok { - delete(c.data, k) - } - return "", false - } - for i, v := range c.order { - if v == k { - copy(c.order[i:], c.order[i+1:]) - c.order[len(c.order)-1] = k - break - } - } - return ent.val, true -} -func (c *lru) put(k, v string) { - c.mu.Lock() - defer c.mu.Unlock() - if _, exists := c.data[k]; !exists && len(c.data) >= c.cap { - if len(c.order) > 0 { - ev := c.order[0] - delete(c.data, ev) - c.order = c.order[1:] - } - } - c.data[k] = cacheEntry{val: v, exp: time.Now().Add(c.ttl)} - for i, v2 := range c.order { - if v2 == k { - copy(c.order[i:], c.order[i+1:]) - c.order = c.order[:len(c.order)-1] - break - } - } - c.order = append(c.order, k) -} - -func getEnv(key, defaultVal string) string { - if v := os.Getenv(key); v != "" { - return v - } - return defaultVal -} - -func getEnvInt(key string, defaultVal int) int { - if v := os.Getenv(key); v != "" { - if n, err := strconv.Atoi(v); err == nil && n > 0 { - return n - } - } - return defaultVal -} - -func getEnvDuration(key string, defaultVal time.Duration) time.Duration { - if v := os.Getenv(key); v != "" { - if d, err := time.ParseDuration(v); err == nil && d > 0 { - return d - } - } - return defaultVal -} - -func main() { - c := cfg{ - Port: getEnv("FLUXER_GEOIP_PORT", "8080"), - DBPath: getEnv("GEOIP_DB_PATH", "/data/ipinfo_lite.mmdb"), - TTL: getEnvDuration("GEOIP_CACHE_TTL", 10*time.Minute), - Cap: getEnvInt("GEOIP_CACHE_SIZE", 20000), - } - - db, err := maxminddb.Open(c.DBPath) - if err != nil { - log.Fatalf("open mmdb: %v", err) - } - defer db.Close() - - cache := newLRU(c.TTL, c.Cap) - - http.HandleFunc("/_health", func(w http.ResponseWriter, _ *http.Request) { - w.Header().Set("Content-Type", "text/plain; charset=utf-8") - w.WriteHeader(http.StatusOK) - _, _ = w.Write([]byte("ok")) - }) - - http.HandleFunc("/lookup", func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "text/plain; charset=utf-8") - - ipStr := r.URL.Query().Get("ip") - if ipStr == "" { - http.Error(w, "missing 'ip' query param", http.StatusBadRequest) - return - } - addr, err := netip.ParseAddr(strings.TrimSpace(strings.Trim(ipStr, "[]"))) - if err != nil { - http.Error(w, "invalid ip", http.StatusBadRequest) - return - } - - key := addr.String() - if v, ok := cache.get(key); ok { - if v == "" { - w.WriteHeader(http.StatusNoContent) - return - } - _, _ = w.Write([]byte(v)) - return - } - - var rec liteRecord - if err := db.Lookup(addr).Decode(&rec); err != nil { - http.Error(w, "lookup error", http.StatusInternalServerError) - return - } - - cc := strings.TrimSpace(rec.CountryCode) - cache.put(key, cc) - - if cc == "" { - w.WriteHeader(http.StatusNoContent) - return - } - _, _ = w.Write([]byte(cc)) - }) - - log.Printf("geoip-lite (text+cache) listening on :%s (ttl=%s cap=%d)", c.Port, c.TTL, c.Cap) - log.Fatal(http.ListenAndServe(":"+c.Port, nil)) -} diff --git a/fluxer_marketing/locales/ar/messages.po b/fluxer_marketing/locales/ar/messages.po index 58d5fc75..864786d6 100644 --- a/fluxer_marketing/locales/ar/messages.po +++ b/fluxer_marketing/locales/ar/messages.po @@ -206,7 +206,7 @@ msgid "Blue (Da Ba Dee)" msgstr "أزرق (Da Ba Dee)" #: src/fluxer_marketing/components/footer.gleam:157 -#: src/fluxer_marketing/components/navigation.gleam:449 +#: src/fluxer_marketing/components/navigation.gleam:458 msgid "Bluesky" msgstr "Bluesky" @@ -363,15 +363,15 @@ msgstr "وضع مضغوط وخيارات العرض" msgid "Company" msgstr "الشركة" -#: src/fluxer_marketing/components/footer.gleam:271 +#: src/fluxer_marketing/components/footer.gleam:282 #: src/fluxer_marketing/components/navigation.gleam:376 #: src/fluxer_marketing/pages/company_page.gleam:41 #: src/fluxer_marketing/pages/company_page.gleam:44 msgid "Company Information" msgstr "معلومات الشركة" -#: src/fluxer_marketing/components/footer.gleam:278 -#: src/fluxer_marketing/components/navigation.gleam:426 +#: src/fluxer_marketing/components/footer.gleam:289 +#: src/fluxer_marketing/components/navigation.gleam:435 msgid "Connect" msgstr "تواصل" @@ -1010,10 +1010,6 @@ msgstr "كيف تخطط لاستخدام Fluxer مع مجتمعك" msgid "Hungarian" msgstr "المجرية" -#: src/fluxer_marketing/components/footer.gleam:329 -msgid "IP geolocation data by " -msgstr "بيانات تحديد الموقع الجغرافي عبر عنوان IP من " - #: src/fluxer_marketing/pages/help_article_page.gleam:223 #: src/fluxer_marketing/pages/help_article_page.gleam:365 msgid "If you couldn't find what you're looking for, our support team is here to help." @@ -1267,7 +1263,7 @@ msgid "Open Fluxer" msgstr "افتح Fluxer" #: src/fluxer_marketing/components/hero.gleam:82 -#: src/fluxer_marketing/components/navigation.gleam:473 +#: src/fluxer_marketing/components/navigation.gleam:482 msgid "Open in Browser" msgstr "افتح في المتصفح" @@ -1501,7 +1497,7 @@ msgstr "جاهز للترقية؟" msgid "Register now" msgstr "سجّل الآن" -#: src/fluxer_marketing/components/footer.gleam:311 +#: src/fluxer_marketing/components/footer.gleam:322 msgid "Report a bug" msgstr "أبلِغ عن خلل" @@ -1609,7 +1605,9 @@ msgstr "نتائج البحث عن: " msgid "Search through message history" msgstr "البحث في سجل الرسائل" +#: src/fluxer_marketing/components/footer.gleam:271 #: src/fluxer_marketing/components/get_involved_section.gleam:194 +#: src/fluxer_marketing/components/navigation.gleam:424 #: src/fluxer_marketing/pages/careers_page.gleam:156 #: src/fluxer_marketing/pages/security_page.gleam:41 msgid "Security Bug Bounty" @@ -1680,7 +1678,7 @@ msgid "Soundboard" msgstr "لوحة الأصوات" #: src/fluxer_marketing/components/footer.gleam:143 -#: src/fluxer_marketing/components/navigation.gleam:438 +#: src/fluxer_marketing/components/navigation.gleam:447 msgid "Source Code" msgstr "الكود المصدري" @@ -1781,6 +1779,10 @@ msgstr "هذا ليس بديلاً كاملًا لتطبيق سطح المكتب msgid "This page doesn't exist. But there's plenty more to explore." msgstr "هذه الصفحة غير موجودة، لكن هناك الكثير لاستكشافه." +#: src/fluxer_marketing/components/footer.gleam:342 +msgid "This product includes GeoLite2 Data created by MaxMind, available from " +msgstr "يتضمن هذا المنتج بيانات GeoLite2 التي أنشأتها MaxMind، والمتاحة من " + #: src/fluxer_marketing/components/coming_features.gleam:37 msgid "Threads and forums" msgstr "سلاسل المحادثة والمنتديات" @@ -2066,7 +2068,7 @@ msgstr "نتائج لـ" msgid "{0} of {1} slots left" msgstr "تبقّى {0} من أصل {1} مقعدًا" -#: src/fluxer_marketing/components/footer.gleam:325 +#: src/fluxer_marketing/components/footer.gleam:336 msgid "© Fluxer Platform AB (Swedish limited liability company: 559537-3993)" msgstr "© Fluxer Platform AB (شركة سويدية ذات مسؤولية محدودة: 559537-3993)" diff --git a/fluxer_marketing/locales/bg/messages.po b/fluxer_marketing/locales/bg/messages.po index 4be44272..eb459235 100644 --- a/fluxer_marketing/locales/bg/messages.po +++ b/fluxer_marketing/locales/bg/messages.po @@ -206,7 +206,7 @@ msgid "Blue (Da Ba Dee)" msgstr "Синьо (Da Ba Dee)" #: src/fluxer_marketing/components/footer.gleam:157 -#: src/fluxer_marketing/components/navigation.gleam:449 +#: src/fluxer_marketing/components/navigation.gleam:458 msgid "Bluesky" msgstr "Bluesky" @@ -363,15 +363,15 @@ msgstr "Компактен режим и опции за показване" msgid "Company" msgstr "Компания" -#: src/fluxer_marketing/components/footer.gleam:271 +#: src/fluxer_marketing/components/footer.gleam:282 #: src/fluxer_marketing/components/navigation.gleam:376 #: src/fluxer_marketing/pages/company_page.gleam:41 #: src/fluxer_marketing/pages/company_page.gleam:44 msgid "Company Information" msgstr "Информация за компанията" -#: src/fluxer_marketing/components/footer.gleam:278 -#: src/fluxer_marketing/components/navigation.gleam:426 +#: src/fluxer_marketing/components/footer.gleam:289 +#: src/fluxer_marketing/components/navigation.gleam:435 msgid "Connect" msgstr "Свържете се" @@ -1010,10 +1010,6 @@ msgstr "Как планирате да използвате Fluxer с вашат msgid "Hungarian" msgstr "Унгарски" -#: src/fluxer_marketing/components/footer.gleam:329 -msgid "IP geolocation data by " -msgstr "Данни за IP геолокация от " - #: src/fluxer_marketing/pages/help_article_page.gleam:223 #: src/fluxer_marketing/pages/help_article_page.gleam:365 msgid "If you couldn't find what you're looking for, our support team is here to help." @@ -1267,7 +1263,7 @@ msgid "Open Fluxer" msgstr "Отворете Fluxer" #: src/fluxer_marketing/components/hero.gleam:82 -#: src/fluxer_marketing/components/navigation.gleam:473 +#: src/fluxer_marketing/components/navigation.gleam:482 msgid "Open in Browser" msgstr "Отваряне в браузъра" @@ -1501,7 +1497,7 @@ msgstr "Готови ли сте да надградите?" msgid "Register now" msgstr "Регистрирайте се сега" -#: src/fluxer_marketing/components/footer.gleam:311 +#: src/fluxer_marketing/components/footer.gleam:322 msgid "Report a bug" msgstr "Докладвайте бъг" @@ -1609,7 +1605,9 @@ msgstr "Резултати от търсенето за: " msgid "Search through message history" msgstr "Търсене в историята на съобщенията" +#: src/fluxer_marketing/components/footer.gleam:271 #: src/fluxer_marketing/components/get_involved_section.gleam:194 +#: src/fluxer_marketing/components/navigation.gleam:424 #: src/fluxer_marketing/pages/careers_page.gleam:156 #: src/fluxer_marketing/pages/security_page.gleam:41 msgid "Security Bug Bounty" @@ -1680,7 +1678,7 @@ msgid "Soundboard" msgstr "Soundboard" #: src/fluxer_marketing/components/footer.gleam:143 -#: src/fluxer_marketing/components/navigation.gleam:438 +#: src/fluxer_marketing/components/navigation.gleam:447 msgid "Source Code" msgstr "Изходен код" @@ -1781,6 +1779,10 @@ msgstr "Това все още не е пълен заместител на на msgid "This page doesn't exist. But there's plenty more to explore." msgstr "Тази страница не съществува. Но има още много за разглеждане." +#: src/fluxer_marketing/components/footer.gleam:342 +msgid "This product includes GeoLite2 Data created by MaxMind, available from " +msgstr "Това изделие включва GeoLite2 данни, създадени от MaxMind, налични от " + #: src/fluxer_marketing/components/coming_features.gleam:37 msgid "Threads and forums" msgstr "Нишки и форуми" @@ -2066,7 +2068,7 @@ msgstr "резултати за" msgid "{0} of {1} slots left" msgstr "Остават {0} от {1} места" -#: src/fluxer_marketing/components/footer.gleam:325 +#: src/fluxer_marketing/components/footer.gleam:336 msgid "© Fluxer Platform AB (Swedish limited liability company: 559537-3993)" msgstr "© Fluxer Platform AB (шведско дружество с ограничена отговорност: 559537-3993)" diff --git a/fluxer_marketing/locales/cs/messages.po b/fluxer_marketing/locales/cs/messages.po index 3c25156e..df51deaa 100644 --- a/fluxer_marketing/locales/cs/messages.po +++ b/fluxer_marketing/locales/cs/messages.po @@ -206,7 +206,7 @@ msgid "Blue (Da Ba Dee)" msgstr "Modrá (Da Ba Dee)" #: src/fluxer_marketing/components/footer.gleam:157 -#: src/fluxer_marketing/components/navigation.gleam:449 +#: src/fluxer_marketing/components/navigation.gleam:458 msgid "Bluesky" msgstr "Bluesky" @@ -363,15 +363,15 @@ msgstr "Kompaktní režim a možnosti zobrazení" msgid "Company" msgstr "Společnost" -#: src/fluxer_marketing/components/footer.gleam:271 +#: src/fluxer_marketing/components/footer.gleam:282 #: src/fluxer_marketing/components/navigation.gleam:376 #: src/fluxer_marketing/pages/company_page.gleam:41 #: src/fluxer_marketing/pages/company_page.gleam:44 msgid "Company Information" msgstr "Informace o společnosti" -#: src/fluxer_marketing/components/footer.gleam:278 -#: src/fluxer_marketing/components/navigation.gleam:426 +#: src/fluxer_marketing/components/footer.gleam:289 +#: src/fluxer_marketing/components/navigation.gleam:435 msgid "Connect" msgstr "Spojit se" @@ -1010,10 +1010,6 @@ msgstr "Jak plánujete používat Fluxer se svou komunitou" msgid "Hungarian" msgstr "Maďarština" -#: src/fluxer_marketing/components/footer.gleam:329 -msgid "IP geolocation data by " -msgstr "Geolokační data podle IP od " - #: src/fluxer_marketing/pages/help_article_page.gleam:223 #: src/fluxer_marketing/pages/help_article_page.gleam:365 msgid "If you couldn't find what you're looking for, our support team is here to help." @@ -1267,7 +1263,7 @@ msgid "Open Fluxer" msgstr "Otevřít Fluxer" #: src/fluxer_marketing/components/hero.gleam:82 -#: src/fluxer_marketing/components/navigation.gleam:473 +#: src/fluxer_marketing/components/navigation.gleam:482 msgid "Open in Browser" msgstr "Otevřít v prohlížeči" @@ -1501,7 +1497,7 @@ msgstr "Jste připraveni na upgrade?" msgid "Register now" msgstr "Zaregistrujte se" -#: src/fluxer_marketing/components/footer.gleam:311 +#: src/fluxer_marketing/components/footer.gleam:322 msgid "Report a bug" msgstr "Nahlásit chybu" @@ -1609,7 +1605,9 @@ msgstr "Výsledky vyhledávání pro: " msgid "Search through message history" msgstr "Vyhledávání v historii zpráv" +#: src/fluxer_marketing/components/footer.gleam:271 #: src/fluxer_marketing/components/get_involved_section.gleam:194 +#: src/fluxer_marketing/components/navigation.gleam:424 #: src/fluxer_marketing/pages/careers_page.gleam:156 #: src/fluxer_marketing/pages/security_page.gleam:41 msgid "Security Bug Bounty" @@ -1680,7 +1678,7 @@ msgid "Soundboard" msgstr "Zvukový panel" #: src/fluxer_marketing/components/footer.gleam:143 -#: src/fluxer_marketing/components/navigation.gleam:438 +#: src/fluxer_marketing/components/navigation.gleam:447 msgid "Source Code" msgstr "Zdrojový kód" @@ -1781,6 +1779,10 @@ msgstr "Zatím to není plnohodnotná náhrada desktopové aplikace a některé msgid "This page doesn't exist. But there's plenty more to explore." msgstr "Tato stránka neexistuje. Ale je tu spousta dalšího k prozkoumání." +#: src/fluxer_marketing/components/footer.gleam:342 +msgid "This product includes GeoLite2 Data created by MaxMind, available from " +msgstr "Tento produkt obsahuje data GeoLite2 vytvořená společností MaxMind, dostupná z " + #: src/fluxer_marketing/components/coming_features.gleam:37 msgid "Threads and forums" msgstr "Vlákna a fóra" @@ -2066,7 +2068,7 @@ msgstr "výsledky pro" msgid "{0} of {1} slots left" msgstr "Zbývá {0} z {1} slotů" -#: src/fluxer_marketing/components/footer.gleam:325 +#: src/fluxer_marketing/components/footer.gleam:336 msgid "© Fluxer Platform AB (Swedish limited liability company: 559537-3993)" msgstr "© Fluxer Platform AB (švédská společnost s ručením omezeným: 559537-3993)" diff --git a/fluxer_marketing/locales/da/messages.po b/fluxer_marketing/locales/da/messages.po index 400ac3be..34b8c6f6 100644 --- a/fluxer_marketing/locales/da/messages.po +++ b/fluxer_marketing/locales/da/messages.po @@ -206,7 +206,7 @@ msgid "Blue (Da Ba Dee)" msgstr "Blå (Da Ba Dee)" #: src/fluxer_marketing/components/footer.gleam:157 -#: src/fluxer_marketing/components/navigation.gleam:449 +#: src/fluxer_marketing/components/navigation.gleam:458 msgid "Bluesky" msgstr "Bluesky" @@ -363,15 +363,15 @@ msgstr "Kompakt tilstand og visningsindstillinger" msgid "Company" msgstr "Virksomhed" -#: src/fluxer_marketing/components/footer.gleam:271 +#: src/fluxer_marketing/components/footer.gleam:282 #: src/fluxer_marketing/components/navigation.gleam:376 #: src/fluxer_marketing/pages/company_page.gleam:41 #: src/fluxer_marketing/pages/company_page.gleam:44 msgid "Company Information" msgstr "Virksomhedsoplysninger" -#: src/fluxer_marketing/components/footer.gleam:278 -#: src/fluxer_marketing/components/navigation.gleam:426 +#: src/fluxer_marketing/components/footer.gleam:289 +#: src/fluxer_marketing/components/navigation.gleam:435 msgid "Connect" msgstr "Kontakt" @@ -1010,10 +1010,6 @@ msgstr "Hvordan du planlægger at bruge Fluxer med dit fællesskab" msgid "Hungarian" msgstr "Ungarsk" -#: src/fluxer_marketing/components/footer.gleam:329 -msgid "IP geolocation data by " -msgstr "IP-geolokationsdata leveret af " - #: src/fluxer_marketing/pages/help_article_page.gleam:223 #: src/fluxer_marketing/pages/help_article_page.gleam:365 msgid "If you couldn't find what you're looking for, our support team is here to help." @@ -1267,7 +1263,7 @@ msgid "Open Fluxer" msgstr "Åbn Fluxer" #: src/fluxer_marketing/components/hero.gleam:82 -#: src/fluxer_marketing/components/navigation.gleam:473 +#: src/fluxer_marketing/components/navigation.gleam:482 msgid "Open in Browser" msgstr "Åbn i browseren" @@ -1501,7 +1497,7 @@ msgstr "Klar til at opgradere?" msgid "Register now" msgstr "Tilmeld dig nu" -#: src/fluxer_marketing/components/footer.gleam:311 +#: src/fluxer_marketing/components/footer.gleam:322 msgid "Report a bug" msgstr "Rapportér en fejl" @@ -1609,7 +1605,9 @@ msgstr "Søgeresultater for: " msgid "Search through message history" msgstr "Søg i beskedhistorik" +#: src/fluxer_marketing/components/footer.gleam:271 #: src/fluxer_marketing/components/get_involved_section.gleam:194 +#: src/fluxer_marketing/components/navigation.gleam:424 #: src/fluxer_marketing/pages/careers_page.gleam:156 #: src/fluxer_marketing/pages/security_page.gleam:41 msgid "Security Bug Bounty" @@ -1680,7 +1678,7 @@ msgid "Soundboard" msgstr "Soundboard" #: src/fluxer_marketing/components/footer.gleam:143 -#: src/fluxer_marketing/components/navigation.gleam:438 +#: src/fluxer_marketing/components/navigation.gleam:447 msgid "Source Code" msgstr "Kildekode" @@ -1781,6 +1779,10 @@ msgstr "Dette er endnu ikke en fuld erstatning for desktop-appen, og nogle ting, msgid "This page doesn't exist. But there's plenty more to explore." msgstr "Denne side findes ikke. Men der er masser mere at udforske." +#: src/fluxer_marketing/components/footer.gleam:342 +msgid "This product includes GeoLite2 Data created by MaxMind, available from " +msgstr "Dette produkt inkluderer GeoLite2-data oprettet af MaxMind, tilgængelig fra " + #: src/fluxer_marketing/components/coming_features.gleam:37 msgid "Threads and forums" msgstr "Tråde og fora" @@ -2066,7 +2068,7 @@ msgstr "resultater for" msgid "{0} of {1} slots left" msgstr "{0} af {1} pladser tilbage" -#: src/fluxer_marketing/components/footer.gleam:325 +#: src/fluxer_marketing/components/footer.gleam:336 msgid "© Fluxer Platform AB (Swedish limited liability company: 559537-3993)" msgstr "© Fluxer Platform AB (svensk aktieselskab: 559537-3993)" diff --git a/fluxer_marketing/locales/de/messages.po b/fluxer_marketing/locales/de/messages.po index 353f176b..621c6fcb 100644 --- a/fluxer_marketing/locales/de/messages.po +++ b/fluxer_marketing/locales/de/messages.po @@ -206,7 +206,7 @@ msgid "Blue (Da Ba Dee)" msgstr "Blau (Da Ba Dee)" #: src/fluxer_marketing/components/footer.gleam:157 -#: src/fluxer_marketing/components/navigation.gleam:449 +#: src/fluxer_marketing/components/navigation.gleam:458 msgid "Bluesky" msgstr "Bluesky" @@ -363,15 +363,15 @@ msgstr "Kompaktmodus und Anzeigeoptionen" msgid "Company" msgstr "Unternehmen" -#: src/fluxer_marketing/components/footer.gleam:271 +#: src/fluxer_marketing/components/footer.gleam:282 #: src/fluxer_marketing/components/navigation.gleam:376 #: src/fluxer_marketing/pages/company_page.gleam:41 #: src/fluxer_marketing/pages/company_page.gleam:44 msgid "Company Information" msgstr "Unternehmensinformationen" -#: src/fluxer_marketing/components/footer.gleam:278 -#: src/fluxer_marketing/components/navigation.gleam:426 +#: src/fluxer_marketing/components/footer.gleam:289 +#: src/fluxer_marketing/components/navigation.gleam:435 msgid "Connect" msgstr "Kontakt" @@ -1010,10 +1010,6 @@ msgstr "Wie du Fluxer in deiner Community nutzen möchtest" msgid "Hungarian" msgstr "Ungarisch" -#: src/fluxer_marketing/components/footer.gleam:329 -msgid "IP geolocation data by " -msgstr "IP-Geolokalisierungsdaten von " - #: src/fluxer_marketing/pages/help_article_page.gleam:223 #: src/fluxer_marketing/pages/help_article_page.gleam:365 msgid "If you couldn't find what you're looking for, our support team is here to help." @@ -1267,7 +1263,7 @@ msgid "Open Fluxer" msgstr "Fluxer öffnen" #: src/fluxer_marketing/components/hero.gleam:82 -#: src/fluxer_marketing/components/navigation.gleam:473 +#: src/fluxer_marketing/components/navigation.gleam:482 msgid "Open in Browser" msgstr "Im Browser öffnen" @@ -1501,7 +1497,7 @@ msgstr "Bereit fürs Upgrade?" msgid "Register now" msgstr "Jetzt registrieren" -#: src/fluxer_marketing/components/footer.gleam:311 +#: src/fluxer_marketing/components/footer.gleam:322 msgid "Report a bug" msgstr "Bug melden" @@ -1609,7 +1605,9 @@ msgstr "Suchergebnisse für: " msgid "Search through message history" msgstr "Nachrichtenverlauf durchsuchen" +#: src/fluxer_marketing/components/footer.gleam:271 #: src/fluxer_marketing/components/get_involved_section.gleam:194 +#: src/fluxer_marketing/components/navigation.gleam:424 #: src/fluxer_marketing/pages/careers_page.gleam:156 #: src/fluxer_marketing/pages/security_page.gleam:41 msgid "Security Bug Bounty" @@ -1680,7 +1678,7 @@ msgid "Soundboard" msgstr "Soundboard" #: src/fluxer_marketing/components/footer.gleam:143 -#: src/fluxer_marketing/components/navigation.gleam:438 +#: src/fluxer_marketing/components/navigation.gleam:447 msgid "Source Code" msgstr "Quellcode" @@ -1781,6 +1779,10 @@ msgstr "Das ist noch kein vollständiger Ersatz für die Desktop-App – und ein msgid "This page doesn't exist. But there's plenty more to explore." msgstr "Diese Seite gibt es nicht. Aber es gibt noch viel mehr zu entdecken." +#: src/fluxer_marketing/components/footer.gleam:342 +msgid "This product includes GeoLite2 Data created by MaxMind, available from " +msgstr "Dieses Produkt enthält GeoLite2-Daten, die von MaxMind erstellt wurden, verfügbar unter " + #: src/fluxer_marketing/components/coming_features.gleam:37 msgid "Threads and forums" msgstr "Threads und Foren" @@ -2066,7 +2068,7 @@ msgstr "Ergebnisse für" msgid "{0} of {1} slots left" msgstr "{0} von {1} Plätzen übrig" -#: src/fluxer_marketing/components/footer.gleam:325 +#: src/fluxer_marketing/components/footer.gleam:336 msgid "© Fluxer Platform AB (Swedish limited liability company: 559537-3993)" msgstr "© Fluxer Platform AB (schwedische GmbH: 559537-3993)" diff --git a/fluxer_marketing/locales/el/messages.po b/fluxer_marketing/locales/el/messages.po index dfa6b157..126fb115 100644 --- a/fluxer_marketing/locales/el/messages.po +++ b/fluxer_marketing/locales/el/messages.po @@ -206,7 +206,7 @@ msgid "Blue (Da Ba Dee)" msgstr "Μπλε (Da Ba Dee)" #: src/fluxer_marketing/components/footer.gleam:157 -#: src/fluxer_marketing/components/navigation.gleam:449 +#: src/fluxer_marketing/components/navigation.gleam:458 msgid "Bluesky" msgstr "Bluesky" @@ -363,15 +363,15 @@ msgstr "Συμπαγής λειτουργία και επιλογές εμφάν msgid "Company" msgstr "Εταιρεία" -#: src/fluxer_marketing/components/footer.gleam:271 +#: src/fluxer_marketing/components/footer.gleam:282 #: src/fluxer_marketing/components/navigation.gleam:376 #: src/fluxer_marketing/pages/company_page.gleam:41 #: src/fluxer_marketing/pages/company_page.gleam:44 msgid "Company Information" msgstr "Πληροφορίες εταιρείας" -#: src/fluxer_marketing/components/footer.gleam:278 -#: src/fluxer_marketing/components/navigation.gleam:426 +#: src/fluxer_marketing/components/footer.gleam:289 +#: src/fluxer_marketing/components/navigation.gleam:435 msgid "Connect" msgstr "Συνδέσου" @@ -1010,10 +1010,6 @@ msgstr "Πώς σκοπεύεις να χρησιμοποιήσεις το Fluxe msgid "Hungarian" msgstr "Ουγγρικά" -#: src/fluxer_marketing/components/footer.gleam:329 -msgid "IP geolocation data by " -msgstr "Δεδομένα γεωεντοπισμού IP από " - #: src/fluxer_marketing/pages/help_article_page.gleam:223 #: src/fluxer_marketing/pages/help_article_page.gleam:365 msgid "If you couldn't find what you're looking for, our support team is here to help." @@ -1267,7 +1263,7 @@ msgid "Open Fluxer" msgstr "Άνοιξε το Fluxer" #: src/fluxer_marketing/components/hero.gleam:82 -#: src/fluxer_marketing/components/navigation.gleam:473 +#: src/fluxer_marketing/components/navigation.gleam:482 msgid "Open in Browser" msgstr "Άνοιξε στο πρόγραμμα περιήγησης" @@ -1501,7 +1497,7 @@ msgstr "Έτοιμος/έτοιμη για αναβάθμιση;" msgid "Register now" msgstr "Κάνε εγγραφή τώρα" -#: src/fluxer_marketing/components/footer.gleam:311 +#: src/fluxer_marketing/components/footer.gleam:322 msgid "Report a bug" msgstr "Ανάφερε bug" @@ -1609,7 +1605,9 @@ msgstr "Αποτελέσματα αναζήτησης για: " msgid "Search through message history" msgstr "Αναζήτηση στο ιστορικό μηνυμάτων" +#: src/fluxer_marketing/components/footer.gleam:271 #: src/fluxer_marketing/components/get_involved_section.gleam:194 +#: src/fluxer_marketing/components/navigation.gleam:424 #: src/fluxer_marketing/pages/careers_page.gleam:156 #: src/fluxer_marketing/pages/security_page.gleam:41 msgid "Security Bug Bounty" @@ -1680,7 +1678,7 @@ msgid "Soundboard" msgstr "Πίνακας ήχων" #: src/fluxer_marketing/components/footer.gleam:143 -#: src/fluxer_marketing/components/navigation.gleam:438 +#: src/fluxer_marketing/components/navigation.gleam:447 msgid "Source Code" msgstr "Πηγαίος κώδικας" @@ -1781,6 +1779,10 @@ msgstr "Αυτό δεν αντικαθιστά πλήρως ακόμη την ε msgid "This page doesn't exist. But there's plenty more to explore." msgstr "Αυτή η σελίδα δεν υπάρχει. Αλλά υπάρχουν πολλά ακόμα να εξερευνήσεις." +#: src/fluxer_marketing/components/footer.gleam:342 +msgid "This product includes GeoLite2 Data created by MaxMind, available from " +msgstr "Αυτό το προϊόν περιλαμβάνει δεδομένα GeoLite2 που δημιουργήθηκαν από την MaxMind, διαθέσιμα από " + #: src/fluxer_marketing/components/coming_features.gleam:37 msgid "Threads and forums" msgstr "Νήματα και φόρουμ" @@ -2066,7 +2068,7 @@ msgstr "αποτελέσματα για" msgid "{0} of {1} slots left" msgstr "Απομένουν {0} από {1} θέσεις" -#: src/fluxer_marketing/components/footer.gleam:325 +#: src/fluxer_marketing/components/footer.gleam:336 msgid "© Fluxer Platform AB (Swedish limited liability company: 559537-3993)" msgstr "© Fluxer Platform AB (σουηδική ανώνυμη εταιρεία: 559537-3993)" diff --git a/fluxer_marketing/locales/en-GB/messages.po b/fluxer_marketing/locales/en-GB/messages.po index 3e580793..feee4908 100644 --- a/fluxer_marketing/locales/en-GB/messages.po +++ b/fluxer_marketing/locales/en-GB/messages.po @@ -206,7 +206,7 @@ msgid "Blue (Da Ba Dee)" msgstr "Blue (Da Ba Dee)" #: src/fluxer_marketing/components/footer.gleam:157 -#: src/fluxer_marketing/components/navigation.gleam:449 +#: src/fluxer_marketing/components/navigation.gleam:458 msgid "Bluesky" msgstr "Bluesky" @@ -363,15 +363,15 @@ msgstr "Compact mode and display options" msgid "Company" msgstr "Company" -#: src/fluxer_marketing/components/footer.gleam:271 +#: src/fluxer_marketing/components/footer.gleam:282 #: src/fluxer_marketing/components/navigation.gleam:376 #: src/fluxer_marketing/pages/company_page.gleam:41 #: src/fluxer_marketing/pages/company_page.gleam:44 msgid "Company Information" msgstr "Company information" -#: src/fluxer_marketing/components/footer.gleam:278 -#: src/fluxer_marketing/components/navigation.gleam:426 +#: src/fluxer_marketing/components/footer.gleam:289 +#: src/fluxer_marketing/components/navigation.gleam:435 msgid "Connect" msgstr "Connect" @@ -1010,10 +1010,6 @@ msgstr "How you plan to use Fluxer with your community" msgid "Hungarian" msgstr "Hungarian" -#: src/fluxer_marketing/components/footer.gleam:329 -msgid "IP geolocation data by " -msgstr "IP geolocation data by " - #: src/fluxer_marketing/pages/help_article_page.gleam:223 #: src/fluxer_marketing/pages/help_article_page.gleam:365 msgid "If you couldn't find what you're looking for, our support team is here to help." @@ -1267,7 +1263,7 @@ msgid "Open Fluxer" msgstr "Open Fluxer" #: src/fluxer_marketing/components/hero.gleam:82 -#: src/fluxer_marketing/components/navigation.gleam:473 +#: src/fluxer_marketing/components/navigation.gleam:482 msgid "Open in Browser" msgstr "Open in browser" @@ -1501,7 +1497,7 @@ msgstr "Ready to upgrade?" msgid "Register now" msgstr "Register now" -#: src/fluxer_marketing/components/footer.gleam:311 +#: src/fluxer_marketing/components/footer.gleam:322 msgid "Report a bug" msgstr "Report a bug" @@ -1609,7 +1605,9 @@ msgstr "Search results for: " msgid "Search through message history" msgstr "Search through message history" +#: src/fluxer_marketing/components/footer.gleam:271 #: src/fluxer_marketing/components/get_involved_section.gleam:194 +#: src/fluxer_marketing/components/navigation.gleam:424 #: src/fluxer_marketing/pages/careers_page.gleam:156 #: src/fluxer_marketing/pages/security_page.gleam:41 msgid "Security Bug Bounty" @@ -1680,7 +1678,7 @@ msgid "Soundboard" msgstr "Soundboard" #: src/fluxer_marketing/components/footer.gleam:143 -#: src/fluxer_marketing/components/navigation.gleam:438 +#: src/fluxer_marketing/components/navigation.gleam:447 msgid "Source Code" msgstr "Source code" @@ -1781,6 +1779,10 @@ msgstr "This isn't a full replacement for the desktop app yet, and some things y msgid "This page doesn't exist. But there's plenty more to explore." msgstr "This page doesn't exist. But there's plenty more to explore." +#: src/fluxer_marketing/components/footer.gleam:342 +msgid "This product includes GeoLite2 Data created by MaxMind, available from " +msgstr "This product includes GeoLite2 Data created by MaxMind, available from " + #: src/fluxer_marketing/components/coming_features.gleam:37 msgid "Threads and forums" msgstr "Threads and forums" @@ -2066,7 +2068,7 @@ msgstr "results for" msgid "{0} of {1} slots left" msgstr "{0} of {1} slots left" -#: src/fluxer_marketing/components/footer.gleam:325 +#: src/fluxer_marketing/components/footer.gleam:336 msgid "© Fluxer Platform AB (Swedish limited liability company: 559537-3993)" msgstr "© Fluxer Platform AB (Swedish limited liability company: 559537-3993)" diff --git a/fluxer_marketing/locales/es-419/messages.po b/fluxer_marketing/locales/es-419/messages.po index 8810a3b7..58cec901 100644 --- a/fluxer_marketing/locales/es-419/messages.po +++ b/fluxer_marketing/locales/es-419/messages.po @@ -206,7 +206,7 @@ msgid "Blue (Da Ba Dee)" msgstr "Azul (Da Ba Dee)" #: src/fluxer_marketing/components/footer.gleam:157 -#: src/fluxer_marketing/components/navigation.gleam:449 +#: src/fluxer_marketing/components/navigation.gleam:458 msgid "Bluesky" msgstr "Bluesky" @@ -363,15 +363,15 @@ msgstr "Modo compacto y opciones de visualización" msgid "Company" msgstr "Empresa" -#: src/fluxer_marketing/components/footer.gleam:271 +#: src/fluxer_marketing/components/footer.gleam:282 #: src/fluxer_marketing/components/navigation.gleam:376 #: src/fluxer_marketing/pages/company_page.gleam:41 #: src/fluxer_marketing/pages/company_page.gleam:44 msgid "Company Information" msgstr "Información de la empresa" -#: src/fluxer_marketing/components/footer.gleam:278 -#: src/fluxer_marketing/components/navigation.gleam:426 +#: src/fluxer_marketing/components/footer.gleam:289 +#: src/fluxer_marketing/components/navigation.gleam:435 msgid "Connect" msgstr "Conéctate" @@ -1010,10 +1010,6 @@ msgstr "Cómo planeas usar Fluxer con tu comunidad" msgid "Hungarian" msgstr "Húngaro" -#: src/fluxer_marketing/components/footer.gleam:329 -msgid "IP geolocation data by " -msgstr "Datos de geolocalización por IP de " - #: src/fluxer_marketing/pages/help_article_page.gleam:223 #: src/fluxer_marketing/pages/help_article_page.gleam:365 msgid "If you couldn't find what you're looking for, our support team is here to help." @@ -1267,7 +1263,7 @@ msgid "Open Fluxer" msgstr "Abrir Fluxer" #: src/fluxer_marketing/components/hero.gleam:82 -#: src/fluxer_marketing/components/navigation.gleam:473 +#: src/fluxer_marketing/components/navigation.gleam:482 msgid "Open in Browser" msgstr "Abrir en el navegador" @@ -1501,7 +1497,7 @@ msgstr "¿Listo para mejorar tu plan?" msgid "Register now" msgstr "Regístrate ahora" -#: src/fluxer_marketing/components/footer.gleam:311 +#: src/fluxer_marketing/components/footer.gleam:322 msgid "Report a bug" msgstr "Reportar un bug" @@ -1609,7 +1605,9 @@ msgstr "Resultados de búsqueda para: " msgid "Search through message history" msgstr "Busca en el historial de mensajes" +#: src/fluxer_marketing/components/footer.gleam:271 #: src/fluxer_marketing/components/get_involved_section.gleam:194 +#: src/fluxer_marketing/components/navigation.gleam:424 #: src/fluxer_marketing/pages/careers_page.gleam:156 #: src/fluxer_marketing/pages/security_page.gleam:41 msgid "Security Bug Bounty" @@ -1680,7 +1678,7 @@ msgid "Soundboard" msgstr "Panel de sonidos" #: src/fluxer_marketing/components/footer.gleam:143 -#: src/fluxer_marketing/components/navigation.gleam:438 +#: src/fluxer_marketing/components/navigation.gleam:447 msgid "Source Code" msgstr "Código fuente" @@ -1781,6 +1779,10 @@ msgstr "Todavía no reemplaza por completo a la app de escritorio, y aún faltan msgid "This page doesn't exist. But there's plenty more to explore." msgstr "Esta página no existe, pero hay mucho más por explorar." +#: src/fluxer_marketing/components/footer.gleam:342 +msgid "This product includes GeoLite2 Data created by MaxMind, available from " +msgstr "Este producto incluye datos de GeoLite2 creados por MaxMind, disponibles en " + #: src/fluxer_marketing/components/coming_features.gleam:37 msgid "Threads and forums" msgstr "Hilos y foros" @@ -2066,7 +2068,7 @@ msgstr "resultados para" msgid "{0} of {1} slots left" msgstr "Quedan {0} de {1} cupos" -#: src/fluxer_marketing/components/footer.gleam:325 +#: src/fluxer_marketing/components/footer.gleam:336 msgid "© Fluxer Platform AB (Swedish limited liability company: 559537-3993)" msgstr "© Fluxer Platform AB (sociedad de responsabilidad limitada sueca: 559537-3993)" diff --git a/fluxer_marketing/locales/es-ES/messages.po b/fluxer_marketing/locales/es-ES/messages.po index 02bc82ba..38bad6e2 100644 --- a/fluxer_marketing/locales/es-ES/messages.po +++ b/fluxer_marketing/locales/es-ES/messages.po @@ -206,7 +206,7 @@ msgid "Blue (Da Ba Dee)" msgstr "Azul (Da Ba Dee)" #: src/fluxer_marketing/components/footer.gleam:157 -#: src/fluxer_marketing/components/navigation.gleam:449 +#: src/fluxer_marketing/components/navigation.gleam:458 msgid "Bluesky" msgstr "Bluesky" @@ -363,15 +363,15 @@ msgstr "Modo compacto y opciones de visualización" msgid "Company" msgstr "Empresa" -#: src/fluxer_marketing/components/footer.gleam:271 +#: src/fluxer_marketing/components/footer.gleam:282 #: src/fluxer_marketing/components/navigation.gleam:376 #: src/fluxer_marketing/pages/company_page.gleam:41 #: src/fluxer_marketing/pages/company_page.gleam:44 msgid "Company Information" msgstr "Información de la empresa" -#: src/fluxer_marketing/components/footer.gleam:278 -#: src/fluxer_marketing/components/navigation.gleam:426 +#: src/fluxer_marketing/components/footer.gleam:289 +#: src/fluxer_marketing/components/navigation.gleam:435 msgid "Connect" msgstr "Conectar" @@ -1010,10 +1010,6 @@ msgstr "Cómo piensas usar Fluxer con tu comunidad" msgid "Hungarian" msgstr "Húngaro" -#: src/fluxer_marketing/components/footer.gleam:329 -msgid "IP geolocation data by " -msgstr "Datos de geolocalización por IP de " - #: src/fluxer_marketing/pages/help_article_page.gleam:223 #: src/fluxer_marketing/pages/help_article_page.gleam:365 msgid "If you couldn't find what you're looking for, our support team is here to help." @@ -1267,7 +1263,7 @@ msgid "Open Fluxer" msgstr "Abrir Fluxer" #: src/fluxer_marketing/components/hero.gleam:82 -#: src/fluxer_marketing/components/navigation.gleam:473 +#: src/fluxer_marketing/components/navigation.gleam:482 msgid "Open in Browser" msgstr "Abrir en el navegador" @@ -1501,7 +1497,7 @@ msgstr "¿Listo para mejorar?" msgid "Register now" msgstr "Regístrate ahora" -#: src/fluxer_marketing/components/footer.gleam:311 +#: src/fluxer_marketing/components/footer.gleam:322 msgid "Report a bug" msgstr "Reportar un error" @@ -1609,7 +1605,9 @@ msgstr "Resultados de búsqueda para: " msgid "Search through message history" msgstr "Busca en el historial de mensajes" +#: src/fluxer_marketing/components/footer.gleam:271 #: src/fluxer_marketing/components/get_involved_section.gleam:194 +#: src/fluxer_marketing/components/navigation.gleam:424 #: src/fluxer_marketing/pages/careers_page.gleam:156 #: src/fluxer_marketing/pages/security_page.gleam:41 msgid "Security Bug Bounty" @@ -1680,7 +1678,7 @@ msgid "Soundboard" msgstr "Panel de sonidos" #: src/fluxer_marketing/components/footer.gleam:143 -#: src/fluxer_marketing/components/navigation.gleam:438 +#: src/fluxer_marketing/components/navigation.gleam:447 msgid "Source Code" msgstr "Código fuente" @@ -1781,6 +1779,10 @@ msgstr "Aún no sustituye por completo a la app de escritorio, y todavía faltan msgid "This page doesn't exist. But there's plenty more to explore." msgstr "Esta página no existe. Pero aún hay mucho más por explorar." +#: src/fluxer_marketing/components/footer.gleam:342 +msgid "This product includes GeoLite2 Data created by MaxMind, available from " +msgstr "Este producto incluye datos de GeoLite2 creados por MaxMind, disponibles en " + #: src/fluxer_marketing/components/coming_features.gleam:37 msgid "Threads and forums" msgstr "Hilos y foros" @@ -2066,7 +2068,7 @@ msgstr "resultados para" msgid "{0} of {1} slots left" msgstr "Quedan {0} de {1} plazas" -#: src/fluxer_marketing/components/footer.gleam:325 +#: src/fluxer_marketing/components/footer.gleam:336 msgid "© Fluxer Platform AB (Swedish limited liability company: 559537-3993)" msgstr "© Fluxer Platform AB (sociedad limitada sueca: 559537-3993)" diff --git a/fluxer_marketing/locales/fi/messages.po b/fluxer_marketing/locales/fi/messages.po index 2a161cfa..ef6d2f93 100644 --- a/fluxer_marketing/locales/fi/messages.po +++ b/fluxer_marketing/locales/fi/messages.po @@ -206,7 +206,7 @@ msgid "Blue (Da Ba Dee)" msgstr "Sininen (Da Ba Dee)" #: src/fluxer_marketing/components/footer.gleam:157 -#: src/fluxer_marketing/components/navigation.gleam:449 +#: src/fluxer_marketing/components/navigation.gleam:458 msgid "Bluesky" msgstr "Bluesky" @@ -363,15 +363,15 @@ msgstr "Kompakti tila ja näyttöasetukset" msgid "Company" msgstr "Yritys" -#: src/fluxer_marketing/components/footer.gleam:271 +#: src/fluxer_marketing/components/footer.gleam:282 #: src/fluxer_marketing/components/navigation.gleam:376 #: src/fluxer_marketing/pages/company_page.gleam:41 #: src/fluxer_marketing/pages/company_page.gleam:44 msgid "Company Information" msgstr "Yritystiedot" -#: src/fluxer_marketing/components/footer.gleam:278 -#: src/fluxer_marketing/components/navigation.gleam:426 +#: src/fluxer_marketing/components/footer.gleam:289 +#: src/fluxer_marketing/components/navigation.gleam:435 msgid "Connect" msgstr "Yhteydet" @@ -1010,10 +1010,6 @@ msgstr "Miten aiot käyttää Fluxeria yhteisösi kanssa" msgid "Hungarian" msgstr "unkari" -#: src/fluxer_marketing/components/footer.gleam:329 -msgid "IP geolocation data by " -msgstr "IP-paikannustiedot tarjoaa " - #: src/fluxer_marketing/pages/help_article_page.gleam:223 #: src/fluxer_marketing/pages/help_article_page.gleam:365 msgid "If you couldn't find what you're looking for, our support team is here to help." @@ -1267,7 +1263,7 @@ msgid "Open Fluxer" msgstr "Avaa Fluxer" #: src/fluxer_marketing/components/hero.gleam:82 -#: src/fluxer_marketing/components/navigation.gleam:473 +#: src/fluxer_marketing/components/navigation.gleam:482 msgid "Open in Browser" msgstr "Avaa selaimessa" @@ -1501,7 +1497,7 @@ msgstr "Valmiina päivittämään?" msgid "Register now" msgstr "Rekisteröidy nyt" -#: src/fluxer_marketing/components/footer.gleam:311 +#: src/fluxer_marketing/components/footer.gleam:322 msgid "Report a bug" msgstr "Ilmoita virheestä" @@ -1609,7 +1605,9 @@ msgstr "Hakutulokset haulle: " msgid "Search through message history" msgstr "Hae viestihistoriasta" +#: src/fluxer_marketing/components/footer.gleam:271 #: src/fluxer_marketing/components/get_involved_section.gleam:194 +#: src/fluxer_marketing/components/navigation.gleam:424 #: src/fluxer_marketing/pages/careers_page.gleam:156 #: src/fluxer_marketing/pages/security_page.gleam:41 msgid "Security Bug Bounty" @@ -1680,7 +1678,7 @@ msgid "Soundboard" msgstr "Äänipaneeli" #: src/fluxer_marketing/components/footer.gleam:143 -#: src/fluxer_marketing/components/navigation.gleam:438 +#: src/fluxer_marketing/components/navigation.gleam:447 msgid "Source Code" msgstr "Lähdekoodi" @@ -1781,6 +1779,10 @@ msgstr "Tämä ei vielä täysin korvaa työpöytäsovellusta, ja osa mobiilicha msgid "This page doesn't exist. But there's plenty more to explore." msgstr "Tätä sivua ei ole olemassa. Mutta tutkittavaa riittää vielä paljon." +#: src/fluxer_marketing/components/footer.gleam:342 +msgid "This product includes GeoLite2 Data created by MaxMind, available from " +msgstr "Tämä tuote sisältää GeoLite2-tiedot, jotka on luonut MaxMind, saatavilla osoitteesta " + #: src/fluxer_marketing/components/coming_features.gleam:37 msgid "Threads and forums" msgstr "Ketjut ja foorumit" @@ -2066,7 +2068,7 @@ msgstr "hakutulokset haulle" msgid "{0} of {1} slots left" msgstr "{0}/{1} paikkaa jäljellä" -#: src/fluxer_marketing/components/footer.gleam:325 +#: src/fluxer_marketing/components/footer.gleam:336 msgid "© Fluxer Platform AB (Swedish limited liability company: 559537-3993)" msgstr "© Fluxer Platform AB (ruotsalainen osakeyhtiö: 559537-3993)" diff --git a/fluxer_marketing/locales/fr/messages.po b/fluxer_marketing/locales/fr/messages.po index 0195eb01..c0b9cf60 100644 --- a/fluxer_marketing/locales/fr/messages.po +++ b/fluxer_marketing/locales/fr/messages.po @@ -206,7 +206,7 @@ msgid "Blue (Da Ba Dee)" msgstr "Bleu (Da Ba Dee)" #: src/fluxer_marketing/components/footer.gleam:157 -#: src/fluxer_marketing/components/navigation.gleam:449 +#: src/fluxer_marketing/components/navigation.gleam:458 msgid "Bluesky" msgstr "Bluesky" @@ -363,15 +363,15 @@ msgstr "Mode compact et options d’affichage" msgid "Company" msgstr "Entreprise" -#: src/fluxer_marketing/components/footer.gleam:271 +#: src/fluxer_marketing/components/footer.gleam:282 #: src/fluxer_marketing/components/navigation.gleam:376 #: src/fluxer_marketing/pages/company_page.gleam:41 #: src/fluxer_marketing/pages/company_page.gleam:44 msgid "Company Information" msgstr "Informations sur l’entreprise" -#: src/fluxer_marketing/components/footer.gleam:278 -#: src/fluxer_marketing/components/navigation.gleam:426 +#: src/fluxer_marketing/components/footer.gleam:289 +#: src/fluxer_marketing/components/navigation.gleam:435 msgid "Connect" msgstr "Contact" @@ -1010,10 +1010,6 @@ msgstr "Comment vous comptez utiliser Fluxer avec votre communauté" msgid "Hungarian" msgstr "Hongrois" -#: src/fluxer_marketing/components/footer.gleam:329 -msgid "IP geolocation data by " -msgstr "Données de géolocalisation IP fournies par " - #: src/fluxer_marketing/pages/help_article_page.gleam:223 #: src/fluxer_marketing/pages/help_article_page.gleam:365 msgid "If you couldn't find what you're looking for, our support team is here to help." @@ -1267,7 +1263,7 @@ msgid "Open Fluxer" msgstr "Ouvrir Fluxer" #: src/fluxer_marketing/components/hero.gleam:82 -#: src/fluxer_marketing/components/navigation.gleam:473 +#: src/fluxer_marketing/components/navigation.gleam:482 msgid "Open in Browser" msgstr "Ouvrir dans le navigateur" @@ -1501,7 +1497,7 @@ msgstr "Prêt à passer à la version supérieure ?" msgid "Register now" msgstr "S’inscrire maintenant" -#: src/fluxer_marketing/components/footer.gleam:311 +#: src/fluxer_marketing/components/footer.gleam:322 msgid "Report a bug" msgstr "Signaler un bug" @@ -1609,7 +1605,9 @@ msgstr "Résultats de recherche pour : " msgid "Search through message history" msgstr "Rechercher dans l’historique des messages" +#: src/fluxer_marketing/components/footer.gleam:271 #: src/fluxer_marketing/components/get_involved_section.gleam:194 +#: src/fluxer_marketing/components/navigation.gleam:424 #: src/fluxer_marketing/pages/careers_page.gleam:156 #: src/fluxer_marketing/pages/security_page.gleam:41 msgid "Security Bug Bounty" @@ -1680,7 +1678,7 @@ msgid "Soundboard" msgstr "Table de sons" #: src/fluxer_marketing/components/footer.gleam:143 -#: src/fluxer_marketing/components/navigation.gleam:438 +#: src/fluxer_marketing/components/navigation.gleam:447 msgid "Source Code" msgstr "Code source" @@ -1781,6 +1779,10 @@ msgstr "Ce n’est pas encore un remplacement complet de l’app de bureau, et c msgid "This page doesn't exist. But there's plenty more to explore." msgstr "Cette page n’existe pas. Mais il y a encore plein de choses à découvrir." +#: src/fluxer_marketing/components/footer.gleam:342 +msgid "This product includes GeoLite2 Data created by MaxMind, available from " +msgstr "Ce produit inclut les données GeoLite2 créées par MaxMind, disponibles à partir de " + #: src/fluxer_marketing/components/coming_features.gleam:37 msgid "Threads and forums" msgstr "Fils de discussion et forums" @@ -2066,7 +2068,7 @@ msgstr "résultats pour" msgid "{0} of {1} slots left" msgstr "Plus que {0} places sur {1}" -#: src/fluxer_marketing/components/footer.gleam:325 +#: src/fluxer_marketing/components/footer.gleam:336 msgid "© Fluxer Platform AB (Swedish limited liability company: 559537-3993)" msgstr "© Fluxer Platform AB (société à responsabilité limitée suédoise : 559537-3993)" diff --git a/fluxer_marketing/locales/he/messages.po b/fluxer_marketing/locales/he/messages.po index dd9ef6f3..87c43776 100644 --- a/fluxer_marketing/locales/he/messages.po +++ b/fluxer_marketing/locales/he/messages.po @@ -206,7 +206,7 @@ msgid "Blue (Da Ba Dee)" msgstr "כחול (Da Ba Dee)" #: src/fluxer_marketing/components/footer.gleam:157 -#: src/fluxer_marketing/components/navigation.gleam:449 +#: src/fluxer_marketing/components/navigation.gleam:458 msgid "Bluesky" msgstr "Bluesky" @@ -363,15 +363,15 @@ msgstr "מצב קומפקטי ואפשרויות תצוגה" msgid "Company" msgstr "חברה" -#: src/fluxer_marketing/components/footer.gleam:271 +#: src/fluxer_marketing/components/footer.gleam:282 #: src/fluxer_marketing/components/navigation.gleam:376 #: src/fluxer_marketing/pages/company_page.gleam:41 #: src/fluxer_marketing/pages/company_page.gleam:44 msgid "Company Information" msgstr "מידע על החברה" -#: src/fluxer_marketing/components/footer.gleam:278 -#: src/fluxer_marketing/components/navigation.gleam:426 +#: src/fluxer_marketing/components/footer.gleam:289 +#: src/fluxer_marketing/components/navigation.gleam:435 msgid "Connect" msgstr "יצירת קשר" @@ -1010,10 +1010,6 @@ msgstr "איך אתם מתכננים להשתמש ב-Fluxer עם הקהילה ש msgid "Hungarian" msgstr "הונגרית" -#: src/fluxer_marketing/components/footer.gleam:329 -msgid "IP geolocation data by " -msgstr "נתוני מיקום לפי IP מאת " - #: src/fluxer_marketing/pages/help_article_page.gleam:223 #: src/fluxer_marketing/pages/help_article_page.gleam:365 msgid "If you couldn't find what you're looking for, our support team is here to help." @@ -1267,7 +1263,7 @@ msgid "Open Fluxer" msgstr "פתחו את Fluxer" #: src/fluxer_marketing/components/hero.gleam:82 -#: src/fluxer_marketing/components/navigation.gleam:473 +#: src/fluxer_marketing/components/navigation.gleam:482 msgid "Open in Browser" msgstr "פתחו בדפדפן" @@ -1501,7 +1497,7 @@ msgstr "מוכנים לשדרג?" msgid "Register now" msgstr "הירשמו עכשיו" -#: src/fluxer_marketing/components/footer.gleam:311 +#: src/fluxer_marketing/components/footer.gleam:322 msgid "Report a bug" msgstr "דווחו על באג" @@ -1609,7 +1605,9 @@ msgstr "תוצאות חיפוש עבור: " msgid "Search through message history" msgstr "חיפוש בהיסטוריית הודעות" +#: src/fluxer_marketing/components/footer.gleam:271 #: src/fluxer_marketing/components/get_involved_section.gleam:194 +#: src/fluxer_marketing/components/navigation.gleam:424 #: src/fluxer_marketing/pages/careers_page.gleam:156 #: src/fluxer_marketing/pages/security_page.gleam:41 msgid "Security Bug Bounty" @@ -1680,7 +1678,7 @@ msgid "Soundboard" msgstr "לוח צלילים" #: src/fluxer_marketing/components/footer.gleam:143 -#: src/fluxer_marketing/components/navigation.gleam:438 +#: src/fluxer_marketing/components/navigation.gleam:447 msgid "Source Code" msgstr "קוד מקור" @@ -1781,6 +1779,10 @@ msgstr "זה עדיין לא תחליף מלא לאפליקציית הדסקטו msgid "This page doesn't exist. But there's plenty more to explore." msgstr "העמוד הזה לא קיים. אבל יש עוד המון מה לגלות." +#: src/fluxer_marketing/components/footer.gleam:342 +msgid "This product includes GeoLite2 Data created by MaxMind, available from " +msgstr "מוצר זה כולל נתוני GeoLite2 שנוצרו על ידי MaxMind, זמינים מ-" + #: src/fluxer_marketing/components/coming_features.gleam:37 msgid "Threads and forums" msgstr "שרשורים ופורומים" @@ -2066,7 +2068,7 @@ msgstr "תוצאות עבור" msgid "{0} of {1} slots left" msgstr "נשארו {0} מתוך {1} מקומות" -#: src/fluxer_marketing/components/footer.gleam:325 +#: src/fluxer_marketing/components/footer.gleam:336 msgid "© Fluxer Platform AB (Swedish limited liability company: 559537-3993)" msgstr "© Fluxer Platform AB (חברה שוודית בערבון מוגבל: 559537-3993)" diff --git a/fluxer_marketing/locales/hi/messages.po b/fluxer_marketing/locales/hi/messages.po index 54c492ef..9f41fa43 100644 --- a/fluxer_marketing/locales/hi/messages.po +++ b/fluxer_marketing/locales/hi/messages.po @@ -206,7 +206,7 @@ msgid "Blue (Da Ba Dee)" msgstr "नीला (Da Ba Dee)" #: src/fluxer_marketing/components/footer.gleam:157 -#: src/fluxer_marketing/components/navigation.gleam:449 +#: src/fluxer_marketing/components/navigation.gleam:458 msgid "Bluesky" msgstr "Bluesky" @@ -363,15 +363,15 @@ msgstr "कॉम्पैक्ट मोड और डिस्प्ले msgid "Company" msgstr "कंपनी" -#: src/fluxer_marketing/components/footer.gleam:271 +#: src/fluxer_marketing/components/footer.gleam:282 #: src/fluxer_marketing/components/navigation.gleam:376 #: src/fluxer_marketing/pages/company_page.gleam:41 #: src/fluxer_marketing/pages/company_page.gleam:44 msgid "Company Information" msgstr "कंपनी जानकारी" -#: src/fluxer_marketing/components/footer.gleam:278 -#: src/fluxer_marketing/components/navigation.gleam:426 +#: src/fluxer_marketing/components/footer.gleam:289 +#: src/fluxer_marketing/components/navigation.gleam:435 msgid "Connect" msgstr "जुड़ें" @@ -1010,10 +1010,6 @@ msgstr "आप अपने समुदाय के साथ Fluxer का msgid "Hungarian" msgstr "हंगेरियन" -#: src/fluxer_marketing/components/footer.gleam:329 -msgid "IP geolocation data by " -msgstr "IP जियोलोकेशन डेटा: " - #: src/fluxer_marketing/pages/help_article_page.gleam:223 #: src/fluxer_marketing/pages/help_article_page.gleam:365 msgid "If you couldn't find what you're looking for, our support team is here to help." @@ -1267,7 +1263,7 @@ msgid "Open Fluxer" msgstr "Fluxer खोलें" #: src/fluxer_marketing/components/hero.gleam:82 -#: src/fluxer_marketing/components/navigation.gleam:473 +#: src/fluxer_marketing/components/navigation.gleam:482 msgid "Open in Browser" msgstr "ब्राउज़र में खोलें" @@ -1501,7 +1497,7 @@ msgstr "अपग्रेड के लिए तैयार?" msgid "Register now" msgstr "अभी रजिस्टर करें" -#: src/fluxer_marketing/components/footer.gleam:311 +#: src/fluxer_marketing/components/footer.gleam:322 msgid "Report a bug" msgstr "बग रिपोर्ट करें" @@ -1609,7 +1605,9 @@ msgstr "इसके लिए खोज परिणाम: " msgid "Search through message history" msgstr "संदेश इतिहास में खोजें" +#: src/fluxer_marketing/components/footer.gleam:271 #: src/fluxer_marketing/components/get_involved_section.gleam:194 +#: src/fluxer_marketing/components/navigation.gleam:424 #: src/fluxer_marketing/pages/careers_page.gleam:156 #: src/fluxer_marketing/pages/security_page.gleam:41 msgid "Security Bug Bounty" @@ -1680,7 +1678,7 @@ msgid "Soundboard" msgstr "साउंडबोर्ड" #: src/fluxer_marketing/components/footer.gleam:143 -#: src/fluxer_marketing/components/navigation.gleam:438 +#: src/fluxer_marketing/components/navigation.gleam:447 msgid "Source Code" msgstr "स्रोत कोड" @@ -1781,6 +1779,10 @@ msgstr "यह अभी डेस्कटॉप ऐप का पूरा व msgid "This page doesn't exist. But there's plenty more to explore." msgstr "यह पेज मौजूद नहीं है। लेकिन देखने के लिए और भी बहुत कुछ है।" +#: src/fluxer_marketing/components/footer.gleam:342 +msgid "This product includes GeoLite2 Data created by MaxMind, available from " +msgstr "यह उत्पाद GeoLite2 डेटा शामिल करता है जिसे MaxMind द्वारा बनाया गया है, उपलब्ध है" + #: src/fluxer_marketing/components/coming_features.gleam:37 msgid "Threads and forums" msgstr "थ्रेड्स और फ़ोरम्स" @@ -2066,7 +2068,7 @@ msgstr "परिणाम:" msgid "{0} of {1} slots left" msgstr "{1} में से {0} स्लॉट बचे हैं" -#: src/fluxer_marketing/components/footer.gleam:325 +#: src/fluxer_marketing/components/footer.gleam:336 msgid "© Fluxer Platform AB (Swedish limited liability company: 559537-3993)" msgstr "© Fluxer Platform AB (स्वीडन की लिमिटेड लायबिलिटी कंपनी: 559537-3993)" diff --git a/fluxer_marketing/locales/hr/messages.po b/fluxer_marketing/locales/hr/messages.po index d318739d..be81b820 100644 --- a/fluxer_marketing/locales/hr/messages.po +++ b/fluxer_marketing/locales/hr/messages.po @@ -206,7 +206,7 @@ msgid "Blue (Da Ba Dee)" msgstr "Plava (Da Ba Dee)" #: src/fluxer_marketing/components/footer.gleam:157 -#: src/fluxer_marketing/components/navigation.gleam:449 +#: src/fluxer_marketing/components/navigation.gleam:458 msgid "Bluesky" msgstr "Bluesky" @@ -363,15 +363,15 @@ msgstr "Kompaktni način rada i opcije prikaza" msgid "Company" msgstr "Tvrtka" -#: src/fluxer_marketing/components/footer.gleam:271 +#: src/fluxer_marketing/components/footer.gleam:282 #: src/fluxer_marketing/components/navigation.gleam:376 #: src/fluxer_marketing/pages/company_page.gleam:41 #: src/fluxer_marketing/pages/company_page.gleam:44 msgid "Company Information" msgstr "Informacije o tvrtki" -#: src/fluxer_marketing/components/footer.gleam:278 -#: src/fluxer_marketing/components/navigation.gleam:426 +#: src/fluxer_marketing/components/footer.gleam:289 +#: src/fluxer_marketing/components/navigation.gleam:435 msgid "Connect" msgstr "Poveži se" @@ -1010,10 +1010,6 @@ msgstr "Kako planirate koristiti Fluxer sa svojom zajednicom" msgid "Hungarian" msgstr "Mađarski" -#: src/fluxer_marketing/components/footer.gleam:329 -msgid "IP geolocation data by " -msgstr "IP geolokacijski podaci od " - #: src/fluxer_marketing/pages/help_article_page.gleam:223 #: src/fluxer_marketing/pages/help_article_page.gleam:365 msgid "If you couldn't find what you're looking for, our support team is here to help." @@ -1267,7 +1263,7 @@ msgid "Open Fluxer" msgstr "Otvori Fluxer" #: src/fluxer_marketing/components/hero.gleam:82 -#: src/fluxer_marketing/components/navigation.gleam:473 +#: src/fluxer_marketing/components/navigation.gleam:482 msgid "Open in Browser" msgstr "Otvori u pregledniku" @@ -1501,7 +1497,7 @@ msgstr "Spremni ste za nadogradnju?" msgid "Register now" msgstr "Registrirajte se sada" -#: src/fluxer_marketing/components/footer.gleam:311 +#: src/fluxer_marketing/components/footer.gleam:322 msgid "Report a bug" msgstr "Prijavi grešku" @@ -1609,7 +1605,9 @@ msgstr "Rezultati pretrage za: " msgid "Search through message history" msgstr "Pretražujte povijest poruka" +#: src/fluxer_marketing/components/footer.gleam:271 #: src/fluxer_marketing/components/get_involved_section.gleam:194 +#: src/fluxer_marketing/components/navigation.gleam:424 #: src/fluxer_marketing/pages/careers_page.gleam:156 #: src/fluxer_marketing/pages/security_page.gleam:41 msgid "Security Bug Bounty" @@ -1680,7 +1678,7 @@ msgid "Soundboard" msgstr "Soundboard" #: src/fluxer_marketing/components/footer.gleam:143 -#: src/fluxer_marketing/components/navigation.gleam:438 +#: src/fluxer_marketing/components/navigation.gleam:447 msgid "Source Code" msgstr "Izvorni kod" @@ -1781,6 +1779,10 @@ msgstr "Ovo još nije potpuna zamjena za desktop aplikaciju i neke stvari koje b msgid "This page doesn't exist. But there's plenty more to explore." msgstr "Ova stranica ne postoji. Ali ima još puno toga za istražiti." +#: src/fluxer_marketing/components/footer.gleam:342 +msgid "This product includes GeoLite2 Data created by MaxMind, available from " +msgstr "Ovaj proizvod uključuje GeoLite2 podatke koje je stvorio MaxMind, dostupne na " + #: src/fluxer_marketing/components/coming_features.gleam:37 msgid "Threads and forums" msgstr "Niti i forumi" @@ -2066,7 +2068,7 @@ msgstr "rezultata za" msgid "{0} of {1} slots left" msgstr "Preostalo je {0} od {1} mjesta" -#: src/fluxer_marketing/components/footer.gleam:325 +#: src/fluxer_marketing/components/footer.gleam:336 msgid "© Fluxer Platform AB (Swedish limited liability company: 559537-3993)" msgstr "© Fluxer Platform AB (švedsko društvo s ograničenom odgovornošću: 559537-3993)" diff --git a/fluxer_marketing/locales/hu/messages.po b/fluxer_marketing/locales/hu/messages.po index dbec9375..5aa1d5c8 100644 --- a/fluxer_marketing/locales/hu/messages.po +++ b/fluxer_marketing/locales/hu/messages.po @@ -206,7 +206,7 @@ msgid "Blue (Da Ba Dee)" msgstr "Kék (Da Ba Dee)" #: src/fluxer_marketing/components/footer.gleam:157 -#: src/fluxer_marketing/components/navigation.gleam:449 +#: src/fluxer_marketing/components/navigation.gleam:458 msgid "Bluesky" msgstr "Bluesky" @@ -363,15 +363,15 @@ msgstr "Kompakt mód és megjelenítési beállítások" msgid "Company" msgstr "Cég" -#: src/fluxer_marketing/components/footer.gleam:271 +#: src/fluxer_marketing/components/footer.gleam:282 #: src/fluxer_marketing/components/navigation.gleam:376 #: src/fluxer_marketing/pages/company_page.gleam:41 #: src/fluxer_marketing/pages/company_page.gleam:44 msgid "Company Information" msgstr "Céginformációk" -#: src/fluxer_marketing/components/footer.gleam:278 -#: src/fluxer_marketing/components/navigation.gleam:426 +#: src/fluxer_marketing/components/footer.gleam:289 +#: src/fluxer_marketing/components/navigation.gleam:435 msgid "Connect" msgstr "Kapcsolat" @@ -1010,10 +1010,6 @@ msgstr "Hogyan tervezed használni a Fluxert a közösségeddel" msgid "Hungarian" msgstr "Magyar" -#: src/fluxer_marketing/components/footer.gleam:329 -msgid "IP geolocation data by " -msgstr "IP-helymeghatározási adatok: " - #: src/fluxer_marketing/pages/help_article_page.gleam:223 #: src/fluxer_marketing/pages/help_article_page.gleam:365 msgid "If you couldn't find what you're looking for, our support team is here to help." @@ -1267,7 +1263,7 @@ msgid "Open Fluxer" msgstr "Fluxer megnyitása" #: src/fluxer_marketing/components/hero.gleam:82 -#: src/fluxer_marketing/components/navigation.gleam:473 +#: src/fluxer_marketing/components/navigation.gleam:482 msgid "Open in Browser" msgstr "Megnyitás böngészőben" @@ -1501,7 +1497,7 @@ msgstr "Készen állsz a váltásra?" msgid "Register now" msgstr "Regisztrálj most" -#: src/fluxer_marketing/components/footer.gleam:311 +#: src/fluxer_marketing/components/footer.gleam:322 msgid "Report a bug" msgstr "Hiba jelentése" @@ -1609,7 +1605,9 @@ msgstr "Keresési találatok erre: " msgid "Search through message history" msgstr "Keresés az üzenetelőzményekben" +#: src/fluxer_marketing/components/footer.gleam:271 #: src/fluxer_marketing/components/get_involved_section.gleam:194 +#: src/fluxer_marketing/components/navigation.gleam:424 #: src/fluxer_marketing/pages/careers_page.gleam:156 #: src/fluxer_marketing/pages/security_page.gleam:41 msgid "Security Bug Bounty" @@ -1680,7 +1678,7 @@ msgid "Soundboard" msgstr "Hangpanel" #: src/fluxer_marketing/components/footer.gleam:143 -#: src/fluxer_marketing/components/navigation.gleam:438 +#: src/fluxer_marketing/components/navigation.gleam:447 msgid "Source Code" msgstr "Forráskód" @@ -1781,6 +1779,10 @@ msgstr "Ez még nem teljes értékű helyettesítője az asztali appnak, és né msgid "This page doesn't exist. But there's plenty more to explore." msgstr "Ez az oldal nem létezik. De bőven van még mit felfedezni." +#: src/fluxer_marketing/components/footer.gleam:342 +msgid "This product includes GeoLite2 Data created by MaxMind, available from " +msgstr "Ez a termék a MaxMind által létrehozott GeoLite2 adatokat tartalmazza, elérhető innen " + #: src/fluxer_marketing/components/coming_features.gleam:37 msgid "Threads and forums" msgstr "Szálak és fórumok" @@ -2066,7 +2068,7 @@ msgstr "találat erre" msgid "{0} of {1} slots left" msgstr "Még {0}/{1} hely van" -#: src/fluxer_marketing/components/footer.gleam:325 +#: src/fluxer_marketing/components/footer.gleam:336 msgid "© Fluxer Platform AB (Swedish limited liability company: 559537-3993)" msgstr "© Fluxer Platform AB (svéd korlátolt felelősségű társaság: 559537-3993)" diff --git a/fluxer_marketing/locales/id/messages.po b/fluxer_marketing/locales/id/messages.po index 13263e76..f55013d8 100644 --- a/fluxer_marketing/locales/id/messages.po +++ b/fluxer_marketing/locales/id/messages.po @@ -206,7 +206,7 @@ msgid "Blue (Da Ba Dee)" msgstr "Biru (Da Ba Dee)" #: src/fluxer_marketing/components/footer.gleam:157 -#: src/fluxer_marketing/components/navigation.gleam:449 +#: src/fluxer_marketing/components/navigation.gleam:458 msgid "Bluesky" msgstr "Bluesky" @@ -363,15 +363,15 @@ msgstr "Mode ringkas dan opsi tampilan" msgid "Company" msgstr "Perusahaan" -#: src/fluxer_marketing/components/footer.gleam:271 +#: src/fluxer_marketing/components/footer.gleam:282 #: src/fluxer_marketing/components/navigation.gleam:376 #: src/fluxer_marketing/pages/company_page.gleam:41 #: src/fluxer_marketing/pages/company_page.gleam:44 msgid "Company Information" msgstr "Informasi Perusahaan" -#: src/fluxer_marketing/components/footer.gleam:278 -#: src/fluxer_marketing/components/navigation.gleam:426 +#: src/fluxer_marketing/components/footer.gleam:289 +#: src/fluxer_marketing/components/navigation.gleam:435 msgid "Connect" msgstr "Terhubung" @@ -1010,10 +1010,6 @@ msgstr "Rencana kamu menggunakan Fluxer untuk komunitasmu" msgid "Hungarian" msgstr "Hungaria" -#: src/fluxer_marketing/components/footer.gleam:329 -msgid "IP geolocation data by " -msgstr "Data geolokasi IP oleh " - #: src/fluxer_marketing/pages/help_article_page.gleam:223 #: src/fluxer_marketing/pages/help_article_page.gleam:365 msgid "If you couldn't find what you're looking for, our support team is here to help." @@ -1267,7 +1263,7 @@ msgid "Open Fluxer" msgstr "Buka Fluxer" #: src/fluxer_marketing/components/hero.gleam:82 -#: src/fluxer_marketing/components/navigation.gleam:473 +#: src/fluxer_marketing/components/navigation.gleam:482 msgid "Open in Browser" msgstr "Buka di browser" @@ -1501,7 +1497,7 @@ msgstr "Siap upgrade?" msgid "Register now" msgstr "Daftar sekarang" -#: src/fluxer_marketing/components/footer.gleam:311 +#: src/fluxer_marketing/components/footer.gleam:322 msgid "Report a bug" msgstr "Laporkan bug" @@ -1609,7 +1605,9 @@ msgstr "Hasil pencarian untuk: " msgid "Search through message history" msgstr "Cari di riwayat pesan" +#: src/fluxer_marketing/components/footer.gleam:271 #: src/fluxer_marketing/components/get_involved_section.gleam:194 +#: src/fluxer_marketing/components/navigation.gleam:424 #: src/fluxer_marketing/pages/careers_page.gleam:156 #: src/fluxer_marketing/pages/security_page.gleam:41 msgid "Security Bug Bounty" @@ -1680,7 +1678,7 @@ msgid "Soundboard" msgstr "Soundboard" #: src/fluxer_marketing/components/footer.gleam:143 -#: src/fluxer_marketing/components/navigation.gleam:438 +#: src/fluxer_marketing/components/navigation.gleam:447 msgid "Source Code" msgstr "Kode Sumber" @@ -1781,6 +1779,10 @@ msgstr "Ini belum sepenuhnya menggantikan aplikasi desktop, dan beberapa hal yan msgid "This page doesn't exist. But there's plenty more to explore." msgstr "Halaman ini tidak ada. Tapi masih banyak hal lain yang bisa kamu jelajahi." +#: src/fluxer_marketing/components/footer.gleam:342 +msgid "This product includes GeoLite2 Data created by MaxMind, available from " +msgstr "Produk ini mencakup Data GeoLite2 yang dibuat oleh MaxMind, tersedia dari " + #: src/fluxer_marketing/components/coming_features.gleam:37 msgid "Threads and forums" msgstr "Thread dan forum" @@ -2066,7 +2068,7 @@ msgstr "hasil untuk" msgid "{0} of {1} slots left" msgstr "Tersisa {0} dari {1} slot" -#: src/fluxer_marketing/components/footer.gleam:325 +#: src/fluxer_marketing/components/footer.gleam:336 msgid "© Fluxer Platform AB (Swedish limited liability company: 559537-3993)" msgstr "© Fluxer Platform AB (perseroan terbatas Swedia: 559537-3993)" diff --git a/fluxer_marketing/locales/it/messages.po b/fluxer_marketing/locales/it/messages.po index 2a839255..cbc5d7e9 100644 --- a/fluxer_marketing/locales/it/messages.po +++ b/fluxer_marketing/locales/it/messages.po @@ -206,7 +206,7 @@ msgid "Blue (Da Ba Dee)" msgstr "Blu (Da Ba Dee)" #: src/fluxer_marketing/components/footer.gleam:157 -#: src/fluxer_marketing/components/navigation.gleam:449 +#: src/fluxer_marketing/components/navigation.gleam:458 msgid "Bluesky" msgstr "Bluesky" @@ -363,15 +363,15 @@ msgstr "Modalità compatta e opzioni di visualizzazione" msgid "Company" msgstr "Azienda" -#: src/fluxer_marketing/components/footer.gleam:271 +#: src/fluxer_marketing/components/footer.gleam:282 #: src/fluxer_marketing/components/navigation.gleam:376 #: src/fluxer_marketing/pages/company_page.gleam:41 #: src/fluxer_marketing/pages/company_page.gleam:44 msgid "Company Information" msgstr "Informazioni sull’azienda" -#: src/fluxer_marketing/components/footer.gleam:278 -#: src/fluxer_marketing/components/navigation.gleam:426 +#: src/fluxer_marketing/components/footer.gleam:289 +#: src/fluxer_marketing/components/navigation.gleam:435 msgid "Connect" msgstr "Contatti" @@ -1010,10 +1010,6 @@ msgstr "Come pensi di usare Fluxer con la tua community" msgid "Hungarian" msgstr "Ungherese" -#: src/fluxer_marketing/components/footer.gleam:329 -msgid "IP geolocation data by " -msgstr "Dati di geolocalizzazione IP forniti da " - #: src/fluxer_marketing/pages/help_article_page.gleam:223 #: src/fluxer_marketing/pages/help_article_page.gleam:365 msgid "If you couldn't find what you're looking for, our support team is here to help." @@ -1267,7 +1263,7 @@ msgid "Open Fluxer" msgstr "Apri Fluxer" #: src/fluxer_marketing/components/hero.gleam:82 -#: src/fluxer_marketing/components/navigation.gleam:473 +#: src/fluxer_marketing/components/navigation.gleam:482 msgid "Open in Browser" msgstr "Apri nel browser" @@ -1501,7 +1497,7 @@ msgstr "Pronto a fare l’upgrade?" msgid "Register now" msgstr "Registrati ora" -#: src/fluxer_marketing/components/footer.gleam:311 +#: src/fluxer_marketing/components/footer.gleam:322 msgid "Report a bug" msgstr "Segnala un bug" @@ -1609,7 +1605,9 @@ msgstr "Risultati di ricerca per: " msgid "Search through message history" msgstr "Cerca nella cronologia dei messaggi" +#: src/fluxer_marketing/components/footer.gleam:271 #: src/fluxer_marketing/components/get_involved_section.gleam:194 +#: src/fluxer_marketing/components/navigation.gleam:424 #: src/fluxer_marketing/pages/careers_page.gleam:156 #: src/fluxer_marketing/pages/security_page.gleam:41 msgid "Security Bug Bounty" @@ -1680,7 +1678,7 @@ msgid "Soundboard" msgstr "Soundboard" #: src/fluxer_marketing/components/footer.gleam:143 -#: src/fluxer_marketing/components/navigation.gleam:438 +#: src/fluxer_marketing/components/navigation.gleam:447 msgid "Source Code" msgstr "Codice sorgente" @@ -1781,6 +1779,10 @@ msgstr "Non è ancora un sostituto completo dell’app desktop, e mancano ancora msgid "This page doesn't exist. But there's plenty more to explore." msgstr "Questa pagina non esiste. Ma c’è molto altro da scoprire." +#: src/fluxer_marketing/components/footer.gleam:342 +msgid "This product includes GeoLite2 Data created by MaxMind, available from " +msgstr "Questo prodotto include i dati GeoLite2 creati da MaxMind, disponibili presso " + #: src/fluxer_marketing/components/coming_features.gleam:37 msgid "Threads and forums" msgstr "Thread e forum" @@ -2066,7 +2068,7 @@ msgstr "risultati per" msgid "{0} of {1} slots left" msgstr "{0} posti rimasti su {1}" -#: src/fluxer_marketing/components/footer.gleam:325 +#: src/fluxer_marketing/components/footer.gleam:336 msgid "© Fluxer Platform AB (Swedish limited liability company: 559537-3993)" msgstr "© Fluxer Platform AB (società a responsabilità limitata svedese: 559537-3993)" diff --git a/fluxer_marketing/locales/ja/messages.po b/fluxer_marketing/locales/ja/messages.po index 58fbe5d3..79d1ae72 100644 --- a/fluxer_marketing/locales/ja/messages.po +++ b/fluxer_marketing/locales/ja/messages.po @@ -206,7 +206,7 @@ msgid "Blue (Da Ba Dee)" msgstr "ブルー(Da Ba Dee)" #: src/fluxer_marketing/components/footer.gleam:157 -#: src/fluxer_marketing/components/navigation.gleam:449 +#: src/fluxer_marketing/components/navigation.gleam:458 msgid "Bluesky" msgstr "Bluesky" @@ -363,15 +363,15 @@ msgstr "コンパクトモードと表示設定" msgid "Company" msgstr "会社" -#: src/fluxer_marketing/components/footer.gleam:271 +#: src/fluxer_marketing/components/footer.gleam:282 #: src/fluxer_marketing/components/navigation.gleam:376 #: src/fluxer_marketing/pages/company_page.gleam:41 #: src/fluxer_marketing/pages/company_page.gleam:44 msgid "Company Information" msgstr "会社情報" -#: src/fluxer_marketing/components/footer.gleam:278 -#: src/fluxer_marketing/components/navigation.gleam:426 +#: src/fluxer_marketing/components/footer.gleam:289 +#: src/fluxer_marketing/components/navigation.gleam:435 msgid "Connect" msgstr "つながる" @@ -1010,10 +1010,6 @@ msgstr "コミュニティでFluxerをどう使う予定か" msgid "Hungarian" msgstr "ハンガリー語" -#: src/fluxer_marketing/components/footer.gleam:329 -msgid "IP geolocation data by " -msgstr "IPジオロケーションデータ提供:" - #: src/fluxer_marketing/pages/help_article_page.gleam:223 #: src/fluxer_marketing/pages/help_article_page.gleam:365 msgid "If you couldn't find what you're looking for, our support team is here to help." @@ -1267,7 +1263,7 @@ msgid "Open Fluxer" msgstr "Fluxerを開く" #: src/fluxer_marketing/components/hero.gleam:82 -#: src/fluxer_marketing/components/navigation.gleam:473 +#: src/fluxer_marketing/components/navigation.gleam:482 msgid "Open in Browser" msgstr "ブラウザで開く" @@ -1501,7 +1497,7 @@ msgstr "アップグレードしますか?" msgid "Register now" msgstr "今すぐ登録" -#: src/fluxer_marketing/components/footer.gleam:311 +#: src/fluxer_marketing/components/footer.gleam:322 msgid "Report a bug" msgstr "バグを報告" @@ -1609,7 +1605,9 @@ msgstr "検索結果:" msgid "Search through message history" msgstr "メッセージ履歴を検索" +#: src/fluxer_marketing/components/footer.gleam:271 #: src/fluxer_marketing/components/get_involved_section.gleam:194 +#: src/fluxer_marketing/components/navigation.gleam:424 #: src/fluxer_marketing/pages/careers_page.gleam:156 #: src/fluxer_marketing/pages/security_page.gleam:41 msgid "Security Bug Bounty" @@ -1680,7 +1678,7 @@ msgid "Soundboard" msgstr "サウンドボード" #: src/fluxer_marketing/components/footer.gleam:143 -#: src/fluxer_marketing/components/navigation.gleam:438 +#: src/fluxer_marketing/components/navigation.gleam:447 msgid "Source Code" msgstr "ソースコード" @@ -1781,6 +1779,10 @@ msgstr "まだデスクトップアプリの完全な代替ではなく、モバ msgid "This page doesn't exist. But there's plenty more to explore." msgstr "このページは存在しません。でも、まだまだ見るものはたくさんあります。" +#: src/fluxer_marketing/components/footer.gleam:342 +msgid "This product includes GeoLite2 Data created by MaxMind, available from " +msgstr "この製品には、MaxMindによって作成されたGeoLite2データが含まれています。入手先は" + #: src/fluxer_marketing/components/coming_features.gleam:37 msgid "Threads and forums" msgstr "スレッド&フォーラム" @@ -2066,7 +2068,7 @@ msgstr "件の検索結果:" msgid "{0} of {1} slots left" msgstr "残り{0}/{1}枠" -#: src/fluxer_marketing/components/footer.gleam:325 +#: src/fluxer_marketing/components/footer.gleam:336 msgid "© Fluxer Platform AB (Swedish limited liability company: 559537-3993)" msgstr "© Fluxer Platform AB(スウェーデンの有限責任会社:559537-3993)" diff --git a/fluxer_marketing/locales/ko/messages.po b/fluxer_marketing/locales/ko/messages.po index 61591758..c3c5dc18 100644 --- a/fluxer_marketing/locales/ko/messages.po +++ b/fluxer_marketing/locales/ko/messages.po @@ -206,7 +206,7 @@ msgid "Blue (Da Ba Dee)" msgstr "파랑 (Da Ba Dee)" #: src/fluxer_marketing/components/footer.gleam:157 -#: src/fluxer_marketing/components/navigation.gleam:449 +#: src/fluxer_marketing/components/navigation.gleam:458 msgid "Bluesky" msgstr "Bluesky" @@ -363,15 +363,15 @@ msgstr "컴팩트 모드 및 표시 설정" msgid "Company" msgstr "회사" -#: src/fluxer_marketing/components/footer.gleam:271 +#: src/fluxer_marketing/components/footer.gleam:282 #: src/fluxer_marketing/components/navigation.gleam:376 #: src/fluxer_marketing/pages/company_page.gleam:41 #: src/fluxer_marketing/pages/company_page.gleam:44 msgid "Company Information" msgstr "회사 정보" -#: src/fluxer_marketing/components/footer.gleam:278 -#: src/fluxer_marketing/components/navigation.gleam:426 +#: src/fluxer_marketing/components/footer.gleam:289 +#: src/fluxer_marketing/components/navigation.gleam:435 msgid "Connect" msgstr "연락하기" @@ -1010,10 +1010,6 @@ msgstr "커뮤니티와 함께 Fluxer를 어떻게 활용할 계획인지" msgid "Hungarian" msgstr "헝가리어" -#: src/fluxer_marketing/components/footer.gleam:329 -msgid "IP geolocation data by " -msgstr "IP 위치 데이터 제공: " - #: src/fluxer_marketing/pages/help_article_page.gleam:223 #: src/fluxer_marketing/pages/help_article_page.gleam:365 msgid "If you couldn't find what you're looking for, our support team is here to help." @@ -1267,7 +1263,7 @@ msgid "Open Fluxer" msgstr "Fluxer 열기" #: src/fluxer_marketing/components/hero.gleam:82 -#: src/fluxer_marketing/components/navigation.gleam:473 +#: src/fluxer_marketing/components/navigation.gleam:482 msgid "Open in Browser" msgstr "브라우저에서 열기" @@ -1501,7 +1497,7 @@ msgstr "업그레이드할 준비가 되셨나요?" msgid "Register now" msgstr "지금 등록하기" -#: src/fluxer_marketing/components/footer.gleam:311 +#: src/fluxer_marketing/components/footer.gleam:322 msgid "Report a bug" msgstr "버그 신고" @@ -1609,7 +1605,9 @@ msgstr "검색 결과: " msgid "Search through message history" msgstr "메시지 기록 검색" +#: src/fluxer_marketing/components/footer.gleam:271 #: src/fluxer_marketing/components/get_involved_section.gleam:194 +#: src/fluxer_marketing/components/navigation.gleam:424 #: src/fluxer_marketing/pages/careers_page.gleam:156 #: src/fluxer_marketing/pages/security_page.gleam:41 msgid "Security Bug Bounty" @@ -1680,7 +1678,7 @@ msgid "Soundboard" msgstr "사운드보드" #: src/fluxer_marketing/components/footer.gleam:143 -#: src/fluxer_marketing/components/navigation.gleam:438 +#: src/fluxer_marketing/components/navigation.gleam:447 msgid "Source Code" msgstr "소스 코드" @@ -1781,6 +1779,10 @@ msgstr "아직 데스크톱 앱을 완전히 대체하진 못하며, 모바일 msgid "This page doesn't exist. But there's plenty more to explore." msgstr "이 페이지는 존재하지 않아요. 그래도 둘러볼 곳은 아직 많이 있습니다." +#: src/fluxer_marketing/components/footer.gleam:342 +msgid "This product includes GeoLite2 Data created by MaxMind, available from " +msgstr "이 제품에는 MaxMind에 의해 생성된 GeoLite2 데이터가 포함되어 있으며, 다음에서 이용 가능합니다." + #: src/fluxer_marketing/components/coming_features.gleam:37 msgid "Threads and forums" msgstr "스레드 및 포럼" @@ -2066,7 +2068,7 @@ msgstr "개의 결과" msgid "{0} of {1} slots left" msgstr "남은 슬롯: {0}/{1}" -#: src/fluxer_marketing/components/footer.gleam:325 +#: src/fluxer_marketing/components/footer.gleam:336 msgid "© Fluxer Platform AB (Swedish limited liability company: 559537-3993)" msgstr "© Fluxer Platform AB (스웨덴 유한회사: 559537-3993)" diff --git a/fluxer_marketing/locales/lt/messages.po b/fluxer_marketing/locales/lt/messages.po index 9a40b945..887831e4 100644 --- a/fluxer_marketing/locales/lt/messages.po +++ b/fluxer_marketing/locales/lt/messages.po @@ -206,7 +206,7 @@ msgid "Blue (Da Ba Dee)" msgstr "Mėlyna (Da Ba Dee)" #: src/fluxer_marketing/components/footer.gleam:157 -#: src/fluxer_marketing/components/navigation.gleam:449 +#: src/fluxer_marketing/components/navigation.gleam:458 msgid "Bluesky" msgstr "Bluesky" @@ -363,15 +363,15 @@ msgstr "Kompaktiškas režimas ir rodymo parinktys" msgid "Company" msgstr "Įmonė" -#: src/fluxer_marketing/components/footer.gleam:271 +#: src/fluxer_marketing/components/footer.gleam:282 #: src/fluxer_marketing/components/navigation.gleam:376 #: src/fluxer_marketing/pages/company_page.gleam:41 #: src/fluxer_marketing/pages/company_page.gleam:44 msgid "Company Information" msgstr "Informacija apie įmonę" -#: src/fluxer_marketing/components/footer.gleam:278 -#: src/fluxer_marketing/components/navigation.gleam:426 +#: src/fluxer_marketing/components/footer.gleam:289 +#: src/fluxer_marketing/components/navigation.gleam:435 msgid "Connect" msgstr "Susisiekite" @@ -1010,10 +1010,6 @@ msgstr "Kaip planuojate naudoti Fluxer su savo bendruomene" msgid "Hungarian" msgstr "Vengrų" -#: src/fluxer_marketing/components/footer.gleam:329 -msgid "IP geolocation data by " -msgstr "IP geolokacijos duomenis teikia " - #: src/fluxer_marketing/pages/help_article_page.gleam:223 #: src/fluxer_marketing/pages/help_article_page.gleam:365 msgid "If you couldn't find what you're looking for, our support team is here to help." @@ -1267,7 +1263,7 @@ msgid "Open Fluxer" msgstr "Atidaryti Fluxer" #: src/fluxer_marketing/components/hero.gleam:82 -#: src/fluxer_marketing/components/navigation.gleam:473 +#: src/fluxer_marketing/components/navigation.gleam:482 msgid "Open in Browser" msgstr "Atidaryti naršyklėje" @@ -1501,7 +1497,7 @@ msgstr "Pasiruošę atsinaujinti?" msgid "Register now" msgstr "Registruokitės dabar" -#: src/fluxer_marketing/components/footer.gleam:311 +#: src/fluxer_marketing/components/footer.gleam:322 msgid "Report a bug" msgstr "Pranešti apie klaidą" @@ -1609,7 +1605,9 @@ msgstr "Paieškos rezultatai: " msgid "Search through message history" msgstr "Ieškokite žinučių istorijoje" +#: src/fluxer_marketing/components/footer.gleam:271 #: src/fluxer_marketing/components/get_involved_section.gleam:194 +#: src/fluxer_marketing/components/navigation.gleam:424 #: src/fluxer_marketing/pages/careers_page.gleam:156 #: src/fluxer_marketing/pages/security_page.gleam:41 msgid "Security Bug Bounty" @@ -1680,7 +1678,7 @@ msgid "Soundboard" msgstr "Garso pultas" #: src/fluxer_marketing/components/footer.gleam:143 -#: src/fluxer_marketing/components/navigation.gleam:438 +#: src/fluxer_marketing/components/navigation.gleam:447 msgid "Source Code" msgstr "Šaltinio kodas" @@ -1781,6 +1779,10 @@ msgstr "Kol kas tai dar nėra pilnavertis kompiuterio programos pakaitalas, o ka msgid "This page doesn't exist. But there's plenty more to explore." msgstr "Šis puslapis neegzistuoja. Tačiau yra daugybė kitų dalykų, kuriuos galite atrasti." +#: src/fluxer_marketing/components/footer.gleam:342 +msgid "This product includes GeoLite2 Data created by MaxMind, available from " +msgstr "Šiame produkte yra GeoLite2 duomenys, kuriuos sukūrė MaxMind, pasiekiami iš " + #: src/fluxer_marketing/components/coming_features.gleam:37 msgid "Threads and forums" msgstr "Gijos ir forumai" @@ -2066,7 +2068,7 @@ msgstr "rezultatai pagal" msgid "{0} of {1} slots left" msgstr "Liko {0} iš {1} vietų" -#: src/fluxer_marketing/components/footer.gleam:325 +#: src/fluxer_marketing/components/footer.gleam:336 msgid "© Fluxer Platform AB (Swedish limited liability company: 559537-3993)" msgstr "© Fluxer Platform AB (Švedijos ribotos atsakomybės bendrovė: 559537-3993)" diff --git a/fluxer_marketing/locales/messages.pot b/fluxer_marketing/locales/messages.pot index f17036da..fff82723 100644 --- a/fluxer_marketing/locales/messages.pot +++ b/fluxer_marketing/locales/messages.pot @@ -1023,10 +1023,6 @@ msgstr "" msgid "Hungarian" msgstr "" -#: src/fluxer_marketing/components/footer.gleam:340 -msgid "IP geolocation data by " -msgstr "" - #: src/fluxer_marketing/pages/help_article_page.gleam:223 #: src/fluxer_marketing/pages/help_article_page.gleam:365 msgid "If you couldn't find what you're looking for, our support team is here to help." @@ -1796,6 +1792,10 @@ msgstr "" msgid "This page doesn't exist. But there's plenty more to explore." msgstr "" +#: src/fluxer_marketing/components/footer.gleam:342 +msgid "This product includes GeoLite2 Data created by MaxMind, available from " +msgstr "" + #: src/fluxer_marketing/components/coming_features.gleam:37 msgid "Threads and forums" msgstr "" diff --git a/fluxer_marketing/locales/nl/messages.po b/fluxer_marketing/locales/nl/messages.po index f956ba6e..d44a5f4a 100644 --- a/fluxer_marketing/locales/nl/messages.po +++ b/fluxer_marketing/locales/nl/messages.po @@ -206,7 +206,7 @@ msgid "Blue (Da Ba Dee)" msgstr "Blauw (Da Ba Dee)" #: src/fluxer_marketing/components/footer.gleam:157 -#: src/fluxer_marketing/components/navigation.gleam:449 +#: src/fluxer_marketing/components/navigation.gleam:458 msgid "Bluesky" msgstr "Bluesky" @@ -363,15 +363,15 @@ msgstr "Compacte modus en weergaveopties" msgid "Company" msgstr "Bedrijf" -#: src/fluxer_marketing/components/footer.gleam:271 +#: src/fluxer_marketing/components/footer.gleam:282 #: src/fluxer_marketing/components/navigation.gleam:376 #: src/fluxer_marketing/pages/company_page.gleam:41 #: src/fluxer_marketing/pages/company_page.gleam:44 msgid "Company Information" msgstr "Bedrijfsinformatie" -#: src/fluxer_marketing/components/footer.gleam:278 -#: src/fluxer_marketing/components/navigation.gleam:426 +#: src/fluxer_marketing/components/footer.gleam:289 +#: src/fluxer_marketing/components/navigation.gleam:435 msgid "Connect" msgstr "Contact" @@ -1010,10 +1010,6 @@ msgstr "Hoe je Fluxer met je community wilt gebruiken" msgid "Hungarian" msgstr "Hongaars" -#: src/fluxer_marketing/components/footer.gleam:329 -msgid "IP geolocation data by " -msgstr "IP-geolocatiegegevens door " - #: src/fluxer_marketing/pages/help_article_page.gleam:223 #: src/fluxer_marketing/pages/help_article_page.gleam:365 msgid "If you couldn't find what you're looking for, our support team is here to help." @@ -1267,7 +1263,7 @@ msgid "Open Fluxer" msgstr "Fluxer openen" #: src/fluxer_marketing/components/hero.gleam:82 -#: src/fluxer_marketing/components/navigation.gleam:473 +#: src/fluxer_marketing/components/navigation.gleam:482 msgid "Open in Browser" msgstr "Openen in de browser" @@ -1501,7 +1497,7 @@ msgstr "Klaar om te upgraden?" msgid "Register now" msgstr "Nu registreren" -#: src/fluxer_marketing/components/footer.gleam:311 +#: src/fluxer_marketing/components/footer.gleam:322 msgid "Report a bug" msgstr "Meld een bug" @@ -1609,7 +1605,9 @@ msgstr "Zoekresultaten voor: " msgid "Search through message history" msgstr "Zoek in je berichtgeschiedenis" +#: src/fluxer_marketing/components/footer.gleam:271 #: src/fluxer_marketing/components/get_involved_section.gleam:194 +#: src/fluxer_marketing/components/navigation.gleam:424 #: src/fluxer_marketing/pages/careers_page.gleam:156 #: src/fluxer_marketing/pages/security_page.gleam:41 msgid "Security Bug Bounty" @@ -1680,7 +1678,7 @@ msgid "Soundboard" msgstr "Soundboard" #: src/fluxer_marketing/components/footer.gleam:143 -#: src/fluxer_marketing/components/navigation.gleam:438 +#: src/fluxer_marketing/components/navigation.gleam:447 msgid "Source Code" msgstr "Broncode" @@ -1781,6 +1779,10 @@ msgstr "Dit is nog geen volledige vervanging voor de desktopapp, en sommige ding msgid "This page doesn't exist. But there's plenty more to explore." msgstr "Deze pagina bestaat niet. Maar er valt nog genoeg te ontdekken." +#: src/fluxer_marketing/components/footer.gleam:342 +msgid "This product includes GeoLite2 Data created by MaxMind, available from " +msgstr "Dit product bevat GeoLite2-gegevens gemaakt door MaxMind, beschikbaar van " + #: src/fluxer_marketing/components/coming_features.gleam:37 msgid "Threads and forums" msgstr "Threads en forums" @@ -2066,7 +2068,7 @@ msgstr "resultaten voor" msgid "{0} of {1} slots left" msgstr "Nog {0} van {1} plekken over" -#: src/fluxer_marketing/components/footer.gleam:325 +#: src/fluxer_marketing/components/footer.gleam:336 msgid "© Fluxer Platform AB (Swedish limited liability company: 559537-3993)" msgstr "© Fluxer Platform AB (Zweedse besloten vennootschap: 559537-3993)" diff --git a/fluxer_marketing/locales/no/messages.po b/fluxer_marketing/locales/no/messages.po index e3a8c370..4fb98dd5 100644 --- a/fluxer_marketing/locales/no/messages.po +++ b/fluxer_marketing/locales/no/messages.po @@ -206,7 +206,7 @@ msgid "Blue (Da Ba Dee)" msgstr "Blå (Da Ba Dee)" #: src/fluxer_marketing/components/footer.gleam:157 -#: src/fluxer_marketing/components/navigation.gleam:449 +#: src/fluxer_marketing/components/navigation.gleam:458 msgid "Bluesky" msgstr "Bluesky" @@ -363,15 +363,15 @@ msgstr "Kompaktmodus og visningsvalg" msgid "Company" msgstr "Selskap" -#: src/fluxer_marketing/components/footer.gleam:271 +#: src/fluxer_marketing/components/footer.gleam:282 #: src/fluxer_marketing/components/navigation.gleam:376 #: src/fluxer_marketing/pages/company_page.gleam:41 #: src/fluxer_marketing/pages/company_page.gleam:44 msgid "Company Information" msgstr "Selskapsinformasjon" -#: src/fluxer_marketing/components/footer.gleam:278 -#: src/fluxer_marketing/components/navigation.gleam:426 +#: src/fluxer_marketing/components/footer.gleam:289 +#: src/fluxer_marketing/components/navigation.gleam:435 msgid "Connect" msgstr "Kontakt" @@ -1010,10 +1010,6 @@ msgstr "Hvordan du planlegger å bruke Fluxer i fellesskapet ditt" msgid "Hungarian" msgstr "Ungarsk" -#: src/fluxer_marketing/components/footer.gleam:329 -msgid "IP geolocation data by " -msgstr "IP-geolokasjonsdata fra " - #: src/fluxer_marketing/pages/help_article_page.gleam:223 #: src/fluxer_marketing/pages/help_article_page.gleam:365 msgid "If you couldn't find what you're looking for, our support team is here to help." @@ -1267,7 +1263,7 @@ msgid "Open Fluxer" msgstr "Åpne Fluxer" #: src/fluxer_marketing/components/hero.gleam:82 -#: src/fluxer_marketing/components/navigation.gleam:473 +#: src/fluxer_marketing/components/navigation.gleam:482 msgid "Open in Browser" msgstr "Åpne i nettleseren" @@ -1501,7 +1497,7 @@ msgstr "Klar for å oppgradere?" msgid "Register now" msgstr "Registrer deg nå" -#: src/fluxer_marketing/components/footer.gleam:311 +#: src/fluxer_marketing/components/footer.gleam:322 msgid "Report a bug" msgstr "Rapporter en feil" @@ -1609,7 +1605,9 @@ msgstr "Søkeresultater for: " msgid "Search through message history" msgstr "Søk i meldingshistorikken" +#: src/fluxer_marketing/components/footer.gleam:271 #: src/fluxer_marketing/components/get_involved_section.gleam:194 +#: src/fluxer_marketing/components/navigation.gleam:424 #: src/fluxer_marketing/pages/careers_page.gleam:156 #: src/fluxer_marketing/pages/security_page.gleam:41 msgid "Security Bug Bounty" @@ -1680,7 +1678,7 @@ msgid "Soundboard" msgstr "Lydbrett" #: src/fluxer_marketing/components/footer.gleam:143 -#: src/fluxer_marketing/components/navigation.gleam:438 +#: src/fluxer_marketing/components/navigation.gleam:447 msgid "Source Code" msgstr "Kildekode" @@ -1781,6 +1779,10 @@ msgstr "Dette er ikke en full erstatning for skrivebordsappen ennå, og noen tin msgid "This page doesn't exist. But there's plenty more to explore." msgstr "Denne siden finnes ikke. Men det er mye mer å utforske." +#: src/fluxer_marketing/components/footer.gleam:342 +msgid "This product includes GeoLite2 Data created by MaxMind, available from " +msgstr "Dette produktet inkluderer GeoLite2-data laget av MaxMind, tilgjengelig fra " + #: src/fluxer_marketing/components/coming_features.gleam:37 msgid "Threads and forums" msgstr "Tråder og forum" @@ -2066,7 +2068,7 @@ msgstr "resultater for" msgid "{0} of {1} slots left" msgstr "{0} av {1} plasser igjen" -#: src/fluxer_marketing/components/footer.gleam:325 +#: src/fluxer_marketing/components/footer.gleam:336 msgid "© Fluxer Platform AB (Swedish limited liability company: 559537-3993)" msgstr "© Fluxer Platform AB (svensk aksjeselskap: 559537-3993)" diff --git a/fluxer_marketing/locales/pl/messages.po b/fluxer_marketing/locales/pl/messages.po index 98fbd682..1671465d 100644 --- a/fluxer_marketing/locales/pl/messages.po +++ b/fluxer_marketing/locales/pl/messages.po @@ -206,7 +206,7 @@ msgid "Blue (Da Ba Dee)" msgstr "Niebieski (Da Ba Dee)" #: src/fluxer_marketing/components/footer.gleam:157 -#: src/fluxer_marketing/components/navigation.gleam:449 +#: src/fluxer_marketing/components/navigation.gleam:458 msgid "Bluesky" msgstr "Bluesky" @@ -363,15 +363,15 @@ msgstr "Tryb kompaktowy i opcje wyświetlania" msgid "Company" msgstr "Firma" -#: src/fluxer_marketing/components/footer.gleam:271 +#: src/fluxer_marketing/components/footer.gleam:282 #: src/fluxer_marketing/components/navigation.gleam:376 #: src/fluxer_marketing/pages/company_page.gleam:41 #: src/fluxer_marketing/pages/company_page.gleam:44 msgid "Company Information" msgstr "Informacje o firmie" -#: src/fluxer_marketing/components/footer.gleam:278 -#: src/fluxer_marketing/components/navigation.gleam:426 +#: src/fluxer_marketing/components/footer.gleam:289 +#: src/fluxer_marketing/components/navigation.gleam:435 msgid "Connect" msgstr "Kontakt" @@ -1010,10 +1010,6 @@ msgstr "Jak planujesz używać Fluxer ze swoją społecznością" msgid "Hungarian" msgstr "Węgierski" -#: src/fluxer_marketing/components/footer.gleam:329 -msgid "IP geolocation data by " -msgstr "Dane geolokalizacji IP od " - #: src/fluxer_marketing/pages/help_article_page.gleam:223 #: src/fluxer_marketing/pages/help_article_page.gleam:365 msgid "If you couldn't find what you're looking for, our support team is here to help." @@ -1267,7 +1263,7 @@ msgid "Open Fluxer" msgstr "Otwórz Fluxer" #: src/fluxer_marketing/components/hero.gleam:82 -#: src/fluxer_marketing/components/navigation.gleam:473 +#: src/fluxer_marketing/components/navigation.gleam:482 msgid "Open in Browser" msgstr "Otwórz w przeglądarce" @@ -1501,7 +1497,7 @@ msgstr "Gotowy na upgrade?" msgid "Register now" msgstr "Zarejestruj się" -#: src/fluxer_marketing/components/footer.gleam:311 +#: src/fluxer_marketing/components/footer.gleam:322 msgid "Report a bug" msgstr "Zgłoś błąd" @@ -1609,7 +1605,9 @@ msgstr "Wyniki wyszukiwania dla: " msgid "Search through message history" msgstr "Przeszukuj historię wiadomości" +#: src/fluxer_marketing/components/footer.gleam:271 #: src/fluxer_marketing/components/get_involved_section.gleam:194 +#: src/fluxer_marketing/components/navigation.gleam:424 #: src/fluxer_marketing/pages/careers_page.gleam:156 #: src/fluxer_marketing/pages/security_page.gleam:41 msgid "Security Bug Bounty" @@ -1680,7 +1678,7 @@ msgid "Soundboard" msgstr "Panel dźwięków" #: src/fluxer_marketing/components/footer.gleam:143 -#: src/fluxer_marketing/components/navigation.gleam:438 +#: src/fluxer_marketing/components/navigation.gleam:447 msgid "Source Code" msgstr "Kod źródłowy" @@ -1781,6 +1779,10 @@ msgstr "To jeszcze nie jest pełny zamiennik aplikacji desktopowej, a niektóryc msgid "This page doesn't exist. But there's plenty more to explore." msgstr "Ta strona nie istnieje. Ale jest jeszcze sporo do odkrycia." +#: src/fluxer_marketing/components/footer.gleam:342 +msgid "This product includes GeoLite2 Data created by MaxMind, available from " +msgstr "Ten produkt zawiera dane GeoLite2 utworzone przez MaxMind, dostępne w " + #: src/fluxer_marketing/components/coming_features.gleam:37 msgid "Threads and forums" msgstr "Wątki i fora" @@ -2066,7 +2068,7 @@ msgstr "wyniki dla" msgid "{0} of {1} slots left" msgstr "Zostało {0} z {1} miejsc" -#: src/fluxer_marketing/components/footer.gleam:325 +#: src/fluxer_marketing/components/footer.gleam:336 msgid "© Fluxer Platform AB (Swedish limited liability company: 559537-3993)" msgstr "© Fluxer Platform AB (szwedzka spółka z o.o.: 559537-3993)" diff --git a/fluxer_marketing/locales/pt-BR/messages.po b/fluxer_marketing/locales/pt-BR/messages.po index 2222ccd6..5199289e 100644 --- a/fluxer_marketing/locales/pt-BR/messages.po +++ b/fluxer_marketing/locales/pt-BR/messages.po @@ -206,7 +206,7 @@ msgid "Blue (Da Ba Dee)" msgstr "Azul (Da Ba Dee)" #: src/fluxer_marketing/components/footer.gleam:157 -#: src/fluxer_marketing/components/navigation.gleam:449 +#: src/fluxer_marketing/components/navigation.gleam:458 msgid "Bluesky" msgstr "Bluesky" @@ -363,15 +363,15 @@ msgstr "Modo compacto e opções de exibição" msgid "Company" msgstr "Empresa" -#: src/fluxer_marketing/components/footer.gleam:271 +#: src/fluxer_marketing/components/footer.gleam:282 #: src/fluxer_marketing/components/navigation.gleam:376 #: src/fluxer_marketing/pages/company_page.gleam:41 #: src/fluxer_marketing/pages/company_page.gleam:44 msgid "Company Information" msgstr "Informações da empresa" -#: src/fluxer_marketing/components/footer.gleam:278 -#: src/fluxer_marketing/components/navigation.gleam:426 +#: src/fluxer_marketing/components/footer.gleam:289 +#: src/fluxer_marketing/components/navigation.gleam:435 msgid "Connect" msgstr "Conectar" @@ -1010,10 +1010,6 @@ msgstr "Como você pretende usar o Fluxer com a sua comunidade" msgid "Hungarian" msgstr "Húngaro" -#: src/fluxer_marketing/components/footer.gleam:329 -msgid "IP geolocation data by " -msgstr "Dados de geolocalização por IP fornecidos por " - #: src/fluxer_marketing/pages/help_article_page.gleam:223 #: src/fluxer_marketing/pages/help_article_page.gleam:365 msgid "If you couldn't find what you're looking for, our support team is here to help." @@ -1267,7 +1263,7 @@ msgid "Open Fluxer" msgstr "Abrir Fluxer" #: src/fluxer_marketing/components/hero.gleam:82 -#: src/fluxer_marketing/components/navigation.gleam:473 +#: src/fluxer_marketing/components/navigation.gleam:482 msgid "Open in Browser" msgstr "Abrir no navegador" @@ -1501,7 +1497,7 @@ msgstr "Pronto para fazer upgrade?" msgid "Register now" msgstr "Cadastre-se agora" -#: src/fluxer_marketing/components/footer.gleam:311 +#: src/fluxer_marketing/components/footer.gleam:322 msgid "Report a bug" msgstr "Relatar bug" @@ -1609,7 +1605,9 @@ msgstr "Resultados da busca por: " msgid "Search through message history" msgstr "Buscar no histórico de mensagens" +#: src/fluxer_marketing/components/footer.gleam:271 #: src/fluxer_marketing/components/get_involved_section.gleam:194 +#: src/fluxer_marketing/components/navigation.gleam:424 #: src/fluxer_marketing/pages/careers_page.gleam:156 #: src/fluxer_marketing/pages/security_page.gleam:41 msgid "Security Bug Bounty" @@ -1680,7 +1678,7 @@ msgid "Soundboard" msgstr "Painel de sons" #: src/fluxer_marketing/components/footer.gleam:143 -#: src/fluxer_marketing/components/navigation.gleam:438 +#: src/fluxer_marketing/components/navigation.gleam:447 msgid "Source Code" msgstr "Código-fonte" @@ -1781,6 +1779,10 @@ msgstr "Isso ainda não substitui completamente o app de desktop, e ainda faltam msgid "This page doesn't exist. But there's plenty more to explore." msgstr "Esta página não existe. Mas ainda tem muita coisa para explorar." +#: src/fluxer_marketing/components/footer.gleam:342 +msgid "This product includes GeoLite2 Data created by MaxMind, available from " +msgstr "Este produto inclui os dados GeoLite2 criados pela MaxMind, disponíveis em " + #: src/fluxer_marketing/components/coming_features.gleam:37 msgid "Threads and forums" msgstr "Tópicos e fóruns" @@ -2066,7 +2068,7 @@ msgstr "resultados para" msgid "{0} of {1} slots left" msgstr "{0} de {1} vagas restantes" -#: src/fluxer_marketing/components/footer.gleam:325 +#: src/fluxer_marketing/components/footer.gleam:336 msgid "© Fluxer Platform AB (Swedish limited liability company: 559537-3993)" msgstr "© Fluxer Platform AB (sociedade limitada sueca: 559537-3993)" diff --git a/fluxer_marketing/locales/ro/messages.po b/fluxer_marketing/locales/ro/messages.po index d43a4de6..10f1d323 100644 --- a/fluxer_marketing/locales/ro/messages.po +++ b/fluxer_marketing/locales/ro/messages.po @@ -206,7 +206,7 @@ msgid "Blue (Da Ba Dee)" msgstr "Albastru (Da Ba Dee)" #: src/fluxer_marketing/components/footer.gleam:157 -#: src/fluxer_marketing/components/navigation.gleam:449 +#: src/fluxer_marketing/components/navigation.gleam:458 msgid "Bluesky" msgstr "Bluesky" @@ -363,15 +363,15 @@ msgstr "Mod compact și opțiuni de afișare" msgid "Company" msgstr "Companie" -#: src/fluxer_marketing/components/footer.gleam:271 +#: src/fluxer_marketing/components/footer.gleam:282 #: src/fluxer_marketing/components/navigation.gleam:376 #: src/fluxer_marketing/pages/company_page.gleam:41 #: src/fluxer_marketing/pages/company_page.gleam:44 msgid "Company Information" msgstr "Informații despre companie" -#: src/fluxer_marketing/components/footer.gleam:278 -#: src/fluxer_marketing/components/navigation.gleam:426 +#: src/fluxer_marketing/components/footer.gleam:289 +#: src/fluxer_marketing/components/navigation.gleam:435 msgid "Connect" msgstr "Conectează-te" @@ -1010,10 +1010,6 @@ msgstr "Cum plănuiești să folosești Fluxer cu comunitatea ta" msgid "Hungarian" msgstr "Maghiară" -#: src/fluxer_marketing/components/footer.gleam:329 -msgid "IP geolocation data by " -msgstr "Date de geolocalizare IP de la " - #: src/fluxer_marketing/pages/help_article_page.gleam:223 #: src/fluxer_marketing/pages/help_article_page.gleam:365 msgid "If you couldn't find what you're looking for, our support team is here to help." @@ -1267,7 +1263,7 @@ msgid "Open Fluxer" msgstr "Deschide Fluxer" #: src/fluxer_marketing/components/hero.gleam:82 -#: src/fluxer_marketing/components/navigation.gleam:473 +#: src/fluxer_marketing/components/navigation.gleam:482 msgid "Open in Browser" msgstr "Deschide în browser" @@ -1501,7 +1497,7 @@ msgstr "Ești gata de upgrade?" msgid "Register now" msgstr "Înregistrează-te acum" -#: src/fluxer_marketing/components/footer.gleam:311 +#: src/fluxer_marketing/components/footer.gleam:322 msgid "Report a bug" msgstr "Raportează un bug" @@ -1609,7 +1605,9 @@ msgstr "Rezultatele căutării pentru: " msgid "Search through message history" msgstr "Caută în istoricul mesajelor" +#: src/fluxer_marketing/components/footer.gleam:271 #: src/fluxer_marketing/components/get_involved_section.gleam:194 +#: src/fluxer_marketing/components/navigation.gleam:424 #: src/fluxer_marketing/pages/careers_page.gleam:156 #: src/fluxer_marketing/pages/security_page.gleam:41 msgid "Security Bug Bounty" @@ -1680,7 +1678,7 @@ msgid "Soundboard" msgstr "Panou de sunete" #: src/fluxer_marketing/components/footer.gleam:143 -#: src/fluxer_marketing/components/navigation.gleam:438 +#: src/fluxer_marketing/components/navigation.gleam:447 msgid "Source Code" msgstr "Cod sursă" @@ -1781,6 +1779,10 @@ msgstr "Încă nu înlocuiește complet aplicația desktop, iar unele lucruri pe msgid "This page doesn't exist. But there's plenty more to explore." msgstr "Pagina aceasta nu există. Dar mai sunt multe de explorat." +#: src/fluxer_marketing/components/footer.gleam:342 +msgid "This product includes GeoLite2 Data created by MaxMind, available from " +msgstr "Acest produs include datele GeoLite2 create de MaxMind, disponibile de la " + #: src/fluxer_marketing/components/coming_features.gleam:37 msgid "Threads and forums" msgstr "Fire de discuție și forumuri" @@ -2066,7 +2068,7 @@ msgstr "rezultate pentru" msgid "{0} of {1} slots left" msgstr "{0} din {1} locuri rămase" -#: src/fluxer_marketing/components/footer.gleam:325 +#: src/fluxer_marketing/components/footer.gleam:336 msgid "© Fluxer Platform AB (Swedish limited liability company: 559537-3993)" msgstr "© Fluxer Platform AB (societate cu răspundere limitată din Suedia: 559537-3993)" diff --git a/fluxer_marketing/locales/ru/messages.po b/fluxer_marketing/locales/ru/messages.po index 456c8e1c..b3b21e7b 100644 --- a/fluxer_marketing/locales/ru/messages.po +++ b/fluxer_marketing/locales/ru/messages.po @@ -206,7 +206,7 @@ msgid "Blue (Da Ba Dee)" msgstr "Синий (Da Ba Dee)" #: src/fluxer_marketing/components/footer.gleam:157 -#: src/fluxer_marketing/components/navigation.gleam:449 +#: src/fluxer_marketing/components/navigation.gleam:458 msgid "Bluesky" msgstr "Bluesky" @@ -363,15 +363,15 @@ msgstr "Компактный режим и настройки отображен msgid "Company" msgstr "Компания" -#: src/fluxer_marketing/components/footer.gleam:271 +#: src/fluxer_marketing/components/footer.gleam:282 #: src/fluxer_marketing/components/navigation.gleam:376 #: src/fluxer_marketing/pages/company_page.gleam:41 #: src/fluxer_marketing/pages/company_page.gleam:44 msgid "Company Information" msgstr "О компании" -#: src/fluxer_marketing/components/footer.gleam:278 -#: src/fluxer_marketing/components/navigation.gleam:426 +#: src/fluxer_marketing/components/footer.gleam:289 +#: src/fluxer_marketing/components/navigation.gleam:435 msgid "Connect" msgstr "Связаться" @@ -1010,10 +1010,6 @@ msgstr "Как вы планируете использовать Fluxer в св msgid "Hungarian" msgstr "Венгерский" -#: src/fluxer_marketing/components/footer.gleam:329 -msgid "IP geolocation data by " -msgstr "Данные IP-геолокации предоставлены " - #: src/fluxer_marketing/pages/help_article_page.gleam:223 #: src/fluxer_marketing/pages/help_article_page.gleam:365 msgid "If you couldn't find what you're looking for, our support team is here to help." @@ -1267,7 +1263,7 @@ msgid "Open Fluxer" msgstr "Открыть Fluxer" #: src/fluxer_marketing/components/hero.gleam:82 -#: src/fluxer_marketing/components/navigation.gleam:473 +#: src/fluxer_marketing/components/navigation.gleam:482 msgid "Open in Browser" msgstr "Открыть в браузере" @@ -1501,7 +1497,7 @@ msgstr "Готовы перейти на более высокий тариф?" msgid "Register now" msgstr "Зарегистрироваться" -#: src/fluxer_marketing/components/footer.gleam:311 +#: src/fluxer_marketing/components/footer.gleam:322 msgid "Report a bug" msgstr "Сообщить об ошибке" @@ -1609,7 +1605,9 @@ msgstr "Результаты поиска по запросу: " msgid "Search through message history" msgstr "Поиск по истории сообщений" +#: src/fluxer_marketing/components/footer.gleam:271 #: src/fluxer_marketing/components/get_involved_section.gleam:194 +#: src/fluxer_marketing/components/navigation.gleam:424 #: src/fluxer_marketing/pages/careers_page.gleam:156 #: src/fluxer_marketing/pages/security_page.gleam:41 msgid "Security Bug Bounty" @@ -1680,7 +1678,7 @@ msgid "Soundboard" msgstr "Саундборд" #: src/fluxer_marketing/components/footer.gleam:143 -#: src/fluxer_marketing/components/navigation.gleam:438 +#: src/fluxer_marketing/components/navigation.gleam:447 msgid "Source Code" msgstr "Исходный код" @@ -1781,6 +1779,10 @@ msgstr "Пока это не полноценная замена десктоп- msgid "This page doesn't exist. But there's plenty more to explore." msgstr "Этой страницы нет. Но впереди ещё много интересного." +#: src/fluxer_marketing/components/footer.gleam:342 +msgid "This product includes GeoLite2 Data created by MaxMind, available from " +msgstr "Этот продукт включает данные GeoLite2, созданные компанией MaxMind, доступные по адресу " + #: src/fluxer_marketing/components/coming_features.gleam:37 msgid "Threads and forums" msgstr "Треды и форумы" @@ -2066,7 +2068,7 @@ msgstr "результатов по запросу" msgid "{0} of {1} slots left" msgstr "Осталось {0} из {1} мест" -#: src/fluxer_marketing/components/footer.gleam:325 +#: src/fluxer_marketing/components/footer.gleam:336 msgid "© Fluxer Platform AB (Swedish limited liability company: 559537-3993)" msgstr "© Fluxer Platform AB (шведская компания с ограниченной ответственностью: 559537-3993)" diff --git a/fluxer_marketing/locales/sv-SE/messages.po b/fluxer_marketing/locales/sv-SE/messages.po index 62017deb..69e6ba6a 100644 --- a/fluxer_marketing/locales/sv-SE/messages.po +++ b/fluxer_marketing/locales/sv-SE/messages.po @@ -206,7 +206,7 @@ msgid "Blue (Da Ba Dee)" msgstr "Blå (Da Ba Dee)" #: src/fluxer_marketing/components/footer.gleam:157 -#: src/fluxer_marketing/components/navigation.gleam:449 +#: src/fluxer_marketing/components/navigation.gleam:458 msgid "Bluesky" msgstr "Bluesky" @@ -363,15 +363,15 @@ msgstr "Kompakt läge och visningsalternativ" msgid "Company" msgstr "Företag" -#: src/fluxer_marketing/components/footer.gleam:271 +#: src/fluxer_marketing/components/footer.gleam:282 #: src/fluxer_marketing/components/navigation.gleam:376 #: src/fluxer_marketing/pages/company_page.gleam:41 #: src/fluxer_marketing/pages/company_page.gleam:44 msgid "Company Information" msgstr "Företagsinformation" -#: src/fluxer_marketing/components/footer.gleam:278 -#: src/fluxer_marketing/components/navigation.gleam:426 +#: src/fluxer_marketing/components/footer.gleam:289 +#: src/fluxer_marketing/components/navigation.gleam:435 msgid "Connect" msgstr "Kontakt" @@ -1010,10 +1010,6 @@ msgstr "Hur du tänker använda Fluxer i din community" msgid "Hungarian" msgstr "Ungerska" -#: src/fluxer_marketing/components/footer.gleam:329 -msgid "IP geolocation data by " -msgstr "IP-geolokaliseringsdata från " - #: src/fluxer_marketing/pages/help_article_page.gleam:223 #: src/fluxer_marketing/pages/help_article_page.gleam:365 msgid "If you couldn't find what you're looking for, our support team is here to help." @@ -1267,7 +1263,7 @@ msgid "Open Fluxer" msgstr "Öppna Fluxer" #: src/fluxer_marketing/components/hero.gleam:82 -#: src/fluxer_marketing/components/navigation.gleam:473 +#: src/fluxer_marketing/components/navigation.gleam:482 msgid "Open in Browser" msgstr "Öppna i webbläsaren" @@ -1501,7 +1497,7 @@ msgstr "Redo att uppgradera?" msgid "Register now" msgstr "Registrera dig nu" -#: src/fluxer_marketing/components/footer.gleam:311 +#: src/fluxer_marketing/components/footer.gleam:322 msgid "Report a bug" msgstr "Rapportera en bugg" @@ -1609,7 +1605,9 @@ msgstr "Sökresultat för: " msgid "Search through message history" msgstr "Sök i meddelandehistoriken" +#: src/fluxer_marketing/components/footer.gleam:271 #: src/fluxer_marketing/components/get_involved_section.gleam:194 +#: src/fluxer_marketing/components/navigation.gleam:424 #: src/fluxer_marketing/pages/careers_page.gleam:156 #: src/fluxer_marketing/pages/security_page.gleam:41 msgid "Security Bug Bounty" @@ -1680,7 +1678,7 @@ msgid "Soundboard" msgstr "Soundboard" #: src/fluxer_marketing/components/footer.gleam:143 -#: src/fluxer_marketing/components/navigation.gleam:438 +#: src/fluxer_marketing/components/navigation.gleam:447 msgid "Source Code" msgstr "Källkod" @@ -1781,6 +1779,10 @@ msgstr "Det här ersätter inte datorappen fullt ut ännu, och vissa saker man f msgid "This page doesn't exist. But there's plenty more to explore." msgstr "Den här sidan finns inte. Men det finns mycket mer att utforska." +#: src/fluxer_marketing/components/footer.gleam:342 +msgid "This product includes GeoLite2 Data created by MaxMind, available from " +msgstr "Denna produkt inkluderar GeoLite2-data skapad av MaxMind, tillgänglig från " + #: src/fluxer_marketing/components/coming_features.gleam:37 msgid "Threads and forums" msgstr "Trådar och forum" @@ -2066,7 +2068,7 @@ msgstr "resultat för" msgid "{0} of {1} slots left" msgstr "{0} av {1} platser kvar" -#: src/fluxer_marketing/components/footer.gleam:325 +#: src/fluxer_marketing/components/footer.gleam:336 msgid "© Fluxer Platform AB (Swedish limited liability company: 559537-3993)" msgstr "© Fluxer Platform AB (svenskt aktiebolag, org.nr: 559537-3993)" diff --git a/fluxer_marketing/locales/th/messages.po b/fluxer_marketing/locales/th/messages.po index 8303dab6..77ddf209 100644 --- a/fluxer_marketing/locales/th/messages.po +++ b/fluxer_marketing/locales/th/messages.po @@ -206,7 +206,7 @@ msgid "Blue (Da Ba Dee)" msgstr "น้ำเงิน (Da Ba Dee)" #: src/fluxer_marketing/components/footer.gleam:157 -#: src/fluxer_marketing/components/navigation.gleam:449 +#: src/fluxer_marketing/components/navigation.gleam:458 msgid "Bluesky" msgstr "Bluesky" @@ -363,15 +363,15 @@ msgstr "โหมดกะทัดรัดและตัวเลือกก msgid "Company" msgstr "บริษัท" -#: src/fluxer_marketing/components/footer.gleam:271 +#: src/fluxer_marketing/components/footer.gleam:282 #: src/fluxer_marketing/components/navigation.gleam:376 #: src/fluxer_marketing/pages/company_page.gleam:41 #: src/fluxer_marketing/pages/company_page.gleam:44 msgid "Company Information" msgstr "ข้อมูลบริษัท" -#: src/fluxer_marketing/components/footer.gleam:278 -#: src/fluxer_marketing/components/navigation.gleam:426 +#: src/fluxer_marketing/components/footer.gleam:289 +#: src/fluxer_marketing/components/navigation.gleam:435 msgid "Connect" msgstr "ติดต่อเรา" @@ -1010,10 +1010,6 @@ msgstr "คุณวางแผนจะใช้ Fluxer กับชุมช msgid "Hungarian" msgstr "ฮังการี" -#: src/fluxer_marketing/components/footer.gleam:329 -msgid "IP geolocation data by " -msgstr "ข้อมูลระบุตำแหน่งจาก IP โดย " - #: src/fluxer_marketing/pages/help_article_page.gleam:223 #: src/fluxer_marketing/pages/help_article_page.gleam:365 msgid "If you couldn't find what you're looking for, our support team is here to help." @@ -1267,7 +1263,7 @@ msgid "Open Fluxer" msgstr "เปิด Fluxer" #: src/fluxer_marketing/components/hero.gleam:82 -#: src/fluxer_marketing/components/navigation.gleam:473 +#: src/fluxer_marketing/components/navigation.gleam:482 msgid "Open in Browser" msgstr "เปิดในเบราว์เซอร์" @@ -1501,7 +1497,7 @@ msgstr "พร้อมอัปเกรดหรือยัง?" msgid "Register now" msgstr "ลงทะเบียนตอนนี้" -#: src/fluxer_marketing/components/footer.gleam:311 +#: src/fluxer_marketing/components/footer.gleam:322 msgid "Report a bug" msgstr "รายงานบั๊ก" @@ -1609,7 +1605,9 @@ msgstr "ผลการค้นหาสำหรับ: " msgid "Search through message history" msgstr "ค้นหาในประวัติข้อความ" +#: src/fluxer_marketing/components/footer.gleam:271 #: src/fluxer_marketing/components/get_involved_section.gleam:194 +#: src/fluxer_marketing/components/navigation.gleam:424 #: src/fluxer_marketing/pages/careers_page.gleam:156 #: src/fluxer_marketing/pages/security_page.gleam:41 msgid "Security Bug Bounty" @@ -1680,7 +1678,7 @@ msgid "Soundboard" msgstr "ซาวด์บอร์ด" #: src/fluxer_marketing/components/footer.gleam:143 -#: src/fluxer_marketing/components/navigation.gleam:438 +#: src/fluxer_marketing/components/navigation.gleam:447 msgid "Source Code" msgstr "ซอร์สโค้ด" @@ -1781,6 +1779,10 @@ msgstr "ตอนนี้ยังไม่ใช่ตัวแทนเดส msgid "This page doesn't exist. But there's plenty more to explore." msgstr "หน้านี้ไม่มีอยู่ แต่ยังมีอีกหลายอย่างให้สำรวจ" +#: src/fluxer_marketing/components/footer.gleam:342 +msgid "This product includes GeoLite2 Data created by MaxMind, available from " +msgstr "ผลิตภัณฑ์นี้รวมถึงข้อมูล GeoLite2 ที่สร้างโดย MaxMind ซึ่งสามารถดาวน์โหลดได้จาก " + #: src/fluxer_marketing/components/coming_features.gleam:37 msgid "Threads and forums" msgstr "เธรดและฟอรั่ม" @@ -2066,7 +2068,7 @@ msgstr "ผลลัพธ์สำหรับ" msgid "{0} of {1} slots left" msgstr "เหลืออีก {0} จาก {1} สิทธิ์" -#: src/fluxer_marketing/components/footer.gleam:325 +#: src/fluxer_marketing/components/footer.gleam:336 msgid "© Fluxer Platform AB (Swedish limited liability company: 559537-3993)" msgstr "© Fluxer Platform AB (บริษัทจำกัดในสวีเดน: 559537-3993)" diff --git a/fluxer_marketing/locales/tr/messages.po b/fluxer_marketing/locales/tr/messages.po index 56337eac..013d696b 100644 --- a/fluxer_marketing/locales/tr/messages.po +++ b/fluxer_marketing/locales/tr/messages.po @@ -206,7 +206,7 @@ msgid "Blue (Da Ba Dee)" msgstr "Mavi (Da Ba Dee)" #: src/fluxer_marketing/components/footer.gleam:157 -#: src/fluxer_marketing/components/navigation.gleam:449 +#: src/fluxer_marketing/components/navigation.gleam:458 msgid "Bluesky" msgstr "Bluesky" @@ -363,15 +363,15 @@ msgstr "Kompakt mod ve görüntü seçenekleri" msgid "Company" msgstr "Şirket" -#: src/fluxer_marketing/components/footer.gleam:271 +#: src/fluxer_marketing/components/footer.gleam:282 #: src/fluxer_marketing/components/navigation.gleam:376 #: src/fluxer_marketing/pages/company_page.gleam:41 #: src/fluxer_marketing/pages/company_page.gleam:44 msgid "Company Information" msgstr "Şirket Bilgileri" -#: src/fluxer_marketing/components/footer.gleam:278 -#: src/fluxer_marketing/components/navigation.gleam:426 +#: src/fluxer_marketing/components/footer.gleam:289 +#: src/fluxer_marketing/components/navigation.gleam:435 msgid "Connect" msgstr "Bağlan" @@ -1010,10 +1010,6 @@ msgstr "Fluxer'ı topluluğunla nasıl kullanmayı planlıyorsun?" msgid "Hungarian" msgstr "Macarca" -#: src/fluxer_marketing/components/footer.gleam:329 -msgid "IP geolocation data by " -msgstr "IP konum verileri sağlayıcısı: " - #: src/fluxer_marketing/pages/help_article_page.gleam:223 #: src/fluxer_marketing/pages/help_article_page.gleam:365 msgid "If you couldn't find what you're looking for, our support team is here to help." @@ -1267,7 +1263,7 @@ msgid "Open Fluxer" msgstr "Fluxer'ı aç" #: src/fluxer_marketing/components/hero.gleam:82 -#: src/fluxer_marketing/components/navigation.gleam:473 +#: src/fluxer_marketing/components/navigation.gleam:482 msgid "Open in Browser" msgstr "Tarayıcıda aç" @@ -1501,7 +1497,7 @@ msgstr "Yükseltmeye hazır mısın?" msgid "Register now" msgstr "Hemen kayıt ol" -#: src/fluxer_marketing/components/footer.gleam:311 +#: src/fluxer_marketing/components/footer.gleam:322 msgid "Report a bug" msgstr "Hata bildir" @@ -1609,7 +1605,9 @@ msgstr "Şunun için arama sonuçları: " msgid "Search through message history" msgstr "Mesaj geçmişinde ara" +#: src/fluxer_marketing/components/footer.gleam:271 #: src/fluxer_marketing/components/get_involved_section.gleam:194 +#: src/fluxer_marketing/components/navigation.gleam:424 #: src/fluxer_marketing/pages/careers_page.gleam:156 #: src/fluxer_marketing/pages/security_page.gleam:41 msgid "Security Bug Bounty" @@ -1680,7 +1678,7 @@ msgid "Soundboard" msgstr "Ses panosu" #: src/fluxer_marketing/components/footer.gleam:143 -#: src/fluxer_marketing/components/navigation.gleam:438 +#: src/fluxer_marketing/components/navigation.gleam:447 msgid "Source Code" msgstr "Kaynak kodu" @@ -1781,6 +1779,10 @@ msgstr "Bu henüz masaüstü uygulamasının tam bir yerine geçmiyor; mobil soh msgid "This page doesn't exist. But there's plenty more to explore." msgstr "Bu sayfa yok. Ama keşfedecek daha pek çok şey var." +#: src/fluxer_marketing/components/footer.gleam:342 +msgid "This product includes GeoLite2 Data created by MaxMind, available from " +msgstr "Bu ürün, MaxMind tarafından oluşturulan GeoLite2 Verilerini içermektedir, şu adresten temin edilebilir." + #: src/fluxer_marketing/components/coming_features.gleam:37 msgid "Threads and forums" msgstr "Thread'ler ve forumlar" @@ -2066,7 +2068,7 @@ msgstr "sonuç" msgid "{0} of {1} slots left" msgstr "{1} kontenjandan {0} kaldı" -#: src/fluxer_marketing/components/footer.gleam:325 +#: src/fluxer_marketing/components/footer.gleam:336 msgid "© Fluxer Platform AB (Swedish limited liability company: 559537-3993)" msgstr "© Fluxer Platform AB (İsveç limited şirketi: 559537-3993)" diff --git a/fluxer_marketing/locales/uk/messages.po b/fluxer_marketing/locales/uk/messages.po index 083eb0c9..86760b35 100644 --- a/fluxer_marketing/locales/uk/messages.po +++ b/fluxer_marketing/locales/uk/messages.po @@ -206,7 +206,7 @@ msgid "Blue (Da Ba Dee)" msgstr "Синій (Da Ba Dee)" #: src/fluxer_marketing/components/footer.gleam:157 -#: src/fluxer_marketing/components/navigation.gleam:449 +#: src/fluxer_marketing/components/navigation.gleam:458 msgid "Bluesky" msgstr "Bluesky" @@ -363,15 +363,15 @@ msgstr "Компактний режим і параметри відображе msgid "Company" msgstr "Компанія" -#: src/fluxer_marketing/components/footer.gleam:271 +#: src/fluxer_marketing/components/footer.gleam:282 #: src/fluxer_marketing/components/navigation.gleam:376 #: src/fluxer_marketing/pages/company_page.gleam:41 #: src/fluxer_marketing/pages/company_page.gleam:44 msgid "Company Information" msgstr "Інформація про компанію" -#: src/fluxer_marketing/components/footer.gleam:278 -#: src/fluxer_marketing/components/navigation.gleam:426 +#: src/fluxer_marketing/components/footer.gleam:289 +#: src/fluxer_marketing/components/navigation.gleam:435 msgid "Connect" msgstr "Зв’язок" @@ -1010,10 +1010,6 @@ msgstr "Як ви плануєте використовувати Fluxer зі с msgid "Hungarian" msgstr "Угорська" -#: src/fluxer_marketing/components/footer.gleam:329 -msgid "IP geolocation data by " -msgstr "Дані геолокації за IP від " - #: src/fluxer_marketing/pages/help_article_page.gleam:223 #: src/fluxer_marketing/pages/help_article_page.gleam:365 msgid "If you couldn't find what you're looking for, our support team is here to help." @@ -1267,7 +1263,7 @@ msgid "Open Fluxer" msgstr "Відкрити Fluxer" #: src/fluxer_marketing/components/hero.gleam:82 -#: src/fluxer_marketing/components/navigation.gleam:473 +#: src/fluxer_marketing/components/navigation.gleam:482 msgid "Open in Browser" msgstr "Відкрити в браузері" @@ -1501,7 +1497,7 @@ msgstr "Готові оновитися?" msgid "Register now" msgstr "Зареєструватися" -#: src/fluxer_marketing/components/footer.gleam:311 +#: src/fluxer_marketing/components/footer.gleam:322 msgid "Report a bug" msgstr "Повідомити про баг" @@ -1609,7 +1605,9 @@ msgstr "Результати пошуку для: " msgid "Search through message history" msgstr "Пошук в історії повідомлень" +#: src/fluxer_marketing/components/footer.gleam:271 #: src/fluxer_marketing/components/get_involved_section.gleam:194 +#: src/fluxer_marketing/components/navigation.gleam:424 #: src/fluxer_marketing/pages/careers_page.gleam:156 #: src/fluxer_marketing/pages/security_page.gleam:41 msgid "Security Bug Bounty" @@ -1680,7 +1678,7 @@ msgid "Soundboard" msgstr "Саундборд" #: src/fluxer_marketing/components/footer.gleam:143 -#: src/fluxer_marketing/components/navigation.gleam:438 +#: src/fluxer_marketing/components/navigation.gleam:447 msgid "Source Code" msgstr "Вихідний код" @@ -1781,6 +1779,10 @@ msgstr "Це ще не повна заміна десктопного засто msgid "This page doesn't exist. But there's plenty more to explore." msgstr "Цієї сторінки не існує. Але попереду ще багато цікавого." +#: src/fluxer_marketing/components/footer.gleam:342 +msgid "This product includes GeoLite2 Data created by MaxMind, available from " +msgstr "Цей продукт містить дані GeoLite2, створені компанією MaxMind, доступні з " + #: src/fluxer_marketing/components/coming_features.gleam:37 msgid "Threads and forums" msgstr "Гілки та форуми" @@ -2066,7 +2068,7 @@ msgstr "результати для" msgid "{0} of {1} slots left" msgstr "Залишилося {0} із {1} місць" -#: src/fluxer_marketing/components/footer.gleam:325 +#: src/fluxer_marketing/components/footer.gleam:336 msgid "© Fluxer Platform AB (Swedish limited liability company: 559537-3993)" msgstr "© Fluxer Platform AB (шведська компанія з обмеженою відповідальністю: 559537-3993)" diff --git a/fluxer_marketing/locales/vi/messages.po b/fluxer_marketing/locales/vi/messages.po index a9ee1cb1..20dc3db9 100644 --- a/fluxer_marketing/locales/vi/messages.po +++ b/fluxer_marketing/locales/vi/messages.po @@ -206,7 +206,7 @@ msgid "Blue (Da Ba Dee)" msgstr "Xanh dương (Da Ba Dee)" #: src/fluxer_marketing/components/footer.gleam:157 -#: src/fluxer_marketing/components/navigation.gleam:449 +#: src/fluxer_marketing/components/navigation.gleam:458 msgid "Bluesky" msgstr "Bluesky" @@ -363,15 +363,15 @@ msgstr "Chế độ gọn và tùy chọn hiển thị" msgid "Company" msgstr "Công ty" -#: src/fluxer_marketing/components/footer.gleam:271 +#: src/fluxer_marketing/components/footer.gleam:282 #: src/fluxer_marketing/components/navigation.gleam:376 #: src/fluxer_marketing/pages/company_page.gleam:41 #: src/fluxer_marketing/pages/company_page.gleam:44 msgid "Company Information" msgstr "Thông tin công ty" -#: src/fluxer_marketing/components/footer.gleam:278 -#: src/fluxer_marketing/components/navigation.gleam:426 +#: src/fluxer_marketing/components/footer.gleam:289 +#: src/fluxer_marketing/components/navigation.gleam:435 msgid "Connect" msgstr "Kết nối" @@ -1010,10 +1010,6 @@ msgstr "Bạn dự định dùng Fluxer với cộng đồng của mình như th msgid "Hungarian" msgstr "Tiếng Hungary" -#: src/fluxer_marketing/components/footer.gleam:329 -msgid "IP geolocation data by " -msgstr "Dữ liệu định vị IP bởi " - #: src/fluxer_marketing/pages/help_article_page.gleam:223 #: src/fluxer_marketing/pages/help_article_page.gleam:365 msgid "If you couldn't find what you're looking for, our support team is here to help." @@ -1267,7 +1263,7 @@ msgid "Open Fluxer" msgstr "Mở Fluxer" #: src/fluxer_marketing/components/hero.gleam:82 -#: src/fluxer_marketing/components/navigation.gleam:473 +#: src/fluxer_marketing/components/navigation.gleam:482 msgid "Open in Browser" msgstr "Mở trong trình duyệt" @@ -1501,7 +1497,7 @@ msgstr "Sẵn sàng nâng cấp?" msgid "Register now" msgstr "Đăng ký ngay" -#: src/fluxer_marketing/components/footer.gleam:311 +#: src/fluxer_marketing/components/footer.gleam:322 msgid "Report a bug" msgstr "Báo lỗi" @@ -1609,7 +1605,9 @@ msgstr "Kết quả tìm kiếm cho: " msgid "Search through message history" msgstr "Tìm trong lịch sử tin nhắn" +#: src/fluxer_marketing/components/footer.gleam:271 #: src/fluxer_marketing/components/get_involved_section.gleam:194 +#: src/fluxer_marketing/components/navigation.gleam:424 #: src/fluxer_marketing/pages/careers_page.gleam:156 #: src/fluxer_marketing/pages/security_page.gleam:41 msgid "Security Bug Bounty" @@ -1680,7 +1678,7 @@ msgid "Soundboard" msgstr "Bảng âm thanh" #: src/fluxer_marketing/components/footer.gleam:143 -#: src/fluxer_marketing/components/navigation.gleam:438 +#: src/fluxer_marketing/components/navigation.gleam:447 msgid "Source Code" msgstr "Mã nguồn" @@ -1781,6 +1779,10 @@ msgstr "Hiện đây vẫn chưa thể thay thế hoàn toàn ứng dụng máy msgid "This page doesn't exist. But there's plenty more to explore." msgstr "Trang này không tồn tại. Nhưng vẫn còn rất nhiều thứ để khám phá." +#: src/fluxer_marketing/components/footer.gleam:342 +msgid "This product includes GeoLite2 Data created by MaxMind, available from " +msgstr "Sản phẩm này bao gồm Dữ liệu GeoLite2 được tạo bởi MaxMind, có sẵn từ " + #: src/fluxer_marketing/components/coming_features.gleam:37 msgid "Threads and forums" msgstr "Chuỗi thảo luận và diễn đàn" @@ -2066,7 +2068,7 @@ msgstr "kết quả cho" msgid "{0} of {1} slots left" msgstr "Còn {0}/{1} suất" -#: src/fluxer_marketing/components/footer.gleam:325 +#: src/fluxer_marketing/components/footer.gleam:336 msgid "© Fluxer Platform AB (Swedish limited liability company: 559537-3993)" msgstr "© Fluxer Platform AB (công ty TNHH tại Thụy Điển: 559537-3993)" diff --git a/fluxer_marketing/locales/zh-CN/messages.po b/fluxer_marketing/locales/zh-CN/messages.po index 76005bf0..661144a4 100644 --- a/fluxer_marketing/locales/zh-CN/messages.po +++ b/fluxer_marketing/locales/zh-CN/messages.po @@ -206,7 +206,7 @@ msgid "Blue (Da Ba Dee)" msgstr "蓝色(Da Ba Dee)" #: src/fluxer_marketing/components/footer.gleam:157 -#: src/fluxer_marketing/components/navigation.gleam:449 +#: src/fluxer_marketing/components/navigation.gleam:458 msgid "Bluesky" msgstr "Bluesky" @@ -363,15 +363,15 @@ msgstr "紧凑模式与显示选项" msgid "Company" msgstr "公司" -#: src/fluxer_marketing/components/footer.gleam:271 +#: src/fluxer_marketing/components/footer.gleam:282 #: src/fluxer_marketing/components/navigation.gleam:376 #: src/fluxer_marketing/pages/company_page.gleam:41 #: src/fluxer_marketing/pages/company_page.gleam:44 msgid "Company Information" msgstr "公司信息" -#: src/fluxer_marketing/components/footer.gleam:278 -#: src/fluxer_marketing/components/navigation.gleam:426 +#: src/fluxer_marketing/components/footer.gleam:289 +#: src/fluxer_marketing/components/navigation.gleam:435 msgid "Connect" msgstr "联系" @@ -1010,10 +1010,6 @@ msgstr "你计划如何在社区中使用 Fluxer" msgid "Hungarian" msgstr "匈牙利语" -#: src/fluxer_marketing/components/footer.gleam:329 -msgid "IP geolocation data by " -msgstr "IP 地理定位数据由 " - #: src/fluxer_marketing/pages/help_article_page.gleam:223 #: src/fluxer_marketing/pages/help_article_page.gleam:365 msgid "If you couldn't find what you're looking for, our support team is here to help." @@ -1267,7 +1263,7 @@ msgid "Open Fluxer" msgstr "打开 Fluxer" #: src/fluxer_marketing/components/hero.gleam:82 -#: src/fluxer_marketing/components/navigation.gleam:473 +#: src/fluxer_marketing/components/navigation.gleam:482 msgid "Open in Browser" msgstr "在浏览器中打开" @@ -1501,7 +1497,7 @@ msgstr "准备升级了吗?" msgid "Register now" msgstr "立即注册" -#: src/fluxer_marketing/components/footer.gleam:311 +#: src/fluxer_marketing/components/footer.gleam:322 msgid "Report a bug" msgstr "报告 Bug" @@ -1609,7 +1605,9 @@ msgstr "搜索结果: " msgid "Search through message history" msgstr "搜索消息记录" +#: src/fluxer_marketing/components/footer.gleam:271 #: src/fluxer_marketing/components/get_involved_section.gleam:194 +#: src/fluxer_marketing/components/navigation.gleam:424 #: src/fluxer_marketing/pages/careers_page.gleam:156 #: src/fluxer_marketing/pages/security_page.gleam:41 msgid "Security Bug Bounty" @@ -1680,7 +1678,7 @@ msgid "Soundboard" msgstr "音效面板" #: src/fluxer_marketing/components/footer.gleam:143 -#: src/fluxer_marketing/components/navigation.gleam:438 +#: src/fluxer_marketing/components/navigation.gleam:447 msgid "Source Code" msgstr "源代码" @@ -1781,6 +1779,10 @@ msgstr "这还不能完全替代桌面端应用,移动聊天应用常见的一 msgid "This page doesn't exist. But there's plenty more to explore." msgstr "这个页面不存在,但还有更多内容值得探索。" +#: src/fluxer_marketing/components/footer.gleam:342 +msgid "This product includes GeoLite2 Data created by MaxMind, available from " +msgstr "本产品包含由 MaxMind 创建的 GeoLite2 数据,可从 " + #: src/fluxer_marketing/components/coming_features.gleam:37 msgid "Threads and forums" msgstr "线程与论坛" @@ -2066,7 +2068,7 @@ msgstr "条结果,关键词:" msgid "{0} of {1} slots left" msgstr "还剩 {0}/{1} 个名额" -#: src/fluxer_marketing/components/footer.gleam:325 +#: src/fluxer_marketing/components/footer.gleam:336 msgid "© Fluxer Platform AB (Swedish limited liability company: 559537-3993)" msgstr "© Fluxer Platform AB(瑞典有限责任公司:559537-3993)" diff --git a/fluxer_marketing/locales/zh-TW/messages.po b/fluxer_marketing/locales/zh-TW/messages.po index 97d93cec..8e1d50ad 100644 --- a/fluxer_marketing/locales/zh-TW/messages.po +++ b/fluxer_marketing/locales/zh-TW/messages.po @@ -206,7 +206,7 @@ msgid "Blue (Da Ba Dee)" msgstr "藍色(Da Ba Dee)" #: src/fluxer_marketing/components/footer.gleam:157 -#: src/fluxer_marketing/components/navigation.gleam:449 +#: src/fluxer_marketing/components/navigation.gleam:458 msgid "Bluesky" msgstr "Bluesky" @@ -363,15 +363,15 @@ msgstr "精簡模式與顯示選項" msgid "Company" msgstr "公司" -#: src/fluxer_marketing/components/footer.gleam:271 +#: src/fluxer_marketing/components/footer.gleam:282 #: src/fluxer_marketing/components/navigation.gleam:376 #: src/fluxer_marketing/pages/company_page.gleam:41 #: src/fluxer_marketing/pages/company_page.gleam:44 msgid "Company Information" msgstr "公司資訊" -#: src/fluxer_marketing/components/footer.gleam:278 -#: src/fluxer_marketing/components/navigation.gleam:426 +#: src/fluxer_marketing/components/footer.gleam:289 +#: src/fluxer_marketing/components/navigation.gleam:435 msgid "Connect" msgstr "連結" @@ -1010,10 +1010,6 @@ msgstr "你打算如何在社群中使用 Fluxer" msgid "Hungarian" msgstr "匈牙利語" -#: src/fluxer_marketing/components/footer.gleam:329 -msgid "IP geolocation data by " -msgstr "IP 地理定位資料提供: " - #: src/fluxer_marketing/pages/help_article_page.gleam:223 #: src/fluxer_marketing/pages/help_article_page.gleam:365 msgid "If you couldn't find what you're looking for, our support team is here to help." @@ -1267,7 +1263,7 @@ msgid "Open Fluxer" msgstr "開啟 Fluxer" #: src/fluxer_marketing/components/hero.gleam:82 -#: src/fluxer_marketing/components/navigation.gleam:473 +#: src/fluxer_marketing/components/navigation.gleam:482 msgid "Open in Browser" msgstr "在瀏覽器中開啟" @@ -1501,7 +1497,7 @@ msgstr "準備升級了嗎?" msgid "Register now" msgstr "立即註冊" -#: src/fluxer_marketing/components/footer.gleam:311 +#: src/fluxer_marketing/components/footer.gleam:322 msgid "Report a bug" msgstr "回報錯誤" @@ -1609,7 +1605,9 @@ msgstr "搜尋結果: " msgid "Search through message history" msgstr "搜尋訊息歷史紀錄" +#: src/fluxer_marketing/components/footer.gleam:271 #: src/fluxer_marketing/components/get_involved_section.gleam:194 +#: src/fluxer_marketing/components/navigation.gleam:424 #: src/fluxer_marketing/pages/careers_page.gleam:156 #: src/fluxer_marketing/pages/security_page.gleam:41 msgid "Security Bug Bounty" @@ -1680,7 +1678,7 @@ msgid "Soundboard" msgstr "音效板" #: src/fluxer_marketing/components/footer.gleam:143 -#: src/fluxer_marketing/components/navigation.gleam:438 +#: src/fluxer_marketing/components/navigation.gleam:447 msgid "Source Code" msgstr "原始碼" @@ -1781,6 +1779,10 @@ msgstr "目前還無法完全取代桌面版,行動聊天 App 常見的一些 msgid "This page doesn't exist. But there's plenty more to explore." msgstr "這個頁面不存在,不過還有很多內容等你探索。" +#: src/fluxer_marketing/components/footer.gleam:342 +msgid "This product includes GeoLite2 Data created by MaxMind, available from " +msgstr "此產品包含由 MaxMind 創建的 GeoLite2 數據,可從 " + #: src/fluxer_marketing/components/coming_features.gleam:37 msgid "Threads and forums" msgstr "討論串與論壇" @@ -2066,7 +2068,7 @@ msgstr "筆結果,關於" msgid "{0} of {1} slots left" msgstr "剩下 {0}/{1} 個名額" -#: src/fluxer_marketing/components/footer.gleam:325 +#: src/fluxer_marketing/components/footer.gleam:336 msgid "© Fluxer Platform AB (Swedish limited liability company: 559537-3993)" msgstr "© Fluxer Platform AB(瑞典有限公司:559537-3993)" diff --git a/fluxer_marketing/src/fluxer_marketing.gleam b/fluxer_marketing/src/fluxer_marketing.gleam index 21ac5798..d5c5ad29 100644 --- a/fluxer_marketing/src/fluxer_marketing.gleam +++ b/fluxer_marketing/src/fluxer_marketing.gleam @@ -83,7 +83,11 @@ fn handle_request( let locale = get_request_locale(req) let base_url = cfg.marketing_endpoint <> cfg.base_path - let country_code = geoip.country_code(req, cfg.geoip_host) + let country_code = + geoip.country_code( + req, + geoip.Settings(api_host: cfg.api_host, rpc_secret: cfg.gateway_rpc_secret), + ) let user_agent = case request.get_header(req, "user-agent") { Ok(ua) -> ua @@ -107,7 +111,6 @@ fn handle_request( base_path: cfg.base_path, platform: platform, architecture: architecture, - geoip_host: cfg.geoip_host, release_channel: cfg.release_channel, visionary_slots: visionary_slots.current(slots_cache), metrics_endpoint: cfg.metrics_endpoint, diff --git a/fluxer_marketing/src/fluxer_marketing/components/footer.gleam b/fluxer_marketing/src/fluxer_marketing/components/footer.gleam index d357c1e4..f6fd9795 100644 --- a/fluxer_marketing/src/fluxer_marketing/components/footer.gleam +++ b/fluxer_marketing/src/fluxer_marketing/components/footer.gleam @@ -337,16 +337,20 @@ pub fn render(ctx: Context) -> Element(a) { )), ]), html.p([attribute.class("body-sm text-white/80")], [ - html.text(g_(i18n_ctx, "IP geolocation data by ")), + html.text(g_( + i18n_ctx, + "This product includes GeoLite2 Data created by MaxMind, available from ", + )), html.a( [ - attribute.href("https://ipinfo.io"), + attribute.href("https://www.maxmind.com"), attribute.target("_blank"), attribute.rel("noopener noreferrer"), attribute.class("hover:underline"), ], - [html.text("IPinfo")], + [html.text("MaxMind")], ), + html.text("."), ]), ]), ]), diff --git a/fluxer_marketing/src/fluxer_marketing/config.gleam b/fluxer_marketing/src/fluxer_marketing/config.gleam index 75e8e32d..2edad5fb 100644 --- a/fluxer_marketing/src/fluxer_marketing/config.gleam +++ b/fluxer_marketing/src/fluxer_marketing/config.gleam @@ -32,7 +32,6 @@ pub type Config { marketing_endpoint: String, port: Int, base_path: String, - geoip_host: String, build_timestamp: String, release_channel: String, gateway_rpc_secret: String, @@ -101,7 +100,6 @@ pub fn load_config() -> Result(Config, String) { "FLUXER_MARKETING_ENDPOINT", )) use base_path_raw <- result.try(required_env("FLUXER_PATH_MARKETING")) - use geoip_host <- result.try(required_env("GEOIP_HOST")) use port <- result.try(required_int_env("FLUXER_MARKETING_PORT")) use release_channel <- result.try(required_env("RELEASE_CHANNEL")) use gateway_rpc_secret <- result.try(required_env("GATEWAY_RPC_SECRET")) @@ -125,7 +123,6 @@ pub fn load_config() -> Result(Config, String) { marketing_endpoint: marketing_endpoint, port: port, base_path: base_path, - geoip_host: geoip_host, build_timestamp: optional_env("BUILD_TIMESTAMP") |> option.unwrap(""), release_channel: release_channel, gateway_rpc_secret: gateway_rpc_secret, diff --git a/fluxer_marketing/src/fluxer_marketing/geoip.gleam b/fluxer_marketing/src/fluxer_marketing/geoip.gleam index b6677251..bd2ccebf 100644 --- a/fluxer_marketing/src/fluxer_marketing/geoip.gleam +++ b/fluxer_marketing/src/fluxer_marketing/geoip.gleam @@ -15,180 +15,180 @@ //// You should have received a copy of the GNU Affero General Public License //// along with Fluxer. If not, see . -import gleam/bit_array +import gleam/dynamic/decode +import gleam/http import gleam/http/request import gleam/httpc +import gleam/int import gleam/json import gleam/list import gleam/result import gleam/string import wisp -const default_cc = "US" - -pub fn country_code(req: wisp.Request, geoip_host: String) -> String { - let get_header = fn(name) { request.get_header(req, name) } - country_code_core(get_header, geoip_host, fetch_country_code_http) +pub type Settings { + Settings(api_host: String, rpc_secret: String) } -pub fn country_code_core( - get_header: fn(String) -> Result(String, Nil), - geoip_host: String, - fetch_country: fn(String) -> Result(String, Nil), -) -> String { - case geoip_host { +const default_cc = "US" + +const log_prefix = "[geoip]" + +pub fn country_code(req: wisp.Request, settings: Settings) -> String { + case extract_client_ip(req) { "" -> default_cc - _ -> { - case extract_client_ip_from(get_header) { - "" -> default_cc - ip -> { - let url = - "http://" <> geoip_host <> "/lookup?ip=" <> percent_encode_ip(ip) - case fetch_country(url) { - Ok(body) -> { - let cc = string.uppercase(string.trim(body)) - case is_valid_country_code(cc) { - True -> cc - False -> default_cc - } - } - Error(_) -> default_cc - } + ip -> + case fetch_country_code(settings, ip) { + Ok(code) -> code + Error(_) -> default_cc + } + } +} + +fn fetch_country_code(settings: Settings, ip: String) -> Result(String, Nil) { + case rpc_url(settings.api_host) { + "" -> { + log_missing_api_host(settings.api_host) + Error(Nil) + } + url -> { + let body = + json.object([ + #("type", json.string("geoip_lookup")), + #("ip", json.string(ip)), + ]) + |> json.to_string + + let assert Ok(req) = request.to(url) + let req = + req + |> request.set_method(http.Post) + |> request.prepend_header("content-type", "application/json") + |> request.prepend_header( + "Authorization", + "Bearer " <> settings.rpc_secret, + ) + |> request.set_body(body) + + case httpc.send(req) { + Ok(resp) if resp.status >= 200 && resp.status < 300 -> + decode_country_code(resp.body) + Ok(resp) -> { + log_rpc_status(settings.api_host, resp.status, resp.body) + Error(Nil) + } + Error(error) -> { + log_rpc_error(settings.api_host, string.inspect(error)) + Error(Nil) } } } } } -fn fetch_country_code_http(url: String) -> Result(String, Nil) { - let assert Ok(req) = request.to(url) - let req = request.prepend_header(req, "accept", "text/plain") - case httpc.send(req) { - Ok(resp) if resp.status >= 200 && resp.status < 300 -> Ok(resp.body) - _ -> Error(Nil) +fn decode_country_code(body: String) -> Result(String, Nil) { + let response_decoder = { + use data <- decode.field("data", { + use code <- decode.field("country_code", decode.string) + decode.success(code) + }) + decode.success(data) + } + + case json.parse(from: body, using: response_decoder) { + Ok(code) -> Ok(string.uppercase(string.trim(code))) + Error(_) -> Error(Nil) } } fn extract_client_ip(req: wisp.Request) -> String { - extract_client_ip_from(fn(name) { request.get_header(req, name) }) -} - -pub fn extract_client_ip_from( - get_header: fn(String) -> Result(String, Nil), -) -> String { - case get_header("x-forwarded-for") { - Ok(xff) -> { + case request.get_header(req, "x-forwarded-for") { + Ok(xff) -> xff |> string.split(",") |> list.first |> result.unwrap("") |> string.trim |> strip_brackets - |> validate_ip - } Error(_) -> "" } } -fn validate_ip(s: String) -> String { - case string.contains(s, ".") || string.contains(s, ":") { - True -> s - False -> "" - } -} - pub fn strip_brackets(ip: String) -> String { let len = string.length(ip) - let has_brackets = + case len >= 2 && string.first(ip) == Ok("[") && string.slice(ip, len - 1, 1) == "]" - - case has_brackets { + { True -> string.slice(ip, 1, len - 2) False -> ip } } -pub fn percent_encode_ip(s: String) -> String { - s - |> string.replace("%", "%25") - |> string.replace(":", "%3A") - |> string.replace(" ", "%20") +fn log_missing_api_host(host: String) -> Nil { + wisp.log_warning( + string.concat([log_prefix, " missing api_host (", host, ")"]), + ) } -pub fn is_ascii_upper_alpha2(s: String) -> Bool { - case string.byte_size(s) == 2 { - False -> False - True -> - case string.to_graphemes(s) { - [a, b] -> is_uppercase_letter(a) && is_uppercase_letter(b) - _ -> False +fn log_rpc_status(api_host: String, status: Int, body: String) -> Nil { + wisp.log_warning( + string.concat([ + log_prefix, + " rpc returned status ", + int.to_string(status), + " from ", + host_display(api_host), + ": ", + response_snippet(body), + ]), + ) +} + +fn log_rpc_error(api_host: String, message: String) -> Nil { + wisp.log_warning( + string.concat([ + log_prefix, + " rpc request to ", + host_display(api_host), + " failed: ", + message, + ]), + ) +} + +fn host_display(api_host: String) -> String { + case string.contains(api_host, "://") { + True -> api_host + False -> "http://" <> api_host + } +} + +fn rpc_url(api_host: String) -> String { + let host = string.trim(api_host) + case host { + "" -> "" + _ -> { + let base = case string.contains(host, "://") { + True -> host + False -> "http://" <> host } + + let normalized = case string.ends_with(base, "/") { + True -> string.slice(base, 0, string.length(base) - 1) + False -> base + } + + normalized <> "/_rpc" + } } } -fn is_valid_country_code(s: String) -> Bool { - is_ascii_upper_alpha2(s) -} - -fn is_uppercase_letter(g: String) -> Bool { - case bit_array.from_string(g) { - <> -> c >= 65 && c <= 90 - _ -> False - } -} - -pub fn debug_info(req: wisp.Request, geoip_host: String) -> String { - let xff = - request.get_header(req, "x-forwarded-for") |> result.unwrap("(not set)") - let ip = extract_client_ip(req) - let host_display = case geoip_host { - "" -> "(not set)" - h -> h - } - - let url = case ip { - "" -> "(empty IP - no URL)" - _ -> "http://" <> host_display <> "/lookup?ip=" <> percent_encode_ip(ip) - } - - let response = case ip { - "" -> "(empty IP - no request)" - _ -> fetch_geoip_debug(url) - } - - json.object([ - #("x_forwarded_for_header", json.string(xff)), - #("extracted_ip", json.string(ip)), - #( - "stripped_brackets", - json.string(case ip { - "" -> "(empty)" - i -> strip_brackets(i) - }), - ), - #( - "percent_encoded", - json.string(case ip { - "" -> "(empty)" - i -> percent_encode_ip(i) - }), - ), - #("geoip_host", json.string(host_display)), - #("geoip_url", json.string(url)), - #("geoip_response", json.string(response)), - #("final_country_code", json.string(country_code(req, geoip_host))), - ]) - |> json.to_string -} - -fn fetch_geoip_debug(url: String) -> String { - let assert Ok(req) = request.to(url) - let req = request.prepend_header(req, "accept", "text/plain") - case httpc.send(req) { - Ok(resp) -> - "Status: " <> string.inspect(resp.status) <> ", Body: " <> resp.body - Error(_) -> "(request failed)" +fn response_snippet(body: String) -> String { + let len = string.length(body) + case len <= 256 { + True -> body + False -> string.slice(body, 0, 256) <> "..." } } diff --git a/fluxer_marketing/src/fluxer_marketing/router.gleam b/fluxer_marketing/src/fluxer_marketing/router.gleam index f3d99c35..6cd5b15d 100644 --- a/fluxer_marketing/src/fluxer_marketing/router.gleam +++ b/fluxer_marketing/src/fluxer_marketing/router.gleam @@ -16,7 +16,6 @@ //// along with Fluxer. If not, see . import fluxer_marketing/badge_proxy -import fluxer_marketing/geoip import fluxer_marketing/help_center import fluxer_marketing/locale import fluxer_marketing/pages/careers_page @@ -50,8 +49,6 @@ pub fn handle_request(req: Request, ctx: Context) -> Response { case wisp.path_segments(req) { [] -> home_page.render(req, ctx) ["_locale"] -> handle_locale_change(req, ctx) - ["_debug", "geoip"] -> handle_geoip_debug(req, ctx) - ["api", "badges", "product-hunt"] -> badge_proxy.product_hunt(ctx.badge_featured_cache) @@ -378,10 +375,3 @@ pub fn update_locale_in_path(path: String, new_locale: locale.Locale) -> String _ -> path } } - -fn handle_geoip_debug(req: Request, ctx: Context) -> Response { - let json_body = geoip.debug_info(req, ctx.geoip_host) - wisp.response(200) - |> wisp.set_header("content-type", "application/json; charset=utf-8") - |> wisp.string_body(json_body) -} diff --git a/fluxer_marketing/src/fluxer_marketing/web.gleam b/fluxer_marketing/src/fluxer_marketing/web.gleam index a46ad9d6..5738141b 100644 --- a/fluxer_marketing/src/fluxer_marketing/web.gleam +++ b/fluxer_marketing/src/fluxer_marketing/web.gleam @@ -52,7 +52,6 @@ pub type Context { base_path: String, platform: Platform, architecture: Architecture, - geoip_host: String, release_channel: String, visionary_slots: VisionarySlots, metrics_endpoint: Option(String), diff --git a/fluxer_marketing/test/geoip_test.gleam b/fluxer_marketing/test/geoip_test.gleam deleted file mode 100644 index e3c51b46..00000000 --- a/fluxer_marketing/test/geoip_test.gleam +++ /dev/null @@ -1,142 +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 . - -import fluxer_marketing/geoip -import gleam/list -import gleam/string -import gleeunit -import gleeunit/should - -pub fn main() { - gleeunit.main() -} - -fn gh(headers: List(#(String, String))) -> fn(String) -> Result(String, Nil) { - fn(name) { - let lname = string.lowercase(name) - - let found = - list.find(headers, fn(pair) { - let #(k, _) = pair - string.lowercase(k) == lname - }) - - case found { - Ok(#(_, v)) -> Ok(v) - Error(_) -> Error(Nil) - } - } -} - -fn gh_empty() -> fn(String) -> Result(String, Nil) { - fn(_) { Error(Nil) } -} - -fn http_ok(body: String) -> fn(String) -> Result(String, Nil) { - fn(_url) { Ok(body) } -} - -fn http_err() -> fn(String) -> Result(String, Nil) { - fn(_url) { Error(Nil) } -} - -pub fn no_host_defaults_to_us_test() { - let cc = geoip.country_code_core(gh_empty(), "", http_ok("SE")) - cc |> should.equal("US") -} - -pub fn missing_ip_defaults_to_us_test() { - let cc = geoip.country_code_core(gh_empty(), "geoip:8080", http_ok("SE")) - cc |> should.equal("US") -} - -pub fn invalid_ip_defaults_to_us_test() { - let cc = - geoip.country_code_core( - gh([#("x-forwarded-for", "not_an_ip")]), - "geoip:8080", - http_ok("SE"), - ) - cc |> should.equal("US") -} - -pub fn ipv4_success_uppercases_and_validates_test() { - let cc = - geoip.country_code_core( - gh([#("x-forwarded-for", "8.8.8.8")]), - "geoip:8080", - http_ok("se"), - ) - cc |> should.equal("SE") -} - -pub fn ipv6_bracketed_success_test() { - let cc = - geoip.country_code_core( - gh([#("x-forwarded-for", "[2001:db8::1]")]), - "geoip:8080", - http_ok("de"), - ) - cc |> should.equal("DE") -} - -pub fn multiple_xff_uses_first_token_test() { - let cc = - geoip.country_code_core( - gh([#("x-forwarded-for", "1.1.1.1, 8.8.8.8")]), - "geoip:8080", - http_ok("gb"), - ) - cc |> should.equal("GB") -} - -pub fn http_error_falls_back_test() { - let cc = - geoip.country_code_core( - gh([#("x-forwarded-for", "8.8.4.4")]), - "geoip:8080", - http_err(), - ) - cc |> should.equal("US") -} - -pub fn invalid_body_falls_back_test() { - let cc = - geoip.country_code_core( - gh([#("x-forwarded-for", "8.8.4.4")]), - "geoip:8080", - http_ok("USA"), - ) - cc |> should.equal("US") -} - -pub fn strip_brackets_helper_test() { - geoip.strip_brackets("[::1]") |> should.equal("::1") - geoip.strip_brackets("127.0.0.1") |> should.equal("127.0.0.1") -} - -pub fn percent_encode_ip_helper_test() { - geoip.percent_encode_ip("2001:db8::1") |> should.equal("2001%3Adb8%3A%3A1") - geoip.percent_encode_ip("100% legit") |> should.equal("100%25%20legit") -} - -pub fn is_ascii_upper_alpha2_helper_test() { - geoip.is_ascii_upper_alpha2("US") |> should.equal(True) - geoip.is_ascii_upper_alpha2("uS") |> should.equal(False) - geoip.is_ascii_upper_alpha2("USA") |> should.equal(False) - geoip.is_ascii_upper_alpha2("") |> should.equal(False) -} diff --git a/justfile b/justfile index 62f1b515..b8c522db 100644 --- a/justfile +++ b/justfile @@ -1,108 +1,64 @@ -# Fluxer Development Justfile -# Uses the dev CLI (Go) as a compose wrapper +env_file := "dev/.env" +compose_file := "dev/compose.yaml" +data_compose := "dev/compose.data.yaml" +network_name := "fluxer-shared" +compose_base := "docker compose --env-file " + env_file + " -f " + compose_file +livekit_template := "dev/templates/livekit.yaml" -# Dev CLI command -devctl := "go run ./dev" - -# --------------------------------------------------------------------------- -# Docker Compose: Service Management -# --------------------------------------------------------------------------- - -# Start all or selected services in the background -# Usage: -# just up # all services -# just up api gateway # specific services up *SERVICES: - {{devctl}} ensure-network - {{devctl}} up {{SERVICES}} + just ensure-network + {{compose_base}} up -d {{SERVICES}} -# Start all or selected services and watch for changes -# Usage: -# just watch -# just watch api gateway watch *SERVICES: - {{devctl}} ensure-network - docker compose --env-file dev/.env -f dev/compose.yaml watch {{SERVICES}} + just ensure-network + {{compose_base}} watch {{SERVICES}} -# Stop and remove containers (preserves volumes) -# Usage: -# just down down: - {{devctl}} down + {{compose_base}} down -# Stop and remove containers including volumes -# Usage: -# just nuke nuke: - {{devctl}} down --volumes + {{compose_base}} down -v -# Restart all or selected services -# Usage: -# just restart -# just restart api gateway restart *SERVICES: - {{devctl}} restart {{SERVICES}} + {{compose_base}} restart {{SERVICES}} -# Show logs for all or selected services -# Usage: -# just logs -# just logs api -# just logs api gateway logs *SERVICES: - {{devctl}} logs {{SERVICES}} + {{compose_base}} logs -f --tail 100 {{SERVICES}} -# List running containers -# Usage: -# just ps ps: - {{devctl}} ps + {{compose_base}} ps -# Open a shell in a service container -# Usage: -# just sh api -sh SERVICE: - {{devctl}} sh {{SERVICE}} +sh SERVICE shell="sh": + {{compose_base}} exec {{SERVICE}} {{shell}} -# Execute a command in a service container -# Usage: -# just exec api "env | sort" exec SERVICE CMD: - {{devctl}} exec {{SERVICE}} sh -c "{{CMD}}" + {{compose_base}} exec {{SERVICE}} sh -c "{{CMD}}" -# --------------------------------------------------------------------------- -# Configuration & Setup -# --------------------------------------------------------------------------- - -# Sync LiveKit configuration from environment variables -# Usage: -# just livekit-sync livekit-sync: - {{devctl}} livekit-sync + set -euo pipefail + if [ ! -f {{env_file}} ]; then + echo "{{env_file}} missing" + exit 1 + fi + node --env-file {{env_file}} scripts/just/livekit-sync.js --output dev/livekit.yaml -# Download GeoIP database -# Usage: -# just geoip-download -# just geoip-download TOKEN=xxx -geoip-download TOKEN='': - if [ "{{TOKEN}}" = "" ]; then {{devctl}} geoip-download; else {{devctl}} geoip-download --token {{TOKEN}}; fi - -# Ensure Docker network exists -# Usage: -# just ensure-network ensure-network: - {{devctl}} ensure-network + set -euo pipefail + docker network inspect {{network_name}} >/dev/null 2>&1 || docker network create {{network_name}} -# Bootstrap development environment -# Usage: -# just bootstrap bootstrap: just ensure-network just livekit-sync - just geoip-download -# --------------------------------------------------------------------------- -# Cassandra Migrations -# --------------------------------------------------------------------------- +setup: + set -euo pipefail + just ensure-network + if [ ! -f dev/.env ]; then + cp dev/.env.example dev/.env + fi + if [ ! -f dev/livekit.yaml ]; then + cp {{livekit_template}} dev/livekit.yaml + fi mig name: @cargo run --release --quiet --manifest-path scripts/cassandra-migrate/Cargo.toml -- create "{{name}}" @@ -116,38 +72,21 @@ mig-up host="localhost" user="cassandra" pass="cassandra": mig-status host="localhost" user="cassandra" pass="cassandra": @cargo run --release --quiet --manifest-path scripts/cassandra-migrate/Cargo.toml -- --host "{{host}}" --username "{{user}}" --password "{{pass}}" status -# --------------------------------------------------------------------------- -# Utilities -# --------------------------------------------------------------------------- - -# Run license enforcer lic: @cargo run --release --quiet --manifest-path scripts/license-enforcer/Cargo.toml -# Generate snowflake IDs snow count="1": @cargo run --release --quiet --manifest-path scripts/snowflake-generator/Cargo.toml -- --count {{count}} -# --------------------------------------------------------------------------- -# Integration Tests -# --------------------------------------------------------------------------- - -# Spin up the full integration stack, run the Go tests, then tear everything down integration-tests: set -euo pipefail trap 'docker compose -f tests/integration/compose.yaml down' EXIT docker compose -f tests/integration/compose.yaml up --build --abort-on-container-exit integration-tests -# --------------------------------------------------------------------------- -# Go Tooling & QA -# --------------------------------------------------------------------------- - -# Install pinned Go tooling (staticcheck, golangci-lint) with Go 1.25.5 go-tools-install: GOTOOLCHAIN=go1.25.5 go install honnef.co/go/tools/cmd/staticcheck@2025.1.1 GOTOOLCHAIN=go1.25.5 go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.61.0 -# Run formatting, tests, and linters for integration tests go-integration-check: gofmt -w tests/integration go test ./tests/integration/... diff --git a/scripts/cassandra-migrate/Dockerfile b/scripts/cassandra-migrate/Dockerfile index b936bc11..53911153 100644 --- a/scripts/cassandra-migrate/Dockerfile +++ b/scripts/cassandra-migrate/Dockerfile @@ -1,4 +1,4 @@ -FROM rustlang/rust:nightly as builder +FROM rustlang/rust:nightly AS builder WORKDIR /workspace diff --git a/scripts/just/livekit-sync.js b/scripts/just/livekit-sync.js new file mode 100755 index 00000000..91b506c4 --- /dev/null +++ b/scripts/just/livekit-sync.js @@ -0,0 +1,51 @@ +#!/usr/bin/env node +const fs = require('node:fs'); +const path = require('node:path'); + +const args = process.argv.slice(2); +const outputIdx = args.indexOf('--output'); +const outputArg = outputIdx >= 0 && args[outputIdx + 1] ? args[outputIdx + 1] : 'dev/livekit.yaml'; + +const voiceEnabled = (process.env.VOICE_ENABLED || '').trim().toLowerCase() === 'true'; +if (!voiceEnabled) { + process.exit(0); +} + +const apiKey = (process.env.LIVEKIT_API_KEY || '').trim(); +const apiSecret = (process.env.LIVEKIT_API_SECRET || '').trim(); +const webhookUrl = (process.env.LIVEKIT_WEBHOOK_URL || '').trim(); +if (!apiKey || !apiSecret || !webhookUrl) { + process.exit(0); +} + +const redisUrl = (process.env.REDIS_URL || '').trim(); +const redisAddr = redisUrl.replace(/^redis:\/\//, '') || 'redis:6379'; + +const yaml = `port: 7880 + +redis: + address: "${redisAddr}" + db: 0 + +keys: + "${apiKey}": "${apiSecret}" + +rtc: + tcp_port: 7881 + +webhook: + api_key: "${apiKey}" + urls: + - "${webhookUrl}" + +room: + auto_create: true + max_participants: 100 + empty_timeout: 300 + +development: true + +`; +const outputPath = path.resolve(outputArg); +fs.mkdirSync(path.dirname(outputPath), {recursive: true}); +fs.writeFileSync(outputPath, yaml, {encoding: 'utf-8', mode: 0o600});