diff --git a/internal/profile/repository.go b/internal/profile/repository.go index 9b89398..eb8da65 100644 --- a/internal/profile/repository.go +++ b/internal/profile/repository.go @@ -3,13 +3,15 @@ package profile import ( "context" - "golang.org/x/oauth2" - "source.toby3d.me/website/indieauth/internal/domain" ) type Repository interface { - Get(ctx context.Context, token *oauth2.Token) (*domain.Profile, error) + Get(ctx context.Context, me *domain.Me) (*domain.Profile, error) } -var ErrNotExist error = domain.NewError(domain.ErrorCodeServerError, "not found link back to provided Me", "") +var ErrNotExist error = domain.NewError( + domain.ErrorCodeServerError, + "no profile data for the provided Me", + "https://indieweb.org/h-card", +) diff --git a/internal/profile/repository/http/http_profile.go b/internal/profile/repository/http/http_profile.go new file mode 100644 index 0000000..8d72140 --- /dev/null +++ b/internal/profile/repository/http/http_profile.go @@ -0,0 +1,90 @@ +package http + +import ( + "context" + "fmt" + + http "github.com/valyala/fasthttp" + + "source.toby3d.me/website/indieauth/internal/domain" + "source.toby3d.me/website/indieauth/internal/profile" + "source.toby3d.me/website/indieauth/internal/util" +) + +type httpProfileRepository struct { + client *http.Client +} + +const ( + ErrPrefix string = "http" + DefaultMaxRedirectsCount int = 10 + + hCard string = "h-card" + propertyEmail string = "email" + propertyName string = "name" + propertyPhoto string = "photo" + propertyURL string = "url" +) + +func NewHTPPClientRepository(client *http.Client) profile.Repository { + return &httpProfileRepository{ + client: client, + } +} + +func (repo *httpProfileRepository) Get(ctx context.Context, me *domain.Me) (*domain.Profile, error) { + req := http.AcquireRequest() + defer http.ReleaseRequest(req) + req.Header.SetMethod(http.MethodGet) + req.SetRequestURI(me.String()) + + resp := http.AcquireResponse() + defer http.ReleaseResponse(resp) + + if err := repo.client.DoRedirects(req, resp, DefaultMaxRedirectsCount); err != nil { + return nil, fmt.Errorf("%s: cannot fetch user by me: %w", ErrPrefix, err) + } + + result := domain.NewProfile() + + for _, name := range util.ExtractProperty(resp, hCard, propertyName) { + if n, ok := name.(string); ok { + result.Name = append(result.Name, n) + } + } + + for _, rawEmail := range util.ExtractProperty(resp, hCard, propertyEmail) { + email, ok := rawEmail.(string) + if !ok { + continue + } + + if e, err := domain.ParseEmail(email); err == nil { + result.Email = append(result.Email, e) + } + } + + for _, rawURL := range util.ExtractProperty(resp, hCard, propertyURL) { + url, ok := rawURL.(string) + if !ok { + continue + } + + if u, err := domain.ParseURL(url); err == nil { + result.URL = append(result.URL, u) + } + } + + for _, rawPhoto := range util.ExtractProperty(resp, hCard, propertyPhoto) { + photo, ok := rawPhoto.(string) + if !ok { + continue + } + + if p, err := domain.ParseURL(photo); err == nil { + result.Photo = append(result.Photo, p) + } + } + + return result, nil +} diff --git a/internal/profile/repository/memory/memory_profile.go b/internal/profile/repository/memory/memory_profile.go index 78d19b7..1968b21 100644 --- a/internal/profile/repository/memory/memory_profile.go +++ b/internal/profile/repository/memory/memory_profile.go @@ -6,8 +6,6 @@ import ( "path" "sync" - "golang.org/x/oauth2" - "source.toby3d.me/website/indieauth/internal/domain" "source.toby3d.me/website/indieauth/internal/profile" ) @@ -27,8 +25,8 @@ func NewMemoryProfileRepository(store *sync.Map) profile.Repository { } } -func (repo *memoryProfileRepository) Get(_ context.Context, token *oauth2.Token) (*domain.Profile, error) { - src, ok := repo.store.Load(path.Join(DefaultPathPrefix, token.AccessToken)) +func (repo *memoryProfileRepository) Get(_ context.Context, me *domain.Me) (*domain.Profile, error) { + src, ok := repo.store.Load(path.Join(DefaultPathPrefix, me.String())) if !ok { return nil, fmt.Errorf("%s: cannot find profile in store: %w", ErrPrefix, profile.ErrNotExist) } diff --git a/internal/profile/usecase.go b/internal/profile/usecase.go new file mode 100644 index 0000000..84294d3 --- /dev/null +++ b/internal/profile/usecase.go @@ -0,0 +1,17 @@ +package profile + +import ( + "context" + + "source.toby3d.me/website/indieauth/internal/domain" +) + +type UseCase interface { + Fetch(ctx context.Context, me *domain.Me) (*domain.Profile, error) +} + +var ErrScopeRequired error = domain.NewError( + domain.ErrorCodeInsufficientScope, + "token with 'profile' scopes is required to view profile data", + "https://indieauth.net/source/#user-information", +) diff --git a/internal/profile/usecase/profile_ucase.go b/internal/profile/usecase/profile_ucase.go new file mode 100644 index 0000000..963492f --- /dev/null +++ b/internal/profile/usecase/profile_ucase.go @@ -0,0 +1,28 @@ +package usecase + +import ( + "context" + "fmt" + + "source.toby3d.me/website/indieauth/internal/domain" + "source.toby3d.me/website/indieauth/internal/profile" +) + +type profileUseCase struct { + profiles profile.Repository +} + +func NewProfileUseCase(profiles profile.Repository) profile.UseCase { + return &profileUseCase{ + profiles: profiles, + } +} + +func (uc *profileUseCase) Fetch(ctx context.Context, me *domain.Me) (*domain.Profile, error) { + result, err := uc.profiles.Get(ctx, me) + if err != nil { + return nil, fmt.Errorf("cannot fetch profile info: %w", err) + } + + return result, nil +}