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)
// 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
// though figuring out a nice generic-ish way to handle those checks would be nice too
router.HandleFunc(
"PATCH /accounts/{accountId}",
requireValidSessionMiddleware(linstromUpdateAccount),

View file

@ -81,6 +81,89 @@ func linstromGetAccount(w http.ResponseWriter, r *http.Request) {
func linstromUpdateAccount(w http.ResponseWriter, r *http.Request) {
store := StorageFromRequest(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) {}

View file

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

View file

@ -5,7 +5,7 @@ import "net/http"
// Mounted at /api
func setupApiRouter() http.Handler {
router := http.NewServeMux()
router.Handle("/linstrom/", setupLinstromApiRouter())
router.Handle("/linstrom/", http.StripPrefix("/linstrom", setupLinstromApiRouter()))
// Section MastoApi
// 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 {
// Not authenticated, ensure that no existing name is registered with
_, err = store.FindLocalAccountByUsername(username.Username)
log.Debug().Bool("err-equals-not_found", err == storage.ErrEntryNotFound).Send()
switch err {
case nil:
// 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
}
func ActorIdFromRequest(r *http.Request) (string, bool) {
id, ok := r.Context().Value(ContextKeyActorId).(string)
return id, ok
}
func NoteIdFromRequest(r *http.Request) string {
return r.PathValue("noteId")
}
@ -34,3 +39,16 @@ func NoteIdFromRequest(r *http.Request) string {
func AccountIdFromRequest(r *http.Request) string {
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
// Name of the role
Name string `gorm:"primaryKey"`
Name string `gorm:"primaryKey;unique"`
// Priority of the role
// 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
// Default priority for new roles is 1 to always overwrite default user
// 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)
// If yes, Name will be the id of the user in question
IsUserRole bool

View file

@ -69,7 +69,7 @@ var DefaultUserRole = Role{
// Role providing maximum permissions
var FullAdminRole = Role{
Name: "fullAdmin",
Priority: math.MaxUint,
Priority: math.MaxUint32,
IsUserRole: false,
IsBuiltIn: true,
@ -125,7 +125,7 @@ var FullAdminRole = Role{
// Role for totally freezing an account, blocking all activity from it
var AccountFreezeRole = Role{
Name: "accountFreeze",
Priority: math.MaxUint - 1,
Priority: math.MaxUint32 - 1,
IsUserRole: false,
IsBuiltIn: true,
@ -181,8 +181,64 @@ var AccountFreezeRole = Role{
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{
&DefaultUserRole,
&FullAdminRole,
&AccountFreezeRole,
&ServerActorRole,
}

View file

@ -53,8 +53,11 @@ func NewStorage(dbUrl string, cache *cache.Cache) (*Storage, error) {
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 {
return nil, err
return nil, fmt.Errorf("self insertion failed: %w", err)
}
return s, nil
@ -68,12 +71,15 @@ func (s *Storage) insertSelfFromConfig() error {
// Insert server info
serverData := RemoteServer{}
err = s.db.Where("id = 1").
// Set once on creation
Attrs(RemoteServer{
Domain: config.GlobalConfig.General.GetFullDomain(),
}).
// Set every time
Assign(RemoteServer{
IsSelf: true,
Name: config.GlobalConfig.Self.ServerDisplayName,
IsSelf: true,
Name: config.GlobalConfig.Self.ServerDisplayName,
ServerType: REMOTE_SERVER_LINSTROM,
// Icon: "", // TODO: Set to server icon media
}).FirstOrCreate(&serverData).Error
if err != nil {
@ -91,6 +97,7 @@ func (s *Storage) insertSelfFromConfig() error {
err = s.db.Where(Account{ID: ServerActorId}).
// Values to always (re)set after launch
Assign(Account{
Username: "self",
DisplayName: config.GlobalConfig.Self.ServerActorDisplayName,
// Server: serverData,
ServerId: serverData.ID,
@ -107,7 +114,7 @@ func (s *Storage) insertSelfFromConfig() error {
RestrictedFollow: false,
IdentifiesAs: []Being{},
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
Attrs(Account{
@ -120,3 +127,13 @@ func (s *Storage) insertSelfFromConfig() error {
}
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/rs/zerolog/log"
"gitlab.com/mstarongitlab/linstrom/ap"
"gitlab.com/mstarongitlab/linstrom/config"
"gorm.io/gorm"
)
@ -61,6 +60,9 @@ type Account struct {
// The roles assocciated with an account. Values are the names of the roles
Roles []string `gorm:"serializer:json"`
Location *string
Birthday *time.Time
// --- And internal account stuff ---
// 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
@ -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")
res := s.db.First(acc, id)
acc = &Account{ID: id}
res := s.db.First(acc)
if res.Error != nil {
if errors.Is(res.Error, gorm.ErrRecordNotFound) {
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
return s.FindAccountById(*cacheAccId)
} else {
if !errors.Is(err, errCacheNotFound) {
if err != errCacheNotFound {
log.Error().Err(err).Str("account-username", username).Msg("Problem while checking cache for account")
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
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{}
res := s.db.Where("username = ?", username).
Where("server = ?", config.GlobalConfig.General.GetFullDomain()).
Where("server_id = ?", serverSelf.ID).
First(&acc)
if res.Error != nil {
if errors.Is(res.Error, gorm.ErrRecordNotFound) {
@ -222,7 +218,7 @@ func (s *Storage) FindLocalAccountByUsername(username string) (*Account, error)
} else {
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")
if err = s.cache.Set(cacheUserIdToAccPrefix+acc.ID, &acc); err != nil {

View file

@ -1,5 +1,7 @@
package storage
import "gitlab.com/mstarongitlab/goutils/sliceutils"
// What kind of being a user identifies as
type Being string
@ -11,3 +13,9 @@ const (
BEING_ROBOT = Being("robot")
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))
}