Compare commits
2 commits
db85e831d9
...
6cc699cbbd
Author | SHA1 | Date | |
---|---|---|---|
6cc699cbbd | |||
b01f60d273 |
63 changed files with 118 additions and 5560 deletions
17
auth/auth.go
17
auth/auth.go
|
@ -1,17 +0,0 @@
|
|||
package auth
|
||||
|
||||
import (
|
||||
"git.mstar.dev/mstar/linstrom/storage"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type Authentication struct {
|
||||
// For when in-depth access is needed
|
||||
db *gorm.DB
|
||||
// Primary method to acquire account data
|
||||
store *storage.Storage
|
||||
}
|
||||
|
||||
func NewAuth(db *gorm.DB, store *storage.Storage) *Authentication {
|
||||
return &Authentication{db, store}
|
||||
}
|
116
auth/checks.go
116
auth/checks.go
|
@ -1,116 +0,0 @@
|
|||
package auth
|
||||
|
||||
import (
|
||||
"git.mstar.dev/mstar/goutils/sliceutils"
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"git.mstar.dev/mstar/linstrom/storage"
|
||||
)
|
||||
|
||||
// Can actorId read the account with targetId?
|
||||
func (a *Authentication) CanReadAccount(actorId *string, targetId string) bool {
|
||||
targetAccount, err := a.store.FindAccountById(targetId)
|
||||
if err != nil {
|
||||
if err == storage.ErrEntryNotFound {
|
||||
return true
|
||||
}
|
||||
log.Error().
|
||||
Err(err).
|
||||
Str("account-id", targetId).
|
||||
Msg("Failed to receive account for permission check")
|
||||
return false
|
||||
}
|
||||
if actorId == nil {
|
||||
// TODO: Decide if roles should have a field to declare an account as follow only/hidden
|
||||
// and then check for that flag here
|
||||
return true
|
||||
}
|
||||
roles, err := a.store.FindRolesByNames(targetAccount.Roles)
|
||||
if err != nil {
|
||||
log.Error().
|
||||
Err(err).
|
||||
Strs("role-names", targetAccount.Roles).
|
||||
Msg("Failed to get roles for target account")
|
||||
return false
|
||||
}
|
||||
combined := storage.CollapseRolesIntoOne(roles...)
|
||||
return !sliceutils.Contains(combined.BlockedUsers, *actorId)
|
||||
}
|
||||
|
||||
// Can actorId edit the account with targetId?
|
||||
// If actorId is nil, it is assumed to be an anonymous user trying to edit the target account
|
||||
// if targetId is nil, it is assumed that the actor is editing themselves
|
||||
func (a *Authentication) CanEditAccount(actorId *string, targetId *string) bool {
|
||||
// WARN: This entire function feels wrong, idk
|
||||
// Only the owner of an account should be able to edit said account's data
|
||||
// But how do moderation actions play with this? Do they count as edit or as something separate?
|
||||
if actorId == nil {
|
||||
return false
|
||||
}
|
||||
if targetId == nil {
|
||||
targetId = actorId
|
||||
}
|
||||
targetAccount, err := a.store.FindAccountById(*targetId)
|
||||
if err != nil {
|
||||
if err != storage.ErrEntryNotFound {
|
||||
log.Error().
|
||||
Err(err).
|
||||
Str("target-id", *targetId).
|
||||
Msg("Failed to receive account for permission checks")
|
||||
}
|
||||
return false
|
||||
}
|
||||
if targetId == actorId {
|
||||
targetRoles, err := a.store.FindRolesByNames(targetAccount.Roles)
|
||||
if err != nil {
|
||||
log.Error().
|
||||
Err(err).
|
||||
Strs("role-names", targetAccount.Roles).
|
||||
Msg("Failed to get roles from storage")
|
||||
return false
|
||||
}
|
||||
combined := storage.CollapseRolesIntoOne(targetRoles...)
|
||||
return *combined.CanLogin
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Can actorId delete the account with targetId?
|
||||
// If actorId is nil, it is assumed to be an anonymous user trying to delete the target account
|
||||
// if targetId is nil, it is assumed that the actor is deleting themselves
|
||||
func (a *Authentication) CanDeleteAccount(actorId *string, targetId *string) bool {
|
||||
if actorId == nil {
|
||||
return false
|
||||
}
|
||||
acc, err := a.store.FindAccountById(*actorId)
|
||||
if err != nil {
|
||||
// TODO: Logging
|
||||
return false
|
||||
}
|
||||
roles, err := a.store.FindRolesByNames(acc.Roles)
|
||||
if err != nil {
|
||||
// TODO: Logging
|
||||
return false
|
||||
}
|
||||
collapsed := storage.CollapseRolesIntoOne(roles...)
|
||||
if targetId == nil {
|
||||
return *collapsed.CanLogin
|
||||
} else {
|
||||
return *collapsed.CanDeleteAccounts
|
||||
}
|
||||
}
|
||||
|
||||
// Can actorId create a new post at all?
|
||||
// Specific restrictions regarding the content are not checked
|
||||
func (a *Authentication) CanCreatePost(actorId string) bool { return true }
|
||||
|
||||
// Ensures that a given post conforms with all roles attached to the author account.
|
||||
// Returns the conforming note (or nil of it can't be changed to conform)
|
||||
// and whether the note was changed
|
||||
func (a *Authentication) EnsureNoteConformsWithRoles(note *storage.Note) (*storage.Note, bool) {
|
||||
return note, false
|
||||
}
|
||||
|
||||
// Does the given note conform with the roles attached to the author account?
|
||||
func (a *Authentication) DoesNoteConform(note *storage.Note) bool { return true }
|
107
main.go
107
main.go
|
@ -5,24 +5,22 @@ import (
|
|||
"embed"
|
||||
"flag"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path"
|
||||
"time"
|
||||
"sync"
|
||||
|
||||
"git.mstar.dev/mstar/goutils/other"
|
||||
"github.com/go-webauthn/webauthn/webauthn"
|
||||
"github.com/mstarongithub/passkey"
|
||||
"github.com/rs/zerolog/log"
|
||||
"gopkg.in/natefinch/lumberjack.v2"
|
||||
"gorm.io/driver/postgres"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"git.mstar.dev/mstar/linstrom/config"
|
||||
"git.mstar.dev/mstar/linstrom/server"
|
||||
"git.mstar.dev/mstar/linstrom/shared"
|
||||
"git.mstar.dev/mstar/linstrom/storage"
|
||||
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"
|
||||
webpublic "git.mstar.dev/mstar/linstrom/web/public"
|
||||
)
|
||||
|
@ -43,6 +41,8 @@ var defaultDuck string
|
|||
var duckFS embed.FS
|
||||
|
||||
func main() {
|
||||
_ = reactiveFS
|
||||
_ = nojsFS
|
||||
other.SetupFlags()
|
||||
flag.Parse()
|
||||
logfile := getLogFilePathOrNil()
|
||||
|
@ -71,13 +71,7 @@ func main() {
|
|||
if *shared.FlagConfigOnly {
|
||||
return
|
||||
}
|
||||
if *shared.FlagStartNew {
|
||||
log.Info().Msg("Starting new system")
|
||||
newServer()
|
||||
} else {
|
||||
log.Info().Msg("Starting old system")
|
||||
oldServer()
|
||||
}
|
||||
newServer()
|
||||
}
|
||||
|
||||
func getLogFilePathOrNil() *string {
|
||||
|
@ -90,57 +84,8 @@ func getLogFilePathOrNil() *string {
|
|||
}
|
||||
}
|
||||
|
||||
func oldServer() {
|
||||
storageCache, err := cache.NewCache(
|
||||
config.GlobalConfig.Storage.MaxInMemoryCacheSize,
|
||||
config.GlobalConfig.Storage.RedisUrl,
|
||||
)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("Failed to start cache")
|
||||
}
|
||||
|
||||
// var store *storage.Storage
|
||||
// if config.GlobalConfig.Storage.DbIsPostgres != nil && *config.GlobalConfig.Storage.DbIsPostgres {
|
||||
// store, err = storage.NewStoragePostgres(config.GlobalConfig.Storage.DatabaseUrl, storageCache)
|
||||
// } else {
|
||||
// store, err = storage.NewStorageSqlite(config.GlobalConfig.Storage.DatabaseUrl, storageCache)
|
||||
// }
|
||||
//
|
||||
store, err := storage.NewStorage(config.GlobalConfig.Storage.BuildPostgresDSN(), storageCache)
|
||||
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("Failed to setup storage")
|
||||
}
|
||||
|
||||
pkey, err := passkey.New(passkey.Config{
|
||||
WebauthnConfig: &webauthn.Config{
|
||||
RPDisplayName: "Linstrom",
|
||||
RPID: "localhost",
|
||||
RPOrigins: []string{"http://localhost:8000"},
|
||||
},
|
||||
UserStore: store,
|
||||
SessionStore: store,
|
||||
SessionMaxAge: time.Hour * 24,
|
||||
}, passkey.WithLogger(&shared.ZerologWrapper{}))
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("Failed to setup passkey support")
|
||||
}
|
||||
|
||||
server := server.NewServer(
|
||||
store,
|
||||
pkey,
|
||||
shared.NewFSWrapper(reactiveFS, "frontend-reactive/dist/", false),
|
||||
shared.NewFSWrapper(nojsFS, "frontend-noscript/", false),
|
||||
&defaultDuck,
|
||||
)
|
||||
server.Start(":8000")
|
||||
// TODO: Set up media server
|
||||
// TODO: Set up queues
|
||||
// TODO: Set up plugins
|
||||
}
|
||||
|
||||
func newServer() {
|
||||
log.Info().Msg("Connectin to db")
|
||||
log.Info().Msg("Connecting to db")
|
||||
db, err := gorm.Open(
|
||||
postgres.Open(config.GlobalConfig.Storage.BuildPostgresDSN()),
|
||||
&gorm.Config{
|
||||
|
@ -159,21 +104,49 @@ func newServer() {
|
|||
if err = storagenew.InsertSelf(); err != nil {
|
||||
log.Fatal().Err(err).Msg("Failed to insert self properly")
|
||||
}
|
||||
log.Info().Msg("Inserting placeholder/unknown user")
|
||||
if err = storagenew.InsertUnknownActorPlaceholder(); err != nil {
|
||||
log.Fatal().Err(err).Msg("Failed to insert self properly")
|
||||
}
|
||||
debugShutdownChan := make(chan *sync.WaitGroup, 1)
|
||||
interuptChan := make(chan os.Signal, 1)
|
||||
signal.Notify(interuptChan, os.Interrupt)
|
||||
if *shared.FlagStartDebugServer {
|
||||
go func() {
|
||||
log.Info().Msg("Starting debug server")
|
||||
if err = webdebug.New(*shared.FlagDebugPort).Start(); err != nil {
|
||||
s := webdebug.New(*shared.FlagDebugPort)
|
||||
go func() {
|
||||
wg := <-debugShutdownChan
|
||||
if err := s.Stop(); err != nil {
|
||||
log.Fatal().Err(err).Msg("Failed to cleanly stop debug server")
|
||||
}
|
||||
log.Info().Msg("Debug server stopped")
|
||||
wg.Done()
|
||||
}()
|
||||
if err = s.Start(); err != nil && err != http.ErrServerClosed {
|
||||
log.Fatal().Err(err).Msg("Debug server failed")
|
||||
}
|
||||
}()
|
||||
}
|
||||
log.Info().Msg("Starting public server")
|
||||
public := webpublic.New(
|
||||
fmt.Sprintf(":%v", config.GlobalConfig.General.PrivatePort),
|
||||
&defaultDuck,
|
||||
duckFS,
|
||||
)
|
||||
if err = public.Start(); err != nil {
|
||||
log.Fatal().Err(err).Msg("Failed to start public server")
|
||||
go func() {
|
||||
log.Info().Msg("Starting public server")
|
||||
if err = public.Start(); err != nil && err != http.ErrServerClosed {
|
||||
log.Fatal().Err(err).Msg("Failed to start public server")
|
||||
}
|
||||
}()
|
||||
<-interuptChan
|
||||
log.Info().Msg("Received interrupt, shutting down")
|
||||
wg := sync.WaitGroup{}
|
||||
wg.Add(1)
|
||||
debugShutdownChan <- &wg
|
||||
if err = public.Stop(); err != nil {
|
||||
log.Fatal().Err(err).Msg("Failed to stop public server")
|
||||
}
|
||||
log.Info().Msg("Public server stopped")
|
||||
wg.Wait()
|
||||
}
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
package plugins
|
|
@ -1,5 +0,0 @@
|
|||
package plugins
|
||||
|
||||
// TODO: Think about how to enable server side processing plugins
|
||||
|
||||
// Options: Lua, wasm/wasi (probably prefered)
|
|
@ -1,4 +0,0 @@
|
|||
// Job queue for all inbound events
|
||||
// Well, I say queue but it's more of a security measure adding the inbound job to the db as backup
|
||||
// in case processing fails for any reason
|
||||
package inboundeventqueue
|
|
@ -1,5 +0,0 @@
|
|||
// Queue for outbound events
|
||||
// Actual queue here since other servers can't be expected to go at the same speed as Linstrom (be it slower or faster)
|
||||
// This queue should enforce data consistency (new jobs are first stored in the db) and a fair processing speed for
|
||||
// other servers, depending on their processing speed
|
||||
package outgoingeventqueue
|
|
@ -1,186 +0,0 @@
|
|||
package server
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"git.mstar.dev/mstar/goutils/other"
|
||||
"git.mstar.dev/mstar/linstrom/storage"
|
||||
)
|
||||
|
||||
func setupLinstromApiRouter() http.Handler {
|
||||
router := http.NewServeMux()
|
||||
router.Handle("/v1/", http.StripPrefix("/v1", setupLinstromApiV1Router()))
|
||||
router.Handle("/s2s/v1/", http.StripPrefix("/s2s/v1", setupLinstromS2SApiV1Router()))
|
||||
|
||||
return router
|
||||
}
|
||||
|
||||
func setupLinstromApiV1Router() http.Handler {
|
||||
router := http.NewServeMux()
|
||||
|
||||
// Notes
|
||||
|
||||
// Get a note
|
||||
router.HandleFunc("GET /notes/{noteId}", linstromGetNote)
|
||||
// Send a new note
|
||||
router.HandleFunc("POST /notes", linstromNewNote)
|
||||
// Update a note
|
||||
router.HandleFunc("PATCH /notes/{noteId}", linstromUpdateNote)
|
||||
// Delete a note
|
||||
router.HandleFunc("DELETE /notes/{noteId}", linstromDeleteNote)
|
||||
|
||||
// Reactions
|
||||
|
||||
// Get all reactions for a note
|
||||
router.HandleFunc("GET /notes/{noteId}/reactions", linstromGetReactions)
|
||||
// Send a new reaction to a note
|
||||
router.HandleFunc("POST /notes/{noteId}/reactions", linstromAddReaction)
|
||||
// Update own reaction on a note
|
||||
router.HandleFunc("PATCH /notes/{noteId}/reactions", linstromUpdateReaction)
|
||||
// Remove own reaction on a note
|
||||
router.HandleFunc("DELETE /notes/{noteId}/reactions", linstromDeleteReaction)
|
||||
|
||||
// Boosts
|
||||
|
||||
// Get all boosters of a note
|
||||
router.HandleFunc("GET /notes/{noteId}/boosts", linstromGetBoosts)
|
||||
// Boost a note
|
||||
router.HandleFunc("POST /notes/{noteId}/boosts", linstromAddBoost)
|
||||
// Unboost a note
|
||||
router.HandleFunc("DELETE /notes/{noteId}/boosts", linstromRemoveBoost)
|
||||
|
||||
// Quotes
|
||||
|
||||
// Get all quotes of a note
|
||||
router.HandleFunc("GET /notes/{noteId}/quotes", linstromGetQuotes)
|
||||
// Create a new quote message of a given note
|
||||
router.HandleFunc("POST /notes/{noteId}/quotes", linstromAddQuote)
|
||||
|
||||
// Pinning
|
||||
|
||||
// Pin a note to account profile
|
||||
router.HandleFunc("POST /notes/{noteId}/pin", linstromPinNote)
|
||||
// Unpin a note from account profile
|
||||
router.HandleFunc("DELETE /notes/{noteId}/pin", linstromUnpinNote)
|
||||
// Reports
|
||||
router.HandleFunc("POST /notes/{noteId}/report", linstromReportNote)
|
||||
router.HandleFunc("DELETE /notes/{noteId}/report", linstromRetractReportNote)
|
||||
// Admin
|
||||
router.HandleFunc("POST /notes/{noteId}/admin/cw", linstromForceCWNote)
|
||||
|
||||
// Accounts
|
||||
// Creating a new account happens either during fetch of a remote one or during registration with a passkey
|
||||
|
||||
// Get an account
|
||||
router.HandleFunc("GET /accounts/{accountId}", linstromGetAccount)
|
||||
// Update own account
|
||||
// Technically also requires authenticated account to also be owner or correct admin perms,
|
||||
// but that's annoying to handle in a general sense. So leaving that to the function
|
||||
// though figuring out a nice generic-ish way to handle those checks would be nice too
|
||||
router.HandleFunc(
|
||||
"PATCH /accounts/{accountId}",
|
||||
requireValidSessionMiddleware(linstromUpdateAccount),
|
||||
)
|
||||
// Delete own account
|
||||
// Technically also requires authenticated account to also be owner or correct admin perms,
|
||||
// but that's annoying to handle in a general sense. So leaving that to the function
|
||||
router.HandleFunc(
|
||||
"DELETE /accounts/{accountId}",
|
||||
requireValidSessionMiddleware(linstromDeleteAccount),
|
||||
)
|
||||
// Follow
|
||||
// Is logged in following accountId
|
||||
router.HandleFunc(
|
||||
"GET /accounts/{accountId}/follow/to",
|
||||
requireValidSessionMiddleware(linstromIsFollowingToAccount),
|
||||
)
|
||||
// Is accountId following logged in
|
||||
router.HandleFunc(
|
||||
"GET /accounts/{accountId}/follow/from",
|
||||
requireValidSessionMiddleware(linstromIsFollowingFromAccount),
|
||||
)
|
||||
// Send follow request to accountId
|
||||
router.HandleFunc("POST /accounts/{accountId}/follow", linstromFollowAccount)
|
||||
// Undo follow request to accountId
|
||||
router.HandleFunc("DELETE /accounts/{accountId}/follow", linstromUnfollowAccount)
|
||||
|
||||
// Block
|
||||
|
||||
// Is logged in account blocking target account
|
||||
router.HandleFunc("GET /accounts/{accountId}/block", linstromIsBlockingAccount)
|
||||
// Block target account
|
||||
router.HandleFunc("POST /accounts/{accountId}/block", linstromBlockAccount)
|
||||
// Unblock target account
|
||||
router.HandleFunc("DELETE /accounts/{accountId}/block", linstromUnblockAccount)
|
||||
|
||||
// Mute
|
||||
|
||||
// Has logged in account muted target account?
|
||||
router.HandleFunc("GET /accounts/{accountId}/mute", linstromIsMutedAccount)
|
||||
// Mute target account
|
||||
router.HandleFunc("POST /accounts/{accountId}/mute", linstromMuteAccount)
|
||||
// Unmute target account
|
||||
router.HandleFunc("DELETE /accounts/{accountId}/mute", linstromUnmuteAccount)
|
||||
|
||||
// Report
|
||||
|
||||
// Report a target account
|
||||
router.HandleFunc("POST /accounts/{accountId}/reports", linstromReportAccount)
|
||||
// Undo report on target account
|
||||
router.HandleFunc("DELETE /accounts/{accountId}/reports", linstromRetractReportAccount)
|
||||
|
||||
// Admin
|
||||
|
||||
// Add new role to account
|
||||
router.Handle(
|
||||
"POST /accounts/{accountId}/admin/roles",
|
||||
buildRequirePermissionsMiddleware(
|
||||
&storage.Role{CanAssignRoles: other.IntoPointer(true)},
|
||||
)(
|
||||
http.HandlerFunc(linstromAdminAddRoleAccount),
|
||||
),
|
||||
)
|
||||
// Remove role from account
|
||||
router.Handle(
|
||||
"DELETE /accounts/{accountId}/admin/roles/{roleName}",
|
||||
buildRequirePermissionsMiddleware(&storage.Role{CanAssignRoles: other.IntoPointer(true)})(
|
||||
http.HandlerFunc(linstromAdminRemoveRoleAccount),
|
||||
),
|
||||
)
|
||||
// Send a warning to account
|
||||
router.HandleFunc("POST /accounts/{accountId}/admin/warn", linstromAdminWarnAccount)
|
||||
|
||||
// Roles
|
||||
|
||||
// Get a role
|
||||
router.HandleFunc("GET /roles/{roleId}", linstromGetRole)
|
||||
// Create a new role
|
||||
router.HandleFunc("POST /roles", linstromCreateRole)
|
||||
// Update a role. Builtin roles cannot be edited
|
||||
router.HandleFunc("PATCH /roles/{roleId}", linstromUpdateRole)
|
||||
// Delete a role. Builtin roles cannot be deleted
|
||||
router.HandleFunc("DELETE /roles/{roleId}", linstromDeleteRole)
|
||||
|
||||
// Media metadata
|
||||
|
||||
// Get the metadata for some media
|
||||
router.HandleFunc("GET /media/{mediaId}", linstromGetMediaMetadata)
|
||||
// Upload new media
|
||||
router.HandleFunc("POST /media", linstromNewMediaMetadata)
|
||||
// Update the metadata for some media
|
||||
router.HandleFunc("PATCH /media/{mediaId}", linstromUpdateMediaMetadata)
|
||||
// Delete a media entry
|
||||
router.HandleFunc("DELETE /media/{mediaId}", linstromDeleteMediaMetadata)
|
||||
|
||||
// Event streams
|
||||
router.HandleFunc("/streams", linstromEventStream)
|
||||
|
||||
return router
|
||||
}
|
||||
|
||||
func setupLinstromS2SApiV1Router() http.Handler {
|
||||
router := http.NewServeMux()
|
||||
// TODO: Figure out a decent server to server API definition
|
||||
router.HandleFunc("/", placeholderEndpoint)
|
||||
return router
|
||||
}
|
|
@ -1,375 +0,0 @@
|
|||
package server
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
httputil "git.mstar.dev/mstar/goutils/http"
|
||||
"git.mstar.dev/mstar/goutils/sliceutils"
|
||||
"github.com/google/jsonapi"
|
||||
"github.com/rs/zerolog/hlog"
|
||||
|
||||
"git.mstar.dev/mstar/linstrom/storage"
|
||||
)
|
||||
|
||||
// No create account. That happens during passkey registration
|
||||
// and remote accounts are getting created at fetch time
|
||||
func linstromGetAccount(w http.ResponseWriter, r *http.Request) {
|
||||
store := StorageFromRequest(r)
|
||||
log := hlog.FromRequest(r)
|
||||
|
||||
accId := AccountIdFromRequest(r)
|
||||
acc, err := store.FindAccountById(accId)
|
||||
switch err {
|
||||
case nil:
|
||||
// Ok, do nothing
|
||||
case storage.ErrEntryNotFound:
|
||||
httputil.HttpErr(w, HttpErrIdNotFound, "account not found", http.StatusNotFound)
|
||||
return
|
||||
default:
|
||||
log.Error().Err(err).Str("account-id", accId).Msg("Failed to get account from storage")
|
||||
httputil.HttpErr(
|
||||
w,
|
||||
HttpErrIdDbFailure,
|
||||
"Failed to get account from storage",
|
||||
http.StatusInternalServerError,
|
||||
)
|
||||
return
|
||||
}
|
||||
actorId, ok := r.Context().Value(ContextKeyActorId).(string)
|
||||
if ok {
|
||||
// Logged in user is accessing account, check if target account has them blocked
|
||||
roles, err := store.FindRolesByNames(acc.Roles)
|
||||
if err != nil {
|
||||
log.Error().
|
||||
Err(err).
|
||||
Strs("role-names", acc.Roles).
|
||||
Msg("Failed to get roles from storage")
|
||||
httputil.HttpErr(
|
||||
w,
|
||||
HttpErrIdDbFailure,
|
||||
"Failed to get roles of target account",
|
||||
http.StatusInternalServerError,
|
||||
)
|
||||
return
|
||||
}
|
||||
collapsedRole := storage.CollapseRolesIntoOne(roles...)
|
||||
if sliceutils.Contains(collapsedRole.BlockedUsers, actorId) {
|
||||
// Actor account is in list of blocked accounts, deny access
|
||||
httputil.HttpErr(w, HttpErrIdNotAuthenticated, "Access forbidden", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
outAccount, err := convertAccountStorageToLinstrom(acc, store)
|
||||
if err != nil {
|
||||
log.Error().
|
||||
Err(err).
|
||||
Msg("Failed to convert storage account (and attached data) into linstrom API representation")
|
||||
httputil.HttpErr(
|
||||
w,
|
||||
HttpErrIdConversionFailure,
|
||||
"Failed to convert storage account and attached data into API representation",
|
||||
http.StatusInternalServerError,
|
||||
)
|
||||
return
|
||||
}
|
||||
err = jsonapi.MarshalPayload(w, outAccount)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Any("account", outAccount).Msg("Failed to marshal and write account")
|
||||
}
|
||||
}
|
||||
|
||||
func linstromUpdateAccount(w http.ResponseWriter, r *http.Request) {
|
||||
store := StorageFromRequest(r)
|
||||
log := hlog.FromRequest(r)
|
||||
// Assumption: There must be a valid session once this function is called due to middlewares
|
||||
actorId, _ := ActorIdFromRequest(r)
|
||||
apiTarget := linstromAccount{}
|
||||
err := jsonapi.UnmarshalPayload(r.Body, &apiTarget)
|
||||
if err != nil {
|
||||
httputil.HttpErr(w, HttpErrIdBadRequest, "bad body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
targetAccId := AccountIdFromRequest(r)
|
||||
if apiTarget.Id != targetAccId {
|
||||
httputil.HttpErr(
|
||||
w,
|
||||
HttpErrIdBadRequest,
|
||||
"Provided entity's id doesn't match path id",
|
||||
http.StatusConflict,
|
||||
)
|
||||
return
|
||||
}
|
||||
if !(actorId == apiTarget.Id) {
|
||||
httputil.HttpErr(w, HttpErrIdNotAuthenticated, "Invalid permissions", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
dbTarget, err := store.FindAccountById(apiTarget.Id)
|
||||
// Assumption: The only sort of errors that can be returned are db failures.
|
||||
// The account not existing is not possible anymore since this is in a valid session
|
||||
// and a session is only injected if the actor account can be found
|
||||
if err != nil {
|
||||
log.Error().
|
||||
Err(err).
|
||||
Str("account-id", actorId).
|
||||
Msg("Failed to get account from db despite valid session")
|
||||
httputil.HttpErr(
|
||||
w,
|
||||
HttpErrIdDbFailure,
|
||||
"Failed to get account despite valid session",
|
||||
http.StatusInternalServerError,
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
// location, birthday, icon, banner, background, custom fields
|
||||
// bluesky federation, uhhh
|
||||
|
||||
dbTarget.DisplayName = apiTarget.DisplayName
|
||||
dbTarget.Indexable = apiTarget.Indexable
|
||||
dbTarget.Description = apiTarget.Description
|
||||
// TODO: Figure out how to properly update custom fields
|
||||
dbTarget.Gender = apiTarget.Pronouns
|
||||
dbTarget.IdentifiesAs = sliceutils.Map(
|
||||
sliceutils.Filter(apiTarget.IdentifiesAs, func(t string) bool {
|
||||
return storage.IsValidBeing(t)
|
||||
}),
|
||||
func(t string) storage.Being { return storage.Being(t) },
|
||||
)
|
||||
dbTarget.Indexable = apiTarget.Indexable
|
||||
dbTarget.RestrictedFollow = apiTarget.RestrictedFollow
|
||||
err = store.UpdateAccount(dbTarget)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to update account in db")
|
||||
httputil.HttpErr(
|
||||
w,
|
||||
HttpErrIdDbFailure,
|
||||
"Failed to update db entries",
|
||||
http.StatusInternalServerError,
|
||||
)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
newAccData, err := convertAccountStorageToLinstrom(dbTarget, store)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to convert updated account back into api form")
|
||||
httputil.HttpErr(
|
||||
w,
|
||||
HttpErrIdConversionFailure,
|
||||
"Failed to convert updated account back into api form",
|
||||
http.StatusInternalServerError,
|
||||
)
|
||||
return
|
||||
}
|
||||
err = jsonapi.MarshalPayload(w, newAccData)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to marshal and write updated account")
|
||||
}
|
||||
}
|
||||
|
||||
func linstromDeleteAccount(w http.ResponseWriter, r *http.Request) {
|
||||
actorId, _ := ActorIdFromRequest(r)
|
||||
log := hlog.FromRequest(r)
|
||||
store := StorageFromRequest(r)
|
||||
targetAccountId := AccountIdFromRequest(r)
|
||||
if targetAccountId != actorId {
|
||||
log.Debug().
|
||||
Str("actor-id", actorId).
|
||||
Str("target-id", targetAccountId).
|
||||
Msg("Invalid attempt to delete account")
|
||||
httputil.HttpErr(w, HttpErrIdNotAuthenticated, "Action forbidden", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
log.Info().Str("account-id", actorId).Msg("Deleting account")
|
||||
acc, err := store.FindAccountById(targetAccountId)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Str("account-id", actorId).Msg("Failed to get account for deletion")
|
||||
httputil.HttpErr(
|
||||
w,
|
||||
HttpErrIdDbFailure,
|
||||
"Failed to get account from db",
|
||||
http.StatusInternalServerError,
|
||||
)
|
||||
return
|
||||
}
|
||||
// allRoles, err := store.FindRolesByNames(acc.Roles)
|
||||
// collapsedRole := storage.CollapseRolesIntoOne(allRoles...)
|
||||
|
||||
// TODO: Start job of sending out deletion messages to all federated servers
|
||||
|
||||
// Clean up related data first
|
||||
// TODO: Also delete media files
|
||||
err = store.DeleteRoleByName(acc.ID)
|
||||
if err != nil {
|
||||
log.Error().
|
||||
Err(err).
|
||||
Str("role-name", acc.ID).
|
||||
Msg("Failed to delete user role for account deletion request")
|
||||
httputil.HttpErr(
|
||||
w,
|
||||
HttpErrIdDbFailure,
|
||||
"Failed to delete user role",
|
||||
http.StatusInternalServerError,
|
||||
)
|
||||
return
|
||||
}
|
||||
err = store.DeleteAllUserFieldsForAccountId(acc.ID)
|
||||
if err != nil {
|
||||
log.Error().
|
||||
Err(err).
|
||||
Str("account-id", acc.ID).
|
||||
Msg("Failed to delete custom info fields for account deletion")
|
||||
httputil.HttpErr(
|
||||
w,
|
||||
HttpErrIdDbFailure,
|
||||
"Failed to delete custom info fields",
|
||||
http.StatusInternalServerError,
|
||||
)
|
||||
return
|
||||
}
|
||||
err = store.DeleteAccount(actorId)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Str("account-id", acc.ID).Msg("Failed to delete account")
|
||||
httputil.HttpErr(
|
||||
w,
|
||||
HttpErrIdDbFailure,
|
||||
"Failed to delete account from db",
|
||||
http.StatusInternalServerError,
|
||||
)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Is logged in following accountId
|
||||
func linstromIsFollowingToAccount(w http.ResponseWriter, r *http.Request) {
|
||||
store := StorageFromRequest(r)
|
||||
log := hlog.FromRequest(r)
|
||||
actorId, _ := ActorIdFromRequest(r)
|
||||
targetId := AccountIdFromRequest(r)
|
||||
|
||||
relation, err := store.GetRelationBetween(actorId, targetId)
|
||||
var outData linstromRelation
|
||||
switch err {
|
||||
case nil:
|
||||
outData = linstromRelation{
|
||||
Id: relation.ID,
|
||||
CreatedAt: relation.CreatedAt,
|
||||
UpdatedAt: relation.UpdatedAt,
|
||||
FromId: relation.FromId,
|
||||
ToId: relation.ToId,
|
||||
Accepted: relation.Accepted,
|
||||
Requested: true,
|
||||
}
|
||||
case storage.ErrEntryNotFound:
|
||||
outData = linstromRelation{
|
||||
Id: relation.ID,
|
||||
CreatedAt: relation.CreatedAt,
|
||||
UpdatedAt: relation.UpdatedAt,
|
||||
FromId: relation.FromId,
|
||||
ToId: relation.ToId,
|
||||
Accepted: false,
|
||||
Requested: false,
|
||||
}
|
||||
default:
|
||||
log.Error().
|
||||
Err(err).
|
||||
Str("from-id", actorId).
|
||||
Str("to-id", targetId).
|
||||
Msg("Failed to get follow relation")
|
||||
httputil.HttpErr(
|
||||
w,
|
||||
HttpErrIdDbFailure,
|
||||
"Failed to get relation",
|
||||
http.StatusInternalServerError,
|
||||
)
|
||||
}
|
||||
|
||||
err = jsonapi.MarshalPayload(w, outData)
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Msg("Failed to marshal response")
|
||||
httputil.HttpErr(
|
||||
w,
|
||||
HttpErrIdJsonMarshalFail,
|
||||
"Failed to marshal response",
|
||||
http.StatusInternalServerError,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func linstromIsFollowingFromAccount(w http.ResponseWriter, r *http.Request) {
|
||||
store := StorageFromRequest(r)
|
||||
log := hlog.FromRequest(r)
|
||||
actorId, _ := ActorIdFromRequest(r)
|
||||
targetId := AccountIdFromRequest(r)
|
||||
|
||||
relation, err := store.GetRelationBetween(targetId, actorId)
|
||||
var outData linstromRelation
|
||||
switch err {
|
||||
case nil:
|
||||
outData = linstromRelation{
|
||||
Id: relation.ID,
|
||||
CreatedAt: relation.CreatedAt,
|
||||
UpdatedAt: relation.UpdatedAt,
|
||||
FromId: relation.FromId,
|
||||
ToId: relation.ToId,
|
||||
Accepted: relation.Accepted,
|
||||
Requested: true,
|
||||
}
|
||||
case storage.ErrEntryNotFound:
|
||||
outData = linstromRelation{
|
||||
Id: relation.ID,
|
||||
CreatedAt: relation.CreatedAt,
|
||||
UpdatedAt: relation.UpdatedAt,
|
||||
FromId: relation.FromId,
|
||||
ToId: relation.ToId,
|
||||
Accepted: false,
|
||||
Requested: false,
|
||||
}
|
||||
default:
|
||||
log.Error().
|
||||
Err(err).
|
||||
Str("from-id", targetId).
|
||||
Str("to-id", actorId).
|
||||
Msg("Failed to get follow relation")
|
||||
httputil.HttpErr(
|
||||
w,
|
||||
HttpErrIdDbFailure,
|
||||
"Failed to get relation",
|
||||
http.StatusInternalServerError,
|
||||
)
|
||||
}
|
||||
|
||||
err = jsonapi.MarshalPayload(w, outData)
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Msg("Failed to marshal response")
|
||||
httputil.HttpErr(
|
||||
w,
|
||||
HttpErrIdJsonMarshalFail,
|
||||
"Failed to marshal response",
|
||||
http.StatusInternalServerError,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func linstromFollowAccount(w http.ResponseWriter, r *http.Request) {}
|
||||
func linstromUnfollowAccount(w http.ResponseWriter, r *http.Request) {}
|
||||
|
||||
func linstromIsBlockingAccount(w http.ResponseWriter, r *http.Request) {}
|
||||
func linstromBlockAccount(w http.ResponseWriter, r *http.Request) {}
|
||||
func linstromUnblockAccount(w http.ResponseWriter, r *http.Request) {}
|
||||
|
||||
func linstromIsMutedAccount(w http.ResponseWriter, r *http.Request) {}
|
||||
func linstromMuteAccount(w http.ResponseWriter, r *http.Request) {}
|
||||
func linstromUnmuteAccount(w http.ResponseWriter, r *http.Request) {}
|
||||
|
||||
func linstromReportAccount(w http.ResponseWriter, r *http.Request) {}
|
||||
func linstromRetractReportAccount(w http.ResponseWriter, r *http.Request) {}
|
||||
|
||||
func linstromAdminAddRoleAccount(w http.ResponseWriter, r *http.Request) {}
|
||||
func linstromAdminRemoveRoleAccount(w http.ResponseWriter, r *http.Request) {}
|
||||
func linstromAdminWarnAccount(w http.ResponseWriter, r *http.Request) {}
|
||||
|
||||
func linstromGetRole(w http.ResponseWriter, r *http.Request) {}
|
||||
func linstromCreateRole(w http.ResponseWriter, r *http.Request) {}
|
||||
func linstromUpdateRole(w http.ResponseWriter, r *http.Request) {}
|
||||
func linstromDeleteRole(w http.ResponseWriter, r *http.Request) {}
|
|
@ -1,8 +0,0 @@
|
|||
package server
|
||||
|
||||
import "net/http"
|
||||
|
||||
func linstromGetMediaMetadata(w http.ResponseWriter, r *http.Request) {}
|
||||
func linstromNewMediaMetadata(w http.ResponseWriter, r *http.Request) {}
|
||||
func linstromUpdateMediaMetadata(w http.ResponseWriter, r *http.Request) {}
|
||||
func linstromDeleteMediaMetadata(w http.ResponseWriter, r *http.Request) {}
|
|
@ -1,158 +0,0 @@
|
|||
package server
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
httputil "git.mstar.dev/mstar/goutils/http"
|
||||
"github.com/google/jsonapi"
|
||||
"github.com/rs/zerolog/hlog"
|
||||
|
||||
"git.mstar.dev/mstar/linstrom/shared"
|
||||
"git.mstar.dev/mstar/linstrom/storage"
|
||||
)
|
||||
|
||||
// Notes
|
||||
func linstromGetNote(w http.ResponseWriter, r *http.Request) {
|
||||
store := StorageFromRequest(r)
|
||||
noteId := NoteIdFromRequest(r)
|
||||
log := hlog.FromRequest(r)
|
||||
sNote, err := store.FindNoteById(noteId)
|
||||
switch err {
|
||||
case nil:
|
||||
// Found, progress past switch statement
|
||||
case storage.ErrEntryNotFound:
|
||||
httputil.HttpErr(w, HttpErrIdNotFound, "Note not found", http.StatusNotFound)
|
||||
return
|
||||
default:
|
||||
log.Error().Err(err).Str("note-id", noteId).Msg("Failed to get note from db")
|
||||
httputil.HttpErr(
|
||||
w,
|
||||
HttpErrIdDbFailure,
|
||||
"Failed to get note from db",
|
||||
http.StatusInternalServerError,
|
||||
)
|
||||
return
|
||||
}
|
||||
note, err := convertNoteStorageToLinstrom(sNote, store)
|
||||
if err != nil {
|
||||
log.Error().
|
||||
Err(err).
|
||||
Str("note-id", noteId).
|
||||
Msg("Failed to convert note into linstrom api form")
|
||||
httputil.HttpErr(
|
||||
w,
|
||||
HttpErrIdConversionFailure,
|
||||
"Failed to convert note",
|
||||
http.StatusInternalServerError,
|
||||
)
|
||||
return
|
||||
}
|
||||
err = jsonapi.MarshalPayload(w, note)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Any("note", note).Msg("Failed to marshal and send note")
|
||||
httputil.HttpErr(
|
||||
w,
|
||||
HttpErrIdJsonMarshalFail,
|
||||
"Failed to convert note",
|
||||
http.StatusInternalServerError,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func linstromUpdateNote(w http.ResponseWriter, r *http.Request) {}
|
||||
func linstromNewNote(w http.ResponseWriter, r *http.Request) {
|
||||
store := StorageFromRequest(r)
|
||||
actorId, ok := ActorIdFromRequest(r)
|
||||
log := hlog.FromRequest(r)
|
||||
|
||||
if !ok {
|
||||
httputil.HttpErr(
|
||||
w,
|
||||
HttpErrIdNotAuthenticated,
|
||||
"Needs a valid session to create new notes",
|
||||
http.StatusUnauthorized,
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
newNote := linstromNote{}
|
||||
err := jsonapi.UnmarshalPayload(r.Body, &newNote)
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Msg("Failed to unmarshal body")
|
||||
httputil.HttpErr(w, HttpErrIdBadRequest, "bad body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if newNote.AuthorId != actorId {
|
||||
log.Debug().
|
||||
Str("actor-id", actorId).
|
||||
Str("target-id", newNote.AuthorId).
|
||||
Msg("Blocking attempt at creating a note for a different account")
|
||||
httputil.HttpErr(
|
||||
w,
|
||||
HttpErrIdNotAllowed,
|
||||
"creating a note for someone else is not allowed",
|
||||
http.StatusForbidden,
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
rawTags := shared.TagsFromText(newNote.RawContent)
|
||||
|
||||
note, err := store.CreateNoteLocal(
|
||||
actorId,
|
||||
newNote.RawContent,
|
||||
newNote.ContentWarning,
|
||||
time.Now(),
|
||||
newNote.AttachmentIds,
|
||||
newNote.EmoteIds,
|
||||
newNote.InReplyToId,
|
||||
newNote.QuotesId,
|
||||
storage.NoteAccessLevel(newNote.AccessLevel),
|
||||
rawTags,
|
||||
// tags []string
|
||||
)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Any("note", newNote).Msg("Failed to insert new note into storage")
|
||||
httputil.HttpErr(
|
||||
w,
|
||||
HttpErrIdDbFailure,
|
||||
"Failed to insert new note into db",
|
||||
http.StatusInternalServerError,
|
||||
)
|
||||
return
|
||||
}
|
||||
_ = note
|
||||
}
|
||||
func linstromDeleteNote(w http.ResponseWriter, r *http.Request) {}
|
||||
|
||||
// Reactions
|
||||
func linstromGetReactions(w http.ResponseWriter, r *http.Request) {}
|
||||
func linstromAddReaction(w http.ResponseWriter, r *http.Request) {}
|
||||
func linstromDeleteReaction(w http.ResponseWriter, r *http.Request) {}
|
||||
func linstromUpdateReaction(w http.ResponseWriter, r *http.Request) {}
|
||||
|
||||
// Boosts
|
||||
func linstromGetBoosts(w http.ResponseWriter, r *http.Request) {}
|
||||
func linstromAddBoost(w http.ResponseWriter, r *http.Request) {}
|
||||
func linstromRemoveBoost(w http.ResponseWriter, r *http.Request) {}
|
||||
|
||||
// Quotes
|
||||
func linstromGetQuotes(w http.ResponseWriter, r *http.Request) {}
|
||||
func linstromAddQuote(w http.ResponseWriter, r *http.Request) {}
|
||||
|
||||
// No delete quote since quotes are their own notes with an extra attribute
|
||||
|
||||
// Pinning
|
||||
func linstromPinNote(w http.ResponseWriter, r *http.Request) {}
|
||||
func linstromUnpinNote(w http.ResponseWriter, r *http.Request) {}
|
||||
|
||||
// Reporting
|
||||
func linstromReportNote(w http.ResponseWriter, r *http.Request) {}
|
||||
func linstromRetractReportNote(w http.ResponseWriter, r *http.Request) {}
|
||||
|
||||
// Admin tools
|
||||
// TODO: Figure out more admin tools for managing notes
|
||||
// Delete can be done via normal note delete, common permission check
|
||||
func linstromForceCWNote(w http.ResponseWriter, r *http.Request) {}
|
|
@ -1,24 +0,0 @@
|
|||
package server
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/rs/zerolog/hlog"
|
||||
)
|
||||
|
||||
// TODO: Decide where to put data stream handlers
|
||||
|
||||
var websocketUpgrader = websocket.Upgrader{}
|
||||
|
||||
// Entrypoint for a new stream will be in here at least
|
||||
func linstromEventStream(w http.ResponseWriter, r *http.Request) {
|
||||
log := hlog.FromRequest(r)
|
||||
conn, err := websocketUpgrader.Upgrade(w, r, nil)
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Msg("Failed to upgrade connection to websocket")
|
||||
}
|
||||
defer conn.Close()
|
||||
// TODO: Handle initial request for what events to receive
|
||||
// TODO: Stream all requested events until connection closes (due to bad data from client or disconnect)
|
||||
}
|
|
@ -1,184 +0,0 @@
|
|||
package server
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"git.mstar.dev/mstar/goutils/sliceutils"
|
||||
"git.mstar.dev/mstar/linstrom/storage"
|
||||
)
|
||||
|
||||
func convertAccountStorageToLinstrom(
|
||||
acc *storage.Account,
|
||||
store *storage.Storage,
|
||||
) (*linstromAccount, error) {
|
||||
storageServer, err := store.FindRemoteServerById(acc.ServerId)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("remote server: %w", err)
|
||||
}
|
||||
apiServer, err := convertServerStorageToLinstrom(storageServer, store)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("remote server conversion: %w", err)
|
||||
}
|
||||
storageIcon, err := store.GetMediaMetadataById(acc.Icon)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("icon: %w", err)
|
||||
}
|
||||
var apiBanner *linstromMediaMetadata
|
||||
if acc.Banner != nil {
|
||||
storageBanner, err := store.GetMediaMetadataById(*acc.Banner)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("banner: %w", err)
|
||||
}
|
||||
apiBanner = convertMediaMetadataStorageToLinstrom(storageBanner)
|
||||
}
|
||||
|
||||
var apiBackground *linstromMediaMetadata
|
||||
if acc.Background != nil {
|
||||
storageBackground, err := store.GetMediaMetadataById(*acc.Background)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("banner: %w", err)
|
||||
}
|
||||
apiBackground = convertMediaMetadataStorageToLinstrom(storageBackground)
|
||||
}
|
||||
storageFields, err := store.FindMultipleUserFieldsById(acc.CustomFields)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("customFields: %w", err)
|
||||
}
|
||||
|
||||
return &linstromAccount{
|
||||
Id: acc.ID,
|
||||
CreatedAt: acc.CreatedAt,
|
||||
UpdatedAt: &acc.UpdatedAt,
|
||||
Username: acc.Username,
|
||||
OriginServer: apiServer,
|
||||
OriginServerId: int(acc.ServerId),
|
||||
DisplayName: acc.DisplayName,
|
||||
CustomFields: sliceutils.Map(
|
||||
storageFields,
|
||||
func(t storage.UserInfoField) *linstromCustomAccountField {
|
||||
return convertInfoFieldStorageToLinstrom(t)
|
||||
},
|
||||
),
|
||||
CustomFieldIds: acc.CustomFields,
|
||||
IsBot: acc.IsBot,
|
||||
Description: acc.Description,
|
||||
Icon: convertMediaMetadataStorageToLinstrom(storageIcon),
|
||||
IconId: acc.Icon,
|
||||
Banner: apiBanner,
|
||||
BannerId: acc.Banner,
|
||||
Background: apiBackground,
|
||||
BackgroundId: acc.Background,
|
||||
RelationIds: acc.Relations,
|
||||
Indexable: acc.Indexable,
|
||||
RestrictedFollow: acc.RestrictedFollow,
|
||||
IdentifiesAs: sliceutils.Map(
|
||||
acc.IdentifiesAs,
|
||||
func(t storage.Being) string { return string(t) },
|
||||
),
|
||||
Pronouns: acc.Gender,
|
||||
Roles: acc.Roles,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func convertServerStorageToLinstrom(
|
||||
server *storage.RemoteServer,
|
||||
store *storage.Storage,
|
||||
) (*linstromOriginServer, error) {
|
||||
storageMeta, err := store.GetMediaMetadataById(server.Icon)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("icon metadata: %w", err)
|
||||
}
|
||||
return &linstromOriginServer{
|
||||
Id: server.ID,
|
||||
CreatedAt: server.CreatedAt,
|
||||
UpdatedAt: &server.UpdatedAt,
|
||||
ServerType: string(server.ServerType),
|
||||
Domain: server.Domain,
|
||||
DisplayName: server.Name,
|
||||
Icon: convertMediaMetadataStorageToLinstrom(storageMeta),
|
||||
IsSelf: server.IsSelf,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func convertMediaMetadataStorageToLinstrom(metadata *storage.MediaMetadata) *linstromMediaMetadata {
|
||||
return &linstromMediaMetadata{
|
||||
Id: metadata.ID,
|
||||
CreatedAt: metadata.CreatedAt,
|
||||
UpdatedAt: &metadata.UpdatedAt,
|
||||
IsRemote: metadata.Remote,
|
||||
Url: metadata.Location,
|
||||
MimeType: metadata.Type,
|
||||
Name: metadata.Name,
|
||||
AltText: metadata.AltText,
|
||||
Blurred: metadata.Blurred,
|
||||
}
|
||||
}
|
||||
|
||||
func convertInfoFieldStorageToLinstrom(field storage.UserInfoField) *linstromCustomAccountField {
|
||||
return &linstromCustomAccountField{
|
||||
Id: field.ID,
|
||||
CreatedAt: field.CreatedAt,
|
||||
UpdatedAt: &field.UpdatedAt,
|
||||
Key: field.Name,
|
||||
Value: field.Value,
|
||||
Verified: &field.Confirmed,
|
||||
BelongsToId: field.BelongsTo,
|
||||
}
|
||||
}
|
||||
|
||||
func convertNoteStorageToLinstrom(
|
||||
note *storage.Note,
|
||||
store *storage.Storage,
|
||||
) (*linstromNote, error) {
|
||||
panic("Not implemented")
|
||||
}
|
||||
|
||||
func convertEmoteStorageToLinstrom(
|
||||
emote *storage.Emote,
|
||||
store *storage.Storage,
|
||||
) (*linstromEmote, error) {
|
||||
storageServer, err := store.FindRemoteServerById(emote.ServerId)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("server: %w", err)
|
||||
}
|
||||
server, err := convertServerStorageToLinstrom(storageServer, store)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("server conversion: %w", err)
|
||||
}
|
||||
storageMedia, err := store.GetMediaMetadataById(emote.MetadataId)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("media metadata: %w", err)
|
||||
}
|
||||
media := convertMediaMetadataStorageToLinstrom(storageMedia)
|
||||
|
||||
return &linstromEmote{
|
||||
Id: emote.ID,
|
||||
MetadataId: emote.MetadataId,
|
||||
Metadata: media,
|
||||
Name: emote.Name,
|
||||
ServerId: emote.ServerId,
|
||||
Server: server,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func convertReactionStorageToLinstrom(
|
||||
reaction *storage.Reaction,
|
||||
store *storage.Storage,
|
||||
) (*linstromReaction, error) {
|
||||
storageEmote, err := store.GetEmoteById(reaction.EmoteId)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("emote: %w", err)
|
||||
}
|
||||
emote, err := convertEmoteStorageToLinstrom(storageEmote, store)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("emote conversion: %w", err)
|
||||
}
|
||||
|
||||
return &linstromReaction{
|
||||
Id: reaction.ID,
|
||||
NoteId: reaction.NoteId,
|
||||
ReactorId: reaction.ReactorId,
|
||||
EmoteId: reaction.EmoteId,
|
||||
Emote: emote,
|
||||
}, nil
|
||||
}
|
|
@ -1,8 +0,0 @@
|
|||
// Code generated by cmd/RolesApiConverter DO NOT EDIT.
|
||||
// If you need to refresh the content, run go generate again
|
||||
package server
|
||||
|
||||
import "git.mstar.dev/mstar/linstrom/storage"
|
||||
func convertRoleStorageToLinstrom(r storage.Role) linstromRole {
|
||||
return linstromRole{Id:r.ID,CreatedAt:r.CreatedAt,UpdatedAt:r.UpdatedAt,Name:r.Name,Priority:r.Priority,IsUserRole:r.IsUserRole,IsBuiltIn:r.IsBuiltIn,CanRecoverDeletedNotes:r.CanRecoverDeletedNotes,CanSendFollowerOnlyNotes:r.CanSendFollowerOnlyNotes,CanSendReplies:r.CanSendReplies,AutoNsfwMedia:r.AutoNsfwMedia,WithholdNotesForManualApproval:r.WithholdNotesForManualApproval,CanAffectOtherAdmins:r.CanAffectOtherAdmins,CanAssignRoles:r.CanAssignRoles,CanOverwriteDisplayNames:r.CanOverwriteDisplayNames,CanManageAvatarDecorations:r.CanManageAvatarDecorations,CanSendMedia:r.CanSendMedia,ScanCreatedLocalNotes:r.ScanCreatedLocalNotes,CanSendCustomEmotes:r.CanSendCustomEmotes,CanSendPublicNotes:r.CanSendPublicNotes,CanIncludeSurvey:r.CanIncludeSurvey,AutoCwPostsText:r.AutoCwPostsText,CanManageCustomEmotes:r.CanManageCustomEmotes,CanSendAnnouncements:r.CanSendAnnouncements,CanSendLocalNotes:r.CanSendLocalNotes,CanBoost:r.CanBoost,CanLogin:r.CanLogin,CanMentionOthers:r.CanMentionOthers,CanManageAds:r.CanManageAds,CanQuote:r.CanQuote,CanChangeDisplayName:r.CanChangeDisplayName,CanSubmitReports:r.CanSubmitReports,FullAdmin:r.FullAdmin,CanSendPrivateNotes:r.CanSendPrivateNotes,CanIncludeLinks:r.CanIncludeLinks,CanFederateFedi:r.CanFederateFedi,HasMentionCountLimit:r.HasMentionCountLimit,AutoCwPosts:r.AutoCwPosts,ScanCreatedPublicNotes:r.ScanCreatedPublicNotes,DisallowInteractionsWith:r.DisallowInteractionsWith,CanDeleteNotes:r.CanDeleteNotes,CanConfirmWithheldNotes:r.CanConfirmWithheldNotes,ScanCreatedPrivateNotes:r.ScanCreatedPrivateNotes,WithholdNotesBasedOnRegex:r.WithholdNotesBasedOnRegex,WithholdNotesRegexes:r.WithholdNotesRegexes,CanSupressInteractionsBetweenUsers:r.CanSupressInteractionsBetweenUsers,CanViewDeletedNotes:r.CanViewDeletedNotes,CanSendCustomReactions:r.CanSendCustomReactions,CanFederateBsky:r.CanFederateBsky,BlockedUsers:r.BlockedUsers,MentionLimit:r.MentionLimit,ScanCreatedFollowerOnlyNotes:r.ScanCreatedFollowerOnlyNotes,}
|
||||
}
|
|
@ -1,118 +0,0 @@
|
|||
package server
|
||||
|
||||
// Contains types used by the Linstrom API. Types comply with the jsonapi spec
|
||||
|
||||
import "time"
|
||||
|
||||
var (
|
||||
_ = linstromRole{}
|
||||
_ = linstromRelation{}
|
||||
)
|
||||
|
||||
type linstromNote struct {
|
||||
Id string `jsonapi:"primary,notes"`
|
||||
RawContent string `jsonapi:"attr,content"`
|
||||
OriginServer *linstromOriginServer `jsonapi:"relation,origin-server"`
|
||||
OriginServerId int `jsonapi:"attr,origin-server-id"`
|
||||
ReactionCount string `jsonapi:"attr,reaction-count"`
|
||||
CreatedAt time.Time `jsonapi:"attr,created-at"`
|
||||
UpdatedAt *time.Time `jsonapi:"attr,updated-at,omitempty"`
|
||||
Author *linstromAccount `jsonapi:"relation,author"`
|
||||
AuthorId string `jsonapi:"attr,author-id"`
|
||||
ContentWarning *string `jsonapi:"attr,content-warning,omitempty"`
|
||||
InReplyToId *string `jsonapi:"attr,in-reply-to-id,omitempty"`
|
||||
QuotesId *string `jsonapi:"attr,quotes-id,omitempty"`
|
||||
EmoteIds []string `jsonapi:"attr,emotes,omitempty"`
|
||||
Attachments []*linstromMediaMetadata `jsonapi:"relation,attachments,omitempty"`
|
||||
AttachmentIds []string `jsonapi:"attr,attachment-ids"`
|
||||
AccessLevel uint8 `jsonapi:"attr,access-level"`
|
||||
Pings []*linstromAccount `jsonapi:"relation,pings,omitempty"`
|
||||
PingIds []string `jsonapi:"attr,ping-ids,omitempty"`
|
||||
ReactionIds []uint `jsonapi:"attr,reaction-ids"`
|
||||
}
|
||||
|
||||
type linstromOriginServer struct {
|
||||
Id uint `jsonapi:"primary,origins"`
|
||||
CreatedAt time.Time `jsonapi:"attr,created-at"`
|
||||
UpdatedAt *time.Time `jsonapi:"attr,updated-at,omitempty"`
|
||||
ServerType string `jsonapi:"attr,server-type"` // one of "Linstrom", "Mastodon", "Plemora", "Misskey" or "Wafrn"
|
||||
Domain string `jsonapi:"attr,domain"`
|
||||
DisplayName string `jsonapi:"attr,display-name"`
|
||||
Icon *linstromMediaMetadata `jsonapi:"relation,icon"`
|
||||
IsSelf bool `jsonapi:"attr,is-self"`
|
||||
}
|
||||
|
||||
type linstromMediaMetadata struct {
|
||||
Id string `jsonapi:"primary,media"`
|
||||
CreatedAt time.Time `jsonapi:"attr,created-at"`
|
||||
UpdatedAt *time.Time `jsonapi:"attr,updated-at,omitempty"`
|
||||
IsRemote bool `jsonapi:"attr,is-remote"`
|
||||
Url string `jsonapi:"attr,url"`
|
||||
MimeType string `jsonapi:"attr,mime-type"`
|
||||
Name string `jsonapi:"attr,name"`
|
||||
AltText string `jsonapi:"attr,alt-text"`
|
||||
Blurred bool `jsonapi:"attr,blurred"`
|
||||
}
|
||||
|
||||
type linstromAccount struct {
|
||||
Id string `jsonapi:"primary,accounts"`
|
||||
CreatedAt time.Time `jsonapi:"attr,created-at"`
|
||||
UpdatedAt *time.Time `jsonapi:"attr,updated-at,omitempty"`
|
||||
Username string `jsonapi:"attr,username"`
|
||||
OriginServer *linstromOriginServer `jsonapi:"relation,origin-server"`
|
||||
OriginServerId int `jsonapi:"attr,origin-server-id"`
|
||||
DisplayName string `jsonapi:"attr,display-name"`
|
||||
CustomFields []*linstromCustomAccountField `jsonapi:"relation,custom-fields"`
|
||||
CustomFieldIds []uint `jsonapi:"attr,custom-field-ids"`
|
||||
IsBot bool `jsonapi:"attr,is-bot"`
|
||||
Description string `jsonapi:"attr,description"`
|
||||
Icon *linstromMediaMetadata `jsonapi:"relation,icon"`
|
||||
IconId string `jsonapi:"attr,icon-id"`
|
||||
Banner *linstromMediaMetadata `jsonapi:"relation,banner"`
|
||||
BannerId *string `jsonapi:"attr,banner-id"`
|
||||
Background *linstromMediaMetadata `jsonapi:"relation,background"`
|
||||
BackgroundId *string `jsonapi:"attr,background-id"`
|
||||
RelationIds []uint `jsonapi:"attr,follows-ids"`
|
||||
Indexable bool `jsonapi:"attr,indexable"`
|
||||
RestrictedFollow bool `jsonapi:"attr,restricted-follow"`
|
||||
IdentifiesAs []string `jsonapi:"attr,identifies-as"`
|
||||
Pronouns []string `jsonapi:"attr,pronouns"`
|
||||
Roles []string `jsonapi:"attr,roles"`
|
||||
}
|
||||
|
||||
type linstromCustomAccountField struct {
|
||||
Id uint `jsonapi:"primary,custom-account-fields"`
|
||||
CreatedAt time.Time `jsonapi:"attr,created-at"`
|
||||
UpdatedAt *time.Time `jsonapi:"attr,updated-at,omitempty"`
|
||||
Key string `jsonapi:"attr,key"`
|
||||
Value string `jsonapi:"attr,value"`
|
||||
Verified *bool `jsonapi:"attr,verified,omitempty"`
|
||||
BelongsToId string `jsonapi:"attr,belongs-to-id"`
|
||||
}
|
||||
|
||||
type linstromRelation struct {
|
||||
Id uint `jsonapi:"primary,relations"`
|
||||
CreatedAt time.Time `jsonapi:"attr,created-at"`
|
||||
UpdatedAt time.Time `jsonapi:"attr,updated-at"`
|
||||
FromId string `jsonapi:"attr,from-id"`
|
||||
ToId string `jsonapi:"attr,to-id"`
|
||||
Requested bool `jsonapi:"attr,requested"`
|
||||
Accepted bool `jsonapi:"attr,accepted"`
|
||||
}
|
||||
|
||||
type linstromReaction struct {
|
||||
Id uint `jsonapi:"primary,reactions"`
|
||||
NoteId string `jsonapi:"attr,note-id"`
|
||||
ReactorId string `jsonapi:"attr,reactor-id"`
|
||||
EmoteId uint `jsonapi:"attr,emote-id"`
|
||||
Emote *linstromEmote `jsonapi:"relation,emote"`
|
||||
}
|
||||
|
||||
type linstromEmote struct {
|
||||
Id uint `jsonapi:"primary,emotes"`
|
||||
MetadataId string `jsonapi:"attr,metadata-id"`
|
||||
Metadata *linstromMediaMetadata `jsonapi:"relation,metadata"`
|
||||
Name string `jsonapi:"attr,name"`
|
||||
ServerId uint `jsonapi:"attr,server-id"`
|
||||
Server *linstromOriginServer `jsonapi:"relation,server"`
|
||||
}
|
|
@ -1,59 +0,0 @@
|
|||
// Code generated by cmd/RolesApiTypeGenerator DO NOT EDIT.
|
||||
// If you need to refresh the content, run go generate again
|
||||
package server
|
||||
|
||||
import "time"
|
||||
type linstromRole struct {
|
||||
Id uint `jsonapi:"primary,roles"`
|
||||
CreatedAt time.Time `jsonapi:"attr,created-at"`
|
||||
UpdatedAt time.Time `jsonapi:"attr,updated-at"`
|
||||
Name string `jsonapi:"attr,name"`
|
||||
Priority uint32 `jsonapi:"attr,priority"`
|
||||
IsUserRole bool `jsonapi:"attr,is-user-role"`
|
||||
IsBuiltIn bool `jsonapi:"attr,is-built-in"`
|
||||
CanIncludeLinks *bool `jsonapi:"attr,can-include-links"`
|
||||
CanIncludeSurvey *bool `jsonapi:"attr,can-include-survey"`
|
||||
CanLogin *bool `jsonapi:"attr,can-login"`
|
||||
CanSupressInteractionsBetweenUsers *bool `jsonapi:"attr,can-supress-interactions-between-users"`
|
||||
CanManageCustomEmotes *bool `jsonapi:"attr,can-manage-custom-emotes"`
|
||||
CanSendAnnouncements *bool `jsonapi:"attr,can-send-announcements"`
|
||||
CanSendReplies *bool `jsonapi:"attr,can-send-replies"`
|
||||
CanRecoverDeletedNotes *bool `jsonapi:"attr,can-recover-deleted-notes"`
|
||||
CanMentionOthers *bool `jsonapi:"attr,can-mention-others"`
|
||||
CanSendFollowerOnlyNotes *bool `jsonapi:"attr,can-send-follower-only-notes"`
|
||||
CanSendPrivateNotes *bool `jsonapi:"attr,can-send-private-notes"`
|
||||
HasMentionCountLimit *bool `jsonapi:"attr,has-mention-count-limit"`
|
||||
MentionLimit *uint32 `jsonapi:"attr,mention-limit"`
|
||||
ScanCreatedFollowerOnlyNotes *bool `jsonapi:"attr,scan-created-follower-only-notes"`
|
||||
FullAdmin *bool `jsonapi:"attr,full-admin"`
|
||||
CanSendPublicNotes *bool `jsonapi:"attr,can-send-public-notes"`
|
||||
CanFederateBsky *bool `jsonapi:"attr,can-federate-bsky"`
|
||||
CanSubmitReports *bool `jsonapi:"attr,can-submit-reports"`
|
||||
AutoNsfwMedia *bool `jsonapi:"attr,auto-nsfw-media"`
|
||||
AutoCwPostsText *string `jsonapi:"attr,auto-cw-posts-text"`
|
||||
WithholdNotesForManualApproval *bool `jsonapi:"attr,withhold-notes-for-manual-approval"`
|
||||
CanManageAds *bool `jsonapi:"attr,can-manage-ads"`
|
||||
CanFederateFedi *bool `jsonapi:"attr,can-federate-fedi"`
|
||||
CanChangeDisplayName *bool `jsonapi:"attr,can-change-display-name"`
|
||||
ScanCreatedPublicNotes *bool `jsonapi:"attr,scan-created-public-notes"`
|
||||
ScanCreatedPrivateNotes *bool `jsonapi:"attr,scan-created-private-notes"`
|
||||
CanAssignRoles *bool `jsonapi:"attr,can-assign-roles"`
|
||||
CanSendCustomReactions *bool `jsonapi:"attr,can-send-custom-reactions"`
|
||||
BlockedUsers []string `jsonapi:"attr,blocked-users"`
|
||||
DisallowInteractionsWith []string `jsonapi:"attr,disallow-interactions-with"`
|
||||
CanDeleteNotes *bool `jsonapi:"attr,can-delete-notes"`
|
||||
CanViewDeletedNotes *bool `jsonapi:"attr,can-view-deleted-notes"`
|
||||
CanManageAvatarDecorations *bool `jsonapi:"attr,can-manage-avatar-decorations"`
|
||||
CanSendMedia *bool `jsonapi:"attr,can-send-media"`
|
||||
CanSendLocalNotes *bool `jsonapi:"attr,can-send-local-notes"`
|
||||
CanBoost *bool `jsonapi:"attr,can-boost"`
|
||||
AutoCwPosts *bool `jsonapi:"attr,auto-cw-posts"`
|
||||
ScanCreatedLocalNotes *bool `jsonapi:"attr,scan-created-local-notes"`
|
||||
WithholdNotesRegexes []string `jsonapi:"attr,withhold-notes-regexes"`
|
||||
CanSendCustomEmotes *bool `jsonapi:"attr,can-send-custom-emotes"`
|
||||
WithholdNotesBasedOnRegex *bool `jsonapi:"attr,withhold-notes-based-on-regex"`
|
||||
CanAffectOtherAdmins *bool `jsonapi:"attr,can-affect-other-admins"`
|
||||
CanConfirmWithheldNotes *bool `jsonapi:"attr,can-confirm-withheld-notes"`
|
||||
CanOverwriteDisplayNames *bool `jsonapi:"attr,can-overwrite-display-names"`
|
||||
CanQuote *bool `jsonapi:"attr,can-quote"`
|
||||
}
|
|
@ -1,288 +0,0 @@
|
|||
package server
|
||||
|
||||
import "net/http"
|
||||
|
||||
// Mounted at /api
|
||||
func setupApiRouter() http.Handler {
|
||||
router := http.NewServeMux()
|
||||
router.Handle("/linstrom/", http.StripPrefix("/linstrom", setupLinstromApiRouter()))
|
||||
|
||||
// Section MastoApi
|
||||
// First segment are endpoints that will need to be moved to primary router since at top route
|
||||
router.HandleFunc("GET /oauth/authorize", placeholderEndpoint)
|
||||
router.HandleFunc("POST /oauth/token", placeholderEndpoint)
|
||||
router.HandleFunc("POST /oauth/revoke", placeholderEndpoint)
|
||||
router.HandleFunc("GET /.well-known/oauth-authorization-server", placeholderEndpoint)
|
||||
// These ones are actually mounted under /api/
|
||||
router.HandleFunc("POST /v1/apps", placeholderEndpoint)
|
||||
router.HandleFunc("GET /v1/apps/verify_credentials", placeholderEndpoint)
|
||||
router.HandleFunc("POST /v1/emails/confirmations", placeholderEndpoint)
|
||||
router.HandleFunc("POST /v1/accounts", placeholderEndpoint)
|
||||
router.HandleFunc("GET /v1/accounts/verify_credentials", placeholderEndpoint)
|
||||
router.HandleFunc("PATCH /v1/accounts/update_credentials", placeholderEndpoint)
|
||||
router.HandleFunc("GET /v1/accounts/{id}", placeholderEndpoint)
|
||||
router.HandleFunc("GET /v1/accounts", placeholderEndpoint)
|
||||
router.HandleFunc("GET /v1/accounts/{id}/statuses", placeholderEndpoint)
|
||||
router.HandleFunc("GET /v1/accounts/{id}/followers", placeholderEndpoint)
|
||||
router.HandleFunc("GET /v1/accounts/{id}/following", placeholderEndpoint)
|
||||
router.HandleFunc("GET /v1/accounts/{id}/featured_tags", placeholderEndpoint)
|
||||
router.HandleFunc("GET /v1/accounts/{id}/lists", placeholderEndpoint)
|
||||
router.HandleFunc("POST /v1/accounts/{id}/follow", placeholderEndpoint)
|
||||
router.HandleFunc("POST /v1/accounts/{id}/unfollow", placeholderEndpoint)
|
||||
router.HandleFunc("POST /v1/accounts/{id}/remove_from_followers", placeholderEndpoint)
|
||||
router.HandleFunc("POST /v1/accounts/{id}/block", placeholderEndpoint)
|
||||
router.HandleFunc("POST /v1/accounts/{id}/unblock", placeholderEndpoint)
|
||||
router.HandleFunc("POST /v1/accounts/{id}/mute", placeholderEndpoint)
|
||||
router.HandleFunc("POST /v1/accounts/{id}/unmute", placeholderEndpoint)
|
||||
router.HandleFunc("POST /v1/accounts/{id}/pin", placeholderEndpoint)
|
||||
router.HandleFunc("POST /v1/accounts/{id}/unpin", placeholderEndpoint)
|
||||
router.HandleFunc("POST /v1/accounts/{id}/note", placeholderEndpoint)
|
||||
router.HandleFunc("GET /v1/accounts/relationships", placeholderEndpoint)
|
||||
router.HandleFunc("GET /v1/accounts/familiar_followers", placeholderEndpoint)
|
||||
router.HandleFunc("GET /v1/accounts/search", placeholderEndpoint)
|
||||
router.HandleFunc("GET /v1/accounts/lookup", placeholderEndpoint)
|
||||
router.HandleFunc("GET /v1/bookmarks", placeholderEndpoint)
|
||||
router.HandleFunc("GET /v1/favourites", placeholderEndpoint)
|
||||
router.HandleFunc("GET /v1/mutes", placeholderEndpoint)
|
||||
router.HandleFunc("GET /v1/blocks", placeholderEndpoint)
|
||||
router.HandleFunc("GET /v1/domain_blocks", placeholderEndpoint)
|
||||
router.HandleFunc("POST /v1/domain_blocks", placeholderEndpoint)
|
||||
router.HandleFunc("DELETE /v1/domain_blocks", placeholderEndpoint)
|
||||
router.HandleFunc("GET /v2/filters", placeholderEndpoint)
|
||||
router.HandleFunc("GET /v2/filters/{id}", placeholderEndpoint)
|
||||
router.HandleFunc("POST /v2/filters", placeholderEndpoint)
|
||||
router.HandleFunc("PUT /v2/filters/{id}", placeholderEndpoint)
|
||||
router.HandleFunc("DELETE /v2/filters/{id}", placeholderEndpoint)
|
||||
router.HandleFunc("GET /v2/filters/:filter_id/keywords", placeholderEndpoint)
|
||||
router.HandleFunc("POST /v2/filters/:filter_id/keywords", placeholderEndpoint)
|
||||
router.HandleFunc("GET /v2/filters/keywords/{id}", placeholderEndpoint)
|
||||
router.HandleFunc("PUT /v2/filters/keywords/{id}", placeholderEndpoint)
|
||||
router.HandleFunc("DELETE /v2/filters/keywords/{id}", placeholderEndpoint)
|
||||
router.HandleFunc("GET /v2/filters/:filter_id/statuses", placeholderEndpoint)
|
||||
router.HandleFunc("POST /v2/filters/:filter_id/statuses", placeholderEndpoint)
|
||||
router.HandleFunc("GET /v2/filters/statuses/{id}", placeholderEndpoint)
|
||||
router.HandleFunc("DELETE /v2/filters/statuses/{id}", placeholderEndpoint)
|
||||
router.HandleFunc("POST /v1/reports", placeholderEndpoint)
|
||||
router.HandleFunc("GET /v1/follow_requests", placeholderEndpoint)
|
||||
router.HandleFunc("POST /v1/follow_requests/:account_id/authorize", placeholderEndpoint)
|
||||
router.HandleFunc("POST /v1/follow_requests/:account_id/reject", placeholderEndpoint)
|
||||
router.HandleFunc("GET /v1/endorsements", placeholderEndpoint)
|
||||
router.HandleFunc("GET /v1/featured_tags", placeholderEndpoint)
|
||||
router.HandleFunc("POST /v1/featured_tags", placeholderEndpoint)
|
||||
router.HandleFunc("DELETE /v1/featured_tags/{id}", placeholderEndpoint)
|
||||
router.HandleFunc("GET /v1/featured_tags/suggestions", placeholderEndpoint)
|
||||
router.HandleFunc("GET /v1/preferences", placeholderEndpoint)
|
||||
router.HandleFunc("GET /v1/followed_tags", placeholderEndpoint)
|
||||
router.HandleFunc("GET /v2/suggestions", placeholderEndpoint)
|
||||
router.HandleFunc("DELETE /v1/suggestions/:account_id", placeholderEndpoint)
|
||||
router.HandleFunc("GET /v1/tags/{id}", placeholderEndpoint)
|
||||
router.HandleFunc("POST /v1/tags/{id}/follow", placeholderEndpoint)
|
||||
router.HandleFunc("POST /v1/tags/{id}/unfollow", placeholderEndpoint)
|
||||
router.HandleFunc("DELETE /v1/profile/avatar", placeholderEndpoint)
|
||||
router.HandleFunc("DELETE /v1/profile/header", placeholderEndpoint)
|
||||
router.HandleFunc("POST /v1/statuses", placeholderEndpoint)
|
||||
router.HandleFunc("GET /v1/statuses/{id}", placeholderEndpoint)
|
||||
router.HandleFunc("GET /v1/statuses", placeholderEndpoint)
|
||||
router.HandleFunc("DELETE /v1/statuses/{id}", placeholderEndpoint)
|
||||
router.HandleFunc("GET /v1/statuses/{id}/context", placeholderEndpoint)
|
||||
router.HandleFunc("POST /v1/statuses/{id}/translate", placeholderEndpoint)
|
||||
router.HandleFunc("GET /v1/statuses/{id}/reblogged_by", placeholderEndpoint)
|
||||
router.HandleFunc("GET /v1/statuses/{id}/favourited_by", placeholderEndpoint)
|
||||
router.HandleFunc("POST /v1/statuses/{id}/favourite", placeholderEndpoint)
|
||||
router.HandleFunc("POST /v1/statuses/{id}/unfavourite", placeholderEndpoint)
|
||||
router.HandleFunc("POST /v1/statuses/{id}/reblog", placeholderEndpoint)
|
||||
router.HandleFunc("POST /v1/statuses/{id}/unreblog", placeholderEndpoint)
|
||||
router.HandleFunc("POST /v1/statuses/{id}/bookmark", placeholderEndpoint)
|
||||
router.HandleFunc("POST /v1/statuses/{id}/unbookmark", placeholderEndpoint)
|
||||
router.HandleFunc("POST /v1/statuses/{id}/mute", placeholderEndpoint)
|
||||
router.HandleFunc("POST /v1/statuses/{id}/unmute", placeholderEndpoint)
|
||||
router.HandleFunc("POST /v1/statuses/{id}/pin", placeholderEndpoint)
|
||||
router.HandleFunc("POST /v1/statuses/{id}/unpin", placeholderEndpoint)
|
||||
router.HandleFunc("PUT /v1/statuses/{id}", placeholderEndpoint)
|
||||
router.HandleFunc("GET /v1/statuses/{id}/history", placeholderEndpoint)
|
||||
router.HandleFunc("GET /v1/statuses/{id}/source", placeholderEndpoint)
|
||||
router.HandleFunc("POST /v2/media", placeholderEndpoint)
|
||||
router.HandleFunc("GET /v1/media/{id}", placeholderEndpoint)
|
||||
router.HandleFunc("PUT /v1/media/{id}", placeholderEndpoint)
|
||||
router.HandleFunc("GET /v1/polls/{id}", placeholderEndpoint)
|
||||
router.HandleFunc("POST /v1/polls/{id}/votes", placeholderEndpoint)
|
||||
router.HandleFunc("GET /v1/scheduled_statuses", placeholderEndpoint)
|
||||
router.HandleFunc("GET /v1/scheduled_statuses/{id}", placeholderEndpoint)
|
||||
router.HandleFunc("PUT /v1/scheduled_statuses/{id}", placeholderEndpoint)
|
||||
router.HandleFunc("DELETE /v1/scheduled_statuses/{id}", placeholderEndpoint)
|
||||
router.HandleFunc("GET /v1/timelines/public", placeholderEndpoint)
|
||||
router.HandleFunc("GET /v1/timelines/tag/:hashtag", placeholderEndpoint)
|
||||
router.HandleFunc("GET /v1/timelines/home", placeholderEndpoint)
|
||||
router.HandleFunc("GET /v1/timelines/link", placeholderEndpoint) // ?url=:url
|
||||
router.HandleFunc("GET /v1/timelines/list/:list_id", placeholderEndpoint)
|
||||
router.HandleFunc("GET /v1/conversations", placeholderEndpoint)
|
||||
router.HandleFunc("DELETE /v1/conversations/{id}", placeholderEndpoint)
|
||||
router.HandleFunc("POST /v1/conversations/{id}/read", placeholderEndpoint)
|
||||
router.HandleFunc("GET /v1/lists", placeholderEndpoint)
|
||||
router.HandleFunc("GET /v1/lists/{id}", placeholderEndpoint)
|
||||
router.HandleFunc("POST /v1/lists", placeholderEndpoint)
|
||||
router.HandleFunc("PUT /v1/lists/{id}", placeholderEndpoint)
|
||||
router.HandleFunc("DELETE /v1/lists/{id}", placeholderEndpoint)
|
||||
router.HandleFunc("GET /v1/lists/{id}/accounts", placeholderEndpoint)
|
||||
router.HandleFunc("POST /v1/lists/{id}/accounts", placeholderEndpoint)
|
||||
router.HandleFunc("DELETE /v1/lists/{id}/accounts", placeholderEndpoint)
|
||||
router.HandleFunc("GET /v1/markers", placeholderEndpoint)
|
||||
router.HandleFunc("POST /v1/markers", placeholderEndpoint)
|
||||
router.HandleFunc("GET /v1/streaming/health", placeholderEndpoint)
|
||||
router.HandleFunc("GET /v1/streaming/user", placeholderEndpoint)
|
||||
router.HandleFunc("GET /v1/streaming/user/notification", placeholderEndpoint)
|
||||
router.HandleFunc("GET /v1/streaming/public", placeholderEndpoint)
|
||||
router.HandleFunc("GET /v1/streaming/public/local", placeholderEndpoint)
|
||||
router.HandleFunc("GET /v1/streaming/public/remote", placeholderEndpoint)
|
||||
router.HandleFunc("GET /v1/streaming/hashtag", placeholderEndpoint)
|
||||
router.HandleFunc("GET /v1/streaming/hashtag/local", placeholderEndpoint)
|
||||
router.HandleFunc("GET /v1/streaming/list", placeholderEndpoint)
|
||||
router.HandleFunc("GET /v1/streaming/direct", placeholderEndpoint)
|
||||
router.HandleFunc("GET /v2/notifications", placeholderEndpoint)
|
||||
router.HandleFunc("GET /v2/notifications/:group_key", placeholderEndpoint)
|
||||
router.HandleFunc("POST /v2/notifications/:group_key/dismiss", placeholderEndpoint)
|
||||
router.HandleFunc("GET /v2/notifications/:group_key/accounts", placeholderEndpoint)
|
||||
router.HandleFunc("GET /v2/notifications/unread_count", placeholderEndpoint)
|
||||
router.HandleFunc("GET /v1/notifications", placeholderEndpoint)
|
||||
router.HandleFunc("GET /v1/notifications/{id}", placeholderEndpoint)
|
||||
router.HandleFunc("POST /v1/notifications/clear", placeholderEndpoint)
|
||||
router.HandleFunc("POST /v1/notifications/{id}/dismiss", placeholderEndpoint)
|
||||
router.HandleFunc("GET /v1/notifications/unread_count", placeholderEndpoint)
|
||||
router.HandleFunc("GET /v2/notifications/policy", placeholderEndpoint)
|
||||
router.HandleFunc("PATCH /v2/notifications/policy", placeholderEndpoint)
|
||||
router.HandleFunc("GET /v1/notifications/requests", placeholderEndpoint)
|
||||
router.HandleFunc("GET /v1/notifications/requests/{id}", placeholderEndpoint)
|
||||
router.HandleFunc("POST /v1/notifications/requests/{id}/accept", placeholderEndpoint)
|
||||
router.HandleFunc("POST /v1/notifications/requests/{id}/dismiss", placeholderEndpoint)
|
||||
router.HandleFunc("POST /v1/notifications/requests/accept", placeholderEndpoint)
|
||||
router.HandleFunc("POST /v1/notifications/requests/dismiss", placeholderEndpoint)
|
||||
router.HandleFunc("GET /v1/notifications/requests/merged", placeholderEndpoint)
|
||||
router.HandleFunc("POST /v1/push/subscription", placeholderEndpoint)
|
||||
router.HandleFunc("GET /v1/push/subscription", placeholderEndpoint)
|
||||
router.HandleFunc("PUT /v1/push/subscription", placeholderEndpoint)
|
||||
router.HandleFunc("DELETE /v1/push/subscription", placeholderEndpoint)
|
||||
router.HandleFunc("GET /v2/search", placeholderEndpoint)
|
||||
router.HandleFunc("GET /v2/instance", placeholderEndpoint)
|
||||
router.HandleFunc("GET /v1/instance/peers", placeholderEndpoint)
|
||||
router.HandleFunc("GET /v1/instance/activity", placeholderEndpoint)
|
||||
router.HandleFunc("GET /v1/instance/rules", placeholderEndpoint)
|
||||
router.HandleFunc("GET /v1/instance/domain_blocks", placeholderEndpoint)
|
||||
router.HandleFunc("GET /v1/instance/extended_description", placeholderEndpoint)
|
||||
router.HandleFunc("GET /v1/instance/translation_languages", placeholderEndpoint)
|
||||
router.HandleFunc("GET /v1/trends/tags", placeholderEndpoint)
|
||||
router.HandleFunc("GET /v1/trends/statuses", placeholderEndpoint)
|
||||
router.HandleFunc("GET /v1/trends/links", placeholderEndpoint)
|
||||
router.HandleFunc("GET /v1/directory", placeholderEndpoint)
|
||||
router.HandleFunc("GET /v1/custom_emojis", placeholderEndpoint)
|
||||
router.HandleFunc("GET /v1/announcements", placeholderEndpoint)
|
||||
router.HandleFunc("POST /v1/announcements/{id}/dismiss", placeholderEndpoint)
|
||||
router.HandleFunc("PUT /v1/announcements/{id}/reactions/:name", placeholderEndpoint)
|
||||
router.HandleFunc("DELETE /v1/announcements/{id}/reactions/:name", placeholderEndpoint)
|
||||
router.HandleFunc("GET /v1/admin/accounts", placeholderEndpoint)
|
||||
router.HandleFunc("GET /v2/admin/accounts", placeholderEndpoint)
|
||||
router.HandleFunc("GET /v1/admin/accounts/{id}", placeholderEndpoint)
|
||||
router.HandleFunc("POST /v1/admin/accounts/{id}/approve", placeholderEndpoint)
|
||||
router.HandleFunc("POST /v1/admin/accounts/{id}/reject", placeholderEndpoint)
|
||||
router.HandleFunc("DELETE /v1/admin/accounts/{id}", placeholderEndpoint)
|
||||
router.HandleFunc("POST /v1/admin/accounts/{id}/action", placeholderEndpoint)
|
||||
router.HandleFunc("POST /v1/admin/accounts/{id}/enable", placeholderEndpoint)
|
||||
router.HandleFunc("POST /v1/admin/accounts/{id}/unsilence", placeholderEndpoint)
|
||||
router.HandleFunc("POST /v1/admin/accounts/{id}/unsuspend", placeholderEndpoint)
|
||||
router.HandleFunc("POST /v1/admin/accounts/{id}/unsensitive", placeholderEndpoint)
|
||||
router.HandleFunc("GET /v1/admin/canonical_email_blocks", placeholderEndpoint)
|
||||
router.HandleFunc("GET /v1/admin/canonical_email_blocks/{id}", placeholderEndpoint)
|
||||
router.HandleFunc("POST /v1/admin/canonical_email_blocks/test", placeholderEndpoint)
|
||||
router.HandleFunc("POST /v1/admin/canonical_email_blocks", placeholderEndpoint)
|
||||
router.HandleFunc("DELETE /v1/admin/canonical_email_blocks/{id}", placeholderEndpoint)
|
||||
router.HandleFunc("POST /v1/admin/dimensions", placeholderEndpoint)
|
||||
router.HandleFunc("GET /v1/admin/domain_allows", placeholderEndpoint)
|
||||
router.HandleFunc("GET /v1/admin/domain_allows/{id}", placeholderEndpoint)
|
||||
router.HandleFunc("POST /v1/admin/domain_allows", placeholderEndpoint)
|
||||
router.HandleFunc("DELETE /v1/admin/domain_allows/{id}", placeholderEndpoint)
|
||||
router.HandleFunc("GET /v1/admin/domain_blocks", placeholderEndpoint)
|
||||
router.HandleFunc("GET /v1/admin/domain_blocks/{id}", placeholderEndpoint)
|
||||
router.HandleFunc("POST /v1/admin/domain_blocks", placeholderEndpoint)
|
||||
router.HandleFunc("PUT /v1/admin/domain_blocks/{id}", placeholderEndpoint)
|
||||
router.HandleFunc("DELETE /v1/admin/domain_blocks/{id}", placeholderEndpoint)
|
||||
router.HandleFunc("GET /v1/admin/email_domain_blocks", placeholderEndpoint)
|
||||
router.HandleFunc("GET /v1/admin/email_domain_blocks/{id}", placeholderEndpoint)
|
||||
router.HandleFunc("POST /v1/admin/email_domain_blocks", placeholderEndpoint)
|
||||
router.HandleFunc("DELETE /v1/admin/email_domain_blocks/{id}", placeholderEndpoint)
|
||||
router.HandleFunc("GET /v1/admin/ip_blocks", placeholderEndpoint)
|
||||
router.HandleFunc("GET /v1/admin/ip_blocks/{id}", placeholderEndpoint)
|
||||
router.HandleFunc("POST /v1/admin/ip_blocks", placeholderEndpoint)
|
||||
router.HandleFunc("PUT /v1/admin/ip_blocks/{id}", placeholderEndpoint)
|
||||
router.HandleFunc("DELETE /v1/admin/ip_blocks/{id}", placeholderEndpoint)
|
||||
router.HandleFunc("POST /v1/admin/measures", placeholderEndpoint)
|
||||
router.HandleFunc("GET /v1/admin/reports", placeholderEndpoint)
|
||||
router.HandleFunc("GET /v1/admin/reports/{id}", placeholderEndpoint)
|
||||
router.HandleFunc("PUT /v1/admin/reports/{id}", placeholderEndpoint)
|
||||
router.HandleFunc("POST /v1/admin/reports/{id}/assign_to_self", placeholderEndpoint)
|
||||
router.HandleFunc("POST /v1/admin/reports/{id}/unassign", placeholderEndpoint)
|
||||
router.HandleFunc("POST /v1/admin/reports/{id}/resolve", placeholderEndpoint)
|
||||
router.HandleFunc("POST /v1/admin/reports/{id}/reopen", placeholderEndpoint)
|
||||
router.HandleFunc("POST /v1/admin/retention", placeholderEndpoint)
|
||||
router.HandleFunc("GET /v1/admin/trends/links", placeholderEndpoint)
|
||||
router.HandleFunc("GET /v1/admin/trends/statuses", placeholderEndpoint)
|
||||
router.HandleFunc("GET /v1/admin/trends/tags", placeholderEndpoint)
|
||||
router.HandleFunc("GET /oembed", placeholderEndpoint)
|
||||
|
||||
router.HandleFunc(
|
||||
"GET /v1/accounts/{id}/identity_proofs",
|
||||
placeholderEndpoint,
|
||||
) // Deprecated
|
||||
router.HandleFunc(
|
||||
"GET /v1/filters",
|
||||
placeholderEndpoint,
|
||||
) // Deprecated
|
||||
router.HandleFunc(
|
||||
"GET /v1/filters/{id}",
|
||||
placeholderEndpoint,
|
||||
) // Deprecated
|
||||
router.HandleFunc(
|
||||
"POST /v1/filters",
|
||||
placeholderEndpoint,
|
||||
) // Deprecated
|
||||
router.HandleFunc(
|
||||
"PUT /v1/filters/{id}",
|
||||
placeholderEndpoint,
|
||||
) // Deprecated
|
||||
router.HandleFunc(
|
||||
"DELETE /v1/filters/{id}",
|
||||
placeholderEndpoint,
|
||||
) // Deprecated
|
||||
router.HandleFunc(
|
||||
"GET /v1/suggestions",
|
||||
placeholderEndpoint,
|
||||
) // Deprecated
|
||||
router.HandleFunc(
|
||||
"GET /v1/statuses/{id}/card",
|
||||
placeholderEndpoint,
|
||||
) // Deprecated
|
||||
router.HandleFunc(
|
||||
"POST /v1/media",
|
||||
placeholderEndpoint,
|
||||
) // Deprecated
|
||||
router.HandleFunc(
|
||||
"GET /v1/timelines/direct",
|
||||
placeholderEndpoint,
|
||||
) // Deprecated
|
||||
router.HandleFunc(
|
||||
"POST /v1/notifications/dismiss",
|
||||
placeholderEndpoint,
|
||||
) // Removed
|
||||
router.HandleFunc(
|
||||
"GET /v1/search",
|
||||
placeholderEndpoint,
|
||||
) // Removed
|
||||
router.HandleFunc(
|
||||
"GET /v1/instance",
|
||||
placeholderEndpoint,
|
||||
) // Deprecated
|
||||
router.HandleFunc(
|
||||
"GET /proofs",
|
||||
placeholderEndpoint,
|
||||
) // Removed
|
||||
|
||||
return router
|
||||
}
|
|
@ -1,23 +0,0 @@
|
|||
package server
|
||||
|
||||
const ContextKeyPasskeyUsername = "context-passkey-username"
|
||||
|
||||
type ContextKey string
|
||||
|
||||
const (
|
||||
ContextKeyStorage ContextKey = "Context key for storage"
|
||||
ContextKeyActorId ContextKey = "Context key for actor id"
|
||||
)
|
||||
|
||||
const (
|
||||
HttpErrIdPlaceholder = iota
|
||||
HttpErrIdMissingContextValue
|
||||
HttpErrIdDbFailure
|
||||
HttpErrIdNotAuthenticated
|
||||
HttpErrIdJsonMarshalFail
|
||||
HttpErrIdBadRequest
|
||||
HttpErrIdAlreadyExists
|
||||
HttpErrIdNotFound
|
||||
HttpErrIdConversionFailure
|
||||
HttpErrIdNotAllowed
|
||||
)
|
|
@ -1,38 +0,0 @@
|
|||
package server
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"git.mstar.dev/mstar/linstrom/storage"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// Mount under /.well-known/webfinger
|
||||
func webfingerHandler(w http.ResponseWriter, r *http.Request) {
|
||||
logger := zerolog.Ctx(r.Context())
|
||||
store := storage.Storage{}
|
||||
|
||||
requestedResource := r.FormValue("resource")
|
||||
if requestedResource == "" {
|
||||
http.Error(w, "bad request. Include \"resource\" parameter", http.StatusBadRequest)
|
||||
logger.Debug().Msg("Resource parameter missing. Cancelling")
|
||||
return
|
||||
}
|
||||
accName := strings.TrimPrefix(requestedResource, "acc:")
|
||||
acc, err := store.FindAccountByFullHandle(accName)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
http.Error(w, "account not found", http.StatusNotFound)
|
||||
logger.Debug().Str("account-name", accName).Msg("Account not found")
|
||||
} else {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
logger.Error().Err(err).Msg("Error while searching for account")
|
||||
return
|
||||
}
|
||||
}
|
||||
fmt.Fprint(w, acc)
|
||||
}
|
|
@ -1,22 +0,0 @@
|
|||
package server
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// Mounted at /
|
||||
func setupFrontendRouter(interactiveFs, noscriptFs fs.FS) http.Handler {
|
||||
router := http.NewServeMux()
|
||||
router.Handle("/noscript/", http.StripPrefix("/noscript", http.FileServerFS(noscriptFs)))
|
||||
router.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
http.ServeFileFS(w, r, interactiveFs, "index.html")
|
||||
})
|
||||
router.Handle("/assets/", http.FileServerFS(interactiveFs))
|
||||
router.HandleFunc(
|
||||
"/robots.txt",
|
||||
func(w http.ResponseWriter, r *http.Request) { http.ServeFileFS(w, r, interactiveFs, "robots.txt") },
|
||||
)
|
||||
|
||||
return router
|
||||
}
|
|
@ -1,77 +0,0 @@
|
|||
package server
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/pprof"
|
||||
"runtime"
|
||||
"runtime/debug"
|
||||
"time"
|
||||
|
||||
httputil "git.mstar.dev/mstar/goutils/http"
|
||||
)
|
||||
|
||||
// Mounted at /profiling
|
||||
func setupProfilingHandler() http.Handler {
|
||||
router := http.NewServeMux()
|
||||
router.HandleFunc("/", profilingRootHandler)
|
||||
router.HandleFunc("GET /current-goroutines", metricActiveGoroutinesHandler)
|
||||
router.HandleFunc("GET /memory", metricMemoryStatsHandler)
|
||||
router.HandleFunc("GET /pprof/cpu", pprof.Profile)
|
||||
router.Handle("GET /pprof/memory", pprof.Handler("heap"))
|
||||
router.Handle("GET /pprof/goroutines", pprof.Handler("goroutine"))
|
||||
router.Handle("GET /pprof/blockers", pprof.Handler("block"))
|
||||
|
||||
return router
|
||||
}
|
||||
|
||||
func isAliveHandler(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Fprint(w, "yup")
|
||||
}
|
||||
|
||||
func profilingRootHandler(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Fprint(
|
||||
w,
|
||||
"Endpoints: /, /{memory,current-goroutines}, /pprof/{cpu,memory,goroutines,blockers}",
|
||||
)
|
||||
}
|
||||
|
||||
func metricActiveGoroutinesHandler(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Fprintf(w, "{\"goroutines\": %d}", runtime.NumGoroutine())
|
||||
}
|
||||
|
||||
func metricMemoryStatsHandler(w http.ResponseWriter, r *http.Request) {
|
||||
type OutData struct {
|
||||
CollectedAt time.Time `json:"collected_at"`
|
||||
HeapUsed uint64 `json:"heap_used"`
|
||||
HeapIdle uint64 `json:"heap_idle"`
|
||||
StackUsed uint64 `json:"stack_used"`
|
||||
GCLastFired time.Time `json:"gc_last_fired"`
|
||||
GCNextTargetHeapSize uint64 `json:"gc_next_target_heap_size"`
|
||||
}
|
||||
stats := runtime.MemStats{}
|
||||
gcStats := debug.GCStats{}
|
||||
runtime.ReadMemStats(&stats)
|
||||
debug.ReadGCStats(&gcStats)
|
||||
outData := OutData{
|
||||
CollectedAt: time.Now(),
|
||||
HeapUsed: stats.HeapInuse,
|
||||
HeapIdle: stats.HeapIdle,
|
||||
StackUsed: stats.StackInuse,
|
||||
GCLastFired: gcStats.LastGC,
|
||||
GCNextTargetHeapSize: stats.NextGC,
|
||||
}
|
||||
|
||||
jsonData, err := json.Marshal(&outData)
|
||||
if err != nil {
|
||||
httputil.HttpErr(
|
||||
w,
|
||||
HttpErrIdJsonMarshalFail,
|
||||
"Failed to encode return data",
|
||||
http.StatusInternalServerError,
|
||||
)
|
||||
return
|
||||
}
|
||||
fmt.Fprint(w, string(jsonData))
|
||||
}
|
|
@ -1,239 +0,0 @@
|
|||
package server
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
httputil "git.mstar.dev/mstar/goutils/http"
|
||||
"github.com/rs/zerolog/hlog"
|
||||
|
||||
"git.mstar.dev/mstar/linstrom/storage"
|
||||
)
|
||||
|
||||
func forceCorrectPasskeyAuthFlowMiddleware(
|
||||
handler http.Handler,
|
||||
) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
log := hlog.FromRequest(r)
|
||||
// Don't fuck with the request if not intended for starting to register or login
|
||||
if strings.HasSuffix(r.URL.Path, "loginFinish") {
|
||||
log.Debug().Msg("Request to finish login method, doing nothing")
|
||||
handler.ServeHTTP(w, r)
|
||||
return
|
||||
} else if strings.HasSuffix(r.URL.Path, "registerFinish") {
|
||||
handler.ServeHTTP(w, r)
|
||||
// Force unset session cookie here
|
||||
w.Header().Del("Set-Cookie")
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: "sid",
|
||||
Value: "",
|
||||
Path: "",
|
||||
MaxAge: 0,
|
||||
Expires: time.UnixMilli(0),
|
||||
})
|
||||
return
|
||||
} else if strings.HasSuffix(r.URL.Path, "loginBegin") {
|
||||
fuckWithLoginRequest(w, r, handler)
|
||||
} else if strings.HasSuffix(r.URL.Path, "registerBegin") {
|
||||
fuckWithRegisterRequest(w, r, handler)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func fuckWithRegisterRequest(
|
||||
w http.ResponseWriter,
|
||||
r *http.Request,
|
||||
nextHandler http.Handler,
|
||||
) {
|
||||
log := hlog.FromRequest(r)
|
||||
log.Debug().Msg("Messing with register start request")
|
||||
store := StorageFromRequest(r)
|
||||
if store == nil {
|
||||
return
|
||||
}
|
||||
cookie, cookieErr := r.Cookie("sid")
|
||||
var username struct {
|
||||
Username string `json:"username"`
|
||||
}
|
||||
body, _ := io.ReadAll(r.Body)
|
||||
log.Debug().Bytes("body", body).Msg("Body of auth begin request")
|
||||
err := json.Unmarshal(body, &username)
|
||||
if err != nil {
|
||||
httputil.HttpErr(
|
||||
w,
|
||||
HttpErrIdBadRequest,
|
||||
"Not a username json object",
|
||||
http.StatusBadRequest,
|
||||
)
|
||||
return
|
||||
}
|
||||
if cookieErr == nil {
|
||||
// Already authenticated, overwrite username to logged in account's name
|
||||
// Get session from cookie
|
||||
log.Debug().Msg("Session token exists, force overwriting username of register request")
|
||||
session, ok := store.GetSession(cookie.Value)
|
||||
if !ok {
|
||||
log.Error().Str("session-id", cookie.Value).Msg("Passkey session missing")
|
||||
httputil.HttpErr(
|
||||
w,
|
||||
HttpErrIdDbFailure,
|
||||
"Passkey session missing",
|
||||
http.StatusInternalServerError,
|
||||
)
|
||||
return
|
||||
}
|
||||
acc, err := store.FindAccountByPasskeyId(session.UserID)
|
||||
// Assume account must exist if a session for it exists
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to get account from passkey id from session")
|
||||
httputil.HttpErr(
|
||||
w,
|
||||
HttpErrIdDbFailure,
|
||||
"Failed to get authenticated account",
|
||||
http.StatusInternalServerError,
|
||||
)
|
||||
return
|
||||
}
|
||||
// Replace whatever username may be given with username of logged in account
|
||||
newBody := strings.ReplaceAll(string(body), username.Username, acc.Username)
|
||||
// Assign to request
|
||||
r.Body = io.NopCloser(strings.NewReader(newBody))
|
||||
r.ContentLength = int64(len(newBody))
|
||||
// And pass on
|
||||
nextHandler.ServeHTTP(w, r)
|
||||
} else {
|
||||
// Not authenticated, ensure that no existing name is registered with
|
||||
_, err = store.FindLocalAccountByUsername(username.Username)
|
||||
log.Debug().Bool("err-equals-not_found", err == storage.ErrEntryNotFound).Send()
|
||||
switch err {
|
||||
case nil:
|
||||
// No error while getting account means account exists, refuse access
|
||||
log.Info().
|
||||
Str("username", username.Username).
|
||||
Msg("Account with same name already exists, preventing login")
|
||||
httputil.HttpErr(
|
||||
w,
|
||||
HttpErrIdAlreadyExists,
|
||||
"Account with that name already exists",
|
||||
http.StatusBadRequest,
|
||||
)
|
||||
case storage.ErrEntryNotFound:
|
||||
// Didn't find account with that name, give access
|
||||
log.Debug().
|
||||
Str("username", username.Username).
|
||||
Msg("No account with this username exists yet, passing through")
|
||||
// Copy original body since previous reader hit EOF
|
||||
r.Body = io.NopCloser(strings.NewReader(string(body)))
|
||||
r.ContentLength = int64(len(body))
|
||||
nextHandler.ServeHTTP(w, r)
|
||||
default:
|
||||
// Some other error, log it and return appropriate message
|
||||
log.Error().
|
||||
Err(err).
|
||||
Str("username", username.Username).
|
||||
Msg("Failed to check if account with username already exists")
|
||||
httputil.HttpErr(
|
||||
w,
|
||||
HttpErrIdDbFailure,
|
||||
"Failed to check if account with that name already exists",
|
||||
http.StatusInternalServerError,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func fuckWithLoginRequest(
|
||||
w http.ResponseWriter,
|
||||
r *http.Request,
|
||||
nextHandler http.Handler,
|
||||
) {
|
||||
log := hlog.FromRequest(r)
|
||||
log.Debug().Msg("Messing with login start request")
|
||||
store := StorageFromRequest(r)
|
||||
if store == nil {
|
||||
return
|
||||
}
|
||||
cookie, cookieErr := r.Cookie("sid")
|
||||
var username struct {
|
||||
Username string `json:"username"`
|
||||
}
|
||||
// Force ignore cookie for now
|
||||
_ = cookieErr
|
||||
var err error = errors.New("placeholder")
|
||||
if err == nil {
|
||||
// Someone is logged in, overwrite username with logged in account's one
|
||||
body, _ := io.ReadAll(r.Body)
|
||||
log.Debug().Bytes("body", body).Msg("Body of auth begin request")
|
||||
err := json.Unmarshal(body, &username)
|
||||
if err != nil {
|
||||
httputil.HttpErr(
|
||||
w,
|
||||
HttpErrIdBadRequest,
|
||||
"Not a username json object",
|
||||
http.StatusBadRequest,
|
||||
)
|
||||
return
|
||||
}
|
||||
session, ok := store.GetSession(cookie.Value)
|
||||
if !ok {
|
||||
log.Error().Str("session-id", cookie.Value).Msg("Passkey session missing")
|
||||
httputil.HttpErr(
|
||||
w,
|
||||
HttpErrIdDbFailure,
|
||||
"Passkey session missing",
|
||||
http.StatusInternalServerError,
|
||||
)
|
||||
return
|
||||
}
|
||||
acc, err := store.FindAccountByPasskeyId(session.UserID)
|
||||
// Assume account must exist if a session for it exists
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to get account from passkey id from session")
|
||||
httputil.HttpErr(
|
||||
w,
|
||||
HttpErrIdDbFailure,
|
||||
"Failed to get authenticated account",
|
||||
http.StatusInternalServerError,
|
||||
)
|
||||
return
|
||||
}
|
||||
// Replace whatever username may be given with username of logged in account
|
||||
newBody := strings.ReplaceAll(string(body), username.Username, acc.Username)
|
||||
// Assign to request
|
||||
r.Body = io.NopCloser(strings.NewReader(newBody))
|
||||
r.ContentLength = int64(len(newBody))
|
||||
// And pass on
|
||||
nextHandler.ServeHTTP(w, r)
|
||||
} else {
|
||||
// No one logged in, check if user exists to prevent creating a bugged account
|
||||
body, _ := io.ReadAll(r.Body)
|
||||
log.Debug().Bytes("body", body).Msg("Body of auth begin request")
|
||||
err := json.Unmarshal(body, &username)
|
||||
if err != nil {
|
||||
httputil.HttpErr(w, HttpErrIdBadRequest, "Not a username json object", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
_, err = store.FindLocalAccountByUsername(username.Username)
|
||||
switch err {
|
||||
case nil:
|
||||
// All good, account exists, keep going
|
||||
// Do nothing in this branch
|
||||
case storage.ErrEntryNotFound:
|
||||
// Account doesn't exist, catch it
|
||||
httputil.HttpErr(w, HttpErrIdNotFound, "Username not found", http.StatusNotFound)
|
||||
return
|
||||
default:
|
||||
// catch db failures
|
||||
log.Error().Err(err).Str("username", username.Username).Msg("Db failure while getting account")
|
||||
httputil.HttpErr(w, HttpErrIdDbFailure, "Failed to check for account in db", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
// Restore body as new reader of the same content
|
||||
r.Body = io.NopCloser(strings.NewReader(string(body)))
|
||||
nextHandler.ServeHTTP(w, r)
|
||||
}
|
||||
}
|
|
@ -1,237 +0,0 @@
|
|||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
httputil "git.mstar.dev/mstar/goutils/http"
|
||||
"github.com/rs/zerolog/hlog"
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"git.mstar.dev/mstar/linstrom/config"
|
||||
"git.mstar.dev/mstar/linstrom/storage"
|
||||
)
|
||||
|
||||
type HandlerBuilder func(http.Handler) http.Handler
|
||||
|
||||
func ChainMiddlewares(base http.Handler, links ...HandlerBuilder) http.Handler {
|
||||
slices.Reverse(links)
|
||||
for _, f := range links {
|
||||
base = f(base)
|
||||
}
|
||||
return base
|
||||
}
|
||||
|
||||
func ContextValsMiddleware(pairs map[any]any) HandlerBuilder {
|
||||
return func(h http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
for key, val := range pairs {
|
||||
ctx = context.WithValue(ctx, key, val)
|
||||
}
|
||||
newRequest := r.WithContext(ctx)
|
||||
h.ServeHTTP(w, newRequest)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func LoggingMiddleware(handler http.Handler) http.Handler {
|
||||
return ChainMiddlewares(handler,
|
||||
hlog.NewHandler(log.Logger),
|
||||
hlog.AccessHandler(func(r *http.Request, status, size int, duration time.Duration) {
|
||||
if strings.HasPrefix(r.URL.Path, "/assets") {
|
||||
return
|
||||
}
|
||||
hlog.FromRequest(r).Info().
|
||||
Str("method", r.Method).
|
||||
Stringer("url", r.URL).
|
||||
Int("status", status).
|
||||
Int("size", size).
|
||||
Dur("duration", duration).
|
||||
Send()
|
||||
}),
|
||||
hlog.RemoteAddrHandler("ip"),
|
||||
hlog.UserAgentHandler("user_agent"),
|
||||
hlog.RefererHandler("referer"),
|
||||
hlog.RequestIDHandler("req_id", "Request-Id"),
|
||||
)
|
||||
}
|
||||
|
||||
func passkeyIdToAccountIdTransformerMiddleware(handler http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
s := StorageFromRequest(r)
|
||||
if s == nil {
|
||||
return
|
||||
}
|
||||
log := hlog.FromRequest(r)
|
||||
passkeyId, ok := r.Context().Value(ContextKeyPasskeyUsername).(string)
|
||||
if !ok {
|
||||
httputil.HttpErr(
|
||||
w,
|
||||
HttpErrIdMissingContextValue,
|
||||
"Actor name missing",
|
||||
http.StatusInternalServerError,
|
||||
)
|
||||
return
|
||||
}
|
||||
log.Debug().Bytes("passkey-bytes", []byte(passkeyId)).Msg("Id from passkey auth")
|
||||
acc, err := s.FindAccountByPasskeyId([]byte(passkeyId))
|
||||
if err != nil {
|
||||
httputil.HttpErr(
|
||||
w,
|
||||
HttpErrIdDbFailure,
|
||||
"Failed to get account from storage",
|
||||
http.StatusInternalServerError,
|
||||
)
|
||||
return
|
||||
}
|
||||
r = r.WithContext(context.WithValue(r.Context(), ContextKeyActorId, acc.ID))
|
||||
handler.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
func profilingAuthenticationMiddleware(handler http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.FormValue("password") != config.GlobalConfig.Admin.ProfilingPassword {
|
||||
httputil.HttpErr(w, HttpErrIdNotAuthenticated, "Bad password", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
handler.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
// Middleware for inserting a logged in account's id into the request context if a session exists
|
||||
// Does not cancel requests ever. If an error occurs, it's treated as if no session is set
|
||||
func checkSessionMiddleware(handler http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
cookie, err := r.Cookie("sid")
|
||||
log := hlog.FromRequest(r)
|
||||
if err != nil {
|
||||
// No cookie is ok, this function is only for inserting account id into the context
|
||||
// if one exists, not for checking permissions
|
||||
log.Debug().Msg("No session cookie, passing along")
|
||||
handler.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
store := StorageFromRequest(r)
|
||||
session, ok := store.GetSession(cookie.Value)
|
||||
if !ok {
|
||||
// Failed to get session from cookie id. Log, then move on as if no session is set
|
||||
log.Warn().
|
||||
Str("session-id", cookie.Value).
|
||||
Msg("Cookie with session id found, but session doesn't exist")
|
||||
handler.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
if session.Expires.Before(time.Now()) {
|
||||
// Session expired. Move on as if no session was set
|
||||
store.DeleteSession(cookie.Value)
|
||||
handler.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
acc, err := store.FindAccountByPasskeyId(session.UserID)
|
||||
switch err {
|
||||
case storage.ErrEntryNotFound:
|
||||
log.Info().Msg("No account for the given passkey id found. It probably got deleted")
|
||||
handler.ServeHTTP(w, r)
|
||||
return
|
||||
case nil:
|
||||
default:
|
||||
// Failed to get account for passkey id. Log, then move on as if no session is set
|
||||
log.Error().
|
||||
Err(err).
|
||||
Bytes("passkey-id", session.UserID).
|
||||
Msg("Failed to get account with passkey id while checking session. Ignoring session")
|
||||
handler.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
handler.ServeHTTP(
|
||||
w,
|
||||
r.WithContext(
|
||||
context.WithValue(
|
||||
r.Context(),
|
||||
ContextKeyActorId,
|
||||
acc.ID,
|
||||
),
|
||||
),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
func requireValidSessionMiddleware(
|
||||
h func(http.ResponseWriter, *http.Request),
|
||||
) func(http.ResponseWriter, *http.Request) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
_, ok := r.Context().Value(ContextKeyActorId).(string)
|
||||
if !ok {
|
||||
httputil.HttpErr(
|
||||
w,
|
||||
HttpErrIdNotAuthenticated,
|
||||
"Not authenticated",
|
||||
http.StatusUnauthorized,
|
||||
)
|
||||
return
|
||||
}
|
||||
h(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
func buildRequirePermissionsMiddleware(permissionRole *storage.Role) HandlerBuilder {
|
||||
return func(h http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
accId, ok := r.Context().Value(ContextKeyActorId).(string)
|
||||
if !ok {
|
||||
httputil.HttpErr(
|
||||
w,
|
||||
HttpErrIdNotAuthenticated,
|
||||
"Not authenticated",
|
||||
http.StatusUnauthorized,
|
||||
)
|
||||
return
|
||||
}
|
||||
store := StorageFromRequest(r)
|
||||
log := hlog.FromRequest(r)
|
||||
acc, err := store.FindAccountById(accId)
|
||||
// Assumption: If this handler is hit, the middleware for checking if a session exists at all has already passed
|
||||
// and thus a valid account id must exist in the context
|
||||
if err != nil {
|
||||
log.Error().
|
||||
Err(err).
|
||||
Str("account-id", accId).
|
||||
Msg("Error while getting account from session")
|
||||
httputil.HttpErr(
|
||||
w,
|
||||
HttpErrIdDbFailure,
|
||||
"Error while getting account from session",
|
||||
http.StatusInternalServerError,
|
||||
)
|
||||
return
|
||||
}
|
||||
roles, err := store.FindRolesByNames(acc.Roles)
|
||||
// Assumption: There will always be at least two roles per user, default user and user-specific one
|
||||
if err != nil {
|
||||
httputil.HttpErr(
|
||||
w,
|
||||
HttpErrIdDbFailure,
|
||||
"Failed to get roles for account",
|
||||
http.StatusInternalServerError,
|
||||
)
|
||||
return
|
||||
}
|
||||
collapsedRole := storage.CollapseRolesIntoOne(roles...)
|
||||
if !storage.CompareRoles(&collapsedRole, permissionRole) {
|
||||
httputil.HttpErr(
|
||||
w,
|
||||
HttpErrIdNotAuthenticated,
|
||||
"Insufficient permisions",
|
||||
http.StatusForbidden,
|
||||
)
|
||||
return
|
||||
}
|
||||
h.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
}
|
|
@ -1,13 +0,0 @@
|
|||
package remotestorage
|
||||
|
||||
import "git.mstar.dev/mstar/linstrom/storage"
|
||||
|
||||
// Wrapper around db storage
|
||||
// storage.Storage is for the db and cache access only,
|
||||
// while this one wraps storage.Storage to also provide remote fetching of missing resources.
|
||||
// So if an account doesn't exist in db or cache, this wrapper will attempt to fetch it
|
||||
type RemoteStorage struct {
|
||||
store *storage.Storage
|
||||
}
|
||||
|
||||
// TODO: Implement just about everything storage has, but with remote fetching if storage fails
|
|
@ -1 +0,0 @@
|
|||
package server
|
|
@ -1,98 +0,0 @@
|
|||
package server
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
|
||||
httputil "git.mstar.dev/mstar/goutils/http"
|
||||
"github.com/mstarongithub/passkey"
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"git.mstar.dev/mstar/linstrom/storage"
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
store *storage.Storage
|
||||
router http.Handler
|
||||
server *http.Server
|
||||
}
|
||||
|
||||
func NewServer(
|
||||
store *storage.Storage,
|
||||
pkey *passkey.Passkey,
|
||||
reactiveFS, staticFS fs.FS,
|
||||
placeholderFile *string,
|
||||
) *Server {
|
||||
handler := buildRootHandler(pkey, reactiveFS, staticFS, placeholderFile)
|
||||
handler = ChainMiddlewares(handler, LoggingMiddleware, ContextValsMiddleware(map[any]any{
|
||||
ContextKeyStorage: store,
|
||||
}))
|
||||
|
||||
server := http.Server{
|
||||
Handler: handler,
|
||||
}
|
||||
return &Server{
|
||||
store: store,
|
||||
router: handler,
|
||||
server: &server,
|
||||
}
|
||||
}
|
||||
|
||||
func buildRootHandler(
|
||||
pkey *passkey.Passkey,
|
||||
reactiveFS, staticFS fs.FS,
|
||||
placeholderFile *string,
|
||||
) http.Handler {
|
||||
mux := http.NewServeMux()
|
||||
mux.Handle(
|
||||
"/webauthn/",
|
||||
http.StripPrefix(
|
||||
"/webauthn",
|
||||
forceCorrectPasskeyAuthFlowMiddleware(buildPasskeyAuthRouter(pkey)),
|
||||
),
|
||||
)
|
||||
mux.Handle("/", setupFrontendRouter(reactiveFS, staticFS))
|
||||
mux.Handle("/pk/", http.StripPrefix("/pk", http.FileServer(http.Dir("pk-auth"))))
|
||||
mux.HandleFunc("/alive", isAliveHandler)
|
||||
mux.Handle("/api/", http.StripPrefix("/api", checkSessionMiddleware(setupApiRouter())))
|
||||
|
||||
mux.Handle(
|
||||
"/profiling/",
|
||||
http.StripPrefix("/profiling", profilingAuthenticationMiddleware(setupProfilingHandler())),
|
||||
)
|
||||
// temporary until proper route structure exists
|
||||
mux.Handle(
|
||||
"/authonly/",
|
||||
pkey.Auth(
|
||||
ContextKeyPasskeyUsername,
|
||||
nil,
|
||||
func(w http.ResponseWriter, r *http.Request) {
|
||||
httputil.HttpErr(
|
||||
w,
|
||||
HttpErrIdNotAuthenticated,
|
||||
"Not authenticated",
|
||||
http.StatusUnauthorized,
|
||||
)
|
||||
},
|
||||
)(ChainMiddlewares(setupTestEndpoints(), passkeyIdToAccountIdTransformerMiddleware)),
|
||||
)
|
||||
|
||||
mux.HandleFunc("/placeholder-file", func(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Fprint(w, *placeholderFile)
|
||||
})
|
||||
|
||||
return mux
|
||||
}
|
||||
|
||||
func buildPasskeyAuthRouter(pkey *passkey.Passkey) http.Handler {
|
||||
router := http.NewServeMux()
|
||||
pkey.MountRoutes(router, "/")
|
||||
return router
|
||||
}
|
||||
|
||||
func (s *Server) Start(addr string) error {
|
||||
log.Info().Str("addr", addr).Msg("Starting server")
|
||||
s.server.Addr = addr
|
||||
return s.server.ListenAndServe()
|
||||
}
|
|
@ -1,16 +0,0 @@
|
|||
package server
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func setupTestEndpoints() http.Handler {
|
||||
router := http.NewServeMux()
|
||||
router.HandleFunc(
|
||||
"/",
|
||||
func(w http.ResponseWriter, r *http.Request) { fmt.Fprint(w, "test root") },
|
||||
)
|
||||
|
||||
return router
|
||||
}
|
|
@ -1,55 +0,0 @@
|
|||
package server
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
httputil "git.mstar.dev/mstar/goutils/http"
|
||||
"github.com/rs/zerolog/hlog"
|
||||
|
||||
"git.mstar.dev/mstar/linstrom/storage"
|
||||
)
|
||||
|
||||
func placeholderEndpoint(w http.ResponseWriter, r *http.Request) {
|
||||
hlog.FromRequest(r).Error().Stringer("url", r.URL).Msg("Placeholder endpoint accessed")
|
||||
httputil.HttpErr(
|
||||
w,
|
||||
HttpErrIdPlaceholder,
|
||||
"Endpoint not implemented yet, this is a placeholder",
|
||||
http.StatusInternalServerError,
|
||||
)
|
||||
}
|
||||
|
||||
func StorageFromRequest(r *http.Request) *storage.Storage {
|
||||
store, ok := r.Context().Value(ContextKeyStorage).(*storage.Storage)
|
||||
if !ok {
|
||||
hlog.FromRequest(r).Fatal().Msg("Failed to get storage reference from context")
|
||||
return nil
|
||||
}
|
||||
return store
|
||||
}
|
||||
|
||||
func ActorIdFromRequest(r *http.Request) (string, bool) {
|
||||
id, ok := r.Context().Value(ContextKeyActorId).(string)
|
||||
return id, ok
|
||||
}
|
||||
|
||||
func NoteIdFromRequest(r *http.Request) string {
|
||||
return r.PathValue("noteId")
|
||||
}
|
||||
|
||||
func AccountIdFromRequest(r *http.Request) string {
|
||||
return r.PathValue("accountId")
|
||||
}
|
||||
|
||||
func CheckIfAccountIdHasPermissions(accId string, perms storage.Role, store *storage.Storage) bool {
|
||||
acc, err := store.FindAccountById(accId)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
roles, err := store.FindRolesByNames(acc.Roles)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
collapsed := storage.CollapseRolesIntoOne(roles...)
|
||||
return storage.CompareRoles(&collapsed, &perms)
|
||||
}
|
|
@ -8,6 +8,9 @@ const (
|
|||
// where multiple releases in a day are required
|
||||
Version = "0.0.1 pre-alpha"
|
||||
// Username for the server actor
|
||||
ServerActorName = "server.actor"
|
||||
ServerActorName = "server.actor"
|
||||
// Username for the placeholder actor where the actual actor is unknown
|
||||
// Examples include likes, boosts and followers / following
|
||||
UnknownActorName = "unknown.actor"
|
||||
FeedUsernameSuffix = "-feed"
|
||||
)
|
||||
|
|
|
@ -19,7 +19,6 @@ 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")
|
||||
FlagStartDebugServer *bool = flag.Bool(
|
||||
"debugserver",
|
||||
false,
|
||||
|
|
74
storage-new/unknown.go
Normal file
74
storage-new/unknown.go
Normal file
|
@ -0,0 +1,74 @@
|
|||
package storage
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"database/sql"
|
||||
|
||||
"git.mstar.dev/mstar/goutils/other"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"git.mstar.dev/mstar/linstrom/shared"
|
||||
"git.mstar.dev/mstar/linstrom/storage-new/dbgen"
|
||||
"git.mstar.dev/mstar/linstrom/storage-new/models"
|
||||
)
|
||||
|
||||
var UnknownActorId string
|
||||
|
||||
func InsertUnknownActorPlaceholder() error {
|
||||
dbUser, err := dbgen.User.GetByUsername(shared.UnknownActorName)
|
||||
if err == nil {
|
||||
UnknownActorId = dbUser.ID
|
||||
return nil
|
||||
}
|
||||
if err != gorm.ErrRecordNotFound {
|
||||
return err
|
||||
}
|
||||
server, err := dbgen.RemoteServer.Where(dbgen.RemoteServer.IsSelf.Is(true)).First()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
publicEdKeyBytes, privateEdKeyBytes, err := shared.GenerateKeypair(true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
publicRsaKeyBytes, privateRsaKeyBytes, err := shared.GenerateKeypair(false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
pkeyId := make([]byte, 64)
|
||||
_, err = rand.Read(pkeyId)
|
||||
if err != nil {
|
||||
return other.Error(
|
||||
"storage",
|
||||
"failed to generate passkey ID for linstrom account",
|
||||
err,
|
||||
)
|
||||
}
|
||||
user := models.User{
|
||||
ID: shared.NewId(),
|
||||
Username: shared.UnknownActorName,
|
||||
Server: *server,
|
||||
ServerId: server.ID,
|
||||
DisplayName: "Placeholder user",
|
||||
Description: "Placeholder user for when the actual users are unknown, like with Mastodon likes and boosts",
|
||||
IsBot: true,
|
||||
IconId: sql.NullString{Valid: false},
|
||||
Background: nil,
|
||||
BackgroundId: sql.NullString{Valid: false},
|
||||
Banner: nil,
|
||||
BannerId: sql.NullString{Valid: false},
|
||||
Indexable: false,
|
||||
PublicKeyEd: publicEdKeyBytes,
|
||||
PrivateKeyEd: privateEdKeyBytes,
|
||||
PublicKeyRsa: publicRsaKeyBytes,
|
||||
PrivateKeyRsa: privateRsaKeyBytes,
|
||||
Verified: true,
|
||||
FinishedRegistration: true,
|
||||
PasskeyId: pkeyId,
|
||||
}
|
||||
err = dbgen.User.Save(&user)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
|
@ -1,44 +0,0 @@
|
|||
package storage
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type AccessToken struct {
|
||||
gorm.Model
|
||||
BelongsToUserId string
|
||||
Name string
|
||||
Token string
|
||||
ExpiresAt time.Time
|
||||
}
|
||||
|
||||
func (s *Storage) GetTokensForAccId(accId uint) ([]AccessToken, error) {
|
||||
// TODO: Implement me
|
||||
panic("Not implemented")
|
||||
}
|
||||
|
||||
func (s *Storage) NewAccessToken(
|
||||
forAccId uint,
|
||||
name string,
|
||||
expiresAt time.Time,
|
||||
) (*AccessToken, error) {
|
||||
// TODO: Implement me
|
||||
panic("Not implemented")
|
||||
}
|
||||
|
||||
func (s *Storage) ExtendToken(accId uint, newExpiry time.Time) error {
|
||||
// TODO: Implement me
|
||||
panic("Not implemented")
|
||||
}
|
||||
|
||||
func (s *Storage) RenameToken(accId, oldName string, newName string) error {
|
||||
// TODO: Implement me
|
||||
panic("Not implemented")
|
||||
}
|
||||
|
||||
func (s *Storage) DiscardToken(accId uint, name string) error {
|
||||
// TODO: Implement me
|
||||
panic("Not implemented")
|
||||
}
|
|
@ -1,25 +0,0 @@
|
|||
package storage
|
||||
|
||||
import (
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type AccountRelation struct {
|
||||
gorm.Model
|
||||
FromId string
|
||||
ToId string
|
||||
Accepted bool
|
||||
}
|
||||
|
||||
func (s *Storage) GetRelationBetween(fromId, toId string) (*AccountRelation, error) {
|
||||
rel := AccountRelation{}
|
||||
err := s.db.Where(AccountRelation{FromId: fromId, ToId: toId}).First(&rel).Error
|
||||
switch err {
|
||||
case gorm.ErrRecordNotFound:
|
||||
return nil, ErrEntryNotFound
|
||||
case nil:
|
||||
return &rel, nil
|
||||
default:
|
||||
return nil, err
|
||||
}
|
||||
}
|
121
storage/cache.go
121
storage/cache.go
|
@ -1,121 +0,0 @@
|
|||
package storage
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"github.com/redis/go-redis/v9"
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"git.mstar.dev/mstar/linstrom/shared"
|
||||
)
|
||||
|
||||
// various prefixes for accessing items in the cache (since it's a simple key-value store)
|
||||
const (
|
||||
cacheUserHandleToIdPrefix = "acc-name-to-id:"
|
||||
cacheLocalUsernameToIdPrefix = "acc-local-name-to-id:"
|
||||
cachePasskeyIdToAccIdPrefix = "acc-pkey-id-to-id:"
|
||||
cacheUserIdToAccPrefix = "acc-id-to-data:"
|
||||
cacheNoteIdToNotePrefix = "note-id-to-data:"
|
||||
)
|
||||
|
||||
// An error describing the case where some value was just not found in the cache
|
||||
var errCacheNotFound = errors.New("not found in cache")
|
||||
|
||||
// Find an account id in cache using a given user handle ("@bob@example.com" or "bob@example.com")
|
||||
// accId contains the Id of the account if found
|
||||
// err contains an error describing why an account's id couldn't be found
|
||||
// The most common one should be errCacheNotFound
|
||||
func (s *Storage) cacheHandleToAccUid(handle string) (accId *string, err error) {
|
||||
defer shared.Untrace(shared.Trace(&log.Logger))
|
||||
// Where to put the data (in case it's found)
|
||||
var target string
|
||||
found, err := s.cache.Get(cacheUserHandleToIdPrefix+strings.TrimLeft(handle, "@"), &target)
|
||||
// If nothing was found, check error
|
||||
if !found {
|
||||
// Case error is set and NOT redis' error for nothing found: Return that error
|
||||
if err != nil && !errors.Is(err, redis.Nil) {
|
||||
return nil, err
|
||||
} else {
|
||||
// Else return errCacheNotFound
|
||||
return nil, errCacheNotFound
|
||||
}
|
||||
}
|
||||
return &target, nil
|
||||
}
|
||||
|
||||
// Find a local account's id in cache using a given username ("bob")
|
||||
// accId containst the Id of the account if found
|
||||
// err contains an error describing why an account's id couldn't be found
|
||||
// The most common one should be errCacheNotFound
|
||||
func (s *Storage) cacheLocalUsernameToAccUid(username string) (accId *string, err error) {
|
||||
defer shared.Untrace(shared.Trace(&log.Logger))
|
||||
// Where to put the data (in case it's found)
|
||||
var target string
|
||||
found, err := s.cache.Get(cacheLocalUsernameToIdPrefix+username, &target)
|
||||
// If nothing was found, check error
|
||||
if !found {
|
||||
// Case error is set and NOT redis' error for nothing found: Return that error
|
||||
if err != nil && !errors.Is(err, redis.Nil) {
|
||||
return nil, err
|
||||
} else {
|
||||
// Else return errCacheNotFound
|
||||
return nil, errCacheNotFound
|
||||
}
|
||||
}
|
||||
return &target, nil
|
||||
}
|
||||
|
||||
func (s *Storage) cachePkeyIdToAccId(pkeyId []byte) (accId *string, err error) {
|
||||
defer shared.Untrace(shared.Trace(&log.Logger))
|
||||
// Where to put the data (in case it's found)
|
||||
var target string
|
||||
found, err := s.cache.Get(cachePasskeyIdToAccIdPrefix+string(pkeyId), &target)
|
||||
// If nothing was found, check error
|
||||
if !found {
|
||||
// Case error is set and NOT redis' error for nothing found: Return that error
|
||||
if err != nil && !errors.Is(err, redis.Nil) {
|
||||
return nil, err
|
||||
} else {
|
||||
// Else return errCacheNotFound
|
||||
return nil, errCacheNotFound
|
||||
}
|
||||
}
|
||||
return &target, nil
|
||||
}
|
||||
|
||||
// Find an account's data in cache using a given account id
|
||||
// acc contains the full account as stored last time if found
|
||||
// err contains an error describing why an account couldn't be found
|
||||
// The most common one should be errCacheNotFound
|
||||
func (s *Storage) cacheAccIdToData(id string) (acc *Account, err error) {
|
||||
defer shared.Untrace(shared.Trace(&log.Logger))
|
||||
var target Account
|
||||
found, err := s.cache.Get(cacheUserIdToAccPrefix+id, &target)
|
||||
if !found {
|
||||
if err != nil && !errors.Is(err, redis.Nil) {
|
||||
return nil, err
|
||||
} else {
|
||||
return nil, errCacheNotFound
|
||||
}
|
||||
}
|
||||
return &target, nil
|
||||
}
|
||||
|
||||
// Find a cached note given its ID
|
||||
// note contains the full note as stored last time if found
|
||||
// err contains an error describing why a note couldn't be found
|
||||
// The most common one should be errCacheNotFound
|
||||
func (s *Storage) cacheNoteIdToData(id string) (note *Note, err error) {
|
||||
defer shared.Untrace(shared.Trace(&log.Logger))
|
||||
target := Note{}
|
||||
found, err := s.cache.Get(cacheNoteIdToNotePrefix+id, &target)
|
||||
if !found {
|
||||
if err != nil && !errors.Is(err, redis.Nil) {
|
||||
return nil, err
|
||||
} else {
|
||||
return nil, errCacheNotFound
|
||||
}
|
||||
}
|
||||
return &target, nil
|
||||
}
|
105
storage/cache/cache.go
vendored
105
storage/cache/cache.go
vendored
|
@ -1,105 +0,0 @@
|
|||
package cache
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/dgraph-io/ristretto"
|
||||
"github.com/eko/gocache/lib/v4/cache"
|
||||
"github.com/eko/gocache/lib/v4/store"
|
||||
redis_store "github.com/eko/gocache/store/redis/v4"
|
||||
ristretto_store "github.com/eko/gocache/store/ristretto/v4"
|
||||
"github.com/redis/go-redis/v9"
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"git.mstar.dev/mstar/linstrom/config"
|
||||
"git.mstar.dev/mstar/linstrom/shared"
|
||||
)
|
||||
|
||||
type Cache struct {
|
||||
cache *cache.ChainCache[[]byte]
|
||||
decoders *DecoderPool
|
||||
encoders *EncoderPool
|
||||
}
|
||||
|
||||
var ctxBackground = context.Background()
|
||||
|
||||
// TODO: Maybe also include metrics
|
||||
func NewCache(maxSize int64, redisUrl *string) (*Cache, error) {
|
||||
// ristretto is an in-memory cache
|
||||
log.Debug().Int64("max-size", maxSize).Msg("Setting up ristretto")
|
||||
ristrettoCache, err := ristretto.NewCache(&ristretto.Config{
|
||||
// The *10 is a recommendation from ristretto
|
||||
NumCounters: maxSize * 10,
|
||||
MaxCost: maxSize,
|
||||
BufferItems: 64, // Same here
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ristretto cache error: %w", err)
|
||||
}
|
||||
ristrettoStore := ristretto_store.NewRistretto(
|
||||
ristrettoCache,
|
||||
store.WithExpiration(
|
||||
time.Second*time.Duration(config.GlobalConfig.Storage.MaxInMemoryCacheSize),
|
||||
),
|
||||
)
|
||||
|
||||
var cacheManager *cache.ChainCache[[]byte]
|
||||
if redisUrl != nil {
|
||||
opts, err := redis.ParseURL(*redisUrl)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
redisClient := redis.NewClient(opts)
|
||||
redisStore := redis_store.NewRedis(
|
||||
redisClient,
|
||||
store.WithExpiration(
|
||||
time.Second*time.Duration(*config.GlobalConfig.Storage.MaxRedisCacheTTL),
|
||||
),
|
||||
)
|
||||
cacheManager = cache.NewChain(
|
||||
cache.New[[]byte](ristrettoStore),
|
||||
cache.New[[]byte](redisStore),
|
||||
)
|
||||
} else {
|
||||
cacheManager = cache.NewChain(cache.New[[]byte](ristrettoStore))
|
||||
}
|
||||
|
||||
return &Cache{
|
||||
cache: cacheManager,
|
||||
decoders: NewDecoderPool(),
|
||||
encoders: NewEncoderPool(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *Cache) Get(key string, target any) (bool, error) {
|
||||
defer shared.Untrace(shared.Trace(&log.Logger))
|
||||
return false, nil
|
||||
data, err := c.cache.Get(ctxBackground, key)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
err = c.decoders.Decode(data, target)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return true, err
|
||||
}
|
||||
|
||||
func (c *Cache) Set(key string, value any) error {
|
||||
defer shared.Untrace(shared.Trace(&log.Logger))
|
||||
return nil
|
||||
data, err := c.encoders.Encode(value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = c.cache.Set(ctxBackground, key, data, nil)
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *Cache) Delete(key string) {
|
||||
defer shared.Untrace(shared.Trace(&log.Logger))
|
||||
// Error return doesn't matter here. Delete is delete is gone
|
||||
_ = c.cache.Delete(ctxBackground, key)
|
||||
}
|
164
storage/cache/coderPools.go
vendored
164
storage/cache/coderPools.go
vendored
|
@ -1,164 +0,0 @@
|
|||
package cache
|
||||
|
||||
import (
|
||||
"io"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type EncoderPool struct {
|
||||
encoders []*gobEncoder
|
||||
lock sync.RWMutex
|
||||
}
|
||||
|
||||
func NewEncoderPool() *EncoderPool {
|
||||
return &EncoderPool{
|
||||
encoders: []*gobEncoder{},
|
||||
lock: sync.RWMutex{},
|
||||
}
|
||||
}
|
||||
|
||||
// Encode some value with gob
|
||||
func (p *EncoderPool) Encode(raw any) ([]byte, error) {
|
||||
var encoder *gobEncoder
|
||||
// First try to find an available encoder
|
||||
// Read only lock should be fine here since locks are atomic i
|
||||
//and thus no two goroutines should be able to lock the same encoder at the same time
|
||||
// One of those attempts is going to fail and continue looking for another available one
|
||||
p.lock.RLock()
|
||||
for _, v := range p.encoders {
|
||||
// If we can lock one, it's available
|
||||
if v.TryLock() {
|
||||
// Keep the reference, then break
|
||||
encoder = v
|
||||
break
|
||||
}
|
||||
}
|
||||
p.lock.RUnlock()
|
||||
// Didn't find an available encoder, create new one and add to pool
|
||||
if encoder == nil {
|
||||
encoder = p.expand()
|
||||
}
|
||||
// Ensure we free the encoder at the end
|
||||
defer encoder.Unlock()
|
||||
// Clear the buffer to avoid funky output from previous operations
|
||||
encoder.Buffer.Reset()
|
||||
if err := encoder.Encoder.Encode(raw); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
data, err := io.ReadAll(encoder.Buffer)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// Expands the pool of available encoders by one and returns a reference to the new one
|
||||
// The new encoder is already locked and ready for use
|
||||
func (p *EncoderPool) expand() *gobEncoder {
|
||||
enc := newEncoder()
|
||||
// Lock everything. First the pool fully since we need to overwrite the encoders slice
|
||||
p.lock.Lock()
|
||||
// And then the new encoder to make it available for use by the caller
|
||||
// so that they don't have to search for it again
|
||||
enc.Lock()
|
||||
p.encoders = append(p.encoders, &enc)
|
||||
p.lock.Unlock()
|
||||
return &enc
|
||||
}
|
||||
|
||||
// Prune all encoders not currently used from the pool
|
||||
func (p *EncoderPool) Prune() {
|
||||
stillActiveEncoders := []*gobEncoder{}
|
||||
p.lock.Lock()
|
||||
for _, v := range p.encoders {
|
||||
if !v.TryLock() {
|
||||
// Can't lock, encoder in use, keep it
|
||||
stillActiveEncoders = append(stillActiveEncoders, v)
|
||||
continue
|
||||
}
|
||||
// If we reach here, the encoder was available (since not locked), unlock and continue
|
||||
v.Unlock()
|
||||
}
|
||||
// Overwrite list of available encoders to only contain the ones we found to still be active
|
||||
p.encoders = stillActiveEncoders
|
||||
p.lock.Unlock()
|
||||
}
|
||||
|
||||
type DecoderPool struct {
|
||||
encoders []*gobDecoder
|
||||
lock sync.RWMutex
|
||||
}
|
||||
|
||||
func NewDecoderPool() *DecoderPool {
|
||||
return &DecoderPool{
|
||||
encoders: []*gobDecoder{},
|
||||
lock: sync.RWMutex{},
|
||||
}
|
||||
}
|
||||
|
||||
// Decode some value with gob
|
||||
func (p *DecoderPool) Decode(raw []byte, target any) error {
|
||||
var encoder *gobDecoder
|
||||
// First try to find an available encoder
|
||||
// Read only lock should be fine here since locks are atomic i
|
||||
//and thus no two goroutines should be able to lock the same encoder at the same time
|
||||
// One of those attempts is going to fail and continue looking for another available one
|
||||
p.lock.RLock()
|
||||
for _, v := range p.encoders {
|
||||
// If we can lock one, it's available
|
||||
if v.TryLock() {
|
||||
// Keep the reference, then break
|
||||
encoder = v
|
||||
break
|
||||
}
|
||||
}
|
||||
p.lock.RUnlock()
|
||||
// Didn't find an available encoder, create new one and add to pool
|
||||
if encoder == nil {
|
||||
encoder = p.expand()
|
||||
}
|
||||
// Desure we free the encoder at the end
|
||||
defer encoder.Unlock()
|
||||
// Clear the buffer to avoid funky output from previous operations
|
||||
encoder.Buffer.Reset()
|
||||
// Write the raw data to the buffer, then decode it
|
||||
// The write will always succeed (or panic)
|
||||
_, _ = encoder.Buffer.Write(raw)
|
||||
err := encoder.Decoder.Decode(target)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Expands the pool of available encoders by one and returns a reference to the new one
|
||||
// The new encoder is already locked and ready for use
|
||||
func (p *DecoderPool) expand() *gobDecoder {
|
||||
enc := newDecoder()
|
||||
// Lock everything. First the pool fully since we need to overwrite the encoders slice
|
||||
p.lock.Lock()
|
||||
// And then the new encoder to make it available for use by the caller
|
||||
// so that they don't have to search for it again
|
||||
enc.Lock()
|
||||
p.encoders = append(p.encoders, &enc)
|
||||
p.lock.Unlock()
|
||||
return &enc
|
||||
}
|
||||
|
||||
// Prune all encoders not currently used from the pool
|
||||
func (p *DecoderPool) Prune() {
|
||||
stillActiveDecoders := []*gobDecoder{}
|
||||
p.lock.Lock()
|
||||
for _, v := range p.encoders {
|
||||
if !v.TryLock() {
|
||||
// Can't lock, encoder in use, keep it
|
||||
stillActiveDecoders = append(stillActiveDecoders, v)
|
||||
continue
|
||||
}
|
||||
// If we reach here, the encoder was available (since not locked), unlock and continue
|
||||
v.Unlock()
|
||||
}
|
||||
// Overwrite list of available encoders to only contain the ones we found to still be active
|
||||
p.encoders = stillActiveDecoders
|
||||
p.lock.Unlock()
|
||||
}
|
35
storage/cache/lockedCoders.go
vendored
35
storage/cache/lockedCoders.go
vendored
|
@ -1,35 +0,0 @@
|
|||
package cache
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/gob"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type gobEncoder struct {
|
||||
sync.Mutex
|
||||
Encoder *gob.Encoder
|
||||
Buffer *bytes.Buffer
|
||||
}
|
||||
|
||||
func newEncoder() gobEncoder {
|
||||
buf := bytes.Buffer{}
|
||||
return gobEncoder{
|
||||
Encoder: gob.NewEncoder(&buf),
|
||||
Buffer: &buf,
|
||||
}
|
||||
}
|
||||
|
||||
type gobDecoder struct {
|
||||
sync.Mutex
|
||||
Decoder *gob.Decoder
|
||||
Buffer *bytes.Buffer
|
||||
}
|
||||
|
||||
func newDecoder() gobDecoder {
|
||||
buf := bytes.Buffer{}
|
||||
return gobDecoder{
|
||||
Decoder: gob.NewDecoder(&buf),
|
||||
Buffer: &buf,
|
||||
}
|
||||
}
|
|
@ -1,25 +0,0 @@
|
|||
package storage
|
||||
|
||||
import "gorm.io/gorm"
|
||||
|
||||
type Emote struct {
|
||||
gorm.Model
|
||||
// Metadata MediaMetadata // `gorm:"foreignKey:MetadataId"`
|
||||
MetadataId string
|
||||
Name string
|
||||
// Server RemoteServer // `gorm:"foreignKey:ServerId;references:ID"`
|
||||
ServerId uint
|
||||
}
|
||||
|
||||
func (s *Storage) GetEmoteById(id uint) (*Emote, error) {
|
||||
out := Emote{}
|
||||
err := s.db.First(&out, id).Error
|
||||
switch err {
|
||||
case nil:
|
||||
return &out, nil
|
||||
case gorm.ErrRecordNotFound:
|
||||
return nil, ErrEntryNotFound
|
||||
default:
|
||||
return nil, err
|
||||
}
|
||||
}
|
|
@ -1,15 +0,0 @@
|
|||
package storage
|
||||
|
||||
import "errors"
|
||||
|
||||
type ErrNotImplemented struct{}
|
||||
|
||||
func (n ErrNotImplemented) Error() string {
|
||||
return "Not implemented yet"
|
||||
}
|
||||
|
||||
var ErrEntryNotFound = errors.New("entry not found")
|
||||
var ErrEntryAlreadyExists = errors.New("entry already exists")
|
||||
var ErrNothingToChange = errors.New("nothing to change")
|
||||
var ErrInvalidData = errors.New("invalid data")
|
||||
var ErrNotAllowed = errors.New("action not allowed")
|
|
@ -1,64 +0,0 @@
|
|||
package storage
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"gorm.io/gorm/logger"
|
||||
)
|
||||
|
||||
type gormLogger struct {
|
||||
logger zerolog.Logger
|
||||
}
|
||||
|
||||
func newGormLogger(zerologger zerolog.Logger) *gormLogger {
|
||||
return &gormLogger{zerologger}
|
||||
}
|
||||
|
||||
func (g *gormLogger) LogMode(newLevel logger.LogLevel) logger.Interface {
|
||||
switch newLevel {
|
||||
case logger.Error:
|
||||
g.logger = g.logger.Level(zerolog.ErrorLevel)
|
||||
case logger.Warn:
|
||||
g.logger = g.logger.Level(zerolog.WarnLevel)
|
||||
case logger.Info:
|
||||
g.logger = g.logger.Level(zerolog.InfoLevel)
|
||||
case logger.Silent:
|
||||
g.logger = g.logger.Level(zerolog.Disabled)
|
||||
}
|
||||
return g
|
||||
}
|
||||
func (g *gormLogger) Info(ctx context.Context, format string, args ...interface{}) {
|
||||
g.logger.Info().Ctx(ctx).Msgf(format, args...)
|
||||
}
|
||||
func (g *gormLogger) Warn(ctx context.Context, format string, args ...interface{}) {
|
||||
g.logger.Warn().Ctx(ctx).Msgf(format, args...)
|
||||
}
|
||||
func (g *gormLogger) Error(ctx context.Context, format string, args ...interface{}) {
|
||||
g.logger.Error().Ctx(ctx).Msgf(format, args...)
|
||||
}
|
||||
|
||||
func (g *gormLogger) Trace(
|
||||
ctx context.Context,
|
||||
begin time.Time,
|
||||
fc func() (sql string, rowsAffected int64),
|
||||
err error,
|
||||
) {
|
||||
sql, rowsAffected := fc()
|
||||
g.logger.Trace().
|
||||
Ctx(ctx).
|
||||
Time("gorm-begin", begin).
|
||||
Err(err).
|
||||
Str("gorm-query", sql).
|
||||
Int64("gorm-rows-affected", rowsAffected).
|
||||
Send()
|
||||
}
|
||||
|
||||
func (g *gormLogger) OverwriteLoggingLevel(new zerolog.Level) {
|
||||
g.logger = g.logger.Level(new)
|
||||
}
|
||||
|
||||
func (g *gormLogger) OverwriteLogger(new zerolog.Logger) {
|
||||
g.logger = new
|
||||
}
|
|
@ -1,11 +0,0 @@
|
|||
package storage
|
||||
|
||||
// Contains various functions for housekeeping
|
||||
// Things like true deletion of soft deleted data after some time
|
||||
// Or removing inactive access tokens
|
||||
// All of this will be handled by goroutines
|
||||
|
||||
// TODO: Delete everything soft deleted and older than a month
|
||||
// TODO: Delete old tokens not in active use anymore
|
||||
// TODO: Start jobs where the last check-in was more than an hour ago
|
||||
//
|
|
@ -1,68 +0,0 @@
|
|||
package storage
|
||||
|
||||
import (
|
||||
"github.com/rs/zerolog/log"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"git.mstar.dev/mstar/linstrom/shared"
|
||||
)
|
||||
|
||||
// Auto-generate string names for the various constants
|
||||
//go:generate stringer -type InboundJobSource
|
||||
|
||||
type InboundJobSource int
|
||||
|
||||
// TODO: Adjust and expand these constants later, depending on sources
|
||||
const (
|
||||
InJobSourceAccInbox InboundJobSource = iota
|
||||
InJobSourceServerInbox
|
||||
InJobSourceApiMasto
|
||||
InJobSourceApiLinstrom
|
||||
)
|
||||
|
||||
// Store inbound jobs from api and ap in the db until they finished processing
|
||||
// Ensures data consistency in case the server is forced to restart unexpectedly
|
||||
// Inbound jobs must allways be processed in order from oldest to newest to ensure consistency
|
||||
type InboundJob struct {
|
||||
gorm.Model
|
||||
// Raw data, could be json or gob data, check source for how to interpret
|
||||
RawData []byte `gorm:"->;<-create"`
|
||||
// Where this job is coming from. Important for figuring out how to decode the raw data and what to do with it
|
||||
Source InboundJobSource `gorm:"->;<-create"`
|
||||
|
||||
// Section: Various data
|
||||
// TODO: Expand based on needs
|
||||
|
||||
// If from an inbox, include the owner id here
|
||||
InboxOwner *string `gorm:"->;<-create"`
|
||||
}
|
||||
|
||||
func (s *Storage) AddNewInboundJob(data []byte, source InboundJobSource, inboxOwner *string) {
|
||||
defer shared.Untrace(shared.Trace(&log.Logger))
|
||||
newJob := InboundJob{
|
||||
RawData: data,
|
||||
Source: source,
|
||||
InboxOwner: inboxOwner,
|
||||
}
|
||||
s.db.Create(&newJob)
|
||||
}
|
||||
|
||||
// Get the specified amount of jobs, sorted by age (oldest first)
|
||||
func (s *Storage) GetOldestInboundJobs(amount uint) ([]InboundJob, error) {
|
||||
defer shared.Untrace(shared.Trace(&log.Logger))
|
||||
jobs := []InboundJob{}
|
||||
switch err := s.db.Order("id asc, created_at asc").Limit(int(amount)).Find(jobs).Error; err {
|
||||
case gorm.ErrRecordNotFound:
|
||||
return nil, ErrEntryNotFound
|
||||
case nil:
|
||||
return jobs, nil
|
||||
default:
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Storage) CompleteInboundJob(id uint) error {
|
||||
defer shared.Untrace(shared.Trace(&log.Logger))
|
||||
s.db.Delete(InboundJob{Model: gorm.Model{ID: id}})
|
||||
return nil
|
||||
}
|
|
@ -1,26 +0,0 @@
|
|||
// Code generated by "stringer -type InboundJobSource"; DO NOT EDIT.
|
||||
|
||||
package storage
|
||||
|
||||
import "strconv"
|
||||
|
||||
func _() {
|
||||
// An "invalid array index" compiler error signifies that the constant values have changed.
|
||||
// Re-run the stringer command to generate them again.
|
||||
var x [1]struct{}
|
||||
_ = x[InJobSourceAccInbox-0]
|
||||
_ = x[InJobSourceServerInbox-1]
|
||||
_ = x[InJobSourceApiMasto-2]
|
||||
_ = x[InJobSourceApiLinstrom-3]
|
||||
}
|
||||
|
||||
const _InboundJobSource_name = "InJobSourceAccInboxInJobSourceServerInboxInJobSourceApiMastoInJobSourceApiLinstrom"
|
||||
|
||||
var _InboundJobSource_index = [...]uint8{0, 19, 41, 60, 82}
|
||||
|
||||
func (i InboundJobSource) String() string {
|
||||
if i < 0 || i >= InboundJobSource(len(_InboundJobSource_index)-1) {
|
||||
return "InboundJobSource(" + strconv.FormatInt(int64(i), 10) + ")"
|
||||
}
|
||||
return _InboundJobSource_name[_InboundJobSource_index[i]:_InboundJobSource_index[i+1]]
|
||||
}
|
|
@ -1,95 +0,0 @@
|
|||
package storage
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"git.mstar.dev/mstar/linstrom/shared"
|
||||
)
|
||||
|
||||
// MediaMetadata contains metadata about some media
|
||||
// Metadata includes whether it's a remote file or not, what the name is,
|
||||
// the MIME type, and an identifier pointing to its location
|
||||
type MediaMetadata struct {
|
||||
ID string `gorm:"primarykey"` // The unique ID of this media file
|
||||
CreatedAt time.Time // When this entry was created
|
||||
UpdatedAt time.Time // When this entry was last updated
|
||||
// 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"`
|
||||
OwnedBy string // Account id this media belongs to
|
||||
Remote bool // whether the attachment is a remote one
|
||||
// Where the media is stored. Url
|
||||
Location string
|
||||
Type string // What media type this is following mime types, eg image/png
|
||||
// Name of the file
|
||||
// Could be <emote-name>.png, <server-name>.webp for example. Or the name the file was uploaded with
|
||||
Name string
|
||||
// Alternative description of the media file's content
|
||||
AltText string
|
||||
// Whether the media is to be blurred by default
|
||||
Blurred bool
|
||||
}
|
||||
|
||||
func (s *Storage) NewMediaMetadata(
|
||||
ownerId, location, mediaType, name string,
|
||||
) (*MediaMetadata, error) {
|
||||
defer shared.Untrace(shared.Trace(&log.Logger))
|
||||
newMedia := MediaMetadata{
|
||||
OwnedBy: ownerId,
|
||||
Location: location,
|
||||
Name: name,
|
||||
Type: mediaType,
|
||||
}
|
||||
s.db.Create(&newMedia)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (s *Storage) FuzzyFindMediaMetadataByName(name string) ([]MediaMetadata, error) {
|
||||
defer shared.Untrace(shared.Trace(&log.Logger))
|
||||
notes := []MediaMetadata{}
|
||||
err := s.db.Where("name LIKE %?%", name).Find(notes).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return notes, nil
|
||||
}
|
||||
|
||||
func (s *Storage) GetMediaMetadataById(id string) (*MediaMetadata, error) {
|
||||
defer shared.Untrace(shared.Trace(&log.Logger))
|
||||
media := MediaMetadata{ID: id}
|
||||
err := s.db.First(&media).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &media, nil
|
||||
}
|
||||
|
||||
func (s *Storage) FuzzyFindMediaMetadataByLocation(location string) ([]MediaMetadata, error) {
|
||||
defer shared.Untrace(shared.Trace(&log.Logger))
|
||||
data := []MediaMetadata{}
|
||||
if err := s.db.Where("location LIKE %?%", location).Find(data).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func (s *Storage) DeleteMediaMetadataById(id string) error {
|
||||
defer shared.Untrace(shared.Trace(&log.Logger))
|
||||
return s.db.Delete(MediaMetadata{ID: id}).Error
|
||||
}
|
||||
|
||||
func (s *Storage) DeleteMediaMetadataByFuzzyLocation(location string) error {
|
||||
defer shared.Untrace(shared.Trace(&log.Logger))
|
||||
var tmp MediaMetadata
|
||||
return s.db.Where("location LIKE %?%", location).Delete(&tmp).Error
|
||||
}
|
||||
|
||||
func (s *Storage) DeleteMediaMetadataByFuzzyName(name string) error {
|
||||
defer shared.Untrace(shared.Trace(&log.Logger))
|
||||
var tmp MediaMetadata
|
||||
return s.db.Where("name LIKE %?%", name).Delete(&tmp).Error
|
||||
}
|
|
@ -1,91 +0,0 @@
|
|||
package mediaprovider
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"image"
|
||||
"image/jpeg"
|
||||
"image/png"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"github.com/gabriel-vasile/mimetype"
|
||||
"github.com/gen2brain/avif"
|
||||
"golang.org/x/image/draw"
|
||||
"golang.org/x/image/webp"
|
||||
)
|
||||
|
||||
var ErrUnknownImageType = errors.New("unknown image format")
|
||||
|
||||
func Compress(dataReader io.Reader, mimeType *string) (io.Reader, error) {
|
||||
// TODO: Get inspired by GTS and use wasm ffmpeg (https://codeberg.org/gruf/go-ffmpreg) for compression
|
||||
data, err := io.ReadAll(dataReader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if mimeType == nil {
|
||||
tmp := mimetype.Detect(data).String()
|
||||
mimeType = &tmp
|
||||
}
|
||||
uberType, subType, _ := strings.Cut(*mimeType, "/")
|
||||
var dataOut []byte
|
||||
switch uberType {
|
||||
case "text":
|
||||
case "application":
|
||||
case "image":
|
||||
dataOut, err = compressImage(data, subType, 1280, 720)
|
||||
case "video":
|
||||
dataOut, err = compressVideo(data, subType)
|
||||
case "audio":
|
||||
case "font":
|
||||
default:
|
||||
}
|
||||
if err != nil && err != ErrUnknownImageType {
|
||||
return nil, err
|
||||
}
|
||||
dataOut = compressBase64(dataOut)
|
||||
return bytes.NewReader(dataOut), nil
|
||||
}
|
||||
|
||||
func compressVideo(dataIn []byte, subType string) (dataOut []byte, err error) {
|
||||
// TODO: Implement me
|
||||
panic("Implement me")
|
||||
}
|
||||
|
||||
func compressImage(
|
||||
dataIn []byte,
|
||||
subType string,
|
||||
maxSizeX, maxSizeY uint,
|
||||
) (dataOut []byte, err error) {
|
||||
imageSize := image.Rect(0, 0, int(maxSizeX), int(maxSizeY))
|
||||
dst := image.NewRGBA(imageSize)
|
||||
var sourceImage image.Image
|
||||
switch subType {
|
||||
case "png":
|
||||
sourceImage, err = png.Decode(bytes.NewReader(dataIn))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
case "jpg", "jpeg":
|
||||
sourceImage, err = jpeg.Decode(bytes.NewReader(dataIn))
|
||||
case "webp":
|
||||
sourceImage, err = webp.Decode(bytes.NewReader(dataIn))
|
||||
case "avif":
|
||||
sourceImage, err = avif.Decode(bytes.NewReader(dataIn))
|
||||
default:
|
||||
return nil, ErrUnknownImageType
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
draw.CatmullRom.Scale(dst, imageSize, sourceImage, sourceImage.Bounds(), draw.Src, nil)
|
||||
return dst.Pix, nil
|
||||
}
|
||||
|
||||
func compressBase64(dataIn []byte) []byte {
|
||||
result := []byte{}
|
||||
base64.StdEncoding.Encode(result, dataIn)
|
||||
return result
|
||||
}
|
|
@ -1,59 +0,0 @@
|
|||
package mediaprovider
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/minio/minio-go/v7"
|
||||
"github.com/minio/minio-go/v7/pkg/credentials"
|
||||
"git.mstar.dev/mstar/linstrom/config"
|
||||
)
|
||||
|
||||
type Provider struct {
|
||||
client *minio.Client
|
||||
}
|
||||
|
||||
// Create a new storage provider using the values from the global config
|
||||
// It also sets up the bucket in the s3 provider if it doesn't exist yet
|
||||
func NewProviderFromConfig() (*Provider, error) {
|
||||
client, err := minio.New(config.GlobalConfig.S3.Endpoint, &minio.Options{
|
||||
Creds: credentials.NewStaticV4(
|
||||
config.GlobalConfig.S3.KeyId,
|
||||
config.GlobalConfig.S3.Secret,
|
||||
"",
|
||||
),
|
||||
Region: config.GlobalConfig.S3.Region,
|
||||
Secure: config.GlobalConfig.S3.UseSSL,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err = setupBucket(client, config.GlobalConfig.S3.BucketName); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Provider{
|
||||
client: client,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func setupBucket(client *minio.Client, bucketName string) error {
|
||||
ctx := context.Background()
|
||||
// Give it half a minute tops to check if a bucket with the given name already exists
|
||||
existsContext, cancel := context.WithTimeout(ctx, time.Second*30)
|
||||
defer cancel()
|
||||
ok, err := client.BucketExists(existsContext, bucketName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if ok {
|
||||
return nil
|
||||
}
|
||||
// Same timeout for creating the new bucket if it doesn't exist
|
||||
createContext, createCancel := context.WithTimeout(ctx, time.Second*30)
|
||||
defer createCancel()
|
||||
err = client.MakeBucket(createContext, bucketName, minio.MakeBucketOptions{
|
||||
Region: config.GlobalConfig.S3.Region,
|
||||
})
|
||||
return err
|
||||
}
|
|
@ -1,41 +0,0 @@
|
|||
package storage
|
||||
|
||||
import (
|
||||
"database/sql/driver"
|
||||
"errors"
|
||||
)
|
||||
|
||||
// For pretty printing during debug
|
||||
// If `go generate` is run, it'll generate the necessary function and data for pretty printing
|
||||
//go:generate stringer -type NoteAccessLevel
|
||||
|
||||
// What feed a note is targeting (public, home, followers or dm)
|
||||
type NoteAccessLevel uint8
|
||||
|
||||
const (
|
||||
// The note is intended for the public
|
||||
NOTE_TARGET_PUBLIC NoteAccessLevel = 0
|
||||
// The note is intended only for the home screen
|
||||
// not really any idea what the difference is compared to public
|
||||
// Maybe home notes don't show up on the server feed but still for everyone's home feed if it reaches them via follow or boost
|
||||
NOTE_TARGET_HOME NoteAccessLevel = 1 << iota
|
||||
// The note is intended only for followers
|
||||
NOTE_TARGET_FOLLOWERS
|
||||
// The note is intended only for a DM to one or more targets
|
||||
NOTE_TARGET_DM
|
||||
)
|
||||
|
||||
// Converts the NoteTarget value into a type the DB can use
|
||||
func (n *NoteAccessLevel) Value() (driver.Value, error) {
|
||||
return n, nil
|
||||
}
|
||||
|
||||
// Converts the raw value from the DB into a NoteTarget
|
||||
func (n *NoteAccessLevel) Scan(value any) error {
|
||||
vBig, ok := value.(int64)
|
||||
if !ok {
|
||||
return errors.New("not an int64")
|
||||
}
|
||||
*n = NoteAccessLevel(vBig)
|
||||
return nil
|
||||
}
|
|
@ -1,37 +0,0 @@
|
|||
// Code generated by "stringer -type NoteAccessLevel"; DO NOT EDIT.
|
||||
|
||||
package storage
|
||||
|
||||
import "strconv"
|
||||
|
||||
func _() {
|
||||
// An "invalid array index" compiler error signifies that the constant values have changed.
|
||||
// Re-run the stringer command to generate them again.
|
||||
var x [1]struct{}
|
||||
_ = x[NOTE_TARGET_PUBLIC-0]
|
||||
_ = x[NOTE_TARGET_HOME-2]
|
||||
_ = x[NOTE_TARGET_FOLLOWERS-4]
|
||||
_ = x[NOTE_TARGET_DM-8]
|
||||
}
|
||||
|
||||
const (
|
||||
_NoteAccessLevel_name_0 = "NOTE_TARGET_PUBLIC"
|
||||
_NoteAccessLevel_name_1 = "NOTE_TARGET_HOME"
|
||||
_NoteAccessLevel_name_2 = "NOTE_TARGET_FOLLOWERS"
|
||||
_NoteAccessLevel_name_3 = "NOTE_TARGET_DM"
|
||||
)
|
||||
|
||||
func (i NoteAccessLevel) String() string {
|
||||
switch {
|
||||
case i == 0:
|
||||
return _NoteAccessLevel_name_0
|
||||
case i == 2:
|
||||
return _NoteAccessLevel_name_1
|
||||
case i == 4:
|
||||
return _NoteAccessLevel_name_2
|
||||
case i == 8:
|
||||
return _NoteAccessLevel_name_3
|
||||
default:
|
||||
return "NoteAccessLevel(" + strconv.FormatInt(int64(i), 10) + ")"
|
||||
}
|
||||
}
|
164
storage/notes.go
164
storage/notes.go
|
@ -1,164 +0,0 @@
|
|||
package storage
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"git.mstar.dev/mstar/linstrom/shared"
|
||||
)
|
||||
|
||||
// Note represents an ActivityPub note
|
||||
// ActivityPub notes can be quite a few things, depending on fields provided.
|
||||
// A survey, a reply, a quote of another note, etc
|
||||
// And depending on the origin server of a note, they are treated differently
|
||||
// with for example rendering or available actions
|
||||
// This struct attempts to contain all information necessary for easily working with a note
|
||||
type Note struct {
|
||||
ID string `gorm:"primarykey"` // Make ID a string (uuid) for other implementations
|
||||
CreatedAt time.Time // When this entry was created
|
||||
UpdatedAt time.Time // When this entry was last updated
|
||||
// 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"`
|
||||
// Creator Account // `gorm:"foreignKey:CreatorId;references:ID"` // Account that created the post
|
||||
CreatorId string
|
||||
Remote bool // Whether the note is originally a remote one and just "cached"
|
||||
// Raw content of the note. So without additional formatting applied
|
||||
// Might already have formatting applied beforehand from the origin server
|
||||
RawContent string
|
||||
ContentWarning *string // Content warnings of the note, if it contains any
|
||||
Attachments []string `gorm:"serializer:json"` // List of Ids for mediaFiles
|
||||
Emotes []string `gorm:"serializer:json"` // Emotes used in that message
|
||||
RepliesTo *string // Url of the message this replies to
|
||||
Quotes *string // url of the message this note quotes
|
||||
AccessLevel NoteAccessLevel // Where to send this message to (public, home, followers, dm)
|
||||
Pings []string `gorm:"serializer:json"` // Who is being tagged in this message. Also serves as DM targets
|
||||
OriginServer string // Url of the origin server. Also the primary key for those
|
||||
Tags []string `gorm:"serializer:json"` // Hashtags
|
||||
}
|
||||
|
||||
func (s *Storage) FindNoteById(id string) (*Note, error) {
|
||||
defer shared.Untrace(shared.Trace(&log.Logger))
|
||||
note := &Note{}
|
||||
cacheNote, err := s.cacheNoteIdToData(id)
|
||||
switch err {
|
||||
case nil:
|
||||
return cacheNote, nil
|
||||
// Empty case, not found in cache means check db
|
||||
case errCacheNotFound:
|
||||
default:
|
||||
return nil, err
|
||||
}
|
||||
switch err {
|
||||
|
||||
}
|
||||
err = s.db.Find(note, id).Error
|
||||
switch err {
|
||||
case nil:
|
||||
if err = s.cache.Set(cacheNoteIdToNotePrefix+id, note); err != nil {
|
||||
log.Warn().Err(err).Str("note-id", id).Msg("Failed to place note in cache")
|
||||
}
|
||||
return note, nil
|
||||
case gorm.ErrRecordNotFound:
|
||||
return nil, ErrEntryNotFound
|
||||
default:
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Storage) FindNotesByFuzzyContent(fuzzyContent string) ([]Note, error) {
|
||||
defer shared.Untrace(shared.Trace(&log.Logger))
|
||||
notes := []Note{}
|
||||
// TODO: Figure out if cache can be used here too
|
||||
err := s.db.Where("raw_content LIKE %?%", fuzzyContent).Find(notes).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return notes, nil
|
||||
}
|
||||
|
||||
func (s *Storage) FindNotesByAuthorHandle(handle string) ([]Note, error) {
|
||||
defer shared.Untrace(shared.Trace(&log.Logger))
|
||||
acc, err := s.FindAccountByFullHandle(handle)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("account with handle %s not found: %w", handle, err)
|
||||
}
|
||||
return s.FindNotesByAuthorId(acc.ID)
|
||||
}
|
||||
|
||||
func (s *Storage) FindNotesByAuthorId(id string) ([]Note, error) {
|
||||
defer shared.Untrace(shared.Trace(&log.Logger))
|
||||
notes := []Note{}
|
||||
err := s.db.Where("creator = ?", id).Find(notes).Error
|
||||
switch err {
|
||||
case nil:
|
||||
return notes, nil
|
||||
case gorm.ErrRecordNotFound:
|
||||
return nil, ErrEntryNotFound
|
||||
default:
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Storage) UpdateNote(note *Note) error {
|
||||
defer shared.Untrace(shared.Trace(&log.Logger))
|
||||
if note == nil || note.ID == "" {
|
||||
return ErrInvalidData
|
||||
}
|
||||
err := s.db.Save(note).Error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = s.cache.Set(cacheNoteIdToNotePrefix+note.ID, note)
|
||||
if err != nil {
|
||||
log.Warn().
|
||||
Err(err).
|
||||
Msg("Failed to update note into cache. Cache and db might be out of sync, a force sync is recommended")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Storage) CreateNoteLocal(
|
||||
creatorId string,
|
||||
rawContent string,
|
||||
contentWarning *string,
|
||||
timestamp time.Time,
|
||||
attachmentIds []string,
|
||||
emoteIds []string,
|
||||
repliesToId *string,
|
||||
quotesId *string,
|
||||
accessLevel NoteAccessLevel,
|
||||
tags []string,
|
||||
) (*Note, error) {
|
||||
defer shared.Untrace(shared.Trace(&log.Logger))
|
||||
// TODO: Implement me
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (s *Storage) CreateNoteRemote(
|
||||
creatorId string,
|
||||
rawContent string,
|
||||
contentWarning *string,
|
||||
timestamp time.Time,
|
||||
attachmentIds []string,
|
||||
emoteIds []string,
|
||||
repliesToId *string,
|
||||
quotesId *string,
|
||||
accessLevel NoteAccessLevel,
|
||||
tags []string,
|
||||
originId string,
|
||||
) (*Note, error) {
|
||||
defer shared.Untrace(shared.Trace(&log.Logger))
|
||||
// TODO: Implement me
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (s *Storage) DeleteNote(id string) {
|
||||
defer shared.Untrace(shared.Trace(&log.Logger))
|
||||
s.cache.Delete(cacheNoteIdToNotePrefix + id)
|
||||
s.db.Delete(Note{ID: id})
|
||||
}
|
|
@ -1,65 +0,0 @@
|
|||
package storage
|
||||
|
||||
import (
|
||||
"github.com/rs/zerolog/log"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"git.mstar.dev/mstar/linstrom/shared"
|
||||
)
|
||||
|
||||
type OutboundJob struct {
|
||||
gorm.Model // Include full model. Gives ID, created and updated at timestamps as well as soft deletes
|
||||
// Read (and create) only values to ensure consistency
|
||||
TargetServer string `gorm:"->;<-:create"` // The url of the target server
|
||||
TargetPath string `gorm:"->;<-:create"` // The full path of api endpoint targeted
|
||||
Data []byte `gorm:"->;<-:create"` // The raw data to send
|
||||
}
|
||||
|
||||
func (s *Storage) AddNewOutboundJob(data []byte, targetDomain string, targetUrl string) {
|
||||
defer shared.Untrace(shared.Trace(&log.Logger))
|
||||
newJob := OutboundJob{
|
||||
Data: data,
|
||||
TargetServer: targetDomain,
|
||||
TargetPath: targetUrl,
|
||||
}
|
||||
s.db.Create(&newJob)
|
||||
}
|
||||
|
||||
// Get the specified amount of jobs, sorted by age (oldest first)
|
||||
func (s *Storage) GetOldestOutboundJobs(amount uint) ([]OutboundJob, error) {
|
||||
defer shared.Untrace(shared.Trace(&log.Logger))
|
||||
jobs := []OutboundJob{}
|
||||
err := s.db.Order("id asc, created_at asc").Limit(int(amount)).Find(jobs).Error
|
||||
switch err {
|
||||
case gorm.ErrRecordNotFound:
|
||||
return nil, ErrEntryNotFound
|
||||
case nil:
|
||||
return jobs, nil
|
||||
default:
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Storage) GetOutboundJobsForDomain(domain string, amount uint) ([]OutboundJob, error) {
|
||||
defer shared.Untrace(shared.Trace(&log.Logger))
|
||||
jobs := []OutboundJob{}
|
||||
err := s.db.Where("target_server = ?", domain).
|
||||
Order("id asc, created_at asc").
|
||||
Limit(int(amount)).
|
||||
Find(jobs).
|
||||
Error
|
||||
switch err {
|
||||
case gorm.ErrRecordNotFound:
|
||||
return nil, ErrEntryNotFound
|
||||
case nil:
|
||||
return jobs, nil
|
||||
default:
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Storage) CompleteOutboundJob(id uint) error {
|
||||
defer shared.Untrace(shared.Trace(&log.Logger))
|
||||
s.db.Delete(OutboundJob{Model: gorm.Model{ID: id}})
|
||||
return nil
|
||||
}
|
|
@ -1,60 +0,0 @@
|
|||
package storage
|
||||
|
||||
import (
|
||||
"github.com/go-webauthn/webauthn/webauthn"
|
||||
"github.com/google/uuid"
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"git.mstar.dev/mstar/linstrom/shared"
|
||||
)
|
||||
|
||||
// Session data used during login attempts with a passkey
|
||||
// Not actually used afterwards to verify a normal session
|
||||
// NOTE: Doesn't contain a DeletedAt field, thus deletions are automatically hard and not reversible
|
||||
type PasskeySession struct {
|
||||
ID string `gorm:"primarykey"`
|
||||
Data webauthn.SessionData `gorm:"serializer:json"`
|
||||
}
|
||||
|
||||
// ---- Section SessionStore
|
||||
|
||||
// Generate some id for a new session. Just returns a new uuid
|
||||
func (s *Storage) GenSessionID() (string, error) {
|
||||
defer shared.Untrace(shared.Trace(&log.Logger))
|
||||
x := uuid.NewString()
|
||||
log.Debug().Str("session-id", x).Msg("Generated new passkey session id")
|
||||
return x, nil
|
||||
}
|
||||
|
||||
// Look for an active session with a given id
|
||||
// Returns the session if found and a bool indicating if a session was found
|
||||
func (s *Storage) GetSession(sessionId string) (*webauthn.SessionData, bool) {
|
||||
defer shared.Untrace(shared.Trace(&log.Logger))
|
||||
log.Debug().Str("id", sessionId).Msg("Looking for passkey session")
|
||||
session := PasskeySession{}
|
||||
res := s.db.Where("id = ?", sessionId).First(&session)
|
||||
if res.Error != nil {
|
||||
return nil, false
|
||||
}
|
||||
log.Debug().Str("id", sessionId).Any("webauthn-data", &session).Msg("Found passkey session")
|
||||
return &session.Data, true
|
||||
}
|
||||
|
||||
// Save (or update) a session with the new data
|
||||
func (s *Storage) SaveSession(token string, data *webauthn.SessionData) {
|
||||
defer shared.Untrace(shared.Trace(&log.Logger))
|
||||
log.Debug().Str("id", token).Any("webauthn-data", data).Msg("Saving passkey session")
|
||||
session := PasskeySession{
|
||||
ID: token,
|
||||
Data: *data,
|
||||
}
|
||||
s.db.Save(&session)
|
||||
}
|
||||
|
||||
// Delete a session
|
||||
// NOTE: This is a hard delete since the session struct contains no DeletedAt field
|
||||
func (s *Storage) DeleteSession(token string) {
|
||||
defer shared.Untrace(shared.Trace(&log.Logger))
|
||||
log.Debug().Str("id", token).Msg("Deleting passkey session (if one exists)")
|
||||
s.db.Delete(&PasskeySession{ID: token})
|
||||
}
|
|
@ -1,10 +0,0 @@
|
|||
package storage
|
||||
|
||||
import "gorm.io/gorm"
|
||||
|
||||
type Reaction struct {
|
||||
gorm.Model
|
||||
NoteId string
|
||||
ReactorId string
|
||||
EmoteId uint
|
||||
}
|
|
@ -1,113 +0,0 @@
|
|||
package storage
|
||||
|
||||
import (
|
||||
"github.com/rs/zerolog/log"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"git.mstar.dev/mstar/linstrom/shared"
|
||||
)
|
||||
|
||||
type RemoteServer struct {
|
||||
gorm.Model
|
||||
ServerType RemoteServerType // What software the server is running. Useful for formatting
|
||||
Domain string // `gorm:"primaryKey"` // Domain the server exists under. Additional primary key
|
||||
Name string // What the server wants to be known as (usually same as url)
|
||||
Icon string // ID of a media file
|
||||
IsSelf bool // Whether this server is yours truly
|
||||
}
|
||||
|
||||
func (s *Storage) FindRemoteServerByDomain(url string) (*RemoteServer, error) {
|
||||
defer shared.Untrace(shared.Trace(&log.Logger))
|
||||
server := RemoteServer{}
|
||||
err := s.db.Where("domain = ?").First(&server).Error
|
||||
switch err {
|
||||
case nil:
|
||||
return &server, nil
|
||||
case gorm.ErrRecordNotFound:
|
||||
return nil, ErrEntryNotFound
|
||||
default:
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Find a remote server with a given display name
|
||||
func (s *Storage) FindRemoteServerByDisplayName(displayName string) (*RemoteServer, error) {
|
||||
defer shared.Untrace(shared.Trace(&log.Logger))
|
||||
server := RemoteServer{}
|
||||
err := s.db.Where("name = ?", displayName).First(&server).Error
|
||||
switch err {
|
||||
case nil:
|
||||
return &server, nil
|
||||
case gorm.ErrRecordNotFound:
|
||||
return nil, ErrEntryNotFound
|
||||
default:
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Storage) FindRemoteServerById(id uint) (*RemoteServer, error) {
|
||||
defer shared.Untrace(shared.Trace(&log.Logger))
|
||||
server := RemoteServer{}
|
||||
err := s.db.First(&server, id).Error
|
||||
switch err {
|
||||
case nil:
|
||||
return &server, nil
|
||||
case gorm.ErrRecordNotFound:
|
||||
return nil, ErrEntryNotFound
|
||||
default:
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Create a new remote server
|
||||
func (s *Storage) NewRemoteServer(
|
||||
url, displayName, icon string,
|
||||
serverType RemoteServerType,
|
||||
) (*RemoteServer, error) {
|
||||
defer shared.Untrace(shared.Trace(&log.Logger))
|
||||
_, err := s.FindRemoteServerByDomain(url)
|
||||
switch err {
|
||||
case nil:
|
||||
return nil, ErrEntryAlreadyExists
|
||||
case ErrEntryNotFound: // Empty case, not found is what we want
|
||||
default:
|
||||
return nil, err
|
||||
}
|
||||
server := RemoteServer{
|
||||
Domain: url,
|
||||
Name: displayName,
|
||||
Icon: icon,
|
||||
ServerType: serverType,
|
||||
}
|
||||
err = s.db.Create(&server).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &server, nil
|
||||
}
|
||||
|
||||
// Update a remote server with the given url
|
||||
// If displayName is set, update that
|
||||
// If icon is set, update that
|
||||
// Returns the updated version
|
||||
func (s *Storage) UpdateRemoteServer(url string, displayName, icon *string) (*RemoteServer, error) {
|
||||
defer shared.Untrace(shared.Trace(&log.Logger))
|
||||
if displayName == nil && icon == nil {
|
||||
return nil, ErrNothingToChange
|
||||
}
|
||||
server, err := s.FindRemoteServerByDomain(url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if displayName != nil {
|
||||
server.Name = *displayName
|
||||
}
|
||||
if icon != nil {
|
||||
server.Icon = *icon
|
||||
}
|
||||
err = s.db.Save(server).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return server, nil
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
package storage
|
||||
|
||||
// TODO: More helper stuff
|
||||
|
||||
func (s *Storage) NewRemoteUser(fullHandle string) (*Account, error) {
|
||||
return nil, nil
|
||||
}
|
253
storage/roles.go
253
storage/roles.go
|
@ -1,253 +0,0 @@
|
|||
package storage
|
||||
|
||||
import (
|
||||
"git.mstar.dev/mstar/goutils/sliceutils"
|
||||
"github.com/rs/zerolog/log"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"git.mstar.dev/mstar/linstrom/shared"
|
||||
)
|
||||
|
||||
// Could I collapse all these go:generate command into more condensed ones?
|
||||
// Yes
|
||||
// Will I do that?
|
||||
// No
|
||||
// This is explicit in what is being done. And easier to understand
|
||||
|
||||
//go:generate go build -o RolesGenerator ../cmd/RolesGenerator/main.go
|
||||
//go:generate ./RolesGenerator -input=roles.go -output=rolesshared_generated.go
|
||||
//go:generate rm RolesGenerator
|
||||
|
||||
//go:generate go build -o ApiGenerator ../cmd/RolesApiTypeGenerator/main.go
|
||||
//go:generate ./ApiGenerator -input=roles.go -output=../server/apiLinstromTypes_generated.go
|
||||
//go:generate rm ApiGenerator
|
||||
|
||||
//go:generate go build -o HelperGenerator ../cmd/RolesApiConverter/main.go
|
||||
//go:generate ./HelperGenerator -input=roles.go -output=../server/apiLinstromTypeHelpers_generated.go
|
||||
//go:generate rm HelperGenerator
|
||||
|
||||
//go:generate go build -o FrontendGenerator ../cmd/RolesFrontendGenerator/main.go
|
||||
//go:generate ./FrontendGenerator -input=roles.go -output=../frontend-reactive/app/models/role.ts
|
||||
//go:generate rm FrontendGenerator
|
||||
|
||||
// A role is, in concept, similar to how Discord handles roles
|
||||
// Some permission can be either disallowed (&false), don't care (nil) or allowed (&true)
|
||||
// Don't care just says to use the value from the next lower role where it is set
|
||||
type Role struct {
|
||||
// TODO: More control options
|
||||
// Extend upon whatever Masto, Akkoma and Misskey have
|
||||
// Lots of details please
|
||||
|
||||
// --- Role metadata ---
|
||||
|
||||
// Include full db model stuff
|
||||
gorm.Model
|
||||
|
||||
// Name of the role
|
||||
Name string `gorm:"primaryKey;unique"`
|
||||
|
||||
// Priority of the role
|
||||
// Lower priority gets applied first and thus overwritten by higher priority ones
|
||||
// If two roles have the same priority, the order is undetermined and may be random
|
||||
// Default priority for new roles is 1 to always overwrite default user
|
||||
// And full admin has max priority possible
|
||||
Priority uint32
|
||||
// Whether this role is for a for a single user only (like custom, per user permissions in Discord)
|
||||
// If yes, Name will be the id of the user in question
|
||||
IsUserRole bool
|
||||
|
||||
// Whether this role is one built into Linstrom from the start or not
|
||||
// Note: Built-in roles can't be modified
|
||||
IsBuiltIn bool
|
||||
|
||||
// --- User permissions ---
|
||||
CanSendMedia *bool // Local & remote
|
||||
CanSendCustomEmotes *bool // Local & remote
|
||||
CanSendCustomReactions *bool // Local & remote
|
||||
CanSendPublicNotes *bool // Local & remote
|
||||
CanSendLocalNotes *bool // Local & remote
|
||||
CanSendFollowerOnlyNotes *bool // Local & remote
|
||||
CanSendPrivateNotes *bool // Local & remote
|
||||
CanSendReplies *bool // Local & remote
|
||||
CanQuote *bool // Local only
|
||||
CanBoost *bool // Local only
|
||||
CanIncludeLinks *bool // Local & remote
|
||||
CanIncludeSurvey *bool // Local
|
||||
CanFederateFedi *bool // Local & remote
|
||||
CanFederateBsky *bool // Local
|
||||
|
||||
CanChangeDisplayName *bool // Local
|
||||
|
||||
// Internal ids of accounts blocked by this role
|
||||
BlockedUsers []string `gorm:"type:bytes;serializer:gob"` // Local
|
||||
CanSubmitReports *bool // Local & remote
|
||||
// If disabled, an account can no longer be interacted with. The owner can no longer change anything about it
|
||||
// And the UI will show a notice (and maybe include that info in the AP data too)
|
||||
// Only moderators and admins will be able to edit the account's roles
|
||||
CanLogin *bool // Local
|
||||
|
||||
CanMentionOthers *bool // Local & remote
|
||||
HasMentionCountLimit *bool // Local & remote
|
||||
MentionLimit *uint32 // Local & remote
|
||||
|
||||
// CanViewBoosts *bool
|
||||
// CanViewQuotes *bool
|
||||
// CanViewMedia *bool
|
||||
// CanViewCustomEmotes *bool
|
||||
|
||||
// --- Automod ---
|
||||
AutoNsfwMedia *bool // Local & remote
|
||||
AutoCwPosts *bool // Local & remote
|
||||
AutoCwPostsText *string // Local & remote
|
||||
ScanCreatedPublicNotes *bool // Local & remote
|
||||
ScanCreatedLocalNotes *bool // Local & remote
|
||||
ScanCreatedFollowerOnlyNotes *bool // Local & remote
|
||||
ScanCreatedPrivateNotes *bool // Local & remote
|
||||
// Blocks all interactions and federation between users with the role and all included ids/handles
|
||||
// TODO: Decide whether this is a list of handles or of account ids
|
||||
// Handles would increase the load due to having to search for them first
|
||||
// while ids would require to store every single account mentioned
|
||||
// which could cause escalating storage costs
|
||||
DisallowInteractionsWith []string `gorm:"type:bytes;serializer:gob"` // Local & remote
|
||||
|
||||
WithholdNotesForManualApproval *bool // Local & remote
|
||||
WithholdNotesBasedOnRegex *bool // Local & remote
|
||||
WithholdNotesRegexes []string `gorm:"type:bytes;serializer:gob"` // Local & remote
|
||||
|
||||
// --- Admin perms ---
|
||||
// If set, counts as all permissions being set as given and all restrictions being disabled
|
||||
FullAdmin *bool // Local
|
||||
CanAffectOtherAdmins *bool // Local
|
||||
CanDeleteNotes *bool // Local
|
||||
CanConfirmWithheldNotes *bool // Local
|
||||
CanAssignRoles *bool // Local
|
||||
CanSupressInteractionsBetweenUsers *bool // Local
|
||||
CanOverwriteDisplayNames *bool // Local
|
||||
CanManageCustomEmotes *bool // Local
|
||||
CanViewDeletedNotes *bool // Local
|
||||
CanRecoverDeletedNotes *bool // Local
|
||||
CanManageAvatarDecorations *bool // Local
|
||||
CanManageAds *bool // Local
|
||||
CanSendAnnouncements *bool // Local
|
||||
CanDeleteAccounts *bool // Local
|
||||
}
|
||||
|
||||
/*
|
||||
Mastodon permissions (highest permission to lowest):
|
||||
- Admin
|
||||
- Devops
|
||||
- View Audit log
|
||||
- View Dashboard
|
||||
- Manage Reports
|
||||
- Manage Federation
|
||||
- Manage Settings
|
||||
- Manage Blocks
|
||||
- Manage Taxonomies
|
||||
- Manage Appeals
|
||||
- Manage Users
|
||||
- Manage Invites
|
||||
- Manage Rules
|
||||
- Manage Announcements
|
||||
- Manage Custom Emojis
|
||||
- Manage Webhooks
|
||||
- Invite Users
|
||||
- Manage Roles
|
||||
- Manage User Access
|
||||
- Delete User Data
|
||||
*/
|
||||
|
||||
/*
|
||||
Misskey "permissions" (no order):
|
||||
- Global timeline available (interact with global timeline I think)
|
||||
- Local timeline available (same as global, but for local)
|
||||
- b-something timeline available
|
||||
- Can send public notes
|
||||
- How many mentions a note can have
|
||||
- Can invite others
|
||||
- How many invites can be sent
|
||||
- InviteLimitCycle (whatever that means)
|
||||
- Invite Expiration time (duration of how long invites stay valid I think)
|
||||
- Manage custom emojis
|
||||
- Manage custom avatar decorations
|
||||
- Seach for notes
|
||||
- Use translator
|
||||
- Hide ads from self
|
||||
- How much storage space the user has
|
||||
- Whether to mark all posts from account as nsfw
|
||||
- max number of pinned messages
|
||||
- max number of antennas
|
||||
- max number of muted words
|
||||
- max number of webhooks
|
||||
- max number of clips
|
||||
- max number of notes contained in a clip (? I think. Don't know enough about clips)
|
||||
- Max number of lists of users
|
||||
- max number of users in a user list
|
||||
- rate limit multiplier
|
||||
- max number of applied avatar decorations
|
||||
*/
|
||||
|
||||
func (s *Storage) NewEmptyRole(name string) (*Role, error) {
|
||||
defer shared.Untrace(shared.Trace(&log.Logger))
|
||||
// Check if a role with the given name already exists
|
||||
_, err := s.FindRoleByName(name)
|
||||
switch err {
|
||||
case nil:
|
||||
return nil, ErrEntryAlreadyExists
|
||||
case ErrEntryNotFound: // Empty case, since this is what we want
|
||||
default:
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// New roles have a priority of 1 by default
|
||||
newRole := Role{Name: name, Priority: 1}
|
||||
err = s.db.Create(&newRole).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &newRole, nil
|
||||
}
|
||||
|
||||
func (s *Storage) FindRoleByName(name string) (*Role, error) {
|
||||
defer shared.Untrace(shared.Trace(&log.Logger))
|
||||
role := Role{}
|
||||
err := s.db.Where("name = ?", name).First(&role).Error
|
||||
switch err {
|
||||
case nil:
|
||||
return &role, nil
|
||||
case gorm.ErrRecordNotFound:
|
||||
return nil, ErrEntryNotFound
|
||||
default:
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Storage) FindRolesByNames(names []string) ([]Role, error) {
|
||||
defer shared.Untrace(shared.Trace(&log.Logger))
|
||||
roles := []Role{}
|
||||
err := s.db.Where("name IN ?", names).Find(&roles).Error
|
||||
switch err {
|
||||
case nil:
|
||||
return roles, nil
|
||||
case gorm.ErrRecordNotFound:
|
||||
return nil, ErrEntryNotFound
|
||||
default:
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Storage) UpdateRole(role *Role) error {
|
||||
defer shared.Untrace(shared.Trace(&log.Logger))
|
||||
return s.db.Save(role).Error
|
||||
}
|
||||
|
||||
func (s *Storage) DeleteRoleByName(name string) error {
|
||||
// Prevent deletion of built-in roles
|
||||
if sliceutils.Contains(
|
||||
sliceutils.Map(allDefaultRoles, func(t *Role) string { return t.Name }),
|
||||
name,
|
||||
) {
|
||||
return ErrNotAllowed
|
||||
}
|
||||
defer shared.Untrace(shared.Trace(&log.Logger))
|
||||
return s.db.Where(&Role{Name: name, IsBuiltIn: false}).Delete(&Role{}).Error
|
||||
}
|
|
@ -1,244 +0,0 @@
|
|||
package storage
|
||||
|
||||
import (
|
||||
"math"
|
||||
|
||||
"git.mstar.dev/mstar/goutils/other"
|
||||
)
|
||||
|
||||
// Default role every user has. Defines sane defaults for a normal user
|
||||
// Will get overwritten by just about every other role due to every other role having higher priority
|
||||
var DefaultUserRole = Role{
|
||||
Name: "Default",
|
||||
Priority: 0,
|
||||
IsUserRole: false,
|
||||
IsBuiltIn: true,
|
||||
|
||||
CanSendMedia: other.IntoPointer(true),
|
||||
CanSendCustomEmotes: other.IntoPointer(true),
|
||||
CanSendCustomReactions: other.IntoPointer(true),
|
||||
CanSendPublicNotes: other.IntoPointer(true),
|
||||
CanSendLocalNotes: other.IntoPointer(true),
|
||||
CanSendFollowerOnlyNotes: other.IntoPointer(true),
|
||||
CanSendPrivateNotes: other.IntoPointer(true),
|
||||
CanSendReplies: other.IntoPointer(true),
|
||||
CanQuote: other.IntoPointer(true),
|
||||
CanBoost: other.IntoPointer(true),
|
||||
CanIncludeLinks: other.IntoPointer(true),
|
||||
CanIncludeSurvey: other.IntoPointer(true),
|
||||
CanFederateFedi: other.IntoPointer(true),
|
||||
CanFederateBsky: other.IntoPointer(true),
|
||||
|
||||
CanChangeDisplayName: other.IntoPointer(true),
|
||||
|
||||
BlockedUsers: []string{},
|
||||
CanSubmitReports: other.IntoPointer(true),
|
||||
CanLogin: other.IntoPointer(true),
|
||||
|
||||
CanMentionOthers: other.IntoPointer(true),
|
||||
HasMentionCountLimit: other.IntoPointer(false),
|
||||
MentionLimit: other.IntoPointer(
|
||||
uint32(math.MaxUint32),
|
||||
), // Set this to max, even if not used due to *HasMentionCountLimit == false
|
||||
|
||||
AutoNsfwMedia: other.IntoPointer(false),
|
||||
AutoCwPosts: other.IntoPointer(false),
|
||||
AutoCwPostsText: nil,
|
||||
WithholdNotesForManualApproval: other.IntoPointer(false),
|
||||
ScanCreatedPublicNotes: other.IntoPointer(false),
|
||||
ScanCreatedLocalNotes: other.IntoPointer(false),
|
||||
ScanCreatedFollowerOnlyNotes: other.IntoPointer(false),
|
||||
ScanCreatedPrivateNotes: other.IntoPointer(false),
|
||||
DisallowInteractionsWith: []string{},
|
||||
|
||||
FullAdmin: other.IntoPointer(false),
|
||||
CanAffectOtherAdmins: other.IntoPointer(false),
|
||||
CanDeleteNotes: other.IntoPointer(false),
|
||||
CanConfirmWithheldNotes: other.IntoPointer(false),
|
||||
CanAssignRoles: other.IntoPointer(false),
|
||||
CanSupressInteractionsBetweenUsers: other.IntoPointer(false),
|
||||
CanOverwriteDisplayNames: other.IntoPointer(false),
|
||||
CanManageCustomEmotes: other.IntoPointer(false),
|
||||
CanViewDeletedNotes: other.IntoPointer(false),
|
||||
CanRecoverDeletedNotes: other.IntoPointer(false),
|
||||
CanManageAvatarDecorations: other.IntoPointer(false),
|
||||
CanManageAds: other.IntoPointer(false),
|
||||
CanSendAnnouncements: other.IntoPointer(false),
|
||||
}
|
||||
|
||||
// Role providing maximum permissions
|
||||
var FullAdminRole = Role{
|
||||
Name: "fullAdmin",
|
||||
Priority: math.MaxUint32,
|
||||
IsUserRole: false,
|
||||
IsBuiltIn: true,
|
||||
|
||||
CanSendMedia: other.IntoPointer(true),
|
||||
CanSendCustomEmotes: other.IntoPointer(true),
|
||||
CanSendCustomReactions: other.IntoPointer(true),
|
||||
CanSendPublicNotes: other.IntoPointer(true),
|
||||
CanSendLocalNotes: other.IntoPointer(true),
|
||||
CanSendFollowerOnlyNotes: other.IntoPointer(true),
|
||||
CanSendPrivateNotes: other.IntoPointer(true),
|
||||
CanQuote: other.IntoPointer(true),
|
||||
CanBoost: other.IntoPointer(true),
|
||||
CanIncludeLinks: other.IntoPointer(true),
|
||||
CanIncludeSurvey: other.IntoPointer(true),
|
||||
|
||||
CanChangeDisplayName: other.IntoPointer(true),
|
||||
|
||||
BlockedUsers: []string{},
|
||||
CanSubmitReports: other.IntoPointer(true),
|
||||
CanLogin: other.IntoPointer(true),
|
||||
|
||||
CanMentionOthers: other.IntoPointer(true),
|
||||
HasMentionCountLimit: other.IntoPointer(false),
|
||||
MentionLimit: other.IntoPointer(
|
||||
uint32(math.MaxUint32),
|
||||
), // Set this to max, even if not used due to *HasMentionCountLimit == false
|
||||
|
||||
AutoNsfwMedia: other.IntoPointer(false),
|
||||
AutoCwPosts: other.IntoPointer(false),
|
||||
AutoCwPostsText: nil,
|
||||
WithholdNotesForManualApproval: other.IntoPointer(false),
|
||||
ScanCreatedPublicNotes: other.IntoPointer(false),
|
||||
ScanCreatedLocalNotes: other.IntoPointer(false),
|
||||
ScanCreatedFollowerOnlyNotes: other.IntoPointer(false),
|
||||
ScanCreatedPrivateNotes: other.IntoPointer(false),
|
||||
DisallowInteractionsWith: []string{},
|
||||
|
||||
FullAdmin: other.IntoPointer(true),
|
||||
CanAffectOtherAdmins: other.IntoPointer(true),
|
||||
CanDeleteNotes: other.IntoPointer(true),
|
||||
CanConfirmWithheldNotes: other.IntoPointer(true),
|
||||
CanAssignRoles: other.IntoPointer(true),
|
||||
CanSupressInteractionsBetweenUsers: other.IntoPointer(true),
|
||||
CanOverwriteDisplayNames: other.IntoPointer(true),
|
||||
CanManageCustomEmotes: other.IntoPointer(true),
|
||||
CanViewDeletedNotes: other.IntoPointer(true),
|
||||
CanRecoverDeletedNotes: other.IntoPointer(true),
|
||||
CanManageAvatarDecorations: other.IntoPointer(true),
|
||||
CanManageAds: other.IntoPointer(true),
|
||||
CanSendAnnouncements: other.IntoPointer(true),
|
||||
}
|
||||
|
||||
// Role for totally freezing an account, blocking all activity from it
|
||||
var AccountFreezeRole = Role{
|
||||
Name: "accountFreeze",
|
||||
Priority: math.MaxUint32 - 1,
|
||||
IsUserRole: false,
|
||||
IsBuiltIn: true,
|
||||
|
||||
CanSendMedia: other.IntoPointer(false),
|
||||
CanSendCustomEmotes: other.IntoPointer(false),
|
||||
CanSendCustomReactions: other.IntoPointer(false),
|
||||
CanSendPublicNotes: other.IntoPointer(false),
|
||||
CanSendLocalNotes: other.IntoPointer(false),
|
||||
CanSendFollowerOnlyNotes: other.IntoPointer(false),
|
||||
CanSendPrivateNotes: other.IntoPointer(false),
|
||||
CanSendReplies: other.IntoPointer(false),
|
||||
CanQuote: other.IntoPointer(false),
|
||||
CanBoost: other.IntoPointer(false),
|
||||
CanIncludeLinks: other.IntoPointer(false),
|
||||
CanIncludeSurvey: other.IntoPointer(false),
|
||||
CanFederateBsky: other.IntoPointer(false),
|
||||
CanFederateFedi: other.IntoPointer(false),
|
||||
|
||||
CanChangeDisplayName: other.IntoPointer(false),
|
||||
|
||||
BlockedUsers: []string{},
|
||||
CanSubmitReports: other.IntoPointer(false),
|
||||
CanLogin: other.IntoPointer(false),
|
||||
|
||||
CanMentionOthers: other.IntoPointer(false),
|
||||
HasMentionCountLimit: other.IntoPointer(false),
|
||||
MentionLimit: other.IntoPointer(
|
||||
uint32(math.MaxUint32),
|
||||
), // Set this to max, even if not used due to *HasMentionCountLimit == false
|
||||
|
||||
AutoNsfwMedia: other.IntoPointer(true),
|
||||
AutoCwPosts: other.IntoPointer(false),
|
||||
AutoCwPostsText: other.IntoPointer("Account frozen"),
|
||||
WithholdNotesForManualApproval: other.IntoPointer(true),
|
||||
ScanCreatedPublicNotes: other.IntoPointer(false),
|
||||
ScanCreatedLocalNotes: other.IntoPointer(false),
|
||||
ScanCreatedFollowerOnlyNotes: other.IntoPointer(false),
|
||||
ScanCreatedPrivateNotes: other.IntoPointer(false),
|
||||
DisallowInteractionsWith: []string{},
|
||||
|
||||
FullAdmin: other.IntoPointer(false),
|
||||
CanAffectOtherAdmins: other.IntoPointer(false),
|
||||
CanDeleteNotes: other.IntoPointer(false),
|
||||
CanConfirmWithheldNotes: other.IntoPointer(false),
|
||||
CanAssignRoles: other.IntoPointer(false),
|
||||
CanSupressInteractionsBetweenUsers: other.IntoPointer(false),
|
||||
CanOverwriteDisplayNames: other.IntoPointer(false),
|
||||
CanManageCustomEmotes: other.IntoPointer(false),
|
||||
CanViewDeletedNotes: other.IntoPointer(false),
|
||||
CanRecoverDeletedNotes: other.IntoPointer(false),
|
||||
CanManageAvatarDecorations: other.IntoPointer(false),
|
||||
CanManageAds: other.IntoPointer(false),
|
||||
CanSendAnnouncements: other.IntoPointer(false),
|
||||
}
|
||||
|
||||
var ServerActorRole = Role{
|
||||
Name: "ServerActor",
|
||||
Priority: math.MaxUint32,
|
||||
IsUserRole: true,
|
||||
IsBuiltIn: true,
|
||||
|
||||
CanSendMedia: other.IntoPointer(true),
|
||||
CanSendCustomEmotes: other.IntoPointer(true),
|
||||
CanSendCustomReactions: other.IntoPointer(true),
|
||||
CanSendPublicNotes: other.IntoPointer(true),
|
||||
CanSendLocalNotes: other.IntoPointer(true),
|
||||
CanSendFollowerOnlyNotes: other.IntoPointer(true),
|
||||
CanSendPrivateNotes: other.IntoPointer(true),
|
||||
CanQuote: other.IntoPointer(true),
|
||||
CanBoost: other.IntoPointer(true),
|
||||
CanIncludeLinks: other.IntoPointer(true),
|
||||
CanIncludeSurvey: other.IntoPointer(true),
|
||||
|
||||
CanChangeDisplayName: other.IntoPointer(true),
|
||||
|
||||
BlockedUsers: []string{},
|
||||
CanSubmitReports: other.IntoPointer(true),
|
||||
CanLogin: other.IntoPointer(true),
|
||||
|
||||
CanMentionOthers: other.IntoPointer(true),
|
||||
HasMentionCountLimit: other.IntoPointer(false),
|
||||
MentionLimit: other.IntoPointer(
|
||||
uint32(math.MaxUint32),
|
||||
), // Set this to max, even if not used due to *HasMentionCountLimit == false
|
||||
|
||||
AutoNsfwMedia: other.IntoPointer(false),
|
||||
AutoCwPosts: other.IntoPointer(false),
|
||||
AutoCwPostsText: nil,
|
||||
WithholdNotesForManualApproval: other.IntoPointer(false),
|
||||
ScanCreatedPublicNotes: other.IntoPointer(false),
|
||||
ScanCreatedLocalNotes: other.IntoPointer(false),
|
||||
ScanCreatedFollowerOnlyNotes: other.IntoPointer(false),
|
||||
ScanCreatedPrivateNotes: other.IntoPointer(false),
|
||||
DisallowInteractionsWith: []string{},
|
||||
|
||||
FullAdmin: other.IntoPointer(true),
|
||||
CanAffectOtherAdmins: other.IntoPointer(true),
|
||||
CanDeleteNotes: other.IntoPointer(true),
|
||||
CanConfirmWithheldNotes: other.IntoPointer(true),
|
||||
CanAssignRoles: other.IntoPointer(true),
|
||||
CanSupressInteractionsBetweenUsers: other.IntoPointer(true),
|
||||
CanOverwriteDisplayNames: other.IntoPointer(true),
|
||||
CanManageCustomEmotes: other.IntoPointer(true),
|
||||
CanViewDeletedNotes: other.IntoPointer(true),
|
||||
CanRecoverDeletedNotes: other.IntoPointer(true),
|
||||
CanManageAvatarDecorations: other.IntoPointer(true),
|
||||
CanManageAds: other.IntoPointer(true),
|
||||
CanSendAnnouncements: other.IntoPointer(true),
|
||||
}
|
||||
|
||||
var allDefaultRoles = []*Role{
|
||||
&DefaultUserRole,
|
||||
&FullAdminRole,
|
||||
&AccountFreezeRole,
|
||||
&ServerActorRole,
|
||||
}
|
File diff suppressed because one or more lines are too long
|
@ -1,39 +0,0 @@
|
|||
package storage
|
||||
|
||||
import (
|
||||
"database/sql/driver"
|
||||
"errors"
|
||||
)
|
||||
|
||||
// TODO: Decide whether to turn this into an int too to save resources
|
||||
// And then use go:generate instead for pretty printing
|
||||
|
||||
// What software a server is running
|
||||
// Mostly important for rendering
|
||||
type RemoteServerType string
|
||||
|
||||
const (
|
||||
// Includes forks like glitch-soc, etc
|
||||
REMOTE_SERVER_MASTODON = RemoteServerType("Mastodon")
|
||||
// Includes forks like Ice Shrimp, Sharkey, Cutiekey, etc
|
||||
REMOTE_SERVER_MISSKEY = RemoteServerType("Misskey")
|
||||
// Includes Akkoma
|
||||
REMOTE_SERVER_PLEMORA = RemoteServerType("Plemora")
|
||||
// Wafrn is a new entry
|
||||
REMOTE_SERVER_WAFRN = RemoteServerType("Wafrn")
|
||||
// And of course, yours truly
|
||||
REMOTE_SERVER_LINSTROM = RemoteServerType("Linstrom")
|
||||
)
|
||||
|
||||
func (r *RemoteServerType) Value() (driver.Value, error) {
|
||||
return r, nil
|
||||
}
|
||||
|
||||
func (r *RemoteServerType) Scan(raw any) error {
|
||||
if v, ok := raw.(string); ok {
|
||||
*r = RemoteServerType(v)
|
||||
return nil
|
||||
} else {
|
||||
return errors.New("value not a string")
|
||||
}
|
||||
}
|
|
@ -1,164 +0,0 @@
|
|||
// TODO: Unify function names
|
||||
|
||||
// Storage is the handler for cache and db access
|
||||
// It handles storing various data in the database as well as caching that data
|
||||
// Said data includes notes, accounts, metadata about media files, servers and similar
|
||||
package storage
|
||||
|
||||
import (
|
||||
"crypto/ed25519"
|
||||
"fmt"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
"gorm.io/driver/postgres"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"git.mstar.dev/mstar/linstrom/config"
|
||||
"git.mstar.dev/mstar/linstrom/shared"
|
||||
"git.mstar.dev/mstar/linstrom/storage/cache"
|
||||
)
|
||||
|
||||
// Always keep a reference of the server's own RemoteServer entry here
|
||||
// Removes the need to perform a db request every time a new local anything
|
||||
// is created
|
||||
var serverSelf RemoteServer
|
||||
|
||||
// Storage is responsible for all database, cache and media related actions
|
||||
// and serves as the lowest layer of the cake
|
||||
type Storage struct {
|
||||
db *gorm.DB
|
||||
cache *cache.Cache
|
||||
}
|
||||
|
||||
func NewStorage(dbUrl string, cache *cache.Cache) (*Storage, error) {
|
||||
defer shared.Untrace(shared.Trace(&log.Logger))
|
||||
db, err := gorm.Open(postgres.Open(dbUrl), &gorm.Config{
|
||||
Logger: newGormLogger(log.Logger),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = db.AutoMigrate(
|
||||
MediaMetadata{},
|
||||
Account{},
|
||||
RemoteServer{},
|
||||
Note{},
|
||||
Role{},
|
||||
PasskeySession{},
|
||||
InboundJob{},
|
||||
OutboundJob{},
|
||||
AccessToken{},
|
||||
Emote{},
|
||||
UserInfoField{},
|
||||
AccountRelation{},
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to apply migrations: %w", err)
|
||||
}
|
||||
|
||||
s := &Storage{db, cache}
|
||||
|
||||
if err = s.insertDefaultRoles(); err != nil {
|
||||
return nil, fmt.Errorf("default roles insertion failed: %w", err)
|
||||
}
|
||||
|
||||
if err = s.insertPlaceholderFile(); err != nil {
|
||||
return nil, fmt.Errorf("placeholder file insertion failed: %w", err)
|
||||
}
|
||||
|
||||
if err = s.insertSelfFromConfig(); err != nil {
|
||||
return nil, fmt.Errorf("self insertion failed: %w", err)
|
||||
}
|
||||
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func (s *Storage) insertSelfFromConfig() error {
|
||||
defer shared.Untrace(shared.Trace(&log.Logger))
|
||||
const ServerActorId = "self"
|
||||
|
||||
var err error
|
||||
|
||||
// Insert server info
|
||||
serverData := RemoteServer{}
|
||||
err = s.db.Where("id = 1").
|
||||
// Set once on creation
|
||||
Attrs(RemoteServer{
|
||||
Domain: config.GlobalConfig.General.GetFullDomain(),
|
||||
}).
|
||||
// Set every time
|
||||
Assign(RemoteServer{
|
||||
IsSelf: true,
|
||||
Name: config.GlobalConfig.Self.ServerDisplayName,
|
||||
ServerType: REMOTE_SERVER_LINSTROM,
|
||||
Icon: "placeholder", // TODO: Set to server icon media
|
||||
}).FirstOrCreate(&serverData).Error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Set module specific global var
|
||||
serverSelf = serverData
|
||||
|
||||
// Insert server actor
|
||||
serverActor := Account{}
|
||||
serverActorPublicKey, serverActorPrivateKey, err := ed25519.GenerateKey(nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = s.db.Where(Account{ID: ServerActorId}).
|
||||
// Values to always (re)set after launch
|
||||
Assign(Account{
|
||||
Username: "self",
|
||||
DisplayName: config.GlobalConfig.Self.ServerActorDisplayName,
|
||||
// Server: serverData,
|
||||
ServerId: serverData.ID,
|
||||
// CustomFields: []uint{},
|
||||
Description: "Server actor of a Linstrom server",
|
||||
// Tags: []string{},
|
||||
IsBot: true,
|
||||
// Followers: []string{},
|
||||
// Follows: []string{},
|
||||
Indexable: false,
|
||||
RestrictedFollow: false,
|
||||
IdentifiesAs: []Being{},
|
||||
Gender: []string{},
|
||||
Roles: []string{"ServerActor"}, // TODO: Add server actor role once created
|
||||
}).
|
||||
// Values that'll only be set on first creation
|
||||
Attrs(Account{
|
||||
PublicKey: serverActorPublicKey,
|
||||
PrivateKey: serverActorPrivateKey,
|
||||
Icon: "placeholder",
|
||||
Background: nil,
|
||||
Banner: nil,
|
||||
}).
|
||||
FirstOrCreate(&serverActor).Error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Storage) insertDefaultRoles() error {
|
||||
defer shared.Untrace(shared.Trace(&log.Logger))
|
||||
for _, role := range allDefaultRoles {
|
||||
log.Debug().Str("role-name", role.Name).Msg("Inserting default role")
|
||||
if err := s.db.FirstOrCreate(role).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Storage) insertPlaceholderFile() error {
|
||||
defer shared.Untrace(shared.Trace(&log.Logger))
|
||||
return s.db.Model(&MediaMetadata{}).Assign(&MediaMetadata{
|
||||
ID: "placeholder",
|
||||
Type: "image/webp",
|
||||
Name: "placeholderFile",
|
||||
Blurred: false,
|
||||
Remote: false,
|
||||
Location: "/placeholder-file",
|
||||
AltText: "Greyscale image of a pidgeon, captioned with the text \"Duck\"",
|
||||
}).FirstOrCreate(&MediaMetadata{}).Error
|
||||
}
|
|
@ -1,60 +0,0 @@
|
|||
package storage
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/glebarez/sqlite"
|
||||
"git.mstar.dev/mstar/linstrom/storage/cache"
|
||||
"gorm.io/driver/postgres"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// Storage is responsible for all database, cache and media related actions
|
||||
// and serves as the lowest layer of the cake
|
||||
type Storage struct {
|
||||
db *gorm.DB
|
||||
cache *cache.Cache
|
||||
}
|
||||
|
||||
var ErrInvalidData = errors.New("invalid data")
|
||||
|
||||
// Build a new storage using sqlite as database backend
|
||||
func NewStorageSqlite(filePath string, cache *cache.Cache) (*Storage, error) {
|
||||
db, err := gorm.Open(sqlite.Open(filePath))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return storageFromEmptyDb(db, cache)
|
||||
}
|
||||
|
||||
func NewStoragePostgres(dbUrl string, cache *cache.Cache) (*Storage, error) {
|
||||
db, err := gorm.Open(postgres.Open(dbUrl))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return storageFromEmptyDb(db, cache)
|
||||
}
|
||||
|
||||
func storageFromEmptyDb(db *gorm.DB, cache *cache.Cache) (*Storage, error) {
|
||||
// AutoMigrate ensures the db is in a state where all the structs given here
|
||||
// have their own tables and relations setup. It also updates tables if necessary
|
||||
err := db.AutoMigrate(
|
||||
MediaMetadata{},
|
||||
Account{},
|
||||
RemoteServer{},
|
||||
Note{},
|
||||
Role{},
|
||||
PasskeySession{},
|
||||
InboundJob{},
|
||||
OutboundJob{},
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// And finally, build the actual storage struct
|
||||
return &Storage{
|
||||
db: db,
|
||||
cache: cache,
|
||||
}, nil
|
||||
}
|
483
storage/user.go
483
storage/user.go
|
@ -1,483 +0,0 @@
|
|||
package storage
|
||||
|
||||
import (
|
||||
"crypto/ed25519"
|
||||
"crypto/rand"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.mstar.dev/mstar/linstrom/activitypub"
|
||||
"git.mstar.dev/mstar/linstrom/shared"
|
||||
"github.com/go-webauthn/webauthn/webauthn"
|
||||
"github.com/google/uuid"
|
||||
"github.com/mstarongithub/passkey"
|
||||
"github.com/rs/zerolog/log"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// Database representation of a user account
|
||||
// This can be a bot, remote or not
|
||||
// If remote, this is used for caching the account
|
||||
type Account struct {
|
||||
ID string `gorm:"primarykey"` // ID is a uuid for this account
|
||||
// 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
|
||||
// When this account was last updated. Will also be used for refreshing remote accounts. Automatically set by gorm
|
||||
UpdatedAt time.Time
|
||||
// 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 // `gorm:"foreignKey:ServerId;references:ID"` // The server this user is from
|
||||
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
|
||||
CustomFields []uint `gorm:"serializer:json"` // IDs to the custom fields a user has
|
||||
Description string // The description of a user account
|
||||
Tags []string `gorm:"serializer:json"` // Hashtags
|
||||
IsBot bool // Whether to mark this account as a script controlled one
|
||||
Relations []uint `gorm:"serializer:json"` // List of ids of all relations this account has. Both follows and followers
|
||||
Icon string // ID of a media file used as icon
|
||||
Background *string // ID of a media file used as background image
|
||||
Banner *string // ID of a media file used as banner
|
||||
Indexable bool // Whether this account can be found by crawlers
|
||||
PublicKey []byte // The public key of the account
|
||||
// Whether this account restricts following
|
||||
// If true, the owner must approve of a follow request first
|
||||
RestrictedFollow bool
|
||||
// List of things the owner identifies as
|
||||
// Example [cat human robot] means that the owner probably identifies as
|
||||
// a cyborg-catgirl/boy/human or a cathuman shaped robot, refer to Gender for pronouns
|
||||
IdentifiesAs []Being `gorm:"serializer:json"`
|
||||
// List of pronouns the owner identifies with
|
||||
// An unordered list since the owner can freely set it
|
||||
// Examples: [she her], [it they its them] or, if you want to go fancy, [this is super serious]
|
||||
Gender []string `gorm:"serializer:json"`
|
||||
// The roles assocciated with an account. Values are the names of the roles
|
||||
Roles []string `gorm:"serializer:json"`
|
||||
|
||||
Location *string
|
||||
Birthday *time.Time
|
||||
|
||||
// --- And internal account stuff ---
|
||||
// Still public fields since they wouldn't be able to be stored in the db otherwise
|
||||
PrivateKey []byte // The private key of the account. Nil if remote user
|
||||
WebAuthnId []byte // The unique and random ID of this account used for passkey authentication
|
||||
// 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
|
||||
// TODO: Turn this into a map to give passkeys names.
|
||||
// Needed for supporting a decent passkey management interface.
|
||||
// Or check if webauthn.Credential has sufficiently easy to identify data
|
||||
// to use instead of a string mapping
|
||||
PasskeyCredentials []webauthn.Credential `gorm:"serializer:json"` // Webauthn credentials data
|
||||
// Has a RemoteAccountLinks included if remote user
|
||||
RemoteLinks *RemoteAccountLinks
|
||||
}
|
||||
|
||||
// Contains static and cached info about a remote account, mostly links
|
||||
type RemoteAccountLinks struct {
|
||||
// ---- Section: gorm
|
||||
// Sets this struct up as a value that an Account may have
|
||||
gorm.Model
|
||||
AccountID string
|
||||
|
||||
// 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
|
||||
InboxLink string
|
||||
OutboxLink *string
|
||||
FeaturedLink *string
|
||||
FeaturedTagsLink *string
|
||||
}
|
||||
|
||||
// Find an account in the db using a given full handle (@max@example.com)
|
||||
// Returns an account and nil if an account is found, otherwise nil and the error
|
||||
func (s *Storage) FindAccountByFullHandle(handle string) (*Account, error) {
|
||||
defer shared.Untrace(shared.Trace(&log.Logger))
|
||||
log.Debug().Str("account-handle", handle).Msg("Looking for account by handle")
|
||||
log.Debug().Str("account-handle", handle).Msg("Checking if there's a cache hit")
|
||||
|
||||
// Try and find the account in cache first
|
||||
cacheAccId, err := s.cacheHandleToAccUid(handle)
|
||||
if err == nil {
|
||||
log.Info().Str("account-handle", handle).Msg("Hit account handle in cache")
|
||||
// Then always load via id since unique key access should be faster than string matching
|
||||
return s.FindAccountById(*cacheAccId)
|
||||
} else {
|
||||
if !errors.Is(err, errCacheNotFound) {
|
||||
log.Error().Err(err).Str("account-handle", handle).Msg("Problem while checking cache for account")
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Failed to find in cache, go the slow route of hitting the db
|
||||
log.Debug().Str("account-handle", handle).Msg("Didn't hit account in cache, going to db")
|
||||
name, server, err := activitypub.SplitFullHandle(handle)
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Str("account-handle", handle).Msg("Failed to split up account handle")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
acc := Account{}
|
||||
res := s.db.Where("name = ?", name).Where("server = ?", server).First(&acc)
|
||||
if res.Error != nil {
|
||||
if errors.Is(res.Error, gorm.ErrRecordNotFound) {
|
||||
log.Info().Str("account-handle", handle).Msg("Account with handle not found")
|
||||
} else {
|
||||
log.Error().Err(err).Str("account-handle", handle).Msg("Failed to get account with handle")
|
||||
}
|
||||
return nil, res.Error
|
||||
}
|
||||
log.Info().Str("account-handle", handle).Msg("Found account, also inserting into cache")
|
||||
if err = s.cache.Set(cacheUserIdToAccPrefix+acc.ID, &acc); err != nil {
|
||||
log.Warn().
|
||||
Err(err).
|
||||
Str("account-handle", handle).
|
||||
Msg("Found account but failed to insert into cache")
|
||||
}
|
||||
if err = s.cache.Set(cacheUserHandleToIdPrefix+strings.TrimLeft(handle, "@"), acc.ID); err != nil {
|
||||
log.Warn().
|
||||
Err(err).
|
||||
Str("account-handle", handle).
|
||||
Msg("Failed to store handle to id in cache")
|
||||
}
|
||||
return &acc, nil
|
||||
}
|
||||
|
||||
// Find an account given a specific ID
|
||||
func (s *Storage) FindAccountById(id string) (*Account, error) {
|
||||
defer shared.Untrace(shared.Trace(&log.Logger))
|
||||
log.Debug().Str("account-id", id).Msg("Looking for account by id")
|
||||
log.Debug().Str("account-id", id).Msg("First trying to hit cache")
|
||||
acc, err := s.cacheAccIdToData(id)
|
||||
if err == nil {
|
||||
log.Info().Str("account-id", id).Msg("Found account in cache")
|
||||
return acc, nil
|
||||
} else if !errors.Is(err, errCacheNotFound) {
|
||||
log.Error().Err(err).Str("account-id", id).Msg("Error while looking for account in cache")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
log.Debug().Str("account-id", id).Msg("Didn't hit account in cache, checking db")
|
||||
acc = &Account{}
|
||||
res := s.db.Where(Account{ID: id}).First(acc)
|
||||
if res.Error != nil {
|
||||
if errors.Is(res.Error, gorm.ErrRecordNotFound) {
|
||||
log.Warn().Str("account-id", id).Msg("Account not found")
|
||||
return nil, ErrEntryNotFound
|
||||
} else {
|
||||
log.Error().Err(res.Error).Str("account-id", id).Msg("Failed to look for account")
|
||||
return nil, res.Error
|
||||
}
|
||||
}
|
||||
log.Info().Str("account-id", id).Msg("Found account in db, also adding to cache")
|
||||
if err = s.cache.Set(cacheUserIdToAccPrefix+id, acc); err != nil {
|
||||
log.Warn().Err(err).Str("account-id", id).Msg("Failed to add account to cache")
|
||||
}
|
||||
return acc, nil
|
||||
}
|
||||
|
||||
func (s *Storage) FindLocalAccountByUsername(username string) (*Account, error) {
|
||||
defer shared.Untrace(shared.Trace(&log.Logger))
|
||||
log.Debug().Str("account-username", username).Msg("Looking for local account")
|
||||
log.Debug().Str("account-username", username).Msg("Checking cache first")
|
||||
|
||||
// Try and find the account in cache first
|
||||
cacheAccId, err := s.cacheLocalUsernameToAccUid(username)
|
||||
if err == nil {
|
||||
log.Info().Str("account-username", username).Msg("Hit account handle in cache")
|
||||
// Then always load via id since unique key access should be faster than string matching
|
||||
return s.FindAccountById(*cacheAccId)
|
||||
} else {
|
||||
if err != errCacheNotFound {
|
||||
log.Error().Err(err).Str("account-username", username).Msg("Problem while checking cache for account")
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Failed to find in cache, go the slow route of hitting the db
|
||||
log.Debug().Str("account-username", username).Msg("Didn't hit account in cache, going to db")
|
||||
|
||||
acc := Account{}
|
||||
res := s.db.Where("username = ?", username).
|
||||
Where("server_id = ?", serverSelf.ID).
|
||||
First(&acc)
|
||||
if res.Error != nil {
|
||||
if errors.Is(res.Error, gorm.ErrRecordNotFound) {
|
||||
log.Info().
|
||||
Str("account-username", username).
|
||||
Msg("Local account with username not found")
|
||||
} else {
|
||||
log.Error().Err(err).Str("account-username", username).Msg("Failed to get local account with username")
|
||||
}
|
||||
return nil, ErrEntryNotFound
|
||||
}
|
||||
log.Info().Str("account-username", username).Msg("Found account, also inserting into cache")
|
||||
if err = s.cache.Set(cacheUserIdToAccPrefix+acc.ID, &acc); err != nil {
|
||||
log.Warn().
|
||||
Err(err).
|
||||
Str("account-username", username).
|
||||
Msg("Found account but failed to insert into cache")
|
||||
}
|
||||
if err = s.cache.Set(cacheLocalUsernameToIdPrefix+username, acc.ID); err != nil {
|
||||
log.Warn().
|
||||
Err(err).
|
||||
Str("account-username", username).
|
||||
Msg("Failed to store local username to id in cache")
|
||||
}
|
||||
return &acc, nil
|
||||
}
|
||||
|
||||
func (s *Storage) FindAccountByPasskeyId(pkeyId []byte) (*Account, error) {
|
||||
defer shared.Untrace(shared.Trace(&log.Logger))
|
||||
log.Debug().Bytes("account-passkey-id", pkeyId).Msg("Looking for account")
|
||||
log.Debug().Bytes("account-passkey-id", pkeyId).Msg("Checking cache first")
|
||||
|
||||
// Try and find the account in cache first
|
||||
cacheAccId, err := s.cachePkeyIdToAccId(pkeyId)
|
||||
if err == nil {
|
||||
log.Info().Bytes("account-passkey-id", pkeyId).Msg("Hit passkey id in cache")
|
||||
// Then always load via id since unique key access should be faster than string matching
|
||||
return s.FindAccountById(*cacheAccId)
|
||||
} else {
|
||||
if err != errCacheNotFound {
|
||||
log.Error().Err(err).Bytes("account-passkey-id", pkeyId).Msg("Problem while checking cache for account")
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Failed to find in cache, go the slow route of hitting the db
|
||||
log.Debug().Bytes("account-passkey-id", pkeyId).Msg("Didn't hit account in cache, going to db")
|
||||
|
||||
acc := Account{}
|
||||
res := s.db.Where("web_authn_id = ?", pkeyId).
|
||||
First(&acc)
|
||||
if res.Error != nil {
|
||||
if res.Error == gorm.ErrRecordNotFound {
|
||||
log.Info().
|
||||
Bytes("account-passkey-id", pkeyId).
|
||||
Msg("Local account with passkey id not found")
|
||||
return nil, ErrEntryNotFound
|
||||
} else {
|
||||
log.Error().Err(res.Error).Bytes("account-passkey-id", pkeyId).Msg("Failed to get local account with passkey id")
|
||||
return nil, res.Error
|
||||
}
|
||||
}
|
||||
log.Info().Bytes("account-passkey-id", pkeyId).Msg("Found account, also inserting into cache")
|
||||
// if err = s.cache.Set(cacheUserIdToAccPrefix+acc.ID, &acc); err != nil {
|
||||
// log.Warn().
|
||||
// Err(err).
|
||||
// Bytes("account-passkey-id", pkeyId).
|
||||
// Msg("Found account but failed to insert into cache")
|
||||
// }
|
||||
// if err = s.cache.Set(cachePasskeyIdToAccIdPrefix+string(pkeyId), acc.ID); err != nil {
|
||||
// log.Warn().
|
||||
// Err(err).
|
||||
// Bytes("account-passkey-id", pkeyId).
|
||||
// Msg("Failed to store local username to id in cache")
|
||||
// }
|
||||
return &acc, nil
|
||||
}
|
||||
|
||||
// Update a given account in storage and cache
|
||||
func (s *Storage) UpdateAccount(acc *Account) error {
|
||||
defer shared.Untrace(shared.Trace(&log.Logger))
|
||||
// If the account is nil or doesn't have an id, error out
|
||||
if acc == nil || acc.ID == "" {
|
||||
return ErrInvalidData
|
||||
}
|
||||
res := s.db.Save(acc)
|
||||
if res.Error != nil {
|
||||
return res.Error
|
||||
}
|
||||
if err := s.cache.Set(cacheUserIdToAccPrefix+acc.ID, acc); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Create a new empty account for future use
|
||||
func (s *Storage) NewEmptyAccount() (*Account, error) {
|
||||
defer shared.Untrace(shared.Trace(&log.Logger))
|
||||
log.Debug().Msg("Creating new empty account")
|
||||
acc := Account{}
|
||||
// Generate the 64 bit id for passkey and webauthn stuff
|
||||
log.Debug().Msg("Creating webauthn id for new account")
|
||||
data := make([]byte, 64)
|
||||
c, err := rand.Read(data)
|
||||
for err != nil || c != len(data) || c < 64 {
|
||||
data = make([]byte, 64)
|
||||
c, err = rand.Read(data)
|
||||
}
|
||||
log.Debug().Msg("Random webauthn id for new account created")
|
||||
acc.ID = uuid.NewString()
|
||||
|
||||
accountRole, err := s.NewEmptyRole(acc.ID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to generate account role for new account: %w", err)
|
||||
}
|
||||
accountRole.IsUserRole = true
|
||||
if err = s.UpdateRole(accountRole); err != nil {
|
||||
return nil, fmt.Errorf("failed to generate account role for new account: %w", err)
|
||||
}
|
||||
|
||||
acc.WebAuthnId = data
|
||||
acc.Relations = []uint{}
|
||||
acc.Tags = []string{}
|
||||
acc.Gender = []string{}
|
||||
acc.CustomFields = []uint{}
|
||||
acc.IdentifiesAs = []Being{}
|
||||
acc.PasskeyCredentials = []webauthn.Credential{}
|
||||
acc.Roles = []string{DefaultUserRole.Name, accountRole.Name}
|
||||
acc.Icon = "placeholder"
|
||||
log.Debug().Any("account", &acc).Msg("Saving new account in db")
|
||||
res := s.db.Save(&acc)
|
||||
if res.Error != nil {
|
||||
log.Error().Err(res.Error).Msg("Failed to safe new account")
|
||||
return nil, res.Error
|
||||
}
|
||||
log.Info().Str("account-id", acc.ID).Msg("Created new account")
|
||||
return &acc, nil
|
||||
}
|
||||
|
||||
// Create a new local account using the given handle
|
||||
// The handle in this case is only the part before the domain (example: @bob@example.com would have a handle of bob)
|
||||
// It also sets up a bunch of values that tend to be obvious for local accounts
|
||||
func (s *Storage) NewLocalAccount(handle string) (*Account, error) {
|
||||
defer shared.Untrace(shared.Trace(&log.Logger))
|
||||
log.Trace().Caller().Send()
|
||||
log.Debug().Str("account-handle", handle).Msg("Creating new local account")
|
||||
acc, err := s.NewEmptyAccount()
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to create empty account for use")
|
||||
return nil, err
|
||||
}
|
||||
acc.Username = handle
|
||||
// acc.Server = serverSelf
|
||||
acc.ServerId = serverSelf.ID
|
||||
acc.DisplayName = handle
|
||||
|
||||
publicKey, privateKey, err := ed25519.GenerateKey(nil)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to generate key pair for new local account")
|
||||
return nil, err
|
||||
}
|
||||
acc.PrivateKey = privateKey
|
||||
acc.PublicKey = publicKey
|
||||
|
||||
log.Debug().
|
||||
Str("account-handle", handle).
|
||||
Str("account-id", acc.ID).
|
||||
Msg("Saving new local account")
|
||||
res := s.db.Save(acc)
|
||||
if res.Error != nil {
|
||||
log.Error().Err(res.Error).Any("account-full", acc).Msg("Failed to save local account")
|
||||
return nil, res.Error
|
||||
}
|
||||
log.Info().
|
||||
Str("account-handle", handle).
|
||||
Str("account-id", acc.ID).
|
||||
Msg("Created new local account")
|
||||
return acc, nil
|
||||
}
|
||||
|
||||
func (s *Storage) DeleteAccount(accId string) error {
|
||||
defer shared.Untrace(shared.Trace(&log.Logger))
|
||||
return s.db.Delete(&Account{ID: accId}).Error
|
||||
}
|
||||
|
||||
// ---- Section WebAuthn.User
|
||||
// Implements the webauthn.User interface for interaction with passkeys
|
||||
|
||||
func (a *Account) WebAuthnID() []byte {
|
||||
defer shared.Untrace(shared.Trace(&log.Logger))
|
||||
return a.WebAuthnId
|
||||
}
|
||||
|
||||
func (u *Account) WebAuthnName() string {
|
||||
defer shared.Untrace(shared.Trace(&log.Logger))
|
||||
return u.Username
|
||||
}
|
||||
|
||||
func (u *Account) WebAuthnDisplayName() string {
|
||||
defer shared.Untrace(shared.Trace(&log.Logger))
|
||||
return u.DisplayName
|
||||
}
|
||||
|
||||
func (u *Account) WebAuthnCredentials() []webauthn.Credential {
|
||||
defer shared.Untrace(shared.Trace(&log.Logger))
|
||||
return u.PasskeyCredentials
|
||||
}
|
||||
|
||||
func (u *Account) WebAuthnIcon() string {
|
||||
defer shared.Untrace(shared.Trace(&log.Logger))
|
||||
return ""
|
||||
}
|
||||
|
||||
// ---- Section passkey.User
|
||||
// Implements the passkey.User interface
|
||||
|
||||
func (u *Account) PutCredential(new webauthn.Credential) {
|
||||
defer shared.Untrace(shared.Trace(&log.Logger))
|
||||
u.PasskeyCredentials = append(u.PasskeyCredentials, new)
|
||||
}
|
||||
|
||||
// Section passkey.UserStore
|
||||
// Implements the passkey.UserStore interface
|
||||
|
||||
func (s *Storage) GetOrCreateUser(userID string) passkey.User {
|
||||
defer shared.Untrace(shared.Trace(&log.Logger))
|
||||
log.Debug().
|
||||
Str("account-handle", userID).
|
||||
Msg("Looking for or creating account for passkey stuff")
|
||||
acc := &Account{}
|
||||
res := s.db.Where(Account{Username: userID, ServerId: serverSelf.ID}).
|
||||
First(acc)
|
||||
if errors.Is(res.Error, gorm.ErrRecordNotFound) {
|
||||
log.Debug().Str("account-handle", userID)
|
||||
var err error
|
||||
acc, err = s.NewLocalAccount(userID)
|
||||
if err != nil {
|
||||
log.Error().
|
||||
Err(err).
|
||||
Str("account-handle", userID).
|
||||
Msg("Failed to create new account for webauthn request")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return acc
|
||||
}
|
||||
|
||||
func (s *Storage) GetUserByWebAuthnId(id []byte) passkey.User {
|
||||
defer shared.Untrace(shared.Trace(&log.Logger))
|
||||
log.Debug().Bytes("webauthn-id", id).Msg("Looking for account with webauthn id")
|
||||
acc := Account{}
|
||||
res := s.db.Where(Account{WebAuthnId: id}).First(&acc)
|
||||
if res.Error != nil {
|
||||
log.Error().
|
||||
Err(res.Error).
|
||||
Bytes("webauthn-id", id).
|
||||
Msg("Failed to find user with webauthn ID")
|
||||
return nil
|
||||
}
|
||||
log.Info().Msg("Found account with given webauthn id")
|
||||
return &acc
|
||||
}
|
||||
|
||||
func (s *Storage) SaveUser(rawUser passkey.User) {
|
||||
defer shared.Untrace(shared.Trace(&log.Logger))
|
||||
user, ok := rawUser.(*Account)
|
||||
if !ok {
|
||||
log.Error().Any("raw-user", rawUser).Msg("Failed to cast raw user to db account")
|
||||
}
|
||||
s.db.Save(user)
|
||||
}
|
|
@ -1,21 +0,0 @@
|
|||
package storage
|
||||
|
||||
import "git.mstar.dev/mstar/goutils/sliceutils"
|
||||
|
||||
// What kind of being a user identifies as
|
||||
type Being string
|
||||
|
||||
const (
|
||||
BEING_HUMAN = Being("human")
|
||||
BEING_CAT = Being("cat")
|
||||
BEING_FOX = Being("fox")
|
||||
BEING_DOG = Being("dog")
|
||||
BEING_ROBOT = Being("robot")
|
||||
BEING_DOLL = Being("doll")
|
||||
)
|
||||
|
||||
var allBeings = []Being{BEING_HUMAN, BEING_CAT, BEING_FOX, BEING_DOG, BEING_ROBOT, BEING_DOLL}
|
||||
|
||||
func IsValidBeing(toCheck string) bool {
|
||||
return sliceutils.Contains(allBeings, Being(toCheck))
|
||||
}
|
|
@ -1,71 +0,0 @@
|
|||
package storage
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"git.mstar.dev/mstar/linstrom/shared"
|
||||
)
|
||||
|
||||
// Describes a custom attribute field for accounts
|
||||
type UserInfoField struct {
|
||||
gorm.Model // Can actually just embed this as is here as those are not something directly exposed :3
|
||||
Name string
|
||||
Value string
|
||||
LastUrlCheckDate *time.Time // Used if the value is an url to somewhere. Empty if value is not an url
|
||||
// If the value is an url, this attribute indicates whether Linstrom was able to verify ownership
|
||||
// of the provided url via the common method of
|
||||
// "Does the target url contain a rel='me' link to the owner's account"
|
||||
Confirmed bool
|
||||
BelongsTo string // Id of account this info field belongs to
|
||||
}
|
||||
|
||||
// TODO: Add functions to store, load, update and delete these
|
||||
|
||||
func (s *Storage) FindUserFieldById(id uint) (*UserInfoField, error) {
|
||||
defer shared.Untrace(shared.Trace(&log.Logger))
|
||||
entry := UserInfoField{}
|
||||
err := s.db.First(&entry, id).Error
|
||||
switch err {
|
||||
case nil:
|
||||
return &entry, nil
|
||||
case gorm.ErrRecordNotFound:
|
||||
return nil, ErrEntryNotFound
|
||||
default:
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Storage) FindMultipleUserFieldsById(ids []uint) ([]UserInfoField, error) {
|
||||
defer shared.Untrace(shared.Trace(&log.Logger))
|
||||
entries := []UserInfoField{}
|
||||
err := s.db.Where(ids).Find(&entries).Error
|
||||
switch err {
|
||||
case gorm.ErrRecordNotFound:
|
||||
return nil, ErrEntryNotFound
|
||||
case nil:
|
||||
return entries, nil
|
||||
default:
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Storage) AddNewUserField(name, value, belongsToId string) (*UserInfoField, error) {
|
||||
// TODO: Implement me
|
||||
panic("Not implemented")
|
||||
}
|
||||
|
||||
func (s *Storage) DeleteUserField(id uint) error {
|
||||
defer shared.Untrace(shared.Trace(&log.Logger))
|
||||
return s.db.Delete(UserInfoField{Model: gorm.Model{ID: id}}).Error
|
||||
}
|
||||
|
||||
func (s *Storage) DeleteAllUserFieldsForAccountId(id string) error {
|
||||
defer shared.Untrace(shared.Trace(&log.Logger))
|
||||
return s.db.Model(&UserInfoField{}).
|
||||
Where(&UserInfoField{BelongsTo: id}).
|
||||
Delete(&UserInfoField{}).
|
||||
Error
|
||||
}
|
Loading…
Reference in a new issue