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/gorilla/websocket v1.5.3
github.com/minio/minio-go/v7 v7.0.80 github.com/minio/minio-go/v7 v7.0.80
github.com/mstarongithub/passkey v0.0.0-20240817142622-de6912c8303e 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/redis/go-redis/v9 v9.0.2
github.com/rs/zerolog v1.33.0 github.com/rs/zerolog v1.33.0
github.com/xhit/go-simple-mail/v2 v2.16.0
gitlab.com/mstarongitlab/goap v1.1.0 gitlab.com/mstarongitlab/goap v1.1.0
golang.org/x/crypto v0.36.0 golang.org/x/crypto v0.36.0
golang.org/x/image v0.20.0 golang.org/x/image v0.20.0
@ -29,7 +29,6 @@ require (
gorm.io/gen v0.3.26 gorm.io/gen v0.3.26
gorm.io/gorm v1.25.12 gorm.io/gorm v1.25.12
gorm.io/plugin/dbresolver v1.5.3 gorm.io/plugin/dbresolver v1.5.3
github.com/pquerna/otp v1.4.0
) )
require ( require (
@ -45,7 +44,6 @@ require (
github.com/fxamacker/cbor/v2 v2.7.0 // indirect github.com/fxamacker/cbor/v2 v2.7.0 // indirect
github.com/go-ini/ini v1.67.0 // indirect github.com/go-ini/ini v1.67.0 // indirect
github.com/go-sql-driver/mysql v1.8.1 // 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/go-webauthn/x v0.1.14 // indirect
github.com/goccy/go-json v0.10.3 // indirect github.com/goccy/go-json v0.10.3 // indirect
github.com/golang-jwt/jwt/v5 v5.2.1 // 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/objx v0.5.2 // indirect
github.com/stretchr/testify v1.9.0 // indirect github.com/stretchr/testify v1.9.0 // indirect
github.com/tetratelabs/wazero v1.7.3 // 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 github.com/x448/float16 v0.8.4 // indirect
gitlab.com/mstarongitlab/goutils v1.3.0 // indirect gitlab.com/mstarongitlab/goutils v1.3.0 // indirect
golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f // 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 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= 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-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 h1:Fgx0/wlmkClTKlnOsdOQ+K5HcHDsDcYIvtYmfhEOSUc=
github.com/go-webauthn/webauthn v0.11.2/go.mod h1:aOtudaF94pM71g3jRwTYYwQTG1KyTILTcZqN1srkmD0= github.com/go-webauthn/webauthn v0.11.2/go.mod h1:aOtudaF94pM71g3jRwTYYwQTG1KyTILTcZqN1srkmD0=
github.com/go-webauthn/x v0.1.14 h1:1wrB8jzXAofojJPAaRxnZhRgagvLGnLjhCAwg3kTpT0= 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/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 h1:PBH5KVahrt3S2AHgEjKu4u+LlDbbk+nsGE3KLucy6Rw=
github.com/tetratelabs/wazero v1.7.3/go.mod h1:ytl6Zuh20R/eROuyDaGPkp82O9C/DJfXAwJfQ3X6/7Y= 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 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= 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.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.27/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= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=

View file

@ -1,10 +1,15 @@
# Feature goals
## Easy ## Easy
- optional content filter with Microsoft's ai scan thing (user and server level) - 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) - 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 - Maybe even with different highlighting options
## Medium ## Medium
- optional automatic server screening - optional automatic server screening
- metadata sharing (thing like link previews or blocklists) - metadata sharing (thing like link previews or blocklists)
- asks (in some way that is compatible with wafrn hopefully) - 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) - Database converter (Masto/Akoma/Mk -> Linstrom, maybe also other way around)
## Hard ## Hard
- custom "ads" created and controlled by server admins - 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) - extended account moderation (user and server)
- custom api for working around AP being a pos: - custom api for working around AP being a pos:
- includes messages always being encrypted - includes messages always being encrypted
- bunch of other optimisations - bunch of other optimisations
- Utilise `net/rpc`
## Variable difficutly
# Variable difficutly
- Multiple built-in frontends - Multiple built-in frontends
- Primary using ember, focus on good looking and most feature complete - Primary using ember, focus on good looking and most feature complete
- Modifyable using htmx (not sure on this one yet) - Modifyable using htmx (not sure on this one yet)

View file

@ -20,6 +20,7 @@ import (
storagenew "git.mstar.dev/mstar/linstrom/storage-new" storagenew "git.mstar.dev/mstar/linstrom/storage-new"
"git.mstar.dev/mstar/linstrom/storage-new/dbgen" "git.mstar.dev/mstar/linstrom/storage-new/dbgen"
"git.mstar.dev/mstar/linstrom/storage/cache" "git.mstar.dev/mstar/linstrom/storage/cache"
webdebug "git.mstar.dev/mstar/linstrom/web/debug"
) )
// TODO: Add frontend overwrite // TODO: Add frontend overwrite
@ -124,4 +125,11 @@ func newServer() {
if err = storagenew.InsertSelf(); err != nil { if err = storagenew.InsertSelf(); err != nil {
log.Fatal().Err(err).Msg("Failed to insert self properly") 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, false,
"If set, the server will only validate the config (or write the default one) and then quit", "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() { 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 schema.Tabler
GetByUsername(username string) (result *models.User, err error) 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 // 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) { func (u userDo) GetByUsername(username string) (result *models.User, err error) {
var params []interface{} var params []interface{}
var generateSQL strings.Builder var generateSQL strings.Builder
params = append(params, username) 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 var executeSQL *gorm.DB
executeSQL = u.UnderlyingDB().Raw(generateSQL.String(), params...).Take(&result) // ignore_security_alert 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 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 { func (u userDo) Debug() IUserDo {
return u.withDO(u.DO.Debug()) 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 // identifier for users and other servers, especially when changing the username
// (username != display name) might be a future feature // (username != display name) might be a future feature
// Same also applies for other types that use a UUID as primary key // 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) // 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 // Assume unchangable (once set by a user) to be kind to other implementations
// Would be an easy avenue to fuck with them though // Would be an easy avenue to fuck with them though
Username string `gorm:"unique"` Username string `gorm:"unique" json:"username"`
CreatedAt time.Time // When this entry was created. Automatically set by gorm 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 // 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) // 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 // 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 // If not null, this entry is marked as deleted
DeletedAt gorm.DeletedAt `gorm:"index"` DeletedAt gorm.DeletedAt `gorm:"index" json:"deleted_at"`
Server RemoteServer Server RemoteServer ` json:"-"`
ServerId uint // Id of the server this user is from, needed for including RemoteServer ServerId uint ` json:"server_id"` // 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 DisplayName string ` json:"display_name"` // The display name of the user. Can be different from the handle
Description string // The description of a user account Description string ` json:"description"` // The description of a user account
IsBot bool // Whether to mark this account as a script controlled one IsBot bool ` json:"is_bot"` // Whether to mark this account as a script controlled one
Icon *MediaMetadata Icon *MediaMetadata ` json:"-"`
IconId sql.NullString // ID of a media file used as icon IconId sql.NullString ` json:"icon_id"` // ID of a media file used as icon
Background *MediaMetadata Background *MediaMetadata ` json:"-"`
BackgroundId sql.NullString // ID of a media file used as background image BackgroundId sql.NullString ` json:"background_id"` // ID of a media file used as background image
Banner *MediaMetadata Banner *MediaMetadata ` json:"-"`
BannerId sql.NullString // ID of a media file used as banner BannerId sql.NullString ` json:"banner_id"` // ID of a media file used as banner
Indexable bool // Whether this account can be found by crawlers Indexable bool ` json:"indexable"` // Whether this account can be found by crawlers
PublicKey []byte // The public key of the account PublicKey []byte ` json:"public_key"` // The public key of the account
// Whether this account restricts following // Whether this account restricts following
// If true, the owner must approve of a follow request first // If true, the owner must approve of a follow request first
RestrictedFollow bool RestrictedFollow bool ` json:"restricted_follow"`
Location sql.NullString Location sql.NullString `json:"location"`
Birthday sql.NullTime Birthday sql.NullTime `json:"birthday"`
// Whether the account got verified and is allowed to be active // 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 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 // 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 // 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 // In theory, could also slash Id in half, but that would be a lot more calculations than the
// saved space is worth // saved space is worth
PasskeyId []byte PasskeyId []byte `json:"-"`
FinishedRegistration bool // Whether this account has completed registration yet FinishedRegistration bool `json:"-"` // Whether this account has completed registration yet
PrivateKey []byte PrivateKey []byte `json:"-"`
// ---- "Remote" linked values // ---- "Remote" linked values
InfoFields []UserInfoField InfoFields []UserInfoField `json:"-"`
BeingTypes []UserToBeing BeingTypes []UserToBeing `json:"-"`
Tags []UserToTag Tags []UserToTag `json:"-"`
Relations []UserToUserRelation Relations []UserToUserRelation `json:"-"`
Pronouns []UserToPronoun Pronouns []UserToPronoun `json:"-"`
Roles []UserToRole Roles []UserToRole `json:"-"`
RemoteInfo *UserRemoteLinks RemoteInfo *UserRemoteLinks `json:"-"`
AuthMethods []UserAuthMethod AuthMethods []UserAuthMethod `json:"-"`
} }
type IUser interface { type IUser interface {
// Get a user by a username // 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) 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 { func New() *Server {
handler := http.NewServeMux() handler := http.NewServeMux()
handler.HandleFunc("GET /non-deleted", getNonDeletedUsers)
handler.HandleFunc("POST /local-user", createLocalUser)
handler.HandleFunc("GET /delete", deleteUser)
web := http.Server{ web := http.Server{
Addr: DebugAddr, Addr: DebugAddr,
Handler: handler, Handler: handler,
@ -26,6 +29,7 @@ func (s *Server) Start() error {
} }
return nil return nil
} }
func (s *Server) Stop() error { func (s *Server) Stop() error {
return s.server.Shutdown(context.Background()) 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" 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
}