Merge branch 'main' of gitlab.com:mstarongitlab/linstrom
also delete server-old with all its problems
This commit is contained in:
commit
d99efca667
47 changed files with 2200 additions and 694 deletions
6
.gitignore
vendored
6
.gitignore
vendored
|
@ -3,6 +3,7 @@ db.sqlite
|
||||||
.env
|
.env
|
||||||
.lapce/
|
.lapce/
|
||||||
*/tmp.*
|
*/tmp.*
|
||||||
|
<<<<<<< HEAD
|
||||||
/frontend/
|
/frontend/
|
||||||
|
|
||||||
|
|
||||||
|
@ -32,3 +33,8 @@ db.sqlite
|
||||||
|
|
||||||
# broccoli-debug
|
# broccoli-debug
|
||||||
/frontend-src/DEBUG/
|
/frontend-src/DEBUG/
|
||||||
|
=======
|
||||||
|
|
||||||
|
config.toml
|
||||||
|
.air.toml
|
||||||
|
>>>>>>> 0ec0ad42bca3a05b05256862904285bb74e314cb
|
||||||
|
|
26
Containerfile
Normal file
26
Containerfile
Normal file
|
@ -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"
|
85
ap/getRemoteUser.go
Normal file
85
ap/getRemoteUser.go
Normal file
|
@ -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
|
||||||
|
}
|
19
ap/util.go
Normal file
19
ap/util.go
Normal file
|
@ -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
|
||||||
|
}
|
87
ap/webfinger.go
Normal file
87
ap/webfinger.go
Normal file
|
@ -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 <domain>/.well-known/webfinger
|
||||||
|
// The url parameter `resource` tells the remote server what data we want, should always be an account
|
||||||
|
// The prefix `acct:<full-handle>` (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
|
||||||
|
}
|
184
config/config.go
184
config/config.go
|
@ -1,11 +1,12 @@
|
||||||
package config
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/BurntSushi/toml"
|
"github.com/BurntSushi/toml"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
"gitlab.com/mstarongitlab/goutils/other"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ConfigSSL struct {
|
type ConfigSSL struct {
|
||||||
|
@ -19,25 +20,44 @@ type ConfigSSL struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type ConfigGeneral struct {
|
type ConfigGeneral struct {
|
||||||
Domain string `toml:"domain"` // The domain this server operates under
|
Protocol string `toml:"protocol"` // The protocol with which to access the server publicly (http/https)
|
||||||
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
|
// The subdomain under which the server lives (example: "linstrom" if the full domain is linstrom.example.com)
|
||||||
PublicPort int `toml:"public_port"` // The public facing port, usually either 80 or 443
|
Subdomain *string `toml:"subdomain"`
|
||||||
PrivatePort int `toml:"private_port"` // The port the server should launch at
|
// The root domain under which the server lives (example: "example.com" if the full domain is linstrom.example.com)
|
||||||
// Explanation:
|
Domain string `toml:"domain"`
|
||||||
// The public port is the port to connect to from outside the server to access it.
|
// The port on which the server runs on
|
||||||
// The private port is where the server will open for itself on launch
|
PrivatePort int `toml:"private_port"`
|
||||||
// Important for reverse proxies like nginx or traeffik
|
// The port under which the public can reach the server (useful if running behind a reverse proxy)
|
||||||
// Where you the public port would be either 80 (http) or 443 (https), but the private one could be 3000 for example
|
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 {
|
type ConfigAdmin struct {
|
||||||
|
// Name of the server's root admin account
|
||||||
Username string `toml:"username"`
|
Username string `toml:"username"`
|
||||||
PasswordHash string `toml:"password_hash"`
|
// 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 {
|
type ConfigStorage struct {
|
||||||
IsPostgres bool `toml:"is_postgres"`
|
// Url to the database to use
|
||||||
Uri string `toml:"uri"`
|
// 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 {
|
type ConfigMail struct {
|
||||||
|
@ -56,24 +76,20 @@ type Config struct {
|
||||||
General ConfigGeneral `toml:"general"`
|
General ConfigGeneral `toml:"general"`
|
||||||
SSL ConfigSSL `toml:"ssl"`
|
SSL ConfigSSL `toml:"ssl"`
|
||||||
Admin ConfigAdmin `toml:"admin"`
|
Admin ConfigAdmin `toml:"admin"`
|
||||||
|
Webauthn ConfigWebAuthn `toml:"webauthn"`
|
||||||
Storage ConfigStorage `toml:"storage"`
|
Storage ConfigStorage `toml:"storage"`
|
||||||
Mail ConfigMail `toml:"mail"`
|
Mail ConfigMail `toml:"mail"`
|
||||||
}
|
}
|
||||||
|
|
||||||
const DEFAULT_CONFIG_FILE_PATH = "config.toml"
|
var GlobalConfig Config
|
||||||
|
|
||||||
// "Global" variable for accessing the config
|
var defaultConfig Config = 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{
|
|
||||||
General: ConfigGeneral{
|
General: ConfigGeneral{
|
||||||
|
Protocol: "http",
|
||||||
|
Subdomain: nil,
|
||||||
Domain: "localhost",
|
Domain: "localhost",
|
||||||
PublicPort: 8080,
|
|
||||||
PrivatePort: 8080,
|
PrivatePort: 8080,
|
||||||
|
PublicPort: nil,
|
||||||
},
|
},
|
||||||
SSL: ConfigSSL{
|
SSL: ConfigSSL{
|
||||||
HandleSSL: false,
|
HandleSSL: false,
|
||||||
|
@ -82,54 +98,94 @@ var defaultConfig = Config{
|
||||||
AdminMail: nil,
|
AdminMail: nil,
|
||||||
},
|
},
|
||||||
Admin: ConfigAdmin{
|
Admin: ConfigAdmin{
|
||||||
Username: "admin",
|
Username: "server-admin",
|
||||||
PasswordHash: "", // No password
|
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{
|
Storage: ConfigStorage{
|
||||||
IsPostgres: false,
|
DatabaseUrl: "db.sqlite",
|
||||||
Uri: "db.sqlite",
|
DbIsPostgres: other.IntoPointer(false),
|
||||||
|
RedisUrl: nil,
|
||||||
|
MaxInMemoryCacheSize: 1e6, // 1 Megabyte
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
func LoadConfigFromFile(file string, tryDefaultPath, saveIntoGlobal bool) (*Config, error) {
|
func (gc *ConfigGeneral) GetFullDomain() string {
|
||||||
conf := &Config{}
|
if gc.Subdomain != nil {
|
||||||
data, err := os.ReadFile(file)
|
return *gc.Subdomain + gc.Domain
|
||||||
if err != nil && !tryDefaultPath {
|
|
||||||
return nil, fmt.Errorf("failed to read config %s: %w", file, err)
|
|
||||||
}
|
}
|
||||||
if err != nil && tryDefaultPath {
|
return gc.Domain
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadFromDefaultPath() (*Config, error) {
|
func (gc *ConfigGeneral) GetFullPublicUrl() string {
|
||||||
data, err := os.ReadFile(DEFAULT_CONFIG_FILE_PATH)
|
str := gc.Protocol + gc.GetFullDomain()
|
||||||
if err != nil {
|
if gc.PublicPort != nil {
|
||||||
return nil, fmt.Errorf(
|
str += fmt.Sprint(*gc.PublicPort)
|
||||||
"failed to load default config path %s: %w",
|
} else {
|
||||||
DEFAULT_CONFIG_FILE_PATH,
|
str += fmt.Sprint(gc.PrivatePort)
|
||||||
err,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
conf := Config{}
|
return str
|
||||||
err = toml.Unmarshal(data, &conf)
|
}
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf(
|
func WriteDefaultConfig(toFile string) error {
|
||||||
"failed to parse file content of %s as toml: %w",
|
log.Trace().Caller().Send()
|
||||||
DEFAULT_CONFIG_FILE_PATH,
|
log.Info().Str("config-file", toFile).Msg("Writing default config to file")
|
||||||
err,
|
file, err := os.Create(toFile)
|
||||||
)
|
if err != nil {
|
||||||
}
|
log.Error().
|
||||||
return &conf, nil
|
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
|
||||||
}
|
}
|
||||||
|
|
25
flags.go
Normal file
25
flags.go
Normal file
|
@ -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()
|
||||||
|
}
|
11
frontend-noscript/index.html
Normal file
11
frontend-noscript/index.html
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
<html>
|
||||||
|
<!--
|
||||||
|
TODO: Implement me
|
||||||
|
-->
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Will contain templates for use in the no js frontend version
|
||||||
|
The no js frontend will be minimal and likely just barely usable, so not ever the default
|
||||||
|
-->
|
||||||
|
|
||||||
|
</html>
|
8
future-useful-libs.md
Normal file
8
future-useful-libs.md
Normal file
|
@ -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)
|
60
go.mod
60
go.mod
|
@ -1,28 +1,57 @@
|
||||||
module gitlab.com/mstarongitlab/linstrom
|
module gitlab.com/mstarongitlab/linstrom
|
||||||
|
|
||||||
go 1.22.5
|
go 1.23
|
||||||
|
|
||||||
|
toolchain go1.23.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/BurntSushi/toml v1.4.0
|
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/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/google/uuid v1.6.0
|
||||||
github.com/piprate/json-gold v0.5.0
|
github.com/mstarongithub/passkey v0.0.0-20240817142622-de6912c8303e
|
||||||
github.com/sethvargo/go-limiter v1.0.0
|
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
|
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/driver/postgres v1.5.7
|
||||||
gorm.io/gorm v1.25.10
|
gorm.io/gorm v1.25.10
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
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/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/glebarez/go-sqlite v1.21.2 // indirect
|
||||||
github.com/go-test/deep v1.1.1 // 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/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/pgpassfile v1.0.0 // indirect
|
||||||
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
|
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
|
||||||
github.com/jackc/pgx/v5 v5.4.3 // 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-colorable v0.1.13 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.19 // indirect
|
github.com/mattn/go-isatty v0.0.19 // indirect
|
||||||
github.com/mitchellh/mapstructure v1.5.0 // 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/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/toorop/go-dkim v0.0.0-20201103131630-e1cd1a0a5208 // indirect
|
||||||
github.com/x448/float16 v0.8.4 // indirect
|
github.com/x448/float16 v0.8.4 // indirect
|
||||||
gitlab.com/mstarongitlab/goutils v1.2.0 // indirect
|
golang.org/x/crypto v0.26.0 // indirect
|
||||||
golang.org/x/crypto v0.21.0 // indirect
|
golang.org/x/sys v0.23.0 // indirect
|
||||||
golang.org/x/sys v0.19.0 // indirect
|
golang.org/x/text v0.17.0 // indirect
|
||||||
golang.org/x/text v0.15.0 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
modernc.org/libc v1.22.5 // indirect
|
modernc.org/libc v1.22.5 // indirect
|
||||||
modernc.org/mathutil v1.5.0 // indirect
|
modernc.org/mathutil v1.5.0 // indirect
|
||||||
modernc.org/memory v1.5.0 // indirect
|
modernc.org/memory v1.5.0 // indirect
|
||||||
|
|
561
go.sum
561
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 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0=
|
||||||
github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
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/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.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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/dgraph-io/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 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
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/eko/gocache/lib/v4 v4.1.6 h1:5WWIGISKhE7mfkyF+SJyWwqa4Dp2mkdX8QsZpnENqJI=
|
||||||
github.com/fxamacker/cbor/v2 v2.6.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ=
|
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 h1:3a6LFC4sKahUunAmynQKLZceZCOzUthkRkEAl9gAXWo=
|
||||||
github.com/glebarez/go-sqlite v1.21.2/go.mod h1:sfxdZyhQjTM2Wry3gVYWaW072Ri1WMdWJi0k6+3382k=
|
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 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw=
|
||||||
github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ=
|
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 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U=
|
||||||
github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
|
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.11.2 h1:Fgx0/wlmkClTKlnOsdOQ+K5HcHDsDcYIvtYmfhEOSUc=
|
||||||
github.com/go-webauthn/webauthn v0.10.2/go.mod h1:Gd1IDsGAybuvK1NkwUTLbGmeksxuRJjVN2PE/xsPxHs=
|
github.com/go-webauthn/webauthn v0.11.2/go.mod h1:aOtudaF94pM71g3jRwTYYwQTG1KyTILTcZqN1srkmD0=
|
||||||
github.com/go-webauthn/x v0.1.9 h1:v1oeLmoaa+gPOaZqUdDentu6Rl7HkSSsmOT6gxEQHhE=
|
github.com/go-webauthn/x v0.1.14 h1:1wrB8jzXAofojJPAaRxnZhRgagvLGnLjhCAwg3kTpT0=
|
||||||
github.com/go-webauthn/x v0.1.9/go.mod h1:pJNMlIMP1SU7cN8HNlKJpLEnFHCygLCvaLZ8a1xeoQA=
|
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/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 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
|
||||||
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
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/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58=
|
||||||
github.com/google/go-tpm v0.9.0/go.mod h1:FkNVkc6C+IsvDI9Jw1OveJmxGZUUaKxtrpOS47QWKfU=
|
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 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
|
||||||
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo=
|
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 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
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 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
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=
|
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/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 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||||
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
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 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
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.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 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
|
||||||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
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 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||||
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
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 h1:RmGh1PYboCFcchVFuh2pbSWAZy4XJaqTMU4KQYsApbM=
|
||||||
github.com/piprate/json-gold v0.5.0/go.mod h1:WZ501QQMbZZ+3pXFPhQKzNwS1+jls0oqov3uQ2WasLs=
|
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/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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
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 h1:J9b7z+QKAmPf4YLrFg6oQUotqHQeUNWwkvo7jZp1GLU=
|
||||||
github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35/go.mod h1:prYjPmNq4d1NPVmpShWobRqXY3q7Vp+80DqgxxUrUIA=
|
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-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 h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
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/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 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8=
|
||||||
github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
|
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/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||||
github.com/sethvargo/go-limiter v1.0.0/go.mod h1:01b6tW25Ap+MeLYBuD4aHunMrJoNO5PVUFdS9rac3II=
|
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
|
||||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
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.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.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 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
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/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 h1:ouGy/Ww4kuaqu2E2UrDw7SvLaziWTB60ICLkIkNVccA=
|
||||||
github.com/xhit/go-simple-mail/v2 v2.16.0/go.mod h1:b7P5ygho6SYE+VIqpxA6QkYfv4teeyG4MKqB3utRu98=
|
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=
|
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
gitlab.com/mstarongitlab/block-things-middleware v0.0.0-20240722113247-31e2984cb9d5/go.mod h1:XKgioEQc65Hhx9hd/DbkwbDv0PJv+oYlAOIZt2TCvHw=
|
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
gitlab.com/mstarongitlab/goutils v1.2.0 h1:hVpc2VikWkgmX7Gbey9I72eqgmg/6GcKZ4q+M9ZBd0E=
|
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
gitlab.com/mstarongitlab/goutils v1.2.0/go.mod h1:SvqfzFxgashuZPqR9kPwQ9gFA7I1yskZjhmGmY2pAow=
|
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||||
golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA=
|
gitlab.com/mstarongitlab/goap v1.1.0 h1:uN05RP+Tq2NR2IuPq6XQa5oLpfailpoEvxo1SfehxBA=
|
||||||
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
|
gitlab.com/mstarongitlab/goap v1.1.0/go.mod h1:rt9IYvJBPh1z6t+vvzifmxDtGjGlr8683tSPfa5dbXI=
|
||||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
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-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.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.12.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.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM=
|
||||||
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
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 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.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 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
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/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 h1:dQpO+33KalOA+aFYGlK+EfxcI5MbO7EP2yYygwh9h+s=
|
||||||
gorm.io/gorm v1.25.10/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
|
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 h1:91BNch/e5B0uPbJFgqbxXuOnxBQjlS//icfQEGmvyjE=
|
||||||
modernc.org/libc v1.22.5/go.mod h1:jj+Z7dTNX8fBScMVNRAYZ/jF91K8fdT2hYMThc3YjBY=
|
modernc.org/libc v1.22.5/go.mod h1:jj+Z7dTNX8fBScMVNRAYZ/jF91K8fdT2hYMThc3YjBY=
|
||||||
modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ=
|
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/memory v1.5.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU=
|
||||||
modernc.org/sqlite v1.23.1 h1:nrSBg4aRQQwq59JpvGEQ15tNxoO5pX/kUjcRNwSAGQM=
|
modernc.org/sqlite v1.23.1 h1:nrSBg4aRQQwq59JpvGEQ15tNxoO5pX/kUjcRNwSAGQM=
|
||||||
modernc.org/sqlite v1.23.1/go.mod h1:OrDj17Mggn6MhE+iPbBNf7RGKODDE9NFT0f3EwDzJqk=
|
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=
|
||||||
|
|
26
goals.md
Normal file
26
goals.md
Normal file
|
@ -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
|
90
main.go
Normal file
90
main.go
Normal file
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
50
mod-tools.md
Normal file
50
mod-tools.md
Normal file
|
@ -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)
|
21
outgoingEventQueue/queue.go
Normal file
21
outgoingEventQueue/queue.go
Normal file
|
@ -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
|
||||||
|
*/
|
|
@ -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,
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -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) {
|
|
||||||
|
|
||||||
}
|
|
|
@ -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
|
|
|
@ -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)
|
|
||||||
}
|
|
|
@ -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))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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))
|
|
||||||
})
|
|
||||||
}
|
|
|
@ -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)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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,
|
|
||||||
}
|
|
||||||
}
|
|
38
server/endpoints_ap.go
Normal file
38
server/endpoints_ap.go
Normal file
|
@ -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)
|
||||||
|
}
|
13
server/remoteServer/remoteServer.go
Normal file
13
server/remoteServer/remoteServer.go
Normal file
|
@ -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
|
73
storage/cache.go
Normal file
73
storage/cache.go
Normal file
|
@ -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
|
||||||
|
}
|
90
storage/cache/cache.go
vendored
Normal file
90
storage/cache/cache.go
vendored
Normal file
|
@ -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)
|
||||||
|
}
|
164
storage/cache/coderPools.go
vendored
Normal file
164
storage/cache/coderPools.go
vendored
Normal file
|
@ -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()
|
||||||
|
}
|
35
storage/cache/lockedCoders.go
vendored
Normal file
35
storage/cache/lockedCoders.go
vendored
Normal file
|
@ -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,
|
||||||
|
}
|
||||||
|
}
|
11
storage/errors.go
Normal file
11
storage/errors.go
Normal file
|
@ -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")
|
6
storage/housekeeping.go
Normal file
6
storage/housekeeping.go
Normal file
|
@ -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
|
|
@ -6,29 +6,72 @@ import (
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
type MediaFile struct {
|
// MediaMetadata contains metadata about some media
|
||||||
ID string `gorm:"primarykey"`
|
// Metadata includes whether it's a remote file or not, what the name is,
|
||||||
CreatedAt time.Time
|
// the MIME type, and an identifier pointing to its location
|
||||||
UpdatedAt time.Time
|
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"`
|
DeletedAt gorm.DeletedAt `gorm:"index"`
|
||||||
Remote bool // whether the attachment is a remote one
|
Remote bool // whether the attachment is a remote one
|
||||||
Link string // url if remote attachment, identifier if local
|
// Where the media is stored. Url if remote file,
|
||||||
Type string // What media type this is, eg image/png
|
Location string
|
||||||
// Whether this media has been cached locally
|
Type string // What media type this is following mime types, eg image/png
|
||||||
// Only really used for user and server icons, not attachments
|
// Descriptive name for a media file
|
||||||
// If true, Link will be read as file path. url otherwise
|
// Emote name for example or servername.filetype for a server's icon
|
||||||
// Reason: Attachments would take way to much space considering that they are often only loaded a few times at most
|
Name string
|
||||||
// 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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Placeholder media file. Acts as placeholder for media file fields that have not been initialised yet but need a value
|
func (s *Storage) NewMediaMetadata(location, mediaType, name string) (*MediaMetadata, error) {
|
||||||
var placeholderMediaFile = &MediaFile{
|
newMedia := MediaMetadata{
|
||||||
ID: "placeholder",
|
Location: location,
|
||||||
Remote: false,
|
Name: name,
|
||||||
Link: "placeholder", // TODO: Replace this with a file path to a staticly included image
|
Type: mediaType,
|
||||||
Type: "image/png",
|
}
|
||||||
LocallyCached: true,
|
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
|
||||||
}
|
}
|
||||||
|
|
3
storage/mediaProvider/mediaProvider.go
Normal file
3
storage/mediaProvider/mediaProvider.go
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
package mediaprovider
|
||||||
|
|
||||||
|
// TODO: Implement me
|
|
@ -5,19 +5,32 @@ import (
|
||||||
"errors"
|
"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
|
type NoteTarget uint8
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
// The note is intended for the public
|
||||||
NOTE_TARGET_PUBLIC = NoteTarget(0)
|
NOTE_TARGET_PUBLIC = NoteTarget(0)
|
||||||
|
// 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)
|
NOTE_TARGET_HOME = NoteTarget(1 << iota)
|
||||||
|
// The note is intended only for followers
|
||||||
NOTE_TARGET_FOLLOWERS
|
NOTE_TARGET_FOLLOWERS
|
||||||
|
// The note is intended only for a DM to one or more targets
|
||||||
NOTE_TARGET_DM
|
NOTE_TARGET_DM
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Converts the NoteTarget value into a type the DB can use
|
||||||
func (n *NoteTarget) Value() (driver.Value, error) {
|
func (n *NoteTarget) Value() (driver.Value, error) {
|
||||||
return n, nil
|
return n, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Converts the raw value from the DB into a NoteTarget
|
||||||
func (n *NoteTarget) Scan(value any) error {
|
func (n *NoteTarget) Scan(value any) error {
|
||||||
vBig, ok := value.(int64)
|
vBig, ok := value.(int64)
|
||||||
if !ok {
|
if !ok {
|
||||||
|
|
113
storage/notes.go
113
storage/notes.go
|
@ -1,17 +1,28 @@
|
||||||
package storage
|
package storage
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
"gorm.io/gorm"
|
"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 {
|
type Note struct {
|
||||||
ID string `gorm:"primarykey"` // Make ID a string (uuid) for other implementations
|
ID string `gorm:"primarykey"` // Make ID a string (uuid) for other implementations
|
||||||
CreatedAt time.Time
|
CreatedAt time.Time // When this entry was created
|
||||||
UpdatedAt time.Time
|
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"`
|
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"
|
Remote bool // Whether the note is originally a remote one and just "cached"
|
||||||
// Raw content of the note. So without additional formatting applied
|
// Raw content of the note. So without additional formatting applied
|
||||||
// Might already have formatting applied beforehand from the origin server
|
// Might already have formatting applied beforehand from the origin server
|
||||||
|
@ -27,20 +38,88 @@ type Note struct {
|
||||||
Tags []string `gorm:"serializer:json"` // Hashtags
|
Tags []string `gorm:"serializer:json"` // Hashtags
|
||||||
}
|
}
|
||||||
|
|
||||||
var placeholderNote = &Note{
|
func (s *Storage) FindNoteById(id string) (*Note, error) {
|
||||||
ID: "placeholder",
|
note := &Note{}
|
||||||
Creator: "placeholder",
|
cacheNote, err := s.cacheNoteIdToData(id)
|
||||||
Remote: false,
|
switch err {
|
||||||
RawContent: "placeholder",
|
case nil:
|
||||||
ContentWarning: nil,
|
return cacheNote, nil
|
||||||
Attachments: []string{},
|
// Empty case, not found in cache means check db
|
||||||
Emotes: []string{},
|
case errCacheNotFound:
|
||||||
RepliesTo: nil,
|
default:
|
||||||
Quotes: nil,
|
return nil, err
|
||||||
Target: NOTE_TARGET_HOME,
|
}
|
||||||
Pings: []string{},
|
switch err {
|
||||||
OriginServer: "placeholder",
|
|
||||||
Tags: []string{},
|
}
|
||||||
|
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
|
// Try and find a note with a given ID
|
||||||
|
|
54
storage/passkeySessions.go
Normal file
54
storage/passkeySessions.go
Normal file
|
@ -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})
|
||||||
|
}
|
|
@ -8,8 +8,11 @@ import (
|
||||||
|
|
||||||
type RemoteServer struct {
|
type RemoteServer struct {
|
||||||
ID string `gorm:"primarykey"` // ID is also server url
|
ID string `gorm:"primarykey"` // ID is also server url
|
||||||
CreatedAt time.Time
|
CreatedAt time.Time // When this entry was created
|
||||||
UpdatedAt time.Time
|
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"`
|
DeletedAt gorm.DeletedAt `gorm:"index"`
|
||||||
ServerType RemoteServerType // What software the server is running. Useful for formatting
|
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)
|
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
|
IsSelf bool // Whether this server is yours truly
|
||||||
}
|
}
|
||||||
|
|
||||||
var placeholderServer = &RemoteServer{
|
func (s *Storage) FindRemoteServer(url string) (*RemoteServer, error) {
|
||||||
ID: "placeholder",
|
// TODO: Implement me
|
||||||
ServerType: REMOTE_SERVER_LINSTROM,
|
panic("not implemented")
|
||||||
Name: "placeholder",
|
}
|
||||||
Icon: "placeholder",
|
|
||||||
IsSelf: false,
|
// 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")
|
||||||
}
|
}
|
||||||
|
|
7
storage/remoteUser.go
Normal file
7
storage/remoteUser.go
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
package storage
|
||||||
|
|
||||||
|
// TODO: More helper stuff
|
||||||
|
|
||||||
|
func (s *Storage) NewRemoteUser(fullHandle string) (*Account, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
11
storage/roles.go
Normal file
11
storage/roles.go
Normal file
|
@ -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
|
||||||
|
}
|
|
@ -5,6 +5,10 @@ import (
|
||||||
"errors"
|
"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
|
// Mostly important for rendering
|
||||||
type RemoteServerType string
|
type RemoteServerType string
|
||||||
|
|
||||||
|
|
|
@ -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
|
package storage
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
@ -5,6 +10,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/glebarez/sqlite"
|
"github.com/glebarez/sqlite"
|
||||||
|
"gitlab.com/mstarongitlab/linstrom/storage/cache"
|
||||||
"gorm.io/driver/postgres"
|
"gorm.io/driver/postgres"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
@ -13,68 +19,51 @@ import (
|
||||||
// and serves as the lowest layer of the cake
|
// and serves as the lowest layer of the cake
|
||||||
type Storage struct {
|
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
|
// 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))
|
db, err := gorm.Open(sqlite.Open(filePath))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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))
|
db, err := gorm.Open(postgres.Open(dbUrl))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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
|
// 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
|
// have their own tables and relations setup. It also updates tables if necessary
|
||||||
if err := db.AutoMigrate(
|
err := db.AutoMigrate(
|
||||||
placeholderMediaFile,
|
MediaMetadata{},
|
||||||
placeholderUser,
|
Account{},
|
||||||
placeholderNote,
|
RemoteServer{},
|
||||||
placeholderServer,
|
Note{},
|
||||||
); err != nil {
|
Role{},
|
||||||
return nil, fmt.Errorf("problem while auto migrating: %w", err)
|
PasskeySession{},
|
||||||
}
|
)
|
||||||
// Afterwards add the placeholder entries for each table.
|
if err != nil {
|
||||||
// FirstOrCreate either creates a new entry or retrieves the first matching one
|
return nil, err
|
||||||
// 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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// And finally, build the actual storage struct
|
// And finally, build the actual storage struct
|
||||||
return &Storage{
|
return &Storage{
|
||||||
db: db,
|
db: db,
|
||||||
|
cache: cache,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Storage) FindLocalAccount(handle string) (*User, error) {
|
// TODO: Placeholder. Update to proper implementation later. Including signature
|
||||||
acc := User{}
|
func (s *Storage) FindLocalAccount(handle string) (string, error) {
|
||||||
res := s.db.Where("handle = ?", handle).First(&acc)
|
return handle, nil
|
||||||
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")
|
|
||||||
}
|
}
|
||||||
|
|
382
storage/user.go
382
storage/user.go
|
@ -1,24 +1,34 @@
|
||||||
package storage
|
package storage
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/rand"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/go-webauthn/webauthn/webauthn"
|
"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"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Database representation of a user account
|
// Database representation of a user account
|
||||||
// This can be a bot, remote or not
|
// This can be a bot, remote or not
|
||||||
// If remote, this is used for caching the account
|
// If remote, this is used for caching the account
|
||||||
type User struct {
|
type Account struct {
|
||||||
ID string `gorm:"primarykey"` // ID is a uuid for this account
|
ID string `gorm:"primarykey"` // ID is a uuid for this account
|
||||||
Handle string // Handle is the full handle, eg @max@example.com
|
// Handle of the user (eg "max" if the full username is @max@example.com)
|
||||||
CreatedAt time.Time // When this entry was created
|
// Assume unchangable (once set by a user) to be kind to other implementations
|
||||||
UpdatedAt time.Time // When this account was last updated. Will also be used for refreshing remote accounts
|
// 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"`
|
DeletedAt gorm.DeletedAt `gorm:"index"`
|
||||||
Remote bool // Whether the account is a local or remote one
|
Remote bool // Whether the account is a local or remote one
|
||||||
Server string // The url of the server this account is from
|
Server string // The url of the server this account is from
|
||||||
|
@ -39,110 +49,292 @@ type User struct {
|
||||||
RestrictedFollow bool
|
RestrictedFollow bool
|
||||||
// List of things the owner identifies as
|
// List of things the owner identifies as
|
||||||
// Example [cat human robot] means that the owner probably identifies as
|
// Example [cat human robot] means that the owner probably identifies as
|
||||||
// a cyborg-catgirl/boy/human
|
// a cyborg-catgirl/boy/human or a cathuman shaped robot, refer to Gender for pronouns
|
||||||
IdentifiesAs []Being
|
IdentifiesAs []Being `gorm:"serializer:json"`
|
||||||
// List of pronouns the owner identifies with
|
// List of pronouns the owner identifies with
|
||||||
// An unordered list since the owner can freely set it
|
// An unordered list since the owner can freely set it
|
||||||
// Examples: [she her], [it they its them]
|
// 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 ---
|
// --- And internal account stuff ---
|
||||||
// Still public fields since they wouldn't be able to be stored in the db otherwise
|
// 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
|
PrivateKeyPem *string // The private key of the account. Nil if remote user
|
||||||
// Restrictions applied to the account
|
WebAuthnId []byte // The unique and random ID of this account used for passkey authentication
|
||||||
// Flag value, can be multiple
|
// Whether the account got verified and is allowed to be active
|
||||||
Restrictions AccountRestriction
|
// 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{
|
// Contains static and cached info about a remote account, mostly links
|
||||||
ID: "placeholder",
|
type RemoteAccountLinks struct {
|
||||||
Handle: "placeholder",
|
// ---- Section: gorm
|
||||||
Remote: false,
|
// Sets this struct up as a value that an Account may have
|
||||||
Server: "placeholder",
|
gorm.Model
|
||||||
DisplayName: "placeholder",
|
AccountID string
|
||||||
CustomFields: []uint{},
|
|
||||||
Description: "placeholder",
|
// Just about every link here is optional to accomodate for servers with only minimal accounts
|
||||||
Tags: []string{},
|
// Minimal being handle, ap link and inbox
|
||||||
IsBot: true,
|
ApLink string
|
||||||
Follows: []string{},
|
ViewLink *string
|
||||||
Followers: []string{},
|
FollowersLink *string
|
||||||
Icon: "placeholder",
|
FollowingLink *string
|
||||||
Background: "placeholder",
|
InboxLink string
|
||||||
Banner: "placeholder",
|
OutboxLink *string
|
||||||
Indexable: false,
|
FeaturedLink *string
|
||||||
PublicKeyPem: nil,
|
FeaturedTagsLink *string
|
||||||
RestrictedFollow: false,
|
|
||||||
IdentifiesAs: []Being{BEING_ROBOT},
|
|
||||||
Gender: []string{"it", "its"},
|
|
||||||
PasswordHash: []byte("placeholder"),
|
|
||||||
TotpToken: []byte("placeholder"),
|
|
||||||
Passkeys: map[string]webauthn.Credential{},
|
|
||||||
PrivateKeyPem: nil,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewEmptyUser() *User {
|
// Find an account in the db using a given full handle (@max@example.com)
|
||||||
return &User{
|
// Returns an account and nil if an account is found, otherwise nil and the error
|
||||||
ID: uuid.NewString(),
|
func (s *Storage) FindAccountByFullHandle(handle string) (*Account, error) {
|
||||||
Handle: "placeholder",
|
log.Trace().Caller().Send()
|
||||||
Remote: false,
|
log.Debug().Str("account-handle", handle).Msg("Looking for account by handle")
|
||||||
Server: "placeholder",
|
log.Debug().Str("account-handle", handle).Msg("Checking if there's a cache hit")
|
||||||
DisplayName: "placeholder",
|
|
||||||
CustomFields: []uint{},
|
// Try and find the account in cache first
|
||||||
Description: "placeholder",
|
cacheAccId, err := s.cacheHandleToAccUid(handle)
|
||||||
Tags: []string{},
|
if err == nil {
|
||||||
IsBot: true,
|
log.Info().Str("account-handle", handle).Msg("Hit account handle in cache")
|
||||||
Follows: []string{},
|
// Then always load via id since unique key access should be faster than string matching
|
||||||
Followers: []string{},
|
return s.FindAccountById(*cacheAccId)
|
||||||
Icon: "placeholder",
|
} else {
|
||||||
Background: "placeholder",
|
if !errors.Is(err, errCacheNotFound) {
|
||||||
Banner: "placeholder",
|
log.Error().Err(err).Str("account-handle", handle).Msg("Problem while checking cache for account")
|
||||||
Indexable: false,
|
return nil, err
|
||||||
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,
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// 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)
|
|
||||||
}
|
}
|
||||||
// Then check if an error occured and said error is NotFound
|
|
||||||
// If it is, just pass it forward
|
// Failed to find in cache, go the slow route of hitting the db
|
||||||
if res.Error != nil && errors.Is(res.Error, gorm.ErrRecordNotFound) {
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
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 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
|
// Find an account given a specific ID
|
||||||
func (u *User) GetHandleNameOnly() string {
|
func (s *Storage) FindAccountById(id string) (*Account, error) {
|
||||||
// First remove the leading @ (TrimPrefix) to achieve a format of username@server
|
log.Trace().Caller().Send()
|
||||||
// Then split at the @ to the server and user seperately
|
log.Debug().Str("account-id", id).Msg("Looking for account by id")
|
||||||
// And return the first element since that is the username
|
log.Debug().Str("account-id", id).Msg("First trying to hit cache")
|
||||||
// Note: Getting the first element will always be safe
|
acc, err := s.cacheAccIdToData(id)
|
||||||
// since trim returns a string guaranteed (empty is ok)
|
if err == nil {
|
||||||
// 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
|
log.Info().Str("account-id", id).Msg("Found account in cache")
|
||||||
return strings.Split(strings.TrimPrefix(u.Handle, "@"), "@")[0]
|
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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,6 @@
|
||||||
package storage
|
package storage
|
||||||
|
|
||||||
import (
|
// What kind of being a user identifies as
|
||||||
"database/sql/driver"
|
|
||||||
"errors"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Being string
|
type Being string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -15,16 +11,3 @@ const (
|
||||||
BEING_ROBOT = Being("robot")
|
BEING_ROBOT = Being("robot")
|
||||||
BEING_DOLL = Being("doll")
|
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")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -6,9 +6,12 @@ import (
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Describes a custom attribute field for accounts
|
||||||
type UserInfoField struct {
|
type UserInfoField struct {
|
||||||
gorm.Model // Can actually just embed this as is here as those are not something directly exposed :3
|
gorm.Model // Can actually just embed this as is here as those are not something directly exposed :3
|
||||||
Name string
|
Name string
|
||||||
Value string
|
Value string
|
||||||
LastUrlCheckDate *time.Time // Used if the value is an url to somewhere. Empty if value is not an url
|
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
|
||||||
|
|
Loading…
Reference in a new issue