From 07d98d1ef5a4f4ef8fb0d0197baafc37153c113e Mon Sep 17 00:00:00 2001 From: mStar Date: Thu, 12 Sep 2024 16:57:53 +0200 Subject: [PATCH] Moew weork done --- outgoingEventQueue/queue.go | 1 + storage/cache.go | 30 ++++++++++++- storage/errors.go | 8 +++- storage/mediaFile.go | 15 +++++-- storage/noteTargets.go | 6 ++- storage/notes.go | 86 +++++++++++++++++++++++++++++++++---- storage/passkeySessions.go | 9 ++++ storage/serverTypes.go | 3 ++ storage/user.go | 4 ++ storage/userInfoFields.go | 1 + 10 files changed, 147 insertions(+), 16 deletions(-) create mode 100644 outgoingEventQueue/queue.go diff --git a/outgoingEventQueue/queue.go b/outgoingEventQueue/queue.go new file mode 100644 index 0000000..12e524f --- /dev/null +++ b/outgoingEventQueue/queue.go @@ -0,0 +1 @@ +package outgoingeventqueue diff --git a/storage/cache.go b/storage/cache.go index d1d5562..789d7fc 100644 --- a/storage/cache.go +++ b/storage/cache.go @@ -7,15 +7,21 @@ import ( "github.com/redis/go-redis/v9" ) +// various prefixes for accessing items in the cache (since it's a simple key-value store) const ( cacheUserHandleToIdPrefix = "acc-name-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 -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) var target string 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 -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 found, err := s.cache.Get(cacheUserIdToAccPrefix+id, &target) if !found { @@ -45,3 +54,20 @@ func (s *Storage) cacheAccIdToData(id string) (*Account, error) { } 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 +} diff --git a/storage/errors.go b/storage/errors.go index a9bc1e1..ac1ee99 100644 --- a/storage/errors.go +++ b/storage/errors.go @@ -1,7 +1,11 @@ package storage -type NotImplementedError struct{} +import "errors" -func (n NotImplementedError) Error() string { +type ErrNotImplemented struct{} + +func (n ErrNotImplemented) Error() string { return "Not implemented yet" } + +var ErrEntryNotFound = errors.New("entry not found") diff --git a/storage/mediaFile.go b/storage/mediaFile.go index 7ae3cae..c3a55c3 100644 --- a/storage/mediaFile.go +++ b/storage/mediaFile.go @@ -6,6 +6,10 @@ import ( "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 { ID string `gorm:"primarykey"` // The unique ID of this media file CreatedAt time.Time // When this entry was created @@ -15,13 +19,18 @@ type MediaFile struct { // If not null, this entry is marked as deleted DeletedAt gorm.DeletedAt `gorm:"index"` Remote bool // whether the attachment is a remote one - Link string // url if remote attachment, identifier if local - Type string // What media type this is, eg image/png + // Always an url, either an absolute path to a local file or an url to a remote file + Link string + Type string // What media type this is following mime types, eg image/png // 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 // 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 // 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 + // Descriptive name for a media file + // Emote name for example or servername.filetype for a server's icon + Name string } diff --git a/storage/noteTargets.go b/storage/noteTargets.go index 50b8abf..5fcdc82 100644 --- a/storage/noteTargets.go +++ b/storage/noteTargets.go @@ -5,6 +5,10 @@ import ( "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) type NoteTarget uint8 @@ -14,7 +18,7 @@ const ( // 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 = NoteTarget(1 << iota) + NOTE_TARGET_HOME = NoteTarget(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 diff --git a/storage/notes.go b/storage/notes.go index 7d4c3b2..3809429 100644 --- a/storage/notes.go +++ b/storage/notes.go @@ -1,11 +1,19 @@ package storage import ( + "fmt" "time" + "github.com/rs/zerolog/log" "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 { ID string `gorm:"primarykey"` // Make ID a string (uuid) for other implementations CreatedAt time.Time // When this entry was created @@ -31,23 +39,85 @@ type Note struct { } func (s *Storage) FindNoteById(id string) (*Note, error) { - // TODO: Implement me - panic("not implemented") + 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) { - // TODO: Implement me - panic("not implemented") + 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) { - // TODO: Implement me - panic("not implemented") + 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) { - // 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") } -// Update, create, delete +func (s *Storage) DeleteNote(id string) { + s.cache.Delete(cacheNoteIdToNotePrefix + id) + s.db.Delete(Note{ID: id}) +} diff --git a/storage/passkeySessions.go b/storage/passkeySessions.go index aaba918..71dfb6a 100644 --- a/storage/passkeySessions.go +++ b/storage/passkeySessions.go @@ -6,6 +6,9 @@ import ( "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 { ID string `gorm:"primarykey"` Data webauthn.SessionData `gorm:"serializer:json"` @@ -13,12 +16,15 @@ type PasskeySession struct { // ---- Section SessionStore +// Generate some id for a new session. Just returns a new uuid func (s *Storage) GenSessionID() (string, error) { 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) { log.Debug().Str("id", sessionId).Msg("Looking for passkey session") session := PasskeySession{} @@ -30,6 +36,7 @@ func (s *Storage) GetSession(sessionId string) (*webauthn.SessionData, bool) { return &session.Data, true } +// Save (or update) a session with the new data func (s *Storage) SaveSession(token string, data *webauthn.SessionData) { log.Debug().Str("id", token).Any("webauthn-data", data).Msg("Saving passkey session") session := PasskeySession{ @@ -39,6 +46,8 @@ func (s *Storage) SaveSession(token string, data *webauthn.SessionData) { 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) { log.Debug().Str("id", token).Msg("Deleting passkey session (if one exists)") s.db.Delete(&PasskeySession{ID: token}) diff --git a/storage/serverTypes.go b/storage/serverTypes.go index 111dd85..f69166c 100644 --- a/storage/serverTypes.go +++ b/storage/serverTypes.go @@ -5,6 +5,9 @@ import ( "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 diff --git a/storage/user.go b/storage/user.go index 43cd979..8ec499c 100644 --- a/storage/user.go +++ b/storage/user.go @@ -191,6 +191,7 @@ func (s *Storage) UpdateAccount(acc *Account) error { return nil } +// Create a new empty account for future use func (s *Storage) NewEmptyAccount() (*Account, error) { log.Trace().Caller().Send() log.Debug().Msg("Creating new empty account") @@ -219,6 +220,9 @@ func (s *Storage) NewEmptyAccount() (*Account, error) { 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) { log.Trace().Caller().Send() log.Debug().Str("account-handle", handle).Msg("Creating new local account") diff --git a/storage/userInfoFields.go b/storage/userInfoFields.go index a3dc5ea..fa2915f 100644 --- a/storage/userInfoFields.go +++ b/storage/userInfoFields.go @@ -6,6 +6,7 @@ import ( "gorm.io/gorm" ) +// 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