From 5e93ecee736e591a605a23e11a66a2cef5acc505 Mon Sep 17 00:00:00 2001 From: mstar Date: Tue, 15 Apr 2025 17:18:56 +0200 Subject: [PATCH] User import now works --- activitypub/import.go | 197 ++++++++++++++++----- storage-new/dbgen/user_remote_links.gen.go | 36 ++-- storage-new/dbgen/user_to_beings.gen.go | 6 +- storage-new/dbgen/users.gen.go | 14 +- storage-new/models/User.go | 23 +-- storage-new/models/UserBeingType.go | 2 +- storage-new/models/UserBeings.go | 2 +- storage-new/models/UserRemote.go | 18 +- temp.toml | 4 +- web/debug/users.go | 5 +- web/public/api/activitypub/user.go | 11 +- web/shared/User.go | 32 ++-- 12 files changed, 241 insertions(+), 109 deletions(-) diff --git a/activitypub/import.go b/activitypub/import.go index 004c6c8..062c962 100644 --- a/activitypub/import.go +++ b/activitypub/import.go @@ -2,6 +2,7 @@ package activitypub import ( "encoding/json" + "encoding/pem" "errors" "fmt" "io" @@ -19,48 +20,39 @@ import ( webshared "git.mstar.dev/mstar/linstrom/web/shared" ) -var interestingMetadata = []string{ - "nodeDescription", - "maintainer", - "tosUrl", - "nodeAdmins", - "privacyPolicyUrl", - "inquiryUrl", - "impressumUrl", - "donationUrl", - "staffAccounts", +type inboundImportUserKey struct { + Id string `json:"id"` + Owner string `json:"owner"` + Pem string `json:"publicKeyPem"` +} +type inboundImportUserMedia struct { + Type string `json:"type"` + Url string `json:"url"` + MediaType string `json:"mediaType"` +} +type inboundImportUser struct { + Id string `json:"id"` + Type string `json:"type"` + PreferredUsername string `json:"preferredUsername"` + Inbox string `json:"inbox"` + Outbox *string `json:"outbox"` + PublicKey *inboundImportUserKey `json:"publicKey"` + Published *time.Time `json:"published"` + DisplayName *string `json:"name"` + Description *string `json:"summary,omitempty"` + PublicUrl *string `json:"url"` + Icon *inboundImportUserMedia `json:"icon,omitempty"` + Banner *inboundImportUserMedia `json:"image,omitempty"` + Discoverable *bool `json:"discoverable"` + Location *string `json:"vcard:Address,omitempty"` + Birthday *string `json:"vcard:bday,omitempty"` + SpeakAsCat bool `json:"speakAsCat"` + IsCat bool `json:"isCat"` + RestrictedFollow *bool `json:"manuallyApprovesFollowers"` } func ImportRemoteAccount(targetName string) (string, error) { - type InboundUserKey struct { - Id string `json:"id"` - Owner string `json:"owner"` - Pem string `json:"publicKeyPem"` - } - type InboundUserMedia struct { - Type string `json:"type"` - Url string `json:"url"` - MediaType string `json:"mediaType"` - } - type InboundUser struct { - Id string `json:"id"` - Type string `json:"type"` - PreferredUsername string `json:"preferredUsername"` - Inbox string `json:"inbox"` - PublicKey *InboundUserKey `json:"publicKey"` - Published *time.Time `json:"published"` - DisplayName *string `json:"name"` - Description *string `json:"summary,omitempty"` - PublicUrl *string `json:"url"` - Icon *InboundUserMedia `json:"icon,omitempty"` - Banner *InboundUserMedia `json:"image,omitempty"` - Discoverable *bool `json:"discoverable"` - Location *string `json:"vcard:Address,omitempty"` - Birthday *string `json:"vcard:bday,omitempty"` - SpeakAsCat bool `json:"speakAsCat"` - IsCat bool `json:"isCat"` - RestrictedFollow *bool `json:"manuallyApprovesFollowers"` - } + // Get the target user's link first webfinger, err := GetAccountWebfinger(targetName) if err != nil { @@ -93,14 +85,137 @@ func ImportRemoteAccount(targetName string) (string, error) { if response.StatusCode != 200 { return "", fmt.Errorf("activitypub: invalid status code: %v", response.StatusCode) } - var data InboundUser + var data inboundImportUser err = json.Unmarshal(body, &data) if err != nil { return "", other.Error("activitypub", "failed to unmarshal response", err) } log.Debug().Any("received-data", data).Msg("Response data") + + // TODO: Store received user in db + _, host, _ := SplitFullHandle(targetName) + hostId, err := ImportRemoteServer(host) + if err != nil { + return "", other.Error("activitypub", "failed to import host of target user", err) + } + + user, err := dbgen.User. + Where(dbgen.User.Username.Eq(targetName)). + Where(dbgen.User.ServerId.Eq(hostId)). + Preload(dbgen.User.RemoteInfo). + Preload(dbgen.User.InfoFields). + Preload(dbgen.User.BeingTypes). + FirstOrCreate() + if err != nil { + return "", other.Error("activitypub", "failed to find or create user in db", err) + } + user.Verified = true + user.FinishedRegistration = true + + if user.RemoteInfo == nil { + user.RemoteInfo = &models.UserRemoteLinks{ + UserId: user.ID, + ApLink: data.Id, + } + err = dbgen.UserRemoteLinks.Create(user.RemoteInfo) + if err != nil { + return "", other.Error("activitypub", "failed to create remote data for user", err) + } + err = dbgen.User.RemoteInfo.Model(user).Replace(user.RemoteInfo) + if err != nil { + return "", other.Error("activitypub", "failed to connect remote data to user", err) + } + user.RemoteInfoId.Int64 = int64(user.RemoteInfo.ID) + user.RemoteInfoId.Valid = true + } + if data.DisplayName != nil { + user.DisplayName = *data.DisplayName + } + if data.Outbox != nil { + user.RemoteInfo.OutboxLink.String = *data.Outbox + user.RemoteInfo.OutboxLink.Valid = true + } + user.RemoteInfo.InboxLink = data.Inbox + if data.PublicKey != nil { + pemBlock, _ := pem.Decode([]byte(data.PublicKey.Pem)) + if pemBlock.Type != "PUBLIC KEY" && pemBlock.Type != "RSA PUBLIC KEY" { + return "", fmt.Errorf("activitypub: invalid public key block type: %v", pemBlock.Type) + } + user.PublicKeyRsa = pemBlock.Bytes + } + // Assume published day of user won't change + if data.Description != nil { + user.Description = *data.Description + } + if data.PublicUrl != nil { + user.RemoteInfo.ViewLink.String = *data.PublicUrl + user.RemoteInfo.ViewLink.Valid = true + } + if data.Discoverable != nil { + user.Indexable = *data.Discoverable + } else { + // Assume false per default + user.Indexable = false + } + if data.Location != nil { + user.Location.String = *data.Location + user.Location.Valid = true + } + if data.Birthday != nil { + user.Birthday.String = *data.Birthday + user.Birthday.Valid = true + } + if data.RestrictedFollow != nil { + user.RestrictedFollow = *data.RestrictedFollow + } else { + // Assume not restricted if not included in received data + user.RestrictedFollow = false + } + if data.IsCat { + if !sliceutils.ContainsFunc(user.BeingTypes, func(t models.UserToBeing) bool { + return t.Being == string(models.BEING_CAT) + }) { + log.Debug().Msg("user doesn't contain cat yet") + bt := models.UserToBeing{UserId: user.ID, Being: string(models.BEING_CAT)} + if err = dbgen.UserToBeing.Create(&bt); err != nil { + log.Warn().Err(err).Msg("Failed to append cat being type to imported user") + } + if err = dbgen.User.BeingTypes.Model(user).Append(&bt); err != nil { + log.Warn(). + Err(err). + Msg("Failed to append cat being type to imported user relations") + } + user.BeingTypes = append(user.BeingTypes, bt) + } + } else { + if sliceutils.ContainsFunc(user.BeingTypes, func(t models.UserToBeing) bool { + return t.Being == string(models.BEING_CAT) + }) { + _, err = dbgen.UserToBeing.Where(dbgen.UserToBeing.UserId.Eq(user.ID)).Where(dbgen.UserToBeing.Being.Eq(models.BEING_CAT)).Delete() + if err != nil { + log.Warn().Err(err).Msg("Failed to remove cat being type from user") + } + } + } + // Don't handle SpeakAsCat yet, as not included in stored data yet + + // Handle media last as more complicated + // Icon *inboundImportUserMedia `json:"icon,omitempty"` + // Banner *inboundImportUserMedia `json:"image,omitempty"` + + id := user.ID + user.ID = "" + if _, err = dbgen.User.Where(dbgen.User.ID.Eq(id)).UpdateColumns(user); err != nil { + return "", other.Error("activitypub", "failed to update imported user", err) + } + rlid := user.RemoteInfo.ID + user.RemoteInfo.ID = 0 + if _, err = dbgen.UserRemoteLinks.Where(dbgen.UserRemoteLinks.ID.Eq(rlid)).UpdateColumns(user.RemoteInfo); err != nil { + return "", other.Error("activitypub", "failed to update imported user's remote links", err) + } + log.Info().Str("user", targetName).Msg("Import completed") - return "", nil + return id, nil } func ImportRemoteServer(host string) (uint, error) { diff --git a/storage-new/dbgen/user_remote_links.gen.go b/storage-new/dbgen/user_remote_links.gen.go index db2c8a6..08aa5ca 100644 --- a/storage-new/dbgen/user_remote_links.gen.go +++ b/storage-new/dbgen/user_remote_links.gen.go @@ -32,13 +32,13 @@ func newUserRemoteLinks(db *gorm.DB, opts ...gen.DOOption) userRemoteLinks { _userRemoteLinks.DeletedAt = field.NewField(tableName, "deleted_at") _userRemoteLinks.UserId = field.NewString(tableName, "user_id") _userRemoteLinks.ApLink = field.NewString(tableName, "ap_link") - _userRemoteLinks.ViewLink = field.NewString(tableName, "view_link") - _userRemoteLinks.FollowersLink = field.NewString(tableName, "followers_link") - _userRemoteLinks.FollowingLink = field.NewString(tableName, "following_link") + _userRemoteLinks.ViewLink = field.NewField(tableName, "view_link") + _userRemoteLinks.FollowersLink = field.NewField(tableName, "followers_link") + _userRemoteLinks.FollowingLink = field.NewField(tableName, "following_link") _userRemoteLinks.InboxLink = field.NewString(tableName, "inbox_link") - _userRemoteLinks.OutboxLink = field.NewString(tableName, "outbox_link") - _userRemoteLinks.FeaturedLink = field.NewString(tableName, "featured_link") - _userRemoteLinks.FeaturedTagsLink = field.NewString(tableName, "featured_tags_link") + _userRemoteLinks.OutboxLink = field.NewField(tableName, "outbox_link") + _userRemoteLinks.FeaturedLink = field.NewField(tableName, "featured_link") + _userRemoteLinks.FeaturedTagsLink = field.NewField(tableName, "featured_tags_link") _userRemoteLinks.User = userRemoteLinksBelongsToUser{ db: db.Session(&gorm.Session{}), @@ -227,13 +227,13 @@ type userRemoteLinks struct { DeletedAt field.Field UserId field.String ApLink field.String - ViewLink field.String - FollowersLink field.String - FollowingLink field.String + ViewLink field.Field + FollowersLink field.Field + FollowingLink field.Field InboxLink field.String - OutboxLink field.String - FeaturedLink field.String - FeaturedTagsLink field.String + OutboxLink field.Field + FeaturedLink field.Field + FeaturedTagsLink field.Field User userRemoteLinksBelongsToUser fieldMap map[string]field.Expr @@ -257,13 +257,13 @@ func (u *userRemoteLinks) updateTableName(table string) *userRemoteLinks { u.DeletedAt = field.NewField(table, "deleted_at") u.UserId = field.NewString(table, "user_id") u.ApLink = field.NewString(table, "ap_link") - u.ViewLink = field.NewString(table, "view_link") - u.FollowersLink = field.NewString(table, "followers_link") - u.FollowingLink = field.NewString(table, "following_link") + u.ViewLink = field.NewField(table, "view_link") + u.FollowersLink = field.NewField(table, "followers_link") + u.FollowingLink = field.NewField(table, "following_link") u.InboxLink = field.NewString(table, "inbox_link") - u.OutboxLink = field.NewString(table, "outbox_link") - u.FeaturedLink = field.NewString(table, "featured_link") - u.FeaturedTagsLink = field.NewString(table, "featured_tags_link") + u.OutboxLink = field.NewField(table, "outbox_link") + u.FeaturedLink = field.NewField(table, "featured_link") + u.FeaturedTagsLink = field.NewField(table, "featured_tags_link") u.fillFieldMap() diff --git a/storage-new/dbgen/user_to_beings.gen.go b/storage-new/dbgen/user_to_beings.gen.go index 7e0076f..85ebfe1 100644 --- a/storage-new/dbgen/user_to_beings.gen.go +++ b/storage-new/dbgen/user_to_beings.gen.go @@ -28,7 +28,7 @@ func newUserToBeing(db *gorm.DB, opts ...gen.DOOption) userToBeing { _userToBeing.ALL = field.NewAsterisk(tableName) _userToBeing.ID = field.NewUint64(tableName, "id") _userToBeing.UserId = field.NewString(tableName, "user_id") - _userToBeing.Being = field.NewField(tableName, "being") + _userToBeing.Being = field.NewString(tableName, "being") _userToBeing.User = userToBeingBelongsToUser{ db: db.Session(&gorm.Session{}), @@ -213,7 +213,7 @@ type userToBeing struct { ALL field.Asterisk ID field.Uint64 UserId field.String - Being field.Field + Being field.String User userToBeingBelongsToUser fieldMap map[string]field.Expr @@ -233,7 +233,7 @@ func (u *userToBeing) updateTableName(table string) *userToBeing { u.ALL = field.NewAsterisk(table) u.ID = field.NewUint64(table, "id") u.UserId = field.NewString(table, "user_id") - u.Being = field.NewField(table, "being") + u.Being = field.NewString(table, "being") u.fillFieldMap() diff --git a/storage-new/dbgen/users.gen.go b/storage-new/dbgen/users.gen.go index 801dc2c..5c5a9a2 100644 --- a/storage-new/dbgen/users.gen.go +++ b/storage-new/dbgen/users.gen.go @@ -43,13 +43,14 @@ func newUser(db *gorm.DB, opts ...gen.DOOption) user { _user.PublicKeyRsa = field.NewBytes(tableName, "public_key_rsa") _user.PublicKeyEd = field.NewBytes(tableName, "public_key_ed") _user.RestrictedFollow = field.NewBool(tableName, "restricted_follow") - _user.Location = field.NewField(tableName, "location") _user.Birthday = field.NewField(tableName, "birthday") + _user.Location = field.NewField(tableName, "location") _user.Verified = field.NewBool(tableName, "verified") _user.PasskeyId = field.NewBytes(tableName, "passkey_id") _user.FinishedRegistration = field.NewBool(tableName, "finished_registration") _user.PrivateKeyRsa = field.NewBytes(tableName, "private_key_rsa") _user.PrivateKeyEd = field.NewBytes(tableName, "private_key_ed") + _user.RemoteInfoId = field.NewField(tableName, "remote_info_id") _user.RemoteInfo = userHasOneRemoteInfo{ db: db.Session(&gorm.Session{}), @@ -383,13 +384,14 @@ type user struct { PublicKeyRsa field.Bytes PublicKeyEd field.Bytes RestrictedFollow field.Bool - Location field.Field Birthday field.Field + Location field.Field Verified field.Bool PasskeyId field.Bytes FinishedRegistration field.Bool PrivateKeyRsa field.Bytes PrivateKeyEd field.Bytes + RemoteInfoId field.Field RemoteInfo userHasOneRemoteInfo InfoFields userHasManyInfoFields @@ -445,13 +447,14 @@ func (u *user) updateTableName(table string) *user { u.PublicKeyRsa = field.NewBytes(table, "public_key_rsa") u.PublicKeyEd = field.NewBytes(table, "public_key_ed") u.RestrictedFollow = field.NewBool(table, "restricted_follow") - u.Location = field.NewField(table, "location") u.Birthday = field.NewField(table, "birthday") + u.Location = field.NewField(table, "location") u.Verified = field.NewBool(table, "verified") u.PasskeyId = field.NewBytes(table, "passkey_id") u.FinishedRegistration = field.NewBool(table, "finished_registration") u.PrivateKeyRsa = field.NewBytes(table, "private_key_rsa") u.PrivateKeyEd = field.NewBytes(table, "private_key_ed") + u.RemoteInfoId = field.NewField(table, "remote_info_id") u.fillFieldMap() @@ -468,7 +471,7 @@ func (u *user) GetFieldByName(fieldName string) (field.OrderExpr, bool) { } func (u *user) fillFieldMap() { - u.fieldMap = make(map[string]field.Expr, 35) + u.fieldMap = make(map[string]field.Expr, 36) u.fieldMap["id"] = u.ID u.fieldMap["username"] = u.Username u.fieldMap["created_at"] = u.CreatedAt @@ -485,13 +488,14 @@ func (u *user) fillFieldMap() { u.fieldMap["public_key_rsa"] = u.PublicKeyRsa u.fieldMap["public_key_ed"] = u.PublicKeyEd u.fieldMap["restricted_follow"] = u.RestrictedFollow - u.fieldMap["location"] = u.Location u.fieldMap["birthday"] = u.Birthday + u.fieldMap["location"] = u.Location u.fieldMap["verified"] = u.Verified u.fieldMap["passkey_id"] = u.PasskeyId u.fieldMap["finished_registration"] = u.FinishedRegistration u.fieldMap["private_key_rsa"] = u.PrivateKeyRsa u.fieldMap["private_key_ed"] = u.PrivateKeyEd + u.fieldMap["remote_info_id"] = u.RemoteInfoId } diff --git a/storage-new/models/User.go b/storage-new/models/User.go index 501174f..befa840 100644 --- a/storage-new/models/User.go +++ b/storage-new/models/User.go @@ -55,12 +55,14 @@ type User struct { // If true, the owner must approve of a follow request first RestrictedFollow bool + // Technically should be a timestamp, but can't trust other implementations + // to enforce this in a consistent format + Birthday sql.NullString Location sql.NullString - Birthday sql.NullTime // 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 + // No impact on remote accounts Verified bool // 64 byte unique id for passkeys, because UUIDs are 128 bytes and passkey spec says 64 bytes max // In theory, could also slash Id in half, but that would be a lot more calculations than the @@ -71,14 +73,15 @@ type User struct { PrivateKeyEd []byte // ---- "Remote" linked values - InfoFields []UserInfoField - BeingTypes []UserToBeing - Tags []UserToTag - Relations []UserToUserRelation - Pronouns []UserToPronoun - Roles []UserToRole - RemoteInfo *UserRemoteLinks - AuthMethods []UserAuthMethod + InfoFields []UserInfoField + BeingTypes []UserToBeing + Tags []UserToTag + Relations []UserToUserRelation + Pronouns []UserToPronoun + Roles []UserToRole + RemoteInfo *UserRemoteLinks + RemoteInfoId sql.NullInt64 + AuthMethods []UserAuthMethod } type IUser interface { diff --git a/storage-new/models/UserBeingType.go b/storage-new/models/UserBeingType.go index aefe74b..8393e0c 100644 --- a/storage-new/models/UserBeingType.go +++ b/storage-new/models/UserBeingType.go @@ -21,7 +21,7 @@ const ( 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)) + *ct = BeingType(value.(string)) return nil } diff --git a/storage-new/models/UserBeings.go b/storage-new/models/UserBeings.go index f4d767f..5672c3f 100644 --- a/storage-new/models/UserBeings.go +++ b/storage-new/models/UserBeings.go @@ -8,5 +8,5 @@ type UserToBeing struct { ID uint64 `gorm:"primarykey"` User User UserId string - Being BeingType `gorm:"type:being_type"` + Being string } diff --git a/storage-new/models/UserRemote.go b/storage-new/models/UserRemote.go index 23f9fa4..c044316 100644 --- a/storage-new/models/UserRemote.go +++ b/storage-new/models/UserRemote.go @@ -1,6 +1,10 @@ package models -import "gorm.io/gorm" +import ( + "database/sql" + + "gorm.io/gorm" +) // UserRemoteLinks contains cached links for remote users type UserRemoteLinks struct { @@ -13,11 +17,11 @@ type UserRemoteLinks struct { // 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 + ViewLink sql.NullString + FollowersLink sql.NullString + FollowingLink sql.NullString InboxLink string - OutboxLink *string - FeaturedLink *string - FeaturedTagsLink *string + OutboxLink sql.NullString + FeaturedLink sql.NullString + FeaturedTagsLink sql.NullString } diff --git a/temp.toml b/temp.toml index 6a71c2d..fcab56e 100644 --- a/temp.toml +++ b/temp.toml @@ -1,7 +1,7 @@ [general] protocol = "https" - domain = "serveo.net" - subdomain = "cc45720a387f04ba6a748a2627327a77" + domain = "lhr.life" + subdomain = "47565bb39100de" private_port = 8080 public_port = 443 diff --git a/web/debug/users.go b/web/debug/users.go index 1387af2..c013c75 100644 --- a/web/debug/users.go +++ b/web/debug/users.go @@ -158,12 +158,13 @@ func createLocalUser(w http.ResponseWriter, r *http.Request) { PasskeyId: pkeyId, } if data.Birthday != nil { - user.Birthday = sql.NullTime{Valid: true, Time: *data.Birthday} + user.Birthday = sql.NullString{Valid: true, String: data.Birthday.Format("2006-Jan-02")} + // user.Birthday = sql.NullTime{Valid: true, Time: *data.Birthday} } if data.Location != nil { user.Location = sql.NullString{Valid: true, String: *data.Location} } - if err = u.Create(&user); err != nil { + if err = query.Create(&user); err != nil { log.Error().Err(err).Msg("failed to create new local user") webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError) } diff --git a/web/public/api/activitypub/user.go b/web/public/api/activitypub/user.go index 556b2cf..13ce667 100644 --- a/web/public/api/activitypub/user.go +++ b/web/public/api/activitypub/user.go @@ -8,7 +8,6 @@ import ( "time" webutils "git.mstar.dev/mstar/goutils/http" - "git.mstar.dev/mstar/goutils/other" "git.mstar.dev/mstar/goutils/sliceutils" "github.com/rs/zerolog/hlog" @@ -119,7 +118,7 @@ func users(w http.ResponseWriter, r *http.Request) { } } if sliceutils.ContainsFunc(user.BeingTypes, func(t models.UserToBeing) bool { - return t.Being == models.BEING_CAT + return t.Being == string(models.BEING_CAT) }) { data.IsCat = true // data.SpeakAsCat = true // TODO: Move to check of separate field in db model @@ -128,10 +127,16 @@ func users(w http.ResponseWriter, r *http.Request) { data.Location = &user.Location.String } if user.Birthday.Valid { - data.Birthday = other.IntoPointer(user.Birthday.Time.Format("2006-Jan-02")) //YYYY-Month-DD + data.Birthday = &user.Birthday.String + // data.Birthday = other.IntoPointer(user.Birthday.Time.Format("2006-Jan-02")) //YYYY-Month-DD } encoded, err := json.Marshal(data) + if err != nil { + log.Error().Err(err).Msg("Failed to marshal response") + webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError) + return + } w.Header().Add("Content-Type", "application/activity+json") fmt.Fprint(w, string(encoded)) } diff --git a/web/shared/User.go b/web/shared/User.go index 05b8abe..4cb8cbc 100644 --- a/web/shared/User.go +++ b/web/shared/User.go @@ -15,21 +15,21 @@ type User struct { // All data here will always be included, even if empty, // in which case it will be marked as null instead of omitted - ID string `json:"id"` - CreatedAt time.Time `json:"created_at"` - ServerId uint `json:"server_id"` - Displayname string `json:"displayname"` - Username string `json:"username"` - Description string `json:"description"` - IsBot bool `json:"is_bot"` - IconId *string `json:"icon_id"` - BackgroundId *string `json:"background_id"` - BannerId *string `json:"banner_id"` - Indexable bool `json:"indexable"` - PublicKey []byte `json:"public_key"` - RestrictedFollow bool `json:"restricted_follow"` - Location *string `json:"location"` - Birthday *time.Time `json:"birthday"` + ID string `json:"id"` + CreatedAt time.Time `json:"created_at"` + ServerId uint `json:"server_id"` + Displayname string `json:"displayname"` + Username string `json:"username"` + Description string `json:"description"` + IsBot bool `json:"is_bot"` + IconId *string `json:"icon_id"` + BackgroundId *string `json:"background_id"` + BannerId *string `json:"banner_id"` + Indexable bool `json:"indexable"` + PublicKey []byte `json:"public_key"` + RestrictedFollow bool `json:"restricted_follow"` + Location *string `json:"location"` + Birthday *string `json:"birthday"` // ---- Section Debug data ---- // All these entries should only be available @@ -113,7 +113,7 @@ func (u *User) FromModel(m *models.User) { u.Location = &m.Location.String } if m.Birthday.Valid { - u.Birthday = &m.Birthday.Time + u.Birthday = &m.Birthday.String } u.Verified = &m.Verified u.FinishedRegistration = &m.FinishedRegistration