chore: move apps to go.sour.is/tools
This commit is contained in:
parent
813c2e898d
commit
ee45a0fd49
37
.air.toml
37
.air.toml
|
@ -1,37 +0,0 @@
|
|||
root = "."
|
||||
testdata_dir = "testdata"
|
||||
tmp_dir = "tmp"
|
||||
|
||||
[build]
|
||||
args_bin = []
|
||||
bin = "./tmp/ev"
|
||||
cmd = "go build -o ./tmp/ev ./cmd/ev"
|
||||
delay = 1000
|
||||
exclude_dir = ["assets", "tmp", "vendor", "testdata", "data", "build"]
|
||||
exclude_file = []
|
||||
exclude_regex = ["_test.go"]
|
||||
exclude_unchanged = false
|
||||
follow_symlink = false
|
||||
full_bin = ""
|
||||
include_dir = []
|
||||
include_ext = ["go", "tpl", "tmpl", "html", "css"]
|
||||
kill_delay = "0s"
|
||||
log = "build-errors.log"
|
||||
send_interrupt = false
|
||||
stop_on_error = true
|
||||
|
||||
[color]
|
||||
app = ""
|
||||
build = "yellow"
|
||||
main = "magenta"
|
||||
runner = "green"
|
||||
watcher = "cyan"
|
||||
|
||||
[log]
|
||||
time = false
|
||||
|
||||
[misc]
|
||||
clean_on_exit = false
|
||||
|
||||
[screen]
|
||||
clear_on_rebuild = false
|
|
@ -1,5 +0,0 @@
|
|||
[lavana]
|
||||
lavana.sour.is
|
||||
|
||||
[kapha]
|
||||
kapha.sour.is
|
|
@ -1,16 +0,0 @@
|
|||
---
|
||||
- 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
|
37
.drone.yml
37
.drone.yml
|
@ -12,40 +12,3 @@ 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
|
||||
|
|
40
Makefile
40
Makefile
|
@ -1,40 +0,0 @@
|
|||
export PATH:=$(shell go env GOPATH)/bin:$(PATH)
|
||||
export EV_DATA=mem:
|
||||
export EV_HTTP=:8080
|
||||
export WEBFINGER_DOMAINS=localhost
|
||||
|
||||
.DEFAULT_GOAL := air
|
||||
|
||||
-include local.mk
|
||||
|
||||
|
||||
air: gen
|
||||
ifeq (, $(shell which air))
|
||||
go install github.com/cosmtrek/air@latest
|
||||
endif
|
||||
air ./cmd/ev
|
||||
|
||||
run:
|
||||
go build ./cmd/ev && ./ev
|
||||
|
||||
test:
|
||||
go test -cover -race ./...
|
||||
|
||||
|
||||
GQLS=gqlgen.yml
|
||||
GQLS:=$(GQLS) $(wildcard api/gql_ev/*.go)
|
||||
GQLS:=$(GQLS) $(wildcard pkg/*/*.graphqls)
|
||||
GQLS:=$(GQLS) $(wildcard app/*/*.graphqls)
|
||||
GQLS:=$(GQLS) $(wildcard app/*/*.go)
|
||||
GQLSRC=internal/graph/generated/generated.go
|
||||
|
||||
clean:
|
||||
rm -f "$(GQLSRC)"
|
||||
gen: gql
|
||||
gql: $(GQLSRC)
|
||||
$(GQLSRC): $(GQLS)
|
||||
ifeq (, $(shell which gqlgen))
|
||||
go install github.com/99designs/gqlgen@latest
|
||||
endif
|
||||
gqlgen
|
||||
|
|
@ -3,3 +3,5 @@
|
|||
This project is learnings in building an eventstore and applications around it.
|
||||
|
||||
Feel free to explore.
|
||||
|
||||
For examples of use of this see <go.sour.is/tools>
|
|
@ -1,3 +0,0 @@
|
|||
# App examples
|
||||
|
||||
These applications are to demonstrate how the EV library can be used.
|
|
@ -1,37 +0,0 @@
|
|||
scalar Time
|
||||
scalar Map
|
||||
|
||||
type Connection @goModel(model: "go.sour.is/pkg/gql.Connection") {
|
||||
paging: PageInfo!
|
||||
edges: [Edge!]!
|
||||
}
|
||||
input PageInput @goModel(model: "go.sour.is/pkg/gql.PageInput") {
|
||||
after: Int = 0
|
||||
before: Int
|
||||
count: Int = 30
|
||||
}
|
||||
type PageInfo @goModel(model: "go.sour.is/pkg/gql.PageInfo") {
|
||||
next: Boolean!
|
||||
prev: Boolean!
|
||||
|
||||
begin: Int!
|
||||
end: Int!
|
||||
}
|
||||
interface Edge @goModel(model: "go.sour.is/pkg/gql.Edge"){
|
||||
id: ID!
|
||||
}
|
||||
|
||||
directive @goModel(
|
||||
model: String
|
||||
models: [String!]
|
||||
) on OBJECT | INPUT_OBJECT | SCALAR | ENUM | INTERFACE | UNION
|
||||
|
||||
directive @goField(
|
||||
forceResolver: Boolean
|
||||
name: String
|
||||
) on INPUT_FIELD_DEFINITION | FIELD_DEFINITION
|
||||
|
||||
directive @goTag(
|
||||
key: String!
|
||||
value: String
|
||||
) on INPUT_FIELD_DEFINITION | FIELD_DEFINITION
|
|
@ -1,66 +0,0 @@
|
|||
package gql
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/99designs/gqlgen/graphql"
|
||||
"go.sour.is/pkg/gql"
|
||||
"go.sour.is/pkg/gql/resolver"
|
||||
|
||||
"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 {
|
||||
msgbus.MsgbusResolver
|
||||
salty.SaltyResolver
|
||||
gql_es.EventResolver
|
||||
}
|
||||
|
||||
// Query returns generated.QueryResolver implementation.
|
||||
func (r *Resolver) Query() generated.QueryResolver { return r }
|
||||
|
||||
// Query returns generated.QueryResolver implementation.
|
||||
func (r *Resolver) Mutation() generated.MutationResolver { return r }
|
||||
|
||||
// Subscription returns generated.SubscriptionResolver implementation.
|
||||
func (r *Resolver) Subscription() generated.SubscriptionResolver { return r }
|
||||
|
||||
// func (r *Resolver) isResolver() {}
|
||||
func (r *Resolver) ExecutableSchema() graphql.ExecutableSchema {
|
||||
return generated.NewExecutableSchema(generated.Config{Resolvers: r})
|
||||
}
|
||||
func (r *Resolver) BaseResolver() resolver.IsResolver {
|
||||
return &noop{}
|
||||
}
|
||||
|
||||
type noop struct{}
|
||||
|
||||
var _ msgbus.MsgbusResolver = (*noop)(nil)
|
||||
var _ salty.SaltyResolver = (*noop)(nil)
|
||||
var _ gql_es.EventResolver = (*noop)(nil)
|
||||
|
||||
func (*noop) IsResolver() {}
|
||||
func (*noop) CreateSaltyUser(ctx context.Context, nick string, pubkey string) (*salty.SaltyUser, error) {
|
||||
panic("not implemented")
|
||||
}
|
||||
func (*noop) Posts(ctx context.Context, name, tag string, paging *gql.PageInput) (*gql.Connection, error) {
|
||||
panic("not implemented")
|
||||
}
|
||||
func (*noop) SaltyUser(ctx context.Context, nick string) (*salty.SaltyUser, error) {
|
||||
panic("not implemented")
|
||||
}
|
||||
func (*noop) PostAdded(ctx context.Context, name, tag string, after int64) (<-chan *msgbus.PostEvent, error) {
|
||||
panic("not implemented")
|
||||
}
|
||||
func (*noop) Events(ctx context.Context, streamID string, paging *gql.PageInput) (*gql.Connection, error) {
|
||||
panic("not implemented")
|
||||
}
|
||||
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")
|
||||
}
|
|
@ -1,9 +0,0 @@
|
|||
# extend type Query{
|
||||
# namespaces: [String!]!
|
||||
# keys(namespace: String!) [String!]!
|
||||
# get(namespace: String! keys: [String!]) [String]!
|
||||
# }
|
||||
|
||||
# extend type Mutation{
|
||||
# set(namespace: String! key: String! value: String): Bool!
|
||||
# }
|
|
@ -1 +0,0 @@
|
|||
package mercury
|
|
@ -1,16 +0,0 @@
|
|||
extend type Query {
|
||||
posts(name: String!, tag: String! = "", paging: PageInput): Connection!
|
||||
}
|
||||
extend type Subscription {
|
||||
"""after == 0 start from begining, after == -1 start from end"""
|
||||
postAdded(name: String!, tag: String! = "", after: Int! = -1): PostEvent
|
||||
}
|
||||
type PostEvent implements Edge @goModel(model: "go.sour.is/ev/app/msgbus.PostEvent") {
|
||||
id: ID!
|
||||
|
||||
payload: String!
|
||||
payloadJSON: Map!
|
||||
tags: [String!]!
|
||||
|
||||
meta: Meta!
|
||||
}
|
|
@ -1,597 +0,0 @@
|
|||
package msgbus
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"hash/fnv"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
"go.opentelemetry.io/otel/metric"
|
||||
"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 {
|
||||
es *ev.EventStore
|
||||
|
||||
m_gql_posts metric.Int64Counter
|
||||
m_gql_post_added metric.Int64Counter
|
||||
m_gql_post_added_event metric.Int64Counter
|
||||
m_req_time metric.Int64Histogram
|
||||
}
|
||||
|
||||
type MsgbusResolver interface {
|
||||
Posts(ctx context.Context, name, tag string, paging *gql.PageInput) (*gql.Connection, error)
|
||||
PostAdded(ctx context.Context, name, tag string, after int64) (<-chan *PostEvent, error)
|
||||
IsResolver()
|
||||
}
|
||||
|
||||
func New(ctx context.Context, es *ev.EventStore) (*service, error) {
|
||||
ctx, span := lg.Span(ctx)
|
||||
defer span.End()
|
||||
|
||||
if err := event.Register(ctx, &PostEvent{}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := event.RegisterName(ctx, "domain.PostEvent", &PostEvent{}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
m := lg.Meter(ctx)
|
||||
|
||||
svc := &service{es: es}
|
||||
|
||||
var err, errs error
|
||||
svc.m_gql_posts, err = m.Int64Counter("msgbus_posts",
|
||||
metric.WithDescription("msgbus graphql posts requests"),
|
||||
)
|
||||
errs = multierr.Append(errs, err)
|
||||
|
||||
svc.m_gql_post_added, err = m.Int64Counter("msgbus_post_added",
|
||||
metric.WithDescription("msgbus graphql post added subcription requests"),
|
||||
)
|
||||
errs = multierr.Append(errs, err)
|
||||
|
||||
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)
|
||||
|
||||
span.RecordError(err)
|
||||
|
||||
return svc, errs
|
||||
}
|
||||
|
||||
var upgrader = websocket.Upgrader{
|
||||
WriteBufferSize: 4096,
|
||||
CheckOrigin: func(r *http.Request) bool {
|
||||
return true
|
||||
},
|
||||
}
|
||||
|
||||
func (s *service) IsResolver() {}
|
||||
func (s *service) RegisterHTTP(mux *http.ServeMux) {
|
||||
mux.Handle("/inbox/", lg.Htrace(http.StripPrefix("/inbox/", s), "inbox"))
|
||||
}
|
||||
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)
|
||||
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
if r.Header.Get("Upgrade") == "websocket" {
|
||||
s.websocket(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
s.get(w, r)
|
||||
case http.MethodPost, http.MethodPut:
|
||||
s.post(w, r)
|
||||
default:
|
||||
w.WriteHeader(http.StatusMethodNotAllowed)
|
||||
}
|
||||
}
|
||||
|
||||
// Posts is the resolver for the events field.
|
||||
func (s *service) Posts(ctx context.Context, name, tag string, paging *gql.PageInput) (*gql.Connection, error) {
|
||||
ctx, span := lg.Span(ctx)
|
||||
defer span.End()
|
||||
|
||||
s.m_gql_posts.Add(ctx, 1)
|
||||
|
||||
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 {
|
||||
span.RecordError(err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
edges := make([]gql.Edge, 0, len(lis))
|
||||
for i := range lis {
|
||||
span.AddEvent(fmt.Sprint("post ", i, " of ", len(lis)))
|
||||
e := lis[i]
|
||||
|
||||
post, ok := e.(*PostEvent)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
edges = append(edges, post)
|
||||
}
|
||||
|
||||
var first, last uint64
|
||||
if first, err = s.es.FirstIndex(ctx, streamID); err != nil {
|
||||
span.RecordError(err)
|
||||
return nil, err
|
||||
}
|
||||
if last, err = s.es.LastIndex(ctx, streamID); err != nil {
|
||||
span.RecordError(err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &gql.Connection{
|
||||
Paging: &gql.PageInfo{
|
||||
Next: lis.Last().EventMeta().Position < last,
|
||||
Prev: lis.First().EventMeta().Position > first,
|
||||
Begin: lis.First().EventMeta().Position,
|
||||
End: lis.Last().EventMeta().Position,
|
||||
},
|
||||
Edges: edges,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *service) PostAdded(ctx context.Context, name, tag string, after int64) (<-chan *PostEvent, error) {
|
||||
ctx, span := lg.Span(ctx)
|
||||
defer span.End()
|
||||
|
||||
r.m_gql_post_added.Add(ctx, 1)
|
||||
|
||||
es := r.es.EventStream()
|
||||
if es == nil {
|
||||
return nil, fmt.Errorf("EventStore does not implement streaming")
|
||||
}
|
||||
|
||||
streamID := withTag("post-"+name, tag)
|
||||
|
||||
sub, err := es.Subscribe(ctx, streamID, after)
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ch := make(chan *PostEvent)
|
||||
|
||||
go func() {
|
||||
ctx, span := lg.Span(ctx)
|
||||
defer span.End()
|
||||
|
||||
{
|
||||
ctx, span := lg.Fork(ctx)
|
||||
defer func() {
|
||||
defer span.End()
|
||||
ctx, cancel := context.WithTimeout(ctx, 1*time.Second)
|
||||
defer cancel()
|
||||
err := sub.Close(ctx)
|
||||
span.RecordError(err)
|
||||
}()
|
||||
}
|
||||
|
||||
for <-sub.Recv(ctx) {
|
||||
events, err := sub.Events(ctx)
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
break
|
||||
}
|
||||
span.AddEvent(fmt.Sprintf("received %d events", len(events)))
|
||||
r.m_gql_post_added_event.Add(ctx, int64(len(events)))
|
||||
|
||||
for _, e := range events {
|
||||
if p, ok := e.(*PostEvent); ok {
|
||||
select {
|
||||
case ch <- p:
|
||||
continue
|
||||
case <-ctx.Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return ch, nil
|
||||
}
|
||||
|
||||
func (s *service) get(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
ctx, span := lg.Span(ctx)
|
||||
defer span.End()
|
||||
|
||||
start := time.Now()
|
||||
defer s.m_req_time.Record(ctx, time.Since(start).Milliseconds())
|
||||
|
||||
name, tag, _ := strings.Cut(r.URL.Path, "/")
|
||||
if name == "" {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
streamID := withTag("post-"+name, tag)
|
||||
|
||||
var first event.Event = event.NilEvent
|
||||
if lis, err := s.es.Read(ctx, streamID, 0, 1); err == nil && len(lis) > 0 {
|
||||
first = lis[0]
|
||||
}
|
||||
|
||||
var pos, count int64 = 0, ev.AllEvents
|
||||
qry := r.URL.Query()
|
||||
|
||||
if i, err := strconv.ParseInt(qry.Get("index"), 10, 64); err == nil && i > 1 {
|
||||
pos = i - 1
|
||||
}
|
||||
if i, err := strconv.ParseInt(qry.Get("pos"), 10, 64); err == nil {
|
||||
pos = i
|
||||
}
|
||||
if i, err := strconv.ParseInt(qry.Get("n"), 10, 64); err == nil {
|
||||
count = i
|
||||
}
|
||||
|
||||
span.AddEvent(fmt.Sprint("GET topic=", streamID, " idx=", pos, " n=", count))
|
||||
events, err := s.es.Read(ctx, streamID, pos, count)
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if strings.Contains(r.Header.Get("Accept"), "application/json") {
|
||||
w.Header().Add("Content-Type", "application/json")
|
||||
|
||||
if err = encodeJSON(w, first, events...); err != nil {
|
||||
span.RecordError(err)
|
||||
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
for i := range events {
|
||||
fmt.Fprintln(w, events[i])
|
||||
}
|
||||
}
|
||||
func (s *service) post(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
ctx, span := lg.Span(ctx)
|
||||
defer span.End()
|
||||
|
||||
start := time.Now()
|
||||
defer s.m_req_time.Record(ctx, time.Since(start).Milliseconds())
|
||||
|
||||
name, tags, _ := strings.Cut(r.URL.Path, "/")
|
||||
if name == "" {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
var first event.Event = event.NilEvent
|
||||
if lis, err := s.es.Read(ctx, "post-"+name, 0, 1); err == nil && len(lis) > 0 {
|
||||
first = lis[0]
|
||||
}
|
||||
|
||||
b, err := io.ReadAll(io.LimitReader(r.Body, 64*1024))
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
r.Body.Close()
|
||||
|
||||
if name == "" {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
events := event.NewEvents(&PostEvent{
|
||||
payload: b,
|
||||
tags: fields(tags),
|
||||
})
|
||||
|
||||
_, err = s.es.Append(ctx, "post-"+name, events)
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if first == event.NilEvent {
|
||||
first = events.First()
|
||||
}
|
||||
|
||||
m := events.First().EventMeta()
|
||||
span.AddEvent(fmt.Sprint("POST topic=", name, " tags=", tags, " idx=", m.Position, " id=", m.EventID))
|
||||
|
||||
w.WriteHeader(http.StatusAccepted)
|
||||
if strings.Contains(r.Header.Get("Accept"), "application/json") {
|
||||
w.Header().Add("Content-Type", "application/json")
|
||||
if err = encodeJSON(w, first, events...); err != nil {
|
||||
span.RecordError(err)
|
||||
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
span.AddEvent("finish response")
|
||||
|
||||
w.Header().Add("Content-Type", "text/plain")
|
||||
fmt.Fprintf(w, "OK %d %s", m.Position, m.EventID)
|
||||
}
|
||||
func (s *service) websocket(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
ctx, span := lg.Span(ctx)
|
||||
defer span.End()
|
||||
|
||||
name, tag, _ := strings.Cut(r.URL.Path, "/")
|
||||
if name == "" {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
streamID := withTag("post-"+name, tag)
|
||||
|
||||
var first event.Event = event.NilEvent
|
||||
if lis, err := s.es.Read(ctx, streamID, 0, 1); err == nil && len(lis) > 0 {
|
||||
first = lis[0]
|
||||
}
|
||||
|
||||
var pos int64 = 0
|
||||
qry := r.URL.Query()
|
||||
|
||||
if i, err := strconv.ParseInt(qry.Get("index"), 10, 64); err == nil && i > 0 {
|
||||
pos = i - 1
|
||||
}
|
||||
|
||||
span.AddEvent(fmt.Sprint("WS topic=", streamID, " idx=", pos))
|
||||
|
||||
c, err := upgrader.Upgrade(w, r, nil)
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
return
|
||||
}
|
||||
defer c.Close()
|
||||
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
c.SetCloseHandler(func(code int, text string) error {
|
||||
cancel()
|
||||
return nil
|
||||
})
|
||||
go func() {
|
||||
for {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return
|
||||
}
|
||||
mt, message, err := c.ReadMessage()
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
return
|
||||
}
|
||||
span.AddEvent(fmt.Sprintf("recv: %d %s", mt, message))
|
||||
}
|
||||
}()
|
||||
|
||||
es := s.es.EventStream()
|
||||
if es == nil {
|
||||
span.AddEvent("EventStore does not implement streaming")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
sub, err := es.Subscribe(ctx, streamID, pos)
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
return
|
||||
}
|
||||
{
|
||||
ctx, span := lg.Fork(ctx)
|
||||
defer func() {
|
||||
defer span.End()
|
||||
ctx, cancel := context.WithTimeout(ctx, 1*time.Second)
|
||||
defer cancel()
|
||||
err := sub.Close(ctx)
|
||||
span.RecordError(err)
|
||||
}()
|
||||
}
|
||||
|
||||
span.AddEvent("start ws")
|
||||
for <-sub.Recv(ctx) {
|
||||
events, err := sub.Events(ctx)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
span.AddEvent(fmt.Sprint("got events ", len(events)))
|
||||
for i := range events {
|
||||
e, ok := events[i].(*PostEvent)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
span.AddEvent(fmt.Sprint("send", i, e.String()))
|
||||
|
||||
var b bytes.Buffer
|
||||
if err = encodeJSON(&b, first, e); err != nil {
|
||||
span.RecordError(err)
|
||||
}
|
||||
|
||||
err = c.WriteMessage(websocket.TextMessage, b.Bytes())
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type PostEvent struct {
|
||||
payload []byte
|
||||
tags []string
|
||||
|
||||
event.IsEvent
|
||||
}
|
||||
|
||||
func (e *PostEvent) Values() any {
|
||||
if e == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return struct {
|
||||
Payload []byte `json:"payload"`
|
||||
Tags []string `json:"tags,omitempty"`
|
||||
}{
|
||||
Payload: e.payload,
|
||||
Tags: e.tags,
|
||||
}
|
||||
}
|
||||
func (e *PostEvent) MarshalBinary() ([]byte, error) {
|
||||
j := e.Values()
|
||||
return json.Marshal(&j)
|
||||
}
|
||||
func (e *PostEvent) UnmarshalBinary(b []byte) error {
|
||||
j := struct {
|
||||
Payload []byte
|
||||
Tags []string
|
||||
}{}
|
||||
err := json.Unmarshal(b, &j)
|
||||
e.payload = j.Payload
|
||||
e.tags = j.Tags
|
||||
|
||||
return err
|
||||
}
|
||||
func (e *PostEvent) MarshalJSON() ([]byte, error) { return e.MarshalBinary() }
|
||||
func (e *PostEvent) UnmarshalJSON(b []byte) error { return e.UnmarshalBinary(b) }
|
||||
|
||||
func (e *PostEvent) ID() string { return e.EventMeta().GetEventID() }
|
||||
func (e *PostEvent) Tags() []string { return e.tags }
|
||||
func (e *PostEvent) Payload() string { return string(e.payload) }
|
||||
func (e *PostEvent) PayloadJSON(ctx context.Context) (m map[string]interface{}, err error) {
|
||||
err = json.Unmarshal([]byte(e.payload), &m)
|
||||
return
|
||||
}
|
||||
func (e *PostEvent) Meta() event.Meta { return e.EventMeta() }
|
||||
func (e *PostEvent) IsEdge() {}
|
||||
|
||||
func (e *PostEvent) String() string {
|
||||
var b bytes.Buffer
|
||||
|
||||
b.WriteString(strconv.FormatUint(e.EventMeta().Position, 10))
|
||||
b.WriteRune('\t')
|
||||
|
||||
b.WriteString(e.EventMeta().EventID.String())
|
||||
b.WriteRune('\t')
|
||||
b.WriteString(string(e.payload))
|
||||
if len(e.tags) > 0 {
|
||||
b.WriteRune('\t')
|
||||
b.WriteString(strings.Join(e.tags, ","))
|
||||
}
|
||||
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func fields(s string) []string {
|
||||
if s == "" {
|
||||
return nil
|
||||
}
|
||||
return strings.Split(s, ";")
|
||||
}
|
||||
|
||||
func encodeJSON(w io.Writer, first event.Event, events ...event.Event) error {
|
||||
out := make([]struct {
|
||||
ID uint64 `json:"id"`
|
||||
Payload []byte `json:"payload"`
|
||||
Created string `json:"created"`
|
||||
Tags []string `json:"tags"`
|
||||
Topic struct {
|
||||
Name string `json:"name"`
|
||||
TTL uint64 `json:"ttl"`
|
||||
Seq uint64 `json:"seq"`
|
||||
Created string `json:"created"`
|
||||
} `json:"topic"`
|
||||
}, len(events))
|
||||
|
||||
for i := range events {
|
||||
e, ok := events[i].(*PostEvent)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
out[i].ID = e.EventMeta().Position
|
||||
out[i].Created = e.EventMeta().Created().Format(time.RFC3339Nano)
|
||||
out[i].Payload = e.payload
|
||||
out[i].Tags = e.tags
|
||||
out[i].Topic.Name = strings.TrimPrefix(e.EventMeta().StreamID, "post-")
|
||||
out[i].Topic.Created = first.EventMeta().Created().Format(time.RFC3339Nano)
|
||||
out[i].Topic.Seq = e.EventMeta().Position
|
||||
}
|
||||
|
||||
if len(out) == 1 {
|
||||
return json.NewEncoder(w).Encode(out[0])
|
||||
}
|
||||
|
||||
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))
|
||||
}
|
|
@ -1,25 +0,0 @@
|
|||
package msgbus
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestUnmarshal(t *testing.T) {
|
||||
m := &PostEvent{}
|
||||
s := `{"Payload":"QkVHSU4gU0FMVFBBQ0sgRU5DUllQVEVEIE1FU1NBR0UuIGtlRElETVFXWXZWUjU4QiBGVGZUZURRTkhzT2ZuZWMgWUV1dkNYSTNoVDBYZzZKIDJxeXBJcmdsT3ZlZjlqciA0dHVQaWpJRVgxdlpoTEkgUTVKTzNvYVY5cnNna01BIEFOeTJwYjg2Qkd6N0JGMCA4MXJuZk9OV2RRM0VldFAgSmU0ZFlHeUI4NkRydkVrIGNqcFpoajNmcEJUcDdiZiBpMktwRDJQM1kzNVJBQU8gWmIyZGZtOVpneHZNSVJ2IDJsVVRCWTQxVEtZNkJhTyB2NGVIeXF1MENjQkR4dW8gSEZIekxJd3BBb3ZoRGt1IGFJdXRZYzdhZ3puMUxvNCBZQWFyUDZxVVVtTVlrQXAgYkdSYTZLZWVOa3ZzTDdMIHFoMWd6WUlnS2l6cW51eCB1SVQ0QTdaU1BscWxlR1IgbTk3M2ZoNUduWEZTM3MwIDJzQ2FvclpmN2c1RUo5TiBlS1hkZkFSMWF6TVRBek8gSmNEM1hDNDBwVTRpaG9mIE8wYnB2RU1UOVlUb3ZOWCBobVUxZWZ6enpyMUFDdXcgWExwcUhlVXNXdEtGcXRnIHdyWEZleExBYU50T21jRSBOeFFDUi4gRU5EIFNBTFRQQUNLIEVOQ1JZUFRFRCBNRVNTQUdFLgo=","Tags":null}`
|
||||
|
||||
err := json.Unmarshal([]byte(s), m)
|
||||
t.Log(err)
|
||||
}
|
||||
|
||||
func TestMarshal(t *testing.T) {
|
||||
m := &PostEvent{
|
||||
payload: []byte("QkVHSU4gU0FMVFBBQ0sgRU5DUllQVEVEIE1FU1NBR0UuIGtlRElETVFXWXZWUjU4QiBGVGZUZURRTkhzT2ZuZWMgWUV1dkNYSTNoVDBYZzZKIDJxeXBJcmdsT3ZlZjlqciA0dHVQaWpJRVgxdlpoTEkgUTVKTzNvYVY5cnNna01BIEFOeTJwYjg2Qkd6N0JGMCA4MXJuZk9OV2RRM0VldFAgSmU0ZFlHeUI4NkRydkVrIGNqcFpoajNmcEJUcDdiZiBpMktwRDJQM1kzNVJBQU8gWmIyZGZtOVpneHZNSVJ2IDJsVVRCWTQxVEtZNkJhTyB2NGVIeXF1MENjQkR4dW8gSEZIekxJd3BBb3ZoRGt1IGFJdXRZYzdhZ3puMUxvNCBZQWFyUDZxVVVtTVlrQXAgYkdSYTZLZWVOa3ZzTDdMIHFoMWd6WUlnS2l6cW51eCB1SVQ0QTdaU1BscWxlR1IgbTk3M2ZoNUduWEZTM3MwIDJzQ2FvclpmN2c1RUo5TiBlS1hkZkFSMWF6TVRBek8gSmNEM1hDNDBwVTRpaG9mIE8wYnB2RU1UOVlUb3ZOWCBobVUxZWZ6enpyMUFDdXcgWExwcUhlVXNXdEtGcXRnIHdyWEZleExBYU50T21jRSBOeFFDUi4gRU5EIFNBTFRQQUNLIEVOQ1JZUFRFRCBNRVNTQUdFLgo="),
|
||||
}
|
||||
b, err := json.Marshal(m)
|
||||
t.Log(err)
|
||||
|
||||
err = json.Unmarshal(b, m)
|
||||
t.Log(err)
|
||||
}
|
6
app/peerfinder/assets/bootstrap.min.css
vendored
6
app/peerfinder/assets/bootstrap.min.css
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -1,58 +0,0 @@
|
|||
/* 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) {
|
||||
|
||||
}
|
|
@ -1,56 +0,0 @@
|
|||
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)
|
|
@ -1,111 +0,0 @@
|
|||
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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,230 +0,0 @@
|
|||
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)
|
||||
}
|
|
@ -1,713 +0,0 @@
|
|||
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
|
||||
// }
|
|
@ -1,216 +0,0 @@
|
|||
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
|
||||
}
|
|
@ -1,38 +0,0 @@
|
|||
{{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}}
|
|
@ -1,65 +0,0 @@
|
|||
{{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}}
|
|
@ -1,50 +0,0 @@
|
|||
{{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,179 +0,0 @@
|
|||
package peerfinder
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"go.sour.is/pkg/lg"
|
||||
"go.sour.is/pkg/locker"
|
||||
"go.uber.org/multierr"
|
||||
|
||||
"go.sour.is/ev"
|
||||
"go.sour.is/ev/pkg/event"
|
||||
)
|
||||
|
||||
const (
|
||||
aggInfo = "pf-info"
|
||||
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 {
|
||||
es *ev.EventStore
|
||||
statusURL string
|
||||
|
||||
state *locker.Locked[state]
|
||||
up atomic.Bool
|
||||
stop func()
|
||||
}
|
||||
|
||||
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)
|
||||
defer span.End()
|
||||
|
||||
loadTemplates()
|
||||
|
||||
if err := event.Register(ctx, &RequestSubmitted{}, &ResultSubmitted{}, &VersionChanged{}); err != nil {
|
||||
span.RecordError(err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
svc := &service{
|
||||
es: es,
|
||||
statusURL: statusURL,
|
||||
state: locker.New(&state{
|
||||
peers: make(map[string]*Peer),
|
||||
requests: make(map[string]*Request),
|
||||
})}
|
||||
|
||||
return svc, nil
|
||||
}
|
||||
func (s *service) loadResult(ctx context.Context, request *Request) (*Request, error) {
|
||||
if request == nil {
|
||||
return request, nil
|
||||
}
|
||||
|
||||
return request, s.state.Use(ctx, func(ctx context.Context, t *state) error {
|
||||
|
||||
for i := range request.Responses {
|
||||
res := request.Responses[i]
|
||||
if peer, ok := t.peers[res.PeerID]; ok {
|
||||
res.Peer = peer
|
||||
res.Peer.ID = ""
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
func (s *service) Run(ctx context.Context) (err error) {
|
||||
var errs error
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
for {
|
||||
var events event.Events
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
s.state.Use(ctx, func(ctx context.Context, state *state) error {
|
||||
return state.ApplyEvents(events)
|
||||
})
|
||||
events = events[:0]
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
func (s *state) ApplyEvents(events event.Events) error {
|
||||
for _, e := range events {
|
||||
switch e := e.(type) {
|
||||
case *RequestSubmitted:
|
||||
if _, ok := s.requests[e.RequestID()]; !ok {
|
||||
s.requests[e.RequestID()] = &Request{}
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func Projector(e event.Event) []event.Event {
|
||||
m := e.EventMeta()
|
||||
streamID := m.StreamID
|
||||
streamPos := m.Position
|
||||
|
||||
switch e := e.(type) {
|
||||
case *RequestSubmitted:
|
||||
e1 := event.NewPtr(streamID, streamPos)
|
||||
event.SetStreamID(aggRequest(e.RequestID()), e1)
|
||||
|
||||
return []event.Event{e1}
|
||||
case *ResultSubmitted:
|
||||
e1 := event.NewPtr(streamID, streamPos)
|
||||
event.SetStreamID(aggRequest(e.RequestID), e1)
|
||||
|
||||
e2 := event.NewPtr(streamID, streamPos)
|
||||
event.SetStreamID(aggPeer(e.PeerID), e2)
|
||||
|
||||
return []event.Event{e1, e2}
|
||||
}
|
||||
return nil
|
||||
}
|
|
@ -1,241 +0,0 @@
|
|||
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
|
||||
}
|
|
@ -1,156 +0,0 @@
|
|||
package salty
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/keys-pub/keys"
|
||||
|
||||
"go.sour.is/pkg/lg"
|
||||
)
|
||||
|
||||
// Config represents a Salty Config for a User which at a minimum is required
|
||||
// to have an Endpoint and Key (Public Key)
|
||||
type Config struct {
|
||||
Endpoint string `json:"endpoint"`
|
||||
Key string `json:"key"`
|
||||
}
|
||||
|
||||
type Capabilities struct {
|
||||
AcceptEncoding string
|
||||
}
|
||||
|
||||
func (c Capabilities) String() string {
|
||||
if c.AcceptEncoding == "" {
|
||||
return "<nil>"
|
||||
}
|
||||
return fmt.Sprint("accept-encoding: ", c.AcceptEncoding)
|
||||
}
|
||||
|
||||
type Addr struct {
|
||||
User string
|
||||
Domain string
|
||||
|
||||
capabilities Capabilities
|
||||
discoveredDomain string
|
||||
dns DNSResolver
|
||||
endpoint *url.URL
|
||||
key *keys.EdX25519PublicKey
|
||||
}
|
||||
|
||||
// ParseAddr parsers a Salty Address for a user into it's user and domain
|
||||
// parts and returns an Addr object with the User and Domain and a method
|
||||
// for returning the expected User's Well-Known URI
|
||||
func (s *service) ParseAddr(addr string) (*Addr, error) {
|
||||
parts := strings.Split(strings.ToLower(addr), "@")
|
||||
if len(parts) != 2 {
|
||||
return nil, fmt.Errorf("expected nick@domain found %q", addr)
|
||||
}
|
||||
|
||||
return &Addr{User: parts[0], Domain: parts[1], dns: s.dns}, nil
|
||||
}
|
||||
|
||||
func (a *Addr) String() string {
|
||||
return fmt.Sprintf("%s@%s", a.User, a.Domain)
|
||||
}
|
||||
|
||||
// Hash returns the Hex(SHA256Sum()) of the Address
|
||||
func (a *Addr) Hash() string {
|
||||
return fmt.Sprintf("%x", sha256.Sum256([]byte(strings.ToLower(a.String()))))
|
||||
}
|
||||
|
||||
// URI returns the Well-Known URI for this Addr
|
||||
func (a *Addr) URI() string {
|
||||
return fmt.Sprintf("https://%s/.well-known/salty/%s.json", a.DiscoveredDomain(), a.User)
|
||||
}
|
||||
|
||||
// HashURI returns the Well-Known HashURI for this Addr
|
||||
func (a *Addr) HashURI() string {
|
||||
return fmt.Sprintf("https://%s/.well-known/salty/%s.json", a.DiscoveredDomain(), a.Hash())
|
||||
}
|
||||
|
||||
// DiscoveredDomain returns the discovered domain (if any) of fallbacks to the Domain
|
||||
func (a *Addr) DiscoveredDomain() string {
|
||||
if a.discoveredDomain != "" {
|
||||
return a.discoveredDomain
|
||||
}
|
||||
return a.Domain
|
||||
}
|
||||
|
||||
func (a *Addr) Refresh(ctx context.Context) error {
|
||||
ctx, span := lg.Span(ctx)
|
||||
defer span.End()
|
||||
|
||||
span.AddEvent(fmt.Sprintf("Looking up SRV record for _salty._tcp.%s", a.Domain))
|
||||
if _, srv, err := a.dns.LookupSRV(ctx, "salty", "tcp", a.Domain); err == nil {
|
||||
if len(srv) > 0 {
|
||||
a.discoveredDomain = strings.TrimSuffix(srv[0].Target, ".")
|
||||
}
|
||||
span.AddEvent(fmt.Sprintf("Discovered salty services %s", a.discoveredDomain))
|
||||
} else if err != nil {
|
||||
span.RecordError(fmt.Errorf("error looking up SRV record for _salty._tcp.%s : %s", a.Domain, err))
|
||||
}
|
||||
|
||||
config, cap, err := fetchConfig(ctx, a.HashURI())
|
||||
if err != nil {
|
||||
// Fallback to plain user nick
|
||||
span.RecordError(err)
|
||||
|
||||
config, cap, err = fetchConfig(ctx, a.URI())
|
||||
}
|
||||
if err != nil {
|
||||
err = fmt.Errorf("error looking up user %s: %w", a, err)
|
||||
span.RecordError(err)
|
||||
return err
|
||||
}
|
||||
key, err := keys.NewEdX25519PublicKeyFromID(keys.ID(config.Key))
|
||||
if err != nil {
|
||||
err = fmt.Errorf("error parsing public key %s: %w", config.Key, err)
|
||||
span.RecordError(err)
|
||||
return err
|
||||
}
|
||||
a.key = key
|
||||
|
||||
u, err := url.Parse(config.Endpoint)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("error parsing endpoint %s: %w", config.Endpoint, err)
|
||||
span.RecordError(err)
|
||||
return err
|
||||
}
|
||||
a.endpoint = u
|
||||
a.capabilities = cap
|
||||
|
||||
span.AddEvent(fmt.Sprintf("Discovered endpoint: %v", a.endpoint))
|
||||
span.AddEvent(fmt.Sprintf("Discovered capability: %v", a.capabilities))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func fetchConfig(ctx context.Context, addr string) (config Config, cap Capabilities, err error) {
|
||||
ctx, span := lg.Span(ctx)
|
||||
defer span.End()
|
||||
|
||||
var req *http.Request
|
||||
req, err = http.NewRequestWithContext(ctx, http.MethodGet, addr, nil)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
res, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err = json.NewDecoder(res.Body).Decode(&config); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
cap.AcceptEncoding = res.Header.Get("Accept-Encoding")
|
||||
|
||||
return
|
||||
}
|
|
@ -1,94 +0,0 @@
|
|||
package salty
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/keys-pub/keys"
|
||||
"github.com/oklog/ulid/v2"
|
||||
|
||||
"go.sour.is/ev/pkg/event"
|
||||
"go.sour.is/pkg/gql"
|
||||
)
|
||||
|
||||
type SaltyUser struct {
|
||||
pubkey *keys.EdX25519PublicKey
|
||||
inbox ulid.ULID
|
||||
|
||||
event.IsAggregate
|
||||
}
|
||||
|
||||
var _ event.Aggregate = (*SaltyUser)(nil)
|
||||
|
||||
// ApplyEvent applies the event to the aggrigate state
|
||||
func (a *SaltyUser) ApplyEvent(lis ...event.Event) {
|
||||
for _, e := range lis {
|
||||
switch e := e.(type) {
|
||||
case *UserRegistered:
|
||||
// a.name = e.Name
|
||||
a.pubkey = e.Pubkey
|
||||
a.inbox = e.EventMeta().EventID
|
||||
// a.SetStreamID(a.streamID())
|
||||
default:
|
||||
log.Printf("unknown event %T", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (a *SaltyUser) OnUserRegister(pubkey *keys.EdX25519PublicKey) error {
|
||||
if err := event.NotExists(a); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
event.Raise(a, &UserRegistered{Pubkey: pubkey})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *SaltyUser) Inbox() string { return a.inbox.String() }
|
||||
func (a *SaltyUser) Pubkey() string { return a.pubkey.String() }
|
||||
func (s *SaltyUser) Endpoint(ctx context.Context) (string, error) {
|
||||
svc := gql.FromContext[contextKey, *service](ctx, saltyKey)
|
||||
return url.JoinPath(svc.BaseURL(), s.inbox.String())
|
||||
}
|
||||
|
||||
type UserRegistered struct {
|
||||
Name string
|
||||
Pubkey *keys.EdX25519PublicKey
|
||||
|
||||
event.IsEvent
|
||||
}
|
||||
|
||||
var _ event.Event = (*UserRegistered)(nil)
|
||||
|
||||
func (e *UserRegistered) MarshalBinary() (text []byte, err error) {
|
||||
var b bytes.Buffer
|
||||
b.WriteString(e.Name)
|
||||
b.WriteRune('\t')
|
||||
b.WriteString(e.Pubkey.String())
|
||||
|
||||
return b.Bytes(), nil
|
||||
}
|
||||
func (e *UserRegistered) UnmarshalBinary(b []byte) error {
|
||||
name, pub, ok := bytes.Cut(b, []byte{'\t'})
|
||||
if !ok {
|
||||
return fmt.Errorf("parse error")
|
||||
}
|
||||
|
||||
var err error
|
||||
e.Name = string(name)
|
||||
e.Pubkey, err = keys.NewEdX25519PublicKeyFromID(keys.ID(pub))
|
||||
|
||||
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)
|
||||
}
|
|
@ -1,13 +0,0 @@
|
|||
extend type Query {
|
||||
saltyUser(nick: String!): SaltyUser
|
||||
}
|
||||
|
||||
extend type Mutation {
|
||||
createSaltyUser(nick: String! pubkey: String!): SaltyUser
|
||||
}
|
||||
|
||||
type SaltyUser @goModel(model: "go.sour.is/ev/app/salty.SaltyUser"){
|
||||
pubkey: String!
|
||||
inbox: String!
|
||||
endpoint: String!
|
||||
}
|
|
@ -1,397 +0,0 @@
|
|||
package salty
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/keys-pub/keys"
|
||||
"go.mills.io/saltyim"
|
||||
"go.opentelemetry.io/otel/metric"
|
||||
"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 {
|
||||
LookupSRV(ctx context.Context, service, proto, name string) (string, []*net.SRV, error)
|
||||
}
|
||||
|
||||
type service struct {
|
||||
baseURL string
|
||||
es *ev.EventStore
|
||||
dns DNSResolver
|
||||
|
||||
m_create_user metric.Int64Counter
|
||||
m_get_user metric.Int64Counter
|
||||
m_api_ping metric.Int64Counter
|
||||
m_api_register metric.Int64Counter
|
||||
m_api_lookup metric.Int64Counter
|
||||
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 {
|
||||
name string
|
||||
}
|
||||
|
||||
var saltyKey = contextKey{"salty"}
|
||||
|
||||
type SaltyResolver interface {
|
||||
CreateSaltyUser(ctx context.Context, nick string, pub string) (*SaltyUser, error)
|
||||
SaltyUser(ctx context.Context, nick string) (*SaltyUser, error)
|
||||
IsResolver()
|
||||
}
|
||||
|
||||
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, &UserRegistered{}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := event.RegisterName(ctx, "domain.UserRegistered", &UserRegistered{}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
m := lg.Meter(ctx)
|
||||
|
||||
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
|
||||
svc.m_create_user, err = m.Int64Counter("salty_create_user",
|
||||
metric.WithDescription("salty create user graphql called"),
|
||||
)
|
||||
errs = multierr.Append(errs, err)
|
||||
|
||||
svc.m_get_user, err = m.Int64Counter("salty_get_user",
|
||||
metric.WithDescription("salty get user graphql called"),
|
||||
)
|
||||
errs = multierr.Append(errs, err)
|
||||
|
||||
svc.m_api_ping, err = m.Int64Counter("salty_api_ping",
|
||||
metric.WithDescription("salty api ping called"),
|
||||
)
|
||||
errs = multierr.Append(errs, err)
|
||||
|
||||
svc.m_api_register, err = m.Int64Counter("salty_api_register",
|
||||
metric.WithDescription("salty api register"),
|
||||
)
|
||||
errs = multierr.Append(errs, err)
|
||||
|
||||
svc.m_api_lookup, err = m.Int64Counter("salty_api_lookup",
|
||||
metric.WithDescription("salty api ping lookup"),
|
||||
)
|
||||
errs = multierr.Append(errs, err)
|
||||
|
||||
svc.m_api_send, err = m.Int64Counter("salty_api_send",
|
||||
metric.WithDescription("salty api ping send"),
|
||||
)
|
||||
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)
|
||||
|
||||
return svc, errs
|
||||
}
|
||||
|
||||
func (s *service) BaseURL() string {
|
||||
if s == nil {
|
||||
return "http://missing.context/"
|
||||
}
|
||||
return s.baseURL
|
||||
}
|
||||
|
||||
func (s *service) RegisterHTTP(mux *http.ServeMux) {
|
||||
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) {
|
||||
mux.HandleFunc("/ping", s.apiv1)
|
||||
mux.HandleFunc("/register", s.apiv1)
|
||||
mux.HandleFunc("/lookup/", 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) {
|
||||
ctx := r.Context()
|
||||
ctx, span := lg.Span(ctx)
|
||||
defer span.End()
|
||||
|
||||
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")
|
||||
|
||||
span.AddEvent(fmt.Sprint("find ", addr))
|
||||
a, err := ev.Update(ctx, s.es, addr, func(ctx context.Context, agg *SaltyUser) error { return nil })
|
||||
switch {
|
||||
case errors.Is(err, event.ErrShouldExist):
|
||||
span.RecordError(err)
|
||||
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
case err != nil:
|
||||
span.RecordError(err)
|
||||
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
basePath, _ := url.JoinPath(s.baseURL, a.inbox.String())
|
||||
|
||||
err = json.NewEncoder(w).Encode(
|
||||
struct {
|
||||
Endpoint string `json:"endpoint"`
|
||||
Key string `json:"key"`
|
||||
}{
|
||||
Endpoint: basePath,
|
||||
Key: a.pubkey.ID().String(),
|
||||
})
|
||||
if err != nil {
|
||||
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) {
|
||||
ctx, span := lg.Span(ctx)
|
||||
defer span.End()
|
||||
|
||||
s.m_create_user.Add(ctx, 1)
|
||||
start := time.Now()
|
||||
defer s.m_req_time.Record(ctx, time.Since(start).Milliseconds())
|
||||
|
||||
streamID := NickToStreamID(nick)
|
||||
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))
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
a, err := ev.Create(ctx, s.es, streamID, func(ctx context.Context, agg *SaltyUser) error {
|
||||
return agg.OnUserRegister(key)
|
||||
})
|
||||
switch {
|
||||
case errors.Is(err, ev.ErrShouldNotExist):
|
||||
span.RecordError(err)
|
||||
return nil, fmt.Errorf("user exists: %w", err)
|
||||
|
||||
case err != nil:
|
||||
span.RecordError(err)
|
||||
return nil, fmt.Errorf("internal error: %w", err)
|
||||
}
|
||||
|
||||
return a, nil
|
||||
}
|
||||
func (s *service) SaltyUser(ctx context.Context, nick string) (*SaltyUser, error) {
|
||||
ctx, span := lg.Span(ctx)
|
||||
defer span.End()
|
||||
|
||||
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))))
|
||||
span.AddEvent(streamID)
|
||||
|
||||
a, err := ev.Update(ctx, s.es, streamID, func(ctx context.Context, agg *SaltyUser) error { return nil })
|
||||
switch {
|
||||
case errors.Is(err, ev.ErrShouldExist):
|
||||
span.RecordError(err)
|
||||
return nil, fmt.Errorf("user not found")
|
||||
|
||||
case err != nil:
|
||||
span.RecordError(err)
|
||||
return nil, fmt.Errorf("%w internal error", err)
|
||||
}
|
||||
|
||||
return a, err
|
||||
}
|
||||
|
||||
func (s *service) apiv1(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
ctx, span := lg.Span(ctx)
|
||||
defer span.End()
|
||||
|
||||
start := time.Now()
|
||||
defer s.m_req_time.Record(ctx, time.Since(start).Nanoseconds())
|
||||
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
switch {
|
||||
case r.URL.Path == "/ping":
|
||||
s.m_api_ping.Add(ctx, 1)
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, _ = w.Write([]byte(`{}`))
|
||||
|
||||
case strings.HasPrefix(r.URL.Path, "/lookup/"):
|
||||
s.m_api_lookup.Add(ctx, 1)
|
||||
|
||||
addr, err := s.ParseAddr(strings.TrimPrefix(r.URL.Path, "/lookup/"))
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
err = addr.Refresh(ctx)
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
err = json.NewEncoder(w).Encode(addr)
|
||||
span.RecordError(err)
|
||||
return
|
||||
|
||||
default:
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
case http.MethodPost:
|
||||
switch r.URL.Path {
|
||||
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":
|
||||
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:
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
default:
|
||||
w.WriteHeader(http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
package twtxt
|
|
@ -1,35 +0,0 @@
|
|||
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
|
||||
}
|
|
@ -1,46 +0,0 @@
|
|||
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)
|
||||
}
|
|
@ -1,44 +0,0 @@
|
|||
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)
|
|
@ -1,426 +0,0 @@
|
|||
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()
|
||||
}
|
|
@ -1,310 +0,0 @@
|
|||
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
6
app/webfinger/ui/assets/bootstrap.min.css
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -1,95 +0,0 @@
|
|||
/* 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;
|
||||
}
|
||||
}
|
|
@ -1,28 +0,0 @@
|
|||
{{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}}
|
|
@ -1,131 +0,0 @@
|
|||
{{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}}
|
|
@ -1,416 +0,0 @@
|
|||
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
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
# 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`.
|
|
@ -1,34 +0,0 @@
|
|||
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
|
||||
})
|
|
@ -1,44 +0,0 @@
|
|||
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
|
||||
})
|
|
@ -1,64 +0,0 @@
|
|||
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
|
||||
})
|
|
@ -1,17 +0,0 @@
|
|||
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
|
||||
})
|
|
@ -1,52 +0,0 @@
|
|||
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
|
||||
})
|
|
@ -1,41 +0,0 @@
|
|||
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
|
||||
}
|
|
@ -1,54 +0,0 @@
|
|||
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
|
||||
})
|
|
@ -1,41 +0,0 @@
|
|||
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
|
||||
})
|
|
@ -1,47 +0,0 @@
|
|||
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 +0,0 @@
|
|||
../ev/app.msgbus.go
|
|
@ -1 +0,0 @@
|
|||
../ev/main.go
|
|
@ -1 +0,0 @@
|
|||
../ev/svc.es.go
|
|
@ -1 +0,0 @@
|
|||
../ev/svc.gql.go
|
|
@ -1 +0,0 @@
|
|||
../ev/svc.http.go
|
|
@ -1 +0,0 @@
|
|||
../ev/app.peerfinder.go
|
|
@ -1 +0,0 @@
|
|||
../ev/main.go
|
|
@ -1 +0,0 @@
|
|||
../ev/svc.es.go
|
|
@ -1 +0,0 @@
|
|||
../ev/svc.http.go
|
|
@ -1 +0,0 @@
|
|||
../ev/app.msgbus.go
|
|
@ -1 +0,0 @@
|
|||
../ev/app.salty.go
|
|
@ -1 +0,0 @@
|
|||
../ev/main.go
|
|
@ -1 +0,0 @@
|
|||
../ev/svc.es.go
|
|
@ -1 +0,0 @@
|
|||
../ev/svc.http.go
|
|
@ -1,290 +0,0 @@
|
|||
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 +0,0 @@
|
|||
../ev/app.webfinger.go
|
|
@ -1 +0,0 @@
|
|||
../ev/main.go
|
|
@ -1 +0,0 @@
|
|||
../ev/svc.es.go
|
|
@ -1 +0,0 @@
|
|||
../ev/svc.http.go
|
|
@ -1,123 +0,0 @@
|
|||
//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 }
|
|
@ -21,8 +21,8 @@ import (
|
|||
"go.sour.is/pkg/locker"
|
||||
|
||||
"go.sour.is/ev"
|
||||
"go.sour.is/ev/pkg/driver"
|
||||
"go.sour.is/ev/pkg/event"
|
||||
"go.sour.is/ev/driver"
|
||||
"go.sour.is/ev/event"
|
||||
)
|
||||
|
||||
const CachSize = 1000
|
|
@ -4,7 +4,7 @@ package driver
|
|||
import (
|
||||
"context"
|
||||
|
||||
"go.sour.is/ev/pkg/event"
|
||||
"go.sour.is/ev/event"
|
||||
"go.sour.is/pkg/math"
|
||||
)
|
||||
|
|
@ -10,8 +10,8 @@ import (
|
|||
"go.sour.is/pkg/locker"
|
||||
|
||||
"go.sour.is/ev"
|
||||
"go.sour.is/ev/pkg/driver"
|
||||
"go.sour.is/ev/pkg/event"
|
||||
"go.sour.is/ev/driver"
|
||||
"go.sour.is/ev/event"
|
||||
)
|
||||
|
||||
const AppendOnly = ev.AppendOnly
|
|
@ -8,8 +8,8 @@ import (
|
|||
"go.sour.is/pkg/lg"
|
||||
|
||||
"go.sour.is/ev"
|
||||
"go.sour.is/ev/pkg/driver"
|
||||
"go.sour.is/ev/pkg/event"
|
||||
"go.sour.is/ev/driver"
|
||||
"go.sour.is/ev/event"
|
||||
)
|
||||
|
||||
type projector struct {
|
|
@ -7,9 +7,9 @@ import (
|
|||
"github.com/matryer/is"
|
||||
|
||||
"go.sour.is/ev"
|
||||
"go.sour.is/ev/pkg/driver"
|
||||
"go.sour.is/ev/pkg/driver/projecter"
|
||||
"go.sour.is/ev/pkg/event"
|
||||
"go.sour.is/ev/driver"
|
||||
"go.sour.is/ev/driver/projecter"
|
||||
"go.sour.is/ev/event"
|
||||
)
|
||||
|
||||
type mockDriver struct {
|
|
@ -5,8 +5,8 @@ import (
|
|||
"errors"
|
||||
|
||||
"go.sour.is/ev"
|
||||
"go.sour.is/ev/pkg/driver"
|
||||
"go.sour.is/ev/pkg/event"
|
||||
"go.sour.is/ev/driver"
|
||||
"go.sour.is/ev/event"
|
||||
"go.sour.is/pkg/lg"
|
||||
)
|
||||
|
|
@ -9,11 +9,12 @@ import (
|
|||
"go.opentelemetry.io/otel/attribute"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
|
||||
"go.sour.is/ev"
|
||||
"go.sour.is/ev/pkg/driver"
|
||||
"go.sour.is/ev/pkg/event"
|
||||
"go.sour.is/pkg/lg"
|
||||
"go.sour.is/pkg/locker"
|
||||
|
||||
"go.sour.is/ev"
|
||||
"go.sour.is/ev/driver"
|
||||
"go.sour.is/ev/event"
|
||||
)
|
||||
|
||||
type state struct {
|
8
ev.go
8
ev.go
|
@ -9,12 +9,12 @@ import (
|
|||
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"go.opentelemetry.io/otel/metric"
|
||||
"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"
|
||||
"go.uber.org/multierr"
|
||||
|
||||
"go.sour.is/ev/driver"
|
||||
"go.sour.is/ev/event"
|
||||
)
|
||||
|
||||
type config struct {
|
||||
|
|
12
ev_test.go
12
ev_test.go
|
@ -11,12 +11,11 @@ import (
|
|||
"go.uber.org/multierr"
|
||||
|
||||
"go.sour.is/ev"
|
||||
"go.sour.is/ev/app/peerfinder"
|
||||
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"
|
||||
memstore "go.sour.is/ev/driver/mem-store"
|
||||
"go.sour.is/ev/driver/projecter"
|
||||
resolvelinks "go.sour.is/ev/driver/resolve-links"
|
||||
"go.sour.is/ev/driver/streamer"
|
||||
"go.sour.is/ev/event"
|
||||
)
|
||||
|
||||
var (
|
||||
|
@ -176,7 +175,6 @@ func TestUnwrapProjector(t *testing.T) {
|
|||
projecter.New(
|
||||
ctx,
|
||||
projecter.DefaultProjection,
|
||||
peerfinder.Projector,
|
||||
),
|
||||
)
|
||||
is.NoErr(err)
|
||||
|
|
|
@ -3,7 +3,7 @@ package event_test
|
|||
import (
|
||||
"testing"
|
||||
|
||||
"go.sour.is/ev/pkg/event"
|
||||
"go.sour.is/ev/event"
|
||||
)
|
||||
|
||||
type Agg struct {
|
|
@ -8,7 +8,7 @@ import (
|
|||
|
||||
"github.com/matryer/is"
|
||||
|
||||
"go.sour.is/ev/pkg/event"
|
||||
"go.sour.is/ev/event"
|
||||
)
|
||||
|
||||
type DummyEvent struct {
|
67
go.mod
67
go.mod
|
@ -3,13 +3,8 @@ module go.sour.is/ev
|
|||
go 1.19
|
||||
|
||||
require (
|
||||
github.com/99designs/gqlgen v0.17.34
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/gorilla/websocket v1.5.0
|
||||
github.com/ravilushqa/otelgqlgen v0.13.1 // indirect
|
||||
github.com/rs/cors v1.9.0
|
||||
github.com/tidwall/wal v1.1.7
|
||||
github.com/vektah/gqlparser/v2 v2.5.6
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.42.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/runtime v0.42.0 // indirect
|
||||
go.opentelemetry.io/otel v1.16.0
|
||||
|
@ -18,99 +13,43 @@ require (
|
|||
go.opentelemetry.io/otel/sdk v1.16.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk/metric v0.39.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.16.0
|
||||
golang.org/x/sync v0.3.0 // indirect
|
||||
)
|
||||
|
||||
require github.com/tj/go-semver v1.0.0
|
||||
require go.sour.is/pkg v0.0.2-0.20230726225143-5ad7fce0ac22
|
||||
|
||||
require github.com/stretchr/testify v1.8.4 // indirect
|
||||
|
||||
require (
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible
|
||||
go.sour.is/pkg v0.0.2-0.20230726225143-5ad7fce0ac22
|
||||
)
|
||||
|
||||
require (
|
||||
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/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/cenkalti/backoff/v4 v4.2.1 // 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/dchest/bcrypt_pbkdf v0.0.0-20150205184540-83f37f9c154a // indirect
|
||||
github.com/dchest/blake2b v1.0.0 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.3 // indirect
|
||||
github.com/go-logr/logr v1.2.4 // indirect
|
||||
github.com/golang/protobuf v1.5.3 // indirect
|
||||
github.com/google/go-cmp v0.5.9 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.11.3 // indirect
|
||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||
github.com/hashicorp/go-multierror v1.1.1 // 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/oklog/ulid v1.3.1
|
||||
github.com/petermattis/goid v0.0.0-20221215004737-a150e88a970d // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/posener/formatter v1.0.0 // indirect
|
||||
github.com/prometheus/client_golang v1.16.0 // indirect
|
||||
github.com/prometheus/client_model v0.4.0 // indirect
|
||||
github.com/prometheus/common v0.44.0 // indirect
|
||||
github.com/prometheus/procfs v0.11.0 // indirect
|
||||
github.com/sasha-s/go-deadlock v0.3.1 // 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/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/v2 v2.0.0 // indirect
|
||||
github.com/writeas/go-strip-markdown/v2 v2.1.1 // indirect
|
||||
go.mills.io/salty v0.0.0-20220322161301-ce2b9f6573fa // indirect
|
||||
go.opentelemetry.io/contrib v1.16.1 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.16.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.16.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v0.19.0 // indirect
|
||||
go.yarn.social/lextwt v0.0.0-20230226040904-0097db79a25e // indirect
|
||||
go.yarn.social/types v0.0.0-20230129042829-96789c694b24 // indirect
|
||||
golang.org/x/crypto v0.6.0 // indirect
|
||||
golang.org/x/net v0.10.0 // indirect
|
||||
golang.org/x/sys v0.10.0 // indirect
|
||||
golang.org/x/text v0.9.0 // indirect
|
||||
google.golang.org/appengine v1.6.7 // indirect
|
||||
google.golang.org/genproto v0.0.0-20230306155012-7f2fa6fef1f4 // indirect
|
||||
google.golang.org/grpc v1.55.0 // indirect
|
||||
google.golang.org/protobuf v1.31.0 // indirect
|
||||
nhooyr.io/websocket v1.8.7 // indirect
|
||||
)
|
||||
|
||||
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/matryer/is v1.4.1
|
||||
github.com/oklog/ulid/v2 v2.1.0
|
||||
github.com/tidwall/gjson v1.14.4 // indirect
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
github.com/tidwall/pretty v1.2.1 // indirect
|
||||
github.com/tidwall/tinylru v1.1.0 // indirect
|
||||
gitlab.com/jamietanna/content-negotiation-go v0.2.0
|
||||
go.mills.io/saltyim v0.0.0-20230128070719-15a64de82829
|
||||
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
|
||||
)
|
||||
|
|
239
go.sum
239
go.sum
|
@ -31,36 +31,12 @@ 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.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=
|
||||
git.mills.io/prologic/bitcask v1.0.2 h1:Iy9x3mVVd1fB+SWY0LTmsSDPGbzMrd7zCZPKbsb/tDA=
|
||||
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/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/ScaleFT/sshkeys v0.0.0-20200327173127-6142f742bca5/go.mod h1:gxOHeajFfvGQh/fxlC8oOKBe23xnnJTif00IFFbiT+o=
|
||||
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/go.mod h1:veldBMzWxcCG2ZvUTKD2kJNRdCk5hVbJomOvKkmgYbo=
|
||||
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/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/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/avast/retry-go v3.0.0+incompatible h1:4SOWQ7Qs+oroOTQOYnAHqelpCO0biHSxpiH9JdtuBj0=
|
||||
github.com/avast/retry-go v3.0.0+incompatible/go.mod h1:XtSnn+n/sHqQIpZ10K1qAevBhOOCWBLXXy3hyiqqBrY=
|
||||
github.com/badgerodon/ioutil v0.0.0-20150716134133-06e58e34b867/go.mod h1:Ctq1YQi0dOq7QgBLZZ7p1Fr3IbAAqL/yMqDIHoe9WtE=
|
||||
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/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM=
|
||||
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=
|
||||
|
@ -79,23 +55,8 @@ github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWH
|
|||
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-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
|
||||
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/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dchest/bcrypt_pbkdf v0.0.0-20150205184540-83f37f9c154a h1:saTgr5tMLFnmy/yg3qDTft4rE5DY2uJ/cCxCe3q0XTU=
|
||||
github.com/dchest/bcrypt_pbkdf v0.0.0-20150205184540-83f37f9c154a/go.mod h1:Bw9BbhOJVNR+t0jCqx2GC6zv0TGBsShs56Y3gfSCvl0=
|
||||
github.com/dchest/blake2b v1.0.0 h1:KK9LimVmE0MjRl9095XJmKqZ+iLxWATvlcpVFRtaw6s=
|
||||
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/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.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
||||
|
@ -105,13 +66,7 @@ github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.
|
|||
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/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
github.com/flynn/noise v1.0.0/go.mod h1:xbMo+0i6+IGbYdJhF31t2eR1BIU0CYc12+BNAKwUTag=
|
||||
github.com/fsnotify/fsnotify v1.5.1 h1:mZcQUHVQUQWoPXXtuf9yuEXKudkV2sx1E06UadKWpgI=
|
||||
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/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=
|
||||
|
@ -120,23 +75,6 @@ github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ=
|
|||
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/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q=
|
||||
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/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 v1.0.0/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4=
|
||||
github.com/golang/glog v1.1.0 h1:/d3pCKDPWNnvIWe0vVUpNP32qc8U3PDVxySP/y360qE=
|
||||
|
@ -165,7 +103,6 @@ github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QD
|
|||
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM=
|
||||
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
|
||||
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
|
@ -181,8 +118,6 @@ github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
|
|||
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.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
||||
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
|
||||
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
|
@ -194,100 +129,30 @@ 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/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.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.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/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/v2 v2.7.0/go.mod h1:hgWBS7lorOAVIJEQMi4ZsPv9hVvWI6+ch50m39Pf2Ks=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.11.3 h1:lLT7ZLSzGLI08vc9cpd+tYmNWjdKDqyr/2L+f6U12Fk=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.11.3/go.mod h1:o//XUCC/F+yRGJoPO/VU0GSB0f8Nhgmxx0VIRUvaC0w=
|
||||
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.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.3 h1:kmRrRLlInXvng0SmLxmQpQkpbYAvcXm7NPDrgxJa9mE=
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.3/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
|
||||
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/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
|
||||
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
|
||||
github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U=
|
||||
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
|
||||
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-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/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/go.mod h1:+41yREqLkYyGfGf4OkhUn/ljwe/+kwhrlTq1/46Jj8c=
|
||||
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/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
|
||||
github.com/klauspost/compress v1.15.15 h1:EF27CXIuDsYJ6mmvtBRlEuB2UVOqHG1tAXgZ7yIO+lw=
|
||||
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.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/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/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y=
|
||||
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
|
||||
github.com/likexian/doh-go v0.6.4 h1:UnTrIVAOwkBvKU6qOt2W3C5yC9/YO02UVPPcN26iZDY=
|
||||
github.com/likexian/doh-go v0.6.4/go.mod h1:9jHpL/WPYmOM8+93RwXDf5TpZZwQjHrmIglXmjHpLlA=
|
||||
github.com/likexian/gokit v0.21.11/go.mod h1:0WlTw7IPdiMtrwu0t5zrLM7XXik27Ey6MhUJHio2fVo=
|
||||
github.com/likexian/gokit v0.25.9 h1:rzSQ/dP7Qw+QUzSuWlrLF0AtZS3Di6uO5yWOKhx2Gk4=
|
||||
github.com/likexian/gokit v0.25.9/go.mod h1:oDDqJUcnnF9uAKuw54F7s6oEG+OJ7eallfDW2dq0A/o=
|
||||
github.com/logrusorgru/aurora v2.0.3+incompatible h1:tOpm7WcpBTn4fjmVfgpQq0EfczGlG91VSDkswnjF5A8=
|
||||
github.com/logrusorgru/aurora v2.0.3+incompatible/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4=
|
||||
github.com/matryer/is v1.4.1 h1:55ehd8zaGABKLXQUe2awZ99BD/PTc2ls+KV/dXphgEQ=
|
||||
github.com/matryer/is v1.4.1/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
|
||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
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/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-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/nullrocks/identicon v0.0.0-20180626043057-7875f45b0022 h1:Ys0rDzh8s4UMlGaDa1UTA0sfKgvF0hQZzTYX8ktjiDc=
|
||||
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/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ=
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
|
||||
github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o=
|
||||
github.com/petermattis/goid v0.0.0-20180202154549-b0b1615b78e5/go.mod h1:jvVRKCrJTQWu0XVbaOlby/2lO20uSCHEMzzplHXte1o=
|
||||
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/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/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/posener/formatter v1.0.0 h1:TwXJq26f9ERTjCpZj8xEWj77WPWfX/nBgGx52Ap/gYM=
|
||||
github.com/posener/formatter v1.0.0/go.mod h1:xrC89js6vw5dde/9yUKKU9MY5ivn980yX4VG7gYQTvU=
|
||||
github.com/prometheus/client_golang v1.16.0 h1:yk/hx9hDbrGHovbci4BY+pRMfSuuat626eFsHb7tmT8=
|
||||
github.com/prometheus/client_golang v1.16.0/go.mod h1:Zsulrv/L9oM40tJ7T815tM89lFEugiJ9HzIqaAx4LKc=
|
||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
|
@ -297,33 +162,15 @@ github.com/prometheus/common v0.44.0 h1:+5BrQJwiBB9xsMygAB3TNvpQKOwlkc25LbISbrdO
|
|||
github.com/prometheus/common v0.44.0/go.mod h1:ofAIvZbQ1e/nugmZGz4/qCb9Ap1VoSTIO7x0VV9VvuY=
|
||||
github.com/prometheus/procfs v0.11.0 h1:5EAgkfkMl659uZPbe9AS2N68a7Cc1TJbPEuGzFuRbyk=
|
||||
github.com/prometheus/procfs v0.11.0/go.mod h1:nwNm2aOCAYw8uTR/9bWRREkZFxAUcWzPHWJq+XBB/FM=
|
||||
github.com/ravilushqa/otelgqlgen v0.13.1 h1:V+zFE75iDd2/CSzy5kKnb+Fi09SsE5535wv9U2nUEFE=
|
||||
github.com/ravilushqa/otelgqlgen v0.13.1/go.mod h1:ZIyWykK2paCuNi9k8gk5edcNSwDJuxZaW90vZXpafxw=
|
||||
github.com/robfig/cron v1.2.0 h1:ZjScXvvxeQ63Dbyxy76Fj3AT3Ut0aKsyd2/tl3DTMuQ=
|
||||
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.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.9.0 h1:l9HGsTsHJcvW14Nk7J9KFz8bzeAWXn3CG6bgt7LsrAE=
|
||||
github.com/rs/cors v1.9.0/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU=
|
||||
github.com/sasha-s/go-deadlock v0.3.1 h1:sqv7fDNShgjcaxkO0JNcOAlr8B9+cV5Ey/OB71efZx0=
|
||||
github.com/sasha-s/go-deadlock v0.3.1/go.mod h1:F73l+cr82YSh10GxyRI6qZiCgK64VaZjwesgfQ1/iLM=
|
||||
github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8=
|
||||
github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I=
|
||||
github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0=
|
||||
github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
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.3.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
|
||||
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.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.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
github.com/taigrr/go-colorhash v0.0.0-20220329080504-742db7f45eae h1:RXzKJmV0lGvBpY8/43bJShhPYIssF7X18UVMs9KIgIQ=
|
||||
github.com/taigrr/go-colorhash v0.0.0-20220329080504-742db7f45eae/go.mod h1:1xBq06Fnn6nvsoWc+BCWwFvC0Zx04/FA3mpq937vlyI=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
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=
|
||||
|
@ -336,50 +183,14 @@ 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/wal v1.1.7 h1:emc1TRjIVsdKKSnpwGBAcsAGg0767SvUk8+ygx7Bb+4=
|
||||
github.com/tidwall/wal v1.1.7/go.mod h1:r6lR1j27W9EPalgHiB7zLJDYu3mzW5BQP5KrzBpYY/E=
|
||||
github.com/timewasted/go-accept-headers v0.0.0-20130320203746-c78f304b1b09 h1:QVxbx5l/0pzciWYOynixQMtUhPYC3YKD6EcUlOsgGqw=
|
||||
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/go.mod h1:gUYDtqQw1JS3ZJ8UWVcGTGqqr6YIN3CWg+kkNaLt55U=
|
||||
github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo=
|
||||
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
|
||||
github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs=
|
||||
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/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.2 h1:gnjoVuB/kljJ5wICEEOpx98oXMWPLj22G67Vbd1qPqc=
|
||||
github.com/vmihailenco/tagparser v0.1.2/go.mod h1:OeAg3pn3UbLjkWt+rN9oFYB6u/cQgqMEUPoW2WPyhdI=
|
||||
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.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
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=
|
||||
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.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
|
||||
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go.opentelemetry.io/contrib v1.16.1 h1:EpASvVyGx6/ZTlmXzxYfTMZxHROelCeXXa2uLiwltcs=
|
||||
go.opentelemetry.io/contrib v1.16.1/go.mod h1:gIzjwWFoGazJmtCaDgViqOSJPde2mCWzv60o0bWPcZs=
|
||||
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.42.0/go.mod h1:XiYsayHc36K3EByOO6nbAXnAWbrUxdjUROCEeeROOH8=
|
||||
go.opentelemetry.io/contrib/instrumentation/runtime v0.42.0 h1:EbmAUG9hEAMXyfWEasIt2kmh/WmXUznUksChApTgBGc=
|
||||
|
@ -409,21 +220,11 @@ go.sour.is/pkg v0.0.2-0.20230726225143-5ad7fce0ac22 h1:KRe6xPl7ryo+l0SLN9u4I/jgu
|
|||
go.sour.is/pkg v0.0.2-0.20230726225143-5ad7fce0ac22/go.mod h1:jOdxxKILf+kXQmk1cG3sN4Ogxk8C4/14mxhy/QEJjv4=
|
||||
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
go.yarn.social/lextwt v0.0.0-20230226040904-0097db79a25e h1:dWzBF1VL1BCQfneVAp/PCNVkc0QNkTK/lnqpjm6tZfM=
|
||||
go.yarn.social/lextwt v0.0.0-20230226040904-0097db79a25e/go.mod h1:3n4ul1qR3tYnV9og38s5YcQxnRjbz4w/XGywPFZpg4k=
|
||||
go.yarn.social/types v0.0.0-20230129042829-96789c694b24 h1:bGluHPQHHEcw38keQfObj0JlYkOMHZKoWna0pwsSMIU=
|
||||
go.yarn.social/types v0.0.0-20230129042829-96789c694b24/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-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/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-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.6.0 h1:qfktjS5LUO+fFKeJXZ+ikTRijMmljikvG68fpMMruSc=
|
||||
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||
|
@ -434,7 +235,6 @@ 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-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-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-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
|
@ -455,7 +255,6 @@ 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.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
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-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
|
@ -468,7 +267,6 @@ golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR
|
|||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-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-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=
|
||||
|
@ -483,11 +281,7 @@ golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/
|
|||
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-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-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
|
@ -504,10 +298,6 @@ 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-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-20210220032951-036812b2e83c/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-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
|
@ -521,11 +311,9 @@ golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7w
|
|||
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-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-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-20200219091948-cb0a6d8edb6c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
|
@ -536,29 +324,17 @@ golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7w
|
|||
golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-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-20210510120138-977fb7262007/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-20220520151302-bc2c85ada10a/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-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA=
|
||||
golang.org/x/sys v0.10.0/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-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.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.8.0 h1:n5xxQn2i3PC0yLAbjTpNT85q/Kgzcr2gIoX9OrJUols=
|
||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.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=
|
||||
|
@ -604,7 +380,6 @@ 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-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
|
@ -631,8 +406,6 @@ google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7
|
|||
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
|
||||
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
|
||||
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
|
@ -701,19 +474,11 @@ google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs
|
|||
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/yaml.v2 v2.2.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.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
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.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.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-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
|
@ -721,8 +486,6 @@ 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-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=
|
||||
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/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
|
||||
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
|
||||
|
|
|
@ -13,7 +13,7 @@ import (
|
|||
"go.sour.is/pkg/lg"
|
||||
|
||||
"go.sour.is/ev"
|
||||
"go.sour.is/ev/pkg/event"
|
||||
"go.sour.is/ev/event"
|
||||
)
|
||||
|
||||
type EventResolver interface {
|
39
gqlgen.yml
39
gqlgen.yml
|
@ -1,39 +0,0 @@
|
|||
schema:
|
||||
- pkg/*/*.graphqls
|
||||
- app/*/*.graphqls
|
||||
|
||||
exec:
|
||||
filename: internal/graph/generated/generated.go
|
||||
package: generated
|
||||
|
||||
federation:
|
||||
filename: internal/graph/generated/federation.go
|
||||
package: generated
|
||||
|
||||
model:
|
||||
filename: internal/graph/model/models_gen.go
|
||||
package: model
|
||||
|
||||
resolver:
|
||||
filename: internal/graph/resolver/resolver.go
|
||||
package: resolver
|
||||
|
||||
models:
|
||||
ID:
|
||||
model:
|
||||
- github.com/99designs/gqlgen/graphql.ID
|
||||
- github.com/99designs/gqlgen/graphql.Int
|
||||
- github.com/99designs/gqlgen/graphql.Int64
|
||||
- github.com/99designs/gqlgen/graphql.Int32
|
||||
- github.com/99designs/gqlgen/graphql.Uint
|
||||
- github.com/99designs/gqlgen/graphql.Uint64
|
||||
- github.com/99designs/gqlgen/graphql.Uint32
|
||||
Int:
|
||||
model:
|
||||
- github.com/99designs/gqlgen/graphql.Int64
|
||||
- github.com/99designs/gqlgen/graphql.Int32
|
||||
- github.com/99designs/gqlgen/graphql.Int
|
||||
- github.com/99designs/gqlgen/graphql.Uint64
|
||||
- github.com/99designs/gqlgen/graphql.Uint32
|
||||
- github.com/99designs/gqlgen/graphql.Uint
|
||||
|
|
@ -1,35 +0,0 @@
|
|||
// Code generated by github.com/99designs/gqlgen, DO NOT EDIT.
|
||||
|
||||
package generated
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"github.com/99designs/gqlgen/plugin/federation/fedruntime"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrUnknownType = errors.New("unknown type")
|
||||
ErrTypeNotFound = errors.New("type not found")
|
||||
)
|
||||
|
||||
func (ec *executionContext) __resolve__service(ctx context.Context) (fedruntime.Service, error) {
|
||||
if ec.DisableIntrospection {
|
||||
return fedruntime.Service{}, errors.New("federated introspection disabled")
|
||||
}
|
||||
|
||||
var sdl []string
|
||||
|
||||
for _, src := range sources {
|
||||
if src.BuiltIn {
|
||||
continue
|
||||
}
|
||||
sdl = append(sdl, src.Input)
|
||||
}
|
||||
|
||||
return fedruntime.Service{
|
||||
SDL: strings.Join(sdl, "\n"),
|
||||
}, nil
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -1 +0,0 @@
|
|||
package generated
|
|
@ -1,3 +0,0 @@
|
|||
// Code generated by github.com/99designs/gqlgen, DO NOT EDIT.
|
||||
|
||||
package model
|
|
@ -1,64 +0,0 @@
|
|||
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,3 +0,0 @@
|
|||
# Pkg Tools
|
||||
|
||||
This is a collection of modules that provide simple reusable functions.
|
Loading…
Reference in New Issue
Block a user