mirror of
https://github.com/benbusby/farside.git
synced 2025-03-13 10:46:35 -04:00
Rewrite project, add daily update of services list
The project was rewritten from Elixir to Go, primarily because: - I don't write Elixir anymore and don't want to maintain a project in a language I no longer write - I already write Go for other projects, including my day job, so it's a safer bet for a project that I want to maintain long term - Go allows me to build portable executables that will make it easier for others to run farside on their own machines The Go version of Farsside also has a built in task to fetch the latest services{-full}.json file from the repo and ingest it, which makes running a farside server a lot simpler. It also automatically fetches the latest instance state from https://farside.link unless configured as a primary farside node, which will allow others to use farside without increasing traffic to all instances that are queried by farside (just to the farside node itself).
This commit is contained in:
parent
e0e395f3c8
commit
b5bad4defc
38
.github/workflows/elixir.yml
vendored
38
.github/workflows/elixir.yml
vendored
@ -1,38 +0,0 @@
|
||||
name: Elixir CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
pull_request:
|
||||
branches: [ main ]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
name: Build and test
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Set up Elixir
|
||||
uses: erlef/setup-beam@v1
|
||||
with:
|
||||
elixir-version: '1.12.3'
|
||||
otp-version: '24'
|
||||
|
||||
- name: Restore dependencies cache
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: deps
|
||||
key: ${{ runner.os }}-mix-${{ hashFiles('**/mix.lock') }}
|
||||
restore-keys: ${{ runner.os }}-mix-
|
||||
|
||||
- name: Install dependencies
|
||||
run: mix deps.get
|
||||
|
||||
- name: Initialize services
|
||||
run: FARSIDE_TEST=1 FARSIDE_SERVICES_JSON=services-full.json mix run -e Farside.Instances.sync
|
||||
|
||||
- name: Run tests
|
||||
run: FARSIDE_TEST=1 FARSIDE_SERVICES_JSON=services-full.json mix test --trace
|
19
.github/workflows/tests.yml
vendored
Normal file
19
.github/workflows/tests.yml
vendored
Normal file
@ -0,0 +1,19 @@
|
||||
on: [push, pull_request]
|
||||
name: Tests
|
||||
jobs:
|
||||
test:
|
||||
strategy:
|
||||
matrix:
|
||||
go-version: [1.21.x, 1.22.x, 1.23.x]
|
||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: ${{ matrix.go-version }}
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
- name: Test
|
||||
run: go test -v ./...
|
||||
|
20
.gitignore
vendored
20
.gitignore
vendored
@ -1,18 +1,2 @@
|
||||
/_build
|
||||
/cover
|
||||
/deps
|
||||
/doc
|
||||
/.fetch
|
||||
erl_crash.dump
|
||||
*.ez
|
||||
*.beam
|
||||
/config/*.secret.exs
|
||||
.elixir_ls/
|
||||
|
||||
# Ignore results from update script
|
||||
.update-result*
|
||||
|
||||
*.rdb
|
||||
.idea/
|
||||
*.iml
|
||||
*.cub
|
||||
badger-db
|
||||
farside
|
||||
|
40
README.md
40
README.md
@ -7,7 +7,7 @@
|
||||
|
||||
[](https://github.com/benbusby/farside/releases)
|
||||
[](http://opensource.org/licenses/MIT)
|
||||
[](https://github.com/benbusby/privacy-revolver/actions/workflows/elixir.yml)
|
||||
[](https://github.com/benbusby/farside/actions/workflows/tests.yml)
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
@ -24,7 +24,6 @@ Contents
|
||||
3. [How It Works](#how-it-works)
|
||||
4. [Cloudflare](#regarding-cloudflare)
|
||||
5. [Development](#development)
|
||||
1. [Compiling](#compiling)
|
||||
1. [Environment Variables](#environment-variables)
|
||||
|
||||
## About
|
||||
@ -187,30 +186,9 @@ that their mission to centralize the entire web behind their service ultimately
|
||||
goes against what Farside is trying to solve. Use at your own discretion.
|
||||
|
||||
## Development
|
||||
- Install [elixir](https://elixir-lang.org/install.html)
|
||||
- (on Debian systems) Install [erlang-dev](https://packages.debian.org/sid/erlang-dev)
|
||||
|
||||
To run Farside without compiling, you can perform the following steps:
|
||||
|
||||
- Install dependencies: `mix deps.get`
|
||||
- Initialize db contents: `FARSIDE_CRON=0 mix run -e Farside.Instances.sync`
|
||||
- Run Farside: `mix run --no-halt`
|
||||
- Uses localhost:4001
|
||||
|
||||
### Compiling
|
||||
|
||||
You can create a standalone Farside app using the steps below. In the example, the
|
||||
Farside executable is copied to `/usr/local/bin`, but can be moved to any preferred
|
||||
destination. Note that the executable still depends on the C runtime of the machine
|
||||
it is built on, so if you want a more portable binary, you should build Farside on a
|
||||
system with older library versions.
|
||||
|
||||
```
|
||||
MIX_ENV=cli && mix deps.get && mix release
|
||||
cp _build/cli/rel/bakeware/farside /usr/local/bin
|
||||
sudo chmod +x /usr/local/bin/farside
|
||||
farside
|
||||
```
|
||||
- Install [Go](https://go.dev/doc/install)
|
||||
- Compile with `go build`
|
||||
|
||||
### Environment Variables
|
||||
|
||||
@ -221,23 +199,23 @@ farside
|
||||
</tr>
|
||||
<tr>
|
||||
<td>FARSIDE_TEST</td>
|
||||
<td>If enabled, bypasses the instance availability check and adds all instances to the pool.</td>
|
||||
<td>If enabled, bypasses the instance availability check and adds all instances to the pool</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>FARSIDE_PORT</td>
|
||||
<td>The port to run Farside on (default: `4001`)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>FARSIDE_DATA_DIR</td>
|
||||
<td>The path to the directory to use for storing instance data (default: `/tmp`)</td>
|
||||
<td>FARSIDE_DB_DIR</td>
|
||||
<td>The path to the directory to use for storing instance data (default: `./`)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>FARSIDE_SERVICES_JSON</td>
|
||||
<td>The JSON file to use for selecting instances (default: `services.json`)</td>
|
||||
<td>FARSIDE_CF_ENABLED</td>
|
||||
<td>Set to 1 to enable redirecting to instances behind cloudflare</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>FARSIDE_CRON</td>
|
||||
<td>Set to 0 to deactivate the scheduled instance availability check (default on).</td>
|
||||
<td>Set to 0 to deactivate the periodic instance availability check</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
@ -1,19 +0,0 @@
|
||||
import Config
|
||||
|
||||
config :farside,
|
||||
update_file: ".update-results",
|
||||
service_prefix: "service-",
|
||||
fallback_suffix: "-fallback",
|
||||
previous_suffix: "-previous",
|
||||
index: "index.eex",
|
||||
route: "route.eex",
|
||||
headers: [
|
||||
{"User-Agent", "Mozilla/5.0 (compatible; Farside/0.1.0; +https://farside.link)"},
|
||||
{"Accept", "text/html"},
|
||||
{"Accept-Language", "en-US,en;q=0.5"},
|
||||
{"Accept-Encoding", "gzip, deflate, br"}
|
||||
],
|
||||
queries: [
|
||||
"weather",
|
||||
"time"
|
||||
]
|
@ -1,6 +0,0 @@
|
||||
import Config
|
||||
|
||||
config :farside,
|
||||
port: System.get_env("FARSIDE_PORT", "4001"),
|
||||
services_json: System.get_env("FARSIDE_SERVICES_JSON", "services.json"),
|
||||
data_dir: System.get_env("FARSIDE_DATA_DIR", File.cwd!)
|
139
db/cron.go
Normal file
139
db/cron.go
Normal file
@ -0,0 +1,139 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/benbusby/farside/services"
|
||||
"github.com/robfig/cron/v3"
|
||||
)
|
||||
|
||||
const defaultPrimary = "https://farside.link/state"
|
||||
const defaultCFPrimary = "https://cf.farside.link/state"
|
||||
|
||||
var LastUpdate time.Time
|
||||
|
||||
func InitCronTasks() {
|
||||
log.Println("Initializing cron tasks...")
|
||||
|
||||
cronDisabled := os.Getenv("FARSIDE_CRON")
|
||||
if len(cronDisabled) == 0 || cronDisabled == "1" {
|
||||
c := cron.New()
|
||||
c.AddFunc("@every 10m", queryServiceInstances)
|
||||
c.AddFunc("@daily", updateServiceList)
|
||||
c.Start()
|
||||
}
|
||||
|
||||
queryServiceInstances()
|
||||
}
|
||||
|
||||
func updateServiceList() {
|
||||
fileName := services.GetServicesFileName()
|
||||
_, _ = services.FetchServicesFile(fileName)
|
||||
services.InitializeServices()
|
||||
}
|
||||
|
||||
func queryServiceInstances() {
|
||||
log.Println("Starting instance queries...")
|
||||
|
||||
isPrimary := os.Getenv("FARSIDE_PRIMARY")
|
||||
if len(isPrimary) == 0 || isPrimary != "1" {
|
||||
remoteServices, err := fetchInstancesFromPrimary()
|
||||
if err != nil {
|
||||
log.Println("Unable to fetch instances from primary", err)
|
||||
}
|
||||
|
||||
for _, service := range remoteServices {
|
||||
SetInstances(service.Type, service.Instances)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
for _, service := range services.ServiceList {
|
||||
fmt.Printf("===== %s =====\n", service.Type)
|
||||
var instances []string
|
||||
for _, instance := range service.Instances {
|
||||
testURL := strings.ReplaceAll(
|
||||
service.TestURL,
|
||||
"<%=query%>",
|
||||
"current+weather")
|
||||
available := queryServiceInstance(
|
||||
instance,
|
||||
testURL,
|
||||
)
|
||||
|
||||
if available {
|
||||
instances = append(instances, instance)
|
||||
}
|
||||
}
|
||||
|
||||
SetInstances(service.Type, instances)
|
||||
}
|
||||
|
||||
LastUpdate = time.Now().UTC()
|
||||
}
|
||||
|
||||
func fetchInstancesFromPrimary() ([]services.Service, error) {
|
||||
primaryURL := defaultPrimary
|
||||
useCF := os.Getenv("FARSIDE_CF_ENABLED")
|
||||
if len(useCF) > 0 && useCF == "1" {
|
||||
primaryURL = defaultCFPrimary
|
||||
}
|
||||
|
||||
resp, err := http.Get(primaryURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
|
||||
bodyBytes, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var serviceList []services.Service
|
||||
err = json.Unmarshal(bodyBytes, &serviceList)
|
||||
return serviceList, err
|
||||
}
|
||||
|
||||
func queryServiceInstance(instance, testURL string) bool {
|
||||
testMode := os.Getenv("FARSIDE_TEST")
|
||||
if len(testMode) > 0 && testMode == "1" {
|
||||
return true
|
||||
}
|
||||
|
||||
ua := "Mozilla/5.0 (compatible; Farside/1.0.0; +https://farside.link)"
|
||||
url := instance + testURL
|
||||
|
||||
req, err := http.NewRequest(http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
fmt.Println(" [ERRO] Failed to create new http request!", err)
|
||||
return false
|
||||
}
|
||||
|
||||
req.Header.Set("User-Agent", ua)
|
||||
client := &http.Client{
|
||||
Timeout: 10 * time.Second,
|
||||
}
|
||||
resp, err := client.Do(req)
|
||||
|
||||
if err != nil {
|
||||
fmt.Println(" [ERRO] Error fetching instance:", err)
|
||||
return false
|
||||
} else if resp.StatusCode != http.StatusOK {
|
||||
fmt.Printf(" [WARN] Received non-200 status for %s\n", url)
|
||||
return false
|
||||
} else {
|
||||
fmt.Printf(" [INFO] Received 200 status for %s\n", url)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
150
db/db.go
Normal file
150
db/db.go
Normal file
@ -0,0 +1,150 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"log"
|
||||
"math/rand"
|
||||
"os"
|
||||
"slices"
|
||||
"time"
|
||||
|
||||
"github.com/benbusby/farside/services"
|
||||
"github.com/dgraph-io/badger/v4"
|
||||
)
|
||||
|
||||
var (
|
||||
badgerDB *badger.DB
|
||||
selectionMap map[string]string
|
||||
|
||||
cachedServiceList []services.Service
|
||||
cacheUpdated time.Time
|
||||
)
|
||||
|
||||
func InitializeDB() error {
|
||||
var err error
|
||||
|
||||
dbDir := os.Getenv("FARSIDE_DB_DIR")
|
||||
if len(dbDir) == 0 {
|
||||
dbDir = "./badger-db"
|
||||
}
|
||||
|
||||
badgerDB, err = badger.Open(badger.DefaultOptions(dbDir))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func SetInstances(service string, instances []string) error {
|
||||
instancesBytes, err := json.Marshal(instances)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = badgerDB.Update(func(txn *badger.Txn) error {
|
||||
err := txn.Set([]byte(service), instancesBytes)
|
||||
return err
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func GetInstance(service string) (string, error) {
|
||||
instances, err := GetAllInstances(service)
|
||||
if err != nil || len(instances) == 0 {
|
||||
if err != nil {
|
||||
log.Println("DB err:", err)
|
||||
}
|
||||
|
||||
link, ok := services.FallbackMap[service]
|
||||
if !ok {
|
||||
return "", errors.New("invalid service")
|
||||
}
|
||||
|
||||
return link, nil
|
||||
}
|
||||
|
||||
previous, ok := selectionMap[service]
|
||||
if ok && len(instances) > 2 {
|
||||
instances = slices.DeleteFunc(instances, func(i string) bool {
|
||||
return i == previous
|
||||
})
|
||||
}
|
||||
|
||||
index := rand.Intn(len(instances))
|
||||
value := instances[index]
|
||||
selectionMap[service] = value
|
||||
return value, nil
|
||||
}
|
||||
|
||||
func GetAllInstances(service string) ([]string, error) {
|
||||
var instances []string
|
||||
err := badgerDB.View(func(txn *badger.Txn) error {
|
||||
item, err := txn.Get([]byte(service))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = item.Value(func(val []byte) error {
|
||||
err := json.Unmarshal(val, &instances)
|
||||
return err
|
||||
})
|
||||
|
||||
return err
|
||||
})
|
||||
|
||||
return instances, err
|
||||
}
|
||||
|
||||
func GetServiceList() []services.Service {
|
||||
if cacheUpdated.Add(5 * time.Minute).After(time.Now().UTC()) {
|
||||
return cachedServiceList
|
||||
}
|
||||
|
||||
canCache := true
|
||||
|
||||
var serviceList []services.Service
|
||||
for _, service := range services.ServiceList {
|
||||
instances, err := GetAllInstances(service.Type)
|
||||
if err != nil {
|
||||
canCache = false
|
||||
instances = []string{service.Fallback}
|
||||
}
|
||||
|
||||
storedService := services.Service{
|
||||
Type: service.Type,
|
||||
Instances: instances,
|
||||
}
|
||||
|
||||
serviceList = append(serviceList, storedService)
|
||||
}
|
||||
|
||||
if canCache {
|
||||
cachedServiceList = serviceList
|
||||
cacheUpdated = time.Now().UTC()
|
||||
}
|
||||
|
||||
return serviceList
|
||||
}
|
||||
|
||||
func CloseDB() error {
|
||||
log.Println("Closing database...")
|
||||
err := badgerDB.Close()
|
||||
if err != nil {
|
||||
log.Println("Error closing database", err)
|
||||
return err
|
||||
}
|
||||
|
||||
log.Println("Database closed!")
|
||||
return nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
selectionMap = make(map[string]string)
|
||||
}
|
60
db/db_test.go
Normal file
60
db/db_test.go
Normal file
@ -0,0 +1,60 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"slices"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
err := InitializeDB()
|
||||
if err != nil {
|
||||
log.Fatalln("Failed to initialize database")
|
||||
}
|
||||
|
||||
exitCode := m.Run()
|
||||
|
||||
_ = CloseDB()
|
||||
os.Exit(exitCode)
|
||||
}
|
||||
|
||||
func TestDatabase(t *testing.T) {
|
||||
var (
|
||||
service = "test"
|
||||
siteA = "a.com"
|
||||
siteB = "b.com"
|
||||
siteC = "c.com"
|
||||
)
|
||||
instances := []string{siteA, siteB, siteC}
|
||||
err := SetInstances(service, instances)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to set instances: %v\n", err)
|
||||
}
|
||||
|
||||
dbInstances, err := GetAllInstances(service)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to retrieve instances: %v\n", err)
|
||||
}
|
||||
|
||||
for _, instance := range instances {
|
||||
idx := slices.Index(dbInstances, instance)
|
||||
if idx < 0 {
|
||||
t.Fatalf("Failed to find instance in list")
|
||||
}
|
||||
}
|
||||
|
||||
firstInstance, err := GetInstance(service)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to fetch single instance: %v\n", err)
|
||||
}
|
||||
|
||||
secondInstance, err := GetInstance(service)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to fetch single instance (second): %v\n", err)
|
||||
} else if firstInstance == secondInstance {
|
||||
t.Fatalf("Same instance was selected twice")
|
||||
}
|
||||
|
||||
_ = CloseDB()
|
||||
}
|
29
go.mod
Normal file
29
go.mod
Normal file
@ -0,0 +1,29 @@
|
||||
module github.com/benbusby/farside
|
||||
|
||||
go 1.23.4
|
||||
|
||||
require (
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/charmbracelet/lipgloss v0.10.0 // indirect
|
||||
github.com/dgraph-io/badger/v4 v4.5.0 // indirect
|
||||
github.com/dgraph-io/ristretto/v2 v2.0.0 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/go-logfmt/logfmt v0.6.0 // indirect
|
||||
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e // indirect
|
||||
github.com/google/flatbuffers v24.3.25+incompatible // indirect
|
||||
github.com/klauspost/compress v1.17.11 // indirect
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
||||
github.com/mattn/go-isatty v0.0.18 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.15 // indirect
|
||||
github.com/muesli/reflow v0.3.0 // indirect
|
||||
github.com/muesli/termenv v0.15.2 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/robfig/cron/v3 v3.0.0 // indirect
|
||||
go.opencensus.io v0.24.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect
|
||||
golang.org/x/net v0.31.0 // indirect
|
||||
golang.org/x/sys v0.27.0 // indirect
|
||||
google.golang.org/protobuf v1.33.0 // indirect
|
||||
)
|
141
go.sum
Normal file
141
go.sum
Normal file
@ -0,0 +1,141 @@
|
||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/charmbracelet/lipgloss v0.10.0 h1:KWeXFSexGcfahHX+54URiZGkBFazf70JNMtwg/AFW3s=
|
||||
github.com/charmbracelet/lipgloss v0.10.0/go.mod h1:Wig9DSfvANsxqkRsqj6x87irdy123SR4dOXlKa91ciE=
|
||||
github.com/charmbracelet/log v0.4.0 h1:G9bQAcx8rWA2T3pWvx7YtPTPwgqpk7D68BX21IRW8ZM=
|
||||
github.com/charmbracelet/log v0.4.0/go.mod h1:63bXt/djrizTec0l11H20t8FDSvA4CRZJ1KH22MdptM=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dgraph-io/badger/v4 v4.5.0 h1:TeJE3I1pIWLBjYhIYCA1+uxrjWEoJXImFBMEBVSm16g=
|
||||
github.com/dgraph-io/badger/v4 v4.5.0/go.mod h1:ysgYmIeG8dS/E8kwxT7xHyc7MkmwNYLRoYnFbr7387A=
|
||||
github.com/dgraph-io/ristretto/v2 v2.0.0 h1:l0yiSOtlJvc0otkqyMaDNysg8E9/F/TYZwMbxscNOAQ=
|
||||
github.com/dgraph-io/ristretto/v2 v2.0.0/go.mod h1:FVFokF2dRqXyPyeMnK1YDy8Fc6aTe0IKgbcd03CYeEk=
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4=
|
||||
github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e h1:1r7pUrabqp18hOBcwBwiTsbnFeTZHV9eER/QT5JVZxY=
|
||||
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
||||
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
||||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
|
||||
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/google/flatbuffers v24.3.25+incompatible h1:CX395cjN9Kke9mmalRoL3d81AtFUxJM+yDthflgJGkI=
|
||||
github.com/google/flatbuffers v24.3.25+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
|
||||
github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||
github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98=
|
||||
github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
|
||||
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
|
||||
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
|
||||
github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
|
||||
github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo=
|
||||
github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/robfig/cron/v3 v3.0.0 h1:kQ6Cb7aHOHTSzNVNEhmp8EcWKLb4CbiMW9h9VyIhO4E=
|
||||
github.com/robfig/cron/v3 v3.0.0/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
|
||||
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
|
||||
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo=
|
||||
golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s=
|
||||
golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
|
||||
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
||||
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
|
||||
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
|
||||
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
142
lib/farside.ex
142
lib/farside.ex
@ -1,142 +0,0 @@
|
||||
defmodule Farside do
|
||||
@service_prefix Application.compile_env!(:farside, :service_prefix)
|
||||
@fallback_suffix Application.compile_env!(:farside, :fallback_suffix)
|
||||
@previous_suffix Application.compile_env!(:farside, :previous_suffix)
|
||||
|
||||
# Define relation between available services and their parent service.
|
||||
# This enables Farside to redirect with links such as:
|
||||
# farside.link/https://www.youtube.com/watch?v=dQw4w9WgXcQ
|
||||
@youtube_regex ~r/youtu(.be|be.com)|invidious|piped/
|
||||
@twitter_regex ~r/twitter.com|x.com|nitter/
|
||||
@reddit_regex ~r/reddit.com|libreddit|redlib/
|
||||
@instagram_regex ~r/instagram.com|proxigram/
|
||||
@wikipedia_regex ~r/wikipedia.org|wikiless/
|
||||
@medium_regex ~r/medium.com|scribe/
|
||||
@odysee_regex ~r/odysee.com|librarian/
|
||||
@imgur_regex ~r/imgur.com|rimgo/
|
||||
@gtranslate_regex ~r/translate.google.com|lingva/
|
||||
@tiktok_regex ~r/tiktok.com|proxitok/
|
||||
@imdb_regex ~r/imdb.com|libremdb/
|
||||
@quora_regex ~r/quora.com|quetre/
|
||||
@gsearch_regex ~r/google.com\/search|whoogle/
|
||||
@fandom_regex ~r/fandom.com|breezewiki/
|
||||
@github_regex ~r/github.com|gothub/
|
||||
@stackoverflow_regex ~r/stackoverflow.com|anonymousoverflow/
|
||||
|
||||
@parent_services %{
|
||||
@youtube_regex => ["invidious", "piped"],
|
||||
@reddit_regex => ["libreddit", "redlib"],
|
||||
@instagram_regex => ["proxigram"],
|
||||
@twitter_regex => ["nitter"],
|
||||
@wikipedia_regex => ["wikiless"],
|
||||
@medium_regex => ["scribe"],
|
||||
@odysee_regex => ["librarian"],
|
||||
@imgur_regex => ["rimgo"],
|
||||
@gtranslate_regex => ["lingva"],
|
||||
@tiktok_regex => ["proxitok"],
|
||||
@imdb_regex => ["libremdb"],
|
||||
@quora_regex => ["quetre"],
|
||||
@gsearch_regex => ["whoogle"],
|
||||
@fandom_regex => ["breezewiki"],
|
||||
@github_regex => ["gothub"],
|
||||
@stackoverflow_regex => ["anonymousoverflow"]
|
||||
}
|
||||
|
||||
def get_services_map do
|
||||
service_list = CubDB.select(CubDB)
|
||||
|> Stream.map(fn {key, _value} -> key end)
|
||||
|> Stream.filter(fn key -> String.starts_with?(key, @service_prefix) end)
|
||||
|> Enum.to_list
|
||||
|
||||
# Match service name to list of available instances
|
||||
Enum.reduce(service_list, %{}, fn service, acc ->
|
||||
instance_list = CubDB.get(CubDB, service)
|
||||
|
||||
Map.put(
|
||||
acc,
|
||||
String.replace_prefix(
|
||||
service,
|
||||
@service_prefix,
|
||||
""
|
||||
),
|
||||
instance_list
|
||||
)
|
||||
end)
|
||||
end
|
||||
|
||||
def get_service(service) do
|
||||
# Check if service has an entry in the db, otherwise try to
|
||||
# match against available parent services
|
||||
service_name = cond do
|
||||
!check_service(service) ->
|
||||
Enum.find_value(
|
||||
@parent_services,
|
||||
fn {k, v} ->
|
||||
String.match?(service, k) && Enum.random(v)
|
||||
end)
|
||||
true ->
|
||||
service
|
||||
end
|
||||
|
||||
service_name
|
||||
end
|
||||
|
||||
def check_service(service) do
|
||||
# Checks to see if a specific service has instances available
|
||||
instances = CubDB.get(CubDB, "#{@service_prefix}#{service}")
|
||||
|
||||
instances != nil && Enum.count(instances) > 0
|
||||
end
|
||||
|
||||
def last_instance(service) do
|
||||
# Fetches the last selected instance for a particular service
|
||||
CubDB.get(CubDB, "#{service}#{@previous_suffix}")
|
||||
end
|
||||
|
||||
def pick_instance(service) do
|
||||
instances = CubDB.get(CubDB, "#{@service_prefix}#{service}")
|
||||
|
||||
# Either pick a random available instance,
|
||||
# or fall back to the default one
|
||||
instance =
|
||||
if instances != nil && Enum.count(instances) > 0 do
|
||||
if Enum.count(instances) == 1 do
|
||||
# If there's only one instance, just return that one...
|
||||
List.first(instances)
|
||||
else
|
||||
# ...otherwise pick a random one from the list, ensuring
|
||||
# that the same instance is never picked twice in a row.
|
||||
instance =
|
||||
Enum.filter(instances, &(&1 != last_instance(service)))
|
||||
|> Enum.random()
|
||||
|
||||
CubDB.put(CubDB, "#{service}#{@previous_suffix}", instance)
|
||||
|
||||
instance
|
||||
end
|
||||
else
|
||||
CubDB.get(CubDB, "#{service}#{@fallback_suffix}")
|
||||
end
|
||||
instance
|
||||
end
|
||||
|
||||
def amend_instance(instance, service, path) do
|
||||
cond do
|
||||
String.match?(service, @fandom_regex) ->
|
||||
# Fandom links require the subdomain to be preserved, otherwise the
|
||||
# requested path won't work.
|
||||
if String.contains?(service, ".fandom.com") do
|
||||
wiki = String.replace(service, ".fandom.com", "")
|
||||
"#{instance}/#{wiki}"
|
||||
else
|
||||
instance
|
||||
end
|
||||
true ->
|
||||
instance
|
||||
end
|
||||
end
|
||||
|
||||
def get_last_updated do
|
||||
CubDB.get(CubDB, "last_updated")
|
||||
end
|
||||
end
|
@ -1,29 +0,0 @@
|
||||
defmodule Farside.Application do
|
||||
@moduledoc false
|
||||
|
||||
use Application
|
||||
|
||||
@impl true
|
||||
def start(_type, _args) do
|
||||
farside_port = Application.fetch_env!(:farside, :port)
|
||||
data_dir = Application.fetch_env!(:farside, :data_dir)
|
||||
IO.puts "Running on http://localhost:#{farside_port}"
|
||||
|
||||
children = [
|
||||
Plug.Cowboy.child_spec(
|
||||
scheme: :http,
|
||||
plug: Farside.Router,
|
||||
options: [
|
||||
port: String.to_integer(farside_port)
|
||||
]
|
||||
),
|
||||
{PlugAttack.Storage.Ets, name: Farside.Throttle.Storage, clean_period: 60_000},
|
||||
{CubDB, [data_dir: data_dir, name: CubDB, auto_compact: true]},
|
||||
Farside.Scheduler,
|
||||
Farside.Server
|
||||
]
|
||||
|
||||
opts = [strategy: :one_for_one, name: Farside.Supervisor]
|
||||
Supervisor.start_link(children, opts)
|
||||
end
|
||||
end
|
@ -1,134 +0,0 @@
|
||||
defmodule Farside.Instances do
|
||||
@fallback_suffix Application.fetch_env!(:farside, :fallback_suffix)
|
||||
@update_file Application.fetch_env!(:farside, :update_file)
|
||||
@service_prefix Application.fetch_env!(:farside, :service_prefix)
|
||||
@headers Application.fetch_env!(:farside, :headers)
|
||||
@queries Application.fetch_env!(:farside, :queries)
|
||||
@debug_header "======== "
|
||||
@debug_spacer " "
|
||||
|
||||
# These instance uptimes are inspected as part of the nightly Farside build,
|
||||
# and should not be included in the constant periodic update.
|
||||
@skip_service_updates ["searxng", "nitter"]
|
||||
|
||||
def sync() do
|
||||
File.rename(@update_file, "#{@update_file}-prev")
|
||||
update()
|
||||
|
||||
# Add UTC time of last update
|
||||
CubDB.put(CubDB, "last_updated", Calendar.strftime(DateTime.utc_now(), "%c"))
|
||||
end
|
||||
|
||||
def request(url) do
|
||||
IO.puts("#{@debug_spacer}#{url}")
|
||||
|
||||
cond do
|
||||
System.get_env("FARSIDE_TEST") ->
|
||||
:good
|
||||
|
||||
true ->
|
||||
HTTPoison.get(url, @headers)
|
||||
|> then(&elem(&1, 1))
|
||||
|> Map.get(:status_code)
|
||||
|> case do
|
||||
n when n < 300 ->
|
||||
IO.puts("#{@debug_spacer}✓ [#{n}]")
|
||||
:good
|
||||
|
||||
n ->
|
||||
IO.puts("#{@debug_spacer}x [#{(n && n) || "error"}]")
|
||||
:bad
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def update() do
|
||||
services_json = Application.fetch_env!(:farside, :services_json)
|
||||
{:ok, file} = File.read(services_json)
|
||||
{:ok, json} = Jason.decode(file)
|
||||
|
||||
# Loop through all instances and check each for availability
|
||||
for service_json <- json do
|
||||
service_atom = for {key, val} <- service_json, into: %{} do
|
||||
{String.to_existing_atom(key), val}
|
||||
end
|
||||
|
||||
service = struct(%Service{}, service_atom)
|
||||
|
||||
IO.puts("#{@debug_header}#{service.type}")
|
||||
|
||||
result = cond do
|
||||
Enum.member?(@skip_service_updates, service.type) ->
|
||||
get_service_vals(service.instances)
|
||||
true ->
|
||||
Enum.filter(service.instances, fn instance_url ->
|
||||
test_url = get_test_val(instance_url)
|
||||
test_path = get_test_val(service.test_url)
|
||||
test_request_url = gen_validation_url(test_url, test_path)
|
||||
|
||||
service_url = get_service_val(instance_url)
|
||||
service_path = get_service_val(service.test_url)
|
||||
service_request_url = gen_validation_url(service_url, service_path)
|
||||
|
||||
cond do
|
||||
service_url != test_url ->
|
||||
service_up = request(service_request_url)
|
||||
test_up = request(test_request_url)
|
||||
|
||||
service_up == :good && test_up == :good
|
||||
true ->
|
||||
request(test_request_url) == :good
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
add_to_db(service, result)
|
||||
log_results(service.type, result)
|
||||
end
|
||||
end
|
||||
|
||||
def add_to_db(service, instances) do
|
||||
# Ensure only service URLs are inserted, not test URLs (separated by "|")
|
||||
instances = get_service_vals(instances)
|
||||
|
||||
# Remove previous list of instances
|
||||
CubDB.delete(CubDB, "#{@service_prefix}#{service.type}")
|
||||
|
||||
# Update with new list of available instances
|
||||
CubDB.put(CubDB, "#{@service_prefix}#{service.type}", instances)
|
||||
|
||||
# Set fallback to one of the available instances,
|
||||
# or the default instance if all are "down"
|
||||
if Enum.count(instances) > 0 do
|
||||
CubDB.put(CubDB, "#{service.type}#{@fallback_suffix}", Enum.random(instances))
|
||||
else
|
||||
CubDB.put(CubDB, "#{service.type}#{@fallback_suffix}", service.fallback)
|
||||
end
|
||||
end
|
||||
|
||||
def log_results(service_name, results) do
|
||||
{:ok, file} = File.open(@update_file, [:append, {:delayed_write, 100, 20}])
|
||||
IO.write(file, "#{service_name}: #{inspect(results)}\n")
|
||||
File.close(file)
|
||||
end
|
||||
|
||||
def gen_validation_url(url, path) do
|
||||
url <> EEx.eval_string(path, query: Enum.random(@queries))
|
||||
end
|
||||
|
||||
def get_service_vals(services) do
|
||||
Enum.map(services, fn x -> get_service_val(x) end)
|
||||
end
|
||||
|
||||
def get_service_val(service) do
|
||||
String.split(service, "|") |> List.first
|
||||
end
|
||||
|
||||
def get_test_vals(services) do
|
||||
Enum.map(services, fn x -> get_test_val(x) end)
|
||||
end
|
||||
|
||||
def get_test_val(service) do
|
||||
String.split(service, "|") |> List.last
|
||||
end
|
||||
end
|
@ -1,78 +0,0 @@
|
||||
defmodule Farside.Router do
|
||||
@index Application.fetch_env!(:farside, :index)
|
||||
@route Application.fetch_env!(:farside, :route)
|
||||
|
||||
use Plug.Router
|
||||
|
||||
plug(RemoteIp)
|
||||
plug(Farside.Throttle)
|
||||
plug(:match)
|
||||
plug(:dispatch)
|
||||
|
||||
def get_query_params(conn) do
|
||||
cond do
|
||||
String.length(conn.query_string) > 0 ->
|
||||
"?#{conn.query_string}"
|
||||
|
||||
true ->
|
||||
""
|
||||
end
|
||||
end
|
||||
|
||||
match "/" do
|
||||
resp =
|
||||
EEx.eval_file(
|
||||
@index,
|
||||
last_updated: Farside.get_last_updated(),
|
||||
services: Farside.get_services_map()
|
||||
)
|
||||
|
||||
put_resp_header(conn, "content-type", "text/html")
|
||||
|> send_resp(200, resp)
|
||||
end
|
||||
|
||||
match "/_/:service/*glob" do
|
||||
r_path = String.slice(conn.request_path, 2..-1)
|
||||
|
||||
resp =
|
||||
EEx.eval_file(
|
||||
@route,
|
||||
instance_url: "#{r_path}#{get_query_params(conn)}"
|
||||
)
|
||||
|
||||
send_resp(conn, 200, resp)
|
||||
end
|
||||
|
||||
match "/:service/*glob" do
|
||||
service_name = cond do
|
||||
service =~ "http" ->
|
||||
List.first(glob)
|
||||
true ->
|
||||
service
|
||||
end
|
||||
|
||||
path = cond do
|
||||
service_name != service ->
|
||||
Enum.join(Enum.slice(glob, 1..-1), "/")
|
||||
true ->
|
||||
Enum.join(glob, "/")
|
||||
end
|
||||
|
||||
cond do
|
||||
conn.assigns[:throttle] != nil ->
|
||||
send_resp(conn, :too_many_requests, "Too many requests - max request rate is 1 per second")
|
||||
true ->
|
||||
instance = Farside.get_service(service_name)
|
||||
|> Farside.pick_instance
|
||||
|> Farside.amend_instance(service_name, path)
|
||||
|
||||
# Redirect to the available instance
|
||||
conn
|
||||
|> Plug.Conn.resp(:found, "")
|
||||
|> Plug.Conn.put_resp_header(
|
||||
"location",
|
||||
"#{instance}/#{path}#{get_query_params(conn)}"
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
@ -1,3 +0,0 @@
|
||||
defmodule Farside.Scheduler do
|
||||
use Quantum, otp_app: :farside
|
||||
end
|
@ -1,25 +0,0 @@
|
||||
defmodule Farside.Server do
|
||||
use GenServer
|
||||
import Crontab.CronExpression
|
||||
|
||||
def init(init_arg) do
|
||||
{:ok, init_arg}
|
||||
end
|
||||
|
||||
def start_link(arg) do
|
||||
test = System.get_env("FARSIDE_TEST")
|
||||
cron = System.get_env("FARSIDE_CRON")
|
||||
|
||||
if test == "1" || cron == "0" do
|
||||
IO.puts("Skipping sync job setup...")
|
||||
else
|
||||
Farside.Scheduler.new_job()
|
||||
|> Quantum.Job.set_name(:sync)
|
||||
|> Quantum.Job.set_schedule(~e[*/5 * * * *])
|
||||
|> Quantum.Job.set_task(fn -> Farside.Instances.sync() end)
|
||||
|> Farside.Scheduler.add_job()
|
||||
end
|
||||
|
||||
GenServer.start_link(__MODULE__, arg)
|
||||
end
|
||||
end
|
@ -1,20 +0,0 @@
|
||||
defmodule Farside.Throttle do
|
||||
import Plug.Conn
|
||||
use PlugAttack
|
||||
|
||||
rule "throttle per ip", conn do
|
||||
# throttle to 1 request per second
|
||||
throttle(conn.remote_ip,
|
||||
period: 1_000,
|
||||
limit: 1,
|
||||
storage: {PlugAttack.Storage.Ets, Farside.Throttle.Storage}
|
||||
)
|
||||
end
|
||||
|
||||
def allow_action(conn, _data, _opts), do: conn
|
||||
|
||||
def block_action(conn, _data, _opts) do
|
||||
conn = assign(conn, :throttle, 1)
|
||||
conn
|
||||
end
|
||||
end
|
@ -1,6 +0,0 @@
|
||||
defmodule Service do
|
||||
defstruct type: nil,
|
||||
test_url: nil,
|
||||
fallback: nil,
|
||||
instances: []
|
||||
end
|
37
main.go
Normal file
37
main.go
Normal file
@ -0,0 +1,37 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
"github.com/benbusby/farside/db"
|
||||
"github.com/benbusby/farside/server"
|
||||
"github.com/benbusby/farside/services"
|
||||
)
|
||||
|
||||
func main() {
|
||||
err := db.InitializeDB()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
err = services.InitializeServices()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
db.InitCronTasks()
|
||||
|
||||
signalChan := make(chan os.Signal, 1)
|
||||
signal.Notify(signalChan, os.Interrupt, syscall.SIGTERM)
|
||||
|
||||
go func() {
|
||||
<-signalChan
|
||||
_ = db.CloseDB()
|
||||
os.Exit(0)
|
||||
}()
|
||||
|
||||
server.RunServer()
|
||||
}
|
74
mix.exs
74
mix.exs
@ -1,74 +0,0 @@
|
||||
defmodule Farside.MixProject do
|
||||
use Mix.Project
|
||||
|
||||
@source_url "https://github.com/benbusby/farside.git"
|
||||
@version "0.1.1"
|
||||
@app :farside
|
||||
|
||||
def project do
|
||||
[
|
||||
app: @app,
|
||||
version: @version,
|
||||
name: "farside",
|
||||
elixir: "~> 1.8",
|
||||
source_url: @source_url,
|
||||
start_permanent: Mix.env() == :prod || Mix.env() == :cli,
|
||||
deps: deps(),
|
||||
aliases: aliases(),
|
||||
description: description(),
|
||||
package: package(),
|
||||
releases: [{@app, release()}],
|
||||
preferred_cli_env: [release: :cli]
|
||||
]
|
||||
end
|
||||
|
||||
defp aliases do
|
||||
[]
|
||||
end
|
||||
|
||||
# Run "mix help compile.app" to learn about applications.
|
||||
def application do
|
||||
[
|
||||
extra_applications: [:logger],
|
||||
mod: {Farside.Application, []}
|
||||
]
|
||||
end
|
||||
|
||||
# Run "mix help deps" to learn about dependencies.
|
||||
defp deps do
|
||||
[
|
||||
{:httpoison, "~> 1.8"},
|
||||
{:jason, "~> 1.1"},
|
||||
{:plug_attack, "~> 0.4.2"},
|
||||
{:plug_cowboy, "~> 2.0"},
|
||||
{:quantum, "~> 3.0"},
|
||||
{:remote_ip, "~> 1.1"},
|
||||
{:cubdb, "~> 2.0.1"},
|
||||
{:bakeware, "~> 0.2.4"}
|
||||
]
|
||||
end
|
||||
|
||||
defp description() do
|
||||
"A redirecting service for FOSS alternative frontends."
|
||||
end
|
||||
|
||||
defp package() do
|
||||
[
|
||||
name: "farside",
|
||||
files: ["lib", "mix.exs", "README*"],
|
||||
maintainers: ["Ben Busby"],
|
||||
licenses: ["MIT"],
|
||||
links: %{"GitHub" => "https://github.com/benbusby/farside"}
|
||||
]
|
||||
end
|
||||
|
||||
defp release() do
|
||||
[
|
||||
overwrite: true,
|
||||
cookie: "#{@app}_cookie",
|
||||
quiet: true,
|
||||
steps: [:assemble, &Bakeware.assemble/1],
|
||||
strip_beams: Mix.env() == :cli
|
||||
]
|
||||
end
|
||||
end
|
31
mix.lock
31
mix.lock
@ -1,31 +0,0 @@
|
||||
%{
|
||||
"bakeware": {:hex, :bakeware, "0.2.4", "0aaf49b34f4bab2aa433f9ff1485d9401e421603160abd6d269c469fc7b65212", [:make, :mix], [{:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "7b97bcf6fbeee53bb32441d6c495bf478d26f9575633cfef6831e421e86ada6d"},
|
||||
"certifi": {:hex, :certifi, "2.9.0", "6f2a475689dd47f19fb74334859d460a2dc4e3252a3324bd2111b8f0429e7e21", [:rebar3], [], "hexpm", "266da46bdb06d6c6d35fde799bcb28d36d985d424ad7c08b5bb48f5b5cdd4641"},
|
||||
"combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [:mix], [], "hexpm", "1b1dbc1790073076580d0d1d64e42eae2366583e7aecd455d1215b0d16f2451b"},
|
||||
"cowboy": {:hex, :cowboy, "2.9.0", "865dd8b6607e14cf03282e10e934023a1bd8be6f6bacf921a7e2a96d800cd452", [:make, :rebar3], [{:cowlib, "2.11.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "2c729f934b4e1aa149aff882f57c6372c15399a20d54f65c8d67bef583021bde"},
|
||||
"cowboy_telemetry": {:hex, :cowboy_telemetry, "0.4.0", "f239f68b588efa7707abce16a84d0d2acf3a0f50571f8bb7f56a15865aae820c", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7d98bac1ee4565d31b62d59f8823dfd8356a169e7fcbb83831b8a5397404c9de"},
|
||||
"cowlib": {:hex, :cowlib, "2.11.0", "0b9ff9c346629256c42ebe1eeb769a83c6cb771a6ee5960bd110ab0b9b872063", [:make, :rebar3], [], "hexpm", "2b3e9da0b21c4565751a6d4901c20d1b4cc25cbb7fd50d91d2ab6dd287bc86a9"},
|
||||
"crontab": {:hex, :crontab, "1.1.11", "4028ced51b813a5061f85b689d4391ef0c27550c8ab09aaf139e4295c3d93ea4", [:mix], [{:ecto, "~> 1.0 or ~> 2.0 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}], "hexpm", "ecb045f9ac14a3e2990e54368f70cdb6e2f2abafc5bc329d6c31f0c74b653787"},
|
||||
"cubdb": {:hex, :cubdb, "2.0.1", "24cab8fb4128df704c52ed641f5ed70af352f7a3a80cebbb44c3bbadc3fd5f45", [:mix], [], "hexpm", "57cf25aebfc34f4580d9075da06882b4fe3e0739f5353d4dcc213e9cc1b10cdf"},
|
||||
"elixir_make": {:hex, :elixir_make, "0.7.6", "67716309dc5d43e16b5abbd00c01b8df6a0c2ab54a8f595468035a50189f9169", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}], "hexpm", "5a0569756b0f7873a77687800c164cca6dfc03a09418e6fcf853d78991f49940"},
|
||||
"gen_stage": {:hex, :gen_stage, "1.1.2", "b1656cd4ba431ed02c5656fe10cb5423820847113a07218da68eae5d6a260c23", [:mix], [], "hexpm", "9e39af23140f704e2b07a3e29d8f05fd21c2aaf4088ff43cb82be4b9e3148d02"},
|
||||
"hackney": {:hex, :hackney, "1.18.1", "f48bf88f521f2a229fc7bae88cf4f85adc9cd9bcf23b5dc8eb6a1788c662c4f6", [:rebar3], [{:certifi, "~>2.9.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~>6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~>1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.3.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~>1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "a4ecdaff44297e9b5894ae499e9a070ea1888c84afdd1fd9b7b2bc384950128e"},
|
||||
"httpoison": {:hex, :httpoison, "1.8.2", "9eb9c63ae289296a544842ef816a85d881d4a31f518a0fec089aaa744beae290", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "2bb350d26972e30c96e2ca74a1aaf8293d61d0742ff17f01e0279fef11599921"},
|
||||
"idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"},
|
||||
"jason": {:hex, :jason, "1.4.0", "e855647bc964a44e2f67df589ccf49105ae039d4179db7f6271dfd3843dc27e6", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "79a3791085b2a0f743ca04cec0f7be26443738779d09302e01318f97bdb82121"},
|
||||
"metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"},
|
||||
"mime": {:hex, :mime, "2.0.3", "3676436d3d1f7b81b5a2d2bd8405f412c677558c81b1c92be58c00562bb59095", [:mix], [], "hexpm", "27a30bf0db44d25eecba73755acf4068cbfe26a4372f9eb3e4ea3a45956bff6b"},
|
||||
"mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"},
|
||||
"parse_trans": {:hex, :parse_trans, "3.3.1", "16328ab840cc09919bd10dab29e431da3af9e9e7e7e6f0089dd5a2d2820011d8", [:rebar3], [], "hexpm", "07cd9577885f56362d414e8c4c4e6bdf10d43a8767abb92d24cbe8b24c54888b"},
|
||||
"plug": {:hex, :plug, "1.14.0", "ba4f558468f69cbd9f6b356d25443d0b796fbdc887e03fa89001384a9cac638f", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "bf020432c7d4feb7b3af16a0c2701455cbbbb95e5b6866132cb09eb0c29adc14"},
|
||||
"plug_attack": {:hex, :plug_attack, "0.4.3", "88e6c464d68b1491aa083a0347d59d58ba71a7e591a7f8e1b675e8c7792a0ba8", [:mix], [{:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "9ed6fb8a6f613a36040f2875130a21187126c5625092f24bc851f7f12a8cbdc1"},
|
||||
"plug_cowboy": {:hex, :plug_cowboy, "2.6.0", "d1cf12ff96a1ca4f52207c5271a6c351a4733f413803488d75b70ccf44aebec2", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "073cf20b753ce6682ed72905cd62a2d4bd9bad1bf9f7feb02a1b8e525bd94fa6"},
|
||||
"plug_crypto": {:hex, :plug_crypto, "1.2.3", "8f77d13aeb32bfd9e654cb68f0af517b371fb34c56c9f2b58fe3df1235c1251a", [:mix], [], "hexpm", "b5672099c6ad5c202c45f5a403f21a3411247f164e4a8fab056e5cd8a290f4a2"},
|
||||
"quantum": {:hex, :quantum, "3.5.0", "8d2c5ba68c55991e8975aca368e3ab844ba01f4b87c4185a7403280e2c99cf34", [:mix], [{:crontab, "~> 1.1", [hex: :crontab, repo: "hexpm", optional: false]}, {:gen_stage, "~> 0.14 or ~> 1.0", [hex: :gen_stage, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:telemetry_registry, "~> 0.2", [hex: :telemetry_registry, repo: "hexpm", optional: false]}], "hexpm", "cab737d1d9779f43cb1d701f46dd05ea58146fd96238d91c9e0da662c1982bb6"},
|
||||
"ranch": {:hex, :ranch, "1.8.0", "8c7a100a139fd57f17327b6413e4167ac559fbc04ca7448e9be9057311597a1d", [:make, :rebar3], [], "hexpm", "49fbcfd3682fab1f5d109351b61257676da1a2fdbe295904176d5e521a2ddfe5"},
|
||||
"remote_ip": {:hex, :remote_ip, "1.1.0", "cb308841595d15df3f9073b7c39243a1dd6ca56e5020295cb012c76fbec50f2d", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "616ffdf66aaad6a72fc546dabf42eed87e2a99e97b09cbd92b10cc180d02ed74"},
|
||||
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.7", "354c321cf377240c7b8716899e182ce4890c5938111a1296add3ec74cf1715df", [:make, :mix, :rebar3], [], "hexpm", "fe4c190e8f37401d30167c8c405eda19469f34577987c76dde613e838bbc67f8"},
|
||||
"telemetry": {:hex, :telemetry, "1.1.0", "a589817034a27eab11144ad24d5c0f9fab1f58173274b1e9bae7074af9cbee51", [:rebar3], [], "hexpm", "b727b2a1f75614774cff2d7565b64d0dfa5bd52ba517f16543e6fc7efcc0df48"},
|
||||
"telemetry_registry": {:hex, :telemetry_registry, "0.3.0", "6768f151ea53fc0fbca70dbff5b20a8d663ee4e0c0b2ae589590e08658e76f1e", [:mix, :rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "492e2adbc609f3e79ece7f29fec363a97a2c484ac78a83098535d6564781e917"},
|
||||
"unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"},
|
||||
}
|
10
route.eex
10
route.eex
@ -1,10 +0,0 @@
|
||||
<head>
|
||||
<title>Farside Redirect</title>
|
||||
<meta http-equiv="refresh" content="1; url=<%= instance_url %>">
|
||||
<script>
|
||||
history.pushState({page: 1}, "Farside Redirect");
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<span>Redirecting to <%= instance_url %>...</span>
|
||||
</body>
|
@ -1,3 +1,6 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<title>Farside</title>
|
||||
<style>
|
||||
@ -44,19 +47,20 @@
|
||||
<div id="child-div">
|
||||
<h1>Farside [<a href="https://sr.ht/~benbusby/farside">SourceHut</a>, <a href="https://github.com/benbusby/farside">GitHub</a>]</h1>
|
||||
<hr>
|
||||
<h3>Last synced <%= last_updated %> UTC</h2>
|
||||
<h3>Updated: {{ .LastUpdated }}</h2>
|
||||
<div>
|
||||
<ul>
|
||||
<%= for {service, instance_list} <- services do %>
|
||||
<li><a href="/<%= service %>"><%= service %></a></li>
|
||||
<ul>
|
||||
<%= for url <- instance_list do %>
|
||||
<li><a href="<%= url %>"><%= url %></a></li>
|
||||
<% end%>
|
||||
</ul>
|
||||
<% end %>
|
||||
</ul>
|
||||
{{ range $i, $service := .ServiceList }}
|
||||
<li><a href="/{{ $service.Type }}">{{ $service.Type }}</a></li>
|
||||
<ul>
|
||||
{{ range $j, $instance := $service.Instances }}
|
||||
<li><a href="{{ $instance }}">{{ $instance }}</li>
|
||||
{{ end }}
|
||||
</ul>
|
||||
{{ end }}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
10
server/route.html
Normal file
10
server/route.html
Normal file
@ -0,0 +1,10 @@
|
||||
<head>
|
||||
<title>Farside Redirect</title>
|
||||
<meta http-equiv="refresh" content="1; url={{ .InstanceURL }}">
|
||||
<script>
|
||||
history.pushState({page: 1}, "Farside Redirect");
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<span>Redirecting to {{ .InstanceURL }}...</span>
|
||||
</body>
|
138
server/server.go
Normal file
138
server/server.go
Normal file
@ -0,0 +1,138 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"encoding/json"
|
||||
"html/template"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/benbusby/farside/db"
|
||||
"github.com/benbusby/farside/services"
|
||||
)
|
||||
|
||||
//go:embed index.html
|
||||
var indexHTML string
|
||||
|
||||
//go:embed route.html
|
||||
var routeHTML string
|
||||
|
||||
type indexData struct {
|
||||
LastUpdated time.Time
|
||||
ServiceList []services.Service
|
||||
}
|
||||
|
||||
type routeData struct {
|
||||
InstanceURL string
|
||||
}
|
||||
|
||||
func home(w http.ResponseWriter, r *http.Request) {
|
||||
serviceList := db.GetServiceList()
|
||||
data := indexData{
|
||||
LastUpdated: db.LastUpdate,
|
||||
ServiceList: serviceList,
|
||||
}
|
||||
|
||||
tmpl, err := template.New("").Parse(indexHTML)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
http.Error(w, "Error parsing template", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
|
||||
err = tmpl.Execute(w, data)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
http.Error(w, "Error executing template", http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
func state(w http.ResponseWriter, r *http.Request) {
|
||||
storedServices := db.GetServiceList()
|
||||
jsonData, _ := json.Marshal(storedServices)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, _ = w.Write(jsonData)
|
||||
}
|
||||
|
||||
func baseRouting(w http.ResponseWriter, r *http.Request) {
|
||||
routing(w, r, false)
|
||||
}
|
||||
|
||||
func jsRouting(w http.ResponseWriter, r *http.Request) {
|
||||
r.URL.Path = strings.Replace(r.URL.Path, "/_", "", 1)
|
||||
routing(w, r, true)
|
||||
}
|
||||
|
||||
func routing(w http.ResponseWriter, r *http.Request, jsEnabled bool) {
|
||||
value := r.PathValue("routing")
|
||||
if len(value) == 0 {
|
||||
value = r.URL.Path
|
||||
}
|
||||
|
||||
url, _ := url.Parse(value)
|
||||
path := strings.TrimPrefix(url.Path, "/")
|
||||
segments := strings.Split(path, "/")
|
||||
|
||||
target, err := services.MatchRequest(segments[0])
|
||||
if err != nil {
|
||||
log.Printf("Error during match request: %v\n", err)
|
||||
http.Error(w, "No routing found for "+target, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
instance, err := db.GetInstance(target)
|
||||
if err != nil {
|
||||
log.Printf("Error fetching instance from db: %v\n", err)
|
||||
http.Error(
|
||||
w,
|
||||
"Error fetching instance for "+target,
|
||||
http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if len(segments) > 1 {
|
||||
targetPath := strings.Join(segments[1:], "/")
|
||||
instance = instance + "/" + targetPath
|
||||
}
|
||||
|
||||
w.Header().Set("Cache-Control", "no-store, no-cache, must-revalidate, max-age=0")
|
||||
w.Header().Set("Pragma", "no-cache")
|
||||
w.Header().Set("Expires", "0")
|
||||
|
||||
if jsEnabled {
|
||||
data := routeData{
|
||||
InstanceURL: instance,
|
||||
}
|
||||
tmpl, _ := template.New("").Parse(routeHTML)
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
_ = tmpl.Execute(w, data)
|
||||
} else {
|
||||
http.Redirect(w, r, instance, http.StatusFound)
|
||||
}
|
||||
}
|
||||
|
||||
func RunServer() {
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/{$}", home)
|
||||
mux.HandleFunc("/state/{$}", state)
|
||||
mux.HandleFunc("/{routing...}", baseRouting)
|
||||
mux.HandleFunc("/_/{routing...}", jsRouting)
|
||||
|
||||
port := os.Getenv("FARSIDE_PORT")
|
||||
if len(port) == 0 {
|
||||
port = "4001"
|
||||
}
|
||||
|
||||
log.Println("Starting server on http://localhost:" + port)
|
||||
|
||||
err := http.ListenAndServe(":"+port, mux)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
80
server/server_test.go
Normal file
80
server/server_test.go
Normal file
@ -0,0 +1,80 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/benbusby/farside/db"
|
||||
)
|
||||
|
||||
const breezewikiTestSite = "https://breezewikitest.com"
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
err := db.InitializeDB()
|
||||
if err != nil {
|
||||
log.Fatalln("Failed to initialize database", err)
|
||||
}
|
||||
|
||||
err = db.SetInstances("breezewiki", []string{breezewikiTestSite})
|
||||
if err != nil {
|
||||
log.Fatalln("Failed to set instances in db")
|
||||
}
|
||||
|
||||
exitCode := m.Run()
|
||||
|
||||
_ = db.CloseDB()
|
||||
os.Exit(exitCode)
|
||||
}
|
||||
|
||||
func TestBaseRouting(t *testing.T) {
|
||||
req := httptest.NewRequest(http.MethodGet, "/fandom.com", nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
baseRouting(w, req)
|
||||
|
||||
res := w.Result()
|
||||
defer res.Body.Close()
|
||||
|
||||
if res.StatusCode != http.StatusFound {
|
||||
t.Fatalf("Incorrect resp code (%d) in base routing", res.StatusCode)
|
||||
}
|
||||
|
||||
expectedHost, _ := url.Parse(breezewikiTestSite)
|
||||
redirect, err := res.Location()
|
||||
if err != nil {
|
||||
t.Fatalf("Error retrieving direct from request: %v\n", err)
|
||||
} else if redirect.Host != expectedHost.Host {
|
||||
t.Fatalf("Incorrect redirect site -- expected: %s, actual: %s\n",
|
||||
expectedHost.Host,
|
||||
redirect.Host)
|
||||
}
|
||||
}
|
||||
|
||||
func TestJSRouting(t *testing.T) {
|
||||
req := httptest.NewRequest(http.MethodGet, "/_/fandom.com", nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
jsRouting(w, req)
|
||||
|
||||
res := w.Result()
|
||||
defer res.Body.Close()
|
||||
|
||||
if res.StatusCode != http.StatusOK {
|
||||
t.Fatalf("Incorrect resp code (%d) in base routing", res.StatusCode)
|
||||
}
|
||||
|
||||
data, err := io.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
t.Fatalf("Error reading response body: %v", err)
|
||||
}
|
||||
|
||||
if !strings.Contains(string(data), breezewikiTestSite) {
|
||||
t.Fatalf("%s not found in response body (%s)", breezewikiTestSite, string(data))
|
||||
}
|
||||
}
|
110
services/mappings.go
Normal file
110
services/mappings.go
Normal file
@ -0,0 +1,110 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"math/rand"
|
||||
"regexp"
|
||||
)
|
||||
|
||||
type RegexMapping struct {
|
||||
Pattern *regexp.Regexp
|
||||
Targets []string
|
||||
}
|
||||
|
||||
var regexMap = []RegexMapping{
|
||||
{
|
||||
// YouTube
|
||||
Pattern: regexp.MustCompile(`youtu(\.be|be\.com)|invidious|piped`),
|
||||
Targets: []string{"piped", "invidious"},
|
||||
},
|
||||
{
|
||||
// Twitter / X
|
||||
Pattern: regexp.MustCompile(`twitter\.com|x\.com|nitter`),
|
||||
Targets: []string{"nitter"},
|
||||
},
|
||||
{
|
||||
// Reddit
|
||||
Pattern: regexp.MustCompile(`reddit\.com|libreddit|redlib`),
|
||||
Targets: []string{"libreddit", "redlib"},
|
||||
},
|
||||
{
|
||||
// Google Search
|
||||
Pattern: regexp.MustCompile(`google\.com|whoogle|searx|searxng`),
|
||||
Targets: []string{"whoogle", "searx", "searxng"},
|
||||
},
|
||||
{
|
||||
// Instagram
|
||||
Pattern: regexp.MustCompile(`instagram\.com|proxigram`),
|
||||
Targets: []string{"proxigram"},
|
||||
},
|
||||
{
|
||||
// Wikipedia
|
||||
Pattern: regexp.MustCompile(`wikipedia\.org|wikiless`),
|
||||
Targets: []string{"wikiless"},
|
||||
},
|
||||
{
|
||||
// Medium
|
||||
Pattern: regexp.MustCompile(`medium\.com|scribe`),
|
||||
Targets: []string{"scribe"},
|
||||
},
|
||||
{
|
||||
// Odysee
|
||||
Pattern: regexp.MustCompile(`odysee\.com|librarian`),
|
||||
Targets: []string{"librarian"},
|
||||
},
|
||||
{
|
||||
// Imgur
|
||||
Pattern: regexp.MustCompile(`imgur\.com|rimgo`),
|
||||
Targets: []string{"rimgo"},
|
||||
},
|
||||
{
|
||||
// Google Translate
|
||||
Pattern: regexp.MustCompile(`translate\.google\.com|lingva`),
|
||||
Targets: []string{"lingva"},
|
||||
},
|
||||
{
|
||||
// TikTok
|
||||
Pattern: regexp.MustCompile(`tiktok\.com|proxitok`),
|
||||
Targets: []string{"proxitok"},
|
||||
},
|
||||
{
|
||||
// Fandom
|
||||
Pattern: regexp.MustCompile(`fandom\.com|breezewiki`),
|
||||
Targets: []string{"breezewiki"},
|
||||
},
|
||||
{
|
||||
// IMDB
|
||||
Pattern: regexp.MustCompile(`imdb\.com|libremdb`),
|
||||
Targets: []string{"libremdb"},
|
||||
},
|
||||
{
|
||||
// Quora
|
||||
Pattern: regexp.MustCompile(`quora\.com|quetre`),
|
||||
Targets: []string{"quetre"},
|
||||
},
|
||||
{
|
||||
// GitHub
|
||||
Pattern: regexp.MustCompile(`github\.com|gothub`),
|
||||
Targets: []string{"gothub"},
|
||||
},
|
||||
{
|
||||
// StackOverflow
|
||||
Pattern: regexp.MustCompile(`stackoverflow\.com|anonymousoverflow`),
|
||||
Targets: []string{"anonymousoverflow"},
|
||||
},
|
||||
}
|
||||
|
||||
func MatchRequest(service string) (string, error) {
|
||||
for _, mapping := range regexMap {
|
||||
hasMatch := mapping.Pattern.MatchString(service)
|
||||
if !hasMatch {
|
||||
continue
|
||||
}
|
||||
|
||||
index := rand.Intn(len(mapping.Targets))
|
||||
value := mapping.Targets[index]
|
||||
return value, nil
|
||||
}
|
||||
|
||||
return "", errors.New("no match found")
|
||||
}
|
93
services/services.go
Normal file
93
services/services.go
Normal file
@ -0,0 +1,93 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
)
|
||||
|
||||
var (
|
||||
ServiceList []Service
|
||||
FallbackMap map[string]string
|
||||
)
|
||||
|
||||
const (
|
||||
baseRepoLink = "https://git.sr.ht/~benbusby/farside/blob/main/"
|
||||
|
||||
noCFServicesJSON = "services.json"
|
||||
fullServicesJSON = "services-full.json"
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
Type string `json:"type"`
|
||||
TestURL string `json:"test_url,omitempty"`
|
||||
Fallback string `json:"fallback,omimtempty"`
|
||||
Instances []string `json:"instances"`
|
||||
}
|
||||
|
||||
func ingestServicesList(servicesBytes []byte) error {
|
||||
err := json.Unmarshal(servicesBytes, &ServiceList)
|
||||
return err
|
||||
}
|
||||
|
||||
func GetServicesFileName() string {
|
||||
cloudflareEnabled := false
|
||||
|
||||
cfEnabledVar := os.Getenv("FARSIDE_CF_ENABLED")
|
||||
if len(cfEnabledVar) > 0 && cfEnabledVar == "1" {
|
||||
cloudflareEnabled = true
|
||||
}
|
||||
|
||||
serviceJSON := noCFServicesJSON
|
||||
if cloudflareEnabled {
|
||||
serviceJSON = fullServicesJSON
|
||||
}
|
||||
|
||||
return serviceJSON
|
||||
}
|
||||
|
||||
|
||||
func FetchServicesFile(serviceJSON string) ([]byte, error) {
|
||||
resp, err := http.Get(baseRepoLink + serviceJSON)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
|
||||
bodyBytes, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = os.WriteFile(serviceJSON, bodyBytes, 0666)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return bodyBytes, nil
|
||||
}
|
||||
|
||||
func InitializeServices() error {
|
||||
serviceJSON := GetServicesFileName()
|
||||
fileBytes, err := os.ReadFile(serviceJSON)
|
||||
if err != nil {
|
||||
fileBytes, err = FetchServicesFile(serviceJSON)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
err = ingestServicesList(fileBytes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
FallbackMap = make(map[string]string)
|
||||
for _, serviceElement := range ServiceList {
|
||||
FallbackMap[serviceElement.Type] = serviceElement.Fallback
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
@ -1,93 +0,0 @@
|
||||
defmodule FarsideTest do
|
||||
|
||||
use ExUnit.Case
|
||||
use Plug.Test
|
||||
|
||||
alias Farside.Router
|
||||
|
||||
@opts Router.init([])
|
||||
|
||||
def test_conn(path) do
|
||||
:timer.sleep(1000)
|
||||
|
||||
:get
|
||||
|> conn(path, "")
|
||||
|> Router.call(@opts)
|
||||
end
|
||||
|
||||
test "throttle" do
|
||||
first_conn =
|
||||
:get
|
||||
|> conn("/", "")
|
||||
|> Router.call(@opts)
|
||||
|
||||
first_redirect = elem(List.last(first_conn.resp_headers), 1)
|
||||
|
||||
throttled_conn =
|
||||
:get
|
||||
|> conn("/", "")
|
||||
|> Router.call(@opts)
|
||||
|
||||
throttled_redirect = elem(List.last(first_conn.resp_headers), 1)
|
||||
|
||||
assert throttled_conn.state == :sent
|
||||
assert throttled_redirect == first_redirect
|
||||
end
|
||||
|
||||
test "/" do
|
||||
conn = test_conn("/")
|
||||
assert conn.state == :sent
|
||||
assert conn.status == 200
|
||||
end
|
||||
|
||||
test "/:service" do
|
||||
services_json = Application.fetch_env!(:farside, :services_json)
|
||||
{:ok, file} = File.read(services_json)
|
||||
{:ok, service_list} = Jason.decode(file)
|
||||
|
||||
service_names =
|
||||
Enum.map(
|
||||
service_list,
|
||||
fn service -> service["type"] end
|
||||
)
|
||||
|
||||
IO.puts("")
|
||||
|
||||
service_names |>
|
||||
Enum.filter(fn service_name -> service_name != "nitter" end) |>
|
||||
Enum.map(fn service_name ->
|
||||
conn = test_conn("/#{service_name}")
|
||||
first_redirect = elem(List.last(conn.resp_headers), 1)
|
||||
|
||||
IO.puts(" /#{service_name} (#1) -- #{first_redirect}")
|
||||
assert conn.state == :set
|
||||
assert conn.status == 302
|
||||
|
||||
conn = test_conn("/#{service_name}")
|
||||
second_redirect = elem(List.last(conn.resp_headers), 1)
|
||||
|
||||
IO.puts(" /#{service_name} (#2) -- #{second_redirect}")
|
||||
assert conn.state == :set
|
||||
assert conn.status == 302
|
||||
assert first_redirect != second_redirect
|
||||
end)
|
||||
end
|
||||
|
||||
test "/https://..." do
|
||||
parent_service = "https://www.youtube.com"
|
||||
parent_path = "watch?v=dQw4w9WgXcQ"
|
||||
conn = test_conn("/#{parent_service}/#{parent_path}")
|
||||
|
||||
redirect = elem(List.last(conn.resp_headers), 1)
|
||||
|
||||
IO.puts("")
|
||||
IO.puts(" /#{parent_service}/#{parent_path}")
|
||||
IO.puts(" redirected to")
|
||||
IO.puts(" #{redirect}")
|
||||
|
||||
assert conn.state == :set
|
||||
assert conn.status == 302
|
||||
assert redirect =~ parent_path
|
||||
assert !(redirect =~ parent_service)
|
||||
end
|
||||
end
|
@ -1 +0,0 @@
|
||||
ExUnit.start()
|
Loading…
x
Reference in New Issue
Block a user