Compare commits
67 Commits
inprogress
...
0e3c76ee3a
| Author | SHA1 | Date | |
|---|---|---|---|
|
0e3c76ee3a
|
|||
|
70c6996f39
|
|||
|
0f665d3484
|
|||
|
d67d768c1e
|
|||
| 5c06b824e9 | |||
| aef282e19e | |||
| 84f95c1dcd | |||
| a8c7b51518 | |||
| 42b10fab54 | |||
| dc0752153b | |||
| 7d1b8bcc4c | |||
| 7139fd9e42 | |||
| ee55e6b7ed | |||
| 9f44b4e6ab | |||
| ed6ffdcb9f | |||
| 452de79614 | |||
| be08f900a1 | |||
|
|
111b24435d
|
||
|
|
5b9b436125
|
||
| 7c4c1521fd | |||
|
|
12a3e7b1ff
|
||
|
|
b8c2f9f510
|
||
|
|
9168f5c7bc
|
||
|
|
6ee995298b
|
||
|
|
fcc5d08aa7
|
||
|
|
d27eb21ffb
|
||
|
|
8ba2846e62
|
||
|
|
f5e4c4f972
|
||
|
ebd46555ac
|
|||
|
0621e2e815
|
|||
|
|
3f3ea4439c
|
||
|
|
0f504a98e9
|
||
|
|
f9a088269c
|
||
|
|
2b9f0ffa12
|
||
|
|
c7f56789de
|
||
|
|
7d78cfb10a
|
||
|
|
2fb3fae61f
|
||
|
|
5b09ea3e96
|
||
|
|
352443f172
|
||
|
|
17569cfb2b
|
||
|
|
0810ec73a0
|
||
|
|
4fc9c78dae
|
||
|
|
250395d6b3
|
||
|
|
7315759b20
|
||
|
|
bbb45c8854
|
||
|
|
5e31d27c54
|
||
|
|
4b4d3b743d
|
||
|
|
92b813c9ed
|
||
|
|
165492e3ec
|
||
|
|
cb6abfc4ba
|
||
|
|
5bebbc473a
|
||
|
|
4baeb07cb7
|
||
|
|
03df7902ad
|
||
|
|
ab9561f8b3
|
||
|
|
12716ae972
|
||
|
|
6569c58e37
|
||
|
|
5bf052580f
|
||
|
|
9dd9443bc9
|
||
|
|
7ae2a8ad25
|
||
|
|
e118d06985
|
||
|
|
33bba9d8f7
|
||
|
|
e7df4cc479
|
||
|
|
129968d179
|
||
|
|
9878ed4a79
|
||
|
|
9103de501b
|
||
|
|
6425fcadcd
|
||
|
|
4d3c5df454 |
14
.air.toml
14
.air.toml
@@ -1,23 +1,23 @@
|
|||||||
root = "."
|
root = "."
|
||||||
testdata_dir = "data"
|
testdata_dir = "testdata"
|
||||||
tmp_dir = "tmp"
|
tmp_dir = "tmp"
|
||||||
|
|
||||||
[build]
|
[build]
|
||||||
args_bin = []
|
args_bin = []
|
||||||
bin = "./tmp/main"
|
bin = "./tmp/ev"
|
||||||
cmd = "go build -o ./tmp/main ."
|
cmd = "go build -o ./tmp/ev ./cmd/ev"
|
||||||
delay = 1000
|
delay = 1000
|
||||||
exclude_dir = ["assets", "tmp", "vendor", "testdata"]
|
exclude_dir = ["assets", "tmp", "vendor", "testdata", "data", "build"]
|
||||||
exclude_file = []
|
exclude_file = []
|
||||||
exclude_regex = ["_test.go"]
|
exclude_regex = ["_test.go"]
|
||||||
exclude_unchanged = false
|
exclude_unchanged = false
|
||||||
follow_symlink = false
|
follow_symlink = false
|
||||||
full_bin = ""
|
full_bin = ""
|
||||||
include_dir = []
|
include_dir = []
|
||||||
include_ext = ["go", "tpl", "tmpl", "html"]
|
include_ext = ["go", "tpl", "tmpl", "html", "css"]
|
||||||
kill_delay = "1s"
|
kill_delay = "0s"
|
||||||
log = "build-errors.log"
|
log = "build-errors.log"
|
||||||
send_interrupt = true
|
send_interrupt = false
|
||||||
stop_on_error = true
|
stop_on_error = true
|
||||||
|
|
||||||
[color]
|
[color]
|
||||||
|
|||||||
5
.ansible/inventory
Normal file
5
.ansible/inventory
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
[lavana]
|
||||||
|
lavana.sour.is
|
||||||
|
|
||||||
|
[kapha]
|
||||||
|
kapha.sour.is
|
||||||
16
.ansible/playbook.yml
Normal file
16
.ansible/playbook.yml
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
---
|
||||||
|
- name: Deploy EV
|
||||||
|
hosts: lavana
|
||||||
|
|
||||||
|
tasks:
|
||||||
|
- name: Copy build to remote
|
||||||
|
ansible.builtin.copy:
|
||||||
|
src: ev
|
||||||
|
dest: /usr/local/bin/ev
|
||||||
|
owner: root
|
||||||
|
group: root
|
||||||
|
- name: Restart service
|
||||||
|
systemd:
|
||||||
|
name: ev
|
||||||
|
enabled: true
|
||||||
|
state: restarted
|
||||||
51
.drone.yml
Normal file
51
.drone.yml
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
kind: pipeline
|
||||||
|
type: docker
|
||||||
|
name: build
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: test
|
||||||
|
image: golang:1.20
|
||||||
|
commands:
|
||||||
|
- go test -v -race -skip '^TestE2E|TestMain' ./...
|
||||||
|
|
||||||
|
trigger:
|
||||||
|
event:
|
||||||
|
- push
|
||||||
|
- pull_request
|
||||||
|
|
||||||
|
---
|
||||||
|
kind: pipeline
|
||||||
|
type: docker
|
||||||
|
name: deploy
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: build
|
||||||
|
image: golang:1.20
|
||||||
|
environment:
|
||||||
|
GOOS: linux
|
||||||
|
GOARCH: amd64
|
||||||
|
commands:
|
||||||
|
- go build -ldflags "-s -w" -o build/ev ./cmd/ev
|
||||||
|
|
||||||
|
- name: compress
|
||||||
|
image: gruebel/upx:latest
|
||||||
|
commands:
|
||||||
|
- upx --best --lzma -o .ansible/ev build/ev
|
||||||
|
|
||||||
|
- name: deploy
|
||||||
|
image: plugins/ansible:3
|
||||||
|
settings:
|
||||||
|
playbook: .ansible/playbook.yml
|
||||||
|
inventory: .ansible/inventory
|
||||||
|
become: true
|
||||||
|
become_method: sudo
|
||||||
|
ssh_common_args: -p 65535
|
||||||
|
user: deploy
|
||||||
|
private_key:
|
||||||
|
from_secret: drone_ssh
|
||||||
|
|
||||||
|
trigger:
|
||||||
|
event:
|
||||||
|
- promote
|
||||||
|
target:
|
||||||
|
- production
|
||||||
25
.github/workflows/go.yml
vendored
Normal file
25
.github/workflows/go.yml
vendored
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
name: Go
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ "main" ]
|
||||||
|
pull_request:
|
||||||
|
branches: [ "main" ]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Set up Go
|
||||||
|
uses: actions/setup-go@v3
|
||||||
|
with:
|
||||||
|
go-version: 1.19
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: go build -v ./...
|
||||||
|
|
||||||
|
- name: Test
|
||||||
|
run: go test --race -v ./...
|
||||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -5,3 +5,8 @@ data/
|
|||||||
local.mk
|
local.mk
|
||||||
logzio.yml
|
logzio.yml
|
||||||
tmp/
|
tmp/
|
||||||
|
/build
|
||||||
|
/ev
|
||||||
|
acct.yml
|
||||||
|
.DS_Store
|
||||||
|
/*.yaml
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1 +0,0 @@
|
|||||||
local.mk:d632b22a2291637331e5613d35536c69e696447ce407d7320b4c5ab0922b47a9
|
|
||||||
11
LICENSE
Normal file
11
LICENSE
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
Copyright 2023 Jon Lundy <me@sour.is>
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
|
||||||
|
|
||||||
|
1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
|
||||||
|
|
||||||
|
2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
|
||||||
|
|
||||||
|
3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS “AS IS” AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
22
Makefile
22
Makefile
@@ -1,18 +1,21 @@
|
|||||||
export PATH:=$(shell go env GOPATH)/bin:$(PATH)
|
export PATH:=$(shell go env GOPATH)/bin:$(PATH)
|
||||||
export EV_DATA=mem:
|
export EV_DATA=mem:
|
||||||
export EV_HTTP=:8080
|
export EV_HTTP=:8080
|
||||||
export EV_TRACE_SAMPLE=always
|
export WEBFINGER_DOMAINS=localhost
|
||||||
export EV_TRACE_ENDPOINT=localhost:4318
|
|
||||||
|
.DEFAULT_GOAL := air
|
||||||
|
|
||||||
-include local.mk
|
-include local.mk
|
||||||
|
|
||||||
|
|
||||||
air: gen
|
air: gen
|
||||||
ifeq (, $(shell which air))
|
ifeq (, $(shell which air))
|
||||||
go install github.com/cosmtrek/air@latest
|
go install github.com/cosmtrek/air@latest
|
||||||
endif
|
endif
|
||||||
air
|
air ./cmd/ev
|
||||||
|
|
||||||
run:
|
run:
|
||||||
go run .
|
go build ./cmd/ev && ./ev
|
||||||
|
|
||||||
test:
|
test:
|
||||||
go test -cover -race ./...
|
go test -cover -race ./...
|
||||||
@@ -25,6 +28,8 @@ GQLS:=$(GQLS) $(wildcard app/*/*.graphqls)
|
|||||||
GQLS:=$(GQLS) $(wildcard app/*/*.go)
|
GQLS:=$(GQLS) $(wildcard app/*/*.go)
|
||||||
GQLSRC=internal/graph/generated/generated.go
|
GQLSRC=internal/graph/generated/generated.go
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm -f "$(GQLSRC)"
|
||||||
gen: gql
|
gen: gql
|
||||||
gql: $(GQLSRC)
|
gql: $(GQLSRC)
|
||||||
$(GQLSRC): $(GQLS)
|
$(GQLSRC): $(GQLS)
|
||||||
@@ -33,12 +38,3 @@ ifeq (, $(shell which gqlgen))
|
|||||||
endif
|
endif
|
||||||
gqlgen
|
gqlgen
|
||||||
|
|
||||||
|
|
||||||
EV_HOST?=localhost:8080
|
|
||||||
load:
|
|
||||||
watch -n .1 "http POST $(EV_HOST)/inbox/asdf/test a=b one=1 two:='{\"v\":2}' | jq"
|
|
||||||
|
|
||||||
bi:
|
|
||||||
go build .
|
|
||||||
sudo mv ev /usr/local/bin/
|
|
||||||
sudo systemctl restart ev
|
|
||||||
|
|||||||
3
app/README.md
Normal file
3
app/README.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# App examples
|
||||||
|
|
||||||
|
These applications are to demonstrate how the EV library can be used.
|
||||||
@@ -1,22 +1,23 @@
|
|||||||
scalar Time
|
scalar Time
|
||||||
scalar Map
|
scalar Map
|
||||||
|
|
||||||
type Connection @goModel(model: "github.com/sour-is/ev/pkg/gql.Connection") {
|
type Connection @goModel(model: "go.sour.is/pkg/gql.Connection") {
|
||||||
paging: PageInfo!
|
paging: PageInfo!
|
||||||
edges: [Edge!]!
|
edges: [Edge!]!
|
||||||
}
|
}
|
||||||
input PageInput @goModel(model: "github.com/sour-is/ev/pkg/gql.PageInput") {
|
input PageInput @goModel(model: "go.sour.is/pkg/gql.PageInput") {
|
||||||
idx: Int = 0
|
after: Int = 0
|
||||||
|
before: Int
|
||||||
count: Int = 30
|
count: Int = 30
|
||||||
}
|
}
|
||||||
type PageInfo @goModel(model: "github.com/sour-is/ev/pkg/gql.PageInfo") {
|
type PageInfo @goModel(model: "go.sour.is/pkg/gql.PageInfo") {
|
||||||
next: Boolean!
|
next: Boolean!
|
||||||
prev: Boolean!
|
prev: Boolean!
|
||||||
|
|
||||||
begin: Int!
|
begin: Int!
|
||||||
end: Int!
|
end: Int!
|
||||||
}
|
}
|
||||||
interface Edge @goModel(model: "github.com/sour-is/ev/pkg/gql.Edge"){
|
interface Edge @goModel(model: "go.sour.is/pkg/gql.Edge"){
|
||||||
id: ID!
|
id: ID!
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
package playground
|
|
||||||
|
|
||||||
import (
|
|
||||||
"html/template"
|
|
||||||
"net/http"
|
|
||||||
)
|
|
||||||
|
|
||||||
var page = template.Must(template.New("graphiql").Parse(`<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta charset=utf-8/>
|
|
||||||
<meta name="viewport" content="user-scalable=no, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, minimal-ui">
|
|
||||||
<link rel="shortcut icon" href="https://graphcool-playground.netlify.com/favicon.png">
|
|
||||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/graphql-playground-react@{{ .version }}/build/static/css/index.css"
|
|
||||||
integrity="{{ .cssSRI }}" crossorigin="anonymous"/>
|
|
||||||
<link rel="shortcut icon" href="https://cdn.jsdelivr.net/npm/graphql-playground-react@{{ .version }}/build/favicon.png"
|
|
||||||
integrity="{{ .faviconSRI }}" crossorigin="anonymous"/>
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/graphql-playground-react@{{ .version }}/build/static/js/middleware.js"
|
|
||||||
integrity="{{ .jsSRI }}" crossorigin="anonymous"></script>
|
|
||||||
<title>{{.title}}</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<style type="text/css">
|
|
||||||
html { font-family: "Open Sans", sans-serif; overflow: hidden; }
|
|
||||||
body { margin: 0; background: #172a3a; }
|
|
||||||
</style>
|
|
||||||
<div id="root"/>
|
|
||||||
<script type="text/javascript">
|
|
||||||
window.addEventListener('load', function (event) {
|
|
||||||
const root = document.getElementById('root');
|
|
||||||
root.classList.add('playgroundIn');
|
|
||||||
const wsProto = location.protocol == 'https:' ? 'wss:' : 'ws:'
|
|
||||||
GraphQLPlayground.init(root, {
|
|
||||||
endpoint: location.protocol + '//' + location.host + '{{.endpoint}}',
|
|
||||||
subscriptionsEndpoint: wsProto + '//' + location.host + '{{.endpoint }}',
|
|
||||||
shareEnabled: true,
|
|
||||||
settings: {
|
|
||||||
'request.credentials': 'same-origin'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
`))
|
|
||||||
|
|
||||||
func Handler(title string, endpoint string) http.HandlerFunc {
|
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
w.Header().Add("Content-Type", "text/html")
|
|
||||||
err := page.Execute(w, map[string]string{
|
|
||||||
"title": title,
|
|
||||||
"endpoint": endpoint,
|
|
||||||
"version": "1.7.26",
|
|
||||||
"cssSRI": "sha256-dKnNLEFwKSVFpkpjRWe+o/jQDM6n/JsvQ0J3l5Dk3fc=",
|
|
||||||
"faviconSRI": "sha256-GhTyE+McTU79R4+pRO6ih+4TfsTOrpPwD8ReKFzb3PM=",
|
|
||||||
"jsSRI": "sha256-SG9YAy4eywTcLckwij7V4oSCG3hOdV1m+2e1XuNxIgk=",
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -2,58 +2,21 @@ package gql
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
"reflect"
|
|
||||||
"runtime/debug"
|
|
||||||
|
|
||||||
"github.com/99designs/gqlgen/graphql/handler"
|
"github.com/99designs/gqlgen/graphql"
|
||||||
"github.com/ravilushqa/otelgqlgen"
|
"go.sour.is/pkg/gql"
|
||||||
"github.com/sour-is/ev/app/gql/playground"
|
"go.sour.is/pkg/gql/resolver"
|
||||||
"github.com/sour-is/ev/app/msgbus"
|
|
||||||
"github.com/sour-is/ev/app/salty"
|
"go.sour.is/ev/app/msgbus"
|
||||||
"github.com/sour-is/ev/internal/graph/generated"
|
"go.sour.is/ev/app/salty"
|
||||||
"github.com/sour-is/ev/internal/lg"
|
"go.sour.is/ev/internal/graph/generated"
|
||||||
"github.com/sour-is/ev/pkg/es"
|
gql_es "go.sour.is/ev/pkg/gql"
|
||||||
"github.com/sour-is/ev/pkg/gql"
|
|
||||||
"github.com/vektah/gqlparser/v2/gqlerror"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Resolver struct {
|
type Resolver struct {
|
||||||
msgbus.MsgbusResolver
|
msgbus.MsgbusResolver
|
||||||
salty.SaltyResolver
|
salty.SaltyResolver
|
||||||
es.EventResolver
|
gql_es.EventResolver
|
||||||
}
|
|
||||||
|
|
||||||
func New(ctx context.Context, resolvers ...interface{ RegisterHTTP(*http.ServeMux) }) (*Resolver, error) {
|
|
||||||
_, span := lg.Span(ctx)
|
|
||||||
defer span.End()
|
|
||||||
|
|
||||||
r := &Resolver{}
|
|
||||||
v := reflect.ValueOf(r)
|
|
||||||
v = reflect.Indirect(v)
|
|
||||||
noop := reflect.ValueOf(&noop{})
|
|
||||||
|
|
||||||
outer:
|
|
||||||
for _, idx := range reflect.VisibleFields(v.Type()) {
|
|
||||||
field := v.FieldByIndex(idx.Index)
|
|
||||||
|
|
||||||
for i := range resolvers {
|
|
||||||
rs := reflect.ValueOf(resolvers[i])
|
|
||||||
|
|
||||||
if field.IsNil() && rs.Type().Implements(field.Type()) {
|
|
||||||
span.AddEvent(fmt.Sprint("found ", field.Type().Name()))
|
|
||||||
field.Set(rs)
|
|
||||||
continue outer
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
span.AddEvent(fmt.Sprint("default ", field.Type().Name()))
|
|
||||||
field.Set(noop)
|
|
||||||
}
|
|
||||||
|
|
||||||
return r, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Query returns generated.QueryResolver implementation.
|
// Query returns generated.QueryResolver implementation.
|
||||||
@@ -65,68 +28,39 @@ func (r *Resolver) Mutation() generated.MutationResolver { return r }
|
|||||||
// Subscription returns generated.SubscriptionResolver implementation.
|
// Subscription returns generated.SubscriptionResolver implementation.
|
||||||
func (r *Resolver) Subscription() generated.SubscriptionResolver { return r }
|
func (r *Resolver) Subscription() generated.SubscriptionResolver { return r }
|
||||||
|
|
||||||
// ChainMiddlewares will check all embeded resolvers for a GetMiddleware func and add to handler.
|
// func (r *Resolver) isResolver() {}
|
||||||
func (r *Resolver) ChainMiddlewares(h http.Handler) http.Handler {
|
func (r *Resolver) ExecutableSchema() graphql.ExecutableSchema {
|
||||||
v := reflect.ValueOf(r) // Get reflected value of *Resolver
|
return generated.NewExecutableSchema(generated.Config{Resolvers: r})
|
||||||
v = reflect.Indirect(v) // Get the pointed value (returns a zero value on nil)
|
|
||||||
n := v.NumField() // Get number of fields to iterate over.
|
|
||||||
for i := 0; i < n; i++ {
|
|
||||||
f := v.Field(i)
|
|
||||||
if !f.CanInterface() { // Skip non-interface types.
|
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
if iface, ok := f.Interface().(interface {
|
func (r *Resolver) BaseResolver() resolver.IsResolver {
|
||||||
GetMiddleware() func(http.Handler) http.Handler
|
return &noop{}
|
||||||
}); ok {
|
|
||||||
h = iface.GetMiddleware()(h) // Append only items that fulfill the interface.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return h
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Resolver) RegisterHTTP(mux *http.ServeMux) {
|
|
||||||
gql := handler.NewDefaultServer(generated.NewExecutableSchema(generated.Config{Resolvers: r}))
|
|
||||||
gql.SetRecoverFunc(NoopRecover)
|
|
||||||
gql.Use(otelgqlgen.Middleware())
|
|
||||||
mux.Handle("/", playground.Handler("GraphQL playground", "/gql"))
|
|
||||||
mux.Handle("/gql", lg.Htrace(r.ChainMiddlewares(gql), "gql"))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type noop struct{}
|
type noop struct{}
|
||||||
|
|
||||||
func NoopRecover(ctx context.Context, err interface{}) error {
|
|
||||||
if err, ok := err.(string); ok && err == "not implemented" {
|
|
||||||
return gqlerror.Errorf("not implemented")
|
|
||||||
}
|
|
||||||
fmt.Fprintln(os.Stderr, err)
|
|
||||||
fmt.Fprintln(os.Stderr)
|
|
||||||
debug.PrintStack()
|
|
||||||
|
|
||||||
return gqlerror.Errorf("internal system error")
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ msgbus.MsgbusResolver = (*noop)(nil)
|
var _ msgbus.MsgbusResolver = (*noop)(nil)
|
||||||
var _ salty.SaltyResolver = (*noop)(nil)
|
var _ salty.SaltyResolver = (*noop)(nil)
|
||||||
var _ es.EventResolver = (*noop)(nil)
|
var _ gql_es.EventResolver = (*noop)(nil)
|
||||||
|
|
||||||
|
func (*noop) IsResolver() {}
|
||||||
func (*noop) CreateSaltyUser(ctx context.Context, nick string, pubkey string) (*salty.SaltyUser, error) {
|
func (*noop) CreateSaltyUser(ctx context.Context, nick string, pubkey string) (*salty.SaltyUser, error) {
|
||||||
panic("not implemented")
|
panic("not implemented")
|
||||||
}
|
}
|
||||||
func (*noop) Posts(ctx context.Context, streamID string, paging *gql.PageInput) (*gql.Connection, error) {
|
func (*noop) Posts(ctx context.Context, name, tag string, paging *gql.PageInput) (*gql.Connection, error) {
|
||||||
panic("not implemented")
|
panic("not implemented")
|
||||||
}
|
}
|
||||||
func (*noop) SaltyUser(ctx context.Context, nick string) (*salty.SaltyUser, error) {
|
func (*noop) SaltyUser(ctx context.Context, nick string) (*salty.SaltyUser, error) {
|
||||||
panic("not implemented")
|
panic("not implemented")
|
||||||
}
|
}
|
||||||
func (*noop) PostAdded(ctx context.Context, streamID string, after int64) (<-chan *msgbus.PostEvent, error) {
|
func (*noop) PostAdded(ctx context.Context, name, tag string, after int64) (<-chan *msgbus.PostEvent, error) {
|
||||||
panic("not implemented")
|
panic("not implemented")
|
||||||
}
|
}
|
||||||
func (*noop) Events(ctx context.Context, streamID string, paging *gql.PageInput) (*gql.Connection, error) {
|
func (*noop) Events(ctx context.Context, streamID string, paging *gql.PageInput) (*gql.Connection, error) {
|
||||||
panic("not implemented")
|
panic("not implemented")
|
||||||
}
|
}
|
||||||
func (*noop) EventAdded(ctx context.Context, streamID string, after int64) (<-chan *es.GQLEvent, error) {
|
func (*noop) EventAdded(ctx context.Context, streamID string, after int64) (<-chan *gql_es.Event, error) {
|
||||||
|
panic("not implemented")
|
||||||
|
}
|
||||||
|
func (*noop) TruncateStream(ctx context.Context, streamID string, index int64) (bool, error) {
|
||||||
panic("not implemented")
|
panic("not implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (*noop) RegisterHTTP(*http.ServeMux) {}
|
|
||||||
|
|||||||
8
app/mercury/mercury.graphqls
Normal file
8
app/mercury/mercury.graphqls
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
# extend type Query{
|
||||||
|
# keys(namespace: String!) [String!]!
|
||||||
|
# get(namespace: String! keys: [String!]) [String]!
|
||||||
|
# }
|
||||||
|
|
||||||
|
# extend type Mutation{
|
||||||
|
# set(namespace: String! key: String! value: String): Bool!
|
||||||
|
# }
|
||||||
1
app/mercury/service.go
Normal file
1
app/mercury/service.go
Normal file
@@ -0,0 +1 @@
|
|||||||
|
package mercury
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
extend type Query {
|
extend type Query {
|
||||||
posts(streamID: String! paging: PageInput): Connection!
|
posts(name: String!, tag: String! = "", paging: PageInput): Connection!
|
||||||
}
|
}
|
||||||
extend type Subscription {
|
extend type Subscription {
|
||||||
"""after == 0 start from begining, after == -1 start from end"""
|
"""after == 0 start from begining, after == -1 start from end"""
|
||||||
postAdded(streamID: String! after: Int! = -1): PostEvent
|
postAdded(name: String!, tag: String! = "", after: Int! = -1): PostEvent
|
||||||
}
|
}
|
||||||
type PostEvent implements Edge @goModel(model: "github.com/sour-is/ev/app/msgbus.PostEvent") {
|
type PostEvent implements Edge @goModel(model: "go.sour.is/ev/app/msgbus.PostEvent") {
|
||||||
id: ID!
|
id: ID!
|
||||||
|
|
||||||
payload: String!
|
payload: String!
|
||||||
|
|||||||
@@ -3,8 +3,10 @@ package msgbus
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/base64"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"hash/fnv"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
@@ -12,29 +14,32 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gorilla/websocket"
|
"github.com/gorilla/websocket"
|
||||||
"github.com/sour-is/ev/internal/lg"
|
"go.opentelemetry.io/otel/metric"
|
||||||
"github.com/sour-is/ev/pkg/es"
|
|
||||||
"github.com/sour-is/ev/pkg/es/event"
|
|
||||||
"github.com/sour-is/ev/pkg/gql"
|
|
||||||
"go.opentelemetry.io/otel/metric/instrument/syncint64"
|
|
||||||
"go.uber.org/multierr"
|
"go.uber.org/multierr"
|
||||||
|
|
||||||
|
"go.sour.is/ev"
|
||||||
|
"go.sour.is/ev/pkg/event"
|
||||||
|
|
||||||
|
"go.sour.is/pkg/gql"
|
||||||
|
"go.sour.is/pkg/lg"
|
||||||
)
|
)
|
||||||
|
|
||||||
type service struct {
|
type service struct {
|
||||||
es *es.EventStore
|
es *ev.EventStore
|
||||||
|
|
||||||
Mresolver_posts syncint64.Counter
|
m_gql_posts metric.Int64Counter
|
||||||
Mresolver_post_added syncint64.Counter
|
m_gql_post_added metric.Int64Counter
|
||||||
Mresolver_post_added_event syncint64.Counter
|
m_gql_post_added_event metric.Int64Counter
|
||||||
|
m_req_time metric.Int64Histogram
|
||||||
}
|
}
|
||||||
|
|
||||||
type MsgbusResolver interface {
|
type MsgbusResolver interface {
|
||||||
Posts(ctx context.Context, streamID string, paging *gql.PageInput) (*gql.Connection, error)
|
Posts(ctx context.Context, name, tag string, paging *gql.PageInput) (*gql.Connection, error)
|
||||||
PostAdded(ctx context.Context, streamID string, after int64) (<-chan *PostEvent, error)
|
PostAdded(ctx context.Context, name, tag string, after int64) (<-chan *PostEvent, error)
|
||||||
RegisterHTTP(mux *http.ServeMux)
|
IsResolver()
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(ctx context.Context, es *es.EventStore) (*service, error) {
|
func New(ctx context.Context, es *ev.EventStore) (*service, error) {
|
||||||
ctx, span := lg.Span(ctx)
|
ctx, span := lg.Span(ctx)
|
||||||
defer span.End()
|
defer span.End()
|
||||||
|
|
||||||
@@ -50,13 +55,25 @@ func New(ctx context.Context, es *es.EventStore) (*service, error) {
|
|||||||
svc := &service{es: es}
|
svc := &service{es: es}
|
||||||
|
|
||||||
var err, errs error
|
var err, errs error
|
||||||
svc.Mresolver_posts, err = m.SyncInt64().Counter("resolver_posts")
|
svc.m_gql_posts, err = m.Int64Counter("msgbus_posts",
|
||||||
|
metric.WithDescription("msgbus graphql posts requests"),
|
||||||
|
)
|
||||||
errs = multierr.Append(errs, err)
|
errs = multierr.Append(errs, err)
|
||||||
|
|
||||||
svc.Mresolver_post_added, err = m.SyncInt64().Counter("resolver_post_added")
|
svc.m_gql_post_added, err = m.Int64Counter("msgbus_post_added",
|
||||||
|
metric.WithDescription("msgbus graphql post added subcription requests"),
|
||||||
|
)
|
||||||
errs = multierr.Append(errs, err)
|
errs = multierr.Append(errs, err)
|
||||||
|
|
||||||
svc.Mresolver_post_added_event, err = m.SyncInt64().Counter("resolver_post_added")
|
svc.m_gql_post_added_event, err = m.Int64Counter("msgbus_post_event",
|
||||||
|
metric.WithDescription("msgbus graphql post added subscription events"),
|
||||||
|
)
|
||||||
|
errs = multierr.Append(errs, err)
|
||||||
|
|
||||||
|
svc.m_req_time, err = m.Int64Histogram("msgbus_request_time",
|
||||||
|
metric.WithDescription("msgbus graphql post added subscription events"),
|
||||||
|
metric.WithUnit("ns"),
|
||||||
|
)
|
||||||
errs = multierr.Append(errs, err)
|
errs = multierr.Append(errs, err)
|
||||||
|
|
||||||
span.RecordError(err)
|
span.RecordError(err)
|
||||||
@@ -71,13 +88,16 @@ var upgrader = websocket.Upgrader{
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *service) IsResolver() {}
|
||||||
func (s *service) RegisterHTTP(mux *http.ServeMux) {
|
func (s *service) RegisterHTTP(mux *http.ServeMux) {
|
||||||
mux.Handle("/inbox/", lg.Htrace(http.StripPrefix("/inbox/", s), "inbox"))
|
mux.Handle("/inbox/", lg.Htrace(http.StripPrefix("/inbox/", s), "inbox"))
|
||||||
}
|
}
|
||||||
func (s *service) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
func (s *service) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := r.Context()
|
ctx := r.Context()
|
||||||
|
|
||||||
ctx, span := lg.Span(ctx)
|
ctx, span := lg.Span(ctx)
|
||||||
defer span.End()
|
defer span.End()
|
||||||
|
|
||||||
r = r.WithContext(ctx)
|
r = r.WithContext(ctx)
|
||||||
|
|
||||||
switch r.Method {
|
switch r.Method {
|
||||||
@@ -96,13 +116,17 @@ func (s *service) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Posts is the resolver for the events field.
|
// Posts is the resolver for the events field.
|
||||||
func (r *service) Posts(ctx context.Context, streamID string, paging *gql.PageInput) (*gql.Connection, error) {
|
func (s *service) Posts(ctx context.Context, name, tag string, paging *gql.PageInput) (*gql.Connection, error) {
|
||||||
ctx, span := lg.Span(ctx)
|
ctx, span := lg.Span(ctx)
|
||||||
defer span.End()
|
defer span.End()
|
||||||
|
|
||||||
r.Mresolver_posts.Add(ctx, 1)
|
s.m_gql_posts.Add(ctx, 1)
|
||||||
|
|
||||||
lis, err := r.es.Read(ctx, streamID, paging.GetIdx(0), paging.GetCount(30))
|
start := time.Now()
|
||||||
|
defer s.m_req_time.Record(ctx, time.Since(start).Milliseconds())
|
||||||
|
|
||||||
|
streamID := withTag("post-"+name, tag)
|
||||||
|
lis, err := s.es.Read(ctx, streamID, paging.GetIdx(0), paging.GetCount(30))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
span.RecordError(err)
|
span.RecordError(err)
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -122,11 +146,11 @@ func (r *service) Posts(ctx context.Context, streamID string, paging *gql.PageIn
|
|||||||
}
|
}
|
||||||
|
|
||||||
var first, last uint64
|
var first, last uint64
|
||||||
if first, err = r.es.FirstIndex(ctx, streamID); err != nil {
|
if first, err = s.es.FirstIndex(ctx, streamID); err != nil {
|
||||||
span.RecordError(err)
|
span.RecordError(err)
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if last, err = r.es.LastIndex(ctx, streamID); err != nil {
|
if last, err = s.es.LastIndex(ctx, streamID); err != nil {
|
||||||
span.RecordError(err)
|
span.RecordError(err)
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -142,17 +166,19 @@ func (r *service) Posts(ctx context.Context, streamID string, paging *gql.PageIn
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *service) PostAdded(ctx context.Context, streamID string, after int64) (<-chan *PostEvent, error) {
|
func (r *service) PostAdded(ctx context.Context, name, tag string, after int64) (<-chan *PostEvent, error) {
|
||||||
ctx, span := lg.Span(ctx)
|
ctx, span := lg.Span(ctx)
|
||||||
defer span.End()
|
defer span.End()
|
||||||
|
|
||||||
r.Mresolver_post_added.Add(ctx, 1)
|
r.m_gql_post_added.Add(ctx, 1)
|
||||||
|
|
||||||
es := r.es.EventStream()
|
es := r.es.EventStream()
|
||||||
if es == nil {
|
if es == nil {
|
||||||
return nil, fmt.Errorf("EventStore does not implement streaming")
|
return nil, fmt.Errorf("EventStore does not implement streaming")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
streamID := withTag("post-"+name, tag)
|
||||||
|
|
||||||
sub, err := es.Subscribe(ctx, streamID, after)
|
sub, err := es.Subscribe(ctx, streamID, after)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
span.RecordError(err)
|
span.RecordError(err)
|
||||||
@@ -176,14 +202,14 @@ func (r *service) PostAdded(ctx context.Context, streamID string, after int64) (
|
|||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
for sub.Recv(ctx) {
|
for <-sub.Recv(ctx) {
|
||||||
events, err := sub.Events(ctx)
|
events, err := sub.Events(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
span.RecordError(err)
|
span.RecordError(err)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
span.AddEvent(fmt.Sprintf("received %d events", len(events)))
|
span.AddEvent(fmt.Sprintf("received %d events", len(events)))
|
||||||
r.Mresolver_post_added_event.Add(ctx, int64(len(events)))
|
r.m_gql_post_added_event.Add(ctx, int64(len(events)))
|
||||||
|
|
||||||
for _, e := range events {
|
for _, e := range events {
|
||||||
if p, ok := e.(*PostEvent); ok {
|
if p, ok := e.(*PostEvent); ok {
|
||||||
@@ -203,21 +229,26 @@ func (r *service) PostAdded(ctx context.Context, streamID string, after int64) (
|
|||||||
|
|
||||||
func (s *service) get(w http.ResponseWriter, r *http.Request) {
|
func (s *service) get(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := r.Context()
|
ctx := r.Context()
|
||||||
|
|
||||||
ctx, span := lg.Span(ctx)
|
ctx, span := lg.Span(ctx)
|
||||||
defer span.End()
|
defer span.End()
|
||||||
|
|
||||||
name, _, _ := strings.Cut(r.URL.Path, "/")
|
start := time.Now()
|
||||||
|
defer s.m_req_time.Record(ctx, time.Since(start).Milliseconds())
|
||||||
|
|
||||||
|
name, tag, _ := strings.Cut(r.URL.Path, "/")
|
||||||
if name == "" {
|
if name == "" {
|
||||||
w.WriteHeader(http.StatusNotFound)
|
w.WriteHeader(http.StatusNotFound)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
streamID := withTag("post-"+name, tag)
|
||||||
|
|
||||||
var first event.Event = event.NilEvent
|
var first event.Event = event.NilEvent
|
||||||
if lis, err := s.es.Read(ctx, "post-"+name, 0, 1); err == nil && len(lis) > 0 {
|
if lis, err := s.es.Read(ctx, streamID, 0, 1); err == nil && len(lis) > 0 {
|
||||||
first = lis[0]
|
first = lis[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
var pos, count int64 = 0, es.AllEvents
|
var pos, count int64 = 0, ev.AllEvents
|
||||||
qry := r.URL.Query()
|
qry := r.URL.Query()
|
||||||
|
|
||||||
if i, err := strconv.ParseInt(qry.Get("index"), 10, 64); err == nil && i > 1 {
|
if i, err := strconv.ParseInt(qry.Get("index"), 10, 64); err == nil && i > 1 {
|
||||||
@@ -230,8 +261,8 @@ func (s *service) get(w http.ResponseWriter, r *http.Request) {
|
|||||||
count = i
|
count = i
|
||||||
}
|
}
|
||||||
|
|
||||||
span.AddEvent(fmt.Sprint("GET topic=", name, " idx=", pos, " n=", count))
|
span.AddEvent(fmt.Sprint("GET topic=", streamID, " idx=", pos, " n=", count))
|
||||||
events, err := s.es.Read(ctx, "post-"+name, pos, count)
|
events, err := s.es.Read(ctx, streamID, pos, count)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
span.RecordError(err)
|
span.RecordError(err)
|
||||||
w.WriteHeader(http.StatusInternalServerError)
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
@@ -260,6 +291,9 @@ func (s *service) post(w http.ResponseWriter, r *http.Request) {
|
|||||||
ctx, span := lg.Span(ctx)
|
ctx, span := lg.Span(ctx)
|
||||||
defer span.End()
|
defer span.End()
|
||||||
|
|
||||||
|
start := time.Now()
|
||||||
|
defer s.m_req_time.Record(ctx, time.Since(start).Milliseconds())
|
||||||
|
|
||||||
name, tags, _ := strings.Cut(r.URL.Path, "/")
|
name, tags, _ := strings.Cut(r.URL.Path, "/")
|
||||||
if name == "" {
|
if name == "" {
|
||||||
w.WriteHeader(http.StatusNotFound)
|
w.WriteHeader(http.StatusNotFound)
|
||||||
@@ -327,14 +361,16 @@ func (s *service) websocket(w http.ResponseWriter, r *http.Request) {
|
|||||||
ctx, span := lg.Span(ctx)
|
ctx, span := lg.Span(ctx)
|
||||||
defer span.End()
|
defer span.End()
|
||||||
|
|
||||||
name, _, _ := strings.Cut(r.URL.Path, "/")
|
name, tag, _ := strings.Cut(r.URL.Path, "/")
|
||||||
if name == "" {
|
if name == "" {
|
||||||
w.WriteHeader(http.StatusNotFound)
|
w.WriteHeader(http.StatusNotFound)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
streamID := withTag("post-"+name, tag)
|
||||||
|
|
||||||
var first event.Event = event.NilEvent
|
var first event.Event = event.NilEvent
|
||||||
if lis, err := s.es.Read(ctx, "post-"+name, 0, 1); err == nil && len(lis) > 0 {
|
if lis, err := s.es.Read(ctx, streamID, 0, 1); err == nil && len(lis) > 0 {
|
||||||
first = lis[0]
|
first = lis[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -345,7 +381,7 @@ func (s *service) websocket(w http.ResponseWriter, r *http.Request) {
|
|||||||
pos = i - 1
|
pos = i - 1
|
||||||
}
|
}
|
||||||
|
|
||||||
span.AddEvent(fmt.Sprint("WS topic=", name, " idx=", pos))
|
span.AddEvent(fmt.Sprint("WS topic=", streamID, " idx=", pos))
|
||||||
|
|
||||||
c, err := upgrader.Upgrade(w, r, nil)
|
c, err := upgrader.Upgrade(w, r, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -380,7 +416,7 @@ func (s *service) websocket(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
sub, err := es.Subscribe(ctx, "post-"+name, pos)
|
sub, err := es.Subscribe(ctx, streamID, pos)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
span.RecordError(err)
|
span.RecordError(err)
|
||||||
return
|
return
|
||||||
@@ -397,7 +433,7 @@ func (s *service) websocket(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
span.AddEvent("start ws")
|
span.AddEvent("start ws")
|
||||||
for sub.Recv(ctx) {
|
for <-sub.Recv(ctx) {
|
||||||
events, err := sub.Events(ctx)
|
events, err := sub.Events(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
break
|
break
|
||||||
@@ -428,29 +464,17 @@ type PostEvent struct {
|
|||||||
payload []byte
|
payload []byte
|
||||||
tags []string
|
tags []string
|
||||||
|
|
||||||
eventMeta event.Meta
|
event.IsEvent
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *PostEvent) EventMeta() event.Meta {
|
|
||||||
if e == nil {
|
|
||||||
return event.Meta{}
|
|
||||||
}
|
|
||||||
return e.eventMeta
|
|
||||||
}
|
|
||||||
func (e *PostEvent) SetEventMeta(eventMeta event.Meta) {
|
|
||||||
if e == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
e.eventMeta = eventMeta
|
|
||||||
}
|
|
||||||
func (e *PostEvent) Values() any {
|
func (e *PostEvent) Values() any {
|
||||||
if e == nil {
|
if e == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return struct {
|
return struct {
|
||||||
Payload []byte
|
Payload []byte `json:"payload"`
|
||||||
Tags []string
|
Tags []string `json:"tags,omitempty"`
|
||||||
}{
|
}{
|
||||||
Payload: e.payload,
|
Payload: e.payload,
|
||||||
Tags: e.tags,
|
Tags: e.tags,
|
||||||
@@ -474,25 +498,23 @@ func (e *PostEvent) UnmarshalBinary(b []byte) error {
|
|||||||
func (e *PostEvent) MarshalJSON() ([]byte, error) { return e.MarshalBinary() }
|
func (e *PostEvent) MarshalJSON() ([]byte, error) { return e.MarshalBinary() }
|
||||||
func (e *PostEvent) UnmarshalJSON(b []byte) error { return e.UnmarshalBinary(b) }
|
func (e *PostEvent) UnmarshalJSON(b []byte) error { return e.UnmarshalBinary(b) }
|
||||||
|
|
||||||
func (e *PostEvent) ID() string { return e.eventMeta.GetEventID() }
|
func (e *PostEvent) ID() string { return e.EventMeta().GetEventID() }
|
||||||
func (e *PostEvent) Tags() []string { return e.tags }
|
func (e *PostEvent) Tags() []string { return e.tags }
|
||||||
func (e *PostEvent) Payload() string { return string(e.payload) }
|
func (e *PostEvent) Payload() string { return string(e.payload) }
|
||||||
func (e *PostEvent) PayloadJSON(ctx context.Context) (m map[string]interface{}, err error) {
|
func (e *PostEvent) PayloadJSON(ctx context.Context) (m map[string]interface{}, err error) {
|
||||||
err = json.Unmarshal([]byte(e.payload), &m)
|
err = json.Unmarshal([]byte(e.payload), &m)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
func (e *PostEvent) Meta() *event.Meta { return &e.eventMeta }
|
func (e *PostEvent) Meta() event.Meta { return e.EventMeta() }
|
||||||
func (e *PostEvent) IsEdge() {}
|
func (e *PostEvent) IsEdge() {}
|
||||||
|
|
||||||
func (e *PostEvent) String() string {
|
func (e *PostEvent) String() string {
|
||||||
var b bytes.Buffer
|
var b bytes.Buffer
|
||||||
|
|
||||||
// b.WriteString(e.eventMeta.StreamID)
|
b.WriteString(strconv.FormatUint(e.EventMeta().Position, 10))
|
||||||
// b.WriteRune('@')
|
|
||||||
b.WriteString(strconv.FormatUint(e.eventMeta.Position, 10))
|
|
||||||
b.WriteRune('\t')
|
b.WriteRune('\t')
|
||||||
|
|
||||||
b.WriteString(e.eventMeta.EventID.String())
|
b.WriteString(e.EventMeta().EventID.String())
|
||||||
b.WriteRune('\t')
|
b.WriteRune('\t')
|
||||||
b.WriteString(string(e.payload))
|
b.WriteString(string(e.payload))
|
||||||
if len(e.tags) > 0 {
|
if len(e.tags) > 0 {
|
||||||
@@ -507,7 +529,7 @@ func fields(s string) []string {
|
|||||||
if s == "" {
|
if s == "" {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return strings.Split(s, "/")
|
return strings.Split(s, ";")
|
||||||
}
|
}
|
||||||
|
|
||||||
func encodeJSON(w io.Writer, first event.Event, events ...event.Event) error {
|
func encodeJSON(w io.Writer, first event.Event, events ...event.Event) error {
|
||||||
@@ -544,3 +566,32 @@ func encodeJSON(w io.Writer, first event.Event, events ...event.Event) error {
|
|||||||
|
|
||||||
return json.NewEncoder(w).Encode(out)
|
return json.NewEncoder(w).Encode(out)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Projector(e event.Event) []event.Event {
|
||||||
|
m := e.EventMeta()
|
||||||
|
streamID := m.StreamID
|
||||||
|
streamPos := m.Position
|
||||||
|
|
||||||
|
switch e := e.(type) {
|
||||||
|
case *PostEvent:
|
||||||
|
lis := make([]event.Event, len(e.tags))
|
||||||
|
for i := range lis {
|
||||||
|
tag := e.tags[i]
|
||||||
|
ne := event.NewPtr(streamID, streamPos)
|
||||||
|
event.SetStreamID(withTag(streamID, tag), ne)
|
||||||
|
lis[i] = ne
|
||||||
|
}
|
||||||
|
|
||||||
|
return lis
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
func withTag(id, tag string) string {
|
||||||
|
if tag == "" {
|
||||||
|
return id
|
||||||
|
}
|
||||||
|
|
||||||
|
h := fnv.New128a()
|
||||||
|
fmt.Fprint(h, tag)
|
||||||
|
return id + "-" + base64.RawURLEncoding.EncodeToString(h.Sum(nil))
|
||||||
|
}
|
||||||
|
|||||||
6
app/peerfinder/assets/bootstrap.min.css
vendored
Normal file
6
app/peerfinder/assets/bootstrap.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
app/peerfinder/assets/bootstrap.min.css.map
Normal file
1
app/peerfinder/assets/bootstrap.min.css.map
Normal file
File diff suppressed because one or more lines are too long
58
app/peerfinder/assets/peerfinder.css
Normal file
58
app/peerfinder/assets/peerfinder.css
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
/* Space out content a bit */
|
||||||
|
body {
|
||||||
|
padding-top: 20px;
|
||||||
|
padding-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Everything but the jumbotron gets side spacing for mobile first views */
|
||||||
|
.header,
|
||||||
|
.footer {
|
||||||
|
padding-right: 15px;
|
||||||
|
padding-left: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Custom page header */
|
||||||
|
.header {
|
||||||
|
padding-bottom: 20px;
|
||||||
|
border-bottom: 1px solid #e5e5e5;
|
||||||
|
}
|
||||||
|
/* Make the masthead heading the same height as the navigation */
|
||||||
|
.header h3 {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 0;
|
||||||
|
line-height: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Custom page footer */
|
||||||
|
.footer {
|
||||||
|
padding-top: 19px;
|
||||||
|
color: #777;
|
||||||
|
border-top: 1px solid #e5e5e5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-heading a {
|
||||||
|
color: white;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container-narrow > hr {
|
||||||
|
margin: 30px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table tbody tr th {
|
||||||
|
width: 70%
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
body, .panel-body {
|
||||||
|
color: white;
|
||||||
|
background-color: #121212;
|
||||||
|
}
|
||||||
|
.table-striped > tbody > tr:nth-of-type(2n+1) {
|
||||||
|
background-color: darkslategray;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: light) {
|
||||||
|
|
||||||
|
}
|
||||||
56
app/peerfinder/ev-info.go
Normal file
56
app/peerfinder/ev-info.go
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
package peerfinder
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
|
||||||
|
"github.com/tj/go-semver"
|
||||||
|
|
||||||
|
"go.sour.is/ev/pkg/event"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Info struct {
|
||||||
|
ScriptVersion string `json:"script_version"`
|
||||||
|
|
||||||
|
event.IsAggregate
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ event.Aggregate = (*Info)(nil)
|
||||||
|
|
||||||
|
func (a *Info) ApplyEvent(lis ...event.Event) {
|
||||||
|
for _, e := range lis {
|
||||||
|
switch e := e.(type) {
|
||||||
|
case *VersionChanged:
|
||||||
|
a.ScriptVersion = e.ScriptVersion
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func (a *Info) MarshalEnviron() ([]byte, error) {
|
||||||
|
var b bytes.Buffer
|
||||||
|
|
||||||
|
b.WriteString("SCRIPT_VERSION=")
|
||||||
|
b.WriteString(a.ScriptVersion)
|
||||||
|
b.WriteRune('\n')
|
||||||
|
|
||||||
|
return b.Bytes(), nil
|
||||||
|
}
|
||||||
|
func (a *Info) OnUpsert() error {
|
||||||
|
if a.StreamVersion() == 0 {
|
||||||
|
event.Raise(a, &VersionChanged{ScriptVersion: initVersion})
|
||||||
|
}
|
||||||
|
current, _ := semver.Parse(initVersion)
|
||||||
|
previous, _ := semver.Parse(a.ScriptVersion)
|
||||||
|
|
||||||
|
if current.Compare(previous) > 0 {
|
||||||
|
event.Raise(a, &VersionChanged{ScriptVersion: initVersion})
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type VersionChanged struct {
|
||||||
|
ScriptVersion string `json:"script_version"`
|
||||||
|
|
||||||
|
event.IsEvent
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ event.Event = (*VersionChanged)(nil)
|
||||||
111
app/peerfinder/ev-peer.go
Normal file
111
app/peerfinder/ev-peer.go
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
package peerfinder
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/keys-pub/keys/json"
|
||||||
|
"go.sour.is/pkg/set"
|
||||||
|
|
||||||
|
"go.sour.is/ev/pkg/event"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Time time.Time
|
||||||
|
|
||||||
|
func (t *Time) UnmarshalJSON(b []byte) error {
|
||||||
|
time, err := time.Parse(`"2006-01-02 15:04:05"`, string(b))
|
||||||
|
*t = Time(time)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
func (t *Time) MarshalJSON() ([]byte, error) {
|
||||||
|
if t == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
i := *t
|
||||||
|
return time.Time(i).MarshalJSON()
|
||||||
|
}
|
||||||
|
|
||||||
|
type ipFamily string
|
||||||
|
|
||||||
|
const (
|
||||||
|
ipFamilyV4 ipFamily = "IPv4"
|
||||||
|
ipFamilyV6 ipFamily = "IPv6"
|
||||||
|
ipFamilyBoth ipFamily = "both"
|
||||||
|
ipFamilyNone ipFamily = "none"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (t *ipFamily) UnmarshalJSON(b []byte) error {
|
||||||
|
i, err := strconv.Atoi(strings.Trim(string(b), `"`))
|
||||||
|
switch i {
|
||||||
|
case 1:
|
||||||
|
*t = ipFamilyV4
|
||||||
|
case 2:
|
||||||
|
*t = ipFamilyV6
|
||||||
|
case 3:
|
||||||
|
*t = ipFamilyBoth
|
||||||
|
default:
|
||||||
|
*t = ipFamilyNone
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
type peerType []string
|
||||||
|
|
||||||
|
func (t *peerType) UnmarshalJSON(b []byte) error {
|
||||||
|
var bs string
|
||||||
|
json.Unmarshal(b, &bs)
|
||||||
|
*t = strings.Split(bs, ",")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type Peer struct {
|
||||||
|
ID string `json:"peer_id,omitempty"`
|
||||||
|
Owner string `json:"peer_owner"`
|
||||||
|
Nick string `json:"peer_nick"`
|
||||||
|
Name string `json:"peer_name"`
|
||||||
|
Country string `json:"peer_country"`
|
||||||
|
Note string `json:"peer_note"`
|
||||||
|
Family ipFamily `json:"peer_family"`
|
||||||
|
Type peerType `json:"peer_type"`
|
||||||
|
Created Time `json:"peer_created"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Peer) CanSupport(ip string) bool {
|
||||||
|
addr := net.ParseIP(ip)
|
||||||
|
if addr == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if !(addr.IsGlobalUnicast() || addr.IsLoopback() || addr.IsPrivate()) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
switch p.Family {
|
||||||
|
case ipFamilyV4:
|
||||||
|
return addr.To4() != nil
|
||||||
|
case ipFamilyV6:
|
||||||
|
return addr.To16() != nil
|
||||||
|
case ipFamilyNone:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
type PeerResults struct {
|
||||||
|
set.Set[string]
|
||||||
|
event.IsAggregate
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *PeerResults) ApplyEvent(lis ...event.Event) {
|
||||||
|
for _, e := range lis {
|
||||||
|
switch e := e.(type) {
|
||||||
|
case *ResultSubmitted:
|
||||||
|
if p.Set == nil {
|
||||||
|
p.Set = set.New[string]()
|
||||||
|
}
|
||||||
|
p.Set.Add(e.RequestID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
230
app/peerfinder/ev-request.go
Normal file
230
app/peerfinder/ev-request.go
Normal file
@@ -0,0 +1,230 @@
|
|||||||
|
package peerfinder
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/netip"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/oklog/ulid"
|
||||||
|
"go.sour.is/ev/pkg/event"
|
||||||
|
"go.sour.is/pkg/set"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Request struct {
|
||||||
|
event.IsAggregate
|
||||||
|
|
||||||
|
RequestID string `json:"req_id"`
|
||||||
|
RequestIP string `json:"req_ip"`
|
||||||
|
Hidden bool `json:"hide,omitempty"`
|
||||||
|
Created time.Time `json:"req_created"`
|
||||||
|
Family int `json:"family"`
|
||||||
|
|
||||||
|
Responses []*Response `json:"responses"`
|
||||||
|
peers set.Set[string]
|
||||||
|
initial *RequestSubmitted
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ event.Aggregate = (*Request)(nil)
|
||||||
|
|
||||||
|
func (a *Request) ApplyEvent(lis ...event.Event) {
|
||||||
|
for _, e := range lis {
|
||||||
|
switch e := e.(type) {
|
||||||
|
case *RequestSubmitted:
|
||||||
|
a.RequestID = e.EventMeta().EventID.String()
|
||||||
|
a.RequestIP = e.RequestIP
|
||||||
|
a.Hidden = e.Hidden
|
||||||
|
a.Created = ulid.Time(e.EventMeta().EventID.Time())
|
||||||
|
a.Family = e.Family()
|
||||||
|
|
||||||
|
a.initial = e
|
||||||
|
case *ResultSubmitted:
|
||||||
|
if a.peers == nil {
|
||||||
|
a.peers = set.New[string]()
|
||||||
|
}
|
||||||
|
if a.peers.Has(e.PeerID) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
a.peers.Add(e.PeerID)
|
||||||
|
a.Responses = append(a.Responses, &Response{
|
||||||
|
PeerID: e.PeerID,
|
||||||
|
ScriptVersion: e.PeerVersion,
|
||||||
|
Latency: e.Latency,
|
||||||
|
Jitter: e.Jitter,
|
||||||
|
MinRTT: e.MinRTT,
|
||||||
|
MaxRTT: e.MaxRTT,
|
||||||
|
Sent: e.Sent,
|
||||||
|
Received: e.Received,
|
||||||
|
Unreachable: e.Unreachable,
|
||||||
|
Created: ulid.Time(e.EventMeta().EventID.Time()),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Request) MarshalEnviron() ([]byte, error) {
|
||||||
|
return a.initial.MarshalEnviron()
|
||||||
|
}
|
||||||
|
func (a *Request) CreatedString() string {
|
||||||
|
return a.Created.Format("2006-01-02 15:04:05")
|
||||||
|
}
|
||||||
|
|
||||||
|
type ListRequest []*Request
|
||||||
|
|
||||||
|
func (lis ListRequest) Len() int {
|
||||||
|
return len(lis)
|
||||||
|
}
|
||||||
|
func (lis ListRequest) Less(i, j int) bool {
|
||||||
|
return lis[i].Created.Before(lis[j].Created)
|
||||||
|
}
|
||||||
|
func (lis ListRequest) Swap(i, j int) {
|
||||||
|
lis[i], lis[j] = lis[j], lis[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
type Response struct {
|
||||||
|
Peer *Peer `json:"peer"`
|
||||||
|
PeerID string `json:"-"`
|
||||||
|
ScriptVersion string `json:"peer_scriptver"`
|
||||||
|
|
||||||
|
Latency float64 `json:"res_latency"`
|
||||||
|
Jitter float64 `json:"res_jitter,omitempty"`
|
||||||
|
MaxRTT float64 `json:"res_maxrtt,omitempty"`
|
||||||
|
MinRTT float64 `json:"res_minrtt,omitempty"`
|
||||||
|
Sent int `json:"res_sent,omitempty"`
|
||||||
|
Received int `json:"res_recv,omitempty"`
|
||||||
|
Unreachable bool `json:"unreachable,omitempty"`
|
||||||
|
|
||||||
|
Created time.Time `json:"res_created"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ListResponse []*Response
|
||||||
|
|
||||||
|
func (lis ListResponse) Len() int {
|
||||||
|
return len(lis)
|
||||||
|
}
|
||||||
|
func (lis ListResponse) Less(i, j int) bool {
|
||||||
|
if lis[j].Latency == 0.0 && lis[i].Latency > 0.0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if lis[i].Latency == 0.0 && lis[j].Latency > 0.0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return lis[j].Latency >= lis[i].Latency
|
||||||
|
}
|
||||||
|
func (lis ListResponse) Swap(i, j int) {
|
||||||
|
lis[i], lis[j] = lis[j], lis[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
type RequestSubmitted struct {
|
||||||
|
event.IsEvent
|
||||||
|
|
||||||
|
RequestIP string `json:"req_ip"`
|
||||||
|
Hidden bool `json:"hide,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RequestSubmitted) StreamID() string {
|
||||||
|
return r.EventMeta().GetEventID()
|
||||||
|
}
|
||||||
|
func (r *RequestSubmitted) RequestID() string {
|
||||||
|
return r.EventMeta().GetEventID()
|
||||||
|
}
|
||||||
|
func (r *RequestSubmitted) Created() time.Time {
|
||||||
|
return r.EventMeta().Created()
|
||||||
|
}
|
||||||
|
func (r *RequestSubmitted) CreatedString() string {
|
||||||
|
return r.Created().Format("2006-01-02 15:04:05")
|
||||||
|
}
|
||||||
|
func (r *RequestSubmitted) Family() int {
|
||||||
|
if r == nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
ip, err := netip.ParseAddr(r.RequestIP)
|
||||||
|
switch {
|
||||||
|
case err != nil:
|
||||||
|
return 0
|
||||||
|
case ip.Is4():
|
||||||
|
return 1
|
||||||
|
default:
|
||||||
|
return 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func (r *RequestSubmitted) String() string {
|
||||||
|
return fmt.Sprint(r.EventMeta().EventID, r.RequestIP, r.Hidden, r.CreatedString())
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ event.Event = (*RequestSubmitted)(nil)
|
||||||
|
|
||||||
|
func (e *RequestSubmitted) MarshalBinary() (text []byte, err error) {
|
||||||
|
return json.Marshal(e)
|
||||||
|
}
|
||||||
|
func (e *RequestSubmitted) UnmarshalBinary(b []byte) error {
|
||||||
|
return json.Unmarshal(b, e)
|
||||||
|
}
|
||||||
|
func (e *RequestSubmitted) MarshalEnviron() ([]byte, error) {
|
||||||
|
if e == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var b bytes.Buffer
|
||||||
|
b.WriteString("REQ_ID=")
|
||||||
|
b.WriteString(e.RequestID())
|
||||||
|
b.WriteRune('\n')
|
||||||
|
|
||||||
|
b.WriteString("REQ_IP=")
|
||||||
|
b.WriteString(e.RequestIP)
|
||||||
|
b.WriteRune('\n')
|
||||||
|
|
||||||
|
b.WriteString("REQ_FAMILY=")
|
||||||
|
if family := e.Family(); family > 0 {
|
||||||
|
b.WriteString(strconv.Itoa(family))
|
||||||
|
}
|
||||||
|
b.WriteRune('\n')
|
||||||
|
|
||||||
|
b.WriteString("REQ_CREATED=")
|
||||||
|
b.WriteString(e.CreatedString())
|
||||||
|
b.WriteRune('\n')
|
||||||
|
|
||||||
|
return b.Bytes(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type ResultSubmitted struct {
|
||||||
|
event.IsEvent
|
||||||
|
|
||||||
|
RequestID string `json:"req_id"`
|
||||||
|
PeerID string `json:"peer_id"`
|
||||||
|
PeerVersion string `json:"peer_version"`
|
||||||
|
Latency float64 `json:"latency,omitempty"`
|
||||||
|
Jitter float64 `json:"jitter,omitempty"`
|
||||||
|
MaxRTT float64 `json:"maxrtt,omitempty"`
|
||||||
|
MinRTT float64 `json:"minrtt,omitempty"`
|
||||||
|
Sent int `json:"res_sent,omitempty"`
|
||||||
|
Received int `json:"res_recv,omitempty"`
|
||||||
|
Unreachable bool `json:"unreachable,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ResultSubmitted) Created() time.Time {
|
||||||
|
return r.EventMeta().Created()
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ event.Event = (*ResultSubmitted)(nil)
|
||||||
|
|
||||||
|
func (e *ResultSubmitted) String() string {
|
||||||
|
return fmt.Sprintf("id: %s\npeer: %s\nversion: %s\nlatency: %0.4f", e.RequestID, e.PeerID, e.PeerVersion, e.Latency)
|
||||||
|
}
|
||||||
|
|
||||||
|
type RequestTruncated struct {
|
||||||
|
RequestID string
|
||||||
|
|
||||||
|
event.IsEvent
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ event.Event = (*RequestTruncated)(nil)
|
||||||
|
|
||||||
|
func (e *RequestTruncated) String() string {
|
||||||
|
return fmt.Sprintf("request truncated id: %s\n", e.RequestID)
|
||||||
|
}
|
||||||
713
app/peerfinder/http.go
Normal file
713
app/peerfinder/http.go
Normal file
@@ -0,0 +1,713 @@
|
|||||||
|
package peerfinder
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"embed"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"html/template"
|
||||||
|
"io"
|
||||||
|
"io/fs"
|
||||||
|
"log"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"sort"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/oklog/ulid/v2"
|
||||||
|
contentnegotiation "gitlab.com/jamietanna/content-negotiation-go"
|
||||||
|
"go.opentelemetry.io/otel/attribute"
|
||||||
|
"go.sour.is/pkg/lg"
|
||||||
|
|
||||||
|
"go.sour.is/ev"
|
||||||
|
"go.sour.is/ev/pkg/event"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
//go:embed pages/* layouts/* assets/*
|
||||||
|
files embed.FS
|
||||||
|
templates map[string]*template.Template
|
||||||
|
)
|
||||||
|
|
||||||
|
// Args passed to templates
|
||||||
|
type Args struct {
|
||||||
|
RemoteIP string
|
||||||
|
Requests []*Request
|
||||||
|
CountPeers int
|
||||||
|
}
|
||||||
|
|
||||||
|
// requestArgs builds args from http.Request
|
||||||
|
func requestArgs(r *http.Request) Args {
|
||||||
|
remoteIP, _, _ := strings.Cut(r.RemoteAddr, ":")
|
||||||
|
if s := r.Header.Get("X-Forwarded-For"); s != "" {
|
||||||
|
s, _, _ = strings.Cut(s, ", ")
|
||||||
|
remoteIP = s
|
||||||
|
}
|
||||||
|
return Args{
|
||||||
|
RemoteIP: remoteIP,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RegisterHTTP adds handler paths to the ServeMux
|
||||||
|
func (s *service) RegisterHTTP(mux *http.ServeMux) {
|
||||||
|
a, _ := fs.Sub(files, "assets")
|
||||||
|
assets := http.StripPrefix("/peers/assets/", http.FileServer(http.FS(a)))
|
||||||
|
|
||||||
|
mux.Handle("/peers/assets/", lg.Htrace(assets, "peer-assets"))
|
||||||
|
mux.Handle("/peers/", lg.Htrace(s, "peers"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *service) Setup() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServeHTTP handle HTTP requests
|
||||||
|
func (s *service) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := r.Context()
|
||||||
|
|
||||||
|
ctx, span := lg.Span(ctx)
|
||||||
|
defer span.End()
|
||||||
|
|
||||||
|
r = r.WithContext(ctx)
|
||||||
|
|
||||||
|
if !s.up.Load() {
|
||||||
|
w.WriteHeader(http.StatusFailedDependency)
|
||||||
|
fmt.Fprint(w, "Starting up...")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
switch r.Method {
|
||||||
|
case http.MethodGet:
|
||||||
|
switch {
|
||||||
|
case strings.HasPrefix(r.URL.Path, "/peers/pending/"):
|
||||||
|
s.getPending(w, r, strings.TrimPrefix(r.URL.Path, "/peers/pending/"))
|
||||||
|
return
|
||||||
|
|
||||||
|
case strings.HasPrefix(r.URL.Path, "/peers/req/"):
|
||||||
|
s.getResultsForRequest(w, r, strings.TrimPrefix(r.URL.Path, "/peers/req/"))
|
||||||
|
return
|
||||||
|
|
||||||
|
case strings.HasPrefix(r.URL.Path, "/peers/status"):
|
||||||
|
var pickID string
|
||||||
|
if strings.HasPrefix(r.URL.Path, "/peers/status/") {
|
||||||
|
pickID = strings.TrimPrefix(r.URL.Path, "/peers/status/")
|
||||||
|
}
|
||||||
|
|
||||||
|
var requests []*Request
|
||||||
|
|
||||||
|
s.state.Use(r.Context(), func(ctx context.Context, state *state) error {
|
||||||
|
for id, p := range state.peers {
|
||||||
|
fmt.Fprintln(w, "PEER:", id[24:], p.Owner, p.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
if pickID != "" {
|
||||||
|
if rq, ok := state.requests[pickID]; ok {
|
||||||
|
requests = append(requests, rq)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
requests = make([]*Request, 0, len(state.requests))
|
||||||
|
for i := range state.requests {
|
||||||
|
rq := state.requests[i]
|
||||||
|
requests = append(requests, rq)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range requests {
|
||||||
|
rq := requests[i]
|
||||||
|
for i := range rq.Responses {
|
||||||
|
res := rq.Responses[i]
|
||||||
|
if peer, ok := state.peers[res.PeerID]; ok {
|
||||||
|
res.Peer = peer
|
||||||
|
res.Peer.ID = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
for i, rq := range requests {
|
||||||
|
fmt.Fprintln(w, "REQ: ", i, rq.RequestIP, len(rq.Responses))
|
||||||
|
for i, peer := range fnOrderByPeer(rq) {
|
||||||
|
fmt.Fprintln(w, " PEER: ", i, peer.RequestID, peer.Name, peer.Latency, peer.Jitter)
|
||||||
|
for i, res := range peer.Results {
|
||||||
|
fmt.Fprintln(w, " RES: ", i, res.RequestID, res.Latency, res.Jitter)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
s.getResults(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case http.MethodPost:
|
||||||
|
switch {
|
||||||
|
case strings.HasPrefix(r.URL.Path, "/peers/req/"):
|
||||||
|
s.postResult(w, r, strings.TrimPrefix(r.URL.Path, "/peers/req/"))
|
||||||
|
return
|
||||||
|
|
||||||
|
case strings.HasPrefix(r.URL.Path, "/peers/req"):
|
||||||
|
s.postRequest(w, r)
|
||||||
|
return
|
||||||
|
|
||||||
|
default:
|
||||||
|
w.WriteHeader(http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
w.WriteHeader(http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func (s *service) getPending(w http.ResponseWriter, r *http.Request, peerID string) {
|
||||||
|
ctx, span := lg.Span(r.Context())
|
||||||
|
defer span.End()
|
||||||
|
|
||||||
|
span.SetAttributes(
|
||||||
|
attribute.String("peerID", peerID),
|
||||||
|
)
|
||||||
|
|
||||||
|
var peer *Peer
|
||||||
|
err := s.state.Use(ctx, func(ctx context.Context, state *state) error {
|
||||||
|
var ok bool
|
||||||
|
if peer, ok = state.peers[peerID]; !ok {
|
||||||
|
return fmt.Errorf("peer not found: %s", peerID)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
span.RecordError(err)
|
||||||
|
w.WriteHeader(http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
info, err := ev.Upsert(ctx, s.es, aggInfo, func(ctx context.Context, agg *Info) error {
|
||||||
|
return agg.OnUpsert() // initialize if not exists
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
span.RecordError(err)
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
requests, err := s.es.Read(ctx, queueRequests, -1, -30)
|
||||||
|
if err != nil {
|
||||||
|
span.RecordError(err)
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
peerResults := &PeerResults{}
|
||||||
|
peerResults.SetStreamID(aggPeer(peerID))
|
||||||
|
err = s.es.Load(ctx, peerResults)
|
||||||
|
if err != nil && !errors.Is(err, ev.ErrNotFound) {
|
||||||
|
span.RecordError(fmt.Errorf("peer not found: %w", err))
|
||||||
|
w.WriteHeader(http.StatusNotFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
var req *Request
|
||||||
|
for _, e := range requests {
|
||||||
|
r := &Request{}
|
||||||
|
r.ApplyEvent(e)
|
||||||
|
|
||||||
|
if !peerResults.Has(r.RequestID) {
|
||||||
|
if !peer.CanSupport(r.RequestIP) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
req = r
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if req == nil {
|
||||||
|
span.RecordError(fmt.Errorf("request not found"))
|
||||||
|
w.WriteHeader(http.StatusNoContent)
|
||||||
|
}
|
||||||
|
|
||||||
|
negotiator := contentnegotiation.NewNegotiator("application/json", "text/environment", "text/plain", "text/html")
|
||||||
|
negotiated, _, err := negotiator.Negotiate(r.Header.Get("Accept"))
|
||||||
|
if err != nil {
|
||||||
|
span.RecordError(err)
|
||||||
|
w.WriteHeader(http.StatusNotAcceptable)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
span.AddEvent(negotiated.String())
|
||||||
|
mime := negotiated.String()
|
||||||
|
switch mime {
|
||||||
|
case "text/environment":
|
||||||
|
w.Header().Set("content-type", negotiated.String())
|
||||||
|
_, err = encodeTo(w, info.MarshalEnviron, req.MarshalEnviron)
|
||||||
|
case "application/json":
|
||||||
|
w.Header().Set("content-type", negotiated.String())
|
||||||
|
var out interface{} = info
|
||||||
|
if req != nil {
|
||||||
|
out = struct {
|
||||||
|
ScriptVersion string `json:"script_version"`
|
||||||
|
RequestID string `json:"req_id"`
|
||||||
|
RequestIP string `json:"req_ip"`
|
||||||
|
Family string `json:"req_family"`
|
||||||
|
Created string `json:"req_created"`
|
||||||
|
}{
|
||||||
|
info.ScriptVersion,
|
||||||
|
req.RequestID,
|
||||||
|
req.RequestIP,
|
||||||
|
strconv.Itoa(req.Family),
|
||||||
|
req.CreatedString(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
err = json.NewEncoder(w).Encode(out)
|
||||||
|
}
|
||||||
|
span.RecordError(err)
|
||||||
|
}
|
||||||
|
func (s *service) getResults(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx, span := lg.Span(r.Context())
|
||||||
|
defer span.End()
|
||||||
|
|
||||||
|
// events, err := s.es.Read(ctx, queueRequests, -1, -30)
|
||||||
|
// if err != nil {
|
||||||
|
// span.RecordError(err)
|
||||||
|
// w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
// return
|
||||||
|
// }
|
||||||
|
|
||||||
|
// requests := make([]*Request, len(events))
|
||||||
|
// for i, req := range events {
|
||||||
|
// if req, ok := req.(*RequestSubmitted); ok {
|
||||||
|
// requests[i], err = s.loadResult(ctx, req.RequestID())
|
||||||
|
// if err != nil {
|
||||||
|
// span.RecordError(err)
|
||||||
|
// w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
// return
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
var requests ListRequest
|
||||||
|
s.state.Use(ctx, func(ctx context.Context, state *state) error {
|
||||||
|
requests = make([]*Request, 0, len(state.requests))
|
||||||
|
|
||||||
|
for _, req := range state.requests {
|
||||||
|
if req.RequestID == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if req.Hidden {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
requests = append(requests, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
sort.Sort(sort.Reverse(requests))
|
||||||
|
|
||||||
|
args := requestArgs(r)
|
||||||
|
args.Requests = requests[:maxResults]
|
||||||
|
|
||||||
|
s.state.Use(ctx, func(ctx context.Context, state *state) error {
|
||||||
|
args.CountPeers = len(state.peers)
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
t := templates["home.go.tpl"]
|
||||||
|
t.Execute(w, args)
|
||||||
|
}
|
||||||
|
func (s *service) getResultsForRequest(w http.ResponseWriter, r *http.Request, uuid string) {
|
||||||
|
ctx, span := lg.Span(r.Context())
|
||||||
|
defer span.End()
|
||||||
|
|
||||||
|
span.SetAttributes(
|
||||||
|
attribute.String("uuid", uuid),
|
||||||
|
)
|
||||||
|
|
||||||
|
var request *Request
|
||||||
|
err := s.state.Use(ctx, func(ctx context.Context, state *state) error {
|
||||||
|
request = state.requests[uuid]
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
w.WriteHeader(http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
request, err = s.loadResult(ctx, request)
|
||||||
|
|
||||||
|
// request, err := s.loadResult(ctx, uuid)
|
||||||
|
if err != nil {
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
negotiator := contentnegotiation.NewNegotiator("application/json", "text/environment", "text/csv", "text/plain", "text/html")
|
||||||
|
negotiated, _, err := negotiator.Negotiate(r.Header.Get("Accept"))
|
||||||
|
if err != nil {
|
||||||
|
w.WriteHeader(http.StatusNotAcceptable)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
span.AddEvent(negotiated.String())
|
||||||
|
switch negotiated.String() {
|
||||||
|
// case "text/environment":
|
||||||
|
// encodeTo(w, responses.MarshalBinary)
|
||||||
|
case "application/json":
|
||||||
|
json.NewEncoder(w).Encode(request)
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
args := requestArgs(r)
|
||||||
|
args.Requests = append(args.Requests, request)
|
||||||
|
span.AddEvent(fmt.Sprint(args))
|
||||||
|
err := renderTo(w, "req.go.tpl", args)
|
||||||
|
span.RecordError(err)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func (s *service) postRequest(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx, span := lg.Span(r.Context())
|
||||||
|
defer span.End()
|
||||||
|
|
||||||
|
if err := r.ParseForm(); err != nil {
|
||||||
|
w.WriteHeader(http.StatusUnprocessableEntity)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
args := requestArgs(r)
|
||||||
|
requestIP := args.RemoteIP
|
||||||
|
|
||||||
|
if ip := r.Form.Get("req_ip"); ip != "" {
|
||||||
|
requestIP = ip
|
||||||
|
}
|
||||||
|
|
||||||
|
ip := net.ParseIP(requestIP)
|
||||||
|
if ip == nil {
|
||||||
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
req := &RequestSubmitted{
|
||||||
|
RequestIP: ip.String(),
|
||||||
|
}
|
||||||
|
if hidden, err := strconv.ParseBool(r.Form.Get("req_hidden")); err == nil {
|
||||||
|
req.Hidden = hidden
|
||||||
|
}
|
||||||
|
|
||||||
|
span.SetAttributes(
|
||||||
|
attribute.Stringer("req_ip", ip),
|
||||||
|
)
|
||||||
|
|
||||||
|
s.es.Append(ctx, queueRequests, event.NewEvents(req))
|
||||||
|
|
||||||
|
http.Redirect(w, r, "/peers/req/"+req.RequestID(), http.StatusSeeOther)
|
||||||
|
}
|
||||||
|
func (s *service) postResult(w http.ResponseWriter, r *http.Request, reqID string) {
|
||||||
|
ctx, span := lg.Span(r.Context())
|
||||||
|
defer span.End()
|
||||||
|
|
||||||
|
span.SetAttributes(
|
||||||
|
attribute.String("id", reqID),
|
||||||
|
)
|
||||||
|
|
||||||
|
if _, err := ulid.ParseStrict(reqID); err != nil {
|
||||||
|
w.WriteHeader(http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := r.ParseForm(); err != nil {
|
||||||
|
w.WriteHeader(http.StatusUnprocessableEntity)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
form := make([]string, 0, len(r.Form))
|
||||||
|
for k, vals := range r.Form {
|
||||||
|
for _, v := range vals {
|
||||||
|
form = append(form, fmt.Sprint(k, v))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
span.SetAttributes(
|
||||||
|
attribute.StringSlice("form", form),
|
||||||
|
)
|
||||||
|
|
||||||
|
peerID := r.Form.Get("peer_id")
|
||||||
|
|
||||||
|
err := s.state.Use(ctx, func(ctx context.Context, state *state) error {
|
||||||
|
var ok bool
|
||||||
|
if _, ok = state.peers[peerID]; !ok {
|
||||||
|
log.Printf("peer not found: %s\n", peerID)
|
||||||
|
return fmt.Errorf("peer not found: %s", peerID)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
span.RecordError(err)
|
||||||
|
w.WriteHeader(http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
peerResults := &PeerResults{}
|
||||||
|
peerResults.SetStreamID(aggPeer(peerID))
|
||||||
|
err = s.es.Load(ctx, peerResults)
|
||||||
|
if err != nil {
|
||||||
|
span.RecordError(fmt.Errorf("peer not found: %w", err))
|
||||||
|
w.WriteHeader(http.StatusNotFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
if peerResults.Has(reqID) {
|
||||||
|
span.RecordError(fmt.Errorf("request previously recorded: req=%v peer=%v", reqID, peerID))
|
||||||
|
w.WriteHeader(http.StatusAlreadyReported)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var unreach bool
|
||||||
|
latency, err := strconv.ParseFloat(r.Form.Get("res_latency"), 64)
|
||||||
|
if err != nil {
|
||||||
|
unreach = true
|
||||||
|
}
|
||||||
|
|
||||||
|
req := &ResultSubmitted{
|
||||||
|
RequestID: reqID,
|
||||||
|
PeerID: r.Form.Get("peer_id"),
|
||||||
|
PeerVersion: r.Form.Get("peer_version"),
|
||||||
|
Latency: latency,
|
||||||
|
Unreachable: unreach,
|
||||||
|
}
|
||||||
|
|
||||||
|
if jitter, err := strconv.ParseFloat(r.Form.Get("res_jitter"), 64); err == nil {
|
||||||
|
req.Jitter = jitter
|
||||||
|
} else {
|
||||||
|
span.RecordError(err)
|
||||||
|
}
|
||||||
|
if minrtt, err := strconv.ParseFloat(r.Form.Get("res_minrtt"), 64); err == nil {
|
||||||
|
req.MinRTT = minrtt
|
||||||
|
} else {
|
||||||
|
span.RecordError(err)
|
||||||
|
}
|
||||||
|
if maxrtt, err := strconv.ParseFloat(r.Form.Get("res_maxrtt"), 64); err == nil {
|
||||||
|
req.MaxRTT = maxrtt
|
||||||
|
} else {
|
||||||
|
span.RecordError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
span.SetAttributes(
|
||||||
|
attribute.Stringer("result", req),
|
||||||
|
)
|
||||||
|
|
||||||
|
log.Printf("record result: %v", req)
|
||||||
|
s.es.Append(ctx, queueResults, event.NewEvents(req))
|
||||||
|
}
|
||||||
|
|
||||||
|
func renderTo(w io.Writer, name string, args any) (err error) {
|
||||||
|
defer func() {
|
||||||
|
if p := recover(); p != nil {
|
||||||
|
err = fmt.Errorf("panic: %s", p)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprint(w, err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
t, ok := templates[name]
|
||||||
|
if !ok || t == nil {
|
||||||
|
return fmt.Errorf("missing template")
|
||||||
|
}
|
||||||
|
return t.Execute(w, args)
|
||||||
|
}
|
||||||
|
func encodeTo(w io.Writer, fns ...func() ([]byte, error)) (int, error) {
|
||||||
|
i := 0
|
||||||
|
for _, fn := range fns {
|
||||||
|
b, err := fn()
|
||||||
|
if err != nil {
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
|
j, err := w.Write(b)
|
||||||
|
i += j
|
||||||
|
if err != nil {
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return i, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadTemplates() error {
|
||||||
|
if templates != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
templates = make(map[string]*template.Template)
|
||||||
|
tmplFiles, err := fs.ReadDir(files, "pages")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tmpl := range tmplFiles {
|
||||||
|
if tmpl.IsDir() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
pt := template.New(tmpl.Name())
|
||||||
|
pt.Funcs(funcMap)
|
||||||
|
pt, err = pt.ParseFS(files, "pages/"+tmpl.Name(), "layouts/*.go.tpl")
|
||||||
|
if err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
templates[tmpl.Name()] = pt
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var funcMap = map[string]any{
|
||||||
|
"orderByPeer": fnOrderByPeer,
|
||||||
|
"countResponses": fnCountResponses,
|
||||||
|
}
|
||||||
|
|
||||||
|
type peerResult struct {
|
||||||
|
RequestID string
|
||||||
|
Name string
|
||||||
|
Country string
|
||||||
|
Latency float64
|
||||||
|
Jitter float64
|
||||||
|
}
|
||||||
|
type peer struct {
|
||||||
|
RequestID string
|
||||||
|
Name string
|
||||||
|
Note string
|
||||||
|
Nick string
|
||||||
|
Country string
|
||||||
|
Latency float64
|
||||||
|
Jitter float64
|
||||||
|
VPNTypes []string
|
||||||
|
|
||||||
|
Results peerResults
|
||||||
|
}
|
||||||
|
type listPeer []peer
|
||||||
|
|
||||||
|
func (lis listPeer) Len() int {
|
||||||
|
return len(lis)
|
||||||
|
}
|
||||||
|
func (lis listPeer) Less(i, j int) bool {
|
||||||
|
if lis[j].Latency == 0.0 && lis[i].Latency > 0.0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if lis[i].Latency == 0.0 && lis[j].Latency > 0.0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return lis[i].Latency < lis[j].Latency
|
||||||
|
}
|
||||||
|
func (lis listPeer) Swap(i, j int) {
|
||||||
|
lis[i], lis[j] = lis[j], lis[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
type peerResults []peerResult
|
||||||
|
|
||||||
|
func (lis peerResults) Len() int {
|
||||||
|
return len(lis)
|
||||||
|
}
|
||||||
|
func (lis peerResults) Less(i, j int) bool {
|
||||||
|
if lis[j].Latency == 0.0 && lis[i].Latency > 0.0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if lis[i].Latency == 0.0 && lis[j].Latency > 0.0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return lis[i].Latency < lis[j].Latency
|
||||||
|
}
|
||||||
|
func (lis peerResults) Swap(i, j int) {
|
||||||
|
lis[i], lis[j] = lis[j], lis[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
func fnOrderByPeer(rq *Request) listPeer {
|
||||||
|
peers := make(map[string]peer)
|
||||||
|
for i := range rq.Responses {
|
||||||
|
if rq.Responses[i] == nil || rq.Responses[i].Peer == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
rs := rq.Responses[i]
|
||||||
|
|
||||||
|
p, ok := peers[rs.Peer.Owner]
|
||||||
|
|
||||||
|
if !ok {
|
||||||
|
p.RequestID = rq.RequestID
|
||||||
|
p.Country = rs.Peer.Country
|
||||||
|
p.Name = rs.Peer.Name
|
||||||
|
p.Nick = rs.Peer.Nick
|
||||||
|
p.Note = rs.Peer.Note
|
||||||
|
p.Latency = rs.Latency
|
||||||
|
p.Jitter = rs.Jitter
|
||||||
|
p.VPNTypes = rs.Peer.Type
|
||||||
|
}
|
||||||
|
|
||||||
|
p.Results = append(p.Results, peerResult{
|
||||||
|
RequestID: rq.RequestID,
|
||||||
|
Name: rs.Peer.Name,
|
||||||
|
Country: rs.Peer.Country,
|
||||||
|
Latency: rs.Latency,
|
||||||
|
Jitter: rs.Jitter,
|
||||||
|
})
|
||||||
|
|
||||||
|
peers[rs.Peer.Owner] = p
|
||||||
|
}
|
||||||
|
|
||||||
|
peerList := make(listPeer, 0, len(peers))
|
||||||
|
for i := range peers {
|
||||||
|
v := peers[i]
|
||||||
|
sort.Sort(v.Results)
|
||||||
|
|
||||||
|
v.Name = v.Results[0].Name
|
||||||
|
v.Country = v.Results[0].Country
|
||||||
|
v.Latency = v.Results[0].Latency
|
||||||
|
v.Jitter = v.Results[0].Jitter
|
||||||
|
|
||||||
|
peerList = append(peerList, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Sort(peerList)
|
||||||
|
return peerList
|
||||||
|
}
|
||||||
|
func fnCountResponses(rq *Request) int {
|
||||||
|
count := 0
|
||||||
|
for _, res := range rq.Responses {
|
||||||
|
if !res.Unreachable {
|
||||||
|
count++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return count
|
||||||
|
}
|
||||||
|
|
||||||
|
// func filter(peer *Peer, requests, responses event.Events) *RequestSubmitted {
|
||||||
|
// have := make(map[string]struct{}, len(responses))
|
||||||
|
// for _, res := range toList[ResultSubmitted](responses...) {
|
||||||
|
// have[res.RequestID] = struct{}{}
|
||||||
|
// }
|
||||||
|
// for _, req := range reverse(toList[RequestSubmitted](requests...)...) {
|
||||||
|
// if _, ok := have[req.RequestID()]; !ok {
|
||||||
|
// if !peer.CanSupport(req.RequestIP) {
|
||||||
|
// continue
|
||||||
|
// }
|
||||||
|
|
||||||
|
// return req
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// return nil
|
||||||
|
// }
|
||||||
|
// func toList[E any, T es.PE[E]](lis ...event.Event) []T {
|
||||||
|
// newLis := make([]T, 0, len(lis))
|
||||||
|
// for i := range lis {
|
||||||
|
// if e, ok := lis[i].(T); ok {
|
||||||
|
// newLis = append(newLis, e)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// return newLis
|
||||||
|
// }
|
||||||
|
// func reverse[T any](s ...T) []T {
|
||||||
|
// first, last := 0, len(s)-1
|
||||||
|
// for first < last {
|
||||||
|
// s[first], s[last] = s[last], s[first]
|
||||||
|
// first++
|
||||||
|
// last--
|
||||||
|
// }
|
||||||
|
// return s
|
||||||
|
// }
|
||||||
216
app/peerfinder/jobs.go
Normal file
216
app/peerfinder/jobs.go
Normal file
@@ -0,0 +1,216 @@
|
|||||||
|
package peerfinder
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"go.sour.is/ev"
|
||||||
|
"go.sour.is/ev/pkg/event"
|
||||||
|
"go.sour.is/pkg/lg"
|
||||||
|
"go.sour.is/pkg/set"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RefreshJob retrieves peer info from the peerdb
|
||||||
|
func (s *service) RefreshJob(ctx context.Context, _ time.Time) error {
|
||||||
|
ctx, span := lg.Span(ctx)
|
||||||
|
defer span.End()
|
||||||
|
|
||||||
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, s.statusURL, nil)
|
||||||
|
span.RecordError(err)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
req.Header.Set("Accept", "application/json")
|
||||||
|
|
||||||
|
res, err := http.DefaultClient.Do(req)
|
||||||
|
span.RecordError(err)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer res.Body.Close()
|
||||||
|
var peers []*Peer
|
||||||
|
err = json.NewDecoder(res.Body).Decode(&peers)
|
||||||
|
span.RecordError(err)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = s.state.Use(ctx, func(ctx context.Context, t *state) error {
|
||||||
|
for _, peer := range peers {
|
||||||
|
t.peers[peer.ID] = peer
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
span.RecordError(err)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("processed %d peers", len(peers))
|
||||||
|
span.AddEvent(fmt.Sprintf("processed %d peers", len(peers)))
|
||||||
|
|
||||||
|
s.up.Store(true)
|
||||||
|
|
||||||
|
err = s.cleanPeerJobs(ctx)
|
||||||
|
|
||||||
|
span.RecordError(err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
const maxResults = 30
|
||||||
|
|
||||||
|
// CleanJob truncates streams old request data
|
||||||
|
func (s *service) CleanJob(ctx context.Context, now time.Time) error {
|
||||||
|
ctx, span := lg.Span(ctx)
|
||||||
|
defer span.End()
|
||||||
|
|
||||||
|
span.AddEvent("clear peerfinder requests")
|
||||||
|
|
||||||
|
err := s.cleanRequests(ctx, now)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// if err = s.cleanResults(ctx, endRequestID); err != nil {
|
||||||
|
// return err
|
||||||
|
// }
|
||||||
|
|
||||||
|
return s.cleanPeerJobs(ctx)
|
||||||
|
}
|
||||||
|
func (s *service) cleanPeerJobs(ctx context.Context) error {
|
||||||
|
ctx, span := lg.Span(ctx)
|
||||||
|
defer span.End()
|
||||||
|
|
||||||
|
peers := set.New[string]()
|
||||||
|
err := s.state.Use(ctx, func(ctx context.Context, state *state) error {
|
||||||
|
for id := range state.peers {
|
||||||
|
peers.Add(id)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// trunctate all the peer streams to last 30
|
||||||
|
for streamID := range peers {
|
||||||
|
streamID = aggPeer(streamID)
|
||||||
|
first, err := s.es.FirstIndex(ctx, streamID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
last, err := s.es.LastIndex(ctx, streamID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if last-first < maxResults {
|
||||||
|
fmt.Println("SKIP", streamID, first, last)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
newFirst := int64(last - 30)
|
||||||
|
// fmt.Println("TRUNC", streamID, first, newFirst, last)
|
||||||
|
span.AddEvent(fmt.Sprint("TRUNC", streamID, first, newFirst, last))
|
||||||
|
err = s.es.Truncate(ctx, streamID, int64(newFirst))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
func (s *service) cleanRequests(ctx context.Context, now time.Time) error {
|
||||||
|
ctx, span := lg.Span(ctx)
|
||||||
|
defer span.End()
|
||||||
|
|
||||||
|
var streamIDs []string
|
||||||
|
var startPosition, endPosition int64
|
||||||
|
|
||||||
|
first, err := s.es.FirstIndex(ctx, queueRequests)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
last, err := s.es.LastIndex(ctx, queueRequests)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if last-first < maxResults {
|
||||||
|
// fmt.Println("SKIP", queueRequests, first, last)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
startPosition = int64(first - 1)
|
||||||
|
endPosition = int64(last - maxResults)
|
||||||
|
|
||||||
|
for {
|
||||||
|
events, err := s.es.Read(ctx, queueRequests, startPosition, 1000) // read 1000 from the top each loop.
|
||||||
|
if err != nil && !errors.Is(err, ev.ErrNotFound) {
|
||||||
|
span.RecordError(err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(events) == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
startPosition = int64(events.Last().EventMeta().ActualPosition)
|
||||||
|
for _, event := range events {
|
||||||
|
switch e := event.(type) {
|
||||||
|
case *RequestSubmitted:
|
||||||
|
if e.EventMeta().ActualPosition < last-maxResults {
|
||||||
|
streamIDs = append(streamIDs, e.RequestID())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// truncate all reqs to found end position
|
||||||
|
// fmt.Println("TRUNC", queueRequests, int64(endPosition), last)
|
||||||
|
span.AddEvent(fmt.Sprint("TRUNC", queueRequests, int64(endPosition), last))
|
||||||
|
err = s.es.Truncate(ctx, queueRequests, int64(endPosition))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// truncate all the request streams
|
||||||
|
for _, streamID := range streamIDs {
|
||||||
|
s.state.Use(ctx, func(ctx context.Context, state *state) error {
|
||||||
|
return state.ApplyEvents(event.NewEvents(&RequestTruncated{
|
||||||
|
RequestID: streamID,
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
|
||||||
|
err := s.cleanResult(ctx, streamID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
func (s *service) cleanResult(ctx context.Context, requestID string) error {
|
||||||
|
ctx, span := lg.Span(ctx)
|
||||||
|
defer span.End()
|
||||||
|
|
||||||
|
streamID := aggRequest(requestID)
|
||||||
|
|
||||||
|
last, err := s.es.LastIndex(ctx, streamID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// truncate all reqs to found end position
|
||||||
|
// fmt.Println("TRUNC", streamID, last)
|
||||||
|
span.AddEvent(fmt.Sprint("TRUNC", streamID, last))
|
||||||
|
err = s.es.Truncate(ctx, streamID, int64(last))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
38
app/peerfinder/layouts/main.go.tpl
Normal file
38
app/peerfinder/layouts/main.go.tpl
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
{{define "main"}}
|
||||||
|
<!DOCTYPE html>
|
||||||
|
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
{{template "meta" .}}
|
||||||
|
<title>DN42 PingFinder</title>
|
||||||
|
|
||||||
|
<link href="/peers/assets/bootstrap.min.css" rel="stylesheet" integrity="sha384-1q8mTJOASx8j1Au+a5WDVnPi2lkFfwwEAa8hDDdjZlpLegxhjVME1fgjWPGmkzs7" crossorigin="anonymous">
|
||||||
|
<link href="/peers/assets/peerfinder.css" rel="stylesheet" crossorigin="anonymous">
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<div class="container-fluid">
|
||||||
|
<div class="header clearfix">
|
||||||
|
<nav>
|
||||||
|
<ul class="nav nav-pills pull-right">
|
||||||
|
<li role="presentation"><a href="/peers">Home</a></li>
|
||||||
|
<!--
|
||||||
|
<li role="presentation"><a href="/peers/status">Status</a></li>
|
||||||
|
-->
|
||||||
|
<li role="presentation"><a href="//util.sour.is/peer">Sign up/Manage</a></li>
|
||||||
|
<li role="presentation"><a href="https://git.dn42.dev/dn42/pingfinder/src/branch/master/clients">Scripts</a></li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
<h3 class="text-muted">DN42 PeerFinder</h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class=container>
|
||||||
|
{{template "content" .}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
{{end}}
|
||||||
65
app/peerfinder/pages/home.go.tpl
Normal file
65
app/peerfinder/pages/home.go.tpl
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
{{template "main" .}}
|
||||||
|
|
||||||
|
{{define "meta"}}
|
||||||
|
<meta http-equiv="refresh" content="30">
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
{{define "content"}}
|
||||||
|
<h2>What is this?</h2>
|
||||||
|
|
||||||
|
<p>This tool allows you to find "good" peerings
|
||||||
|
for <a href="https://dn42.net">dn42</a>, by measuring the latency from
|
||||||
|
various points in the network towards you.</p>
|
||||||
|
|
||||||
|
<p>If you don't know what dn42 is,
|
||||||
|
read <a href="https://dn42.net/Home">the website</a> and in particular
|
||||||
|
the <a href="https://dn42.net/Getting-started-with-dn42">Getting Started
|
||||||
|
guide</a>.</p>
|
||||||
|
|
||||||
|
<h2>How does it work?</h2>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<ol>
|
||||||
|
<li>You enter your (Internet) IP address</li>
|
||||||
|
<li>Various routers participating in dn42 will ping you over the Internet</li>
|
||||||
|
<li>After a short while, you get back all the latency results</li>
|
||||||
|
<li>You can then peer with people close to you (low latency)</li>
|
||||||
|
</ol>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<form class="form-inline" method="POST" action="/peers/req">
|
||||||
|
<label>Ping IP Address [Check Hidden?]:</label>
|
||||||
|
<div class="input-group input-group-sm">
|
||||||
|
<input class="form-control" type="text" name="req_ip" placeholder="{{ .RemoteIP }}">
|
||||||
|
<span class="input-group-addon">
|
||||||
|
<input type="checkbox" name="req_hidden" value=1 aria-label="Hidden?">
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-default" type="submit">Submit</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<p>If you mark your measurement as hidden, it will not be displayed on the
|
||||||
|
page below. Note that the IP addresses of the target will be shown alongside the result.</p>
|
||||||
|
|
||||||
|
|
||||||
|
<div class=row>
|
||||||
|
<h2>Results</h2>
|
||||||
|
{{ with $args := . }}
|
||||||
|
{{ range $req := .Requests }}
|
||||||
|
{{ if ne $req.RequestID "" }}
|
||||||
|
<div class="panel panel-primary">
|
||||||
|
<div class="panel-heading">
|
||||||
|
<a href="/peers/req/{{ $req.RequestID }}">
|
||||||
|
{{ $req.RequestIP }} on {{ $req.Created.Format "02 Jan 06 15:04 MST" }}
|
||||||
|
</a> — <b>Request ID:</b> {{ $req.RequestID }}
|
||||||
|
<div style='float:right'>
|
||||||
|
<a href="/peers/req/{{ $req.RequestID }}" class='btn btn-success'>{{ countResponses $req }} / {{ $args.CountPeers }} </a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
{{end}}
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{end}}
|
||||||
50
app/peerfinder/pages/req.go.tpl
Normal file
50
app/peerfinder/pages/req.go.tpl
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
{{template "main" .}}
|
||||||
|
|
||||||
|
{{define "meta"}}
|
||||||
|
<meta http-equiv="refresh" content="30">
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
|
||||||
|
{{define "content"}}
|
||||||
|
{{range .Requests}}
|
||||||
|
<h2>Results to {{.RequestIP}}{{if .Hidden}} 👁️{{end}}</h2>
|
||||||
|
|
||||||
|
{{range orderByPeer .}}
|
||||||
|
<div class="panel panel-primary" id="peer-{{.Nick}}">
|
||||||
|
<div class="panel-heading">
|
||||||
|
<b> {{.Country}} :: {{.Name}} :: {{.Nick}} </b>
|
||||||
|
<div style='float:right'>
|
||||||
|
<a class='btn btn-success' href="#peer-{{.Nick}}">{{ if eq .Latency 0.0 }}—{{ else }}{{printf "%0.3f ms" .Latency}}{{ end }}</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="panel-body">
|
||||||
|
<b>Note:</b> {{.Note}}<br/>
|
||||||
|
<b>VPN Types:</b> {{range .VPNTypes}} {{.}} {{end}}<br/>
|
||||||
|
<b>IRC:</b> {{.Nick}}
|
||||||
|
<h4>Other Results</h4>
|
||||||
|
|
||||||
|
<table class="table table-striped">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Peer Name</th>
|
||||||
|
<th>Country</th>
|
||||||
|
<th>Latency</th>
|
||||||
|
<th>Jitter</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{{range .Results}}
|
||||||
|
<tr>
|
||||||
|
<th>{{.Name}}</th>
|
||||||
|
<td>{{.Country}}</td>
|
||||||
|
<td>{{ if eq .Latency 0.0 }}—{{ else }}{{printf "%0.3f ms" .Latency}}{{ end }}</td>
|
||||||
|
<td>{{ if eq .Jitter 0.0 }}—{{ else }}{{ printf "%0.3f ms" .Jitter }}{{ end }}</td>
|
||||||
|
</tr>
|
||||||
|
{{end}}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
{{end}}
|
||||||
|
{{end}}
|
||||||
@@ -1,183 +0,0 @@
|
|||||||
package peerfinder
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"encoding/json"
|
|
||||||
"net/netip"
|
|
||||||
"strconv"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/sour-is/ev/pkg/es/event"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Request struct {
|
|
||||||
eventMeta event.Meta
|
|
||||||
|
|
||||||
RequestIP string `json:"req_ip"`
|
|
||||||
Hidden bool `json:"hide,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Request) StreamID() string {
|
|
||||||
return r.EventMeta().GetEventID()
|
|
||||||
}
|
|
||||||
func (r *Request) RequestID() string {
|
|
||||||
return r.EventMeta().GetEventID()
|
|
||||||
}
|
|
||||||
func (r *Request) Created() time.Time {
|
|
||||||
return r.EventMeta().Created()
|
|
||||||
}
|
|
||||||
func (r *Request) CreatedString() string {
|
|
||||||
return r.Created().Format("2006-01-02 15:04:05")
|
|
||||||
}
|
|
||||||
func (r *Request) Family() int {
|
|
||||||
if r == nil {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
ip, err := netip.ParseAddr(r.RequestIP)
|
|
||||||
switch {
|
|
||||||
case err != nil:
|
|
||||||
return 0
|
|
||||||
case ip.Is4():
|
|
||||||
return 1
|
|
||||||
default:
|
|
||||||
return 2
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ event.Event = (*Request)(nil)
|
|
||||||
|
|
||||||
func (e *Request) EventMeta() event.Meta {
|
|
||||||
if e == nil {
|
|
||||||
return event.Meta{}
|
|
||||||
}
|
|
||||||
return e.eventMeta
|
|
||||||
}
|
|
||||||
func (e *Request) SetEventMeta(m event.Meta) {
|
|
||||||
if e != nil {
|
|
||||||
e.eventMeta = m
|
|
||||||
}
|
|
||||||
}
|
|
||||||
func (e *Request) MarshalBinary() (text []byte, err error) {
|
|
||||||
return json.Marshal(e)
|
|
||||||
}
|
|
||||||
func (e *Request) UnmarshalBinary(b []byte) error {
|
|
||||||
return json.Unmarshal(b, e)
|
|
||||||
}
|
|
||||||
func (e *Request) MarshalEnviron() ([]byte, error) {
|
|
||||||
if e == nil {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var b bytes.Buffer
|
|
||||||
b.WriteString("REQ_ID=")
|
|
||||||
b.WriteString(e.RequestID())
|
|
||||||
b.WriteRune('\n')
|
|
||||||
|
|
||||||
b.WriteString("REQ_IP=")
|
|
||||||
b.WriteString(e.RequestIP)
|
|
||||||
b.WriteRune('\n')
|
|
||||||
|
|
||||||
b.WriteString("REQ_FAMILY=")
|
|
||||||
if family := e.Family(); family > 0 {
|
|
||||||
b.WriteString(strconv.Itoa(family))
|
|
||||||
}
|
|
||||||
b.WriteRune('\n')
|
|
||||||
|
|
||||||
b.WriteString("REQ_CREATED=")
|
|
||||||
b.WriteString(e.CreatedString())
|
|
||||||
b.WriteRune('\n')
|
|
||||||
|
|
||||||
return b.Bytes(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type Result struct {
|
|
||||||
eventMeta event.Meta
|
|
||||||
|
|
||||||
RequestID string `json:"req_id"`
|
|
||||||
PeerID string `json:"peer_id"`
|
|
||||||
PeerVersion string `json:"peer_version"`
|
|
||||||
Latency float64 `json:"latency,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Result) Created() time.Time {
|
|
||||||
return r.eventMeta.Created()
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ event.Event = (*Result)(nil)
|
|
||||||
|
|
||||||
func (e *Result) EventMeta() event.Meta {
|
|
||||||
if e == nil {
|
|
||||||
return event.Meta{}
|
|
||||||
}
|
|
||||||
return e.eventMeta
|
|
||||||
}
|
|
||||||
func (e *Result) SetEventMeta(m event.Meta) {
|
|
||||||
if e != nil {
|
|
||||||
e.eventMeta = m
|
|
||||||
}
|
|
||||||
}
|
|
||||||
func (e *Result) MarshalBinary() (text []byte, err error) {
|
|
||||||
return json.Marshal(e)
|
|
||||||
}
|
|
||||||
func (e *Result) UnmarshalBinary(b []byte) error {
|
|
||||||
return json.Unmarshal(b, e)
|
|
||||||
}
|
|
||||||
|
|
||||||
type Info struct {
|
|
||||||
ScriptVersion string `json:"script_version"`
|
|
||||||
|
|
||||||
event.AggregateRoot
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ event.Aggregate = (*Info)(nil)
|
|
||||||
|
|
||||||
func (a *Info) ApplyEvent(lis ...event.Event) {
|
|
||||||
for _, e := range lis {
|
|
||||||
switch e := e.(type) {
|
|
||||||
case *VersionChanged:
|
|
||||||
a.ScriptVersion = e.ScriptVersion
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
func (a *Info) MarshalEnviron() ([]byte, error) {
|
|
||||||
var b bytes.Buffer
|
|
||||||
|
|
||||||
b.WriteString("SCRIPT_VERSION=")
|
|
||||||
b.WriteString(a.ScriptVersion)
|
|
||||||
b.WriteRune('\n')
|
|
||||||
|
|
||||||
return b.Bytes(), nil
|
|
||||||
}
|
|
||||||
func (a *Info) OnCreate() error {
|
|
||||||
if a.StreamVersion() == 0 {
|
|
||||||
event.Raise(a, &VersionChanged{ScriptVersion: initVersion})
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type VersionChanged struct {
|
|
||||||
ScriptVersion string `json:"script_version"`
|
|
||||||
|
|
||||||
eventMeta event.Meta
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ event.Event = (*VersionChanged)(nil)
|
|
||||||
|
|
||||||
func (e *VersionChanged) EventMeta() event.Meta {
|
|
||||||
if e == nil {
|
|
||||||
return event.Meta{}
|
|
||||||
}
|
|
||||||
return e.eventMeta
|
|
||||||
}
|
|
||||||
func (e *VersionChanged) SetEventMeta(m event.Meta) {
|
|
||||||
if e != nil {
|
|
||||||
e.eventMeta = m
|
|
||||||
}
|
|
||||||
}
|
|
||||||
func (e *VersionChanged) MarshalBinary() (text []byte, err error) {
|
|
||||||
return json.Marshal(e)
|
|
||||||
}
|
|
||||||
func (e *VersionChanged) UnmarshalBinary(b []byte) error {
|
|
||||||
return json.Unmarshal(b, e)
|
|
||||||
}
|
|
||||||
@@ -2,267 +2,178 @@ package peerfinder
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"fmt"
|
||||||
"io"
|
"sync/atomic"
|
||||||
"net/http"
|
"time"
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
ulid "github.com/oklog/ulid/v2"
|
"go.sour.is/pkg/lg"
|
||||||
contentnegotiation "gitlab.com/jamietanna/content-negotiation-go"
|
"go.sour.is/pkg/locker"
|
||||||
|
"go.uber.org/multierr"
|
||||||
|
|
||||||
"github.com/sour-is/ev/internal/lg"
|
"go.sour.is/ev"
|
||||||
"github.com/sour-is/ev/pkg/es"
|
"go.sour.is/ev/pkg/event"
|
||||||
"github.com/sour-is/ev/pkg/es/event"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
queueRequests = "pf-requests"
|
|
||||||
queueResponses = "pf-response-"
|
|
||||||
aggInfo = "pf-info"
|
aggInfo = "pf-info"
|
||||||
initVersion = "1.1.0"
|
queueRequests = "pf-requests"
|
||||||
|
queueResults = "pf-results"
|
||||||
|
initVersion = "1.2.1"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func aggRequest(id string) string { return "pf-request-" + id }
|
||||||
|
func aggPeer(id string) string { return "pf-peer-" + id }
|
||||||
|
|
||||||
type service struct {
|
type service struct {
|
||||||
es *es.EventStore
|
es *ev.EventStore
|
||||||
|
statusURL string
|
||||||
|
|
||||||
|
state *locker.Locked[state]
|
||||||
|
up atomic.Bool
|
||||||
|
stop func()
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(ctx context.Context, es *es.EventStore) (*service, error) {
|
type state struct {
|
||||||
|
peers map[string]*Peer
|
||||||
|
requests map[string]*Request
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(ctx context.Context, es *ev.EventStore, statusURL string) (*service, error) {
|
||||||
ctx, span := lg.Span(ctx)
|
ctx, span := lg.Span(ctx)
|
||||||
defer span.End()
|
defer span.End()
|
||||||
|
|
||||||
if err := event.Register(ctx, &Request{}, &Result{}, &VersionChanged{}); err != nil {
|
loadTemplates()
|
||||||
|
|
||||||
|
if err := event.Register(ctx, &RequestSubmitted{}, &ResultSubmitted{}, &VersionChanged{}); err != nil {
|
||||||
span.RecordError(err)
|
span.RecordError(err)
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
svc := &service{es: es}
|
svc := &service{
|
||||||
|
es: es,
|
||||||
|
statusURL: statusURL,
|
||||||
|
state: locker.New(&state{
|
||||||
|
peers: make(map[string]*Peer),
|
||||||
|
requests: make(map[string]*Request),
|
||||||
|
})}
|
||||||
|
|
||||||
return svc, nil
|
return svc, nil
|
||||||
}
|
}
|
||||||
func (s *service) RegisterHTTP(mux *http.ServeMux) {
|
func (s *service) loadResult(ctx context.Context, request *Request) (*Request, error) {
|
||||||
mux.Handle("/peers/", lg.Htrace(s, "peers"))
|
if request == nil {
|
||||||
|
return request, nil
|
||||||
}
|
}
|
||||||
func (s *service) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
||||||
ctx := r.Context()
|
|
||||||
|
|
||||||
_, span := lg.Span(ctx)
|
return request, s.state.Use(ctx, func(ctx context.Context, t *state) error {
|
||||||
defer span.End()
|
|
||||||
|
|
||||||
r = r.WithContext(ctx)
|
for i := range request.Responses {
|
||||||
|
res := request.Responses[i]
|
||||||
switch r.Method {
|
if peer, ok := t.peers[res.PeerID]; ok {
|
||||||
case http.MethodGet:
|
res.Peer = peer
|
||||||
switch {
|
res.Peer.ID = ""
|
||||||
case strings.HasPrefix(r.URL.Path, "/peers/pending/"):
|
|
||||||
s.getPending(w, r, strings.TrimPrefix(r.URL.Path, "/peers/pending/"))
|
|
||||||
return
|
|
||||||
|
|
||||||
case strings.HasPrefix(r.URL.Path, "/peers/req/"):
|
|
||||||
s.getResults(w, r, strings.TrimPrefix(r.URL.Path, "/peers/req/"))
|
|
||||||
return
|
|
||||||
|
|
||||||
default:
|
|
||||||
w.WriteHeader(http.StatusNotFound)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
case http.MethodPost:
|
|
||||||
switch {
|
|
||||||
case strings.HasPrefix(r.URL.Path, "/peers/req/"):
|
|
||||||
s.postResult(w, r, strings.TrimPrefix(r.URL.Path, "/peers/req/"))
|
|
||||||
return
|
|
||||||
|
|
||||||
case strings.HasPrefix(r.URL.Path, "/peers/req"):
|
|
||||||
s.postRequest(w, r)
|
|
||||||
return
|
|
||||||
|
|
||||||
default:
|
|
||||||
w.WriteHeader(http.StatusNotFound)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
w.WriteHeader(http.StatusMethodNotAllowed)
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *service) getPending(w http.ResponseWriter, r *http.Request, uuid string) {
|
return nil
|
||||||
ctx := r.Context()
|
|
||||||
|
|
||||||
_, span := lg.Span(ctx)
|
|
||||||
defer span.End()
|
|
||||||
|
|
||||||
info, err := es.Upsert(ctx, s.es, "pf-info", func(ctx context.Context, agg *Info) error {
|
|
||||||
return agg.OnCreate() // initialize if not exists
|
|
||||||
})
|
})
|
||||||
if err != nil {
|
}
|
||||||
span.RecordError(err)
|
func (s *service) Run(ctx context.Context) (err error) {
|
||||||
w.WriteHeader(http.StatusInternalServerError)
|
var errs error
|
||||||
return
|
|
||||||
|
ctx, span := lg.Span(ctx)
|
||||||
|
defer span.End()
|
||||||
|
|
||||||
|
ctx, s.stop = context.WithCancel(ctx)
|
||||||
|
|
||||||
|
subReq, e := s.es.EventStream().Subscribe(ctx, queueRequests, 0)
|
||||||
|
errs = multierr.Append(errs, e)
|
||||||
|
|
||||||
|
subRes, e := s.es.EventStream().Subscribe(ctx, queueResults, 0)
|
||||||
|
errs = multierr.Append(errs, e)
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
err = multierr.Combine(subReq.Close(ctx), subRes.Close(ctx), err)
|
||||||
|
}()
|
||||||
|
|
||||||
|
if errs != nil {
|
||||||
|
return errs
|
||||||
}
|
}
|
||||||
|
|
||||||
requests, err := s.es.Read(ctx, queueRequests, -1, -30)
|
for {
|
||||||
if err != nil {
|
var events event.Events
|
||||||
span.RecordError(err)
|
select {
|
||||||
w.WriteHeader(http.StatusInternalServerError)
|
case <-ctx.Done():
|
||||||
return
|
return nil
|
||||||
|
case ok := <-subReq.Recv(ctx):
|
||||||
|
if ok {
|
||||||
|
events, err = subReq.Events(ctx)
|
||||||
|
}
|
||||||
|
case ok := <-subRes.Recv(ctx):
|
||||||
|
if ok {
|
||||||
|
events, err = subRes.Events(ctx)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
responses, err := s.es.Read(ctx, queueResponses+uuid, -1, -30)
|
s.state.Use(ctx, func(ctx context.Context, state *state) error {
|
||||||
if err != nil {
|
return state.ApplyEvents(events)
|
||||||
span.RecordError(err)
|
})
|
||||||
w.WriteHeader(http.StatusInternalServerError)
|
events = events[:0]
|
||||||
return
|
}
|
||||||
|
}
|
||||||
|
func (s *service) Stop(ctx context.Context) (err error) {
|
||||||
|
defer func() {
|
||||||
|
if p := recover(); p != nil {
|
||||||
|
err = fmt.Errorf("PANIC: %v", p)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
s.stop()
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
req := filter(requests, responses)
|
func (s *state) ApplyEvents(events event.Events) error {
|
||||||
|
for _, e := range events {
|
||||||
negotiator := contentnegotiation.NewNegotiator("application/json", "text/environment", "text/plain", "text/html")
|
switch e := e.(type) {
|
||||||
negotiated, _, err := negotiator.Negotiate(r.Header.Get("Accept"))
|
case *RequestSubmitted:
|
||||||
if err != nil {
|
if _, ok := s.requests[e.RequestID()]; !ok {
|
||||||
span.RecordError(err)
|
s.requests[e.RequestID()] = &Request{}
|
||||||
w.WriteHeader(http.StatusNotAcceptable)
|
}
|
||||||
return
|
s.requests[e.RequestID()].ApplyEvent(e)
|
||||||
|
case *ResultSubmitted:
|
||||||
|
if _, ok := s.requests[e.RequestID]; !ok {
|
||||||
|
s.requests[e.RequestID] = &Request{}
|
||||||
|
}
|
||||||
|
s.requests[e.RequestID].ApplyEvent(e)
|
||||||
|
case *RequestTruncated:
|
||||||
|
delete(s.requests, e.RequestID)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
span.AddEvent(negotiated.String())
|
return nil
|
||||||
switch negotiated.String() {
|
|
||||||
case "text/environment":
|
|
||||||
_, err = encodeTo(w, info.MarshalEnviron, req.MarshalEnviron)
|
|
||||||
case "application/json":
|
|
||||||
var out interface{} = info
|
|
||||||
if req != nil {
|
|
||||||
out = struct {
|
|
||||||
ScriptVersion string `json:"script_version"`
|
|
||||||
RequestID string `json:"req_id"`
|
|
||||||
RequestIP string `json:"req_ip"`
|
|
||||||
Family string `json:"req_family"`
|
|
||||||
Created string `json:"req_created"`
|
|
||||||
}{
|
|
||||||
info.ScriptVersion,
|
|
||||||
req.RequestID(),
|
|
||||||
req.RequestIP,
|
|
||||||
strconv.Itoa(req.Family()),
|
|
||||||
req.CreatedString(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
err = json.NewEncoder(w).Encode(out)
|
|
||||||
}
|
|
||||||
span.RecordError(err)
|
|
||||||
}
|
|
||||||
func (s *service) getResults(w http.ResponseWriter, r *http.Request, uuid string) {
|
|
||||||
ctx := r.Context()
|
|
||||||
|
|
||||||
responses, err := s.es.Read(ctx, queueResponses+uuid, -1, es.AllEvents)
|
|
||||||
if err != nil {
|
|
||||||
w.WriteHeader(http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
negotiator := contentnegotiation.NewNegotiator("application/json", "text/environment", "text/csv", "text/plain", "text/html")
|
func Projector(e event.Event) []event.Event {
|
||||||
negotiated, _, err := negotiator.Negotiate("application/json")
|
m := e.EventMeta()
|
||||||
if err != nil {
|
streamID := m.StreamID
|
||||||
w.WriteHeader(http.StatusNotAcceptable)
|
streamPos := m.Position
|
||||||
return
|
|
||||||
}
|
|
||||||
switch negotiated.String() {
|
|
||||||
// case "text/environment":
|
|
||||||
// encodeTo(w, responses.MarshalBinary)
|
|
||||||
case "application/json":
|
|
||||||
json.NewEncoder(w).Encode(responses)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
func (s *service) postRequest(w http.ResponseWriter, r *http.Request) {
|
|
||||||
ctx := r.Context()
|
|
||||||
|
|
||||||
if err := r.ParseForm(); err != nil {
|
switch e := e.(type) {
|
||||||
w.WriteHeader(http.StatusUnprocessableEntity)
|
case *RequestSubmitted:
|
||||||
return
|
e1 := event.NewPtr(streamID, streamPos)
|
||||||
}
|
event.SetStreamID(aggRequest(e.RequestID()), e1)
|
||||||
|
|
||||||
req := &Request{
|
return []event.Event{e1}
|
||||||
RequestIP: r.Form.Get("req_ip"),
|
case *ResultSubmitted:
|
||||||
}
|
e1 := event.NewPtr(streamID, streamPos)
|
||||||
|
event.SetStreamID(aggRequest(e.RequestID), e1)
|
||||||
|
|
||||||
if hidden, err := strconv.ParseBool(r.Form.Get("req_hidden")); err != nil {
|
e2 := event.NewPtr(streamID, streamPos)
|
||||||
req.Hidden = hidden
|
event.SetStreamID(aggPeer(e.PeerID), e2)
|
||||||
}
|
|
||||||
|
|
||||||
s.es.Append(ctx, queueRequests, event.NewEvents(req))
|
return []event.Event{e1, e2}
|
||||||
}
|
|
||||||
func (s *service) postResult(w http.ResponseWriter, r *http.Request, id string) {
|
|
||||||
ctx := r.Context()
|
|
||||||
|
|
||||||
if _, err := ulid.ParseStrict(id); err != nil {
|
|
||||||
w.WriteHeader(http.StatusNotFound)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := r.ParseForm(); err != nil {
|
|
||||||
w.WriteHeader(http.StatusUnprocessableEntity)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
latency, err := strconv.ParseFloat(r.Form.Get("res_latency"), 64)
|
|
||||||
if err != nil {
|
|
||||||
w.WriteHeader(http.StatusUnprocessableEntity)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
req := &Result{
|
|
||||||
RequestID: id,
|
|
||||||
PeerID: r.Form.Get("peer_id"),
|
|
||||||
PeerVersion: r.Form.Get("peer_version"),
|
|
||||||
Latency: latency,
|
|
||||||
}
|
|
||||||
|
|
||||||
s.es.Append(ctx, queueResponses+id, event.NewEvents(req))
|
|
||||||
}
|
|
||||||
|
|
||||||
func filter(requests, responses event.Events) *Request {
|
|
||||||
have := make(map[string]struct{}, len(responses))
|
|
||||||
for _, res := range toList[Result](responses...) {
|
|
||||||
have[res.RequestID] = struct{}{}
|
|
||||||
}
|
|
||||||
for _, req := range reverse(toList[Request](requests...)...) {
|
|
||||||
if _, ok := have[req.RequestID()]; !ok {
|
|
||||||
return req
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
func toList[E any, T es.PE[E]](lis ...event.Event) []T {
|
|
||||||
newLis := make([]T, 0, len(lis))
|
|
||||||
for i := range lis {
|
|
||||||
if e, ok := lis[i].(T); ok {
|
|
||||||
newLis = append(newLis, e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return newLis
|
|
||||||
}
|
|
||||||
func reverse[T any](s ...T) []T {
|
|
||||||
first, last := 0, len(s)-1
|
|
||||||
for first < last {
|
|
||||||
s[first], s[last] = s[last], s[first]
|
|
||||||
first++
|
|
||||||
last--
|
|
||||||
}
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
func encodeTo(w io.Writer, fns ...func() ([]byte, error)) (int, error) {
|
|
||||||
i := 0
|
|
||||||
for _, fn := range fns {
|
|
||||||
b, err := fn()
|
|
||||||
if err != nil {
|
|
||||||
return i, err
|
|
||||||
}
|
|
||||||
|
|
||||||
j, err := w.Write(b)
|
|
||||||
i += j
|
|
||||||
if err != nil {
|
|
||||||
return i, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return i, nil
|
|
||||||
}
|
|
||||||
|
|||||||
241
app/salty/blobs.go
Normal file
241
app/salty/blobs.go
Normal file
@@ -0,0 +1,241 @@
|
|||||||
|
package salty
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"go.opentelemetry.io/otel/metric"
|
||||||
|
"go.sour.is/pkg/authreq"
|
||||||
|
"go.sour.is/pkg/lg"
|
||||||
|
"go.uber.org/multierr"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrAddressExists = errors.New("error: address already exists")
|
||||||
|
ErrBlobNotFound = errors.New("error: blob not found")
|
||||||
|
)
|
||||||
|
|
||||||
|
func WithBlobStore(path string) *withBlobStore {
|
||||||
|
return &withBlobStore{path: path}
|
||||||
|
}
|
||||||
|
|
||||||
|
type withBlobStore struct {
|
||||||
|
path string
|
||||||
|
|
||||||
|
m_get_blob metric.Int64Counter
|
||||||
|
m_put_blob metric.Int64Counter
|
||||||
|
m_delete_blob metric.Int64Counter
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *withBlobStore) ApplySalty(s *service) {}
|
||||||
|
|
||||||
|
func (o *withBlobStore) Setup(ctx context.Context) error {
|
||||||
|
ctx, span := lg.Span(ctx)
|
||||||
|
defer span.End()
|
||||||
|
|
||||||
|
var err, errs error
|
||||||
|
|
||||||
|
err = os.MkdirAll(o.path, 0700)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
m := lg.Meter(ctx)
|
||||||
|
o.m_get_blob, err = m.Int64Counter("salty_get_blob",
|
||||||
|
metric.WithDescription("salty get blob called"),
|
||||||
|
)
|
||||||
|
errs = multierr.Append(errs, err)
|
||||||
|
|
||||||
|
o.m_put_blob, err = m.Int64Counter("salty_put_blob",
|
||||||
|
metric.WithDescription("salty put blob called"),
|
||||||
|
)
|
||||||
|
errs = multierr.Append(errs, err)
|
||||||
|
|
||||||
|
o.m_delete_blob, err = m.Int64Counter("salty_delete_blob",
|
||||||
|
metric.WithDescription("salty delete blob called"),
|
||||||
|
)
|
||||||
|
errs = multierr.Append(errs, err)
|
||||||
|
|
||||||
|
return errs
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *withBlobStore) RegisterAPIv1(mux *http.ServeMux) {
|
||||||
|
mux.Handle("/blob/", authreq.Authorization(o))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *withBlobStore) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx, span := lg.Span(r.Context())
|
||||||
|
defer span.End()
|
||||||
|
|
||||||
|
claims := authreq.FromContext(ctx)
|
||||||
|
if claims == nil {
|
||||||
|
httpError(w, http.StatusUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
signer := claims.Issuer
|
||||||
|
|
||||||
|
key := strings.TrimPrefix(r.URL.Path, "/blob/")
|
||||||
|
|
||||||
|
switch r.Method {
|
||||||
|
case http.MethodDelete:
|
||||||
|
if err := deleteBlob(o.path, key, signer); err != nil {
|
||||||
|
if errors.Is(err, ErrBlobNotFound) {
|
||||||
|
httpError(w, http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
span.RecordError(fmt.Errorf("%w: getting blob %s for %s", err, key, signer))
|
||||||
|
httpError(w, http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
http.Error(w, "Blob Deleted", http.StatusOK)
|
||||||
|
case http.MethodGet, http.MethodHead:
|
||||||
|
blob, err := getBlob(o.path, key, signer)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, ErrBlobNotFound) {
|
||||||
|
httpError(w, http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
span.RecordError(fmt.Errorf("%w: getting blob %s for %s", err, key, signer))
|
||||||
|
httpError(w, http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer blob.Close()
|
||||||
|
|
||||||
|
blob.SetHeaders(r)
|
||||||
|
|
||||||
|
if r.Method == http.MethodGet {
|
||||||
|
_, _ = io.Copy(w, blob)
|
||||||
|
}
|
||||||
|
case http.MethodPut:
|
||||||
|
data, err := io.ReadAll(r.Body)
|
||||||
|
if err != nil {
|
||||||
|
httpError(w, http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer r.Body.Close()
|
||||||
|
|
||||||
|
if err := putBlob(o.path, key, data, signer); err != nil {
|
||||||
|
span.RecordError(fmt.Errorf("%w: putting blob %s for %s", err, key, signer))
|
||||||
|
|
||||||
|
httpError(w, http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
http.Error(w, "Blob Created", http.StatusCreated)
|
||||||
|
default:
|
||||||
|
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func putBlob(path string, key string, data []byte, signer string) error {
|
||||||
|
p := filepath.Join(path, signer, key)
|
||||||
|
if err := os.MkdirAll(p, 0700); err != nil {
|
||||||
|
return fmt.Errorf("error creating blobs paths %s: %w", p, err)
|
||||||
|
}
|
||||||
|
fn := filepath.Join(p, "content")
|
||||||
|
|
||||||
|
if err := os.WriteFile(fn, data, os.FileMode(0600)); err != nil {
|
||||||
|
return fmt.Errorf("error writing blob %s: %w", fn, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getBlob(path string, key string, signer string) (*Blob, error) {
|
||||||
|
p := filepath.Join(path, signer, key)
|
||||||
|
|
||||||
|
if err := os.MkdirAll(p, 0755); err != nil {
|
||||||
|
return nil, fmt.Errorf("error creating blobs paths %s: %w", p, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn := filepath.Join(p, "content")
|
||||||
|
|
||||||
|
if !FileExists(fn) {
|
||||||
|
return nil, ErrBlobNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
return OpenBlob(fn)
|
||||||
|
}
|
||||||
|
|
||||||
|
func deleteBlob(path string, key string, signer string) error {
|
||||||
|
|
||||||
|
p := filepath.Join(path, signer, key)
|
||||||
|
|
||||||
|
if !FileExists(p) {
|
||||||
|
return ErrBlobNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
return os.RemoveAll(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
// FileExists returns true if the given file exists
|
||||||
|
func FileExists(name string) bool {
|
||||||
|
if _, err := os.Stat(name); err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func httpError(w http.ResponseWriter, code int) {
|
||||||
|
http.Error(w, http.StatusText(code), code)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Blob defines the type, filename and whether or not a blob is publicly accessible or not.
|
||||||
|
// A Blob also holds zero or more properties as a map of key/value pairs of string interpreted
|
||||||
|
// by the client.
|
||||||
|
type Blob struct {
|
||||||
|
r io.ReadSeekCloser `json:"-"`
|
||||||
|
|
||||||
|
Type string `json:"type"`
|
||||||
|
Public bool `json:"public"`
|
||||||
|
Filename string `json:"-"`
|
||||||
|
Properties map[string]string `json:"props"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close closes the blob and the underlying io.ReadSeekCloser
|
||||||
|
func (b *Blob) Close() error { return b.r.Close() }
|
||||||
|
|
||||||
|
// Read reads data from the blob from the underlying io.ReadSeekCloser
|
||||||
|
func (b *Blob) Read(p []byte) (n int, err error) { return b.r.Read(p) }
|
||||||
|
|
||||||
|
// SetHeaders sets HTTP headers on the net/http.Request object based on the blob's type, filename
|
||||||
|
// and various other properties (if any).
|
||||||
|
func (b *Blob) SetHeaders(r *http.Request) {
|
||||||
|
// TODO: Implement this...
|
||||||
|
}
|
||||||
|
|
||||||
|
// OpenBlob opens a blob at the given path and returns a Blob object
|
||||||
|
func OpenBlob(fn string) (*Blob, error) {
|
||||||
|
f, err := os.Open(fn)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("%w: opening blob %s", err, fn)
|
||||||
|
}
|
||||||
|
b := &Blob{r: f, Filename: fn}
|
||||||
|
|
||||||
|
props := filepath.Join(filepath.Dir(fn), "props.json")
|
||||||
|
|
||||||
|
if FileExists(filepath.Dir(props)) {
|
||||||
|
pf, err := os.Open(props)
|
||||||
|
if err != nil {
|
||||||
|
return b, fmt.Errorf("%w: opening blob props %s", err, props)
|
||||||
|
}
|
||||||
|
err = json.NewDecoder(pf).Decode(b)
|
||||||
|
if err != nil {
|
||||||
|
return b, fmt.Errorf("%w: opening blob props %s", err, props)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return b, nil
|
||||||
|
}
|
||||||
@@ -10,7 +10,8 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/keys-pub/keys"
|
"github.com/keys-pub/keys"
|
||||||
"github.com/sour-is/ev/internal/lg"
|
|
||||||
|
"go.sour.is/pkg/lg"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Config represents a Salty Config for a User which at a minimum is required
|
// Config represents a Salty Config for a User which at a minimum is required
|
||||||
@@ -25,6 +26,9 @@ type Capabilities struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c Capabilities) String() string {
|
func (c Capabilities) String() string {
|
||||||
|
if c.AcceptEncoding == "" {
|
||||||
|
return "<nil>"
|
||||||
|
}
|
||||||
return fmt.Sprint("accept-encoding: ", c.AcceptEncoding)
|
return fmt.Sprint("accept-encoding: ", c.AcceptEncoding)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,30 +87,40 @@ func (a *Addr) Refresh(ctx context.Context) error {
|
|||||||
defer span.End()
|
defer span.End()
|
||||||
|
|
||||||
span.AddEvent(fmt.Sprintf("Looking up SRV record for _salty._tcp.%s", a.Domain))
|
span.AddEvent(fmt.Sprintf("Looking up SRV record for _salty._tcp.%s", a.Domain))
|
||||||
if target, _, err := a.dns.LookupSRV(ctx, "salty", "tcp", a.Domain); err == nil {
|
if _, srv, err := a.dns.LookupSRV(ctx, "salty", "tcp", a.Domain); err == nil {
|
||||||
a.discoveredDomain = target
|
if len(srv) > 0 {
|
||||||
|
a.discoveredDomain = strings.TrimSuffix(srv[0].Target, ".")
|
||||||
|
}
|
||||||
span.AddEvent(fmt.Sprintf("Discovered salty services %s", a.discoveredDomain))
|
span.AddEvent(fmt.Sprintf("Discovered salty services %s", a.discoveredDomain))
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
span.AddEvent(fmt.Sprintf("error looking up SRV record for _salty._tcp.%s : %s", a.Domain, err))
|
span.RecordError(fmt.Errorf("error looking up SRV record for _salty._tcp.%s : %s", a.Domain, err))
|
||||||
}
|
}
|
||||||
|
|
||||||
config, cap, err := fetchConfig(ctx, a.HashURI())
|
config, cap, err := fetchConfig(ctx, a.HashURI())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Fallback to plain user nick
|
// Fallback to plain user nick
|
||||||
|
span.RecordError(err)
|
||||||
|
|
||||||
config, cap, err = fetchConfig(ctx, a.URI())
|
config, cap, err = fetchConfig(ctx, a.URI())
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("error looking up user %s: %w", a, err)
|
err = fmt.Errorf("error looking up user %s: %w", a, err)
|
||||||
|
span.RecordError(err)
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
key, err := keys.NewEdX25519PublicKeyFromID(keys.ID(config.Key))
|
key, err := keys.NewEdX25519PublicKeyFromID(keys.ID(config.Key))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("error parsing public key %s: %w", config.Key, err)
|
err = fmt.Errorf("error parsing public key %s: %w", config.Key, err)
|
||||||
|
span.RecordError(err)
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
a.key = key
|
a.key = key
|
||||||
|
|
||||||
u, err := url.Parse(config.Endpoint)
|
u, err := url.Parse(config.Endpoint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("error parsing endpoint %s: %w", config.Endpoint, err)
|
err = fmt.Errorf("error parsing endpoint %s: %w", config.Endpoint, err)
|
||||||
|
span.RecordError(err)
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
a.endpoint = u
|
a.endpoint = u
|
||||||
a.capabilities = cap
|
a.capabilities = cap
|
||||||
|
|||||||
@@ -9,19 +9,18 @@ import (
|
|||||||
"net/url"
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
|
||||||
"github.com/keys-pub/keys"
|
"github.com/keys-pub/keys"
|
||||||
"github.com/oklog/ulid/v2"
|
"github.com/oklog/ulid/v2"
|
||||||
"github.com/sour-is/ev/pkg/es/event"
|
|
||||||
"github.com/sour-is/ev/pkg/gql"
|
"go.sour.is/ev/pkg/event"
|
||||||
|
"go.sour.is/pkg/gql"
|
||||||
)
|
)
|
||||||
|
|
||||||
type SaltyUser struct {
|
type SaltyUser struct {
|
||||||
name string
|
|
||||||
pubkey *keys.EdX25519PublicKey
|
pubkey *keys.EdX25519PublicKey
|
||||||
inbox ulid.ULID
|
inbox ulid.ULID
|
||||||
|
|
||||||
event.AggregateRoot
|
event.IsAggregate
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ event.Aggregate = (*SaltyUser)(nil)
|
var _ event.Aggregate = (*SaltyUser)(nil)
|
||||||
@@ -31,26 +30,25 @@ func (a *SaltyUser) ApplyEvent(lis ...event.Event) {
|
|||||||
for _, e := range lis {
|
for _, e := range lis {
|
||||||
switch e := e.(type) {
|
switch e := e.(type) {
|
||||||
case *UserRegistered:
|
case *UserRegistered:
|
||||||
a.name = e.Name
|
// a.name = e.Name
|
||||||
a.pubkey = e.Pubkey
|
a.pubkey = e.Pubkey
|
||||||
a.inbox = e.EventMeta().EventID
|
a.inbox = e.EventMeta().EventID
|
||||||
a.SetStreamID(a.streamID())
|
// a.SetStreamID(a.streamID())
|
||||||
default:
|
default:
|
||||||
log.Printf("unknown event %T", e)
|
log.Printf("unknown event %T", e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *SaltyUser) streamID() string {
|
func (a *SaltyUser) OnUserRegister(pubkey *keys.EdX25519PublicKey) error {
|
||||||
return fmt.Sprintf("saltyuser-%x", sha256.Sum256([]byte(strings.ToLower(a.name))))
|
if err := event.NotExists(a); err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *SaltyUser) OnUserRegister(name string, pubkey *keys.EdX25519PublicKey) error {
|
event.Raise(a, &UserRegistered{Pubkey: pubkey})
|
||||||
event.Raise(a, &UserRegistered{Name: name, Pubkey: pubkey})
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *SaltyUser) Nick() string { return a.name }
|
|
||||||
func (a *SaltyUser) Inbox() string { return a.inbox.String() }
|
func (a *SaltyUser) Inbox() string { return a.inbox.String() }
|
||||||
func (a *SaltyUser) Pubkey() string { return a.pubkey.String() }
|
func (a *SaltyUser) Pubkey() string { return a.pubkey.String() }
|
||||||
func (s *SaltyUser) Endpoint(ctx context.Context) (string, error) {
|
func (s *SaltyUser) Endpoint(ctx context.Context) (string, error) {
|
||||||
@@ -62,22 +60,11 @@ type UserRegistered struct {
|
|||||||
Name string
|
Name string
|
||||||
Pubkey *keys.EdX25519PublicKey
|
Pubkey *keys.EdX25519PublicKey
|
||||||
|
|
||||||
eventMeta event.Meta
|
event.IsEvent
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ event.Event = (*UserRegistered)(nil)
|
var _ event.Event = (*UserRegistered)(nil)
|
||||||
|
|
||||||
func (e *UserRegistered) EventMeta() event.Meta {
|
|
||||||
if e == nil {
|
|
||||||
return event.Meta{}
|
|
||||||
}
|
|
||||||
return e.eventMeta
|
|
||||||
}
|
|
||||||
func (e *UserRegistered) SetEventMeta(m event.Meta) {
|
|
||||||
if e != nil {
|
|
||||||
e.eventMeta = m
|
|
||||||
}
|
|
||||||
}
|
|
||||||
func (e *UserRegistered) MarshalBinary() (text []byte, err error) {
|
func (e *UserRegistered) MarshalBinary() (text []byte, err error) {
|
||||||
var b bytes.Buffer
|
var b bytes.Buffer
|
||||||
b.WriteString(e.Name)
|
b.WriteString(e.Name)
|
||||||
@@ -98,3 +85,10 @@ func (e *UserRegistered) UnmarshalBinary(b []byte) error {
|
|||||||
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func NickToStreamID(nick string) string {
|
||||||
|
return fmt.Sprintf("saltyuser-%x", sha256.Sum256([]byte(strings.ToLower(nick))))
|
||||||
|
}
|
||||||
|
func HashToStreamID(hash string) string {
|
||||||
|
return fmt.Sprint("saltyuser-", hash)
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,8 +6,7 @@ extend type Mutation {
|
|||||||
createSaltyUser(nick: String! pubkey: String!): SaltyUser
|
createSaltyUser(nick: String! pubkey: String!): SaltyUser
|
||||||
}
|
}
|
||||||
|
|
||||||
type SaltyUser @goModel(model: "github.com/sour-is/ev/app/salty.SaltyUser"){
|
type SaltyUser @goModel(model: "go.sour.is/ev/app/salty.SaltyUser"){
|
||||||
nick: String!
|
|
||||||
pubkey: String!
|
pubkey: String!
|
||||||
inbox: String!
|
inbox: String!
|
||||||
endpoint: String!
|
endpoint: String!
|
||||||
|
|||||||
@@ -10,14 +10,17 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/keys-pub/keys"
|
"github.com/keys-pub/keys"
|
||||||
"github.com/sour-is/ev/internal/lg"
|
"go.mills.io/saltyim"
|
||||||
"github.com/sour-is/ev/pkg/es"
|
"go.opentelemetry.io/otel/metric"
|
||||||
"github.com/sour-is/ev/pkg/es/event"
|
|
||||||
"github.com/sour-is/ev/pkg/gql"
|
|
||||||
"go.opentelemetry.io/otel/metric/instrument/syncint64"
|
|
||||||
"go.uber.org/multierr"
|
"go.uber.org/multierr"
|
||||||
|
|
||||||
|
"go.sour.is/ev"
|
||||||
|
"go.sour.is/ev/pkg/event"
|
||||||
|
"go.sour.is/pkg/gql"
|
||||||
|
"go.sour.is/pkg/lg"
|
||||||
)
|
)
|
||||||
|
|
||||||
type DNSResolver interface {
|
type DNSResolver interface {
|
||||||
@@ -26,16 +29,30 @@ type DNSResolver interface {
|
|||||||
|
|
||||||
type service struct {
|
type service struct {
|
||||||
baseURL string
|
baseURL string
|
||||||
es *es.EventStore
|
es *ev.EventStore
|
||||||
dns DNSResolver
|
dns DNSResolver
|
||||||
|
|
||||||
m_create_user syncint64.Counter
|
m_create_user metric.Int64Counter
|
||||||
m_get_user syncint64.Counter
|
m_get_user metric.Int64Counter
|
||||||
m_api_ping syncint64.Counter
|
m_api_ping metric.Int64Counter
|
||||||
m_api_register syncint64.Counter
|
m_api_register metric.Int64Counter
|
||||||
m_api_lookup syncint64.Counter
|
m_api_lookup metric.Int64Counter
|
||||||
m_api_send syncint64.Counter
|
m_api_send metric.Int64Counter
|
||||||
|
m_req_time metric.Int64Histogram
|
||||||
|
|
||||||
|
opts []Option
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Option interface {
|
||||||
|
ApplySalty(*service)
|
||||||
|
}
|
||||||
|
|
||||||
|
type WithBaseURL string
|
||||||
|
|
||||||
|
func (o WithBaseURL) ApplySalty(s *service) {
|
||||||
|
s.baseURL = string(o)
|
||||||
|
}
|
||||||
|
|
||||||
type contextKey struct {
|
type contextKey struct {
|
||||||
name string
|
name string
|
||||||
}
|
}
|
||||||
@@ -45,10 +62,10 @@ var saltyKey = contextKey{"salty"}
|
|||||||
type SaltyResolver interface {
|
type SaltyResolver interface {
|
||||||
CreateSaltyUser(ctx context.Context, nick string, pub string) (*SaltyUser, error)
|
CreateSaltyUser(ctx context.Context, nick string, pub string) (*SaltyUser, error)
|
||||||
SaltyUser(ctx context.Context, nick string) (*SaltyUser, error)
|
SaltyUser(ctx context.Context, nick string) (*SaltyUser, error)
|
||||||
RegisterHTTP(mux *http.ServeMux)
|
IsResolver()
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(ctx context.Context, es *es.EventStore, baseURL string) (*service, error) {
|
func New(ctx context.Context, es *ev.EventStore, opts ...Option) (*service, error) {
|
||||||
ctx, span := lg.Span(ctx)
|
ctx, span := lg.Span(ctx)
|
||||||
defer span.End()
|
defer span.End()
|
||||||
|
|
||||||
@@ -61,26 +78,55 @@ func New(ctx context.Context, es *es.EventStore, baseURL string) (*service, erro
|
|||||||
|
|
||||||
m := lg.Meter(ctx)
|
m := lg.Meter(ctx)
|
||||||
|
|
||||||
svc := &service{baseURL: baseURL, es: es, dns: net.DefaultResolver}
|
svc := &service{opts: opts, es: es, dns: net.DefaultResolver}
|
||||||
|
|
||||||
|
for _, o := range opts {
|
||||||
|
o.ApplySalty(svc)
|
||||||
|
|
||||||
|
if o, ok := o.(interface{ Setup(context.Context) error }); ok {
|
||||||
|
if err := o.Setup(ctx); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var err, errs error
|
var err, errs error
|
||||||
svc.m_create_user, err = m.SyncInt64().Counter("salty_create_user")
|
svc.m_create_user, err = m.Int64Counter("salty_create_user",
|
||||||
|
metric.WithDescription("salty create user graphql called"),
|
||||||
|
)
|
||||||
errs = multierr.Append(errs, err)
|
errs = multierr.Append(errs, err)
|
||||||
|
|
||||||
svc.m_get_user, err = m.SyncInt64().Counter("salty_get_user")
|
svc.m_get_user, err = m.Int64Counter("salty_get_user",
|
||||||
|
metric.WithDescription("salty get user graphql called"),
|
||||||
|
)
|
||||||
errs = multierr.Append(errs, err)
|
errs = multierr.Append(errs, err)
|
||||||
|
|
||||||
svc.m_api_ping, err = m.SyncInt64().Counter("salty_api_ping")
|
svc.m_api_ping, err = m.Int64Counter("salty_api_ping",
|
||||||
|
metric.WithDescription("salty api ping called"),
|
||||||
|
)
|
||||||
errs = multierr.Append(errs, err)
|
errs = multierr.Append(errs, err)
|
||||||
|
|
||||||
svc.m_api_register, err = m.SyncInt64().Counter("salty_api_register")
|
svc.m_api_register, err = m.Int64Counter("salty_api_register",
|
||||||
|
metric.WithDescription("salty api register"),
|
||||||
|
)
|
||||||
errs = multierr.Append(errs, err)
|
errs = multierr.Append(errs, err)
|
||||||
|
|
||||||
svc.m_api_lookup, err = m.SyncInt64().Counter("salty_api_lookup")
|
svc.m_api_lookup, err = m.Int64Counter("salty_api_lookup",
|
||||||
|
metric.WithDescription("salty api ping lookup"),
|
||||||
|
)
|
||||||
errs = multierr.Append(errs, err)
|
errs = multierr.Append(errs, err)
|
||||||
|
|
||||||
svc.m_api_send, err = m.SyncInt64().Counter("salty_api_send")
|
svc.m_api_send, err = m.Int64Counter("salty_api_send",
|
||||||
|
metric.WithDescription("salty api ping send"),
|
||||||
|
)
|
||||||
errs = multierr.Append(errs, err)
|
errs = multierr.Append(errs, err)
|
||||||
|
|
||||||
|
svc.m_req_time, err = m.Int64Histogram("salty_request_time",
|
||||||
|
metric.WithDescription("histogram of requests"),
|
||||||
|
metric.WithUnit("ns"),
|
||||||
|
)
|
||||||
|
errs = multierr.Append(errs, err)
|
||||||
|
|
||||||
span.RecordError(err)
|
span.RecordError(err)
|
||||||
|
|
||||||
return svc, errs
|
return svc, errs
|
||||||
@@ -92,25 +138,48 @@ func (s *service) BaseURL() string {
|
|||||||
}
|
}
|
||||||
return s.baseURL
|
return s.baseURL
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *service) RegisterHTTP(mux *http.ServeMux) {
|
func (s *service) RegisterHTTP(mux *http.ServeMux) {
|
||||||
mux.Handle("/.well-known/salty/", lg.Htrace(s, "lookup"))
|
for _, o := range s.opts {
|
||||||
|
if o, ok := o.(interface{ RegisterHTTP(mux *http.ServeMux) }); ok {
|
||||||
|
o.RegisterHTTP(mux)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
func (s *service) RegisterAPIv1(mux *http.ServeMux) {
|
func (s *service) RegisterAPIv1(mux *http.ServeMux) {
|
||||||
mux.HandleFunc("/ping", s.apiv1)
|
mux.HandleFunc("/ping", s.apiv1)
|
||||||
mux.HandleFunc("/register", s.apiv1)
|
mux.HandleFunc("/register", s.apiv1)
|
||||||
mux.HandleFunc("/lookup/", s.apiv1)
|
mux.HandleFunc("/lookup/", s.apiv1)
|
||||||
mux.HandleFunc("/send", s.apiv1)
|
mux.HandleFunc("/send", s.apiv1)
|
||||||
|
|
||||||
|
for _, o := range s.opts {
|
||||||
|
if o, ok := o.(interface{ RegisterAPIv1(mux *http.ServeMux) }); ok {
|
||||||
|
o.RegisterAPIv1(mux)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func (s *service) RegisterWellKnown(mux *http.ServeMux) {
|
||||||
|
mux.Handle("/salty/", lg.Htrace(s, "lookup"))
|
||||||
|
|
||||||
|
for _, o := range s.opts {
|
||||||
|
if o, ok := o.(interface{ RegisterWellKnown(mux *http.ServeMux) }); ok {
|
||||||
|
o.RegisterWellKnown(mux)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
func (s *service) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
func (s *service) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := r.Context()
|
ctx := r.Context()
|
||||||
ctx, span := lg.Span(ctx)
|
ctx, span := lg.Span(ctx)
|
||||||
defer span.End()
|
defer span.End()
|
||||||
|
|
||||||
addr := "saltyuser-" + strings.TrimPrefix(r.URL.Path, "/.well-known/salty/")
|
start := time.Now()
|
||||||
|
defer s.m_req_time.Record(ctx, time.Since(start).Milliseconds())
|
||||||
|
|
||||||
|
addr := "saltyuser-" + strings.TrimPrefix(r.URL.Path, "/salty/")
|
||||||
addr = strings.TrimSuffix(addr, ".json")
|
addr = strings.TrimSuffix(addr, ".json")
|
||||||
|
|
||||||
span.AddEvent(fmt.Sprint("find ", addr))
|
span.AddEvent(fmt.Sprint("find ", addr))
|
||||||
a, err := es.Update(ctx, s.es, addr, func(ctx context.Context, agg *SaltyUser) error { return nil })
|
a, err := ev.Update(ctx, s.es, addr, func(ctx context.Context, agg *SaltyUser) error { return nil })
|
||||||
switch {
|
switch {
|
||||||
case errors.Is(err, event.ErrShouldExist):
|
case errors.Is(err, event.ErrShouldExist):
|
||||||
span.RecordError(err)
|
span.RecordError(err)
|
||||||
@@ -138,32 +207,50 @@ func (s *service) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
span.RecordError(err)
|
span.RecordError(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *service) IsResolver() {}
|
||||||
|
func (s *service) GetMiddleware() func(http.Handler) http.Handler {
|
||||||
|
return func(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
r = r.WithContext(gql.ToContext(r.Context(), saltyKey, s))
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
func (s *service) CreateSaltyUser(ctx context.Context, nick string, pub string) (*SaltyUser, error) {
|
func (s *service) CreateSaltyUser(ctx context.Context, nick string, pub string) (*SaltyUser, error) {
|
||||||
ctx, span := lg.Span(ctx)
|
ctx, span := lg.Span(ctx)
|
||||||
defer span.End()
|
defer span.End()
|
||||||
|
|
||||||
s.m_create_user.Add(ctx, 1)
|
s.m_create_user.Add(ctx, 1)
|
||||||
|
start := time.Now()
|
||||||
|
defer s.m_req_time.Record(ctx, time.Since(start).Milliseconds())
|
||||||
|
|
||||||
streamID := fmt.Sprintf("saltyuser-%x", sha256.Sum256([]byte(strings.ToLower(nick))))
|
streamID := NickToStreamID(nick)
|
||||||
span.AddEvent(streamID)
|
span.AddEvent(streamID)
|
||||||
|
|
||||||
|
return s.createSaltyUser(ctx, streamID, pub)
|
||||||
|
}
|
||||||
|
func (s *service) createSaltyUser(ctx context.Context, streamID, pub string) (*SaltyUser, error) {
|
||||||
|
ctx, span := lg.Span(ctx)
|
||||||
|
defer span.End()
|
||||||
|
|
||||||
key, err := keys.NewEdX25519PublicKeyFromID(keys.ID(pub))
|
key, err := keys.NewEdX25519PublicKeyFromID(keys.ID(pub))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
span.RecordError(err)
|
span.RecordError(err)
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
a, err := es.Create(ctx, s.es, streamID, func(ctx context.Context, agg *SaltyUser) error {
|
a, err := ev.Create(ctx, s.es, streamID, func(ctx context.Context, agg *SaltyUser) error {
|
||||||
return agg.OnUserRegister(nick, key)
|
return agg.OnUserRegister(key)
|
||||||
})
|
})
|
||||||
switch {
|
switch {
|
||||||
case errors.Is(err, es.ErrShouldNotExist):
|
case errors.Is(err, ev.ErrShouldNotExist):
|
||||||
span.RecordError(err)
|
span.RecordError(err)
|
||||||
return nil, fmt.Errorf("user exists")
|
return nil, fmt.Errorf("user exists: %w", err)
|
||||||
|
|
||||||
case err != nil:
|
case err != nil:
|
||||||
span.RecordError(err)
|
span.RecordError(err)
|
||||||
return nil, fmt.Errorf("internal error")
|
return nil, fmt.Errorf("internal error: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return a, nil
|
return a, nil
|
||||||
@@ -173,13 +260,15 @@ func (s *service) SaltyUser(ctx context.Context, nick string) (*SaltyUser, error
|
|||||||
defer span.End()
|
defer span.End()
|
||||||
|
|
||||||
s.m_get_user.Add(ctx, 1)
|
s.m_get_user.Add(ctx, 1)
|
||||||
|
start := time.Now()
|
||||||
|
defer s.m_req_time.Record(ctx, time.Since(start).Milliseconds())
|
||||||
|
|
||||||
streamID := fmt.Sprintf("saltyuser-%x", sha256.Sum256([]byte(strings.ToLower(nick))))
|
streamID := fmt.Sprintf("saltyuser-%x", sha256.Sum256([]byte(strings.ToLower(nick))))
|
||||||
span.AddEvent(streamID)
|
span.AddEvent(streamID)
|
||||||
|
|
||||||
a, err := es.Update(ctx, s.es, streamID, func(ctx context.Context, agg *SaltyUser) error { return nil })
|
a, err := ev.Update(ctx, s.es, streamID, func(ctx context.Context, agg *SaltyUser) error { return nil })
|
||||||
switch {
|
switch {
|
||||||
case errors.Is(err, es.ErrShouldExist):
|
case errors.Is(err, ev.ErrShouldExist):
|
||||||
span.RecordError(err)
|
span.RecordError(err)
|
||||||
return nil, fmt.Errorf("user not found")
|
return nil, fmt.Errorf("user not found")
|
||||||
|
|
||||||
@@ -190,14 +279,6 @@ func (s *service) SaltyUser(ctx context.Context, nick string) (*SaltyUser, error
|
|||||||
|
|
||||||
return a, err
|
return a, err
|
||||||
}
|
}
|
||||||
func (s *service) GetMiddleware() func(http.Handler) http.Handler {
|
|
||||||
return func(next http.Handler) http.Handler {
|
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
r = r.WithContext(gql.ToContext(r.Context(), saltyKey, s))
|
|
||||||
next.ServeHTTP(w, r)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *service) apiv1(w http.ResponseWriter, r *http.Request) {
|
func (s *service) apiv1(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := r.Context()
|
ctx := r.Context()
|
||||||
@@ -205,14 +286,21 @@ func (s *service) apiv1(w http.ResponseWriter, r *http.Request) {
|
|||||||
ctx, span := lg.Span(ctx)
|
ctx, span := lg.Span(ctx)
|
||||||
defer span.End()
|
defer span.End()
|
||||||
|
|
||||||
|
start := time.Now()
|
||||||
|
defer s.m_req_time.Record(ctx, time.Since(start).Nanoseconds())
|
||||||
|
|
||||||
switch r.Method {
|
switch r.Method {
|
||||||
case http.MethodGet:
|
case http.MethodGet:
|
||||||
switch {
|
switch {
|
||||||
case r.URL.Path == "/ping":
|
case r.URL.Path == "/ping":
|
||||||
|
s.m_api_ping.Add(ctx, 1)
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
_, _ = w.Write([]byte(`{}`))
|
_, _ = w.Write([]byte(`{}`))
|
||||||
|
|
||||||
case strings.HasPrefix(r.URL.Path, "/lookup/"):
|
case strings.HasPrefix(r.URL.Path, "/lookup/"):
|
||||||
|
s.m_api_lookup.Add(ctx, 1)
|
||||||
|
|
||||||
addr, err := s.ParseAddr(strings.TrimPrefix(r.URL.Path, "/lookup/"))
|
addr, err := s.ParseAddr(strings.TrimPrefix(r.URL.Path, "/lookup/"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
span.RecordError(err)
|
span.RecordError(err)
|
||||||
@@ -226,7 +314,8 @@ func (s *service) apiv1(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
json.NewEncoder(w).Encode(addr)
|
err = json.NewEncoder(w).Encode(addr)
|
||||||
|
span.RecordError(err)
|
||||||
return
|
return
|
||||||
|
|
||||||
default:
|
default:
|
||||||
@@ -237,8 +326,65 @@ func (s *service) apiv1(w http.ResponseWriter, r *http.Request) {
|
|||||||
case http.MethodPost:
|
case http.MethodPost:
|
||||||
switch r.URL.Path {
|
switch r.URL.Path {
|
||||||
case "/register":
|
case "/register":
|
||||||
|
s.m_api_register.Add(ctx, 1)
|
||||||
|
|
||||||
|
req, signer, err := saltyim.NewRegisterRequest(r.Body)
|
||||||
|
if err != nil {
|
||||||
|
span.RecordError(fmt.Errorf("error parsing register request: %w", err))
|
||||||
|
http.Error(w, "Bad Request", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if signer != req.Key {
|
||||||
|
http.Error(w, "Bad Request", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = s.createSaltyUser(ctx, HashToStreamID(req.Hash), req.Key)
|
||||||
|
if errors.Is(err, event.ErrShouldNotExist) {
|
||||||
|
http.Error(w, "Already Exists", http.StatusConflict)
|
||||||
|
return
|
||||||
|
} else if err != nil {
|
||||||
|
http.Error(w, "Error", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
http.Error(w, "Endpoint Created", http.StatusCreated)
|
||||||
|
return
|
||||||
|
|
||||||
case "/send":
|
case "/send":
|
||||||
|
s.m_api_send.Add(ctx, 1)
|
||||||
|
|
||||||
|
req, signer, err := saltyim.NewSendRequest(r.Body)
|
||||||
|
if err != nil {
|
||||||
|
span.RecordError(fmt.Errorf("error parsing send request: %w", err))
|
||||||
|
http.Error(w, "Bad Request", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// TODO: Do something with signer?
|
||||||
|
span.AddEvent(fmt.Sprintf("request signed by %s", signer))
|
||||||
|
|
||||||
|
u, err := url.Parse(req.Endpoint)
|
||||||
|
if err != nil {
|
||||||
|
span.RecordError(fmt.Errorf("error parsing endpoint %s: %w", req.Endpoint, err))
|
||||||
|
http.Error(w, "Bad Endpoint", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !u.IsAbs() {
|
||||||
|
span.RecordError(fmt.Errorf("endpoint %s is not an absolute uri: %w", req.Endpoint, err))
|
||||||
|
http.Error(w, "Bad Endpoint", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Queue up an internal retry and return immediately on failure?
|
||||||
|
if err := saltyim.Send(req.Endpoint, req.Message, req.Capabilities); err != nil {
|
||||||
|
span.RecordError(fmt.Errorf("error sending message to %s: %w", req.Endpoint, err))
|
||||||
|
http.Error(w, "Send Error", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
http.Error(w, "Message Accepted", http.StatusAccepted)
|
||||||
|
|
||||||
|
return
|
||||||
|
|
||||||
default:
|
default:
|
||||||
w.WriteHeader(http.StatusNotFound)
|
w.WriteHeader(http.StatusNotFound)
|
||||||
|
|||||||
1
app/twtxt/twtxt.go
Normal file
1
app/twtxt/twtxt.go
Normal file
@@ -0,0 +1 @@
|
|||||||
|
package twtxt
|
||||||
35
app/webfinger/addr.go
Normal file
35
app/webfinger/addr.go
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
package webfinger
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Addr struct {
|
||||||
|
prefix []string
|
||||||
|
URL *url.URL
|
||||||
|
}
|
||||||
|
|
||||||
|
func Parse(s string) *Addr {
|
||||||
|
addr := &Addr{}
|
||||||
|
|
||||||
|
addr.URL, _ = url.Parse(s)
|
||||||
|
|
||||||
|
if addr.URL.Opaque == "" {
|
||||||
|
return addr
|
||||||
|
}
|
||||||
|
|
||||||
|
var hasPfx = true
|
||||||
|
pfx := addr.URL.Scheme
|
||||||
|
|
||||||
|
for hasPfx {
|
||||||
|
addr.prefix = append(addr.prefix, pfx)
|
||||||
|
pfx, addr.URL.Opaque, hasPfx = strings.Cut(addr.URL.Opaque, ":")
|
||||||
|
}
|
||||||
|
|
||||||
|
user, host, _ := strings.Cut(pfx, "@")
|
||||||
|
addr.URL.User = url.User(user)
|
||||||
|
addr.URL.Host = host
|
||||||
|
|
||||||
|
return addr
|
||||||
|
}
|
||||||
46
app/webfinger/client.go
Normal file
46
app/webfinger/client.go
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
package webfinger
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/ed25519"
|
||||||
|
"encoding/base64"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/golang-jwt/jwt/v4"
|
||||||
|
"github.com/oklog/ulid/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
defaultExpire = 30 * time.Minute
|
||||||
|
defaultIssuer = "sour.is/webfinger"
|
||||||
|
defaultAudience = "sour.is/webfinger"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewSignedRequest(jrd *JRD, key ed25519.PrivateKey) (string, error) {
|
||||||
|
type claims struct {
|
||||||
|
PubKey string `json:"pub"`
|
||||||
|
*JRD
|
||||||
|
jwt.RegisteredClaims
|
||||||
|
}
|
||||||
|
pub := []byte(key.Public().(ed25519.PublicKey))
|
||||||
|
|
||||||
|
j := claims{
|
||||||
|
PubKey: enc(pub),
|
||||||
|
JRD: jrd.CloneValues(),
|
||||||
|
RegisteredClaims: jwt.RegisteredClaims{
|
||||||
|
ID: ulid.Make().String(),
|
||||||
|
Subject: jrd.Subject,
|
||||||
|
Audience: jwt.ClaimStrings{defaultAudience},
|
||||||
|
ExpiresAt: jwt.NewNumericDate(time.Now().Add(defaultExpire)),
|
||||||
|
IssuedAt: jwt.NewNumericDate(time.Now()),
|
||||||
|
Issuer: defaultIssuer,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
j.JRD.Subject = "" // move subject into registered claims.
|
||||||
|
|
||||||
|
token := jwt.NewWithClaims(jwt.SigningMethodEdDSA, &j)
|
||||||
|
return token.SignedString(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func enc(b []byte) string {
|
||||||
|
return base64.RawURLEncoding.EncodeToString(b)
|
||||||
|
}
|
||||||
44
app/webfinger/events.go
Normal file
44
app/webfinger/events.go
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
package webfinger
|
||||||
|
|
||||||
|
import (
|
||||||
|
"go.sour.is/ev/pkg/event"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SubjectSet struct {
|
||||||
|
Subject string `json:"subject"`
|
||||||
|
Aliases []string `json:"aliases,omitempty"`
|
||||||
|
Properties map[string]*string `json:"properties,omitempty"`
|
||||||
|
|
||||||
|
event.IsEvent `json:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SubjectDeleted struct {
|
||||||
|
Subject string `json:"subject"`
|
||||||
|
|
||||||
|
event.IsEvent `json:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ event.Event = (*SubjectDeleted)(nil)
|
||||||
|
|
||||||
|
type LinkSet struct {
|
||||||
|
Index uint64 `json:"idx"`
|
||||||
|
Rel string `json:"rel"`
|
||||||
|
Type string `json:"type,omitempty"`
|
||||||
|
HRef string `json:"href,omitempty"`
|
||||||
|
Titles map[string]string `json:"titles,omitempty"`
|
||||||
|
Properties map[string]*string `json:"properties,omitempty"`
|
||||||
|
Template string `json:"template,omitempty"`
|
||||||
|
|
||||||
|
event.IsEvent `json:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ event.Event = (*LinkSet)(nil)
|
||||||
|
|
||||||
|
type LinkDeleted struct {
|
||||||
|
Index uint64 `json:"idx"`
|
||||||
|
Rel string `json:"rel"`
|
||||||
|
|
||||||
|
event.IsEvent `json:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ event.Event = (*LinkDeleted)(nil)
|
||||||
426
app/webfinger/jrd.go
Normal file
426
app/webfinger/jrd.go
Normal file
@@ -0,0 +1,426 @@
|
|||||||
|
package webfinger
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"hash/fnv"
|
||||||
|
"sort"
|
||||||
|
|
||||||
|
"go.sour.is/pkg/set"
|
||||||
|
"go.sour.is/pkg/slice"
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
|
||||||
|
"go.sour.is/ev/pkg/event"
|
||||||
|
)
|
||||||
|
|
||||||
|
func StreamID(subject string) string {
|
||||||
|
h := fnv.New128a()
|
||||||
|
h.Write([]byte(subject))
|
||||||
|
return "webfinger." + base64.RawURLEncoding.EncodeToString(h.Sum(nil))
|
||||||
|
}
|
||||||
|
|
||||||
|
// JRD is a JSON Resource Descriptor, specifying properties and related links
|
||||||
|
// for a resource.
|
||||||
|
type JRD struct {
|
||||||
|
Subject string `json:"subject,omitempty" yaml:"subject,omitempty"`
|
||||||
|
Aliases []string `json:"aliases,omitempty" yaml:"aliases,omitempty"`
|
||||||
|
Properties map[string]*string `json:"properties,omitempty" yaml:"properties,omitempty"`
|
||||||
|
Links Links `json:"links,omitempty" yaml:"links,omitempty"`
|
||||||
|
deleted bool
|
||||||
|
event.IsAggregate `json:"-" yaml:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *JRD) CloneValues() *JRD {
|
||||||
|
m := make(map[string]*string, len(a.Properties))
|
||||||
|
for k, v := range a.Properties {
|
||||||
|
m[k] = v
|
||||||
|
}
|
||||||
|
return &JRD{
|
||||||
|
Subject: a.Subject,
|
||||||
|
Aliases: append([]string{}, a.Aliases...),
|
||||||
|
Properties: m,
|
||||||
|
Links: append([]*Link{}, a.Links...),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ event.Aggregate = (*JRD)(nil)
|
||||||
|
|
||||||
|
// Link is a link to a related resource.
|
||||||
|
type Link struct {
|
||||||
|
Index uint64 `json:"-" yaml:"-"`
|
||||||
|
Rel string `json:"rel,omitempty"`
|
||||||
|
Type string `json:"type,omitempty"`
|
||||||
|
HRef string `json:"href,omitempty"`
|
||||||
|
Titles map[string]string `json:"titles,omitempty"`
|
||||||
|
Properties map[string]*string `json:"properties,omitempty"`
|
||||||
|
Template string `json:"template,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Links []*Link
|
||||||
|
|
||||||
|
// Len is the number of elements in the collection.
|
||||||
|
func (l Links) Len() int {
|
||||||
|
if l == nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return len(l)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Less reports whether the element with index i
|
||||||
|
func (l Links) Less(i int, j int) bool {
|
||||||
|
if l[i] == nil || l[j] == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if l[i].Rel == l[j].Rel {
|
||||||
|
return l[i].Type < l[j].Type
|
||||||
|
}
|
||||||
|
return l[i].Rel < l[j].Rel
|
||||||
|
}
|
||||||
|
|
||||||
|
// Swap swaps the elements with indexes i and j.
|
||||||
|
func (l Links) Swap(i int, j int) {
|
||||||
|
if l == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
l[i], l[j] = l[j], l[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseJRD parses the JRD using json.Unmarshal.
|
||||||
|
func ParseJRD(blob []byte) (*JRD, error) {
|
||||||
|
jrd := JRD{}
|
||||||
|
err := json.Unmarshal(blob, &jrd)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
for i := range jrd.Links {
|
||||||
|
jrd.Links[i].Index = uint64(i)
|
||||||
|
}
|
||||||
|
return &jrd, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetLinkByRel returns the first *Link with the specified rel value.
|
||||||
|
func (jrd *JRD) GetLinkByRel(rel string) *Link {
|
||||||
|
for _, link := range jrd.Links {
|
||||||
|
if link.Rel == rel {
|
||||||
|
return link
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetLinksByRel returns each *Link with the specified rel value.
|
||||||
|
func (jrd *JRD) GetLinksByRel(rel ...string) []*Link {
|
||||||
|
var lis []*Link
|
||||||
|
rels := set.New(rel...)
|
||||||
|
|
||||||
|
for _, link := range jrd.Links {
|
||||||
|
if rels.Has(link.Rel) {
|
||||||
|
lis = append(lis, link)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return lis
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetProperty Returns the property value as a string.
|
||||||
|
// Per spec a property value can be null, empty string is returned in this case.
|
||||||
|
func (jrd *JRD) GetProperty(uri string) string {
|
||||||
|
if jrd.Properties[uri] == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return *jrd.Properties[uri]
|
||||||
|
}
|
||||||
|
func (a *JRD) SetProperty(name string, value *string) {
|
||||||
|
if a.Properties == nil {
|
||||||
|
a.Properties = make(map[string]*string)
|
||||||
|
}
|
||||||
|
a.Properties[name] = value
|
||||||
|
}
|
||||||
|
func (a *JRD) DeleteProperty(name string) {
|
||||||
|
if a.Properties == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
delete(a.Properties, name)
|
||||||
|
}
|
||||||
|
func (a *JRD) IsDeleted() bool {
|
||||||
|
return a.deleted
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetProperty Returns the property value as a string.
|
||||||
|
// Per spec a property value can be null, empty string is returned in this case.
|
||||||
|
func (link *Link) GetProperty(uri string) string {
|
||||||
|
if link.Properties[uri] == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return *link.Properties[uri]
|
||||||
|
}
|
||||||
|
func (link *Link) SetProperty(name string, value *string) {
|
||||||
|
if link.Properties == nil {
|
||||||
|
link.Properties = make(map[string]*string)
|
||||||
|
}
|
||||||
|
link.Properties[name] = value
|
||||||
|
}
|
||||||
|
func (link *Link) DeleteProperty(name string) {
|
||||||
|
if link.Properties == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
delete(link.Properties, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ApplyEvent implements event.Aggregate
|
||||||
|
func (a *JRD) ApplyEvent(events ...event.Event) {
|
||||||
|
for _, e := range events {
|
||||||
|
switch e := e.(type) {
|
||||||
|
case *SubjectSet:
|
||||||
|
a.deleted = false
|
||||||
|
|
||||||
|
a.Subject = e.Subject
|
||||||
|
a.Aliases = e.Aliases
|
||||||
|
a.Properties = e.Properties
|
||||||
|
|
||||||
|
case *SubjectDeleted:
|
||||||
|
a.deleted = true
|
||||||
|
|
||||||
|
a.Subject = e.Subject
|
||||||
|
a.Aliases = a.Aliases[:0]
|
||||||
|
a.Links = a.Links[:0]
|
||||||
|
a.Properties = map[string]*string{}
|
||||||
|
|
||||||
|
case *LinkSet:
|
||||||
|
link, ok := slice.FindFn(func(l *Link) bool { return l.Index == e.Index }, a.Links...)
|
||||||
|
if !ok {
|
||||||
|
link = &Link{}
|
||||||
|
link.Index = uint64(len(a.Links))
|
||||||
|
a.Links = append(a.Links, link)
|
||||||
|
}
|
||||||
|
|
||||||
|
link.Rel = e.Rel
|
||||||
|
link.HRef = e.HRef
|
||||||
|
link.Type = e.Type
|
||||||
|
link.Titles = e.Titles
|
||||||
|
link.Properties = e.Properties
|
||||||
|
link.Template = e.Template
|
||||||
|
|
||||||
|
case *LinkDeleted:
|
||||||
|
a.Links = slice.FilterFn(func(link *Link) bool { return link.Index != e.Index }, a.Links...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const NSauth = "https://sour.is/ns/auth"
|
||||||
|
const NSpubkey = "https://sour.is/ns/pubkey"
|
||||||
|
const NSredirect = "https://sour.is/rel/redirect"
|
||||||
|
|
||||||
|
func (a *JRD) OnAuth(claim, auth *JRD) error {
|
||||||
|
pubkey := claim.Properties[NSpubkey]
|
||||||
|
|
||||||
|
if v, ok := auth.Properties[NSpubkey]; ok && v != nil && cmpPtr(v, pubkey) {
|
||||||
|
// pubkey matches!
|
||||||
|
} else {
|
||||||
|
return fmt.Errorf("pubkey does not match")
|
||||||
|
}
|
||||||
|
|
||||||
|
if a.Version() > 0 && !a.IsDeleted() && a.Subject != claim.Subject {
|
||||||
|
return fmt.Errorf("subject does not match")
|
||||||
|
}
|
||||||
|
|
||||||
|
if auth.Subject == claim.Subject {
|
||||||
|
claim.SetProperty(NSpubkey, pubkey)
|
||||||
|
} else {
|
||||||
|
claim.SetProperty(NSauth, &auth.Subject)
|
||||||
|
claim.DeleteProperty(NSpubkey)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *JRD) OnDelete(jrd *JRD) error {
|
||||||
|
if a.Version() == 0 || a.IsDeleted() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
event.Raise(a, &SubjectDeleted{Subject: jrd.Subject})
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *JRD) OnClaims(jrd *JRD) error {
|
||||||
|
|
||||||
|
err := a.OnSubjectSet(jrd.Subject, jrd.Aliases, jrd.Properties)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, z := range slice.Align(
|
||||||
|
a.Links, // old
|
||||||
|
jrd.Links, // new
|
||||||
|
func(l, r *Link) bool { return l.Index < r.Index },
|
||||||
|
) {
|
||||||
|
// Not in new == delete
|
||||||
|
if z.Key == nil {
|
||||||
|
link := *z.Value
|
||||||
|
event.Raise(a, &LinkDeleted{Index: link.Index, Rel: link.Rel})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Not in old == create
|
||||||
|
if z.Value == nil {
|
||||||
|
link := *z.Key
|
||||||
|
event.Raise(a, &LinkSet{
|
||||||
|
Index: link.Index,
|
||||||
|
Rel: link.Rel,
|
||||||
|
Type: link.Type,
|
||||||
|
HRef: link.HRef,
|
||||||
|
Titles: link.Titles,
|
||||||
|
Properties: link.Properties,
|
||||||
|
Template: link.Template,
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// in both == compare
|
||||||
|
a.OnLinkSet(*z.Key, *z.Value)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *JRD) OnSubjectSet(subject string, aliases []string, props map[string]*string) error {
|
||||||
|
modified := false
|
||||||
|
e := &SubjectSet{
|
||||||
|
Subject: subject,
|
||||||
|
Aliases: aliases,
|
||||||
|
Properties: props,
|
||||||
|
}
|
||||||
|
|
||||||
|
if subject != a.Subject {
|
||||||
|
modified = true
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Strings(aliases)
|
||||||
|
sort.Strings(a.Aliases)
|
||||||
|
for _, z := range slice.Zip(aliases, a.Aliases) {
|
||||||
|
if z.Key != z.Value {
|
||||||
|
modified = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, z := range slice.Zip(
|
||||||
|
slice.Zip(slice.FromMap(props)),
|
||||||
|
slice.Zip(slice.FromMap(a.Properties)),
|
||||||
|
) {
|
||||||
|
newValue := z.Key
|
||||||
|
curValue := z.Value
|
||||||
|
|
||||||
|
if newValue.Key != curValue.Key {
|
||||||
|
modified = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if !cmpPtr(newValue.Value, curValue.Value) {
|
||||||
|
modified = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if modified {
|
||||||
|
event.Raise(a, e)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *JRD) OnLinkSet(o, n *Link) error {
|
||||||
|
modified := false
|
||||||
|
e := &LinkSet{
|
||||||
|
Index: n.Index,
|
||||||
|
Rel: n.Rel,
|
||||||
|
Type: n.Type,
|
||||||
|
HRef: n.HRef,
|
||||||
|
Titles: n.Titles,
|
||||||
|
Properties: n.Properties,
|
||||||
|
Template: n.Template,
|
||||||
|
}
|
||||||
|
|
||||||
|
if n.Rel != o.Rel {
|
||||||
|
modified = true
|
||||||
|
}
|
||||||
|
if n.Type != o.Type {
|
||||||
|
modified = true
|
||||||
|
}
|
||||||
|
if n.HRef != o.HRef {
|
||||||
|
modified = true
|
||||||
|
}
|
||||||
|
if n.Template != o.Template {
|
||||||
|
fmt.Println(360, n.Template, o.Template, e.Template)
|
||||||
|
|
||||||
|
modified = true
|
||||||
|
}
|
||||||
|
|
||||||
|
nKeys := slice.FromMapKeys(n.Properties)
|
||||||
|
sort.Strings(nKeys)
|
||||||
|
|
||||||
|
oKeys := slice.FromMapKeys(o.Properties)
|
||||||
|
sort.Strings(oKeys)
|
||||||
|
|
||||||
|
for _, z := range slice.Zip(
|
||||||
|
slice.Zip(nKeys, slice.FromMapValues(n.Titles, nKeys)),
|
||||||
|
slice.Zip(oKeys, slice.FromMapValues(o.Titles, oKeys)),
|
||||||
|
) {
|
||||||
|
if z.Key != z.Value {
|
||||||
|
modified = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
nKeys = slice.FromMapKeys(n.Properties)
|
||||||
|
sort.Strings(nKeys)
|
||||||
|
|
||||||
|
oKeys = slice.FromMapKeys(o.Properties)
|
||||||
|
sort.Strings(oKeys)
|
||||||
|
|
||||||
|
for _, z := range slice.Zip(
|
||||||
|
slice.Zip(nKeys, slice.FromMapValues(n.Properties, nKeys)),
|
||||||
|
slice.Zip(oKeys, slice.FromMapValues(o.Properties, oKeys)),
|
||||||
|
) {
|
||||||
|
newValue := z.Key
|
||||||
|
curValue := z.Value
|
||||||
|
|
||||||
|
if newValue.Key != curValue.Key {
|
||||||
|
modified = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if !cmpPtr(newValue.Value, curValue.Value) {
|
||||||
|
modified = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if modified {
|
||||||
|
event.Raise(a, e)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func cmpPtr[T comparable](l, r *T) bool {
|
||||||
|
if l == nil {
|
||||||
|
return r == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if r == nil {
|
||||||
|
return l == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return *l == *r
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *JRD) String() string {
|
||||||
|
b := &bytes.Buffer{}
|
||||||
|
y := yaml.NewEncoder(b)
|
||||||
|
_ = y.Encode(a)
|
||||||
|
|
||||||
|
return b.String()
|
||||||
|
}
|
||||||
310
app/webfinger/jrd_test.go
Normal file
310
app/webfinger/jrd_test.go
Normal file
@@ -0,0 +1,310 @@
|
|||||||
|
package webfinger_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/ed25519"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
jwt "github.com/golang-jwt/jwt/v4"
|
||||||
|
"github.com/matryer/is"
|
||||||
|
"go.sour.is/ev"
|
||||||
|
"go.uber.org/multierr"
|
||||||
|
|
||||||
|
"go.sour.is/ev/app/webfinger"
|
||||||
|
memstore "go.sour.is/ev/pkg/driver/mem-store"
|
||||||
|
"go.sour.is/ev/pkg/driver/projecter"
|
||||||
|
"go.sour.is/ev/pkg/driver/streamer"
|
||||||
|
"go.sour.is/ev/pkg/event"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestParseJRD(t *testing.T) {
|
||||||
|
|
||||||
|
// Adapted from spec http://tools.ietf.org/html/rfc6415#appendix-A
|
||||||
|
blob := `
|
||||||
|
{
|
||||||
|
"subject":"http://blog.example.com/article/id/314",
|
||||||
|
"aliases":[
|
||||||
|
"http://blog.example.com/cool_new_thing",
|
||||||
|
"http://blog.example.com/steve/article/7"],
|
||||||
|
"properties":{
|
||||||
|
"http://blgx.example.net/ns/version":"1.3",
|
||||||
|
"http://blgx.example.net/ns/ext":null
|
||||||
|
},
|
||||||
|
"links":[
|
||||||
|
{
|
||||||
|
"rel":"author",
|
||||||
|
"type":"text/html",
|
||||||
|
"href":"http://blog.example.com/author/steve",
|
||||||
|
"titles":{
|
||||||
|
"default":"About the Author",
|
||||||
|
"en-us":"Author Information"
|
||||||
|
},
|
||||||
|
"properties":{
|
||||||
|
"http://example.com/role":"editor"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rel":"author",
|
||||||
|
"href":"http://example.com/author/john",
|
||||||
|
"titles":{
|
||||||
|
"default":"The other author"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rel":"copyright"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
`
|
||||||
|
obj, err := webfinger.ParseJRD([]byte(blob))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if got, want := obj.Subject, "http://blog.example.com/article/id/314"; got != want {
|
||||||
|
t.Errorf("JRD.Subject is %q, want %q", got, want)
|
||||||
|
}
|
||||||
|
if got, want := obj.GetProperty("http://blgx.example.net/ns/version"), "1.3"; got != want {
|
||||||
|
t.Errorf("obj.GetProperty('http://blgx.example.net/ns/version') returned %q, want %q", got, want)
|
||||||
|
}
|
||||||
|
if got, want := obj.GetProperty("http://blgx.example.net/ns/ext"), ""; got != want {
|
||||||
|
t.Errorf("obj.GetProperty('http://blgx.example.net/ns/ext') returned %q, want %q", got, want)
|
||||||
|
}
|
||||||
|
if obj.GetLinkByRel("copyright") == nil {
|
||||||
|
t.Error("obj.GetLinkByRel('copyright') returned nil, want non-nil value")
|
||||||
|
}
|
||||||
|
if got, want := obj.GetLinkByRel("author").Titles["default"], "About the Author"; got != want {
|
||||||
|
t.Errorf("obj.GetLinkByRel('author').Titles['default'] returned %q, want %q", got, want)
|
||||||
|
}
|
||||||
|
if got, want := obj.GetLinkByRel("author").GetProperty("http://example.com/role"), "editor"; got != want {
|
||||||
|
t.Errorf("obj.GetLinkByRel('author').GetProperty('http://example.com/role') returned %q, want %q", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEncodeJRD(t *testing.T) {
|
||||||
|
s, err := json.Marshal(&webfinger.JRD{
|
||||||
|
Subject: "test",
|
||||||
|
Properties: map[string]*string{
|
||||||
|
"https://sour.is/ns/prop1": nil,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if string(s) != `{"subject":"test","properties":{"https://sour.is/ns/prop1":null}}` {
|
||||||
|
t.Fatal("output does not match")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestApplyEvents(t *testing.T) {
|
||||||
|
is := is.New(t)
|
||||||
|
|
||||||
|
events := event.NewEvents(
|
||||||
|
&webfinger.SubjectSet{
|
||||||
|
Subject: "acct:me@sour.is",
|
||||||
|
Properties: map[string]*string{
|
||||||
|
"https://sour.is/ns/pubkey": ptr("kex1d330ama4vnu3vll5dgwjv3k0pcxsccc5k2xy3j8khndggkszsmsq3hl4ru"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
&webfinger.LinkSet{
|
||||||
|
Index: 0,
|
||||||
|
Rel: "salty:public",
|
||||||
|
Type: "application/json+salty",
|
||||||
|
},
|
||||||
|
&webfinger.LinkSet{
|
||||||
|
Index: 1,
|
||||||
|
Rel: "salty:private",
|
||||||
|
Type: "application/json+salty",
|
||||||
|
},
|
||||||
|
&webfinger.LinkSet{
|
||||||
|
Index: 0,
|
||||||
|
Rel: "salty:public",
|
||||||
|
Type: "application/json+salty",
|
||||||
|
HRef: "https://ev.sour.is/inbox/01GAEMKXYJ4857JQP1MJGD61Z5",
|
||||||
|
Properties: map[string]*string{
|
||||||
|
"pub": ptr("kex1r8zshlvkc787pxvauaq7hd6awa9kmheddxjj9k80qmenyxk6284s50uvpw"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
&webfinger.LinkDeleted{
|
||||||
|
Index: 1,
|
||||||
|
Rel: "salty:private",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
event.SetStreamID(webfinger.StreamID("acct:me@sour.is"), events...)
|
||||||
|
|
||||||
|
jrd := &webfinger.JRD{}
|
||||||
|
jrd.ApplyEvent(events...)
|
||||||
|
|
||||||
|
s, err := json.Marshal(jrd)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
is.Equal(string(s), `{"subject":"acct:me@sour.is","properties":{"https://sour.is/ns/pubkey":"kex1d330ama4vnu3vll5dgwjv3k0pcxsccc5k2xy3j8khndggkszsmsq3hl4ru"},"links":[{"rel":"salty:public","type":"application/json+salty","href":"https://ev.sour.is/inbox/01GAEMKXYJ4857JQP1MJGD61Z5","properties":{"pub":"kex1r8zshlvkc787pxvauaq7hd6awa9kmheddxjj9k80qmenyxk6284s50uvpw"}}]}`)
|
||||||
|
|
||||||
|
events = event.NewEvents(
|
||||||
|
&webfinger.SubjectDeleted{},
|
||||||
|
)
|
||||||
|
event.SetStreamID(webfinger.StreamID("acct:me@sour.is"), events...)
|
||||||
|
|
||||||
|
jrd.ApplyEvent(events...)
|
||||||
|
|
||||||
|
s, err = json.Marshal(jrd)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Log(string(s))
|
||||||
|
if string(s) != `{}` {
|
||||||
|
t.Fatal("output does not match")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCommands(t *testing.T) {
|
||||||
|
is := is.New(t)
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
pub, priv, err := ed25519.GenerateKey(nil)
|
||||||
|
is.NoErr(err)
|
||||||
|
|
||||||
|
token := jwt.NewWithClaims(jwt.SigningMethodEdDSA, jwt.MapClaims{
|
||||||
|
"sub": "acct:me@sour.is",
|
||||||
|
"pub": enc(pub),
|
||||||
|
"aliases": []string{"acct:xuu@sour.is"},
|
||||||
|
"properties": map[string]*string{
|
||||||
|
"https://example.com/ns/asdf": nil,
|
||||||
|
webfinger.NSpubkey: ptr(enc(pub)),
|
||||||
|
},
|
||||||
|
"links": []map[string]any{{
|
||||||
|
"rel": "salty:public",
|
||||||
|
"type": "application/json+salty",
|
||||||
|
"href": "https://ev.sour.is",
|
||||||
|
"titles": map[string]string{"default": "Jon Lundy"},
|
||||||
|
"properties": map[string]*string{
|
||||||
|
"pub": ptr("kex140fwaena9t0mrgnjeare5zuknmmvl0vc7agqy5yr938vusxfh9ys34vd2p"),
|
||||||
|
},
|
||||||
|
}},
|
||||||
|
"exp": time.Now().Add(30 * time.Second).Unix(),
|
||||||
|
})
|
||||||
|
aToken, err := token.SignedString(priv)
|
||||||
|
is.NoErr(err)
|
||||||
|
|
||||||
|
es, err := ev.Open(ctx, "mem:", streamer.New(ctx), projecter.New(ctx))
|
||||||
|
is.NoErr(err)
|
||||||
|
|
||||||
|
type claims struct {
|
||||||
|
Subject string `json:"sub"`
|
||||||
|
PubKey string `json:"pub"`
|
||||||
|
*webfinger.JRD
|
||||||
|
jwt.StandardClaims
|
||||||
|
}
|
||||||
|
|
||||||
|
token, err = jwt.ParseWithClaims(
|
||||||
|
aToken,
|
||||||
|
&claims{},
|
||||||
|
func(tok *jwt.Token) (any, error) {
|
||||||
|
c, ok := tok.Claims.(*claims)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("wrong type of claim")
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JRD.Subject = c.Subject
|
||||||
|
c.StandardClaims.Subject = c.Subject
|
||||||
|
|
||||||
|
c.SetProperty(webfinger.NSpubkey, &c.PubKey)
|
||||||
|
|
||||||
|
pub, err := dec(c.PubKey)
|
||||||
|
return ed25519.PublicKey(pub), err
|
||||||
|
},
|
||||||
|
jwt.WithValidMethods([]string{"EdDSA"}),
|
||||||
|
jwt.WithJSONNumber(),
|
||||||
|
)
|
||||||
|
is.NoErr(err)
|
||||||
|
|
||||||
|
c, ok := token.Claims.(*claims)
|
||||||
|
is.True(ok)
|
||||||
|
|
||||||
|
t.Logf("%#v", c)
|
||||||
|
a, err := ev.Upsert(ctx, es, webfinger.StreamID(c.Subject), func(ctx context.Context, a *webfinger.JRD) error {
|
||||||
|
var auth *webfinger.JRD
|
||||||
|
|
||||||
|
// does the target have a pubkey for self auth?
|
||||||
|
if _, ok := a.Properties[webfinger.NSpubkey]; ok {
|
||||||
|
auth = a
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check current version for auth.
|
||||||
|
if authID, ok := a.Properties[webfinger.NSauth]; ok && authID != nil && auth == nil {
|
||||||
|
auth = &webfinger.JRD{}
|
||||||
|
auth.SetStreamID(webfinger.StreamID(*authID))
|
||||||
|
err := es.Load(ctx, auth)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if a.Version() == 0 || a.IsDeleted() {
|
||||||
|
// else does the new object claim auth from another object?
|
||||||
|
if authID, ok := c.Properties[webfinger.NSauth]; ok && authID != nil && auth == nil {
|
||||||
|
auth = &webfinger.JRD{}
|
||||||
|
auth.SetStreamID(webfinger.StreamID(*authID))
|
||||||
|
err := es.Load(ctx, auth)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// fall back to use auth from submitted claims
|
||||||
|
if auth == nil {
|
||||||
|
auth = c.JRD
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if auth == nil {
|
||||||
|
return fmt.Errorf("auth not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
err = a.OnAuth(c.JRD, auth)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return a.OnClaims(c.JRD)
|
||||||
|
})
|
||||||
|
is.NoErr(err)
|
||||||
|
|
||||||
|
for _, e := range a.Events(false) {
|
||||||
|
t.Log(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ptr[T any](v T) *T {
|
||||||
|
return &v
|
||||||
|
}
|
||||||
|
func enc(b []byte) string {
|
||||||
|
return base64.RawURLEncoding.EncodeToString(b)
|
||||||
|
}
|
||||||
|
func dec(s string) ([]byte, error) {
|
||||||
|
s = strings.TrimSpace(s)
|
||||||
|
return base64.RawURLEncoding.DecodeString(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMain(m *testing.M) {
|
||||||
|
ctx, stop := context.WithCancel(context.Background())
|
||||||
|
defer stop()
|
||||||
|
|
||||||
|
err := multierr.Combine(
|
||||||
|
ev.Init(ctx),
|
||||||
|
event.Init(ctx),
|
||||||
|
memstore.Init(ctx),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
m.Run()
|
||||||
|
}
|
||||||
6
app/webfinger/ui/assets/bootstrap.min.css
vendored
Normal file
6
app/webfinger/ui/assets/bootstrap.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
app/webfinger/ui/assets/bootstrap.min.css.map
Normal file
1
app/webfinger/ui/assets/bootstrap.min.css.map
Normal file
File diff suppressed because one or more lines are too long
95
app/webfinger/ui/assets/webfinger.css
Normal file
95
app/webfinger/ui/assets/webfinger.css
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
/* Space out content a bit */
|
||||||
|
body {
|
||||||
|
padding-top: 20px;
|
||||||
|
padding-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Everything but the jumbotron gets side spacing for mobile first views */
|
||||||
|
.header,
|
||||||
|
.footer {
|
||||||
|
padding-right: 15px;
|
||||||
|
padding-left: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Custom page header */
|
||||||
|
.header {
|
||||||
|
padding-bottom: 20px;
|
||||||
|
border-bottom: 1px solid #e5e5e5;
|
||||||
|
}
|
||||||
|
/* Make the masthead heading the same height as the navigation */
|
||||||
|
.header h3 {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 0;
|
||||||
|
line-height: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Custom page footer */
|
||||||
|
.footer {
|
||||||
|
padding-top: 19px;
|
||||||
|
color: #777;
|
||||||
|
border-top: 1px solid #e5e5e5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-heading a {
|
||||||
|
color: white;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container-narrow > hr {
|
||||||
|
margin: 30px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
body, .panel-body {
|
||||||
|
color: white;
|
||||||
|
background-color: #222;
|
||||||
|
}
|
||||||
|
nav.navbar-default {
|
||||||
|
background-color: rgb(35, 29, 71);
|
||||||
|
}
|
||||||
|
.navbar-default .navbar-brand {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
.panel-primary, .list-group, .list-group-item {
|
||||||
|
color: white;
|
||||||
|
background-color: #16181c;
|
||||||
|
}
|
||||||
|
.table > tbody > tr.active > th, .table > tbody > tr.active > td {
|
||||||
|
background-color: rgb(35, 29, 71);
|
||||||
|
}
|
||||||
|
.table-striped > tbody > tr:nth-of-type(2n+1) {
|
||||||
|
background-color: rgb(35, 29, 71);
|
||||||
|
}
|
||||||
|
.panel pre {
|
||||||
|
color: white;
|
||||||
|
background-color: #16181c;
|
||||||
|
}
|
||||||
|
.panel .panel-primary > .panel-heading {
|
||||||
|
background-color: rgb(35, 29, 71);
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel a {
|
||||||
|
color: cornflowerblue;
|
||||||
|
}
|
||||||
|
|
||||||
|
code {
|
||||||
|
color: white;
|
||||||
|
background-color: #282b32;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@import url(https://cdn.jsdelivr.net/npm/firacode@6.2.0/distr/fira_code.css);
|
||||||
|
|
||||||
|
code { font-family: 'Fira Code', monospace; }
|
||||||
|
|
||||||
|
@media (min-width: 100) {
|
||||||
|
.truncate {
|
||||||
|
width: 750px;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
width: 750px;
|
||||||
|
}
|
||||||
|
}
|
||||||
28
app/webfinger/ui/layouts/main.go.tpl
Normal file
28
app/webfinger/ui/layouts/main.go.tpl
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
{{define "main"}}
|
||||||
|
<!DOCTYPE html>
|
||||||
|
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
{{template "meta" .}}
|
||||||
|
<title>👉 Webfinger 👈</title>
|
||||||
|
|
||||||
|
|
||||||
|
<link href="/webfinger/assets/bootstrap.min.css" rel="stylesheet" integrity="sha384-1q8mTJOASx8j1Au+a5WDVnPi2lkFfwwEAa8hDDdjZlpLegxhjVME1fgjWPGmkzs7" crossorigin="anonymous">
|
||||||
|
<link href="/webfinger/assets/webfinger.css" rel="stylesheet" crossorigin="anonymous">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<nav class="navbar navbar-default">
|
||||||
|
<div class="container-fluid">
|
||||||
|
<a class="navbar-brand" href="/webfinger">👉 Webfinger 👈</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div class=container>
|
||||||
|
{{template "content" .}}
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
{{end}}
|
||||||
131
app/webfinger/ui/pages/home.go.tpl
Normal file
131
app/webfinger/ui/pages/home.go.tpl
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
{{template "main" .}}
|
||||||
|
|
||||||
|
{{define "meta"}}{{end}}
|
||||||
|
|
||||||
|
{{define "content"}}
|
||||||
|
<form method="GET">
|
||||||
|
<div class="input-group">
|
||||||
|
<span class="input-group-addon" id="basic-addon1">resource</span>
|
||||||
|
<input name="resource" class="form-control" placeholder="acct:..."/>
|
||||||
|
<span class="input-group-btn">
|
||||||
|
<button class="btn btn-default" type="submit">Go!</button>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<br/>
|
||||||
|
|
||||||
|
{{ if ne .Err nil }}
|
||||||
|
<div class="alert alert-danger" role="alert">
|
||||||
|
{{ .Err }}
|
||||||
|
</div>
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
|
{{ if ne .JRD nil }}
|
||||||
|
<div class="panel panel-primary">
|
||||||
|
<div class="panel-heading">Webfinger Result</div>
|
||||||
|
|
||||||
|
<table class="table">
|
||||||
|
<tr>
|
||||||
|
<th style="width:98px">Subject</th>
|
||||||
|
<td>
|
||||||
|
<div class="media">
|
||||||
|
<div class="media-body">
|
||||||
|
{{ .JRD.Subject }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{ with .JRD.GetLinkByRel "http://webfinger.net/rel/avatar" }}
|
||||||
|
{{ if ne . nil }}
|
||||||
|
<div class="media-left media-middle">
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-body">
|
||||||
|
<img src="{{ .HRef }}" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{ end }}
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
{{if ne (len .JRD.Aliases) 0}}
|
||||||
|
<tr>
|
||||||
|
<th>Aliases</th>
|
||||||
|
<td>
|
||||||
|
<ul class="list-group">
|
||||||
|
{{ range .JRD.Aliases }}<li class="list-group-item">{{ . }}</li>
|
||||||
|
{{ end }}
|
||||||
|
</ul>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
|
{{ if ne (len .JRD.Properties) 0 }}
|
||||||
|
<tr>
|
||||||
|
<th>Properties</th>
|
||||||
|
<td>
|
||||||
|
<div class="list-group truncate">
|
||||||
|
{{ range $key, $value := .JRD.Properties }}<div class="list-group-item">
|
||||||
|
<h5 class="list-group-item-heading" title="{{ $key }}">{{ propName $key }}</h5>
|
||||||
|
<code class="list-group-item-text">{{ $value }}</code>
|
||||||
|
</div>
|
||||||
|
{{ end }}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
|
{{ if ne (len .JRD.Links) 0 }}
|
||||||
|
{{ range .JRD.Links }}
|
||||||
|
<tr class="active">
|
||||||
|
{{ if ne (len .Template) 0 }}
|
||||||
|
<th> Template </th>
|
||||||
|
<td>{{ .Template }}</td>
|
||||||
|
{{ else }}
|
||||||
|
<th> Link </th>
|
||||||
|
<td>{{ if ne (len .HRef) 0 }}<a href="{{ .HRef }}" target="_blank">{{ .HRef }}</a>{{ end }}</td>
|
||||||
|
{{ end }}
|
||||||
|
<tr>
|
||||||
|
<tr>
|
||||||
|
<th> Properties </th>
|
||||||
|
<td>
|
||||||
|
<div class="list-group">
|
||||||
|
<div class="list-group-item truncate">
|
||||||
|
<h5 class="list-group-item-heading">rel<h5>
|
||||||
|
<code class="list-group-item-text">{{ .Rel }}</code>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{ if ne (len .Type) 0 }}<div class="list-group-item truncate">
|
||||||
|
<h5 class="list-group-item-heading">type</h5>
|
||||||
|
<code class="list-group-item-text">{{ .Type }}</code>
|
||||||
|
</div>
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
|
{{ range $key, $value := .Properties }}<div class="list-group-item truncate">
|
||||||
|
<h5 class="list-group-item-heading" title="{{ $key }}">{{ propName $key }}</h5>
|
||||||
|
<code class="list-group-item-text">{{ $value }}</code>
|
||||||
|
</div>
|
||||||
|
{{ end }}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{{ end }}
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
|
<div class="panel panel-primary">
|
||||||
|
<div class="panel-heading">Raw JRD</div>
|
||||||
|
|
||||||
|
<pre style="height: 15em; overflow-y: auto; border: 0px">
|
||||||
|
Status: {{ .Status }}
|
||||||
|
|
||||||
|
{{ .Body | printf "%s" }}
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{end}}
|
||||||
416
app/webfinger/webfinger.go
Normal file
416
app/webfinger/webfinger.go
Normal file
@@ -0,0 +1,416 @@
|
|||||||
|
package webfinger
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/ed25519"
|
||||||
|
"embed"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"html"
|
||||||
|
"html/template"
|
||||||
|
"io"
|
||||||
|
"io/fs"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/golang-jwt/jwt/v4"
|
||||||
|
"go.sour.is/pkg/lg"
|
||||||
|
"go.sour.is/pkg/set"
|
||||||
|
|
||||||
|
"go.sour.is/ev"
|
||||||
|
"go.sour.is/ev/pkg/event"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
//go:embed ui/*/*
|
||||||
|
files embed.FS
|
||||||
|
templates map[string]*template.Template
|
||||||
|
)
|
||||||
|
|
||||||
|
type service struct {
|
||||||
|
es *ev.EventStore
|
||||||
|
self set.Set[string]
|
||||||
|
cache func(string) bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type Option interface {
|
||||||
|
ApplyWebfinger(s *service)
|
||||||
|
}
|
||||||
|
|
||||||
|
type WithHostnames []string
|
||||||
|
|
||||||
|
func (o WithHostnames) ApplyWebfinger(s *service) {
|
||||||
|
s.self = set.New(o...)
|
||||||
|
}
|
||||||
|
|
||||||
|
type WithCache func(string) bool
|
||||||
|
|
||||||
|
func (o WithCache) ApplyWebfinger(s *service) {
|
||||||
|
s.cache = o
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(ctx context.Context, es *ev.EventStore, opts ...Option) (*service, error) {
|
||||||
|
ctx, span := lg.Span(ctx)
|
||||||
|
defer span.End()
|
||||||
|
|
||||||
|
if err := event.Register(
|
||||||
|
ctx,
|
||||||
|
&SubjectSet{},
|
||||||
|
&SubjectDeleted{},
|
||||||
|
&LinkSet{},
|
||||||
|
&LinkDeleted{},
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
svc := &service{es: es}
|
||||||
|
|
||||||
|
for _, o := range opts {
|
||||||
|
o.ApplyWebfinger(svc)
|
||||||
|
}
|
||||||
|
|
||||||
|
return svc, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *service) RegisterHTTP(mux *http.ServeMux) {
|
||||||
|
a, _ := fs.Sub(files, "ui/assets")
|
||||||
|
assets := http.StripPrefix("/webfinger/assets/", http.FileServer(http.FS(a)))
|
||||||
|
|
||||||
|
mux.Handle("/webfinger", s.ui())
|
||||||
|
mux.Handle("/webfinger/assets/", assets)
|
||||||
|
}
|
||||||
|
func (s *service) RegisterWellKnown(mux *http.ServeMux) {
|
||||||
|
mux.Handle("/webfinger", lg.Htrace(s, "webfinger"))
|
||||||
|
}
|
||||||
|
func (s *service) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := r.Context()
|
||||||
|
ctx, span := lg.Span(ctx)
|
||||||
|
defer span.End()
|
||||||
|
|
||||||
|
if r.URL.Path != "/webfinger" {
|
||||||
|
w.WriteHeader(http.StatusNotFound)
|
||||||
|
fmt.Fprint(w, http.StatusText(http.StatusNotFound))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
switch r.Method {
|
||||||
|
case http.MethodPut, http.MethodDelete:
|
||||||
|
if r.ContentLength > 4096 {
|
||||||
|
w.WriteHeader(http.StatusRequestEntityTooLarge)
|
||||||
|
fmt.Fprint(w, http.StatusText(http.StatusRequestEntityTooLarge))
|
||||||
|
span.AddEvent("request too large")
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
body, err := io.ReadAll(io.LimitReader(r.Body, 4096))
|
||||||
|
if err != nil {
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
fmt.Fprint(w, http.StatusText(http.StatusInternalServerError))
|
||||||
|
span.RecordError(err)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
r.Body.Close()
|
||||||
|
|
||||||
|
type claims struct {
|
||||||
|
PubKey string `json:"pub"`
|
||||||
|
*JRD
|
||||||
|
jwt.RegisteredClaims
|
||||||
|
}
|
||||||
|
|
||||||
|
token, err := jwt.ParseWithClaims(
|
||||||
|
string(body),
|
||||||
|
&claims{},
|
||||||
|
func(tok *jwt.Token) (any, error) {
|
||||||
|
c, ok := tok.Claims.(*claims)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("wrong type of claim")
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.JRD == nil {
|
||||||
|
c.JRD = &JRD{}
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JRD.Subject = c.RegisteredClaims.Subject
|
||||||
|
|
||||||
|
c.SetProperty(NSpubkey, &c.PubKey)
|
||||||
|
|
||||||
|
pub, err := dec(c.PubKey)
|
||||||
|
return ed25519.PublicKey(pub), err
|
||||||
|
},
|
||||||
|
jwt.WithValidMethods([]string{"EdDSA"}),
|
||||||
|
jwt.WithJSONNumber(),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
w.WriteHeader(http.StatusUnprocessableEntity)
|
||||||
|
fmt.Fprint(w, http.StatusText(http.StatusUnprocessableEntity), ": ", err.Error())
|
||||||
|
span.RecordError(err)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c, ok := token.Claims.(*claims)
|
||||||
|
if !ok {
|
||||||
|
w.WriteHeader(http.StatusUnprocessableEntity)
|
||||||
|
fmt.Fprint(w, http.StatusText(http.StatusUnprocessableEntity))
|
||||||
|
span.AddEvent("not a claim")
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.ID != "" && s.cache != nil {
|
||||||
|
if ok := s.cache(c.ID); ok {
|
||||||
|
w.WriteHeader(http.StatusAlreadyReported)
|
||||||
|
fmt.Fprint(w, http.StatusText(http.StatusAlreadyReported))
|
||||||
|
span.AddEvent("already seen ID")
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
json.NewEncoder(os.Stdout).Encode(c.JRD)
|
||||||
|
|
||||||
|
for i := range c.JRD.Links {
|
||||||
|
c.JRD.Links[i].Index = uint64(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
a, err := ev.Upsert(ctx, s.es, StreamID(c.JRD.Subject), func(ctx context.Context, a *JRD) error {
|
||||||
|
var auth *JRD
|
||||||
|
|
||||||
|
for i := range a.Links {
|
||||||
|
a.Links[i].Index = uint64(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
// does the target have a pubkey for self auth?
|
||||||
|
if _, ok := a.Properties[NSpubkey]; ok {
|
||||||
|
auth = a
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check current version for auth.
|
||||||
|
if authID, ok := a.Properties[NSauth]; ok && authID != nil && auth == nil {
|
||||||
|
auth = &JRD{}
|
||||||
|
auth.SetStreamID(StreamID(*authID))
|
||||||
|
err := s.es.Load(ctx, auth)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if a.Version() == 0 || a.IsDeleted() {
|
||||||
|
// else does the new object claim auth from another object?
|
||||||
|
if authID, ok := c.Properties[NSauth]; ok && authID != nil && auth == nil {
|
||||||
|
auth = &JRD{}
|
||||||
|
auth.SetStreamID(StreamID(*authID))
|
||||||
|
err := s.es.Load(ctx, auth)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// fall back to use auth from submitted claims
|
||||||
|
if auth == nil {
|
||||||
|
auth = c.JRD
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if auth == nil {
|
||||||
|
return fmt.Errorf("auth not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
err = a.OnAuth(c.JRD, auth)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.Method == http.MethodDelete {
|
||||||
|
return a.OnDelete(c.JRD)
|
||||||
|
}
|
||||||
|
return a.OnClaims(c.JRD)
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
w.WriteHeader(http.StatusUnprocessableEntity)
|
||||||
|
fmt.Fprint(w, http.StatusText(http.StatusUnprocessableEntity), ": ", err.Error())
|
||||||
|
span.RecordError(err)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if version := a.Version(); r.Method == http.MethodDelete && version > 0 {
|
||||||
|
err = s.es.Truncate(ctx, a.StreamID(), int64(version))
|
||||||
|
span.RecordError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/jrd+json")
|
||||||
|
if r.Method == http.MethodDelete {
|
||||||
|
w.WriteHeader(http.StatusNoContent)
|
||||||
|
} else {
|
||||||
|
w.WriteHeader(http.StatusCreated)
|
||||||
|
}
|
||||||
|
j := json.NewEncoder(w)
|
||||||
|
j.SetIndent("", " ")
|
||||||
|
err = j.Encode(a)
|
||||||
|
span.RecordError(err)
|
||||||
|
|
||||||
|
case http.MethodGet:
|
||||||
|
resource := r.URL.Query().Get("resource")
|
||||||
|
rels := r.URL.Query()["rel"]
|
||||||
|
|
||||||
|
if resource == "" {
|
||||||
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if u := Parse(resource); u != nil && !s.self.Has(u.URL.Host) {
|
||||||
|
redirect := &url.URL{}
|
||||||
|
redirect.Scheme = "https"
|
||||||
|
redirect.Host = u.URL.Host
|
||||||
|
redirect.RawQuery = r.URL.RawQuery
|
||||||
|
redirect.Path = "/.well-known/webfinger"
|
||||||
|
|
||||||
|
w.Header().Set("location", redirect.String())
|
||||||
|
w.WriteHeader(http.StatusSeeOther)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
a := &JRD{}
|
||||||
|
a.SetStreamID(StreamID(resource))
|
||||||
|
err := s.es.Load(ctx, a)
|
||||||
|
if err != nil {
|
||||||
|
span.RecordError(err)
|
||||||
|
|
||||||
|
if errors.Is(err, ev.ErrNotFound) {
|
||||||
|
w.WriteHeader(http.StatusNotFound)
|
||||||
|
fmt.Fprint(w, http.StatusText(http.StatusNotFound))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
fmt.Fprint(w, http.StatusText(http.StatusInternalServerError))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if a.IsDeleted() {
|
||||||
|
w.WriteHeader(http.StatusGone)
|
||||||
|
fmt.Fprint(w, http.StatusText(http.StatusGone))
|
||||||
|
span.AddEvent("is deleted")
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(rels) > 0 {
|
||||||
|
a.Links = a.GetLinksByRel(rels...)
|
||||||
|
}
|
||||||
|
|
||||||
|
if a.Properties != nil {
|
||||||
|
if redirect, ok := a.Properties[NSredirect]; ok && redirect != nil {
|
||||||
|
w.Header().Set("location", *redirect)
|
||||||
|
w.WriteHeader(http.StatusSeeOther)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/jrd+json")
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
|
||||||
|
j := json.NewEncoder(w)
|
||||||
|
j.SetIndent("", " ")
|
||||||
|
err = j.Encode(a)
|
||||||
|
span.RecordError(err)
|
||||||
|
|
||||||
|
default:
|
||||||
|
w.Header().Set("Allow", "GET, PUT, DELETE, OPTIONS")
|
||||||
|
w.WriteHeader(http.StatusMethodNotAllowed)
|
||||||
|
fmt.Fprint(w, http.StatusText(http.StatusMethodNotAllowed))
|
||||||
|
span.AddEvent("method not allow: " + r.Method)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func (s *service) ui() http.HandlerFunc {
|
||||||
|
loadTemplates()
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
args := struct {
|
||||||
|
Req *http.Request
|
||||||
|
Status int
|
||||||
|
Body []byte
|
||||||
|
JRD *JRD
|
||||||
|
Err error
|
||||||
|
}{Status: http.StatusOK}
|
||||||
|
|
||||||
|
if r.URL.Query().Has("resource") {
|
||||||
|
args.Req, args.Err = http.NewRequestWithContext(r.Context(), http.MethodGet, r.URL.String(), nil)
|
||||||
|
if args.Err != nil {
|
||||||
|
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
wr := httptest.NewRecorder()
|
||||||
|
s.ServeHTTP(wr, args.Req)
|
||||||
|
|
||||||
|
args.Status = wr.Code
|
||||||
|
|
||||||
|
switch wr.Code {
|
||||||
|
case http.StatusSeeOther:
|
||||||
|
res, err := http.DefaultClient.Get(wr.Header().Get("location"))
|
||||||
|
args.Err = err
|
||||||
|
if err == nil {
|
||||||
|
args.Status = res.StatusCode
|
||||||
|
args.Body, args.Err = io.ReadAll(res.Body)
|
||||||
|
}
|
||||||
|
case http.StatusOK:
|
||||||
|
args.Body, args.Err = io.ReadAll(wr.Body)
|
||||||
|
if args.Err == nil {
|
||||||
|
args.JRD, args.Err = ParseJRD(args.Body)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if args.Err == nil && args.Body != nil {
|
||||||
|
args.JRD, args.Err = ParseJRD(args.Body)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
t := templates["home.go.tpl"]
|
||||||
|
err := t.Execute(w, args)
|
||||||
|
if err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func dec(s string) ([]byte, error) {
|
||||||
|
s = strings.TrimSpace(s)
|
||||||
|
return base64.RawURLEncoding.DecodeString(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
var funcMap = map[string]any{
|
||||||
|
"propName": func(in string) string { return in[strings.LastIndex(in, "/")+1:] },
|
||||||
|
"escape": html.EscapeString,
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadTemplates() error {
|
||||||
|
if templates != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
templates = make(map[string]*template.Template)
|
||||||
|
tmplFiles, err := fs.ReadDir(files, "ui/pages")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tmpl := range tmplFiles {
|
||||||
|
if tmpl.IsDir() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
pt := template.New(tmpl.Name())
|
||||||
|
pt.Funcs(funcMap)
|
||||||
|
pt, err = pt.ParseFS(files, "ui/pages/"+tmpl.Name(), "ui/layouts/*.go.tpl")
|
||||||
|
if err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
templates[tmpl.Name()] = pt
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
3
cmd/README.md
Normal file
3
cmd/README.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# Cmd
|
||||||
|
|
||||||
|
These are examples that can be built using EV. Because they are modular the apps can be mixed an matched by including the different source files linked from `cmd/ev`.
|
||||||
34
cmd/ev/app.msgbus.go
Normal file
34
cmd/ev/app.msgbus.go
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"go.sour.is/ev"
|
||||||
|
"go.sour.is/ev/app/msgbus"
|
||||||
|
"go.sour.is/ev/pkg/driver/projecter"
|
||||||
|
"go.sour.is/pkg/lg"
|
||||||
|
"go.sour.is/pkg/service"
|
||||||
|
"go.sour.is/pkg/slice"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ = apps.Register(50, func(ctx context.Context, svc *service.Harness) error {
|
||||||
|
ctx, span := lg.Span(ctx)
|
||||||
|
defer span.End()
|
||||||
|
|
||||||
|
span.AddEvent("Enable Msgbus")
|
||||||
|
eventstore, ok := slice.Find[*ev.EventStore](svc.Services...)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("*es.EventStore not found in services")
|
||||||
|
}
|
||||||
|
eventstore.Option(projecter.New(ctx, msgbus.Projector))
|
||||||
|
|
||||||
|
msgbus, err := msgbus.New(ctx, eventstore)
|
||||||
|
if err != nil {
|
||||||
|
span.RecordError(err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
svc.Add(msgbus)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
44
cmd/ev/app.peerfinder.go
Normal file
44
cmd/ev/app.peerfinder.go
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"go.sour.is/pkg/env"
|
||||||
|
"go.sour.is/pkg/lg"
|
||||||
|
"go.sour.is/pkg/service"
|
||||||
|
"go.sour.is/pkg/slice"
|
||||||
|
|
||||||
|
"go.sour.is/ev"
|
||||||
|
"go.sour.is/ev/app/peerfinder"
|
||||||
|
"go.sour.is/ev/pkg/driver/projecter"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ = apps.Register(50, func(ctx context.Context, svc *service.Harness) error {
|
||||||
|
ctx, span := lg.Span(ctx)
|
||||||
|
defer span.End()
|
||||||
|
|
||||||
|
span.AddEvent("Enable Peers")
|
||||||
|
|
||||||
|
eventstore, ok := slice.Find[*ev.EventStore](svc.Services...)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("*es.EventStore not found in services")
|
||||||
|
}
|
||||||
|
eventstore.Option(projecter.New(ctx, peerfinder.Projector))
|
||||||
|
|
||||||
|
peers, err := peerfinder.New(ctx, eventstore, env.Secret("PEER_STATUS", "").Secret())
|
||||||
|
if err != nil {
|
||||||
|
span.RecordError(err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
svc.RunOnce(ctx, peers.RefreshJob)
|
||||||
|
svc.NewCron("0,15,30,45", peers.RefreshJob)
|
||||||
|
svc.RunOnce(ctx, peers.CleanJob)
|
||||||
|
svc.NewCron("0 1", peers.CleanJob)
|
||||||
|
svc.OnStart(peers.Run)
|
||||||
|
svc.OnStop(peers.Stop)
|
||||||
|
|
||||||
|
svc.Add(peers)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
64
cmd/ev/app.salty.go
Normal file
64
cmd/ev/app.salty.go
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"go.sour.is/ev"
|
||||||
|
"go.sour.is/ev/app/salty"
|
||||||
|
"go.sour.is/pkg/env"
|
||||||
|
"go.sour.is/pkg/lg"
|
||||||
|
"go.sour.is/pkg/service"
|
||||||
|
"go.sour.is/pkg/slice"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ = apps.Register(50, func(ctx context.Context, svc *service.Harness) error {
|
||||||
|
ctx, span := lg.Span(ctx)
|
||||||
|
defer span.End()
|
||||||
|
|
||||||
|
span.AddEvent("Enable Salty")
|
||||||
|
eventstore, ok := slice.Find[*ev.EventStore](svc.Services...)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("*es.EventStore not found in services")
|
||||||
|
}
|
||||||
|
|
||||||
|
addr := "localhost"
|
||||||
|
if ht, ok := slice.Find[*http.Server](svc.Services...); ok {
|
||||||
|
addr = ht.Addr
|
||||||
|
}
|
||||||
|
|
||||||
|
var opts []salty.Option
|
||||||
|
|
||||||
|
base, err := url.JoinPath(env.Default("SALTY_BASE_URL", "http://"+addr), "inbox")
|
||||||
|
if err != nil {
|
||||||
|
span.RecordError(err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
opts = append(opts, salty.WithBaseURL(base))
|
||||||
|
|
||||||
|
if p := env.Default("SALTY_BLOB_DIR", ""); p != "" {
|
||||||
|
if strings.HasPrefix(p, "~/") {
|
||||||
|
home, _ := os.UserHomeDir()
|
||||||
|
p = filepath.Join(home, strings.TrimPrefix(p, "~/"))
|
||||||
|
}
|
||||||
|
opts = append(opts, salty.WithBlobStore(p))
|
||||||
|
}
|
||||||
|
|
||||||
|
salty, err := salty.New(
|
||||||
|
ctx,
|
||||||
|
eventstore,
|
||||||
|
opts...,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
span.RecordError(err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
svc.Add(salty)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
17
cmd/ev/app.twtxt.go
Normal file
17
cmd/ev/app.twtxt.go
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"go.sour.is/pkg/lg"
|
||||||
|
"go.sour.is/pkg/service"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ = apps.Register(50, func(ctx context.Context, svc *service.Harness) error {
|
||||||
|
_, span := lg.Span(ctx)
|
||||||
|
defer span.End()
|
||||||
|
|
||||||
|
span.AddEvent("Enable Twtxt")
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
52
cmd/ev/app.webfinger.go
Normal file
52
cmd/ev/app.webfinger.go
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/patrickmn/go-cache"
|
||||||
|
|
||||||
|
"go.sour.is/ev"
|
||||||
|
"go.sour.is/ev/app/webfinger"
|
||||||
|
"go.sour.is/pkg/env"
|
||||||
|
"go.sour.is/pkg/lg"
|
||||||
|
"go.sour.is/pkg/service"
|
||||||
|
"go.sour.is/pkg/slice"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
defaultExpire = 3 * time.Minute
|
||||||
|
cleanupInterval = 10 * time.Minute
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ = apps.Register(50, func(ctx context.Context, svc *service.Harness) error {
|
||||||
|
ctx, span := lg.Span(ctx)
|
||||||
|
defer span.End()
|
||||||
|
|
||||||
|
span.AddEvent("Enable WebFinger")
|
||||||
|
eventstore, ok := slice.Find[*ev.EventStore](svc.Services...)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("*es.EventStore not found in services")
|
||||||
|
}
|
||||||
|
|
||||||
|
cache := cache.New(defaultExpire, cleanupInterval)
|
||||||
|
var withCache webfinger.WithCache = (func(s string) bool {
|
||||||
|
if _, ok := cache.Get(s); ok {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
cache.SetDefault(s, true)
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
var withHostnames webfinger.WithHostnames = strings.Fields(env.Default("WEBFINGER_DOMAINS", "sour.is"))
|
||||||
|
|
||||||
|
wf, err := webfinger.New(ctx, eventstore, withCache, withHostnames)
|
||||||
|
if err != nil {
|
||||||
|
span.RecordError(err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
svc.Add(wf)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
41
cmd/ev/main.go
Normal file
41
cmd/ev/main.go
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
|
||||||
|
"go.sour.is/pkg/lg"
|
||||||
|
"go.sour.is/pkg/service"
|
||||||
|
)
|
||||||
|
|
||||||
|
var apps service.Apps
|
||||||
|
var appName, version = service.AppName()
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, os.Kill)
|
||||||
|
go func() {
|
||||||
|
<-ctx.Done()
|
||||||
|
defer cancel() // restore interrupt function
|
||||||
|
}()
|
||||||
|
if err := run(ctx); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func run(ctx context.Context) error {
|
||||||
|
svc := &service.Harness{}
|
||||||
|
ctx, stop := lg.Init(ctx, appName)
|
||||||
|
svc.OnStop(stop)
|
||||||
|
svc.Add(lg.NewHTTP(ctx))
|
||||||
|
svc.Setup(ctx, apps.Apps()...)
|
||||||
|
|
||||||
|
// Run application
|
||||||
|
if err := svc.Run(ctx, appName, version); err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
54
cmd/ev/svc.es.go
Normal file
54
cmd/ev/svc.es.go
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"go.sour.is/pkg/env"
|
||||||
|
"go.sour.is/pkg/lg"
|
||||||
|
"go.sour.is/pkg/service"
|
||||||
|
"go.uber.org/multierr"
|
||||||
|
|
||||||
|
"go.sour.is/ev"
|
||||||
|
diskstore "go.sour.is/ev/pkg/driver/disk-store"
|
||||||
|
memstore "go.sour.is/ev/pkg/driver/mem-store"
|
||||||
|
"go.sour.is/ev/pkg/driver/projecter"
|
||||||
|
resolvelinks "go.sour.is/ev/pkg/driver/resolve-links"
|
||||||
|
"go.sour.is/ev/pkg/driver/streamer"
|
||||||
|
"go.sour.is/ev/pkg/event"
|
||||||
|
gql_ev "go.sour.is/ev/pkg/gql"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ = apps.Register(10, func(ctx context.Context, svc *service.Harness) error {
|
||||||
|
ctx, span := lg.Span(ctx)
|
||||||
|
defer span.End()
|
||||||
|
|
||||||
|
// setup eventstore
|
||||||
|
err := multierr.Combine(
|
||||||
|
ev.Init(ctx),
|
||||||
|
event.Init(ctx),
|
||||||
|
diskstore.Init(ctx),
|
||||||
|
memstore.Init(ctx),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
span.RecordError(err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
eventstore, err := ev.Open(
|
||||||
|
ctx,
|
||||||
|
env.Default("EV_DATA", "mem:"),
|
||||||
|
resolvelinks.New(),
|
||||||
|
streamer.New(ctx),
|
||||||
|
projecter.New(
|
||||||
|
ctx,
|
||||||
|
projecter.DefaultProjection,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
span.RecordError(err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
svc.Add(eventstore, &gql_ev.EventStore{EventStore: eventstore})
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
41
cmd/ev/svc.gql.go
Normal file
41
cmd/ev/svc.gql.go
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"go.sour.is/pkg/gql/resolver"
|
||||||
|
"go.sour.is/pkg/lg"
|
||||||
|
"go.sour.is/pkg/mux"
|
||||||
|
"go.sour.is/pkg/service"
|
||||||
|
"go.sour.is/pkg/slice"
|
||||||
|
|
||||||
|
"go.sour.is/ev/app/gql"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ = apps.Register(90, func(ctx context.Context, svc *service.Harness) error {
|
||||||
|
ctx, span := lg.Span(ctx)
|
||||||
|
defer span.End()
|
||||||
|
|
||||||
|
span.AddEvent("Enable GraphQL")
|
||||||
|
gql, err := resolver.New(ctx, &gql.Resolver{}, slice.FilterType[resolver.IsResolver](svc.Services...)...)
|
||||||
|
if err != nil {
|
||||||
|
span.RecordError(err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
gql.CheckOrigin = func(r *http.Request) bool {
|
||||||
|
switch r.Header.Get("Origin") {
|
||||||
|
case "https://ev.sour.is", "https://www.graphqlbin.com", "http://localhost:8080":
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
svc.Add(gql)
|
||||||
|
svc.Add(mux.RegisterHTTP(func(mux *http.ServeMux) {
|
||||||
|
mux.Handle("/", http.RedirectHandler("/playground", http.StatusTemporaryRedirect))
|
||||||
|
}))
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
47
cmd/ev/svc.http.go
Normal file
47
cmd/ev/svc.http.go
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/rs/cors"
|
||||||
|
|
||||||
|
"go.sour.is/pkg/env"
|
||||||
|
"go.sour.is/pkg/lg"
|
||||||
|
"go.sour.is/pkg/mux"
|
||||||
|
"go.sour.is/pkg/service"
|
||||||
|
"go.sour.is/pkg/slice"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ = apps.Register(20, func(ctx context.Context, svc *service.Harness) error {
|
||||||
|
s := &http.Server{}
|
||||||
|
svc.Add(s)
|
||||||
|
|
||||||
|
mux := mux.New()
|
||||||
|
s.Handler = cors.AllowAll().Handler(mux)
|
||||||
|
|
||||||
|
// s.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// log.Println(r.URL.Path)
|
||||||
|
// mux.ServeHTTP(w, r)
|
||||||
|
// })
|
||||||
|
|
||||||
|
s.Addr = env.Default("EV_HTTP", ":8080")
|
||||||
|
if strings.HasPrefix(s.Addr, ":") {
|
||||||
|
s.Addr = "[::]" + s.Addr
|
||||||
|
}
|
||||||
|
svc.OnStart(func(ctx context.Context) error {
|
||||||
|
_, span := lg.Span(ctx)
|
||||||
|
defer span.End()
|
||||||
|
|
||||||
|
log.Print("Listen on ", s.Addr)
|
||||||
|
span.AddEvent("begin listen and serve on " + s.Addr)
|
||||||
|
|
||||||
|
mux.Add(slice.FilterType[interface{ RegisterHTTP(*http.ServeMux) }](svc.Services...)...)
|
||||||
|
return s.ListenAndServe()
|
||||||
|
})
|
||||||
|
svc.OnStop(s.Shutdown)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
1
cmd/msgbus/app.msgbus.go
Symbolic link
1
cmd/msgbus/app.msgbus.go
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
../ev/app.msgbus.go
|
||||||
1
cmd/msgbus/main.go
Symbolic link
1
cmd/msgbus/main.go
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
../ev/main.go
|
||||||
1
cmd/msgbus/svc.es.go
Symbolic link
1
cmd/msgbus/svc.es.go
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
../ev/svc.es.go
|
||||||
1
cmd/msgbus/svc.gql.go
Symbolic link
1
cmd/msgbus/svc.gql.go
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
../ev/svc.gql.go
|
||||||
1
cmd/msgbus/svc.http.go
Symbolic link
1
cmd/msgbus/svc.http.go
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
../ev/svc.http.go
|
||||||
1
cmd/peers/app.peerfinder.go
Symbolic link
1
cmd/peers/app.peerfinder.go
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
../ev/app.peerfinder.go
|
||||||
1
cmd/peers/main.go
Symbolic link
1
cmd/peers/main.go
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
../ev/main.go
|
||||||
1
cmd/peers/svc.es.go
Symbolic link
1
cmd/peers/svc.es.go
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
../ev/svc.es.go
|
||||||
1
cmd/peers/svc.http.go
Symbolic link
1
cmd/peers/svc.http.go
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
../ev/svc.http.go
|
||||||
1
cmd/salty/app.msgbus.go
Symbolic link
1
cmd/salty/app.msgbus.go
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
../ev/app.msgbus.go
|
||||||
1
cmd/salty/app.salty.go
Symbolic link
1
cmd/salty/app.salty.go
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
../ev/app.salty.go
|
||||||
1
cmd/salty/main.go
Symbolic link
1
cmd/salty/main.go
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
../ev/main.go
|
||||||
1
cmd/salty/svc.es.go
Symbolic link
1
cmd/salty/svc.es.go
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
../ev/svc.es.go
|
||||||
1
cmd/salty/svc.http.go
Symbolic link
1
cmd/salty/svc.http.go
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
../ev/svc.http.go
|
||||||
290
cmd/webfinger-cli/main.go
Normal file
290
cmd/webfinger-cli/main.go
Normal file
@@ -0,0 +1,290 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"context"
|
||||||
|
"crypto/ed25519"
|
||||||
|
"encoding/base64"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/docopt/docopt-go"
|
||||||
|
"go.sour.is/pkg/xdg"
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
|
||||||
|
"go.sour.is/ev/app/webfinger"
|
||||||
|
)
|
||||||
|
|
||||||
|
var usage = `Webfinger CLI.
|
||||||
|
usage:
|
||||||
|
webfinger-cli gen [--key KEY] [--force]
|
||||||
|
webfinger-cli get [--host HOST] <subject> [<rel>...]
|
||||||
|
webfinger-cli put [--host HOST] [--key KEY] <filename>
|
||||||
|
webfinger-cli rm [--host HOST] [--key KEY] <subject>
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--key <key> From key [default: ` + xdg.Get(xdg.EnvConfigHome, "webfinger/$USER.key") + `]
|
||||||
|
--host <host> Hostname to use [default: https://ev.sour.is]
|
||||||
|
--force, -f Force recreate key for gen
|
||||||
|
`
|
||||||
|
|
||||||
|
type opts struct {
|
||||||
|
Gen bool `docopt:"gen"`
|
||||||
|
Get bool `docopt:"get"`
|
||||||
|
Put bool `docopt:"put"`
|
||||||
|
Remove bool `docopt:"rm"`
|
||||||
|
|
||||||
|
Key string `docopt:"--key"`
|
||||||
|
Host string `docopt:"--host"`
|
||||||
|
File string `docopt:"<filename>"`
|
||||||
|
Subject string `docopt:"<subject>"`
|
||||||
|
Rel []string `docopt:"<rel>"`
|
||||||
|
|
||||||
|
Force bool `docopt:"--force"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
o, err := docopt.ParseDoc(usage)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
os.Exit(2)
|
||||||
|
}
|
||||||
|
|
||||||
|
var opts opts
|
||||||
|
o.Bind(&opts)
|
||||||
|
|
||||||
|
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, os.Kill)
|
||||||
|
go func() {
|
||||||
|
<-ctx.Done()
|
||||||
|
defer cancel() // restore interrupt function
|
||||||
|
}()
|
||||||
|
|
||||||
|
if err := run(opts); err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func run(opts opts) error {
|
||||||
|
// fmt.Fprintf(os.Stderr, "%#v\n", opts)
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case opts.Gen:
|
||||||
|
err := mkKeyfile(opts.Key, opts.Force)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fmt.Println("wrote keyfile to", opts.Key)
|
||||||
|
case opts.Get:
|
||||||
|
url, err := url.Parse(opts.Host)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
url.Path = "/.well-known/webfinger"
|
||||||
|
query := url.Query()
|
||||||
|
query.Set("resource", opts.Subject)
|
||||||
|
for _, rel := range opts.Rel {
|
||||||
|
query.Add("rel", rel)
|
||||||
|
}
|
||||||
|
url.RawQuery = query.Encode()
|
||||||
|
|
||||||
|
req, err := http.NewRequest(http.MethodGet, url.String(), nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := http.DefaultClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer res.Body.Close()
|
||||||
|
|
||||||
|
s, err := io.ReadAll(res.Body)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println(string(s))
|
||||||
|
case opts.Remove:
|
||||||
|
url, err := url.Parse(opts.Host)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
url.Path = "/.well-known/webfinger"
|
||||||
|
|
||||||
|
key, err := readKeyfile(opts.Key)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
jrd := &webfinger.JRD{Subject: opts.Subject}
|
||||||
|
token, err := webfinger.NewSignedRequest(jrd, key)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
body := strings.NewReader(token)
|
||||||
|
req, err := http.NewRequest(http.MethodDelete, url.String(), body)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := http.DefaultClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer res.Body.Close()
|
||||||
|
|
||||||
|
s, err := io.ReadAll(res.Body)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println(res.Status, string(s))
|
||||||
|
case opts.Put:
|
||||||
|
url, err := url.Parse(opts.Host)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
url.Path = "/.well-known/webfinger"
|
||||||
|
|
||||||
|
key, err := readKeyfile(opts.Key)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Fprintln(os.Stderr, opts.File)
|
||||||
|
fp, err := os.Open(opts.File)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
y := yaml.NewDecoder(fp)
|
||||||
|
|
||||||
|
for err == nil {
|
||||||
|
jrd := &webfinger.JRD{}
|
||||||
|
|
||||||
|
err = y.Decode(jrd)
|
||||||
|
if err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Fprintln(os.Stderr, jrd)
|
||||||
|
|
||||||
|
token, err := webfinger.NewSignedRequest(jrd, key)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
body := strings.NewReader(token)
|
||||||
|
|
||||||
|
req, err := http.NewRequest(http.MethodPut, url.String(), body)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := http.DefaultClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer res.Body.Close()
|
||||||
|
|
||||||
|
s, err := io.ReadAll(res.Body)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println(res.Status, string(s))
|
||||||
|
|
||||||
|
}
|
||||||
|
if err != nil && !errors.Is(err, io.EOF) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func enc(b []byte) string {
|
||||||
|
return base64.RawURLEncoding.EncodeToString(b)
|
||||||
|
}
|
||||||
|
func dec(s string) ([]byte, error) {
|
||||||
|
s = strings.TrimSpace(s)
|
||||||
|
return base64.RawURLEncoding.DecodeString(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func mkKeyfile(keyfile string, force bool) error {
|
||||||
|
pub, priv, err := ed25519.GenerateKey(nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = os.MkdirAll(filepath.Dir(keyfile), 0700)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = os.Stat(keyfile)
|
||||||
|
if !os.IsNotExist(err) {
|
||||||
|
if force {
|
||||||
|
fmt.Println("removing keyfile", keyfile)
|
||||||
|
err = os.Remove(keyfile)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return fmt.Errorf("the keyfile %s exists. use --force", keyfile)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fp, err := os.OpenFile(keyfile, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0600)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fmt.Fprint(fp, "# pub: ", enc(pub), "\n", enc(priv))
|
||||||
|
|
||||||
|
return fp.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func readKeyfile(keyfile string) (ed25519.PrivateKey, error) {
|
||||||
|
fd, err := os.Stat(keyfile)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if fd.Mode()&0066 != 0 {
|
||||||
|
return nil, fmt.Errorf("permissions are too weak")
|
||||||
|
}
|
||||||
|
|
||||||
|
f, err := os.Open(keyfile)
|
||||||
|
scan := bufio.NewScanner(f)
|
||||||
|
|
||||||
|
var key ed25519.PrivateKey
|
||||||
|
for scan.Scan() {
|
||||||
|
txt := scan.Text()
|
||||||
|
if strings.HasPrefix(txt, "#") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(txt) == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
txt = strings.TrimPrefix(txt, "# priv: ")
|
||||||
|
b, err := dec(txt)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
key = b
|
||||||
|
}
|
||||||
|
|
||||||
|
return key, err
|
||||||
|
}
|
||||||
1
cmd/webfinger/app.webfinger.go
Symbolic link
1
cmd/webfinger/app.webfinger.go
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
../ev/app.webfinger.go
|
||||||
1
cmd/webfinger/main.go
Symbolic link
1
cmd/webfinger/main.go
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
../ev/main.go
|
||||||
1
cmd/webfinger/svc.es.go
Symbolic link
1
cmd/webfinger/svc.es.go
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
../ev/svc.es.go
|
||||||
1
cmd/webfinger/svc.http.go
Symbolic link
1
cmd/webfinger/svc.http.go
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
../ev/svc.http.go
|
||||||
123
cmd/webfinger/webfinger_e2e_test.go
Normal file
123
cmd/webfinger/webfinger_e2e_test.go
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
//go:build ignore
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/ed25519"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/matryer/is"
|
||||||
|
"go.sour.is/ev/app/webfinger"
|
||||||
|
"go.sour.is/ev/pkg/service"
|
||||||
|
"golang.org/x/sync/errgroup"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMain(m *testing.M) {
|
||||||
|
data, err := os.MkdirTemp("", "data*")
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("error creating data dir: %s\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(data)
|
||||||
|
|
||||||
|
os.Setenv("EV_DATA", "mem:")
|
||||||
|
os.Setenv("EV_HTTP", "[::1]:61234")
|
||||||
|
os.Setenv("WEBFINGER_DOMAINS", "sour.is")
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
running := make(chan struct{})
|
||||||
|
apps.Register(99, func(ctx context.Context, s *service.Harness) error {
|
||||||
|
go func() {
|
||||||
|
<-s.OnRunning()
|
||||||
|
close(running)
|
||||||
|
}()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
wg, ctx := errgroup.WithContext(ctx)
|
||||||
|
wg.Go(func() error {
|
||||||
|
// Run application
|
||||||
|
if err := run(ctx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
wg.Go(func() error {
|
||||||
|
<-running
|
||||||
|
m.Run()
|
||||||
|
cancel()
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
if err := wg.Wait(); err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestE2EGetHTTP(t *testing.T) {
|
||||||
|
is := is.New(t)
|
||||||
|
res, err := http.DefaultClient.Get("http://[::1]:61234/.well-known/webfinger")
|
||||||
|
is.NoErr(err)
|
||||||
|
is.Equal(res.StatusCode, http.StatusBadRequest)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestE2ECreateResource(t *testing.T) {
|
||||||
|
is := is.New(t)
|
||||||
|
|
||||||
|
_, priv, err := ed25519.GenerateKey(nil)
|
||||||
|
is.NoErr(err)
|
||||||
|
|
||||||
|
jrd := &webfinger.JRD{
|
||||||
|
Subject: "acct:me@sour.is",
|
||||||
|
Properties: map[string]*string{
|
||||||
|
"foo": ptr("bar"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// create
|
||||||
|
token, err := webfinger.NewSignedRequest(jrd, priv)
|
||||||
|
is.NoErr(err)
|
||||||
|
|
||||||
|
req, err := http.NewRequest(http.MethodPut, "http://[::1]:61234/.well-known/webfinger", strings.NewReader(token))
|
||||||
|
is.NoErr(err)
|
||||||
|
|
||||||
|
res, err := http.DefaultClient.Do(req)
|
||||||
|
is.NoErr(err)
|
||||||
|
|
||||||
|
is.Equal(res.StatusCode, http.StatusCreated)
|
||||||
|
|
||||||
|
// repeat
|
||||||
|
req, err = http.NewRequest(http.MethodPut, "http://[::1]:61234/.well-known/webfinger", strings.NewReader(token))
|
||||||
|
is.NoErr(err)
|
||||||
|
|
||||||
|
res, err = http.DefaultClient.Do(req)
|
||||||
|
is.NoErr(err)
|
||||||
|
|
||||||
|
is.Equal(res.StatusCode, http.StatusAlreadyReported)
|
||||||
|
|
||||||
|
// fetch
|
||||||
|
req, err = http.NewRequest(http.MethodGet, "http://[::1]:61234/.well-known/webfinger?resource=acct:me@sour.is", nil)
|
||||||
|
is.NoErr(err)
|
||||||
|
|
||||||
|
res, err = http.DefaultClient.Do(req)
|
||||||
|
is.NoErr(err)
|
||||||
|
|
||||||
|
is.Equal(res.StatusCode, http.StatusOK)
|
||||||
|
|
||||||
|
resJRD := &webfinger.JRD{}
|
||||||
|
err = json.NewDecoder(res.Body).Decode(resJRD)
|
||||||
|
is.NoErr(err)
|
||||||
|
is.Equal(jrd.Subject, resJRD.Subject)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ptr[T any](t T) *T { return &t }
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
package es
|
// package es implements an event store and drivers for extending its functionality.
|
||||||
|
package ev
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
@@ -6,12 +7,14 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/sour-is/ev/internal/lg"
|
"go.opentelemetry.io/otel/attribute"
|
||||||
"github.com/sour-is/ev/pkg/es/driver"
|
"go.opentelemetry.io/otel/metric"
|
||||||
"github.com/sour-is/ev/pkg/es/event"
|
|
||||||
"github.com/sour-is/ev/pkg/locker"
|
|
||||||
"go.opentelemetry.io/otel/metric/instrument/syncint64"
|
|
||||||
"go.uber.org/multierr"
|
"go.uber.org/multierr"
|
||||||
|
|
||||||
|
"go.sour.is/ev/pkg/driver"
|
||||||
|
"go.sour.is/ev/pkg/event"
|
||||||
|
"go.sour.is/pkg/lg"
|
||||||
|
"go.sour.is/pkg/locker"
|
||||||
)
|
)
|
||||||
|
|
||||||
type config struct {
|
type config struct {
|
||||||
@@ -29,19 +32,19 @@ func Init(ctx context.Context) error {
|
|||||||
m := lg.Meter(ctx)
|
m := lg.Meter(ctx)
|
||||||
|
|
||||||
var err, errs error
|
var err, errs error
|
||||||
Mes_open, err = m.SyncInt64().Counter("es_open")
|
Mes_open, err = m.Int64Counter("es_open")
|
||||||
errs = multierr.Append(errs, err)
|
errs = multierr.Append(errs, err)
|
||||||
|
|
||||||
Mes_read, err = m.SyncInt64().Counter("es_read")
|
Mes_read, err = m.Int64Counter("es_read")
|
||||||
errs = multierr.Append(errs, err)
|
errs = multierr.Append(errs, err)
|
||||||
|
|
||||||
Mes_load, err = m.SyncInt64().Counter("es_load")
|
Mes_load, err = m.Int64Counter("es_load")
|
||||||
errs = multierr.Append(errs, err)
|
errs = multierr.Append(errs, err)
|
||||||
|
|
||||||
Mes_save, err = m.SyncInt64().Counter("es_save")
|
Mes_save, err = m.Int64Counter("es_save")
|
||||||
errs = multierr.Append(errs, err)
|
errs = multierr.Append(errs, err)
|
||||||
|
|
||||||
Mes_append, err = m.SyncInt64().Counter("es_append")
|
Mes_append, err = m.Int64Counter("es_append")
|
||||||
errs = multierr.Append(errs, err)
|
errs = multierr.Append(errs, err)
|
||||||
|
|
||||||
return errs
|
return errs
|
||||||
@@ -51,7 +54,7 @@ func Register(ctx context.Context, name string, d driver.Driver) error {
|
|||||||
ctx, span := lg.Span(ctx)
|
ctx, span := lg.Span(ctx)
|
||||||
defer span.End()
|
defer span.End()
|
||||||
|
|
||||||
return drivers.Modify(ctx, func(c *config) error {
|
return drivers.Use(ctx, func(ctx context.Context, c *config) error {
|
||||||
if _, set := c.drivers[name]; set {
|
if _, set := c.drivers[name]; set {
|
||||||
return fmt.Errorf("driver %s already set", name)
|
return fmt.Errorf("driver %s already set", name)
|
||||||
}
|
}
|
||||||
@@ -65,17 +68,19 @@ type EventStore struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
Mes_open syncint64.Counter
|
Mes_open metric.Int64Counter
|
||||||
Mes_read syncint64.Counter
|
Mes_read metric.Int64Counter
|
||||||
Mes_load syncint64.Counter
|
Mes_load metric.Int64Counter
|
||||||
Mes_save syncint64.Counter
|
Mes_save metric.Int64Counter
|
||||||
Mes_append syncint64.Counter
|
Mes_append metric.Int64Counter
|
||||||
)
|
)
|
||||||
|
|
||||||
func Open(ctx context.Context, dsn string, options ...Option) (*EventStore, error) {
|
func Open(ctx context.Context, dsn string, options ...Option) (*EventStore, error) {
|
||||||
ctx, span := lg.Span(ctx)
|
ctx, span := lg.Span(ctx)
|
||||||
defer span.End()
|
defer span.End()
|
||||||
|
|
||||||
|
span.SetAttributes(attribute.String("dsn", dsn))
|
||||||
|
|
||||||
name, _, ok := strings.Cut(dsn, ":")
|
name, _, ok := strings.Cut(dsn, ":")
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, fmt.Errorf("%w: no scheme", ErrNoDriver)
|
return nil, fmt.Errorf("%w: no scheme", ErrNoDriver)
|
||||||
@@ -95,15 +100,19 @@ func Open(ctx context.Context, dsn string, options ...Option) (*EventStore, erro
|
|||||||
conn, err := d.Open(ctx, dsn)
|
conn, err := d.Open(ctx, dsn)
|
||||||
|
|
||||||
es := &EventStore{Driver: conn}
|
es := &EventStore{Driver: conn}
|
||||||
for _, o := range options {
|
es.Option(options...)
|
||||||
o.Apply(es)
|
|
||||||
}
|
|
||||||
|
|
||||||
Mes_open.Add(ctx, 1)
|
Mes_open.Add(ctx, 1)
|
||||||
|
|
||||||
return es, err
|
return es, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (es *EventStore) Option(options ...Option) {
|
||||||
|
for _, o := range options {
|
||||||
|
o.Apply(es)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type Option interface {
|
type Option interface {
|
||||||
Apply(*EventStore)
|
Apply(*EventStore)
|
||||||
}
|
}
|
||||||
@@ -117,7 +126,11 @@ func (es *EventStore) Save(ctx context.Context, agg event.Aggregate) (uint64, er
|
|||||||
return 0, nil
|
return 0, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
Mes_save.Add(ctx, 1)
|
span.SetAttributes(
|
||||||
|
attribute.String("agg.type", event.TypeOf(agg)),
|
||||||
|
attribute.String("agg.streamID", agg.StreamID()),
|
||||||
|
attribute.Int64("agg.version", int64(agg.StreamVersion())),
|
||||||
|
)
|
||||||
|
|
||||||
l, err := es.EventLog(ctx, agg.StreamID())
|
l, err := es.EventLog(ctx, agg.StreamID())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -128,6 +141,7 @@ func (es *EventStore) Save(ctx context.Context, agg event.Aggregate) (uint64, er
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
|
Mes_save.Add(ctx, int64(count))
|
||||||
|
|
||||||
agg.Commit()
|
agg.Commit()
|
||||||
return count, err
|
return count, err
|
||||||
@@ -136,39 +150,90 @@ func (es *EventStore) Load(ctx context.Context, agg event.Aggregate) error {
|
|||||||
ctx, span := lg.Span(ctx)
|
ctx, span := lg.Span(ctx)
|
||||||
defer span.End()
|
defer span.End()
|
||||||
|
|
||||||
Mes_load.Add(ctx, 1)
|
span.SetAttributes(
|
||||||
|
attribute.String("agg.type", event.TypeOf(agg)),
|
||||||
|
attribute.String("agg.streamID", agg.StreamID()),
|
||||||
|
)
|
||||||
l, err := es.Driver.EventLog(ctx, agg.StreamID())
|
l, err := es.Driver.EventLog(ctx, agg.StreamID())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
first, err := l.FirstIndex(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
events, err := l.Read(ctx, 0, AllEvents)
|
events, err := l.Read(ctx, 0, AllEvents)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if len(events) == 0 {
|
||||||
|
return ErrNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
Mes_load.Add(ctx, events.Count())
|
||||||
|
event.Start(agg, first-1)
|
||||||
event.Append(agg, events...)
|
event.Append(agg, events...)
|
||||||
|
|
||||||
|
span.SetAttributes(
|
||||||
|
attribute.Int64("agg.version", int64(agg.StreamVersion())),
|
||||||
|
)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
func (es *EventStore) Read(ctx context.Context, streamID string, pos, count int64) (event.Events, error) {
|
func (es *EventStore) Read(ctx context.Context, streamID string, after, count int64) (event.Events, error) {
|
||||||
ctx, span := lg.Span(ctx)
|
ctx, span := lg.Span(ctx)
|
||||||
defer span.End()
|
defer span.End()
|
||||||
|
|
||||||
Mes_read.Add(ctx, 1)
|
span.SetAttributes(
|
||||||
|
attribute.String("streamID", streamID),
|
||||||
|
attribute.Int64("after", after),
|
||||||
|
attribute.Int64("count", count),
|
||||||
|
)
|
||||||
|
|
||||||
l, err := es.Driver.EventLog(ctx, streamID)
|
l, err := es.Driver.EventLog(ctx, streamID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return l.Read(ctx, pos, count)
|
|
||||||
|
events, err := l.Read(ctx, after, count)
|
||||||
|
Mes_read.Add(ctx, events.Count())
|
||||||
|
|
||||||
|
return events, err
|
||||||
|
}
|
||||||
|
func (es *EventStore) ReadN(ctx context.Context, streamID string, index ...uint64) (event.Events, error) {
|
||||||
|
ctx, span := lg.Span(ctx)
|
||||||
|
defer span.End()
|
||||||
|
|
||||||
|
lis := make([]int64, len(index))
|
||||||
|
for i, j := range index {
|
||||||
|
lis[i] = int64(j)
|
||||||
|
}
|
||||||
|
|
||||||
|
span.SetAttributes(
|
||||||
|
attribute.String("streamID", streamID),
|
||||||
|
attribute.Int64Slice("index", lis),
|
||||||
|
)
|
||||||
|
|
||||||
|
l, err := es.Driver.EventLog(ctx, streamID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
events, err := l.ReadN(ctx, index...)
|
||||||
|
Mes_read.Add(ctx, events.Count())
|
||||||
|
|
||||||
|
return events, err
|
||||||
}
|
}
|
||||||
func (es *EventStore) Append(ctx context.Context, streamID string, events event.Events) (uint64, error) {
|
func (es *EventStore) Append(ctx context.Context, streamID string, events event.Events) (uint64, error) {
|
||||||
ctx, span := lg.Span(ctx)
|
ctx, span := lg.Span(ctx)
|
||||||
defer span.End()
|
defer span.End()
|
||||||
|
|
||||||
Mes_append.Add(ctx, 1)
|
Mes_append.Add(ctx, 1)
|
||||||
|
span.SetAttributes(
|
||||||
|
attribute.String("ev.streamID", streamID),
|
||||||
|
)
|
||||||
|
|
||||||
l, err := es.Driver.EventLog(ctx, streamID)
|
l, err := es.Driver.EventLog(ctx, streamID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -180,6 +245,10 @@ func (es *EventStore) FirstIndex(ctx context.Context, streamID string) (uint64,
|
|||||||
ctx, span := lg.Span(ctx)
|
ctx, span := lg.Span(ctx)
|
||||||
defer span.End()
|
defer span.End()
|
||||||
|
|
||||||
|
span.SetAttributes(
|
||||||
|
attribute.String("ev.streamID", streamID),
|
||||||
|
)
|
||||||
|
|
||||||
l, err := es.Driver.EventLog(ctx, streamID)
|
l, err := es.Driver.EventLog(ctx, streamID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
@@ -190,6 +259,10 @@ func (es *EventStore) LastIndex(ctx context.Context, streamID string) (uint64, e
|
|||||||
ctx, span := lg.Span(ctx)
|
ctx, span := lg.Span(ctx)
|
||||||
defer span.End()
|
defer span.End()
|
||||||
|
|
||||||
|
span.SetAttributes(
|
||||||
|
attribute.String("ev.streamID", streamID),
|
||||||
|
)
|
||||||
|
|
||||||
l, err := es.Driver.EventLog(ctx, streamID)
|
l, err := es.Driver.EventLog(ctx, streamID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
@@ -210,6 +283,27 @@ func (es *EventStore) EventStream() driver.EventStream {
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
func (es *EventStore) Truncate(ctx context.Context, streamID string, index int64) error {
|
||||||
|
ctx, span := lg.Span(ctx)
|
||||||
|
defer span.End()
|
||||||
|
|
||||||
|
up, err := es.Driver.EventLog(ctx, streamID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for up != nil {
|
||||||
|
if up, ok := up.(driver.EventLogWithTruncate); ok {
|
||||||
|
err = up.Truncate(ctx, index)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
up = Unwrap(up)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ErrNoDriver
|
||||||
|
}
|
||||||
|
|
||||||
func Unwrap[T any](t T) T {
|
func Unwrap[T any](t T) T {
|
||||||
if unwrap, ok := any(t).(interface{ Unwrap() T }); ok {
|
if unwrap, ok := any(t).(interface{ Unwrap() T }); ok {
|
||||||
@@ -224,6 +318,7 @@ var ErrNoDriver = errors.New("no driver")
|
|||||||
var ErrWrongVersion = errors.New("wrong version")
|
var ErrWrongVersion = errors.New("wrong version")
|
||||||
var ErrShouldExist = event.ErrShouldExist
|
var ErrShouldExist = event.ErrShouldExist
|
||||||
var ErrShouldNotExist = event.ErrShouldNotExist
|
var ErrShouldNotExist = event.ErrShouldNotExist
|
||||||
|
var ErrNotFound = errors.New("not found")
|
||||||
|
|
||||||
type PA[T any] interface {
|
type PA[T any] interface {
|
||||||
event.Aggregate
|
event.Aggregate
|
||||||
@@ -241,8 +336,11 @@ func Create[A any, T PA[A]](ctx context.Context, es *EventStore, streamID string
|
|||||||
|
|
||||||
agg = new(A)
|
agg = new(A)
|
||||||
agg.SetStreamID(streamID)
|
agg.SetStreamID(streamID)
|
||||||
|
span.SetAttributes(
|
||||||
|
attribute.String("agg.streamID", streamID),
|
||||||
|
)
|
||||||
|
|
||||||
if err = es.Load(ctx, agg); err != nil {
|
if err = es.Load(ctx, agg); err != nil && !errors.Is(err, ErrNotFound) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -271,6 +369,9 @@ func Update[A any, T PA[A]](ctx context.Context, es *EventStore, streamID string
|
|||||||
|
|
||||||
agg = new(A)
|
agg = new(A)
|
||||||
agg.SetStreamID(streamID)
|
agg.SetStreamID(streamID)
|
||||||
|
span.SetAttributes(
|
||||||
|
attribute.String("agg.streamID", streamID),
|
||||||
|
)
|
||||||
|
|
||||||
if err = es.Load(ctx, agg); err != nil {
|
if err = es.Load(ctx, agg); err != nil {
|
||||||
return
|
return
|
||||||
@@ -298,8 +399,11 @@ func Upsert[A any, T PA[A]](ctx context.Context, es *EventStore, streamID string
|
|||||||
|
|
||||||
agg = new(A)
|
agg = new(A)
|
||||||
agg.SetStreamID(streamID)
|
agg.SetStreamID(streamID)
|
||||||
|
span.SetAttributes(
|
||||||
|
attribute.String("agg.streamID", streamID),
|
||||||
|
)
|
||||||
|
|
||||||
if err = es.Load(ctx, agg); err != nil {
|
if err = es.Load(ctx, agg); err != nil && !errors.Is(err, ErrNotFound) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1,20 +1,22 @@
|
|||||||
package es_test
|
package ev_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/matryer/is"
|
"github.com/matryer/is"
|
||||||
|
"go.uber.org/multierr"
|
||||||
|
|
||||||
"github.com/sour-is/ev/pkg/es"
|
"go.sour.is/ev"
|
||||||
memstore "github.com/sour-is/ev/pkg/es/driver/mem-store"
|
"go.sour.is/ev/app/peerfinder"
|
||||||
"github.com/sour-is/ev/pkg/es/driver/projecter"
|
memstore "go.sour.is/ev/pkg/driver/mem-store"
|
||||||
"github.com/sour-is/ev/pkg/es/driver/streamer"
|
"go.sour.is/ev/pkg/driver/projecter"
|
||||||
"github.com/sour-is/ev/pkg/es/event"
|
resolvelinks "go.sour.is/ev/pkg/driver/resolve-links"
|
||||||
|
"go.sour.is/ev/pkg/driver/streamer"
|
||||||
|
"go.sour.is/ev/pkg/event"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -26,12 +28,9 @@ type Thing struct {
|
|||||||
Name string
|
Name string
|
||||||
Value string
|
Value string
|
||||||
|
|
||||||
event.AggregateRoot
|
event.IsAggregate
|
||||||
}
|
}
|
||||||
|
|
||||||
// func (a *Thing) StreamID() string {
|
|
||||||
// return fmt.Sprintf("thing-%s", a.Name)
|
|
||||||
// }
|
|
||||||
func (a *Thing) ApplyEvent(lis ...event.Event) {
|
func (a *Thing) ApplyEvent(lis ...event.Event) {
|
||||||
for _, e := range lis {
|
for _, e := range lis {
|
||||||
switch e := e.(type) {
|
switch e := e.(type) {
|
||||||
@@ -48,26 +47,7 @@ func (a *Thing) OnSetValue(value string) error {
|
|||||||
type ValueSet struct {
|
type ValueSet struct {
|
||||||
Value string
|
Value string
|
||||||
|
|
||||||
eventMeta event.Meta
|
event.IsEvent
|
||||||
}
|
|
||||||
|
|
||||||
func (e *ValueSet) EventMeta() event.Meta {
|
|
||||||
if e == nil {
|
|
||||||
return event.Meta{}
|
|
||||||
}
|
|
||||||
return e.eventMeta
|
|
||||||
}
|
|
||||||
func (e *ValueSet) SetEventMeta(eventMeta event.Meta) {
|
|
||||||
if e == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
e.eventMeta = eventMeta
|
|
||||||
}
|
|
||||||
func (e *ValueSet) MarshalBinary() ([]byte, error) {
|
|
||||||
return json.Marshal(e)
|
|
||||||
}
|
|
||||||
func (e *ValueSet) UnmarshalBinary(b []byte) error {
|
|
||||||
return json.Unmarshal(b, e)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestES(t *testing.T) {
|
func TestES(t *testing.T) {
|
||||||
@@ -78,26 +58,23 @@ func TestES(t *testing.T) {
|
|||||||
err := event.Register(ctx, &ValueSet{})
|
err := event.Register(ctx, &ValueSet{})
|
||||||
is.NoErr(err)
|
is.NoErr(err)
|
||||||
|
|
||||||
es.Init(ctx)
|
|
||||||
memstore.Init(ctx)
|
|
||||||
|
|
||||||
{
|
{
|
||||||
store, err := es.Open(ctx, "mem")
|
store, err := ev.Open(ctx, "mem")
|
||||||
is.True(errors.Is(err, es.ErrNoDriver))
|
is.True(errors.Is(err, ev.ErrNoDriver))
|
||||||
is.True(store.EventStream() == nil)
|
is.True(store.EventStream() == nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
_, err := es.Open(ctx, "bogo:")
|
_, err := ev.Open(ctx, "bogo:")
|
||||||
is.True(errors.Is(err, es.ErrNoDriver))
|
is.True(errors.Is(err, ev.ErrNoDriver))
|
||||||
}
|
}
|
||||||
|
|
||||||
store, err := es.Open(ctx, "mem:", streamer.New(ctx), projecter.New(ctx))
|
store, err := ev.Open(ctx, "mem:", streamer.New(ctx), projecter.New(ctx))
|
||||||
is.NoErr(err)
|
is.NoErr(err)
|
||||||
|
|
||||||
thing := &Thing{Name: "time"}
|
thing := &Thing{Name: "time"}
|
||||||
err = store.Load(ctx, thing)
|
err = store.Load(ctx, thing)
|
||||||
is.NoErr(err)
|
is.True(errors.Is(err, ev.ErrNotFound))
|
||||||
|
|
||||||
t.Log(thing.StreamVersion(), thing.Name, thing.Value)
|
t.Log(thing.StreamVersion(), thing.Name, thing.Value)
|
||||||
|
|
||||||
@@ -138,13 +115,10 @@ func TestESOperations(t *testing.T) {
|
|||||||
is := is.New(t)
|
is := is.New(t)
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
es.Init(ctx)
|
store, err := ev.Open(ctx, "mem:", streamer.New(ctx), projecter.New(ctx))
|
||||||
memstore.Init(ctx)
|
|
||||||
|
|
||||||
store, err := es.Open(ctx, "mem:", streamer.New(ctx), projecter.New(ctx))
|
|
||||||
is.NoErr(err)
|
is.NoErr(err)
|
||||||
|
|
||||||
thing, err := es.Create(ctx, store, "thing-1", func(ctx context.Context, agg *Thing) error {
|
thing, err := ev.Create(ctx, store, "thing-1", func(ctx context.Context, agg *Thing) error {
|
||||||
return agg.OnSetValue("foo")
|
return agg.OnSetValue("foo")
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -152,7 +126,7 @@ func TestESOperations(t *testing.T) {
|
|||||||
is.Equal(thing.Version(), uint64(1))
|
is.Equal(thing.Version(), uint64(1))
|
||||||
is.Equal(thing.Value, "foo")
|
is.Equal(thing.Value, "foo")
|
||||||
|
|
||||||
thing, err = es.Update(ctx, store, "thing-1", func(ctx context.Context, agg *Thing) error {
|
thing, err = ev.Update(ctx, store, "thing-1", func(ctx context.Context, agg *Thing) error {
|
||||||
return agg.OnSetValue("bar")
|
return agg.OnSetValue("bar")
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -160,7 +134,7 @@ func TestESOperations(t *testing.T) {
|
|||||||
is.Equal(thing.Version(), uint64(2))
|
is.Equal(thing.Version(), uint64(2))
|
||||||
is.Equal(thing.Value, "bar")
|
is.Equal(thing.Value, "bar")
|
||||||
|
|
||||||
thing, err = es.Upsert(ctx, store, "thing-2", func(ctx context.Context, agg *Thing) error {
|
thing, err = ev.Upsert(ctx, store, "thing-2", func(ctx context.Context, agg *Thing) error {
|
||||||
return agg.OnSetValue("bin")
|
return agg.OnSetValue("bin")
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -168,7 +142,7 @@ func TestESOperations(t *testing.T) {
|
|||||||
is.Equal(thing.Version(), uint64(1))
|
is.Equal(thing.Version(), uint64(1))
|
||||||
is.Equal(thing.Value, "bin")
|
is.Equal(thing.Value, "bin")
|
||||||
|
|
||||||
thing, err = es.Upsert(ctx, store, "thing-2", func(ctx context.Context, agg *Thing) error {
|
thing, err = ev.Upsert(ctx, store, "thing-2", func(ctx context.Context, agg *Thing) error {
|
||||||
return agg.OnSetValue("baz")
|
return agg.OnSetValue("baz")
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -184,6 +158,47 @@ func TestUnwrap(t *testing.T) {
|
|||||||
err := errors.New("foo")
|
err := errors.New("foo")
|
||||||
werr := fmt.Errorf("wrap: %w", err)
|
werr := fmt.Errorf("wrap: %w", err)
|
||||||
|
|
||||||
is.Equal(es.Unwrap(werr), err)
|
is.Equal(ev.Unwrap(werr), err)
|
||||||
is.Equal(es.Unwrap("test"), "")
|
is.Equal(ev.Unwrap("test"), "")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUnwrapProjector(t *testing.T) {
|
||||||
|
is := is.New(t)
|
||||||
|
|
||||||
|
ctx, stop := context.WithCancel(context.Background())
|
||||||
|
defer stop()
|
||||||
|
|
||||||
|
es, err := ev.Open(
|
||||||
|
ctx,
|
||||||
|
"mem:",
|
||||||
|
resolvelinks.New(),
|
||||||
|
streamer.New(ctx),
|
||||||
|
projecter.New(
|
||||||
|
ctx,
|
||||||
|
projecter.DefaultProjection,
|
||||||
|
peerfinder.Projector,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
is.NoErr(err)
|
||||||
|
|
||||||
|
stream := es.EventStream()
|
||||||
|
is.True(stream != nil)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMain(m *testing.M) {
|
||||||
|
ctx, stop := context.WithCancel(context.Background())
|
||||||
|
defer stop()
|
||||||
|
|
||||||
|
err := multierr.Combine(
|
||||||
|
ev.Init(ctx),
|
||||||
|
event.Init(ctx),
|
||||||
|
memstore.Init(ctx),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
m.Run()
|
||||||
}
|
}
|
||||||
138
go.mod
138
go.mod
@@ -1,86 +1,116 @@
|
|||||||
module github.com/sour-is/ev
|
module go.sour.is/ev
|
||||||
|
|
||||||
go 1.19
|
go 1.19
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/99designs/gqlgen v0.17.13
|
github.com/99designs/gqlgen v0.17.34
|
||||||
github.com/go-logr/stdr v1.2.2
|
github.com/go-logr/stdr v1.2.2 // indirect
|
||||||
github.com/gorilla/websocket v1.5.0
|
github.com/gorilla/websocket v1.5.0
|
||||||
github.com/logzio/logzio-go v1.0.6
|
github.com/ravilushqa/otelgqlgen v0.13.1 // indirect
|
||||||
github.com/ravilushqa/otelgqlgen v0.9.0
|
|
||||||
github.com/rs/cors v1.8.2
|
github.com/rs/cors v1.8.2
|
||||||
github.com/tidwall/wal v1.1.7
|
github.com/tidwall/wal v1.1.7
|
||||||
github.com/vektah/gqlparser/v2 v2.4.7
|
github.com/vektah/gqlparser/v2 v2.5.6
|
||||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.34.0
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.42.0 // indirect
|
||||||
go.opentelemetry.io/contrib/instrumentation/runtime v0.34.0
|
go.opentelemetry.io/contrib/instrumentation/runtime v0.42.0 // indirect
|
||||||
go.opentelemetry.io/otel v1.9.0
|
go.opentelemetry.io/otel v1.16.0
|
||||||
go.opentelemetry.io/otel/exporters/prometheus v0.31.0
|
go.opentelemetry.io/otel/exporters/prometheus v0.39.0 // indirect
|
||||||
go.opentelemetry.io/otel/metric v0.31.0
|
go.opentelemetry.io/otel/metric v1.16.0
|
||||||
go.opentelemetry.io/otel/sdk v1.9.0
|
go.opentelemetry.io/otel/sdk v1.16.0 // indirect
|
||||||
go.opentelemetry.io/otel/sdk/metric v0.31.0
|
go.opentelemetry.io/otel/sdk/metric v0.39.0 // indirect
|
||||||
go.opentelemetry.io/otel/trace v1.9.0
|
go.opentelemetry.io/otel/trace v1.16.0
|
||||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4
|
golang.org/x/sync v0.3.0 // indirect
|
||||||
|
)
|
||||||
|
|
||||||
|
require github.com/tj/go-semver v1.0.0
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/patrickmn/go-cache v2.1.0+incompatible
|
||||||
|
go.sour.is/pkg v0.0.2-0.20230726225143-5ad7fce0ac22
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/ScaleFT/sshkeys v0.0.0-20200327173127-6142f742bca5 // indirect
|
github.com/hashicorp/golang-lru/v2 v2.0.3 // indirect
|
||||||
|
github.com/julienschmidt/httprouter v1.3.0 // indirect
|
||||||
|
github.com/taigrr/go-colorhash v0.0.0-20220329080504-742db7f45eae // indirect
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
git.mills.io/prologic/msgbus v0.1.20 // indirect
|
||||||
|
github.com/ScaleFT/sshkeys v1.2.0 // indirect
|
||||||
github.com/agnivade/levenshtein v1.1.1 // indirect
|
github.com/agnivade/levenshtein v1.1.1 // indirect
|
||||||
github.com/beeker1121/goque v2.1.0+incompatible // indirect
|
github.com/andybalholm/brotli v1.0.4 // indirect
|
||||||
|
github.com/avast/retry-go v3.0.0+incompatible // indirect
|
||||||
github.com/beorn7/perks v1.0.1 // indirect
|
github.com/beorn7/perks v1.0.1 // indirect
|
||||||
github.com/cenkalti/backoff/v4 v4.1.3 // indirect
|
github.com/cenkalti/backoff/v4 v4.2.1 // indirect
|
||||||
github.com/cespare/xxhash/v2 v2.1.2 // indirect
|
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||||
|
github.com/cyphar/filepath-securejoin v0.2.3 // indirect
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/dchest/bcrypt_pbkdf v0.0.0-20150205184540-83f37f9c154a // indirect
|
github.com/dchest/bcrypt_pbkdf v0.0.0-20150205184540-83f37f9c154a // indirect
|
||||||
github.com/dchest/blake2b v1.0.0 // indirect
|
github.com/dchest/blake2b v1.0.0 // indirect
|
||||||
github.com/felixge/httpsnoop v1.0.3 // indirect
|
github.com/felixge/httpsnoop v1.0.3 // indirect
|
||||||
github.com/go-logr/logr v1.2.3 // indirect
|
github.com/go-logr/logr v1.2.4 // indirect
|
||||||
github.com/go-ole/go-ole v1.2.6 // indirect
|
github.com/golang/protobuf v1.5.3 // indirect
|
||||||
github.com/golang/protobuf v1.5.2 // indirect
|
github.com/google/go-cmp v0.5.9 // indirect
|
||||||
github.com/golang/snappy v0.0.4 // indirect
|
|
||||||
github.com/google/go-cmp v0.5.8 // indirect
|
|
||||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0 // indirect
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0 // indirect
|
||||||
github.com/hashicorp/golang-lru v0.5.4 // indirect
|
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||||
github.com/keybase/saltpack v0.0.0-20200430135328-e19b1910c0c5 // indirect
|
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||||
github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect
|
github.com/jpillora/backoff v1.0.0 // indirect
|
||||||
|
github.com/keybase/go-codec v0.0.0-20180928230036-164397562123 // indirect
|
||||||
|
github.com/keybase/saltpack v0.0.0-20221220231257-f6cce11cfd0f // indirect
|
||||||
|
github.com/klauspost/compress v1.15.15 // indirect
|
||||||
|
github.com/likexian/doh-go v0.6.4 // indirect
|
||||||
|
github.com/likexian/gokit v0.25.9 // indirect
|
||||||
|
github.com/logrusorgru/aurora v2.0.3+incompatible // indirect
|
||||||
|
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
|
||||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||||
github.com/onsi/ginkgo v1.14.0 // indirect
|
github.com/oklog/ulid v1.3.1
|
||||||
github.com/onsi/gomega v1.10.3 // indirect
|
github.com/petermattis/goid v0.0.0-20221215004737-a150e88a970d // indirect
|
||||||
github.com/pkg/errors v0.9.1 // indirect
|
github.com/pkg/errors v0.9.1 // indirect
|
||||||
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
|
github.com/posener/formatter v1.0.0 // indirect
|
||||||
github.com/prometheus/client_golang v1.12.2 // indirect
|
github.com/prometheus/client_golang v1.16.0 // indirect
|
||||||
github.com/prometheus/client_model v0.2.0 // indirect
|
github.com/prometheus/client_model v0.4.0 // indirect
|
||||||
github.com/prometheus/common v0.32.1 // indirect
|
github.com/prometheus/common v0.42.0 // indirect
|
||||||
github.com/prometheus/procfs v0.7.3 // indirect
|
github.com/prometheus/procfs v0.10.1 // indirect
|
||||||
github.com/shirou/gopsutil/v3 v3.22.3 // indirect
|
github.com/sasha-s/go-deadlock v0.3.1 // indirect
|
||||||
github.com/syndtr/goleveldb v1.0.0 // indirect
|
github.com/sirupsen/logrus v1.9.0 // indirect
|
||||||
|
github.com/timewasted/go-accept-headers v0.0.0-20130320203746-c78f304b1b09 // indirect
|
||||||
github.com/tyler-smith/go-bip39 v1.1.0 // indirect
|
github.com/tyler-smith/go-bip39 v1.1.0 // indirect
|
||||||
github.com/vmihailenco/msgpack/v4 v4.3.12 // indirect
|
github.com/vmihailenco/msgpack/v4 v4.3.12 // indirect
|
||||||
|
github.com/vmihailenco/msgpack/v5 v5.3.5 // indirect
|
||||||
github.com/vmihailenco/tagparser v0.1.2 // indirect
|
github.com/vmihailenco/tagparser v0.1.2 // indirect
|
||||||
github.com/yusufpapurcu/wmi v1.2.2 // indirect
|
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
|
||||||
go.opentelemetry.io/contrib v1.9.0 // indirect
|
github.com/writeas/go-strip-markdown/v2 v2.1.1 // indirect
|
||||||
go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.9.0 // indirect
|
go.mills.io/salty v0.0.0-20220322161301-ce2b9f6573fa // indirect
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.9.0 // indirect
|
go.opentelemetry.io/contrib v1.16.1 // indirect
|
||||||
go.opentelemetry.io/proto/otlp v0.18.0 // indirect
|
go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.16.0 // indirect
|
||||||
go.uber.org/atomic v1.9.0 // indirect
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.16.0 // indirect
|
||||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 // indirect
|
go.opentelemetry.io/proto/otlp v0.19.0 // indirect
|
||||||
golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f // indirect
|
go.yarn.social/lextwt v0.0.0-20221221200320-31bca76a2587 // indirect
|
||||||
golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9 // indirect
|
go.yarn.social/types v0.0.0-20221027173319-2d00e96a95c1 // indirect
|
||||||
golang.org/x/text v0.3.7 // indirect
|
golang.org/x/crypto v0.5.0 // indirect
|
||||||
|
golang.org/x/net v0.8.0 // indirect
|
||||||
|
golang.org/x/sys v0.8.0 // indirect
|
||||||
|
golang.org/x/text v0.9.0 // indirect
|
||||||
google.golang.org/appengine v1.6.7 // indirect
|
google.golang.org/appengine v1.6.7 // indirect
|
||||||
google.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1 // indirect
|
google.golang.org/genproto v0.0.0-20230306155012-7f2fa6fef1f4 // indirect
|
||||||
google.golang.org/grpc v1.46.2 // indirect
|
google.golang.org/grpc v1.55.0 // indirect
|
||||||
google.golang.org/protobuf v1.28.0 // indirect
|
google.golang.org/protobuf v1.30.0 // indirect
|
||||||
|
nhooyr.io/websocket v1.8.7 // indirect
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815
|
||||||
|
github.com/golang-jwt/jwt/v4 v4.5.0
|
||||||
github.com/keys-pub/keys v0.1.22
|
github.com/keys-pub/keys v0.1.22
|
||||||
github.com/matryer/is v1.4.0
|
github.com/matryer/is v1.4.1
|
||||||
github.com/oklog/ulid/v2 v2.1.0
|
github.com/oklog/ulid/v2 v2.1.0
|
||||||
github.com/tidwall/gjson v1.10.2 // indirect
|
github.com/tidwall/gjson v1.14.4 // indirect
|
||||||
github.com/tidwall/match v1.1.1 // indirect
|
github.com/tidwall/match v1.1.1 // indirect
|
||||||
github.com/tidwall/pretty v1.2.0 // indirect
|
github.com/tidwall/pretty v1.2.1 // indirect
|
||||||
github.com/tidwall/tinylru v1.1.0 // indirect
|
github.com/tidwall/tinylru v1.1.0 // indirect
|
||||||
gitlab.com/jamietanna/content-negotiation-go v0.2.0
|
gitlab.com/jamietanna/content-negotiation-go v0.2.0
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.9.0
|
go.mills.io/saltyim v0.0.0-20230128070719-15a64de82829
|
||||||
go.uber.org/multierr v1.8.0
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.16.0 // indirect
|
||||||
|
go.uber.org/multierr v1.11.0
|
||||||
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
)
|
)
|
||||||
|
|||||||
465
go.sum
465
go.sum
@@ -31,41 +31,43 @@ cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohl
|
|||||||
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
|
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=
|
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=
|
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||||
github.com/99designs/gqlgen v0.17.13 h1:ETUEqvRg5Zvr1lXtpoRdj026fzVay0ZlJPwI33qXLIw=
|
git.mills.io/prologic/bitcask v1.0.2 h1:Iy9x3mVVd1fB+SWY0LTmsSDPGbzMrd7zCZPKbsb/tDA=
|
||||||
github.com/99designs/gqlgen v0.17.13/go.mod h1:w1brbeOdqVyNJI553BGwtwdVcYu1LKeYE1opLWN9RgQ=
|
git.mills.io/prologic/msgbus v0.1.20 h1:RWhuRgLRHkaWKqgBgpQQhgtFXQBXJjO7Fardu6kcmUo=
|
||||||
|
git.mills.io/prologic/msgbus v0.1.20/go.mod h1:ZFnDXoFvujU18Hv45pk0isCWAGjpkHpY9+/WSLzKJek=
|
||||||
|
git.mills.io/prologic/observe v0.0.0-20210712230028-fc31c7aa2bd1 h1:e6ZyAOFGLZJZYL2galNvfuNMqeQDdilmQ5WRBXCNL5s=
|
||||||
|
git.mills.io/prologic/useragent v0.0.0-20210714100044-d249fe7921a0 h1:MojWEgZyiugUbgyjydrdSAkHlADnbt90dXyURRYFzQ4=
|
||||||
|
github.com/99designs/gqlgen v0.17.34 h1:5cS5/OKFguQt+Ws56uj9FlG2xm1IlcJWNF2jrMIKYFQ=
|
||||||
|
github.com/99designs/gqlgen v0.17.34/go.mod h1:Axcd3jIFHBVcqzixujJQr1wGqE+lGTpz6u4iZBZg1G8=
|
||||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||||
github.com/BurntSushi/toml v1.1.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
|
|
||||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||||
|
github.com/NYTimes/gziphandler v1.1.1 h1:ZUDjpQae29j0ryrS0u/B8HZfJBtBQHjqw2rQ2cqUQ3I=
|
||||||
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
||||||
github.com/ScaleFT/sshkeys v0.0.0-20200327173127-6142f742bca5 h1:VauE2GcJNZFun2Och6tIT2zJZK1v6jxALQDA9BIji/E=
|
|
||||||
github.com/ScaleFT/sshkeys v0.0.0-20200327173127-6142f742bca5/go.mod h1:gxOHeajFfvGQh/fxlC8oOKBe23xnnJTif00IFFbiT+o=
|
github.com/ScaleFT/sshkeys v0.0.0-20200327173127-6142f742bca5/go.mod h1:gxOHeajFfvGQh/fxlC8oOKBe23xnnJTif00IFFbiT+o=
|
||||||
github.com/agnivade/levenshtein v1.0.1/go.mod h1:CURSv5d9Uaml+FovSIICkLbAUZ9S4RqaHDIsdSBg7lM=
|
github.com/ScaleFT/sshkeys v1.2.0 h1:5BRp6rTVIhJzXT3VcUQrKgXR8zWA3sOsNeuyW15WUA8=
|
||||||
|
github.com/ScaleFT/sshkeys v1.2.0/go.mod h1:gxOHeajFfvGQh/fxlC8oOKBe23xnnJTif00IFFbiT+o=
|
||||||
|
github.com/abcum/lcp v0.0.0-20201209214815-7a3f3840be81 h1:uHogIJ9bXH75ZYrXnVShHIyywFiUZ7OOabwd9Sfd8rw=
|
||||||
github.com/agnivade/levenshtein v1.1.1 h1:QY8M92nrzkmr798gCo3kmMyqXFzdQVpxLlGPRBij0P8=
|
github.com/agnivade/levenshtein v1.1.1 h1:QY8M92nrzkmr798gCo3kmMyqXFzdQVpxLlGPRBij0P8=
|
||||||
github.com/agnivade/levenshtein v1.1.1/go.mod h1:veldBMzWxcCG2ZvUTKD2kJNRdCk5hVbJomOvKkmgYbo=
|
github.com/agnivade/levenshtein v1.1.1/go.mod h1:veldBMzWxcCG2ZvUTKD2kJNRdCk5hVbJomOvKkmgYbo=
|
||||||
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/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ=
|
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ=
|
||||||
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
|
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
|
||||||
|
github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY=
|
||||||
|
github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
|
||||||
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
|
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
|
||||||
github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig+0+Ap1h4unLjW6YQJpKZVmUzxsD4E/Q=
|
github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig+0+Ap1h4unLjW6YQJpKZVmUzxsD4E/Q=
|
||||||
github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE=
|
github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE=
|
||||||
github.com/beeker1121/goque v2.1.0+incompatible h1:m5pZ5b8nqzojS2DF2ioZphFYQUqGYsDORq6uefUItPM=
|
github.com/avast/retry-go v3.0.0+incompatible h1:4SOWQ7Qs+oroOTQOYnAHqelpCO0biHSxpiH9JdtuBj0=
|
||||||
github.com/beeker1121/goque v2.1.0+incompatible/go.mod h1:L6dOWBhDOnxUVQsb0wkLve0VCnt2xJW/MI8pdRX4ANw=
|
github.com/avast/retry-go v3.0.0+incompatible/go.mod h1:XtSnn+n/sHqQIpZ10K1qAevBhOOCWBLXXy3hyiqqBrY=
|
||||||
github.com/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT8A=
|
github.com/badgerodon/ioutil v0.0.0-20150716134133-06e58e34b867/go.mod h1:Ctq1YQi0dOq7QgBLZZ7p1Fr3IbAAqL/yMqDIHoe9WtE=
|
||||||
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 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||||
github.com/cenkalti/backoff/v4 v4.1.3 h1:cFAlzYUlVYDysBEH2T5hyJZMh3+5+WCBvSnK6Q8UtC4=
|
github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM=
|
||||||
github.com/cenkalti/backoff/v4 v4.1.3/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw=
|
github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM=
|
||||||
|
github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
||||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||||
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
|
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
|
||||||
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE=
|
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
|
||||||
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
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/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/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||||
@@ -76,10 +78,10 @@ github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XP
|
|||||||
github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
|
github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
|
||||||
github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
|
github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
|
||||||
github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
|
github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
|
||||||
github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
|
|
||||||
github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
|
github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
|
||||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||||
|
github.com/cyphar/filepath-securejoin v0.2.3 h1:YX6ebbZCZP7VkM3scTTokDgBL2TY741X51MTk3ycuNI=
|
||||||
|
github.com/cyphar/filepath-securejoin v0.2.3/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4=
|
||||||
github.com/danieljoos/wincred v1.1.0/go.mod h1:XYlo+eRTsVA9aHGp7NGjFkPla4m+DCL7hqDjlFjiygg=
|
github.com/danieljoos/wincred v1.1.0/go.mod h1:XYlo+eRTsVA9aHGp7NGjFkPla4m+DCL7hqDjlFjiygg=
|
||||||
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=
|
||||||
@@ -90,43 +92,55 @@ github.com/dchest/blake2b v1.0.0 h1:KK9LimVmE0MjRl9095XJmKqZ+iLxWATvlcpVFRtaw6s=
|
|||||||
github.com/dchest/blake2b v1.0.0/go.mod h1:U034kXgbJpCle2wSk5ybGIVhOSHCVLMDqOzcPEA0F7s=
|
github.com/dchest/blake2b v1.0.0/go.mod h1:U034kXgbJpCle2wSk5ybGIVhOSHCVLMDqOzcPEA0F7s=
|
||||||
github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48 h1:fRzb/w+pyskVMQ+UbP35JkH8yB7MYb4q/qhBarqZE6g=
|
github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48 h1:fRzb/w+pyskVMQ+UbP35JkH8yB7MYb4q/qhBarqZE6g=
|
||||||
github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA=
|
github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA=
|
||||||
|
github.com/disintegration/gift v1.2.1 h1:Y005a1X4Z7Uc+0gLpSAsKhWi4qLtsdEcMIbbdvdZ6pc=
|
||||||
|
github.com/disintegration/imageorient v0.0.0-20180920195336-8147d86e83ec h1:YrB6aVr9touOt75I9O1SiancmR2GMg45U9UYf0gtgWg=
|
||||||
|
github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815 h1:bWDMxwH3px2JBh6AyO7hdCn/PkvCZXii8TGj7sbtEbQ=
|
||||||
|
github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=
|
||||||
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
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.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/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
||||||
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
|
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
|
||||||
github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ=
|
github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ=
|
||||||
github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0=
|
github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0=
|
||||||
github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE=
|
|
||||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||||
github.com/felixge/httpsnoop v1.0.3 h1:s/nj+GCswXYzN5v2DpNMuMQYe+0DDwt5WVCU6CWBdXk=
|
github.com/felixge/httpsnoop v1.0.3 h1:s/nj+GCswXYzN5v2DpNMuMQYe+0DDwt5WVCU6CWBdXk=
|
||||||
github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||||
github.com/flynn/noise v1.0.0/go.mod h1:xbMo+0i6+IGbYdJhF31t2eR1BIU0CYc12+BNAKwUTag=
|
github.com/flynn/noise v1.0.0/go.mod h1:xbMo+0i6+IGbYdJhF31t2eR1BIU0CYc12+BNAKwUTag=
|
||||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
github.com/fsnotify/fsnotify v1.5.1 h1:mZcQUHVQUQWoPXXtuf9yuEXKudkV2sx1E06UadKWpgI=
|
||||||
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
|
|
||||||
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
|
||||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||||
|
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
||||||
|
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||||
|
github.com/gin-gonic/gin v1.6.3 h1:ahKqKTFpO5KTPHxWZjEdPScmYaGtLo8Y4DMHoEsnp14=
|
||||||
|
github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M=
|
||||||
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
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-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-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-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
|
||||||
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-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-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||||
github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0=
|
github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ=
|
||||||
github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||||
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
|
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||||
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q=
|
||||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
|
||||||
|
github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no=
|
||||||
|
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
|
||||||
|
github.com/go-playground/validator/v10 v10.2.0 h1:KgJ0snyC2R9VXYN2rneOtQcw5aHQB1Vv0sFl1UcHBOY=
|
||||||
|
github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI=
|
||||||
|
github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee h1:s+21KNqlpePfkah2I+gwHF8xmJWRjooY+5248k6m4A0=
|
||||||
|
github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo=
|
||||||
|
github.com/gobwas/pool v0.2.0 h1:QEmUOlnSjWtnpRGHF3SauEiOsy82Cup83Vf2LcMlnc8=
|
||||||
|
github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
|
||||||
|
github.com/gobwas/ws v1.0.2 h1:CoAavW/wd/kulfZmSIBt6p24n4j7tHgNVCjsfHVNUbo=
|
||||||
|
github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM=
|
||||||
github.com/godbus/dbus v4.1.0+incompatible/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw=
|
github.com/godbus/dbus v4.1.0+incompatible/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw=
|
||||||
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw=
|
||||||
|
github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg=
|
||||||
|
github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
|
||||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||||
github.com/golang/glog v1.0.0 h1:nfP3RFugxnNRyKgeWd4oI1nYvXpxrx8ck8ZrcizshdQ=
|
|
||||||
github.com/golang/glog v1.0.0/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4=
|
github.com/golang/glog v1.0.0/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4=
|
||||||
|
github.com/golang/glog v1.1.0 h1:/d3pCKDPWNnvIWe0vVUpNP32qc8U3PDVxySP/y360qE=
|
||||||
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
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-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/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||||
@@ -153,11 +167,9 @@ github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw
|
|||||||
github.com/golang/protobuf v1.4.3/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.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||||
github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM=
|
github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM=
|
||||||
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/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||||
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
|
||||||
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
|
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||||
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
|
||||||
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
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/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.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||||
@@ -167,12 +179,10 @@ github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
|
|||||||
github.com/google/go-cmp v0.4.1/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.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.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.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=
|
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||||
github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=
|
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
|
||||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
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 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/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
|
||||||
@@ -185,212 +195,231 @@ github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hf
|
|||||||
github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||||
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||||
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
|
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/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
|
||||||
|
github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||||
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
|
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
|
||||||
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||||
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
|
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
|
||||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0 h1:BZHcxBETFHIdVyhyEfOvn/RdU/QGdLI4y34qQGjGWO0=
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0 h1:BZHcxBETFHIdVyhyEfOvn/RdU/QGdLI4y34qQGjGWO0=
|
||||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0/go.mod h1:hgWBS7lorOAVIJEQMi4ZsPv9hVvWI6+ch50m39Pf2Ks=
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0/go.mod h1:hgWBS7lorOAVIJEQMi4ZsPv9hVvWI6+ch50m39Pf2Ks=
|
||||||
|
github.com/h2non/filetype v1.1.3 h1:FKkx9QbD7HR/zjK1Ia5XiBsq9zdLi5Kf3zGyFTAFkGg=
|
||||||
|
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||||
|
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
|
||||||
|
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||||
|
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
|
||||||
|
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
|
||||||
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
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/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||||
github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc=
|
github.com/hashicorp/golang-lru/v2 v2.0.3 h1:kmRrRLlInXvng0SmLxmQpQkpbYAvcXm7NPDrgxJa9mE=
|
||||||
github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
|
github.com/hashicorp/golang-lru/v2 v2.0.3/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
|
||||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
|
||||||
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||||
|
github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA=
|
||||||
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
|
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.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||||
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||||
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/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.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/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 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U=
|
||||||
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
|
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
|
||||||
github.com/kevinmbeaulieu/eq-go v1.0.0/go.mod h1:G3S8ajA56gKBZm4UB9AOyoOS37JO3roToPzKNM8dtdM=
|
github.com/justinas/nosurf v1.1.1 h1:92Aw44hjSK4MxJeMSyDa7jwuI9GR2J/JCQiaKvXXSlk=
|
||||||
|
github.com/keybase/go-codec v0.0.0-20180928230036-164397562123 h1:yg56lYPqh9suJepqxOMd/liFgU/x+maRPiB30JNYykM=
|
||||||
github.com/keybase/go-codec v0.0.0-20180928230036-164397562123/go.mod h1:r/eVVWCngg6TsFV/3HuS9sWhDkAzGG8mXhiuYA+Z/20=
|
github.com/keybase/go-codec v0.0.0-20180928230036-164397562123/go.mod h1:r/eVVWCngg6TsFV/3HuS9sWhDkAzGG8mXhiuYA+Z/20=
|
||||||
github.com/keybase/go-keychain v0.0.0-20201121013009-976c83ec27a6/go.mod h1:N83iQ9rnnzi2KZuTu+0xBcD1JNWn1jSN140ggAF7HeE=
|
github.com/keybase/go-keychain v0.0.0-20201121013009-976c83ec27a6/go.mod h1:N83iQ9rnnzi2KZuTu+0xBcD1JNWn1jSN140ggAF7HeE=
|
||||||
github.com/keybase/go.dbus v0.0.0-20200324223359-a94be52c0b03/go.mod h1:a8clEhrrGV/d76/f9r2I41BwANMihfZYV9C223vaxqE=
|
github.com/keybase/go.dbus v0.0.0-20200324223359-a94be52c0b03/go.mod h1:a8clEhrrGV/d76/f9r2I41BwANMihfZYV9C223vaxqE=
|
||||||
github.com/keybase/saltpack v0.0.0-20200430135328-e19b1910c0c5 h1:X6nYzCVURqxDv0GuyptaCcRFTXPM0rSGNUrTeQ2NKUQ=
|
|
||||||
github.com/keybase/saltpack v0.0.0-20200430135328-e19b1910c0c5/go.mod h1:FNSq71OhXv/Z1W9M37nnHxJVhXitc03z6qshCbAten8=
|
github.com/keybase/saltpack v0.0.0-20200430135328-e19b1910c0c5/go.mod h1:FNSq71OhXv/Z1W9M37nnHxJVhXitc03z6qshCbAten8=
|
||||||
|
github.com/keybase/saltpack v0.0.0-20221220231257-f6cce11cfd0f h1:q6W4z1Qbv9UfOPCpX/ymoiNVbT/+PuCVtOWHu2O5Pss=
|
||||||
|
github.com/keybase/saltpack v0.0.0-20221220231257-f6cce11cfd0f/go.mod h1:8hM5WwVH+oXJVaxqscISOuOjPHV20Htnl56CBLAPzMY=
|
||||||
github.com/keys-pub/keys v0.1.22 h1:bO0nx7c3HuC8dqjmjZ8njC8DzpuKWnOZQ5njaO5+A+o=
|
github.com/keys-pub/keys v0.1.22 h1:bO0nx7c3HuC8dqjmjZ8njC8DzpuKWnOZQ5njaO5+A+o=
|
||||||
github.com/keys-pub/keys v0.1.22/go.mod h1:+41yREqLkYyGfGf4OkhUn/ljwe/+kwhrlTq1/46Jj8c=
|
github.com/keys-pub/keys v0.1.22/go.mod h1:+41yREqLkYyGfGf4OkhUn/ljwe/+kwhrlTq1/46Jj8c=
|
||||||
github.com/keys-pub/secretservice v0.0.0-20200519003656-26e44b8df47f/go.mod h1:YRHMiVbZqh7u8xRm77CvwJNAZdDlNXwWvQ4DK0N9mYg=
|
github.com/keys-pub/secretservice v0.0.0-20200519003656-26e44b8df47f/go.mod h1:YRHMiVbZqh7u8xRm77CvwJNAZdDlNXwWvQ4DK0N9mYg=
|
||||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
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/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
|
||||||
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
github.com/klauspost/compress v1.15.15 h1:EF27CXIuDsYJ6mmvtBRlEuB2UVOqHG1tAXgZ7yIO+lw=
|
||||||
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
github.com/klauspost/compress v1.15.15/go.mod h1:ZcK2JAFqKOpnBlxcLsJzYfrS9X1akm9fHZNnD9+Vo/4=
|
||||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||||
github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
|
|
||||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||||
|
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
|
||||||
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
github.com/logrusorgru/aurora/v3 v3.0.0/go.mod h1:vsR12bk5grlLvLXAYrBsb5Oc/N+LxAlxggSjiwMnCUc=
|
github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y=
|
||||||
github.com/logzio/logzio-go v1.0.6 h1:BIVu5TWDZc0vlEkwSDjoxPlV/aMJV2LdM3k+CjdzFDg=
|
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
|
||||||
github.com/logzio/logzio-go v1.0.6/go.mod h1:ljlI3Zfi3hntJiHqCqWSUPT9cZP6yvDHUzDl5ZLGYRE=
|
github.com/likexian/doh-go v0.6.4 h1:UnTrIVAOwkBvKU6qOt2W3C5yC9/YO02UVPPcN26iZDY=
|
||||||
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
|
github.com/likexian/doh-go v0.6.4/go.mod h1:9jHpL/WPYmOM8+93RwXDf5TpZZwQjHrmIglXmjHpLlA=
|
||||||
github.com/matryer/is v1.4.0 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE=
|
github.com/likexian/gokit v0.21.11/go.mod h1:0WlTw7IPdiMtrwu0t5zrLM7XXik27Ey6MhUJHio2fVo=
|
||||||
github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
|
github.com/likexian/gokit v0.25.9 h1:rzSQ/dP7Qw+QUzSuWlrLF0AtZS3Di6uO5yWOKhx2Gk4=
|
||||||
github.com/matryer/moq v0.2.7/go.mod h1:kITsx543GOENm48TUAQyJ9+SAvFSr7iGQXPoth/VUBk=
|
github.com/likexian/gokit v0.25.9/go.mod h1:oDDqJUcnnF9uAKuw54F7s6oEG+OJ7eallfDW2dq0A/o=
|
||||||
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
|
github.com/logrusorgru/aurora v2.0.3+incompatible h1:tOpm7WcpBTn4fjmVfgpQq0EfczGlG91VSDkswnjF5A8=
|
||||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
github.com/logrusorgru/aurora v2.0.3+incompatible/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4=
|
||||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
github.com/matryer/is v1.4.1 h1:55ehd8zaGABKLXQUe2awZ99BD/PTc2ls+KV/dXphgEQ=
|
||||||
github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 h1:I0XW9+e1XWDxdcEniV4rQAIOPUGDq67JSCiRCgGCZLI=
|
github.com/matryer/is v1.4.1/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
|
||||||
github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
|
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||||
github.com/mitchellh/mapstructure v1.3.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
|
||||||
|
github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo=
|
||||||
|
github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
|
||||||
|
github.com/maxence-charriere/go-app/v9 v9.4.1 h1:uDrMIvjzkXwBjw5594i7ZqD5LY5iN7j1KeMImjWAYiw=
|
||||||
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/mlctrez/goapp-mdc v0.2.6 h1:/nSRAqC3xz+GFCd3Zl3yI87bBeEpJVXi5FSMnRighXo=
|
||||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/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 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 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||||
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/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||||
github.com/nxadm/tail v1.4.4 h1:DQuhQpB1tVlglWS2hLQ5OV6B5r8aGxSrPc5Qo6uTN78=
|
github.com/nullrocks/identicon v0.0.0-20180626043057-7875f45b0022 h1:Ys0rDzh8s4UMlGaDa1UTA0sfKgvF0hQZzTYX8ktjiDc=
|
||||||
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
|
github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4=
|
||||||
|
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
|
||||||
github.com/oklog/ulid/v2 v2.1.0 h1:+9lhoxAP56we25tyYETBBY1YLA2SaoLvUFgrP2miPJU=
|
github.com/oklog/ulid/v2 v2.1.0 h1:+9lhoxAP56we25tyYETBBY1YLA2SaoLvUFgrP2miPJU=
|
||||||
github.com/oklog/ulid/v2 v2.1.0/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ=
|
github.com/oklog/ulid/v2 v2.1.0/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ=
|
||||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
|
||||||
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
|
||||||
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
|
|
||||||
github.com/onsi/ginkgo v1.14.0 h1:2mOpI4JVVPBN+WQRa0WKH2eXR+Ey+uK4n7Zj0aYpIQA=
|
|
||||||
github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY=
|
|
||||||
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
|
||||||
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
|
|
||||||
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
|
|
||||||
github.com/onsi/gomega v1.10.3 h1:gph6h/qe9GSUw1NhH1gp+qb+h8rXD8Cy60Z32Qw3ELA=
|
|
||||||
github.com/onsi/gomega v1.10.3/go.mod h1:V9xEwhxec5O8UDM77eCW8vLymOMltsqPVYWrpDsH8xc=
|
|
||||||
github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o=
|
github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o=
|
||||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/petermattis/goid v0.0.0-20180202154549-b0b1615b78e5/go.mod h1:jvVRKCrJTQWu0XVbaOlby/2lO20uSCHEMzzplHXte1o=
|
||||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/petermattis/goid v0.0.0-20221215004737-a150e88a970d h1:htwtWgtQo8YS6JFWWi2DNgY0RwSGJ1ruMoxY6CUUclk=
|
||||||
|
github.com/petermattis/goid v0.0.0-20221215004737-a150e88a970d/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4=
|
||||||
|
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
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/plar/go-adaptive-radix-tree v1.0.4 h1:Ucd8R6RH2E7RW8ZtDKrsWyOD3paG2qqJO0I20WQ8oWQ=
|
||||||
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/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw=
|
github.com/posener/formatter v1.0.0 h1:TwXJq26f9ERTjCpZj8xEWj77WPWfX/nBgGx52Ap/gYM=
|
||||||
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
github.com/posener/formatter v1.0.0/go.mod h1:xrC89js6vw5dde/9yUKKU9MY5ivn980yX4VG7gYQTvU=
|
||||||
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
github.com/prometheus/client_golang v1.16.0 h1:yk/hx9hDbrGHovbci4BY+pRMfSuuat626eFsHb7tmT8=
|
||||||
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
|
github.com/prometheus/client_golang v1.16.0/go.mod h1:Zsulrv/L9oM40tJ7T815tM89lFEugiJ9HzIqaAx4LKc=
|
||||||
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.2 h1:51L9cDoUHVrXx4zWYlcLQIZ+d+VXHgqnYKkIuq4g/34=
|
|
||||||
github.com/prometheus/client_golang v1.12.2/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY=
|
|
||||||
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.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||||
github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M=
|
github.com/prometheus/client_model v0.4.0 h1:5lQXD3cAg1OXBf4Wq03gTrXHeaV0TQvGfUooCfx1yqY=
|
||||||
github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
github.com/prometheus/client_model v0.4.0/go.mod h1:oMQmHW1/JoDwqLtg57MGgP/Fb1CJEYF2imWWhWtMkYU=
|
||||||
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
|
github.com/prometheus/common v0.42.0 h1:EKsfXEYo4JpWMHH5cg+KOUWeuJSov1Id8zGR8eeI1YM=
|
||||||
github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=
|
github.com/prometheus/common v0.42.0/go.mod h1:xBwqVerjNdUDjgODMpudtOMwlOwf2SaTr1yjz4b7Zbc=
|
||||||
github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc=
|
github.com/prometheus/procfs v0.10.1 h1:kYK1Va/YMlutzCGazswoHKo//tZVlFpKYh+PymziUAg=
|
||||||
github.com/prometheus/common v0.32.1 h1:hWIdL3N2HoUx3B8j3YN9mWor0qhY/NlEKZEaXxuIRh4=
|
github.com/prometheus/procfs v0.10.1/go.mod h1:nwNm2aOCAYw8uTR/9bWRREkZFxAUcWzPHWJq+XBB/FM=
|
||||||
github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls=
|
github.com/ravilushqa/otelgqlgen v0.13.1 h1:V+zFE75iDd2/CSzy5kKnb+Fi09SsE5535wv9U2nUEFE=
|
||||||
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
github.com/ravilushqa/otelgqlgen v0.13.1/go.mod h1:ZIyWykK2paCuNi9k8gk5edcNSwDJuxZaW90vZXpafxw=
|
||||||
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
|
github.com/robfig/cron v1.2.0 h1:ZjScXvvxeQ63Dbyxy76Fj3AT3Ut0aKsyd2/tl3DTMuQ=
|
||||||
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 h1:4jVXhlkAyzOScmCkXBTOLRLTz8EeU+eyjrwB/EPq0VU=
|
|
||||||
github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
|
|
||||||
github.com/ravilushqa/otelgqlgen v0.9.0 h1:NgbrU9OHPKB/X6Ja618+SqPNls05M3T8aV1fkUiR9ow=
|
|
||||||
github.com/ravilushqa/otelgqlgen v0.9.0/go.mod h1:TqSvbt/7E23CHOOgL6G+42kCbhvxUpT/21tMsarq4Hk=
|
|
||||||
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
|
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
|
||||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||||
|
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
|
||||||
|
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
|
||||||
|
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
|
||||||
github.com/rs/cors v1.8.2 h1:KCooALfAYGs415Cwu5ABvv9n9509fSiG5SQJn/AQo4U=
|
github.com/rs/cors v1.8.2 h1:KCooALfAYGs415Cwu5ABvv9n9509fSiG5SQJn/AQo4U=
|
||||||
github.com/rs/cors v1.8.2/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU=
|
github.com/rs/cors v1.8.2/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU=
|
||||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
github.com/sasha-s/go-deadlock v0.3.1 h1:sqv7fDNShgjcaxkO0JNcOAlr8B9+cV5Ey/OB71efZx0=
|
||||||
github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
|
github.com/sasha-s/go-deadlock v0.3.1/go.mod h1:F73l+cr82YSh10GxyRI6qZiCgK64VaZjwesgfQ1/iLM=
|
||||||
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
|
github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8=
|
||||||
github.com/shirou/gopsutil/v3 v3.22.3 h1:UebRzEomgMpv61e3hgD1tGooqX5trFbdU/ehphbHd00=
|
github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I=
|
||||||
github.com/shirou/gopsutil/v3 v3.22.3/go.mod h1:D01hZJ4pVHPpCTZ3m3T2+wDF2YAGfd+H4ifUguaQzHM=
|
github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0=
|
||||||
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
|
||||||
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
|
|
||||||
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
||||||
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.3.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
|
github.com/stretchr/objx v0.3.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
|
||||||
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.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||||
|
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
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.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||||
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
|
github.com/taigrr/go-colorhash v0.0.0-20220329080504-742db7f45eae h1:RXzKJmV0lGvBpY8/43bJShhPYIssF7X18UVMs9KIgIQ=
|
||||||
github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE=
|
github.com/taigrr/go-colorhash v0.0.0-20220329080504-742db7f45eae/go.mod h1:1xBq06Fnn6nvsoWc+BCWwFvC0Zx04/FA3mpq937vlyI=
|
||||||
github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ=
|
|
||||||
github.com/tidwall/gjson v1.10.2 h1:APbLGOM0rrEkd8WBw9C24nllro4ajFuJu0Sc9hRz8Bo=
|
|
||||||
github.com/tidwall/gjson v1.10.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
github.com/tidwall/gjson v1.10.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||||
|
github.com/tidwall/gjson v1.14.4 h1:uo0p8EbA09J7RQaflQ1aBRffTR7xedD2bcIVSYxLnkM=
|
||||||
|
github.com/tidwall/gjson v1.14.4/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||||
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
||||||
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
|
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
|
||||||
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
|
|
||||||
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||||
|
github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
|
||||||
|
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||||
github.com/tidwall/tinylru v1.1.0 h1:XY6IUfzVTU9rpwdhKUF6nQdChgCdGjkMfLzbWyiau6I=
|
github.com/tidwall/tinylru v1.1.0 h1:XY6IUfzVTU9rpwdhKUF6nQdChgCdGjkMfLzbWyiau6I=
|
||||||
github.com/tidwall/tinylru v1.1.0/go.mod h1:3+bX+TJ2baOLMWTnlyNWHh4QMnFyARg2TLTQ6OFbzw8=
|
github.com/tidwall/tinylru v1.1.0/go.mod h1:3+bX+TJ2baOLMWTnlyNWHh4QMnFyARg2TLTQ6OFbzw8=
|
||||||
github.com/tidwall/wal v1.1.7 h1:emc1TRjIVsdKKSnpwGBAcsAGg0767SvUk8+ygx7Bb+4=
|
github.com/tidwall/wal v1.1.7 h1:emc1TRjIVsdKKSnpwGBAcsAGg0767SvUk8+ygx7Bb+4=
|
||||||
github.com/tidwall/wal v1.1.7/go.mod h1:r6lR1j27W9EPalgHiB7zLJDYu3mzW5BQP5KrzBpYY/E=
|
github.com/tidwall/wal v1.1.7/go.mod h1:r6lR1j27W9EPalgHiB7zLJDYu3mzW5BQP5KrzBpYY/E=
|
||||||
github.com/tklauser/go-sysconf v0.3.10/go.mod h1:C8XykCvCb+Gn0oNCWPIlcb0RuglQTYaQ2hGm7jmxEFk=
|
github.com/timewasted/go-accept-headers v0.0.0-20130320203746-c78f304b1b09 h1:QVxbx5l/0pzciWYOynixQMtUhPYC3YKD6EcUlOsgGqw=
|
||||||
github.com/tklauser/numcpus v0.4.0/go.mod h1:1+UI3pD8NW14VMwdgJNJ1ESk2UnwhAnz5hMwiKKqXCQ=
|
github.com/timewasted/go-accept-headers v0.0.0-20130320203746-c78f304b1b09/go.mod h1:Uy/Rnv5WKuOO+PuDhuYLEpUiiKIZtss3z519uk67aF0=
|
||||||
|
github.com/tj/assert v0.0.0-20190920132354-ee03d75cd160 h1:NSWpaDaurcAJY7PkL8Xt0PhZE7qpvbZl5ljd8r6U0bI=
|
||||||
|
github.com/tj/assert v0.0.0-20190920132354-ee03d75cd160/go.mod h1:mZ9/Rh9oLWpLLDRpvE+3b7gP/C2YyLFYxNmcLnPTMe0=
|
||||||
|
github.com/tj/go-semver v1.0.0 h1:vpn6Jmn6Hi3QSmrP1PzYcqScop9IZiGCVOSn18wzu8w=
|
||||||
|
github.com/tj/go-semver v1.0.0/go.mod h1:YZuwVc013rh7KDV0k6tPbWrFeEHBHcp8amfJL+nHzjM=
|
||||||
github.com/tyler-smith/go-bip39 v1.1.0 h1:5eUemwrMargf3BSLRRCalXT93Ns6pQJIjYQN2nyfOP8=
|
github.com/tyler-smith/go-bip39 v1.1.0 h1:5eUemwrMargf3BSLRRCalXT93Ns6pQJIjYQN2nyfOP8=
|
||||||
github.com/tyler-smith/go-bip39 v1.1.0/go.mod h1:gUYDtqQw1JS3ZJ8UWVcGTGqqr6YIN3CWg+kkNaLt55U=
|
github.com/tyler-smith/go-bip39 v1.1.0/go.mod h1:gUYDtqQw1JS3ZJ8UWVcGTGqqr6YIN3CWg+kkNaLt55U=
|
||||||
github.com/urfave/cli/v2 v2.8.1/go.mod h1:Z41J9TPoffeoqP0Iza0YbAhGvymRdZAd2uPmZ5JxRdY=
|
github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo=
|
||||||
github.com/vektah/gqlparser/v2 v2.4.6/go.mod h1:flJWIR04IMQPGz+BXLrORkrARBxv/rtyIAFvd/MceW0=
|
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
|
||||||
github.com/vektah/gqlparser/v2 v2.4.7 h1:yub2WLoSIr+chP1zMv6bjrsgTasfubxGZJeC8ISEpgE=
|
github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs=
|
||||||
github.com/vektah/gqlparser/v2 v2.4.7/go.mod h1:flJWIR04IMQPGz+BXLrORkrARBxv/rtyIAFvd/MceW0=
|
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
|
||||||
|
github.com/unrolled/logger v0.0.0-20201216141554-31a3694fe979 h1:47+K4wN0S8L3fUwgZtPEBIfNqtAE3tUvBfvHVZJAXfg=
|
||||||
|
github.com/unrolled/render v1.4.1 h1:VdpMc2YkAOWzbmC/P2yoHhRDXgsaCQHcTJ1KK6SNCA4=
|
||||||
|
github.com/vektah/gqlparser/v2 v2.5.6 h1:Ou14T0N1s191eRMZ1gARVqohcbe1e8FrcONScsq8cRU=
|
||||||
|
github.com/vektah/gqlparser/v2 v2.5.6/go.mod h1:z8xXUff237NntSuH8mLFijZ+1tjV1swDbpDqjJmk6ME=
|
||||||
github.com/vmihailenco/msgpack/v4 v4.3.12 h1:07s4sz9IReOgdikxLTKNbBdqDMLsjPKXwvCazn8G65U=
|
github.com/vmihailenco/msgpack/v4 v4.3.12 h1:07s4sz9IReOgdikxLTKNbBdqDMLsjPKXwvCazn8G65U=
|
||||||
github.com/vmihailenco/msgpack/v4 v4.3.12/go.mod h1:gborTTJjAo/GWTqqRjrLCn9pgNN+NXzzngzBKDPIqw4=
|
github.com/vmihailenco/msgpack/v4 v4.3.12/go.mod h1:gborTTJjAo/GWTqqRjrLCn9pgNN+NXzzngzBKDPIqw4=
|
||||||
|
github.com/vmihailenco/msgpack/v5 v5.3.5 h1:5gO0H1iULLWGhs2H5tbAHIZTV8/cYafcFOr9znI5mJU=
|
||||||
|
github.com/vmihailenco/msgpack/v5 v5.3.5/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc=
|
||||||
github.com/vmihailenco/tagparser v0.1.1/go.mod h1:OeAg3pn3UbLjkWt+rN9oFYB6u/cQgqMEUPoW2WPyhdI=
|
github.com/vmihailenco/tagparser v0.1.1/go.mod h1:OeAg3pn3UbLjkWt+rN9oFYB6u/cQgqMEUPoW2WPyhdI=
|
||||||
github.com/vmihailenco/tagparser v0.1.2 h1:gnjoVuB/kljJ5wICEEOpx98oXMWPLj22G67Vbd1qPqc=
|
github.com/vmihailenco/tagparser v0.1.2 h1:gnjoVuB/kljJ5wICEEOpx98oXMWPLj22G67Vbd1qPqc=
|
||||||
github.com/vmihailenco/tagparser v0.1.2/go.mod h1:OeAg3pn3UbLjkWt+rN9oFYB6u/cQgqMEUPoW2WPyhdI=
|
github.com/vmihailenco/tagparser v0.1.2/go.mod h1:OeAg3pn3UbLjkWt+rN9oFYB6u/cQgqMEUPoW2WPyhdI=
|
||||||
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=
|
github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g=
|
||||||
|
github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
|
||||||
|
github.com/writeas/go-strip-markdown/v2 v2.1.1 h1:hAxUM21Uhznf/FnbVGiJciqzska6iLei22Ijc3q2e28=
|
||||||
|
github.com/writeas/go-strip-markdown/v2 v2.1.1/go.mod h1:UvvgPJgn1vvN8nWuE5e7v/+qmDu3BSVnKAB6Gl7hFzA=
|
||||||
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
github.com/yusufpapurcu/wmi v1.2.2 h1:KBNDSne4vP5mbSWnJbO+51IMOXJB67QiYCSBrubbPRg=
|
|
||||||
github.com/yusufpapurcu/wmi v1.2.2/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
|
||||||
gitlab.com/jamietanna/content-negotiation-go v0.2.0 h1:vT0OLEPQ6DYRG3/1F7joXSNjVQHGivJ6+JzODlJfjWw=
|
gitlab.com/jamietanna/content-negotiation-go v0.2.0 h1:vT0OLEPQ6DYRG3/1F7joXSNjVQHGivJ6+JzODlJfjWw=
|
||||||
gitlab.com/jamietanna/content-negotiation-go v0.2.0/go.mod h1:n4ZZ8/X5TstnjYRnjEtR/fC7MCTe+aRKM7PQlLBH3PQ=
|
gitlab.com/jamietanna/content-negotiation-go v0.2.0/go.mod h1:n4ZZ8/X5TstnjYRnjEtR/fC7MCTe+aRKM7PQlLBH3PQ=
|
||||||
|
go.mills.io/salty v0.0.0-20220322161301-ce2b9f6573fa h1:KBxzYJMWP7MXd72RgqsMCGOSEqV6aaDDSdSb8usJCzQ=
|
||||||
|
go.mills.io/salty v0.0.0-20220322161301-ce2b9f6573fa/go.mod h1:bQ9yvK7wwThD4tzoioJq/YAuwYOB2XA9tAUHIYtjre8=
|
||||||
|
go.mills.io/saltyim v0.0.0-20230128070719-15a64de82829 h1:rzgfYKbCt8N0vVD3CAMoPwtvj4Zr1l3Cyl3rjN4+kHg=
|
||||||
|
go.mills.io/saltyim v0.0.0-20230128070719-15a64de82829/go.mod h1:ldLxf9b9mfq3QMHXenH42tvkUGJ0UlSQ/QUoTKvefs8=
|
||||||
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
|
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.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
|
||||||
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
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.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||||
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||||
go.opentelemetry.io/contrib v1.9.0 h1:2KAoCVu4OMI9TYoSWvcV7+UbbIPOi4623S77nV+M/Ks=
|
go.opentelemetry.io/contrib v1.16.1 h1:EpASvVyGx6/ZTlmXzxYfTMZxHROelCeXXa2uLiwltcs=
|
||||||
go.opentelemetry.io/contrib v1.9.0/go.mod h1:yp0N4+hnpWCpnMzs6T6WbD9Amfg7reEZsS0jAd/5M2Q=
|
go.opentelemetry.io/contrib v1.16.1/go.mod h1:gIzjwWFoGazJmtCaDgViqOSJPde2mCWzv60o0bWPcZs=
|
||||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.34.0 h1:9NkMW03wwEzPtP/KciZ4Ozu/Uz5ZA7kfqXJIObnrjGU=
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.42.0 h1:pginetY7+onl4qN1vl0xW/V/v6OBZ0vVdH+esuJgvmM=
|
||||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.34.0/go.mod h1:548ZsYzmT4PL4zWKRd8q/N4z0Wxzn/ZxUE+lkEpwWQA=
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.42.0/go.mod h1:XiYsayHc36K3EByOO6nbAXnAWbrUxdjUROCEeeROOH8=
|
||||||
go.opentelemetry.io/contrib/instrumentation/runtime v0.34.0 h1:zt4RDodWkgiHk8tyUmFOjFoOOfyGH7vwIbUzKP6CCh8=
|
go.opentelemetry.io/contrib/instrumentation/runtime v0.42.0 h1:EbmAUG9hEAMXyfWEasIt2kmh/WmXUznUksChApTgBGc=
|
||||||
go.opentelemetry.io/contrib/instrumentation/runtime v0.34.0/go.mod h1:5wIoZE96WbcQVU3D6UF/ukRfFQXbB6OYgeWi9CjHa90=
|
go.opentelemetry.io/contrib/instrumentation/runtime v0.42.0/go.mod h1:rD9feqRYP24P14t5kmhNMqsqm1jvKmpx2H2rKVw52V8=
|
||||||
go.opentelemetry.io/otel v1.9.0 h1:8WZNQFIB2a71LnANS9JeyidJKKGOOremcUtb/OtHISw=
|
go.opentelemetry.io/otel v1.16.0 h1:Z7GVAX/UkAXPKsy94IU+i6thsQS4nb7LviLpnaNeW8s=
|
||||||
go.opentelemetry.io/otel v1.9.0/go.mod h1:np4EoPGzoPs3O67xUVNoPPcmSvsfOxNlNA4F4AC+0Eo=
|
go.opentelemetry.io/otel v1.16.0/go.mod h1:vl0h9NUa1D5s1nv3A5vZOYWn8av4K8Ml6JDeHrT/bx4=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.9.0 h1:ggqApEjDKczicksfvZUCxuvoyDmR6Sbm56LwiK8DVR0=
|
go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.16.0 h1:t4ZwRPU+emrcvM2e9DHd0Fsf0JTPVcbfa/BhTDF03d0=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.9.0/go.mod h1:78XhIg8Ht9vR4tbLNUhXsiOnE2HOuSeKAiAcoVQEpOY=
|
go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.16.0/go.mod h1:vLarbg68dH2Wa77g71zmKQqlQ8+8Rq3GRG31uc0WcWI=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.9.0 h1:NN90Cuna0CnBg8YNu1Q0V35i2E8LDByFOwHRCq/ZP9I=
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.16.0 h1:cbsD4cUcviQGXdw8+bo5x2wazq10SKz8hEbtCRPcU78=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.9.0/go.mod h1:0EsCXjZAiiZGnLdEUXM9YjCKuuLZMYyglh2QDXcYKVA=
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.16.0/go.mod h1:JgXSGah17croqhJfhByOLVY719k1emAXC8MVhCIJlRs=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.9.0 h1:FAF9l8Wjxi9Ad2k/vLTfHZyzXYX72C62wBGpV3G6AIo=
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.16.0 h1:iqjq9LAB8aK++sKVcELezzn655JnBNdsDhghU4G/So8=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.9.0/go.mod h1:smUdtylgc0YQiUr2PuifS4hBXhAS5xtR6WQhxP1wiNA=
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.16.0/go.mod h1:hGXzO5bhhSHZnKvrDaXB82Y9DRFour0Nz/KrBh7reWw=
|
||||||
go.opentelemetry.io/otel/exporters/prometheus v0.31.0 h1:jwtnOGBM8dIty5AVZ+9ZCzZexCea3aVKmUfZAQcHqxs=
|
go.opentelemetry.io/otel/exporters/prometheus v0.39.0 h1:whAaiHxOatgtKd+w0dOi//1KUxj3KoPINZdtDaDj3IA=
|
||||||
go.opentelemetry.io/otel/exporters/prometheus v0.31.0/go.mod h1:QarXIB8L79IwIPoNgG3A6zNvBgVmcppeFogV1d8612s=
|
go.opentelemetry.io/otel/exporters/prometheus v0.39.0/go.mod h1:4jo5Q4CROlCpSPsXLhymi+LYrDXd2ObU5wbKayfZs7Y=
|
||||||
go.opentelemetry.io/otel/metric v0.31.0 h1:6SiklT+gfWAwWUR0meEMxQBtihpiEs4c+vL9spDTqUs=
|
go.opentelemetry.io/otel/metric v1.16.0 h1:RbrpwVG1Hfv85LgnZ7+txXioPDoh6EdbZHo26Q3hqOo=
|
||||||
go.opentelemetry.io/otel/metric v0.31.0/go.mod h1:ohmwj9KTSIeBnDBm/ZwH2PSZxZzoOaG2xZeekTRzL5A=
|
go.opentelemetry.io/otel/metric v1.16.0/go.mod h1:QE47cpOmkwipPiefDwo2wDzwJrlfxxNYodqc4xnGCo4=
|
||||||
go.opentelemetry.io/otel/sdk v1.9.0 h1:LNXp1vrr83fNXTHgU8eO89mhzxb/bbWAsHG6fNf3qWo=
|
go.opentelemetry.io/otel/sdk v1.16.0 h1:Z1Ok1YsijYL0CSJpHt4cS3wDDh7p572grzNrBMiMWgE=
|
||||||
go.opentelemetry.io/otel/sdk v1.9.0/go.mod h1:AEZc8nt5bd2F7BC24J5R0mrjYnpEgYHyTcM/vrSple4=
|
go.opentelemetry.io/otel/sdk v1.16.0/go.mod h1:tMsIuKXuuIWPBAOrH+eHtvhTL+SntFtXF9QD68aP6p4=
|
||||||
go.opentelemetry.io/otel/sdk/metric v0.31.0 h1:2sZx4R43ZMhJdteKAlKoHvRgrMp53V1aRxvEf5lCq8Q=
|
go.opentelemetry.io/otel/sdk/metric v0.39.0 h1:Kun8i1eYf48kHH83RucG93ffz0zGV1sh46FAScOTuDI=
|
||||||
go.opentelemetry.io/otel/sdk/metric v0.31.0/go.mod h1:fl0SmNnX9mN9xgU6OLYLMBMrNAsaZQi7qBwprwO3abk=
|
go.opentelemetry.io/otel/sdk/metric v0.39.0/go.mod h1:piDIRgjcK7u0HCL5pCA4e74qpK/jk3NiUoAHATVAmiI=
|
||||||
go.opentelemetry.io/otel/trace v1.9.0 h1:oZaCNJUjWcg60VXWee8lJKlqhPbXAPB51URuR47pQYc=
|
go.opentelemetry.io/otel/trace v1.16.0 h1:8JRpaObFoW0pxuVPapkgH8UhHQj+bJW8jJsCZEu5MQs=
|
||||||
go.opentelemetry.io/otel/trace v1.9.0/go.mod h1:2737Q0MuG8q1uILYm2YYVkAyLtOofiTNGg6VODnOiPo=
|
go.opentelemetry.io/otel/trace v1.16.0/go.mod h1:Yt9vYq1SdNz3xdjZZK7wcXv1qv2pwLkqr2QVwea0ef0=
|
||||||
go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
|
go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
|
||||||
go.opentelemetry.io/proto/otlp v0.18.0 h1:W5hyXNComRa23tGpKwG+FRAc4rfF6ZUg1JReK+QHS80=
|
go.opentelemetry.io/proto/otlp v0.19.0 h1:IVN6GR+mhC4s5yfcTbmzHYODqvWAp3ZedA2SJPI1Nnw=
|
||||||
go.opentelemetry.io/proto/otlp v0.18.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U=
|
go.opentelemetry.io/proto/otlp v0.19.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U=
|
||||||
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
go.sour.is/pkg v0.0.1 h1:wajjul3FNTljfcsNqNHsnelyVvsq8buuKrfxncJDuu0=
|
||||||
go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
|
go.sour.is/pkg v0.0.1/go.mod h1:jOdxxKILf+kXQmk1cG3sN4Ogxk8C4/14mxhy/QEJjv4=
|
||||||
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
go.sour.is/pkg v0.0.2-0.20230726225143-5ad7fce0ac22 h1:KRe6xPl7ryo+l0SLN9u4I/jguHGvIx70/Ru7CLiT64o=
|
||||||
go.uber.org/multierr v1.8.0 h1:dg6GjLku4EH+249NNmoIciG9N/jURbDG+pFlTkhzIC8=
|
go.sour.is/pkg v0.0.2-0.20230726225143-5ad7fce0ac22/go.mod h1:jOdxxKILf+kXQmk1cG3sN4Ogxk8C4/14mxhy/QEJjv4=
|
||||||
go.uber.org/multierr v1.8.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak=
|
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||||
|
go.yarn.social/lextwt v0.0.0-20221221200320-31bca76a2587 h1:MuJTXSxPUNxqzT+q+fulCjkivDeC5jjZ7TSsf/f/24M=
|
||||||
|
go.yarn.social/lextwt v0.0.0-20221221200320-31bca76a2587/go.mod h1:XcveSVLWxkBBW32VEAnewBcnEYw8O2Vb/xqJmXC54Hs=
|
||||||
|
go.yarn.social/types v0.0.0-20221025190911-9524f5b4a743/go.mod h1:XN+G4HprNn/Gp7OF2zveqsCRSWFCHtOaIRh2GlcK+U4=
|
||||||
|
go.yarn.social/types v0.0.0-20221027173319-2d00e96a95c1 h1:H3W7HmWrVpHs7WcncxifE7lr9JUApKPGqZTWmIaU5F4=
|
||||||
|
go.yarn.social/types v0.0.0-20221027173319-2d00e96a95c1/go.mod h1:+xnDkQ0T0S8emxWIsvxlCAoyF8gBaj0q81hr/VrKc0c=
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
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-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-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
@@ -399,8 +428,10 @@ golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPh
|
|||||||
golang.org/x/crypto v0.0.0-20200427165652-729f1e841bcc/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
golang.org/x/crypto v0.0.0-20200427165652-729f1e841bcc/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
|
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
|
||||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 h1:7I4JAnoQBe7ZtJcBaYHi5UtiO8tQHbUSXxL+pnGRANg=
|
|
||||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
|
golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw=
|
||||||
|
golang.org/x/crypto v0.5.0 h1:U/0M97KRkSFvyD/3FSmdP5W5swImpNgle/EHFhOsQPE=
|
||||||
|
golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU=
|
||||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
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-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-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||||
@@ -411,6 +442,7 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0
|
|||||||
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/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-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-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
|
||||||
|
golang.org/x/exp v0.0.0-20220328175248-053ad81199eb h1:pC9Okm6BVmxEw76PUu0XUbOTQ92JX11hfvqTjAV3qxM=
|
||||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
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/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-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||||
@@ -431,12 +463,9 @@ golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzB
|
|||||||
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/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.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
|
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||||
golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY=
|
|
||||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
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-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/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-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-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-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
@@ -444,10 +473,10 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn
|
|||||||
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/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-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-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-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-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-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20191116160921-f9c825593386/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-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-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-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
@@ -458,25 +487,22 @@ golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/
|
|||||||
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/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-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-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||||
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/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-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-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-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-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||||
golang.org/x/net v0.0.0-20201006153459-a7d1128ccaa0/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
|
||||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
|
||||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
golang.org/x/net v0.0.0-20210326060303-6b1517762897/go.mod h1:uSPa2vr4CLtc/ILN5odXGNXS6mhrKVzTaCXzk9m6W3k=
|
golang.org/x/net v0.0.0-20210326060303-6b1517762897/go.mod h1:uSPa2vr4CLtc/ILN5odXGNXS6mhrKVzTaCXzk9m6W3k=
|
||||||
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
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-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||||
golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f h1:OfiFi4JbukWwe3lzw+xunroH1mnC1e2Gy5cxNJApiSY=
|
golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
|
||||||
golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ=
|
||||||
|
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
|
||||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
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-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-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-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-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-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
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-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
@@ -486,32 +512,24 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ
|
|||||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/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-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-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.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 h1:uVc8UZUe6tr40fFVnUP5Oj+veunVezqYl9z7DYw9xzw=
|
|
||||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
|
||||||
|
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
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-20180909124046-d0be0721c37e/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-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-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-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-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-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-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-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-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/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-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-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/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-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-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
@@ -523,41 +541,34 @@ golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7w
|
|||||||
golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/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-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-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20200523222454-059865788121/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-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20210324051608-47abb6519492/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20210324051608-47abb6519492/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/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-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-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU=
|
||||||
golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9 h1:nhht2DYV/Sn3qOayu8lM+cU1ii9sTLUeBQwQQfUHtrs=
|
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/term v0.0.0-20210317153231-de623e64d2a6 h1:EC6+IGYTjPpRfv9a2b/6Puw0W+hLtAhkV1tPsXhutqs=
|
|
||||||
golang.org/x/term v0.0.0-20210317153231-de623e64d2a6/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20210317153231-de623e64d2a6/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
|
golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
|
golang.org/x/term v0.6.0 h1:clScbb1cHjoCkyRbWwBEUZ5H/tIFu5TAXIqaZD0Gcjw=
|
||||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
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.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.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.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.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
|
||||||
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
|
|
||||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
|
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
|
golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
|
||||||
|
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
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-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/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
@@ -601,12 +612,10 @@ golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roY
|
|||||||
golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
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-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||||
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||||
golang.org/x/tools v0.1.9/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU=
|
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||||
golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E=
|
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
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-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-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
|
|
||||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/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.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.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
|
||||||
@@ -662,8 +671,9 @@ google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7Fc
|
|||||||
google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
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-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/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||||
google.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1 h1:b9mVrqYfq3P4bCdaLg1qtBnPzUYgglsIdjZkL/fQVOE=
|
|
||||||
google.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
|
google.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
|
||||||
|
google.golang.org/genproto v0.0.0-20230306155012-7f2fa6fef1f4 h1:DdoeryqhaXp1LtT/emMP1BRJPHHKFi5akj/nbx/zNTA=
|
||||||
|
google.golang.org/genproto v0.0.0-20230306155012-7f2fa6fef1f4/go.mod h1:NWraEVixdDnqcqQ30jipen1STv2r/n24Wb7twVTGR4s=
|
||||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
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.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
||||||
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
||||||
@@ -680,8 +690,8 @@ google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTp
|
|||||||
google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
|
google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
|
||||||
google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=
|
google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=
|
||||||
google.golang.org/grpc v1.42.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU=
|
google.golang.org/grpc v1.42.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU=
|
||||||
google.golang.org/grpc v1.46.2 h1:u+MLGgVf7vRdjEYZ8wDFhAVNmhkbJ5hmrA1LMWK1CAQ=
|
google.golang.org/grpc v1.55.0 h1:3Oj82/tFSCeUrRTg/5E/7d/W5A1tj6Ky1ABAuZuv5ag=
|
||||||
google.golang.org/grpc v1.46.2/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk=
|
google.golang.org/grpc v1.55.0/go.mod h1:iYEXKGkEBhg1PjZQvoYEVPTDkHo1/bjTnfwTeGONTY8=
|
||||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
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-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 v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||||
@@ -695,30 +705,23 @@ google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlba
|
|||||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
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.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||||
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||||
google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw=
|
google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
|
||||||
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||||
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-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-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 h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
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/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
|
||||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
|
||||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
|
||||||
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.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.3/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.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.8/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 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
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.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/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=
|
||||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
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-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-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||||
@@ -726,6 +729,8 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh
|
|||||||
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
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.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
||||||
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
||||||
|
nhooyr.io/websocket v1.8.7 h1:usjR2uOr/zjjkVMy0lW+PPohFok7PCow5sDjLgX4P4g=
|
||||||
|
nhooyr.io/websocket v1.8.7/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0=
|
||||||
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
|
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/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
|
||||||
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
|
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
|
||||||
|
|||||||
31
gqlgen.yml
31
gqlgen.yml
@@ -1,48 +1,23 @@
|
|||||||
# Where are all the schema files located? globs are supported eg src/**/*.graphqls
|
|
||||||
schema:
|
schema:
|
||||||
- pkg/*/*.graphqls
|
- pkg/*/*.graphqls
|
||||||
- app/*/*.graphqls
|
- app/*/*.graphqls
|
||||||
|
|
||||||
# Where should the generated server code go?
|
|
||||||
exec:
|
exec:
|
||||||
filename: internal/graph/generated/generated.go
|
filename: internal/graph/generated/generated.go
|
||||||
package: generated
|
package: generated
|
||||||
|
|
||||||
# Uncomment to enable federation
|
|
||||||
federation:
|
federation:
|
||||||
filename: internal/graph/generated/federation.go
|
filename: internal/graph/generated/federation.go
|
||||||
package: generated
|
package: generated
|
||||||
|
|
||||||
# Where should any generated models go?
|
|
||||||
model:
|
model:
|
||||||
filename: internal/graph/model/models_gen.go
|
filename: internal/graph/model/models_gen.go
|
||||||
package: model
|
package: model
|
||||||
|
|
||||||
# Where should the resolver implementations go?
|
resolver:
|
||||||
# resolver:
|
filename: internal/graph/resolver/resolver.go
|
||||||
# layout: follow-schema
|
package: resolver
|
||||||
# dir: internal/graph
|
|
||||||
# package: graph
|
|
||||||
|
|
||||||
# Optional: turn on use `gqlgen:"fieldName"` tags in your models
|
|
||||||
# struct_tag: json
|
|
||||||
|
|
||||||
# Optional: turn on to use []Thing instead of []*Thing
|
|
||||||
# omit_slice_element_pointers: false
|
|
||||||
|
|
||||||
# Optional: set to speed up generation time by not performing a final validation pass.
|
|
||||||
# skip_validation: true
|
|
||||||
|
|
||||||
# gqlgen will search for any type names in the schema in these go packages
|
|
||||||
# if they match it will use them, otherwise it will generate them.
|
|
||||||
# autobind:
|
|
||||||
# - "github.com/sour-is/ev/pkg/gql"
|
|
||||||
|
|
||||||
# This section declares type mapping between the GraphQL and go type systems
|
|
||||||
#
|
|
||||||
# The first line in each type will be used as defaults for resolver arguments and
|
|
||||||
# modelgen, the others will be allowed when binding to fields. Configure them to
|
|
||||||
# your liking
|
|
||||||
models:
|
models:
|
||||||
ID:
|
ID:
|
||||||
model:
|
model:
|
||||||
|
|||||||
37
httpmux.go
37
httpmux.go
@@ -1,37 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/rs/cors"
|
|
||||||
)
|
|
||||||
|
|
||||||
type mux struct {
|
|
||||||
*http.ServeMux
|
|
||||||
api *http.ServeMux
|
|
||||||
}
|
|
||||||
|
|
||||||
func httpMux(fns ...interface{ RegisterHTTP(*http.ServeMux) }) http.Handler {
|
|
||||||
mux := newMux()
|
|
||||||
for _, fn := range fns {
|
|
||||||
fn.RegisterHTTP(mux.ServeMux)
|
|
||||||
|
|
||||||
if fn, ok := fn.(interface{ RegisterAPIv1(*http.ServeMux) }); ok {
|
|
||||||
fn.RegisterAPIv1(mux.api)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return cors.AllowAll().Handler(mux)
|
|
||||||
}
|
|
||||||
func newMux() *mux {
|
|
||||||
mux := &mux{
|
|
||||||
api: http.NewServeMux(),
|
|
||||||
ServeMux: http.NewServeMux(),
|
|
||||||
}
|
|
||||||
mux.Handle("/api/v1/", http.StripPrefix("/api/v1/", mux.api))
|
|
||||||
|
|
||||||
return mux
|
|
||||||
}
|
|
||||||
func (m mux) HandleAPIv1(pattern string, handler http.Handler) {
|
|
||||||
m.api.Handle(pattern, handler)
|
|
||||||
}
|
|
||||||
60
internal/clean/eventstore.go
Normal file
60
internal/clean/eventstore.go
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
package clean
|
||||||
|
|
||||||
|
import "encoding"
|
||||||
|
|
||||||
|
type EventLog[T, K, C comparable, E any] interface {
|
||||||
|
EventLog(T) List[K, C, E]
|
||||||
|
}
|
||||||
|
|
||||||
|
type EventStore[T, K, C comparable, E, A any] interface {
|
||||||
|
Bus[T, K, E]
|
||||||
|
EventLog[T, K, C, E]
|
||||||
|
|
||||||
|
Load(T, A) error
|
||||||
|
Store(A) error
|
||||||
|
Truncate(T) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type Event[T, C comparable, V any] struct {
|
||||||
|
Topic T
|
||||||
|
Position C
|
||||||
|
Payload V
|
||||||
|
}
|
||||||
|
|
||||||
|
type codec interface {
|
||||||
|
encoding.BinaryMarshaler
|
||||||
|
encoding.BinaryUnmarshaler
|
||||||
|
}
|
||||||
|
|
||||||
|
type aggr = struct{}
|
||||||
|
|
||||||
|
type evvent = Event[string, uint64, codec]
|
||||||
|
type evvee = EventStore[string, string, uint64, evvent, aggr]
|
||||||
|
type evvesub = Subscription[Event[string, uint64, codec]]
|
||||||
|
|
||||||
|
type PAGE = Page[string, string]
|
||||||
|
type LOG struct{}
|
||||||
|
|
||||||
|
var _ List[string, string, evvee] = (*LOG)(nil)
|
||||||
|
|
||||||
|
func (*LOG) First(n uint64, after string) ([]PAGE, error) { panic("N/A") }
|
||||||
|
func (*LOG) Last(n uint64, before string) ([]PAGE, error) { panic("N/A") }
|
||||||
|
|
||||||
|
type SUB struct{}
|
||||||
|
|
||||||
|
var _ evvesub = (*SUB)(nil)
|
||||||
|
|
||||||
|
func (*SUB) Recv() error { return nil }
|
||||||
|
func (*SUB) Events() []evvent { return nil }
|
||||||
|
func (*SUB) Close() {}
|
||||||
|
|
||||||
|
type EV struct{}
|
||||||
|
|
||||||
|
var _ evvee = (*EV)(nil)
|
||||||
|
|
||||||
|
func (*EV) Emit(topic string, event evvent) error { panic("N/A") }
|
||||||
|
func (*EV) EventLog(topic string) List[string, uint64, evvent] { panic("N/A") }
|
||||||
|
func (*EV) Subscribe(topic string, after uint64) evvesub { panic("N/A") }
|
||||||
|
func (*EV) Load(topic string, a aggr) error { panic("N/A") }
|
||||||
|
func (*EV) Store(a aggr) error { panic("N/A") }
|
||||||
|
func (*EV) Truncate(topic string) error { panic("N/A") }
|
||||||
38
internal/clean/interfaces.go
Normal file
38
internal/clean/interfaces.go
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
package clean
|
||||||
|
|
||||||
|
type GPD[K comparable, V any] interface {
|
||||||
|
Get(...K) ([]V, error)
|
||||||
|
Put(K, V) error
|
||||||
|
Delete(K) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type Edge[C, K comparable] struct {
|
||||||
|
Key K
|
||||||
|
Kursor C
|
||||||
|
}
|
||||||
|
type Page[C, K comparable] struct {
|
||||||
|
Edges Edge[C, K]
|
||||||
|
Start C
|
||||||
|
End C
|
||||||
|
Next bool
|
||||||
|
Prev bool
|
||||||
|
}
|
||||||
|
type List[K, C comparable, V any] interface {
|
||||||
|
First(n uint64, after C) ([]Page[C, K], error)
|
||||||
|
Last(n uint64, before C) ([]Page[C, K], error)
|
||||||
|
}
|
||||||
|
type Emitter[T comparable, E any] interface {
|
||||||
|
Emit(T, E) error
|
||||||
|
}
|
||||||
|
type Subscription[E any] interface {
|
||||||
|
Recv() error
|
||||||
|
Events() []E
|
||||||
|
Close()
|
||||||
|
}
|
||||||
|
type Subscriber[T comparable, E any] interface {
|
||||||
|
Subscribe(T, uint64) Subscription[E]
|
||||||
|
}
|
||||||
|
type Bus[T, K comparable, E any] interface {
|
||||||
|
Emitter[T, E]
|
||||||
|
Subscriber[T, E]
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
1
internal/graph/generated/pkg.go
Normal file
1
internal/graph/generated/pkg.go
Normal file
@@ -0,0 +1 @@
|
|||||||
|
package generated
|
||||||
64
internal/graph/resolver/resolver.go
Normal file
64
internal/graph/resolver/resolver.go
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
package resolver
|
||||||
|
|
||||||
|
// THIS CODE IS A STARTING POINT ONLY. IT WILL NOT BE UPDATED WITH SCHEMA CHANGES.
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"go.sour.is/pkg/gql"
|
||||||
|
|
||||||
|
"go.sour.is/ev/app/msgbus"
|
||||||
|
"go.sour.is/ev/app/salty"
|
||||||
|
"go.sour.is/ev/internal/graph/generated"
|
||||||
|
gql_es "go.sour.is/ev/pkg/gql"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Resolver struct{}
|
||||||
|
|
||||||
|
// // foo
|
||||||
|
func (r *mutationResolver) TruncateStream(ctx context.Context, streamID string, index int64) (bool, error) {
|
||||||
|
panic("not implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
// // foo
|
||||||
|
func (r *mutationResolver) CreateSaltyUser(ctx context.Context, nick string, pubkey string) (*salty.SaltyUser, error) {
|
||||||
|
panic("not implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
// // foo
|
||||||
|
func (r *queryResolver) Events(ctx context.Context, streamID string, paging *gql.PageInput) (*gql.Connection, error) {
|
||||||
|
panic("not implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
// // foo
|
||||||
|
func (r *queryResolver) Posts(ctx context.Context, name, tag string, paging *gql.PageInput) (*gql.Connection, error) {
|
||||||
|
panic("not implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
// // foo
|
||||||
|
func (r *queryResolver) SaltyUser(ctx context.Context, nick string) (*salty.SaltyUser, error) {
|
||||||
|
panic("not implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
// // foo
|
||||||
|
func (r *subscriptionResolver) EventAdded(ctx context.Context, streamID string, after int64) (<-chan *gql_es.Event, error) {
|
||||||
|
panic("not implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
// // foo
|
||||||
|
func (r *subscriptionResolver) PostAdded(ctx context.Context, name, tag string, after int64) (<-chan *msgbus.PostEvent, error) {
|
||||||
|
panic("not implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mutation returns generated.MutationResolver implementation.
|
||||||
|
func (r *Resolver) Mutation() generated.MutationResolver { return &mutationResolver{r} }
|
||||||
|
|
||||||
|
// Query returns generated.QueryResolver implementation.
|
||||||
|
func (r *Resolver) Query() generated.QueryResolver { return &queryResolver{r} }
|
||||||
|
|
||||||
|
// Subscription returns generated.SubscriptionResolver implementation.
|
||||||
|
func (r *Resolver) Subscription() generated.SubscriptionResolver { return &subscriptionResolver{r} }
|
||||||
|
|
||||||
|
type mutationResolver struct{ *Resolver }
|
||||||
|
type queryResolver struct{ *Resolver }
|
||||||
|
type subscriptionResolver struct{ *Resolver }
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
package lg
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"log"
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"go.uber.org/multierr"
|
|
||||||
)
|
|
||||||
|
|
||||||
func Init(ctx context.Context, name string) (context.Context, func() error) {
|
|
||||||
ctx, span := Span(ctx)
|
|
||||||
defer span.End()
|
|
||||||
|
|
||||||
stop := [3]func() error{
|
|
||||||
initLogger(name),
|
|
||||||
}
|
|
||||||
ctx, stop[1] = initMetrics(ctx, name)
|
|
||||||
ctx, stop[2] = initTracing(ctx, name)
|
|
||||||
|
|
||||||
reverse(stop[:])
|
|
||||||
|
|
||||||
return ctx, func() error {
|
|
||||||
log.Println("flushing logs...")
|
|
||||||
errs := make([]error, len(stop))
|
|
||||||
for i, fn := range stop {
|
|
||||||
if fn != nil {
|
|
||||||
errs[i] = fn()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
log.Println("all stopped.")
|
|
||||||
return multierr.Combine(errs...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func env(name, defaultValue string) string {
|
|
||||||
name = strings.TrimSpace(name)
|
|
||||||
defaultValue = strings.TrimSpace(defaultValue)
|
|
||||||
if v := strings.TrimSpace(os.Getenv(name)); v != "" {
|
|
||||||
log.Println("# ", name, "=", v)
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
log.Println("# ", name, "=", defaultValue, "(default)")
|
|
||||||
return defaultValue
|
|
||||||
}
|
|
||||||
|
|
||||||
type secret string
|
|
||||||
|
|
||||||
func (s secret) String() string {
|
|
||||||
if s == "" {
|
|
||||||
return "(nil)"
|
|
||||||
}
|
|
||||||
return "***"
|
|
||||||
}
|
|
||||||
func (s secret) Secret() string {
|
|
||||||
return string(s)
|
|
||||||
}
|
|
||||||
func envSecret(name, defaultValue string) secret {
|
|
||||||
name = strings.TrimSpace(name)
|
|
||||||
defaultValue = strings.TrimSpace(defaultValue)
|
|
||||||
if v := strings.TrimSpace(os.Getenv(name)); v != "" {
|
|
||||||
log.Println("# ", name, "=", secret(v))
|
|
||||||
return secret(v)
|
|
||||||
}
|
|
||||||
log.Println("# ", name, "=", secret(defaultValue), "(default)")
|
|
||||||
return secret(defaultValue)
|
|
||||||
}
|
|
||||||
@@ -1,111 +0,0 @@
|
|||||||
package lg
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"encoding/json"
|
|
||||||
"io"
|
|
||||||
"log"
|
|
||||||
"os"
|
|
||||||
"runtime/debug"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/go-logr/stdr"
|
|
||||||
"github.com/logzio/logzio-go"
|
|
||||||
"go.opentelemetry.io/otel"
|
|
||||||
)
|
|
||||||
|
|
||||||
type logzwriter struct {
|
|
||||||
name string
|
|
||||||
pkg string
|
|
||||||
goversion string
|
|
||||||
hostname string
|
|
||||||
|
|
||||||
w io.Writer
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *logzwriter) Write(b []byte) (int, error) {
|
|
||||||
i := 0
|
|
||||||
for _, sp := range bytes.Split(b, []byte("\n")) {
|
|
||||||
msg := struct {
|
|
||||||
Message string `json:"message"`
|
|
||||||
Host string `json:"host"`
|
|
||||||
GoVersion string `json:"go_version"`
|
|
||||||
Package string `json:"pkg"`
|
|
||||||
App string `json:"app"`
|
|
||||||
}{
|
|
||||||
Message: strings.TrimSpace(string(sp)),
|
|
||||||
Host: l.hostname,
|
|
||||||
GoVersion: l.goversion,
|
|
||||||
Package: l.pkg,
|
|
||||||
App: l.name,
|
|
||||||
}
|
|
||||||
|
|
||||||
if msg.Message == "" || strings.HasPrefix(msg.Message, "#") {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
b, err := json.Marshal(msg)
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
j, err := l.w.Write(b)
|
|
||||||
|
|
||||||
i += j
|
|
||||||
if err != nil {
|
|
||||||
return i, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return i, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func initLogger(name string) func() error {
|
|
||||||
log.SetPrefix("[" + name + "] ")
|
|
||||||
log.SetFlags(log.LstdFlags&^(log.Ldate|log.Ltime) | log.Lshortfile)
|
|
||||||
|
|
||||||
token := envSecret("LOGZIO_LOG_TOKEN", "")
|
|
||||||
if token == "" {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
l, err := logzio.New(
|
|
||||||
token.Secret(),
|
|
||||||
// logzio.SetDebug(os.Stderr),
|
|
||||||
logzio.SetUrl(env("LOGZIO_LOG_URL", "https://listener.lg.io:8071")),
|
|
||||||
logzio.SetDrainDuration(time.Second*5),
|
|
||||||
logzio.SetTempDirectory(env("LOGZIO_DIR", os.TempDir())),
|
|
||||||
logzio.SetCheckDiskSpace(true),
|
|
||||||
logzio.SetDrainDiskThreshold(70),
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
w := io.MultiWriter(os.Stderr, lzw(l, name))
|
|
||||||
log.SetOutput(w)
|
|
||||||
otel.SetLogger(stdr.New(log.Default()))
|
|
||||||
|
|
||||||
return func() error {
|
|
||||||
defer log.Println("logger stopped")
|
|
||||||
log.SetOutput(os.Stderr)
|
|
||||||
l.Stop()
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
func lzw(l io.Writer, name string) io.Writer {
|
|
||||||
lz := &logzwriter{
|
|
||||||
name: name,
|
|
||||||
w: l,
|
|
||||||
}
|
|
||||||
|
|
||||||
if info, ok := debug.ReadBuildInfo(); ok {
|
|
||||||
lz.goversion = info.GoVersion
|
|
||||||
lz.pkg = info.Path
|
|
||||||
}
|
|
||||||
if hostname, err := os.Hostname(); err == nil {
|
|
||||||
lz.hostname = hostname
|
|
||||||
}
|
|
||||||
|
|
||||||
return lz
|
|
||||||
}
|
|
||||||
@@ -1,99 +0,0 @@
|
|||||||
package lg
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"log"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
"runtime/debug"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"go.opentelemetry.io/contrib/instrumentation/runtime"
|
|
||||||
"go.opentelemetry.io/otel/attribute"
|
|
||||||
"go.opentelemetry.io/otel/exporters/prometheus"
|
|
||||||
"go.opentelemetry.io/otel/metric"
|
|
||||||
"go.opentelemetry.io/otel/metric/global"
|
|
||||||
"go.opentelemetry.io/otel/sdk/metric/aggregator/histogram"
|
|
||||||
controller "go.opentelemetry.io/otel/sdk/metric/controller/basic"
|
|
||||||
"go.opentelemetry.io/otel/sdk/metric/export/aggregation"
|
|
||||||
processor "go.opentelemetry.io/otel/sdk/metric/processor/basic"
|
|
||||||
selector "go.opentelemetry.io/otel/sdk/metric/selector/simple"
|
|
||||||
"go.opentelemetry.io/otel/sdk/resource"
|
|
||||||
semconv "go.opentelemetry.io/otel/semconv/v1.4.0"
|
|
||||||
)
|
|
||||||
|
|
||||||
var meterKey = contextKey{"meter"}
|
|
||||||
var promHTTPKey = contextKey{"promHTTP"}
|
|
||||||
|
|
||||||
func Meter(ctx context.Context) metric.Meter {
|
|
||||||
if t := fromContext[contextKey, metric.Meter](ctx, tracerKey); t != nil {
|
|
||||||
return t
|
|
||||||
}
|
|
||||||
return global.Meter("")
|
|
||||||
}
|
|
||||||
func NewHTTP(ctx context.Context) *httpHandle {
|
|
||||||
t := fromContext[contextKey, *prometheus.Exporter](ctx, promHTTPKey)
|
|
||||||
return &httpHandle{t}
|
|
||||||
}
|
|
||||||
|
|
||||||
func initMetrics(ctx context.Context, name string) (context.Context, func() error) {
|
|
||||||
goversion := ""
|
|
||||||
pkg := ""
|
|
||||||
host := ""
|
|
||||||
if info, ok := debug.ReadBuildInfo(); ok {
|
|
||||||
goversion = info.GoVersion
|
|
||||||
pkg = info.Path
|
|
||||||
}
|
|
||||||
if h, err := os.Hostname(); err == nil {
|
|
||||||
host = h
|
|
||||||
}
|
|
||||||
|
|
||||||
config := prometheus.Config{}
|
|
||||||
cont := controller.New(
|
|
||||||
processor.NewFactory(
|
|
||||||
selector.NewWithHistogramDistribution(
|
|
||||||
histogram.WithExplicitBoundaries(config.DefaultHistogramBoundaries),
|
|
||||||
),
|
|
||||||
aggregation.CumulativeTemporalitySelector(),
|
|
||||||
processor.WithMemory(true),
|
|
||||||
),
|
|
||||||
controller.WithResource(
|
|
||||||
resource.NewWithAttributes(
|
|
||||||
semconv.SchemaURL,
|
|
||||||
attribute.String("app", name),
|
|
||||||
attribute.String("host", host),
|
|
||||||
attribute.String("go_version", goversion),
|
|
||||||
attribute.String("pkg", pkg),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
ex, err := prometheus.New(config, cont)
|
|
||||||
if err != nil {
|
|
||||||
return ctx, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx = toContext(ctx, promHTTPKey, ex)
|
|
||||||
|
|
||||||
global.SetMeterProvider(cont)
|
|
||||||
m := cont.Meter(name)
|
|
||||||
ctx = toContext(ctx, meterKey, m)
|
|
||||||
runtime.Start()
|
|
||||||
|
|
||||||
return ctx, func() error {
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
defer log.Println("metrics stopped")
|
|
||||||
return cont.Stop(ctx)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type httpHandle struct {
|
|
||||||
exp *prometheus.Exporter
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *httpHandle) RegisterHTTP(mux *http.ServeMux) {
|
|
||||||
if h.exp == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
mux.Handle("/metrics", h.exp)
|
|
||||||
}
|
|
||||||
@@ -1,166 +0,0 @@
|
|||||||
package lg
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
"net/http"
|
|
||||||
"runtime"
|
|
||||||
"strconv"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
|
|
||||||
"go.opentelemetry.io/otel"
|
|
||||||
"go.opentelemetry.io/otel/attribute"
|
|
||||||
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp"
|
|
||||||
"go.opentelemetry.io/otel/propagation"
|
|
||||||
"go.opentelemetry.io/otel/sdk/resource"
|
|
||||||
sdktrace "go.opentelemetry.io/otel/sdk/trace"
|
|
||||||
semconv "go.opentelemetry.io/otel/semconv/v1.4.0"
|
|
||||||
"go.opentelemetry.io/otel/trace"
|
|
||||||
)
|
|
||||||
|
|
||||||
type contextKey struct {
|
|
||||||
name string
|
|
||||||
}
|
|
||||||
|
|
||||||
var tracerKey = contextKey{"tracer"}
|
|
||||||
|
|
||||||
func Tracer(ctx context.Context) trace.Tracer {
|
|
||||||
if t := fromContext[contextKey, trace.Tracer](ctx, tracerKey); t != nil {
|
|
||||||
return t
|
|
||||||
}
|
|
||||||
return otel.Tracer("")
|
|
||||||
}
|
|
||||||
|
|
||||||
func attrs(ctx context.Context) (string, []attribute.KeyValue) {
|
|
||||||
var attrs []attribute.KeyValue
|
|
||||||
var name string
|
|
||||||
if pc, file, line, ok := runtime.Caller(2); ok {
|
|
||||||
if fn := runtime.FuncForPC(pc); fn != nil {
|
|
||||||
name = fn.Name()
|
|
||||||
}
|
|
||||||
attrs = append(attrs,
|
|
||||||
attribute.String("pc", fmt.Sprintf("%v", pc)),
|
|
||||||
attribute.String("file", file),
|
|
||||||
attribute.Int("line", line),
|
|
||||||
attribute.String("name", name),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return name, attrs
|
|
||||||
}
|
|
||||||
|
|
||||||
func Span(ctx context.Context, opts ...trace.SpanStartOption) (context.Context, trace.Span) {
|
|
||||||
name, attrs := attrs(ctx)
|
|
||||||
ctx, span := Tracer(ctx).Start(ctx, name, opts...)
|
|
||||||
span.SetAttributes(attrs...)
|
|
||||||
|
|
||||||
return ctx, span
|
|
||||||
}
|
|
||||||
|
|
||||||
func Fork(ctx context.Context, opts ...trace.SpanStartOption) (context.Context, trace.Span) {
|
|
||||||
name, attrs := attrs(ctx)
|
|
||||||
childCTX, childSpan := Tracer(ctx).Start(context.Background(), name, append(opts, trace.WithLinks(trace.LinkFromContext(ctx)))...)
|
|
||||||
childSpan.SetAttributes(attrs...)
|
|
||||||
|
|
||||||
_, span := Tracer(ctx).Start(ctx, name, append(opts, trace.WithLinks(trace.LinkFromContext(childCTX)))...)
|
|
||||||
span.SetAttributes(attrs...)
|
|
||||||
defer span.End()
|
|
||||||
|
|
||||||
return childCTX, childSpan
|
|
||||||
}
|
|
||||||
|
|
||||||
type SampleRate string
|
|
||||||
|
|
||||||
const (
|
|
||||||
SampleAlways SampleRate = "always"
|
|
||||||
SampleNever SampleRate = "never"
|
|
||||||
)
|
|
||||||
|
|
||||||
func initTracing(ctx context.Context, name string) (context.Context, func() error) {
|
|
||||||
res, err := resource.New(ctx,
|
|
||||||
resource.WithAttributes(
|
|
||||||
semconv.ServiceNameKey.String(name),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
log.Println(wrap(err, "failed to create trace resource"))
|
|
||||||
return ctx, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
exporterAddr := env("EV_TRACE_ENDPOINT", "")
|
|
||||||
if exporterAddr == "" {
|
|
||||||
return ctx, nil
|
|
||||||
}
|
|
||||||
traceExporter, err := otlptracehttp.New(ctx,
|
|
||||||
otlptracehttp.WithInsecure(),
|
|
||||||
otlptracehttp.WithEndpoint(exporterAddr),
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
log.Println(wrap(err, "failed to create trace exporter"))
|
|
||||||
return ctx, nil
|
|
||||||
}
|
|
||||||
bsp := sdktrace.NewBatchSpanProcessor(traceExporter)
|
|
||||||
|
|
||||||
var sample sdktrace.TracerProviderOption
|
|
||||||
sampleRate := SampleRate(env("EV_TRACE_SAMPLE", string(SampleNever)))
|
|
||||||
switch sampleRate {
|
|
||||||
case "always":
|
|
||||||
sample = sdktrace.WithSampler(sdktrace.AlwaysSample())
|
|
||||||
case "never":
|
|
||||||
sample = sdktrace.WithSampler(sdktrace.NeverSample())
|
|
||||||
default:
|
|
||||||
if v, err := strconv.Atoi(string(sampleRate)); err != nil {
|
|
||||||
sample = sdktrace.WithSampler(sdktrace.NeverSample())
|
|
||||||
} else {
|
|
||||||
sample = sdktrace.WithSampler(sdktrace.TraceIDRatioBased(float64(v) * 0.01))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
tracerProvider := sdktrace.NewTracerProvider(
|
|
||||||
sample,
|
|
||||||
sdktrace.WithResource(res),
|
|
||||||
sdktrace.WithSpanProcessor(bsp),
|
|
||||||
)
|
|
||||||
otel.SetTracerProvider(tracerProvider)
|
|
||||||
otel.SetTextMapPropagator(propagation.TraceContext{})
|
|
||||||
|
|
||||||
ctx = toContext(ctx, tracerKey, otel.Tracer(name))
|
|
||||||
|
|
||||||
return ctx, func() error {
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
defer log.Println("tracer stopped")
|
|
||||||
return wrap(tracerProvider.Shutdown(ctx), "failed to shutdown TracerProvider")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func wrap(err error, s string) error {
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf(s, err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
func reverse[T any](s []T) {
|
|
||||||
first, last := 0, len(s)-1
|
|
||||||
for first < last {
|
|
||||||
s[first], s[last] = s[last], s[first]
|
|
||||||
first++
|
|
||||||
last--
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func Htrace(h http.Handler, name string) http.Handler {
|
|
||||||
return otelhttp.NewHandler(h, name)
|
|
||||||
}
|
|
||||||
|
|
||||||
func toContext[K comparable, V any](ctx context.Context, key K, value V) context.Context {
|
|
||||||
return context.WithValue(ctx, key, value)
|
|
||||||
}
|
|
||||||
func fromContext[K comparable, V any](ctx context.Context, key K) V {
|
|
||||||
var empty V
|
|
||||||
if v, ok := ctx.Value(key).(V); ok {
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
return empty
|
|
||||||
}
|
|
||||||
166
main.go
166
main.go
@@ -1,166 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"log"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
"os"
|
|
||||||
"os/signal"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"go.uber.org/multierr"
|
|
||||||
"golang.org/x/sync/errgroup"
|
|
||||||
|
|
||||||
"github.com/sour-is/ev/app/gql"
|
|
||||||
"github.com/sour-is/ev/app/msgbus"
|
|
||||||
"github.com/sour-is/ev/app/peerfinder"
|
|
||||||
"github.com/sour-is/ev/app/salty"
|
|
||||||
"github.com/sour-is/ev/internal/lg"
|
|
||||||
"github.com/sour-is/ev/pkg/es"
|
|
||||||
diskstore "github.com/sour-is/ev/pkg/es/driver/disk-store"
|
|
||||||
memstore "github.com/sour-is/ev/pkg/es/driver/mem-store"
|
|
||||||
"github.com/sour-is/ev/pkg/es/driver/projecter"
|
|
||||||
"github.com/sour-is/ev/pkg/es/driver/streamer"
|
|
||||||
"github.com/sour-is/ev/pkg/es/event"
|
|
||||||
"github.com/sour-is/ev/pkg/set"
|
|
||||||
)
|
|
||||||
|
|
||||||
const AppName string = "sour.is-ev"
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, os.Kill)
|
|
||||||
go func() {
|
|
||||||
<-ctx.Done()
|
|
||||||
defer cancel()
|
|
||||||
}()
|
|
||||||
|
|
||||||
ctx, stop := lg.Init(ctx, AppName)
|
|
||||||
defer stop()
|
|
||||||
|
|
||||||
if err := run(ctx); err != nil && err != http.ErrServerClosed {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
func run(ctx context.Context) error {
|
|
||||||
g, ctx := errgroup.WithContext(ctx)
|
|
||||||
|
|
||||||
{
|
|
||||||
ctx, span := lg.Span(ctx)
|
|
||||||
|
|
||||||
err := multierr.Combine(
|
|
||||||
es.Init(ctx),
|
|
||||||
event.Init(ctx),
|
|
||||||
diskstore.Init(ctx),
|
|
||||||
memstore.Init(ctx),
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
span.RecordError(err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
es, err := es.Open(ctx, env("EV_DATA", "mem:"), streamer.New(ctx), projecter.New(ctx))
|
|
||||||
if err != nil {
|
|
||||||
span.RecordError(err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
s := http.Server{
|
|
||||||
Addr: env("EV_HTTP", ":8080"),
|
|
||||||
}
|
|
||||||
|
|
||||||
if strings.HasPrefix(s.Addr, ":") {
|
|
||||||
s.Addr = "[::]" + s.Addr
|
|
||||||
}
|
|
||||||
|
|
||||||
enable := set.New(strings.Fields(env("EV_ENABLE", "salty msgbus gql peers"))...)
|
|
||||||
var svcs []interface{ RegisterHTTP(*http.ServeMux) }
|
|
||||||
|
|
||||||
svcs = append(svcs, es)
|
|
||||||
|
|
||||||
if enable.Has("salty") {
|
|
||||||
span.AddEvent("Enable Salty")
|
|
||||||
base, err := url.JoinPath(env("EV_BASE_URL", "http://"+s.Addr), "inbox")
|
|
||||||
if err != nil {
|
|
||||||
span.RecordError(err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
salty, err := salty.New(ctx, es, base)
|
|
||||||
if err != nil {
|
|
||||||
span.RecordError(err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
svcs = append(svcs, salty)
|
|
||||||
}
|
|
||||||
|
|
||||||
if enable.Has("msgbus") {
|
|
||||||
span.AddEvent("Enable Msgbus")
|
|
||||||
msgbus, err := msgbus.New(ctx, es)
|
|
||||||
if err != nil {
|
|
||||||
span.RecordError(err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
svcs = append(svcs, msgbus)
|
|
||||||
}
|
|
||||||
|
|
||||||
if enable.Has("peers") {
|
|
||||||
span.AddEvent("Enable Peers")
|
|
||||||
peers, err := peerfinder.New(ctx, es)
|
|
||||||
if err != nil {
|
|
||||||
span.RecordError(err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
svcs = append(svcs, peers)
|
|
||||||
}
|
|
||||||
|
|
||||||
if enable.Has("gql") {
|
|
||||||
span.AddEvent("Enable GraphQL")
|
|
||||||
gql, err := gql.New(ctx, svcs...)
|
|
||||||
if err != nil {
|
|
||||||
span.RecordError(err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
svcs = append(svcs, gql)
|
|
||||||
}
|
|
||||||
svcs = append(svcs, lg.NewHTTP(ctx))
|
|
||||||
|
|
||||||
s.Handler = httpMux(svcs...)
|
|
||||||
|
|
||||||
log.Print("Listen on ", s.Addr)
|
|
||||||
span.AddEvent("begin listen and serve on " + s.Addr)
|
|
||||||
|
|
||||||
Mup, err := lg.Meter(ctx).SyncInt64().UpDownCounter("up")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
Mup.Add(ctx, 1)
|
|
||||||
|
|
||||||
g.Go(s.ListenAndServe)
|
|
||||||
|
|
||||||
g.Go(func() error {
|
|
||||||
<-ctx.Done()
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
return s.Shutdown(ctx)
|
|
||||||
})
|
|
||||||
|
|
||||||
span.End()
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := g.Wait(); err != nil && err != http.ErrServerClosed {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
func env(name, defaultValue string) string {
|
|
||||||
name = strings.TrimSpace(name)
|
|
||||||
defaultValue = strings.TrimSpace(defaultValue)
|
|
||||||
if v := strings.TrimSpace(os.Getenv(name)); v != "" {
|
|
||||||
log.Println("#", name, "=", v)
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
log.Println("#", name, "=", defaultValue, "(default)")
|
|
||||||
return defaultValue
|
|
||||||
}
|
|
||||||
3
pkg/README.md
Normal file
3
pkg/README.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# Pkg Tools
|
||||||
|
|
||||||
|
This is a collection of modules that provide simple reusable functions.
|
||||||
238
pkg/cache/cache.go
vendored
238
pkg/cache/cache.go
vendored
@@ -1,238 +0,0 @@
|
|||||||
package cache
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"sync"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
// DefaultEvictedBufferSize defines the default buffer size to store evicted key/val
|
|
||||||
DefaultEvictedBufferSize = 16
|
|
||||||
)
|
|
||||||
|
|
||||||
// Cache is a thread-safe fixed size LRU cache.
|
|
||||||
type Cache[K comparable, V any] struct {
|
|
||||||
lru *LRU[K, V]
|
|
||||||
evictedKeys []K
|
|
||||||
evictedVals []V
|
|
||||||
onEvictedCB func(ctx context.Context, k K, v V)
|
|
||||||
lock sync.RWMutex
|
|
||||||
}
|
|
||||||
|
|
||||||
// New creates an LRU of the given size.
|
|
||||||
func NewCache[K comparable, V any](size int) (*Cache[K, V], error) {
|
|
||||||
return NewWithEvict[K, V](size, nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewWithEvict constructs a fixed size cache with the given eviction
|
|
||||||
// callback.
|
|
||||||
func NewWithEvict[K comparable, V any](size int, onEvicted func(context.Context, K, V)) (c *Cache[K, V], err error) {
|
|
||||||
// create a cache with default settings
|
|
||||||
c = &Cache[K, V]{
|
|
||||||
onEvictedCB: onEvicted,
|
|
||||||
}
|
|
||||||
if onEvicted != nil {
|
|
||||||
c.initEvictBuffers()
|
|
||||||
onEvicted = c.onEvicted
|
|
||||||
}
|
|
||||||
c.lru, err = NewLRU(size, onEvicted)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Cache[K, V]) initEvictBuffers() {
|
|
||||||
c.evictedKeys = make([]K, 0, DefaultEvictedBufferSize)
|
|
||||||
c.evictedVals = make([]V, 0, DefaultEvictedBufferSize)
|
|
||||||
}
|
|
||||||
|
|
||||||
// onEvicted save evicted key/val and sent in externally registered callback
|
|
||||||
// outside of critical section
|
|
||||||
func (c *Cache[K, V]) onEvicted(ctx context.Context, k K, v V) {
|
|
||||||
c.evictedKeys = append(c.evictedKeys, k)
|
|
||||||
c.evictedVals = append(c.evictedVals, v)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Purge is used to completely clear the cache.
|
|
||||||
func (c *Cache[K, V]) Purge(ctx context.Context) {
|
|
||||||
var ks []K
|
|
||||||
var vs []V
|
|
||||||
c.lock.Lock()
|
|
||||||
c.lru.Purge(ctx)
|
|
||||||
if c.onEvictedCB != nil && len(c.evictedKeys) > 0 {
|
|
||||||
ks, vs = c.evictedKeys, c.evictedVals
|
|
||||||
c.initEvictBuffers()
|
|
||||||
}
|
|
||||||
c.lock.Unlock()
|
|
||||||
// invoke callback outside of critical section
|
|
||||||
if c.onEvictedCB != nil {
|
|
||||||
for i := 0; i < len(ks); i++ {
|
|
||||||
c.onEvictedCB(ctx, ks[i], vs[i])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add adds a value to the cache. Returns true if an eviction occurred.
|
|
||||||
func (c *Cache[K, V]) Add(ctx context.Context, key K, value V) (evicted bool) {
|
|
||||||
var k K
|
|
||||||
var v V
|
|
||||||
c.lock.Lock()
|
|
||||||
evicted = c.lru.Add(ctx, key, value)
|
|
||||||
if c.onEvictedCB != nil && evicted {
|
|
||||||
k, v = c.evictedKeys[0], c.evictedVals[0]
|
|
||||||
c.evictedKeys, c.evictedVals = c.evictedKeys[:0], c.evictedVals[:0]
|
|
||||||
}
|
|
||||||
c.lock.Unlock()
|
|
||||||
if c.onEvictedCB != nil && evicted {
|
|
||||||
c.onEvictedCB(ctx, k, v)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get looks up a key's value from the cache.
|
|
||||||
func (c *Cache[K, V]) Get(key K) (value *V, ok bool) {
|
|
||||||
c.lock.Lock()
|
|
||||||
value, ok = c.lru.Get(key)
|
|
||||||
c.lock.Unlock()
|
|
||||||
return value, ok
|
|
||||||
}
|
|
||||||
|
|
||||||
// Contains checks if a key is in the cache, without updating the
|
|
||||||
// recent-ness or deleting it for being stale.
|
|
||||||
func (c *Cache[K, V]) Contains(key K) bool {
|
|
||||||
c.lock.RLock()
|
|
||||||
containKey := c.lru.Contains(key)
|
|
||||||
c.lock.RUnlock()
|
|
||||||
return containKey
|
|
||||||
}
|
|
||||||
|
|
||||||
// Peek returns the key value (or undefined if not found) without updating
|
|
||||||
// the "recently used"-ness of the key.
|
|
||||||
func (c *Cache[K, V]) Peek(key K) (value *V, ok bool) {
|
|
||||||
c.lock.RLock()
|
|
||||||
value, ok = c.lru.Peek(key)
|
|
||||||
c.lock.RUnlock()
|
|
||||||
return value, ok
|
|
||||||
}
|
|
||||||
|
|
||||||
// ContainsOrAdd checks if a key is in the cache without updating the
|
|
||||||
// recent-ness or deleting it for being stale, and if not, adds the value.
|
|
||||||
// Returns whether found and whether an eviction occurred.
|
|
||||||
func (c *Cache[K, V]) ContainsOrAdd(ctx context.Context, key K, value V) (ok, evicted bool) {
|
|
||||||
var k K
|
|
||||||
var v V
|
|
||||||
c.lock.Lock()
|
|
||||||
if c.lru.Contains(key) {
|
|
||||||
c.lock.Unlock()
|
|
||||||
return true, false
|
|
||||||
}
|
|
||||||
evicted = c.lru.Add(ctx, key, value)
|
|
||||||
if c.onEvictedCB != nil && evicted {
|
|
||||||
k, v = c.evictedKeys[0], c.evictedVals[0]
|
|
||||||
c.evictedKeys, c.evictedVals = c.evictedKeys[:0], c.evictedVals[:0]
|
|
||||||
}
|
|
||||||
c.lock.Unlock()
|
|
||||||
if c.onEvictedCB != nil && evicted {
|
|
||||||
c.onEvictedCB(ctx, k, v)
|
|
||||||
}
|
|
||||||
return false, evicted
|
|
||||||
}
|
|
||||||
|
|
||||||
// PeekOrAdd checks if a key is in the cache without updating the
|
|
||||||
// recent-ness or deleting it for being stale, and if not, adds the value.
|
|
||||||
// Returns whether found and whether an eviction occurred.
|
|
||||||
func (c *Cache[K, V]) PeekOrAdd(ctx context.Context, key K, value V) (previous interface{}, ok, evicted bool) {
|
|
||||||
var k K
|
|
||||||
var v V
|
|
||||||
c.lock.Lock()
|
|
||||||
previous, ok = c.lru.Peek(key)
|
|
||||||
if ok {
|
|
||||||
c.lock.Unlock()
|
|
||||||
return previous, true, false
|
|
||||||
}
|
|
||||||
evicted = c.lru.Add(ctx, key, value)
|
|
||||||
if c.onEvictedCB != nil && evicted {
|
|
||||||
k, v = c.evictedKeys[0], c.evictedVals[0]
|
|
||||||
c.evictedKeys, c.evictedVals = c.evictedKeys[:0], c.evictedVals[:0]
|
|
||||||
}
|
|
||||||
c.lock.Unlock()
|
|
||||||
if c.onEvictedCB != nil && evicted {
|
|
||||||
c.onEvictedCB(ctx, k, v)
|
|
||||||
}
|
|
||||||
return nil, false, evicted
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove removes the provided key from the cache.
|
|
||||||
func (c *Cache[K, V]) Remove(ctx context.Context, key K) (present bool) {
|
|
||||||
var k K
|
|
||||||
var v V
|
|
||||||
c.lock.Lock()
|
|
||||||
present = c.lru.Remove(ctx, key)
|
|
||||||
if c.onEvictedCB != nil && present {
|
|
||||||
k, v = c.evictedKeys[0], c.evictedVals[0]
|
|
||||||
c.evictedKeys, c.evictedVals = c.evictedKeys[:0], c.evictedVals[:0]
|
|
||||||
}
|
|
||||||
c.lock.Unlock()
|
|
||||||
if c.onEvictedCB != nil && present {
|
|
||||||
c.onEvicted(ctx, k, v)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Resize changes the cache size.
|
|
||||||
func (c *Cache[K, V]) Resize(ctx context.Context, size int) (evicted int) {
|
|
||||||
var ks []K
|
|
||||||
var vs []V
|
|
||||||
c.lock.Lock()
|
|
||||||
evicted = c.lru.Resize(ctx, size)
|
|
||||||
if c.onEvictedCB != nil && evicted > 0 {
|
|
||||||
ks, vs = c.evictedKeys, c.evictedVals
|
|
||||||
c.initEvictBuffers()
|
|
||||||
}
|
|
||||||
c.lock.Unlock()
|
|
||||||
if c.onEvictedCB != nil && evicted > 0 {
|
|
||||||
for i := 0; i < len(ks); i++ {
|
|
||||||
c.onEvictedCB(ctx, ks[i], vs[i])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return evicted
|
|
||||||
}
|
|
||||||
|
|
||||||
// RemoveOldest removes the oldest item from the cache.
|
|
||||||
func (c *Cache[K, V]) RemoveOldest(ctx context.Context) (key *K, value *V, ok bool) {
|
|
||||||
var k K
|
|
||||||
var v V
|
|
||||||
c.lock.Lock()
|
|
||||||
key, value, ok = c.lru.RemoveOldest(ctx)
|
|
||||||
if c.onEvictedCB != nil && ok {
|
|
||||||
k, v = c.evictedKeys[0], c.evictedVals[0]
|
|
||||||
c.evictedKeys, c.evictedVals = c.evictedKeys[:0], c.evictedVals[:0]
|
|
||||||
}
|
|
||||||
c.lock.Unlock()
|
|
||||||
if c.onEvictedCB != nil && ok {
|
|
||||||
c.onEvictedCB(ctx, k, v)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetOldest returns the oldest entry
|
|
||||||
func (c *Cache[K, V]) GetOldest() (key *K, value *V, ok bool) {
|
|
||||||
c.lock.RLock()
|
|
||||||
key, value, ok = c.lru.GetOldest()
|
|
||||||
c.lock.RUnlock()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Keys returns a slice of the keys in the cache, from oldest to newest.
|
|
||||||
func (c *Cache[K, V]) Keys() []K {
|
|
||||||
c.lock.RLock()
|
|
||||||
keys := c.lru.Keys()
|
|
||||||
c.lock.RUnlock()
|
|
||||||
return keys
|
|
||||||
}
|
|
||||||
|
|
||||||
// Len returns the number of items in the cache.
|
|
||||||
func (c *Cache[K, V]) Len() int {
|
|
||||||
c.lock.RLock()
|
|
||||||
length := c.lru.Len()
|
|
||||||
c.lock.RUnlock()
|
|
||||||
return length
|
|
||||||
}
|
|
||||||
235
pkg/cache/list.go
vendored
235
pkg/cache/list.go
vendored
@@ -1,235 +0,0 @@
|
|||||||
// Copyright 2009 The Go Authors. All rights reserved.
|
|
||||||
// Use of this source code is governed by a BSD-style
|
|
||||||
// license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
// Package list implements a doubly linked list.
|
|
||||||
//
|
|
||||||
// To iterate over a list (where l is a *List):
|
|
||||||
//
|
|
||||||
// for e := l.Front(); e != nil; e = e.Next() {
|
|
||||||
// // do something with e.Value
|
|
||||||
// }
|
|
||||||
package cache
|
|
||||||
|
|
||||||
// Element is an element of a linked list.
|
|
||||||
type Element[V any] struct {
|
|
||||||
// Next and previous pointers in the doubly-linked list of elements.
|
|
||||||
// To simplify the implementation, internally a list l is implemented
|
|
||||||
// as a ring, such that &l.root is both the next element of the last
|
|
||||||
// list element (l.Back()) and the previous element of the first list
|
|
||||||
// element (l.Front()).
|
|
||||||
next, prev *Element[V]
|
|
||||||
|
|
||||||
// The list to which this element belongs.
|
|
||||||
list *List[V]
|
|
||||||
|
|
||||||
// The value stored with this element.
|
|
||||||
Value V
|
|
||||||
}
|
|
||||||
|
|
||||||
// Next returns the next list element or nil.
|
|
||||||
func (e *Element[V]) Next() *Element[V] {
|
|
||||||
if p := e.next; e.list != nil && p != &e.list.root {
|
|
||||||
return p
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prev returns the previous list element or nil.
|
|
||||||
func (e *Element[V]) Prev() *Element[V] {
|
|
||||||
if p := e.prev; e.list != nil && p != &e.list.root {
|
|
||||||
return p
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// List represents a doubly linked list.
|
|
||||||
// The zero value for List is an empty list ready to use.
|
|
||||||
type List[V any] struct {
|
|
||||||
root Element[V] // sentinel list element, only &root, root.prev, and root.next are used
|
|
||||||
len int // current list length excluding (this) sentinel element
|
|
||||||
}
|
|
||||||
|
|
||||||
// Init initializes or clears list l.
|
|
||||||
func (l *List[V]) Init() *List[V] {
|
|
||||||
l.root.next = &l.root
|
|
||||||
l.root.prev = &l.root
|
|
||||||
l.len = 0
|
|
||||||
return l
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewList returns an initialized list.
|
|
||||||
func NewList[V any]() *List[V] { return new(List[V]).Init() }
|
|
||||||
|
|
||||||
// Len returns the number of elements of list l.
|
|
||||||
// The complexity is O(1).
|
|
||||||
func (l *List[V]) Len() int { return l.len }
|
|
||||||
|
|
||||||
// Front returns the first element of list l or nil if the list is empty.
|
|
||||||
func (l *List[V]) Front() *Element[V] {
|
|
||||||
if l.len == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return l.root.next
|
|
||||||
}
|
|
||||||
|
|
||||||
// Back returns the last element of list l or nil if the list is empty.
|
|
||||||
func (l *List[V]) Back() *Element[V] {
|
|
||||||
if l.len == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return l.root.prev
|
|
||||||
}
|
|
||||||
|
|
||||||
// lazyInit lazily initializes a zero List value.
|
|
||||||
func (l *List[V]) lazyInit() {
|
|
||||||
if l.root.next == nil {
|
|
||||||
l.Init()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// insert inserts e after at, increments l.len, and returns e.
|
|
||||||
func (l *List[V]) insert(e, at *Element[V]) *Element[V] {
|
|
||||||
e.prev = at
|
|
||||||
e.next = at.next
|
|
||||||
e.prev.next = e
|
|
||||||
e.next.prev = e
|
|
||||||
e.list = l
|
|
||||||
l.len++
|
|
||||||
return e
|
|
||||||
}
|
|
||||||
|
|
||||||
// insertValue is a convenience wrapper for insert(&Element{Value: v}, at).
|
|
||||||
func (l *List[V]) insertValue(v V, at *Element[V]) *Element[V] {
|
|
||||||
return l.insert(&Element[V]{Value: v}, at)
|
|
||||||
}
|
|
||||||
|
|
||||||
// remove removes e from its list, decrements l.len
|
|
||||||
func (l *List[V]) remove(e *Element[V]) {
|
|
||||||
e.prev.next = e.next
|
|
||||||
e.next.prev = e.prev
|
|
||||||
e.next = nil // avoid memory leaks
|
|
||||||
e.prev = nil // avoid memory leaks
|
|
||||||
e.list = nil
|
|
||||||
l.len--
|
|
||||||
}
|
|
||||||
|
|
||||||
// move moves e to next to at.
|
|
||||||
func (l *List[V]) move(e, at *Element[V]) {
|
|
||||||
if e == at {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
e.prev.next = e.next
|
|
||||||
e.next.prev = e.prev
|
|
||||||
|
|
||||||
e.prev = at
|
|
||||||
e.next = at.next
|
|
||||||
e.prev.next = e
|
|
||||||
e.next.prev = e
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove removes e from l if e is an element of list l.
|
|
||||||
// It returns the element value e.Value.
|
|
||||||
// The element must not be nil.
|
|
||||||
func (l *List[V]) Remove(e *Element[V]) any {
|
|
||||||
if e.list == l {
|
|
||||||
// if e.list == l, l must have been initialized when e was inserted
|
|
||||||
// in l or l == nil (e is a zero Element) and l.remove will crash
|
|
||||||
l.remove(e)
|
|
||||||
}
|
|
||||||
return e.Value
|
|
||||||
}
|
|
||||||
|
|
||||||
// PushFront inserts a new element e with value v at the front of list l and returns e.
|
|
||||||
func (l *List[V]) PushFront(v V) *Element[V] {
|
|
||||||
l.lazyInit()
|
|
||||||
return l.insertValue(v, &l.root)
|
|
||||||
}
|
|
||||||
|
|
||||||
// PushBack inserts a new element e with value v at the back of list l and returns e.
|
|
||||||
func (l *List[V]) PushBack(v V) *Element[V] {
|
|
||||||
l.lazyInit()
|
|
||||||
return l.insertValue(v, l.root.prev)
|
|
||||||
}
|
|
||||||
|
|
||||||
// InsertBefore inserts a new element e with value v immediately before mark and returns e.
|
|
||||||
// If mark is not an element of l, the list is not modified.
|
|
||||||
// The mark must not be nil.
|
|
||||||
func (l *List[V]) InsertBefore(v V, mark *Element[V]) *Element[V] {
|
|
||||||
if mark.list != l {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
// see comment in List.Remove about initialization of l
|
|
||||||
return l.insertValue(v, mark.prev)
|
|
||||||
}
|
|
||||||
|
|
||||||
// InsertAfter inserts a new element e with value v immediately after mark and returns e.
|
|
||||||
// If mark is not an element of l, the list is not modified.
|
|
||||||
// The mark must not be nil.
|
|
||||||
func (l *List[V]) InsertAfter(v V, mark *Element[V]) *Element[V] {
|
|
||||||
if mark.list != l {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
// see comment in List.Remove about initialization of l
|
|
||||||
return l.insertValue(v, mark)
|
|
||||||
}
|
|
||||||
|
|
||||||
// MoveToFront moves element e to the front of list l.
|
|
||||||
// If e is not an element of l, the list is not modified.
|
|
||||||
// The element must not be nil.
|
|
||||||
func (l *List[V]) MoveToFront(e *Element[V]) {
|
|
||||||
if e.list != l || l.root.next == e {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// see comment in List.Remove about initialization of l
|
|
||||||
l.move(e, &l.root)
|
|
||||||
}
|
|
||||||
|
|
||||||
// MoveToBack moves element e to the back of list l.
|
|
||||||
// If e is not an element of l, the list is not modified.
|
|
||||||
// The element must not be nil.
|
|
||||||
func (l *List[V]) MoveToBack(e *Element[V]) {
|
|
||||||
if e.list != l || l.root.prev == e {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// see comment in List.Remove about initialization of l
|
|
||||||
l.move(e, l.root.prev)
|
|
||||||
}
|
|
||||||
|
|
||||||
// MoveBefore moves element e to its new position before mark.
|
|
||||||
// If e or mark is not an element of l, or e == mark, the list is not modified.
|
|
||||||
// The element and mark must not be nil.
|
|
||||||
func (l *List[V]) MoveBefore(e, mark *Element[V]) {
|
|
||||||
if e.list != l || e == mark || mark.list != l {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
l.move(e, mark.prev)
|
|
||||||
}
|
|
||||||
|
|
||||||
// MoveAfter moves element e to its new position after mark.
|
|
||||||
// If e or mark is not an element of l, or e == mark, the list is not modified.
|
|
||||||
// The element and mark must not be nil.
|
|
||||||
func (l *List[V]) MoveAfter(e, mark *Element[V]) {
|
|
||||||
if e.list != l || e == mark || mark.list != l {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
l.move(e, mark)
|
|
||||||
}
|
|
||||||
|
|
||||||
// PushBackList inserts a copy of another list at the back of list l.
|
|
||||||
// The lists l and other may be the same. They must not be nil.
|
|
||||||
func (l *List[V]) PushBackList(other *List[V]) {
|
|
||||||
l.lazyInit()
|
|
||||||
for i, e := other.Len(), other.Front(); i > 0; i, e = i-1, e.Next() {
|
|
||||||
l.insertValue(e.Value, l.root.prev)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// PushFrontList inserts a copy of another list at the front of list l.
|
|
||||||
// The lists l and other may be the same. They must not be nil.
|
|
||||||
func (l *List[V]) PushFrontList(other *List[V]) {
|
|
||||||
l.lazyInit()
|
|
||||||
for i, e := other.Len(), other.Back(); i > 0; i, e = i-1, e.Prev() {
|
|
||||||
l.insertValue(e.Value, &l.root)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
175
pkg/cache/lru.go
vendored
175
pkg/cache/lru.go
vendored
@@ -1,175 +0,0 @@
|
|||||||
package cache
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
)
|
|
||||||
|
|
||||||
// EvictCallback is used to get a callback when a cache entry is evicted
|
|
||||||
type EvictCallback[K comparable, V any] func(context.Context, K, V)
|
|
||||||
|
|
||||||
// LRU implements a non-thread safe fixed size LRU cache
|
|
||||||
type LRU[K comparable, V any] struct {
|
|
||||||
size int
|
|
||||||
evictList *List[entry[K, V]]
|
|
||||||
items map[K]*Element[entry[K, V]]
|
|
||||||
onEvict EvictCallback[K, V]
|
|
||||||
}
|
|
||||||
|
|
||||||
// entry is used to hold a value in the evictList
|
|
||||||
type entry[K comparable, V any] struct {
|
|
||||||
key K
|
|
||||||
value V
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewLRU constructs an LRU of the given size
|
|
||||||
func NewLRU[K comparable, V any](size int, onEvict EvictCallback[K, V]) (*LRU[K, V], error) {
|
|
||||||
if size <= 0 {
|
|
||||||
return nil, errors.New("must provide a positive size")
|
|
||||||
}
|
|
||||||
c := &LRU[K, V]{
|
|
||||||
size: size,
|
|
||||||
evictList: NewList[entry[K, V]](),
|
|
||||||
items: make(map[K]*Element[entry[K, V]]),
|
|
||||||
onEvict: onEvict,
|
|
||||||
}
|
|
||||||
return c, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Purge is used to completely clear the cache.
|
|
||||||
func (c *LRU[K, V]) Purge(ctx context.Context) {
|
|
||||||
for k, v := range c.items {
|
|
||||||
if c.onEvict != nil {
|
|
||||||
c.onEvict(ctx, k, v.Value.value)
|
|
||||||
}
|
|
||||||
delete(c.items, k)
|
|
||||||
}
|
|
||||||
c.evictList.Init()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add adds a value to the cache. Returns true if an eviction occurred.
|
|
||||||
func (c *LRU[K, V]) Add(ctx context.Context, key K, value V) (evicted bool) {
|
|
||||||
// Check for existing item
|
|
||||||
if ent, ok := c.items[key]; ok {
|
|
||||||
c.evictList.MoveToFront(ent)
|
|
||||||
ent.Value.value = value
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add new item
|
|
||||||
entry := c.evictList.PushFront(entry[K, V]{key, value})
|
|
||||||
c.items[key] = entry
|
|
||||||
|
|
||||||
evict := c.evictList.Len() > c.size
|
|
||||||
// Verify size not exceeded
|
|
||||||
if evict {
|
|
||||||
c.removeOldest(ctx)
|
|
||||||
}
|
|
||||||
return evict
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get looks up a key's value from the cache.
|
|
||||||
func (c *LRU[K, V]) Get(key K) (value *V, ok bool) {
|
|
||||||
if ent, ok := c.items[key]; ok {
|
|
||||||
c.evictList.MoveToFront(ent)
|
|
||||||
if ent == nil {
|
|
||||||
return nil, false
|
|
||||||
}
|
|
||||||
return &ent.Value.value, true
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Contains checks if a key is in the cache, without updating the recent-ness
|
|
||||||
// or deleting it for being stale.
|
|
||||||
func (c *LRU[K, V]) Contains(key K) (ok bool) {
|
|
||||||
_, ok = c.items[key]
|
|
||||||
return ok
|
|
||||||
}
|
|
||||||
|
|
||||||
// Peek returns the key value (or undefined if not found) without updating
|
|
||||||
// the "recently used"-ness of the key.
|
|
||||||
func (c *LRU[K, V]) Peek(key K) (value *V, ok bool) {
|
|
||||||
if ent, ok := c.items[key]; ok {
|
|
||||||
return &ent.Value.value, true
|
|
||||||
}
|
|
||||||
return nil, false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove removes the provided key from the cache, returning if the
|
|
||||||
// key was contained.
|
|
||||||
func (c *LRU[K, V]) Remove(ctx context.Context, key K) (present bool) {
|
|
||||||
if ent, ok := c.items[key]; ok {
|
|
||||||
c.removeElement(ctx, ent)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// RemoveOldest removes the oldest item from the cache.
|
|
||||||
func (c *LRU[K, V]) RemoveOldest(ctx context.Context) (key *K, value *V, ok bool) {
|
|
||||||
ent := c.evictList.Back()
|
|
||||||
if ent != nil {
|
|
||||||
c.removeElement(ctx, ent)
|
|
||||||
kv := ent.Value
|
|
||||||
return &kv.key, &kv.value, true
|
|
||||||
}
|
|
||||||
return nil, nil, false
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetOldest returns the oldest entry
|
|
||||||
func (c *LRU[K, V]) GetOldest() (key *K, value *V, ok bool) {
|
|
||||||
ent := c.evictList.Back()
|
|
||||||
if ent != nil {
|
|
||||||
kv := ent.Value
|
|
||||||
return &kv.key, &kv.value, true
|
|
||||||
}
|
|
||||||
return nil, nil, false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Keys returns a slice of the keys in the cache, from oldest to newest.
|
|
||||||
func (c *LRU[K, V]) Keys() []K {
|
|
||||||
keys := make([]K, len(c.items))
|
|
||||||
i := 0
|
|
||||||
for ent := c.evictList.Back(); ent != nil; ent = ent.Prev() {
|
|
||||||
keys[i] = ent.Value.key
|
|
||||||
i++
|
|
||||||
}
|
|
||||||
return keys
|
|
||||||
}
|
|
||||||
|
|
||||||
// Len returns the number of items in the cache.
|
|
||||||
func (c *LRU[K, V]) Len() int {
|
|
||||||
return c.evictList.Len()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Resize changes the cache size.
|
|
||||||
func (c *LRU[K, V]) Resize(ctx context.Context, size int) (evicted int) {
|
|
||||||
diff := c.Len() - size
|
|
||||||
if diff < 0 {
|
|
||||||
diff = 0
|
|
||||||
}
|
|
||||||
for i := 0; i < diff; i++ {
|
|
||||||
c.removeOldest(ctx)
|
|
||||||
}
|
|
||||||
c.size = size
|
|
||||||
return diff
|
|
||||||
}
|
|
||||||
|
|
||||||
// removeOldest removes the oldest item from the cache.
|
|
||||||
func (c *LRU[K, V]) removeOldest(ctx context.Context) {
|
|
||||||
ent := c.evictList.Back()
|
|
||||||
if ent != nil {
|
|
||||||
c.removeElement(ctx, ent)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// removeElement is used to remove a given list element from the cache
|
|
||||||
func (c *LRU[K, V]) removeElement(ctx context.Context, e *Element[entry[K, V]]) {
|
|
||||||
c.evictList.Remove(e)
|
|
||||||
kv := e.Value
|
|
||||||
delete(c.items, kv.key)
|
|
||||||
if c.onEvict != nil {
|
|
||||||
c.onEvict(ctx, kv.key, kv.value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user