More progress. Fixed storage bug. Need to get media stuff going
This commit is contained in:
Melody Becker 2024-11-05 16:29:01 +01:00
parent 1bb6cd8a70
commit 83f47d17be
11 changed files with 209 additions and 27 deletions

View file

@ -42,6 +42,7 @@ func setupLinstromApiV1Router() http.Handler {
router.HandleFunc("GET /accounts/{accountId}", linstromGetAccount) router.HandleFunc("GET /accounts/{accountId}", linstromGetAccount)
// Technically also requires authenticated account to also be owner or correct admin perms, // Technically also requires authenticated account to also be owner or correct admin perms,
// but that's annoying to handle in a general sense. So leaving that to the function // but that's annoying to handle in a general sense. So leaving that to the function
// though figuring out a nice generic-ish way to handle those checks would be nice too
router.HandleFunc( router.HandleFunc(
"PATCH /accounts/{accountId}", "PATCH /accounts/{accountId}",
requireValidSessionMiddleware(linstromUpdateAccount), requireValidSessionMiddleware(linstromUpdateAccount),

View file

@ -81,6 +81,89 @@ func linstromGetAccount(w http.ResponseWriter, r *http.Request) {
func linstromUpdateAccount(w http.ResponseWriter, r *http.Request) { func linstromUpdateAccount(w http.ResponseWriter, r *http.Request) {
store := StorageFromRequest(r) store := StorageFromRequest(r)
log := hlog.FromRequest(r) log := hlog.FromRequest(r)
// Assumption: There must be a valid session once this function is called due to middlewares
actorId, _ := ActorIdFromRequest(r)
apiTarget := linstromAccount{}
err := jsonapi.UnmarshalPayload(r.Body, &apiTarget)
if err != nil {
other.HttpErr(w, HttpErrIdBadRequest, "bad body", http.StatusBadRequest)
return
}
targetAccId := AccountIdFromRequest(r)
if apiTarget.Id != targetAccId {
other.HttpErr(
w,
HttpErrIdBadRequest,
"Provided entity's id doesn't match path id",
http.StatusConflict,
)
return
}
if !(actorId == apiTarget.Id) {
other.HttpErr(w, HttpErrIdNotAuthenticated, "Invalid permissions", http.StatusForbidden)
return
}
dbTarget, err := store.FindAccountById(apiTarget.Id)
// Assumption: The only sort of errors that can be returned are db failures.
// The account not existing is not possible anymore since this is in a valid session
// and a session is only injected if the actor account can be found
if err != nil {
log.Error().
Err(err).
Str("account-id", actorId).
Msg("Failed to get account from db despite valid session")
other.HttpErr(
w,
HttpErrIdDbFailure,
"Failed to get account despite valid session",
http.StatusInternalServerError,
)
return
}
// location, birthday, icon, banner, background, custom fields
// bluesky federation, uhhh
dbTarget.DisplayName = apiTarget.DisplayName
dbTarget.Indexable = apiTarget.Indexable
dbTarget.Description = apiTarget.Description
// TODO: Figure out how to properly update custom fields
dbTarget.Gender = apiTarget.Pronouns
dbTarget.IdentifiesAs = sliceutils.Map(
sliceutils.Filter(apiTarget.IdentifiesAs, func(t string) bool {
return storage.IsValidBeing(t)
}),
func(t string) storage.Being { return storage.Being(t) },
)
dbTarget.Indexable = apiTarget.Indexable
dbTarget.RestrictedFollow = apiTarget.RestrictedFollow
err = store.UpdateAccount(dbTarget)
if err != nil {
log.Error().Err(err).Msg("Failed to update account in db")
other.HttpErr(
w,
HttpErrIdDbFailure,
"Failed to update db entries",
http.StatusInternalServerError,
)
return
}
w.WriteHeader(http.StatusOK)
newAccData, err := convertAccountStorageToLinstrom(dbTarget, store)
if err != nil {
log.Error().Err(err).Msg("Failed to convert updated account back into api form")
other.HttpErr(
w,
HttpErrIdConverionFailure,
"Failed to convert updated account back into api form",
http.StatusInternalServerError,
)
return
}
err = jsonapi.MarshalPayload(w, newAccData)
if err != nil {
log.Error().Err(err).Msg("Failed to marshal and write updated account")
}
} }
func linstromDeleteAccount(w http.ResponseWriter, r *http.Request) {} func linstromDeleteAccount(w http.ResponseWriter, r *http.Request) {}

View file

@ -1,6 +1,8 @@
package server package server
import ( import (
"fmt"
"gitlab.com/mstarongitlab/goutils/sliceutils" "gitlab.com/mstarongitlab/goutils/sliceutils"
"gitlab.com/mstarongitlab/linstrom/storage" "gitlab.com/mstarongitlab/linstrom/storage"
) )
@ -11,23 +13,23 @@ func convertAccountStorageToLinstrom(
) (*linstromAccount, error) { ) (*linstromAccount, error) {
storageServer, err := store.FindRemoteServerById(acc.ServerId) storageServer, err := store.FindRemoteServerById(acc.ServerId)
if err != nil { if err != nil {
return nil, err return nil, fmt.Errorf("remote server: %w", err)
} }
apiServer, err := convertServerStorageToLinstrom(storageServer, store) apiServer, err := convertServerStorageToLinstrom(storageServer, store)
if err != nil { if err != nil {
return nil, err return nil, fmt.Errorf("remote server conversion: %w", err)
} }
storageIcon, err := store.GetMediaMetadataById(acc.Icon) storageIcon, err := store.GetMediaMetadataById(acc.Icon)
if err != nil { if err != nil {
return nil, err return nil, fmt.Errorf("icon: %w", err)
} }
storageBanner, err := store.GetMediaMetadataById(acc.Banner) storageBanner, err := store.GetMediaMetadataById(acc.Banner)
if err != nil { if err != nil {
return nil, err return nil, fmt.Errorf("banner: %w", err)
} }
storageFields, err := store.FindMultipleUserFieldsById(acc.CustomFields) storageFields, err := store.FindMultipleUserFieldsById(acc.CustomFields)
if err != nil { if err != nil {
return nil, err return nil, fmt.Errorf("customFields: %w", err)
} }
return &linstromAccount{ return &linstromAccount{
@ -68,7 +70,7 @@ func convertServerStorageToLinstrom(
) (*linstromOriginServer, error) { ) (*linstromOriginServer, error) {
storageMeta, err := store.GetMediaMetadataById(server.Icon) storageMeta, err := store.GetMediaMetadataById(server.Icon)
if err != nil { if err != nil {
return nil, err return nil, fmt.Errorf("icon metadata: %w", err)
} }
return &linstromOriginServer{ return &linstromOriginServer{
Id: server.ID, Id: server.ID,

View file

@ -5,7 +5,7 @@ import "net/http"
// Mounted at /api // Mounted at /api
func setupApiRouter() http.Handler { func setupApiRouter() http.Handler {
router := http.NewServeMux() router := http.NewServeMux()
router.Handle("/linstrom/", setupLinstromApiRouter()) router.Handle("/linstrom/", http.StripPrefix("/linstrom", setupLinstromApiRouter()))
// Section MastoApi // Section MastoApi
// First segment are endpoints that will need to be moved to primary router since at top route // First segment are endpoints that will need to be moved to primary router since at top route

View file

@ -103,6 +103,7 @@ func fuckWithRegisterRequest(
} else { } else {
// Not authenticated, ensure that no existing name is registered with // Not authenticated, ensure that no existing name is registered with
_, err = store.FindLocalAccountByUsername(username.Username) _, err = store.FindLocalAccountByUsername(username.Username)
log.Debug().Bool("err-equals-not_found", err == storage.ErrEntryNotFound).Send()
switch err { switch err {
case nil: case nil:
// No error while getting account means account exists, refuse access // No error while getting account means account exists, refuse access

View file

@ -27,6 +27,11 @@ func StorageFromRequest(r *http.Request) *storage.Storage {
return store return store
} }
func ActorIdFromRequest(r *http.Request) (string, bool) {
id, ok := r.Context().Value(ContextKeyActorId).(string)
return id, ok
}
func NoteIdFromRequest(r *http.Request) string { func NoteIdFromRequest(r *http.Request) string {
return r.PathValue("noteId") return r.PathValue("noteId")
} }
@ -34,3 +39,16 @@ func NoteIdFromRequest(r *http.Request) string {
func AccountIdFromRequest(r *http.Request) string { func AccountIdFromRequest(r *http.Request) string {
return r.PathValue("accountId") return r.PathValue("accountId")
} }
func CheckIfAccountIdHasPermissions(accId string, perms storage.Role, store *storage.Storage) bool {
acc, err := store.FindAccountById(accId)
if err != nil {
return false
}
roles, err := store.FindRolesByNames(acc.Roles)
if err != nil {
return false
}
collapsed := storage.CollapseRolesIntoOne(roles...)
return storage.CompareRoles(&collapsed, &perms)
}

View file

@ -22,14 +22,14 @@ type Role struct {
gorm.Model gorm.Model
// Name of the role // Name of the role
Name string `gorm:"primaryKey"` Name string `gorm:"primaryKey;unique"`
// Priority of the role // Priority of the role
// Lower priority gets applied first and thus overwritten by higher priority ones // Lower priority gets applied first and thus overwritten by higher priority ones
// If two roles have the same priority, the order is undetermined and may be random // If two roles have the same priority, the order is undetermined and may be random
// Default priority for new roles is 1 to always overwrite default user // Default priority for new roles is 1 to always overwrite default user
// And full admin has max priority possible // And full admin has max priority possible
Priority uint Priority uint32
// Whether this role is for a for a single user only (like custom, per user permissions in Discord) // Whether this role is for a for a single user only (like custom, per user permissions in Discord)
// If yes, Name will be the id of the user in question // If yes, Name will be the id of the user in question
IsUserRole bool IsUserRole bool

View file

@ -69,7 +69,7 @@ var DefaultUserRole = Role{
// Role providing maximum permissions // Role providing maximum permissions
var FullAdminRole = Role{ var FullAdminRole = Role{
Name: "fullAdmin", Name: "fullAdmin",
Priority: math.MaxUint, Priority: math.MaxUint32,
IsUserRole: false, IsUserRole: false,
IsBuiltIn: true, IsBuiltIn: true,
@ -125,7 +125,7 @@ var FullAdminRole = Role{
// Role for totally freezing an account, blocking all activity from it // Role for totally freezing an account, blocking all activity from it
var AccountFreezeRole = Role{ var AccountFreezeRole = Role{
Name: "accountFreeze", Name: "accountFreeze",
Priority: math.MaxUint - 1, Priority: math.MaxUint32 - 1,
IsUserRole: false, IsUserRole: false,
IsBuiltIn: true, IsBuiltIn: true,
@ -181,8 +181,64 @@ var AccountFreezeRole = Role{
CanSendAnnouncements: other.IntoPointer(false), CanSendAnnouncements: other.IntoPointer(false),
} }
var ServerActorRole = Role{
Name: "ServerActor",
Priority: math.MaxUint32,
IsUserRole: true,
IsBuiltIn: true,
CanSendMedia: other.IntoPointer(true),
CanSendCustomEmotes: other.IntoPointer(true),
CanSendCustomReactions: other.IntoPointer(true),
CanSendPublicNotes: other.IntoPointer(true),
CanSendLocalNotes: other.IntoPointer(true),
CanSendFollowerOnlyNotes: other.IntoPointer(true),
CanSendPrivateNotes: other.IntoPointer(true),
CanQuote: other.IntoPointer(true),
CanBoost: other.IntoPointer(true),
CanIncludeLinks: other.IntoPointer(true),
CanIncludeSurvey: other.IntoPointer(true),
CanChangeDisplayName: other.IntoPointer(true),
BlockedUsers: []string{},
CanSubmitReports: other.IntoPointer(true),
CanLogin: other.IntoPointer(true),
CanMentionOthers: other.IntoPointer(true),
HasMentionCountLimit: other.IntoPointer(false),
MentionLimit: other.IntoPointer(
uint32(math.MaxUint32),
), // Set this to max, even if not used due to *HasMentionCountLimit == false
AutoNsfwMedia: other.IntoPointer(false),
AutoCwPosts: other.IntoPointer(false),
AutoCwPostsText: nil,
WithholdNotesForManualApproval: other.IntoPointer(false),
ScanCreatedPublicNotes: other.IntoPointer(false),
ScanCreatedLocalNotes: other.IntoPointer(false),
ScanCreatedFollowerOnlyNotes: other.IntoPointer(false),
ScanCreatedPrivateNotes: other.IntoPointer(false),
DisallowInteractionsWith: []string{},
FullAdmin: other.IntoPointer(true),
CanAffectOtherAdmins: other.IntoPointer(true),
CanDeleteNotes: other.IntoPointer(true),
CanConfirmWithheldNotes: other.IntoPointer(true),
CanAssignRoles: other.IntoPointer(true),
CanSupressInteractionsBetweenUsers: other.IntoPointer(true),
CanOverwriteDisplayNames: other.IntoPointer(true),
CanManageCustomEmotes: other.IntoPointer(true),
CanViewDeletedNotes: other.IntoPointer(true),
CanRecoverDeletedNotes: other.IntoPointer(true),
CanManageAvatarDecorations: other.IntoPointer(true),
CanManageAds: other.IntoPointer(true),
CanSendAnnouncements: other.IntoPointer(true),
}
var allDefaultRoles = []*Role{ var allDefaultRoles = []*Role{
&DefaultUserRole, &DefaultUserRole,
&FullAdminRole, &FullAdminRole,
&AccountFreezeRole, &AccountFreezeRole,
&ServerActorRole,
} }

View file

@ -53,8 +53,11 @@ func NewStorage(dbUrl string, cache *cache.Cache) (*Storage, error) {
s := &Storage{db, cache} s := &Storage{db, cache}
if err = s.insertDefaultRoles(); err != nil {
return nil, fmt.Errorf("default roles insertion failed: %w", err)
}
if err = s.insertSelfFromConfig(); err != nil { if err = s.insertSelfFromConfig(); err != nil {
return nil, err return nil, fmt.Errorf("self insertion failed: %w", err)
} }
return s, nil return s, nil
@ -68,12 +71,15 @@ func (s *Storage) insertSelfFromConfig() error {
// Insert server info // Insert server info
serverData := RemoteServer{} serverData := RemoteServer{}
err = s.db.Where("id = 1"). err = s.db.Where("id = 1").
// Set once on creation
Attrs(RemoteServer{ Attrs(RemoteServer{
Domain: config.GlobalConfig.General.GetFullDomain(), Domain: config.GlobalConfig.General.GetFullDomain(),
}). }).
// Set every time
Assign(RemoteServer{ Assign(RemoteServer{
IsSelf: true, IsSelf: true,
Name: config.GlobalConfig.Self.ServerDisplayName, Name: config.GlobalConfig.Self.ServerDisplayName,
ServerType: REMOTE_SERVER_LINSTROM,
// Icon: "", // TODO: Set to server icon media // Icon: "", // TODO: Set to server icon media
}).FirstOrCreate(&serverData).Error }).FirstOrCreate(&serverData).Error
if err != nil { if err != nil {
@ -91,6 +97,7 @@ func (s *Storage) insertSelfFromConfig() error {
err = s.db.Where(Account{ID: ServerActorId}). err = s.db.Where(Account{ID: ServerActorId}).
// Values to always (re)set after launch // Values to always (re)set after launch
Assign(Account{ Assign(Account{
Username: "self",
DisplayName: config.GlobalConfig.Self.ServerActorDisplayName, DisplayName: config.GlobalConfig.Self.ServerActorDisplayName,
// Server: serverData, // Server: serverData,
ServerId: serverData.ID, ServerId: serverData.ID,
@ -107,7 +114,7 @@ func (s *Storage) insertSelfFromConfig() error {
RestrictedFollow: false, RestrictedFollow: false,
IdentifiesAs: []Being{}, IdentifiesAs: []Being{},
Gender: []string{}, Gender: []string{},
Roles: []string{}, // TODO: Add server actor role once created Roles: []string{"ServerActor"}, // TODO: Add server actor role once created
}). }).
// Values that'll only be set on first creation // Values that'll only be set on first creation
Attrs(Account{ Attrs(Account{
@ -120,3 +127,13 @@ func (s *Storage) insertSelfFromConfig() error {
} }
return nil return nil
} }
func (s *Storage) insertDefaultRoles() error {
for _, role := range allDefaultRoles {
log.Debug().Str("role-name", role.Name).Msg("Inserting default role")
if err := s.db.FirstOrCreate(role).Error; err != nil {
return err
}
}
return nil
}

View file

@ -13,7 +13,6 @@ import (
"github.com/mstarongithub/passkey" "github.com/mstarongithub/passkey"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"gitlab.com/mstarongitlab/linstrom/ap" "gitlab.com/mstarongitlab/linstrom/ap"
"gitlab.com/mstarongitlab/linstrom/config"
"gorm.io/gorm" "gorm.io/gorm"
) )
@ -61,6 +60,9 @@ type Account struct {
// The roles assocciated with an account. Values are the names of the roles // The roles assocciated with an account. Values are the names of the roles
Roles []string `gorm:"serializer:json"` Roles []string `gorm:"serializer:json"`
Location *string
Birthday *time.Time
// --- And internal account stuff --- // --- And internal account stuff ---
// Still public fields since they wouldn't be able to be stored in the db otherwise // Still public fields since they wouldn't be able to be stored in the db otherwise
PrivateKey []byte // The private key of the account. Nil if remote user PrivateKey []byte // The private key of the account. Nil if remote user
@ -166,7 +168,8 @@ func (s *Storage) FindAccountById(id string) (*Account, error) {
} }
log.Debug().Str("account-id", id).Msg("Didn't hit account in cache, checking db") log.Debug().Str("account-id", id).Msg("Didn't hit account in cache, checking db")
res := s.db.First(acc, id) acc = &Account{ID: id}
res := s.db.First(acc)
if res.Error != nil { if res.Error != nil {
if errors.Is(res.Error, gorm.ErrRecordNotFound) { if errors.Is(res.Error, gorm.ErrRecordNotFound) {
log.Warn().Str("account-id", id).Msg("Account not found") log.Warn().Str("account-id", id).Msg("Account not found")
@ -194,7 +197,7 @@ func (s *Storage) FindLocalAccountByUsername(username string) (*Account, error)
// Then always load via id since unique key access should be faster than string matching // Then always load via id since unique key access should be faster than string matching
return s.FindAccountById(*cacheAccId) return s.FindAccountById(*cacheAccId)
} else { } else {
if !errors.Is(err, errCacheNotFound) { if err != errCacheNotFound {
log.Error().Err(err).Str("account-username", username).Msg("Problem while checking cache for account") log.Error().Err(err).Str("account-username", username).Msg("Problem while checking cache for account")
return nil, err return nil, err
} }
@ -202,17 +205,10 @@ func (s *Storage) FindLocalAccountByUsername(username string) (*Account, error)
// Failed to find in cache, go the slow route of hitting the db // Failed to find in cache, go the slow route of hitting the db
log.Debug().Str("account-username", username).Msg("Didn't hit account in cache, going to db") log.Debug().Str("account-username", username).Msg("Didn't hit account in cache, going to db")
if err != nil {
log.Warn().
Err(err).
Str("account-username", username).
Msg("Failed to split up account username")
return nil, err
}
acc := Account{} acc := Account{}
res := s.db.Where("username = ?", username). res := s.db.Where("username = ?", username).
Where("server = ?", config.GlobalConfig.General.GetFullDomain()). Where("server_id = ?", serverSelf.ID).
First(&acc) First(&acc)
if res.Error != nil { if res.Error != nil {
if errors.Is(res.Error, gorm.ErrRecordNotFound) { if errors.Is(res.Error, gorm.ErrRecordNotFound) {
@ -222,7 +218,7 @@ func (s *Storage) FindLocalAccountByUsername(username string) (*Account, error)
} else { } else {
log.Error().Err(err).Str("account-username", username).Msg("Failed to get local account with username") log.Error().Err(err).Str("account-username", username).Msg("Failed to get local account with username")
} }
return nil, res.Error return nil, ErrEntryNotFound
} }
log.Info().Str("account-username", username).Msg("Found account, also inserting into cache") log.Info().Str("account-username", username).Msg("Found account, also inserting into cache")
if err = s.cache.Set(cacheUserIdToAccPrefix+acc.ID, &acc); err != nil { if err = s.cache.Set(cacheUserIdToAccPrefix+acc.ID, &acc); err != nil {

View file

@ -1,5 +1,7 @@
package storage package storage
import "gitlab.com/mstarongitlab/goutils/sliceutils"
// What kind of being a user identifies as // What kind of being a user identifies as
type Being string type Being string
@ -11,3 +13,9 @@ const (
BEING_ROBOT = Being("robot") BEING_ROBOT = Being("robot")
BEING_DOLL = Being("doll") BEING_DOLL = Being("doll")
) )
var allBeings = []Being{BEING_HUMAN, BEING_CAT, BEING_FOX, BEING_DOG, BEING_ROBOT, BEING_DOLL}
func IsValidBeing(toCheck string) bool {
return sliceutils.Contains(allBeings, Being(toCheck))
}