diff --git a/Code-Style.md b/Code-Style.md new file mode 100644 index 0000000..d58dca1 --- /dev/null +++ b/Code-Style.md @@ -0,0 +1,21 @@ +# Code style guide + +## Go + +- Use `gofmt` +- `jsonapi` fields must use dasharised names + (eg `some-field` instead of `some_field` or `someField`) +- Function, struct and variable names must describe their purpose adequatly. + Making them longer for that purpose is fine +- Follow [https://go.dev/doc/effective_go] + (which also happens to be convenient place to start learning Go) +- Always use zerolog for console output. In http handlers, + use `hlog.FromRequest` to get a logger instance prepared with a bunch of metadata +- As Linstrom is both intended for active use as well as providing a learning resource, + all functions and structs must be documented + +## JS/TS + + + +- Biome should do most of the work diff --git a/duck.webp b/duck.webp new file mode 100644 index 0000000..3a42edc Binary files /dev/null and b/duck.webp differ diff --git a/main.go b/main.go index a39ac7f..41bac48 100644 --- a/main.go +++ b/main.go @@ -27,6 +27,9 @@ var reactiveFS embed.FS //go:embed frontend-noscript var nojsFS embed.FS +//go:embed duck.webp +var placeholderFile string + func main() { setLogger() setLogLevel() @@ -76,6 +79,7 @@ func main() { pkey, util.NewFSWrapper(reactiveFS, "frontend-reactive/dist/", false), util.NewFSWrapper(nojsFS, "frontend-noscript/", false), + &placeholderFile, ) server.Start(":8000") // TODO: Set up media server diff --git a/server/apiLinstrom.go b/server/apiLinstrom.go index ea95d8b..5c7109c 100644 --- a/server/apiLinstrom.go +++ b/server/apiLinstrom.go @@ -5,6 +5,7 @@ import "net/http" func setupLinstromApiRouter() http.Handler { router := http.NewServeMux() router.Handle("/v1/", http.StripPrefix("/v1", setupLinstromApiV1Router())) + router.Handle("/s2s/v1/", http.StripPrefix("/s2s/v1", setupLinstromS2SApiV1Router())) return router } @@ -93,3 +94,10 @@ func setupLinstromApiV1Router() http.Handler { return router } + +func setupLinstromS2SApiV1Router() http.Handler { + router := http.NewServeMux() + // TODO: Figure out a decent server to server API definition + router.HandleFunc("/", placeholderEndpoint) + return router +} diff --git a/server/apiLinstromAccounts.go b/server/apiLinstromAccounts.go index 90f04ce..5476546 100644 --- a/server/apiLinstromAccounts.go +++ b/server/apiLinstromAccounts.go @@ -165,7 +165,37 @@ func linstromUpdateAccount(w http.ResponseWriter, r *http.Request) { log.Error().Err(err).Msg("Failed to marshal and write updated account") } } -func linstromDeleteAccount(w http.ResponseWriter, r *http.Request) {} +func linstromDeleteAccount(w http.ResponseWriter, r *http.Request) { + actorId, _ := ActorIdFromRequest(r) + log := hlog.FromRequest(r) + store := StorageFromRequest(r) + targetAccountId := AccountIdFromRequest(r) + if targetAccountId != actorId { + other.HttpErr(w, HttpErrIdNotAuthenticated, "Action forbidden", http.StatusForbidden) + return + } + acc, err := store.FindAccountById(targetAccountId) + if err != nil { + log.Error().Err(err).Str("account-id", actorId).Msg("Failed to get account for deletion") + other.HttpErr( + w, + HttpErrIdDbFailure, + "Failed to get account from db", + http.StatusInternalServerError, + ) + return + } + // allRoles, err := store.FindRolesByNames(acc.Roles) + // collapsedRole := storage.CollapseRolesIntoOne(allRoles...) + + // TODO: Start job of sending out deletion messages to all federated servers + + // Clean up related data first + // User role + // Custom fields + _ = acc + store.DeleteAccount(actorId) +} func linstromIsFollowingAccount(w http.ResponseWriter, r *http.Request) {} func linstromFollowAccount(w http.ResponseWriter, r *http.Request) {} diff --git a/server/apiLinstromTypeHelpers.go b/server/apiLinstromTypeHelpers.go index a47ffeb..141a904 100644 --- a/server/apiLinstromTypeHelpers.go +++ b/server/apiLinstromTypeHelpers.go @@ -23,9 +23,22 @@ func convertAccountStorageToLinstrom( if err != nil { return nil, fmt.Errorf("icon: %w", err) } - storageBanner, err := store.GetMediaMetadataById(acc.Banner) - if err != nil { - return nil, fmt.Errorf("banner: %w", err) + var apiBanner *linstromMediaMetadata + if acc.Banner != nil { + storageBanner, err := store.GetMediaMetadataById(*acc.Banner) + if err != nil { + return nil, fmt.Errorf("banner: %w", err) + } + apiBanner = convertMediaMetadataStorageToLinstrom(storageBanner) + } + + var apiBackground *linstromMediaMetadata + if acc.Background != nil { + storageBackground, err := store.GetMediaMetadataById(*acc.Background) + if err != nil { + return nil, fmt.Errorf("banner: %w", err) + } + apiBackground = convertMediaMetadataStorageToLinstrom(storageBackground) } storageFields, err := store.FindMultipleUserFieldsById(acc.CustomFields) if err != nil { @@ -50,7 +63,11 @@ func convertAccountStorageToLinstrom( IsBot: acc.IsBot, Description: acc.Description, Icon: convertMediaMetadataStorageToLinstrom(storageIcon), - Banner: convertMediaMetadataStorageToLinstrom(storageBanner), + IconId: acc.Icon, + Banner: apiBanner, + BannerId: acc.Banner, + Background: apiBackground, + BackgroundId: acc.Background, FollowerIds: acc.Followers, FollowingIds: acc.Follows, Indexable: acc.Indexable, diff --git a/server/apiLinstromTypes.go b/server/apiLinstromTypes.go index b064c0f..5a5170e 100644 --- a/server/apiLinstromTypes.go +++ b/server/apiLinstromTypes.go @@ -16,85 +16,89 @@ var ( 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"` + 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"` + 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"` + 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"` + PingIds []string `jsonapi:"attr,ping-ids,omitempty"` } type linstromOriginServer struct { Id uint `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", "Mastodon", "Plemora", "Misskey" or "Wafrn" + CreatedAt time.Time `jsonapi:"attr,created-at"` + UpdatedAt *time.Time `jsonapi:"attr,updated-at,omitempty"` + ServerType string `jsonapi:"attr,server-type"` // one of "Linstrom", "Mastodon", "Plemora", "Misskey" or "Wafrn" Domain string `jsonapi:"attr,domain"` - DisplayName string `jsonapi:"attr,display_name"` + DisplayName string `jsonapi:"attr,display-name"` Icon *linstromMediaMetadata `jsonapi:"relation,icon"` - IsSelf bool `jsonapi:"attr,is_self"` + 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"` + Id string `jsonapi:"primary,media"` + 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"` + MimeType string `jsonapi:"attr,mime-type"` Name string `jsonapi:"attr,name"` - AltText string `jsonapi:"attr,alt_text"` + 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"` + 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"` + 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"` + IconId string `jsonapi:"attr,icon-id"` Banner *linstromMediaMetadata `jsonapi:"relation,banner"` - FollowerIds []string `jsonapi:"attr,follows_ids"` - FollowingIds []string `jsonapi:"attr,following_ids"` + BannerId *string `jsonapi:"attr,banner-id"` + Background *linstromMediaMetadata `jsonapi:"relation,background"` + BackgroundId *string `jsonapi:"attr,background-id"` + 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"` + RestrictedFollow bool `jsonapi:"attr,restricted-follow"` + IdentifiesAs []string `jsonapi:"attr,identifies-as"` Pronouns []string `jsonapi:"attr,pronouns"` Roles []string `jsonapi:"attr,roles"` } type linstromCustomAccountField struct { - Id uint `jsonapi:"primary,custom_account_fields"` - CreatedAt time.Time `jsonapi:"attr,created_at"` - UpdatedAt *time.Time `jsonapi:"attr,updated_at,omitempty"` + Id uint `jsonapi:"primary,custom-account-fields"` + 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"` + BelongsToId string `jsonapi:"attr,belongs-to-id"` } // Role is essentially just a carbon copy of storage/roles.go type linstromRole struct { Id uint `jsonapi:"primary,roles"` - CreatedAt time.Time `jsonapi:"attr,created_at"` - UpdatedAt *time.Time `jsonapi:"attr,updated_at,omitempty"` + CreatedAt time.Time `jsonapi:"attr,created-at"` + UpdatedAt *time.Time `jsonapi:"attr,updated-at,omitempty"` // Name of the role Name string `jsonapi:"attr,name"` @@ -107,35 +111,35 @@ type linstromRole struct { Priority uint `jsonapi:"attr,priority"` // Whether this role is for a for a single user only (like custom, per user permissions in Discord) // If yes, Name will be the id of the user in question - IsUserRole bool `jsonapi:"attr,is_user_role"` + IsUserRole bool `jsonapi:"attr,is-user-role"` // Whether this role is one built into Linstrom from the start or not // Note: Built-in roles can't be modified - IsBuiltIn bool `jsonapi:"attr,is_builtin"` + IsBuiltIn bool `jsonapi:"attr,is-builtin"` // --- User permissions --- - CanSendMedia *bool `jsonapi:"attr,can_send_media"` - CanSendCustomEmotes *bool `jsonapi:"attr,can_send_custom_emotes"` - CanSendCustomReactions *bool `jsonapi:"attr,can_send_custom_reactions"` - CanSendPublicNotes *bool `jsonapi:"attr,can_send_public_notes"` - CanSendLocalNotes *bool `jsonapi:"attr,can_send_local_notes"` - CanSendFollowerOnlyNotes *bool `jsonapi:"attr,can_send_follower_only_notes"` - CanSendPrivateNotes *bool `jsonapi:"attr,can_send_private_notes"` - CanSendReplies *bool `jsonapi:"attr,can_send_replies"` - CanQuote *bool `jsonapi:"attr,can_quote"` - CanBoost *bool `jsonapi:"attr,can_boost"` - CanIncludeLinks *bool `jsonapi:"attr,can_include_links"` - CanIncludeSurvey *bool `jsonapi:"attr,can_include_survey"` + CanSendMedia *bool `jsonapi:"attr,can-send-media"` + CanSendCustomEmotes *bool `jsonapi:"attr,can-send-custom-emotes"` + CanSendCustomReactions *bool `jsonapi:"attr,can-send-custom-reactions"` + CanSendPublicNotes *bool `jsonapi:"attr,can-send-public-notes"` + CanSendLocalNotes *bool `jsonapi:"attr,can-send-local-notes"` + CanSendFollowerOnlyNotes *bool `jsonapi:"attr,can-send-follower-only-notes"` + CanSendPrivateNotes *bool `jsonapi:"attr,can-send-private-notes"` + CanSendReplies *bool `jsonapi:"attr,can-send-replies"` + CanQuote *bool `jsonapi:"attr,can-quote"` + CanBoost *bool `jsonapi:"attr,can-boost"` + CanIncludeLinks *bool `jsonapi:"attr,can-include-links"` + CanIncludeSurvey *bool `jsonapi:"attr,can-include-survey"` - CanChangeDisplayName *bool `jsonapi:"attr,can_change_display_name"` + CanChangeDisplayName *bool `jsonapi:"attr,can-change-display-name"` - BlockedUsers []string `jsonapi:"attr,blocked_users"` - CanSubmitReports *bool `jsonapi:"attr,can_submit_reports"` - CanLogin *bool `jsonapi:"attr,can_login"` + BlockedUsers []string `jsonapi:"attr,blocked-users"` + CanSubmitReports *bool `jsonapi:"attr,can-submit-reports"` + CanLogin *bool `jsonapi:"attr,can-login"` - CanMentionOthers *bool `jsonapi:"attr,can_mention_others"` - HasMentionCountLimit *bool `jsonapi:"attr,has_mention_count_limit"` - MentionLimit *uint32 `jsonapi:"attr,mention_count_limit"` + CanMentionOthers *bool `jsonapi:"attr,can-mention-others"` + HasMentionCountLimit *bool `jsonapi:"attr,has-mention-count-limit"` + MentionLimit *uint32 `jsonapi:"attr,mention-count-limit"` // CanViewBoosts *bool // CanViewQuotes *bool @@ -143,32 +147,32 @@ type linstromRole struct { // CanViewCustomEmotes *bool // --- Automod --- - AutoNsfwMedia *bool `jsonapi:"attr,auto_nsfw_media"` - AutoCwPosts *bool `jsonapi:"attr,auto_cw_posts"` - AutoCwPostsText *string `jsonapi:"attr,auto_cw_posts_text"` - ScanCreatedPublicNotes *bool `jsonapi:"attr,scan_created_public_notes"` - ScanCreatedLocalNotes *bool `jsonapi:"attr,scan_created_local_notes"` - ScanCreatedFollowerOnlyNotes *bool `jsonapi:"attr,scan_created_follower_only_notes"` - ScanCreatedPrivateNotes *bool `jsonapi:"attr,scan_created_private_notes"` - DisallowInteractionsWith []string `jsonapi:"attr,disallow_interactions_with"` + AutoNsfwMedia *bool `jsonapi:"attr,auto-nsfw-media"` + AutoCwPosts *bool `jsonapi:"attr,auto-cw-posts"` + AutoCwPostsText *string `jsonapi:"attr,auto-cw-posts-text"` + ScanCreatedPublicNotes *bool `jsonapi:"attr,scan-created-public-notes"` + ScanCreatedLocalNotes *bool `jsonapi:"attr,scan-created-local-notes"` + ScanCreatedFollowerOnlyNotes *bool `jsonapi:"attr,scan-created-follower-only-notes"` + ScanCreatedPrivateNotes *bool `jsonapi:"attr,scan-created-private-notes"` + DisallowInteractionsWith []string `jsonapi:"attr,disallow-interactions-with"` - WithholdNotesForManualApproval *bool `jsonapi:"attr,withhold_notes_for_manual_approval"` - WithholdNotesBasedOnRegex *bool `jsonapi:"attr,withhold_notes_based_on_regex"` - WithholdNotesRegexes []string `jsonapi:"attr,withhold_notes_regexes"` + WithholdNotesForManualApproval *bool `jsonapi:"attr,withhold-notes-for-manual-approval"` + WithholdNotesBasedOnRegex *bool `jsonapi:"attr,withhold-notes-based-on-regex"` + WithholdNotesRegexes []string `jsonapi:"attr,withhold-notes-regexes"` // --- Admin perms --- // If set, counts as all permissions being set as given and all restrictions being disabled - FullAdmin *bool `jsonapi:"attr,full_admin"` - CanAffectOtherAdmins *bool `jsonapi:"attr,can_affect_other_admins"` - CanDeleteNotes *bool `jsonapi:"attr,can_delete_notes"` - CanConfirmWithheldNotes *bool `jsonapi:"attr,can_confirm_withheld_notes"` - CanAssignRoles *bool `jsonapi:"attr,can_assign_roles"` - CanSupressInteractionsBetweenUsers *bool `jsonapi:"attr,can_supress_interactions_between_users"` - CanOverwriteDisplayNames *bool `jsonapi:"attr,can_overwrite_display_names"` - CanManageCustomEmotes *bool `jsonapi:"attr,can_manage_custom_emotes"` - CanViewDeletedNotes *bool `jsonapi:"attr,can_view_deleted_notes"` - CanRecoverDeletedNotes *bool `jsonapi:"attr,can_recover_deleted_notes"` - CanManageAvatarDecorations *bool `jsonapi:"attr,can_manage_avatar_decorations"` - CanManageAds *bool `jsonapi:"attr,can_manage_ads"` - CanSendAnnouncements *bool `jsonapi:"attr,can_send_announcements"` + FullAdmin *bool `jsonapi:"attr,full-admin"` + CanAffectOtherAdmins *bool `jsonapi:"attr,can-affect-other-admins"` + CanDeleteNotes *bool `jsonapi:"attr,can-delete-notes"` + CanConfirmWithheldNotes *bool `jsonapi:"attr,can-confirm-withheld-notes"` + CanAssignRoles *bool `jsonapi:"attr,can-assign-roles"` + CanSupressInteractionsBetweenUsers *bool `jsonapi:"attr,can-supress-interactions-between-users"` + CanOverwriteDisplayNames *bool `jsonapi:"attr,can-overwrite-display-names"` + CanManageCustomEmotes *bool `jsonapi:"attr,can-manage-custom-emotes"` + CanViewDeletedNotes *bool `jsonapi:"attr,can-view-deleted-notes"` + CanRecoverDeletedNotes *bool `jsonapi:"attr,can-recover-deleted-notes"` + CanManageAvatarDecorations *bool `jsonapi:"attr,can-manage-avatar-decorations"` + CanManageAds *bool `jsonapi:"attr,can-manage-ads"` + CanSendAnnouncements *bool `jsonapi:"attr,can-send-announcements"` } diff --git a/server/server.go b/server/server.go index 58151c1..32ef801 100644 --- a/server/server.go +++ b/server/server.go @@ -1,6 +1,7 @@ package server import ( + "fmt" "io/fs" "net/http" @@ -15,8 +16,13 @@ type Server struct { router http.Handler } -func NewServer(store *storage.Storage, pkey *passkey.Passkey, reactiveFS, staticFS fs.FS) *Server { - handler := buildRootHandler(pkey, reactiveFS, staticFS) +func NewServer( + store *storage.Storage, + pkey *passkey.Passkey, + reactiveFS, staticFS fs.FS, + placeholderFile *string, +) *Server { + handler := buildRootHandler(pkey, reactiveFS, staticFS, placeholderFile) handler = ChainMiddlewares(handler, LoggingMiddleware, ContextValsMiddleware(map[any]any{ ContextKeyStorage: store, })) @@ -26,7 +32,11 @@ func NewServer(store *storage.Storage, pkey *passkey.Passkey, reactiveFS, static } } -func buildRootHandler(pkey *passkey.Passkey, reactiveFS, staticFS fs.FS) http.Handler { +func buildRootHandler( + pkey *passkey.Passkey, + reactiveFS, staticFS fs.FS, + placeholderFile *string, +) http.Handler { mux := http.NewServeMux() mux.Handle( "/webauthn/", @@ -61,6 +71,10 @@ func buildRootHandler(pkey *passkey.Passkey, reactiveFS, staticFS fs.FS) http.Ha )(ChainMiddlewares(setupTestEndpoints(), passkeyIdToAccountIdTransformerMiddleware)), ) + mux.HandleFunc("/placeholder-file", func(w http.ResponseWriter, r *http.Request) { + fmt.Fprint(w, placeholderFile) + }) + return mux }