From b74a8886b02e4bb035b5c0cb1bf073bf2bc58988 Mon Sep 17 00:00:00 2001 From: Hong Minhee Date: Tue, 25 Jun 2024 22:36:24 +0900 Subject: [PATCH] Pinned posts --- .vscode/settings.json | 2 + README.md | 2 +- bun.lockb | Bin 148585 -> 148585 bytes drizzle/0024_pinned_posts.sql | 21 + drizzle/0025_accounts.featured_url.sql | 1 + drizzle/meta/0024_snapshot.json | 1266 +++++++++++++++++++++++ drizzle/meta/0025_snapshot.json | 1272 ++++++++++++++++++++++++ drizzle/meta/_journal.json | 14 + package.json | 2 +- src/accounts.tsx | 1 + src/api/v1/accounts.ts | 12 +- src/api/v1/statuses.ts | 113 ++- src/components/Post.tsx | 6 +- src/entities/status.ts | 6 + src/federation/account.ts | 23 + src/federation/collection.ts | 33 + src/federation/index.ts | 75 ++ src/profile.tsx | 54 +- src/schema.ts | 119 ++- 19 files changed, 2982 insertions(+), 40 deletions(-) create mode 100644 drizzle/0024_pinned_posts.sql create mode 100644 drizzle/0025_accounts.featured_url.sql create mode 100644 drizzle/meta/0024_snapshot.json create mode 100644 drizzle/meta/0025_snapshot.json create mode 100644 src/federation/collection.ts diff --git a/.vscode/settings.json b/.vscode/settings.json index 7d9ffca..adb487a 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -26,6 +26,7 @@ }, "cSpell.words": [ "activitypub", + "bigserial", "biomejs", "blurhash", "bunx", @@ -36,6 +37,7 @@ "fedi", "fedify", "fediverse", + "fkey", "hono", "htmls", "ilike", diff --git a/README.md b/README.md index 01e9b2d..4048ecf 100644 --- a/README.md +++ b/README.md @@ -123,7 +123,7 @@ Current features and roadmap - [x] View posts - [x] Post visibility - [x] Post language -- [ ] Pinned posts +- [x] Pinned posts - [x] Mentions - [x] Hashtags - [x] Media attachments diff --git a/bun.lockb b/bun.lockb index 9548f1ef3a368bed525bb3faf58ae2211307b6e6..ad3d52afe0bc722faeb640d2a5efffee0506f392 100755 GIT binary patch delta 155 zcmV;M0A&B^hzaS436L%zf+mzHYE&9GZm){t-d2?+z&GRZsa(T-X=l-5G-IhTu}E!ZJgpedl$IMu4=q)$dP4BD;+S_Jks zbrwp`krr1p|l;};L|Dqq8iiS37^*=-@PjZ1knu}Vs9Xw^gzM{YRIv{{bqOP8R|a Jw~qP&+etl%Ov(TN diff --git a/drizzle/0024_pinned_posts.sql b/drizzle/0024_pinned_posts.sql new file mode 100644 index 0000000..46a94ff --- /dev/null +++ b/drizzle/0024_pinned_posts.sql @@ -0,0 +1,21 @@ +CREATE TABLE IF NOT EXISTS "pinned_posts" ( + "index" bigserial PRIMARY KEY NOT NULL, + "post_id" uuid NOT NULL, + "account_id" uuid NOT NULL, + "created" timestamp with time zone DEFAULT now() NOT NULL, + CONSTRAINT "pinned_posts_post_id_account_id_unique" UNIQUE("post_id","account_id") +); +--> statement-breakpoint +ALTER TABLE "posts" ADD CONSTRAINT "posts_id_actor_id_unique" UNIQUE("id", "actor_id"); +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "pinned_posts" ADD CONSTRAINT "pinned_posts_account_id_accounts_id_fk" FOREIGN KEY ("account_id") REFERENCES "public"."accounts"("id") ON DELETE cascade ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "pinned_posts" ADD CONSTRAINT "pinned_posts_post_id_account_id_posts_id_actor_id_fk" FOREIGN KEY ("post_id","account_id") REFERENCES "public"."posts"("id","actor_id") ON DELETE cascade ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; diff --git a/drizzle/0025_accounts.featured_url.sql b/drizzle/0025_accounts.featured_url.sql new file mode 100644 index 0000000..5517e4d --- /dev/null +++ b/drizzle/0025_accounts.featured_url.sql @@ -0,0 +1 @@ +ALTER TABLE "accounts" ADD COLUMN "featured_url" text; \ No newline at end of file diff --git a/drizzle/meta/0024_snapshot.json b/drizzle/meta/0024_snapshot.json new file mode 100644 index 0000000..80c68eb --- /dev/null +++ b/drizzle/meta/0024_snapshot.json @@ -0,0 +1,1266 @@ +{ + "id": "5124b513-c942-4658-87bd-48ffb544249e", + "prevId": "e5cbde0d-8c7a-4495-bb42-817987717f2f", + "version": "6", + "dialect": "postgresql", + "tables": { + "public.access_tokens": { + "name": "access_tokens", + "schema": "", + "columns": { + "code": { + "name": "code", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "application_id": { + "name": "application_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "account_owner_id": { + "name": "account_owner_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "grant_type": { + "name": "grant_type", + "type": "grant_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'authorization_code'" + }, + "scopes": { + "name": "scopes", + "type": "scope[]", + "primaryKey": false, + "notNull": true + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "access_tokens_application_id_applications_id_fk": { + "name": "access_tokens_application_id_applications_id_fk", + "tableFrom": "access_tokens", + "tableTo": "applications", + "columnsFrom": [ + "application_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "access_tokens_account_owner_id_account_owners_id_fk": { + "name": "access_tokens_account_owner_id_account_owners_id_fk", + "tableFrom": "access_tokens", + "tableTo": "account_owners", + "columnsFrom": [ + "account_owner_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.account_owners": { + "name": "account_owners", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "handle": { + "name": "handle", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "rsa_private_key_jwk": { + "name": "rsa_private_key_jwk", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "rsa_public_key_jwk": { + "name": "rsa_public_key_jwk", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "ed25519_private_key_jwk": { + "name": "ed25519_private_key_jwk", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "ed25519_public_key_jwk": { + "name": "ed25519_public_key_jwk", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "fields": { + "name": "fields", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{}'::json" + }, + "bio": { + "name": "bio", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "followed_tags": { + "name": "followed_tags", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": [] + }, + "visibility": { + "name": "visibility", + "type": "post_visibility", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'public'" + }, + "language": { + "name": "language", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'en'" + } + }, + "indexes": {}, + "foreignKeys": { + "account_owners_id_accounts_id_fk": { + "name": "account_owners_id_accounts_id_fk", + "tableFrom": "account_owners", + "tableTo": "accounts", + "columnsFrom": [ + "id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "account_owners_handle_unique": { + "name": "account_owners_handle_unique", + "nullsNotDistinct": false, + "columns": [ + "handle" + ] + } + } + }, + "public.accounts": { + "name": "accounts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "iri": { + "name": "iri", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "account_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "handle": { + "name": "handle", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "bio_html": { + "name": "bio_html", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "protected": { + "name": "protected", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "avatar_url": { + "name": "avatar_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cover_url": { + "name": "cover_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "inbox_url": { + "name": "inbox_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "followers_url": { + "name": "followers_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "shared_inbox_url": { + "name": "shared_inbox_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "following_count": { + "name": "following_count", + "type": "bigint", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "followers_count": { + "name": "followers_count", + "type": "bigint", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "posts_count": { + "name": "posts_count", + "type": "bigint", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "field_htmls": { + "name": "field_htmls", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{}'::json" + }, + "sensitive": { + "name": "sensitive", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "published": { + "name": "published", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "updated": { + "name": "updated", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "accounts_iri_unique": { + "name": "accounts_iri_unique", + "nullsNotDistinct": false, + "columns": [ + "iri" + ] + }, + "accounts_handle_unique": { + "name": "accounts_handle_unique", + "nullsNotDistinct": false, + "columns": [ + "handle" + ] + } + } + }, + "public.applications": { + "name": "applications", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(256)", + "primaryKey": false, + "notNull": true + }, + "redirect_uris": { + "name": "redirect_uris", + "type": "text[]", + "primaryKey": false, + "notNull": true + }, + "scopes": { + "name": "scopes", + "type": "scope[]", + "primaryKey": false, + "notNull": true + }, + "website": { + "name": "website", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_secret": { + "name": "client_secret", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "applications_client_id_unique": { + "name": "applications_client_id_unique", + "nullsNotDistinct": false, + "columns": [ + "client_id" + ] + } + } + }, + "public.bookmarks": { + "name": "bookmarks", + "schema": "", + "columns": { + "post_id": { + "name": "post_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "account_owner_id": { + "name": "account_owner_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "bookmarks_post_id_posts_id_fk": { + "name": "bookmarks_post_id_posts_id_fk", + "tableFrom": "bookmarks", + "tableTo": "posts", + "columnsFrom": [ + "post_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "bookmarks_account_owner_id_account_owners_id_fk": { + "name": "bookmarks_account_owner_id_account_owners_id_fk", + "tableFrom": "bookmarks", + "tableTo": "account_owners", + "columnsFrom": [ + "account_owner_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "bookmarks_post_id_account_owner_id_pk": { + "name": "bookmarks_post_id_account_owner_id_pk", + "columns": [ + "post_id", + "account_owner_id" + ] + } + }, + "uniqueConstraints": {} + }, + "public.credentials": { + "name": "credentials", + "schema": "", + "columns": { + "email": { + "name": "email", + "type": "varchar(254)", + "primaryKey": true, + "notNull": true + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.follows": { + "name": "follows", + "schema": "", + "columns": { + "iri": { + "name": "iri", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "following_id": { + "name": "following_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "follower_id": { + "name": "follower_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "shares": { + "name": "shares", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "notify": { + "name": "notify", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "languages": { + "name": "languages", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "approved": { + "name": "approved", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "follows_following_id_accounts_id_fk": { + "name": "follows_following_id_accounts_id_fk", + "tableFrom": "follows", + "tableTo": "accounts", + "columnsFrom": [ + "following_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "follows_follower_id_accounts_id_fk": { + "name": "follows_follower_id_accounts_id_fk", + "tableFrom": "follows", + "tableTo": "accounts", + "columnsFrom": [ + "follower_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "follows_following_id_follower_id_pk": { + "name": "follows_following_id_follower_id_pk", + "columns": [ + "following_id", + "follower_id" + ] + } + }, + "uniqueConstraints": { + "follows_iri_unique": { + "name": "follows_iri_unique", + "nullsNotDistinct": false, + "columns": [ + "iri" + ] + } + } + }, + "public.likes": { + "name": "likes", + "schema": "", + "columns": { + "post_id": { + "name": "post_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "likes_post_id_posts_id_fk": { + "name": "likes_post_id_posts_id_fk", + "tableFrom": "likes", + "tableTo": "posts", + "columnsFrom": [ + "post_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "likes_account_id_accounts_id_fk": { + "name": "likes_account_id_accounts_id_fk", + "tableFrom": "likes", + "tableTo": "accounts", + "columnsFrom": [ + "account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "likes_post_id_account_id_pk": { + "name": "likes_post_id_account_id_pk", + "columns": [ + "post_id", + "account_id" + ] + } + }, + "uniqueConstraints": {} + }, + "public.markers": { + "name": "markers", + "schema": "", + "columns": { + "account_owner_id": { + "name": "account_owner_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "marker_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "last_read_id": { + "name": "last_read_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "updated": { + "name": "updated", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "markers_account_owner_id_account_owners_id_fk": { + "name": "markers_account_owner_id_account_owners_id_fk", + "tableFrom": "markers", + "tableTo": "account_owners", + "columnsFrom": [ + "account_owner_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "markers_account_owner_id_type_pk": { + "name": "markers_account_owner_id_type_pk", + "columns": [ + "account_owner_id", + "type" + ] + } + }, + "uniqueConstraints": {} + }, + "public.media": { + "name": "media", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "post_id": { + "name": "post_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "width": { + "name": "width", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "height": { + "name": "height", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "thumbnail_type": { + "name": "thumbnail_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "thumbnail_url": { + "name": "thumbnail_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "thumbnail_width": { + "name": "thumbnail_width", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "thumbnail_height": { + "name": "thumbnail_height", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "media_post_id_posts_id_fk": { + "name": "media_post_id_posts_id_fk", + "tableFrom": "media", + "tableTo": "posts", + "columnsFrom": [ + "post_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.mentions": { + "name": "mentions", + "schema": "", + "columns": { + "post_id": { + "name": "post_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "mentions_post_id_posts_id_fk": { + "name": "mentions_post_id_posts_id_fk", + "tableFrom": "mentions", + "tableTo": "posts", + "columnsFrom": [ + "post_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mentions_account_id_accounts_id_fk": { + "name": "mentions_account_id_accounts_id_fk", + "tableFrom": "mentions", + "tableTo": "accounts", + "columnsFrom": [ + "account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "mentions_post_id_account_id_pk": { + "name": "mentions_post_id_account_id_pk", + "columns": [ + "post_id", + "account_id" + ] + } + }, + "uniqueConstraints": {} + }, + "public.pinned_posts": { + "name": "pinned_posts", + "schema": "", + "columns": { + "index": { + "name": "index", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "post_id": { + "name": "post_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "pinned_posts_account_id_accounts_id_fk": { + "name": "pinned_posts_account_id_accounts_id_fk", + "tableFrom": "pinned_posts", + "tableTo": "accounts", + "columnsFrom": [ + "account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "pinned_posts_post_id_account_id_posts_id_actor_id_fk": { + "name": "pinned_posts_post_id_account_id_posts_id_actor_id_fk", + "tableFrom": "pinned_posts", + "tableTo": "posts", + "columnsFrom": [ + "post_id", + "account_id" + ], + "columnsTo": [ + "id", + "actor_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "pinned_posts_post_id_account_id_unique": { + "name": "pinned_posts_post_id_account_id_unique", + "nullsNotDistinct": false, + "columns": [ + "post_id", + "account_id" + ] + } + } + }, + "public.posts": { + "name": "posts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "iri": { + "name": "iri", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "post_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "actor_id": { + "name": "actor_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "application_id": { + "name": "application_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "reply_target_id": { + "name": "reply_target_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "sharing_id": { + "name": "sharing_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "visibility": { + "name": "visibility", + "type": "post_visibility", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "summary_html": { + "name": "summary_html", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "content_html": { + "name": "content_html", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "language": { + "name": "language", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tags": { + "name": "tags", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "sensitive": { + "name": "sensitive", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "preview_card": { + "name": "preview_card", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "replies_count": { + "name": "replies_count", + "type": "bigint", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "shares_count": { + "name": "shares_count", + "type": "bigint", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "likes_count": { + "name": "likes_count", + "type": "bigint", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "published": { + "name": "published", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "updated": { + "name": "updated", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "posts_actor_id_accounts_id_fk": { + "name": "posts_actor_id_accounts_id_fk", + "tableFrom": "posts", + "tableTo": "accounts", + "columnsFrom": [ + "actor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "posts_application_id_applications_id_fk": { + "name": "posts_application_id_applications_id_fk", + "tableFrom": "posts", + "tableTo": "applications", + "columnsFrom": [ + "application_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "posts_reply_target_id_posts_id_fk": { + "name": "posts_reply_target_id_posts_id_fk", + "tableFrom": "posts", + "tableTo": "posts", + "columnsFrom": [ + "reply_target_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "posts_sharing_id_posts_id_fk": { + "name": "posts_sharing_id_posts_id_fk", + "tableFrom": "posts", + "tableTo": "posts", + "columnsFrom": [ + "sharing_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "posts_iri_unique": { + "name": "posts_iri_unique", + "nullsNotDistinct": false, + "columns": [ + "iri" + ] + }, + "posts_id_actor_id_unique": { + "name": "posts_id_actor_id_unique", + "nullsNotDistinct": false, + "columns": [ + "id", + "actor_id" + ] + } + } + } + }, + "enums": { + "public.account_type": { + "name": "account_type", + "schema": "public", + "values": [ + "Application", + "Group", + "Organization", + "Person", + "Service" + ] + }, + "public.grant_type": { + "name": "grant_type", + "schema": "public", + "values": [ + "authorization_code", + "client_credentials" + ] + }, + "public.marker_type": { + "name": "marker_type", + "schema": "public", + "values": [ + "notifications", + "home" + ] + }, + "public.post_type": { + "name": "post_type", + "schema": "public", + "values": [ + "Article", + "Note" + ] + }, + "public.post_visibility": { + "name": "post_visibility", + "schema": "public", + "values": [ + "public", + "unlisted", + "private", + "direct" + ] + }, + "public.scope": { + "name": "scope", + "schema": "public", + "values": [ + "read", + "read:accounts", + "read:blocks", + "read:bookmarks", + "read:favourites", + "read:filters", + "read:follows", + "read:lists", + "read:mutes", + "read:notifications", + "read:search", + "read:statuses", + "write", + "write:accounts", + "write:blocks", + "write:bookmarks", + "write:conversations", + "write:favourites", + "write:filters", + "write:follows", + "write:lists", + "write:media", + "write:mutes", + "write:notifications", + "write:reports", + "write:statuses", + "follow", + "push" + ] + } + }, + "schemas": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/0025_snapshot.json b/drizzle/meta/0025_snapshot.json new file mode 100644 index 0000000..847124f --- /dev/null +++ b/drizzle/meta/0025_snapshot.json @@ -0,0 +1,1272 @@ +{ + "id": "c84f7161-a0c6-4bb4-adc9-82576a44bf62", + "prevId": "5124b513-c942-4658-87bd-48ffb544249e", + "version": "6", + "dialect": "postgresql", + "tables": { + "public.access_tokens": { + "name": "access_tokens", + "schema": "", + "columns": { + "code": { + "name": "code", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "application_id": { + "name": "application_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "account_owner_id": { + "name": "account_owner_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "grant_type": { + "name": "grant_type", + "type": "grant_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'authorization_code'" + }, + "scopes": { + "name": "scopes", + "type": "scope[]", + "primaryKey": false, + "notNull": true + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "access_tokens_application_id_applications_id_fk": { + "name": "access_tokens_application_id_applications_id_fk", + "tableFrom": "access_tokens", + "tableTo": "applications", + "columnsFrom": [ + "application_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "access_tokens_account_owner_id_account_owners_id_fk": { + "name": "access_tokens_account_owner_id_account_owners_id_fk", + "tableFrom": "access_tokens", + "tableTo": "account_owners", + "columnsFrom": [ + "account_owner_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.account_owners": { + "name": "account_owners", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "handle": { + "name": "handle", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "rsa_private_key_jwk": { + "name": "rsa_private_key_jwk", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "rsa_public_key_jwk": { + "name": "rsa_public_key_jwk", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "ed25519_private_key_jwk": { + "name": "ed25519_private_key_jwk", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "ed25519_public_key_jwk": { + "name": "ed25519_public_key_jwk", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "fields": { + "name": "fields", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{}'::json" + }, + "bio": { + "name": "bio", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "followed_tags": { + "name": "followed_tags", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": [] + }, + "visibility": { + "name": "visibility", + "type": "post_visibility", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'public'" + }, + "language": { + "name": "language", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'en'" + } + }, + "indexes": {}, + "foreignKeys": { + "account_owners_id_accounts_id_fk": { + "name": "account_owners_id_accounts_id_fk", + "tableFrom": "account_owners", + "tableTo": "accounts", + "columnsFrom": [ + "id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "account_owners_handle_unique": { + "name": "account_owners_handle_unique", + "nullsNotDistinct": false, + "columns": [ + "handle" + ] + } + } + }, + "public.accounts": { + "name": "accounts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "iri": { + "name": "iri", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "account_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "handle": { + "name": "handle", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "bio_html": { + "name": "bio_html", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "protected": { + "name": "protected", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "avatar_url": { + "name": "avatar_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cover_url": { + "name": "cover_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "inbox_url": { + "name": "inbox_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "followers_url": { + "name": "followers_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "shared_inbox_url": { + "name": "shared_inbox_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "featured_url": { + "name": "featured_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "following_count": { + "name": "following_count", + "type": "bigint", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "followers_count": { + "name": "followers_count", + "type": "bigint", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "posts_count": { + "name": "posts_count", + "type": "bigint", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "field_htmls": { + "name": "field_htmls", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{}'::json" + }, + "sensitive": { + "name": "sensitive", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "published": { + "name": "published", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "updated": { + "name": "updated", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "accounts_iri_unique": { + "name": "accounts_iri_unique", + "nullsNotDistinct": false, + "columns": [ + "iri" + ] + }, + "accounts_handle_unique": { + "name": "accounts_handle_unique", + "nullsNotDistinct": false, + "columns": [ + "handle" + ] + } + } + }, + "public.applications": { + "name": "applications", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(256)", + "primaryKey": false, + "notNull": true + }, + "redirect_uris": { + "name": "redirect_uris", + "type": "text[]", + "primaryKey": false, + "notNull": true + }, + "scopes": { + "name": "scopes", + "type": "scope[]", + "primaryKey": false, + "notNull": true + }, + "website": { + "name": "website", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_secret": { + "name": "client_secret", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "applications_client_id_unique": { + "name": "applications_client_id_unique", + "nullsNotDistinct": false, + "columns": [ + "client_id" + ] + } + } + }, + "public.bookmarks": { + "name": "bookmarks", + "schema": "", + "columns": { + "post_id": { + "name": "post_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "account_owner_id": { + "name": "account_owner_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "bookmarks_post_id_posts_id_fk": { + "name": "bookmarks_post_id_posts_id_fk", + "tableFrom": "bookmarks", + "tableTo": "posts", + "columnsFrom": [ + "post_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "bookmarks_account_owner_id_account_owners_id_fk": { + "name": "bookmarks_account_owner_id_account_owners_id_fk", + "tableFrom": "bookmarks", + "tableTo": "account_owners", + "columnsFrom": [ + "account_owner_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "bookmarks_post_id_account_owner_id_pk": { + "name": "bookmarks_post_id_account_owner_id_pk", + "columns": [ + "post_id", + "account_owner_id" + ] + } + }, + "uniqueConstraints": {} + }, + "public.credentials": { + "name": "credentials", + "schema": "", + "columns": { + "email": { + "name": "email", + "type": "varchar(254)", + "primaryKey": true, + "notNull": true + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.follows": { + "name": "follows", + "schema": "", + "columns": { + "iri": { + "name": "iri", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "following_id": { + "name": "following_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "follower_id": { + "name": "follower_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "shares": { + "name": "shares", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "notify": { + "name": "notify", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "languages": { + "name": "languages", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "approved": { + "name": "approved", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "follows_following_id_accounts_id_fk": { + "name": "follows_following_id_accounts_id_fk", + "tableFrom": "follows", + "tableTo": "accounts", + "columnsFrom": [ + "following_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "follows_follower_id_accounts_id_fk": { + "name": "follows_follower_id_accounts_id_fk", + "tableFrom": "follows", + "tableTo": "accounts", + "columnsFrom": [ + "follower_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "follows_following_id_follower_id_pk": { + "name": "follows_following_id_follower_id_pk", + "columns": [ + "following_id", + "follower_id" + ] + } + }, + "uniqueConstraints": { + "follows_iri_unique": { + "name": "follows_iri_unique", + "nullsNotDistinct": false, + "columns": [ + "iri" + ] + } + } + }, + "public.likes": { + "name": "likes", + "schema": "", + "columns": { + "post_id": { + "name": "post_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "likes_post_id_posts_id_fk": { + "name": "likes_post_id_posts_id_fk", + "tableFrom": "likes", + "tableTo": "posts", + "columnsFrom": [ + "post_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "likes_account_id_accounts_id_fk": { + "name": "likes_account_id_accounts_id_fk", + "tableFrom": "likes", + "tableTo": "accounts", + "columnsFrom": [ + "account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "likes_post_id_account_id_pk": { + "name": "likes_post_id_account_id_pk", + "columns": [ + "post_id", + "account_id" + ] + } + }, + "uniqueConstraints": {} + }, + "public.markers": { + "name": "markers", + "schema": "", + "columns": { + "account_owner_id": { + "name": "account_owner_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "marker_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "last_read_id": { + "name": "last_read_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "updated": { + "name": "updated", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "markers_account_owner_id_account_owners_id_fk": { + "name": "markers_account_owner_id_account_owners_id_fk", + "tableFrom": "markers", + "tableTo": "account_owners", + "columnsFrom": [ + "account_owner_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "markers_account_owner_id_type_pk": { + "name": "markers_account_owner_id_type_pk", + "columns": [ + "account_owner_id", + "type" + ] + } + }, + "uniqueConstraints": {} + }, + "public.media": { + "name": "media", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "post_id": { + "name": "post_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "width": { + "name": "width", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "height": { + "name": "height", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "thumbnail_type": { + "name": "thumbnail_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "thumbnail_url": { + "name": "thumbnail_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "thumbnail_width": { + "name": "thumbnail_width", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "thumbnail_height": { + "name": "thumbnail_height", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "media_post_id_posts_id_fk": { + "name": "media_post_id_posts_id_fk", + "tableFrom": "media", + "tableTo": "posts", + "columnsFrom": [ + "post_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.mentions": { + "name": "mentions", + "schema": "", + "columns": { + "post_id": { + "name": "post_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "mentions_post_id_posts_id_fk": { + "name": "mentions_post_id_posts_id_fk", + "tableFrom": "mentions", + "tableTo": "posts", + "columnsFrom": [ + "post_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mentions_account_id_accounts_id_fk": { + "name": "mentions_account_id_accounts_id_fk", + "tableFrom": "mentions", + "tableTo": "accounts", + "columnsFrom": [ + "account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "mentions_post_id_account_id_pk": { + "name": "mentions_post_id_account_id_pk", + "columns": [ + "post_id", + "account_id" + ] + } + }, + "uniqueConstraints": {} + }, + "public.pinned_posts": { + "name": "pinned_posts", + "schema": "", + "columns": { + "index": { + "name": "index", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "post_id": { + "name": "post_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "pinned_posts_account_id_accounts_id_fk": { + "name": "pinned_posts_account_id_accounts_id_fk", + "tableFrom": "pinned_posts", + "tableTo": "accounts", + "columnsFrom": [ + "account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "pinned_posts_post_id_account_id_posts_id_actor_id_fk": { + "name": "pinned_posts_post_id_account_id_posts_id_actor_id_fk", + "tableFrom": "pinned_posts", + "tableTo": "posts", + "columnsFrom": [ + "post_id", + "account_id" + ], + "columnsTo": [ + "id", + "actor_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "pinned_posts_post_id_account_id_unique": { + "name": "pinned_posts_post_id_account_id_unique", + "nullsNotDistinct": false, + "columns": [ + "post_id", + "account_id" + ] + } + } + }, + "public.posts": { + "name": "posts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "iri": { + "name": "iri", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "post_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "actor_id": { + "name": "actor_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "application_id": { + "name": "application_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "reply_target_id": { + "name": "reply_target_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "sharing_id": { + "name": "sharing_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "visibility": { + "name": "visibility", + "type": "post_visibility", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "summary_html": { + "name": "summary_html", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "content_html": { + "name": "content_html", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "language": { + "name": "language", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tags": { + "name": "tags", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "sensitive": { + "name": "sensitive", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "preview_card": { + "name": "preview_card", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "replies_count": { + "name": "replies_count", + "type": "bigint", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "shares_count": { + "name": "shares_count", + "type": "bigint", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "likes_count": { + "name": "likes_count", + "type": "bigint", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "published": { + "name": "published", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "updated": { + "name": "updated", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "posts_actor_id_accounts_id_fk": { + "name": "posts_actor_id_accounts_id_fk", + "tableFrom": "posts", + "tableTo": "accounts", + "columnsFrom": [ + "actor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "posts_application_id_applications_id_fk": { + "name": "posts_application_id_applications_id_fk", + "tableFrom": "posts", + "tableTo": "applications", + "columnsFrom": [ + "application_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "posts_reply_target_id_posts_id_fk": { + "name": "posts_reply_target_id_posts_id_fk", + "tableFrom": "posts", + "tableTo": "posts", + "columnsFrom": [ + "reply_target_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "posts_sharing_id_posts_id_fk": { + "name": "posts_sharing_id_posts_id_fk", + "tableFrom": "posts", + "tableTo": "posts", + "columnsFrom": [ + "sharing_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "posts_iri_unique": { + "name": "posts_iri_unique", + "nullsNotDistinct": false, + "columns": [ + "iri" + ] + }, + "posts_id_actor_id_unique": { + "name": "posts_id_actor_id_unique", + "nullsNotDistinct": false, + "columns": [ + "id", + "actor_id" + ] + } + } + } + }, + "enums": { + "public.account_type": { + "name": "account_type", + "schema": "public", + "values": [ + "Application", + "Group", + "Organization", + "Person", + "Service" + ] + }, + "public.grant_type": { + "name": "grant_type", + "schema": "public", + "values": [ + "authorization_code", + "client_credentials" + ] + }, + "public.marker_type": { + "name": "marker_type", + "schema": "public", + "values": [ + "notifications", + "home" + ] + }, + "public.post_type": { + "name": "post_type", + "schema": "public", + "values": [ + "Article", + "Note" + ] + }, + "public.post_visibility": { + "name": "post_visibility", + "schema": "public", + "values": [ + "public", + "unlisted", + "private", + "direct" + ] + }, + "public.scope": { + "name": "scope", + "schema": "public", + "values": [ + "read", + "read:accounts", + "read:blocks", + "read:bookmarks", + "read:favourites", + "read:filters", + "read:follows", + "read:lists", + "read:mutes", + "read:notifications", + "read:search", + "read:statuses", + "write", + "write:accounts", + "write:blocks", + "write:bookmarks", + "write:conversations", + "write:favourites", + "write:filters", + "write:follows", + "write:lists", + "write:media", + "write:mutes", + "write:notifications", + "write:reports", + "write:statuses", + "follow", + "push" + ] + } + }, + "schemas": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index 98a8858..8b888d7 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -169,6 +169,20 @@ "when": 1718518714023, "tag": "0023_ed25519-keys", "breakpoints": true + }, + { + "idx": 24, + "version": "6", + "when": 1719304863977, + "tag": "0024_pinned_posts", + "breakpoints": true + }, + { + "idx": 25, + "version": "6", + "when": 1719307716065, + "tag": "0025_accounts.featured_url", + "breakpoints": true } ] } \ No newline at end of file diff --git a/package.json b/package.json index f182b48..0889c7a 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "dependencies": { "@aws-sdk/client-s3": "^3.577.0", "@aws-sdk/credential-providers": "^3.577.0", - "@fedify/fedify": "^0.11.0-dev.240", + "@fedify/fedify": "^0.11.0-dev.243", "@fedify/markdown-it-hashtag": "0.2.0", "@fedify/markdown-it-mention": "^0.1.1", "@fedify/redis": "^0.1.1", diff --git a/src/accounts.tsx b/src/accounts.tsx index 5bfdea0..ab84d20 100644 --- a/src/accounts.tsx +++ b/src/accounts.tsx @@ -111,6 +111,7 @@ app.post("/", async (c) => { inboxUrl: fedCtx.getInboxUri(username).href, followersUrl: fedCtx.getFollowersUri(username).href, sharedInboxUrl: fedCtx.getInboxUri().href, + featuredUrl: fedCtx.getFeaturedUri(username).href, published: new Date(), }) .returning(); diff --git a/src/api/v1/accounts.ts b/src/api/v1/accounts.ts index 81b83ef..e0368ea 100644 --- a/src/api/v1/accounts.ts +++ b/src/api/v1/accounts.ts @@ -34,6 +34,7 @@ import { accounts, follows, mentions, + pinnedPosts, posts, } from "../../schema"; import { search } from "../../search"; @@ -469,7 +470,7 @@ app.get( ); } const query = c.req.valid("query"); - if (query.pinned || query.only_media) { + if (query.only_media) { return c.json([]); // FIXME } const following = await db @@ -497,6 +498,15 @@ app.get( ), ), ), + query.pinned === "true" + ? inArray( + posts.id, + db + .select({ id: pinnedPosts.postId }) + .from(pinnedPosts) + .where(eq(pinnedPosts.accountId, id)), + ) + : undefined, query.exclude_replies === "true" ? isNull(posts.replyTargetId) : undefined, diff --git a/src/api/v1/statuses.ts b/src/api/v1/statuses.ts index 5c919f1..15bd9d3 100644 --- a/src/api/v1/statuses.ts +++ b/src/api/v1/statuses.ts @@ -1,4 +1,4 @@ -import { Note, Undo } from "@fedify/fedify"; +import { Add, Note, Remove, Undo } from "@fedify/fedify"; import * as vocab from "@fedify/fedify/vocab"; import { zValidator } from "@hono/zod-validator"; import { and, eq, isNull, sql } from "drizzle-orm"; @@ -20,11 +20,13 @@ import { type Like, type NewBookmark, type NewLike, + type NewPinnedPost, type NewPost, bookmarks, likes, media, mentions, + pinnedPosts, posts, } from "../../schema"; import search from "../../search"; @@ -736,4 +738,113 @@ app.post( }, ); +app.post( + "/:id/pin", + tokenRequired, + scopeRequired(["write:accounts"]), + async (c) => { + const owner = c.get("token").accountOwner; + if (owner == null) { + return c.json( + { error: "This method requires an authenticated user" }, + 422, + ); + } + const postId = c.req.param("id"); + const post = await db.query.posts.findFirst({ + where: eq(posts.id, postId), + }); + if (post == null) { + return c.json({ error: "Record not found" }, 404); + } + if (post.accountId !== owner.id) { + return c.json( + { error: "Validation failed: Someone else's post cannot be pinned" }, + 422, + ); + } + const result = await db + .insert(pinnedPosts) + .values({ + postId, + accountId: owner.id, + } satisfies NewPinnedPost) + .returning(); + const fedCtx = federation.createContext(c.req.raw, undefined); + await fedCtx.sendActivity( + owner, + "followers", + new Add({ + id: new URL( + `#add/${result[0].index}`, + fedCtx.getFeaturedUri(owner.handle), + ), + actor: new URL(owner.account.iri), + object: new URL(post.iri), + target: fedCtx.getFeaturedUri(owner.handle), + }), + { + preferSharedInbox: true, + excludeBaseUris: [new URL(c.req.url)], + }, + ); + const resultPost = await db.query.posts.findFirst({ + where: eq(posts.id, postId), + with: getPostRelations(owner.id), + }); + return c.json(serializePost(resultPost!, owner, c.req.url)); + }, +); + +app.post( + "/:id/unpin", + tokenRequired, + scopeRequired(["write:accounts"]), + async (c) => { + const owner = c.get("token").accountOwner; + if (owner == null) { + return c.json( + { error: "This method requires an authenticated user" }, + 422, + ); + } + const postId = c.req.param("id"); + const result = await db + .delete(pinnedPosts) + .where( + and( + eq(pinnedPosts.postId, postId), + eq(pinnedPosts.accountId, owner.id), + ), + ) + .returning(); + if (result.length < 1) { + return c.json({ error: "Record not found" }, 404); + } + const post = await db.query.posts.findFirst({ + where: eq(posts.id, postId), + with: getPostRelations(owner.id), + }); + const fedCtx = federation.createContext(c.req.raw, undefined); + await fedCtx.sendActivity( + owner, + "followers", + new Remove({ + id: new URL( + `#remove/${result[0].index}`, + fedCtx.getFeaturedUri(owner.handle), + ), + actor: new URL(owner.account.iri), + object: new URL(post!.iri), + target: fedCtx.getFeaturedUri(owner.handle), + }), + { + preferSharedInbox: true, + excludeBaseUris: [new URL(c.req.url)], + }, + ); + return c.json(serializePost(post!, owner, c.req.url)); + }, +); + export default app; diff --git a/src/components/Post.tsx b/src/components/Post.tsx index 87b75b2..2534263 100644 --- a/src/components/Post.tsx +++ b/src/components/Post.tsx @@ -14,14 +14,15 @@ export interface PostProps { | null; replyTarget: (DbPost & { account: Account }) | null; }; + pinned?: boolean; } -export const Post: FC = ({ post }) => { +export const Post: FC = ({ post, pinned }) => { if (post.sharing != null) return ; const account = post.account; return ( -
+
{account.avatarUrl && ( @@ -74,6 +75,7 @@ export const Post: FC = ({ post }) => { + {pinned ? · Pinned : ""}

diff --git a/src/entities/status.ts b/src/entities/status.ts index d7e872c..54e962d 100644 --- a/src/entities/status.ts +++ b/src/entities/status.ts @@ -8,6 +8,7 @@ import { type Like, type Medium, type Mention, + type PinnedPost, type Post, bookmarks, likes, @@ -32,6 +33,7 @@ export function getPostRelations(ownerId: string) { likes: { where: eq(likes.accountId, ownerId) }, shares: { where: eq(posts.accountId, ownerId) }, bookmarks: { where: eq(bookmarks.accountOwnerId, ownerId) }, + pin: true, }, }, media: true, @@ -39,6 +41,7 @@ export function getPostRelations(ownerId: string) { likes: { where: eq(likes.accountId, ownerId) }, shares: { where: eq(posts.accountId, ownerId) }, bookmarks: { where: eq(bookmarks.accountOwnerId, ownerId) }, + pin: true, } as const; } @@ -59,6 +62,7 @@ export function serializePost( likes: Like[]; shares: Post[]; bookmarks: Bookmark[]; + pin: PinnedPost | null; }) | null; media: Medium[]; @@ -68,6 +72,7 @@ export function serializePost( likes: Like[]; shares: Post[]; bookmarks: Bookmark[]; + pin: PinnedPost | null; }, currentAccountOwner: { id: string }, baseUrl: URL | string, @@ -97,6 +102,7 @@ export function serializePost( bookmarked: post.bookmarks.some( (bookmark) => bookmark.accountOwnerId === currentAccountOwner.id, ), + pinned: post.pin != null && post.pin.accountId === currentAccountOwner.id, content: post.contentHtml ?? "", reblog: post.sharing == null diff --git a/src/federation/account.ts b/src/federation/account.ts index 5c59f8e..63b8dba 100644 --- a/src/federation/account.ts +++ b/src/federation/account.ts @@ -1,7 +1,9 @@ import { type Actor, + Article, type DocumentLoader, Link, + Note, PropertyValue, getActorHandle, getActorTypeName, @@ -22,7 +24,10 @@ import type { PostgresJsQueryResultHKT } from "drizzle-orm/postgres-js"; import type MeiliSearch from "meilisearch"; import { uuidv7 } from "uuidv7-js"; import * as schema from "../schema"; +import type { NewPinnedPost, Post } from "../schema"; +import { iterateCollection } from "./collection"; import { toDate } from "./date"; +import { persistPost } from "./post"; export async function persistAccount( db: PgDatabase< @@ -65,6 +70,23 @@ export async function persistAccount( fieldHtmls[attachment.name.toString()] = attachment.value.toString(); } } + const featuredCollection = await actor.getFeatured(opts); + if (featuredCollection != null) { + const posts: Post[] = []; + for await (const item of iterateCollection(featuredCollection, opts)) { + if (item instanceof Note || item instanceof Article) { + const post = await persistPost(db, search, item, options); + if (post == null) continue; + posts.unshift(post); + } + } + for (const post of posts) { + await db.insert(schema.pinnedPosts).values({ + postId: post.id, + accountId: post.accountId, + } satisfies NewPinnedPost); + } + } const values: Omit = { type: getActorTypeName(actor), name: actor?.name?.toString() ?? actor?.preferredUsername?.toString() ?? "", @@ -79,6 +101,7 @@ export async function persistAccount( inboxUrl: actor.inboxId.href, followersUrl: followers?.id?.href, sharedInboxUrl: actor.endpoints?.sharedInbox?.href, + featuredUrl: actor.featuredId?.href, followingCount: (await actor.getFollowing(opts))?.totalItems ?? 0, followersCount: followers?.totalItems ?? 0, postsCount: (await actor.getOutbox(opts))?.totalItems ?? 0, diff --git a/src/federation/collection.ts b/src/federation/collection.ts new file mode 100644 index 0000000..359d78f --- /dev/null +++ b/src/federation/collection.ts @@ -0,0 +1,33 @@ +import { + type Object as APObject, + type Collection, + type DocumentLoader, + Link, +} from "@fedify/fedify"; + +export interface IterateCollectionOptions { + documentLoader?: DocumentLoader; + contextLoader?: DocumentLoader; + suppressError?: boolean; +} + +export async function* iterateCollection( + collection: Collection, + options?: IterateCollectionOptions, +): AsyncIterable { + if (collection.firstId == null) { + for await (const item of collection.getItems(options)) { + if (item instanceof Link) continue; + yield item; + } + return; + } + let part = await collection.getFirst(options); + while (part != null && !(part instanceof Link)) { + for await (const item of part.getItems(options)) { + if (item instanceof Link) continue; + yield item; + } + part = await part.getNext(options); + } +} diff --git a/src/federation/index.ts b/src/federation/index.ts index 2a2b446..5b880a3 100644 --- a/src/federation/index.ts +++ b/src/federation/index.ts @@ -1,6 +1,7 @@ import { Accept, Activity, + Add, Announce, Article, Create, @@ -12,6 +13,7 @@ import { Note, PropertyValue, Reject, + Remove, Undo, Update, createFederation, @@ -37,10 +39,12 @@ import db from "../db"; import redis, { createRedis } from "../redis"; import { type NewLike, + type NewPinnedPost, accountOwners, accounts, follows, likes, + pinnedPosts, posts, } from "../schema"; import { search } from "../search"; @@ -95,6 +99,7 @@ federation following: ctx.getFollowingUri(handle), outbox: ctx.getOutboxUri(handle), liked: ctx.getLikedUri(handle), + featured: ctx.getFeaturedUri(handle), inbox: ctx.getInboxUri(handle), endpoints: new Endpoints({ sharedInbox: ctx.getInboxUri(), @@ -307,6 +312,34 @@ federation return result[0].cnt; }); +federation.setFeaturedDispatcher("/@{handle}/pinned", async (ctx, handle) => { + const owner = await db.query.accountOwners.findFirst({ + where: eq(accountOwners.handle, handle), + with: { account: true }, + }); + if (owner == null) return null; + const items = await db.query.pinnedPosts.findMany({ + where: eq(pinnedPosts.accountId, owner.id), + orderBy: desc(pinnedPosts.index), + with: { + post: { + with: { + account: { with: { owner: true } }, + replyTarget: true, + media: true, + mentions: { with: { account: true } }, + }, + }, + }, + }); + return { + items: items + .map((p) => p.post) + .filter((p) => p.visibility === "public" || p.visibility === "unlisted") + .map((p) => toObject(p, ctx)), + }; +}); + const inboxLogger = getLogger(["hollo", "inbox"]); federation @@ -543,6 +576,48 @@ federation }); } }) + .on(Add, async (ctx, add) => { + if (add.targetId == null) return; + const accountList = await db.query.accounts.findMany({ + where: eq(accounts.featuredUrl, add.targetId.href), + }); + const object = await add.getObject(); + if (object instanceof Note || object instanceof Article) { + await db.transaction(async (tx) => { + const post = await persistPost(tx, search, object, ctx); + if (post == null) return; + for (const account of accountList) { + await tx.insert(pinnedPosts).values({ + postId: post.id, + accountId: account.id, + } satisfies NewPinnedPost); + } + }); + } + }) + .on(Remove, async (ctx, remove) => { + if (remove.targetId == null) return; + const accountList = await db.query.accounts.findMany({ + where: eq(accounts.featuredUrl, remove.targetId.href), + }); + const object = await remove.getObject(); + if (object instanceof Note || object instanceof Article) { + await db.transaction(async (tx) => { + const post = await persistPost(tx, search, object, ctx); + if (post == null) return; + for (const account of accountList) { + await tx + .delete(pinnedPosts) + .where( + and( + eq(pinnedPosts.postId, post.id), + eq(pinnedPosts.accountId, account.id), + ), + ); + } + }); + } + }) .on(Undo, async (ctx, undo) => { const object = await undo.getObject(); if ( diff --git a/src/profile.tsx b/src/profile.tsx index d56ef5b..15fcfff 100644 --- a/src/profile.tsx +++ b/src/profile.tsx @@ -11,6 +11,7 @@ import { type Medium, type Post, accountOwners, + pinnedPosts, posts, } from "./schema"; @@ -44,7 +45,37 @@ app.get("/", async (c) => { replyTarget: { with: { account: true } }, }, }); - return c.html(); + const pinnedPostList = await db.query.pinnedPosts.findMany({ + where: and(eq(pinnedPosts.accountId, owner.id)), + orderBy: desc(pinnedPosts.index), + with: { + post: { + with: { + account: true, + media: true, + sharing: { + with: { + account: true, + media: true, + replyTarget: { with: { account: true } }, + }, + }, + replyTarget: { with: { account: true } }, + }, + }, + }, + }); + return c.html( + p.post) + .filter( + (p) => p.visibility === "public" || p.visibility === "unlisted", + )} + />, + ); }); export interface ProfilePageProps { @@ -61,9 +92,25 @@ export interface ProfilePageProps { | null; replyTarget: (Post & { account: Account }) | null; })[]; + pinnedPosts: (Post & { + account: Account; + media: Medium[]; + sharing: + | (Post & { + account: Account; + media: Medium[]; + replyTarget: (Post & { account: Account }) | null; + }) + | null; + replyTarget: (Post & { account: Account }) | null; + })[]; } -export const ProfilePage: FC = ({ accountOwner, posts }) => { +export const ProfilePage: FC = ({ + accountOwner, + posts, + pinnedPosts, +}) => { return ( = ({ accountOwner, posts }) => { imageUrl={accountOwner.account.avatarUrl} > + {pinnedPosts.map((post) => ( + + ))} {posts.map((post) => ( ))} diff --git a/src/schema.ts b/src/schema.ts index 25fcaf6..6af5bb8 100644 --- a/src/schema.ts +++ b/src/schema.ts @@ -2,7 +2,9 @@ import { relations } from "drizzle-orm"; import { type AnyPgColumn, bigint, + bigserial, boolean, + foreignKey, integer, json, jsonb, @@ -11,6 +13,7 @@ import { primaryKey, text, timestamp, + unique, uuid, varchar, } from "drizzle-orm/pg-core"; @@ -49,6 +52,7 @@ export const accounts = pgTable("accounts", { inboxUrl: text("inbox_url").notNull(), followersUrl: text("followers_url"), sharedInboxUrl: text("shared_inbox_url"), + featuredUrl: text("featured_url"), followingCount: bigint("following_count", { mode: "number" }).default(0), followersCount: bigint("followers_count", { mode: "number" }).default(0), postsCount: bigint("posts_count", { mode: "number" }).default(0), @@ -71,6 +75,7 @@ export const accountRelations = relations(accounts, ({ one, many }) => ({ posts: many(posts), mentions: many(mentions), likes: many(likes), + pinnedPosts: many(pinnedPosts), })); export type Account = typeof accounts.$inferSelect; @@ -251,39 +256,50 @@ export const postTypeEnum = pgEnum("post_type", ["Article", "Note"]); export type PostType = (typeof postTypeEnum.enumValues)[number]; -export const posts = pgTable("posts", { - id: uuid("id").primaryKey(), - iri: text("iri").notNull().unique(), - type: postTypeEnum("type").notNull(), - accountId: uuid("actor_id") - .notNull() - .references(() => accounts.id, { onDelete: "cascade" }), - applicationId: uuid("application_id").references(() => applications.id, { - onDelete: "set null", - }), - replyTargetId: uuid("reply_target_id").references( - (): AnyPgColumn => posts.id, - { onDelete: "set null" }, - ), - sharingId: uuid("sharing_id").references((): AnyPgColumn => posts.id, { - onDelete: "cascade", +export const posts = pgTable( + "posts", + { + id: uuid("id").primaryKey(), + iri: text("iri").notNull().unique(), + type: postTypeEnum("type").notNull(), + accountId: uuid("actor_id") + .notNull() + .references(() => accounts.id, { onDelete: "cascade" }), + applicationId: uuid("application_id").references(() => applications.id, { + onDelete: "set null", + }), + replyTargetId: uuid("reply_target_id").references( + (): AnyPgColumn => posts.id, + { onDelete: "set null" }, + ), + sharingId: uuid("sharing_id").references((): AnyPgColumn => posts.id, { + onDelete: "cascade", + }), + visibility: postVisibilityEnum("visibility").notNull(), + summaryHtml: text("summary_html"), + summary: text("summary"), + contentHtml: text("content_html"), + content: text("content"), + language: text("language"), + tags: jsonb("tags").notNull().default({}).$type>(), + sensitive: boolean("sensitive").notNull().default(false), + url: text("url"), + previewCard: jsonb("preview_card").$type(), + repliesCount: bigint("replies_count", { mode: "number" }).default(0), + sharesCount: bigint("shares_count", { mode: "number" }).default(0), + likesCount: bigint("likes_count", { mode: "number" }).default(0), + published: timestamp("published", { withTimezone: true }), + updated: timestamp("updated", { withTimezone: true }) + .notNull() + .defaultNow(), + }, + (table) => ({ + uniqueIdAccountId: unique("posts_id_actor_id_unique").on( + table.id, + table.accountId, + ), }), - visibility: postVisibilityEnum("visibility").notNull(), - summaryHtml: text("summary_html"), - summary: text("summary"), - contentHtml: text("content_html"), - content: text("content"), - language: text("language"), - tags: jsonb("tags").notNull().default({}).$type>(), - sensitive: boolean("sensitive").notNull().default(false), - url: text("url"), - previewCard: jsonb("preview_card").$type(), - repliesCount: bigint("replies_count", { mode: "number" }).default(0), - sharesCount: bigint("shares_count", { mode: "number" }).default(0), - likesCount: bigint("likes_count", { mode: "number" }).default(0), - published: timestamp("published", { withTimezone: true }), - updated: timestamp("updated", { withTimezone: true }).notNull().defaultNow(), -}); +); export type Post = typeof posts.$inferSelect; export type NewPost = typeof posts.$inferInsert; @@ -317,6 +333,10 @@ export const postRelations = relations(posts, ({ one, many }) => ({ media: many(media), mentions: many(mentions), bookmarks: many(bookmarks), + pin: one(pinnedPosts, { + fields: [posts.id, posts.accountId], + references: [pinnedPosts.postId, pinnedPosts.accountId], + }), })); export const media = pgTable("media", { @@ -373,6 +393,41 @@ export const mentionRelations = relations(mentions, ({ one }) => ({ }), })); +export const pinnedPosts = pgTable( + "pinned_posts", + { + index: bigserial("index", { mode: "number" }).notNull().primaryKey(), + postId: uuid("post_id").notNull(), + accountId: uuid("account_id") + .notNull() + .references(() => accounts.id, { onDelete: "cascade" }), + created: timestamp("created", { withTimezone: true }) + .notNull() + .defaultNow(), + }, + (table) => ({ + uniquePostIdAccountId: unique().on(table.postId, table.accountId), + postReference: foreignKey({ + columns: [table.postId, table.accountId], + foreignColumns: [posts.id, posts.accountId], + }).onDelete("cascade"), + }), +); + +export const pinnedPostRelations = relations(pinnedPosts, ({ one }) => ({ + post: one(posts, { + fields: [pinnedPosts.postId, pinnedPosts.accountId], + references: [posts.id, posts.accountId], + }), + account: one(accounts, { + fields: [pinnedPosts.accountId], + references: [accounts.id], + }), +})); + +export type PinnedPost = typeof pinnedPosts.$inferSelect; +export type NewPinnedPost = typeof pinnedPosts.$inferInsert; + export const likes = pgTable( "likes", {