Compare commits

...

6 commits

Author SHA1 Message Date
954e4c5a34
Add debug server flag and run it in main
Some checks are pending
/ test (push) Waiting to run
2025-04-07 17:43:05 +02:00
59b2bc0deb
Add methods to db user 2025-04-07 17:42:53 +02:00
d691b5193e
Add utility interfaces 2025-04-07 17:42:32 +02:00
f1d4a7251b
go mod tidy 2025-04-07 17:42:24 +02:00
befaccd59c
Move new web stuff into dedicated folder 2025-04-07 17:42:04 +02:00
7bb32cb429
Update goals 2025-04-07 17:41:52 +02:00
12 changed files with 410 additions and 51 deletions

5
go.mod
View file

@ -19,9 +19,9 @@ require (
github.com/gorilla/websocket v1.5.3
github.com/minio/minio-go/v7 v7.0.80
github.com/mstarongithub/passkey v0.0.0-20240817142622-de6912c8303e
github.com/pquerna/otp v1.4.0
github.com/redis/go-redis/v9 v9.0.2
github.com/rs/zerolog v1.33.0
github.com/xhit/go-simple-mail/v2 v2.16.0
gitlab.com/mstarongitlab/goap v1.1.0
golang.org/x/crypto v0.36.0
golang.org/x/image v0.20.0
@ -29,7 +29,6 @@ require (
gorm.io/gen v0.3.26
gorm.io/gorm v1.25.12
gorm.io/plugin/dbresolver v1.5.3
github.com/pquerna/otp v1.4.0
)
require (
@ -45,7 +44,6 @@ require (
github.com/fxamacker/cbor/v2 v2.7.0 // indirect
github.com/go-ini/ini v1.67.0 // indirect
github.com/go-sql-driver/mysql v1.8.1 // indirect
github.com/go-test/deep v1.1.1 // indirect
github.com/go-webauthn/x v0.1.14 // indirect
github.com/goccy/go-json v0.10.3 // indirect
github.com/golang-jwt/jwt/v5 v5.2.1 // indirect
@ -79,7 +77,6 @@ require (
github.com/stretchr/objx v0.5.2 // indirect
github.com/stretchr/testify v1.9.0 // indirect
github.com/tetratelabs/wazero v1.7.3 // indirect
github.com/toorop/go-dkim v0.0.0-20201103131630-e1cd1a0a5208 // indirect
github.com/x448/float16 v0.8.4 // indirect
gitlab.com/mstarongitlab/goutils v1.3.0 // indirect
golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f // indirect

6
go.sum
View file

@ -114,8 +114,6 @@ github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U=
github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
github.com/go-webauthn/webauthn v0.11.2 h1:Fgx0/wlmkClTKlnOsdOQ+K5HcHDsDcYIvtYmfhEOSUc=
github.com/go-webauthn/webauthn v0.11.2/go.mod h1:aOtudaF94pM71g3jRwTYYwQTG1KyTILTcZqN1srkmD0=
github.com/go-webauthn/x v0.1.14 h1:1wrB8jzXAofojJPAaRxnZhRgagvLGnLjhCAwg3kTpT0=
@ -327,12 +325,8 @@ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsT
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/tetratelabs/wazero v1.7.3 h1:PBH5KVahrt3S2AHgEjKu4u+LlDbbk+nsGE3KLucy6Rw=
github.com/tetratelabs/wazero v1.7.3/go.mod h1:ytl6Zuh20R/eROuyDaGPkp82O9C/DJfXAwJfQ3X6/7Y=
github.com/toorop/go-dkim v0.0.0-20201103131630-e1cd1a0a5208 h1:PM5hJF7HVfNWmCjMdEfbuOBNXSVF2cMFGgQTPdKCbwM=
github.com/toorop/go-dkim v0.0.0-20201103131630-e1cd1a0a5208/go.mod h1:BzWtXXrXzZUvMacR0oF/fbDDgUPO8L36tDMmRAf14ns=
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
github.com/xhit/go-simple-mail/v2 v2.16.0 h1:ouGy/Ww4kuaqu2E2UrDw7SvLaziWTB60ICLkIkNVccA=
github.com/xhit/go-simple-mail/v2 v2.16.0/go.mod h1:b7P5ygho6SYE+VIqpxA6QkYfv4teeyG4MKqB3utRu98=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=

View file

@ -1,10 +1,15 @@
# Feature goals
## Easy
- optional content filter with Microsoft's ai scan thing (user and server level)
- lockdown mode (all incoming stuff will be bonked immediately) (user and server)
- Post highlighting (opposite of muting) where if a post contains some specific thing, it gets some highlight
- Post highlighting (opposite of muting) where if a post contains some
specific thing, it gets some highlight
- Maybe even with different highlighting options
## Medium
- optional automatic server screening
- metadata sharing (thing like link previews or blocklists)
- asks (in some way that is compatible with wafrn hopefully)
@ -12,14 +17,18 @@
- Database converter (Masto/Akoma/Mk -> Linstrom, maybe also other way around)
## Hard
- custom "ads" created and controlled by server admins
- some sort of subscription/payment system (opt-in (you have to opt in to potentially see monetised stuff in the first place))
- some sort of subscription/payment system (opt-in (you have to opt in to
potentially see monetised stuff in the first place))
- extended account moderation (user and server)
- custom api for working around AP being a pos:
- includes messages always being encrypted
- bunch of other optimisations
- Utilise `net/rpc`
## Variable difficutly
# Variable difficutly
- Multiple built-in frontends
- Primary using ember, focus on good looking and most feature complete
- Modifyable using htmx (not sure on this one yet)

View file

@ -20,6 +20,7 @@ import (
storagenew "git.mstar.dev/mstar/linstrom/storage-new"
"git.mstar.dev/mstar/linstrom/storage-new/dbgen"
"git.mstar.dev/mstar/linstrom/storage/cache"
webdebug "git.mstar.dev/mstar/linstrom/web/debug"
)
// TODO: Add frontend overwrite
@ -124,4 +125,11 @@ func newServer() {
if err = storagenew.InsertSelf(); err != nil {
log.Fatal().Err(err).Msg("Failed to insert self properly")
}
if *shared.FlagStartDebugServer {
log.Info().Msg("Starting debug server")
// TODO: Move into goroutine once public server also exists
if err = webdebug.New().Start(); err != nil {
log.Fatal().Err(err).Msg("Debug server failed")
}
}
}

View file

@ -19,7 +19,12 @@ var (
false,
"If set, the server will only validate the config (or write the default one) and then quit",
)
FlagStartNew *bool = flag.Bool("new", false, "Start the new system")
FlagStartNew *bool = flag.Bool("new", false, "Start the new system")
FlagStartDebugServer *bool = flag.Bool(
"debugserver",
false,
"Also start the local debugging server",
)
)
func flagUsage() {

9
shared/interfaces.go Normal file
View file

@ -0,0 +1,9 @@
package shared
type Clonable interface {
Clone() Clonable
}
type Santisable interface {
Sanitize()
}

View file

@ -1457,17 +1457,20 @@ type IUserDo interface {
schema.Tabler
GetByUsername(username string) (result *models.User, err error)
GetPagedTruePublic(pageNr uint) (result []models.User, err error)
GetPagedAllDeleted(pageNr uint) (result []models.User, err error)
GetPagedAllNonDeleted(pageNr uint) (result []models.User, err error)
}
// Get a user by a username
//
// SELECT * FROM @@table WHERE username = @username LIMIT 1
// SELECT * FROM @@table WHERE username = @username AND deleted_at IS NULL LIMIT 1
func (u userDo) GetByUsername(username string) (result *models.User, err error) {
var params []interface{}
var generateSQL strings.Builder
params = append(params, username)
generateSQL.WriteString("SELECT * FROM users WHERE username = ? LIMIT 1 ")
generateSQL.WriteString("SELECT * FROM users WHERE username = ? AND deleted_at IS NULL LIMIT 1 ")
var executeSQL *gorm.DB
executeSQL = u.UnderlyingDB().Raw(generateSQL.String(), params...).Take(&result) // ignore_security_alert
@ -1476,6 +1479,79 @@ func (u userDo) GetByUsername(username string) (result *models.User, err error)
return
}
// Get all true public accounts (verified & no restricted follow & indexable)
// in a paged manner, sorted by date saved
//
// SELECT * FROM @@table WHERE
//
// deleted_at IS NULL AND
// verified = true AND
// restricted_follow = false AND
// indexable = true
//
// ORDER BY created_at ASC
// LIMIT 50
// OFFSET @pageNr * 50
func (u userDo) GetPagedTruePublic(pageNr uint) (result []models.User, err error) {
var params []interface{}
var generateSQL strings.Builder
params = append(params, pageNr)
generateSQL.WriteString("SELECT * FROM users WHERE deleted_at IS NULL AND verified = true AND restricted_follow = false AND indexable = true ORDER BY created_at ASC LIMIT 50 OFFSET ? * 50 ")
var executeSQL *gorm.DB
executeSQL = u.UnderlyingDB().Raw(generateSQL.String(), params...).Find(&result) // ignore_security_alert
err = executeSQL.Error
return
}
// Get all deleted accounts in a paged manner, sorted by date saved
//
// SELECT * FROM @@table WHERE
//
// deleted_at IS NOT NULL AND
//
// ORDER BY created_at ASC
// LIMIT 50
// OFFSET @pageNr * 50
func (u userDo) GetPagedAllDeleted(pageNr uint) (result []models.User, err error) {
var params []interface{}
var generateSQL strings.Builder
params = append(params, pageNr)
generateSQL.WriteString("SELECT * FROM users WHERE deleted_at IS NOT NULL AND ORDER BY created_at ASC LIMIT 50 OFFSET ? * 50 ")
var executeSQL *gorm.DB
executeSQL = u.UnderlyingDB().Raw(generateSQL.String(), params...).Find(&result) // ignore_security_alert
err = executeSQL.Error
return
}
// Get all accounts that aren't deleted in a paged manner, sorted by date saved
//
// SELECT * FROM @@table WHERE
//
// deleted_at IS NULL
//
// ORDER BY created_at ASC
// LIMIT 50
// OFFSET @pageNr * 50
func (u userDo) GetPagedAllNonDeleted(pageNr uint) (result []models.User, err error) {
var params []interface{}
var generateSQL strings.Builder
params = append(params, pageNr)
generateSQL.WriteString("SELECT * FROM users WHERE deleted_at IS NULL ORDER BY created_at ASC LIMIT 50 OFFSET ? * 50 ")
var executeSQL *gorm.DB
executeSQL = u.UnderlyingDB().Raw(generateSQL.String(), params...).Find(&result) // ignore_security_alert
err = executeSQL.Error
return
}
func (u userDo) Debug() IUserDo {
return u.withDO(u.DO.Debug())
}

View file

@ -25,63 +25,91 @@ type User struct {
// identifier for users and other servers, especially when changing the username
// (username != display name) might be a future feature
// Same also applies for other types that use a UUID as primary key
ID string `gorm:"primarykey;type:uuid;default:gen_random_uuid()"`
ID string `gorm:"primarykey;type:uuid;default:gen_random_uuid()" json:"id"`
// 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
Username string `gorm:"unique" json:"username"`
CreatedAt time.Time ` json:"created_at"` // 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
UpdatedAt time.Time ` json:"updated_at"`
// 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
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 *MediaMetadata
IconId sql.NullString // ID of a media file used as icon
Background *MediaMetadata
BackgroundId sql.NullString // ID of a media file used as background image
Banner *MediaMetadata
BannerId sql.NullString // 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
DeletedAt gorm.DeletedAt `gorm:"index" json:"deleted_at"`
Server RemoteServer ` json:"-"`
ServerId uint ` json:"server_id"` // Id of the server this user is from, needed for including RemoteServer
DisplayName string ` json:"display_name"` // The display name of the user. Can be different from the handle
Description string ` json:"description"` // The description of a user account
IsBot bool ` json:"is_bot"` // Whether to mark this account as a script controlled one
Icon *MediaMetadata ` json:"-"`
IconId sql.NullString ` json:"icon_id"` // ID of a media file used as icon
Background *MediaMetadata ` json:"-"`
BackgroundId sql.NullString ` json:"background_id"` // ID of a media file used as background image
Banner *MediaMetadata ` json:"-"`
BannerId sql.NullString ` json:"banner_id"` // ID of a media file used as banner
Indexable bool ` json:"indexable"` // Whether this account can be found by crawlers
PublicKey []byte ` json:"public_key"` // The public key of the account
// Whether this account restricts following
// If true, the owner must approve of a follow request first
RestrictedFollow bool
RestrictedFollow bool ` json:"restricted_follow"`
Location sql.NullString
Birthday sql.NullTime
Location sql.NullString `json:"location"`
Birthday sql.NullTime `json:"birthday"`
// 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
Verified bool `json:"verified"`
// 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
// saved space is worth
PasskeyId []byte
FinishedRegistration bool // Whether this account has completed registration yet
PrivateKey []byte
PasskeyId []byte `json:"-"`
FinishedRegistration bool `json:"-"` // Whether this account has completed registration yet
PrivateKey []byte `json:"-"`
// ---- "Remote" linked values
InfoFields []UserInfoField
BeingTypes []UserToBeing
Tags []UserToTag
Relations []UserToUserRelation
Pronouns []UserToPronoun
Roles []UserToRole
RemoteInfo *UserRemoteLinks
AuthMethods []UserAuthMethod
InfoFields []UserInfoField `json:"-"`
BeingTypes []UserToBeing `json:"-"`
Tags []UserToTag `json:"-"`
Relations []UserToUserRelation `json:"-"`
Pronouns []UserToPronoun `json:"-"`
Roles []UserToRole `json:"-"`
RemoteInfo *UserRemoteLinks `json:"-"`
AuthMethods []UserAuthMethod `json:"-"`
}
type IUser interface {
// Get a user by a username
//
// SELECT * FROM @@table WHERE username = @username LIMIT 1
// SELECT * FROM @@table WHERE username = @username AND deleted_at IS NULL LIMIT 1
GetByUsername(username string) (*gen.T, error)
// Get all true public accounts (verified & no restricted follow & indexable)
// in a paged manner, sorted by date saved
//
// SELECT * FROM @@table WHERE
// deleted_at IS NULL AND
// verified = true AND
// restricted_follow = false AND
// indexable = true
// ORDER BY created_at ASC
// LIMIT 50
// OFFSET @pageNr * 50
GetPagedTruePublic(pageNr uint) ([]gen.T, error)
// Get all deleted accounts in a paged manner, sorted by date saved
//
// SELECT * FROM @@table WHERE
// deleted_at IS NOT NULL AND
// ORDER BY created_at ASC
// LIMIT 50
// OFFSET @pageNr * 50
GetPagedAllDeleted(pageNr uint) ([]gen.T, error)
// Get all accounts that aren't deleted in a paged manner, sorted by date saved
//
// SELECT * FROM @@table WHERE
// deleted_at IS NULL
// ORDER BY created_at ASC
// LIMIT 50
// OFFSET @pageNr * 50
GetPagedAllNonDeleted(pageNr uint) ([]gen.T, error)
}

View file

@ -13,6 +13,9 @@ type Server struct {
func New() *Server {
handler := http.NewServeMux()
handler.HandleFunc("GET /non-deleted", getNonDeletedUsers)
handler.HandleFunc("POST /local-user", createLocalUser)
handler.HandleFunc("GET /delete", deleteUser)
web := http.Server{
Addr: DebugAddr,
Handler: handler,
@ -26,6 +29,7 @@ func (s *Server) Start() error {
}
return nil
}
func (s *Server) Stop() error {
return s.server.Shutdown(context.Background())
}

119
web/debug/users.go Normal file
View file

@ -0,0 +1,119 @@
package webdebug
import (
"crypto/ed25519"
"crypto/rand"
"database/sql"
"encoding/json"
"fmt"
"net/http"
"strconv"
"time"
httputils "git.mstar.dev/mstar/goutils/http"
"git.mstar.dev/mstar/goutils/sliceutils"
"github.com/rs/zerolog/log"
"git.mstar.dev/mstar/linstrom/storage-new/dbgen"
"git.mstar.dev/mstar/linstrom/storage-new/models"
webshared "git.mstar.dev/mstar/linstrom/web/shared"
)
func getNonDeletedUsers(w http.ResponseWriter, r *http.Request) {
pageStr := r.FormValue("page")
page := 0
if pageStr != "" {
var err error
page, err = strconv.Atoi(pageStr)
if err != nil {
httputils.HttpErr(w, 0, "page is not a number", http.StatusBadRequest)
return
}
}
users, err := dbgen.User.GetPagedAllNonDeleted(uint(page))
if err != nil {
httputils.HttpErr(w, 0, "failed to get users", http.StatusInternalServerError)
return
}
marshalled, err := json.Marshal(sliceutils.Map(users, func(t models.User) webshared.User {
u := webshared.User{}
u.FromModel(&t)
return u
}))
if err != nil {
httputils.HttpErr(w, 0, "failed to marshal users", http.StatusInternalServerError)
return
}
fmt.Fprint(w, string(marshalled))
}
func createLocalUser(w http.ResponseWriter, r *http.Request) {
type Inbound struct {
Username string `json:"username"`
Displayname string `json:"displayname"`
Description string `json:"description"`
Birthday *time.Time `json:"birthday"`
Location *string `json:"location"`
IsBot bool `json:"is_bot"`
}
jsonDecoder := json.NewDecoder(r.Body)
data := Inbound{}
err := jsonDecoder.Decode(&data)
if err != nil {
httputils.HttpErr(w, 0, "decode failed", http.StatusBadRequest)
return
}
publicKey, privateKey, err := ed25519.GenerateKey(nil)
pkeyId := make([]byte, 64)
_, err = rand.Read(pkeyId)
if err != nil {
log.Error().Err(err).Msg("Failed to generate passkey id")
httputils.HttpErr(w, 0, "failed to generate passkey id", http.StatusInternalServerError)
return
}
u := dbgen.User
query := u.Select(
u.Username,
u.DisplayName,
u.Description,
u.IsBot,
u.ServerId,
u.PrivateKey,
u.PublicKey,
u.PasskeyId,
)
if data.Birthday != nil {
query = query.Select(u.Birthday)
}
if data.Location != nil {
query = query.Select(u.Location)
}
user := models.User{
Username: data.Username,
DisplayName: data.Displayname,
Description: data.Description,
IsBot: data.IsBot,
ServerId: 1, // Hardcoded, Self is always first ID
PublicKey: publicKey,
PrivateKey: privateKey,
PasskeyId: pkeyId,
}
if data.Birthday != nil {
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 {
log.Error().Err(err).Msg("failed to create new local user")
httputils.HttpErr(w, 0, "db failure", http.StatusInternalServerError)
}
}
func deleteUser(w http.ResponseWriter, r *http.Request) {
id := r.FormValue("id")
dbgen.User.Where(dbgen.User.ID.Eq(id)).Delete()
w.WriteHeader(http.StatusOK)
}

View file

@ -1,4 +1,4 @@
package web
package webpublic
import "net/http"

110
web/shared/User.go Normal file
View file

@ -0,0 +1,110 @@
package webshared
import (
"slices"
"time"
"git.mstar.dev/mstar/linstrom/shared"
"git.mstar.dev/mstar/linstrom/storage-new/models"
)
// Web/json representation of a user
type User struct {
// ---- Section public data ----
// 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"`
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"`
// ---- Section Debug data ----
// All these entries should only be available
// for the debug server and should be cleared
// before serving on the public server.
// Every debug field must be omitempty
Verified *bool `json:"verified,omitempty"`
FinishedRegistration *bool `json:"finished_registration,omitempty"`
}
// Compiler assertations for interface implementations
var _ shared.Santisable = &User{}
var _ shared.Clonable = &User{}
func (u *User) Sanitize() {
u.Verified = nil
u.FinishedRegistration = nil
}
func (u *User) Clone() shared.Clonable {
user := *u
if u.IconId != nil {
tmp := *u.IconId
user.IconId = &tmp
}
if u.BackgroundId != nil {
tmp := *u.BackgroundId
user.BackgroundId = &tmp
}
if u.BannerId != nil {
tmp := *u.BannerId
user.BannerId = &tmp
}
if u.Location != nil {
tmp := *u.Location
user.Location = &tmp
}
if u.Birthday != nil {
tmp := *u.Birthday
user.Birthday = &tmp
}
if u.Verified != nil {
tmp := *u.Verified
user.Verified = &tmp
}
if u.FinishedRegistration != nil {
tmp := *u.FinishedRegistration
user.FinishedRegistration = &tmp
}
user.PublicKey = slices.Clone(u.PublicKey)
return &user
}
func (u *User) FromModel(m *models.User) {
u.ID = m.ID
u.CreatedAt = m.CreatedAt
u.ServerId = m.ServerId
u.Displayname = m.DisplayName
u.IsBot = m.IsBot
if m.IconId.Valid {
u.IconId = &m.IconId.String
}
if m.BackgroundId.Valid {
u.BackgroundId = &m.IconId.String
}
if m.BannerId.Valid {
u.BannerId = &m.IconId.String
}
u.Indexable = m.Indexable
u.PublicKey = append(u.PublicKey, m.PublicKey...)
u.RestrictedFollow = m.RestrictedFollow
if m.Location.Valid {
u.Location = &m.Location.String
}
if m.Birthday.Valid {
u.Birthday = &m.Birthday.Time
}
u.Verified = &m.Verified
u.FinishedRegistration = &m.FinishedRegistration
}