More work on the API. Primarely defining jsonapi types

This commit is contained in:
Melody Becker 2024-10-30 16:05:20 +01:00
parent 4f4d05a335
commit 0ed50aca60
13 changed files with 165 additions and 48 deletions

View file

@ -31,6 +31,14 @@ func setupLinstromApiV1Router() http.Handler {
// Pinning // Pinning
router.HandleFunc("POST /note/{noteId}/pin", linstromPinNote) router.HandleFunc("POST /note/{noteId}/pin", linstromPinNote)
router.HandleFunc("DELETE /note/{noteId}/pin", linstromUnpinNote) 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 return router
} }

View file

@ -1,9 +1,15 @@
package server package server
import "net/http" import (
"net/http"
)
// Notes // 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 linstromUpdateNote(w http.ResponseWriter, r *http.Request) {}
func linstromNewNote(w http.ResponseWriter, r *http.Request) {} func linstromNewNote(w http.ResponseWriter, r *http.Request) {}
func linstromDeleteNote(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 // No delete quote since quotes are their own notes with an extra attribute
// Pinning
func linstromPinNote(w http.ResponseWriter, r *http.Request) {} func linstromPinNote(w http.ResponseWriter, r *http.Request) {}
func linstromUnpinNote(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) {}

View file

@ -1,5 +1,24 @@
package server package server
import (
"net/http"
"github.com/gorilla/websocket"
"github.com/rs/zerolog/hlog"
)
// TODO: Decide where to put data stream handlers // TODO: Decide where to put data stream handlers
var websocketUpgrader = websocket.Upgrader{}
// Entrypoint for a new stream will be in here at least // 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)
}

View file

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

View file

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

View file

@ -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) store, ok := r.Context().Value(ContextKeyStorage).(*storage.Storage)
if !ok { if !ok {
other.HttpErr( hlog.FromRequest(r).Fatal().Msg("Failed to get storage reference from context")
w,
HttpErrIdMissingContextValue,
"Missing storage in context",
http.StatusInternalServerError,
)
return nil return nil
} }
return store return store
} }
func NoteIdFromRequest(r *http.Request) string {
return r.PathValue("noteId")
}

View file

@ -24,6 +24,10 @@ type MediaMetadata struct {
// Descriptive name for a media file // Descriptive name for a media file
// Emote name for example or servername.filetype for a server's icon // Emote name for example or servername.filetype for a server's icon
Name string 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) { func (s *Storage) NewMediaMetadata(location, mediaType, name string) (*MediaMetadata, error) {

View file

@ -19,6 +19,7 @@ import (
var ErrUnknownImageType = errors.New("unknown image format") var ErrUnknownImageType = errors.New("unknown image format")
func Compress(dataReader io.Reader, mimeType *string) (io.Reader, error) { 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) data, err := io.ReadAll(dataReader)
if err != nil { if err != nil {
return nil, err return nil, err
@ -51,7 +52,11 @@ func compressVideo(dataIn []byte, subType string) (dataOut []byte, err error) {
panic("Implement me") 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)) imageSize := image.Rect(0, 0, int(maxSizeX), int(maxSizeY))
dst := image.NewRGBA(imageSize) dst := image.NewRGBA(imageSize)
var sourceImage image.Image var sourceImage image.Image

View file

@ -10,15 +10,15 @@ import (
//go:generate stringer -type NoteTarget //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 NoteAccessLevel uint8
const ( const (
// The note is intended for the public // 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 // 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 NoteAccessLevel = 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
@ -26,16 +26,16 @@ const (
) )
// Converts the NoteTarget value into a type the DB can use // 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 return n, nil
} }
// Converts the raw value from the DB into a NoteTarget // 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) vBig, ok := value.(int64)
if !ok { if !ok {
return errors.New("not an int64") return errors.New("not an int64")
} }
*n = NoteTarget(vBig) *n = NoteAccessLevel(vBig)
return nil return nil
} }

View file

@ -29,11 +29,11 @@ type Note struct {
// Might already have formatting applied beforehand from the origin server // Might already have formatting applied beforehand from the origin server
RawContent string RawContent string
ContentWarning *string // Content warnings of the note, if it contains any ContentWarning *string // Content warnings of the note, if it contains any
Attachments []string `gorm:"serializer:json"` // Links to attachments Attachments []string `gorm:"serializer:json"` // List of Ids for mediaFiles
Emotes []string `gorm:"serializer:json"` // Emotes used in that message Emotes []string `gorm:"serializer:json"` // Emotes used in that message
RepliesTo *string // Url of the message this replies to RepliesTo *string // Url of the message this replies to
Quotes *string // url of the message this note quotes Quotes *string // url of the message this note quotes
Target NoteTarget // Where to send this message to (public, home, followers, dm) 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 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 OriginServer string // Url of the origin server. Also the primary key for those
Tags []string `gorm:"serializer:json"` // Hashtags Tags []string `gorm:"serializer:json"` // Hashtags

View file

@ -21,7 +21,7 @@ const (
_NoteTarget_name_3 = "NOTE_TARGET_DM" _NoteTarget_name_3 = "NOTE_TARGET_DM"
) )
func (i NoteTarget) String() string { func (i NoteAccessLevel) String() string {
switch { switch {
case i == 0: case i == 0:
return _NoteTarget_name_0 return _NoteTarget_name_0

View file

@ -19,6 +19,8 @@ const (
REMOTE_SERVER_MISSKEY = RemoteServerType("Misskey") REMOTE_SERVER_MISSKEY = RemoteServerType("Misskey")
// Includes Akkoma // Includes Akkoma
REMOTE_SERVER_PLEMORA = RemoteServerType("Plemora") REMOTE_SERVER_PLEMORA = RemoteServerType("Plemora")
// Wafrn is a new entry
REMOTE_SERVER_WAFRN = RemoteServerType("Wafrn")
// And of course, yours truly // And of course, yours truly
REMOTE_SERVER_LINSTROM = RemoteServerType("Linstrom") REMOTE_SERVER_LINSTROM = RemoteServerType("Linstrom")
) )

View file

@ -12,6 +12,11 @@ type UserInfoField struct {
Name string Name string
Value string Value string
LastUrlCheckDate *time.Time // Used if the value is an url to somewhere. Empty if value is not an url 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 // TODO: Add functions to store, load, update and delete these