Compare commits
6 commits
befbc07946
...
954e4c5a34
Author | SHA1 | Date | |
---|---|---|---|
954e4c5a34 | |||
59b2bc0deb | |||
d691b5193e | |||
f1d4a7251b | |||
befaccd59c | |||
7bb32cb429 |
12 changed files with 410 additions and 51 deletions
5
go.mod
5
go.mod
|
@ -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
6
go.sum
|
@ -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=
|
||||
|
|
15
goals.md
15
goals.md
|
@ -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)
|
||||
|
|
8
main.go
8
main.go
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,6 +20,11 @@ var (
|
|||
"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")
|
||||
FlagStartDebugServer *bool = flag.Bool(
|
||||
"debugserver",
|
||||
false,
|
||||
"Also start the local debugging server",
|
||||
)
|
||||
)
|
||||
|
||||
func flagUsage() {
|
||||
|
|
9
shared/interfaces.go
Normal file
9
shared/interfaces.go
Normal file
|
@ -0,0 +1,9 @@
|
|||
package shared
|
||||
|
||||
type Clonable interface {
|
||||
Clone() Clonable
|
||||
}
|
||||
|
||||
type Santisable interface {
|
||||
Sanitize()
|
||||
}
|
|
@ -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())
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
119
web/debug/users.go
Normal 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)
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package web
|
||||
package webpublic
|
||||
|
||||
import "net/http"
|
||||
|
110
web/shared/User.go
Normal file
110
web/shared/User.go
Normal 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
|
||||
}
|
Loading…
Reference in a new issue