Moew weork done
This commit is contained in:
parent
814316ab1e
commit
07d98d1ef5
10 changed files with 147 additions and 16 deletions
1
outgoingEventQueue/queue.go
Normal file
1
outgoingEventQueue/queue.go
Normal file
|
@ -0,0 +1 @@
|
||||||
|
package outgoingeventqueue
|
|
@ -7,15 +7,21 @@ import (
|
||||||
"github.com/redis/go-redis/v9"
|
"github.com/redis/go-redis/v9"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// various prefixes for accessing items in the cache (since it's a simple key-value store)
|
||||||
const (
|
const (
|
||||||
cacheUserHandleToIdPrefix = "acc-name-to-id:"
|
cacheUserHandleToIdPrefix = "acc-name-to-id:"
|
||||||
cacheUserIdToAccPrefix = "acc-id-to-data:"
|
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")
|
var errCacheNotFound = errors.New("not found in cache")
|
||||||
|
|
||||||
// Find an account id in cache using a given user handle
|
// Find an account id in cache using a given user handle
|
||||||
func (s *Storage) cacheHandleToAccUid(handle string) (*string, error) {
|
// 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) {
|
||||||
// Where to put the data (in case it's found)
|
// Where to put the data (in case it's found)
|
||||||
var target string
|
var target string
|
||||||
found, err := s.cache.Get(cacheUserHandleToIdPrefix+strings.TrimLeft(handle, "@"), &target)
|
found, err := s.cache.Get(cacheUserHandleToIdPrefix+strings.TrimLeft(handle, "@"), &target)
|
||||||
|
@ -33,7 +39,10 @@ func (s *Storage) cacheHandleToAccUid(handle string) (*string, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find an account's data in cache using a given account id
|
// Find an account's data in cache using a given account id
|
||||||
func (s *Storage) cacheAccIdToData(id string) (*Account, error) {
|
// 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) {
|
||||||
var target Account
|
var target Account
|
||||||
found, err := s.cache.Get(cacheUserIdToAccPrefix+id, &target)
|
found, err := s.cache.Get(cacheUserIdToAccPrefix+id, &target)
|
||||||
if !found {
|
if !found {
|
||||||
|
@ -45,3 +54,20 @@ func (s *Storage) cacheAccIdToData(id string) (*Account, error) {
|
||||||
}
|
}
|
||||||
return &target, nil
|
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) {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
|
@ -1,7 +1,11 @@
|
||||||
package storage
|
package storage
|
||||||
|
|
||||||
type NotImplementedError struct{}
|
import "errors"
|
||||||
|
|
||||||
func (n NotImplementedError) Error() string {
|
type ErrNotImplemented struct{}
|
||||||
|
|
||||||
|
func (n ErrNotImplemented) Error() string {
|
||||||
return "Not implemented yet"
|
return "Not implemented yet"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var ErrEntryNotFound = errors.New("entry not found")
|
||||||
|
|
|
@ -6,6 +6,10 @@ import (
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// MediaFile represents a file containing some media
|
||||||
|
// This media may be stored locally via a file system or S3 bucket
|
||||||
|
// or remote on a different server
|
||||||
|
// Additionally, it contains some useful data for more easily working with it
|
||||||
type MediaFile struct {
|
type MediaFile struct {
|
||||||
ID string `gorm:"primarykey"` // The unique ID of this media file
|
ID string `gorm:"primarykey"` // The unique ID of this media file
|
||||||
CreatedAt time.Time // When this entry was created
|
CreatedAt time.Time // When this entry was created
|
||||||
|
@ -15,13 +19,18 @@ type MediaFile struct {
|
||||||
// If not null, this entry is marked as deleted
|
// If not null, this entry is marked as deleted
|
||||||
DeletedAt gorm.DeletedAt `gorm:"index"`
|
DeletedAt gorm.DeletedAt `gorm:"index"`
|
||||||
Remote bool // whether the attachment is a remote one
|
Remote bool // whether the attachment is a remote one
|
||||||
Link string // url if remote attachment, identifier if local
|
// Always an url, either an absolute path to a local file or an url to a remote file
|
||||||
Type string // What media type this is, eg image/png
|
Link string
|
||||||
|
Type string // What media type this is following mime types, eg image/png
|
||||||
// Whether this media has been cached locally
|
// Whether this media has been cached locally
|
||||||
// Only really used for user and server icons, not attachments
|
// Used for user and server icons as well as emotes, not attachments
|
||||||
// If true, Link will be read as file path. url otherwise
|
// If true, Link will be read as file path. url otherwise
|
||||||
// Reason: Attachments would take way to much space considering that they are often only loaded a few times at most
|
// Reason: Attachments would take way to much space considering that they are often only loaded a few times at most
|
||||||
// And caching a file for those few times would be a waste of storage
|
// And caching a file for those few times would be a waste of storage
|
||||||
// Caching user and server icons locally however should reduce burden on remote servers by quite a bit though
|
// Caching user and server icons locally however should reduce burden on remote servers by quite a bit though
|
||||||
|
// TODO: Decide later during usage if attachment caching would be a good idea
|
||||||
LocallyCached bool
|
LocallyCached bool
|
||||||
|
// Descriptive name for a media file
|
||||||
|
// Emote name for example or servername.filetype for a server's icon
|
||||||
|
Name string
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,10 @@ import (
|
||||||
"errors"
|
"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 NoteTarget
|
||||||
|
|
||||||
// What feed a note is targeting (public, home, followers or dm)
|
// What feed a note is targeting (public, home, followers or dm)
|
||||||
type NoteTarget uint8
|
type NoteTarget uint8
|
||||||
|
|
||||||
|
@ -14,7 +18,7 @@ const (
|
||||||
// The note is intended only for the home screen
|
// The note is intended only for the home screen
|
||||||
// not really any idea what the difference is compared to public
|
// 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
|
// 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 = NoteTarget(1 << iota)
|
NOTE_TARGET_HOME = NoteTarget(1 << iota)
|
||||||
// The note is intended only for followers
|
// The note is intended only for followers
|
||||||
NOTE_TARGET_FOLLOWERS
|
NOTE_TARGET_FOLLOWERS
|
||||||
// The note is intended only for a DM to one or more targets
|
// The note is intended only for a DM to one or more targets
|
||||||
|
|
|
@ -1,11 +1,19 @@
|
||||||
package storage
|
package storage
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// 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 {
|
type Note struct {
|
||||||
ID string `gorm:"primarykey"` // Make ID a string (uuid) for other implementations
|
ID string `gorm:"primarykey"` // Make ID a string (uuid) for other implementations
|
||||||
CreatedAt time.Time // When this entry was created
|
CreatedAt time.Time // When this entry was created
|
||||||
|
@ -31,23 +39,85 @@ type Note struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Storage) FindNoteById(id string) (*Note, error) {
|
func (s *Storage) FindNoteById(id string) (*Note, error) {
|
||||||
// TODO: Implement me
|
note := &Note{}
|
||||||
panic("not implemented")
|
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) {
|
func (s *Storage) FindNotesByFuzzyContent(fuzzyContent string) ([]Note, error) {
|
||||||
// TODO: Implement me
|
notes := []Note{}
|
||||||
panic("not implemented")
|
// 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) {
|
func (s *Storage) FindNotesByAuthorHandle(handle string) ([]Note, error) {
|
||||||
// TODO: Implement me
|
acc, err := s.FindAccountByFullHandle(handle)
|
||||||
panic("not implemented")
|
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) {
|
func (s *Storage) FindNotesByAuthorId(id string) ([]Note, error) {
|
||||||
// TODO: Implement me
|
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 {
|
||||||
|
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) CreateNote() (*Note, error) {
|
||||||
|
// TODO: Think of good arguments and implement me
|
||||||
panic("not implemented")
|
panic("not implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update, create, delete
|
func (s *Storage) DeleteNote(id string) {
|
||||||
|
s.cache.Delete(cacheNoteIdToNotePrefix + id)
|
||||||
|
s.db.Delete(Note{ID: id})
|
||||||
|
}
|
||||||
|
|
|
@ -6,6 +6,9 @@ import (
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// 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 {
|
type PasskeySession struct {
|
||||||
ID string `gorm:"primarykey"`
|
ID string `gorm:"primarykey"`
|
||||||
Data webauthn.SessionData `gorm:"serializer:json"`
|
Data webauthn.SessionData `gorm:"serializer:json"`
|
||||||
|
@ -13,12 +16,15 @@ type PasskeySession struct {
|
||||||
|
|
||||||
// ---- Section SessionStore
|
// ---- Section SessionStore
|
||||||
|
|
||||||
|
// Generate some id for a new session. Just returns a new uuid
|
||||||
func (s *Storage) GenSessionID() (string, error) {
|
func (s *Storage) GenSessionID() (string, error) {
|
||||||
x := uuid.NewString()
|
x := uuid.NewString()
|
||||||
log.Debug().Str("session-id", x).Msg("Generated new passkey session id")
|
log.Debug().Str("session-id", x).Msg("Generated new passkey session id")
|
||||||
return x, nil
|
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) {
|
func (s *Storage) GetSession(sessionId string) (*webauthn.SessionData, bool) {
|
||||||
log.Debug().Str("id", sessionId).Msg("Looking for passkey session")
|
log.Debug().Str("id", sessionId).Msg("Looking for passkey session")
|
||||||
session := PasskeySession{}
|
session := PasskeySession{}
|
||||||
|
@ -30,6 +36,7 @@ func (s *Storage) GetSession(sessionId string) (*webauthn.SessionData, bool) {
|
||||||
return &session.Data, true
|
return &session.Data, true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Save (or update) a session with the new data
|
||||||
func (s *Storage) SaveSession(token string, data *webauthn.SessionData) {
|
func (s *Storage) SaveSession(token string, data *webauthn.SessionData) {
|
||||||
log.Debug().Str("id", token).Any("webauthn-data", data).Msg("Saving passkey session")
|
log.Debug().Str("id", token).Any("webauthn-data", data).Msg("Saving passkey session")
|
||||||
session := PasskeySession{
|
session := PasskeySession{
|
||||||
|
@ -39,6 +46,8 @@ func (s *Storage) SaveSession(token string, data *webauthn.SessionData) {
|
||||||
s.db.Save(&session)
|
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) {
|
func (s *Storage) DeleteSession(token string) {
|
||||||
log.Debug().Str("id", token).Msg("Deleting passkey session (if one exists)")
|
log.Debug().Str("id", token).Msg("Deleting passkey session (if one exists)")
|
||||||
s.db.Delete(&PasskeySession{ID: token})
|
s.db.Delete(&PasskeySession{ID: token})
|
||||||
|
|
|
@ -5,6 +5,9 @@ import (
|
||||||
"errors"
|
"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
|
// What software a server is running
|
||||||
// Mostly important for rendering
|
// Mostly important for rendering
|
||||||
type RemoteServerType string
|
type RemoteServerType string
|
||||||
|
|
|
@ -191,6 +191,7 @@ func (s *Storage) UpdateAccount(acc *Account) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Create a new empty account for future use
|
||||||
func (s *Storage) NewEmptyAccount() (*Account, error) {
|
func (s *Storage) NewEmptyAccount() (*Account, error) {
|
||||||
log.Trace().Caller().Send()
|
log.Trace().Caller().Send()
|
||||||
log.Debug().Msg("Creating new empty account")
|
log.Debug().Msg("Creating new empty account")
|
||||||
|
@ -219,6 +220,9 @@ func (s *Storage) NewEmptyAccount() (*Account, error) {
|
||||||
return &acc, nil
|
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) {
|
func (s *Storage) NewLocalAccount(handle string) (*Account, error) {
|
||||||
log.Trace().Caller().Send()
|
log.Trace().Caller().Send()
|
||||||
log.Debug().Str("account-handle", handle).Msg("Creating new local account")
|
log.Debug().Str("account-handle", handle).Msg("Creating new local account")
|
||||||
|
|
|
@ -6,6 +6,7 @@ import (
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Describes a custom attribute field for accounts
|
||||||
type UserInfoField struct {
|
type UserInfoField struct {
|
||||||
gorm.Model // Can actually just embed this as is here as those are not something directly exposed :3
|
gorm.Model // Can actually just embed this as is here as those are not something directly exposed :3
|
||||||
Name string
|
Name string
|
||||||
|
|
Loading…
Reference in a new issue