✨ Added profile package for fetching profile info from providers
This commit is contained in:
parent
6b05c5170f
commit
2ca16a8fbb
|
@ -0,0 +1,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)
|
||||||
|
}
|
||||||
|
|
||||||
|
var ErrNotExist error = domain.NewError(domain.ErrorCodeServerError, "not found link back to provided Me", "")
|
|
@ -0,0 +1,76 @@
|
||||||
|
package github
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
github "github.com/google/go-github/v41/github"
|
||||||
|
"golang.org/x/oauth2"
|
||||||
|
|
||||||
|
"source.toby3d.me/website/indieauth/internal/domain"
|
||||||
|
"source.toby3d.me/website/indieauth/internal/profile"
|
||||||
|
)
|
||||||
|
|
||||||
|
type githubProfileRepository struct{}
|
||||||
|
|
||||||
|
const ErrPrefix string = "github"
|
||||||
|
|
||||||
|
func NewGithubProfileRepository() profile.Repository {
|
||||||
|
return &githubProfileRepository{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (repo *githubProfileRepository) Get(ctx context.Context, token *oauth2.Token) (*domain.Profile, error) {
|
||||||
|
user, _, err := github.NewClient(oauth2.NewClient(ctx, oauth2.StaticTokenSource(token))).Users.Get(ctx, "")
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("%s: cannot get user info: %w", ErrPrefix, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
result := new(domain.Profile)
|
||||||
|
|
||||||
|
// NOTE(toby3d): Profile names.
|
||||||
|
if user.Name != nil {
|
||||||
|
result.Name = []string{*user.Name}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NOTE(toby3d): Profile photos.
|
||||||
|
if user.AvatarURL != nil {
|
||||||
|
if u, err := domain.ParseURL(*user.AvatarURL); err == nil {
|
||||||
|
result.Photo = []*domain.URL{u}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NOTE(toby3d): Profile URLs.
|
||||||
|
result.URL = make([]*domain.URL, 0)
|
||||||
|
var twitterURL *string
|
||||||
|
|
||||||
|
if user.TwitterUsername != nil {
|
||||||
|
u := "https://twitter.com/" + *user.TwitterUsername
|
||||||
|
twitterURL = &u
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, src := range []*string{
|
||||||
|
user.HTMLURL, // NOTE(toby3d): always available.
|
||||||
|
user.Blog,
|
||||||
|
twitterURL,
|
||||||
|
} {
|
||||||
|
if src == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
u, err := domain.ParseURL(*src)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
result.URL = append(result.URL, u)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NOTE(toby3d): Profile Emails.
|
||||||
|
if user.Email != nil {
|
||||||
|
if email, err := domain.ParseEmail(*user.Email); err == nil {
|
||||||
|
result.Email = []*domain.Email{email}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
|
@ -0,0 +1,90 @@
|
||||||
|
package gitlab
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
gitlab "github.com/xanzy/go-gitlab"
|
||||||
|
"golang.org/x/oauth2"
|
||||||
|
|
||||||
|
"source.toby3d.me/website/indieauth/internal/domain"
|
||||||
|
"source.toby3d.me/website/indieauth/internal/profile"
|
||||||
|
)
|
||||||
|
|
||||||
|
type gitlabProfileRepository struct{}
|
||||||
|
|
||||||
|
const ErrPrefix string = "gitlab"
|
||||||
|
|
||||||
|
func NewGitlabProfileRepository() profile.Repository {
|
||||||
|
return &gitlabProfileRepository{}
|
||||||
|
}
|
||||||
|
|
||||||
|
//nolint: funlen // NOTE(toby3d): uses hyphenation on new lines for readability.
|
||||||
|
func (repo *gitlabProfileRepository) Get(_ context.Context, token *oauth2.Token) (*domain.Profile, error) {
|
||||||
|
client, err := gitlab.NewClient(token.AccessToken)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("%s: cannot create client: %w", ErrPrefix, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
user, _, err := client.Users.CurrentUser()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("%s: cannot get user info: %w", ErrPrefix, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
result := new(domain.Profile)
|
||||||
|
|
||||||
|
// NOTE(toby3d): Profile names.
|
||||||
|
if user.Name != "" {
|
||||||
|
result.Name = []string{user.Name}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NOTE(toby3d): Profile photos.
|
||||||
|
if user.AvatarURL != "" {
|
||||||
|
if u, err := domain.ParseURL(user.AvatarURL); err == nil {
|
||||||
|
result.Photo = []*domain.URL{u}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NOTE(toby3d): Profile URLs.
|
||||||
|
result.URL = make([]*domain.URL, 0)
|
||||||
|
|
||||||
|
for _, src := range []string{
|
||||||
|
user.WebURL, // NOTE(toby3d): always available.
|
||||||
|
user.WebsiteURL,
|
||||||
|
"https://twitter.com/" + user.Twitter,
|
||||||
|
// TODO(toby3d): Skype field
|
||||||
|
// TODO(toby3d): LinkedIn field
|
||||||
|
} {
|
||||||
|
if src == "" || src == "https://twitter.com/" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
u, err := domain.ParseURL(user.WebsiteURL)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
result.URL = append(result.URL, u)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NOTE(toby3d): Profile Emails.
|
||||||
|
result.Email = make([]*domain.Email, 0)
|
||||||
|
|
||||||
|
for _, src := range []string{
|
||||||
|
user.PublicEmail,
|
||||||
|
user.Email,
|
||||||
|
} {
|
||||||
|
if src == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
email, err := domain.ParseEmail(src)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
result.Email = append(result.Email, email)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
|
@ -0,0 +1,77 @@
|
||||||
|
package mastodon
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
mastodon "github.com/mattn/go-mastodon"
|
||||||
|
"golang.org/x/oauth2"
|
||||||
|
|
||||||
|
"source.toby3d.me/website/indieauth/internal/domain"
|
||||||
|
"source.toby3d.me/website/indieauth/internal/profile"
|
||||||
|
)
|
||||||
|
|
||||||
|
type mastodonProfileRepository struct {
|
||||||
|
server string
|
||||||
|
}
|
||||||
|
|
||||||
|
const ErrPrefix string = "mastodon"
|
||||||
|
|
||||||
|
func NewMastodonProfileRepository(server string) profile.Repository {
|
||||||
|
return &mastodonProfileRepository{
|
||||||
|
server: server,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (repo *mastodonProfileRepository) Get(ctx context.Context, token *oauth2.Token) (*domain.Profile, error) {
|
||||||
|
account, err := mastodon.NewClient(&mastodon.Config{
|
||||||
|
Server: repo.server,
|
||||||
|
AccessToken: token.AccessToken,
|
||||||
|
}).GetAccountCurrentUser(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("%s: cannot get account info: %w", ErrPrefix, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
result := new(domain.Profile)
|
||||||
|
|
||||||
|
// NOTE(toby3d): Profile names.
|
||||||
|
if account.DisplayName != "" {
|
||||||
|
result.Name = []string{account.DisplayName}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NOTE(toby3d): Profile photos.
|
||||||
|
if account.Avatar != "" {
|
||||||
|
if u, err := domain.ParseURL(account.Avatar); err == nil {
|
||||||
|
result.Photo = []*domain.URL{u}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NOTE(toby3d): Profile URLs.
|
||||||
|
result.URL = make([]*domain.URL, 0)
|
||||||
|
|
||||||
|
// NOTE(toby3d): must be always available
|
||||||
|
if account.URL != "" {
|
||||||
|
if u, err := domain.ParseURL(account.URL); err == nil {
|
||||||
|
result.URL = append(result.URL, u)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range account.Fields {
|
||||||
|
// NOTE(toby3d): ignore non-verified fields that contain either
|
||||||
|
// free-form text or links in them have not yet been verified.
|
||||||
|
if account.Fields[i].VerifiedAt.IsZero() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
u, err := domain.ParseURL(account.Fields[i].Value)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
result.URL = append(result.URL, u)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WARN(toby3d): Mastodon does not provide an email via API.
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
|
@ -0,0 +1,42 @@
|
||||||
|
package memory
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"path"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"golang.org/x/oauth2"
|
||||||
|
|
||||||
|
"source.toby3d.me/website/indieauth/internal/domain"
|
||||||
|
"source.toby3d.me/website/indieauth/internal/profile"
|
||||||
|
)
|
||||||
|
|
||||||
|
type memoryProfileRepository struct {
|
||||||
|
store *sync.Map
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
ErrPrefix string = "memory"
|
||||||
|
DefaultPathPrefix string = "profiles"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewMemoryProfileRepository(store *sync.Map) profile.Repository {
|
||||||
|
return &memoryProfileRepository{
|
||||||
|
store: store,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (repo *memoryProfileRepository) Get(_ context.Context, token *oauth2.Token) (*domain.Profile, error) {
|
||||||
|
src, ok := repo.store.Load(path.Join(DefaultPathPrefix, token.AccessToken))
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("%s: cannot find profile in store: %w", ErrPrefix, profile.ErrNotExist)
|
||||||
|
}
|
||||||
|
|
||||||
|
result, ok := src.(*domain.Profile)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("%s: cannot decode profile from store: %w", ErrPrefix, profile.ErrNotExist)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
Loading…
Reference in New Issue