Moew weork done

This commit is contained in:
Melody Becker 2024-09-12 16:57:53 +02:00
parent 814316ab1e
commit 07d98d1ef5
10 changed files with 147 additions and 16 deletions

View file

@ -0,0 +1 @@
package outgoingeventqueue

View file

@ -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
}

View file

@ -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")

View file

@ -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
}

View file

@ -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

View file

@ -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})
}

View file

@ -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})

View file

@ -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

View file

@ -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")

View file

@ -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