mirror of
https://github.com/versia-pub/versia-go.git
synced 2026-03-12 20:19:15 +01:00
refactor!: working WD-4 user discovery
This commit is contained in:
parent
cf0053312d
commit
61891d891a
91 changed files with 12768 additions and 5562 deletions
|
|
@ -6,6 +6,7 @@ var (
|
|||
ErrUnauthorized = NewAPIError(401, "Unauthorized")
|
||||
ErrForbidden = NewAPIError(403, "Forbidden")
|
||||
ErrNotFound = NewAPIError(404, "Not found")
|
||||
ErrUserNotFound = ErrNotFound(map[string]any{"reason": "user not found"})
|
||||
ErrConflict = NewAPIError(409, "Conflict")
|
||||
ErrUsernameTaken = NewAPIError(409, "Username is taken")
|
||||
ErrRateLimitExceeded = NewAPIError(429, "Rate limit exceeded")
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package api_schema
|
|||
|
||||
import (
|
||||
"github.com/google/uuid"
|
||||
"github.com/lysand-org/versia-go/pkg/lysand"
|
||||
)
|
||||
|
||||
type User struct {
|
||||
|
|
@ -9,9 +10,16 @@ type User struct {
|
|||
Username string `json:"username"`
|
||||
}
|
||||
|
||||
type LysandUser lysand.User
|
||||
|
||||
type FetchUserResponse = APIResponse[User]
|
||||
|
||||
type CreateUserRequest struct {
|
||||
Username string `json:"username" validate:"required,username_regex,min=3,max=32"`
|
||||
Username string `json:"username" validate:"required,username_regex,min=1,max=32"`
|
||||
Password string `json:"password" validate:"required,min=8,max=256"`
|
||||
}
|
||||
|
||||
type SearchUserRequest struct {
|
||||
Username string `query:"username" validate:"required,username_regex,min=1,max=32"`
|
||||
Domain *string `query:"domain" validate:"domain_regex"`
|
||||
}
|
||||
|
|
|
|||
96
internal/entity/server_metadata.go
Normal file
96
internal/entity/server_metadata.go
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
package entity
|
||||
|
||||
import (
|
||||
"github.com/lysand-org/versia-go/ent"
|
||||
"github.com/lysand-org/versia-go/pkg/lysand"
|
||||
versiacrypto "github.com/lysand-org/versia-go/pkg/lysand/crypto"
|
||||
)
|
||||
|
||||
type InstanceMetadata struct {
|
||||
*ent.InstanceMetadata
|
||||
|
||||
Moderators []User
|
||||
ModeratorsCollection *lysand.URL
|
||||
|
||||
Admins []User
|
||||
AdminsCollection *lysand.URL
|
||||
|
||||
SharedInbox *lysand.URL
|
||||
|
||||
PublicKey *lysand.SPKIPublicKey
|
||||
|
||||
Logo *lysand.ImageContentTypeMap
|
||||
Banner *lysand.ImageContentTypeMap
|
||||
}
|
||||
|
||||
func NewInstanceMetadata(dbData *ent.InstanceMetadata) (*InstanceMetadata, error) {
|
||||
n := &InstanceMetadata{
|
||||
InstanceMetadata: dbData,
|
||||
PublicKey: &lysand.SPKIPublicKey{},
|
||||
}
|
||||
|
||||
var err error
|
||||
if n.PublicKey.Key, err = versiacrypto.ToTypedKey(dbData.PublicKeyAlgorithm, dbData.PublicKey); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if n.SharedInbox, err = lysand.ParseURL(dbData.SharedInboxURI); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if dbData.ModeratorsURI != nil {
|
||||
if n.ModeratorsCollection, err = lysand.ParseURL(*dbData.ModeratorsURI); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if dbData.AdminsURI != nil {
|
||||
if n.AdminsCollection, err = lysand.ParseURL(*dbData.AdminsURI); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
for _, r := range dbData.Edges.Moderators {
|
||||
u, err := NewUser(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
n.Moderators = append(n.Moderators, *u)
|
||||
}
|
||||
|
||||
for _, r := range dbData.Edges.Admins {
|
||||
u, err := NewUser(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
n.Admins = append(n.Admins, *u)
|
||||
}
|
||||
|
||||
return n, nil
|
||||
}
|
||||
|
||||
func (m InstanceMetadata) ToLysand() lysand.InstanceMetadata {
|
||||
return lysand.InstanceMetadata{
|
||||
Extensions: m.Extensions,
|
||||
Name: m.Name,
|
||||
Description: m.Description,
|
||||
Host: m.Host,
|
||||
SharedInbox: m.SharedInbox,
|
||||
Moderators: m.ModeratorsCollection,
|
||||
Admins: m.AdminsCollection,
|
||||
Logo: m.Logo,
|
||||
Banner: m.Banner,
|
||||
PublicKey: lysand.InstancePublicKey{
|
||||
Algorithm: m.PublicKeyAlgorithm,
|
||||
Key: m.PublicKey,
|
||||
},
|
||||
Software: lysand.InstanceSoftware{
|
||||
Name: m.SoftwareName,
|
||||
Version: m.SoftwareVersion,
|
||||
},
|
||||
Compatibility: lysand.InstanceCompatibility{
|
||||
Versions: m.SupportedVersions,
|
||||
Extensions: m.SupportedExtensions,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@ package entity
|
|||
|
||||
import (
|
||||
"github.com/lysand-org/versia-go/internal/helpers"
|
||||
versiacrypto "github.com/lysand-org/versia-go/pkg/lysand/crypto"
|
||||
"net/url"
|
||||
|
||||
"github.com/lysand-org/versia-go/ent"
|
||||
|
|
@ -12,12 +13,14 @@ import (
|
|||
type User struct {
|
||||
*ent.User
|
||||
|
||||
URI *lysand.URL
|
||||
Inbox *lysand.URL
|
||||
Outbox *lysand.URL
|
||||
Featured *lysand.URL
|
||||
Followers *lysand.URL
|
||||
Following *lysand.URL
|
||||
URI *lysand.URL
|
||||
PKActorURI *lysand.URL
|
||||
PublicKey *lysand.SPKIPublicKey
|
||||
Inbox *lysand.URL
|
||||
Outbox *lysand.URL
|
||||
Featured *lysand.URL
|
||||
Followers *lysand.URL
|
||||
Following *lysand.URL
|
||||
|
||||
DisplayName string
|
||||
LysandAvatar lysand.ImageContentTypeMap
|
||||
|
|
@ -25,38 +28,52 @@ type User struct {
|
|||
Signer lysand.Signer
|
||||
}
|
||||
|
||||
func NewUser(dbUser *ent.User) (*User, error) {
|
||||
u := &User{User: dbUser}
|
||||
func NewUser(dbData *ent.User) (*User, error) {
|
||||
u := &User{
|
||||
User: dbData,
|
||||
PublicKey: &lysand.SPKIPublicKey{
|
||||
Key: nil,
|
||||
Algorithm: dbData.PublicKeyAlgorithm,
|
||||
},
|
||||
DisplayName: dbData.Username,
|
||||
|
||||
u.DisplayName = u.Username
|
||||
if dbUser.DisplayName != nil {
|
||||
u.DisplayName = *dbUser.DisplayName
|
||||
LysandAvatar: lysandAvatar(dbData),
|
||||
LysandBiography: lysandBiography(dbData),
|
||||
}
|
||||
|
||||
if dbData.DisplayName != nil {
|
||||
u.DisplayName = *dbData.DisplayName
|
||||
}
|
||||
|
||||
var err error
|
||||
if u.URI, err = lysand.ParseURL(dbUser.URI); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if u.Inbox, err = lysand.ParseURL(dbUser.Inbox); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if u.Outbox, err = lysand.ParseURL(dbUser.Outbox); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if u.Featured, err = lysand.ParseURL(dbUser.Featured); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if u.Followers, err = lysand.ParseURL(dbUser.Followers); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if u.Following, err = lysand.ParseURL(dbUser.Following); err != nil {
|
||||
if u.PublicKey.Key, err = versiacrypto.ToTypedKey(dbData.PublicKeyAlgorithm, dbData.PublicKey); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if u.URI, err = lysand.ParseURL(dbData.URI); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if u.PKActorURI, err = lysand.ParseURL(dbData.PublicKeyActor); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if u.Inbox, err = lysand.ParseURL(dbData.Inbox); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if u.Outbox, err = lysand.ParseURL(dbData.Outbox); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if u.Featured, err = lysand.ParseURL(dbData.Featured); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if u.Followers, err = lysand.ParseURL(dbData.Followers); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if u.Following, err = lysand.ParseURL(dbData.Following); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
u.LysandAvatar = lysandAvatar(dbUser)
|
||||
u.LysandBiography = lysandBiography(dbUser)
|
||||
u.Signer = lysand.Signer{
|
||||
PrivateKey: dbUser.PrivateKey,
|
||||
PrivateKey: dbData.PrivateKey,
|
||||
UserURL: u.URI.ToStd(),
|
||||
}
|
||||
|
||||
|
|
@ -76,9 +93,10 @@ func (u User) ToLysand() *lysand.User {
|
|||
Avatar: u.LysandAvatar,
|
||||
Header: imageMap(u.Edges.HeaderImage),
|
||||
Indexable: u.Indexable,
|
||||
PublicKey: lysand.PublicKey{
|
||||
Actor: utils.UserAPIURL(u.ID),
|
||||
PublicKey: lysand.SPKIPublicKey(u.PublicKey),
|
||||
PublicKey: lysand.UserPublicKey{
|
||||
Actor: u.PKActorURI,
|
||||
Algorithm: u.PublicKeyAlgorithm,
|
||||
Key: u.PublicKey,
|
||||
},
|
||||
Bio: u.LysandBiography,
|
||||
Fields: u.Fields,
|
||||
|
|
@ -88,10 +106,6 @@ func (u User) ToLysand() *lysand.User {
|
|||
Featured: u.Featured,
|
||||
Followers: u.Followers,
|
||||
Following: u.Following,
|
||||
|
||||
// TODO: Remove these, they got deprecated and moved into an extension
|
||||
Likes: utils.UserLikesAPIURL(u.ID),
|
||||
Dislikes: utils.UserDislikesAPIURL(u.ID),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,17 +4,22 @@ import (
|
|||
"github.com/go-logr/logr"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/lysand-org/versia-go/config"
|
||||
"github.com/lysand-org/versia-go/internal/service"
|
||||
"github.com/lysand-org/versia-go/pkg/webfinger"
|
||||
)
|
||||
|
||||
type Handler struct {
|
||||
instanceMetadataService service.InstanceMetadataService
|
||||
|
||||
hostMeta webfinger.HostMeta
|
||||
|
||||
log logr.Logger
|
||||
}
|
||||
|
||||
func New(log logr.Logger) *Handler {
|
||||
func New(instanceMetadataService service.InstanceMetadataService, log logr.Logger) *Handler {
|
||||
return &Handler{
|
||||
instanceMetadataService: instanceMetadataService,
|
||||
|
||||
hostMeta: webfinger.NewHostMeta(config.C.PublicAddress),
|
||||
|
||||
log: log.WithName("users"),
|
||||
|
|
@ -22,7 +27,11 @@ func New(log logr.Logger) *Handler {
|
|||
}
|
||||
|
||||
func (i *Handler) Register(r fiber.Router) {
|
||||
r.Get("/.well-known/lysand", i.GetLysandServerMetadata)
|
||||
r.Get("/.well-known/versia", i.GetLysandInstanceMetadata)
|
||||
r.Get("/.well-known/versia/admins", i.GetLysandInstanceMetadata)
|
||||
r.Get("/.well-known/versia/moderators", i.GetLysandInstanceMetadata)
|
||||
|
||||
// Webfinger host meta spec
|
||||
r.Get("/.well-known/host-meta", i.GetHostMeta)
|
||||
r.Get("/.well-known/host-meta.json", i.GetHostMetaJSON)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,14 @@
|
|||
package meta_handler
|
||||
|
||||
import (
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
func (i *Handler) GetLysandInstanceMetadata(c *fiber.Ctx) error {
|
||||
m, err := i.instanceMetadataService.Ours(c.UserContext())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.JSON(m.ToLysand())
|
||||
}
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
package meta_handler
|
||||
|
||||
import (
|
||||
"github.com/Masterminds/semver"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/lysand-org/versia-go/config"
|
||||
"github.com/lysand-org/versia-go/pkg/lysand"
|
||||
)
|
||||
|
||||
func (i *Handler) GetLysandServerMetadata(c *fiber.Ctx) error {
|
||||
return c.JSON(lysand.ServerMetadata{
|
||||
// TODO: Get version from build linker flags
|
||||
Version: semver.MustParse("0.0.0-dev"),
|
||||
|
||||
Name: config.C.InstanceName,
|
||||
Description: config.C.InstanceDescription,
|
||||
Website: lysand.URLFromStd(config.C.PublicAddress),
|
||||
|
||||
// TODO: Get more info
|
||||
Moderators: nil,
|
||||
Admins: nil,
|
||||
Logo: nil,
|
||||
Banner: nil,
|
||||
|
||||
SupportedExtensions: []string{},
|
||||
Extensions: map[string]any{},
|
||||
})
|
||||
}
|
||||
51
internal/handlers/user_handler/app_user_search.go
Normal file
51
internal/handlers/user_handler/app_user_search.go
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
package user_handler
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/lysand-org/versia-go/internal/api_schema"
|
||||
"github.com/lysand-org/versia-go/pkg/webfinger"
|
||||
"net"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
func (i *Handler) SearchUser(c *fiber.Ctx) error {
|
||||
var req api_schema.SearchUserRequest
|
||||
if err := c.QueryParser(&req); err != nil {
|
||||
return api_schema.ErrInvalidRequestBody(nil)
|
||||
}
|
||||
|
||||
if err := i.bodyValidator.Validate(req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
u, err := i.userService.Search(c.UserContext(), req)
|
||||
if err != nil {
|
||||
// TODO: Move into service error
|
||||
if errors.Is(err, syscall.ECONNREFUSED) {
|
||||
return api_schema.ErrBadRequest(map[string]any{"reason": "Remote server is offline"})
|
||||
}
|
||||
|
||||
if errors.Is(err, webfinger.ErrUserNotFound) {
|
||||
return api_schema.ErrUserNotFound
|
||||
}
|
||||
|
||||
var dnsErr *net.DNSError
|
||||
if errors.As(err, &dnsErr) {
|
||||
if dnsErr.IsNotFound {
|
||||
return api_schema.ErrBadRequest(map[string]any{"reason": fmt.Sprintf("Could not resolve %s", dnsErr.Name)})
|
||||
}
|
||||
|
||||
if dnsErr.IsTimeout {
|
||||
return api_schema.ErrInternalServerError(map[string]any{"reason": "Local DNS server timed out"})
|
||||
}
|
||||
}
|
||||
|
||||
i.log.Error(err, "Failed to search for user", "username", req.Username)
|
||||
|
||||
return api_schema.ErrInternalServerError(nil)
|
||||
}
|
||||
|
||||
return c.JSON((*api_schema.LysandUser)(u.ToLysand()))
|
||||
}
|
||||
|
|
@ -8,9 +8,11 @@ import (
|
|||
)
|
||||
|
||||
type Handler struct {
|
||||
userService service.UserService
|
||||
federationService service.FederationService
|
||||
inboxService service.InboxService
|
||||
requestSigner service.RequestSigner
|
||||
|
||||
userService service.UserService
|
||||
inboxService service.InboxService
|
||||
|
||||
bodyValidator validators.BodyValidator
|
||||
requestValidator validators.RequestValidator
|
||||
|
|
@ -18,11 +20,13 @@ type Handler struct {
|
|||
log logr.Logger
|
||||
}
|
||||
|
||||
func New(userService service.UserService, federationService service.FederationService, inboxService service.InboxService, bodyValidator validators.BodyValidator, requestValidator validators.RequestValidator, log logr.Logger) *Handler {
|
||||
func New(federationService service.FederationService, requestSigner service.RequestSigner, userService service.UserService, inboxService service.InboxService, bodyValidator validators.BodyValidator, requestValidator validators.RequestValidator, log logr.Logger) *Handler {
|
||||
return &Handler{
|
||||
userService: userService,
|
||||
federationService: federationService,
|
||||
inboxService: inboxService,
|
||||
requestSigner: requestSigner,
|
||||
|
||||
userService: userService,
|
||||
inboxService: inboxService,
|
||||
|
||||
bodyValidator: bodyValidator,
|
||||
requestValidator: requestValidator,
|
||||
|
|
@ -41,6 +45,7 @@ func (i *Handler) Register(r fiber.Router) {
|
|||
r.Get("/api/app/users/:id", i.GetUser)
|
||||
r.Post("/api/app/users/", i.CreateUser)
|
||||
|
||||
r.Get("/api/users/search", i.SearchUser)
|
||||
r.Get("/api/users/:id", i.GetLysandUser)
|
||||
r.Post("/api/users/:id/inbox", i.LysandInbox)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,5 +27,5 @@ func (i *Handler) GetLysandUser(c *fiber.Ctx) error {
|
|||
return api_schema.ErrNotFound(map[string]any{"id": parsedRequestedUserID})
|
||||
}
|
||||
|
||||
return c.JSON(u.ToLysand())
|
||||
return i.requestSigner.Sign(c, u.Signer, u.ToLysand())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
package user_handler
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/lysand-org/versia-go/config"
|
||||
"github.com/lysand-org/versia-go/internal/api_schema"
|
||||
"github.com/lysand-org/versia-go/internal/helpers"
|
||||
"github.com/lysand-org/versia-go/pkg/webfinger"
|
||||
)
|
||||
|
|
@ -16,13 +18,19 @@ func (i *Handler) Webfinger(c *fiber.Ctx) error {
|
|||
}
|
||||
|
||||
if userID.Domain != config.C.PublicAddress.Host {
|
||||
return c.Status(fiber.StatusNotFound).JSON(webfinger.Response{
|
||||
return c.Status(fiber.StatusBadRequest).JSON(webfinger.Response{
|
||||
Error: helpers.StringPtr("The requested user is a remote user"),
|
||||
})
|
||||
}
|
||||
|
||||
wf, err := i.userService.GetWebfingerForUser(c.UserContext(), userID.ID)
|
||||
if err != nil {
|
||||
if errors.Is(err, api_schema.ErrUserNotFound) {
|
||||
return c.Status(fiber.StatusNotFound).JSON(webfinger.Response{
|
||||
Error: helpers.StringPtr("User could not be found"),
|
||||
})
|
||||
}
|
||||
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(webfinger.Response{
|
||||
Error: helpers.StringPtr("Failed to query user"),
|
||||
})
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ func NewFollowRepositoryImpl(db *ent.Client, log logr.Logger, telemetry *unitel.
|
|||
}
|
||||
|
||||
func (i FollowRepositoryImpl) GetByID(ctx context.Context, id uuid.UUID) (*entity.Follow, error) {
|
||||
s := i.telemetry.StartSpan(ctx, "function", "repository/repo_impls.FollowRepositoryImpl.GetByID").
|
||||
s := i.telemetry.StartSpan(ctx, "function", "repo_impls/FollowRepositoryImpl.GetByID").
|
||||
AddAttribute("followID", id)
|
||||
defer s.End()
|
||||
ctx = s.Context()
|
||||
|
|
@ -58,7 +58,7 @@ func (i FollowRepositoryImpl) GetByID(ctx context.Context, id uuid.UUID) (*entit
|
|||
}
|
||||
|
||||
func (i FollowRepositoryImpl) Follow(ctx context.Context, follower, followee *entity.User) (*entity.Follow, error) {
|
||||
s := i.telemetry.StartSpan(ctx, "function", "repository/repo_impls.FollowRepositoryImpl.Follow").
|
||||
s := i.telemetry.StartSpan(ctx, "function", "repo_impls/FollowRepositoryImpl.Follow").
|
||||
AddAttribute("follower", follower.URI).
|
||||
AddAttribute("followee", followee.URI)
|
||||
defer s.End()
|
||||
|
|
@ -101,7 +101,7 @@ func (i FollowRepositoryImpl) Follow(ctx context.Context, follower, followee *en
|
|||
}
|
||||
|
||||
func (i FollowRepositoryImpl) Unfollow(ctx context.Context, follower, followee *entity.User) error {
|
||||
s := i.telemetry.StartSpan(ctx, "function", "repository/repo_impls.FollowRepositoryImpl.Unfollow").
|
||||
s := i.telemetry.StartSpan(ctx, "function", "repo_impls/FollowRepositoryImpl.Unfollow").
|
||||
AddAttribute("follower", follower.URI).
|
||||
AddAttribute("followee", followee.URI)
|
||||
defer s.End()
|
||||
|
|
@ -121,7 +121,7 @@ func (i FollowRepositoryImpl) Unfollow(ctx context.Context, follower, followee *
|
|||
}
|
||||
|
||||
func (i FollowRepositoryImpl) AcceptFollow(ctx context.Context, follower, followee *entity.User) error {
|
||||
s := i.telemetry.StartSpan(ctx, "function", "repository/repo_impls.FollowRepositoryImpl.AcceptFollow").
|
||||
s := i.telemetry.StartSpan(ctx, "function", "repo_impls/FollowRepositoryImpl.AcceptFollow").
|
||||
AddAttribute("follower", follower.URI).
|
||||
AddAttribute("followee", followee.URI)
|
||||
defer s.End()
|
||||
|
|
@ -141,7 +141,7 @@ func (i FollowRepositoryImpl) AcceptFollow(ctx context.Context, follower, follow
|
|||
}
|
||||
|
||||
func (i FollowRepositoryImpl) RejectFollow(ctx context.Context, follower, followee *entity.User) error {
|
||||
s := i.telemetry.StartSpan(ctx, "function", "repository/repo_impls.FollowRepositoryImpl.RejectFollow").
|
||||
s := i.telemetry.StartSpan(ctx, "function", "repo_impls/FollowRepositoryImpl.RejectFollow").
|
||||
AddAttribute("follower", follower.URI).
|
||||
AddAttribute("followee", followee.URI)
|
||||
defer s.End()
|
||||
|
|
|
|||
|
|
@ -0,0 +1,62 @@
|
|||
package repo_impls
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.devminer.xyz/devminer/unitel"
|
||||
"github.com/go-logr/logr"
|
||||
"github.com/lysand-org/versia-go/ent"
|
||||
"github.com/lysand-org/versia-go/ent/instancemetadata"
|
||||
"github.com/lysand-org/versia-go/internal/entity"
|
||||
"github.com/lysand-org/versia-go/internal/repository"
|
||||
"github.com/lysand-org/versia-go/internal/service"
|
||||
"github.com/lysand-org/versia-go/pkg/lysand"
|
||||
)
|
||||
|
||||
var _ repository.InstanceMetadataRepository = (*InstanceMetadataRepositoryImpl)(nil)
|
||||
|
||||
type InstanceMetadataRepositoryImpl struct {
|
||||
federationService service.FederationService
|
||||
|
||||
db *ent.Client
|
||||
log logr.Logger
|
||||
telemetry *unitel.Telemetry
|
||||
}
|
||||
|
||||
func NewInstanceMetadataRepositoryImpl(federationService service.FederationService, db *ent.Client, log logr.Logger, telemetry *unitel.Telemetry) repository.InstanceMetadataRepository {
|
||||
return &InstanceMetadataRepositoryImpl{
|
||||
federationService: federationService,
|
||||
|
||||
db: db,
|
||||
log: log,
|
||||
telemetry: telemetry,
|
||||
}
|
||||
}
|
||||
|
||||
func (i *InstanceMetadataRepositoryImpl) GetByHost(ctx context.Context, host string) (*entity.InstanceMetadata, error) {
|
||||
s := i.telemetry.StartSpan(ctx, "function", "repo_impls/InstanceMetadataRepositoryImpl.GetByHost").
|
||||
AddAttribute("host", host)
|
||||
defer s.End()
|
||||
ctx = s.Context()
|
||||
|
||||
m, err := i.db.InstanceMetadata.Query().
|
||||
Where(instancemetadata.Host(host)).
|
||||
WithAdmins().
|
||||
WithModerators().
|
||||
Only(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return entity.NewInstanceMetadata(m)
|
||||
}
|
||||
|
||||
func (i *InstanceMetadataRepositoryImpl) ImportFromLysandByURI(ctx context.Context, uri *lysand.URL) (*entity.InstanceMetadata, error) {
|
||||
s := i.telemetry.StartSpan(ctx, "function", "repo_impls/InstanceMetadataRepositoryImpl.ImportFromLysandByURI").
|
||||
AddAttribute("uri", uri.String())
|
||||
defer s.End()
|
||||
ctx = s.Context()
|
||||
|
||||
//i.federationService.
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
|
@ -15,32 +15,38 @@ type Factory[T any] func(db *ent.Client, log logr.Logger, telemetry *unitel.Tele
|
|||
var _ repository.Manager = (*ManagerImpl)(nil)
|
||||
|
||||
type ManagerImpl struct {
|
||||
users repository.UserRepository
|
||||
notes repository.NoteRepository
|
||||
follows repository.FollowRepository
|
||||
users repository.UserRepository
|
||||
notes repository.NoteRepository
|
||||
follows repository.FollowRepository
|
||||
instanceMetadata repository.InstanceMetadataRepository
|
||||
|
||||
uRFactory Factory[repository.UserRepository]
|
||||
nRFactory Factory[repository.NoteRepository]
|
||||
fRFactory Factory[repository.FollowRepository]
|
||||
uRFactory Factory[repository.UserRepository]
|
||||
nRFactory Factory[repository.NoteRepository]
|
||||
fRFactory Factory[repository.FollowRepository]
|
||||
imRFactory Factory[repository.InstanceMetadataRepository]
|
||||
|
||||
db *ent.Client
|
||||
log logr.Logger
|
||||
telemetry *unitel.Telemetry
|
||||
}
|
||||
|
||||
func NewManagerImpl(db *ent.Client, telemetry *unitel.Telemetry, log logr.Logger, userRepositoryFunc Factory[repository.UserRepository], noteRepositoryFunc Factory[repository.NoteRepository], followRepositoryFunc Factory[repository.FollowRepository]) *ManagerImpl {
|
||||
userRepository := userRepositoryFunc(db, log.WithName("users"), telemetry)
|
||||
noteRepository := noteRepositoryFunc(db, log.WithName("notes"), telemetry)
|
||||
followRepository := followRepositoryFunc(db, log.WithName("follows"), telemetry)
|
||||
|
||||
func NewManagerImpl(
|
||||
db *ent.Client, telemetry *unitel.Telemetry, log logr.Logger,
|
||||
userRepositoryFunc Factory[repository.UserRepository],
|
||||
noteRepositoryFunc Factory[repository.NoteRepository],
|
||||
followRepositoryFunc Factory[repository.FollowRepository],
|
||||
instanceMetadataRepositoryFunc Factory[repository.InstanceMetadataRepository],
|
||||
) *ManagerImpl {
|
||||
return &ManagerImpl{
|
||||
users: userRepository,
|
||||
notes: noteRepository,
|
||||
follows: followRepository,
|
||||
users: userRepositoryFunc(db, log.WithName("users"), telemetry),
|
||||
notes: noteRepositoryFunc(db, log.WithName("notes"), telemetry),
|
||||
follows: followRepositoryFunc(db, log.WithName("follows"), telemetry),
|
||||
instanceMetadata: instanceMetadataRepositoryFunc(db, log.WithName("instanceMetadata"), telemetry),
|
||||
|
||||
uRFactory: userRepositoryFunc,
|
||||
nRFactory: noteRepositoryFunc,
|
||||
fRFactory: followRepositoryFunc,
|
||||
uRFactory: userRepositoryFunc,
|
||||
nRFactory: noteRepositoryFunc,
|
||||
fRFactory: followRepositoryFunc,
|
||||
imRFactory: instanceMetadataRepositoryFunc,
|
||||
|
||||
db: db,
|
||||
log: log,
|
||||
|
|
@ -49,11 +55,17 @@ func NewManagerImpl(db *ent.Client, telemetry *unitel.Telemetry, log logr.Logger
|
|||
}
|
||||
|
||||
func (i *ManagerImpl) withDB(db *ent.Client) *ManagerImpl {
|
||||
return NewManagerImpl(db, i.telemetry, i.log, i.uRFactory, i.nRFactory, i.fRFactory)
|
||||
return NewManagerImpl(
|
||||
db, i.telemetry, i.log,
|
||||
i.uRFactory,
|
||||
i.nRFactory,
|
||||
i.fRFactory,
|
||||
i.imRFactory,
|
||||
)
|
||||
}
|
||||
|
||||
func (i *ManagerImpl) Atomic(ctx context.Context, fn func(ctx context.Context, tx repository.Manager) error) error {
|
||||
s := i.telemetry.StartSpan(ctx, "function", "repository/repo_impls.ManagerImpl.Atomic")
|
||||
s := i.telemetry.StartSpan(ctx, "function", "repo_impls/ManagerImpl.Atomic")
|
||||
defer s.End()
|
||||
ctx = s.Context()
|
||||
|
||||
|
|
@ -88,3 +100,7 @@ func (i *ManagerImpl) Notes() repository.NoteRepository {
|
|||
func (i *ManagerImpl) Follows() repository.FollowRepository {
|
||||
return i.follows
|
||||
}
|
||||
|
||||
func (i *ManagerImpl) InstanceMetadata() repository.InstanceMetadataRepository {
|
||||
return i.instanceMetadata
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ func NewNoteRepositoryImpl(db *ent.Client, log logr.Logger, telemetry *unitel.Te
|
|||
}
|
||||
|
||||
func (i *NoteRepositoryImpl) NewNote(ctx context.Context, author *entity.User, content string, mentions []*entity.User) (*entity.Note, error) {
|
||||
s := i.telemetry.StartSpan(ctx, "function", "repository/repo_impls.NoteRepositoryImpl.NewNote")
|
||||
s := i.telemetry.StartSpan(ctx, "function", "repo_impls/NoteRepositoryImpl.NewNote")
|
||||
defer s.End()
|
||||
ctx = s.Context()
|
||||
|
||||
|
|
@ -63,7 +63,7 @@ func (i *NoteRepositoryImpl) NewNote(ctx context.Context, author *entity.User, c
|
|||
}
|
||||
|
||||
func (i *NoteRepositoryImpl) ImportLysandNote(ctx context.Context, lNote *lysand.Note) (*entity.Note, error) {
|
||||
s := i.telemetry.StartSpan(ctx, "function", "repository/repo_impls.NoteRepositoryImpl.ImportLysandNote")
|
||||
s := i.telemetry.StartSpan(ctx, "function", "repo_impls/NoteRepositoryImpl.ImportLysandNote")
|
||||
defer s.End()
|
||||
ctx = s.Context()
|
||||
|
||||
|
|
@ -91,7 +91,7 @@ func (i *NoteRepositoryImpl) ImportLysandNote(ctx context.Context, lNote *lysand
|
|||
}
|
||||
|
||||
func (i *NoteRepositoryImpl) GetByID(ctx context.Context, id uuid.UUID) (*entity.Note, error) {
|
||||
s := i.telemetry.StartSpan(ctx, "function", "repository/repo_impls.NoteRepositoryImpl.LookupByIDOrUsername")
|
||||
s := i.telemetry.StartSpan(ctx, "function", "repo_impls/NoteRepositoryImpl.LookupByIDOrUsername")
|
||||
defer s.End()
|
||||
ctx = s.Context()
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import (
|
|||
"context"
|
||||
"crypto/ed25519"
|
||||
"errors"
|
||||
"github.com/lysand-org/versia-go/config"
|
||||
"github.com/lysand-org/versia-go/internal/repository"
|
||||
"github.com/lysand-org/versia-go/internal/service"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
|
|
@ -45,7 +46,7 @@ func NewUserRepositoryImpl(federationService service.FederationService, db *ent.
|
|||
}
|
||||
|
||||
func (i *UserRepositoryImpl) NewUser(ctx context.Context, username, password string, priv ed25519.PrivateKey, pub ed25519.PublicKey) (*entity.User, error) {
|
||||
s := i.telemetry.StartSpan(ctx, "function", "repository/repo_impls.UserRepositoryImpl.NewUser")
|
||||
s := i.telemetry.StartSpan(ctx, "function", "repo_impls/UserRepositoryImpl.NewUser")
|
||||
defer s.End()
|
||||
ctx = s.Context()
|
||||
|
||||
|
|
@ -62,8 +63,10 @@ func (i *UserRepositoryImpl) NewUser(ctx context.Context, username, password str
|
|||
SetURI(utils.UserAPIURL(uid).String()).
|
||||
SetUsername(username).
|
||||
SetPasswordHash(pwHash).
|
||||
SetPublicKey(pub).
|
||||
SetPrivateKey(priv).
|
||||
SetPublicKey(pub).
|
||||
SetPublicKeyAlgorithm("ed25519").
|
||||
SetPublicKeyActor(utils.UserAPIURL(uid).String()).
|
||||
SetInbox(utils.UserInboxAPIURL(uid).String()).
|
||||
SetOutbox(utils.UserOutboxAPIURL(uid).String()).
|
||||
SetFeatured(utils.UserFeaturedAPIURL(uid).String()).
|
||||
|
|
@ -82,7 +85,7 @@ func (i *UserRepositoryImpl) NewUser(ctx context.Context, username, password str
|
|||
}
|
||||
|
||||
func (i *UserRepositoryImpl) ImportLysandUserByURI(ctx context.Context, uri *lysand.URL) (*entity.User, error) {
|
||||
s := i.telemetry.StartSpan(ctx, "function", "repository/repo_impls.UserRepositoryImpl.ImportLysandUserByURI")
|
||||
s := i.telemetry.StartSpan(ctx, "function", "repo_impls/UserRepositoryImpl.ImportLysandUserByURI")
|
||||
defer s.End()
|
||||
ctx = s.Context()
|
||||
|
||||
|
|
@ -99,7 +102,9 @@ func (i *UserRepositoryImpl) ImportLysandUserByURI(ctx context.Context, uri *lys
|
|||
SetUsername(lUser.Username).
|
||||
SetNillableDisplayName(lUser.DisplayName).
|
||||
SetBiography(lUser.Bio.String()).
|
||||
SetPublicKey(lUser.PublicKey.PublicKey.ToStd()).
|
||||
SetPublicKey(lUser.PublicKey.RawKey).
|
||||
SetPublicKeyAlgorithm(lUser.PublicKey.Algorithm).
|
||||
SetPublicKeyActor(lUser.PublicKey.Actor.String()).
|
||||
SetIndexable(lUser.Indexable).
|
||||
SetFields(lUser.Fields).
|
||||
SetExtensions(lUser.Extensions).
|
||||
|
|
@ -127,11 +132,66 @@ func (i *UserRepositoryImpl) ImportLysandUserByURI(ctx context.Context, uri *lys
|
|||
return entity.NewUser(u)
|
||||
}
|
||||
|
||||
func (i *UserRepositoryImpl) Resolve(ctx context.Context, uri *lysand.URL) (*entity.User, error) {
|
||||
s := i.telemetry.StartSpan(ctx, "function", "repository/repo_impls.UserRepositoryImpl.Resolve")
|
||||
func (i *UserRepositoryImpl) Discover(ctx context.Context, domain, username string) (*entity.User, error) {
|
||||
s := i.telemetry.StartSpan(ctx, "function", "svc_impls/UserServiceImpl.Search").
|
||||
AddAttribute("username", username).
|
||||
AddAttribute("domain", domain)
|
||||
defer s.End()
|
||||
ctx = s.Context()
|
||||
|
||||
l := i.log.WithValues("domain", domain, "username", username)
|
||||
|
||||
// TODO: This *could* go wrong
|
||||
if domain != config.C.Host {
|
||||
l.V(2).Info("Discovering instance")
|
||||
|
||||
im, err := i.federationService.DiscoverInstance(ctx, domain)
|
||||
if err != nil {
|
||||
l.Error(err, "Failed to discover instance")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
l = l.WithValues("host", im.Host)
|
||||
|
||||
l.V(2).Info("Discovering user")
|
||||
|
||||
wf, err := i.federationService.DiscoverUser(ctx, im.Host, username)
|
||||
if err != nil {
|
||||
l.Error(err, "Failed to discover user")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
l.V(2).Info("Found remote user", "userURI", wf.URI)
|
||||
|
||||
u, err := i.Resolve(ctx, lysand.URLFromStd(wf.URI))
|
||||
if err != nil {
|
||||
l.Error(err, "Failed to resolve user")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return u, nil
|
||||
}
|
||||
|
||||
l.V(2).Info("Finding local user")
|
||||
|
||||
u, err := i.GetLocalByUsername(ctx, username)
|
||||
if err != nil {
|
||||
l.Error(err, "Failed to find local user", "username", username)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
l.V(2).Info("Found local user", "userURI", u.URI)
|
||||
|
||||
return u, nil
|
||||
}
|
||||
|
||||
func (i *UserRepositoryImpl) Resolve(ctx context.Context, uri *lysand.URL) (*entity.User, error) {
|
||||
s := i.telemetry.StartSpan(ctx, "function", "repo_impls/UserRepositoryImpl.Resolve")
|
||||
defer s.End()
|
||||
ctx = s.Context()
|
||||
|
||||
l := i.log.WithValues("uri", uri)
|
||||
|
||||
u, err := i.LookupByURI(ctx, uri)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
@ -139,24 +199,24 @@ func (i *UserRepositoryImpl) Resolve(ctx context.Context, uri *lysand.URL) (*ent
|
|||
|
||||
// check if the user is already imported
|
||||
if u == nil {
|
||||
i.log.V(2).Info("User not found in DB", "uri", uri)
|
||||
l.V(2).Info("User not found in DB")
|
||||
|
||||
u, err := i.ImportLysandUserByURI(ctx, uri)
|
||||
if err != nil {
|
||||
i.log.Error(err, "Failed to import user", "uri", uri)
|
||||
l.Error(err, "Failed to import user")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return u, nil
|
||||
}
|
||||
|
||||
i.log.V(2).Info("User found in DB", "uri", uri)
|
||||
l.V(2).Info("User found in DB")
|
||||
|
||||
return u, nil
|
||||
}
|
||||
|
||||
func (i *UserRepositoryImpl) ResolveMultiple(ctx context.Context, uris []lysand.URL) ([]*entity.User, error) {
|
||||
s := i.telemetry.StartSpan(ctx, "function", "repository/repo_impls.UserRepositoryImpl.ResolveMultiple")
|
||||
s := i.telemetry.StartSpan(ctx, "function", "repo_impls/UserRepositoryImpl.ResolveMultiple")
|
||||
defer s.End()
|
||||
ctx = s.Context()
|
||||
|
||||
|
|
@ -168,25 +228,27 @@ func (i *UserRepositoryImpl) ResolveMultiple(ctx context.Context, uris []lysand.
|
|||
// TODO: Refactor to use async imports using a work queue
|
||||
outer:
|
||||
for _, uri := range uris {
|
||||
l := i.log.WithValues("uri", uri)
|
||||
|
||||
// check if the user is already imported
|
||||
for _, u := range us {
|
||||
if uri.String() == u.URI.String() {
|
||||
i.log.V(2).Info("User found in DB", "uri", uri)
|
||||
l.V(2).Info("User found in DB")
|
||||
|
||||
continue outer
|
||||
}
|
||||
}
|
||||
|
||||
i.log.V(2).Info("User not found in DB", "uri", uri)
|
||||
l.V(2).Info("User not found in DB")
|
||||
|
||||
importedUser, err := i.ImportLysandUserByURI(ctx, &uri)
|
||||
if err != nil {
|
||||
i.log.Error(err, "Failed to import user", "uri", uri)
|
||||
l.Error(err, "Failed to import user")
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
i.log.V(2).Info("Imported user", "uri", uri)
|
||||
l.V(2).Info("Imported user")
|
||||
|
||||
us = append(us, importedUser)
|
||||
}
|
||||
|
|
@ -195,10 +257,12 @@ outer:
|
|||
}
|
||||
|
||||
func (i *UserRepositoryImpl) GetByID(ctx context.Context, uid uuid.UUID) (*entity.User, error) {
|
||||
s := i.telemetry.StartSpan(ctx, "function", "repository/repo_impls.UserRepositoryImpl.GetByID")
|
||||
s := i.telemetry.StartSpan(ctx, "function", "repo_impls/UserRepositoryImpl.GetByID")
|
||||
defer s.End()
|
||||
ctx = s.Context()
|
||||
|
||||
l := i.log.WithValues("id", uid)
|
||||
|
||||
u, err := i.db.User.Query().
|
||||
Where(user.IDEQ(uid)).
|
||||
WithAvatarImage().
|
||||
|
|
@ -206,25 +270,27 @@ func (i *UserRepositoryImpl) GetByID(ctx context.Context, uid uuid.UUID) (*entit
|
|||
Only(ctx)
|
||||
if err != nil {
|
||||
if !ent.IsNotFound(err) {
|
||||
i.log.Error(err, "Failed to query user", "id", uid)
|
||||
l.Error(err, "Failed to query user")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
i.log.V(2).Info("User not found in DB", "id", uid)
|
||||
l.V(2).Info("User not found in DB")
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
i.log.V(2).Info("User found in DB", "id", uid)
|
||||
l.V(2).Info("User found in DB")
|
||||
|
||||
return entity.NewUser(u)
|
||||
}
|
||||
|
||||
func (i *UserRepositoryImpl) GetLocalByID(ctx context.Context, uid uuid.UUID) (*entity.User, error) {
|
||||
s := i.telemetry.StartSpan(ctx, "function", "repository/repo_impls.UserRepositoryImpl.GetLocalByID")
|
||||
s := i.telemetry.StartSpan(ctx, "function", "repo_impls/UserRepositoryImpl.GetLocalByID")
|
||||
defer s.End()
|
||||
ctx = s.Context()
|
||||
|
||||
l := i.log.WithValues("id", uid)
|
||||
|
||||
u, err := i.db.User.Query().
|
||||
Where(user.And(user.ID(uid), user.IsRemote(false))).
|
||||
WithAvatarImage().
|
||||
|
|
@ -232,47 +298,77 @@ func (i *UserRepositoryImpl) GetLocalByID(ctx context.Context, uid uuid.UUID) (*
|
|||
Only(ctx)
|
||||
if err != nil {
|
||||
if !ent.IsNotFound(err) {
|
||||
i.log.Error(err, "Failed to query local user", "id", uid)
|
||||
l.Error(err, "Failed to query local user")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
i.log.V(2).Info("Local user not found in DB", "id", uid)
|
||||
l.V(2).Info("Local user not found in DB")
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
i.log.V(2).Info("Local user found in DB", "id", uid)
|
||||
l.V(2).Info("Local user found in DB", "uri", u.URI)
|
||||
|
||||
return entity.NewUser(u)
|
||||
}
|
||||
|
||||
func (i *UserRepositoryImpl) GetLocalByUsername(ctx context.Context, username string) (*entity.User, error) {
|
||||
s := i.telemetry.StartSpan(ctx, "function", "repo_impls/UserRepositoryImpl.GetLocalByUsername")
|
||||
defer s.End()
|
||||
ctx = s.Context()
|
||||
|
||||
l := i.log.WithValues("username", username)
|
||||
|
||||
u, err := i.db.User.Query().
|
||||
Where(user.And(user.Username(username), user.IsRemote(false))).
|
||||
WithAvatarImage().
|
||||
WithHeaderImage().
|
||||
Only(ctx)
|
||||
if err != nil {
|
||||
if !ent.IsNotFound(err) {
|
||||
l.Error(err, "Failed to query local user")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
l.V(2).Info("Local user not found in DB")
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
l.V(2).Info("Local user found in DB", "uri", u.URI)
|
||||
|
||||
return entity.NewUser(u)
|
||||
}
|
||||
|
||||
func (i *UserRepositoryImpl) LookupByURI(ctx context.Context, uri *lysand.URL) (*entity.User, error) {
|
||||
s := i.telemetry.StartSpan(ctx, "function", "repository/repo_impls.UserRepositoryImpl.LookupByURI")
|
||||
s := i.telemetry.StartSpan(ctx, "function", "repo_impls/UserRepositoryImpl.LookupByURI")
|
||||
defer s.End()
|
||||
ctx = s.Context()
|
||||
|
||||
l := i.log.WithValues("uri", uri)
|
||||
|
||||
// check if the user is already imported
|
||||
u, err := i.db.User.Query().
|
||||
Where(user.URI(uri.String())).
|
||||
Only(ctx)
|
||||
if err != nil {
|
||||
if !ent.IsNotFound(err) {
|
||||
i.log.Error(err, "Failed to query user", "uri", uri)
|
||||
l.Error(err, "Failed to query user")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
i.log.V(2).Info("User not found in DB", "uri", uri)
|
||||
l.V(2).Info("User not found in DB")
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
i.log.V(2).Info("User found in DB", "uri", uri)
|
||||
l.V(2).Info("User found in DB")
|
||||
|
||||
return entity.NewUser(u)
|
||||
}
|
||||
|
||||
func (i *UserRepositoryImpl) LookupByURIs(ctx context.Context, uris []lysand.URL) ([]*entity.User, error) {
|
||||
s := i.telemetry.StartSpan(ctx, "function", "repository/repo_impls.UserRepositoryImpl.LookupByURIs")
|
||||
s := i.telemetry.StartSpan(ctx, "function", "repo_impls/UserRepositoryImpl.LookupByURIs")
|
||||
defer s.End()
|
||||
ctx = s.Context()
|
||||
|
||||
|
|
@ -292,7 +388,7 @@ func (i *UserRepositoryImpl) LookupByURIs(ctx context.Context, uris []lysand.URL
|
|||
}
|
||||
|
||||
func (i *UserRepositoryImpl) LookupByIDOrUsername(ctx context.Context, idOrUsername string) (*entity.User, error) {
|
||||
s := i.telemetry.StartSpan(ctx, "function", "repository/repo_impls.UserRepositoryImpl.LookupByIDOrUsername")
|
||||
s := i.telemetry.StartSpan(ctx, "function", "repo_impls/UserRepositoryImpl.LookupByIDOrUsername")
|
||||
defer s.End()
|
||||
ctx = s.Context()
|
||||
|
||||
|
|
@ -303,6 +399,8 @@ func (i *UserRepositoryImpl) LookupByIDOrUsername(ctx context.Context, idOrUsern
|
|||
preds = append(preds, user.UsernameEQ(idOrUsername))
|
||||
}
|
||||
|
||||
l := i.log.WithValues("idOrUsername", idOrUsername)
|
||||
|
||||
u, err := i.db.User.Query().
|
||||
Where(preds...).
|
||||
WithAvatarImage().
|
||||
|
|
@ -310,16 +408,16 @@ func (i *UserRepositoryImpl) LookupByIDOrUsername(ctx context.Context, idOrUsern
|
|||
Only(ctx)
|
||||
if err != nil {
|
||||
if !ent.IsNotFound(err) {
|
||||
i.log.Error(err, "Failed to query user", "idOrUsername", idOrUsername)
|
||||
l.Error(err, "Failed to query user")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
i.log.V(2).Info("User not found in DB", "idOrUsername", idOrUsername)
|
||||
l.V(2).Info("User not found in DB")
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
i.log.V(2).Info("User found in DB", "idOrUsername", idOrUsername, "id", u.ID)
|
||||
l.V(2).Info("User found in DB", "id", u.ID)
|
||||
|
||||
return entity.NewUser(u)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,9 @@ type UserRepository interface {
|
|||
|
||||
GetByID(ctx context.Context, id uuid.UUID) (*entity.User, error)
|
||||
GetLocalByID(ctx context.Context, id uuid.UUID) (*entity.User, error)
|
||||
GetLocalByUsername(ctx context.Context, username string) (*entity.User, error)
|
||||
|
||||
Discover(ctx context.Context, host, username string) (*entity.User, error)
|
||||
|
||||
Resolve(ctx context.Context, uri *lysand.URL) (*entity.User, error)
|
||||
ResolveMultiple(ctx context.Context, uris []lysand.URL) ([]*entity.User, error)
|
||||
|
|
@ -40,10 +43,16 @@ type NoteRepository interface {
|
|||
GetByID(ctx context.Context, idOrUsername uuid.UUID) (*entity.Note, error)
|
||||
}
|
||||
|
||||
type InstanceMetadataRepository interface {
|
||||
GetByHost(ctx context.Context, host string) (*entity.InstanceMetadata, error)
|
||||
ImportFromLysandByURI(ctx context.Context, uri *lysand.URL) (*entity.InstanceMetadata, error)
|
||||
}
|
||||
|
||||
type Manager interface {
|
||||
Atomic(ctx context.Context, fn func(ctx context.Context, tx Manager) error) error
|
||||
|
||||
Users() UserRepository
|
||||
Notes() NoteRepository
|
||||
Follows() FollowRepository
|
||||
InstanceMetadata() InstanceMetadataRepository
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package service
|
|||
|
||||
import (
|
||||
"context"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/lysand-org/versia-go/internal/repository"
|
||||
|
||||
"github.com/google/uuid"
|
||||
|
|
@ -19,11 +20,16 @@ type UserService interface {
|
|||
GetUserByID(ctx context.Context, id uuid.UUID) (*entity.User, error)
|
||||
|
||||
GetWebfingerForUser(ctx context.Context, userID string) (*webfinger.User, error)
|
||||
|
||||
Search(ctx context.Context, req api_schema.SearchUserRequest) (*entity.User, error)
|
||||
}
|
||||
|
||||
type FederationService interface {
|
||||
SendToInbox(ctx context.Context, author *entity.User, target *entity.User, object any) ([]byte, error)
|
||||
GetUser(ctx context.Context, uri *lysand.URL) (*lysand.User, error)
|
||||
|
||||
DiscoverUser(ctx context.Context, baseURL, username string) (*webfinger.User, error)
|
||||
DiscoverInstance(ctx context.Context, baseURL string) (*lysand.InstanceMetadata, error)
|
||||
}
|
||||
|
||||
type InboxService interface {
|
||||
|
|
@ -44,6 +50,14 @@ type FollowService interface {
|
|||
ImportLysandFollow(ctx context.Context, lFollow *lysand.Follow) (*entity.Follow, error)
|
||||
}
|
||||
|
||||
type InstanceMetadataService interface {
|
||||
Ours(ctx context.Context) (*entity.InstanceMetadata, error)
|
||||
}
|
||||
|
||||
type TaskService interface {
|
||||
ScheduleTask(ctx context.Context, type_ string, data any) error
|
||||
}
|
||||
|
||||
type RequestSigner interface {
|
||||
Sign(c *fiber.Ctx, signer lysand.Signer, body any) error
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,29 +18,31 @@ import (
|
|||
var _ service.InboxService = (*InboxServiceImpl)(nil)
|
||||
|
||||
type InboxServiceImpl struct {
|
||||
repositories repository.Manager
|
||||
|
||||
federationService service.FederationService
|
||||
|
||||
repositories repository.Manager
|
||||
|
||||
telemetry *unitel.Telemetry
|
||||
log logr.Logger
|
||||
}
|
||||
|
||||
func NewInboxService(repositories repository.Manager, federationService service.FederationService, telemetry *unitel.Telemetry, log logr.Logger) *InboxServiceImpl {
|
||||
func NewInboxService(federationService service.FederationService, repositories repository.Manager, telemetry *unitel.Telemetry, log logr.Logger) *InboxServiceImpl {
|
||||
return &InboxServiceImpl{
|
||||
repositories: repositories,
|
||||
federationService: federationService,
|
||||
telemetry: telemetry,
|
||||
log: log,
|
||||
|
||||
repositories: repositories,
|
||||
|
||||
telemetry: telemetry,
|
||||
log: log,
|
||||
}
|
||||
}
|
||||
|
||||
func (i InboxServiceImpl) WithRepositories(repositories repository.Manager) service.InboxService {
|
||||
return NewInboxService(repositories, i.federationService, i.telemetry, i.log)
|
||||
return NewInboxService(i.federationService, repositories, i.telemetry, i.log)
|
||||
}
|
||||
|
||||
func (i InboxServiceImpl) Handle(ctx context.Context, obj any, userId uuid.UUID) error {
|
||||
s := i.telemetry.StartSpan(ctx, "function", "service/svc_impls.InboxServiceImpl.Handle")
|
||||
s := i.telemetry.StartSpan(ctx, "function", "svc_impls/InboxServiceImpl.Handle")
|
||||
defer s.End()
|
||||
ctx = s.Context()
|
||||
|
||||
|
|
@ -87,7 +89,7 @@ func (i InboxServiceImpl) Handle(ctx context.Context, obj any, userId uuid.UUID)
|
|||
}
|
||||
|
||||
func (i InboxServiceImpl) handleFollow(ctx context.Context, o lysand.Follow, u *entity.User) error {
|
||||
s := i.telemetry.StartSpan(ctx, "function", "service/svc_impls.InboxServiceImpl.handleFollow")
|
||||
s := i.telemetry.StartSpan(ctx, "function", "svc_impls/InboxServiceImpl.handleFollow")
|
||||
defer s.End()
|
||||
ctx = s.Context()
|
||||
|
||||
|
|
@ -129,7 +131,7 @@ func (i InboxServiceImpl) handleFollow(ctx context.Context, o lysand.Follow, u *
|
|||
}
|
||||
|
||||
func (i InboxServiceImpl) handleNote(ctx context.Context, o lysand.Note, u *entity.User) error {
|
||||
s := i.telemetry.StartSpan(ctx, "function", "service/svc_impls.InboxServiceImpl.handleNote")
|
||||
s := i.telemetry.StartSpan(ctx, "function", "svc_impls/InboxServiceImpl.handleNote")
|
||||
defer s.End()
|
||||
ctx = s.Context()
|
||||
|
||||
|
|
|
|||
|
|
@ -1,17 +1,34 @@
|
|||
package svc_impls
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"git.devminer.xyz/devminer/unitel"
|
||||
"github.com/go-logr/logr"
|
||||
"github.com/lysand-org/versia-go/internal/entity"
|
||||
"github.com/lysand-org/versia-go/internal/service"
|
||||
"github.com/lysand-org/versia-go/pkg/lysand"
|
||||
versiacrypto "github.com/lysand-org/versia-go/pkg/lysand/crypto"
|
||||
"github.com/lysand-org/versia-go/pkg/protoretry"
|
||||
"github.com/lysand-org/versia-go/pkg/webfinger"
|
||||
"net/http"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
var _ service.FederationService = (*FederationServiceImpl)(nil)
|
||||
var (
|
||||
_ service.FederationService = (*FederationServiceImpl)(nil)
|
||||
|
||||
ErrSignatureValidationFailed = errors.New("signature validation failed")
|
||||
)
|
||||
|
||||
type FederationServiceImpl struct {
|
||||
httpC *protoretry.Client
|
||||
|
||||
federationClient *lysand.FederationClient
|
||||
|
||||
telemetry *unitel.Telemetry
|
||||
|
|
@ -19,38 +36,139 @@ type FederationServiceImpl struct {
|
|||
log logr.Logger
|
||||
}
|
||||
|
||||
func NewFederationServiceImpl(federationClient *lysand.FederationClient, telemetry *unitel.Telemetry, log logr.Logger) *FederationServiceImpl {
|
||||
func NewFederationServiceImpl(httpClient *http.Client, federationClient *lysand.FederationClient, telemetry *unitel.Telemetry, log logr.Logger) *FederationServiceImpl {
|
||||
return &FederationServiceImpl{
|
||||
httpC: protoretry.New(httpClient),
|
||||
federationClient: federationClient,
|
||||
telemetry: telemetry,
|
||||
log: log,
|
||||
}
|
||||
}
|
||||
|
||||
func (i FederationServiceImpl) SendToInbox(ctx context.Context, author *entity.User, target *entity.User, object any) ([]byte, error) {
|
||||
s := i.telemetry.StartSpan(ctx, "function", "service/svc_impls.FederationServiceImpl.SendToInbox")
|
||||
func (i *FederationServiceImpl) GetUser(ctx context.Context, uri *lysand.URL) (*lysand.User, error) {
|
||||
s := i.telemetry.StartSpan(ctx, "function", "svc_impls/FederationServiceImpl.GetUser").
|
||||
AddAttribute("userURI", uri.String())
|
||||
defer s.End()
|
||||
ctx = s.Context()
|
||||
|
||||
response, err := i.federationClient.SendToInbox(ctx, author.Signer, target.ToLysand(), object)
|
||||
body, resp, err := i.httpC.GET(ctx, uri.ToStd())
|
||||
if err != nil {
|
||||
i.log.Error(err, "Failed to send to inbox", "author", author.ID, "target", target.ID)
|
||||
return response, err
|
||||
}
|
||||
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (i FederationServiceImpl) GetUser(ctx context.Context, uri *lysand.URL) (*lysand.User, error) {
|
||||
s := i.telemetry.StartSpan(ctx, "function", "service/svc_impls.FederationServiceImpl.GetUser")
|
||||
defer s.End()
|
||||
ctx = s.Context()
|
||||
|
||||
u, err := i.federationClient.GetUser(ctx, uri.ToStd())
|
||||
if err != nil {
|
||||
i.log.Error(err, "Failed to fetch remote user", "uri", uri)
|
||||
s.SetSimpleStatus(unitel.Error, err.Error())
|
||||
return nil, err
|
||||
}
|
||||
|
||||
u := &lysand.User{}
|
||||
if err := json.Unmarshal(body, u); err != nil {
|
||||
s.SetSimpleStatus(unitel.Error, err.Error())
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fedHeaders, err := lysand.ExtractFederationHeaders(resp.Header)
|
||||
if err != nil {
|
||||
s.SetSimpleStatus(unitel.Error, err.Error())
|
||||
return nil, err
|
||||
}
|
||||
|
||||
v := lysand.Verifier{PublicKey: u.PublicKey.Key.Key}
|
||||
if !v.Verify("GET", uri.ToStd(), body, fedHeaders) {
|
||||
s.SetSimpleStatus(unitel.Error, ErrSignatureValidationFailed.Error())
|
||||
i.log.V(1).Error(ErrSignatureValidationFailed, "signature validation failed", "user", u.URI.String())
|
||||
return nil, ErrSignatureValidationFailed
|
||||
}
|
||||
|
||||
s.SetSimpleStatus(unitel.Ok, "")
|
||||
i.log.V(2).Info("signature verification succeeded", "user", u.URI.String())
|
||||
|
||||
return u, nil
|
||||
}
|
||||
|
||||
func (i *FederationServiceImpl) DiscoverUser(ctx context.Context, baseURL, username string) (*webfinger.User, error) {
|
||||
s := i.telemetry.StartSpan(ctx, "function", "svc_impls/FederationServiceImpl.DiscoverUser").
|
||||
AddAttribute("baseURL", baseURL).
|
||||
AddAttribute("username", username)
|
||||
defer s.End()
|
||||
ctx = s.Context()
|
||||
|
||||
wf, err := webfinger.Discover(i.httpC, ctx, baseURL, username)
|
||||
if err != nil {
|
||||
s.SetSimpleStatus(unitel.Error, err.Error())
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s.SetSimpleStatus(unitel.Ok, "")
|
||||
|
||||
return wf, nil
|
||||
}
|
||||
|
||||
func (i *FederationServiceImpl) DiscoverInstance(ctx context.Context, baseURL string) (*lysand.InstanceMetadata, error) {
|
||||
s := i.telemetry.StartSpan(ctx, "function", "svc_impls/FederationServiceImpl.DiscoverInstance").
|
||||
AddAttribute("baseURL", baseURL)
|
||||
defer s.End()
|
||||
ctx = s.Context()
|
||||
|
||||
body, resp, err := i.httpC.GET(ctx, &url.URL{Scheme: "https", Host: baseURL, Path: "/.well-known/versia"})
|
||||
if err != nil {
|
||||
s.SetSimpleStatus(unitel.Error, err.Error())
|
||||
return nil, err
|
||||
} else if resp.StatusCode >= http.StatusBadRequest {
|
||||
s.SetSimpleStatus(unitel.Error, fmt.Sprintf("unexpected response code: %d", resp.StatusCode))
|
||||
return nil, &lysand.ResponseError{StatusCode: resp.StatusCode, URL: resp.Request.URL}
|
||||
}
|
||||
|
||||
var metadata lysand.InstanceMetadata
|
||||
if err := json.Unmarshal(body, &metadata); err != nil {
|
||||
s.SetSimpleStatus(unitel.Error, err.Error())
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s.SetSimpleStatus(unitel.Ok, "")
|
||||
|
||||
return &metadata, nil
|
||||
}
|
||||
|
||||
func (i *FederationServiceImpl) SendToInbox(ctx context.Context, author *entity.User, user *entity.User, object any) ([]byte, error) {
|
||||
s := i.telemetry.StartSpan(ctx, "function", "svc_impls/FederationServiceImpl.SendToInbox").
|
||||
SetUser(uint64(author.ID.ID()), author.Username, "", "").
|
||||
AddAttribute("author", author.ID).
|
||||
AddAttribute("authorURI", author.URI).
|
||||
AddAttribute("target", user.ID).
|
||||
AddAttribute("targetURI", user.URI)
|
||||
defer s.End()
|
||||
ctx = s.Context()
|
||||
|
||||
uri := user.Inbox.ToStd()
|
||||
|
||||
body, err := json.Marshal(object)
|
||||
if err != nil {
|
||||
s.SetSimpleStatus(unitel.Error, err.Error())
|
||||
return nil, err
|
||||
}
|
||||
|
||||
nonce := make([]byte, 32)
|
||||
if _, err := rand.Read(nonce); err != nil {
|
||||
s.SetSimpleStatus(unitel.Error, err.Error())
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sigData := lysand.NewSignatureData("POST", base64.StdEncoding.EncodeToString(nonce), uri, versiacrypto.SHA256(body))
|
||||
sig := author.Signer.Sign(*sigData)
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "POST", uri.String(), bytes.NewReader(body))
|
||||
if err != nil {
|
||||
s.SetSimpleStatus(unitel.Error, err.Error())
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sig.Inject(req.Header)
|
||||
|
||||
body, _, err = i.httpC.DoReq(req)
|
||||
if err != nil {
|
||||
s.SetSimpleStatus(unitel.Error, err.Error())
|
||||
i.log.Error(err, "Failed to send to inbox", "author", author.URI, "target", user.URI)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s.SetSimpleStatus(unitel.Ok, "")
|
||||
|
||||
return body, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ func NewFollowServiceImpl(federationService service.FederationService, repositor
|
|||
}
|
||||
|
||||
func (i FollowServiceImpl) NewFollow(ctx context.Context, follower, followee *entity.User) (*entity.Follow, error) {
|
||||
s := i.telemetry.StartSpan(ctx, "function", "service/svc_impls.FollowServiceImpl.NewFollow").
|
||||
s := i.telemetry.StartSpan(ctx, "function", "svc_impls/FollowServiceImpl.NewFollow").
|
||||
AddAttribute("follower", follower.URI).
|
||||
AddAttribute("followee", followee.URI)
|
||||
defer s.End()
|
||||
|
|
@ -54,7 +54,7 @@ func (i FollowServiceImpl) NewFollow(ctx context.Context, follower, followee *en
|
|||
}
|
||||
|
||||
func (i FollowServiceImpl) GetFollow(ctx context.Context, id uuid.UUID) (*entity.Follow, error) {
|
||||
s := i.telemetry.StartSpan(ctx, "function", "service/svc_impls.FollowServiceImpl.GetFollow").
|
||||
s := i.telemetry.StartSpan(ctx, "function", "svc_impls/FollowServiceImpl.GetFollow").
|
||||
AddAttribute("followID", id)
|
||||
defer s.End()
|
||||
ctx = s.Context()
|
||||
|
|
@ -70,7 +70,7 @@ func (i FollowServiceImpl) GetFollow(ctx context.Context, id uuid.UUID) (*entity
|
|||
}
|
||||
|
||||
func (i FollowServiceImpl) ImportLysandFollow(ctx context.Context, lFollow *lysand.Follow) (*entity.Follow, error) {
|
||||
s := i.telemetry.StartSpan(ctx, "function", "service/svc_impls.FollowServiceImpl.ImportLysandFollow").
|
||||
s := i.telemetry.StartSpan(ctx, "function", "svc_impls/FollowServiceImpl.ImportLysandFollow").
|
||||
AddAttribute("uri", lFollow.URI.String())
|
||||
defer s.End()
|
||||
ctx = s.Context()
|
||||
|
|
|
|||
52
internal/service/svc_impls/instance_metadata_service_impl.go
Normal file
52
internal/service/svc_impls/instance_metadata_service_impl.go
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
package svc_impls
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/lysand-org/versia-go/config"
|
||||
"github.com/lysand-org/versia-go/ent"
|
||||
"github.com/lysand-org/versia-go/internal/repository"
|
||||
"github.com/lysand-org/versia-go/internal/service"
|
||||
|
||||
"git.devminer.xyz/devminer/unitel"
|
||||
"github.com/go-logr/logr"
|
||||
"github.com/lysand-org/versia-go/internal/entity"
|
||||
)
|
||||
|
||||
var _ service.InstanceMetadataService = (*InstanceMetadataServiceImpl)(nil)
|
||||
|
||||
type InstanceMetadataServiceImpl struct {
|
||||
federationService service.FederationService
|
||||
|
||||
repositories repository.Manager
|
||||
|
||||
telemetry *unitel.Telemetry
|
||||
log logr.Logger
|
||||
}
|
||||
|
||||
func NewInstanceMetadataServiceImpl(federationService service.FederationService, repositories repository.Manager, telemetry *unitel.Telemetry, log logr.Logger) *InstanceMetadataServiceImpl {
|
||||
return &InstanceMetadataServiceImpl{
|
||||
federationService: federationService,
|
||||
|
||||
repositories: repositories,
|
||||
|
||||
telemetry: telemetry,
|
||||
log: log,
|
||||
}
|
||||
}
|
||||
|
||||
func (i InstanceMetadataServiceImpl) Ours(ctx context.Context) (*entity.InstanceMetadata, error) {
|
||||
s := i.telemetry.StartSpan(ctx, "function", "svc_impls/InstanceMetadataServiceImpl.Ours")
|
||||
defer s.End()
|
||||
ctx = s.Context()
|
||||
|
||||
m, err := i.repositories.InstanceMetadata().GetByHost(ctx, config.C.Host)
|
||||
if err != nil {
|
||||
if ent.IsNotFound(err) {
|
||||
panic("could not find our own instance metadata")
|
||||
}
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return m, nil
|
||||
}
|
||||
|
|
@ -39,7 +39,7 @@ func NewNoteServiceImpl(federationService service.FederationService, taskService
|
|||
}
|
||||
|
||||
func (i NoteServiceImpl) CreateNote(ctx context.Context, req api_schema.CreateNoteRequest) (*entity.Note, error) {
|
||||
s := i.telemetry.StartSpan(ctx, "function", "service/svc_impls.NoteServiceImpl.CreateNote")
|
||||
s := i.telemetry.StartSpan(ctx, "function", "svc_impls/NoteServiceImpl.CreateNote")
|
||||
defer s.End()
|
||||
ctx = s.Context()
|
||||
|
||||
|
|
@ -82,7 +82,7 @@ func (i NoteServiceImpl) CreateNote(ctx context.Context, req api_schema.CreateNo
|
|||
}
|
||||
|
||||
func (i NoteServiceImpl) GetNote(ctx context.Context, id uuid.UUID) (*entity.Note, error) {
|
||||
s := i.telemetry.StartSpan(ctx, "function", "service/svc_impls.NoteServiceImpl.GetUserByID")
|
||||
s := i.telemetry.StartSpan(ctx, "function", "svc_impls/NoteServiceImpl.GetUserByID")
|
||||
defer s.End()
|
||||
ctx = s.Context()
|
||||
|
||||
|
|
@ -90,7 +90,7 @@ func (i NoteServiceImpl) GetNote(ctx context.Context, id uuid.UUID) (*entity.Not
|
|||
}
|
||||
|
||||
func (i NoteServiceImpl) ImportLysandNote(ctx context.Context, lNote *lysand.Note) (*entity.Note, error) {
|
||||
s := i.telemetry.StartSpan(ctx, "function", "service/svc_impls.NoteServiceImpl.ImportLysandNote")
|
||||
s := i.telemetry.StartSpan(ctx, "function", "svc_impls/NoteServiceImpl.ImportLysandNote")
|
||||
defer s.End()
|
||||
ctx = s.Context()
|
||||
|
||||
|
|
|
|||
63
internal/service/svc_impls/request_signer_impl.go
Normal file
63
internal/service/svc_impls/request_signer_impl.go
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
package svc_impls
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"git.devminer.xyz/devminer/unitel"
|
||||
"github.com/go-logr/logr"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/lysand-org/versia-go/internal/service"
|
||||
"github.com/lysand-org/versia-go/pkg/lysand"
|
||||
versiacrypto "github.com/lysand-org/versia-go/pkg/lysand/crypto"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
var _ service.RequestSigner = (*RequestSignerImpl)(nil)
|
||||
|
||||
type RequestSignerImpl struct {
|
||||
telemetry *unitel.Telemetry
|
||||
|
||||
log logr.Logger
|
||||
}
|
||||
|
||||
func NewRequestSignerImpl(telemetry *unitel.Telemetry, log logr.Logger) *RequestSignerImpl {
|
||||
return &RequestSignerImpl{
|
||||
telemetry: telemetry,
|
||||
log: log,
|
||||
}
|
||||
}
|
||||
|
||||
func (i *RequestSignerImpl) Sign(c *fiber.Ctx, signer lysand.Signer, body any) error {
|
||||
s := i.telemetry.StartSpan(c.UserContext(), "function", "svc_impls/RequestSignerImpl.Sign")
|
||||
defer s.End()
|
||||
|
||||
j, err := json.Marshal(body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
rawNonce := make([]byte, 64)
|
||||
if _, err := rand.Read(rawNonce); err != nil {
|
||||
return err
|
||||
}
|
||||
nonce := base64.StdEncoding.EncodeToString(rawNonce)
|
||||
|
||||
uri, err := url.ParseRequestURI(string(c.Request().RequestURI()))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
digest := versiacrypto.SHA256(j)
|
||||
|
||||
d := lysand.NewSignatureData(c.Method(), nonce, uri, digest)
|
||||
|
||||
signed := signer.Sign(*d)
|
||||
for k, v := range signed.Headers() {
|
||||
c.Set(k, v)
|
||||
}
|
||||
|
||||
i.log.V(2).Info("signed response", "digest", base64.StdEncoding.EncodeToString(digest), "nonce", nonce)
|
||||
|
||||
return c.Send(j)
|
||||
}
|
||||
|
|
@ -28,7 +28,7 @@ func NewTaskServiceImpl(client *taskqueue.Client, telemetry *unitel.Telemetry, l
|
|||
}
|
||||
|
||||
func (i TaskServiceImpl) ScheduleTask(ctx context.Context, type_ string, data any) error {
|
||||
s := i.telemetry.StartSpan(ctx, "function", "service/svc_impls.TaskServiceImpl.ScheduleTask")
|
||||
s := i.telemetry.StartSpan(ctx, "function", "svc_impls/TaskServiceImpl.ScheduleTask")
|
||||
defer s.End()
|
||||
ctx = s.Context()
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import (
|
|||
"context"
|
||||
"crypto/ed25519"
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
"github.com/lysand-org/versia-go/internal/api_schema"
|
||||
"github.com/lysand-org/versia-go/internal/repository"
|
||||
"github.com/lysand-org/versia-go/internal/service"
|
||||
"net/url"
|
||||
|
|
@ -45,7 +45,7 @@ func (i UserServiceImpl) WithRepositories(repositories repository.Manager) servi
|
|||
}
|
||||
|
||||
func (i UserServiceImpl) NewUser(ctx context.Context, username, password string) (*entity.User, error) {
|
||||
s := i.telemetry.StartSpan(ctx, "function", "service/svc_impls.UserServiceImpl.NewUser")
|
||||
s := i.telemetry.StartSpan(ctx, "function", "svc_impls/UserServiceImpl.NewUser")
|
||||
defer s.End()
|
||||
ctx = s.Context()
|
||||
|
||||
|
|
@ -73,7 +73,7 @@ func (i UserServiceImpl) NewUser(ctx context.Context, username, password string)
|
|||
}
|
||||
|
||||
func (i UserServiceImpl) GetUserByID(ctx context.Context, id uuid.UUID) (*entity.User, error) {
|
||||
s := i.telemetry.StartSpan(ctx, "function", "service/svc_impls.UserServiceImpl.GetUserByID")
|
||||
s := i.telemetry.StartSpan(ctx, "function", "svc_impls/UserServiceImpl.GetUserByID")
|
||||
defer s.End()
|
||||
ctx = s.Context()
|
||||
|
||||
|
|
@ -81,7 +81,7 @@ func (i UserServiceImpl) GetUserByID(ctx context.Context, id uuid.UUID) (*entity
|
|||
}
|
||||
|
||||
func (i UserServiceImpl) GetWebfingerForUser(ctx context.Context, userID string) (*webfinger.User, error) {
|
||||
s := i.telemetry.StartSpan(ctx, "function", "service/svc_impls.UserServiceImpl.GetWebfingerForUser")
|
||||
s := i.telemetry.StartSpan(ctx, "function", "svc_impls/UserServiceImpl.GetWebfingerForUser")
|
||||
defer s.End()
|
||||
ctx = s.Context()
|
||||
|
||||
|
|
@ -90,7 +90,7 @@ func (i UserServiceImpl) GetWebfingerForUser(ctx context.Context, userID string)
|
|||
return nil, err
|
||||
}
|
||||
if u == nil {
|
||||
return nil, fmt.Errorf("user not found")
|
||||
return nil, api_schema.ErrUserNotFound
|
||||
}
|
||||
|
||||
wf := &webfinger.User{
|
||||
|
|
@ -120,3 +120,25 @@ func (i UserServiceImpl) GetWebfingerForUser(ctx context.Context, userID string)
|
|||
|
||||
return wf, nil
|
||||
}
|
||||
|
||||
func (i UserServiceImpl) Search(ctx context.Context, req api_schema.SearchUserRequest) (u *entity.User, err error) {
|
||||
s := i.telemetry.StartSpan(ctx, "function", "svc_impls/UserServiceImpl.Search").
|
||||
AddAttribute("username", req.Username)
|
||||
defer s.End()
|
||||
ctx = s.Context()
|
||||
|
||||
domain := ""
|
||||
if req.Domain != nil {
|
||||
domain = *req.Domain
|
||||
}
|
||||
|
||||
err = i.repositories.Atomic(ctx, func(ctx context.Context, tx repository.Manager) error {
|
||||
var err error
|
||||
if u, err = i.repositories.Users().Discover(ctx, domain, req.Username); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
return
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,6 +24,10 @@ func (t *Handler) FederateNote(ctx context.Context, data FederateNoteData) error
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if n == nil {
|
||||
t.log.V(-1).Info("Could not find note", "id", data.NoteID)
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, uu := range n.Mentions {
|
||||
if !uu.IsRemote {
|
||||
|
|
|
|||
32
internal/utils/fiber.go
Normal file
32
internal/utils/fiber.go
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
package utils
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/valyala/fasthttp/fasthttpadaptor"
|
||||
"io"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func ConvertToStdRequest(c *fiber.Ctx) (*http.Request, error) {
|
||||
stdReq := &http.Request{}
|
||||
if err := fasthttpadaptor.ConvertRequest(c.Context(), stdReq, true); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return stdReq, nil
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
|
@ -73,3 +73,23 @@ func NoteAPIURL(uuid uuid.UUID) *lysand.URL {
|
|||
newPath := &url.URL{Path: fmt.Sprintf("/api/notes/%s/", uuid.String())}
|
||||
return lysand.URLFromStd(config.C.PublicAddress.ResolveReference(newPath))
|
||||
}
|
||||
|
||||
func InstanceMetadataAPIURL() *lysand.URL {
|
||||
newPath := &url.URL{Path: "/.well-known/versia/"}
|
||||
return lysand.URLFromStd(config.C.PublicAddress.ResolveReference(newPath))
|
||||
}
|
||||
|
||||
func InstanceMetadataAdminsAPIURL() *lysand.URL {
|
||||
newPath := &url.URL{Path: "/.well-known/versia/admins/"}
|
||||
return lysand.URLFromStd(config.C.PublicAddress.ResolveReference(newPath))
|
||||
}
|
||||
|
||||
func InstanceMetadataModeratorsAPIURL() *lysand.URL {
|
||||
newPath := &url.URL{Path: "/.well-known/versia/moderators/"}
|
||||
return lysand.URLFromStd(config.C.PublicAddress.ResolveReference(newPath))
|
||||
}
|
||||
|
||||
func SharedInboxAPIURL() *lysand.URL {
|
||||
newPath := &url.URL{Path: "/api/inbox/"}
|
||||
return lysand.URLFromStd(config.C.PublicAddress.ResolveReference(newPath))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,8 +2,10 @@ package val_impls
|
|||
|
||||
import (
|
||||
"errors"
|
||||
"github.com/lysand-org/versia-go/ent/schema"
|
||||
"github.com/lysand-org/versia-go/internal/validators"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/go-logr/logr"
|
||||
|
|
@ -11,11 +13,20 @@ import (
|
|||
universal_translator "github.com/go-playground/universal-translator"
|
||||
"github.com/go-playground/validator/v10"
|
||||
en_translations "github.com/go-playground/validator/v10/translations/en"
|
||||
"github.com/lysand-org/versia-go/ent/schema"
|
||||
"github.com/lysand-org/versia-go/internal/api_schema"
|
||||
)
|
||||
|
||||
var _ validators.BodyValidator = (*BodyValidatorImpl)(nil)
|
||||
type bodyValidator struct {
|
||||
Translated string
|
||||
Validate func(fl validator.FieldLevel) bool
|
||||
}
|
||||
|
||||
var (
|
||||
_ validators.BodyValidator = (*BodyValidatorImpl)(nil)
|
||||
|
||||
fullUserRegex = regexp.MustCompile("^@([a-z0-9_-]+)(?:@([a-zA-Z0-1-_.]+\\.[a-zA-Z0-9-z]+(?::[0-9]+)?))?$")
|
||||
domainRegex = regexp.MustCompile("^[a-zA-Z0-9-_.]+.[a-zA-Z0-9-z]+(?::[0-9]+)?$")
|
||||
)
|
||||
|
||||
type BodyValidatorImpl struct {
|
||||
validator *validator.Validate
|
||||
|
|
@ -28,13 +39,13 @@ type BodyValidatorImpl struct {
|
|||
func NewBodyValidator(log logr.Logger) *BodyValidatorImpl {
|
||||
en := en_locale.New()
|
||||
translator := universal_translator.New(en, en)
|
||||
trans, ok := translator.GetTranslator("en")
|
||||
enTranslator, ok := translator.GetTranslator("en")
|
||||
if !ok {
|
||||
panic("failed to get \"en\" translator")
|
||||
}
|
||||
|
||||
validate := validator.New(validator.WithRequiredStructEnabled())
|
||||
if err := en_translations.RegisterDefaultTranslations(validate, trans); err != nil {
|
||||
if err := en_translations.RegisterDefaultTranslations(validate, enTranslator); err != nil {
|
||||
panic("failed to register default translations")
|
||||
}
|
||||
|
||||
|
|
@ -46,25 +57,65 @@ func NewBodyValidator(log logr.Logger) *BodyValidatorImpl {
|
|||
return name
|
||||
})
|
||||
|
||||
if err := validate.RegisterValidation("username_regex", func(fl validator.FieldLevel) bool {
|
||||
return schema.ValidateUsername(fl.Field().String()) == nil
|
||||
}); err != nil {
|
||||
panic("failed to register username_regex validator")
|
||||
bodyValidators := map[string]bodyValidator{
|
||||
"username_regex": {
|
||||
Translated: "{0} must match '^[a-z0-9_-]+$'!",
|
||||
Validate: func(fl validator.FieldLevel) bool {
|
||||
return schema.ValidateUsername(fl.Field().String()) == nil
|
||||
},
|
||||
},
|
||||
"full_user_regex": {
|
||||
Translated: "{0} must match '^@[a-z0-9_-]+$' or '^@[a-z0-9_-]+@[a-zA-Z0-1-_.]+\\.[a-zA-Z0-9-z]+(?::[0-9]+)?))?$'",
|
||||
Validate: func(fl validator.FieldLevel) bool {
|
||||
f := fl.Field()
|
||||
if f.Type().String() == "string" {
|
||||
return fullUserRegex.Match([]byte(f.String()))
|
||||
}
|
||||
|
||||
return false
|
||||
},
|
||||
},
|
||||
"domain_regex": {
|
||||
Translated: "{0} must match '^[a-zA-Z0-9-_.]+.[a-zA-Z0-9-z]+(?::[0-9]+)?$'",
|
||||
Validate: func(fl validator.FieldLevel) bool {
|
||||
f := fl.Field()
|
||||
t := f.Type().String()
|
||||
if t == "string" {
|
||||
return domainRegex.Match([]byte(f.String()))
|
||||
}
|
||||
|
||||
log.V(-1).Info("got wrong type: %s\n", t)
|
||||
|
||||
return false
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
if err := validate.RegisterTranslation("username_regex", trans, func(ut universal_translator.Translator) error {
|
||||
return trans.Add("user_regex", "{0} must match '^[a-z0-9_-]+$'!", true)
|
||||
}, func(ut universal_translator.Translator, fe validator.FieldError) string {
|
||||
t, _ := ut.T("user_regex", fe.Field())
|
||||
return t
|
||||
}); err != nil {
|
||||
panic("failed to register user_regex translation")
|
||||
for identifier, v := range bodyValidators {
|
||||
if err := validate.RegisterValidation(identifier, v.Validate); err != nil {
|
||||
log.Error(err, "failed to register validator", "identifier", identifier)
|
||||
}
|
||||
|
||||
register := func(ut universal_translator.Translator) error {
|
||||
return enTranslator.Add(identifier, v.Translated, true)
|
||||
}
|
||||
|
||||
translate := func(ut universal_translator.Translator, fe validator.FieldError) string {
|
||||
t, _ := ut.T(identifier, fe.Field())
|
||||
return t
|
||||
}
|
||||
|
||||
if err := validate.RegisterTranslation(identifier, enTranslator, register, translate); err != nil {
|
||||
log.Error(err, "failed to register validator translator", "identifier", identifier)
|
||||
}
|
||||
}
|
||||
|
||||
return &BodyValidatorImpl{
|
||||
validator: validate,
|
||||
translator: translator,
|
||||
enTranslator: trans,
|
||||
enTranslator: enTranslator,
|
||||
|
||||
log: log,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,17 +1,15 @@
|
|||
package val_impls
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"git.devminer.xyz/devminer/unitel"
|
||||
"github.com/go-logr/logr"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/lysand-org/versia-go/internal/repository"
|
||||
"github.com/lysand-org/versia-go/internal/utils"
|
||||
"github.com/lysand-org/versia-go/internal/validators"
|
||||
"github.com/lysand-org/versia-go/pkg/lysand"
|
||||
"github.com/valyala/fasthttp/fasthttpadaptor"
|
||||
"io"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
|
|
@ -38,7 +36,7 @@ func NewRequestValidator(repositories repository.Manager, telemetry *unitel.Tele
|
|||
}
|
||||
|
||||
func (i RequestValidatorImpl) Validate(ctx context.Context, r *http.Request) error {
|
||||
s := i.telemetry.StartSpan(ctx, "function", "validator/val_impls.RequestValidatorImpl.Validate")
|
||||
s := i.telemetry.StartSpan(ctx, "function", "val_impls/RequestValidatorImpl.Validate")
|
||||
defer s.End()
|
||||
ctx = s.Context()
|
||||
|
||||
|
|
@ -55,13 +53,13 @@ func (i RequestValidatorImpl) Validate(ctx context.Context, r *http.Request) err
|
|||
return err
|
||||
}
|
||||
|
||||
body, err := copyBody(r)
|
||||
body, err := utils.CopyBody(r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !(lysand.Verifier{PublicKey: user.PublicKey}).Verify(r.Method, r.URL, body, fedHeaders) {
|
||||
i.log.Info("signature verification failed", "user", user.URI, "url", r.URL.Path)
|
||||
if !(lysand.Verifier{PublicKey: user.PublicKey.Key}).Verify(r.Method, r.URL, body, fedHeaders) {
|
||||
i.log.WithCallDepth(1).Info("signature verification failed", "user", user.URI, "url", r.URL.Path)
|
||||
s.CaptureError(ErrInvalidSignature)
|
||||
|
||||
return ErrInvalidSignature
|
||||
|
|
@ -73,37 +71,14 @@ func (i RequestValidatorImpl) Validate(ctx context.Context, r *http.Request) err
|
|||
}
|
||||
|
||||
func (i RequestValidatorImpl) ValidateFiberCtx(ctx context.Context, c *fiber.Ctx) error {
|
||||
s := i.telemetry.StartSpan(ctx, "function", "validator/val_impls.RequestValidatorImpl.ValidateFiberCtx")
|
||||
s := i.telemetry.StartSpan(ctx, "function", "val_impls/RequestValidatorImpl.ValidateFiberCtx")
|
||||
defer s.End()
|
||||
ctx = s.Context()
|
||||
|
||||
r, err := convertToStdRequest(c)
|
||||
r, err := utils.ConvertToStdRequest(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return i.Validate(ctx, r)
|
||||
}
|
||||
|
||||
func convertToStdRequest(c *fiber.Ctx) (*http.Request, error) {
|
||||
stdReq := &http.Request{}
|
||||
if err := fasthttpadaptor.ConvertRequest(c.Context(), stdReq, true); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return stdReq, nil
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue