package storage import ( "errors" "fmt" "strings" "time" "github.com/go-webauthn/webauthn/webauthn" "github.com/google/uuid" "gorm.io/gorm" ) // Database representation of a user account // This can be a bot, remote or not // If remote, this is used for caching the account type User struct { ID string `gorm:"primarykey"` // ID is a uuid for this account Handle string // Handle is the full handle, eg @max@example.com CreatedAt time.Time // When this entry was created UpdatedAt time.Time // When this account was last updated. Will also be used for refreshing remote accounts DeletedAt gorm.DeletedAt `gorm:"index"` Remote bool // Whether the account is a local or remote one Server string // The url of the server this account is from DisplayName string // The display name of the user. Can be different from the handle CustomFields []uint `gorm:"serializer:json"` // IDs to the custom fields a user has Description string // The description of a user account Tags []string `gorm:"serializer:json"` // Hashtags IsBot bool // Whether to mark this account as a script controlled one Follows []string `gorm:"serializer:json"` // List of handles this account follows Followers []string `gorm:"serializer:json"` // List of handles that follow this account 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 PublicKeyPem *string // The public key of the account // Whether this account restricts following // If true, the owner must approve of a follow request first RestrictedFollow bool // List of things the owner identifies as // Example [cat human robot] means that the owner probably identifies as // a cyborg-catgirl/boy/human IdentifiesAs []Being // List of pronouns the owner identifies with // An unordered list since the owner can freely set it // Examples: [she her], [it they its them] Gender []string // --- And internal account stuff --- // Still public fields since they wouldn't be able to be stored in the db otherwise PasswordHash []byte // Hash of the user's password TotpToken []byte // Token for totp verification // All the registered passkeys, name of passkey to credentials // Could this be exported to another table? Yes // Would it make sense? Probably not // Will just take the performance hit of json conversion // Access should be rare enough anyway Passkeys map[string]webauthn.Credential `gorm:"serializer:json"` PrivateKeyPem *string // The private key of the account. Nil if remote user // Restrictions applied to the account // Flag value, can be multiple Restrictions AccountRestriction } var placeholderUser = &User{ ID: "placeholder", Handle: "placeholder", Remote: false, Server: "placeholder", DisplayName: "placeholder", CustomFields: []uint{}, Description: "placeholder", Tags: []string{}, IsBot: true, Follows: []string{}, Followers: []string{}, Icon: "placeholder", Background: "placeholder", Banner: "placeholder", Indexable: false, PublicKeyPem: nil, RestrictedFollow: false, IdentifiesAs: []Being{BEING_ROBOT}, Gender: []string{"it", "its"}, PasswordHash: []byte("placeholder"), TotpToken: []byte("placeholder"), Passkeys: map[string]webauthn.Credential{}, PrivateKeyPem: nil, } func NewEmptyUser() *User { return &User{ ID: uuid.NewString(), Handle: "placeholder", Remote: false, Server: "placeholder", DisplayName: "placeholder", CustomFields: []uint{}, Description: "placeholder", Tags: []string{}, IsBot: true, Follows: []string{}, Followers: []string{}, Icon: "placeholder", Background: "placeholder", Banner: "placeholder", Indexable: false, PublicKeyPem: nil, RestrictedFollow: false, IdentifiesAs: []Being{BEING_ROBOT}, Gender: []string{"it", "its"}, PasswordHash: []byte("placeholder"), TotpToken: []byte("placeholder"), Passkeys: map[string]webauthn.Credential{}, PrivateKeyPem: nil, } } // Get a stored user by the ID the user has been stored with // Either returns a valid user and nil for the error // Or nil for the user and an error func (s *Storage) GetUserByID(id string) (*User, error) { user := User{} res := s.db.First(&user, "id = ?", id) // Check if any error except NotFound occured // If so, wrap it a little if res.Error != nil && !errors.Is(res.Error, gorm.ErrRecordNotFound) { return nil, fmt.Errorf("problem while getting user from db: %w", res.Error) } // Then check if an error occured and said error is NotFound // If it is, just pass it forward if res.Error != nil && errors.Is(res.Error, gorm.ErrRecordNotFound) { return nil, res.Error } return &user, nil } // Get only the name part of the handle a user has func (u *User) GetHandleNameOnly() string { // First remove the leading @ (TrimPrefix) to achieve a format of username@server // Then split at the @ to the server and user seperately // And return the first element since that is the username // Note: Getting the first element will always be safe // since trim returns a string guaranteed (empty is ok) // and if Split doesn't do anything (eg empty string) it just returns the input in the first elemen it just returns the input in the first element return strings.Split(strings.TrimPrefix(u.Handle, "@"), "@")[0] }