From a653477e7f7f662379bdf3c6f1ae82acc4ac44c4 Mon Sep 17 00:00:00 2001 From: mstar Date: Mon, 4 Nov 2024 07:48:46 +0100 Subject: [PATCH] More API progress This time mainly helper functions for converting an account and associated types into their API representation --- go.mod | 4 +- server/apiLinstromAccounts.go | 40 ++++++++++++ server/apiLinstromTypeHelpers.go | 109 +++++++++++++++++++++++++++++++ server/apiLinstromTypes.go | 2 +- server/constants.go | 1 + storage/remoteServerInfo.go | 17 ++++- storage/user.go | 4 +- storage/userInfoFields.go | 31 +++++++++ 8 files changed, 201 insertions(+), 7 deletions(-) create mode 100644 server/apiLinstromTypeHelpers.go diff --git a/go.mod b/go.mod index 8bce771..b2daec1 100644 --- a/go.mod +++ b/go.mod @@ -14,7 +14,9 @@ require ( github.com/gabriel-vasile/mimetype v1.4.5 github.com/gen2brain/avif v0.3.2 github.com/go-webauthn/webauthn v0.11.2 + github.com/google/jsonapi v1.0.0 github.com/google/uuid v1.6.0 + github.com/gorilla/websocket v1.5.3 github.com/mstarongithub/passkey v0.0.0-20240817142622-de6912c8303e github.com/redis/go-redis/v9 v9.0.2 github.com/rs/zerolog v1.33.0 @@ -55,8 +57,6 @@ require ( github.com/go-webauthn/x v0.1.14 // indirect github.com/golang-jwt/jwt/v5 v5.2.1 // indirect github.com/google/go-tpm v0.9.1 // indirect - github.com/google/jsonapi v1.0.0 // indirect - github.com/gorilla/websocket v1.5.3 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect github.com/jackc/pgx/v5 v5.4.3 // indirect diff --git a/server/apiLinstromAccounts.go b/server/apiLinstromAccounts.go index 26e9433..429a92f 100644 --- a/server/apiLinstromAccounts.go +++ b/server/apiLinstromAccounts.go @@ -3,7 +3,10 @@ package server import ( "net/http" + "github.com/google/jsonapi" "github.com/rs/zerolog/hlog" + "gitlab.com/mstarongitlab/goutils/other" + "gitlab.com/mstarongitlab/linstrom/storage" ) // No create account. That happens during passkey registration @@ -11,8 +14,45 @@ import ( func linstromGetAccount(w http.ResponseWriter, r *http.Request) { store := StorageFromRequest(r) log := hlog.FromRequest(r) + accId := AccountIdFromRequest(r) + acc, err := store.FindAccountById(accId) + switch err { + case nil: + // Ok, do nothing + case storage.ErrEntryNotFound: + other.HttpErr(w, HttpErrIdNotFound, "account not found", http.StatusNotFound) + return + default: + log.Error().Err(err).Str("account-id", accId).Msg("Failed to get account from storage") + other.HttpErr( + w, + HttpErrIdDbFailure, + "Failed to get account from storage", + http.StatusInternalServerError, + ) + return + } + // TODO: Check if caller is actually allowed to view the account requested. + outAccount, err := convertAccountStorageToLinstrom(acc, store) + if err != nil { + log.Error(). + Err(err). + Msg("Failed to convert storage account (and attached data) into linstrom API representation") + other.HttpErr( + w, + HttpErrIdConverionFailure, + "Failed to convert storage account and attached data into API representation", + http.StatusInternalServerError, + ) + return + } + err = jsonapi.MarshalPayload(w, outAccount) + if err != nil { + log.Error().Err(err).Any("account", outAccount).Msg("Failed to marshal and write account") + } } + func linstromUpdateAccount(w http.ResponseWriter, r *http.Request) {} func linstromDeleteAccount(w http.ResponseWriter, r *http.Request) {} diff --git a/server/apiLinstromTypeHelpers.go b/server/apiLinstromTypeHelpers.go new file mode 100644 index 0000000..b2cc272 --- /dev/null +++ b/server/apiLinstromTypeHelpers.go @@ -0,0 +1,109 @@ +package server + +import ( + "gitlab.com/mstarongitlab/goutils/sliceutils" + "gitlab.com/mstarongitlab/linstrom/storage" +) + +func convertAccountStorageToLinstrom( + acc *storage.Account, + store *storage.Storage, +) (*linstromAccount, error) { + storageServer, err := store.FindRemoteServerById(acc.ServerId) + if err != nil { + return nil, err + } + apiServer, err := convertServerStorageToLinstrom(storageServer, store) + if err != nil { + return nil, err + } + storageIcon, err := store.GetMediaMetadataById(acc.Icon) + if err != nil { + return nil, err + } + storageBanner, err := store.GetMediaMetadataById(acc.Banner) + if err != nil { + return nil, err + } + storageFields, err := store.FindMultipleUserFieldsById(acc.CustomFields) + if err != nil { + return nil, err + } + + return &linstromAccount{ + Id: acc.ID, + CreatedAt: acc.CreatedAt, + UpdatedAt: &acc.UpdatedAt, + Username: acc.Username, + OriginServer: apiServer, + OriginServerId: int(acc.ServerId), + DisplayName: acc.DisplayName, + CustomFields: sliceutils.Map( + storageFields, + func(t storage.UserInfoField) *linstromCustomAccountField { + return convertInfoFieldStorageToLinstrom(t) + }, + ), + CustomFieldIds: acc.CustomFields, + IsBot: acc.IsBot, + Description: acc.Description, + Icon: convertMediaMetadataStorageToLinstrom(storageIcon), + Banner: convertMediaMetadataStorageToLinstrom(storageBanner), + FollowerIds: acc.Followers, + FollowingIds: acc.Follows, + Indexable: acc.Indexable, + RestrictedFollow: acc.RestrictedFollow, + IdentifiesAs: sliceutils.Map( + acc.IdentifiesAs, + func(t storage.Being) string { return string(t) }, + ), + Pronouns: acc.Gender, + Roles: acc.Roles, + }, nil +} + +func convertServerStorageToLinstrom( + server *storage.RemoteServer, + store *storage.Storage, +) (*linstromOriginServer, error) { + storageMeta, err := store.GetMediaMetadataById(server.Icon) + if err != nil { + return nil, err + } + return &linstromOriginServer{ + Id: server.ID, + CreatedAt: server.CreatedAt, + UpdatedAt: &server.UpdatedAt, + ServerType: string(server.ServerType), + Domain: server.Domain, + DisplayName: server.Name, + Icon: convertMediaMetadataStorageToLinstrom(storageMeta), + IsSelf: server.IsSelf, + }, nil +} + +func convertMediaMetadataStorageToLinstrom(metadata *storage.MediaMetadata) *linstromMediaMetadata { + return &linstromMediaMetadata{ + Id: metadata.ID, + CreatedAt: metadata.CreatedAt, + UpdatedAt: &metadata.UpdatedAt, + IsRemote: metadata.Remote, + Url: metadata.Location, + MimeType: metadata.Type, + Name: metadata.Name, + AltText: metadata.AltText, + Blurred: metadata.Blurred, + } +} + +func convertInfoFieldStorageToLinstrom(field storage.UserInfoField) *linstromCustomAccountField { + return &linstromCustomAccountField{ + Id: field.ID, + CreatedAt: field.CreatedAt, + UpdatedAt: &field.UpdatedAt, + Key: field.Name, + Value: field.Value, + Verified: &field.Confirmed, + BelongsToId: field.BelongsTo, + } +} diff --git a/server/apiLinstromTypes.go b/server/apiLinstromTypes.go index 2416c49..b064c0f 100644 --- a/server/apiLinstromTypes.go +++ b/server/apiLinstromTypes.go @@ -35,7 +35,7 @@ type linstromNote struct { } type linstromOriginServer struct { - Id int `jsonapi:"primary,origins"` + Id uint `jsonapi:"primary,origins"` CreatedAt time.Time `jsonapi:"attr,created_at"` UpdatedAt *time.Time `jsonapi:"attr,updated_at,omitempty"` ServerType string `jsonapi:"attr,server_type"` // one of "Linstrom", "Mastodon", "Plemora", "Misskey" or "Wafrn" diff --git a/server/constants.go b/server/constants.go index 99cfdc6..5a2c304 100644 --- a/server/constants.go +++ b/server/constants.go @@ -18,4 +18,5 @@ const ( HttpErrIdBadRequest HttpErrIdAlreadyExists HttpErrIdNotFound + HttpErrIdConverionFailure ) diff --git a/storage/remoteServerInfo.go b/storage/remoteServerInfo.go index 61ba85f..8379f2e 100644 --- a/storage/remoteServerInfo.go +++ b/storage/remoteServerInfo.go @@ -13,7 +13,7 @@ type RemoteServer struct { IsSelf bool // Whether this server is yours truly } -func (s *Storage) FindRemoteServer(url string) (*RemoteServer, error) { +func (s *Storage) FindRemoteServerByDomain(url string) (*RemoteServer, error) { server := RemoteServer{} err := s.db.Where("domain = ?").First(&server).Error switch err { @@ -40,12 +40,25 @@ func (s *Storage) FindRemoteServerByDisplayName(displayName string) (*RemoteServ } } +func (s *Storage) FindRemoteServerById(id uint) (*RemoteServer, error) { + server := RemoteServer{} + err := s.db.First(&server, id).Error + switch err { + case nil: + return &server, nil + case gorm.ErrRecordNotFound: + return nil, ErrEntryNotFound + default: + return nil, err + } +} + // Create a new remote server func (s *Storage) NewRemoteServer( url, displayName, icon string, serverType RemoteServerType, ) (*RemoteServer, error) { - _, err := s.FindRemoteServer(url) + _, err := s.FindRemoteServerByDomain(url) switch err { case nil: return nil, ErrEntryAlreadyExists diff --git a/storage/user.go b/storage/user.go index fbb9a48..aed39bf 100644 --- a/storage/user.go +++ b/storage/user.go @@ -39,8 +39,8 @@ type Account struct { Description string // The description of a user account Tags []string `gorm:"serializer:json"` // Hashtags IsBot bool // Whether to mark this account as a script controlled one - Follows []string `gorm:"serializer:json"` // List of handles this account follows - Followers []string `gorm:"serializer:json"` // List of handles that follow this account + Follows []string `gorm:"serializer:json"` // List of account ids this account follows + Followers []string `gorm:"serializer:json"` // List of account ids that follow this account Icon string // ID of a media file used as icon Background string // ID of a media file used as background image Banner string // ID of a media file used as banner diff --git a/storage/userInfoFields.go b/storage/userInfoFields.go index a1c4d25..1f063ea 100644 --- a/storage/userInfoFields.go +++ b/storage/userInfoFields.go @@ -20,3 +20,34 @@ type UserInfoField struct { } // TODO: Add functions to store, load, update and delete these + +func (s *Storage) FindUserFieldById(id uint) (*UserInfoField, error) { + entry := UserInfoField{} + err := s.db.First(&entry, id).Error + switch err { + case nil: + return &entry, nil + case gorm.ErrRecordNotFound: + return nil, ErrEntryNotFound + default: + return nil, err + } +} + +func (s *Storage) FindMultipleUserFieldsById(ids []uint) ([]UserInfoField, error) { + entries := []UserInfoField{} + err := s.db.Where(ids).Find(&entries).Error + switch err { + case gorm.ErrRecordNotFound: + return nil, ErrEntryNotFound + case nil: + return entries, nil + default: + return nil, err + } +} + +func (s *Storage) AddNewUserField(name, value, belongsToId string) (*UserInfoField, error) { + // TODO: Implement me + panic("Not implemented") +}