refactor!: working WD-4 user discovery

This commit is contained in:
DevMiner 2024-08-20 22:43:26 +02:00
parent cf0053312d
commit 61891d891a
91 changed files with 12768 additions and 5562 deletions

View file

@ -1,15 +1,7 @@
package lysand
import (
"bytes"
"context"
"crypto/ed25519"
"crypto/rand"
"encoding/base64"
"encoding/json"
"fmt"
"net/http"
"net/url"
)
// User represents a user object in the Lysand protocol. For more information, see the [Spec].
@ -20,7 +12,7 @@ type User struct {
// PublicKey is the public key of the user.
// https://lysand.org/objects/user#public-key
PublicKey PublicKey `json:"public_key"`
PublicKey UserPublicKey `json:"public_key"`
// DisplayName is the display name of the user.
// https://lysand.org/objects/user#display-name
@ -66,14 +58,6 @@ type User struct {
// https://lysand.org/objects/user#following
Following *URL `json:"following"`
// Likes is the likes of the user.
// https://lysand.org/objects/user#likes
Likes *URL `json:"likes"`
// Dislikes is the dislikes of the user.
// https://lysand.org/objects/user#dislikes
Dislikes *URL `json:"dislikes"`
// Inbox is the inbox of the user.
// https://lysand.org/objects/user#posts
Inbox *URL `json:"inbox"`
@ -94,60 +78,3 @@ type Field struct {
Key TextContentTypeMap `json:"key"`
Value TextContentTypeMap `json:"value"`
}
func (c *FederationClient) GetUser(ctx context.Context, uri *url.URL) (*User, error) {
resp, body, err := c.rawGET(ctx, uri)
if err != nil {
return nil, err
}
user := &User{}
if err := json.Unmarshal(body, user); err != nil {
return nil, err
}
fedHeaders, err := ExtractFederationHeaders(resp.Header)
if err != nil {
return nil, err
}
v := Verifier{ed25519.PublicKey(user.PublicKey.PublicKey)}
if !v.Verify("GET", uri, body, fedHeaders) {
c.log.V(2).Info("signature verification failed", "user", user.URI.String())
return nil, fmt.Errorf("signature verification failed")
}
c.log.V(2).Info("signature verification succeeded", "user", user.URI.String())
return user, nil
}
func (c *FederationClient) SendToInbox(ctx context.Context, signer Signer, user *User, object any) ([]byte, error) {
uri := user.Inbox.ToStd()
body, err := json.Marshal(object)
if err != nil {
return nil, err
}
nonce := make([]byte, 32)
if _, err := rand.Read(nonce); err != nil {
return nil, err
}
sigData := NewSignatureData("POST", base64.StdEncoding.EncodeToString(nonce), uri, hashSHA256(body))
sig := signer.Sign(*sigData)
req, err := http.NewRequestWithContext(ctx, "POST", uri.String(), bytes.NewReader(body))
if err != nil {
return nil, err
}
sig.Inject(req.Header)
_, respBody, err := c.doReq(req)
if err != nil {
return nil, err
}
return respBody, nil
}

View file

@ -1,42 +1,5 @@
package lysand
import (
"bytes"
"crypto/ed25519"
"crypto/sha256"
"io"
"net/http"
)
func (c *FederationClient) ValidateSignatureHeader(req *http.Request) (bool, error) {
fedHeaders, err := ExtractFederationHeaders(req.Header)
if err != nil {
return false, err
}
// TODO: Fetch user from database instead of using the URI
user, err := c.GetUser(req.Context(), fedHeaders.SignedBy)
if err != nil {
return false, err
}
body, err := copyBody(req)
if err != nil {
return false, err
}
v := Verifier{ed25519.PublicKey(user.PublicKey.PublicKey)}
valid := v.Verify(req.Method, req.URL, body, fedHeaders)
return valid, nil
}
func hashSHA256(data []byte) []byte {
h := sha256.New()
h.Write(data)
return h.Sum(nil)
}
func must[In any, Out any](fn func(In) (Out, error), v In) Out {
out, err := fn(v)
if err != nil {
@ -45,17 +8,3 @@ func must[In any, Out any](fn func(In) (Out, error), v In) Out {
return out
}
func copyBody(req *http.Request) ([]byte, error) {
body, err := io.ReadAll(req.Body)
if err != nil {
return nil, err
}
if err := req.Body.Close(); err != nil {
return nil, err
}
req.Body = io.NopCloser(bytes.NewBuffer(body))
return body, nil
}

23
pkg/lysand/crypto/keys.go Normal file
View file

@ -0,0 +1,23 @@
package versiacrypto
import (
"crypto/ed25519"
"fmt"
)
type UnknownPublicKeyTypeError struct {
Got string
}
func (i UnknownPublicKeyTypeError) Error() string {
return fmt.Sprintf("unknown public key type: \"%s\"", i.Got)
}
func ToTypedKey(algorithm string, raw []byte) (any, error) {
switch algorithm {
case "ed25519":
return ed25519.PublicKey(raw), nil
default:
return nil, UnknownPublicKeyTypeError{algorithm}
}
}

View file

@ -0,0 +1,9 @@
package versiacrypto
import "crypto/sha256"
func SHA256(data []byte) []byte {
h := sha256.New()
h.Write(data)
return h.Sum(nil)
}

View file

@ -0,0 +1,29 @@
package versiacrypto
import (
"crypto"
"crypto/ed25519"
"fmt"
"reflect"
)
type InvalidPublicKeyTypeError struct {
Got reflect.Type
}
func (i InvalidPublicKeyTypeError) Error() string {
return fmt.Sprintf("failed to convert public key of type \"%s\"", i.Got.String())
}
type Verify = func(data, signature []byte) bool
func NewVerify(pubKey crypto.PublicKey) (Verify, error) {
switch pk := pubKey.(type) {
case ed25519.PublicKey:
return func(data, signature []byte) bool {
return ed25519.Verify(pk, data, signature)
}, nil
default:
return nil, InvalidPublicKeyTypeError{reflect.TypeOf(pk)}
}
}

View file

@ -1,10 +1,9 @@
package lysand
import (
"context"
"fmt"
"github.com/go-logr/logr"
"io"
"github.com/lysand-org/versia-go/pkg/protoretry"
"net/http"
"net/url"
"time"
@ -22,6 +21,7 @@ func (e *ResponseError) Error() string {
type FederationClient struct {
log logr.Logger
httpC *http.Client
hc *protoretry.Client
}
type Opt func(c *FederationClient)
@ -53,49 +53,11 @@ func NewClient(opts ...Opt) *FederationClient {
useragent: "github.com/lysand-org/versia-go/pkg/lysand#0.0.1",
}
c.hc = protoretry.New(c.httpC)
return c
}
func (c *FederationClient) rawGET(ctx context.Context, uri *url.URL) (*http.Response, []byte, error) {
req, err := http.NewRequestWithContext(ctx, "GET", uri.String(), nil)
if err != nil {
return nil, nil, err
}
return c.doReq(req)
}
func (c *FederationClient) rawPOST(ctx context.Context, uri *url.URL, body io.Reader) (*http.Response, []byte, error) {
req, err := http.NewRequestWithContext(ctx, "POST", uri.String(), body)
if err != nil {
return nil, nil, err
}
return c.doReq(req)
}
func (c *FederationClient) doReq(req *http.Request) (*http.Response, []byte, error) {
resp, err := c.httpC.Do(req)
if err != nil {
return nil, nil, err
}
defer resp.Body.Close()
if resp.StatusCode >= http.StatusBadRequest {
return resp, nil, &ResponseError{
StatusCode: resp.StatusCode,
URL: req.URL,
}
}
respBody, err := io.ReadAll(resp.Body)
if err != nil {
return resp, nil, err
}
return resp, respBody, nil
}
type federationClientHTTPTransport struct {
inner http.RoundTripper
useragent string

View file

@ -25,6 +25,14 @@ func (f *FederationHeaders) Inject(h http.Header) {
h.Set("x-signature", base64.StdEncoding.EncodeToString(f.Signature))
}
func (f *FederationHeaders) Headers() map[string]string {
return map[string]string{
"x-signed-by": f.SignedBy.String(),
"x-nonce": f.Nonce,
"x-signature": base64.StdEncoding.EncodeToString(f.Signature),
}
}
func ExtractFederationHeaders(h http.Header) (*FederationHeaders, error) {
signedBy := h.Get("x-signed-by")
if signedBy == "" {
@ -41,14 +49,19 @@ func ExtractFederationHeaders(h http.Header) (*FederationHeaders, error) {
return nil, fmt.Errorf("missing x-nonce header")
}
signature := h.Get("x-signature")
if signature == "" {
rawSignature := h.Get("x-signature")
if rawSignature == "" {
return nil, fmt.Errorf("missing x-signature header")
}
signature, err := base64.StdEncoding.DecodeString(rawSignature)
if err != nil {
return nil, err
}
return &FederationHeaders{
SignedBy: u,
Nonce: nonce,
Signature: []byte(signature),
Signature: signature,
}, nil
}

View file

@ -0,0 +1,111 @@
package lysand
import (
"encoding/json"
)
// InstanceMetadata represents the metadata of a Lysand instance. For more information, see the [Spec].
//
// ! Unlike other entities, instance metadata is not meant to be federated.
//
// [Spec]: https://versia.pub/entities/instance-metadata
type InstanceMetadata struct {
// Type is always "InstanceMetadata"
Type string `json:"type"`
// Extensions is a map of active extensions
Extensions Extensions `json:"extensions,omitempty"`
// Name is the name of the instance
Name string `json:"name"`
// Description is a description of the instance
Description *string `json:"description,omitempty"`
// Host is the hostname of the instance, including the port
Host string `json:"host,omitempty"`
// PublicKey is the public key of the instance
PublicKey InstancePublicKey `json:"public_key"`
// SharedInbox is the URL to the instance's shared inbox
SharedInbox *URL `json:"shared_inbox,omitempty"`
// Moderators is a URL to a collection of moderators
Moderators *URL `json:"moderators,omitempty"`
// Admins is a URL to a collection of administrators
Admins *URL `json:"admins,omitempty"`
// Logo is the URL to the instance's logo
Logo *ImageContentTypeMap `json:"logo,omitempty"`
// Banner is the URL to the instance's banner
Banner *ImageContentTypeMap `json:"banner,omitempty"`
// Software is information about the instance software
Software InstanceSoftware `json:"software"`
// Compatibility is information about the instance's compatibility with different Lysand versions
Compatibility InstanceCompatibility `json:"compatibility"`
}
func (s InstanceMetadata) MarshalJSON() ([]byte, error) {
type instanceMetadata InstanceMetadata
s2 := instanceMetadata(s)
s2.Type = "InstanceMetadata"
return json.Marshal(s2)
}
// InstanceSoftware represents the software of a Lysand instance.
type InstanceSoftware struct {
// Name is the name of the instance software
Name string `json:"name"`
// Version is the version of the instance software
Version string `json:"version"`
}
// InstanceCompatibility represents the compatibility of a Lysand instance.
type InstanceCompatibility struct {
// Versions is a list of versions of Lysand the instance is compatible with
Versions []string `json:"versions"`
// Extensions is a list of extensions supported by the instance
Extensions []string `json:"extensions"`
}
// InstancePublicKey represents the public key of a Versia instance.
type InstancePublicKey struct {
// Algorithm can only be `ed25519` for now
Algorithm string `json:"algorithm"`
Key *SPKIPublicKey `json:"-"`
RawKey json.RawMessage `json:"key"`
}
func (k *InstancePublicKey) UnmarshalJSON(raw []byte) error {
type t InstancePublicKey
k2 := (*t)(k)
if err := json.Unmarshal(raw, k2); err != nil {
return nil
}
var err error
if k2.Key, err = unmarshalSPKIPubKey(k2.Algorithm, k2.RawKey); err != nil {
return nil
}
return nil
}
func (k InstancePublicKey) MarshalJSON() ([]byte, error) {
type t InstancePublicKey
k2 := t(k)
var err error
if k2.RawKey, err = k2.Key.MarshalJSON(); err != nil {
return nil, err
}
return json.Marshal(k2)
}

View file

@ -1,64 +0,0 @@
package lysand
import (
"encoding/json"
"github.com/Masterminds/semver"
)
// ServerMetadata represents the metadata of a Lysand server. For more information, see the [Spec].
//
// ! Unlike other objects, server metadata is not meant to be federated.
//
// [Spec]: https://lysand.org/objects/server-metadata
type ServerMetadata struct {
// Type is always "ServerMetadata"
// https://lysand.org/objects/server-metadata#type
Type string `json:"type"`
// Extensions is a map of active extensions
// https://lysand.org/objects/server-metadata#extensions
Extensions Extensions `json:"extensions,omitempty"`
// Name is the name of the server
// https://lysand.org/objects/server-metadata#name
Name string `json:"name"`
// Version is the version of the server software
// https://lysand.org/objects/server-metadata#version
Version *semver.Version `json:"version"`
// Description is a description of the server
// https://lysand.org/objects/server-metadata#description
Description *string `json:"description,omitempty"`
// Website is the URL to the server's website
// https://lysand.org/objects/server-metadata#website
Website *URL `json:"website,omitempty"`
// Moderators is a list of URLs to moderators
// https://lysand.org/objects/server-metadata#moderators
Moderators []*URL `json:"moderators,omitempty"`
// Admins is a list of URLs to administrators
// https://lysand.org/objects/server-metadata#admins
Admins []*URL `json:"admins,omitempty"`
// Logo is the URL to the server's logo
// https://lysand.org/objects/server-metadata#logo
Logo *ImageContentTypeMap `json:"logo,omitempty"`
// Banner is the URL to the server's banner
// https://lysand.org/objects/server-metadata#banner
Banner *ImageContentTypeMap `json:"banner,omitempty"`
// SupportedExtensions is a list of supported extensions
SupportedExtensions []string `json:"supported_extensions"`
}
func (s ServerMetadata) MarshalJSON() ([]byte, error) {
type serverMetadata ServerMetadata
s2 := serverMetadata(s)
s2.Type = "ServerMetadata"
return json.Marshal(s2)
}

View file

@ -1,10 +1,13 @@
package lysand
import (
"crypto"
"crypto/ed25519"
"encoding/base64"
"fmt"
versiacrypto "github.com/lysand-org/versia-go/pkg/lysand/crypto"
"net/url"
"os"
"strings"
)
@ -32,8 +35,16 @@ func (s *SignatureData) String() string {
return fmt.Sprintf("%s %s?%s %s %s", strings.ToLower(s.RequestMethod), s.URL.Path, s.URL.RawQuery, s.Nonce, base64.StdEncoding.EncodeToString(s.Digest))
}
func (s *SignatureData) Validate(pubKey ed25519.PublicKey, signature []byte) bool {
return ed25519.Verify(pubKey, []byte(s.String()), signature)
func (s *SignatureData) Validate(pubKey crypto.PublicKey, signature []byte) bool {
data := []byte(s.String())
verify, err := versiacrypto.NewVerify(pubKey)
if err != nil {
_, _ = fmt.Fprintf(os.Stderr, "%v\n", err)
return false
}
return verify(data, signature)
}
func (s *SignatureData) Sign(privKey ed25519.PrivateKey) []byte {
@ -54,11 +65,10 @@ func (s Signer) Sign(signatureData SignatureData) *FederationHeaders {
}
type Verifier struct {
PublicKey ed25519.PublicKey
PublicKey crypto.PublicKey
}
func (v Verifier) Verify(method string, u *url.URL, body []byte, fedHeaders *FederationHeaders) bool {
sigData := NewSignatureData(method, fedHeaders.Nonce, u, hashSHA256(body))
return sigData.Validate(v.PublicKey, fedHeaders.Signature)
return NewSignatureData(method, fedHeaders.Nonce, u, versiacrypto.SHA256(body)).
Validate(v.PublicKey, fedHeaders.Signature)
}

View file

@ -1,59 +1,92 @@
package lysand
import (
"crypto/ed25519"
"crypto"
"crypto/x509"
"encoding/base64"
"encoding/json"
"errors"
)
var (
ErrInvalidPublicKeyType = errors.New("invalid public key type")
)
// PublicKey represents a public key for a user. For more information, see the [Spec].
// UserPublicKey represents a public key for a user. For more information, see the [Spec].
//
// [Spec]: https://lysand.org/security/keys#public-key-cryptography
type PublicKey struct {
PublicKey SPKIPublicKey `json:"public_key"`
Actor *URL `json:"actor"`
type UserPublicKey struct {
Actor *URL `json:"actor"`
// Algorithm can only be `ed25519` for now
Algorithm string `json:"algorithm"`
Key *SPKIPublicKey `json:"-"`
RawKey json.RawMessage `json:"key"`
}
// SPKIPublicKey is a type that represents a [ed25519.PublicKey] in the SPKI
// format.
type SPKIPublicKey ed25519.PublicKey
func (k *UserPublicKey) UnmarshalJSON(raw []byte) error {
type t UserPublicKey
k2 := (*t)(k)
// UnmarshalJSON decodes the public key from a base64 encoded string and then unmarshals it from the SPKI form.
func (k *SPKIPublicKey) UnmarshalJSON(raw []byte) error {
rawStr := ""
if err := json.Unmarshal(raw, &rawStr); err != nil {
if err := json.Unmarshal(raw, k2); err != nil {
return err
}
raw, err := base64.StdEncoding.DecodeString(rawStr)
if err != nil {
var err error
if k2.Key, err = unmarshalSPKIPubKey(k2.Algorithm, k2.RawKey); err != nil {
return err
}
parsed, err := x509.ParsePKIXPublicKey(raw)
if err != nil {
return err
}
edKey, ok := parsed.(ed25519.PublicKey)
if !ok {
return ErrInvalidPublicKeyType
}
*k = SPKIPublicKey(edKey)
*k = UserPublicKey(*k2)
return nil
}
func (k UserPublicKey) MarshalJSON() ([]byte, error) {
type t UserPublicKey
k2 := t(k)
var err error
if k2.RawKey, err = k2.Key.MarshalJSON(); err != nil {
return nil, err
}
return json.Marshal(k2)
}
// SPKIPublicKey is a type that represents a [ed25519.PublicKey] in the SPKI
// format.
type SPKIPublicKey struct {
Key any
Algorithm string
}
func unmarshalSPKIPubKey(algorithm string, raw []byte) (*SPKIPublicKey, error) {
rawStr := ""
if err := json.Unmarshal(raw, &rawStr); err != nil {
return nil, err
}
raw, err := base64.StdEncoding.DecodeString(rawStr)
if err != nil {
return nil, err
}
return NewSPKIPubKey(algorithm, raw)
}
// NewSPKIPubKey decodes the public key from a base64 encoded string and then unmarshals it from the SPKI form.
func NewSPKIPubKey(algorithm string, raw []byte) (*SPKIPublicKey, error) {
parsed, err := x509.ParsePKIXPublicKey(raw)
if err != nil {
return nil, err
}
return &SPKIPublicKey{
Key: parsed,
Algorithm: algorithm,
}, nil
}
// MarshalJSON marshals the SPKI-encoded public key to a base64 encoded string.
func (k SPKIPublicKey) MarshalJSON() ([]byte, error) {
raw, err := x509.MarshalPKIXPublicKey(ed25519.PublicKey(k))
raw, err := x509.MarshalPKIXPublicKey(k.Key)
if err != nil {
return nil, err
}
@ -61,6 +94,6 @@ func (k SPKIPublicKey) MarshalJSON() ([]byte, error) {
return json.Marshal(base64.StdEncoding.EncodeToString(raw))
}
func (k SPKIPublicKey) ToStd() ed25519.PublicKey {
return ed25519.PublicKey(k)
func (k SPKIPublicKey) ToKey() crypto.PublicKey {
return k.Key
}

View file

@ -12,20 +12,20 @@ import (
func TestSPKIPublicKey_UnmarshalJSON(t *testing.T) {
expectedPk := must(x509.ParsePKIXPublicKey, must(base64.StdEncoding.DecodeString, "MCowBQYDK2VwAyEAgKNt+9eyOXdb7MSrrmHlsFD2H9NGwC+56PjpWD46Tcs="))
pk := PublicKey{}
pk := UserPublicKey{}
raw := []byte(`{"public_key":"MCowBQYDK2VwAyEAgKNt+9eyOXdb7MSrrmHlsFD2H9NGwC+56PjpWD46Tcs="}`)
if err := json.Unmarshal(raw, &pk); err != nil {
t.Error(err)
}
assert.Equal(t, expectedPk, ed25519.PublicKey(pk.PublicKey))
assert.Equal(t, expectedPk, ed25519.PublicKey(pk.Key))
}
func TestSPKIPublicKey_MarshalJSON(t *testing.T) {
expectedPk := must(x509.ParsePKIXPublicKey, must(base64.StdEncoding.DecodeString, "MCowBQYDK2VwAyEAgKNt+9eyOXdb7MSrrmHlsFD2H9NGwC+56PjpWD46Tcs=")).(ed25519.PublicKey)
pk := PublicKey{
PublicKey: SPKIPublicKey(expectedPk),
pk := UserPublicKey{
Key: SPKIPublicKey(expectedPk),
}
if _, err := json.Marshal(pk); err != nil {
t.Error(err)

62
pkg/protoretry/client.go Normal file
View file

@ -0,0 +1,62 @@
package protoretry
import (
"context"
"errors"
"io"
"net/http"
"net/url"
"syscall"
)
type Client struct {
base *http.Client
}
func New(base *http.Client) *Client {
return &Client{base}
}
func (c *Client) Do(req *http.Request) (*http.Response, error) {
if res, err := c.base.Do(req); err == nil {
return res, nil
} else if !errors.Is(err, syscall.ECONNREFUSED) {
return nil, err
}
req.URL.Scheme = "http"
return c.base.Do(req)
}
func (c *Client) GET(ctx context.Context, u *url.URL) ([]byte, *http.Response, error) {
req, err := http.NewRequestWithContext(ctx, "GET", u.String(), nil)
if err != nil {
return nil, nil, err
}
return c.DoReq(req)
}
func (c *Client) POST(ctx context.Context, u *url.URL, reqBody io.Reader) ([]byte, *http.Response, error) {
req, err := http.NewRequestWithContext(ctx, "POST", u.String(), reqBody)
if err != nil {
return nil, nil, err
}
return c.DoReq(req)
}
func (c *Client) DoReq(req *http.Request) ([]byte, *http.Response, error) {
res, err := c.Do(req)
if err != nil {
return nil, nil, err
}
defer res.Body.Close()
resBody, err := io.ReadAll(res.Body)
if err != nil {
return nil, nil, err
}
return resBody, res, nil
}

View file

@ -71,15 +71,15 @@ type Client struct {
log logr.Logger
}
func NewClient(ctx context.Context, name string, natsClient *nats.Conn, telemetry *unitel.Telemetry, log logr.Logger) (*Client, error) {
func NewClient(ctx context.Context, streamName string, natsClient *nats.Conn, telemetry *unitel.Telemetry, log logr.Logger) (*Client, error) {
js, err := jetstream.New(natsClient)
if err != nil {
return nil, err
}
s, err := js.CreateStream(ctx, jetstream.StreamConfig{
Name: name,
Subjects: []string{name + ".*"},
Name: streamName,
Subjects: []string{streamName + ".*"},
MaxConsumers: -1,
MaxMsgs: -1,
Discard: jetstream.DiscardOld,
@ -89,7 +89,7 @@ func NewClient(ctx context.Context, name string, natsClient *nats.Conn, telemetr
AllowDirect: true,
})
if errors.Is(err, nats.ErrStreamNameAlreadyInUse) {
s, err = js.Stream(ctx, name)
s, err = js.Stream(ctx, streamName)
if err != nil {
return nil, err
}
@ -100,8 +100,8 @@ func NewClient(ctx context.Context, name string, natsClient *nats.Conn, telemetr
stopCh := make(chan struct{})
c := &Client{
name: name,
subject: name + ".tasks",
name: streamName,
subject: streamName + ".tasks",
handlers: map[string][]Handler{},
@ -145,7 +145,7 @@ func (c *Client) Submit(ctx context.Context, task Task) error {
if err != nil {
return err
}
c.log.V(1).Info("submitted task", "id", task.ID, "type", task.Type, "sequence", msg.Sequence)
c.log.V(2).Info("Submitted task", "id", task.ID, "type", task.Type, "sequence", msg.Sequence)
s.AddAttribute("messaging.message.id", msg.Sequence)
@ -153,7 +153,7 @@ func (c *Client) Submit(ctx context.Context, task Task) error {
}
func (c *Client) RegisterHandler(type_ string, handler Handler) {
c.log.V(2).Info("registering handler", "type", type_)
c.log.V(2).Info("Registering handler", "type", type_)
if _, ok := c.handlers[type_]; !ok {
c.handlers[type_] = []Handler{}
@ -161,13 +161,11 @@ func (c *Client) RegisterHandler(type_ string, handler Handler) {
c.handlers[type_] = append(c.handlers[type_], handler)
}
func (c *Client) Start(ctx context.Context) error {
c.log.Info("starting")
func (c *Client) StartConsumer(ctx context.Context, consumerGroup string) error {
c.log.Info("Starting consumer")
sub, err := c.js.CreateConsumer(ctx, c.name, jetstream.ConsumerConfig{
// TODO: set name properly
Name: "versia-go",
Durable: "versia-go",
Durable: consumerGroup,
DeliverPolicy: jetstream.DeliverAllPolicy,
ReplayPolicy: jetstream.ReplayInstantPolicy,
AckPolicy: jetstream.AckExplicitPolicy,
@ -191,16 +189,16 @@ func (c *Client) Start(ctx context.Context) error {
msg, err := m.Next()
if err != nil {
if errors.Is(err, jetstream.ErrMsgIteratorClosed) {
c.log.Info("stopping")
c.log.Info("Stopping")
return
}
c.log.Error(err, "failed to get next message")
c.log.Error(err, "Failed to get next message")
break
}
if err := c.handleTask(ctx, msg); err != nil {
c.log.Error(err, "failed to handle task")
c.log.Error(err, "Failed to handle task")
break
}
}
@ -224,7 +222,7 @@ func (c *Client) handleTask(ctx context.Context, msg jetstream.Msg) error {
var w taskWrapper
if err := json.Unmarshal(data, &w); err != nil {
if err := msg.Nak(); err != nil {
c.log.Error(err, "failed to nak message")
c.log.Error(err, "Failed to nak message")
}
return err
@ -246,21 +244,21 @@ func (c *Client) handleTask(ctx context.Context, msg jetstream.Msg) error {
handlers, ok := c.handlers[w.Task.Type]
if !ok {
c.log.V(1).Info("no handler for task", "type", w.Task.Type)
c.log.V(2).Info("No handler for task", "type", w.Task.Type)
return msg.Nak()
}
var errs CombinedError
for _, handler := range handlers {
if err := handler(ctx, w.Task); err != nil {
c.log.Error(err, "handler failed", "type", w.Task.Type)
c.log.Error(err, "Handler failed", "type", w.Task.Type)
errs.Errors = append(errs.Errors, err)
}
}
if len(errs.Errors) > 0 {
if err := msg.Nak(); err != nil {
c.log.Error(err, "failed to nak message")
c.log.Error(err, "Failed to nak message")
errs.Errors = append(errs.Errors, err)
}

View file

@ -1,13 +1,19 @@
package webfinger
import (
"context"
"encoding/json"
"errors"
"fmt"
"github.com/lysand-org/versia-go/pkg/protoretry"
"net/http"
"net/url"
"strings"
)
var (
ErrInvalidSyntax = errors.New("must follow the format \"acct:<ID|Username>@<DOMAIN>\"")
ErrUserNotFound = errors.New("user could not be found")
)
func ParseResource(res string) (*UserID, error) {
@ -48,7 +54,7 @@ type Response struct {
type Link struct {
Relation string `json:"rel"`
Type any `json:"type"`
Type string `json:"type"`
Link string `json:"href"`
}
@ -70,3 +76,47 @@ func (u User) WebFingerResource() Response {
},
}
}
func Discover(c *protoretry.Client, ctx context.Context, baseURI, username string) (*User, error) {
u := &User{UserID: UserID{ID: username, Domain: baseURI}}
body, resp, err := c.GET(ctx, &url.URL{
Scheme: "https",
Host: u.UserID.Domain,
Path: "/.well-known/webfinger",
RawQuery: url.Values{"resource": []string{"acct:" + u.String()}}.Encode(),
})
if err != nil {
return nil, err
}
if resp.StatusCode == http.StatusNotFound {
return nil, ErrUserNotFound
} else if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
}
var respBody Response
if err := json.Unmarshal(body, &respBody); err != nil {
return nil, err
}
if respBody.Error != nil {
return nil, fmt.Errorf("webfinger error: %s", *respBody.Error)
}
for _, link := range respBody.Links {
if link.Relation == "self" {
if u.URI, err = url.Parse(link.Link); err != nil {
return nil, err
}
} else if link.Relation == "avatar" {
u.AvatarMIMEType = link.Type
if u.Avatar, err = url.Parse(link.Link); err != nil {
return nil, err
}
}
}
return u, nil
}