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