From 402932602dfb6f70968f7938edf2368581535cc4 Mon Sep 17 00:00:00 2001 From: mstar Date: Fri, 21 Feb 2025 15:52:21 +0100 Subject: [PATCH 1/5] WIP New auth management system Wait, what's this? A new commit to Linstrom? And I thought I was done for good with this project now that I've left Fedi. Well, I got bored at work and inspired by a random bit I've seen in Elixir Phoenix's docs. So here is the start of a new subsystem: Authentication Intended to bundle all authentication related checks and updates in one place. Http handlers should not be the ones to perform the logic, too much duplication. Technically, they probably shouldn't even contain any business logic at all, only calling it and transforming it into visible output Also may be considering switching to Vue or at least changing how the ember frontend interacts with the backend --- auth/auth.go | 11 +++++++++++ auth/checks.go | 26 ++++++++++++++++++++++++++ 2 files changed, 37 insertions(+) create mode 100644 auth/auth.go create mode 100644 auth/checks.go diff --git a/auth/auth.go b/auth/auth.go new file mode 100644 index 0000000..1787336 --- /dev/null +++ b/auth/auth.go @@ -0,0 +1,11 @@ +package auth + +import "gorm.io/gorm" + +type Authentication struct { + db *gorm.DB +} + +func NewAuth(db *gorm.DB) *Authentication { + return &Authentication{db} +} diff --git a/auth/checks.go b/auth/checks.go new file mode 100644 index 0000000..9d927a9 --- /dev/null +++ b/auth/checks.go @@ -0,0 +1,26 @@ +package auth + +import "git.mstar.dev/mstar/linstrom/storage" + +// Can actorId access the account with targetId? +func (a *Authentication) CanAccessAccount(actorId *string, targetId string) bool { return true } + +// Can actorId edit the account with targetId? +func (a *Authentication) CanEditAccount(actorId *string, targetIt *string) bool { return true } + +// Can actorId delete the account with targetId? +func (a *Authentication) CanDeleteAccount(actorId *string, targetIt *string) bool { return true } + +// Can actorId create a new post at all? +// Specific restrictions regarding the content are not checked +func (a *Authentication) CanCreatePost(actorId string) bool { return true } + +// Ensures that a given post conforms with all roles attached to the author account. +// Returns the conforming note (or nil of it can't be changed to conform) +// and whether the note was changed +func (a *Authentication) EnsureNoteConformsWithRoles(note *storage.Note) (*storage.Note, bool) { + return note, false +} + +// Does the given note conform with the roles attached to the author account? +func (a *Authentication) DoesNoteConform(note *storage.Note) bool { return true } From ab3051fa7870cdb2686b0877bb1ebc362ed94d8c Mon Sep 17 00:00:00 2001 From: mstar Date: Fri, 28 Feb 2025 15:01:12 +0100 Subject: [PATCH 2/5] More work on auth system I guess, still no motivation though --- README.md | 22 +++++++++- auth/auth.go | 12 ++++-- auth/checks.go | 102 ++++++++++++++++++++++++++++++++++++++++++++--- storage/roles.go | 8 +++- 4 files changed, 132 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 8513be3..7333691 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,8 @@ -Linstrom is a new social media server with the focus on providing users, moderators and admins alike -simple, yet powerful features to moderate the experience. +# Linstrom + +Linstrom is a new social media server with the focus on providing +users, moderators and admins alike simple, yet powerful features +to moderate the experience. ## Federation @@ -20,3 +23,18 @@ And they all have different woes with their software. I want to try and make a server that combines the strengths of various project's tooling while also hopefully solving some of the weaknesses. + +## Permission system + +All permissions operate on a role based system, similar to what Discord offers. +Accounts in power (admins and moderators) are only able to manipulate other accounts +via this role system. This prevents moderators from directly manipulating accounts, +such as changing their username, info fields or otherwise, while still providing +ample control over the activities these accounts can perform. + +These same roles, while most powerful for local accounts, also apply to remote accounts +as much as realisticly possible. Preventing a login on a remote server for example +might not be possible, but blocking all inbound traffic from that account is. + +However, roles are not only for moderators and admins to use. +Normal accounts can also make use of them, all be it in a more limited way. diff --git a/auth/auth.go b/auth/auth.go index 1787336..ffe194b 100644 --- a/auth/auth.go +++ b/auth/auth.go @@ -1,11 +1,17 @@ package auth -import "gorm.io/gorm" +import ( + "git.mstar.dev/mstar/linstrom/storage" + "gorm.io/gorm" +) type Authentication struct { + // For when in-depth access is needed db *gorm.DB + // Primary method to acquire account data + store *storage.Storage } -func NewAuth(db *gorm.DB) *Authentication { - return &Authentication{db} +func NewAuth(db *gorm.DB, store *storage.Storage) *Authentication { + return &Authentication{db, store} } diff --git a/auth/checks.go b/auth/checks.go index 9d927a9..05deb12 100644 --- a/auth/checks.go +++ b/auth/checks.go @@ -1,15 +1,107 @@ package auth -import "git.mstar.dev/mstar/linstrom/storage" +import ( + "git.mstar.dev/mstar/goutils/sliceutils" + "git.mstar.dev/mstar/linstrom/storage" + "github.com/rs/zerolog/log" +) -// Can actorId access the account with targetId? -func (a *Authentication) CanAccessAccount(actorId *string, targetId string) bool { return true } +// Can actorId read the account with targetId? +func (a *Authentication) CanReadAccount(actorId *string, targetId string) bool { + targetAccount, err := a.store.FindAccountById(targetId) + if err != nil { + if err == storage.ErrEntryNotFound { + return true + } + log.Error(). + Err(err). + Str("account-id", targetId). + Msg("Failed to receive account for permission check") + return false + } + if actorId == nil { + // TODO: Decide if roles should have a field to declare an account as follow only/hidden + // and then check for that flag here + return true + } + roles, err := a.store.FindRolesByNames(targetAccount.Roles) + if err != nil { + log.Error(). + Err(err). + Strs("role-names", targetAccount.Roles). + Msg("Failed to get roles for target account") + return false + } + combined := storage.CollapseRolesIntoOne(roles...) + if sliceutils.Contains(combined.BlockedUsers, *actorId) { + return false + } + return true +} // Can actorId edit the account with targetId? -func (a *Authentication) CanEditAccount(actorId *string, targetIt *string) bool { return true } +// If actorId is nil, it is assumed to be an anonymous user trying to edit the target account +// if targetId is nil, it is assumed that the actor is editing themselves +func (a *Authentication) CanEditAccount(actorId *string, targetId *string) bool { + // FIXME: This entire function feels wrong, idk + // Only the owner of an account should be able to edit said account's data + // But how do moderation actions play with this? Do they count as edit or as something separate? + if actorId == nil { + return false + } + if targetId == nil { + targetId = actorId + } + targetAccount, err := a.store.FindAccountById(*targetId) + if err != nil { + if err != storage.ErrEntryNotFound { + log.Error(). + Err(err). + Str("target-id", *targetId). + Msg("Failed to receive account for permission checks") + } + return false + } + if targetId == actorId { + targetRoles, err := a.store.FindRolesByNames(targetAccount.Roles) + if err != nil { + log.Error(). + Err(err). + Strs("role-names", targetAccount.Roles). + Msg("Failed to get roles from storage") + return false + } + combined := storage.CollapseRolesIntoOne(targetRoles...) + return *combined.CanLogin + } else { + return false + } +} // Can actorId delete the account with targetId? -func (a *Authentication) CanDeleteAccount(actorId *string, targetIt *string) bool { return true } +// If actorId is nil, it is assumed to be an anonymous user trying to delete the target account +// if targetId is nil, it is assumed that the actor is deleting themselves +func (a *Authentication) CanDeleteAccount(actorId *string, targetId *string) bool { + if actorId == nil { + return false + } + acc, err := a.store.FindAccountById(*actorId) + if err != nil { + // TODO: Logging + return false + } + roles, err := a.store.FindRolesByNames(acc.Roles) + if err != nil { + // TODO: Logging + return false + } + collapsed := storage.CollapseRolesIntoOne(roles...) + if targetId == nil { + return *collapsed.CanLogin + } else { + return *collapsed.CanDeleteAccounts + } +} // Can actorId create a new post at all? // Specific restrictions regarding the content are not checked diff --git a/storage/roles.go b/storage/roles.go index 9bb8a42..2f50d37 100644 --- a/storage/roles.go +++ b/storage/roles.go @@ -1,9 +1,9 @@ package storage import ( - "github.com/rs/zerolog/log" "git.mstar.dev/mstar/goutils/sliceutils" "git.mstar.dev/mstar/linstrom/util" + "github.com/rs/zerolog/log" "gorm.io/gorm" ) @@ -80,7 +80,10 @@ type Role struct { // Internal ids of accounts blocked by this role BlockedUsers []string `gorm:"type:bytes;serializer:gob"` // Local CanSubmitReports *bool // Local & remote - CanLogin *bool // Local + // If disabled, an account can no longer be interacted with. The owner can no longer change anything about it + // And the UI will show a notice (and maybe include that info in the AP data too) + // Only moderators and admins will be able to edit the account's roles + CanLogin *bool // Local CanMentionOthers *bool // Local & remote HasMentionCountLimit *bool // Local & remote @@ -125,6 +128,7 @@ type Role struct { CanManageAvatarDecorations *bool // Local CanManageAds *bool // Local CanSendAnnouncements *bool // Local + CanDeleteAccounts *bool // Local } /* From a2156afe9d142268f24acd69757e5db8e66565f3 Mon Sep 17 00:00:00 2001 From: mStar Date: Thu, 20 Mar 2025 18:00:59 +0100 Subject: [PATCH 3/5] Update contributing guide Not done though, needs to include proper information --- CONTRIBUTING.md | 55 +++++++++++++++++++++++++++++++++++++------------ 1 file changed, 42 insertions(+), 13 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 199b639..f7e8031 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,28 +1,50 @@ # Contribution Guide -Thank you for your interest in contributing to Linstrom! All contributors are welcome, regardless of their level of experience. +Thank you for your interest in contributing to Linstrom! +All contributors are welcome, regardless of their level of experience. + +Unless it's primarely AI generated. Then you're going to be blocked immediately +and publicly shamed. ## Bug Reports -Use the [bug report issue template](https://git.mstar.dev/mstar/linstrom/issues/new?template=bug-report.md) to file a bug report. Please include a detailed description of the events leading up to the problem, your system configuration, and the program logs. If you're able to reproduce the bug reliably, attaching a debugger to the program, triggering it, and uploading the results would be very helpful. +Use the [bug report issue template](https://git.mstar.dev/mstar/linstrom/issues/new?template=bug-report.md) +to file a bug report. Please include a detailed description of the events +leading up to the problem, your system configuration, and the program logs. +If you're able to reproduce the bug reliably, attaching a debugger to the program, +triggering it, and uploading the results would be very helpful. -This section *should* tell you how to find your logs, attach the debugger, and do whatever else you need for a detailed bug report. But nobody filled it out. Attach a picture of Goatse to your bug reports until we fix this. +This section _should_ tell you how to find your logs, attach the debugger, +and do whatever else you need for a detailed bug report. But nobody filled it out. +Attach a picture of Goatse to your bug reports until we fix this. ## Feature Requests -Use the [feature request issue template](https://git.mstar.dev/mstar/linstrom/issues/new?template=suggestion.md) to suggest new features. Please note that we haven't replaced this placeholder text with the actual criteria we're looking for, which means you should spam us with utterly nonsensical ideas. +Use the [feature request issue template](https://git.mstar.dev/mstar/linstrom/issues/new?template=suggestion.md) +to suggest new features. Please note that we haven't replaced this placeholder +text with the actual criteria we're looking for, which means +you should spam us with utterly nonsensical ideas. ## Submitting Translations -Translation files are part of the project codebase, so you'll have to fork the repository and file a pull request (see [Contributing Code](CONTRIBUTING.md#contributing-code) below). You don't need any programming knowledge to edit the translation files, though. +Translation files are part of the project codebase, so you'll have to +fork the repository and file a pull request +(see [Contributing Code](CONTRIBUTING.md#contributing-code) below). +You don't need any programming knowledge to edit the translation files, though. -This should have been removed and replaced with a quick overview of where the files are and what translators need to do in order to edit them. Nobody did that, so think of this as a free pass to scream profanities into the issue tracker in your native language. +This should have been removed and replaced with a quick overview +of where the files are and what translators need to do in order to edit them. +Nobody did that, so think of this as a free pass to scream profanities +into the issue tracker in your native language. ## Contributing Code ### Forking -If you'd like to have a go at writing some code for Linstrom, fork the repository, then create a new branch with a name that describes the changes you're making. If there's a [relevant issue](https://git.mstar.dev/mstar/linstrom/issues), include the issue number in the branch name: +If you'd like to have a go at writing some code for Linstrom, +fork the repository, then create a new branch with a name that +describes the changes you're making. If there's a [relevant issue](https://git.mstar.dev/mstar/linstrom/issues), +include the issue number in the branch name: ```sh git checkout -b 1337-prevent-computer-from-exploding @@ -30,10 +52,14 @@ git checkout -b 1337-prevent-computer-from-exploding ### Development Environment -The project utilises Go (version 1.23+) for almost everything and node/npm (version 18+) for building the frontend. -The go side also makes use of `go generate` for multiple things, primarely in the storage module +The project utilises Go (version 1.23+) for almost everything and +node/npm (version 18+) for building the frontend. +The go side also makes use of `go generate` for multiple things, +primarely in the storage module -We don't have a development environment, because nobody bothered to fill this out. Please add a new build system to the project specifically for your modifications. Bonus points if it's entirely nonsensical, like `npm` in a C project. +We don't have a development environment, because nobody bothered to fill this out. +Please add a new build system to the project specifically for your modifications. +Bonus points if it's entirely nonsensical, like `npm` in a C project. ### Code Style @@ -43,15 +69,18 @@ For anything node: uhhh, yolo, idk yet ### Pull Requests -Once your modifications are complete, you'll want to fetch the latest changes from this repository, rebase your branch, and publish your changes: +Once your modifications are complete, you'll want to fetch the latest changes +from this repository, rebase your branch, and publish your changes: ```sh git remote add upstream https://git.mstar.dev/mstar/linstrom.git git checkout main git pull upstream main git checkout 1337-prevent-computer-from-exploding -git rebase master +git rebase main git push --set-upstream origin 1337-prevent-computer-from-exploding ``` -Finally, you can [create a pull request](https://git.mstar.dev/mstar/linstrom/pulls). It might not get approved, or you might have to make some additional changes to your code - but don't give up! +Finally, you can [create a pull request](https://git.mstar.dev/mstar/linstrom/pulls). +It might not get approved, or you might have to make some additional changes +to your code - but don't give up! From 0639cde4f253675b756e49f3b098c89f86bd76ad Mon Sep 17 00:00:00 2001 From: mStar Date: Thu, 20 Mar 2025 19:22:15 +0100 Subject: [PATCH 4/5] Delete useless file from storage --- storage/userRestrictionType.go | 34 ---------------------------------- 1 file changed, 34 deletions(-) delete mode 100644 storage/userRestrictionType.go diff --git a/storage/userRestrictionType.go b/storage/userRestrictionType.go deleted file mode 100644 index 543a544..0000000 --- a/storage/userRestrictionType.go +++ /dev/null @@ -1,34 +0,0 @@ -package storage - -type AccountRestriction int64 - -const ( - // Account has no restrictions applied - ACCOUNT_RESTRICTION_NONE = AccountRestriction(0) - // All messages of the account get a content warning applied if none is set - // Warning could be something like "Message auto-CWd by server" - ACCOUNT_RESTRICTION_AUTO_CW = AccountRestriction(1 << iota) - // Disable accessing the account via login or access token - ACCOUNT_RESTRICTION_DISABLE_LOGIN - // Disable sending activities to other servers if the account is local - // Or reject all activities if the account is remote - ACCOUNT_RESTRICTION_NO_FEDERATION - // Disallow sending direct messages from that account - ACCOUNT_RESTRICTION_NO_DMS - // Disallow sending follower only messages from that account - ACCOUNT_RESTRICTION_NO_FOLLOWER_POSTS - // Disable outbound follow requests (restricted account can't send follow requests) - ACCOUNT_RESTRICTION_DISABLE_OUTBOUND_FOLLOWS - // Disable inbound follow requests (all follow requests to that account are automatically rejected) - ACCOUNT_RESTRICTION_DISABLE_INBOUND_FOLLOWS - // Forces all posts by that account to be follower only - ACCOUNT_RESTRICTION_FORCE_FOLLOWERS_ONLY - // Disable all outbound activities of an account. - // Includes sending, updating or deleting own notes - // as well as boosting or reacting to any notes - ACCOUNT_RESTRICTION_DISABLE_ACTIVITIES - // Account can only be viewed while logged in or via verified requests to the AP endpoints - ACCOUNT_RESTRICTIONS_NO_PUBLIC_ACCESS - // Force tag all media the account posts as sensitive - ACCOUNT_RESTRICTIONS_FORCE_MEDIA_SENSITIVE -) From 714f5286410d82399f54cd3cbca2d28d9141550f Mon Sep 17 00:00:00 2001 From: mStar Date: Thu, 20 Mar 2025 21:39:10 +0100 Subject: [PATCH 5/5] Rework backing storage system Step one: Copy the struct definitions over into a new, dedicated submodule Step two: Make a generator script Step three: Define helper functions for various queries --- storage-new/gormLogger.go | 64 +++++ storage-new/migrations.go | 102 ++++++++ storage-new/models/Emote.go | 12 + storage-new/models/MediaMetadata.go | 29 +++ storage-new/models/Note.go | 33 +++ storage-new/models/NoteAccessLevelType.go | 28 ++ storage-new/models/NoteAttachments.go | 6 + storage-new/models/NoteEmotes.go | 6 + storage-new/models/NotePings.go | 6 + storage-new/models/NoteTags.go | 6 + storage-new/models/Reaction.go | 10 + storage-new/models/RemoteServer.go | 12 + .../models/RemoteServerSoftwareType.go | 37 +++ storage-new/models/Role.go | 182 +++++++++++++ storage-new/models/RolesDefaults.go | 240 ++++++++++++++++++ storage-new/models/User.go | 53 ++++ storage-new/models/UserAuthentication.go | 7 + .../models/UserAuthenticationMethod.go | 26 ++ storage-new/models/UserBeingType.go | 27 ++ storage-new/models/UserBeings.go | 6 + storage-new/models/UserInfoField.go | 19 ++ storage-new/models/UserRelation.go | 7 + storage-new/models/UserRelationType.go | 31 +++ storage-new/models/UserRemote.go | 21 ++ storage-new/models/UserRoles.go | 6 + storage-new/models/UserTags.go | 6 + storage-new/storage.go | 1 + 27 files changed, 983 insertions(+) create mode 100644 storage-new/gormLogger.go create mode 100644 storage-new/migrations.go create mode 100644 storage-new/models/Emote.go create mode 100644 storage-new/models/MediaMetadata.go create mode 100644 storage-new/models/Note.go create mode 100644 storage-new/models/NoteAccessLevelType.go create mode 100644 storage-new/models/NoteAttachments.go create mode 100644 storage-new/models/NoteEmotes.go create mode 100644 storage-new/models/NotePings.go create mode 100644 storage-new/models/NoteTags.go create mode 100644 storage-new/models/Reaction.go create mode 100644 storage-new/models/RemoteServer.go create mode 100644 storage-new/models/RemoteServerSoftwareType.go create mode 100644 storage-new/models/Role.go create mode 100644 storage-new/models/RolesDefaults.go create mode 100644 storage-new/models/User.go create mode 100644 storage-new/models/UserAuthentication.go create mode 100644 storage-new/models/UserAuthenticationMethod.go create mode 100644 storage-new/models/UserBeingType.go create mode 100644 storage-new/models/UserBeings.go create mode 100644 storage-new/models/UserInfoField.go create mode 100644 storage-new/models/UserRelation.go create mode 100644 storage-new/models/UserRelationType.go create mode 100644 storage-new/models/UserRemote.go create mode 100644 storage-new/models/UserRoles.go create mode 100644 storage-new/models/UserTags.go create mode 100644 storage-new/storage.go diff --git a/storage-new/gormLogger.go b/storage-new/gormLogger.go new file mode 100644 index 0000000..ed8f976 --- /dev/null +++ b/storage-new/gormLogger.go @@ -0,0 +1,64 @@ +package storage + +import ( + "context" + "time" + + "github.com/rs/zerolog" + "gorm.io/gorm/logger" +) + +type gormLogger struct { + logger zerolog.Logger +} + +func newGormLogger(zerologger zerolog.Logger) *gormLogger { + return &gormLogger{zerologger} +} + +func (g *gormLogger) LogMode(newLevel logger.LogLevel) logger.Interface { + switch newLevel { + case logger.Error: + g.logger = g.logger.Level(zerolog.ErrorLevel) + case logger.Warn: + g.logger = g.logger.Level(zerolog.WarnLevel) + case logger.Info: + g.logger = g.logger.Level(zerolog.InfoLevel) + case logger.Silent: + g.logger = g.logger.Level(zerolog.Disabled) + } + return g +} +func (g *gormLogger) Info(ctx context.Context, format string, args ...interface{}) { + g.logger.Info().Ctx(ctx).Msgf(format, args...) +} +func (g *gormLogger) Warn(ctx context.Context, format string, args ...interface{}) { + g.logger.Warn().Ctx(ctx).Msgf(format, args...) +} +func (g *gormLogger) Error(ctx context.Context, format string, args ...interface{}) { + g.logger.Error().Ctx(ctx).Msgf(format, args...) +} + +func (g *gormLogger) Trace( + ctx context.Context, + begin time.Time, + fc func() (sql string, rowsAffected int64), + err error, +) { + sql, rowsAffected := fc() + g.logger.Trace(). + Ctx(ctx). + Time("gorm-begin", begin). + Err(err). + Str("gorm-query", sql). + Int64("gorm-rows-affected", rowsAffected). + Send() +} + +func (g *gormLogger) OverwriteLoggingLevel(new zerolog.Level) { + g.logger = g.logger.Level(new) +} + +func (g *gormLogger) OverwriteLogger(new zerolog.Logger) { + g.logger = new +} diff --git a/storage-new/migrations.go b/storage-new/migrations.go new file mode 100644 index 0000000..fb6d7a7 --- /dev/null +++ b/storage-new/migrations.go @@ -0,0 +1,102 @@ +package storage + +import ( + "fmt" + "strings" + + "git.mstar.dev/mstar/goutils/sliceutils" + "gorm.io/gorm" + + "git.mstar.dev/mstar/linstrom/storage-new/models" +) + +func migrateTypes(db *gorm.DB) error { + if err := db.AutoMigrate( + &models.Emote{}, + &models.MediaMetadata{}, + &models.Note{}, + &models.NoteToAttachment{}, + &models.NoteToEmote{}, + &models.NoteToPing{}, + &models.NoteTag{}, + &models.Reaction{}, + &models.RemoteServer{}, + &models.Role{}, + &models.User{}, + &models.UserAuthMethod{}, + &models.UserBeings{}, + &models.UserInfoField{}, + &models.UserRelation{}, + &models.UserRemoteLinks{}, + &models.UserRole{}, + &models.UserTag{}, + ); err != nil { + return fmt.Errorf("storage: automigrate structs: %w", err) + } + return nil +} + +// Ensure the being enum exists for the user +func createBeingType(db *gorm.DB) error { + return migrateEnum( + db, + "being_type", + sliceutils.Map(models.AllBeings, func(t models.BeingType) string { return string(t) }), + ) +} + +func createAccountRelationType(db *gorm.DB) error { + return migrateEnum( + db, + "relation_type", + sliceutils.Map( + models.AllRelations, + func(t models.RelationType) string { return string(t) }, + ), + ) +} + +func createAccountAuthMethodType(db *gorm.DB) error { + return migrateEnum( + db, + "auth_method_type", + sliceutils.Map( + models.AllAuthMethods, + func(t models.AuthenticationMethodType) string { return string(t) }, + ), + ) +} + +func createRemoteServerSoftwareType(db *gorm.DB) error { + return migrateEnum( + db, + "server_software_type", + sliceutils.Map( + models.AllServerSoftwareTypes, + func(t models.ServerSoftwareType) string { return string(t) }, + ), + ) +} + +// Helper function for ensuring the existence of an enum with the given values +func migrateEnum(db *gorm.DB, name string, values []string) error { + if err := db.Exec("DROP TYPE IF EXISTS " + name).Error; err != nil { + return fmt.Errorf("storage: migrate %s: %w", name, err) + } + queryBuilder := strings.Builder{} + queryBuilder.WriteString("CREATE TYPE") + queryBuilder.WriteString(name) + queryBuilder.WriteString("AS ENUM (") + blen := len(values) + for i, btype := range values { + queryBuilder.WriteString("'" + string(btype) + "'") + // Append comma everywhere except last entry + if i+1 < blen { + queryBuilder.WriteString(",") + } + } + if err := db.Exec(queryBuilder.String()).Error; err != nil { + return fmt.Errorf("storage: migrate %s: %w", name, err) + } + return nil +} diff --git a/storage-new/models/Emote.go b/storage-new/models/Emote.go new file mode 100644 index 0000000..3e28ff9 --- /dev/null +++ b/storage-new/models/Emote.go @@ -0,0 +1,12 @@ +package models + +import "gorm.io/gorm" + +type Emote struct { + gorm.Model + // Metadata MediaMetadata // `gorm:"foreignKey:MetadataId"` + MetadataId string + Name string + // Server RemoteServer // `gorm:"foreignKey:ServerId;references:ID"` + ServerId uint +} diff --git a/storage-new/models/MediaMetadata.go b/storage-new/models/MediaMetadata.go new file mode 100644 index 0000000..a411c81 --- /dev/null +++ b/storage-new/models/MediaMetadata.go @@ -0,0 +1,29 @@ +package models + +import ( + "time" + + "gorm.io/gorm" +) + +type MediaMetadata struct { + ID string `gorm:"primarykey"` // The unique ID of this media file + CreatedAt time.Time // When this entry was created + UpdatedAt time.Time // When this entry was last updated + // When this entry was deleted (for soft deletions) + // Soft delete means that this entry still exists in the db, but gorm won't include it anymore unless specifically told to + // If not null, this entry is marked as deleted + DeletedAt gorm.DeletedAt `gorm:"index"` + OwnedBy string // Account id this media belongs to + Remote bool // whether the attachment is a remote one + // Where the media is stored. Url + Location string + Type string // What media type this is following mime types, eg image/png + // Name of the file + // Could be .png, .webp for example. Or the name the file was uploaded with + Name string + // Alternative description of the media file's content + AltText string + // Whether the media is to be blurred by default + Blurred bool +} diff --git a/storage-new/models/Note.go b/storage-new/models/Note.go new file mode 100644 index 0000000..a236244 --- /dev/null +++ b/storage-new/models/Note.go @@ -0,0 +1,33 @@ +package models + +import ( + "time" + + "gorm.io/gorm" +) + +// Data defined in extra structs: +// - Attachments: models.NoteToAttachment +// - Emotes: models.NoteToEmote +// - Pings: models.NoteToPing +// - Tags: models.NoteTag +type Note struct { + ID string `gorm:"primarykey"` // Make ID a string (uuid) for other implementations + CreatedAt time.Time // When this entry was created + UpdatedAt time.Time // When this entry was last updated + // When this entry was deleted (for soft deletions) + // Soft delete means that this entry still exists in the db, but gorm won't include it anymore unless specifically told to + // If not null, this entry is marked as deleted + DeletedAt gorm.DeletedAt `gorm:"index"` + // Creator Account // `gorm:"foreignKey:CreatorId;references:ID"` // Account that created the post + CreatorId string + Remote bool // Whether the note is originally a remote one and just "cached" + // Raw content of the note. So without additional formatting applied + // Might already have formatting applied beforehand from the origin server + RawContent string + ContentWarning *string // Content warnings of the note, if it contains any + RepliesTo *string // Url of the message this replies to + Quotes *string // url of the message this note quotes + AccessLevel NoteAccessLevel // Where to send this message to (public, home, followers, dm) + OriginServer string // Url of the origin server. Also the primary key for those +} diff --git a/storage-new/models/NoteAccessLevelType.go b/storage-new/models/NoteAccessLevelType.go new file mode 100644 index 0000000..15a725f --- /dev/null +++ b/storage-new/models/NoteAccessLevelType.go @@ -0,0 +1,28 @@ +package models + +import "database/sql/driver" + +type NoteAccessLevel uint8 + +const ( + // The note is intended for the public + NOTE_TARGET_PUBLIC NoteAccessLevel = 0 + // The note is intended only for the home screen + // not really any idea what the difference is compared to public + // Maybe home notes don't show up on the server feed but still for everyone's home feed if it reaches them via follow or boost + NOTE_TARGET_HOME NoteAccessLevel = 1 << iota + // The note is intended only for followers + NOTE_TARGET_FOLLOWERS + // The note is intended only for a DM to one or more targets + NOTE_TARGET_DM +) + +// Converts the NoteTarget value into a type the DB can use +func (n *NoteAccessLevel) Value() (driver.Value, error) { + return n, nil +} + +func (ct *NoteAccessLevel) Scan(value any) error { + *ct = NoteAccessLevel(value.(uint8)) + return nil +} diff --git a/storage-new/models/NoteAttachments.go b/storage-new/models/NoteAttachments.go new file mode 100644 index 0000000..e29585f --- /dev/null +++ b/storage-new/models/NoteAttachments.go @@ -0,0 +1,6 @@ +package models + +type NoteToAttachment struct { + UserId string + AttachmentId string +} diff --git a/storage-new/models/NoteEmotes.go b/storage-new/models/NoteEmotes.go new file mode 100644 index 0000000..124f1dc --- /dev/null +++ b/storage-new/models/NoteEmotes.go @@ -0,0 +1,6 @@ +package models + +type NoteToEmote struct { + UserId string + EmoteId string +} diff --git a/storage-new/models/NotePings.go b/storage-new/models/NotePings.go new file mode 100644 index 0000000..5e7e90b --- /dev/null +++ b/storage-new/models/NotePings.go @@ -0,0 +1,6 @@ +package models + +type NoteToPing struct { + UserId string + PingTargetId string +} diff --git a/storage-new/models/NoteTags.go b/storage-new/models/NoteTags.go new file mode 100644 index 0000000..31ee396 --- /dev/null +++ b/storage-new/models/NoteTags.go @@ -0,0 +1,6 @@ +package models + +type NoteTag struct { + UserId string + Tag string +} diff --git a/storage-new/models/Reaction.go b/storage-new/models/Reaction.go new file mode 100644 index 0000000..43633cb --- /dev/null +++ b/storage-new/models/Reaction.go @@ -0,0 +1,10 @@ +package models + +import "gorm.io/gorm" + +type Reaction struct { + gorm.Model + NoteId string + ReactorId string + EmoteId uint +} diff --git a/storage-new/models/RemoteServer.go b/storage-new/models/RemoteServer.go new file mode 100644 index 0000000..af063ed --- /dev/null +++ b/storage-new/models/RemoteServer.go @@ -0,0 +1,12 @@ +package models + +import "gorm.io/gorm" + +type RemoteServer struct { + gorm.Model + ServerType ServerSoftwareType // What software the server is running. Useful for formatting + Domain string // `gorm:"primaryKey"` // Domain the server exists under. Additional primary key + Name string // What the server wants to be known as (usually same as url) + Icon string // ID of a media file + IsSelf bool // Whether this server is yours truly +} diff --git a/storage-new/models/RemoteServerSoftwareType.go b/storage-new/models/RemoteServerSoftwareType.go new file mode 100644 index 0000000..f0834f2 --- /dev/null +++ b/storage-new/models/RemoteServerSoftwareType.go @@ -0,0 +1,37 @@ +package models + +import ( + "database/sql/driver" +) + +type ServerSoftwareType string + +const ( + // Includes forks like glitch-soc, etc + ServerSoftwareMastodon = ServerSoftwareType("Mastodon") + // Includes forks like Ice Shrimp, Sharkey, Cutiekey, etc + ServerSoftwareMisskey = ServerSoftwareType("Misskey") + // Includes Akkoma + ServerSoftwarePlemora = ServerSoftwareType("Plemora") + // Wafrn is a new entry + ServerSoftwareWafrn = ServerSoftwareType("Wafrn") + // And of course, yours truly + ServerSoftwareLinstrom = ServerSoftwareType("Linstrom") +) + +var AllServerSoftwareTypes = []ServerSoftwareType{ + ServerSoftwareMastodon, + ServerSoftwareMisskey, + ServerSoftwarePlemora, + ServerSoftwareWafrn, + ServerSoftwareLinstrom, +} + +func (r *ServerSoftwareType) Value() (driver.Value, error) { + return r, nil +} + +func (ct *ServerSoftwareType) Scan(value any) error { + *ct = ServerSoftwareType(value.(string)) + return nil +} diff --git a/storage-new/models/Role.go b/storage-new/models/Role.go new file mode 100644 index 0000000..8a60476 --- /dev/null +++ b/storage-new/models/Role.go @@ -0,0 +1,182 @@ +package models + +import ( + "gorm.io/gorm" +) + +// Could I collapse all these go:generate command into more condensed ones? +// Yes +// Will I do that? +// No +// This is explicit in what is being done. And easier to understand + +//go:generate go build -o RolesGenerator ../cmd/RolesGenerator/main.go +//go:generate ./RolesGenerator -input=roles.go -output=rolesUtil_generated.go +//go:generate rm RolesGenerator + +//go:generate go build -o ApiGenerator ../cmd/RolesApiTypeGenerator/main.go +//go:generate ./ApiGenerator -input=roles.go -output=../server/apiLinstromTypes_generated.go +//go:generate rm ApiGenerator + +//go:generate go build -o HelperGenerator ../cmd/RolesApiConverter/main.go +//go:generate ./HelperGenerator -input=roles.go -output=../server/apiLinstromTypeHelpers_generated.go +//go:generate rm HelperGenerator + +//go:generate go build -o FrontendGenerator ../cmd/RolesFrontendGenerator/main.go +//go:generate ./FrontendGenerator -input=roles.go -output=../frontend-reactive/app/models/role.ts +//go:generate rm FrontendGenerator + +// A role is, in concept, similar to how Discord handles roles +// Some permission can be either disallowed (&false), don't care (nil) or allowed (&true) +// Don't care just says to use the value from the next lower role where it is set +// Blocks are part of the user relations +type Role struct { + // TODO: More control options + // Extend upon whatever Masto, Akkoma and Misskey have + // Lots of details please + + // --- Role metadata --- + + // Include full db model stuff + gorm.Model + + // Name of the role + 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 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 + + // Whether this role is one built into Linstrom from the start or not + // Note: Built-in roles can't be modified + IsBuiltIn bool + + // --- User permissions --- + CanSendMedia *bool // Local & remote + CanSendCustomEmotes *bool // Local & remote + CanSendCustomReactions *bool // Local & remote + CanSendPublicNotes *bool // Local & remote + CanSendLocalNotes *bool // Local & remote + CanSendFollowerOnlyNotes *bool // Local & remote + CanSendPrivateNotes *bool // Local & remote + CanSendReplies *bool // Local & remote + CanQuote *bool // Local only + CanBoost *bool // Local only + CanIncludeLinks *bool // Local & remote + CanIncludeSurvey *bool // Local + CanFederateFedi *bool // Local & remote + CanFederateBsky *bool // Local + + CanChangeDisplayName *bool // Local + + CanSubmitReports *bool // Local & remote + // If disabled, an account can no longer be interacted with. The owner can no longer change anything about it + // And the UI will show a notice (and maybe include that info in the AP data too) + // Only moderators and admins will be able to edit the account's roles + CanLogin *bool // Local + + CanMentionOthers *bool // Local & remote + HasMentionCountLimit *bool // Local & remote + MentionLimit *uint32 // Local & remote + + // CanViewBoosts *bool + // CanViewQuotes *bool + // CanViewMedia *bool + // CanViewCustomEmotes *bool + + // --- Automod --- + AutoNsfwMedia *bool // Local & remote + AutoCwPosts *bool // Local & remote + AutoCwPostsText *string // Local & remote + ScanCreatedPublicNotes *bool // Local & remote + ScanCreatedLocalNotes *bool // Local & remote + ScanCreatedFollowerOnlyNotes *bool // Local & remote + ScanCreatedPrivateNotes *bool // Local & remote + // Blocks all interactions and federation between users with the role and all included ids/handles + // TODO: Decide whether this is a list of handles or of account ids + // Handles would increase the load due to having to search for them first + // while ids would require to store every single account mentioned + // which could cause escalating storage costs + DisallowInteractionsWith []string `gorm:"type:bytes;serializer:gob"` // Local & remote + + WithholdNotesForManualApproval *bool // Local & remote + WithholdNotesBasedOnRegex *bool // Local & remote + WithholdNotesRegexes []string `gorm:"type:bytes;serializer:gob"` // Local & remote + + // --- Admin perms --- + // If set, counts as all permissions being set as given and all restrictions being disabled + FullAdmin *bool // Local + CanAffectOtherAdmins *bool // Local + CanDeleteNotes *bool // Local + CanConfirmWithheldNotes *bool // Local + CanAssignRoles *bool // Local + CanSupressInteractionsBetweenUsers *bool // Local + CanOverwriteDisplayNames *bool // Local + CanManageCustomEmotes *bool // Local + CanViewDeletedNotes *bool // Local + CanRecoverDeletedNotes *bool // Local + CanManageAvatarDecorations *bool // Local + CanManageAds *bool // Local + CanSendAnnouncements *bool // Local + CanDeleteAccounts *bool // Local +} + +/* +Mastodon permissions (highest permission to lowest): +- Admin +- Devops +- View Audit log +- View Dashboard +- Manage Reports +- Manage Federation +- Manage Settings +- Manage Blocks +- Manage Taxonomies +- Manage Appeals +- Manage Users +- Manage Invites +- Manage Rules +- Manage Announcements +- Manage Custom Emojis +- Manage Webhooks +- Invite Users +- Manage Roles +- Manage User Access +- Delete User Data +*/ + +/* +Misskey "permissions" (no order): +- Global timeline available (interact with global timeline I think) +- Local timeline available (same as global, but for local) +- b-something timeline available +- Can send public notes +- How many mentions a note can have +- Can invite others +- How many invites can be sent +- InviteLimitCycle (whatever that means) +- Invite Expiration time (duration of how long invites stay valid I think) +- Manage custom emojis +- Manage custom avatar decorations +- Seach for notes +- Use translator +- Hide ads from self +- How much storage space the user has +- Whether to mark all posts from account as nsfw +- max number of pinned messages +- max number of antennas +- max number of muted words +- max number of webhooks +- max number of clips +- max number of notes contained in a clip (? I think. Don't know enough about clips) +- Max number of lists of users +- max number of users in a user list +- rate limit multiplier +- max number of applied avatar decorations +*/ diff --git a/storage-new/models/RolesDefaults.go b/storage-new/models/RolesDefaults.go new file mode 100644 index 0000000..fc1b746 --- /dev/null +++ b/storage-new/models/RolesDefaults.go @@ -0,0 +1,240 @@ +package models + +import ( + "math" + + "git.mstar.dev/mstar/goutils/other" +) + +// Default role every user has. Defines sane defaults for a normal user +// Will get overwritten by just about every other role due to every other role having higher priority +var DefaultUserRole = Role{ + Name: "Default", + Priority: 0, + IsUserRole: false, + 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), + CanSendReplies: other.IntoPointer(true), + CanQuote: other.IntoPointer(true), + CanBoost: other.IntoPointer(true), + CanIncludeLinks: other.IntoPointer(true), + CanIncludeSurvey: other.IntoPointer(true), + CanFederateFedi: other.IntoPointer(true), + CanFederateBsky: other.IntoPointer(true), + + CanChangeDisplayName: other.IntoPointer(true), + + 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(false), + CanAffectOtherAdmins: other.IntoPointer(false), + CanDeleteNotes: other.IntoPointer(false), + CanConfirmWithheldNotes: other.IntoPointer(false), + CanAssignRoles: other.IntoPointer(false), + CanSupressInteractionsBetweenUsers: other.IntoPointer(false), + CanOverwriteDisplayNames: other.IntoPointer(false), + CanManageCustomEmotes: other.IntoPointer(false), + CanViewDeletedNotes: other.IntoPointer(false), + CanRecoverDeletedNotes: other.IntoPointer(false), + CanManageAvatarDecorations: other.IntoPointer(false), + CanManageAds: other.IntoPointer(false), + CanSendAnnouncements: other.IntoPointer(false), +} + +// Role providing maximum permissions +var FullAdminRole = Role{ + Name: "fullAdmin", + Priority: math.MaxUint32, + IsUserRole: false, + 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), + + 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), +} + +// Role for totally freezing an account, blocking all activity from it +var AccountFreezeRole = Role{ + Name: "accountFreeze", + Priority: math.MaxUint32 - 1, + IsUserRole: false, + IsBuiltIn: true, + + CanSendMedia: other.IntoPointer(false), + CanSendCustomEmotes: other.IntoPointer(false), + CanSendCustomReactions: other.IntoPointer(false), + CanSendPublicNotes: other.IntoPointer(false), + CanSendLocalNotes: other.IntoPointer(false), + CanSendFollowerOnlyNotes: other.IntoPointer(false), + CanSendPrivateNotes: other.IntoPointer(false), + CanSendReplies: other.IntoPointer(false), + CanQuote: other.IntoPointer(false), + CanBoost: other.IntoPointer(false), + CanIncludeLinks: other.IntoPointer(false), + CanIncludeSurvey: other.IntoPointer(false), + CanFederateBsky: other.IntoPointer(false), + CanFederateFedi: other.IntoPointer(false), + + CanChangeDisplayName: other.IntoPointer(false), + + CanSubmitReports: other.IntoPointer(false), + CanLogin: other.IntoPointer(false), + + CanMentionOthers: other.IntoPointer(false), + 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(true), + AutoCwPosts: other.IntoPointer(false), + AutoCwPostsText: other.IntoPointer("Account frozen"), + WithholdNotesForManualApproval: other.IntoPointer(true), + ScanCreatedPublicNotes: other.IntoPointer(false), + ScanCreatedLocalNotes: other.IntoPointer(false), + ScanCreatedFollowerOnlyNotes: other.IntoPointer(false), + ScanCreatedPrivateNotes: other.IntoPointer(false), + DisallowInteractionsWith: []string{}, + + FullAdmin: other.IntoPointer(false), + CanAffectOtherAdmins: other.IntoPointer(false), + CanDeleteNotes: other.IntoPointer(false), + CanConfirmWithheldNotes: other.IntoPointer(false), + CanAssignRoles: other.IntoPointer(false), + CanSupressInteractionsBetweenUsers: other.IntoPointer(false), + CanOverwriteDisplayNames: other.IntoPointer(false), + CanManageCustomEmotes: other.IntoPointer(false), + CanViewDeletedNotes: other.IntoPointer(false), + CanRecoverDeletedNotes: other.IntoPointer(false), + CanManageAvatarDecorations: other.IntoPointer(false), + CanManageAds: 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), + + 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, +} diff --git a/storage-new/models/User.go b/storage-new/models/User.go new file mode 100644 index 0000000..80853a9 --- /dev/null +++ b/storage-new/models/User.go @@ -0,0 +1,53 @@ +package models + +import ( + "time" + + "gorm.io/gorm" +) + +// Data stored in external types: +// - Custom info fields +// - Being types +// - Tags +// - Relations +// - Pronouns +// - Roles +// - AP remote links +// - Auth methods and tokens (hashed pw, totp key, passkey id) +type User struct { + ID string `gorm:"primarykey"` // ID is a uuid for this account + // Username of the user (eg "max" if the full username is @max@example.com) + // Assume unchangable (once set by a user) to be kind to other implementations + // Would be an easy avenue to fuck with them though + Username string `gorm:"unique"` + CreatedAt time.Time // When this entry was created. Automatically set by gorm + // When this account was last updated. Will also be used for refreshing remote accounts. Automatically set by gorm + UpdatedAt time.Time + // When this entry was deleted (for soft deletions) + // Soft delete means that this entry still exists in the db, but gorm won't include it anymore unless specifically told to + // If not null, this entry is marked as deleted + DeletedAt gorm.DeletedAt `gorm:"index"` + // Server RemoteServer // `gorm:"foreignKey:ServerId;references:ID"` // The server this user is from + ServerId uint // Id of the server this user is from, needed for including RemoteServer + DisplayName string // The display name of the user. Can be different from the handle + Description string // The description of a user account + IsBot bool // Whether to mark this account as a script controlled one + 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 + Indexable bool // Whether this account can be found by crawlers + PublicKey []byte // The public key of the account + // Whether this account restricts following + // If true, the owner must approve of a follow request first + RestrictedFollow bool + + Location *string + Birthday *time.Time + + // --- And internal account stuff --- + // Whether the account got verified and is allowed to be active + // For local accounts being active means being allowed to login and perform interactions + // For remote users, if an account is not verified, any interactions it sends are discarded + Verified bool +} diff --git a/storage-new/models/UserAuthentication.go b/storage-new/models/UserAuthentication.go new file mode 100644 index 0000000..9cd0ac8 --- /dev/null +++ b/storage-new/models/UserAuthentication.go @@ -0,0 +1,7 @@ +package models + +type UserAuthMethod struct { + UserId string + AuthMethod AuthenticationMethodType `gorm:"type:auth_method_type"` + Token []byte +} diff --git a/storage-new/models/UserAuthenticationMethod.go b/storage-new/models/UserAuthenticationMethod.go new file mode 100644 index 0000000..73a1552 --- /dev/null +++ b/storage-new/models/UserAuthenticationMethod.go @@ -0,0 +1,26 @@ +package models + +import "database/sql/driver" + +type AuthenticationMethodType string + +const ( + AuthMethodPassword AuthenticationMethodType = "password" + AuthMethodGAuth AuthenticationMethodType = "g-auth" // Google Authenticator / totp + AuthMethodMail AuthenticationMethodType = "mail" + AuthMethodPasskey2fa AuthenticationMethodType = "passkey-2fa" // Passkey used as 2fa factor + AuthMethodPasskey AuthenticationMethodType = "passkey" // Passkey as only auth key +) + +var AllAuthMethods = []AuthenticationMethodType{ + AuthMethodPassword, AuthMethodGAuth, AuthMethodMail, AuthMethodPasskey, AuthMethodPasskey2fa, +} + +func (ct *AuthenticationMethodType) Scan(value any) error { + *ct = AuthenticationMethodType(value.([]byte)) + return nil +} + +func (ct AuthenticationMethodType) Value() (driver.Value, error) { + return string(ct), nil +} diff --git a/storage-new/models/UserBeingType.go b/storage-new/models/UserBeingType.go new file mode 100644 index 0000000..de699ba --- /dev/null +++ b/storage-new/models/UserBeingType.go @@ -0,0 +1,27 @@ +package models + +import ( + "database/sql/driver" +) + +type BeingType string + +const ( + BEING_HUMAN = BeingType("human") + BEING_CAT = BeingType("cat") + BEING_FOX = BeingType("fox") + BEING_DOG = BeingType("dog") + BEING_ROBOT = BeingType("robot") + BEING_DOLL = BeingType("doll") +) + +var AllBeings = []BeingType{BEING_HUMAN, BEING_CAT, BEING_FOX, BEING_DOG, BEING_ROBOT, BEING_DOLL} + +func (ct *BeingType) Scan(value any) error { + *ct = BeingType(value.([]byte)) + return nil +} + +func (ct BeingType) Value() (driver.Value, error) { + return string(ct), nil +} diff --git a/storage-new/models/UserBeings.go b/storage-new/models/UserBeings.go new file mode 100644 index 0000000..632d76c --- /dev/null +++ b/storage-new/models/UserBeings.go @@ -0,0 +1,6 @@ +package models + +type UserBeings struct { + UserId string + Being BeingType `gorm:"type:being_type"` +} diff --git a/storage-new/models/UserInfoField.go b/storage-new/models/UserInfoField.go new file mode 100644 index 0000000..a259148 --- /dev/null +++ b/storage-new/models/UserInfoField.go @@ -0,0 +1,19 @@ +package models + +import ( + "time" + + "gorm.io/gorm" +) + +type UserInfoField struct { + gorm.Model // Can actually just embed this as is here as those are not something directly exposed :3 + Name string + Value string + LastUrlCheckDate *time.Time // Used if the value is an url to somewhere. Empty if value is not an url + // If the value is an url, this attribute indicates whether Linstrom was able to verify ownership + // of the provided url via the common method of + // "Does the target url contain a rel='me' link to the owner's account" + Confirmed bool + UserId string // Id of account this info field belongs to +} diff --git a/storage-new/models/UserRelation.go b/storage-new/models/UserRelation.go new file mode 100644 index 0000000..7b599c7 --- /dev/null +++ b/storage-new/models/UserRelation.go @@ -0,0 +1,7 @@ +package models + +type UserRelation struct { + UserId string + TargetUserId string + Relation RelationType `gorm:"type:relation_type"` +} diff --git a/storage-new/models/UserRelationType.go b/storage-new/models/UserRelationType.go new file mode 100644 index 0000000..fef44fb --- /dev/null +++ b/storage-new/models/UserRelationType.go @@ -0,0 +1,31 @@ +package models + +import "database/sql/driver" + +type RelationType string + +const ( + RelationFollow RelationType = "follow" + RelationMute RelationType = "mute" + RelationNoBoosts RelationType = "no-boosts" + RelationBlock RelationType = "block" + RelationPreventFollow RelationType = "prevent-follow" +) + +// var AllBeings = []BeingType{BEING_HUMAN, BEING_CAT, BEING_FOX, BEING_DOG, BEING_ROBOT, BEING_DOLL} +var AllRelations = []RelationType{ + RelationFollow, + RelationMute, + RelationNoBoosts, + RelationBlock, + RelationPreventFollow, +} + +func (ct *RelationType) Scan(value any) error { + *ct = RelationType(value.([]byte)) + return nil +} + +func (ct RelationType) Value() (driver.Value, error) { + return string(ct), nil +} diff --git a/storage-new/models/UserRemote.go b/storage-new/models/UserRemote.go new file mode 100644 index 0000000..a4e24f5 --- /dev/null +++ b/storage-new/models/UserRemote.go @@ -0,0 +1,21 @@ +package models + +import "gorm.io/gorm" + +type UserRemoteLinks struct { + // ---- Section: gorm + // Sets this struct up as a value that an Account may have + gorm.Model + UserId string + + // Just about every link here is optional to accomodate for servers with only minimal accounts + // Minimal being handle, ap link and inbox + ApLink string + ViewLink *string + FollowersLink *string + FollowingLink *string + InboxLink string + OutboxLink *string + FeaturedLink *string + FeaturedTagsLink *string +} diff --git a/storage-new/models/UserRoles.go b/storage-new/models/UserRoles.go new file mode 100644 index 0000000..641c5f0 --- /dev/null +++ b/storage-new/models/UserRoles.go @@ -0,0 +1,6 @@ +package models + +type UserRole struct { + UserId string + RoleId uint +} diff --git a/storage-new/models/UserTags.go b/storage-new/models/UserTags.go new file mode 100644 index 0000000..9093551 --- /dev/null +++ b/storage-new/models/UserTags.go @@ -0,0 +1,6 @@ +package models + +type UserTag struct { + UserId string + Tag string +} diff --git a/storage-new/storage.go b/storage-new/storage.go new file mode 100644 index 0000000..82be054 --- /dev/null +++ b/storage-new/storage.go @@ -0,0 +1 @@ +package storage