2024-08-11 03:51:22 +02:00
|
|
|
package lysand
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"bytes"
|
|
|
|
|
"context"
|
|
|
|
|
"crypto/ed25519"
|
2024-08-15 19:22:17 +02:00
|
|
|
"crypto/rand"
|
|
|
|
|
"encoding/base64"
|
2024-08-11 03:51:22 +02:00
|
|
|
"encoding/json"
|
|
|
|
|
"fmt"
|
|
|
|
|
"net/http"
|
|
|
|
|
"net/url"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// User represents a user object in the Lysand protocol. For more information, see the [Spec].
|
|
|
|
|
//
|
|
|
|
|
// [Spec]: https://lysand.org/objects/user
|
|
|
|
|
type User struct {
|
|
|
|
|
Entity
|
|
|
|
|
|
|
|
|
|
// PublicKey is the public key of the user.
|
|
|
|
|
// https://lysand.org/objects/user#public-key
|
|
|
|
|
PublicKey PublicKey `json:"public_key"`
|
|
|
|
|
|
|
|
|
|
// DisplayName is the display name of the user.
|
|
|
|
|
// https://lysand.org/objects/user#display-name
|
|
|
|
|
DisplayName *string `json:"display_name,omitempty"`
|
|
|
|
|
|
|
|
|
|
// Username is the username of the user. Must be unique on the instance and match the following regex: ^[a-z0-9_-]+$
|
|
|
|
|
// https://lysand.org/objects/user#username
|
|
|
|
|
Username string `json:"username"`
|
|
|
|
|
|
|
|
|
|
// Indexable is a boolean that indicates whether the user is indexable by search engines.
|
|
|
|
|
// https://lysand.org/objects/user#indexable
|
|
|
|
|
Indexable bool `json:"indexable"`
|
|
|
|
|
|
|
|
|
|
// ManuallyApprovesFollowers is a boolean that indicates whether the user manually approves followers.
|
|
|
|
|
// https://lysand.org/objects/user#manually-approves-followers
|
|
|
|
|
ManuallyApprovesFollowers bool `json:"manually_approves_followers"`
|
|
|
|
|
|
|
|
|
|
// Avatar is the avatar of the user in different image content types.
|
|
|
|
|
// https://lysand.org/objects/user#avatar
|
|
|
|
|
Avatar ImageContentTypeMap `json:"avatar,omitempty"`
|
|
|
|
|
|
|
|
|
|
// Header is the header image of the user in different image content types.
|
|
|
|
|
// https://lysand.org/objects/user#header
|
|
|
|
|
Header ImageContentTypeMap `json:"header,omitempty"`
|
|
|
|
|
|
|
|
|
|
// Bio is the biography of the user in different text content types.
|
|
|
|
|
// https://lysand.org/objects/user#bio
|
|
|
|
|
Bio TextContentTypeMap `json:"bio"`
|
|
|
|
|
|
|
|
|
|
// Fields is a list of fields that the user has filled out.
|
|
|
|
|
// https://lysand.org/objects/user#fields
|
|
|
|
|
Fields []Field `json:"fields,omitempty"`
|
|
|
|
|
|
|
|
|
|
// Featured is the featured posts of the user.
|
|
|
|
|
// https://lysand.org/objects/user#featured
|
|
|
|
|
Featured *URL `json:"featured"`
|
|
|
|
|
|
|
|
|
|
// Followers is the followers of the user.
|
|
|
|
|
// https://lysand.org/objects/user#followers
|
|
|
|
|
Followers *URL `json:"followers"`
|
|
|
|
|
|
|
|
|
|
// Following is the users that the user is following.
|
|
|
|
|
// 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"`
|
|
|
|
|
|
|
|
|
|
// Outbox is the outbox of the user.
|
|
|
|
|
// https://lysand.org/objects/user#outbox
|
|
|
|
|
Outbox *URL `json:"outbox"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (u User) MarshalJSON() ([]byte, error) {
|
|
|
|
|
type user User
|
|
|
|
|
u2 := user(u)
|
|
|
|
|
u2.Type = "User"
|
|
|
|
|
return json.Marshal(u2)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
|
2024-08-15 19:22:17 +02:00
|
|
|
fedHeaders, err := ExtractFederationHeaders(resp.Header)
|
2024-08-11 03:51:22 +02:00
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
v := Verifier{ed25519.PublicKey(user.PublicKey.PublicKey)}
|
2024-08-15 19:22:17 +02:00
|
|
|
if !v.Verify("GET", uri, body, fedHeaders) {
|
2024-08-11 03:51:22 +02:00
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
|
2024-08-15 19:22:17 +02:00
|
|
|
nonce := make([]byte, 32)
|
|
|
|
|
if _, err := rand.Read(nonce); err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
2024-08-11 03:51:22 +02:00
|
|
|
|
2024-08-15 19:22:17 +02:00
|
|
|
sigData := NewSignatureData("POST", base64.StdEncoding.EncodeToString(nonce), uri, hashSHA256(body))
|
2024-08-11 03:51:22 +02:00
|
|
|
sig := signer.Sign(*sigData)
|
|
|
|
|
|
|
|
|
|
req, err := http.NewRequestWithContext(ctx, "POST", uri.String(), bytes.NewReader(body))
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
2024-08-15 19:22:17 +02:00
|
|
|
sig.Inject(req.Header)
|
2024-08-11 03:51:22 +02:00
|
|
|
|
|
|
|
|
_, respBody, err := c.doReq(req)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return respBody, nil
|
|
|
|
|
}
|