diff --git a/.gitignore b/.gitignore index 1f35f8d..27f0708 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ db.sqlite .env .lapce/ */tmp.* +<<<<<<< HEAD /frontend/ @@ -32,3 +33,8 @@ db.sqlite # broccoli-debug /frontend-src/DEBUG/ +======= + +config.toml +.air.toml +>>>>>>> 0ec0ad42bca3a05b05256862904285bb74e314cb diff --git a/Containerfile b/Containerfile new file mode 100644 index 0000000..554588f --- /dev/null +++ b/Containerfile @@ -0,0 +1,26 @@ +FROM docker.io/node:22 AS build-ember +# TODO: Implement ember frontend build +FROM docker.io/golang:1.23 AS build-go +# TODO: Implement golang build +FROM gcr.io/distroless/static-debian12:latest as release +# TODO: Implement final release container + +# OCI labels, following https://github.com/opencontainers/image-spec/blob/main/annotations.md + +# Make this one dynamic +# LABEL org.opencontainers.image.created = rfc timestamp, according to https://datatracker.ietf.org/doc/html/rfc3339#section-5.6 +LABEL org.opencontainers.image.authors="Melody Becker/m*" +# LABEL org.opencontainers.image.url="Main linstrom page url here" +# LABEL org.opencontainers.image.documentation="Url to main linstrom documentation here" +LABEL org.opencontainers.image.source="https://gitlab.com/mstarongitlab/linstrom" +# Make this one dynamic from build tag +LABEL org.opencontainers.image.version="0.0.1" +# LABEL org.opencontainers.image.revision="idk what" +# LABEL org.opencontainers.image.vendor="None/m*" +LABEL org.opencontainers.image.licenses="EUPL-1.2" +# LABEL org.opencontainers.image.ref.name="uhh, no idea either" +LABEL org.opencontainers.image.title="Linstrom" +LABEL org.opencontainers.image.description="A social media server acting as part of the fediverse" +# Make this automatic since latest distroless image gets update constantly +# LABEL org.opencontainers.image.base.digest="digest/hash of base image" +LABEL org.opencontainers.image.base.name="gcr.io/distroless/static-debian12:latest" diff --git a/ap/getRemoteUser.go b/ap/getRemoteUser.go new file mode 100644 index 0000000..aabe689 --- /dev/null +++ b/ap/getRemoteUser.go @@ -0,0 +1,85 @@ +package ap + +import ( + "errors" + "fmt" + "io" + "net/http" + "time" + + "gitlab.com/mstarongitlab/goap" +) + +var ErrNoApUrl = errors.New("no Activitypub url in webfinger") + +func GetRemoteUser(fullHandle string) (goap.BaseApChain, error) { + webfinger, err := GetAccountWebfinger(fullHandle) + if err != nil { + return nil, err + } + + apUrl := "" + for _, link := range webfinger.Links { + if link.Relation == "self" { + apUrl = *link.Href + } + } + if apUrl == "" { + return nil, ErrNoApUrl + } + apRequest, err := http.NewRequest("GET", apUrl, nil) + if err != nil { + return nil, err + } + apRequest.Header.Add("Accept", "application/activity+json,application/ld+json,application/json") + client := http.Client{Timeout: time.Second * 30} + res, err := client.Do(apRequest) + if err != nil { + return nil, err + } + if res.StatusCode != 200 { + return nil, fmt.Errorf("bad status code: %d", res.StatusCode) + } + body, err := io.ReadAll(res.Body) + if err != nil { + return nil, err + } + apObject, _ := goap.Unmarshal(body, nil, nil) + // Check if Id exists + if _, ok := goap.FindAttribute[*goap.UDIdData](apObject); !ok { + return nil, fmt.Errorf("missing attribute for account: Id") + } + // Check that it has the correct object type for an account + if objTypePtr, ok := goap.FindAttribute[*goap.UDTypeData](apObject); !ok { + return nil, fmt.Errorf("missing attribute for account: Type") + } else if objType := *objTypePtr; objType.Type != goap.KEY_ACTIVITYSTREAMS_ACTOR { + return nil, fmt.Errorf("wrong ap object type: %s", objType.Type) + } + // And finally check for inbox + if _, ok := goap.FindAttribute[*goap.W3InboxData](apObject); !ok { + return nil, fmt.Errorf("missing attribute for account: Inbox") + } + return apObject, nil +} + +func GetRemoteObject(target string) (goap.BaseApChain, error) { + apRequest, err := http.NewRequest("GET", target, nil) + if err != nil { + return nil, err + } + apRequest.Header.Add("Accept", "application/activity+json,application/ld+json,application/json") + client := http.Client{Timeout: time.Second * 30} + res, err := client.Do(apRequest) + if err != nil { + return nil, err + } + if res.StatusCode != 200 { + return nil, fmt.Errorf("bad status code: %d", res.StatusCode) + } + body, err := io.ReadAll(res.Body) + if err != nil { + return nil, err + } + apObject, _ := goap.Unmarshal(body, nil, nil) + return apObject, nil +} diff --git a/ap/util.go b/ap/util.go new file mode 100644 index 0000000..b24a2a1 --- /dev/null +++ b/ap/util.go @@ -0,0 +1,19 @@ +package ap + +import "strings" + +type InvalidFullHandleError struct { + Raw string +} + +func (i InvalidFullHandleError) Error() string { + return "Invalid full handle" +} + +func SplitFullHandle(full string) (string, string, error) { + splits := strings.Split(strings.TrimPrefix(full, "@"), "@") + if len(splits) != 2 { + return "", "", InvalidFullHandleError{} + } + return splits[0], splits[1], nil +} diff --git a/ap/webfinger.go b/ap/webfinger.go new file mode 100644 index 0000000..4d15c02 --- /dev/null +++ b/ap/webfinger.go @@ -0,0 +1,87 @@ +package ap + +import ( + "encoding/json" + "errors" + "io" + "net/http" + "time" +) + +// Data returned from a webfinger response (and also sent when asked for an account via webfinger) +type WebfingerData struct { + // What this webfinger data refers to. Accounts are usually `acct:username@host` + Subject string `json:"subject"` + // List of links related to the account + Links []struct { + // What type of link this is + // - `self` refers to the activitypub object and has Type and Href set + // - `http://webfinger.net/rel/profile-page` refers to the public webpage of the account and has Type and Href set + // - `http://ostatus.org/schema/1.0/subscribe` provides a template for subscribing/following the account. Has Template set + // Template will contain a `{uri}` part with which to replace idk yet + Relation string `json:"rel"` + // The content type of the url + Type *string `json:"type"` + // The url + Href *string `json:"href"` + // Template to use for something + Template *string `json:"template"` + } `json:"links"` +} + +var ErrAccountNotFound = errors.New("account not found") +var ErrRemoteServerFailed = errors.New("remote server errored out") + +// Get the webfinger data for a given full account handle (like @bob@example.com or bob@example.com) +func GetAccountWebfinger(fullHandle string) (*WebfingerData, error) { + // First get the handle and server domain from the full handle (and ensure it's a correctly formatted one) + handle, server, err := SplitFullHandle(fullHandle) + if err != nil { + return nil, err + } + webfingerRequest, err := http.NewRequest( + // Webfinger requests are GET + "GET", + // The webfinger url is located at /.well-known/webfinger + // The url parameter `resource` tells the remote server what data we want, should always be an account + // The prefix `acct:` (where full-handle is in the form of bob@example.com) + // tells the remote server that we want the account data of the given account handle + "https://"+server+"/.well-known/webfinger?resource=acct:"+handle+"@"+server, + // No request body since it's a get request and we only need the url parameter + nil, + ) + if err != nil { + return nil, err + } + // Make a http client with a timeout limit of 30 seconds + client := http.Client{Timeout: time.Second * 30} + // Then send the request + result, err := client.Do(webfingerRequest) + if err != nil { + return nil, err + } + + // Code 404 indicates that the account doesn't exist + if result.StatusCode == 404 { + return nil, ErrAccountNotFound + } else if result.StatusCode != 200 { + // And anything other than 200 or 404 is an error for now + // TODO: Add information gathering here to figure out what the remote server's problem is + return nil, ErrRemoteServerFailed + } + + // Get the body data from the response + data, err := io.ReadAll(result.Body) + if err != nil { + return nil, err + } + + // Then try and parse it into webfinger data + webfinger := WebfingerData{} + err = json.Unmarshal(data, &webfinger) + if err != nil { + return nil, err + } + + return &webfinger, nil +} diff --git a/config/config.go b/config/config.go index 1ab3fec..9cb68b5 100644 --- a/config/config.go +++ b/config/config.go @@ -1,11 +1,12 @@ package config import ( - "errors" "fmt" "os" "github.com/BurntSushi/toml" + "github.com/rs/zerolog/log" + "gitlab.com/mstarongitlab/goutils/other" ) type ConfigSSL struct { @@ -19,25 +20,44 @@ type ConfigSSL struct { } type ConfigGeneral struct { - Domain string `toml:"domain"` // The domain this server operates under - FullDomain string `toml:"full_domain"` // The full url the server operates under (without port), eg "http://localhost", the public port will be appended as needed - PublicPort int `toml:"public_port"` // The public facing port, usually either 80 or 443 - PrivatePort int `toml:"private_port"` // The port the server should launch at - // Explanation: - // The public port is the port to connect to from outside the server to access it. - // The private port is where the server will open for itself on launch - // Important for reverse proxies like nginx or traeffik - // Where you the public port would be either 80 (http) or 443 (https), but the private one could be 3000 for example + Protocol string `toml:"protocol"` // The protocol with which to access the server publicly (http/https) + // The subdomain under which the server lives (example: "linstrom" if the full domain is linstrom.example.com) + Subdomain *string `toml:"subdomain"` + // The root domain under which the server lives (example: "example.com" if the full domain is linstrom.example.com) + Domain string `toml:"domain"` + // The port on which the server runs on + PrivatePort int `toml:"private_port"` + // The port under which the public can reach the server (useful if running behind a reverse proxy) + PublicPort *int `toml:"public_port"` + // File to write structured logs to (structured being formatted as json) + // If not set, Linstrom won't write structured logs + StructuredLogFile *string +} + +type ConfigWebAuthn struct { + DisplayName string `toml:"display_name"` + HashingSecret string `toml:"hashing_secret"` } type ConfigAdmin struct { - Username string `toml:"username"` - PasswordHash string `toml:"password_hash"` + // Name of the server's root admin account + Username string `toml:"username"` + // A one time password used to verify account access to the root admin + // after a server has been created and before the account could be linked to a passkey + FirstTimeSetupOTP string `toml:"first_time_setup_otp"` } type ConfigStorage struct { - IsPostgres bool `toml:"is_postgres"` - Uri string `toml:"uri"` + // Url to the database to use + // If DbIsPostgres is either not set or false, the url is expected to be a path to a sqlite file + // Otherwise, it's expected to be an url to a postgres server + DatabaseUrl string `toml:"database_url"` + // Whether the target of the database url is a postgres server + DbIsPostgres *bool `toml:"db_is_postgres,omitempty"` + // Url to redis server. If empty, no redis is used + RedisUrl *string `toml:"redis_url,omitempty"` + // The maximum size of the in-memory cache in bytes + MaxInMemoryCacheSize int64 `toml:"max_in_memory_cache_size"` } type ConfigMail struct { @@ -53,27 +73,23 @@ type ConfigMail struct { } type Config struct { - General ConfigGeneral `toml:"general"` - SSL ConfigSSL `toml:"ssl"` - Admin ConfigAdmin `toml:"admin"` - Storage ConfigStorage `toml:"storage"` - Mail ConfigMail `toml:"mail"` + General ConfigGeneral `toml:"general"` + SSL ConfigSSL `toml:"ssl"` + Admin ConfigAdmin `toml:"admin"` + Webauthn ConfigWebAuthn `toml:"webauthn"` + Storage ConfigStorage `toml:"storage"` + Mail ConfigMail `toml:"mail"` } -const DEFAULT_CONFIG_FILE_PATH = "config.toml" +var GlobalConfig Config -// "Global" variable for accessing the config -// If nil, no config has been loaded yet -var Global *Config - -var ErrGlobalConfigNotSet = errors.New("global config not set") - -// The default config is for a local debug environment -var defaultConfig = Config{ +var defaultConfig Config = Config{ General: ConfigGeneral{ + Protocol: "http", + Subdomain: nil, Domain: "localhost", - PublicPort: 8080, PrivatePort: 8080, + PublicPort: nil, }, SSL: ConfigSSL{ HandleSSL: false, @@ -82,54 +98,94 @@ var defaultConfig = Config{ AdminMail: nil, }, Admin: ConfigAdmin{ - Username: "admin", - PasswordHash: "", // No password + Username: "server-admin", + FirstTimeSetupOTP: "Example otp password", + }, + Webauthn: ConfigWebAuthn{ + DisplayName: "Linstrom", + HashingSecret: "some super secure secret that should never be changed or else password storage breaks", }, Storage: ConfigStorage{ - IsPostgres: false, - Uri: "db.sqlite", + DatabaseUrl: "db.sqlite", + DbIsPostgres: other.IntoPointer(false), + RedisUrl: nil, + MaxInMemoryCacheSize: 1e6, // 1 Megabyte }, } -func LoadConfigFromFile(file string, tryDefaultPath, saveIntoGlobal bool) (*Config, error) { - conf := &Config{} - data, err := os.ReadFile(file) - if err != nil && !tryDefaultPath { - return nil, fmt.Errorf("failed to read config %s: %w", file, err) +func (gc *ConfigGeneral) GetFullDomain() string { + if gc.Subdomain != nil { + return *gc.Subdomain + gc.Domain } - if err != nil && tryDefaultPath { - conf, err = loadFromDefaultPath() - if err != nil { - return nil, fmt.Errorf("failed to read custom and default config: %w", err) - } - } - err = toml.Unmarshal(data, &conf) - if err != nil { - return nil, fmt.Errorf("failed to convert from toml: %w", err) - } - if saveIntoGlobal { - Global = conf - } - return conf, nil + return gc.Domain } -func loadFromDefaultPath() (*Config, error) { - data, err := os.ReadFile(DEFAULT_CONFIG_FILE_PATH) - if err != nil { - return nil, fmt.Errorf( - "failed to load default config path %s: %w", - DEFAULT_CONFIG_FILE_PATH, - err, - ) +func (gc *ConfigGeneral) GetFullPublicUrl() string { + str := gc.Protocol + gc.GetFullDomain() + if gc.PublicPort != nil { + str += fmt.Sprint(*gc.PublicPort) + } else { + str += fmt.Sprint(gc.PrivatePort) } - conf := Config{} - err = toml.Unmarshal(data, &conf) - if err != nil { - return nil, fmt.Errorf( - "failed to parse file content of %s as toml: %w", - DEFAULT_CONFIG_FILE_PATH, - err, - ) - } - return &conf, nil + return str +} + +func WriteDefaultConfig(toFile string) error { + log.Trace().Caller().Send() + log.Info().Str("config-file", toFile).Msg("Writing default config to file") + file, err := os.Create(toFile) + if err != nil { + log.Error(). + Err(err). + Str("config-file", toFile). + Msg("Failed to create file for default config") + return err + } + defer file.Close() + + data, err := toml.Marshal(&defaultConfig) + if err != nil { + log.Error().Err(err).Msg("Failed to marshal default config to toml") + return err + } + _, err = file.Write(data) + if err != nil { + log.Error().Err(err).Str("config-file", toFile).Msg("Failed to write default config") + return err + } + + log.Info().Str("config-file", toFile).Msg("Wrote default config") + return nil +} + +func ReadAndWriteToGlobal(fileName string) error { + log.Trace().Caller().Send() + log.Debug().Str("config-file", fileName).Msg("Attempting to read config file") + data, err := os.ReadFile(fileName) + if err != nil { + log.Warn(). + Str("config-file", fileName). + Err(err). + Msg("Failed to read config file, attempting to write default config") + err = WriteDefaultConfig(fileName) + if err != nil { + log.Error(). + Err(err). + Str("config-file", fileName). + Msg("Failed to create default config file") + return err + } + GlobalConfig = defaultConfig + return nil + } + config := Config{} + log.Debug().Str("config-file", fileName).Msg("Read config file, attempting to unmarshal") + err = toml.Unmarshal(data, &config) + if err != nil { + log.Error().Err(err).Bytes("config-data-raw", data).Msg("Failed to unmarshal config file") + return err + } + GlobalConfig = config + log.Info().Str("config-file", fileName).Msg("Read and applied config file") + return nil } diff --git a/flags.go b/flags.go new file mode 100644 index 0000000..3875190 --- /dev/null +++ b/flags.go @@ -0,0 +1,25 @@ +package main + +import "flag" + +var ( + flagPrettyPrint *bool = flag.Bool( + "pretty", + false, + "If set, pretty prints logging entries which would be json objects otherwise", + ) + flagConfigFile *string = flag.String( + "config", + "config.toml", + "Location of the config file. Defaults to \"config.toml\"", + ) + flagLogLevel *string = flag.String( + "loglevel", + "Info", + "Set the logging level. Options are: Trace, Debug, Info, Warning, Error, Fatal. Capitalisation doesn't matter. Defaults to Info", + ) +) + +func init() { + flag.Parse() +} diff --git a/frontend-noscript/index.html b/frontend-noscript/index.html new file mode 100644 index 0000000..6c25dc5 --- /dev/null +++ b/frontend-noscript/index.html @@ -0,0 +1,11 @@ + + + + + + diff --git a/future-useful-libs.md b/future-useful-libs.md new file mode 100644 index 0000000..f96b7fc --- /dev/null +++ b/future-useful-libs.md @@ -0,0 +1,8 @@ +- golang.org/x/net/html + - Parsing and manipulating html +- https://github.com/chasefleming/elem-go + - Generating html +- mime + - working with mime types +- https://github.com/tursodatabase/go-libsql + - sqlite but probably better (and should also be able to compile to a static binary) diff --git a/go.mod b/go.mod index 68c5782..cbdec82 100644 --- a/go.mod +++ b/go.mod @@ -1,28 +1,57 @@ module gitlab.com/mstarongitlab/linstrom -go 1.22.5 +go 1.23 + +toolchain go1.23.0 require ( github.com/BurntSushi/toml v1.4.0 + github.com/dgraph-io/ristretto v0.1.1 + github.com/eko/gocache/lib/v4 v4.1.6 + github.com/eko/gocache/store/redis/v4 v4.2.2 + github.com/eko/gocache/store/ristretto/v4 v4.2.2 github.com/glebarez/sqlite v1.11.0 - github.com/go-webauthn/webauthn v0.10.2 + github.com/go-webauthn/webauthn v0.11.2 github.com/google/uuid v1.6.0 - github.com/piprate/json-gold v0.5.0 - github.com/sethvargo/go-limiter v1.0.0 + github.com/mstarongithub/passkey v0.0.0-20240817142622-de6912c8303e + github.com/redis/go-redis/v9 v9.0.2 + github.com/rs/zerolog v1.33.0 github.com/xhit/go-simple-mail/v2 v2.16.0 - gitlab.com/mstarongitlab/block-things-middleware v0.0.0-20240722113247-31e2984cb9d5 + gitlab.com/mstarongitlab/goap v1.1.0 + gitlab.com/mstarongitlab/goutils v1.3.0 gorm.io/driver/postgres v1.5.7 gorm.io/gorm v1.25.10 ) require ( + github.com/beorn7/perks v1.0.1 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect + github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b // indirect + github.com/golang/mock v1.6.0 // indirect + github.com/golang/protobuf v1.5.2 // indirect + github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35 // indirect + github.com/prometheus/client_golang v1.14.0 // indirect + github.com/prometheus/client_model v0.3.0 // indirect + github.com/prometheus/common v0.37.0 // indirect + github.com/prometheus/procfs v0.8.0 // indirect + golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f // indirect + golang.org/x/sync v0.8.0 // indirect + google.golang.org/protobuf v1.33.0 // indirect +) + +require ( + github.com/datainq/xml-date-time v0.0.0-20170820214645-2292f08baa38 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect github.com/dustin/go-humanize v1.0.1 // indirect - github.com/fxamacker/cbor/v2 v2.6.0 // indirect + github.com/fxamacker/cbor/v2 v2.7.0 // indirect github.com/glebarez/go-sqlite v1.21.2 // indirect github.com/go-test/deep v1.1.1 // indirect - github.com/go-webauthn/x v0.1.9 // indirect + github.com/go-webauthn/x v0.1.14 // indirect github.com/golang-jwt/jwt/v5 v5.2.1 // indirect - github.com/google/go-tpm v0.9.0 // indirect + github.com/google/go-tpm v0.9.1 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect github.com/jackc/pgx/v5 v5.4.3 // indirect @@ -31,15 +60,18 @@ require ( github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.19 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect - github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35 // indirect + github.com/piprate/json-gold v0.5.0 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect - github.com/rs/zerolog v1.33.0 // indirect + github.com/rogpeppe/go-internal v1.12.0 // indirect + github.com/stretchr/objx v0.5.2 // indirect + github.com/stretchr/testify v1.9.0 // indirect github.com/toorop/go-dkim v0.0.0-20201103131630-e1cd1a0a5208 // indirect github.com/x448/float16 v0.8.4 // indirect - gitlab.com/mstarongitlab/goutils v1.2.0 // indirect - golang.org/x/crypto v0.21.0 // indirect - golang.org/x/sys v0.19.0 // indirect - golang.org/x/text v0.15.0 // indirect + golang.org/x/crypto v0.26.0 // indirect + golang.org/x/sys v0.23.0 // indirect + golang.org/x/text v0.17.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect modernc.org/libc v1.22.5 // indirect modernc.org/mathutil v1.5.0 // indirect modernc.org/memory v1.5.0 // indirect diff --git a/go.sum b/go.sum index f73656a..68572d0 100644 --- a/go.sum +++ b/go.sum @@ -1,32 +1,182 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= +cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= +cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= +cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= +cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= +cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= +cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= +cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= +cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= +cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= +cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= +cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= +cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= +cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= +cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= +cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= +cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0= github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bsm/ginkgo/v2 v2.5.0 h1:aOAnND1T40wEdAtkGSkvSICWeQ8L3UASX7YVCqQx+eQ= +github.com/bsm/ginkgo/v2 v2.5.0/go.mod h1:AiKlXPm7ItEHNc/2+OkrNG4E0ITzojb9/xWzvQ9XZ9w= +github.com/bsm/gomega v1.20.0 h1:JhAwLmtRzXFTx2AkALSLa8ijZafntmhSoU63Ok18Uq8= +github.com/bsm/gomega v1.20.0/go.mod h1:JifAceMQ4crZIWYUKrlGcmbN3bqHogVTADMD2ATsbwk= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +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/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +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/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/datainq/xml-date-time v0.0.0-20170820214645-2292f08baa38 h1:cUoduvNB9/JFJYEVeKy2hX/R5qyg2uwnHVhXCIjhBeI= +github.com/datainq/xml-date-time v0.0.0-20170820214645-2292f08baa38/go.mod h1:a0PnbhSGmzFMGx9KdEqfEdDHoEwTF9KLNbKLcWD8kAo= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgraph-io/ristretto v0.1.1 h1:6CWw5tJNgpegArSHpNHJKldNeq03FQCwYvfMVWajOK8= +github.com/dgraph-io/ristretto v0.1.1/go.mod h1:S1GPSBCYCIhmVNfcth17y2zZtQT6wzkzgwUve0VDWWA= +github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2 h1:tdlZCpZ/P9DhczCTSixgIKmwPv6+wP5DGjqLYw5SUiA= +github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= +github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= 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/fxamacker/cbor/v2 v2.6.0 h1:sU6J2usfADwWlYDAFhZBQ6TnLFBHxgesMrQfQgk1tWA= -github.com/fxamacker/cbor/v2 v2.6.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= +github.com/eko/gocache/lib/v4 v4.1.6 h1:5WWIGISKhE7mfkyF+SJyWwqa4Dp2mkdX8QsZpnENqJI= +github.com/eko/gocache/lib/v4 v4.1.6/go.mod h1:HFxC8IiG2WeRotg09xEnPD72sCheJiTSr4Li5Ameg7g= +github.com/eko/gocache/store/redis/v4 v4.2.2 h1:Thw31fzGuH3WzJywsdbMivOmP550D6JS7GDHhvCJPA0= +github.com/eko/gocache/store/redis/v4 v4.2.2/go.mod h1:LaTxLKx9TG/YUEybQvPMij++D7PBTIJ4+pzvk0ykz0w= +github.com/eko/gocache/store/ristretto/v4 v4.2.2 h1:lXFzoZ5ck6Gy6ON7f5DHSkNt122qN7KoroCVgVwF7oo= +github.com/eko/gocache/store/ristretto/v4 v4.2.2/go.mod h1:uIvBVJzqRepr5L0RsbkfQ2iYfbyos2fuji/s4yM+aUM= +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/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= +github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= github.com/glebarez/go-sqlite v1.21.2 h1:3a6LFC4sKahUunAmynQKLZceZCOzUthkRkEAl9gAXWo= github.com/glebarez/go-sqlite v1.21.2/go.mod h1:sfxdZyhQjTM2Wry3gVYWaW072Ri1WMdWJi0k6+3382k= github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw= github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= +github.com/go-kit/log v0.2.0/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= +github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U= github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= -github.com/go-webauthn/webauthn v0.10.2 h1:OG7B+DyuTytrEPFmTX503K77fqs3HDK/0Iv+z8UYbq4= -github.com/go-webauthn/webauthn v0.10.2/go.mod h1:Gd1IDsGAybuvK1NkwUTLbGmeksxuRJjVN2PE/xsPxHs= -github.com/go-webauthn/x v0.1.9 h1:v1oeLmoaa+gPOaZqUdDentu6Rl7HkSSsmOT6gxEQHhE= -github.com/go-webauthn/x v0.1.9/go.mod h1:pJNMlIMP1SU7cN8HNlKJpLEnFHCygLCvaLZ8a1xeoQA= +github.com/go-webauthn/webauthn v0.11.2 h1:Fgx0/wlmkClTKlnOsdOQ+K5HcHDsDcYIvtYmfhEOSUc= +github.com/go-webauthn/webauthn v0.11.2/go.mod h1:aOtudaF94pM71g3jRwTYYwQTG1KyTILTcZqN1srkmD0= +github.com/go-webauthn/x v0.1.14 h1:1wrB8jzXAofojJPAaRxnZhRgagvLGnLjhCAwg3kTpT0= +github.com/go-webauthn/x v0.1.14/go.mod h1:UuVvFZ8/NbOnkDz3y1NaxtUN87pmtpC1PQ+/5BBQRdc= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= -github.com/google/go-tpm v0.9.0 h1:sQF6YqWMi+SCXpsmS3fd21oPy/vSddwZry4JnmltHVk= -github.com/google/go-tpm v0.9.0/go.mod h1:FkNVkc6C+IsvDI9Jw1OveJmxGZUUaKxtrpOS47QWKfU= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +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/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= +github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= +github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= +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.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +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.4.1/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.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-tpm v0.9.1 h1:0pGc4X//bAlmZzMKf8iz6IsDo1nYTbYJ6FZN/rg4zdM= +github.com/google/go-tpm v0.9.1/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ= github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= @@ -37,34 +187,100 @@ github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= -github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= -github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/mstarongithub/passkey v0.0.0-20240817142622-de6912c8303e h1:BjuYFWZZd3O3RCMezWeO1CaJAOiSv2U3Pd8+nl9gDvA= +github.com/mstarongithub/passkey v0.0.0-20240817142622-de6912c8303e/go.mod h1:vsjtQX07PZmKGSwixqXoKg6bvo3GTCA0GIwjCQ6qpHI= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/piprate/json-gold v0.5.0 h1:RmGh1PYboCFcchVFuh2pbSWAZy4XJaqTMU4KQYsApbM= github.com/piprate/json-gold v0.5.0/go.mod h1:WZ501QQMbZZ+3pXFPhQKzNwS1+jls0oqov3uQ2WasLs= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35 h1:J9b7z+QKAmPf4YLrFg6oQUotqHQeUNWwkvo7jZp1GLU= github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35/go.mod h1:prYjPmNq4d1NPVmpShWobRqXY3q7Vp+80DqgxxUrUIA= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= +github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= +github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= +github.com/prometheus/client_golang v1.12.1/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY= +github.com/prometheus/client_golang v1.14.0 h1:nJdhIvne2eSX/XRAFV9PcvFFRbrjbcTUj0VP62TMhnw= +github.com/prometheus/client_golang v1.14.0/go.mod h1:8vpkKitgIVNcqrRBWh1C4TIUQgYNtG/XQE4E/Zae36Y= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4= +github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w= +github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= +github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= +github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= +github.com/prometheus/common v0.37.0 h1:ccBbHCgIiT9uSoFY0vX8H3zsNR5eLt17/RQLUvn8pXE= +github.com/prometheus/common v0.37.0/go.mod h1:phzohg0JFMnBEFGxTDbfu3QyL5GI8gTQJFhYO5B3mfA= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= +github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= +github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= +github.com/prometheus/procfs v0.8.0 h1:ODq8ZFEaYeCaZOJlZZdJA2AbQR98dSHSM1KW/You5mo= +github.com/prometheus/procfs v0.8.0/go.mod h1:z7EfXMXOkbkqb9IINtpCn86r/to3BnA0uaxHdg830/4= +github.com/redis/go-redis/v9 v9.0.2 h1:BA426Zqe/7r56kCcvxYLWe1mkaz71LKF77GwgFzSxfE= +github.com/redis/go-redis/v9 v9.0.2/go.mod h1:/xDTe9EF1LM61hek62Poq2nzQSGj0xSrEtEHbBQevps= github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8= github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= -github.com/sethvargo/go-limiter v1.0.0 h1:JqW13eWEMn0VFv86OKn8wiYJY/m250WoXdrjRV0kLe4= -github.com/sethvargo/go-limiter v1.0.0/go.mod h1:01b6tW25Ap+MeLYBuD4aHunMrJoNO5PVUFdS9rac3II= -github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= -github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= @@ -74,21 +290,306 @@ github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/xhit/go-simple-mail/v2 v2.16.0 h1:ouGy/Ww4kuaqu2E2UrDw7SvLaziWTB60ICLkIkNVccA= github.com/xhit/go-simple-mail/v2 v2.16.0/go.mod h1:b7P5ygho6SYE+VIqpxA6QkYfv4teeyG4MKqB3utRu98= -gitlab.com/mstarongitlab/block-things-middleware v0.0.0-20240722113247-31e2984cb9d5 h1:BMoO20Z7EEV5QRbxWiun9EoAXPQp6apEbnVgPPV36L0= -gitlab.com/mstarongitlab/block-things-middleware v0.0.0-20240722113247-31e2984cb9d5/go.mod h1:XKgioEQc65Hhx9hd/DbkwbDv0PJv+oYlAOIZt2TCvHw= -gitlab.com/mstarongitlab/goutils v1.2.0 h1:hVpc2VikWkgmX7Gbey9I72eqgmg/6GcKZ4q+M9ZBd0E= -gitlab.com/mstarongitlab/goutils v1.2.0/go.mod h1:SvqfzFxgashuZPqR9kPwQ9gFA7I1yskZjhmGmY2pAow= -golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= -golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= -golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +gitlab.com/mstarongitlab/goap v1.1.0 h1:uN05RP+Tq2NR2IuPq6XQa5oLpfailpoEvxo1SfehxBA= +gitlab.com/mstarongitlab/goap v1.1.0/go.mod h1:rt9IYvJBPh1z6t+vvzifmxDtGjGlr8683tSPfa5dbXI= +gitlab.com/mstarongitlab/goutils v1.3.0 h1:uuxPHjIU36lyJ8/z4T2xI32zOyh53Xj0Au8K12qkaJ4= +gitlab.com/mstarongitlab/goutils v1.3.0/go.mod h1:SvqfzFxgashuZPqR9kPwQ9gFA7I1yskZjhmGmY2pAow= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw= +golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= +golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f h1:99ci1mjWVBWwJiEKYY6jWa4d2nTQVIEhZIptnrVb1XY= +golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f/go.mod h1:/lliqkxwWAhPjf5oSOIJup2XcqJaw8RGS6k3TGEc7GI= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +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-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= +golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +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-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/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-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= +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-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= +golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/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-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20221010170243-090e33056c14/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= -golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= -golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/sys v0.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM= +golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= +golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +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-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= +golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= +google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= +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/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= +google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= +google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +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.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= +google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= +google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +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.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +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/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= @@ -96,6 +597,13 @@ gorm.io/driver/postgres v1.5.7 h1:8ptbNJTDbEmhdr62uReG5BGkdQyeasu/FZHxI0IMGnM= gorm.io/driver/postgres v1.5.7/go.mod h1:3e019WlBaYI5o5LIdNV+LyxCMNtLOQETBXL2h4chKpA= gorm.io/gorm v1.25.10 h1:dQpO+33KalOA+aFYGlK+EfxcI5MbO7EP2yYygwh9h+s= gorm.io/gorm v1.25.10/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= modernc.org/libc v1.22.5 h1:91BNch/e5B0uPbJFgqbxXuOnxBQjlS//icfQEGmvyjE= modernc.org/libc v1.22.5/go.mod h1:jj+Z7dTNX8fBScMVNRAYZ/jF91K8fdT2hYMThc3YjBY= modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ= @@ -104,3 +612,6 @@ modernc.org/memory v1.5.0 h1:N+/8c5rE6EqugZwHii4IFsaJ7MUhoWX07J5tC/iI5Ds= modernc.org/memory v1.5.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU= modernc.org/sqlite v1.23.1 h1:nrSBg4aRQQwq59JpvGEQ15tNxoO5pX/kUjcRNwSAGQM= modernc.org/sqlite v1.23.1/go.mod h1:OrDj17Mggn6MhE+iPbBNf7RGKODDE9NFT0f3EwDzJqk= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= diff --git a/goals.md b/goals.md new file mode 100644 index 0000000..9d34010 --- /dev/null +++ b/goals.md @@ -0,0 +1,26 @@ +## Easy +- optional content filter with Microsoft's ai scan thing (user and server level) +- lockdown mode (all incoming stuff will be bonked immediately) (user and server) +- Post highlighting (opposite of muting) where if a post contains some specific thing, it gets some highlight + - Maybe even with different highlighting options + +## Medium +- optional automatic server screening +- metadata sharing (thing like link previews or blocklists) +- asks (in some way that is compatible with wafrn hopefully) +- rss feed imports +- Database converter (Masto/Akoma/Mk -> Linstrom, maybe also other way around) + +## Hard +- custom "ads" created and controlled by server admins +- some sort of subscription/payment system (opt-in (you have to opt in to potentially see monetised stuff in the first place)) +- extended account moderation (user and server) +- custom api for working around AP being a pos: + - includes messages always being encrypted + - bunch of other optimisations + +# Variable difficutly +- Multiple built-in frontends + - Primary using ember, focus on good looking and most feature complete + - Modifyable using htmx (not sure on this one yet) + - Low resource/no script with pure html and Go templating diff --git a/main.go b/main.go new file mode 100644 index 0000000..0181f58 --- /dev/null +++ b/main.go @@ -0,0 +1,90 @@ +// TODO: Add EUPL banner everywhere +package main + +import ( + "io" + "os" + "strings" + + "github.com/rs/zerolog" + "github.com/rs/zerolog/log" + "gitlab.com/mstarongitlab/linstrom/ap" + "gitlab.com/mstarongitlab/linstrom/config" + "gitlab.com/mstarongitlab/linstrom/storage" + "gitlab.com/mstarongitlab/linstrom/storage/cache" +) + +func main() { + setLogger() + setLogLevel() + if err := config.ReadAndWriteToGlobal(*flagConfigFile); err != nil { + log.Fatal(). + Err(err). + Str("config-file", *flagConfigFile). + Msg("Failed to read config and couldn't write default") + } + res, err := ap.GetAccountWebfinger("@aufricus_athudath@activitypub.academy") + log.Info(). + Err(err). + Any("webfinger", res). + Msg("Webfinger request result for @aufricus_athudath@activitypub.academy") + storageCache, err := cache.NewCache( + config.GlobalConfig.Storage.MaxInMemoryCacheSize, + config.GlobalConfig.Storage.RedisUrl, + ) + if err != nil { + log.Fatal().Err(err).Msg("Failed to start cache") + } + var store *storage.Storage + if config.GlobalConfig.Storage.DbIsPostgres != nil && + *config.GlobalConfig.Storage.DbIsPostgres { + store, err = storage.NewStoragePostgres( + config.GlobalConfig.Storage.DatabaseUrl, + storageCache, + ) + } else { + store, err = storage.NewStorageSqlite(config.GlobalConfig.Storage.DatabaseUrl, storageCache) + } + if err != nil { + log.Fatal().Err(err).Msg("Failed to setup storage") + } + _ = store + // TODO: Set up media server + // TODO: Set up http server + // TODO: Set up plugins + // TODO: Start everything +} + +func setLogLevel() { + log.Info().Str("new-level", *flagLogLevel).Msg("Attempting to set log level") + switch strings.ToLower(*flagLogLevel) { + case "trace": + zerolog.SetGlobalLevel(zerolog.TraceLevel) + case "debug": + zerolog.SetGlobalLevel(zerolog.DebugLevel) + case "info": + zerolog.SetGlobalLevel(zerolog.InfoLevel) + case "warn": + zerolog.SetGlobalLevel(zerolog.WarnLevel) + case "error": + zerolog.SetGlobalLevel(zerolog.ErrorLevel) + case "fatal": + zerolog.SetGlobalLevel(zerolog.FatalLevel) + default: + zerolog.SetGlobalLevel(zerolog.InfoLevel) + } +} + +func setLogger(extraLogWriters ...io.Writer) { + if *flagPrettyPrint { + console := zerolog.ConsoleWriter{Out: os.Stderr} + log.Logger = zerolog.New(zerolog.MultiLevelWriter(append([]io.Writer{console}, extraLogWriters...)...)). + With(). + Timestamp(). + Logger() + } else { + log.Logger = zerolog.New(zerolog.MultiLevelWriter( + append([]io.Writer{log.Logger}, extraLogWriters...)..., + )).With().Timestamp().Logger() + } +} diff --git a/mod-tools.md b/mod-tools.md new file mode 100644 index 0000000..dfa7331 --- /dev/null +++ b/mod-tools.md @@ -0,0 +1,50 @@ +- Note: All server actions could also be dependent on the type of server (Plemora, Misskey, Masto, GTS, Linstrom, etc) +- Bulk moderation: + - Maybe vim-like multiselect via search with regex? + - Role based selection? +- MRF: + - Automated actions on incoming and outgoing messages: + - Apply some regex + - Auto-mark as sensitive + - Mark media as sensitive + - Remove media + - Block entirely + - Force raw text + - Selection mechanism: + - Match a regex + - From user + - From server + - From user with role + - (Optional) On positive result from M$ content scan +- Automod: + - Notes (and replies) get withheld until approval + - Regex based yoink + - All from user + - All from role + - All from server + - (Optional) On positive result from M$ content scan + - Exclude roles from being hit + - Exclude users from being hit +- Local Role permissions: + - Can send activities (Everything) + - Can upload media + - Can send reactions + - Can send replies + - Can send posts (that are not replies) + - Can edit own posts + - Ratelimit (variable speed) +- Emoji management: + - Disable custom remote emoji per default + - Disable select ones + - Based on regex + - Based on specific names + - Based on origin +- Account management: + - Force reset password of local account + - Disable login into account + - Delete account +- Deletion policies: + - Note: Always going to be soft delete first, then hard delete x days later + - delete notifications get send on hard delete + - Delete notifications get send on soft delete + - Delete notifications get send y days after soft delete (where y < x) diff --git a/outgoingEventQueue/queue.go b/outgoingEventQueue/queue.go new file mode 100644 index 0000000..51974c1 --- /dev/null +++ b/outgoingEventQueue/queue.go @@ -0,0 +1,21 @@ +package outgoingeventqueue + +// TODO: Implement me + +/* +Queue for controlled distribution of outgoing events, such as note creations, deletions or updates +Also has to manage the case where an instance can't be reached or where Linstrom has to shut down +In case of a shutdown, it has to store all remaining tasks in the db to try again later after the next boot +Should preferably also be able to tell when a server is just gone and stop bothering about it + + +Implementation idea: New job gets send to queue (via function call). Queue then launches a new goroutine for that job +Goroutine then launches multiple new goroutines, one per targeted inbox, filtered by blocked targets +Each of those will wait for a ticker or a stop signal +On each ticker they'll try to deliver the payload to the target inbox +If a stop signal comes in, push the payload and goal into the db to pick up again later +On stop or sucessful delivery, exit the goroutine +Stop can also be used to suspend outbound traffic to another instance to help reduce the load on them + +TODO: Think of ways to make this more configurable for how data is being sent how fast +*/ diff --git a/server-old/contextVals.go b/server-old/contextVals.go deleted file mode 100644 index 1091ed2..0000000 --- a/server-old/contextVals.go +++ /dev/null @@ -1,29 +0,0 @@ -package server - -import ( - "fmt" - "net/http" - - "github.com/rs/zerolog" - - "gitlab.com/mstarongitlab/linstrom/server/middlewares" - "gitlab.com/mstarongitlab/linstrom/storage" -) - -func ContextGetStorage(w http.ResponseWriter, r *http.Request) *storage.Storage { - val, _ := r.Context().Value(middlewares.CONTEXT_KEY_STORAGE).(*storage.Storage) - return val -} - -func ContextGetLogger(w http.ResponseWriter, r *http.Request) *zerolog.Logger { - val, _ := r.Context().Value(middlewares.CONTEXT_KEY_LOGRUS).(*zerolog.Logger) - return val -} - -func contextFail(w http.ResponseWriter, contextValName string) { - http.Error( - w, - fmt.Sprintf("failed to get %s from request context", contextValName), - http.StatusInternalServerError, - ) -} diff --git a/server-old/endpoint_webfinger.go b/server-old/endpoint_webfinger.go deleted file mode 100644 index 77373c8..0000000 --- a/server-old/endpoint_webfinger.go +++ /dev/null @@ -1,112 +0,0 @@ -package server - -import ( - "encoding/json" - "fmt" - "net/http" - "strings" - - "github.com/sirupsen/logrus" - - "gitlab.com/mstarongitlab/linstrom/config" - "gitlab.com/mstarongitlab/linstrom/server/middlewares" - "gitlab.com/mstarongitlab/linstrom/storage" -) - -type webfingerUrl struct { - Relation string `json:"rel"` - Type string `json:"type"` - Url string `json:"href"` -} - -// NOTE: Unused for now -// Reason: No endpoint for eg follow authorisation yet -type webfingerTemplate struct { - Relation string `json:"rel"` - Template string `json:"template"` -} - -type webfingerResponse struct { - Subject string `json:"subject"` - // Any because it's either a webfingerTemplate or webfingerUrl - Links []any `json:"links"` -} - -// Mount under /.well-known/webfinger -// Handles webfinger requests which are used to determine whether an account exists on this server -// Additionally, a sucessful query will return a set of links related to that account, such as the activitypub view and the web view -func webfingerHandler(w http.ResponseWriter, r *http.Request) { - logEntry, ok := r.Context().Value(middlewares.CONTEXT_KEY_LOGRUS).(*logrus.Entry) - if !ok { - http.Error(w, "couldn't get logging entry from context", http.StatusInternalServerError) - return - } - store := storage.Storage{} - - requestedResource := r.FormValue("resource") - if requestedResource == "" { - http.Error(w, "bad request. Include \"resource\" parameter", http.StatusBadRequest) - logEntry.Infoln("No resource parameter. Cancelling") - return - } - accName := strings.TrimPrefix(requestedResource, "acc:") - acc, err := store.FindLocalAccount(accName) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - logEntry.WithError(err).Warningln("couldn't find account") - return - } - finger, err := accToWebfinger(acc) - if err != nil { - http.Error(w, "failed to build webfinger", http.StatusInternalServerError) - return - } - data, err := json.Marshal(finger) - if err != nil { - http.Error(w, "failed to build json", http.StatusInternalServerError) - return - } - fmt.Fprint(w, string(data)) -} - -func accToWebfinger(acc *storage.User) (*webfingerResponse, error) { - // First ensure config is set - if config.Global == nil { - return nil, config.ErrGlobalConfigNotSet - } - // Then build the ap link - apLink := webfingerUrl{ - Relation: "self", - Type: "application/activity+json", - Url: config.Global.General.FullDomain, - } - if config.Global.General.PublicPort != 80 && - config.Global.General.PublicPort != 443 { - apLink.Url += fmt.Sprintf(":%d", config.Global.General.PublicPort) - } - apLink.Url += "/api/ap/user/" + acc.ID - - // Now the web view - viewLink := webfingerUrl{ - Relation: "http://webfinger.net/rel/profile-page", - Type: "text/html", - Url: config.Global.General.FullDomain, - } - if config.Global.General.PublicPort != 80 && - config.Global.General.PublicPort != 443 { - viewLink.Url += fmt.Sprintf(":%d", config.Global.General.PublicPort) - } - viewLink.Url += "/@" + acc.GetHandleNameOnly() - - // TODO: Add follow authorisation template once the endpoint is available - - response := webfingerResponse{ - Subject: fmt.Sprintf("acct:%s", strings.TrimPrefix(acc.Handle, "@")), - Links: []any{ - apLink, - viewLink, - }, - } - - return &response, nil -} diff --git a/server-old/endpoints_ap.go b/server-old/endpoints_ap.go deleted file mode 100644 index 584b894..0000000 --- a/server-old/endpoints_ap.go +++ /dev/null @@ -1,57 +0,0 @@ -package server - -import ( - "net/http" - - "gitlab.com/mstarongitlab/linstrom/ap" -) - -// Helper func for redirecting a request to another url if that request does not the proper content type headers -func redirectIfNotApRequest(w http.ResponseWriter, r *http.Request, redirectTarget string) bool { - logger := ContextGetLogger(w, r) - if logger == nil { - return false - } - if ap.ContainsApContentHeader(r.Header.Get("Content-Type")) { - return false - } - // redirect with code 307 temporary redirect so that the client sends the same request, but to the given redirect target url instead - http.Redirect(w, r, redirectTarget, http.StatusTemporaryRedirect) - logger.Info(). - Str("from-url", r.URL.RawPath). - Str("to-url", redirectTarget). - Msg("Redirecting non-ap request to ap endpoint to proper endpoint") - return true -} - -// Mount under /api/ap/note/{id} -// Handles requests for the AP version of a note with the given id -// And redirects non-ap requests to the web version -func noteApHandler(w http.ResponseWriter, r *http.Request) { - store := ContextGetStorage(w, r) - if store == nil { - return - } - logger := ContextGetLogger(w, r) - if logger == nil { - return - } - // First things first, get the note id from the url - noteId := r.PathValue("id") - // If there is no id (empty string means no value was provided), error out - if noteId == "" { - logger.Info().Msg("Attempted to request a note without providing an ID") - http.Error(w, "missing note id", http.StatusBadRequest) - return - } - if redirectIfNotApRequest(w, r, "/notes/"+noteId) { - return - } - - note, err := store.FindNoteById(noteId) -} - -// Mount under /api/ap/user/{id} -func userApHandler(w http.ResponseWriter, _ *http.Request) { - -} diff --git a/server-old/endpoints_web_api.go b/server-old/endpoints_web_api.go deleted file mode 100644 index e5e8840..0000000 --- a/server-old/endpoints_web_api.go +++ /dev/null @@ -1,4 +0,0 @@ -package server - -// Endpoints for the frontend to get its data from -// This is only for Linstrom's own web frontend, not for other options such as Mastodon, *key and plemora diff --git a/server-old/endpoints_web_fe.go b/server-old/endpoints_web_fe.go deleted file mode 100644 index 142886b..0000000 --- a/server-old/endpoints_web_fe.go +++ /dev/null @@ -1,20 +0,0 @@ -package server - -import "net/http" - -// Endpoints for the frontend rendering, not data the frontend might use -// Aka serves the static html and assets file needed for rendering the base frontend which will take over from there -// Later on, might also add endpoints for the simple and small frontend - -// Mount under /notes/{id} -// Returns webview for notes -// Though maybe it also just returns the general ember export file and ember then renders the neeeded data -func notesWebFrontendHandler(w http.ResponseWriter, _ *http.Request) { - http.Error(w, "not implemented yet", http.StatusInternalServerError) -} - -// Mount under /@{id} -// Returns webview for accounts -func accountWebFrontendHandler(w http.ResponseWriter, _ *http.Request) { - http.Error(w, "not implemented yet", http.StatusInternalServerError) -} diff --git a/server-old/middlewares/injectContextValues.go b/server-old/middlewares/injectContextValues.go deleted file mode 100644 index e7bd2ef..0000000 --- a/server-old/middlewares/injectContextValues.go +++ /dev/null @@ -1,18 +0,0 @@ -package middlewares - -import ( - "context" - "net/http" -) - -func InjectContextValuesBuilder(values map[ContextKey]any) func(http.Handler) http.Handler { - return func(h http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - ctx := r.Context() - for k, v := range values { - ctx = context.WithValue(ctx, k, v) - } - h.ServeHTTP(w, r.WithContext(ctx)) - }) - } -} diff --git a/server-old/middlewares/injectLogger.go b/server-old/middlewares/injectLogger.go deleted file mode 100644 index a383c1c..0000000 --- a/server-old/middlewares/injectLogger.go +++ /dev/null @@ -1,20 +0,0 @@ -package middlewares - -import ( - "context" - "net/http" - - "github.com/rs/zerolog/log" -) - -const CONTEXT_KEY_LOGRUS = ContextKey("logrus") - -// Inject a logrus entry into the context that has the url path already set -func InjectLoggerMiddleware(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - reqContext := r.Context() - logger := log.With().Ctx(r.Context()).Str("url-path", r.URL.RawPath).Logger() - newContext := context.WithValue(reqContext, CONTEXT_KEY_LOGRUS, &logger) - next.ServeHTTP(w, r.WithContext(newContext)) - }) -} diff --git a/server-old/middlewares/injectStorage.go b/server-old/middlewares/injectStorage.go deleted file mode 100644 index ef5b374..0000000 --- a/server-old/middlewares/injectStorage.go +++ /dev/null @@ -1,22 +0,0 @@ -package middlewares - -import ( - "context" - "net/http" - - "gitlab.com/mstarongitlab/linstrom/storage" -) - -const CONTEXT_KEY_STORAGE = ContextKey("storage") - -// Build a middleware for injecting a storage reference into the context -func InjectStorageMiddlewareBuilder(store *storage.Storage) func(http.Handler) http.Handler { - return func(h http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - reqContext := r.Context() - newContext := context.WithValue(reqContext, CONTEXT_KEY_STORAGE, store) - newRequest := r.WithContext(newContext) - h.ServeHTTP(w, newRequest) - }) - } -} diff --git a/server-old/middlewares/middleswares.go b/server-old/middlewares/middleswares.go deleted file mode 100644 index def18fe..0000000 --- a/server-old/middlewares/middleswares.go +++ /dev/null @@ -1,14 +0,0 @@ -package middlewares - -import "net/http" - -type ContextKey string -type MiddlewareFunc func(http.Handler) http.Handler - -func ChainMiddlewares(start http.Handler, middlewares ...MiddlewareFunc) http.Handler { - next := start - for _, h := range middlewares { - next = h(next) - } - return next -} diff --git a/server-old/middlewares/rateLimit.go b/server-old/middlewares/rateLimit.go deleted file mode 100644 index 994ea68..0000000 --- a/server-old/middlewares/rateLimit.go +++ /dev/null @@ -1,43 +0,0 @@ -package middlewares - -import ( - "fmt" - "net/http" - "strings" - "time" - - "github.com/sethvargo/go-limiter" -) - -type RateLimiter struct { - store limiter.Store - next http.Handler -} - -func (rl *RateLimiter) ServeHTTP(w http.ResponseWriter, r *http.Request) { - // What to not rate limit - // Includes various localhost and loopback interfaces - // TODO: Only allow requests with a valid unlimit token in the "rate-limit-bypass" field bypass rate limit - if r.FormValue("rate-limit-bypass") != "" || - strings.Contains(r.RemoteAddr, "127.0.0.1") || - strings.Contains(r.RemoteAddr, "localhost") || - strings.Contains(r.RemoteAddr, "::1") { - rl.next.ServeHTTP(w, r) - return - } - tokens, remaining, resetTime, ok, err := rl.store.Take(r.Context(), r.RemoteAddr) - if err != nil { - http.Error(w, "rate limiter problem", http.StatusInternalServerError) - return - } - w.Header().Add("X-RateLimit-Limit", fmt.Sprint(tokens)) - w.Header().Add("X-RateLimit-Remaining", fmt.Sprint(remaining)) - w.Header().Add("X-RateLimit-Reset", fmt.Sprint(resetTime)) - if ok { - rl.next.ServeHTTP(w, r) - } else { - t := time.Unix(0, int64(resetTime)).UTC().Format(time.RFC1123) - w.Header().Add("Retry-After", t) - http.Error(w, "rate limited", http.StatusTooManyRequests) - } -} diff --git a/server-old/server.go b/server-old/server.go deleted file mode 100644 index e0c73c1..0000000 --- a/server-old/server.go +++ /dev/null @@ -1,43 +0,0 @@ -package server - -import ( - "net/http" - "regexp" - - blockthingsmiddleware "gitlab.com/mstarongitlab/block-things-middleware" - - "gitlab.com/mstarongitlab/linstrom/server/middlewares" - "gitlab.com/mstarongitlab/linstrom/storage" -) - -type Server struct { - handler http.Handler -} - -func NewServer(store *storage.Storage) *Server { - handler := http.NewServeMux() - handler.HandleFunc("/.well-known/webfinger", webfingerHandler) - // handler.Handle("/api/", http.StripPrefix("/ap", buildApiRouter())) - handler.HandleFunc("/notes/{id}", notesWebFrontendHandler) - handler.HandleFunc("/@{id}", accountWebFrontendHandler) - - withMiddlewares := middlewares.ChainMiddlewares( - handler, - middlewares.InjectLoggerMiddleware, - middlewares.InjectStorageMiddlewareBuilder(store), - middlewares.InjectContextValuesBuilder(map[middlewares.ContextKey]any{ - middlewares.CONTEXT_KEY_STORAGE: store, - }), - - blockthingsmiddleware.BuildMiddleware(blockthingsmiddleware.Config{ - UserAgentRegexes: []*regexp.Regexp{ - regexp.MustCompile("ArchiveBot"), - regexp.MustCompile("GPTBot"), - }, - CheckDomain: false, - }), - ) - return &Server{ - handler: withMiddlewares, - } -} diff --git a/server/endpoints_ap.go b/server/endpoints_ap.go new file mode 100644 index 0000000..e76176b --- /dev/null +++ b/server/endpoints_ap.go @@ -0,0 +1,38 @@ +package server + +import ( + "errors" + "fmt" + "net/http" + "strings" + + "github.com/rs/zerolog" + "gitlab.com/mstarongitlab/linstrom/storage" + "gorm.io/gorm" +) + +// Mount under /.well-known/webfinger +func webfingerHandler(w http.ResponseWriter, r *http.Request) { + logger := zerolog.Ctx(r.Context()) + store := storage.Storage{} + + requestedResource := r.FormValue("resource") + if requestedResource == "" { + http.Error(w, "bad request. Include \"resource\" parameter", http.StatusBadRequest) + logger.Debug().Msg("Resource parameter missing. Cancelling") + return + } + accName := strings.TrimPrefix(requestedResource, "acc:") + acc, err := store.FindAccountByFullHandle(accName) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + http.Error(w, "account not found", http.StatusNotFound) + logger.Debug().Str("account-name", accName).Msg("Account not found") + } else { + http.Error(w, err.Error(), http.StatusInternalServerError) + logger.Error().Err(err).Msg("Error while searching for account") + return + } + } + fmt.Fprint(w, acc) +} diff --git a/server/remoteServer/remoteServer.go b/server/remoteServer/remoteServer.go new file mode 100644 index 0000000..13b147e --- /dev/null +++ b/server/remoteServer/remoteServer.go @@ -0,0 +1,13 @@ +package remotestorage + +import "gitlab.com/mstarongitlab/linstrom/storage" + +// Wrapper around db storage +// storage.Storage is for the db and cache access only, +// while this one wraps storage.Storage to also provide remote fetching of missing resources. +// So if an account doesn't exist in db or cache, this wrapper will attempt to fetch it +type RemoteStorage struct { + store *storage.Storage +} + +// TODO: Implement just about everything storage has, but with remote fetching if storage fails diff --git a/storage/cache.go b/storage/cache.go new file mode 100644 index 0000000..789d7fc --- /dev/null +++ b/storage/cache.go @@ -0,0 +1,73 @@ +package storage + +import ( + "errors" + "strings" + + "github.com/redis/go-redis/v9" +) + +// various prefixes for accessing items in the cache (since it's a simple key-value store) +const ( + cacheUserHandleToIdPrefix = "acc-name-to-id:" + cacheUserIdToAccPrefix = "acc-id-to-data:" + cacheNoteIdToNotePrefix = "note-id-to-data:" +) + +// An error describing the case where some value was just not found in the cache +var errCacheNotFound = errors.New("not found in cache") + +// Find an account id in cache using a given user handle +// accId contains the Id of the account if found +// err contains an error describing why an account's id couldn't be found +// The most common one should be errCacheNotFound +func (s *Storage) cacheHandleToAccUid(handle string) (accId *string, err error) { + // Where to put the data (in case it's found) + var target string + found, err := s.cache.Get(cacheUserHandleToIdPrefix+strings.TrimLeft(handle, "@"), &target) + // If nothing was found, check error + if !found { + // Case error is set and NOT redis' error for nothing found: Return that error + if err != nil && !errors.Is(err, redis.Nil) { + return nil, err + } else { + // Else return errCacheNotFound + return nil, errCacheNotFound + } + } + return &target, nil +} + +// Find an account's data in cache using a given account id +// acc contains the full account as stored last time if found +// err contains an error describing why an account couldn't be found +// The most common one should be errCacheNotFound +func (s *Storage) cacheAccIdToData(id string) (acc *Account, err error) { + var target Account + found, err := s.cache.Get(cacheUserIdToAccPrefix+id, &target) + if !found { + if err != nil && !errors.Is(err, redis.Nil) { + return nil, err + } else { + return nil, errCacheNotFound + } + } + return &target, nil +} + +// Find a cached note given its ID +// note contains the full note as stored last time if found +// err contains an error describing why a note couldn't be found +// The most common one should be errCacheNotFound +func (s *Storage) cacheNoteIdToData(id string) (note *Note, err error) { + target := Note{} + found, err := s.cache.Get(cacheNoteIdToNotePrefix+id, &target) + if !found { + if err != nil && !errors.Is(err, redis.Nil) { + return nil, err + } else { + return nil, errCacheNotFound + } + } + return &target, nil +} diff --git a/storage/cache/cache.go b/storage/cache/cache.go new file mode 100644 index 0000000..243f5b6 --- /dev/null +++ b/storage/cache/cache.go @@ -0,0 +1,90 @@ +package cache + +import ( + "context" + "fmt" + "time" + + "github.com/dgraph-io/ristretto" + "github.com/eko/gocache/lib/v4/cache" + "github.com/eko/gocache/lib/v4/store" + redis_store "github.com/eko/gocache/store/redis/v4" + ristretto_store "github.com/eko/gocache/store/ristretto/v4" + "github.com/redis/go-redis/v9" + "github.com/rs/zerolog/log" +) + +type Cache struct { + cache *cache.ChainCache[[]byte] + decoders *DecoderPool + encoders *EncoderPool +} + +var ctxBackground = context.Background() + +// TODO: Maybe also include metrics +func NewCache(maxSize int64, redisUrl *string) (*Cache, error) { + // ristretto is an in-memory cache + log.Debug().Int64("max-size", maxSize).Msg("Setting up ristretto") + ristrettoCache, err := ristretto.NewCache(&ristretto.Config{ + // The *10 is a recommendation from ristretto + NumCounters: maxSize * 10, + MaxCost: maxSize, + BufferItems: 64, // Same here + }) + if err != nil { + return nil, fmt.Errorf("ristretto cache error: %w", err) + } + ristrettoStore := ristretto_store.NewRistretto( + ristrettoCache, + store.WithExpiration(time.Second*10), + ) + + var cacheManager *cache.ChainCache[[]byte] + if redisUrl != nil { + opts, err := redis.ParseURL(*redisUrl) + if err != nil { + return nil, err + } + redisClient := redis.NewClient(opts) + redisStore := redis_store.NewRedis(redisClient, store.WithExpiration(time.Minute)) + cacheManager = cache.NewChain( + cache.New[[]byte](ristrettoStore), + cache.New[[]byte](redisStore), + ) + } else { + cacheManager = cache.NewChain(cache.New[[]byte](ristrettoStore)) + } + + return &Cache{ + cache: cacheManager, + decoders: NewDecoderPool(), + encoders: NewEncoderPool(), + }, nil +} + +func (c *Cache) Get(key string, target any) (bool, error) { + data, err := c.cache.Get(ctxBackground, key) + if err != nil { + return false, err + } + err = c.decoders.Decode(data, target) + if err != nil { + return false, err + } + return true, err +} + +func (c *Cache) Set(key string, value any) error { + data, err := c.encoders.Encode(value) + if err != nil { + return err + } + err = c.cache.Set(ctxBackground, key, data, nil) + return err +} + +func (c *Cache) Delete(key string) { + // Error return doesn't matter here. Delete is delete is gone + _ = c.cache.Delete(ctxBackground, key) +} diff --git a/storage/cache/coderPools.go b/storage/cache/coderPools.go new file mode 100644 index 0000000..7003939 --- /dev/null +++ b/storage/cache/coderPools.go @@ -0,0 +1,164 @@ +package cache + +import ( + "io" + "sync" +) + +type EncoderPool struct { + encoders []*gobEncoder + lock sync.RWMutex +} + +func NewEncoderPool() *EncoderPool { + return &EncoderPool{ + encoders: []*gobEncoder{}, + lock: sync.RWMutex{}, + } +} + +// Encode some value with gob +func (p *EncoderPool) Encode(raw any) ([]byte, error) { + var encoder *gobEncoder + // First try to find an available encoder + // Read only lock should be fine here since locks are atomic i + //and thus no two goroutines should be able to lock the same encoder at the same time + // One of those attempts is going to fail and continue looking for another available one + p.lock.RLock() + for _, v := range p.encoders { + // If we can lock one, it's available + if v.TryLock() { + // Keep the reference, then break + encoder = v + break + } + } + p.lock.RUnlock() + // Didn't find an available encoder, create new one and add to pool + if encoder == nil { + encoder = p.expand() + } + // Ensure we free the encoder at the end + defer encoder.Unlock() + // Clear the buffer to avoid funky output from previous operations + encoder.Buffer.Reset() + if err := encoder.Encoder.Encode(raw); err != nil { + return nil, err + } + data, err := io.ReadAll(encoder.Buffer) + if err != nil { + return nil, err + } + return data, nil +} + +// Expands the pool of available encoders by one and returns a reference to the new one +// The new encoder is already locked and ready for use +func (p *EncoderPool) expand() *gobEncoder { + enc := newEncoder() + // Lock everything. First the pool fully since we need to overwrite the encoders slice + p.lock.Lock() + // And then the new encoder to make it available for use by the caller + // so that they don't have to search for it again + enc.Lock() + p.encoders = append(p.encoders, &enc) + p.lock.Unlock() + return &enc +} + +// Prune all encoders not currently used from the pool +func (p *EncoderPool) Prune() { + stillActiveEncoders := []*gobEncoder{} + p.lock.Lock() + for _, v := range p.encoders { + if !v.TryLock() { + // Can't lock, encoder in use, keep it + stillActiveEncoders = append(stillActiveEncoders, v) + continue + } + // If we reach here, the encoder was available (since not locked), unlock and continue + v.Unlock() + } + // Overwrite list of available encoders to only contain the ones we found to still be active + p.encoders = stillActiveEncoders + p.lock.Unlock() +} + +type DecoderPool struct { + encoders []*gobDecoder + lock sync.RWMutex +} + +func NewDecoderPool() *DecoderPool { + return &DecoderPool{ + encoders: []*gobDecoder{}, + lock: sync.RWMutex{}, + } +} + +// Decode some value with gob +func (p *DecoderPool) Decode(raw []byte, target any) error { + var encoder *gobDecoder + // First try to find an available encoder + // Read only lock should be fine here since locks are atomic i + //and thus no two goroutines should be able to lock the same encoder at the same time + // One of those attempts is going to fail and continue looking for another available one + p.lock.RLock() + for _, v := range p.encoders { + // If we can lock one, it's available + if v.TryLock() { + // Keep the reference, then break + encoder = v + break + } + } + p.lock.RUnlock() + // Didn't find an available encoder, create new one and add to pool + if encoder == nil { + encoder = p.expand() + } + // Desure we free the encoder at the end + defer encoder.Unlock() + // Clear the buffer to avoid funky output from previous operations + encoder.Buffer.Reset() + // Write the raw data to the buffer, then decode it + // The write will always succeed (or panic) + _, _ = encoder.Buffer.Write(raw) + err := encoder.Decoder.Decode(target) + if err != nil { + return err + } + return nil +} + +// Expands the pool of available encoders by one and returns a reference to the new one +// The new encoder is already locked and ready for use +func (p *DecoderPool) expand() *gobDecoder { + enc := newDecoder() + // Lock everything. First the pool fully since we need to overwrite the encoders slice + p.lock.Lock() + // And then the new encoder to make it available for use by the caller + // so that they don't have to search for it again + enc.Lock() + p.encoders = append(p.encoders, &enc) + p.lock.Unlock() + return &enc +} + +// Prune all encoders not currently used from the pool +func (p *DecoderPool) Prune() { + stillActiveDecoders := []*gobDecoder{} + p.lock.Lock() + for _, v := range p.encoders { + if !v.TryLock() { + // Can't lock, encoder in use, keep it + stillActiveDecoders = append(stillActiveDecoders, v) + continue + } + // If we reach here, the encoder was available (since not locked), unlock and continue + v.Unlock() + } + // Overwrite list of available encoders to only contain the ones we found to still be active + p.encoders = stillActiveDecoders + p.lock.Unlock() +} diff --git a/storage/cache/lockedCoders.go b/storage/cache/lockedCoders.go new file mode 100644 index 0000000..74f3ed7 --- /dev/null +++ b/storage/cache/lockedCoders.go @@ -0,0 +1,35 @@ +package cache + +import ( + "bytes" + "encoding/gob" + "sync" +) + +type gobEncoder struct { + sync.Mutex + Encoder *gob.Encoder + Buffer *bytes.Buffer +} + +func newEncoder() gobEncoder { + buf := bytes.Buffer{} + return gobEncoder{ + Encoder: gob.NewEncoder(&buf), + Buffer: &buf, + } +} + +type gobDecoder struct { + sync.Mutex + Decoder *gob.Decoder + Buffer *bytes.Buffer +} + +func newDecoder() gobDecoder { + buf := bytes.Buffer{} + return gobDecoder{ + Decoder: gob.NewDecoder(&buf), + Buffer: &buf, + } +} diff --git a/storage/errors.go b/storage/errors.go new file mode 100644 index 0000000..ac1ee99 --- /dev/null +++ b/storage/errors.go @@ -0,0 +1,11 @@ +package storage + +import "errors" + +type ErrNotImplemented struct{} + +func (n ErrNotImplemented) Error() string { + return "Not implemented yet" +} + +var ErrEntryNotFound = errors.New("entry not found") diff --git a/storage/housekeeping.go b/storage/housekeeping.go new file mode 100644 index 0000000..aa7a815 --- /dev/null +++ b/storage/housekeeping.go @@ -0,0 +1,6 @@ +package storage + +// Contains various functions for housekeeping +// Things like true deletion of soft deleted data after some time +// Or removing inactive access tokens +// All of this will be handled by goroutines diff --git a/storage/mediaFile.go b/storage/mediaFile.go index 4f96172..5725aac 100644 --- a/storage/mediaFile.go +++ b/storage/mediaFile.go @@ -6,29 +6,72 @@ import ( "gorm.io/gorm" ) -type MediaFile struct { - ID string `gorm:"primarykey"` - CreatedAt time.Time - UpdatedAt time.Time +// MediaMetadata contains metadata about some media +// Metadata includes whether it's a remote file or not, what the name is, +// the MIME type, and an identifier pointing to its location +type MediaMetadata struct { + ID string `gorm:"primarykey"` // The unique ID of this media file + CreatedAt time.Time // When this entry was created + UpdatedAt time.Time // When this entry was last updated + // When this entry was deleted (for soft deletions) + // Soft delete means that this entry still exists in the db, but gorm won't include it anymore unless specifically told to + // If not null, this entry is marked as deleted DeletedAt gorm.DeletedAt `gorm:"index"` Remote bool // whether the attachment is a remote one - Link string // url if remote attachment, identifier if local - Type string // What media type this is, eg image/png - // Whether this media has been cached locally - // Only really used for user and server icons, not attachments - // If true, Link will be read as file path. url otherwise - // Reason: Attachments would take way to much space considering that they are often only loaded a few times at most - // And caching a file for those few times would be a waste of storage - // Caching user and server icons locally however should reduce burden on remote servers by quite a bit though - LocallyCached bool - Sensitive bool // Whether the media is marked as sensitive. If so, hide it in the UI by default + // Where the media is stored. Url if remote file, + Location string + Type string // What media type this is following mime types, eg image/png + // Descriptive name for a media file + // Emote name for example or servername.filetype for a server's icon + Name string } -// Placeholder media file. Acts as placeholder for media file fields that have not been initialised yet but need a value -var placeholderMediaFile = &MediaFile{ - ID: "placeholder", - Remote: false, - Link: "placeholder", // TODO: Replace this with a file path to a staticly included image - Type: "image/png", - LocallyCached: true, +func (s *Storage) NewMediaMetadata(location, mediaType, name string) (*MediaMetadata, error) { + newMedia := MediaMetadata{ + Location: location, + Name: name, + Type: mediaType, + } + s.db.Create(&newMedia) + return nil, nil +} + +func (s *Storage) FuzzyFindMediaMetadataByName(name string) ([]MediaMetadata, error) { + notes := []MediaMetadata{} + err := s.db.Where("name LIKE %?%", name).Find(notes).Error + if err != nil { + return nil, err + } + return notes, nil +} + +func (s *Storage) GetMediaMetadataById(id string) (*MediaMetadata, error) { + media := MediaMetadata{ID: id} + err := s.db.First(&media).Error + if err != nil { + return nil, err + } + return &media, nil +} + +func (s *Storage) FuzzyFindMediaMetadataByLocation(location string) ([]MediaMetadata, error) { + data := []MediaMetadata{} + if err := s.db.Where("location LIKE %?%", location).Find(data).Error; err != nil { + return nil, err + } + return data, nil +} + +func (s *Storage) DeleteMediaMetadataById(id string) error { + return s.db.Delete(MediaMetadata{ID: id}).Error +} + +func (s *Storage) DeleteMediaMetadataByFuzzyLocation(location string) error { + var tmp MediaMetadata + return s.db.Where("location LIKE %?%", location).Delete(&tmp).Error +} + +func (s *Storage) DeleteMediaMetadataByFuzzyName(name string) error { + var tmp MediaMetadata + return s.db.Where("name LIKE %?%", name).Delete(&tmp).Error } diff --git a/storage/mediaProvider/mediaProvider.go b/storage/mediaProvider/mediaProvider.go new file mode 100644 index 0000000..62157aa --- /dev/null +++ b/storage/mediaProvider/mediaProvider.go @@ -0,0 +1,3 @@ +package mediaprovider + +// TODO: Implement me diff --git a/storage/noteTargets.go b/storage/noteTargets.go index af0ed8f..5fcdc82 100644 --- a/storage/noteTargets.go +++ b/storage/noteTargets.go @@ -5,19 +5,32 @@ import ( "errors" ) +// For pretty printing during debug +// If `go generate` is run, it'll generate the necessary function and data for pretty printing +//go:generate stringer -type NoteTarget + +// What feed a note is targeting (public, home, followers or dm) type NoteTarget uint8 const ( + // The note is intended for the public NOTE_TARGET_PUBLIC = NoteTarget(0) - NOTE_TARGET_HOME = NoteTarget(1 << iota) + // The note is intended only for the home screen + // not really any idea what the difference is compared to public + // Maybe home notes don't show up on the server feed but still for everyone's home feed if it reaches them via follow or boost + NOTE_TARGET_HOME = NoteTarget(1 << iota) + // The note is intended only for followers NOTE_TARGET_FOLLOWERS + // The note is intended only for a DM to one or more targets NOTE_TARGET_DM ) +// Converts the NoteTarget value into a type the DB can use func (n *NoteTarget) Value() (driver.Value, error) { return n, nil } +// Converts the raw value from the DB into a NoteTarget func (n *NoteTarget) Scan(value any) error { vBig, ok := value.(int64) if !ok { diff --git a/storage/notes.go b/storage/notes.go index 1a63020..b56daa1 100644 --- a/storage/notes.go +++ b/storage/notes.go @@ -1,17 +1,28 @@ package storage import ( + "fmt" "time" + "github.com/rs/zerolog/log" "gorm.io/gorm" ) +// Note represents an ActivityPub note +// ActivityPub notes can be quite a few things, depending on fields provided. +// A survey, a reply, a quote of another note, etc +// And depending on the origin server of a note, they are treated differently +// with for example rendering or available actions +// This struct attempts to contain all information necessary for easily working with a note type Note struct { - ID string `gorm:"primarykey"` // Make ID a string (uuid) for other implementations - CreatedAt time.Time - UpdatedAt time.Time + ID string `gorm:"primarykey"` // Make ID a string (uuid) for other implementations + CreatedAt time.Time // When this entry was created + UpdatedAt time.Time // When this entry was last updated + // When this entry was deleted (for soft deletions) + // Soft delete means that this entry still exists in the db, but gorm won't include it anymore unless specifically told to + // If not null, this entry is marked as deleted DeletedAt gorm.DeletedAt `gorm:"index"` - Creator string // Full handle of the creator, eg: @max@example.com + Creator string // Id of the author in the db, not the handle Remote bool // Whether the note is originally a remote one and just "cached" // Raw content of the note. So without additional formatting applied // Might already have formatting applied beforehand from the origin server @@ -27,20 +38,88 @@ type Note struct { Tags []string `gorm:"serializer:json"` // Hashtags } -var placeholderNote = &Note{ - ID: "placeholder", - Creator: "placeholder", - Remote: false, - RawContent: "placeholder", - ContentWarning: nil, - Attachments: []string{}, - Emotes: []string{}, - RepliesTo: nil, - Quotes: nil, - Target: NOTE_TARGET_HOME, - Pings: []string{}, - OriginServer: "placeholder", - Tags: []string{}, +func (s *Storage) FindNoteById(id string) (*Note, error) { + note := &Note{} + cacheNote, err := s.cacheNoteIdToData(id) + switch err { + case nil: + return cacheNote, nil + // Empty case, not found in cache means check db + case errCacheNotFound: + default: + return nil, err + } + switch err { + + } + err = s.db.Find(note, id).Error + switch err { + case nil: + if err = s.cache.Set(cacheNoteIdToNotePrefix+id, note); err != nil { + log.Warn().Err(err).Str("note-id", id).Msg("Failed to place note in cache") + } + return note, nil + case gorm.ErrRecordNotFound: + return nil, ErrEntryNotFound + default: + return nil, err + } +} + +func (s *Storage) FindNotesByFuzzyContent(fuzzyContent string) ([]Note, error) { + notes := []Note{} + // TODO: Figure out if cache can be used here too + err := s.db.Where("raw_content LIKE %?%", fuzzyContent).Find(notes).Error + if err != nil { + return nil, err + } + return notes, nil +} + +func (s *Storage) FindNotesByAuthorHandle(handle string) ([]Note, error) { + acc, err := s.FindAccountByFullHandle(handle) + if err != nil { + return nil, fmt.Errorf("account with handle %s not found: %w", handle, err) + } + return s.FindNotesByAuthorId(acc.ID) +} + +func (s *Storage) FindNotesByAuthorId(id string) ([]Note, error) { + notes := []Note{} + err := s.db.Where("creator = ?", id).Find(notes).Error + switch err { + case nil: + return notes, nil + case gorm.ErrRecordNotFound: + return nil, ErrEntryNotFound + default: + return nil, err + } +} + +func (s *Storage) UpdateNote(note *Note) error { + if note == nil || note.ID == "" { + return ErrInvalidData + } + err := s.db.Save(note).Error + if err != nil { + return err + } + err = s.cache.Set(cacheNoteIdToNotePrefix+note.ID, note) + if err != nil { + log.Warn().Err(err).Msg("Failed to update note into cache. Cache and db might be out of sync, a force sync is recommended") + } + return nil +} + +func (s *Storage) CreateNote() (*Note, error) { + // TODO: Think of good arguments and implement me + panic("not implemented") +} + +func (s *Storage) DeleteNote(id string) { + s.cache.Delete(cacheNoteIdToNotePrefix + id) + s.db.Delete(Note{ID: id}) } // Try and find a note with a given ID diff --git a/storage/passkeySessions.go b/storage/passkeySessions.go new file mode 100644 index 0000000..71dfb6a --- /dev/null +++ b/storage/passkeySessions.go @@ -0,0 +1,54 @@ +package storage + +import ( + "github.com/go-webauthn/webauthn/webauthn" + "github.com/google/uuid" + "github.com/rs/zerolog/log" +) + +// Session data used during login attempts with a passkey +// Not actually used afterwards to verify a normal session +// NOTE: Doesn't contain a DeletedAt field, thus deletions are automatically hard and not reversible +type PasskeySession struct { + ID string `gorm:"primarykey"` + Data webauthn.SessionData `gorm:"serializer:json"` +} + +// ---- Section SessionStore + +// Generate some id for a new session. Just returns a new uuid +func (s *Storage) GenSessionID() (string, error) { + x := uuid.NewString() + log.Debug().Str("session-id", x).Msg("Generated new passkey session id") + return x, nil +} + +// Look for an active session with a given id +// Returns the session if found and a bool indicating if a session was found +func (s *Storage) GetSession(sessionId string) (*webauthn.SessionData, bool) { + log.Debug().Str("id", sessionId).Msg("Looking for passkey session") + session := PasskeySession{} + res := s.db.Where("id = ?", sessionId).First(&session) + if res.Error != nil { + return nil, false + } + log.Debug().Str("id", sessionId).Any("webauthn-data", &session).Msg("Found passkey session") + return &session.Data, true +} + +// Save (or update) a session with the new data +func (s *Storage) SaveSession(token string, data *webauthn.SessionData) { + log.Debug().Str("id", token).Any("webauthn-data", data).Msg("Saving passkey session") + session := PasskeySession{ + ID: token, + Data: *data, + } + s.db.Save(&session) +} + +// Delete a session +// NOTE: This is a hard delete since the session struct contains no DeletedAt field +func (s *Storage) DeleteSession(token string) { + log.Debug().Str("id", token).Msg("Deleting passkey session (if one exists)") + s.db.Delete(&PasskeySession{ID: token}) +} diff --git a/storage/remoteServerInfo.go b/storage/remoteServerInfo.go index ab2791b..48a4005 100644 --- a/storage/remoteServerInfo.go +++ b/storage/remoteServerInfo.go @@ -7,9 +7,12 @@ import ( ) type RemoteServer struct { - ID string `gorm:"primarykey"` // ID is also server url - CreatedAt time.Time - UpdatedAt time.Time + ID string `gorm:"primarykey"` // ID is also server url + CreatedAt time.Time // When this entry was created + UpdatedAt time.Time // When this entry was last updated + // When this entry was deleted (for soft deletions) + // Soft delete means that this entry still exists in the db, but gorm won't include it anymore unless specifically told to + // If not null, this entry is marked as deleted DeletedAt gorm.DeletedAt `gorm:"index"` ServerType RemoteServerType // What software the server is running. Useful for formatting Name string // What the server wants to be known as (usually same as url) @@ -17,10 +20,31 @@ type RemoteServer struct { IsSelf bool // Whether this server is yours truly } -var placeholderServer = &RemoteServer{ - ID: "placeholder", - ServerType: REMOTE_SERVER_LINSTROM, - Name: "placeholder", - Icon: "placeholder", - IsSelf: false, +func (s *Storage) FindRemoteServer(url string) (*RemoteServer, error) { + // TODO: Implement me + panic("not implemented") +} + +// Find a remote server with a given display name +func (s *Storage) FindRemoteServerByDisplayName(displayName string) (*RemoteServer, error) { + // TODO: Implement me + panic("not implemented") +} + +// Create a new remote server +func (s *Storage) NewRemoteServer( + url, displayName, icon string, + serverType RemoteServerType, +) (*RemoteServer, error) { + // TODO: Implement me + panic("not implemented") +} + +// Update a remote server with the given url +// If displayName is set, update that +// If icon is set, update that +// Returns the updated version +func (s *Storage) UpdateRemoteServer(url string, displayName, icon *string) (*RemoteServer, error) { + // TODO: Implement me + panic("not implemented") } diff --git a/storage/remoteUser.go b/storage/remoteUser.go new file mode 100644 index 0000000..c2bad26 --- /dev/null +++ b/storage/remoteUser.go @@ -0,0 +1,7 @@ +package storage + +// TODO: More helper stuff + +func (s *Storage) NewRemoteUser(fullHandle string) (*Account, error) { + return nil, nil +} diff --git a/storage/roles.go b/storage/roles.go new file mode 100644 index 0000000..ace6e23 --- /dev/null +++ b/storage/roles.go @@ -0,0 +1,11 @@ +package storage + +type Role struct { + // Name of the role + Name string + // If set, counts as all permissions being set and all restrictions being disabled + FullAdmin bool + // TODO: More control options + // Extend upon whatever Masto, Akkoma and Misskey have + // Lots of details please +} diff --git a/storage/serverTypes.go b/storage/serverTypes.go index 5c5b1f2..f69166c 100644 --- a/storage/serverTypes.go +++ b/storage/serverTypes.go @@ -5,6 +5,10 @@ import ( "errors" ) +// TODO: Decide whether to turn this into an int too to save resources +// And then use go:generate instead for pretty printing + +// What software a server is running // Mostly important for rendering type RemoteServerType string diff --git a/storage/storage.go b/storage/storage.go index 9ccddeb..625dbee 100644 --- a/storage/storage.go +++ b/storage/storage.go @@ -1,3 +1,8 @@ +// TODO: Unify function names + +// Storage is the handler for cache and db access +// It handles storing various data in the database as well as caching that data +// Said data includes notes, accounts, metadata about media files, servers and similar package storage import ( @@ -5,6 +10,7 @@ import ( "fmt" "github.com/glebarez/sqlite" + "gitlab.com/mstarongitlab/linstrom/storage/cache" "gorm.io/driver/postgres" "gorm.io/gorm" ) @@ -12,69 +18,52 @@ import ( // Storage is responsible for all database, cache and media related actions // and serves as the lowest layer of the cake type Storage struct { - db *gorm.DB + db *gorm.DB + cache *cache.Cache } -var ErrAccountNotFound = errors.New("account not found") +var ErrInvalidData = errors.New("invalid data") // Build a new storage using sqlite as database backend -func NewStorageSqlite(filePath string) (*Storage, error) { +func NewStorageSqlite(filePath string, cache *cache.Cache) (*Storage, error) { db, err := gorm.Open(sqlite.Open(filePath)) if err != nil { return nil, err } - return storageFromEmptyDb(db) + return storageFromEmptyDb(db, cache) } -func NewStoragePostgres(dbUrl string) (*Storage, error) { +func NewStoragePostgres(dbUrl string, cache *cache.Cache) (*Storage, error) { db, err := gorm.Open(postgres.Open(dbUrl)) if err != nil { return nil, err } - return storageFromEmptyDb(db) + return storageFromEmptyDb(db, cache) } -func storageFromEmptyDb(db *gorm.DB) (*Storage, error) { +func storageFromEmptyDb(db *gorm.DB, cache *cache.Cache) (*Storage, error) { // AutoMigrate ensures the db is in a state where all the structs given here // have their own tables and relations setup. It also updates tables if necessary - if err := db.AutoMigrate( - placeholderMediaFile, - placeholderUser, - placeholderNote, - placeholderServer, - ); err != nil { - return nil, fmt.Errorf("problem while auto migrating: %w", err) - } - // Afterwards add the placeholder entries for each table. - // FirstOrCreate either creates a new entry or retrieves the first matching one - // We only care about the creation if there is none yet, so no need to carry the result over - if res := db.FirstOrCreate(placeholderMediaFile); res.Error != nil { - return nil, fmt.Errorf("failed to add placeholder media file: %w", res.Error) - } - if res := db.FirstOrCreate(placeholderUser()); res.Error != nil { - return nil, fmt.Errorf("failed to add placeholder media file: %w", res.Error) - } - if res := db.FirstOrCreate(placeholderNote); res.Error != nil { - return nil, fmt.Errorf("failed to add placeholder media file: %w", res.Error) - } - if res := db.FirstOrCreate(placeholderServer); res.Error != nil { - return nil, fmt.Errorf("failed to add placeholder media file: %w", res.Error) + err := db.AutoMigrate( + MediaMetadata{}, + Account{}, + RemoteServer{}, + Note{}, + Role{}, + PasskeySession{}, + ) + if err != nil { + return nil, err } // And finally, build the actual storage struct return &Storage{ - db: db, + db: db, + cache: cache, }, nil } -func (s *Storage) FindLocalAccount(handle string) (*User, error) { - acc := User{} - res := s.db.Where("handle = ?", handle).First(&acc) - if res.Error != nil { - return nil, fmt.Errorf("failed to query db: %w", res.Error) - } - if res.RowsAffected == 0 { - return nil, ErrAccountNotFound - } - return nil, errors.New("unimplemented") +// TODO: Placeholder. Update to proper implementation later. Including signature +func (s *Storage) FindLocalAccount(handle string) (string, error) { + return handle, nil } diff --git a/storage/user.go b/storage/user.go index 5d9dcc8..8ec499c 100644 --- a/storage/user.go +++ b/storage/user.go @@ -1,24 +1,34 @@ package storage import ( + "crypto/rand" "errors" - "fmt" "strings" "time" "github.com/go-webauthn/webauthn/webauthn" - "github.com/google/uuid" + "github.com/mstarongithub/passkey" + "github.com/rs/zerolog/log" + "gitlab.com/mstarongitlab/linstrom/ap" + "gitlab.com/mstarongitlab/linstrom/config" "gorm.io/gorm" ) // Database representation of a user account // This can be a bot, remote or not // If remote, this is used for caching the account -type User struct { - ID string `gorm:"primarykey"` // ID is a uuid for this account - Handle string // Handle is the full handle, eg @max@example.com - CreatedAt time.Time // When this entry was created - UpdatedAt time.Time // When this account was last updated. Will also be used for refreshing remote accounts +type Account struct { + ID string `gorm:"primarykey"` // ID is a uuid for this account + // Handle of the user (eg "max" if the full username is @max@example.com) + // Assume unchangable (once set by a user) to be kind to other implementations + // Would be an easy avenue to fuck with them though + Handle string + CreatedAt time.Time // When this entry was created. Automatically set by gorm + // When this account was last updated. Will also be used for refreshing remote accounts. Automatically set by gorm + UpdatedAt time.Time + // When this entry was deleted (for soft deletions) + // Soft delete means that this entry still exists in the db, but gorm won't include it anymore unless specifically told to + // If not null, this entry is marked as deleted DeletedAt gorm.DeletedAt `gorm:"index"` Remote bool // Whether the account is a local or remote one Server string // The url of the server this account is from @@ -39,110 +49,292 @@ type User struct { RestrictedFollow bool // List of things the owner identifies as // Example [cat human robot] means that the owner probably identifies as - // a cyborg-catgirl/boy/human - IdentifiesAs []Being + // a cyborg-catgirl/boy/human or a cathuman shaped robot, refer to Gender for pronouns + IdentifiesAs []Being `gorm:"serializer:json"` // List of pronouns the owner identifies with // An unordered list since the owner can freely set it // Examples: [she her], [it they its them] - Gender []string + Gender []string `gorm:"serializer:json"` + // The roles assocciated with an account + Roles []string `gorm:"serializer:json"` // --- And internal account stuff --- // Still public fields since they wouldn't be able to be stored in the db otherwise - - PasswordHash []byte // Hash of the user's password - TotpToken []byte // Token for totp verification - // All the registered passkeys, name of passkey to credentials - // Could this be exported to another table? Yes - // Would it make sense? Probably not - // Will just take the performance hit of json conversion - // Access should be rare enough anyway - Passkeys map[string]webauthn.Credential `gorm:"serializer:json"` - PrivateKeyPem *string // The private key of the account. Nil if remote user - // Restrictions applied to the account - // Flag value, can be multiple - Restrictions AccountRestriction + PrivateKeyPem *string // The private key of the account. Nil if remote user + WebAuthnId []byte // The unique and random ID of this account used for passkey authentication + // Whether the account got verified and is allowed to be active + // For local accounts being active means being allowed to login and perform interactions + // For remote users, if an account is not verified, any interactions it sends are discarded + Verified bool + PasskeyCredentials []webauthn.Credential `gorm:"serializer:json"` // Webauthn credentials data + // Has a RemoteAccountLinks included if remote user + RemoteLinks *RemoteAccountLinks } -var placeholderUser = &User{ - ID: "placeholder", - Handle: "placeholder", - Remote: false, - Server: "placeholder", - DisplayName: "placeholder", - CustomFields: []uint{}, - Description: "placeholder", - Tags: []string{}, - IsBot: true, - Follows: []string{}, - Followers: []string{}, - Icon: "placeholder", - Background: "placeholder", - Banner: "placeholder", - Indexable: false, - PublicKeyPem: nil, - RestrictedFollow: false, - IdentifiesAs: []Being{BEING_ROBOT}, - Gender: []string{"it", "its"}, - PasswordHash: []byte("placeholder"), - TotpToken: []byte("placeholder"), - Passkeys: map[string]webauthn.Credential{}, - PrivateKeyPem: nil, +// Contains static and cached info about a remote account, mostly links +type RemoteAccountLinks struct { + // ---- Section: gorm + // Sets this struct up as a value that an Account may have + gorm.Model + AccountID string + + // Just about every link here is optional to accomodate for servers with only minimal accounts + // Minimal being handle, ap link and inbox + ApLink string + ViewLink *string + FollowersLink *string + FollowingLink *string + InboxLink string + OutboxLink *string + FeaturedLink *string + FeaturedTagsLink *string } -func NewEmptyUser() *User { - return &User{ - ID: uuid.NewString(), - Handle: "placeholder", - Remote: false, - Server: "placeholder", - DisplayName: "placeholder", - CustomFields: []uint{}, - Description: "placeholder", - Tags: []string{}, - IsBot: true, - Follows: []string{}, - Followers: []string{}, - Icon: "placeholder", - Background: "placeholder", - Banner: "placeholder", - Indexable: false, - PublicKeyPem: nil, - RestrictedFollow: false, - IdentifiesAs: []Being{BEING_ROBOT}, - Gender: []string{"it", "its"}, - PasswordHash: []byte("placeholder"), - TotpToken: []byte("placeholder"), - Passkeys: map[string]webauthn.Credential{}, - PrivateKeyPem: nil, +// Find an account in the db using a given full handle (@max@example.com) +// Returns an account and nil if an account is found, otherwise nil and the error +func (s *Storage) FindAccountByFullHandle(handle string) (*Account, error) { + log.Trace().Caller().Send() + log.Debug().Str("account-handle", handle).Msg("Looking for account by handle") + log.Debug().Str("account-handle", handle).Msg("Checking if there's a cache hit") + + // Try and find the account in cache first + cacheAccId, err := s.cacheHandleToAccUid(handle) + if err == nil { + log.Info().Str("account-handle", handle).Msg("Hit account handle in cache") + // Then always load via id since unique key access should be faster than string matching + return s.FindAccountById(*cacheAccId) + } else { + if !errors.Is(err, errCacheNotFound) { + log.Error().Err(err).Str("account-handle", handle).Msg("Problem while checking cache for account") + return nil, err + } } -} -// Get a stored user by the ID the user has been stored with -// Either returns a valid user and nil for the error -// Or nil for the user and an error -func (s *Storage) GetUserByID(id string) (*User, error) { - user := User{} - res := s.db.First(&user, "id = ?", id) - // Check if any error except NotFound occured - // If so, wrap it a little - if res.Error != nil && !errors.Is(res.Error, gorm.ErrRecordNotFound) { - return nil, fmt.Errorf("problem while getting user from db: %w", res.Error) + // Failed to find in cache, go the slow route of hitting the db + log.Debug().Str("account-handle", handle).Msg("Didn't hit account in cache, going to db") + name, server, err := ap.SplitFullHandle(handle) + if err != nil { + log.Warn().Err(err).Str("account-handle", handle).Msg("Failed to split up account handle") + return nil, err } - // Then check if an error occured and said error is NotFound - // If it is, just pass it forward - if res.Error != nil && errors.Is(res.Error, gorm.ErrRecordNotFound) { + + acc := Account{} + res := s.db.Where("name = ?", name).Where("server = ?", server).First(&acc) + if res.Error != nil { + if errors.Is(res.Error, gorm.ErrRecordNotFound) { + log.Info().Str("account-handle", handle).Msg("Account with handle not found") + } else { + log.Error().Err(err).Str("account-handle", handle).Msg("Failed to get account with handle") + } return nil, res.Error } - return &user, nil + log.Info().Str("account-handle", handle).Msg("Found account, also inserting into cache") + if err = s.cache.Set(cacheUserIdToAccPrefix+acc.ID, &acc); err != nil { + log.Warn(). + Err(err). + Str("account-handle", handle). + Msg("Found account but failed to insert into cache") + } + if err = s.cache.Set(cacheUserHandleToIdPrefix+strings.TrimLeft(handle, "@"), acc.ID); err != nil { + log.Warn(). + Err(err). + Str("account-handle", handle). + Msg("Failed to store handle to id in cache") + } + return &acc, nil } -// Get only the name part of the handle a user has -func (u *User) GetHandleNameOnly() string { - // First remove the leading @ (TrimPrefix) to achieve a format of username@server - // Then split at the @ to the server and user seperately - // And return the first element since that is the username - // Note: Getting the first element will always be safe - // since trim returns a string guaranteed (empty is ok) - // and if Split doesn't do anything (eg empty string) it just returns the input in the first elemen it just returns the input in the first element - return strings.Split(strings.TrimPrefix(u.Handle, "@"), "@")[0] +// Find an account given a specific ID +func (s *Storage) FindAccountById(id string) (*Account, error) { + log.Trace().Caller().Send() + log.Debug().Str("account-id", id).Msg("Looking for account by id") + log.Debug().Str("account-id", id).Msg("First trying to hit cache") + acc, err := s.cacheAccIdToData(id) + if err == nil { + log.Info().Str("account-id", id).Msg("Found account in cache") + return acc, nil + } else if !errors.Is(err, errCacheNotFound) { + log.Error().Err(err).Str("account-id", id).Msg("Error while looking for account in cache") + return nil, err + } + + log.Debug().Str("account-id", id).Msg("Didn't hit account in cache, checking db") + res := s.db.First(acc, id) + if res.Error != nil { + if errors.Is(res.Error, gorm.ErrRecordNotFound) { + log.Warn().Str("account-id", id).Msg("Account not found") + } else { + log.Error().Err(res.Error).Str("account-id", id).Msg("Failed to look for account") + } + return nil, res.Error + } + log.Info().Str("account-id", id).Msg("Found account in db, also adding to cache") + if err = s.cache.Set(cacheUserIdToAccPrefix+id, acc); err != nil { + log.Warn().Err(err).Str("account-id", id).Msg("Failed to add account to cache") + } + return acc, nil +} + +// Update a given account in storage and cache +func (s *Storage) UpdateAccount(acc *Account) error { + // If the account is nil or doesn't have an id, error out + if acc == nil || acc.ID == "" { + return ErrInvalidData + } + res := s.db.Save(acc) + if res.Error != nil { + return res.Error + } + if err := s.cache.Set(cacheUserIdToAccPrefix+acc.ID, acc); err != nil { + return err + } + return nil +} + +// Create a new empty account for future use +func (s *Storage) NewEmptyAccount() (*Account, error) { + log.Trace().Caller().Send() + log.Debug().Msg("Creating new empty account") + acc := Account{} + // Generate the 64 bit id for passkey and webauthn stuff + data := make([]byte, 64) + c, err := rand.Read(data) + for err != nil || c != len(data) || c < 64 { + data = make([]byte, 64) + c, err = rand.Read(data) + } + acc.WebAuthnId = data + acc.Followers = []string{} + acc.Tags = []string{} + acc.Follows = []string{} + acc.Gender = []string{} + acc.CustomFields = []uint{} + acc.IdentifiesAs = []Being{} + acc.PasskeyCredentials = []webauthn.Credential{} + res := s.db.Save(acc) + if res.Error != nil { + log.Error().Err(res.Error).Msg("Failed to safe new account") + return nil, res.Error + } + log.Info().Str("account-id", acc.ID).Msg("Created new account") + return &acc, nil +} + +// Create a new local account using the given handle +// The handle in this case is only the part before the domain (example: @bob@example.com would have a handle of bob) +// It also sets up a bunch of values that tend to be obvious for local accounts +func (s *Storage) NewLocalAccount(handle string) (*Account, error) { + log.Trace().Caller().Send() + log.Debug().Str("account-handle", handle).Msg("Creating new local account") + acc, err := s.NewEmptyAccount() + if err != nil { + log.Error().Err(err).Msg("Failed to create empty account for use") + return nil, err + } + acc.Handle = handle + acc.Server = config.GlobalConfig.General.GetFullDomain() + acc.Remote = false + acc.DisplayName = handle + + log.Debug(). + Str("account-handle", handle). + Str("account-id", acc.ID). + Msg("Saving new local account") + res := s.db.Save(acc) + if res.Error != nil { + log.Error().Err(res.Error).Any("account-full", acc).Msg("Failed to save local account") + return nil, res.Error + } + log.Info(). + Str("account-handle", handle). + Str("account-id", acc.ID). + Msg("Created new local account") + return acc, nil +} + +// ---- Section WebAuthn.User +// Implements the webauthn.User interface for interaction with passkeys + +func (a *Account) WebAuthnID() []byte { + log.Trace().Caller().Send() + return a.WebAuthnId +} + +func (u *Account) WebAuthnName() string { + log.Trace().Caller().Send() + return u.Handle +} + +func (u *Account) WebAuthnDisplayName() string { + log.Trace().Caller().Send() + return u.DisplayName +} + +func (u *Account) WebAuthnCredentials() []webauthn.Credential { + log.Trace().Caller().Send() + return u.PasskeyCredentials +} + +func (u *Account) WebAuthnIcon() string { + log.Trace().Caller().Send() + return "" +} + +// ---- Section passkey.User +// Implements the passkey.User interface + +func (u *Account) PutCredential(new webauthn.Credential) { + log.Trace().Caller().Send() + u.PasskeyCredentials = append(u.PasskeyCredentials, new) +} + +// Section passkey.UserStore +// Implements the passkey.UserStore interface + +func (s *Storage) GetOrCreateUser(userID string) passkey.User { + log.Trace().Caller().Send() + log.Debug(). + Str("account-handle", userID). + Msg("Looking for or creating account for passkey stuff") + acc := &Account{} + res := s.db.Where(Account{Handle: userID, Server: config.GlobalConfig.General.GetFullDomain()}). + First(acc) + if errors.Is(res.Error, gorm.ErrRecordNotFound) { + log.Debug().Str("account-handle", userID) + var err error + acc, err = s.NewLocalAccount(userID) + if err != nil { + log.Error(). + Err(err). + Str("account-handle", userID). + Msg("Failed to create new account for webauthn request") + return nil + } + } + return acc +} + +func (s *Storage) GetUserByWebAuthnId(id []byte) passkey.User { + log.Trace().Caller().Send() + log.Debug().Bytes("webauthn-id", id).Msg("Looking for account with webauthn id") + acc := Account{} + res := s.db.Where(Account{WebAuthnId: id}).First(&acc) + if res.Error != nil { + log.Error(). + Err(res.Error). + Bytes("webauthn-id", id). + Msg("Failed to find user with webauthn ID") + return nil + } + log.Info().Msg("Found account with given webauthn id") + return &acc +} + +func (s *Storage) SaveUser(rawUser passkey.User) { + log.Trace().Caller().Send() + user, ok := rawUser.(*Account) + if !ok { + log.Error().Any("raw-user", rawUser).Msg("Failed to cast raw user to db account") + } + s.db.Save(user) } diff --git a/storage/userIdentType.go b/storage/userIdentType.go index 8d94d61..3693853 100644 --- a/storage/userIdentType.go +++ b/storage/userIdentType.go @@ -1,10 +1,6 @@ package storage -import ( - "database/sql/driver" - "errors" -) - +// What kind of being a user identifies as type Being string const ( @@ -15,16 +11,3 @@ const ( BEING_ROBOT = Being("robot") BEING_DOLL = Being("doll") ) - -func (r *Being) Value() (driver.Value, error) { - return r, nil -} - -func (r *Being) Scan(raw any) error { - if v, ok := raw.(string); ok { - *r = Being(v) - return nil - } else { - return errors.New("value not a string") - } -} diff --git a/storage/userInfoFields.go b/storage/userInfoFields.go index a3dc5ea..59d95b3 100644 --- a/storage/userInfoFields.go +++ b/storage/userInfoFields.go @@ -6,9 +6,12 @@ import ( "gorm.io/gorm" ) +// Describes a custom attribute field for accounts type UserInfoField struct { gorm.Model // Can actually just embed this as is here as those are not something directly exposed :3 Name string Value string LastUrlCheckDate *time.Time // Used if the value is an url to somewhere. Empty if value is not an url } + +// TODO: Add functions to store, load, update and delete these