meow
More progress. Fixed storage bug. Need to get media stuff going
This commit is contained in:
parent
1bb6cd8a70
commit
83f47d17be
11 changed files with 209 additions and 27 deletions
|
@ -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),
|
||||
|
|
|
@ -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) {}
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
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
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue