From e2fa572e84bae12472dde5ddf1ec11e4346a9aa7 Mon Sep 17 00:00:00 2001 From: AskIt Date: Sun, 25 Jan 2026 23:01:29 +0100 Subject: [PATCH] feat(swarm): Implement decentralized node discovery and content federation system - Add swarm node registry with discovery, gossip, and timeline synchronization - Implement database schema for swarm nodes, seeds, and sync logs with proper indexing - Create swarm API endpoints for node discovery, gossip protocol, timeline sharing, and announcements - Add content moderation features with NSFW flagging and muted nodes support - Implement user settings pages for content filtering and node moderation - Add background scheduler for automated swarm synchronization and node health checks - Create .well-known endpoint for swarm protocol discovery - Remove legacy bot-cron.ts in favor of integrated scheduler system - Add user account NSFW preferences and age verification tracking - Update database schema with swarm-related tables and user moderation fields - Enhance post and user components to support federated content display - Add instrumentation for monitoring swarm operations and node health --- bot-cron.ts | 63 - drizzle/0003_small_reavers.sql | 56 + drizzle/0004_luxuriant_lockheed.sql | 21 + drizzle/meta/0003_snapshot.json | 3392 +++++++++++++++++ drizzle/meta/0004_snapshot.json | 3592 +++++++++++++++++++ drizzle/meta/_journal.json | 14 + src/app/.well-known/synapsis-swarm/route.ts | 77 + src/app/[handle]/page.tsx | 93 +- src/app/[handle]/posts/[id]/page.tsx | 4 +- src/app/admin/page.tsx | 45 + src/app/api/admin/node/route.ts | 2 + src/app/api/cron/swarm/route.ts | 55 + src/app/api/posts/route.ts | 90 +- src/app/api/posts/swarm/route.ts | 58 + src/app/api/settings/account-nsfw/route.ts | 53 + src/app/api/settings/blocked-users/route.ts | 64 + src/app/api/settings/muted-nodes/route.ts | 99 + src/app/api/settings/nsfw/route.ts | 104 + src/app/api/swarm/announce/route.ts | 94 + src/app/api/swarm/gossip/route.ts | 86 + src/app/api/swarm/info/route.ts | 39 + src/app/api/swarm/nodes/route.ts | 143 + src/app/api/swarm/timeline/route.ts | 122 + src/app/api/users/[handle]/block/route.ts | 129 + src/app/explore/page.tsx | 105 +- src/app/globals.css | 111 + src/app/page.tsx | 4 +- src/app/settings/content/page.tsx | 380 ++ src/app/settings/moderation/page.tsx | 313 ++ src/app/settings/page.tsx | 34 +- src/components/Compose.tsx | 40 +- src/components/PostCard.tsx | 213 +- src/db/schema.ts | 158 + src/instrumentation.ts | 19 + src/lib/background/scheduler.ts | 95 + src/lib/swarm/discovery.ts | 200 ++ src/lib/swarm/gossip.ts | 237 ++ src/lib/swarm/index.ts | 20 + src/lib/swarm/registry.ts | 311 ++ src/lib/swarm/timeline.ts | 129 + src/lib/swarm/types.ts | 128 + src/lib/types.ts | 1 + 42 files changed, 10829 insertions(+), 164 deletions(-) delete mode 100644 bot-cron.ts create mode 100644 drizzle/0003_small_reavers.sql create mode 100644 drizzle/0004_luxuriant_lockheed.sql create mode 100644 drizzle/meta/0003_snapshot.json create mode 100644 drizzle/meta/0004_snapshot.json create mode 100644 src/app/.well-known/synapsis-swarm/route.ts create mode 100644 src/app/api/cron/swarm/route.ts create mode 100644 src/app/api/posts/swarm/route.ts create mode 100644 src/app/api/settings/account-nsfw/route.ts create mode 100644 src/app/api/settings/blocked-users/route.ts create mode 100644 src/app/api/settings/muted-nodes/route.ts create mode 100644 src/app/api/settings/nsfw/route.ts create mode 100644 src/app/api/swarm/announce/route.ts create mode 100644 src/app/api/swarm/gossip/route.ts create mode 100644 src/app/api/swarm/info/route.ts create mode 100644 src/app/api/swarm/nodes/route.ts create mode 100644 src/app/api/swarm/timeline/route.ts create mode 100644 src/app/api/users/[handle]/block/route.ts create mode 100644 src/app/settings/content/page.tsx create mode 100644 src/app/settings/moderation/page.tsx create mode 100644 src/instrumentation.ts create mode 100644 src/lib/background/scheduler.ts create mode 100644 src/lib/swarm/discovery.ts create mode 100644 src/lib/swarm/gossip.ts create mode 100644 src/lib/swarm/index.ts create mode 100644 src/lib/swarm/registry.ts create mode 100644 src/lib/swarm/timeline.ts create mode 100644 src/lib/swarm/types.ts diff --git a/bot-cron.ts b/bot-cron.ts deleted file mode 100644 index 2567779..0000000 --- a/bot-cron.ts +++ /dev/null @@ -1,63 +0,0 @@ -/** - * Bot Cron Job Script - * - * Run with PM2: - * pm2 start bot-cron.ts --name "bot-cron" --cron "* * * * *" --no-autorestart - * - * Or for continuous running with internal interval: - * pm2 start bot-cron.ts --name "bot-cron" - */ - -const INTERVAL_MS = 60 * 1000; // 1 minute -const API_URL = process.env.NEXT_PUBLIC_NODE_DOMAIN - ? `https://${process.env.NEXT_PUBLIC_NODE_DOMAIN}/api/cron/bots` - : 'http://localhost:3000/api/cron/bots'; -const AUTH_SECRET = process.env.AUTH_SECRET || ''; - -async function runCron() { - const timestamp = new Date().toISOString(); - console.log(`[${timestamp}] Running bot cron job...`); - - try { - const headers: Record = { - 'Content-Type': 'application/json', - }; - - if (AUTH_SECRET) { - headers['Authorization'] = `Bearer ${AUTH_SECRET}`; - } - - const response = await fetch(API_URL, { - method: 'POST', - headers, - }); - - const data = await response.json(); - - if (response.ok) { - console.log(`[${timestamp}] Cron completed:`, JSON.stringify(data, null, 2)); - } else { - console.error(`[${timestamp}] Cron failed:`, data); - } - } catch (error) { - console.error(`[${timestamp}] Cron error:`, error); - } -} - -// Check if running with PM2 cron (single execution) or continuous mode -const isPM2Cron = process.env.PM2_CRON === 'true' || process.argv.includes('--once'); - -if (isPM2Cron) { - // Single execution mode (PM2 handles scheduling) - runCron().then(() => process.exit(0)); -} else { - // Continuous mode with internal interval - console.log(`Bot cron started. Running every ${INTERVAL_MS / 1000} seconds.`); - console.log(`API URL: ${API_URL}`); - - // Run immediately on start - runCron(); - - // Then run on interval - setInterval(runCron, INTERVAL_MS); -} diff --git a/drizzle/0003_small_reavers.sql b/drizzle/0003_small_reavers.sql new file mode 100644 index 0000000..77911b9 --- /dev/null +++ b/drizzle/0003_small_reavers.sql @@ -0,0 +1,56 @@ +CREATE TABLE "swarm_nodes" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "domain" text NOT NULL, + "name" text, + "description" text, + "logo_url" text, + "public_key" text, + "software_version" text, + "user_count" integer, + "post_count" integer, + "discovered_via" text, + "discovered_at" timestamp DEFAULT now() NOT NULL, + "last_seen_at" timestamp DEFAULT now() NOT NULL, + "last_sync_at" timestamp, + "consecutive_failures" integer DEFAULT 0 NOT NULL, + "is_active" boolean DEFAULT true NOT NULL, + "trust_score" integer DEFAULT 50 NOT NULL, + "capabilities" text, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL, + CONSTRAINT "swarm_nodes_domain_unique" UNIQUE("domain") +); +--> statement-breakpoint +CREATE TABLE "swarm_seeds" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "domain" text NOT NULL, + "priority" integer DEFAULT 100 NOT NULL, + "is_enabled" boolean DEFAULT true NOT NULL, + "last_contact_at" timestamp, + "consecutive_failures" integer DEFAULT 0 NOT NULL, + "created_at" timestamp DEFAULT now() NOT NULL, + CONSTRAINT "swarm_seeds_domain_unique" UNIQUE("domain") +); +--> statement-breakpoint +CREATE TABLE "swarm_sync_log" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "remote_domain" text NOT NULL, + "direction" text NOT NULL, + "nodes_received" integer DEFAULT 0 NOT NULL, + "nodes_sent" integer DEFAULT 0 NOT NULL, + "handles_received" integer DEFAULT 0 NOT NULL, + "handles_sent" integer DEFAULT 0 NOT NULL, + "success" boolean NOT NULL, + "error_message" text, + "duration_ms" integer, + "created_at" timestamp DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE INDEX "swarm_nodes_domain_idx" ON "swarm_nodes" USING btree ("domain");--> statement-breakpoint +CREATE INDEX "swarm_nodes_active_idx" ON "swarm_nodes" USING btree ("is_active");--> statement-breakpoint +CREATE INDEX "swarm_nodes_last_seen_idx" ON "swarm_nodes" USING btree ("last_seen_at");--> statement-breakpoint +CREATE INDEX "swarm_nodes_trust_idx" ON "swarm_nodes" USING btree ("trust_score");--> statement-breakpoint +CREATE INDEX "swarm_seeds_enabled_idx" ON "swarm_seeds" USING btree ("is_enabled");--> statement-breakpoint +CREATE INDEX "swarm_seeds_priority_idx" ON "swarm_seeds" USING btree ("priority");--> statement-breakpoint +CREATE INDEX "swarm_sync_log_remote_idx" ON "swarm_sync_log" USING btree ("remote_domain");--> statement-breakpoint +CREATE INDEX "swarm_sync_log_created_idx" ON "swarm_sync_log" USING btree ("created_at"); \ No newline at end of file diff --git a/drizzle/0004_luxuriant_lockheed.sql b/drizzle/0004_luxuriant_lockheed.sql new file mode 100644 index 0000000..d117519 --- /dev/null +++ b/drizzle/0004_luxuriant_lockheed.sql @@ -0,0 +1,21 @@ +CREATE TABLE "muted_nodes" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "user_id" uuid NOT NULL, + "node_domain" text NOT NULL, + "created_at" timestamp DEFAULT now() NOT NULL +); +--> statement-breakpoint +ALTER TABLE "nodes" ADD COLUMN "is_nsfw" boolean DEFAULT false NOT NULL;--> statement-breakpoint +ALTER TABLE "posts" ADD COLUMN "is_nsfw" boolean DEFAULT false NOT NULL;--> statement-breakpoint +ALTER TABLE "swarm_nodes" ADD COLUMN "is_nsfw" boolean DEFAULT false NOT NULL;--> statement-breakpoint +ALTER TABLE "users" ADD COLUMN "is_nsfw" boolean DEFAULT false NOT NULL;--> statement-breakpoint +ALTER TABLE "users" ADD COLUMN "nsfw_enabled" boolean DEFAULT false NOT NULL;--> statement-breakpoint +ALTER TABLE "users" ADD COLUMN "age_verified_at" timestamp;--> statement-breakpoint +ALTER TABLE "muted_nodes" ADD CONSTRAINT "muted_nodes_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +CREATE INDEX "muted_nodes_user_idx" ON "muted_nodes" USING btree ("user_id");--> statement-breakpoint +CREATE INDEX "muted_nodes_domain_idx" ON "muted_nodes" USING btree ("node_domain");--> statement-breakpoint +CREATE INDEX "blocks_blocked_user_idx" ON "blocks" USING btree ("blocked_user_id");--> statement-breakpoint +CREATE INDEX "mutes_muted_user_idx" ON "mutes" USING btree ("muted_user_id");--> statement-breakpoint +CREATE INDEX "posts_nsfw_idx" ON "posts" USING btree ("is_nsfw");--> statement-breakpoint +CREATE INDEX "swarm_nodes_nsfw_idx" ON "swarm_nodes" USING btree ("is_nsfw");--> statement-breakpoint +CREATE INDEX "users_nsfw_idx" ON "users" USING btree ("is_nsfw"); \ No newline at end of file diff --git a/drizzle/meta/0003_snapshot.json b/drizzle/meta/0003_snapshot.json new file mode 100644 index 0000000..77f6289 --- /dev/null +++ b/drizzle/meta/0003_snapshot.json @@ -0,0 +1,3392 @@ +{ + "id": "44238f9d-2c06-4033-bfba-0e51117d8048", + "prevId": "9320c2d1-0596-438b-b795-6ce615488949", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.blocks": { + "name": "blocks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "blocked_user_id": { + "name": "blocked_user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "blocks_user_idx": { + "name": "blocks_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "blocks_user_id_users_id_fk": { + "name": "blocks_user_id_users_id_fk", + "tableFrom": "blocks", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "blocks_blocked_user_id_users_id_fk": { + "name": "blocks_blocked_user_id_users_id_fk", + "tableFrom": "blocks", + "tableTo": "users", + "columnsFrom": [ + "blocked_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.bot_activity_logs": { + "name": "bot_activity_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "bot_id": { + "name": "bot_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "details": { + "name": "details", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "success": { + "name": "success", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "bot_activity_logs_bot_idx": { + "name": "bot_activity_logs_bot_idx", + "columns": [ + { + "expression": "bot_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "bot_activity_logs_action_idx": { + "name": "bot_activity_logs_action_idx", + "columns": [ + { + "expression": "action", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "bot_activity_logs_created_idx": { + "name": "bot_activity_logs_created_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "bot_activity_logs_bot_id_bots_id_fk": { + "name": "bot_activity_logs_bot_id_bots_id_fk", + "tableFrom": "bot_activity_logs", + "tableTo": "bots", + "columnsFrom": [ + "bot_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.bot_content_items": { + "name": "bot_content_items", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "source_id": { + "name": "source_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "external_id": { + "name": "external_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "published_at": { + "name": "published_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "fetched_at": { + "name": "fetched_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "is_processed": { + "name": "is_processed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "processed_at": { + "name": "processed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "post_id": { + "name": "post_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "interest_score": { + "name": "interest_score", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "interest_reason": { + "name": "interest_reason", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "bot_content_items_source_idx": { + "name": "bot_content_items_source_idx", + "columns": [ + { + "expression": "source_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "bot_content_items_processed_idx": { + "name": "bot_content_items_processed_idx", + "columns": [ + { + "expression": "is_processed", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "bot_content_items_external_idx": { + "name": "bot_content_items_external_idx", + "columns": [ + { + "expression": "external_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "bot_content_items_source_id_bot_content_sources_id_fk": { + "name": "bot_content_items_source_id_bot_content_sources_id_fk", + "tableFrom": "bot_content_items", + "tableTo": "bot_content_sources", + "columnsFrom": [ + "source_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "bot_content_items_post_id_posts_id_fk": { + "name": "bot_content_items_post_id_posts_id_fk", + "tableFrom": "bot_content_items", + "tableTo": "posts", + "columnsFrom": [ + "post_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.bot_content_sources": { + "name": "bot_content_sources", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "bot_id": { + "name": "bot_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "subreddit": { + "name": "subreddit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "api_key_encrypted": { + "name": "api_key_encrypted", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_config": { + "name": "source_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "keywords": { + "name": "keywords", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "last_fetch_at": { + "name": "last_fetch_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "consecutive_errors": { + "name": "consecutive_errors", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "bot_content_sources_bot_idx": { + "name": "bot_content_sources_bot_idx", + "columns": [ + { + "expression": "bot_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "bot_content_sources_type_idx": { + "name": "bot_content_sources_type_idx", + "columns": [ + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "bot_content_sources_bot_id_bots_id_fk": { + "name": "bot_content_sources_bot_id_bots_id_fk", + "tableFrom": "bot_content_sources", + "tableTo": "bots", + "columnsFrom": [ + "bot_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.bot_mentions": { + "name": "bot_mentions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "bot_id": { + "name": "bot_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "post_id": { + "name": "post_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "author_id": { + "name": "author_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_processed": { + "name": "is_processed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "processed_at": { + "name": "processed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "response_post_id": { + "name": "response_post_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "is_remote": { + "name": "is_remote", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "remote_actor_url": { + "name": "remote_actor_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "bot_mentions_bot_idx": { + "name": "bot_mentions_bot_idx", + "columns": [ + { + "expression": "bot_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "bot_mentions_processed_idx": { + "name": "bot_mentions_processed_idx", + "columns": [ + { + "expression": "is_processed", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "bot_mentions_created_idx": { + "name": "bot_mentions_created_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "bot_mentions_bot_id_bots_id_fk": { + "name": "bot_mentions_bot_id_bots_id_fk", + "tableFrom": "bot_mentions", + "tableTo": "bots", + "columnsFrom": [ + "bot_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "bot_mentions_post_id_posts_id_fk": { + "name": "bot_mentions_post_id_posts_id_fk", + "tableFrom": "bot_mentions", + "tableTo": "posts", + "columnsFrom": [ + "post_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "bot_mentions_author_id_users_id_fk": { + "name": "bot_mentions_author_id_users_id_fk", + "tableFrom": "bot_mentions", + "tableTo": "users", + "columnsFrom": [ + "author_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "bot_mentions_response_post_id_posts_id_fk": { + "name": "bot_mentions_response_post_id_posts_id_fk", + "tableFrom": "bot_mentions", + "tableTo": "posts", + "columnsFrom": [ + "response_post_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.bot_rate_limits": { + "name": "bot_rate_limits", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "bot_id": { + "name": "bot_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "window_start": { + "name": "window_start", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "window_type": { + "name": "window_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "post_count": { + "name": "post_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "reply_count": { + "name": "reply_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "bot_rate_limits_bot_window_idx": { + "name": "bot_rate_limits_bot_window_idx", + "columns": [ + { + "expression": "bot_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "window_start", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "bot_rate_limits_bot_id_bots_id_fk": { + "name": "bot_rate_limits_bot_id_bots_id_fk", + "tableFrom": "bot_rate_limits", + "tableTo": "bots", + "columnsFrom": [ + "bot_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.bots": { + "name": "bots", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "owner_id": { + "name": "owner_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "personality_config": { + "name": "personality_config", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "llm_provider": { + "name": "llm_provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "llm_model": { + "name": "llm_model", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "llm_api_key_encrypted": { + "name": "llm_api_key_encrypted", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "schedule_config": { + "name": "schedule_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "autonomous_mode": { + "name": "autonomous_mode", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "is_suspended": { + "name": "is_suspended", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "suspension_reason": { + "name": "suspension_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "suspended_at": { + "name": "suspended_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_post_at": { + "name": "last_post_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "bots_user_id_idx": { + "name": "bots_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "bots_owner_id_idx": { + "name": "bots_owner_id_idx", + "columns": [ + { + "expression": "owner_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "bots_active_idx": { + "name": "bots_active_idx", + "columns": [ + { + "expression": "is_active", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "bots_user_id_users_id_fk": { + "name": "bots_user_id_users_id_fk", + "tableFrom": "bots", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "bots_owner_id_users_id_fk": { + "name": "bots_owner_id_users_id_fk", + "tableFrom": "bots", + "tableTo": "users", + "columnsFrom": [ + "owner_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.follows": { + "name": "follows", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "follower_id": { + "name": "follower_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "following_id": { + "name": "following_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "ap_id": { + "name": "ap_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pending": { + "name": "pending", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "follows_follower_idx": { + "name": "follows_follower_idx", + "columns": [ + { + "expression": "follower_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "follows_following_idx": { + "name": "follows_following_idx", + "columns": [ + { + "expression": "following_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "follows_follower_id_users_id_fk": { + "name": "follows_follower_id_users_id_fk", + "tableFrom": "follows", + "tableTo": "users", + "columnsFrom": [ + "follower_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "follows_following_id_users_id_fk": { + "name": "follows_following_id_users_id_fk", + "tableFrom": "follows", + "tableTo": "users", + "columnsFrom": [ + "following_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "follows_ap_id_unique": { + "name": "follows_ap_id_unique", + "nullsNotDistinct": false, + "columns": [ + "ap_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.handle_registry": { + "name": "handle_registry", + "schema": "", + "columns": { + "handle": { + "name": "handle", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "did": { + "name": "did", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "node_domain": { + "name": "node_domain", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "registered_at": { + "name": "registered_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "handle_registry_updated_idx": { + "name": "handle_registry_updated_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.likes": { + "name": "likes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "post_id": { + "name": "post_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "ap_id": { + "name": "ap_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "likes_user_post_idx": { + "name": "likes_user_post_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "post_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "likes_user_id_users_id_fk": { + "name": "likes_user_id_users_id_fk", + "tableFrom": "likes", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "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" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "likes_ap_id_unique": { + "name": "likes_ap_id_unique", + "nullsNotDistinct": false, + "columns": [ + "ap_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.media": { + "name": "media", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "post_id": { + "name": "post_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "alt_text": { + "name": "alt_text", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mime_type": { + "name": "mime_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "width": { + "name": "width", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "height": { + "name": "height", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "media_user_idx": { + "name": "media_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "media_post_idx": { + "name": "media_post_idx", + "columns": [ + { + "expression": "post_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "media_user_id_users_id_fk": { + "name": "media_user_id_users_id_fk", + "tableFrom": "media", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "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": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mutes": { + "name": "mutes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "muted_user_id": { + "name": "muted_user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "mutes_user_idx": { + "name": "mutes_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mutes_user_id_users_id_fk": { + "name": "mutes_user_id_users_id_fk", + "tableFrom": "mutes", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mutes_muted_user_id_users_id_fk": { + "name": "mutes_muted_user_id_users_id_fk", + "tableFrom": "mutes", + "tableTo": "users", + "columnsFrom": [ + "muted_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.nodes": { + "name": "nodes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "long_description": { + "name": "long_description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rules": { + "name": "rules", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "banner_url": { + "name": "banner_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "logo_url": { + "name": "logo_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "accent_color": { + "name": "accent_color", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'#FFFFFF'" + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "nodes_domain_unique": { + "name": "nodes_domain_unique", + "nullsNotDistinct": false, + "columns": [ + "domain" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.notifications": { + "name": "notifications", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "actor_id": { + "name": "actor_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "post_id": { + "name": "post_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "read_at": { + "name": "read_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "notifications_user_idx": { + "name": "notifications_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "notifications_created_idx": { + "name": "notifications_created_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "notifications_user_id_users_id_fk": { + "name": "notifications_user_id_users_id_fk", + "tableFrom": "notifications", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "notifications_actor_id_users_id_fk": { + "name": "notifications_actor_id_users_id_fk", + "tableFrom": "notifications", + "tableTo": "users", + "columnsFrom": [ + "actor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "notifications_post_id_posts_id_fk": { + "name": "notifications_post_id_posts_id_fk", + "tableFrom": "notifications", + "tableTo": "posts", + "columnsFrom": [ + "post_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.posts": { + "name": "posts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "bot_id": { + "name": "bot_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reply_to_id": { + "name": "reply_to_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "repost_of_id": { + "name": "repost_of_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "likes_count": { + "name": "likes_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "reposts_count": { + "name": "reposts_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "replies_count": { + "name": "replies_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "is_removed": { + "name": "is_removed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "removed_at": { + "name": "removed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "removed_by": { + "name": "removed_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "removed_reason": { + "name": "removed_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ap_id": { + "name": "ap_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ap_url": { + "name": "ap_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "link_preview_url": { + "name": "link_preview_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "link_preview_title": { + "name": "link_preview_title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "link_preview_description": { + "name": "link_preview_description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "link_preview_image": { + "name": "link_preview_image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "posts_user_id_idx": { + "name": "posts_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "posts_bot_id_idx": { + "name": "posts_bot_id_idx", + "columns": [ + { + "expression": "bot_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "posts_created_at_idx": { + "name": "posts_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "posts_reply_to_idx": { + "name": "posts_reply_to_idx", + "columns": [ + { + "expression": "reply_to_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "posts_removed_idx": { + "name": "posts_removed_idx", + "columns": [ + { + "expression": "is_removed", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "posts_user_id_users_id_fk": { + "name": "posts_user_id_users_id_fk", + "tableFrom": "posts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "posts_bot_id_bots_id_fk": { + "name": "posts_bot_id_bots_id_fk", + "tableFrom": "posts", + "tableTo": "bots", + "columnsFrom": [ + "bot_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "posts_removed_by_users_id_fk": { + "name": "posts_removed_by_users_id_fk", + "tableFrom": "posts", + "tableTo": "users", + "columnsFrom": [ + "removed_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "posts_ap_id_unique": { + "name": "posts_ap_id_unique", + "nullsNotDistinct": false, + "columns": [ + "ap_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.remote_followers": { + "name": "remote_followers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "actor_url": { + "name": "actor_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "inbox_url": { + "name": "inbox_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "shared_inbox_url": { + "name": "shared_inbox_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "handle": { + "name": "handle", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "activity_id": { + "name": "activity_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "remote_followers_user_idx": { + "name": "remote_followers_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "remote_followers_actor_idx": { + "name": "remote_followers_actor_idx", + "columns": [ + { + "expression": "actor_url", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "remote_followers_user_id_users_id_fk": { + "name": "remote_followers_user_id_users_id_fk", + "tableFrom": "remote_followers", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "remote_followers_actor_url_unique": { + "name": "remote_followers_actor_url_unique", + "nullsNotDistinct": false, + "columns": [ + "actor_url" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.remote_follows": { + "name": "remote_follows", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "follower_id": { + "name": "follower_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "target_handle": { + "name": "target_handle", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_actor_url": { + "name": "target_actor_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "inbox_url": { + "name": "inbox_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "activity_id": { + "name": "activity_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bio": { + "name": "bio", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "avatar_url": { + "name": "avatar_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "remote_follows_follower_idx": { + "name": "remote_follows_follower_idx", + "columns": [ + { + "expression": "follower_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "remote_follows_target_idx": { + "name": "remote_follows_target_idx", + "columns": [ + { + "expression": "target_handle", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "remote_follows_follower_id_users_id_fk": { + "name": "remote_follows_follower_id_users_id_fk", + "tableFrom": "remote_follows", + "tableTo": "users", + "columnsFrom": [ + "follower_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.remote_posts": { + "name": "remote_posts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "ap_id": { + "name": "ap_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "author_handle": { + "name": "author_handle", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "author_actor_url": { + "name": "author_actor_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "author_display_name": { + "name": "author_display_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "author_avatar_url": { + "name": "author_avatar_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "published_at": { + "name": "published_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "link_preview_url": { + "name": "link_preview_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "link_preview_title": { + "name": "link_preview_title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "link_preview_description": { + "name": "link_preview_description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "link_preview_image": { + "name": "link_preview_image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "media_json": { + "name": "media_json", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "fetched_at": { + "name": "fetched_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "remote_posts_author_idx": { + "name": "remote_posts_author_idx", + "columns": [ + { + "expression": "author_handle", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "remote_posts_published_idx": { + "name": "remote_posts_published_idx", + "columns": [ + { + "expression": "published_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "remote_posts_ap_id_idx": { + "name": "remote_posts_ap_id_idx", + "columns": [ + { + "expression": "ap_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "remote_posts_ap_id_unique": { + "name": "remote_posts_ap_id_unique", + "nullsNotDistinct": false, + "columns": [ + "ap_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.reports": { + "name": "reports", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "reporter_id": { + "name": "reporter_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "target_type": { + "name": "target_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_id": { + "name": "target_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'open'" + }, + "resolved_at": { + "name": "resolved_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "resolved_by": { + "name": "resolved_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "resolution_note": { + "name": "resolution_note", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "reports_status_idx": { + "name": "reports_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "reports_target_idx": { + "name": "reports_target_idx", + "columns": [ + { + "expression": "target_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "reports_reporter_idx": { + "name": "reports_reporter_idx", + "columns": [ + { + "expression": "reporter_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "reports_reporter_id_users_id_fk": { + "name": "reports_reporter_id_users_id_fk", + "tableFrom": "reports", + "tableTo": "users", + "columnsFrom": [ + "reporter_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "reports_resolved_by_users_id_fk": { + "name": "reports_resolved_by_users_id_fk", + "tableFrom": "reports", + "tableTo": "users", + "columnsFrom": [ + "resolved_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sessions": { + "name": "sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "sessions_token_idx": { + "name": "sessions_token_idx", + "columns": [ + { + "expression": "token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sessions_user_idx": { + "name": "sessions_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "sessions_token_unique": { + "name": "sessions_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.swarm_nodes": { + "name": "swarm_nodes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "logo_url": { + "name": "logo_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "software_version": { + "name": "software_version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_count": { + "name": "user_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "post_count": { + "name": "post_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "discovered_via": { + "name": "discovered_via", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "discovered_at": { + "name": "discovered_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_seen_at": { + "name": "last_seen_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_sync_at": { + "name": "last_sync_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "consecutive_failures": { + "name": "consecutive_failures", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "trust_score": { + "name": "trust_score", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 50 + }, + "capabilities": { + "name": "capabilities", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "swarm_nodes_domain_idx": { + "name": "swarm_nodes_domain_idx", + "columns": [ + { + "expression": "domain", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "swarm_nodes_active_idx": { + "name": "swarm_nodes_active_idx", + "columns": [ + { + "expression": "is_active", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "swarm_nodes_last_seen_idx": { + "name": "swarm_nodes_last_seen_idx", + "columns": [ + { + "expression": "last_seen_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "swarm_nodes_trust_idx": { + "name": "swarm_nodes_trust_idx", + "columns": [ + { + "expression": "trust_score", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "swarm_nodes_domain_unique": { + "name": "swarm_nodes_domain_unique", + "nullsNotDistinct": false, + "columns": [ + "domain" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.swarm_seeds": { + "name": "swarm_seeds", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "priority": { + "name": "priority", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 100 + }, + "is_enabled": { + "name": "is_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "last_contact_at": { + "name": "last_contact_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "consecutive_failures": { + "name": "consecutive_failures", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "swarm_seeds_enabled_idx": { + "name": "swarm_seeds_enabled_idx", + "columns": [ + { + "expression": "is_enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "swarm_seeds_priority_idx": { + "name": "swarm_seeds_priority_idx", + "columns": [ + { + "expression": "priority", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "swarm_seeds_domain_unique": { + "name": "swarm_seeds_domain_unique", + "nullsNotDistinct": false, + "columns": [ + "domain" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.swarm_sync_log": { + "name": "swarm_sync_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "remote_domain": { + "name": "remote_domain", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "direction": { + "name": "direction", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "nodes_received": { + "name": "nodes_received", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "nodes_sent": { + "name": "nodes_sent", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "handles_received": { + "name": "handles_received", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "handles_sent": { + "name": "handles_sent", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "success": { + "name": "success", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "duration_ms": { + "name": "duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "swarm_sync_log_remote_idx": { + "name": "swarm_sync_log_remote_idx", + "columns": [ + { + "expression": "remote_domain", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "swarm_sync_log_created_idx": { + "name": "swarm_sync_log_created_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "did": { + "name": "did", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "handle": { + "name": "handle", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bio": { + "name": "bio", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "avatar_url": { + "name": "avatar_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "header_url": { + "name": "header_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "private_key_encrypted": { + "name": "private_key_encrypted", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "node_id": { + "name": "node_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "is_bot": { + "name": "is_bot", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "bot_owner_id": { + "name": "bot_owner_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "is_suspended": { + "name": "is_suspended", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "suspension_reason": { + "name": "suspension_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "suspended_at": { + "name": "suspended_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "is_silenced": { + "name": "is_silenced", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "silence_reason": { + "name": "silence_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "silenced_at": { + "name": "silenced_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "moved_to": { + "name": "moved_to", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "moved_from": { + "name": "moved_from", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "migrated_at": { + "name": "migrated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "followers_count": { + "name": "followers_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "following_count": { + "name": "following_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "posts_count": { + "name": "posts_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "website": { + "name": "website", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "users_handle_idx": { + "name": "users_handle_idx", + "columns": [ + { + "expression": "handle", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "users_did_idx": { + "name": "users_did_idx", + "columns": [ + { + "expression": "did", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "users_suspended_idx": { + "name": "users_suspended_idx", + "columns": [ + { + "expression": "is_suspended", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "users_silenced_idx": { + "name": "users_silenced_idx", + "columns": [ + { + "expression": "is_silenced", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "users_is_bot_idx": { + "name": "users_is_bot_idx", + "columns": [ + { + "expression": "is_bot", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "users_bot_owner_idx": { + "name": "users_bot_owner_idx", + "columns": [ + { + "expression": "bot_owner_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "users_node_id_nodes_id_fk": { + "name": "users_node_id_nodes_id_fk", + "tableFrom": "users", + "tableTo": "nodes", + "columnsFrom": [ + "node_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "users_bot_owner_id_users_id_fk": { + "name": "users_bot_owner_id_users_id_fk", + "tableFrom": "users", + "tableTo": "users", + "columnsFrom": [ + "bot_owner_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "users_did_unique": { + "name": "users_did_unique", + "nullsNotDistinct": false, + "columns": [ + "did" + ] + }, + "users_handle_unique": { + "name": "users_handle_unique", + "nullsNotDistinct": false, + "columns": [ + "handle" + ] + }, + "users_email_unique": { + "name": "users_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/0004_snapshot.json b/drizzle/meta/0004_snapshot.json new file mode 100644 index 0000000..aefaa5c --- /dev/null +++ b/drizzle/meta/0004_snapshot.json @@ -0,0 +1,3592 @@ +{ + "id": "3e8b2279-a62d-4851-9c43-81c2ed361d67", + "prevId": "44238f9d-2c06-4033-bfba-0e51117d8048", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.blocks": { + "name": "blocks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "blocked_user_id": { + "name": "blocked_user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "blocks_user_idx": { + "name": "blocks_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "blocks_blocked_user_idx": { + "name": "blocks_blocked_user_idx", + "columns": [ + { + "expression": "blocked_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "blocks_user_id_users_id_fk": { + "name": "blocks_user_id_users_id_fk", + "tableFrom": "blocks", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "blocks_blocked_user_id_users_id_fk": { + "name": "blocks_blocked_user_id_users_id_fk", + "tableFrom": "blocks", + "tableTo": "users", + "columnsFrom": [ + "blocked_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.bot_activity_logs": { + "name": "bot_activity_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "bot_id": { + "name": "bot_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "details": { + "name": "details", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "success": { + "name": "success", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "bot_activity_logs_bot_idx": { + "name": "bot_activity_logs_bot_idx", + "columns": [ + { + "expression": "bot_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "bot_activity_logs_action_idx": { + "name": "bot_activity_logs_action_idx", + "columns": [ + { + "expression": "action", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "bot_activity_logs_created_idx": { + "name": "bot_activity_logs_created_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "bot_activity_logs_bot_id_bots_id_fk": { + "name": "bot_activity_logs_bot_id_bots_id_fk", + "tableFrom": "bot_activity_logs", + "tableTo": "bots", + "columnsFrom": [ + "bot_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.bot_content_items": { + "name": "bot_content_items", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "source_id": { + "name": "source_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "external_id": { + "name": "external_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "published_at": { + "name": "published_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "fetched_at": { + "name": "fetched_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "is_processed": { + "name": "is_processed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "processed_at": { + "name": "processed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "post_id": { + "name": "post_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "interest_score": { + "name": "interest_score", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "interest_reason": { + "name": "interest_reason", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "bot_content_items_source_idx": { + "name": "bot_content_items_source_idx", + "columns": [ + { + "expression": "source_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "bot_content_items_processed_idx": { + "name": "bot_content_items_processed_idx", + "columns": [ + { + "expression": "is_processed", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "bot_content_items_external_idx": { + "name": "bot_content_items_external_idx", + "columns": [ + { + "expression": "external_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "bot_content_items_source_id_bot_content_sources_id_fk": { + "name": "bot_content_items_source_id_bot_content_sources_id_fk", + "tableFrom": "bot_content_items", + "tableTo": "bot_content_sources", + "columnsFrom": [ + "source_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "bot_content_items_post_id_posts_id_fk": { + "name": "bot_content_items_post_id_posts_id_fk", + "tableFrom": "bot_content_items", + "tableTo": "posts", + "columnsFrom": [ + "post_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.bot_content_sources": { + "name": "bot_content_sources", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "bot_id": { + "name": "bot_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "subreddit": { + "name": "subreddit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "api_key_encrypted": { + "name": "api_key_encrypted", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_config": { + "name": "source_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "keywords": { + "name": "keywords", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "last_fetch_at": { + "name": "last_fetch_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "consecutive_errors": { + "name": "consecutive_errors", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "bot_content_sources_bot_idx": { + "name": "bot_content_sources_bot_idx", + "columns": [ + { + "expression": "bot_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "bot_content_sources_type_idx": { + "name": "bot_content_sources_type_idx", + "columns": [ + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "bot_content_sources_bot_id_bots_id_fk": { + "name": "bot_content_sources_bot_id_bots_id_fk", + "tableFrom": "bot_content_sources", + "tableTo": "bots", + "columnsFrom": [ + "bot_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.bot_mentions": { + "name": "bot_mentions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "bot_id": { + "name": "bot_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "post_id": { + "name": "post_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "author_id": { + "name": "author_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_processed": { + "name": "is_processed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "processed_at": { + "name": "processed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "response_post_id": { + "name": "response_post_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "is_remote": { + "name": "is_remote", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "remote_actor_url": { + "name": "remote_actor_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "bot_mentions_bot_idx": { + "name": "bot_mentions_bot_idx", + "columns": [ + { + "expression": "bot_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "bot_mentions_processed_idx": { + "name": "bot_mentions_processed_idx", + "columns": [ + { + "expression": "is_processed", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "bot_mentions_created_idx": { + "name": "bot_mentions_created_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "bot_mentions_bot_id_bots_id_fk": { + "name": "bot_mentions_bot_id_bots_id_fk", + "tableFrom": "bot_mentions", + "tableTo": "bots", + "columnsFrom": [ + "bot_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "bot_mentions_post_id_posts_id_fk": { + "name": "bot_mentions_post_id_posts_id_fk", + "tableFrom": "bot_mentions", + "tableTo": "posts", + "columnsFrom": [ + "post_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "bot_mentions_author_id_users_id_fk": { + "name": "bot_mentions_author_id_users_id_fk", + "tableFrom": "bot_mentions", + "tableTo": "users", + "columnsFrom": [ + "author_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "bot_mentions_response_post_id_posts_id_fk": { + "name": "bot_mentions_response_post_id_posts_id_fk", + "tableFrom": "bot_mentions", + "tableTo": "posts", + "columnsFrom": [ + "response_post_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.bot_rate_limits": { + "name": "bot_rate_limits", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "bot_id": { + "name": "bot_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "window_start": { + "name": "window_start", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "window_type": { + "name": "window_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "post_count": { + "name": "post_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "reply_count": { + "name": "reply_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "bot_rate_limits_bot_window_idx": { + "name": "bot_rate_limits_bot_window_idx", + "columns": [ + { + "expression": "bot_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "window_start", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "bot_rate_limits_bot_id_bots_id_fk": { + "name": "bot_rate_limits_bot_id_bots_id_fk", + "tableFrom": "bot_rate_limits", + "tableTo": "bots", + "columnsFrom": [ + "bot_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.bots": { + "name": "bots", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "owner_id": { + "name": "owner_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "personality_config": { + "name": "personality_config", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "llm_provider": { + "name": "llm_provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "llm_model": { + "name": "llm_model", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "llm_api_key_encrypted": { + "name": "llm_api_key_encrypted", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "schedule_config": { + "name": "schedule_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "autonomous_mode": { + "name": "autonomous_mode", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "is_suspended": { + "name": "is_suspended", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "suspension_reason": { + "name": "suspension_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "suspended_at": { + "name": "suspended_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_post_at": { + "name": "last_post_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "bots_user_id_idx": { + "name": "bots_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "bots_owner_id_idx": { + "name": "bots_owner_id_idx", + "columns": [ + { + "expression": "owner_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "bots_active_idx": { + "name": "bots_active_idx", + "columns": [ + { + "expression": "is_active", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "bots_user_id_users_id_fk": { + "name": "bots_user_id_users_id_fk", + "tableFrom": "bots", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "bots_owner_id_users_id_fk": { + "name": "bots_owner_id_users_id_fk", + "tableFrom": "bots", + "tableTo": "users", + "columnsFrom": [ + "owner_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.follows": { + "name": "follows", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "follower_id": { + "name": "follower_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "following_id": { + "name": "following_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "ap_id": { + "name": "ap_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pending": { + "name": "pending", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "follows_follower_idx": { + "name": "follows_follower_idx", + "columns": [ + { + "expression": "follower_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "follows_following_idx": { + "name": "follows_following_idx", + "columns": [ + { + "expression": "following_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "follows_follower_id_users_id_fk": { + "name": "follows_follower_id_users_id_fk", + "tableFrom": "follows", + "tableTo": "users", + "columnsFrom": [ + "follower_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "follows_following_id_users_id_fk": { + "name": "follows_following_id_users_id_fk", + "tableFrom": "follows", + "tableTo": "users", + "columnsFrom": [ + "following_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "follows_ap_id_unique": { + "name": "follows_ap_id_unique", + "nullsNotDistinct": false, + "columns": [ + "ap_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.handle_registry": { + "name": "handle_registry", + "schema": "", + "columns": { + "handle": { + "name": "handle", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "did": { + "name": "did", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "node_domain": { + "name": "node_domain", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "registered_at": { + "name": "registered_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "handle_registry_updated_idx": { + "name": "handle_registry_updated_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.likes": { + "name": "likes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "post_id": { + "name": "post_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "ap_id": { + "name": "ap_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "likes_user_post_idx": { + "name": "likes_user_post_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "post_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "likes_user_id_users_id_fk": { + "name": "likes_user_id_users_id_fk", + "tableFrom": "likes", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "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" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "likes_ap_id_unique": { + "name": "likes_ap_id_unique", + "nullsNotDistinct": false, + "columns": [ + "ap_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.media": { + "name": "media", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "post_id": { + "name": "post_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "alt_text": { + "name": "alt_text", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mime_type": { + "name": "mime_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "width": { + "name": "width", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "height": { + "name": "height", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "media_user_idx": { + "name": "media_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "media_post_idx": { + "name": "media_post_idx", + "columns": [ + { + "expression": "post_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "media_user_id_users_id_fk": { + "name": "media_user_id_users_id_fk", + "tableFrom": "media", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "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": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.muted_nodes": { + "name": "muted_nodes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "node_domain": { + "name": "node_domain", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "muted_nodes_user_idx": { + "name": "muted_nodes_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "muted_nodes_domain_idx": { + "name": "muted_nodes_domain_idx", + "columns": [ + { + "expression": "node_domain", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "muted_nodes_user_id_users_id_fk": { + "name": "muted_nodes_user_id_users_id_fk", + "tableFrom": "muted_nodes", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mutes": { + "name": "mutes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "muted_user_id": { + "name": "muted_user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "mutes_user_idx": { + "name": "mutes_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mutes_muted_user_idx": { + "name": "mutes_muted_user_idx", + "columns": [ + { + "expression": "muted_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mutes_user_id_users_id_fk": { + "name": "mutes_user_id_users_id_fk", + "tableFrom": "mutes", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mutes_muted_user_id_users_id_fk": { + "name": "mutes_muted_user_id_users_id_fk", + "tableFrom": "mutes", + "tableTo": "users", + "columnsFrom": [ + "muted_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.nodes": { + "name": "nodes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "long_description": { + "name": "long_description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rules": { + "name": "rules", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "banner_url": { + "name": "banner_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "logo_url": { + "name": "logo_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "accent_color": { + "name": "accent_color", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'#FFFFFF'" + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_nsfw": { + "name": "is_nsfw", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "nodes_domain_unique": { + "name": "nodes_domain_unique", + "nullsNotDistinct": false, + "columns": [ + "domain" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.notifications": { + "name": "notifications", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "actor_id": { + "name": "actor_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "post_id": { + "name": "post_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "read_at": { + "name": "read_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "notifications_user_idx": { + "name": "notifications_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "notifications_created_idx": { + "name": "notifications_created_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "notifications_user_id_users_id_fk": { + "name": "notifications_user_id_users_id_fk", + "tableFrom": "notifications", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "notifications_actor_id_users_id_fk": { + "name": "notifications_actor_id_users_id_fk", + "tableFrom": "notifications", + "tableTo": "users", + "columnsFrom": [ + "actor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "notifications_post_id_posts_id_fk": { + "name": "notifications_post_id_posts_id_fk", + "tableFrom": "notifications", + "tableTo": "posts", + "columnsFrom": [ + "post_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.posts": { + "name": "posts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "bot_id": { + "name": "bot_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reply_to_id": { + "name": "reply_to_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "repost_of_id": { + "name": "repost_of_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "likes_count": { + "name": "likes_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "reposts_count": { + "name": "reposts_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "replies_count": { + "name": "replies_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "is_nsfw": { + "name": "is_nsfw", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "is_removed": { + "name": "is_removed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "removed_at": { + "name": "removed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "removed_by": { + "name": "removed_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "removed_reason": { + "name": "removed_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ap_id": { + "name": "ap_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ap_url": { + "name": "ap_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "link_preview_url": { + "name": "link_preview_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "link_preview_title": { + "name": "link_preview_title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "link_preview_description": { + "name": "link_preview_description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "link_preview_image": { + "name": "link_preview_image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "posts_user_id_idx": { + "name": "posts_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "posts_bot_id_idx": { + "name": "posts_bot_id_idx", + "columns": [ + { + "expression": "bot_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "posts_created_at_idx": { + "name": "posts_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "posts_reply_to_idx": { + "name": "posts_reply_to_idx", + "columns": [ + { + "expression": "reply_to_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "posts_removed_idx": { + "name": "posts_removed_idx", + "columns": [ + { + "expression": "is_removed", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "posts_nsfw_idx": { + "name": "posts_nsfw_idx", + "columns": [ + { + "expression": "is_nsfw", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "posts_user_id_users_id_fk": { + "name": "posts_user_id_users_id_fk", + "tableFrom": "posts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "posts_bot_id_bots_id_fk": { + "name": "posts_bot_id_bots_id_fk", + "tableFrom": "posts", + "tableTo": "bots", + "columnsFrom": [ + "bot_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "posts_removed_by_users_id_fk": { + "name": "posts_removed_by_users_id_fk", + "tableFrom": "posts", + "tableTo": "users", + "columnsFrom": [ + "removed_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "posts_ap_id_unique": { + "name": "posts_ap_id_unique", + "nullsNotDistinct": false, + "columns": [ + "ap_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.remote_followers": { + "name": "remote_followers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "actor_url": { + "name": "actor_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "inbox_url": { + "name": "inbox_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "shared_inbox_url": { + "name": "shared_inbox_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "handle": { + "name": "handle", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "activity_id": { + "name": "activity_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "remote_followers_user_idx": { + "name": "remote_followers_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "remote_followers_actor_idx": { + "name": "remote_followers_actor_idx", + "columns": [ + { + "expression": "actor_url", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "remote_followers_user_id_users_id_fk": { + "name": "remote_followers_user_id_users_id_fk", + "tableFrom": "remote_followers", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "remote_followers_actor_url_unique": { + "name": "remote_followers_actor_url_unique", + "nullsNotDistinct": false, + "columns": [ + "actor_url" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.remote_follows": { + "name": "remote_follows", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "follower_id": { + "name": "follower_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "target_handle": { + "name": "target_handle", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_actor_url": { + "name": "target_actor_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "inbox_url": { + "name": "inbox_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "activity_id": { + "name": "activity_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bio": { + "name": "bio", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "avatar_url": { + "name": "avatar_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "remote_follows_follower_idx": { + "name": "remote_follows_follower_idx", + "columns": [ + { + "expression": "follower_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "remote_follows_target_idx": { + "name": "remote_follows_target_idx", + "columns": [ + { + "expression": "target_handle", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "remote_follows_follower_id_users_id_fk": { + "name": "remote_follows_follower_id_users_id_fk", + "tableFrom": "remote_follows", + "tableTo": "users", + "columnsFrom": [ + "follower_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.remote_posts": { + "name": "remote_posts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "ap_id": { + "name": "ap_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "author_handle": { + "name": "author_handle", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "author_actor_url": { + "name": "author_actor_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "author_display_name": { + "name": "author_display_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "author_avatar_url": { + "name": "author_avatar_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "published_at": { + "name": "published_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "link_preview_url": { + "name": "link_preview_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "link_preview_title": { + "name": "link_preview_title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "link_preview_description": { + "name": "link_preview_description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "link_preview_image": { + "name": "link_preview_image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "media_json": { + "name": "media_json", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "fetched_at": { + "name": "fetched_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "remote_posts_author_idx": { + "name": "remote_posts_author_idx", + "columns": [ + { + "expression": "author_handle", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "remote_posts_published_idx": { + "name": "remote_posts_published_idx", + "columns": [ + { + "expression": "published_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "remote_posts_ap_id_idx": { + "name": "remote_posts_ap_id_idx", + "columns": [ + { + "expression": "ap_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "remote_posts_ap_id_unique": { + "name": "remote_posts_ap_id_unique", + "nullsNotDistinct": false, + "columns": [ + "ap_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.reports": { + "name": "reports", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "reporter_id": { + "name": "reporter_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "target_type": { + "name": "target_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_id": { + "name": "target_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'open'" + }, + "resolved_at": { + "name": "resolved_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "resolved_by": { + "name": "resolved_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "resolution_note": { + "name": "resolution_note", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "reports_status_idx": { + "name": "reports_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "reports_target_idx": { + "name": "reports_target_idx", + "columns": [ + { + "expression": "target_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "reports_reporter_idx": { + "name": "reports_reporter_idx", + "columns": [ + { + "expression": "reporter_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "reports_reporter_id_users_id_fk": { + "name": "reports_reporter_id_users_id_fk", + "tableFrom": "reports", + "tableTo": "users", + "columnsFrom": [ + "reporter_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "reports_resolved_by_users_id_fk": { + "name": "reports_resolved_by_users_id_fk", + "tableFrom": "reports", + "tableTo": "users", + "columnsFrom": [ + "resolved_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sessions": { + "name": "sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "sessions_token_idx": { + "name": "sessions_token_idx", + "columns": [ + { + "expression": "token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sessions_user_idx": { + "name": "sessions_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "sessions_token_unique": { + "name": "sessions_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.swarm_nodes": { + "name": "swarm_nodes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "logo_url": { + "name": "logo_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "software_version": { + "name": "software_version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_count": { + "name": "user_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "post_count": { + "name": "post_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "is_nsfw": { + "name": "is_nsfw", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "discovered_via": { + "name": "discovered_via", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "discovered_at": { + "name": "discovered_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_seen_at": { + "name": "last_seen_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_sync_at": { + "name": "last_sync_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "consecutive_failures": { + "name": "consecutive_failures", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "trust_score": { + "name": "trust_score", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 50 + }, + "capabilities": { + "name": "capabilities", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "swarm_nodes_domain_idx": { + "name": "swarm_nodes_domain_idx", + "columns": [ + { + "expression": "domain", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "swarm_nodes_active_idx": { + "name": "swarm_nodes_active_idx", + "columns": [ + { + "expression": "is_active", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "swarm_nodes_last_seen_idx": { + "name": "swarm_nodes_last_seen_idx", + "columns": [ + { + "expression": "last_seen_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "swarm_nodes_trust_idx": { + "name": "swarm_nodes_trust_idx", + "columns": [ + { + "expression": "trust_score", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "swarm_nodes_nsfw_idx": { + "name": "swarm_nodes_nsfw_idx", + "columns": [ + { + "expression": "is_nsfw", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "swarm_nodes_domain_unique": { + "name": "swarm_nodes_domain_unique", + "nullsNotDistinct": false, + "columns": [ + "domain" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.swarm_seeds": { + "name": "swarm_seeds", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "priority": { + "name": "priority", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 100 + }, + "is_enabled": { + "name": "is_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "last_contact_at": { + "name": "last_contact_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "consecutive_failures": { + "name": "consecutive_failures", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "swarm_seeds_enabled_idx": { + "name": "swarm_seeds_enabled_idx", + "columns": [ + { + "expression": "is_enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "swarm_seeds_priority_idx": { + "name": "swarm_seeds_priority_idx", + "columns": [ + { + "expression": "priority", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "swarm_seeds_domain_unique": { + "name": "swarm_seeds_domain_unique", + "nullsNotDistinct": false, + "columns": [ + "domain" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.swarm_sync_log": { + "name": "swarm_sync_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "remote_domain": { + "name": "remote_domain", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "direction": { + "name": "direction", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "nodes_received": { + "name": "nodes_received", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "nodes_sent": { + "name": "nodes_sent", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "handles_received": { + "name": "handles_received", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "handles_sent": { + "name": "handles_sent", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "success": { + "name": "success", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "duration_ms": { + "name": "duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "swarm_sync_log_remote_idx": { + "name": "swarm_sync_log_remote_idx", + "columns": [ + { + "expression": "remote_domain", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "swarm_sync_log_created_idx": { + "name": "swarm_sync_log_created_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "did": { + "name": "did", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "handle": { + "name": "handle", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bio": { + "name": "bio", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "avatar_url": { + "name": "avatar_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "header_url": { + "name": "header_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "private_key_encrypted": { + "name": "private_key_encrypted", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "node_id": { + "name": "node_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "is_bot": { + "name": "is_bot", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "bot_owner_id": { + "name": "bot_owner_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "is_nsfw": { + "name": "is_nsfw", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "nsfw_enabled": { + "name": "nsfw_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "age_verified_at": { + "name": "age_verified_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "is_suspended": { + "name": "is_suspended", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "suspension_reason": { + "name": "suspension_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "suspended_at": { + "name": "suspended_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "is_silenced": { + "name": "is_silenced", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "silence_reason": { + "name": "silence_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "silenced_at": { + "name": "silenced_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "moved_to": { + "name": "moved_to", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "moved_from": { + "name": "moved_from", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "migrated_at": { + "name": "migrated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "followers_count": { + "name": "followers_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "following_count": { + "name": "following_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "posts_count": { + "name": "posts_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "website": { + "name": "website", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "users_handle_idx": { + "name": "users_handle_idx", + "columns": [ + { + "expression": "handle", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "users_did_idx": { + "name": "users_did_idx", + "columns": [ + { + "expression": "did", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "users_suspended_idx": { + "name": "users_suspended_idx", + "columns": [ + { + "expression": "is_suspended", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "users_silenced_idx": { + "name": "users_silenced_idx", + "columns": [ + { + "expression": "is_silenced", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "users_is_bot_idx": { + "name": "users_is_bot_idx", + "columns": [ + { + "expression": "is_bot", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "users_bot_owner_idx": { + "name": "users_bot_owner_idx", + "columns": [ + { + "expression": "bot_owner_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "users_nsfw_idx": { + "name": "users_nsfw_idx", + "columns": [ + { + "expression": "is_nsfw", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "users_node_id_nodes_id_fk": { + "name": "users_node_id_nodes_id_fk", + "tableFrom": "users", + "tableTo": "nodes", + "columnsFrom": [ + "node_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "users_bot_owner_id_users_id_fk": { + "name": "users_bot_owner_id_users_id_fk", + "tableFrom": "users", + "tableTo": "users", + "columnsFrom": [ + "bot_owner_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "users_did_unique": { + "name": "users_did_unique", + "nullsNotDistinct": false, + "columns": [ + "did" + ] + }, + "users_handle_unique": { + "name": "users_handle_unique", + "nullsNotDistinct": false, + "columns": [ + "handle" + ] + }, + "users_email_unique": { + "name": "users_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index 2a3a6a3..0182dc9 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -22,6 +22,20 @@ "when": 1769367465905, "tag": "0002_add_logo_url", "breakpoints": true + }, + { + "idx": 3, + "version": "7", + "when": 1769370833410, + "tag": "0003_small_reavers", + "breakpoints": true + }, + { + "idx": 4, + "version": "7", + "when": 1769377790354, + "tag": "0004_luxuriant_lockheed", + "breakpoints": true } ] } \ No newline at end of file diff --git a/src/app/.well-known/synapsis-swarm/route.ts b/src/app/.well-known/synapsis-swarm/route.ts new file mode 100644 index 0000000..9c63f2e --- /dev/null +++ b/src/app/.well-known/synapsis-swarm/route.ts @@ -0,0 +1,77 @@ +/** + * Well-Known Synapsis Swarm Endpoint + * + * GET /.well-known/synapsis-swarm + * + * Returns information about this node and the swarm it knows about. + * This is a standardized discovery endpoint that other nodes can use + * to find and join the swarm. + */ + +import { NextResponse } from 'next/server'; +import { buildAnnouncement } from '@/lib/swarm/discovery'; +import { getActiveSwarmNodes, getSwarmStats, getSeedNodes } from '@/lib/swarm/registry'; + +export async function GET(request: Request) { + try { + const { searchParams } = new URL(request.url); + const includeNodes = searchParams.get('nodes') !== 'false'; + const limit = Math.min(parseInt(searchParams.get('limit') || '50'), 200); + + const announcement = await buildAnnouncement(); + const stats = await getSwarmStats(); + const seeds = await getSeedNodes(); + + const response: { + // This node's info + node: { + domain: string; + name: string; + description?: string; + publicKey: string; + softwareVersion: string; + capabilities: string[]; + }; + // Swarm metadata + swarm: { + totalNodes: number; + activeNodes: number; + totalUsers: number; + totalPosts: number; + seeds: string[]; + }; + // Known nodes (optional) + nodes?: Awaited>; + } = { + node: { + domain: announcement.domain, + name: announcement.name, + description: announcement.description, + publicKey: announcement.publicKey, + softwareVersion: announcement.softwareVersion, + capabilities: announcement.capabilities, + }, + swarm: { + ...stats, + seeds, + }, + }; + + if (includeNodes) { + response.nodes = await getActiveSwarmNodes(limit); + } + + return NextResponse.json(response, { + headers: { + 'Content-Type': 'application/json', + 'Cache-Control': 'public, max-age=300', // Cache for 5 minutes + }, + }); + } catch (error) { + console.error('Synapsis swarm well-known error:', error); + return NextResponse.json( + { error: 'Failed to fetch swarm info' }, + { status: 500 } + ); + } +} diff --git a/src/app/[handle]/page.tsx b/src/app/[handle]/page.tsx index a043e62..a603191 100644 --- a/src/app/[handle]/page.tsx +++ b/src/app/[handle]/page.tsx @@ -7,7 +7,7 @@ import { ArrowLeftIcon, CalendarIcon } from '@/components/Icons'; import { PostCard } from '@/components/PostCard'; import { User, Post } from '@/lib/types'; import AutoTextarea from '@/components/AutoTextarea'; -import { Rocket } from 'lucide-react'; +import { Rocket, MoreHorizontal } from 'lucide-react'; import { formatFullHandle } from '@/lib/utils/handle'; import { Bot } from 'lucide-react'; @@ -102,6 +102,8 @@ export default function ProfilePage() { }); const [saveError, setSaveError] = useState(null); const [isSaving, setIsSaving] = useState(false); + const [isBlocked, setIsBlocked] = useState(false); + const [showMenu, setShowMenu] = useState(false); useEffect(() => { setIsEditing(false); @@ -173,6 +175,7 @@ export default function ProfilePage() { useEffect(() => { if (!currentUser || !user || currentUser.handle === user.handle) { setIsFollowing(false); + setIsBlocked(false); return; } @@ -180,6 +183,11 @@ export default function ProfilePage() { .then(res => res.json()) .then(data => setIsFollowing(!!data.following)) .catch(() => setIsFollowing(false)); + + fetch(`/api/users/${handle}/block`) + .then(res => res.json()) + .then(data => setIsBlocked(!!data.blocked)) + .catch(() => setIsBlocked(false)); }, [currentUser, user, handle]); useEffect(() => { @@ -226,6 +234,22 @@ export default function ProfilePage() { } }; + const handleBlock = async () => { + if (!currentUser) return; + + const method = isBlocked ? 'DELETE' : 'POST'; + const res = await fetch(`/api/users/${handle}/block`, { method }); + + if (res.ok) { + setIsBlocked(!isBlocked); + if (!isBlocked) { + // If blocking, also unfollow + setIsFollowing(false); + } + setShowMenu(false); + } + }; + const handleSaveProfile = async () => { if (!isOwnProfile) return; setIsSaving(true); @@ -377,14 +401,67 @@ export default function ProfilePage() { )} -
+
{!isOwnProfile && currentUser && ( - + <> + {!isBlocked && ( + + )} +
+ + {showMenu && ( + <> +
setShowMenu(false)} + /> +
+ +
+ + )} +
+ )} {isOwnProfile && ( diff --git a/src/app/[handle]/posts/[id]/page.tsx b/src/app/[handle]/posts/[id]/page.tsx index 3991aa7..9f38d43 100644 --- a/src/app/[handle]/posts/[id]/page.tsx +++ b/src/app/[handle]/posts/[id]/page.tsx @@ -41,11 +41,11 @@ export default function PostDetailPage() { fetchPostDetail(); }, [id]); - const handlePost = async (content: string, mediaIds: string[], linkPreview?: any, replyToId?: string) => { + const handlePost = async (content: string, mediaIds: string[], linkPreview?: any, replyToId?: string, isNsfw?: boolean) => { const res = await fetch('/api/posts', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ content, mediaIds, linkPreview, replyToId }), + body: JSON.stringify({ content, mediaIds, linkPreview, replyToId, isNsfw }), }); if (res.ok) { diff --git a/src/app/admin/page.tsx b/src/app/admin/page.tsx index d89c838..a8f3454 100644 --- a/src/app/admin/page.tsx +++ b/src/app/admin/page.tsx @@ -70,6 +70,7 @@ export default function AdminPage() { bannerUrl: '', logoUrl: '', accentColor: '#00D4AA', + isNsfw: false, }); const [savingSettings, setSavingSettings] = useState(false); const [isUploadingBanner, setIsUploadingBanner] = useState(false); @@ -136,6 +137,7 @@ export default function AdminPage() { bannerUrl: data.bannerUrl || '', logoUrl: data.logoUrl || '', accentColor: data.accentColor || '#00D4AA', + isNsfw: data.isNsfw || false, }); } catch { // error @@ -682,6 +684,49 @@ export default function AdminPage() { />
+
+
+
+ +

+ {nodeSettings.isNsfw + ? 'This node is marked as NSFW. All content will be hidden from users who haven\'t enabled NSFW viewing.' + : 'Enable this if your node primarily hosts adult or sensitive content. All posts from this node will be treated as NSFW across the swarm.'} +

+
+ +
+
+
- ) : fediversePosts.length === 0 ? ( +
Loading swarm posts...
+ ) : swarmPosts.length === 0 ? (
- -

No fediverse posts yet

+ +

No swarm posts yet

+

+ Posts from other Synapsis nodes will appear here +

) : ( <>
-
Fediverse feed
+
Swarm feed
- This feed shows posts from across the fediverse, including content from accounts that users on this node follow. Discover new voices and conversations from the wider federated network. + Posts from across the Synapsis network. Currently showing posts from {swarmSources.filter(s => s.postCount > 0).length} node{swarmSources.filter(s => s.postCount > 0).length !== 1 ? 's' : ''}.
- {fediversePosts.map((post) => ( - + {swarmPosts.map((post) => ( +
+
+
+
+ {post.author.avatarUrl ? ( + {post.author.displayName} + ) : ( + post.author.displayName?.charAt(0).toUpperCase() || post.author.handle.charAt(0).toUpperCase() + )} +
+
+ {post.author.displayName} + @{post.author.handle}@{post.nodeDomain} +
+
+
{post.content}
+ {post.mediaUrls && post.mediaUrls.length > 0 && ( +
+ {post.mediaUrls.map((url, i) => ( + + ))} +
+ )} +
+ + {new Date(post.createdAt).toLocaleString()} + + + {post.likeCount > 0 && `${post.likeCount} likes`} + {post.likeCount > 0 && post.repostCount > 0 && ' ยท '} + {post.repostCount > 0 && `${post.repostCount} reposts`} + +
+
+
))}
diff --git a/src/app/globals.css b/src/app/globals.css index bde235a..58f17b4 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -467,6 +467,42 @@ a.btn-primary:visited { margin-top: 12px; } +.compose-footer-left { + display: flex; + align-items: center; + gap: 12px; +} + +.compose-nsfw-toggle { + display: flex; + align-items: center; + gap: 4px; + font-size: 12px; + color: var(--foreground-tertiary); + cursor: pointer; + padding: 4px 8px; + border-radius: var(--radius-sm); + transition: all 0.15s ease; +} + +.compose-nsfw-toggle:hover { + background: var(--background-secondary); + color: var(--foreground-secondary); +} + +.compose-nsfw-toggle input { + display: none; +} + +.compose-nsfw-toggle input:checked + svg { + color: var(--warning); +} + +.compose-nsfw-toggle input:checked ~ span { + color: var(--warning); + font-weight: 500; +} + .compose-actions { display: flex; align-items: center; @@ -1373,6 +1409,81 @@ a.btn-primary:visited { -webkit-box-orient: vertical; } +/* Swarm Post Styles */ +.swarm-post-wrapper { + border-bottom: 1px solid var(--border); +} + +.swarm-post-card { + padding: 16px; + border: none; + border-radius: 0; + background: transparent; +} + +.swarm-post-card:hover { + background: var(--background-secondary); +} + +.swarm-post-header { + display: flex; + align-items: center; + gap: 12px; + margin-bottom: 12px; +} + +.swarm-post-meta { + display: flex; + flex-direction: column; + gap: 2px; +} + +.swarm-post-author { + font-weight: 600; + font-size: 15px; + color: var(--foreground); +} + +.swarm-post-handle { + font-size: 13px; + color: var(--foreground-tertiary); +} + +.swarm-post-content { + font-size: 15px; + line-height: 1.5; + color: var(--foreground); + white-space: pre-wrap; + word-break: break-word; +} + +.swarm-post-media { + margin-top: 12px; + display: grid; + gap: 8px; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); +} + +.swarm-post-media img { + width: 100%; + border-radius: var(--radius-md); + max-height: 300px; + object-fit: cover; +} + +.swarm-post-footer { + display: flex; + justify-content: space-between; + align-items: center; + margin-top: 12px; + font-size: 13px; + color: var(--foreground-tertiary); +} + +.swarm-post-stats { + color: var(--foreground-secondary); +} + /* Scrollbar Styling */ ::-webkit-scrollbar { width: 6px; diff --git a/src/app/page.tsx b/src/app/page.tsx index e2f4d17..7512ba6 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -55,11 +55,11 @@ export default function Home() { loadFeed(feedType); }, [feedType]); - const handlePost = async (content: string, mediaIds: string[], linkPreview?: any, replyToId?: string) => { + const handlePost = async (content: string, mediaIds: string[], linkPreview?: any, replyToId?: string, isNsfw?: boolean) => { const res = await fetch('/api/posts', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ content, mediaIds, linkPreview, replyToId }), + body: JSON.stringify({ content, mediaIds, linkPreview, replyToId, isNsfw }), }); if (res.ok) { diff --git a/src/app/settings/content/page.tsx b/src/app/settings/content/page.tsx new file mode 100644 index 0000000..ed6dae2 --- /dev/null +++ b/src/app/settings/content/page.tsx @@ -0,0 +1,380 @@ +'use client'; + +import { useState, useEffect } from 'react'; +import Link from 'next/link'; +import { ArrowLeftIcon } from '@/components/Icons'; +import { Eye, EyeOff, AlertTriangle, Check } from 'lucide-react'; + +interface NsfwSettings { + nsfwEnabled: boolean; + ageVerifiedAt: string | null; + isNsfw: boolean; +} + +export default function ContentSettingsPage() { + const [settings, setSettings] = useState(null); + const [loading, setLoading] = useState(true); + const [saving, setSaving] = useState(false); + const [showAgeModal, setShowAgeModal] = useState(false); + const [error, setError] = useState(null); + const [success, setSuccess] = useState(null); + + useEffect(() => { + fetchSettings(); + }, []); + + const fetchSettings = async () => { + try { + const res = await fetch('/api/settings/nsfw'); + if (res.ok) { + const data = await res.json(); + setSettings(data); + } + } catch { + setError('Failed to load settings'); + } finally { + setLoading(false); + } + }; + + const handleToggleNsfw = async () => { + if (!settings) return; + + // If enabling and not verified, show age modal + if (!settings.nsfwEnabled && !settings.ageVerifiedAt) { + setShowAgeModal(true); + return; + } + + // Otherwise just toggle + setSaving(true); + setError(null); + try { + const res = await fetch('/api/settings/nsfw', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ nsfwEnabled: !settings.nsfwEnabled }), + }); + + if (res.ok) { + const data = await res.json(); + setSettings(prev => prev ? { ...prev, nsfwEnabled: data.nsfwEnabled } : null); + setSuccess(data.nsfwEnabled ? 'NSFW content enabled' : 'NSFW content disabled'); + setTimeout(() => setSuccess(null), 3000); + } else { + const data = await res.json(); + setError(data.error || 'Failed to update'); + } + } catch { + setError('Failed to update settings'); + } finally { + setSaving(false); + } + }; + + const handleAgeConfirm = async () => { + setSaving(true); + setError(null); + try { + const res = await fetch('/api/settings/nsfw', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ nsfwEnabled: true, confirmAge: true }), + }); + + if (res.ok) { + const data = await res.json(); + setSettings(prev => prev ? { + ...prev, + nsfwEnabled: true, + ageVerifiedAt: data.ageVerifiedAt, + } : null); + setShowAgeModal(false); + setSuccess('NSFW content enabled'); + setTimeout(() => setSuccess(null), 3000); + } else { + const data = await res.json(); + setError(data.error || 'Failed to verify'); + } + } catch { + setError('Failed to verify age'); + } finally { + setSaving(false); + } + }; + + const handleToggleAccountNsfw = async () => { + if (!settings) return; + + setSaving(true); + setError(null); + try { + const res = await fetch('/api/settings/account-nsfw', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ isNsfw: !settings.isNsfw }), + }); + + if (res.ok) { + const data = await res.json(); + setSettings(prev => prev ? { ...prev, isNsfw: data.isNsfw } : null); + setSuccess(data.isNsfw ? 'Account marked as NSFW' : 'Account unmarked as NSFW'); + setTimeout(() => setSuccess(null), 3000); + } else { + const data = await res.json(); + setError(data.error || 'Failed to update'); + } + } catch { + setError('Failed to update settings'); + } finally { + setSaving(false); + } + }; + + if (loading) { + return ( +
+
+ Loading... +
+
+ ); + } + + return ( +
+
+ + + +
+

Content Settings

+

+ NSFW and content visibility preferences +

+
+
+ + {error && ( +
+ {error} +
+ )} + + {success && ( +
+ + {success} +
+ )} + +
+ {/* View NSFW Content */} +
+
+
+
+ {settings?.nsfwEnabled ? : } + Show NSFW Content +
+
+ {settings?.nsfwEnabled + ? 'You can see posts marked as sensitive or from NSFW accounts/nodes.' + : 'NSFW content is hidden from your feeds and search results.'} +
+ {settings?.ageVerifiedAt && ( +
+ Age verified on {new Date(settings.ageVerifiedAt).toLocaleDateString()} +
+ )} +
+ +
+
+ + {/* Mark Account as NSFW - only show if NSFW viewing is enabled */} + {settings?.nsfwEnabled && ( +
+
+
+
+ + Mark My Account as NSFW +
+
+ {settings?.isNsfw + ? 'Your account is marked as NSFW. All your posts will be hidden from users who haven\'t enabled NSFW content.' + : 'Enable this if you regularly post adult or sensitive content. Your posts will only be visible to users who have enabled NSFW viewing.'} +
+
+ +
+
+ )} + + {/* Info box */} +
+
+ How NSFW filtering works +
+
    +
  • Content is marked NSFW at three levels: post, account, or node
  • +
  • If any level is NSFW, the content is hidden from non-verified users
  • +
  • You can mark individual posts as NSFW when composing
  • +
  • NSFW content still syncs across the swarm, but is filtered at display time
  • +
+
+
+ + {/* Age Verification Modal */} + {showAgeModal && ( +
+
+
+ +

Age Verification

+
+

+ NSFW content may include adult themes, nudity, or other sensitive material. + By enabling this setting, you confirm that you are at least 18 years old. +

+
+ + +
+
+
+ )} +
+ ); +} diff --git a/src/app/settings/moderation/page.tsx b/src/app/settings/moderation/page.tsx new file mode 100644 index 0000000..c81b494 --- /dev/null +++ b/src/app/settings/moderation/page.tsx @@ -0,0 +1,313 @@ +'use client'; + +import { useState, useEffect } from 'react'; +import Link from 'next/link'; +import { ArrowLeftIcon } from '@/components/Icons'; +import { UserX, Globe, Trash2 } from 'lucide-react'; + +interface BlockedUser { + id: string; + handle: string; + displayName: string | null; + avatarUrl: string | null; + blockedAt: string; +} + +interface MutedNode { + domain: string; + mutedAt: string; +} + +export default function ModerationSettingsPage() { + const [blockedUsers, setBlockedUsers] = useState([]); + const [mutedNodes, setMutedNodes] = useState([]); + const [loading, setLoading] = useState(true); + const [newNodeDomain, setNewNodeDomain] = useState(''); + const [addingNode, setAddingNode] = useState(false); + + useEffect(() => { + loadData(); + }, []); + + const loadData = async () => { + setLoading(true); + try { + const [blockedRes, mutedRes] = await Promise.all([ + fetch('/api/settings/blocked-users'), + fetch('/api/settings/muted-nodes'), + ]); + + if (blockedRes.ok) { + const data = await blockedRes.json(); + setBlockedUsers(data.blockedUsers || []); + } + + if (mutedRes.ok) { + const data = await mutedRes.json(); + setMutedNodes(data.mutedNodes || []); + } + } catch (error) { + console.error('Failed to load moderation settings:', error); + } finally { + setLoading(false); + } + }; + + const handleUnblock = async (userId: string) => { + try { + const res = await fetch(`/api/settings/blocked-users?userId=${userId}`, { + method: 'DELETE', + }); + if (res.ok) { + setBlockedUsers(prev => prev.filter(u => u.id !== userId)); + } + } catch (error) { + console.error('Failed to unblock user:', error); + } + }; + + const handleUnmuteNode = async (domain: string) => { + try { + const res = await fetch(`/api/settings/muted-nodes?domain=${encodeURIComponent(domain)}`, { + method: 'DELETE', + }); + if (res.ok) { + setMutedNodes(prev => prev.filter(n => n.domain !== domain)); + } + } catch (error) { + console.error('Failed to unmute node:', error); + } + }; + + const handleMuteNode = async (e: React.FormEvent) => { + e.preventDefault(); + if (!newNodeDomain.trim() || addingNode) return; + + setAddingNode(true); + try { + const res = await fetch('/api/settings/muted-nodes', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ domain: newNodeDomain.trim() }), + }); + + if (res.ok) { + const data = await res.json(); + setMutedNodes(prev => [ + { domain: data.domain, mutedAt: new Date().toISOString() }, + ...prev.filter(n => n.domain !== data.domain), + ]); + setNewNodeDomain(''); + } + } catch (error) { + console.error('Failed to mute node:', error); + } finally { + setAddingNode(false); + } + }; + + if (loading) { + return ( +
+
+ Loading... +
+
+ ); + } + + return ( +
+
+ + + +
+

Moderation

+

+ Manage blocked users and muted nodes +

+
+
+ +
+ {/* Blocked Users */} +
+
+ +

Blocked Users

+ + {blockedUsers.length} + +
+ + {blockedUsers.length === 0 ? ( +
+ You haven't blocked anyone +
+ ) : ( +
+ {blockedUsers.map((user, i) => ( +
+ + +
+
+ {user.displayName || user.handle} +
+
+ @{user.handle} +
+
+ + +
+ ))} +
+ )} +
+ + {/* Muted Nodes */} +
+
+ +

Muted Nodes

+ + {mutedNodes.length} + +
+ +

+ Posts from muted nodes won't appear in your feeds or search results. +

+ +
+
+ setNewNodeDomain(e.target.value)} + style={{ flex: 1 }} + /> + +
+
+ + {mutedNodes.length === 0 ? ( +
+ No muted nodes +
+ ) : ( +
+ {mutedNodes.map((node, i) => ( +
+
+
{node.domain}
+
+ Muted {new Date(node.mutedAt).toLocaleDateString()} +
+
+ +
+ ))} +
+ )} +
+
+
+ ); +} diff --git a/src/app/settings/page.tsx b/src/app/settings/page.tsx index 6136567..a4ed461 100644 --- a/src/app/settings/page.tsx +++ b/src/app/settings/page.tsx @@ -2,7 +2,7 @@ import Link from 'next/link'; import { ArrowLeftIcon } from '@/components/Icons'; -import { Rocket, Shield, Bell, Bot } from 'lucide-react'; +import { Rocket, Shield, Bell, Bot, Eye, UserX } from 'lucide-react'; export default function SettingsPage() { return ( @@ -41,6 +41,38 @@ export default function SettingsPage() {
+ +
+ + Content Settings +
+
+ NSFW preferences and content visibility +
+ + + +
+ + Moderation +
+
+ Blocked users and muted nodes +
+ + void; + onPost: (content: string, mediaIds: string[], linkPreview?: any, replyToId?: string, isNsfw?: boolean) => void; replyingTo?: Post | null; onCancelReply?: () => void; placeholder?: string; @@ -24,9 +24,23 @@ export function Compose({ onPost, replyingTo, onCancelReply, placeholder = "What const [linkPreview, setLinkPreview] = useState(null); const [fetchingPreview, setFetchingPreview] = useState(false); const [lastDetectedUrl, setLastDetectedUrl] = useState(null); + const [isNsfw, setIsNsfw] = useState(false); + const [canPostNsfw, setCanPostNsfw] = useState(false); const maxLength = 400; const remaining = maxLength - content.length; + // Check if user can post NSFW content + useEffect(() => { + fetch('/api/settings/nsfw') + .then(res => res.ok ? res.json() : null) + .then(data => { + if (data?.nsfwEnabled) { + setCanPostNsfw(true); + } + }) + .catch(() => {}); + }, []); + // Detect URLs in content useEffect(() => { const urlRegex = /(?:https?:\/\/)?((?:[a-zA-Z0-9-]+\.)+[a-z]{2,63})\b([-a-zA-Z0-9@:%_\+.~#?&//=]*)/gi; @@ -62,11 +76,12 @@ export function Compose({ onPost, replyingTo, onCancelReply, placeholder = "What const handleSubmit = async () => { if (!content.trim() || isPosting || isUploading) return; setIsPosting(true); - await onPost(content, attachments.map((item) => item.id).filter(Boolean), linkPreview, replyingTo?.id); + await onPost(content, attachments.map((item) => item.id).filter(Boolean), linkPreview, replyingTo?.id, isNsfw); setContent(''); setAttachments([]); setLinkPreview(null); setLastDetectedUrl(null); + setIsNsfw(false); setIsPosting(false); }; @@ -183,9 +198,22 @@ export function Compose({ onPost, replyingTo, onCancelReply, placeholder = "What
{uploadError}
)}
- - {remaining} - +
+ + {remaining} + + {canPostNsfw && ( + + )} +
{formatFullHandle(post.author.handle)} ยท {formatTime(post.createdAt)}
+ {currentUser && currentUser.id !== post.author.id && ( +
+ + {showMenu && ( + <> +
{ + e.preventDefault(); + e.stopPropagation(); + setShowMenu(false); + }} + /> +
+ + + {(post.nodeDomain || post.author.handle.includes('@')) && ( + + )} +
+ + )} +
+ )}
{post.replyTo && ( diff --git a/src/db/schema.ts b/src/db/schema.ts index 52f66fc..77b6691 100644 --- a/src/db/schema.ts +++ b/src/db/schema.ts @@ -16,6 +16,8 @@ export const nodes = pgTable('nodes', { logoUrl: text('logo_url'), accentColor: text('accent_color').default('#FFFFFF'), publicKey: text('public_key'), + // NSFW settings + isNsfw: boolean('is_nsfw').default(false).notNull(), // Entire node is NSFW createdAt: timestamp('created_at').defaultNow().notNull(), updatedAt: timestamp('updated_at').defaultNow().notNull(), }); @@ -40,6 +42,10 @@ export const users = pgTable('users', { // Bot-related fields isBot: boolean('is_bot').default(false).notNull(), botOwnerId: uuid('bot_owner_id'), + // NSFW settings + isNsfw: boolean('is_nsfw').default(false).notNull(), // Account produces NSFW content + nsfwEnabled: boolean('nsfw_enabled').default(false).notNull(), // User wants to see NSFW content + ageVerifiedAt: timestamp('age_verified_at'), // When user confirmed 18+ // Moderation fields isSuspended: boolean('is_suspended').default(false).notNull(), suspensionReason: text('suspension_reason'), @@ -64,6 +70,7 @@ export const users = pgTable('users', { index('users_silenced_idx').on(table.isSilenced), index('users_is_bot_idx').on(table.isBot), index('users_bot_owner_idx').on(table.botOwnerId), + index('users_nsfw_idx').on(table.isNsfw), foreignKey({ columns: [table.botOwnerId], foreignColumns: [table.id], @@ -101,6 +108,9 @@ export const posts = pgTable('posts', { likesCount: integer('likes_count').default(0).notNull(), repostsCount: integer('reposts_count').default(0).notNull(), repliesCount: integer('replies_count').default(0).notNull(), + // NSFW + isNsfw: boolean('is_nsfw').default(false).notNull(), // This specific post is NSFW + // Moderation isRemoved: boolean('is_removed').default(false).notNull(), removedAt: timestamp('removed_at'), removedBy: uuid('removed_by').references(() => users.id), @@ -121,6 +131,7 @@ export const posts = pgTable('posts', { index('posts_created_at_idx').on(table.createdAt), index('posts_reply_to_idx').on(table.replyToId), index('posts_removed_idx').on(table.isRemoved), + index('posts_nsfw_idx').on(table.isNsfw), ]); export const postsRelations = relations(posts, ({ one, many }) => ({ @@ -386,8 +397,20 @@ export const blocks = pgTable('blocks', { createdAt: timestamp('created_at').defaultNow().notNull(), }, (table) => [ index('blocks_user_idx').on(table.userId), + index('blocks_blocked_user_idx').on(table.blockedUserId), ]); +export const blocksRelations = relations(blocks, ({ one }) => ({ + user: one(users, { + fields: [blocks.userId], + references: [users.id], + }), + blockedUser: one(users, { + fields: [blocks.blockedUserId], + references: [users.id], + }), +})); + export const mutes = pgTable('mutes', { id: uuid('id').primaryKey().defaultRandom(), userId: uuid('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }), @@ -395,8 +418,38 @@ export const mutes = pgTable('mutes', { createdAt: timestamp('created_at').defaultNow().notNull(), }, (table) => [ index('mutes_user_idx').on(table.userId), + index('mutes_muted_user_idx').on(table.mutedUserId), ]); +export const mutesRelations = relations(mutes, ({ one }) => ({ + user: one(users, { + fields: [mutes.userId], + references: [users.id], + }), + mutedUser: one(users, { + fields: [mutes.mutedUserId], + references: [users.id], + }), +})); + +// Muted nodes - hide all content from specific swarm nodes +export const mutedNodes = pgTable('muted_nodes', { + id: uuid('id').primaryKey().defaultRandom(), + userId: uuid('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }), + nodeDomain: text('node_domain').notNull(), // Domain of the muted node + createdAt: timestamp('created_at').defaultNow().notNull(), +}, (table) => [ + index('muted_nodes_user_idx').on(table.userId), + index('muted_nodes_domain_idx').on(table.nodeDomain), +]); + +export const mutedNodesRelations = relations(mutedNodes, ({ one }) => ({ + user: one(users, { + fields: [mutedNodes.userId], + references: [users.id], + }), +})); + // ============================================ // REPORTS (moderation) // ============================================ @@ -658,3 +711,108 @@ export const botRateLimitsRelations = relations(botRateLimits, ({ one }) => ({ references: [bots.id], }), })); + +// ============================================ +// SWARM - Node Discovery Network +// ============================================ + +/** + * Discovered nodes in the swarm network. + * Tracks all known Synapsis nodes discovered through gossip or seed nodes. + */ +export const swarmNodes = pgTable('swarm_nodes', { + id: uuid('id').primaryKey().defaultRandom(), + domain: text('domain').notNull().unique(), + + // Node metadata (fetched from remote) + name: text('name'), + description: text('description'), + logoUrl: text('logo_url'), + publicKey: text('public_key'), + softwareVersion: text('software_version'), + + // Stats (updated periodically) + userCount: integer('user_count'), + postCount: integer('post_count'), + + // NSFW flag (synced from remote node) + isNsfw: boolean('is_nsfw').default(false).notNull(), + + // Discovery metadata + discoveredVia: text('discovered_via'), // Domain of node that told us about this one + discoveredAt: timestamp('discovered_at').defaultNow().notNull(), + + // Health tracking + lastSeenAt: timestamp('last_seen_at').defaultNow().notNull(), + lastSyncAt: timestamp('last_sync_at'), + consecutiveFailures: integer('consecutive_failures').default(0).notNull(), + isActive: boolean('is_active').default(true).notNull(), + + // Trust/reputation (for future spam prevention) + trustScore: integer('trust_score').default(50).notNull(), // 0-100 + + // Capabilities + capabilities: text('capabilities'), // JSON array: ["handles", "gossip", "relay"] + + createdAt: timestamp('created_at').defaultNow().notNull(), + updatedAt: timestamp('updated_at').defaultNow().notNull(), +}, (table) => [ + index('swarm_nodes_domain_idx').on(table.domain), + index('swarm_nodes_active_idx').on(table.isActive), + index('swarm_nodes_last_seen_idx').on(table.lastSeenAt), + index('swarm_nodes_trust_idx').on(table.trustScore), + index('swarm_nodes_nsfw_idx').on(table.isNsfw), +]); + +/** + * Seed nodes - well-known entry points to the swarm. + * These are the bootstrap nodes that new nodes contact first. + */ +export const swarmSeeds = pgTable('swarm_seeds', { + id: uuid('id').primaryKey().defaultRandom(), + domain: text('domain').notNull().unique(), + + // Priority for connection order (lower = higher priority) + priority: integer('priority').default(100).notNull(), + + // Whether this seed is enabled + isEnabled: boolean('is_enabled').default(true).notNull(), + + // Health tracking + lastContactAt: timestamp('last_contact_at'), + consecutiveFailures: integer('consecutive_failures').default(0).notNull(), + + createdAt: timestamp('created_at').defaultNow().notNull(), +}, (table) => [ + index('swarm_seeds_enabled_idx').on(table.isEnabled), + index('swarm_seeds_priority_idx').on(table.priority), +]); + +/** + * Swarm sync log - tracks gossip exchanges between nodes. + */ +export const swarmSyncLog = pgTable('swarm_sync_log', { + id: uuid('id').primaryKey().defaultRandom(), + + // Which node we synced with + remoteDomain: text('remote_domain').notNull(), + + // Direction: 'push' (we sent) or 'pull' (we received) + direction: text('direction').notNull(), + + // What was synced + nodesReceived: integer('nodes_received').default(0).notNull(), + nodesSent: integer('nodes_sent').default(0).notNull(), + handlesReceived: integer('handles_received').default(0).notNull(), + handlesSent: integer('handles_sent').default(0).notNull(), + + // Result + success: boolean('success').notNull(), + errorMessage: text('error_message'), + durationMs: integer('duration_ms'), + + createdAt: timestamp('created_at').defaultNow().notNull(), +}, (table) => [ + index('swarm_sync_log_remote_idx').on(table.remoteDomain), + index('swarm_sync_log_created_idx').on(table.createdAt), +]); diff --git a/src/instrumentation.ts b/src/instrumentation.ts new file mode 100644 index 0000000..4a70a60 --- /dev/null +++ b/src/instrumentation.ts @@ -0,0 +1,19 @@ +/** + * Next.js Instrumentation + * + * This file runs when the Next.js server starts. + * We use it to initialize background tasks like: + * - Swarm announcement (on startup) + * - Bot cron (every 1 minute) + * - Swarm gossip (every 5 minutes) + * + * This eliminates the need for a separate cron process. + */ + +export async function register() { + // Only run on the server (not during build or in edge runtime) + if (process.env.NEXT_RUNTIME === 'nodejs') { + const { startBackgroundTasks } = await import('@/lib/background/scheduler'); + startBackgroundTasks(); + } +} diff --git a/src/lib/background/scheduler.ts b/src/lib/background/scheduler.ts new file mode 100644 index 0000000..2d26afe --- /dev/null +++ b/src/lib/background/scheduler.ts @@ -0,0 +1,95 @@ +/** + * Background Task Scheduler + * + * Runs periodic tasks within the Next.js process: + * - Bot scheduling (every 1 minute) + * - Swarm gossip (every 5 minutes) + * - Swarm announcement (on startup) + */ + +import { processScheduledPosts } from '@/lib/bots/scheduler'; +import { processAllAutonomousBots } from '@/lib/bots/autonomous'; +import { runGossipRound } from '@/lib/swarm/gossip'; +import { announceToSeeds } from '@/lib/swarm/discovery'; +import { getSwarmStats } from '@/lib/swarm/registry'; + +const BOT_INTERVAL_MS = 60 * 1000; // 1 minute +const GOSSIP_INTERVAL_MS = 5 * 60 * 1000; // 5 minutes +const STARTUP_DELAY_MS = 10 * 1000; // Wait 10s for server to be ready + +let isStarted = false; + +function log(category: string, message: string, data?: unknown) { + const timestamp = new Date().toISOString(); + if (data) { + console.log(`[${timestamp}] [${category}] ${message}`, JSON.stringify(data, null, 2)); + } else { + console.log(`[${timestamp}] [${category}] ${message}`); + } +} + +async function runBotTasks() { + try { + const scheduledResult = await processScheduledPosts(); + const autonomousResult = await processAllAutonomousBots(); + + const posted = autonomousResult.filter(r => r.result.posted).length; + if (scheduledResult.processed > 0 || posted > 0) { + log('BOTS', `Processed ${scheduledResult.processed} scheduled, ${posted} autonomous posts`); + } + } catch (error) { + log('BOTS', `Error: ${error}`); + } +} + +async function runSwarmGossip() { + try { + const result = await runGossipRound(); + if (result.contacted > 0) { + log('SWARM', `Gossip: contacted ${result.contacted}, successful ${result.successful}, received ${result.totalNodesReceived} nodes`); + } + } catch (error) { + log('SWARM', `Gossip error: ${error}`); + } +} + +async function announceToSwarm() { + try { + const result = await announceToSeeds(); + log('SWARM', `Announced to seeds: ${result.successful.length} successful, ${result.failed.length} failed`); + + const stats = await getSwarmStats(); + log('SWARM', `Network: ${stats.activeNodes} active nodes, ${stats.totalUsers} users, ${stats.totalPosts} posts`); + } catch (error) { + log('SWARM', `Announcement error: ${error}`); + } +} + +export function startBackgroundTasks() { + // Prevent double-start (Next.js can call register() multiple times in dev) + if (isStarted) return; + isStarted = true; + + log('STARTUP', 'Background task scheduler starting...'); + log('STARTUP', `Bot interval: ${BOT_INTERVAL_MS / 1000}s, Gossip interval: ${GOSSIP_INTERVAL_MS / 1000}s`); + + // Wait for server to be fully ready before starting tasks + setTimeout(async () => { + log('STARTUP', 'Starting background tasks...'); + + // Announce to swarm on startup + await announceToSwarm(); + + // Run initial bot check + await runBotTasks(); + + // Schedule recurring tasks + setInterval(runBotTasks, BOT_INTERVAL_MS); + setInterval(runSwarmGossip, GOSSIP_INTERVAL_MS); + + // First gossip after 30s (let announcement propagate) + setTimeout(runSwarmGossip, 30 * 1000); + + log('STARTUP', 'Background tasks running'); + }, STARTUP_DELAY_MS); +} diff --git a/src/lib/swarm/discovery.ts b/src/lib/swarm/discovery.ts new file mode 100644 index 0000000..b8f70b6 --- /dev/null +++ b/src/lib/swarm/discovery.ts @@ -0,0 +1,200 @@ +/** + * Swarm Discovery + * + * Handles node discovery and announcement in the swarm network. + */ + +import { db, nodes, users, posts } from '@/db'; +import { eq, sql } from 'drizzle-orm'; +import type { SwarmAnnouncement, SwarmNodeInfo, SwarmCapability } from './types'; +import { upsertSwarmNode, getSeedNodes, markNodeSuccess, markNodeFailure } from './registry'; + +/** + * Build this node's announcement payload + */ +export async function buildAnnouncement(): Promise { + const domain = process.env.NEXT_PUBLIC_NODE_DOMAIN || 'localhost:3000'; + + let name = 'Synapsis Node'; + let description: string | undefined; + let logoUrl: string | undefined; + let publicKey = ''; + let userCount = 0; + let postCount = 0; + let isNsfw = false; + + if (db) { + // Get node info + const node = await db.query.nodes.findFirst({ + where: eq(nodes.domain, domain), + }); + + if (node) { + name = node.name; + description = node.description ?? undefined; + logoUrl = node.logoUrl ?? undefined; + publicKey = node.publicKey ?? ''; + isNsfw = node.isNsfw; + } + + // Get counts + const userResult = await db.select({ count: sql`count(*)` }).from(users); + const postResult = await db.select({ count: sql`count(*)` }).from(posts); + + userCount = Number(userResult[0]?.count ?? 0); + postCount = Number(postResult[0]?.count ?? 0); + } + + const capabilities: SwarmCapability[] = ['handles', 'gossip']; + + return { + domain, + name, + description, + logoUrl, + publicKey, + softwareVersion: '0.1.0', // TODO: Get from package.json + userCount, + postCount, + capabilities, + isNsfw, + timestamp: new Date().toISOString(), + }; +} + +/** + * Announce this node to a remote node + */ +export async function announceToNode(targetDomain: string): Promise<{ success: boolean; error?: string }> { + try { + const announcement = await buildAnnouncement(); + + const baseUrl = targetDomain.startsWith('http') ? targetDomain : `https://${targetDomain}`; + const url = `${baseUrl}/api/swarm/announce`; + + const response = await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + }, + body: JSON.stringify(announcement), + }); + + if (!response.ok) { + const error = await response.text(); + await markNodeFailure(targetDomain); + return { success: false, error: `HTTP ${response.status}: ${error}` }; + } + + // The remote node should respond with their info + const remoteInfo = await response.json() as SwarmNodeInfo; + + // Add/update the remote node in our registry + await upsertSwarmNode(remoteInfo, 'direct'); + await markNodeSuccess(targetDomain); + + return { success: true }; + } catch (error) { + await markNodeFailure(targetDomain); + return { + success: false, + error: error instanceof Error ? error.message : 'Unknown error' + }; + } +} + +/** + * Announce to all seed nodes (bootstrap) + */ +export async function announceToSeeds(): Promise<{ + successful: string[]; + failed: { domain: string; error: string }[] +}> { + const seeds = await getSeedNodes(); + const ourDomain = process.env.NEXT_PUBLIC_NODE_DOMAIN; + + // Don't announce to ourselves + const targetSeeds = seeds.filter(s => s !== ourDomain); + + const successful: string[] = []; + const failed: { domain: string; error: string }[] = []; + + for (const seed of targetSeeds) { + const result = await announceToNode(seed); + if (result.success) { + successful.push(seed); + } else { + failed.push({ domain: seed, error: result.error || 'Unknown error' }); + } + } + + return { successful, failed }; +} + +/** + * Fetch node info from a remote node + */ +export async function fetchNodeInfo(domain: string): Promise { + try { + const baseUrl = domain.startsWith('http') ? domain : `https://${domain}`; + + // Try the swarm endpoint first + let response = await fetch(`${baseUrl}/api/swarm/info`, { + headers: { 'Accept': 'application/json' }, + }); + + if (!response.ok) { + // Fall back to standard node endpoint + response = await fetch(`${baseUrl}/api/node`, { + headers: { 'Accept': 'application/json' }, + }); + } + + if (!response.ok) { + return null; + } + + const data = await response.json(); + + return { + domain: data.domain || domain, + name: data.name, + description: data.description, + logoUrl: data.logoUrl, + publicKey: data.publicKey, + softwareVersion: data.softwareVersion, + userCount: data.userCount, + postCount: data.postCount, + capabilities: data.capabilities, + isNsfw: data.isNsfw, + }; + } catch { + return null; + } +} + +/** + * Discover a node and add it to the registry + */ +export async function discoverNode( + domain: string, + discoveredVia?: string +): Promise<{ success: boolean; isNew: boolean; error?: string }> { + const ourDomain = process.env.NEXT_PUBLIC_NODE_DOMAIN; + + // Don't discover ourselves + if (domain === ourDomain) { + return { success: false, isNew: false, error: 'Cannot discover self' }; + } + + const info = await fetchNodeInfo(domain); + + if (!info) { + return { success: false, isNew: false, error: 'Could not fetch node info' }; + } + + const result = await upsertSwarmNode(info, discoveredVia); + + return { success: true, isNew: result.isNew }; +} diff --git a/src/lib/swarm/gossip.ts b/src/lib/swarm/gossip.ts new file mode 100644 index 0000000..69cd900 --- /dev/null +++ b/src/lib/swarm/gossip.ts @@ -0,0 +1,237 @@ +/** + * Swarm Gossip Protocol + * + * Implements epidemic-style gossip for node and handle propagation. + * Nodes periodically exchange their known nodes/handles with random peers. + */ + +import { db, handleRegistry } from '@/db'; +import { desc, gt } from 'drizzle-orm'; +import type { SwarmGossipPayload, SwarmGossipResponse, SwarmSyncResult, SwarmNodeInfo } from './types'; +import { SWARM_CONFIG } from './types'; +import { + getNodesForGossip, + getActiveSwarmNodes, + getNodesSince, + upsertSwarmNodes, + markNodeSuccess, + markNodeFailure, + logSync, +} from './registry'; +import { upsertHandleEntries } from '@/lib/federation/handles'; +import { buildAnnouncement } from './discovery'; + +/** + * Build a gossip payload to send to another node + */ +export async function buildGossipPayload(since?: string): Promise { + const ourDomain = process.env.NEXT_PUBLIC_NODE_DOMAIN || 'localhost:3000'; + + // Get nodes to share + let nodes: SwarmNodeInfo[]; + if (since) { + nodes = await getNodesSince(new Date(since), SWARM_CONFIG.maxNodesPerGossip); + } else { + nodes = await getActiveSwarmNodes(SWARM_CONFIG.maxNodesPerGossip); + } + + // Include ourselves in the node list + const announcement = await buildAnnouncement(); + const selfNode: SwarmNodeInfo = { + domain: announcement.domain, + name: announcement.name, + description: announcement.description, + logoUrl: announcement.logoUrl, + publicKey: announcement.publicKey, + softwareVersion: announcement.softwareVersion, + userCount: announcement.userCount, + postCount: announcement.postCount, + capabilities: announcement.capabilities, + lastSeenAt: new Date().toISOString(), + }; + + // Get handles to share + let handles: SwarmGossipPayload['handles'] = []; + if (db) { + const sinceDate = since ? new Date(since) : undefined; + const handleEntries = await db.query.handleRegistry.findMany({ + where: sinceDate ? gt(handleRegistry.updatedAt, sinceDate) : undefined, + orderBy: [desc(handleRegistry.updatedAt)], + limit: SWARM_CONFIG.maxHandlesPerGossip, + }); + + handles = handleEntries.map(h => ({ + handle: h.handle, + did: h.did, + nodeDomain: h.nodeDomain, + updatedAt: h.updatedAt?.toISOString(), + })); + } + + return { + sender: ourDomain, + nodes: [selfNode, ...nodes], + handles, + timestamp: new Date().toISOString(), + since, + }; +} + +/** + * Process incoming gossip and return our response + */ +export async function processGossip( + payload: SwarmGossipPayload +): Promise { + const startTime = Date.now(); + + // Process incoming nodes + const nodeResult = await upsertSwarmNodes(payload.nodes, payload.sender); + + // Process incoming handles + let handlesResult = { added: 0, updated: 0 }; + if (payload.handles && payload.handles.length > 0) { + handlesResult = await upsertHandleEntries(payload.handles); + } + + // Build our response with nodes/handles to share back + const responsePayload = await buildGossipPayload(payload.since); + + return { + nodes: responsePayload.nodes, + handles: responsePayload.handles, + received: { + nodes: nodeResult.added + nodeResult.updated, + handles: handlesResult.added + handlesResult.updated, + }, + }; +} + +/** + * Send gossip to a specific node + */ +export async function gossipToNode( + targetDomain: string, + since?: string +): Promise { + const startTime = Date.now(); + + try { + const payload = await buildGossipPayload(since); + + const baseUrl = targetDomain.startsWith('http') ? targetDomain : `https://${targetDomain}`; + const url = `${baseUrl}/api/swarm/gossip`; + + const response = await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + }, + body: JSON.stringify(payload), + }); + + const durationMs = Date.now() - startTime; + + if (!response.ok) { + const error = `HTTP ${response.status}`; + await markNodeFailure(targetDomain); + await logSync(targetDomain, 'push', { + success: false, + nodesReceived: 0, + nodesSent: payload.nodes.length, + handlesReceived: 0, + handlesSent: payload.handles?.length || 0, + error, + durationMs, + }); + return { + success: false, + nodesReceived: 0, + nodesSent: payload.nodes.length, + handlesReceived: 0, + handlesSent: payload.handles?.length || 0, + error, + durationMs, + }; + } + + const gossipResponse = await response.json() as SwarmGossipResponse; + + // Process the response (nodes and handles they sent back) + const nodeResult = await upsertSwarmNodes(gossipResponse.nodes, targetDomain); + + let handlesResult = { added: 0, updated: 0 }; + if (gossipResponse.handles && gossipResponse.handles.length > 0) { + handlesResult = await upsertHandleEntries(gossipResponse.handles); + } + + await markNodeSuccess(targetDomain); + + const result: SwarmSyncResult = { + success: true, + nodesReceived: nodeResult.added + nodeResult.updated, + nodesSent: payload.nodes.length, + handlesReceived: handlesResult.added + handlesResult.updated, + handlesSent: payload.handles?.length || 0, + durationMs, + }; + + await logSync(targetDomain, 'push', result); + return result; + } catch (error) { + const durationMs = Date.now() - startTime; + const errorMsg = error instanceof Error ? error.message : 'Unknown error'; + + await markNodeFailure(targetDomain); + + const result: SwarmSyncResult = { + success: false, + nodesReceived: 0, + nodesSent: 0, + handlesReceived: 0, + handlesSent: 0, + error: errorMsg, + durationMs, + }; + + await logSync(targetDomain, 'push', result); + return result; + } +} + +/** + * Run a gossip round - contact random nodes and exchange info + */ +export async function runGossipRound(): Promise<{ + contacted: number; + successful: number; + totalNodesReceived: number; + totalHandlesReceived: number; +}> { + // Get random nodes to gossip with + const targets = await getNodesForGossip(SWARM_CONFIG.gossipFanout); + + let contacted = 0; + let successful = 0; + let totalNodesReceived = 0; + let totalHandlesReceived = 0; + + for (const target of targets) { + contacted++; + const result = await gossipToNode(target.domain); + + if (result.success) { + successful++; + totalNodesReceived += result.nodesReceived; + totalHandlesReceived += result.handlesReceived; + } + } + + return { + contacted, + successful, + totalNodesReceived, + totalHandlesReceived, + }; +} diff --git a/src/lib/swarm/index.ts b/src/lib/swarm/index.ts new file mode 100644 index 0000000..35518f4 --- /dev/null +++ b/src/lib/swarm/index.ts @@ -0,0 +1,20 @@ +/** + * Swarm Module + * + * The Synapsis swarm is a decentralized network of nodes that discover + * and communicate with each other using a hybrid approach: + * + * 1. Seed nodes (like node.synapsis.social) provide initial bootstrap + * 2. Gossip protocol spreads node/handle information epidemically + * 3. Any node can discover the full network without central authority + * + * Usage: + * - On node startup: call announceToSeeds() to register with the network + * - Periodically: call runGossipRound() to exchange info with peers + * - On demand: call discoverNode() to add a specific node + */ + +export * from './types'; +export * from './registry'; +export * from './discovery'; +export * from './gossip'; diff --git a/src/lib/swarm/registry.ts b/src/lib/swarm/registry.ts new file mode 100644 index 0000000..0d3a98d --- /dev/null +++ b/src/lib/swarm/registry.ts @@ -0,0 +1,311 @@ +/** + * Swarm Registry + * + * Manages the local registry of known swarm nodes. + */ + +import { db, swarmNodes, swarmSeeds, swarmSyncLog } from '@/db'; +import { eq, desc, and, gt, lt, sql } from 'drizzle-orm'; +import type { SwarmNodeInfo, SwarmCapability, SwarmSyncResult } from './types'; +import { SWARM_CONFIG, DEFAULT_SEED_NODES } from './types'; + +/** + * Get or create a swarm node entry + */ +export async function upsertSwarmNode( + node: SwarmNodeInfo, + discoveredVia?: string +): Promise<{ isNew: boolean }> { + if (!db) { + return { isNew: false }; + } + + const existing = await db.query.swarmNodes.findFirst({ + where: eq(swarmNodes.domain, node.domain), + }); + + const capabilities = node.capabilities ? JSON.stringify(node.capabilities) : null; + + if (!existing) { + await db.insert(swarmNodes).values({ + domain: node.domain, + name: node.name, + description: node.description, + logoUrl: node.logoUrl, + publicKey: node.publicKey, + softwareVersion: node.softwareVersion, + userCount: node.userCount, + postCount: node.postCount, + isNsfw: node.isNsfw ?? false, + discoveredVia, + capabilities, + lastSeenAt: node.lastSeenAt ? new Date(node.lastSeenAt) : new Date(), + }); + return { isNew: true }; + } + + // Update existing node + await db.update(swarmNodes) + .set({ + name: node.name ?? existing.name, + description: node.description ?? existing.description, + logoUrl: node.logoUrl ?? existing.logoUrl, + publicKey: node.publicKey ?? existing.publicKey, + softwareVersion: node.softwareVersion ?? existing.softwareVersion, + userCount: node.userCount ?? existing.userCount, + postCount: node.postCount ?? existing.postCount, + isNsfw: node.isNsfw ?? existing.isNsfw, + capabilities: capabilities ?? existing.capabilities, + lastSeenAt: new Date(), + consecutiveFailures: 0, + isActive: true, + updatedAt: new Date(), + }) + .where(eq(swarmNodes.domain, node.domain)); + + return { isNew: false }; +} + +/** + * Bulk upsert swarm nodes from gossip + */ +export async function upsertSwarmNodes( + nodes: SwarmNodeInfo[], + discoveredVia: string +): Promise<{ added: number; updated: number }> { + if (!db || nodes.length === 0) { + return { added: 0, updated: 0 }; + } + + let added = 0; + let updated = 0; + + // Filter out our own domain + const ourDomain = process.env.NEXT_PUBLIC_NODE_DOMAIN; + const filteredNodes = nodes.filter(n => n.domain !== ourDomain); + + for (const node of filteredNodes) { + const result = await upsertSwarmNode(node, discoveredVia); + if (result.isNew) { + added++; + } else { + updated++; + } + } + + return { added, updated }; +} + +/** + * Get all active swarm nodes + */ +export async function getActiveSwarmNodes(limit = 100): Promise { + if (!db) { + return []; + } + + const nodes = await db.query.swarmNodes.findMany({ + where: eq(swarmNodes.isActive, true), + orderBy: [desc(swarmNodes.lastSeenAt)], + limit, + }); + + return nodes.map(nodeToInfo); +} + +/** + * Get nodes for gossip (random selection of active nodes) + */ +export async function getNodesForGossip(count: number): Promise { + if (!db) { + return []; + } + + // Get active nodes with decent trust scores, ordered randomly + const nodes = await db.query.swarmNodes.findMany({ + where: and( + eq(swarmNodes.isActive, true), + gt(swarmNodes.trustScore, 20) + ), + orderBy: sql`RANDOM()`, + limit: count, + }); + + return nodes.map(nodeToInfo); +} + +/** + * Get nodes updated since a timestamp (for incremental sync) + */ +export async function getNodesSince(since: Date, limit = 100): Promise { + if (!db) { + return []; + } + + const nodes = await db.query.swarmNodes.findMany({ + where: gt(swarmNodes.updatedAt, since), + orderBy: [desc(swarmNodes.updatedAt)], + limit, + }); + + return nodes.map(nodeToInfo); +} + +/** + * Mark a node as having failed contact + */ +export async function markNodeFailure(domain: string): Promise { + if (!db) return; + + const node = await db.query.swarmNodes.findFirst({ + where: eq(swarmNodes.domain, domain), + }); + + if (!node) return; + + const newFailures = node.consecutiveFailures + 1; + const newTrust = Math.max( + SWARM_CONFIG.minTrustScore, + node.trustScore + SWARM_CONFIG.trustScoreOnFailure + ); + const isActive = newFailures < SWARM_CONFIG.maxConsecutiveFailures; + + await db.update(swarmNodes) + .set({ + consecutiveFailures: newFailures, + trustScore: newTrust, + isActive, + updatedAt: new Date(), + }) + .where(eq(swarmNodes.domain, domain)); +} + +/** + * Mark a node as successfully contacted + */ +export async function markNodeSuccess(domain: string): Promise { + if (!db) return; + + const node = await db.query.swarmNodes.findFirst({ + where: eq(swarmNodes.domain, domain), + }); + + if (!node) return; + + const newTrust = Math.min( + SWARM_CONFIG.maxTrustScore, + node.trustScore + SWARM_CONFIG.trustScoreOnSuccess + ); + + await db.update(swarmNodes) + .set({ + consecutiveFailures: 0, + trustScore: newTrust, + isActive: true, + lastSeenAt: new Date(), + lastSyncAt: new Date(), + updatedAt: new Date(), + }) + .where(eq(swarmNodes.domain, domain)); +} + +/** + * Log a sync operation + */ +export async function logSync( + remoteDomain: string, + direction: 'push' | 'pull', + result: SwarmSyncResult +): Promise { + if (!db) return; + + await db.insert(swarmSyncLog).values({ + remoteDomain, + direction, + nodesReceived: result.nodesReceived, + nodesSent: result.nodesSent, + handlesReceived: result.handlesReceived, + handlesSent: result.handlesSent, + success: result.success, + errorMessage: result.error, + durationMs: result.durationMs, + }); +} + +/** + * Get seed nodes (with fallback to defaults) + */ +export async function getSeedNodes(): Promise { + if (!db) { + return [...DEFAULT_SEED_NODES]; + } + + const seeds = await db.query.swarmSeeds.findMany({ + where: eq(swarmSeeds.isEnabled, true), + orderBy: [swarmSeeds.priority], + }); + + if (seeds.length === 0) { + return [...DEFAULT_SEED_NODES]; + } + + return seeds.map(s => s.domain); +} + +/** + * Add a seed node + */ +export async function addSeedNode(domain: string, priority = 100): Promise { + if (!db) return; + + await db.insert(swarmSeeds) + .values({ domain, priority }) + .onConflictDoUpdate({ + target: swarmSeeds.domain, + set: { priority, isEnabled: true }, + }); +} + +/** + * Get swarm statistics + */ +export async function getSwarmStats() { + if (!db) { + return { + totalNodes: 0, + activeNodes: 0, + totalUsers: 0, + totalPosts: 0, + }; + } + + const allNodes = await db.query.swarmNodes.findMany(); + const activeNodes = allNodes.filter(n => n.isActive); + + const totalUsers = activeNodes.reduce((sum, n) => sum + (n.userCount || 0), 0); + const totalPosts = activeNodes.reduce((sum, n) => sum + (n.postCount || 0), 0); + + return { + totalNodes: allNodes.length, + activeNodes: activeNodes.length, + totalUsers, + totalPosts, + }; +} + +// Helper to convert DB node to SwarmNodeInfo +function nodeToInfo(node: typeof swarmNodes.$inferSelect): SwarmNodeInfo { + return { + domain: node.domain, + name: node.name ?? undefined, + description: node.description ?? undefined, + logoUrl: node.logoUrl ?? undefined, + publicKey: node.publicKey ?? undefined, + softwareVersion: node.softwareVersion ?? undefined, + userCount: node.userCount ?? undefined, + postCount: node.postCount ?? undefined, + capabilities: node.capabilities ? JSON.parse(node.capabilities) : undefined, + isNsfw: node.isNsfw, + lastSeenAt: node.lastSeenAt.toISOString(), + }; +} diff --git a/src/lib/swarm/timeline.ts b/src/lib/swarm/timeline.ts new file mode 100644 index 0000000..6ac005b --- /dev/null +++ b/src/lib/swarm/timeline.ts @@ -0,0 +1,129 @@ +/** + * Swarm Timeline + * + * Fetches and aggregates posts from across the swarm network. + */ + +import { getActiveSwarmNodes } from './registry'; +import type { SwarmPost } from '@/app/api/swarm/timeline/route'; + +interface TimelineResult { + posts: SwarmPost[]; + sources: { domain: string; postCount: number; isNsfw?: boolean; error?: string }[]; + fetchedAt: string; +} + +interface TimelineOptions { + includeNsfw?: boolean; // Whether to include NSFW content +} + +/** + * Fetch timeline from a single node + */ +async function fetchNodeTimeline( + domain: string, + limit: number = 20 +): Promise<{ posts: SwarmPost[]; nodeIsNsfw?: boolean; error?: string }> { + try { + const baseUrl = domain.startsWith('http') ? domain : `https://${domain}`; + const url = `${baseUrl}/api/swarm/timeline?limit=${limit}`; + + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), 5000); // 5s timeout + + const response = await fetch(url, { + headers: { 'Accept': 'application/json' }, + signal: controller.signal, + }); + + clearTimeout(timeout); + + if (!response.ok) { + return { posts: [], error: `HTTP ${response.status}` }; + } + + const data = await response.json(); + return { posts: data.posts || [], nodeIsNsfw: data.nodeIsNsfw }; + } catch (error) { + const message = error instanceof Error ? error.message : 'Unknown error'; + return { posts: [], error: message }; + } +} + +/** + * Fetch aggregated timeline from the swarm + * + * Queries multiple nodes in parallel and merges results. + * Filters out NSFW content unless explicitly requested. + */ +export async function fetchSwarmTimeline( + maxNodes: number = 10, + postsPerNode: number = 10, + options: TimelineOptions = {} +): Promise { + const { includeNsfw = false } = options; + + // Get active nodes to query + const nodes = await getActiveSwarmNodes(maxNodes); + + // Always include our own posts + const ourDomain = process.env.NEXT_PUBLIC_NODE_DOMAIN || 'localhost'; + + // Filter out NSFW nodes if not including NSFW content + const eligibleNodes = includeNsfw + ? nodes + : nodes.filter(n => !n.isNsfw); + + const nodesToQuery = [ + ourDomain, + ...eligibleNodes.map(n => n.domain).filter(d => d !== ourDomain) + ].slice(0, maxNodes); + + // Fetch from all nodes in parallel + const results = await Promise.all( + nodesToQuery.map(async (domain) => { + const result = await fetchNodeTimeline(domain, postsPerNode); + return { + domain, + ...result, + }; + }) + ); + + // Collect all posts and track sources + const allPosts: SwarmPost[] = []; + const sources: TimelineResult['sources'] = []; + + for (const result of results) { + sources.push({ + domain: result.domain, + postCount: result.posts.length, + isNsfw: result.nodeIsNsfw, + error: result.error, + }); + + // Filter NSFW posts if not including NSFW + const filteredPosts = includeNsfw + ? result.posts + : result.posts.filter(p => !p.isNsfw); + + allPosts.push(...filteredPosts); + } + + // Sort by createdAt descending and dedupe by id + const seen = new Set(); + const uniquePosts = allPosts + .sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()) + .filter(post => { + const key = `${post.nodeDomain}:${post.id}`; + if (seen.has(key)) return false; + seen.add(key); + return true; + }); + + return { + posts: uniquePosts, + sources, + fetchedAt: new Date().toISOString(), + }; +} diff --git a/src/lib/swarm/types.ts b/src/lib/swarm/types.ts new file mode 100644 index 0000000..c015e57 --- /dev/null +++ b/src/lib/swarm/types.ts @@ -0,0 +1,128 @@ +/** + * Swarm Types + * + * Type definitions for the Synapsis swarm network. + */ + +export interface SwarmNodeInfo { + domain: string; + name?: string; + description?: string; + logoUrl?: string; + publicKey?: string; + softwareVersion?: string; + userCount?: number; + postCount?: number; + capabilities?: SwarmCapability[]; + isNsfw?: boolean; + lastSeenAt?: string; +} + +export type SwarmCapability = 'handles' | 'gossip' | 'relay' | 'search'; + +export interface SwarmAnnouncement { + domain: string; + name: string; + description?: string; + logoUrl?: string; + publicKey: string; + softwareVersion: string; + userCount: number; + postCount: number; + capabilities: SwarmCapability[]; + isNsfw: boolean; + timestamp: string; + signature?: string; // Signed with node's private key +} + +export interface SwarmGossipPayload { + // The node sending this gossip + sender: string; + + // Nodes this sender knows about + nodes: SwarmNodeInfo[]; + + // Optional: handles to sync (piggyback on gossip) + handles?: { + handle: string; + did: string; + nodeDomain: string; + updatedAt?: string; + }[]; + + // Timestamp for freshness + timestamp: string; + + // Since parameter for incremental sync + since?: string; +} + +export interface SwarmGossipResponse { + // Nodes we're sharing back + nodes: SwarmNodeInfo[]; + + // Handles we're sharing back + handles?: { + handle: string; + did: string; + nodeDomain: string; + updatedAt?: string; + }[]; + + // Stats about what we received + received: { + nodes: number; + handles: number; + }; +} + +export interface SwarmSyncResult { + success: boolean; + nodesReceived: number; + nodesSent: number; + handlesReceived: number; + handlesSent: number; + error?: string; + durationMs: number; +} + +export interface SwarmStats { + totalNodes: number; + activeNodes: number; + totalUsers: number; + totalPosts: number; + lastUpdated: string; +} + +// Default seed nodes for bootstrapping +export const DEFAULT_SEED_NODES = [ + 'node.synapsis.social', +] as const; + +// Swarm configuration +export const SWARM_CONFIG = { + // How often to run gossip (in ms) + gossipIntervalMs: 5 * 60 * 1000, // 5 minutes + + // How many nodes to gossip with per round + gossipFanout: 3, + + // Max nodes to include in a single gossip message + maxNodesPerGossip: 100, + + // Max handles to include in a single gossip message + maxHandlesPerGossip: 500, + + // How long before a node is considered inactive + inactiveThresholdMs: 24 * 60 * 60 * 1000, // 24 hours + + // How many consecutive failures before marking inactive + maxConsecutiveFailures: 5, + + // Trust score adjustments + trustScoreOnSuccess: 1, + trustScoreOnFailure: -5, + minTrustScore: 0, + maxTrustScore: 100, + defaultTrustScore: 50, +} as const; diff --git a/src/lib/types.ts b/src/lib/types.ts index 59c676f..52edb01 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -61,4 +61,5 @@ export interface Post { handle: string; ownerId: string; } | null; + nodeDomain?: string | null; // Domain of the node this post came from (for swarm posts) }