diff --git a/server/apiLinstrom.go b/server/apiLinstrom.go index b17d36a..6355153 100644 --- a/server/apiLinstrom.go +++ b/server/apiLinstrom.go @@ -31,6 +31,14 @@ func setupLinstromApiV1Router() http.Handler { // Pinning router.HandleFunc("POST /note/{noteId}/pin", linstromPinNote) router.HandleFunc("DELETE /note/{noteId}/pin", linstromUnpinNote) + // Reports + router.HandleFunc("POST /note/{noteId}/report", linstromReportNote) + router.HandleFunc("DELETE /note/{noteId}/report", linstromRetractReportNote) + // Admin + router.HandleFunc("POST /note/{noteId}/admin/cw", linstromForceCWNote) + + // Event streams + router.HandleFunc("/streams", linstromEventStream) return router } diff --git a/server/apiLinstromNotes.go b/server/apiLinstromNotes.go index d1495e7..7864bd7 100644 --- a/server/apiLinstromNotes.go +++ b/server/apiLinstromNotes.go @@ -1,9 +1,15 @@ package server -import "net/http" +import ( + "net/http" +) // Notes -func linstromGetNote(w http.ResponseWriter, r *http.Request) {} +func linstromGetNote(w http.ResponseWriter, r *http.Request) { + store := StorageFromRequest(r) + noteId := NoteIdFromRequest(r) + note, err := store.FindNoteById(noteId) +} func linstromUpdateNote(w http.ResponseWriter, r *http.Request) {} func linstromNewNote(w http.ResponseWriter, r *http.Request) {} func linstromDeleteNote(w http.ResponseWriter, r *http.Request) {} @@ -25,5 +31,15 @@ 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) {} diff --git a/server/apiLinstromStreams.go b/server/apiLinstromStreams.go index 5fc3b8b..b5dc306 100644 --- a/server/apiLinstromStreams.go +++ b/server/apiLinstromStreams.go @@ -1,5 +1,24 @@ 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) +} diff --git a/server/apiLinstromTypes.go b/server/apiLinstromTypes.go new file mode 100644 index 0000000..3e79abc --- /dev/null +++ b/server/apiLinstromTypes.go @@ -0,0 +1,81 @@ +package server + +// Contains types used by the Linstrom API. Types comply with the jsonapi spec + +import "time" + +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"` +} + +type linstromOriginServer struct { + Id int `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", "" + 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,medias"` + 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"` + Banner *linstromMediaMetadata `jsonapi:"relation,banner"` + FollowerIds []string `jsonapi:"attr,follows_ids"` + FollowingIds []string `jsonapi:"attr,following_ids"` + Indexable bool `jsonapi:"attr,indexable"` + RestrictedFollow bool `jsonapi:"attr,restricted_follow"` + IdentifiesAs []string `jsonapi:"attr,identifies_as"` + Pronouns []string `jsonapi:"attr,pronouns"` +} + +type linstromCustomAccountField struct { + Id uint + 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"` +} diff --git a/server/contextUtils.go b/server/contextUtils.go deleted file mode 100644 index f815c6f..0000000 --- a/server/contextUtils.go +++ /dev/null @@ -1,22 +0,0 @@ -package server - -import ( - "net/http" - - "gitlab.com/mstarongitlab/goutils/other" - "gitlab.com/mstarongitlab/linstrom/storage" -) - -func StorageFromRequest(w http.ResponseWriter, r *http.Request) *storage.Storage { - store, ok := r.Context().Value(ContextKeyStorage).(*storage.Storage) - if !ok { - other.HttpErr( - w, - HttpErrIdMissingContextValue, - "Missing storage reference", - http.StatusInternalServerError, - ) - return nil - } - return store -} diff --git a/server/utils.go b/server/utils.go index 161debc..88006aa 100644 --- a/server/utils.go +++ b/server/utils.go @@ -18,16 +18,15 @@ func placeholderEndpoint(w http.ResponseWriter, r *http.Request) { ) } -func getStorageFromRequest(w http.ResponseWriter, r *http.Request) *storage.Storage { +func StorageFromRequest(r *http.Request) *storage.Storage { store, ok := r.Context().Value(ContextKeyStorage).(*storage.Storage) if !ok { - other.HttpErr( - w, - HttpErrIdMissingContextValue, - "Missing storage in context", - http.StatusInternalServerError, - ) + hlog.FromRequest(r).Fatal().Msg("Failed to get storage reference from context") return nil } return store } + +func NoteIdFromRequest(r *http.Request) string { + return r.PathValue("noteId") +} diff --git a/storage/mediaFile.go b/storage/mediaFile.go index b2667f2..ddfb46a 100644 --- a/storage/mediaFile.go +++ b/storage/mediaFile.go @@ -24,6 +24,10 @@ type MediaMetadata struct { // Descriptive name for a media file // Emote name for example or servername.filetype for a server's icon 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(location, mediaType, name string) (*MediaMetadata, error) { diff --git a/storage/mediaProvider/preprocessor.go b/storage/mediaProvider/preprocessor.go index 5d3be8b..7c92cf4 100644 --- a/storage/mediaProvider/preprocessor.go +++ b/storage/mediaProvider/preprocessor.go @@ -19,6 +19,7 @@ import ( 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 @@ -51,7 +52,11 @@ func compressVideo(dataIn []byte, subType string) (dataOut []byte, err error) { panic("Implement me") } -func compressImage(dataIn []byte, subType string, maxSizeX, maxSizeY uint) (dataOut []byte, err error) { +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 diff --git a/storage/noteTargets.go b/storage/noteTargets.go index 4aa174d..3a6af6b 100644 --- a/storage/noteTargets.go +++ b/storage/noteTargets.go @@ -10,15 +10,15 @@ import ( //go:generate stringer -type NoteTarget // What feed a note is targeting (public, home, followers or dm) -type NoteTarget uint8 +type NoteAccessLevel uint8 const ( // The note is intended for the public - NOTE_TARGET_PUBLIC NoteTarget = 0 + 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 NoteTarget = 1 << iota + 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 @@ -26,16 +26,16 @@ const ( ) // Converts the NoteTarget value into a type the DB can use -func (n *NoteTarget) Value() (driver.Value, error) { +func (n *NoteAccessLevel) Value() (driver.Value, error) { return n, nil } // Converts the raw value from the DB into a NoteTarget -func (n *NoteTarget) Scan(value any) error { +func (n *NoteAccessLevel) Scan(value any) error { vBig, ok := value.(int64) if !ok { return errors.New("not an int64") } - *n = NoteTarget(vBig) + *n = NoteAccessLevel(vBig) return nil } diff --git a/storage/notes.go b/storage/notes.go index d66c11d..a5a94d0 100644 --- a/storage/notes.go +++ b/storage/notes.go @@ -28,15 +28,15 @@ type Note struct { // 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"` // Links to attachments - 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 - Target NoteTarget // 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 + 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) { diff --git a/storage/notetarget_string.go b/storage/notetarget_string.go index 2b60188..74d3a30 100644 --- a/storage/notetarget_string.go +++ b/storage/notetarget_string.go @@ -21,7 +21,7 @@ const ( _NoteTarget_name_3 = "NOTE_TARGET_DM" ) -func (i NoteTarget) String() string { +func (i NoteAccessLevel) String() string { switch { case i == 0: return _NoteTarget_name_0 diff --git a/storage/serverTypes.go b/storage/serverTypes.go index f69166c..d8cfdc9 100644 --- a/storage/serverTypes.go +++ b/storage/serverTypes.go @@ -19,6 +19,8 @@ const ( 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") ) diff --git a/storage/userInfoFields.go b/storage/userInfoFields.go index 59d95b3..a1c4d25 100644 --- a/storage/userInfoFields.go +++ b/storage/userInfoFields.go @@ -12,6 +12,11 @@ type UserInfoField struct { 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