chore: add apps from go.sour.is/ev

add-keyoxide
xuu 2023-09-29 10:31:25 -06:00
parent 976ce36be2
commit bec2c14d51
80 changed files with 13030 additions and 439 deletions

5
.ansible/inventory Normal file
View File

@ -0,0 +1,5 @@
[lavana]
lavana.sour.is
[kapha]
kapha.sour.is

16
.ansible/playbook.yml Normal file
View File

@ -0,0 +1,16 @@
---
- name: Deploy EV
hosts: lavana
tasks:
- name: Copy build to remote
ansible.builtin.copy:
src: ev
dest: /usr/local/bin/ev
owner: root
group: root
- name: Restart service
systemd:
name: ev
enabled: true
state: restarted

51
.drone.yml Normal file
View File

@ -0,0 +1,51 @@
kind: pipeline
type: docker
name: build
steps:
- name: test
image: golang:1.20
commands:
- go test -v -race -skip '^TestE2E|TestMain' ./...
trigger:
event:
- push
- pull_request
---
kind: pipeline
type: docker
name: deploy
steps:
- name: build
image: golang:1.20
environment:
GOOS: linux
GOARCH: amd64
commands:
- go build -ldflags "-s -w" -o build/ev ./cmd/ev
- name: compress
image: gruebel/upx:latest
commands:
- upx --best --lzma -o .ansible/ev build/ev
- name: deploy
image: plugins/ansible:3
settings:
playbook: .ansible/playbook.yml
inventory: .ansible/inventory
become: true
become_method: sudo
ssh_common_args: -p 65535
user: deploy
private_key:
from_secret: drone_ssh
trigger:
event:
- promote
target:
- production

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
tools

View File

@ -20,5 +20,20 @@ run:
test:
go test -cover -race ./...
GQLS=gqlgen.yml
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
clean:

37
app/gql/common.graphqls Normal file
View File

@ -0,0 +1,37 @@
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

View File

@ -0,0 +1,34 @@
type Meta @goModel(model: "go.sour.is/ev/pkg/event.Meta") {
eventID: String! @goField(name: "getEventID")
streamID: String! @goField(name: "ActualStreamID")
position: Int! @goField(name: "ActualPosition")
created: Time!
}
extend type Query {
events(streamID: String! paging: PageInput): Connection!
}
extend type Mutation {
truncateStream(streamID: String! index:Int!): Boolean!
}
extend type Subscription {
"""after == 0 start from begining, after == -1 start from end"""
eventAdded(streamID: String! after: Int! = -1): Event
}
type Event implements Edge @goModel(model: "go.sour.is/ev/pkg/gql.Event") {
id: ID!
eventID: String!
streamID: String!
position: Int!
values: Map!
bytes: String!
type: String!
created: Time!
meta: Meta!
linked: Event
}

66
app/gql/resolver.go Normal file
View File

@ -0,0 +1,66 @@
package gql
import (
"context"
"github.com/99designs/gqlgen/graphql"
"go.sour.is/pkg/gql"
"go.sour.is/pkg/gql/resolver"
gql_es "go.sour.is/ev/gql"
"go.sour.is/tools/app/msgbus"
"go.sour.is/tools/app/salty"
"go.sour.is/tools/internal/graph/generated"
)
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")
}

View File

@ -0,0 +1,9 @@
# 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
app/mercury/service.go Normal file
View File

@ -0,0 +1 @@
package mercury

View File

@ -0,0 +1,16 @@
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/tools/app/msgbus.PostEvent") {
id: ID!
payload: String!
payloadJSON: Map!
tags: [String!]!
meta: Meta!
}

597
app/msgbus/service.go Normal file
View File

@ -0,0 +1,597 @@
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/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))
}

View File

@ -0,0 +1,25 @@
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)
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,58 @@
/* Space out content a bit */
body {
padding-top: 20px;
padding-bottom: 20px;
}
/* Everything but the jumbotron gets side spacing for mobile first views */
.header,
.footer {
padding-right: 15px;
padding-left: 15px;
}
/* Custom page header */
.header {
padding-bottom: 20px;
border-bottom: 1px solid #e5e5e5;
}
/* Make the masthead heading the same height as the navigation */
.header h3 {
margin-top: 0;
margin-bottom: 0;
line-height: 40px;
}
/* Custom page footer */
.footer {
padding-top: 19px;
color: #777;
border-top: 1px solid #e5e5e5;
}
.panel-heading a {
color: white;
font-weight: bold;
}
.container-narrow > hr {
margin: 30px 0;
}
.table tbody tr th {
width: 70%
}
@media (prefers-color-scheme: dark) {
body, .panel-body {
color: white;
background-color: #121212;
}
.table-striped > tbody > tr:nth-of-type(2n+1) {
background-color: darkslategray;
}
}
@media (prefers-color-scheme: light) {
}

56
app/peerfinder/ev-info.go Normal file
View File

@ -0,0 +1,56 @@
package peerfinder
import (
"bytes"
"github.com/tj/go-semver"
"go.sour.is/ev/event"
)
type Info struct {
ScriptVersion string `json:"script_version"`
event.IsAggregate
}
var _ event.Aggregate = (*Info)(nil)
func (a *Info) ApplyEvent(lis ...event.Event) {
for _, e := range lis {
switch e := e.(type) {
case *VersionChanged:
a.ScriptVersion = e.ScriptVersion
}
}
}
func (a *Info) MarshalEnviron() ([]byte, error) {
var b bytes.Buffer
b.WriteString("SCRIPT_VERSION=")
b.WriteString(a.ScriptVersion)
b.WriteRune('\n')
return b.Bytes(), nil
}
func (a *Info) OnUpsert() error {
if a.StreamVersion() == 0 {
event.Raise(a, &VersionChanged{ScriptVersion: initVersion})
}
current, _ := semver.Parse(initVersion)
previous, _ := semver.Parse(a.ScriptVersion)
if current.Compare(previous) > 0 {
event.Raise(a, &VersionChanged{ScriptVersion: initVersion})
}
return nil
}
type VersionChanged struct {
ScriptVersion string `json:"script_version"`
event.IsEvent
}
var _ event.Event = (*VersionChanged)(nil)

111
app/peerfinder/ev-peer.go Normal file
View File

@ -0,0 +1,111 @@
package peerfinder
import (
"net"
"strconv"
"strings"
"time"
"github.com/keys-pub/keys/json"
"go.sour.is/pkg/set"
"go.sour.is/ev/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)
}
}
}

View File

@ -0,0 +1,230 @@
package peerfinder
import (
"bytes"
"encoding/json"
"fmt"
"net/netip"
"strconv"
"time"
"github.com/oklog/ulid"
"go.sour.is/ev/event"
"go.sour.is/pkg/set"
)
type Request struct {
event.IsAggregate
RequestID string `json:"req_id"`
RequestIP string `json:"req_ip"`
Hidden bool `json:"hide,omitempty"`
Created time.Time `json:"req_created"`
Family int `json:"family"`
Responses []*Response `json:"responses"`
peers set.Set[string]
initial *RequestSubmitted
}
var _ event.Aggregate = (*Request)(nil)
func (a *Request) ApplyEvent(lis ...event.Event) {
for _, e := range lis {
switch e := e.(type) {
case *RequestSubmitted:
a.RequestID = e.EventMeta().EventID.String()
a.RequestIP = e.RequestIP
a.Hidden = e.Hidden
a.Created = ulid.Time(e.EventMeta().EventID.Time())
a.Family = e.Family()
a.initial = e
case *ResultSubmitted:
if a.peers == nil {
a.peers = set.New[string]()
}
if a.peers.Has(e.PeerID) {
continue
}
a.peers.Add(e.PeerID)
a.Responses = append(a.Responses, &Response{
PeerID: e.PeerID,
ScriptVersion: e.PeerVersion,
Latency: e.Latency,
Jitter: e.Jitter,
MinRTT: e.MinRTT,
MaxRTT: e.MaxRTT,
Sent: e.Sent,
Received: e.Received,
Unreachable: e.Unreachable,
Created: ulid.Time(e.EventMeta().EventID.Time()),
})
}
}
}
func (a *Request) MarshalEnviron() ([]byte, error) {
return a.initial.MarshalEnviron()
}
func (a *Request) CreatedString() string {
return a.Created.Format("2006-01-02 15:04:05")
}
type ListRequest []*Request
func (lis ListRequest) Len() int {
return len(lis)
}
func (lis ListRequest) Less(i, j int) bool {
return lis[i].Created.Before(lis[j].Created)
}
func (lis ListRequest) Swap(i, j int) {
lis[i], lis[j] = lis[j], lis[i]
}
type Response struct {
Peer *Peer `json:"peer"`
PeerID string `json:"-"`
ScriptVersion string `json:"peer_scriptver"`
Latency float64 `json:"res_latency"`
Jitter float64 `json:"res_jitter,omitempty"`
MaxRTT float64 `json:"res_maxrtt,omitempty"`
MinRTT float64 `json:"res_minrtt,omitempty"`
Sent int `json:"res_sent,omitempty"`
Received int `json:"res_recv,omitempty"`
Unreachable bool `json:"unreachable,omitempty"`
Created time.Time `json:"res_created"`
}
type ListResponse []*Response
func (lis ListResponse) Len() int {
return len(lis)
}
func (lis ListResponse) Less(i, j int) bool {
if lis[j].Latency == 0.0 && lis[i].Latency > 0.0 {
return true
}
if lis[i].Latency == 0.0 && lis[j].Latency > 0.0 {
return false
}
return lis[j].Latency >= lis[i].Latency
}
func (lis ListResponse) Swap(i, j int) {
lis[i], lis[j] = lis[j], lis[i]
}
type RequestSubmitted struct {
event.IsEvent
RequestIP string `json:"req_ip"`
Hidden bool `json:"hide,omitempty"`
}
func (r *RequestSubmitted) StreamID() string {
return r.EventMeta().GetEventID()
}
func (r *RequestSubmitted) RequestID() string {
return r.EventMeta().GetEventID()
}
func (r *RequestSubmitted) Created() time.Time {
return r.EventMeta().Created()
}
func (r *RequestSubmitted) CreatedString() string {
return r.Created().Format("2006-01-02 15:04:05")
}
func (r *RequestSubmitted) Family() int {
if r == nil {
return 0
}
ip, err := netip.ParseAddr(r.RequestIP)
switch {
case err != nil:
return 0
case ip.Is4():
return 1
default:
return 2
}
}
func (r *RequestSubmitted) String() string {
return fmt.Sprint(r.EventMeta().EventID, r.RequestIP, r.Hidden, r.CreatedString())
}
var _ event.Event = (*RequestSubmitted)(nil)
func (e *RequestSubmitted) MarshalBinary() (text []byte, err error) {
return json.Marshal(e)
}
func (e *RequestSubmitted) UnmarshalBinary(b []byte) error {
return json.Unmarshal(b, e)
}
func (e *RequestSubmitted) MarshalEnviron() ([]byte, error) {
if e == nil {
return nil, nil
}
var b bytes.Buffer
b.WriteString("REQ_ID=")
b.WriteString(e.RequestID())
b.WriteRune('\n')
b.WriteString("REQ_IP=")
b.WriteString(e.RequestIP)
b.WriteRune('\n')
b.WriteString("REQ_FAMILY=")
if family := e.Family(); family > 0 {
b.WriteString(strconv.Itoa(family))
}
b.WriteRune('\n')
b.WriteString("REQ_CREATED=")
b.WriteString(e.CreatedString())
b.WriteRune('\n')
return b.Bytes(), nil
}
type ResultSubmitted struct {
event.IsEvent
RequestID string `json:"req_id"`
PeerID string `json:"peer_id"`
PeerVersion string `json:"peer_version"`
Latency float64 `json:"latency,omitempty"`
Jitter float64 `json:"jitter,omitempty"`
MaxRTT float64 `json:"maxrtt,omitempty"`
MinRTT float64 `json:"minrtt,omitempty"`
Sent int `json:"res_sent,omitempty"`
Received int `json:"res_recv,omitempty"`
Unreachable bool `json:"unreachable,omitempty"`
}
func (r *ResultSubmitted) Created() time.Time {
return r.EventMeta().Created()
}
var _ event.Event = (*ResultSubmitted)(nil)
func (e *ResultSubmitted) String() string {
return fmt.Sprintf("id: %s\npeer: %s\nversion: %s\nlatency: %0.4f", e.RequestID, e.PeerID, e.PeerVersion, e.Latency)
}
type RequestTruncated struct {
RequestID string
event.IsEvent
}
var _ event.Event = (*RequestTruncated)(nil)
func (e *RequestTruncated) String() string {
return fmt.Sprintf("request truncated id: %s\n", e.RequestID)
}

713
app/peerfinder/http.go Normal file
View File

@ -0,0 +1,713 @@
package peerfinder
import (
"context"
"embed"
"encoding/json"
"errors"
"fmt"
"html/template"
"io"
"io/fs"
"log"
"net"
"net/http"
"sort"
"strconv"
"strings"
"github.com/oklog/ulid/v2"
contentnegotiation "gitlab.com/jamietanna/content-negotiation-go"
"go.opentelemetry.io/otel/attribute"
"go.sour.is/pkg/lg"
"go.sour.is/ev"
"go.sour.is/ev/event"
)
var (
//go:embed pages/* layouts/* assets/*
files embed.FS
templates map[string]*template.Template
)
// Args passed to templates
type Args struct {
RemoteIP string
Requests []*Request
CountPeers int
}
// requestArgs builds args from http.Request
func requestArgs(r *http.Request) Args {
remoteIP, _, _ := strings.Cut(r.RemoteAddr, ":")
if s := r.Header.Get("X-Forwarded-For"); s != "" {
s, _, _ = strings.Cut(s, ", ")
remoteIP = s
}
return Args{
RemoteIP: remoteIP,
}
}
// RegisterHTTP adds handler paths to the ServeMux
func (s *service) RegisterHTTP(mux *http.ServeMux) {
a, _ := fs.Sub(files, "assets")
assets := http.StripPrefix("/peers/assets/", http.FileServer(http.FS(a)))
mux.Handle("/peers/assets/", lg.Htrace(assets, "peer-assets"))
mux.Handle("/peers/", lg.Htrace(s, "peers"))
}
func (s *service) Setup() error {
return nil
}
// ServeHTTP handle HTTP requests
func (s *service) ServeHTTP(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
ctx, span := lg.Span(ctx)
defer span.End()
r = r.WithContext(ctx)
if !s.up.Load() {
w.WriteHeader(http.StatusFailedDependency)
fmt.Fprint(w, "Starting up...")
return
}
switch r.Method {
case http.MethodGet:
switch {
case strings.HasPrefix(r.URL.Path, "/peers/pending/"):
s.getPending(w, r, strings.TrimPrefix(r.URL.Path, "/peers/pending/"))
return
case strings.HasPrefix(r.URL.Path, "/peers/req/"):
s.getResultsForRequest(w, r, strings.TrimPrefix(r.URL.Path, "/peers/req/"))
return
case strings.HasPrefix(r.URL.Path, "/peers/status"):
var pickID string
if strings.HasPrefix(r.URL.Path, "/peers/status/") {
pickID = strings.TrimPrefix(r.URL.Path, "/peers/status/")
}
var requests []*Request
s.state.Use(r.Context(), func(ctx context.Context, state *state) error {
for id, p := range state.peers {
fmt.Fprintln(w, "PEER:", id[24:], p.Owner, p.Name)
}
if pickID != "" {
if rq, ok := state.requests[pickID]; ok {
requests = append(requests, rq)
}
} else {
requests = make([]*Request, 0, len(state.requests))
for i := range state.requests {
rq := state.requests[i]
requests = append(requests, rq)
}
}
for i := range requests {
rq := requests[i]
for i := range rq.Responses {
res := rq.Responses[i]
if peer, ok := state.peers[res.PeerID]; ok {
res.Peer = peer
res.Peer.ID = ""
}
}
}
return nil
})
for i, rq := range requests {
fmt.Fprintln(w, "REQ: ", i, rq.RequestIP, len(rq.Responses))
for i, peer := range fnOrderByPeer(rq) {
fmt.Fprintln(w, " PEER: ", i, peer.RequestID, peer.Name, peer.Latency, peer.Jitter)
for i, res := range peer.Results {
fmt.Fprintln(w, " RES: ", i, res.RequestID, res.Latency, res.Jitter)
}
}
}
default:
s.getResults(w, r)
return
}
case http.MethodPost:
switch {
case strings.HasPrefix(r.URL.Path, "/peers/req/"):
s.postResult(w, r, strings.TrimPrefix(r.URL.Path, "/peers/req/"))
return
case strings.HasPrefix(r.URL.Path, "/peers/req"):
s.postRequest(w, r)
return
default:
w.WriteHeader(http.StatusNotFound)
return
}
default:
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
}
func (s *service) getPending(w http.ResponseWriter, r *http.Request, peerID string) {
ctx, span := lg.Span(r.Context())
defer span.End()
span.SetAttributes(
attribute.String("peerID", peerID),
)
var peer *Peer
err := s.state.Use(ctx, func(ctx context.Context, state *state) error {
var ok bool
if peer, ok = state.peers[peerID]; !ok {
return fmt.Errorf("peer not found: %s", peerID)
}
return nil
})
if err != nil {
span.RecordError(err)
w.WriteHeader(http.StatusNotFound)
return
}
info, err := ev.Upsert(ctx, s.es, aggInfo, func(ctx context.Context, agg *Info) error {
return agg.OnUpsert() // initialize if not exists
})
if err != nil {
span.RecordError(err)
w.WriteHeader(http.StatusInternalServerError)
return
}
requests, err := s.es.Read(ctx, queueRequests, -1, -30)
if err != nil {
span.RecordError(err)
w.WriteHeader(http.StatusInternalServerError)
return
}
peerResults := &PeerResults{}
peerResults.SetStreamID(aggPeer(peerID))
err = s.es.Load(ctx, peerResults)
if err != nil && !errors.Is(err, ev.ErrNotFound) {
span.RecordError(fmt.Errorf("peer not found: %w", err))
w.WriteHeader(http.StatusNotFound)
}
var req *Request
for _, e := range requests {
r := &Request{}
r.ApplyEvent(e)
if !peerResults.Has(r.RequestID) {
if !peer.CanSupport(r.RequestIP) {
continue
}
req = r
}
}
if req == nil {
span.RecordError(fmt.Errorf("request not found"))
w.WriteHeader(http.StatusNoContent)
}
negotiator := contentnegotiation.NewNegotiator("application/json", "text/environment", "text/plain", "text/html")
negotiated, _, err := negotiator.Negotiate(r.Header.Get("Accept"))
if err != nil {
span.RecordError(err)
w.WriteHeader(http.StatusNotAcceptable)
return
}
span.AddEvent(negotiated.String())
mime := negotiated.String()
switch mime {
case "text/environment":
w.Header().Set("content-type", negotiated.String())
_, err = encodeTo(w, info.MarshalEnviron, req.MarshalEnviron)
case "application/json":
w.Header().Set("content-type", negotiated.String())
var out interface{} = info
if req != nil {
out = struct {
ScriptVersion string `json:"script_version"`
RequestID string `json:"req_id"`
RequestIP string `json:"req_ip"`
Family string `json:"req_family"`
Created string `json:"req_created"`
}{
info.ScriptVersion,
req.RequestID,
req.RequestIP,
strconv.Itoa(req.Family),
req.CreatedString(),
}
}
err = json.NewEncoder(w).Encode(out)
}
span.RecordError(err)
}
func (s *service) getResults(w http.ResponseWriter, r *http.Request) {
ctx, span := lg.Span(r.Context())
defer span.End()
// events, err := s.es.Read(ctx, queueRequests, -1, -30)
// if err != nil {
// span.RecordError(err)
// w.WriteHeader(http.StatusInternalServerError)
// return
// }
// requests := make([]*Request, len(events))
// for i, req := range events {
// if req, ok := req.(*RequestSubmitted); ok {
// requests[i], err = s.loadResult(ctx, req.RequestID())
// if err != nil {
// span.RecordError(err)
// w.WriteHeader(http.StatusInternalServerError)
// return
// }
// }
// }
var requests ListRequest
s.state.Use(ctx, func(ctx context.Context, state *state) error {
requests = make([]*Request, 0, len(state.requests))
for _, req := range state.requests {
if req.RequestID == "" {
continue
}
if req.Hidden {
continue
}
requests = append(requests, req)
}
return nil
})
sort.Sort(sort.Reverse(requests))
args := requestArgs(r)
args.Requests = requests[:maxResults]
s.state.Use(ctx, func(ctx context.Context, state *state) error {
args.CountPeers = len(state.peers)
return nil
})
t := templates["home.go.tpl"]
t.Execute(w, args)
}
func (s *service) getResultsForRequest(w http.ResponseWriter, r *http.Request, uuid string) {
ctx, span := lg.Span(r.Context())
defer span.End()
span.SetAttributes(
attribute.String("uuid", uuid),
)
var request *Request
err := s.state.Use(ctx, func(ctx context.Context, state *state) error {
request = state.requests[uuid]
return nil
})
if err != nil {
w.WriteHeader(http.StatusNotFound)
return
}
request, err = s.loadResult(ctx, request)
// request, err := s.loadResult(ctx, uuid)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
negotiator := contentnegotiation.NewNegotiator("application/json", "text/environment", "text/csv", "text/plain", "text/html")
negotiated, _, err := negotiator.Negotiate(r.Header.Get("Accept"))
if err != nil {
w.WriteHeader(http.StatusNotAcceptable)
return
}
span.AddEvent(negotiated.String())
switch negotiated.String() {
// case "text/environment":
// encodeTo(w, responses.MarshalBinary)
case "application/json":
json.NewEncoder(w).Encode(request)
return
default:
args := requestArgs(r)
args.Requests = append(args.Requests, request)
span.AddEvent(fmt.Sprint(args))
err := renderTo(w, "req.go.tpl", args)
span.RecordError(err)
return
}
}
func (s *service) postRequest(w http.ResponseWriter, r *http.Request) {
ctx, span := lg.Span(r.Context())
defer span.End()
if err := r.ParseForm(); err != nil {
w.WriteHeader(http.StatusUnprocessableEntity)
return
}
args := requestArgs(r)
requestIP := args.RemoteIP
if ip := r.Form.Get("req_ip"); ip != "" {
requestIP = ip
}
ip := net.ParseIP(requestIP)
if ip == nil {
w.WriteHeader(http.StatusBadRequest)
return
}
req := &RequestSubmitted{
RequestIP: ip.String(),
}
if hidden, err := strconv.ParseBool(r.Form.Get("req_hidden")); err == nil {
req.Hidden = hidden
}
span.SetAttributes(
attribute.Stringer("req_ip", ip),
)
s.es.Append(ctx, queueRequests, event.NewEvents(req))
http.Redirect(w, r, "/peers/req/"+req.RequestID(), http.StatusSeeOther)
}
func (s *service) postResult(w http.ResponseWriter, r *http.Request, reqID string) {
ctx, span := lg.Span(r.Context())
defer span.End()
span.SetAttributes(
attribute.String("id", reqID),
)
if _, err := ulid.ParseStrict(reqID); err != nil {
w.WriteHeader(http.StatusNotFound)
return
}
if err := r.ParseForm(); err != nil {
w.WriteHeader(http.StatusUnprocessableEntity)
return
}
form := make([]string, 0, len(r.Form))
for k, vals := range r.Form {
for _, v := range vals {
form = append(form, fmt.Sprint(k, v))
}
}
span.SetAttributes(
attribute.StringSlice("form", form),
)
peerID := r.Form.Get("peer_id")
err := s.state.Use(ctx, func(ctx context.Context, state *state) error {
var ok bool
if _, ok = state.peers[peerID]; !ok {
log.Printf("peer not found: %s\n", peerID)
return fmt.Errorf("peer not found: %s", peerID)
}
return nil
})
if err != nil {
span.RecordError(err)
w.WriteHeader(http.StatusNotFound)
return
}
peerResults := &PeerResults{}
peerResults.SetStreamID(aggPeer(peerID))
err = s.es.Load(ctx, peerResults)
if err != nil {
span.RecordError(fmt.Errorf("peer not found: %w", err))
w.WriteHeader(http.StatusNotFound)
}
if peerResults.Has(reqID) {
span.RecordError(fmt.Errorf("request previously recorded: req=%v peer=%v", reqID, peerID))
w.WriteHeader(http.StatusAlreadyReported)
return
}
var unreach bool
latency, err := strconv.ParseFloat(r.Form.Get("res_latency"), 64)
if err != nil {
unreach = true
}
req := &ResultSubmitted{
RequestID: reqID,
PeerID: r.Form.Get("peer_id"),
PeerVersion: r.Form.Get("peer_version"),
Latency: latency,
Unreachable: unreach,
}
if jitter, err := strconv.ParseFloat(r.Form.Get("res_jitter"), 64); err == nil {
req.Jitter = jitter
} else {
span.RecordError(err)
}
if minrtt, err := strconv.ParseFloat(r.Form.Get("res_minrtt"), 64); err == nil {
req.MinRTT = minrtt
} else {
span.RecordError(err)
}
if maxrtt, err := strconv.ParseFloat(r.Form.Get("res_maxrtt"), 64); err == nil {
req.MaxRTT = maxrtt
} else {
span.RecordError(err)
}
span.SetAttributes(
attribute.Stringer("result", req),
)
log.Printf("record result: %v", req)
s.es.Append(ctx, queueResults, event.NewEvents(req))
}
func renderTo(w io.Writer, name string, args any) (err error) {
defer func() {
if p := recover(); p != nil {
err = fmt.Errorf("panic: %s", p)
}
if err != nil {
fmt.Fprint(w, err)
}
}()
t, ok := templates[name]
if !ok || t == nil {
return fmt.Errorf("missing template")
}
return t.Execute(w, args)
}
func encodeTo(w io.Writer, fns ...func() ([]byte, error)) (int, error) {
i := 0
for _, fn := range fns {
b, err := fn()
if err != nil {
return i, err
}
j, err := w.Write(b)
i += j
if err != nil {
return i, err
}
}
return i, nil
}
func loadTemplates() error {
if templates != nil {
return nil
}
templates = make(map[string]*template.Template)
tmplFiles, err := fs.ReadDir(files, "pages")
if err != nil {
return err
}
for _, tmpl := range tmplFiles {
if tmpl.IsDir() {
continue
}
pt := template.New(tmpl.Name())
pt.Funcs(funcMap)
pt, err = pt.ParseFS(files, "pages/"+tmpl.Name(), "layouts/*.go.tpl")
if err != nil {
log.Println(err)
return err
}
templates[tmpl.Name()] = pt
}
return nil
}
var funcMap = map[string]any{
"orderByPeer": fnOrderByPeer,
"countResponses": fnCountResponses,
}
type peerResult struct {
RequestID string
Name string
Country string
Latency float64
Jitter float64
}
type peer struct {
RequestID string
Name string
Note string
Nick string
Country string
Latency float64
Jitter float64
VPNTypes []string
Results peerResults
}
type listPeer []peer
func (lis listPeer) Len() int {
return len(lis)
}
func (lis listPeer) Less(i, j int) bool {
if lis[j].Latency == 0.0 && lis[i].Latency > 0.0 {
return true
}
if lis[i].Latency == 0.0 && lis[j].Latency > 0.0 {
return false
}
return lis[i].Latency < lis[j].Latency
}
func (lis listPeer) Swap(i, j int) {
lis[i], lis[j] = lis[j], lis[i]
}
type peerResults []peerResult
func (lis peerResults) Len() int {
return len(lis)
}
func (lis peerResults) Less(i, j int) bool {
if lis[j].Latency == 0.0 && lis[i].Latency > 0.0 {
return true
}
if lis[i].Latency == 0.0 && lis[j].Latency > 0.0 {
return false
}
return lis[i].Latency < lis[j].Latency
}
func (lis peerResults) Swap(i, j int) {
lis[i], lis[j] = lis[j], lis[i]
}
func fnOrderByPeer(rq *Request) listPeer {
peers := make(map[string]peer)
for i := range rq.Responses {
if rq.Responses[i] == nil || rq.Responses[i].Peer == nil {
continue
}
rs := rq.Responses[i]
p, ok := peers[rs.Peer.Owner]
if !ok {
p.RequestID = rq.RequestID
p.Country = rs.Peer.Country
p.Name = rs.Peer.Name
p.Nick = rs.Peer.Nick
p.Note = rs.Peer.Note
p.Latency = rs.Latency
p.Jitter = rs.Jitter
p.VPNTypes = rs.Peer.Type
}
p.Results = append(p.Results, peerResult{
RequestID: rq.RequestID,
Name: rs.Peer.Name,
Country: rs.Peer.Country,
Latency: rs.Latency,
Jitter: rs.Jitter,
})
peers[rs.Peer.Owner] = p
}
peerList := make(listPeer, 0, len(peers))
for i := range peers {
v := peers[i]
sort.Sort(v.Results)
v.Name = v.Results[0].Name
v.Country = v.Results[0].Country
v.Latency = v.Results[0].Latency
v.Jitter = v.Results[0].Jitter
peerList = append(peerList, v)
}
sort.Sort(peerList)
return peerList
}
func fnCountResponses(rq *Request) int {
count := 0
for _, res := range rq.Responses {
if !res.Unreachable {
count++
}
}
return count
}
// func filter(peer *Peer, requests, responses event.Events) *RequestSubmitted {
// have := make(map[string]struct{}, len(responses))
// for _, res := range toList[ResultSubmitted](responses...) {
// have[res.RequestID] = struct{}{}
// }
// for _, req := range reverse(toList[RequestSubmitted](requests...)...) {
// if _, ok := have[req.RequestID()]; !ok {
// if !peer.CanSupport(req.RequestIP) {
// continue
// }
// return req
// }
// }
// return nil
// }
// func toList[E any, T es.PE[E]](lis ...event.Event) []T {
// newLis := make([]T, 0, len(lis))
// for i := range lis {
// if e, ok := lis[i].(T); ok {
// newLis = append(newLis, e)
// }
// }
// return newLis
// }
// func reverse[T any](s ...T) []T {
// first, last := 0, len(s)-1
// for first < last {
// s[first], s[last] = s[last], s[first]
// first++
// last--
// }
// return s
// }

216
app/peerfinder/jobs.go Normal file
View File

@ -0,0 +1,216 @@
package peerfinder
import (
"context"
"encoding/json"
"errors"
"fmt"
"log"
"net/http"
"time"
"go.sour.is/ev"
"go.sour.is/ev/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
}

View File

@ -0,0 +1,38 @@
{{define "main"}}
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
{{template "meta" .}}
<title>DN42 PingFinder</title>
<link href="/peers/assets/bootstrap.min.css" rel="stylesheet" integrity="sha384-1q8mTJOASx8j1Au+a5WDVnPi2lkFfwwEAa8hDDdjZlpLegxhjVME1fgjWPGmkzs7" crossorigin="anonymous">
<link href="/peers/assets/peerfinder.css" rel="stylesheet" crossorigin="anonymous">
</head>
<body>
<div class="container-fluid">
<div class="header clearfix">
<nav>
<ul class="nav nav-pills pull-right">
<li role="presentation"><a href="/peers">Home</a></li>
<!--
<li role="presentation"><a href="/peers/status">Status</a></li>
-->
<li role="presentation"><a href="//util.sour.is/peer">Sign up/Manage</a></li>
<li role="presentation"><a href="https://git.dn42.dev/dn42/pingfinder/src/branch/master/clients">Scripts</a></li>
</ul>
</nav>
<h3 class="text-muted">DN42 PeerFinder</h3>
</div>
</div>
<div class=container>
{{template "content" .}}
</div>
</body>
</html>
{{end}}

View File

@ -0,0 +1,65 @@
{{template "main" .}}
{{define "meta"}}
<meta http-equiv="refresh" content="30">
{{end}}
{{define "content"}}
<h2>What is this?</h2>
<p>This tool allows you to find "good" peerings
for <a href="https://dn42.net">dn42</a>, by measuring the latency from
various points in the network towards you.</p>
<p>If you don't know what dn42 is,
read <a href="https://dn42.net/Home">the website</a> and in particular
the <a href="https://dn42.net/Getting-started-with-dn42">Getting Started
guide</a>.</p>
<h2>How does it work?</h2>
<p>
<ol>
<li>You enter your (Internet) IP address</li>
<li>Various routers participating in dn42 will ping you over the Internet</li>
<li>After a short while, you get back all the latency results</li>
<li>You can then peer with people close to you (low latency)</li>
</ol>
</p>
<form class="form-inline" method="POST" action="/peers/req">
<label>Ping IP Address [Check Hidden?]:</label>
<div class="input-group input-group-sm">
<input class="form-control" type="text" name="req_ip" placeholder="{{ .RemoteIP }}">
<span class="input-group-addon">
<input type="checkbox" name="req_hidden" value=1 aria-label="Hidden?">
</span>
</div>
<button class="btn btn-default" type="submit">Submit</button>
</form>
<p>If you mark your measurement as hidden, it will not be displayed on the
page below. Note that the IP addresses of the target will be shown alongside the result.</p>
<div class=row>
<h2>Results</h2>
{{ with $args := . }}
{{ range $req := .Requests }}
{{ if ne $req.RequestID "" }}
<div class="panel panel-primary">
<div class="panel-heading">
<a href="/peers/req/{{ $req.RequestID }}">
{{ $req.RequestIP }} on {{ $req.Created.Format "02 Jan 06 15:04 MST" }}
</a> &mdash; <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}}

View File

@ -0,0 +1,50 @@
{{template "main" .}}
{{define "meta"}}
<meta http-equiv="refresh" content="30">
{{end}}
{{define "content"}}
{{range .Requests}}
<h2>Results to {{.RequestIP}}{{if .Hidden}} 👁️{{end}}</h2>
{{range orderByPeer .}}
<div class="panel panel-primary" id="peer-{{.Nick}}">
<div class="panel-heading">
<b> {{.Country}} :: {{.Name}} :: {{.Nick}} </b>
<div style='float:right'>
<a class='btn btn-success' href="#peer-{{.Nick}}">{{ if eq .Latency 0.0 }}&mdash;{{ 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 }}&mdash;{{ else }}{{printf "%0.3f ms" .Latency}}{{ end }}</td>
<td>{{ if eq .Jitter 0.0 }}&mdash;{{ else }}{{ printf "%0.3f ms" .Jitter }}{{ end }}</td>
</tr>
{{end}}
</tbody>
</table>
</div>
</div>
{{end}}
{{end}}
{{end}}

179
app/peerfinder/service.go Normal file
View File

@ -0,0 +1,179 @@
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/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
}

241
app/salty/blobs.go Normal file
View File

@ -0,0 +1,241 @@
package salty
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"strings"
"go.opentelemetry.io/otel/metric"
"go.sour.is/pkg/authreq"
"go.sour.is/pkg/lg"
"go.uber.org/multierr"
)
var (
ErrAddressExists = errors.New("error: address already exists")
ErrBlobNotFound = errors.New("error: blob not found")
)
func WithBlobStore(path string) *withBlobStore {
return &withBlobStore{path: path}
}
type withBlobStore struct {
path string
m_get_blob metric.Int64Counter
m_put_blob metric.Int64Counter
m_delete_blob metric.Int64Counter
}
func (o *withBlobStore) ApplySalty(s *service) {}
func (o *withBlobStore) Setup(ctx context.Context) error {
ctx, span := lg.Span(ctx)
defer span.End()
var err, errs error
err = os.MkdirAll(o.path, 0700)
if err != nil {
return err
}
m := lg.Meter(ctx)
o.m_get_blob, err = m.Int64Counter("salty_get_blob",
metric.WithDescription("salty get blob called"),
)
errs = multierr.Append(errs, err)
o.m_put_blob, err = m.Int64Counter("salty_put_blob",
metric.WithDescription("salty put blob called"),
)
errs = multierr.Append(errs, err)
o.m_delete_blob, err = m.Int64Counter("salty_delete_blob",
metric.WithDescription("salty delete blob called"),
)
errs = multierr.Append(errs, err)
return errs
}
func (o *withBlobStore) RegisterAPIv1(mux *http.ServeMux) {
mux.Handle("/blob/", authreq.Authorization(o))
}
func (o *withBlobStore) ServeHTTP(w http.ResponseWriter, r *http.Request) {
ctx, span := lg.Span(r.Context())
defer span.End()
claims := authreq.FromContext(ctx)
if claims == nil {
httpError(w, http.StatusUnauthorized)
return
}
signer := claims.Issuer
key := strings.TrimPrefix(r.URL.Path, "/blob/")
switch r.Method {
case http.MethodDelete:
if err := deleteBlob(o.path, key, signer); err != nil {
if errors.Is(err, ErrBlobNotFound) {
httpError(w, http.StatusNotFound)
return
}
span.RecordError(fmt.Errorf("%w: getting blob %s for %s", err, key, signer))
httpError(w, http.StatusInternalServerError)
return
}
http.Error(w, "Blob Deleted", http.StatusOK)
case http.MethodGet, http.MethodHead:
blob, err := getBlob(o.path, key, signer)
if err != nil {
if errors.Is(err, ErrBlobNotFound) {
httpError(w, http.StatusNotFound)
return
}
span.RecordError(fmt.Errorf("%w: getting blob %s for %s", err, key, signer))
httpError(w, http.StatusInternalServerError)
return
}
defer blob.Close()
blob.SetHeaders(r)
if r.Method == http.MethodGet {
_, _ = io.Copy(w, blob)
}
case http.MethodPut:
data, err := io.ReadAll(r.Body)
if err != nil {
httpError(w, http.StatusInternalServerError)
return
}
defer r.Body.Close()
if err := putBlob(o.path, key, data, signer); err != nil {
span.RecordError(fmt.Errorf("%w: putting blob %s for %s", err, key, signer))
httpError(w, http.StatusInternalServerError)
return
}
http.Error(w, "Blob Created", http.StatusCreated)
default:
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
}
}
func putBlob(path string, key string, data []byte, signer string) error {
p := filepath.Join(path, signer, key)
if err := os.MkdirAll(p, 0700); err != nil {
return fmt.Errorf("error creating blobs paths %s: %w", p, err)
}
fn := filepath.Join(p, "content")
if err := os.WriteFile(fn, data, os.FileMode(0600)); err != nil {
return fmt.Errorf("error writing blob %s: %w", fn, err)
}
return nil
}
func getBlob(path string, key string, signer string) (*Blob, error) {
p := filepath.Join(path, signer, key)
if err := os.MkdirAll(p, 0755); err != nil {
return nil, fmt.Errorf("error creating blobs paths %s: %w", p, err)
}
fn := filepath.Join(p, "content")
if !FileExists(fn) {
return nil, ErrBlobNotFound
}
return OpenBlob(fn)
}
func deleteBlob(path string, key string, signer string) error {
p := filepath.Join(path, signer, key)
if !FileExists(p) {
return ErrBlobNotFound
}
return os.RemoveAll(p)
}
// FileExists returns true if the given file exists
func FileExists(name string) bool {
if _, err := os.Stat(name); err != nil {
if os.IsNotExist(err) {
return false
}
}
return true
}
func httpError(w http.ResponseWriter, code int) {
http.Error(w, http.StatusText(code), code)
}
// Blob defines the type, filename and whether or not a blob is publicly accessible or not.
// A Blob also holds zero or more properties as a map of key/value pairs of string interpreted
// by the client.
type Blob struct {
r io.ReadSeekCloser `json:"-"`
Type string `json:"type"`
Public bool `json:"public"`
Filename string `json:"-"`
Properties map[string]string `json:"props"`
}
// Close closes the blob and the underlying io.ReadSeekCloser
func (b *Blob) Close() error { return b.r.Close() }
// Read reads data from the blob from the underlying io.ReadSeekCloser
func (b *Blob) Read(p []byte) (n int, err error) { return b.r.Read(p) }
// SetHeaders sets HTTP headers on the net/http.Request object based on the blob's type, filename
// and various other properties (if any).
func (b *Blob) SetHeaders(r *http.Request) {
// TODO: Implement this...
}
// OpenBlob opens a blob at the given path and returns a Blob object
func OpenBlob(fn string) (*Blob, error) {
f, err := os.Open(fn)
if err != nil {
return nil, fmt.Errorf("%w: opening blob %s", err, fn)
}
b := &Blob{r: f, Filename: fn}
props := filepath.Join(filepath.Dir(fn), "props.json")
if FileExists(filepath.Dir(props)) {
pf, err := os.Open(props)
if err != nil {
return b, fmt.Errorf("%w: opening blob props %s", err, props)
}
err = json.NewDecoder(pf).Decode(b)
if err != nil {
return b, fmt.Errorf("%w: opening blob props %s", err, props)
}
}
return b, nil
}

156
app/salty/salty-addr.go Normal file
View File

@ -0,0 +1,156 @@
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
}

94
app/salty/salty-user.go Normal file
View File

@ -0,0 +1,94 @@
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/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)
}

13
app/salty/salty.graphqls Normal file
View File

@ -0,0 +1,13 @@
extend type Query {
saltyUser(nick: String!): SaltyUser
}
extend type Mutation {
createSaltyUser(nick: String! pubkey: String!): SaltyUser
}
type SaltyUser @goModel(model: "go.sour.is/tools/app/salty.SaltyUser"){
pubkey: String!
inbox: String!
endpoint: String!
}

397
app/salty/service.go Normal file
View File

@ -0,0 +1,397 @@
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/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
app/twtxt/twtxt.go Normal file
View File

@ -0,0 +1 @@
package twtxt

35
app/webfinger/addr.go Normal file
View File

@ -0,0 +1,35 @@
package webfinger
import (
"net/url"
"strings"
)
type Addr struct {
prefix []string
URL *url.URL
}
func Parse(s string) *Addr {
addr := &Addr{}
addr.URL, _ = url.Parse(s)
if addr.URL.Opaque == "" {
return addr
}
var hasPfx = true
pfx := addr.URL.Scheme
for hasPfx {
addr.prefix = append(addr.prefix, pfx)
pfx, addr.URL.Opaque, hasPfx = strings.Cut(addr.URL.Opaque, ":")
}
user, host, _ := strings.Cut(pfx, "@")
addr.URL.User = url.User(user)
addr.URL.Host = host
return addr
}

46
app/webfinger/client.go Normal file
View File

@ -0,0 +1,46 @@
package webfinger
import (
"crypto/ed25519"
"encoding/base64"
"time"
"github.com/golang-jwt/jwt/v4"
"github.com/oklog/ulid/v2"
)
var (
defaultExpire = 30 * time.Minute
defaultIssuer = "sour.is/webfinger"
defaultAudience = "sour.is/webfinger"
)
func NewSignedRequest(jrd *JRD, key ed25519.PrivateKey) (string, error) {
type claims struct {
PubKey string `json:"pub"`
*JRD
jwt.RegisteredClaims
}
pub := []byte(key.Public().(ed25519.PublicKey))
j := claims{
PubKey: enc(pub),
JRD: jrd.CloneValues(),
RegisteredClaims: jwt.RegisteredClaims{
ID: ulid.Make().String(),
Subject: jrd.Subject,
Audience: jwt.ClaimStrings{defaultAudience},
ExpiresAt: jwt.NewNumericDate(time.Now().Add(defaultExpire)),
IssuedAt: jwt.NewNumericDate(time.Now()),
Issuer: defaultIssuer,
},
}
j.JRD.Subject = "" // move subject into registered claims.
token := jwt.NewWithClaims(jwt.SigningMethodEdDSA, &j)
return token.SignedString(key)
}
func enc(b []byte) string {
return base64.RawURLEncoding.EncodeToString(b)
}

44
app/webfinger/events.go Normal file
View File

@ -0,0 +1,44 @@
package webfinger
import (
"go.sour.is/ev/event"
)
type SubjectSet struct {
Subject string `json:"subject"`
Aliases []string `json:"aliases,omitempty"`
Properties map[string]*string `json:"properties,omitempty"`
event.IsEvent `json:"-"`
}
type SubjectDeleted struct {
Subject string `json:"subject"`
event.IsEvent `json:"-"`
}
var _ event.Event = (*SubjectDeleted)(nil)
type LinkSet struct {
Index uint64 `json:"idx"`
Rel string `json:"rel"`
Type string `json:"type,omitempty"`
HRef string `json:"href,omitempty"`
Titles map[string]string `json:"titles,omitempty"`
Properties map[string]*string `json:"properties,omitempty"`
Template string `json:"template,omitempty"`
event.IsEvent `json:"-"`
}
var _ event.Event = (*LinkSet)(nil)
type LinkDeleted struct {
Index uint64 `json:"idx"`
Rel string `json:"rel"`
event.IsEvent `json:"-"`
}
var _ event.Event = (*LinkDeleted)(nil)

426
app/webfinger/jrd.go Normal file
View File

@ -0,0 +1,426 @@
package webfinger
import (
"bytes"
"encoding/base64"
"encoding/json"
"fmt"
"hash/fnv"
"sort"
"go.sour.is/pkg/set"
"go.sour.is/pkg/slice"
"gopkg.in/yaml.v3"
"go.sour.is/ev/event"
)
func StreamID(subject string) string {
h := fnv.New128a()
h.Write([]byte(subject))
return "webfinger." + base64.RawURLEncoding.EncodeToString(h.Sum(nil))
}
// JRD is a JSON Resource Descriptor, specifying properties and related links
// for a resource.
type JRD struct {
Subject string `json:"subject,omitempty" yaml:"subject,omitempty"`
Aliases []string `json:"aliases,omitempty" yaml:"aliases,omitempty"`
Properties map[string]*string `json:"properties,omitempty" yaml:"properties,omitempty"`
Links Links `json:"links,omitempty" yaml:"links,omitempty"`
deleted bool
event.IsAggregate `json:"-" yaml:"-"`
}
func (a *JRD) CloneValues() *JRD {
m := make(map[string]*string, len(a.Properties))
for k, v := range a.Properties {
m[k] = v
}
return &JRD{
Subject: a.Subject,
Aliases: append([]string{}, a.Aliases...),
Properties: m,
Links: append([]*Link{}, a.Links...),
}
}
var _ event.Aggregate = (*JRD)(nil)
// Link is a link to a related resource.
type Link struct {
Index uint64 `json:"-" yaml:"-"`
Rel string `json:"rel,omitempty"`
Type string `json:"type,omitempty"`
HRef string `json:"href,omitempty"`
Titles map[string]string `json:"titles,omitempty"`
Properties map[string]*string `json:"properties,omitempty"`
Template string `json:"template,omitempty"`
}
type Links []*Link
// Len is the number of elements in the collection.
func (l Links) Len() int {
if l == nil {
return 0
}
return len(l)
}
// Less reports whether the element with index i
func (l Links) Less(i int, j int) bool {
if l[i] == nil || l[j] == nil {
return false
}
if l[i].Rel == l[j].Rel {
return l[i].Type < l[j].Type
}
return l[i].Rel < l[j].Rel
}
// Swap swaps the elements with indexes i and j.
func (l Links) Swap(i int, j int) {
if l == nil {
return
}
l[i], l[j] = l[j], l[i]
}
// ParseJRD parses the JRD using json.Unmarshal.
func ParseJRD(blob []byte) (*JRD, error) {
jrd := JRD{}
err := json.Unmarshal(blob, &jrd)
if err != nil {
return nil, err
}
for i := range jrd.Links {
jrd.Links[i].Index = uint64(i)
}
return &jrd, nil
}
// GetLinkByRel returns the first *Link with the specified rel value.
func (jrd *JRD) GetLinkByRel(rel string) *Link {
for _, link := range jrd.Links {
if link.Rel == rel {
return link
}
}
return nil
}
// GetLinksByRel returns each *Link with the specified rel value.
func (jrd *JRD) GetLinksByRel(rel ...string) []*Link {
var lis []*Link
rels := set.New(rel...)
for _, link := range jrd.Links {
if rels.Has(link.Rel) {
lis = append(lis, link)
}
}
return lis
}
// GetProperty Returns the property value as a string.
// Per spec a property value can be null, empty string is returned in this case.
func (jrd *JRD) GetProperty(uri string) string {
if jrd.Properties[uri] == nil {
return ""
}
return *jrd.Properties[uri]
}
func (a *JRD) SetProperty(name string, value *string) {
if a.Properties == nil {
a.Properties = make(map[string]*string)
}
a.Properties[name] = value
}
func (a *JRD) DeleteProperty(name string) {
if a.Properties == nil {
return
}
delete(a.Properties, name)
}
func (a *JRD) IsDeleted() bool {
return a.deleted
}
// GetProperty Returns the property value as a string.
// Per spec a property value can be null, empty string is returned in this case.
func (link *Link) GetProperty(uri string) string {
if link.Properties[uri] == nil {
return ""
}
return *link.Properties[uri]
}
func (link *Link) SetProperty(name string, value *string) {
if link.Properties == nil {
link.Properties = make(map[string]*string)
}
link.Properties[name] = value
}
func (link *Link) DeleteProperty(name string) {
if link.Properties == nil {
return
}
delete(link.Properties, name)
}
// ApplyEvent implements event.Aggregate
func (a *JRD) ApplyEvent(events ...event.Event) {
for _, e := range events {
switch e := e.(type) {
case *SubjectSet:
a.deleted = false
a.Subject = e.Subject
a.Aliases = e.Aliases
a.Properties = e.Properties
case *SubjectDeleted:
a.deleted = true
a.Subject = e.Subject
a.Aliases = a.Aliases[:0]
a.Links = a.Links[:0]
a.Properties = map[string]*string{}
case *LinkSet:
link, ok := slice.FindFn(func(l *Link) bool { return l.Index == e.Index }, a.Links...)
if !ok {
link = &Link{}
link.Index = uint64(len(a.Links))
a.Links = append(a.Links, link)
}
link.Rel = e.Rel
link.HRef = e.HRef
link.Type = e.Type
link.Titles = e.Titles
link.Properties = e.Properties
link.Template = e.Template
case *LinkDeleted:
a.Links = slice.FilterFn(func(link *Link) bool { return link.Index != e.Index }, a.Links...)
}
}
}
const NSauth = "https://sour.is/ns/auth"
const NSpubkey = "https://sour.is/ns/pubkey"
const NSredirect = "https://sour.is/rel/redirect"
func (a *JRD) OnAuth(claim, auth *JRD) error {
pubkey := claim.Properties[NSpubkey]
if v, ok := auth.Properties[NSpubkey]; ok && v != nil && cmpPtr(v, pubkey) {
// pubkey matches!
} else {
return fmt.Errorf("pubkey does not match")
}
if a.Version() > 0 && !a.IsDeleted() && a.Subject != claim.Subject {
return fmt.Errorf("subject does not match")
}
if auth.Subject == claim.Subject {
claim.SetProperty(NSpubkey, pubkey)
} else {
claim.SetProperty(NSauth, &auth.Subject)
claim.DeleteProperty(NSpubkey)
}
return nil
}
func (a *JRD) OnDelete(jrd *JRD) error {
if a.Version() == 0 || a.IsDeleted() {
return nil
}
event.Raise(a, &SubjectDeleted{Subject: jrd.Subject})
return nil
}
func (a *JRD) OnClaims(jrd *JRD) error {
err := a.OnSubjectSet(jrd.Subject, jrd.Aliases, jrd.Properties)
if err != nil {
return err
}
for _, z := range slice.Align(
a.Links, // old
jrd.Links, // new
func(l, r *Link) bool { return l.Index < r.Index },
) {
// Not in new == delete
if z.Key == nil {
link := *z.Value
event.Raise(a, &LinkDeleted{Index: link.Index, Rel: link.Rel})
continue
}
// Not in old == create
if z.Value == nil {
link := *z.Key
event.Raise(a, &LinkSet{
Index: link.Index,
Rel: link.Rel,
Type: link.Type,
HRef: link.HRef,
Titles: link.Titles,
Properties: link.Properties,
Template: link.Template,
})
continue
}
// in both == compare
a.OnLinkSet(*z.Key, *z.Value)
}
return nil
}
func (a *JRD) OnSubjectSet(subject string, aliases []string, props map[string]*string) error {
modified := false
e := &SubjectSet{
Subject: subject,
Aliases: aliases,
Properties: props,
}
if subject != a.Subject {
modified = true
}
sort.Strings(aliases)
sort.Strings(a.Aliases)
for _, z := range slice.Zip(aliases, a.Aliases) {
if z.Key != z.Value {
modified = true
break
}
}
for _, z := range slice.Zip(
slice.Zip(slice.FromMap(props)),
slice.Zip(slice.FromMap(a.Properties)),
) {
newValue := z.Key
curValue := z.Value
if newValue.Key != curValue.Key {
modified = true
break
}
if !cmpPtr(newValue.Value, curValue.Value) {
modified = true
break
}
}
if modified {
event.Raise(a, e)
}
return nil
}
func (a *JRD) OnLinkSet(o, n *Link) error {
modified := false
e := &LinkSet{
Index: n.Index,
Rel: n.Rel,
Type: n.Type,
HRef: n.HRef,
Titles: n.Titles,
Properties: n.Properties,
Template: n.Template,
}
if n.Rel != o.Rel {
modified = true
}
if n.Type != o.Type {
modified = true
}
if n.HRef != o.HRef {
modified = true
}
if n.Template != o.Template {
fmt.Println(360, n.Template, o.Template, e.Template)
modified = true
}
nKeys := slice.FromMapKeys(n.Properties)
sort.Strings(nKeys)
oKeys := slice.FromMapKeys(o.Properties)
sort.Strings(oKeys)
for _, z := range slice.Zip(
slice.Zip(nKeys, slice.FromMapValues(n.Titles, nKeys)),
slice.Zip(oKeys, slice.FromMapValues(o.Titles, oKeys)),
) {
if z.Key != z.Value {
modified = true
break
}
}
nKeys = slice.FromMapKeys(n.Properties)
sort.Strings(nKeys)
oKeys = slice.FromMapKeys(o.Properties)
sort.Strings(oKeys)
for _, z := range slice.Zip(
slice.Zip(nKeys, slice.FromMapValues(n.Properties, nKeys)),
slice.Zip(oKeys, slice.FromMapValues(o.Properties, oKeys)),
) {
newValue := z.Key
curValue := z.Value
if newValue.Key != curValue.Key {
modified = true
break
}
if !cmpPtr(newValue.Value, curValue.Value) {
modified = true
break
}
}
if modified {
event.Raise(a, e)
}
return nil
}
func cmpPtr[T comparable](l, r *T) bool {
if l == nil {
return r == nil
}
if r == nil {
return l == nil
}
return *l == *r
}
func (a *JRD) String() string {
b := &bytes.Buffer{}
y := yaml.NewEncoder(b)
_ = y.Encode(a)
return b.String()
}

310
app/webfinger/jrd_test.go Normal file
View File

@ -0,0 +1,310 @@
package webfinger_test
import (
"context"
"crypto/ed25519"
"encoding/base64"
"encoding/json"
"fmt"
"strings"
"testing"
"time"
jwt "github.com/golang-jwt/jwt/v4"
"github.com/matryer/is"
"go.sour.is/ev"
"go.uber.org/multierr"
"go.sour.is/tools/app/webfinger"
memstore "go.sour.is/ev/driver/mem-store"
"go.sour.is/ev/driver/projecter"
"go.sour.is/ev/driver/streamer"
"go.sour.is/ev/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()
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,95 @@
/* Space out content a bit */
body {
padding-top: 20px;
padding-bottom: 20px;
}
/* Everything but the jumbotron gets side spacing for mobile first views */
.header,
.footer {
padding-right: 15px;
padding-left: 15px;
}
/* Custom page header */
.header {
padding-bottom: 20px;
border-bottom: 1px solid #e5e5e5;
}
/* Make the masthead heading the same height as the navigation */
.header h3 {
margin-top: 0;
margin-bottom: 0;
line-height: 40px;
}
/* Custom page footer */
.footer {
padding-top: 19px;
color: #777;
border-top: 1px solid #e5e5e5;
}
.panel-heading a {
color: white;
font-weight: bold;
}
.container-narrow > hr {
margin: 30px 0;
}
@media (prefers-color-scheme: dark) {
body, .panel-body {
color: white;
background-color: #222;
}
nav.navbar-default {
background-color: rgb(35, 29, 71);
}
.navbar-default .navbar-brand {
color: white;
}
.panel-primary, .list-group, .list-group-item {
color: white;
background-color: #16181c;
}
.table > tbody > tr.active > th, .table > tbody > tr.active > td {
background-color: rgb(35, 29, 71);
}
.table-striped > tbody > tr:nth-of-type(2n+1) {
background-color: rgb(35, 29, 71);
}
.panel pre {
color: white;
background-color: #16181c;
}
.panel .panel-primary > .panel-heading {
background-color: rgb(35, 29, 71);
}
.panel a {
color: cornflowerblue;
}
code {
color: white;
background-color: #282b32;
}
}
@import url(https://cdn.jsdelivr.net/npm/firacode@6.2.0/distr/fira_code.css);
code { font-family: 'Fira Code', monospace; }
@media (min-width: 100) {
.truncate {
width: 750px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.container {
width: 750px;
}
}

View File

@ -0,0 +1,28 @@
{{define "main"}}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
{{template "meta" .}}
<title>👉 Webfinger 👈</title>
<link href="/webfinger/assets/bootstrap.min.css" rel="stylesheet" integrity="sha384-1q8mTJOASx8j1Au+a5WDVnPi2lkFfwwEAa8hDDdjZlpLegxhjVME1fgjWPGmkzs7" crossorigin="anonymous">
<link href="/webfinger/assets/webfinger.css" rel="stylesheet" crossorigin="anonymous">
</head>
<body>
<nav class="navbar navbar-default">
<div class="container-fluid">
<a class="navbar-brand" href="/webfinger">👉 Webfinger 👈</a>
</div>
</div>
</nav>
<div class=container>
{{template "content" .}}
</div>
</body>
</html>
{{end}}

View File

@ -0,0 +1,131 @@
{{template "main" .}}
{{define "meta"}}{{end}}
{{define "content"}}
<form method="GET">
<div class="input-group">
<span class="input-group-addon" id="basic-addon1">resource</span>
<input name="resource" class="form-control" placeholder="acct:..."/>
<span class="input-group-btn">
<button class="btn btn-default" type="submit">Go!</button>
</span>
</div>
</form>
<br/>
{{ if ne .Err nil }}
<div class="alert alert-danger" role="alert">
{{ .Err }}
</div>
{{ end }}
{{ if ne .JRD nil }}
<div class="panel panel-primary">
<div class="panel-heading">Webfinger Result</div>
<table class="table">
<tr>
<th style="width:98px">Subject</th>
<td>
<div class="media">
<div class="media-body">
{{ .JRD.Subject }}
</div>
{{ with .JRD.GetLinkByRel "http://webfinger.net/rel/avatar" }}
{{ if ne . nil }}
<div class="media-left media-middle">
<div class="panel panel-default">
<div class="panel-body">
<img src="{{ .HRef }}" />
</div>
</div>
</div>
{{ end }}
{{ end }}
</div>
</td>
</tr>
{{if ne (len .JRD.Aliases) 0}}
<tr>
<th>Aliases</th>
<td>
<ul class="list-group">
{{ range .JRD.Aliases }}<li class="list-group-item">{{ . }}</li>
{{ end }}
</ul>
</td>
</tr>
{{ end }}
{{ if ne (len .JRD.Properties) 0 }}
<tr>
<th>Properties</th>
<td>
<div class="list-group truncate">
{{ range $key, $value := .JRD.Properties }}<div class="list-group-item">
<h5 class="list-group-item-heading" title="{{ $key }}">{{ propName $key }}</h5>
<code class="list-group-item-text">{{ $value }}</code>
</div>
{{ end }}
</div>
</td>
</tr>
{{ end }}
{{ if ne (len .JRD.Links) 0 }}
{{ range .JRD.Links }}
<tr class="active">
{{ if ne (len .Template) 0 }}
<th> Template </th>
<td>{{ .Template }}</td>
{{ else }}
<th> Link </th>
<td>{{ if ne (len .HRef) 0 }}<a href="{{ .HRef }}" target="_blank">{{ .HRef }}</a>{{ end }}</td>
{{ end }}
<tr>
<tr>
<th> Properties </th>
<td>
<div class="list-group">
<div class="list-group-item truncate">
<h5 class="list-group-item-heading">rel<h5>
<code class="list-group-item-text">{{ .Rel }}</code>
</div>
{{ if ne (len .Type) 0 }}<div class="list-group-item truncate">
<h5 class="list-group-item-heading">type</h5>
<code class="list-group-item-text">{{ .Type }}</code>
</div>
{{ end }}
{{ range $key, $value := .Properties }}<div class="list-group-item truncate">
<h5 class="list-group-item-heading" title="{{ $key }}">{{ propName $key }}</h5>
<code class="list-group-item-text">{{ $value }}</code>
</div>
{{ end }}
</div>
</td>
</tr>
{{ end }}
{{ end }}
</table>
</div>
{{ end }}
<div class="panel panel-primary">
<div class="panel-heading">Raw JRD</div>
<pre style="height: 15em; overflow-y: auto; border: 0px">
Status: {{ .Status }}
{{ .Body | printf "%s" }}
</pre>
</div>
{{end}}

416
app/webfinger/webfinger.go Normal file
View File

@ -0,0 +1,416 @@
package webfinger
import (
"context"
"crypto/ed25519"
"embed"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"html"
"html/template"
"io"
"io/fs"
"log"
"net/http"
"net/http/httptest"
"net/url"
"os"
"strings"
"github.com/golang-jwt/jwt/v4"
"go.sour.is/pkg/lg"
"go.sour.is/pkg/set"
"go.sour.is/ev"
"go.sour.is/ev/event"
)
var (
//go:embed ui/*/*
files embed.FS
templates map[string]*template.Template
)
type service struct {
es *ev.EventStore
self set.Set[string]
cache func(string) bool
}
type Option interface {
ApplyWebfinger(s *service)
}
type WithHostnames []string
func (o WithHostnames) ApplyWebfinger(s *service) {
s.self = set.New(o...)
}
type WithCache func(string) bool
func (o WithCache) ApplyWebfinger(s *service) {
s.cache = o
}
func New(ctx context.Context, es *ev.EventStore, opts ...Option) (*service, error) {
ctx, span := lg.Span(ctx)
defer span.End()
if err := event.Register(
ctx,
&SubjectSet{},
&SubjectDeleted{},
&LinkSet{},
&LinkDeleted{},
); err != nil {
return nil, err
}
svc := &service{es: es}
for _, o := range opts {
o.ApplyWebfinger(svc)
}
return svc, nil
}
func (s *service) RegisterHTTP(mux *http.ServeMux) {
a, _ := fs.Sub(files, "ui/assets")
assets := http.StripPrefix("/webfinger/assets/", http.FileServer(http.FS(a)))
mux.Handle("/webfinger", s.ui())
mux.Handle("/webfinger/assets/", assets)
}
func (s *service) RegisterWellKnown(mux *http.ServeMux) {
mux.Handle("/webfinger", lg.Htrace(s, "webfinger"))
}
func (s *service) ServeHTTP(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
ctx, span := lg.Span(ctx)
defer span.End()
if r.URL.Path != "/webfinger" {
w.WriteHeader(http.StatusNotFound)
fmt.Fprint(w, http.StatusText(http.StatusNotFound))
return
}
switch r.Method {
case http.MethodPut, http.MethodDelete:
if r.ContentLength > 4096 {
w.WriteHeader(http.StatusRequestEntityTooLarge)
fmt.Fprint(w, http.StatusText(http.StatusRequestEntityTooLarge))
span.AddEvent("request too large")
return
}
body, err := io.ReadAll(io.LimitReader(r.Body, 4096))
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
fmt.Fprint(w, http.StatusText(http.StatusInternalServerError))
span.RecordError(err)
return
}
r.Body.Close()
type claims struct {
PubKey string `json:"pub"`
*JRD
jwt.RegisteredClaims
}
token, err := jwt.ParseWithClaims(
string(body),
&claims{},
func(tok *jwt.Token) (any, error) {
c, ok := tok.Claims.(*claims)
if !ok {
return nil, fmt.Errorf("wrong type of claim")
}
if c.JRD == nil {
c.JRD = &JRD{}
}
c.JRD.Subject = c.RegisteredClaims.Subject
c.SetProperty(NSpubkey, &c.PubKey)
pub, err := dec(c.PubKey)
return ed25519.PublicKey(pub), err
},
jwt.WithValidMethods([]string{"EdDSA"}),
jwt.WithJSONNumber(),
)
if err != nil {
w.WriteHeader(http.StatusUnprocessableEntity)
fmt.Fprint(w, http.StatusText(http.StatusUnprocessableEntity), ": ", err.Error())
span.RecordError(err)
return
}
c, ok := token.Claims.(*claims)
if !ok {
w.WriteHeader(http.StatusUnprocessableEntity)
fmt.Fprint(w, http.StatusText(http.StatusUnprocessableEntity))
span.AddEvent("not a claim")
return
}
if c.ID != "" && s.cache != nil {
if ok := s.cache(c.ID); ok {
w.WriteHeader(http.StatusAlreadyReported)
fmt.Fprint(w, http.StatusText(http.StatusAlreadyReported))
span.AddEvent("already seen ID")
return
}
}
json.NewEncoder(os.Stdout).Encode(c.JRD)
for i := range c.JRD.Links {
c.JRD.Links[i].Index = uint64(i)
}
a, err := ev.Upsert(ctx, s.es, StreamID(c.JRD.Subject), func(ctx context.Context, a *JRD) error {
var auth *JRD
for i := range a.Links {
a.Links[i].Index = uint64(i)
}
// does the target have a pubkey for self auth?
if _, ok := a.Properties[NSpubkey]; ok {
auth = a
}
// Check current version for auth.
if authID, ok := a.Properties[NSauth]; ok && authID != nil && auth == nil {
auth = &JRD{}
auth.SetStreamID(StreamID(*authID))
err := s.es.Load(ctx, auth)
if err != nil {
return err
}
}
if a.Version() == 0 || a.IsDeleted() {
// else does the new object claim auth from another object?
if authID, ok := c.Properties[NSauth]; ok && authID != nil && auth == nil {
auth = &JRD{}
auth.SetStreamID(StreamID(*authID))
err := s.es.Load(ctx, auth)
if err != nil {
return err
}
}
// fall back to use auth from submitted claims
if auth == nil {
auth = c.JRD
}
}
if auth == nil {
return fmt.Errorf("auth not found")
}
err = a.OnAuth(c.JRD, auth)
if err != nil {
return err
}
if r.Method == http.MethodDelete {
return a.OnDelete(c.JRD)
}
return a.OnClaims(c.JRD)
})
if err != nil {
w.WriteHeader(http.StatusUnprocessableEntity)
fmt.Fprint(w, http.StatusText(http.StatusUnprocessableEntity), ": ", err.Error())
span.RecordError(err)
return
}
if version := a.Version(); r.Method == http.MethodDelete && version > 0 {
err = s.es.Truncate(ctx, a.StreamID(), int64(version))
span.RecordError(err)
}
w.Header().Set("Content-Type", "application/jrd+json")
if r.Method == http.MethodDelete {
w.WriteHeader(http.StatusNoContent)
} else {
w.WriteHeader(http.StatusCreated)
}
j := json.NewEncoder(w)
j.SetIndent("", " ")
err = j.Encode(a)
span.RecordError(err)
case http.MethodGet:
resource := r.URL.Query().Get("resource")
rels := r.URL.Query()["rel"]
if resource == "" {
w.WriteHeader(http.StatusBadRequest)
return
}
if u := Parse(resource); u != nil && !s.self.Has(u.URL.Host) {
redirect := &url.URL{}
redirect.Scheme = "https"
redirect.Host = u.URL.Host
redirect.RawQuery = r.URL.RawQuery
redirect.Path = "/.well-known/webfinger"
w.Header().Set("location", redirect.String())
w.WriteHeader(http.StatusSeeOther)
return
}
a := &JRD{}
a.SetStreamID(StreamID(resource))
err := s.es.Load(ctx, a)
if err != nil {
span.RecordError(err)
if errors.Is(err, ev.ErrNotFound) {
w.WriteHeader(http.StatusNotFound)
fmt.Fprint(w, http.StatusText(http.StatusNotFound))
return
}
w.WriteHeader(http.StatusInternalServerError)
fmt.Fprint(w, http.StatusText(http.StatusInternalServerError))
return
}
if a.IsDeleted() {
w.WriteHeader(http.StatusGone)
fmt.Fprint(w, http.StatusText(http.StatusGone))
span.AddEvent("is deleted")
return
}
if len(rels) > 0 {
a.Links = a.GetLinksByRel(rels...)
}
if a.Properties != nil {
if redirect, ok := a.Properties[NSredirect]; ok && redirect != nil {
w.Header().Set("location", *redirect)
w.WriteHeader(http.StatusSeeOther)
return
}
}
w.Header().Set("Content-Type", "application/jrd+json")
w.WriteHeader(http.StatusOK)
j := json.NewEncoder(w)
j.SetIndent("", " ")
err = j.Encode(a)
span.RecordError(err)
default:
w.Header().Set("Allow", "GET, PUT, DELETE, OPTIONS")
w.WriteHeader(http.StatusMethodNotAllowed)
fmt.Fprint(w, http.StatusText(http.StatusMethodNotAllowed))
span.AddEvent("method not allow: " + r.Method)
}
}
func (s *service) ui() http.HandlerFunc {
loadTemplates()
return func(w http.ResponseWriter, r *http.Request) {
args := struct {
Req *http.Request
Status int
Body []byte
JRD *JRD
Err error
}{Status: http.StatusOK}
if r.URL.Query().Has("resource") {
args.Req, args.Err = http.NewRequestWithContext(r.Context(), http.MethodGet, r.URL.String(), nil)
if args.Err != nil {
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
return
}
wr := httptest.NewRecorder()
s.ServeHTTP(wr, args.Req)
args.Status = wr.Code
switch wr.Code {
case http.StatusSeeOther:
res, err := http.DefaultClient.Get(wr.Header().Get("location"))
args.Err = err
if err == nil {
args.Status = res.StatusCode
args.Body, args.Err = io.ReadAll(res.Body)
}
case http.StatusOK:
args.Body, args.Err = io.ReadAll(wr.Body)
if args.Err == nil {
args.JRD, args.Err = ParseJRD(args.Body)
}
}
if args.Err == nil && args.Body != nil {
args.JRD, args.Err = ParseJRD(args.Body)
}
}
t := templates["home.go.tpl"]
err := t.Execute(w, args)
if err != nil {
log.Println(err)
}
}
}
func dec(s string) ([]byte, error) {
s = strings.TrimSpace(s)
return base64.RawURLEncoding.DecodeString(s)
}
var funcMap = map[string]any{
"propName": func(in string) string { return in[strings.LastIndex(in, "/")+1:] },
"escape": html.EscapeString,
}
func loadTemplates() error {
if templates != nil {
return nil
}
templates = make(map[string]*template.Template)
tmplFiles, err := fs.ReadDir(files, "ui/pages")
if err != nil {
return err
}
for _, tmpl := range tmplFiles {
if tmpl.IsDir() {
continue
}
pt := template.New(tmpl.Name())
pt.Funcs(funcMap)
pt, err = pt.ParseFS(files, "ui/pages/"+tmpl.Name(), "ui/layouts/*.go.tpl")
if err != nil {
log.Println(err)
return err
}
templates[tmpl.Name()] = pt
}
return nil
}

3
cmd/README.md Normal file
View File

@ -0,0 +1,3 @@
# Cmd
These are examples that can be built using EV. Because they are modular the apps can be mixed an matched by including the different source files linked from `cmd/ev`.

34
cmd/ev/app.msgbus.go Normal file
View File

@ -0,0 +1,34 @@
package main
import (
"context"
"fmt"
"go.sour.is/ev"
"go.sour.is/tools/app/msgbus"
"go.sour.is/ev/driver/projecter"
"go.sour.is/pkg/lg"
"go.sour.is/pkg/service"
"go.sour.is/pkg/slice"
)
var _ = apps.Register(50, func(ctx context.Context, svc *service.Harness) error {
ctx, span := lg.Span(ctx)
defer span.End()
span.AddEvent("Enable Msgbus")
eventstore, ok := slice.Find[*ev.EventStore](svc.Services...)
if !ok {
return fmt.Errorf("*es.EventStore not found in services")
}
eventstore.Option(projecter.New(ctx, msgbus.Projector))
msgbus, err := msgbus.New(ctx, eventstore)
if err != nil {
span.RecordError(err)
return err
}
svc.Add(msgbus)
return nil
})

44
cmd/ev/app.peerfinder.go Normal file
View File

@ -0,0 +1,44 @@
package main
import (
"context"
"fmt"
"go.sour.is/pkg/env"
"go.sour.is/pkg/lg"
"go.sour.is/pkg/service"
"go.sour.is/pkg/slice"
"go.sour.is/ev"
"go.sour.is/tools/app/peerfinder"
"go.sour.is/ev/driver/projecter"
)
var _ = apps.Register(50, func(ctx context.Context, svc *service.Harness) error {
ctx, span := lg.Span(ctx)
defer span.End()
span.AddEvent("Enable Peers")
eventstore, ok := slice.Find[*ev.EventStore](svc.Services...)
if !ok {
return fmt.Errorf("*es.EventStore not found in services")
}
eventstore.Option(projecter.New(ctx, peerfinder.Projector))
peers, err := peerfinder.New(ctx, eventstore, env.Secret("PEER_STATUS", "").Secret())
if err != nil {
span.RecordError(err)
return err
}
svc.RunOnce(ctx, peers.RefreshJob)
svc.NewCron("0,15,30,45", peers.RefreshJob)
svc.RunOnce(ctx, peers.CleanJob)
svc.NewCron("0 1", peers.CleanJob)
svc.OnStart(peers.Run)
svc.OnStop(peers.Stop)
svc.Add(peers)
return nil
})

64
cmd/ev/app.salty.go Normal file
View File

@ -0,0 +1,64 @@
package main
import (
"context"
"fmt"
"net/http"
"net/url"
"os"
"path/filepath"
"strings"
"go.sour.is/ev"
"go.sour.is/tools/app/salty"
"go.sour.is/pkg/env"
"go.sour.is/pkg/lg"
"go.sour.is/pkg/service"
"go.sour.is/pkg/slice"
)
var _ = apps.Register(50, func(ctx context.Context, svc *service.Harness) error {
ctx, span := lg.Span(ctx)
defer span.End()
span.AddEvent("Enable Salty")
eventstore, ok := slice.Find[*ev.EventStore](svc.Services...)
if !ok {
return fmt.Errorf("*es.EventStore not found in services")
}
addr := "localhost"
if ht, ok := slice.Find[*http.Server](svc.Services...); ok {
addr = ht.Addr
}
var opts []salty.Option
base, err := url.JoinPath(env.Default("SALTY_BASE_URL", "http://"+addr), "inbox")
if err != nil {
span.RecordError(err)
return err
}
opts = append(opts, salty.WithBaseURL(base))
if p := env.Default("SALTY_BLOB_DIR", ""); p != "" {
if strings.HasPrefix(p, "~/") {
home, _ := os.UserHomeDir()
p = filepath.Join(home, strings.TrimPrefix(p, "~/"))
}
opts = append(opts, salty.WithBlobStore(p))
}
salty, err := salty.New(
ctx,
eventstore,
opts...,
)
if err != nil {
span.RecordError(err)
return err
}
svc.Add(salty)
return nil
})

17
cmd/ev/app.twtxt.go Normal file
View File

@ -0,0 +1,17 @@
package main
import (
"context"
"go.sour.is/pkg/lg"
"go.sour.is/pkg/service"
)
var _ = apps.Register(50, func(ctx context.Context, svc *service.Harness) error {
_, span := lg.Span(ctx)
defer span.End()
span.AddEvent("Enable Twtxt")
return nil
})

52
cmd/ev/app.webfinger.go Normal file
View File

@ -0,0 +1,52 @@
package main
import (
"context"
"fmt"
"strings"
"time"
"github.com/patrickmn/go-cache"
"go.sour.is/ev"
"go.sour.is/tools/app/webfinger"
"go.sour.is/pkg/env"
"go.sour.is/pkg/lg"
"go.sour.is/pkg/service"
"go.sour.is/pkg/slice"
)
var (
defaultExpire = 3 * time.Minute
cleanupInterval = 10 * time.Minute
)
var _ = apps.Register(50, func(ctx context.Context, svc *service.Harness) error {
ctx, span := lg.Span(ctx)
defer span.End()
span.AddEvent("Enable WebFinger")
eventstore, ok := slice.Find[*ev.EventStore](svc.Services...)
if !ok {
return fmt.Errorf("*es.EventStore not found in services")
}
cache := cache.New(defaultExpire, cleanupInterval)
var withCache webfinger.WithCache = (func(s string) bool {
if _, ok := cache.Get(s); ok {
return true
}
cache.SetDefault(s, true)
return false
})
var withHostnames webfinger.WithHostnames = strings.Fields(env.Default("WEBFINGER_DOMAINS", "sour.is"))
wf, err := webfinger.New(ctx, eventstore, withCache, withHostnames)
if err != nil {
span.RecordError(err)
return err
}
svc.Add(wf)
return nil
})

41
cmd/ev/main.go Normal file
View File

@ -0,0 +1,41 @@
package main
import (
"context"
"errors"
"log"
"net/http"
"os"
"os/signal"
"go.sour.is/pkg/lg"
"go.sour.is/pkg/service"
)
var apps service.Apps
var appName, version = service.AppName()
func main() {
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, os.Kill)
go func() {
<-ctx.Done()
defer cancel() // restore interrupt function
}()
if err := run(ctx); err != nil {
log.Fatal(err)
os.Exit(1)
}
}
func run(ctx context.Context) error {
svc := &service.Harness{}
ctx, stop := lg.Init(ctx, appName)
svc.OnStop(stop)
svc.Add(lg.NewHTTP(ctx))
svc.Setup(ctx, apps.Apps()...)
// Run application
if err := svc.Run(ctx, appName, version); err != nil && !errors.Is(err, http.ErrServerClosed) {
return err
}
return nil
}

54
cmd/ev/svc.es.go Normal file
View File

@ -0,0 +1,54 @@
package main
import (
"context"
"go.sour.is/pkg/env"
"go.sour.is/pkg/lg"
"go.sour.is/pkg/service"
"go.uber.org/multierr"
"go.sour.is/ev"
diskstore "go.sour.is/ev/driver/disk-store"
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"
gql_ev "go.sour.is/ev/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
})

40
cmd/ev/svc.gql.go Normal file
View File

@ -0,0 +1,40 @@
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/tools/app/gql"
)
var _ = apps.Register(90, func(ctx context.Context, svc *service.Harness) error {
ctx, span := lg.Span(ctx)
defer span.End()
span.AddEvent("Enable GraphQL")
gql, err := resolver.New(ctx, &gql.Resolver{}, slice.FilterType[resolver.IsResolver](svc.Services...)...)
if err != nil {
span.RecordError(err)
return err
}
gql.CheckOrigin = func(r *http.Request) bool {
switch r.Header.Get("Origin") {
case "https://ev.sour.is", "https://www.graphqlbin.com", "http://localhost:8080":
return true
default:
return false
}
}
svc.Add(gql)
svc.Add(mux.RegisterHTTP(func(mux *http.ServeMux) {
mux.Handle("/", http.RedirectHandler("/playground", http.StatusTemporaryRedirect))
}))
return nil
})

47
cmd/ev/svc.http.go Normal file
View File

@ -0,0 +1,47 @@
package main
import (
"context"
"log"
"net/http"
"strings"
"github.com/rs/cors"
"go.sour.is/pkg/env"
"go.sour.is/pkg/lg"
"go.sour.is/pkg/mux"
"go.sour.is/pkg/service"
"go.sour.is/pkg/slice"
)
var _ = apps.Register(20, func(ctx context.Context, svc *service.Harness) error {
s := &http.Server{}
svc.Add(s)
mux := mux.New()
s.Handler = cors.AllowAll().Handler(mux)
// s.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// log.Println(r.URL.Path)
// mux.ServeHTTP(w, r)
// })
s.Addr = env.Default("EV_HTTP", ":8080")
if strings.HasPrefix(s.Addr, ":") {
s.Addr = "[::]" + s.Addr
}
svc.OnStart(func(ctx context.Context) error {
_, span := lg.Span(ctx)
defer span.End()
log.Print("Listen on ", s.Addr)
span.AddEvent("begin listen and serve on " + s.Addr)
mux.Add(slice.FilterType[interface{ RegisterHTTP(*http.ServeMux) }](svc.Services...)...)
return s.ListenAndServe()
})
svc.OnStop(s.Shutdown)
return nil
})

1
cmd/msgbus/app.msgbus.go Symbolic link
View File

@ -0,0 +1 @@
../ev/app.msgbus.go

1
cmd/msgbus/main.go Symbolic link
View File

@ -0,0 +1 @@
../ev/main.go

1
cmd/msgbus/svc.es.go Symbolic link
View File

@ -0,0 +1 @@
../ev/svc.es.go

1
cmd/msgbus/svc.gql.go Symbolic link
View File

@ -0,0 +1 @@
../ev/svc.gql.go

1
cmd/msgbus/svc.http.go Symbolic link
View File

@ -0,0 +1 @@
../ev/svc.http.go

1
cmd/peers/app.peerfinder.go Symbolic link
View File

@ -0,0 +1 @@
../ev/app.peerfinder.go

1
cmd/peers/main.go Symbolic link
View File

@ -0,0 +1 @@
../ev/main.go

1
cmd/peers/svc.es.go Symbolic link
View File

@ -0,0 +1 @@
../ev/svc.es.go

1
cmd/peers/svc.http.go Symbolic link
View File

@ -0,0 +1 @@
../ev/svc.http.go

1
cmd/salty/app.msgbus.go Symbolic link
View File

@ -0,0 +1 @@
../ev/app.msgbus.go

1
cmd/salty/app.salty.go Symbolic link
View File

@ -0,0 +1 @@
../ev/app.salty.go

1
cmd/salty/main.go Symbolic link
View File

@ -0,0 +1 @@
../ev/main.go

1
cmd/salty/svc.es.go Symbolic link
View File

@ -0,0 +1 @@
../ev/svc.es.go

1
cmd/salty/svc.http.go Symbolic link
View File

@ -0,0 +1 @@
../ev/svc.http.go

290
cmd/webfinger-cli/main.go Normal file
View File

@ -0,0 +1,290 @@
package main
import (
"bufio"
"context"
"crypto/ed25519"
"encoding/base64"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"os"
"os/signal"
"path/filepath"
"strings"
"github.com/docopt/docopt-go"
"go.sour.is/pkg/xdg"
"gopkg.in/yaml.v3"
"go.sour.is/tools/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
}

View File

@ -0,0 +1 @@
../ev/app.webfinger.go

1
cmd/webfinger/main.go Symbolic link
View File

@ -0,0 +1 @@
../ev/main.go

1
cmd/webfinger/svc.es.go Symbolic link
View File

@ -0,0 +1 @@
../ev/svc.es.go

1
cmd/webfinger/svc.http.go Symbolic link
View File

@ -0,0 +1 @@
../ev/svc.http.go

View File

@ -0,0 +1,123 @@
//go:build ignore
package main
import (
"context"
"crypto/ed25519"
"encoding/json"
"fmt"
"net/http"
"os"
"strings"
"testing"
"github.com/matryer/is"
"go.sour.is/ev/app/webfinger"
"go.sour.is/ev/pkg/service"
"golang.org/x/sync/errgroup"
)
func TestMain(m *testing.M) {
data, err := os.MkdirTemp("", "data*")
if err != nil {
fmt.Printf("error creating data dir: %s\n", err)
os.Exit(1)
}
defer os.RemoveAll(data)
os.Setenv("EV_DATA", "mem:")
os.Setenv("EV_HTTP", "[::1]:61234")
os.Setenv("WEBFINGER_DOMAINS", "sour.is")
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
running := make(chan struct{})
apps.Register(99, func(ctx context.Context, s *service.Harness) error {
go func() {
<-s.OnRunning()
close(running)
}()
return nil
})
wg, ctx := errgroup.WithContext(ctx)
wg.Go(func() error {
// Run application
if err := run(ctx); err != nil {
return err
}
return nil
})
wg.Go(func() error {
<-running
m.Run()
cancel()
return nil
})
if err := wg.Wait(); err != nil {
fmt.Println(err)
os.Exit(1)
}
}
func TestE2EGetHTTP(t *testing.T) {
is := is.New(t)
res, err := http.DefaultClient.Get("http://[::1]:61234/.well-known/webfinger")
is.NoErr(err)
is.Equal(res.StatusCode, http.StatusBadRequest)
}
func TestE2ECreateResource(t *testing.T) {
is := is.New(t)
_, priv, err := ed25519.GenerateKey(nil)
is.NoErr(err)
jrd := &webfinger.JRD{
Subject: "acct:me@sour.is",
Properties: map[string]*string{
"foo": ptr("bar"),
},
}
// create
token, err := webfinger.NewSignedRequest(jrd, priv)
is.NoErr(err)
req, err := http.NewRequest(http.MethodPut, "http://[::1]:61234/.well-known/webfinger", strings.NewReader(token))
is.NoErr(err)
res, err := http.DefaultClient.Do(req)
is.NoErr(err)
is.Equal(res.StatusCode, http.StatusCreated)
// repeat
req, err = http.NewRequest(http.MethodPut, "http://[::1]:61234/.well-known/webfinger", strings.NewReader(token))
is.NoErr(err)
res, err = http.DefaultClient.Do(req)
is.NoErr(err)
is.Equal(res.StatusCode, http.StatusAlreadyReported)
// fetch
req, err = http.NewRequest(http.MethodGet, "http://[::1]:61234/.well-known/webfinger?resource=acct:me@sour.is", nil)
is.NoErr(err)
res, err = http.DefaultClient.Do(req)
is.NoErr(err)
is.Equal(res.StatusCode, http.StatusOK)
resJRD := &webfinger.JRD{}
err = json.NewDecoder(res.Body).Decode(resJRD)
is.NoErr(err)
is.Equal(jrd.Subject, resJRD.Subject)
}
func ptr[T any](t T) *T { return &t }

121
go.mod
View File

@ -1,48 +1,113 @@
module go.sour.is/tools
go 1.20
go 1.21.0
require go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.42.0
toolchain go1.21.1
require (
github.com/99designs/gqlgen v0.17.38
github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815
github.com/golang-jwt/jwt/v4 v4.5.0
github.com/gorilla/websocket v1.5.0
github.com/keys-pub/keys v0.1.22
github.com/matryer/is v1.4.1
github.com/oklog/ulid v1.3.1
github.com/oklog/ulid/v2 v2.1.0
github.com/patrickmn/go-cache v2.1.0+incompatible
github.com/tj/go-semver v1.0.0
github.com/vektah/gqlparser/v2 v2.5.10
gitlab.com/jamietanna/content-negotiation-go v0.2.0
go.mills.io/saltyim v0.0.0-20230128070719-15a64de82829
go.opentelemetry.io/otel v1.19.0
go.opentelemetry.io/otel/metric v1.19.0
go.sour.is/ev v0.1.1
go.sour.is/pkg v0.0.6-0.20230927171106-f79376a1f12e
gopkg.in/yaml.v3 v3.0.1
)
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/golang/protobuf v1.5.3 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0 // indirect
github.com/google/go-cmp v0.5.9 // indirect
github.com/google/uuid v1.3.1 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.18.0 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.3 // indirect
github.com/jpillora/backoff v1.0.0 // indirect
github.com/julienschmidt/httprouter v1.3.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/prometheus/client_golang v1.16.0 // indirect
github.com/prometheus/client_model v0.4.0 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
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.17.0 // indirect
github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16 // indirect
github.com/prometheus/common v0.44.0 // indirect
github.com/prometheus/procfs v0.11.0 // 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/otel/sdk/metric v0.39.0 // indirect
go.opentelemetry.io/proto/otlp v0.19.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/genproto v0.0.0-20230306155012-7f2fa6fef1f4 // indirect
google.golang.org/grpc v1.55.0 // indirect
github.com/prometheus/procfs v0.12.0 // indirect
github.com/ravilushqa/otelgqlgen v0.13.1 // indirect
github.com/sasha-s/go-deadlock v0.3.1 // indirect
github.com/sirupsen/logrus v1.9.0 // indirect
github.com/taigrr/go-colorhash v0.0.0-20220329080504-742db7f45eae // indirect
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
github.com/tidwall/wal v1.1.7 // 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/contrib/instrumentation/net/http/otelhttp v0.45.0 // indirect
go.opentelemetry.io/contrib/instrumentation/runtime v0.45.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0 // indirect
go.opentelemetry.io/otel/exporters/prometheus v0.42.0 // indirect
go.opentelemetry.io/otel/sdk v1.19.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.19.0 // indirect
go.opentelemetry.io/otel/trace v1.19.0 // indirect
go.opentelemetry.io/proto/otlp v1.0.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.13.0 // indirect
golang.org/x/net v0.15.0 // indirect
golang.org/x/sys v0.12.0 // indirect
golang.org/x/text v0.13.0 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20230920204549-e6e6cdab5c13 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20230920204549-e6e6cdab5c13 // indirect
google.golang.org/grpc v1.58.2 // indirect
google.golang.org/protobuf v1.31.0 // indirect
nhooyr.io/websocket v1.8.7 // indirect
)
require (
github.com/felixge/httpsnoop v1.0.3 // indirect
github.com/go-logr/logr v1.2.4 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/matryer/is v1.4.1
github.com/rs/cors v1.9.0
go.opentelemetry.io/contrib/instrumentation/runtime v0.42.0
go.opentelemetry.io/otel v1.16.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.16.0
go.opentelemetry.io/otel/exporters/prometheus v0.39.0
go.opentelemetry.io/otel/metric v1.16.0 // indirect
go.opentelemetry.io/otel/sdk v1.16.0
go.opentelemetry.io/otel/trace v1.16.0 // indirect
go.sour.is/pkg v0.0.1
github.com/rs/cors v1.10.0
go.uber.org/multierr v1.11.0
golang.org/x/sync v0.3.0
golang.org/x/sync v0.3.0 // indirect
)
replace go.sour.is/pkg => ../go-pkg

745
go.sum
View File

@ -1,494 +1,417 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=
cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
git.mills.io/prologic/bitcask v1.0.2 h1:Iy9x3mVVd1fB+SWY0LTmsSDPGbzMrd7zCZPKbsb/tDA=
git.mills.io/prologic/bitcask v1.0.2/go.mod h1:ppXpR3haeYrijyJDleAkSGH3p90w6sIHxEA/7UHMxH4=
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/observe v0.0.0-20210712230028-fc31c7aa2bd1/go.mod h1:/rNXqsTHGrilgNJYH/8wsIRDScyxXUhpbSdNbBatAKY=
git.mills.io/prologic/useragent v0.0.0-20210714100044-d249fe7921a0 h1:MojWEgZyiugUbgyjydrdSAkHlADnbt90dXyURRYFzQ4=
git.mills.io/prologic/useragent v0.0.0-20210714100044-d249fe7921a0/go.mod h1:PUYe00Ub+JFp0ilwiyPJer7QljkjkVYIX5ZSIEv0auI=
github.com/99designs/gqlgen v0.17.38 h1:3r7G7i8UAdY0iYreNiBAA55auVsrowO0+ZhMl5g4GYU=
github.com/99designs/gqlgen v0.17.38/go.mod h1:2v+dKtpI8mIzYeW9dYN8mO69tMmjszW2xKLNcWR/5wQ=
github.com/NYTimes/gziphandler v1.1.1 h1:ZUDjpQae29j0ryrS0u/B8HZfJBtBQHjqw2rQ2cqUQ3I=
github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c=
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/abcum/lcp v0.0.0-20201209214815-7a3f3840be81/go.mod h1:6ZvnjTZX1LNo1oLpfaJK8h+MXqHxcBFBIwkgsv+xlv0=
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/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/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ=
github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM=
github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI=
github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cncf/xds/go v0.0.0-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/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ=
github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
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/gift v1.2.1/go.mod h1:Jh2i7f7Q2BM7Ezno3PhfezbR1xpUg9dUg3/RlKGr4HI=
github.com/disintegration/imageorient v0.0.0-20180920195336-8147d86e83ec h1:YrB6aVr9touOt75I9O1SiancmR2GMg45U9UYf0gtgWg=
github.com/disintegration/imageorient v0.0.0-20180920195336-8147d86e83ec/go.mod h1:K0KBFIr1gWu/C1Gp10nFAcAE4hsB7JxE6OgLijrJ8Sk=
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/felixge/httpsnoop v1.0.3 h1:s/nj+GCswXYzN5v2DpNMuMQYe+0DDwt5WVCU6CWBdXk=
github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
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/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU=
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-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
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/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/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
github.com/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/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU=
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 v1.1.0 h1:/d3pCKDPWNnvIWe0vVUpNP32qc8U3PDVxySP/y360qE=
github.com/golang/glog v1.1.0/go.mod h1:pfYeQZ3JWZoXTV5sFc986z3HTpwQs9At6P4ImfuP3NQ=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.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/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0 h1:BZHcxBETFHIdVyhyEfOvn/RdU/QGdLI4y34qQGjGWO0=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0/go.mod h1:hgWBS7lorOAVIJEQMi4ZsPv9hVvWI6+ch50m39Pf2Ks=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/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/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4=
github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
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/v2 v2.18.0 h1:RtRsiaGvWxcwd8y3BiRZxsylPT8hLWZ5SPcfI+3IDNk=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.18.0/go.mod h1:TzP6duP4Py2pHLVPPQp42aoYI92+PCrVotyR5e8Vqlk=
github.com/h2non/filetype v1.1.3 h1:FKkx9QbD7HR/zjK1Ia5XiBsq9zdLi5Kf3zGyFTAFkGg=
github.com/h2non/filetype v1.1.3/go.mod h1:319b3zT68BvV+WRj7cwy856M2ehB3HqNOt6sy1HndBY=
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/v2 v2.0.3 h1:kmRrRLlInXvng0SmLxmQpQkpbYAvcXm7NPDrgxJa9mE=
github.com/hashicorp/golang-lru/v2 v2.0.3/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
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/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U=
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/justinas/nosurf v1.1.1 h1:92Aw44hjSK4MxJeMSyDa7jwuI9GR2J/JCQiaKvXXSlk=
github.com/justinas/nosurf v1.1.1/go.mod h1:ALpWdSbuNGy2lZWtyXdjkYv4edL23oSEgfBT1gPJ5BQ=
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/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/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
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/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
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/maxence-charriere/go-app/v9 v9.4.1/go.mod h1:zo0n1kh4OMKn7P+MrTUUi7QwUMU2HOfHsZ293TITtxI=
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/mlctrez/goapp-mdc v0.2.6/go.mod h1:hrbfhTSPD7jaaubJsUweirnVkbwtwQJcjJm5OrH+rVo=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/nullrocks/identicon v0.0.0-20180626043057-7875f45b0022 h1:Ys0rDzh8s4UMlGaDa1UTA0sfKgvF0hQZzTYX8ktjiDc=
github.com/nullrocks/identicon v0.0.0-20180626043057-7875f45b0022/go.mod h1:x4NsS+uc7ecH/Cbm9xKQ6XzmJM57rWTkjywjfB2yQ18=
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/plar/go-adaptive-radix-tree v1.0.4/go.mod h1:Ot8d28EII3i7Lv4PSvBlF8ejiD/CtRYDuPsySJbSaK8=
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/prometheus/client_golang v1.15.1 h1:8tXpTmJbyH5lydzFPoxSIJ0J46jdh3tylbvM1xCv0LI=
github.com/prometheus/client_golang v1.15.1/go.mod h1:e9yaBhRPU2pPNsZwE+JdQl0KEt1N9XgF6zxWmaC0xOk=
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=
github.com/prometheus/client_model v0.4.0 h1:5lQXD3cAg1OXBf4Wq03gTrXHeaV0TQvGfUooCfx1yqY=
github.com/prometheus/client_model v0.4.0/go.mod h1:oMQmHW1/JoDwqLtg57MGgP/Fb1CJEYF2imWWhWtMkYU=
github.com/prometheus/common v0.42.0 h1:EKsfXEYo4JpWMHH5cg+KOUWeuJSov1Id8zGR8eeI1YM=
github.com/prometheus/common v0.42.0/go.mod h1:xBwqVerjNdUDjgODMpudtOMwlOwf2SaTr1yjz4b7Zbc=
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.17.0 h1:rl2sfwZMtSthVU752MqfjQozy7blglC+1SOtjMAMh+Q=
github.com/prometheus/client_golang v1.17.0/go.mod h1:VeL+gMmOAxkS2IqfCq0ZmHSL+LjWfWDUmp1mBz9JgUY=
github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16 h1:v7DLqVdK4VrYkVD5diGdl4sxJurKJEMnODWRJlxV9oM=
github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16/go.mod h1:oMQmHW1/JoDwqLtg57MGgP/Fb1CJEYF2imWWhWtMkYU=
github.com/prometheus/common v0.44.0 h1:+5BrQJwiBB9xsMygAB3TNvpQKOwlkc25LbISbrdOOfY=
github.com/prometheus/common v0.44.0/go.mod h1:ofAIvZbQ1e/nugmZGz4/qCb9Ap1VoSTIO7x0VV9VvuY=
github.com/prometheus/procfs v0.9.0 h1:wzCHvIvM5SxWqYvwgVL7yJY8Lz3PKn49KQtpgMYJfhI=
github.com/prometheus/procfs v0.9.0/go.mod h1:+pB4zwohETzFnmlpe6yd2lSc+0/46IYZRB/chUwxUZY=
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/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/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=
github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
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/robfig/cron v1.2.0/go.mod h1:JGuDeoQd7Z6yL4zQhZ3OPEVHB7fL6Ka6skscFHfmt2k=
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/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
github.com/rs/cors v1.9.0 h1:l9HGsTsHJcvW14Nk7J9KFz8bzeAWXn3CG6bgt7LsrAE=
github.com/rs/cors v1.9.0/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU=
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/rs/cors v1.10.0 h1:62NOS1h+r8p1mW6FM0FSB0exioXLhd/sh15KpjWBZ+8=
github.com/rs/cors v1.10.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/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/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=
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/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=
go.opentelemetry.io/contrib/instrumentation/runtime v0.42.0/go.mod h1:rD9feqRYP24P14t5kmhNMqsqm1jvKmpx2H2rKVw52V8=
go.opentelemetry.io/otel v1.16.0 h1:Z7GVAX/UkAXPKsy94IU+i6thsQS4nb7LviLpnaNeW8s=
go.opentelemetry.io/otel v1.16.0/go.mod h1:vl0h9NUa1D5s1nv3A5vZOYWn8av4K8Ml6JDeHrT/bx4=
go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.16.0 h1:t4ZwRPU+emrcvM2e9DHd0Fsf0JTPVcbfa/BhTDF03d0=
go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.16.0/go.mod h1:vLarbg68dH2Wa77g71zmKQqlQ8+8Rq3GRG31uc0WcWI=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.16.0 h1:cbsD4cUcviQGXdw8+bo5x2wazq10SKz8hEbtCRPcU78=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.16.0/go.mod h1:JgXSGah17croqhJfhByOLVY719k1emAXC8MVhCIJlRs=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.16.0 h1:iqjq9LAB8aK++sKVcELezzn655JnBNdsDhghU4G/So8=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.16.0/go.mod h1:hGXzO5bhhSHZnKvrDaXB82Y9DRFour0Nz/KrBh7reWw=
go.opentelemetry.io/otel/exporters/prometheus v0.39.0 h1:whAaiHxOatgtKd+w0dOi//1KUxj3KoPINZdtDaDj3IA=
go.opentelemetry.io/otel/exporters/prometheus v0.39.0/go.mod h1:4jo5Q4CROlCpSPsXLhymi+LYrDXd2ObU5wbKayfZs7Y=
go.opentelemetry.io/otel/metric v1.16.0 h1:RbrpwVG1Hfv85LgnZ7+txXioPDoh6EdbZHo26Q3hqOo=
go.opentelemetry.io/otel/metric v1.16.0/go.mod h1:QE47cpOmkwipPiefDwo2wDzwJrlfxxNYodqc4xnGCo4=
go.opentelemetry.io/otel/sdk v1.16.0 h1:Z1Ok1YsijYL0CSJpHt4cS3wDDh7p572grzNrBMiMWgE=
go.opentelemetry.io/otel/sdk v1.16.0/go.mod h1:tMsIuKXuuIWPBAOrH+eHtvhTL+SntFtXF9QD68aP6p4=
go.opentelemetry.io/otel/sdk/metric v0.39.0 h1:Kun8i1eYf48kHH83RucG93ffz0zGV1sh46FAScOTuDI=
go.opentelemetry.io/otel/sdk/metric v0.39.0/go.mod h1:piDIRgjcK7u0HCL5pCA4e74qpK/jk3NiUoAHATVAmiI=
go.opentelemetry.io/otel/trace v1.16.0 h1:8JRpaObFoW0pxuVPapkgH8UhHQj+bJW8jJsCZEu5MQs=
go.opentelemetry.io/otel/trace v1.16.0/go.mod h1:Yt9vYq1SdNz3xdjZZK7wcXv1qv2pwLkqr2QVwea0ef0=
go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
go.opentelemetry.io/proto/otlp v0.19.0 h1:IVN6GR+mhC4s5yfcTbmzHYODqvWAp3ZedA2SJPI1Nnw=
go.opentelemetry.io/proto/otlp v0.19.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
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/tidwall/gjson v1.10.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/gjson v1.14.4 h1:uo0p8EbA09J7RQaflQ1aBRffTR7xedD2bcIVSYxLnkM=
github.com/tidwall/gjson v1.14.4/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tidwall/tinylru v1.1.0 h1:XY6IUfzVTU9rpwdhKUF6nQdChgCdGjkMfLzbWyiau6I=
github.com/tidwall/tinylru v1.1.0/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/logger v0.0.0-20201216141554-31a3694fe979/go.mod h1:X5DBNY1yIVkuLwJP3BXlCoQCa5mGg7hPJPIA0Blwc44=
github.com/unrolled/render v1.4.1 h1:VdpMc2YkAOWzbmC/P2yoHhRDXgsaCQHcTJ1KK6SNCA4=
github.com/unrolled/render v1.4.1/go.mod h1:cK4RSTTVdND5j9EYEc0LAMOvdG11JeiKjyjfyZRvV2w=
github.com/vektah/gqlparser/v2 v2.5.10 h1:6zSM4azXC9u4Nxy5YmdmGu4uKamfwsdKTwp5zsEealU=
github.com/vektah/gqlparser/v2 v2.5.10/go.mod h1:1rCcfwB2ekJofmluGWXMSEnPMZgbxzwj6FaZ/4OT8Cc=
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.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.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.45.0 h1:x8Z78aZx8cOF0+Kkazoc7lwUNMGy0LrzEMxTm4BbTxg=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.45.0/go.mod h1:62CPTSry9QZtOaSsE3tOzhx6LzDhHnXJ6xHeMNNiM6Q=
go.opentelemetry.io/contrib/instrumentation/runtime v0.45.0 h1:2JydY5UiDpqvj2p7sO9bgHuhTy4hgTZ0ymehdq/Ob0Q=
go.opentelemetry.io/contrib/instrumentation/runtime v0.45.0/go.mod h1:ch3a5QxOqVWxas4CzjCFFOOQe+7HgAXC/N1oVxS9DK4=
go.opentelemetry.io/otel v1.19.0 h1:MuS/TNf4/j4IXsZuJegVzI1cwut7Qc00344rgH7p8bs=
go.opentelemetry.io/otel v1.19.0/go.mod h1:i0QyjOq3UPoTzff0PJB2N66fb4S0+rSbSB15/oyH9fY=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0 h1:Mne5On7VWdx7omSrSSZvM4Kw7cS7NQkOOmLcgscI51U=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0/go.mod h1:IPtUMKL4O3tH5y+iXVyAXqpAwMuzC1IrxVS81rummfE=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0 h1:IeMeyr1aBvBiPVYihXIaeIZba6b8E1bYp7lbdxK8CQg=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0/go.mod h1:oVdCUtjq9MK9BlS7TtucsQwUcXcymNiEDjgDD2jMtZU=
go.opentelemetry.io/otel/exporters/prometheus v0.42.0 h1:jwV9iQdvp38fxXi8ZC+lNpxjK16MRcZlpDYvbuO1FiA=
go.opentelemetry.io/otel/exporters/prometheus v0.42.0/go.mod h1:f3bYiqNqhoPxkvI2LrXqQVC546K7BuRDL/kKuxkujhA=
go.opentelemetry.io/otel/metric v1.19.0 h1:aTzpGtV0ar9wlV4Sna9sdJyII5jTVJEvKETPiOKwvpE=
go.opentelemetry.io/otel/metric v1.19.0/go.mod h1:L5rUsV9kM1IxCj1MmSdS+JQAcVm319EUrDVLrt7jqt8=
go.opentelemetry.io/otel/sdk v1.19.0 h1:6USY6zH+L8uMH8L3t1enZPR3WFEmSTADlqldyHtJi3o=
go.opentelemetry.io/otel/sdk v1.19.0/go.mod h1:NedEbbS4w3C6zElbLdPJKOpJQOrGUJ+GfzpjUvI0v1A=
go.opentelemetry.io/otel/sdk/metric v1.19.0 h1:EJoTO5qysMsYCa+w4UghwFV/ptQgqSL/8Ni+hx+8i1k=
go.opentelemetry.io/otel/sdk/metric v1.19.0/go.mod h1:XjG0jQyFJrv2PbMvwND7LwCEhsJzCzV5210euduKcKY=
go.opentelemetry.io/otel/trace v1.19.0 h1:DFVQmlVbfVeOuBRrwdtaehRrWiL1JoVs9CPIQ1Dzxpg=
go.opentelemetry.io/otel/trace v1.19.0/go.mod h1:mfaSyvGyEJEI0nyV2I4qhNQnbBOUUmYZpYojqMnX2vo=
go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I=
go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM=
go.sour.is/ev v0.0.4 h1:os4+20wPxNj0tn6MZ7kABwe63txVSsy2AWbM2nqWLqY=
go.sour.is/ev v0.0.4/go.mod h1:XxDMFTOsj9IjQ9T09wnsKw5rcmFSdSl9oYqaCJ1Zoew=
go.sour.is/ev v0.1.0 h1:JR8Y3d5+y/cjb5o+ZQoG22rKKx3GGj/Tu7+n7usax1E=
go.sour.is/ev v0.1.0/go.mod h1:byGQFRbyjOviFkcv/gPavSLMESydCujlQLa5KSFdoQI=
go.sour.is/ev v0.1.1 h1:LGhJkyaltyQXl0w3T8daR8G3AL9Asm9JmDU2fXnak0s=
go.sour.is/ev v0.1.1/go.mod h1:FMGI/8QSqBRNaSuK9EQTbpOM1KS1EzN5AyhHEDzmPr4=
go.sour.is/pkg v0.0.5 h1:ngRYyFl+Ks1S63hnAxcIpaKdoqXGQIKvQnz4QIrYm94=
go.sour.is/pkg v0.0.5/go.mod h1:kb8ERLsUC3DqHNwm7gra2tE6x+2C9g+zxzVEC36R884=
go.sour.is/pkg v0.0.6-0.20230927171106-f79376a1f12e h1:GzHNs5ekwqLD26ZvpxLbXFaElkVPr1WEMpkzLKRCxVU=
go.sour.is/pkg v0.0.6-0.20230927171106-f79376a1f12e/go.mod h1:kb8ERLsUC3DqHNwm7gra2tE6x+2C9g+zxzVEC36R884=
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/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/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=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/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/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
golang.org/x/crypto v0.13.0 h1:mvySKfSWJ+UKUii46M40LOvyWfN0s2U+46/jDd0e6Ck=
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
golang.org/x/exp v0.0.0-20220328175248-053ad81199eb h1:pC9Okm6BVmxEw76PUu0XUbOTQ92JX11hfvqTjAV3qxM=
golang.org/x/exp v0.0.0-20220328175248-053ad81199eb/go.mod h1:lgLbSvA5ygNOMpwM/9anMpWVlVJ7Z+cHWq/eFuinpGE=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191116160921-f9c825593386/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ=
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
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=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
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-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.15.0 h1:ugBLEUaxABaB5AJqW9enI0ACdci2RUd4eP51NTBvuJ8=
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-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=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200219091948-cb0a6d8edb6c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/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.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU=
golang.org/x/sys v0.8.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/sys v0.0.0-20210324051608-47abb6519492/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
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.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
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.12.0 h1:/ZfYdc3zq+q02Rv9vGqTeSItdzZTSNDmfTi0mBAuidU=
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
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.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68=
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
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.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.1.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=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
google.golang.org/genproto v0.0.0-20230306155012-7f2fa6fef1f4 h1:DdoeryqhaXp1LtT/emMP1BRJPHHKFi5akj/nbx/zNTA=
google.golang.org/genproto v0.0.0-20230306155012-7f2fa6fef1f4/go.mod h1:NWraEVixdDnqcqQ30jipen1STv2r/n24Wb7twVTGR4s=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0=
google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=
google.golang.org/grpc v1.42.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU=
google.golang.org/grpc v1.55.0 h1:3Oj82/tFSCeUrRTg/5E/7d/W5A1tj6Ky1ABAuZuv5ag=
google.golang.org/grpc v1.55.0/go.mod h1:iYEXKGkEBhg1PjZQvoYEVPTDkHo1/bjTnfwTeGONTY8=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/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-20230913181813-007df8e322eb h1:XFBgcDwm7irdHTbz4Zk2h7Mh+eis4nfJEFQFYzJzuIA=
google.golang.org/genproto v0.0.0-20230913181813-007df8e322eb/go.mod h1:yZTlhN0tQnXo3h00fuXNCxJdLdIdnVFVBaRJ5LWBbw4=
google.golang.org/genproto/googleapis/api v0.0.0-20230920204549-e6e6cdab5c13 h1:U7+wNaVuSTaUqNvK2+osJ9ejEZxbjHHk8F2b6Hpx0AE=
google.golang.org/genproto/googleapis/api v0.0.0-20230920204549-e6e6cdab5c13/go.mod h1:RdyHbowztCGQySiCvQPgWQWgWhGnouTdCflKoDBt32U=
google.golang.org/genproto/googleapis/rpc v0.0.0-20230920204549-e6e6cdab5c13 h1:N3bU/SQDCDyD6R528GJ/PwW9KjYcJA3dgyH+MovAkIM=
google.golang.org/genproto/googleapis/rpc v0.0.0-20230920204549-e6e6cdab5c13/go.mod h1:KSqppvjFjtoCI+KGd4PELB0qLNxdJHRGqRI09mB6pQA=
google.golang.org/grpc v1.58.2 h1:SXUpjxeVF3FKrTYQI4f4KvbGD5u2xccdYdurwowix5I=
google.golang.org/grpc v1.58.2/go.mod h1:tgX3ZQDlNJGU96V6yHh1T/JeoBQ2TXdr43YbYSsCJk0=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
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-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=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
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=
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=
nhooyr.io/websocket v1.8.7 h1:usjR2uOr/zjjkVMy0lW+PPohFok7PCow5sDjLgX4P4g=
nhooyr.io/websocket v1.8.7/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0=

39
gqlgen.yml Normal file
View File

@ -0,0 +1,39 @@
schema:
- api/*/*.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

View File

@ -0,0 +1,35 @@
// 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

View File

@ -0,0 +1 @@
package generated

View File

@ -0,0 +1,3 @@
// Code generated by github.com/99designs/gqlgen, DO NOT EDIT.
package model

View File

@ -0,0 +1,63 @@
package resolver
// THIS CODE IS A STARTING POINT ONLY. IT WILL NOT BE UPDATED WITH SCHEMA CHANGES.
import (
"context"
gql_ev "go.sour.is/ev/gql"
"go.sour.is/pkg/gql"
"go.sour.is/tools/app/msgbus"
"go.sour.is/tools/app/salty"
"go.sour.is/tools/internal/graph/generated"
)
type Resolver struct{}
// TruncateStream is the resolver for the truncateStream field.
func (r *mutationResolver) TruncateStream(ctx context.Context, streamID string, index int64) (bool, error) {
panic("not implemented")
}
// CreateSaltyUser is the resolver for the createSaltyUser field.
func (r *mutationResolver) CreateSaltyUser(ctx context.Context, nick string, pubkey string) (*salty.SaltyUser, error) {
panic("not implemented")
}
// Events is the resolver for the events field.
func (r *queryResolver) Events(ctx context.Context, streamID string, paging *gql.PageInput) (*gql.Connection, error) {
panic("not implemented")
}
// Posts is the resolver for the posts field.
func (r *queryResolver) Posts(ctx context.Context, name string, tag string, paging *gql.PageInput) (*gql.Connection, error) {
panic("not implemented")
}
// SaltyUser is the resolver for the saltyUser field.
func (r *queryResolver) SaltyUser(ctx context.Context, nick string) (*salty.SaltyUser, error) {
panic("not implemented")
}
// EventAdded is the resolver for the eventAdded field.
func (r *subscriptionResolver) EventAdded(ctx context.Context, streamID string, after int64) (<-chan *gql_ev.Event, error) {
panic("not implemented")
}
// PostAdded is the resolver for the postAdded field.
func (r *subscriptionResolver) PostAdded(ctx context.Context, name string, 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 }