From 3baac85cf76abd2a474e72d7d2e46c8a0aa7569d Mon Sep 17 00:00:00 2001 From: Jesse Wierzbinski Date: Sat, 27 Jul 2024 20:46:19 +0200 Subject: [PATCH] refactor: :recycle: Rewrite relationship system --- classes/functions/relationship.ts | 96 - classes/functions/user.ts | 118 +- drizzle/migrations/0031_mature_demogoblin.sql | 3 + drizzle/migrations/meta/0031_snapshot.json | 2126 +++++++++++++++++ drizzle/migrations/meta/_journal.json | 7 + drizzle/schema.ts | 3 - packages/database-interface/relationship.ts | 295 +++ packages/database-interface/user.ts | 49 + server/api/api/v1/accounts/:id/block.ts | 24 +- server/api/api/v1/accounts/:id/follow.ts | 19 +- server/api/api/v1/accounts/:id/mute.ts | 30 +- server/api/api/v1/accounts/:id/note.ts | 22 +- server/api/api/v1/accounts/:id/pin.ts | 24 +- .../v1/accounts/:id/remove_from_followers.ts | 47 +- server/api/api/v1/accounts/:id/unblock.ts | 24 +- server/api/api/v1/accounts/:id/unfollow.ts | 25 +- server/api/api/v1/accounts/:id/unmute.ts | 27 +- server/api/api/v1/accounts/:id/unpin.ts | 24 +- .../v1/accounts/relationships/index.test.ts | 2 +- .../api/v1/accounts/relationships/index.ts | 36 +- .../follow_requests/:account_id/authorize.ts | 59 +- .../v1/follow_requests/:account_id/reject.ts | 59 +- server/api/users/:uuid/inbox/index.ts | 111 +- 23 files changed, 2634 insertions(+), 596 deletions(-) delete mode 100644 classes/functions/relationship.ts create mode 100644 drizzle/migrations/0031_mature_demogoblin.sql create mode 100644 drizzle/migrations/meta/0031_snapshot.json create mode 100644 packages/database-interface/relationship.ts diff --git a/classes/functions/relationship.ts b/classes/functions/relationship.ts deleted file mode 100644 index 90a18531..00000000 --- a/classes/functions/relationship.ts +++ /dev/null @@ -1,96 +0,0 @@ -import type { Relationship as ApiRelationship } from "@lysand-org/client/types"; -import type { InferSelectModel } from "drizzle-orm"; -import { db } from "~/drizzle/db"; -import { Relationships } from "~/drizzle/schema"; -import type { User } from "~/packages/database-interface/user"; - -export type Relationship = InferSelectModel & { - requestedBy: boolean; -}; - -/** - * Creates a new relationship between two users. - * @param owner The user who owns the relationship. - * @param other The user who is the subject of the relationship. - * @returns The newly created relationship. - */ -export const createNewRelationship = async ( - owner: User, - other: User, -): Promise => { - return { - ...( - await db - .insert(Relationships) - .values({ - ownerId: owner.id, - subjectId: other.id, - languages: [], - following: false, - showingReblogs: false, - notifying: false, - followedBy: false, - blocking: false, - blockedBy: false, - muting: false, - mutingNotifications: false, - requested: false, - domainBlocking: false, - endorsed: false, - note: "", - updatedAt: new Date().toISOString(), - }) - .returning() - )[0], - requestedBy: false, - }; -}; - -export const checkForBidirectionalRelationships = async ( - user1: User, - user2: User, - createIfNotExists = true, -): Promise => { - const relationship1 = await db.query.Relationships.findFirst({ - where: (rel, { and, eq }) => - and(eq(rel.ownerId, user1.id), eq(rel.subjectId, user2.id)), - }); - - const relationship2 = await db.query.Relationships.findFirst({ - where: (rel, { and, eq }) => - and(eq(rel.ownerId, user2.id), eq(rel.subjectId, user1.id)), - }); - - if (!relationship1 && createIfNotExists) { - await createNewRelationship(user1, user2); - } - - if (!relationship2 && createIfNotExists) { - await createNewRelationship(user2, user1); - } - - return !!relationship1 && !!relationship2; -}; - -/** - * Converts the relationship to an API-friendly format. - * @returns The API-friendly relationship. - */ -export const relationshipToApi = (rel: Relationship): ApiRelationship => { - return { - blocked_by: rel.blockedBy, - blocking: rel.blocking, - domain_blocking: rel.domainBlocking, - endorsed: rel.endorsed, - followed_by: rel.followedBy, - following: rel.following, - id: rel.subjectId, - muting: rel.muting, - muting_notifications: rel.mutingNotifications, - notifying: rel.notifying, - requested: rel.requested, - requested_by: rel.requestedBy, - showing_reblogs: rel.showingReblogs, - note: rel.note, - }; -}; diff --git a/classes/functions/user.ts b/classes/functions/user.ts index 206d27d0..7f5753d3 100644 --- a/classes/functions/user.ts +++ b/classes/functions/user.ts @@ -4,13 +4,11 @@ import type { FollowReject, } from "@lysand-org/federation/types"; import { config } from "config-manager"; -import { type InferSelectModel, and, eq, sql } from "drizzle-orm"; +import { type InferSelectModel, eq, sql } from "drizzle-orm"; import { db } from "~/drizzle/db"; import { Applications, type Instances, - Notifications, - Relationships, type Roles, Tokens, type Users, @@ -18,11 +16,6 @@ import { import type { EmojiWithInstance } from "~/packages/database-interface/emoji"; import { User } from "~/packages/database-interface/user"; import type { Application } from "./application"; -import { - type Relationship, - checkForBidirectionalRelationships, - createNewRelationship, -} from "./relationship"; import type { Token } from "./token"; export type UserType = InferSelectModel; @@ -119,85 +112,6 @@ export const getFromHeader = async (value: string): Promise => { return { user, token, application }; }; -export const followRequestUser = async ( - follower: User, - followee: User, - relationshipId: string, - reblogs = false, - notify = false, - languages: string[] = [], -): Promise => { - const isRemote = followee.isRemote(); - - await db - .update(Relationships) - .set({ - following: isRemote ? false : !followee.data.isLocked, - requested: isRemote ? true : followee.data.isLocked, - showingReblogs: reblogs, - notifying: notify, - languages: languages, - }) - .where(eq(Relationships.id, relationshipId)); - - // Set requested_by on other side - await db - .update(Relationships) - .set({ - requestedBy: isRemote ? true : followee.data.isLocked, - followedBy: isRemote ? false : followee.data.isLocked, - }) - .where( - and( - eq(Relationships.ownerId, followee.id), - eq(Relationships.subjectId, follower.id), - ), - ); - - const updatedRelationship = await db.query.Relationships.findFirst({ - where: (rel, { eq }) => eq(rel.id, relationshipId), - }); - - if (!updatedRelationship) { - throw new Error("Failed to update relationship"); - } - - if (isRemote) { - const { ok } = await follower.federateToUser( - followRequestToLysand(follower, followee), - followee, - ); - - if (!ok) { - await db - .update(Relationships) - .set({ - following: false, - requested: false, - }) - .where(eq(Relationships.id, relationshipId)); - - const result = await db.query.Relationships.findFirst({ - where: (rel, { eq }) => eq(rel.id, relationshipId), - }); - - if (!result) { - throw new Error("Failed to update relationship"); - } - - return result; - } - } else { - await db.insert(Notifications).values({ - accountId: follower.id, - type: followee.data.isLocked ? "follow_request" : "follow", - notifiedId: followee.id, - }); - } - - return updatedRelationship; -}; - export const sendFollowAccept = async (follower: User, followee: User) => { await follower.federateToUser( followAcceptToLysand(follower, followee), @@ -344,36 +258,6 @@ export const retrieveToken = async ( ); }; -/** - * Gets the relationship to another user. - * @param other The other user to get the relationship to. - * @returns The relationship to the other user. - */ -export const getRelationshipToOtherUser = async ( - user: User, - other: User, -): Promise => { - await checkForBidirectionalRelationships(user, other); - - const foundRelationship = await db.query.Relationships.findFirst({ - where: (relationship, { and, eq }) => - and( - eq(relationship.ownerId, user.id), - eq(relationship.subjectId, other.id), - ), - }); - - if (!foundRelationship) { - // Create new relationship - - const newRelationship = await createNewRelationship(user, other); - - return newRelationship; - } - - return foundRelationship; -}; - export const followRequestToLysand = ( follower: User, followee: User, diff --git a/drizzle/migrations/0031_mature_demogoblin.sql b/drizzle/migrations/0031_mature_demogoblin.sql new file mode 100644 index 00000000..252283c2 --- /dev/null +++ b/drizzle/migrations/0031_mature_demogoblin.sql @@ -0,0 +1,3 @@ +ALTER TABLE "Relationships" DROP COLUMN IF EXISTS "followed_by";--> statement-breakpoint +ALTER TABLE "Relationships" DROP COLUMN IF EXISTS "blocked_by";--> statement-breakpoint +ALTER TABLE "Relationships" DROP COLUMN IF EXISTS "requested_by"; \ No newline at end of file diff --git a/drizzle/migrations/meta/0031_snapshot.json b/drizzle/migrations/meta/0031_snapshot.json new file mode 100644 index 00000000..adddd21d --- /dev/null +++ b/drizzle/migrations/meta/0031_snapshot.json @@ -0,0 +1,2126 @@ +{ + "id": "93c2cedf-461e-4534-96ec-570c8f6af4fd", + "prevId": "a41c99eb-3a42-43d1-960d-4abb2538649b", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.Applications": { + "name": "Applications", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "uuid_generate_v7()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "website": { + "name": "website", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "vapid_key": { + "name": "vapid_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "secret": { + "name": "secret", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scopes": { + "name": "scopes", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "redirect_uri": { + "name": "redirect_uri", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "Applications_client_id_index": { + "name": "Applications_client_id_index", + "columns": [ + { + "expression": "client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.Attachments": { + "name": "Attachments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "uuid_generate_v7()" + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "remote_url": { + "name": "remote_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "thumbnail_url": { + "name": "thumbnail_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mime_type": { + "name": "mime_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "blurhash": { + "name": "blurhash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sha256": { + "name": "sha256", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "fps": { + "name": "fps", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "duration": { + "name": "duration", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "width": { + "name": "width", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "height": { + "name": "height", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "noteId": { + "name": "noteId", + "type": "uuid", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "Attachments_noteId_Notes_id_fk": { + "name": "Attachments_noteId_Notes_id_fk", + "tableFrom": "Attachments", + "tableTo": "Notes", + "columnsFrom": ["noteId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.Challenges": { + "name": "Challenges", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "uuid_generate_v7()" + }, + "challenge": { + "name": "challenge", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "default": "NOW() + INTERVAL '5 minutes'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.EmojiToNote": { + "name": "EmojiToNote", + "schema": "", + "columns": { + "emojiId": { + "name": "emojiId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "noteId": { + "name": "noteId", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "EmojiToNote_emojiId_noteId_index": { + "name": "EmojiToNote_emojiId_noteId_index", + "columns": [ + { + "expression": "emojiId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "noteId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "EmojiToNote_noteId_index": { + "name": "EmojiToNote_noteId_index", + "columns": [ + { + "expression": "noteId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "EmojiToNote_emojiId_Emojis_id_fk": { + "name": "EmojiToNote_emojiId_Emojis_id_fk", + "tableFrom": "EmojiToNote", + "tableTo": "Emojis", + "columnsFrom": ["emojiId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "EmojiToNote_noteId_Notes_id_fk": { + "name": "EmojiToNote_noteId_Notes_id_fk", + "tableFrom": "EmojiToNote", + "tableTo": "Notes", + "columnsFrom": ["noteId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.EmojiToUser": { + "name": "EmojiToUser", + "schema": "", + "columns": { + "emojiId": { + "name": "emojiId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "EmojiToUser_emojiId_userId_index": { + "name": "EmojiToUser_emojiId_userId_index", + "columns": [ + { + "expression": "emojiId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "userId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "EmojiToUser_userId_index": { + "name": "EmojiToUser_userId_index", + "columns": [ + { + "expression": "userId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "EmojiToUser_emojiId_Emojis_id_fk": { + "name": "EmojiToUser_emojiId_Emojis_id_fk", + "tableFrom": "EmojiToUser", + "tableTo": "Emojis", + "columnsFrom": ["emojiId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "EmojiToUser_userId_Users_id_fk": { + "name": "EmojiToUser_userId_Users_id_fk", + "tableFrom": "EmojiToUser", + "tableTo": "Users", + "columnsFrom": ["userId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.Emojis": { + "name": "Emojis", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "uuid_generate_v7()" + }, + "shortcode": { + "name": "shortcode", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "visible_in_picker": { + "name": "visible_in_picker", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "alt": { + "name": "alt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "content_type": { + "name": "content_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "instanceId": { + "name": "instanceId", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "ownerId": { + "name": "ownerId", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "Emojis_instanceId_Instances_id_fk": { + "name": "Emojis_instanceId_Instances_id_fk", + "tableFrom": "Emojis", + "tableTo": "Instances", + "columnsFrom": ["instanceId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "Emojis_ownerId_Users_id_fk": { + "name": "Emojis_ownerId_Users_id_fk", + "tableFrom": "Emojis", + "tableTo": "Users", + "columnsFrom": ["ownerId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.FilterKeywords": { + "name": "FilterKeywords", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "uuid_generate_v7()" + }, + "filterId": { + "name": "filterId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "keyword": { + "name": "keyword", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "whole_word": { + "name": "whole_word", + "type": "boolean", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "FilterKeywords_filterId_Filters_id_fk": { + "name": "FilterKeywords_filterId_Filters_id_fk", + "tableFrom": "FilterKeywords", + "tableTo": "Filters", + "columnsFrom": ["filterId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.Filters": { + "name": "Filters", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "uuid_generate_v7()" + }, + "userId": { + "name": "userId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "context": { + "name": "context", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "filter_action": { + "name": "filter_action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "Filters_userId_Users_id_fk": { + "name": "Filters_userId_Users_id_fk", + "tableFrom": "Filters", + "tableTo": "Users", + "columnsFrom": ["userId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.Flags": { + "name": "Flags", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "uuid_generate_v7()" + }, + "flag_type": { + "name": "flag_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'other'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "noteId": { + "name": "noteId", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "userId": { + "name": "userId", + "type": "uuid", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "Flags_noteId_Notes_id_fk": { + "name": "Flags_noteId_Notes_id_fk", + "tableFrom": "Flags", + "tableTo": "Notes", + "columnsFrom": ["noteId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "Flags_userId_Users_id_fk": { + "name": "Flags_userId_Users_id_fk", + "tableFrom": "Flags", + "tableTo": "Users", + "columnsFrom": ["userId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.Instances": { + "name": "Instances", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "uuid_generate_v7()" + }, + "base_url": { + "name": "base_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "logo": { + "name": "logo", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "disable_automoderation": { + "name": "disable_automoderation", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "protocol": { + "name": "protocol", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'lysand'" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.Likes": { + "name": "Likes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "uuid_generate_v7()" + }, + "likerId": { + "name": "likerId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "likedId": { + "name": "likedId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "Likes_likerId_Users_id_fk": { + "name": "Likes_likerId_Users_id_fk", + "tableFrom": "Likes", + "tableTo": "Users", + "columnsFrom": ["likerId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "Likes_likedId_Notes_id_fk": { + "name": "Likes_likedId_Notes_id_fk", + "tableFrom": "Likes", + "tableTo": "Notes", + "columnsFrom": ["likedId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.LysandObject": { + "name": "LysandObject", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "uuid_generate_v7()" + }, + "remote_id": { + "name": "remote_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "uri": { + "name": "uri", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "authorId": { + "name": "authorId", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "extra_data": { + "name": "extra_data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "extensions": { + "name": "extensions", + "type": "jsonb", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "LysandObject_remote_id_index": { + "name": "LysandObject_remote_id_index", + "columns": [ + { + "expression": "remote_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "LysandObject_uri_index": { + "name": "LysandObject_uri_index", + "columns": [ + { + "expression": "uri", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "LysandObject_authorId_LysandObject_id_fk": { + "name": "LysandObject_authorId_LysandObject_id_fk", + "tableFrom": "LysandObject", + "tableTo": "LysandObject", + "columnsFrom": ["authorId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.Markers": { + "name": "Markers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "uuid_generate_v7()" + }, + "noteId": { + "name": "noteId", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "notificationId": { + "name": "notificationId", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "userId": { + "name": "userId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "timeline": { + "name": "timeline", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "Markers_noteId_Notes_id_fk": { + "name": "Markers_noteId_Notes_id_fk", + "tableFrom": "Markers", + "tableTo": "Notes", + "columnsFrom": ["noteId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "Markers_notificationId_Notifications_id_fk": { + "name": "Markers_notificationId_Notifications_id_fk", + "tableFrom": "Markers", + "tableTo": "Notifications", + "columnsFrom": ["notificationId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "Markers_userId_Users_id_fk": { + "name": "Markers_userId_Users_id_fk", + "tableFrom": "Markers", + "tableTo": "Users", + "columnsFrom": ["userId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.ModNotes": { + "name": "ModNotes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "uuid_generate_v7()" + }, + "noteId": { + "name": "noteId", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "userId": { + "name": "userId", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "modId": { + "name": "modId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "note": { + "name": "note", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "ModNotes_noteId_Notes_id_fk": { + "name": "ModNotes_noteId_Notes_id_fk", + "tableFrom": "ModNotes", + "tableTo": "Notes", + "columnsFrom": ["noteId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "ModNotes_userId_Users_id_fk": { + "name": "ModNotes_userId_Users_id_fk", + "tableFrom": "ModNotes", + "tableTo": "Users", + "columnsFrom": ["userId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "ModNotes_modId_Users_id_fk": { + "name": "ModNotes_modId_Users_id_fk", + "tableFrom": "ModNotes", + "tableTo": "Users", + "columnsFrom": ["modId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.ModTags": { + "name": "ModTags", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "uuid_generate_v7()" + }, + "noteId": { + "name": "noteId", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "userId": { + "name": "userId", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "modId": { + "name": "modId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "tag": { + "name": "tag", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "ModTags_noteId_Notes_id_fk": { + "name": "ModTags_noteId_Notes_id_fk", + "tableFrom": "ModTags", + "tableTo": "Notes", + "columnsFrom": ["noteId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "ModTags_userId_Users_id_fk": { + "name": "ModTags_userId_Users_id_fk", + "tableFrom": "ModTags", + "tableTo": "Users", + "columnsFrom": ["userId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "ModTags_modId_Users_id_fk": { + "name": "ModTags_modId_Users_id_fk", + "tableFrom": "ModTags", + "tableTo": "Users", + "columnsFrom": ["modId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.NoteToMentions": { + "name": "NoteToMentions", + "schema": "", + "columns": { + "noteId": { + "name": "noteId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "NoteToMentions_noteId_userId_index": { + "name": "NoteToMentions_noteId_userId_index", + "columns": [ + { + "expression": "noteId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "userId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "NoteToMentions_userId_index": { + "name": "NoteToMentions_userId_index", + "columns": [ + { + "expression": "userId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "NoteToMentions_noteId_Notes_id_fk": { + "name": "NoteToMentions_noteId_Notes_id_fk", + "tableFrom": "NoteToMentions", + "tableTo": "Notes", + "columnsFrom": ["noteId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "NoteToMentions_userId_Users_id_fk": { + "name": "NoteToMentions_userId_Users_id_fk", + "tableFrom": "NoteToMentions", + "tableTo": "Users", + "columnsFrom": ["userId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.Notes": { + "name": "Notes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "uuid_generate_v7()" + }, + "uri": { + "name": "uri", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "authorId": { + "name": "authorId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "reblogId": { + "name": "reblogId", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "content_type": { + "name": "content_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text/plain'" + }, + "visibility": { + "name": "visibility", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "replyId": { + "name": "replyId", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "quoteId": { + "name": "quoteId", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "sensitive": { + "name": "sensitive", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "spoiler_text": { + "name": "spoiler_text", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "applicationId": { + "name": "applicationId", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "content_source": { + "name": "content_source", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + } + }, + "indexes": {}, + "foreignKeys": { + "Notes_authorId_Users_id_fk": { + "name": "Notes_authorId_Users_id_fk", + "tableFrom": "Notes", + "tableTo": "Users", + "columnsFrom": ["authorId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "Notes_reblogId_Notes_id_fk": { + "name": "Notes_reblogId_Notes_id_fk", + "tableFrom": "Notes", + "tableTo": "Notes", + "columnsFrom": ["reblogId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "Notes_replyId_Notes_id_fk": { + "name": "Notes_replyId_Notes_id_fk", + "tableFrom": "Notes", + "tableTo": "Notes", + "columnsFrom": ["replyId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "Notes_quoteId_Notes_id_fk": { + "name": "Notes_quoteId_Notes_id_fk", + "tableFrom": "Notes", + "tableTo": "Notes", + "columnsFrom": ["quoteId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "Notes_applicationId_Applications_id_fk": { + "name": "Notes_applicationId_Applications_id_fk", + "tableFrom": "Notes", + "tableTo": "Applications", + "columnsFrom": ["applicationId"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "Notes_uri_unique": { + "name": "Notes_uri_unique", + "nullsNotDistinct": false, + "columns": ["uri"] + } + } + }, + "public.Notifications": { + "name": "Notifications", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "uuid_generate_v7()" + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "notifiedId": { + "name": "notifiedId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "accountId": { + "name": "accountId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "noteId": { + "name": "noteId", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "dismissed": { + "name": "dismissed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "Notifications_notifiedId_Users_id_fk": { + "name": "Notifications_notifiedId_Users_id_fk", + "tableFrom": "Notifications", + "tableTo": "Users", + "columnsFrom": ["notifiedId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "Notifications_accountId_Users_id_fk": { + "name": "Notifications_accountId_Users_id_fk", + "tableFrom": "Notifications", + "tableTo": "Users", + "columnsFrom": ["accountId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "Notifications_noteId_Notes_id_fk": { + "name": "Notifications_noteId_Notes_id_fk", + "tableFrom": "Notifications", + "tableTo": "Notes", + "columnsFrom": ["noteId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.OpenIdAccounts": { + "name": "OpenIdAccounts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "uuid_generate_v7()" + }, + "userId": { + "name": "userId", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "server_id": { + "name": "server_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "issuer_id": { + "name": "issuer_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "OpenIdAccounts_userId_Users_id_fk": { + "name": "OpenIdAccounts_userId_Users_id_fk", + "tableFrom": "OpenIdAccounts", + "tableTo": "Users", + "columnsFrom": ["userId"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.OpenIdLoginFlows": { + "name": "OpenIdLoginFlows", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "uuid_generate_v7()" + }, + "code_verifier": { + "name": "code_verifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "applicationId": { + "name": "applicationId", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "issuer_id": { + "name": "issuer_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "OpenIdLoginFlows_applicationId_Applications_id_fk": { + "name": "OpenIdLoginFlows_applicationId_Applications_id_fk", + "tableFrom": "OpenIdLoginFlows", + "tableTo": "Applications", + "columnsFrom": ["applicationId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.Relationships": { + "name": "Relationships", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "uuid_generate_v7()" + }, + "ownerId": { + "name": "ownerId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "subjectId": { + "name": "subjectId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "following": { + "name": "following", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "showing_reblogs": { + "name": "showing_reblogs", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "notifying": { + "name": "notifying", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "blocking": { + "name": "blocking", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "muting": { + "name": "muting", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "muting_notifications": { + "name": "muting_notifications", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "requested": { + "name": "requested", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "domain_blocking": { + "name": "domain_blocking", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "endorsed": { + "name": "endorsed", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "languages": { + "name": "languages", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "note": { + "name": "note", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "Relationships_ownerId_Users_id_fk": { + "name": "Relationships_ownerId_Users_id_fk", + "tableFrom": "Relationships", + "tableTo": "Users", + "columnsFrom": ["ownerId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "Relationships_subjectId_Users_id_fk": { + "name": "Relationships_subjectId_Users_id_fk", + "tableFrom": "Relationships", + "tableTo": "Users", + "columnsFrom": ["subjectId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.RoleToUsers": { + "name": "RoleToUsers", + "schema": "", + "columns": { + "roleId": { + "name": "roleId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "RoleToUsers_roleId_Roles_id_fk": { + "name": "RoleToUsers_roleId_Roles_id_fk", + "tableFrom": "RoleToUsers", + "tableTo": "Roles", + "columnsFrom": ["roleId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "RoleToUsers_userId_Users_id_fk": { + "name": "RoleToUsers_userId_Users_id_fk", + "tableFrom": "RoleToUsers", + "tableTo": "Users", + "columnsFrom": ["userId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.Roles": { + "name": "Roles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "uuid_generate_v7()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "permissions": { + "name": "permissions", + "type": "text[]", + "primaryKey": false, + "notNull": true + }, + "priority": { + "name": "priority", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "visible": { + "name": "visible", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.Tokens": { + "name": "Tokens", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "uuid_generate_v7()" + }, + "token_type": { + "name": "token_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "redirect_uri": { + "name": "redirect_uri", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "userId": { + "name": "userId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "applicationId": { + "name": "applicationId", + "type": "uuid", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "Tokens_userId_Users_id_fk": { + "name": "Tokens_userId_Users_id_fk", + "tableFrom": "Tokens", + "tableTo": "Users", + "columnsFrom": ["userId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "Tokens_applicationId_Applications_id_fk": { + "name": "Tokens_applicationId_Applications_id_fk", + "tableFrom": "Tokens", + "tableTo": "Applications", + "columnsFrom": ["applicationId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.UserToPinnedNotes": { + "name": "UserToPinnedNotes", + "schema": "", + "columns": { + "userId": { + "name": "userId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "noteId": { + "name": "noteId", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "UserToPinnedNotes_userId_noteId_index": { + "name": "UserToPinnedNotes_userId_noteId_index", + "columns": [ + { + "expression": "userId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "noteId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "UserToPinnedNotes_noteId_index": { + "name": "UserToPinnedNotes_noteId_index", + "columns": [ + { + "expression": "noteId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "UserToPinnedNotes_userId_Users_id_fk": { + "name": "UserToPinnedNotes_userId_Users_id_fk", + "tableFrom": "UserToPinnedNotes", + "tableTo": "Users", + "columnsFrom": ["userId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "UserToPinnedNotes_noteId_Notes_id_fk": { + "name": "UserToPinnedNotes_noteId_Notes_id_fk", + "tableFrom": "UserToPinnedNotes", + "tableTo": "Notes", + "columnsFrom": ["noteId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.Users": { + "name": "Users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "uuid_generate_v7()" + }, + "uri": { + "name": "uri", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "note": { + "name": "note", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "is_admin": { + "name": "is_admin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "email_verification_token": { + "name": "email_verification_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password_reset_token": { + "name": "password_reset_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "fields": { + "name": "fields", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "endpoints": { + "name": "endpoints", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "avatar": { + "name": "avatar", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "header": { + "name": "header", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "is_bot": { + "name": "is_bot", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "is_locked": { + "name": "is_locked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "is_discoverable": { + "name": "is_discoverable", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "sanctions": { + "name": "sanctions", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "instanceId": { + "name": "instanceId", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "disable_automoderation": { + "name": "disable_automoderation", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": { + "Users_uri_index": { + "name": "Users_uri_index", + "columns": [ + { + "expression": "uri", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "Users_username_index": { + "name": "Users_username_index", + "columns": [ + { + "expression": "username", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "Users_email_index": { + "name": "Users_email_index", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "Users_instanceId_Instances_id_fk": { + "name": "Users_instanceId_Instances_id_fk", + "tableFrom": "Users", + "tableTo": "Instances", + "columnsFrom": ["instanceId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} diff --git a/drizzle/migrations/meta/_journal.json b/drizzle/migrations/meta/_journal.json index 5ff07bd7..88857b8f 100644 --- a/drizzle/migrations/meta/_journal.json +++ b/drizzle/migrations/meta/_journal.json @@ -218,6 +218,13 @@ "when": 1721223331975, "tag": "0030_curvy_vulture", "breakpoints": true + }, + { + "idx": 31, + "version": "7", + "when": 1722100203904, + "tag": "0031_mature_demogoblin", + "breakpoints": true } ] } diff --git a/drizzle/schema.ts b/drizzle/schema.ts index 43ccfd83..6d1af586 100644 --- a/drizzle/schema.ts +++ b/drizzle/schema.ts @@ -181,13 +181,10 @@ export const Relationships = pgTable("Relationships", { following: boolean("following").notNull(), showingReblogs: boolean("showing_reblogs").notNull(), notifying: boolean("notifying").notNull(), - followedBy: boolean("followed_by").notNull(), blocking: boolean("blocking").notNull(), - blockedBy: boolean("blocked_by").notNull(), muting: boolean("muting").notNull(), mutingNotifications: boolean("muting_notifications").notNull(), requested: boolean("requested").notNull(), - requestedBy: boolean("requested_by").notNull().default(false), domainBlocking: boolean("domain_blocking").notNull(), endorsed: boolean("endorsed").notNull(), languages: text("languages").array(), diff --git a/packages/database-interface/relationship.ts b/packages/database-interface/relationship.ts new file mode 100644 index 00000000..959242d0 --- /dev/null +++ b/packages/database-interface/relationship.ts @@ -0,0 +1,295 @@ +import type { Relationship as APIRelationship } from "@lysand-org/client/types"; +import { + type InferInsertModel, + type InferSelectModel, + type SQL, + and, + desc, + eq, + inArray, +} from "drizzle-orm"; +import { db } from "~/drizzle/db"; +import { Relationships } from "~/drizzle/schema"; +import { BaseInterface } from "./base"; +import type { User } from "./user"; + +export type RelationshipType = InferSelectModel; + +export type RelationshipWithOpposite = RelationshipType & { + followedBy: boolean; + blockedBy: boolean; + requestedBy: boolean; +}; + +export class Relationship extends BaseInterface< + typeof Relationships, + RelationshipWithOpposite +> { + async reload(): Promise { + const reloaded = await Relationship.fromId(this.data.id); + + if (!reloaded) { + throw new Error("Failed to reload relationship"); + } + + this.data = reloaded.data; + } + + public static async fromId( + id: string | null, + ): Promise { + if (!id) { + return null; + } + + return await Relationship.fromSql(eq(Relationships.id, id)); + } + + public static async fromIds(ids: string[]): Promise { + return await Relationship.manyFromSql(inArray(Relationships.id, ids)); + } + + public static async fromOwnerAndSubject( + owner: User, + subject: User, + ): Promise { + const found = await Relationship.fromSql( + and( + eq(Relationships.ownerId, owner.id), + eq(Relationships.subjectId, subject.id), + ), + ); + + if (!found) { + // Create a new relationship if one doesn't exist + return await Relationship.insert({ + ownerId: owner.id, + subjectId: subject.id, + languages: [], + following: false, + showingReblogs: false, + notifying: false, + blocking: false, + muting: false, + mutingNotifications: false, + requested: false, + domainBlocking: false, + endorsed: false, + note: "", + }); + } + + return found; + } + + public static async fromOwnerAndSubjects( + owner: User, + subjectIds: string[], + ): Promise { + const found = await Relationship.manyFromSql( + and( + eq(Relationships.ownerId, owner.id), + inArray(Relationships.subjectId, subjectIds), + ), + ); + + const missingSubjectsIds = subjectIds.filter( + (id) => !found.find((rel) => rel.data.subjectId === id), + ); + + for (const subjectId of missingSubjectsIds) { + await Relationship.insert({ + ownerId: owner.id, + subjectId: subjectId, + languages: [], + following: false, + showingReblogs: false, + notifying: false, + blocking: false, + muting: false, + mutingNotifications: false, + requested: false, + domainBlocking: false, + endorsed: false, + note: "", + }); + } + + return await Relationship.manyFromSql( + and( + eq(Relationships.ownerId, owner.id), + inArray(Relationships.subjectId, subjectIds), + ), + ); + } + + public static async fromSql( + sql: SQL | undefined, + orderBy: SQL | undefined = desc(Relationships.id), + ): Promise { + const found = await db.query.Relationships.findFirst({ + where: sql, + orderBy, + }); + + if (!found) { + return null; + } + + const opposite = await Relationship.getOpposite(found); + + return new Relationship({ + ...found, + followedBy: opposite.following, + blockedBy: opposite.blocking, + requestedBy: opposite.requested, + }); + } + + public static async manyFromSql( + sql: SQL | undefined, + orderBy: SQL | undefined = desc(Relationships.id), + limit?: number, + offset?: number, + extra?: Parameters[0], + ): Promise { + const found = await db.query.Relationships.findMany({ + where: sql, + orderBy, + limit, + offset, + with: extra?.with, + }); + + const opposites = await Promise.all( + found.map((rel) => Relationship.getOpposite(rel)), + ); + + return found.map((s, i) => { + return new Relationship({ + ...s, + followedBy: opposites[i].following, + blockedBy: opposites[i].blocking, + requestedBy: opposites[i].requested, + }); + }); + } + + public static async getOpposite(oppositeTo: { + subjectId: string; + ownerId: string; + }): Promise { + let output = await db.query.Relationships.findFirst({ + where: (rel, { and, eq }) => + and( + eq(rel.ownerId, oppositeTo.subjectId), + eq(rel.subjectId, oppositeTo.ownerId), + ), + }); + + // If the opposite relationship doesn't exist, create it + if (!output) { + output = ( + await db + .insert(Relationships) + .values({ + ownerId: oppositeTo.subjectId, + subjectId: oppositeTo.ownerId, + languages: [], + following: false, + showingReblogs: false, + notifying: false, + blocking: false, + domainBlocking: false, + endorsed: false, + note: "", + muting: false, + mutingNotifications: false, + requested: false, + }) + .returning() + )[0]; + } + + return output; + } + + async update( + newRelationship: Partial, + ): Promise { + await db + .update(Relationships) + .set(newRelationship) + .where(eq(Relationships.id, this.id)); + + const updated = await Relationship.fromId(this.data.id); + + if (!updated) { + throw new Error("Failed to update relationship"); + } + + this.data = updated.data; + return updated.data; + } + + save(): Promise { + return this.update(this.data); + } + + async delete(ids: string[]): Promise; + async delete(): Promise; + async delete(ids?: unknown): Promise { + if (Array.isArray(ids)) { + await db + .delete(Relationships) + .where(inArray(Relationships.id, ids)); + } else { + await db.delete(Relationships).where(eq(Relationships.id, this.id)); + } + } + + public static async insert( + data: InferInsertModel, + ): Promise { + const inserted = ( + await db.insert(Relationships).values(data).returning() + )[0]; + + const relationship = await Relationship.fromId(inserted.id); + + if (!relationship) { + throw new Error("Failed to insert relationship"); + } + + // Create opposite relationship if necessary + await Relationship.getOpposite({ + subjectId: relationship.data.subjectId, + ownerId: relationship.data.ownerId, + }); + + return relationship; + } + + get id() { + return this.data.id; + } + + public toApi(): APIRelationship { + return { + id: this.data.subjectId, + blocked_by: this.data.blockedBy, + blocking: this.data.blocking, + domain_blocking: this.data.domainBlocking, + endorsed: this.data.endorsed, + followed_by: this.data.followedBy, + following: this.data.following, + muting_notifications: this.data.mutingNotifications, + muting: this.data.muting, + note: this.data.note, + notifying: this.data.notifying, + requested_by: this.data.requestedBy, + requested: this.data.requested, + showing_reblogs: this.data.showingReblogs, + }; + } +} diff --git a/packages/database-interface/user.ts b/packages/database-interface/user.ts index 1ee7513f..25ef7158 100644 --- a/packages/database-interface/user.ts +++ b/packages/database-interface/user.ts @@ -34,6 +34,7 @@ import { htmlToText } from "html-to-text"; import { type UserWithRelations, findManyUsers, + followRequestToLysand, } from "~/classes/functions/user"; import { searchManager } from "~/classes/search/search-manager"; import { db } from "~/drizzle/db"; @@ -41,6 +42,7 @@ import { EmojiToUser, NoteToMentions, Notes, + Notifications, type RolePermissions, UserToPinnedNotes, Users, @@ -50,6 +52,7 @@ import { BaseInterface } from "./base"; import { Emoji } from "./emoji"; import { Instance } from "./instance"; import type { Note } from "./note"; +import { Relationship } from "./relationship"; import { Role } from "./role"; /** @@ -205,6 +208,52 @@ export class User extends BaseInterface { ); } + public async followRequest( + otherUser: User, + options?: { + reblogs?: boolean; + notify?: boolean; + languages?: string[]; + }, + ): Promise { + const foundRelationship = await Relationship.fromOwnerAndSubject( + this, + otherUser, + ); + + await foundRelationship.update({ + following: otherUser.isRemote() ? false : !otherUser.data.isLocked, + requested: otherUser.isRemote() ? true : otherUser.data.isLocked, + showingReblogs: options?.reblogs, + notifying: options?.notify, + languages: options?.languages, + }); + + if (otherUser.isRemote()) { + const { ok } = await this.federateToUser( + followRequestToLysand(this, otherUser), + otherUser, + ); + + if (!ok) { + await foundRelationship.update({ + requested: false, + following: false, + }); + + return foundRelationship; + } + } else { + await db.insert(Notifications).values({ + accountId: this.id, + type: otherUser.data.isLocked ? "follow_request" : "follow", + notifiedId: otherUser.id, + }); + } + + return foundRelationship; + } + static async webFinger( manager: FederationRequester, username: string, diff --git a/server/api/api/v1/accounts/:id/block.ts b/server/api/api/v1/accounts/:id/block.ts index 9f461a24..1c8e131a 100644 --- a/server/api/api/v1/accounts/:id/block.ts +++ b/server/api/api/v1/accounts/:id/block.ts @@ -2,12 +2,9 @@ import { applyConfig, auth, handleZodError } from "@/api"; import { errorResponse, jsonResponse } from "@/response"; import type { Hono } from "@hono/hono"; import { zValidator } from "@hono/zod-validator"; -import { eq } from "drizzle-orm"; import { z } from "zod"; -import { relationshipToApi } from "~/classes/functions/relationship"; -import { getRelationshipToOtherUser } from "~/classes/functions/user"; -import { db } from "~/drizzle/db"; -import { Relationships, RolePermissions } from "~/drizzle/schema"; +import { RolePermissions } from "~/drizzle/schema"; +import { Relationship } from "~/packages/database-interface/relationship"; import { User } from "~/packages/database-interface/user"; export const meta = applyConfig({ @@ -55,22 +52,17 @@ export default (app: Hono) => return errorResponse("User not found", 404); } - const foundRelationship = await getRelationshipToOtherUser( + const foundRelationship = await Relationship.fromOwnerAndSubject( user, otherUser, ); - if (!foundRelationship.blocking) { - foundRelationship.blocking = true; + if (!foundRelationship.data.blocking) { + await foundRelationship.update({ + blocking: true, + }); } - await db - .update(Relationships) - .set({ - blocking: true, - }) - .where(eq(Relationships.id, foundRelationship.id)); - - return jsonResponse(relationshipToApi(foundRelationship)); + return jsonResponse(foundRelationship.toApi()); }, ); diff --git a/server/api/api/v1/accounts/:id/follow.ts b/server/api/api/v1/accounts/:id/follow.ts index 6497fafc..cb8adbf5 100644 --- a/server/api/api/v1/accounts/:id/follow.ts +++ b/server/api/api/v1/accounts/:id/follow.ts @@ -4,12 +4,8 @@ import type { Hono } from "@hono/hono"; import { zValidator } from "@hono/zod-validator"; import ISO6391 from "iso-639-1"; import { z } from "zod"; -import { relationshipToApi } from "~/classes/functions/relationship"; -import { - followRequestUser, - getRelationshipToOtherUser, -} from "~/classes/functions/user"; import { RolePermissions } from "~/drizzle/schema"; +import { Relationship } from "~/packages/database-interface/relationship"; import { User } from "~/packages/database-interface/user"; export const meta = applyConfig({ @@ -69,22 +65,19 @@ export default (app: Hono) => return errorResponse("User not found", 404); } - let relationship = await getRelationshipToOtherUser( + let relationship = await Relationship.fromOwnerAndSubject( user, otherUser, ); - if (!relationship.following) { - relationship = await followRequestUser( - user, - otherUser, - relationship.id, + if (!relationship.data.following) { + relationship = await user.followRequest(otherUser, { reblogs, notify, languages, - ); + }); } - return jsonResponse(relationshipToApi(relationship)); + return jsonResponse(relationship.toApi()); }, ); diff --git a/server/api/api/v1/accounts/:id/mute.ts b/server/api/api/v1/accounts/:id/mute.ts index 64baefa4..44e66bb5 100644 --- a/server/api/api/v1/accounts/:id/mute.ts +++ b/server/api/api/v1/accounts/:id/mute.ts @@ -2,12 +2,9 @@ import { applyConfig, auth, handleZodError } from "@/api"; import { errorResponse, jsonResponse } from "@/response"; import type { Hono } from "@hono/hono"; import { zValidator } from "@hono/zod-validator"; -import { eq } from "drizzle-orm"; import { z } from "zod"; -import { relationshipToApi } from "~/classes/functions/relationship"; -import { getRelationshipToOtherUser } from "~/classes/functions/user"; -import { db } from "~/drizzle/db"; -import { Relationships, RolePermissions } from "~/drizzle/schema"; +import { RolePermissions } from "~/drizzle/schema"; +import { Relationship } from "~/packages/database-interface/relationship"; import { User } from "~/packages/database-interface/user"; export const meta = applyConfig({ @@ -67,28 +64,17 @@ export default (app: Hono) => return errorResponse("User not found", 404); } - const foundRelationship = await getRelationshipToOtherUser( + const foundRelationship = await Relationship.fromOwnerAndSubject( user, otherUser, ); - if (!foundRelationship.muting) { - foundRelationship.muting = true; - } - if (notifications ?? true) { - foundRelationship.mutingNotifications = true; - } - - await db - .update(Relationships) - .set({ - muting: true, - mutingNotifications: notifications ?? true, - }) - .where(eq(Relationships.id, foundRelationship.id)); - // TODO: Implement duration + await foundRelationship.update({ + muting: true, + mutingNotifications: notifications ?? true, + }); - return jsonResponse(relationshipToApi(foundRelationship)); + return jsonResponse(foundRelationship.toApi()); }, ); diff --git a/server/api/api/v1/accounts/:id/note.ts b/server/api/api/v1/accounts/:id/note.ts index 2b38fc1e..74fd8931 100644 --- a/server/api/api/v1/accounts/:id/note.ts +++ b/server/api/api/v1/accounts/:id/note.ts @@ -2,12 +2,9 @@ import { applyConfig, auth, handleZodError } from "@/api"; import { errorResponse, jsonResponse } from "@/response"; import type { Hono } from "@hono/hono"; import { zValidator } from "@hono/zod-validator"; -import { eq } from "drizzle-orm"; import { z } from "zod"; -import { relationshipToApi } from "~/classes/functions/relationship"; -import { getRelationshipToOtherUser } from "~/classes/functions/user"; -import { db } from "~/drizzle/db"; -import { Relationships, RolePermissions } from "~/drizzle/schema"; +import { RolePermissions } from "~/drizzle/schema"; +import { Relationship } from "~/packages/database-interface/relationship"; import { User } from "~/packages/database-interface/user"; export const meta = applyConfig({ @@ -60,20 +57,15 @@ export default (app: Hono) => return errorResponse("User not found", 404); } - const foundRelationship = await getRelationshipToOtherUser( + const foundRelationship = await Relationship.fromOwnerAndSubject( user, otherUser, ); - foundRelationship.note = comment ?? ""; + await foundRelationship.update({ + note: comment, + }); - await db - .update(Relationships) - .set({ - note: foundRelationship.note, - }) - .where(eq(Relationships.id, foundRelationship.id)); - - return jsonResponse(relationshipToApi(foundRelationship)); + return jsonResponse(foundRelationship.toApi()); }, ); diff --git a/server/api/api/v1/accounts/:id/pin.ts b/server/api/api/v1/accounts/:id/pin.ts index a0912bbb..73f24fea 100644 --- a/server/api/api/v1/accounts/:id/pin.ts +++ b/server/api/api/v1/accounts/:id/pin.ts @@ -2,12 +2,9 @@ import { applyConfig, auth, handleZodError } from "@/api"; import { errorResponse, jsonResponse } from "@/response"; import type { Hono } from "@hono/hono"; import { zValidator } from "@hono/zod-validator"; -import { eq } from "drizzle-orm"; import { z } from "zod"; -import { relationshipToApi } from "~/classes/functions/relationship"; -import { getRelationshipToOtherUser } from "~/classes/functions/user"; -import { db } from "~/drizzle/db"; -import { Relationships, RolePermissions } from "~/drizzle/schema"; +import { RolePermissions } from "~/drizzle/schema"; +import { Relationship } from "~/packages/database-interface/relationship"; import { User } from "~/packages/database-interface/user"; export const meta = applyConfig({ @@ -55,22 +52,15 @@ export default (app: Hono) => return errorResponse("User not found", 404); } - const foundRelationship = await getRelationshipToOtherUser( + const foundRelationship = await Relationship.fromOwnerAndSubject( user, otherUser, ); - if (!foundRelationship.endorsed) { - foundRelationship.endorsed = true; - } + await foundRelationship.update({ + endorsed: true, + }); - await db - .update(Relationships) - .set({ - endorsed: true, - }) - .where(eq(Relationships.id, foundRelationship.id)); - - return jsonResponse(relationshipToApi(foundRelationship)); + return jsonResponse(foundRelationship.toApi()); }, ); diff --git a/server/api/api/v1/accounts/:id/remove_from_followers.ts b/server/api/api/v1/accounts/:id/remove_from_followers.ts index be51431a..a2554d0f 100644 --- a/server/api/api/v1/accounts/:id/remove_from_followers.ts +++ b/server/api/api/v1/accounts/:id/remove_from_followers.ts @@ -2,12 +2,9 @@ import { applyConfig, auth, handleZodError } from "@/api"; import { errorResponse, jsonResponse } from "@/response"; import type { Hono } from "@hono/hono"; import { zValidator } from "@hono/zod-validator"; -import { and, eq } from "drizzle-orm"; import { z } from "zod"; -import { relationshipToApi } from "~/classes/functions/relationship"; -import { getRelationshipToOtherUser } from "~/classes/functions/user"; -import { db } from "~/drizzle/db"; -import { Relationships, RolePermissions } from "~/drizzle/schema"; +import { RolePermissions } from "~/drizzle/schema"; +import { Relationship } from "~/packages/database-interface/relationship"; import { User } from "~/packages/database-interface/user"; export const meta = applyConfig({ @@ -55,36 +52,22 @@ export default (app: Hono) => return errorResponse("User not found", 404); } - const foundRelationship = await getRelationshipToOtherUser( + const oppositeRelationship = await Relationship.fromOwnerAndSubject( + otherUser, + self, + ); + + if (oppositeRelationship.data.following) { + await oppositeRelationship.update({ + following: false, + }); + } + + const foundRelationship = await Relationship.fromOwnerAndSubject( self, otherUser, ); - if (foundRelationship.followedBy) { - foundRelationship.followedBy = false; - - await db - .update(Relationships) - .set({ - followedBy: false, - }) - .where(eq(Relationships.id, foundRelationship.id)); - - if (otherUser.isLocal()) { - await db - .update(Relationships) - .set({ - following: false, - }) - .where( - and( - eq(Relationships.ownerId, otherUser.id), - eq(Relationships.subjectId, self.id), - ), - ); - } - } - - return jsonResponse(relationshipToApi(foundRelationship)); + return jsonResponse(foundRelationship.toApi()); }, ); diff --git a/server/api/api/v1/accounts/:id/unblock.ts b/server/api/api/v1/accounts/:id/unblock.ts index ea71bc6e..47b21b7f 100644 --- a/server/api/api/v1/accounts/:id/unblock.ts +++ b/server/api/api/v1/accounts/:id/unblock.ts @@ -2,12 +2,9 @@ import { applyConfig, auth, handleZodError } from "@/api"; import { errorResponse, jsonResponse } from "@/response"; import type { Hono } from "@hono/hono"; import { zValidator } from "@hono/zod-validator"; -import { eq } from "drizzle-orm"; import { z } from "zod"; -import { relationshipToApi } from "~/classes/functions/relationship"; -import { getRelationshipToOtherUser } from "~/classes/functions/user"; -import { db } from "~/drizzle/db"; -import { Relationships, RolePermissions } from "~/drizzle/schema"; +import { RolePermissions } from "~/drizzle/schema"; +import { Relationship } from "~/packages/database-interface/relationship"; import { User } from "~/packages/database-interface/user"; export const meta = applyConfig({ @@ -55,22 +52,17 @@ export default (app: Hono) => return errorResponse("User not found", 404); } - const foundRelationship = await getRelationshipToOtherUser( + const foundRelationship = await Relationship.fromOwnerAndSubject( user, otherUser, ); - if (foundRelationship.blocking) { - foundRelationship.blocking = false; - - await db - .update(Relationships) - .set({ - blocking: false, - }) - .where(eq(Relationships.id, foundRelationship.id)); + if (foundRelationship.data.blocking) { + await foundRelationship.update({ + blocking: false, + }); } - return jsonResponse(relationshipToApi(foundRelationship)); + return jsonResponse(foundRelationship.toApi()); }, ); diff --git a/server/api/api/v1/accounts/:id/unfollow.ts b/server/api/api/v1/accounts/:id/unfollow.ts index 2b0bea37..6a21bbae 100644 --- a/server/api/api/v1/accounts/:id/unfollow.ts +++ b/server/api/api/v1/accounts/:id/unfollow.ts @@ -2,12 +2,9 @@ import { applyConfig, auth, handleZodError } from "@/api"; import { errorResponse, jsonResponse } from "@/response"; import type { Hono } from "@hono/hono"; import { zValidator } from "@hono/zod-validator"; -import { eq } from "drizzle-orm"; import { z } from "zod"; -import { relationshipToApi } from "~/classes/functions/relationship"; -import { getRelationshipToOtherUser } from "~/classes/functions/user"; -import { db } from "~/drizzle/db"; -import { Relationships, RolePermissions } from "~/drizzle/schema"; +import { RolePermissions } from "~/drizzle/schema"; +import { Relationship } from "~/packages/database-interface/relationship"; import { User } from "~/packages/database-interface/user"; export const meta = applyConfig({ @@ -55,23 +52,17 @@ export default (app: Hono) => return errorResponse("User not found", 404); } - const foundRelationship = await getRelationshipToOtherUser( + const foundRelationship = await Relationship.fromOwnerAndSubject( self, otherUser, ); - if (foundRelationship.following) { - foundRelationship.following = false; - - await db - .update(Relationships) - .set({ - following: false, - requested: false, - }) - .where(eq(Relationships.id, foundRelationship.id)); + if (foundRelationship.data.following) { + await foundRelationship.update({ + following: false, + }); } - return jsonResponse(relationshipToApi(foundRelationship)); + return jsonResponse(foundRelationship.toApi()); }, ); diff --git a/server/api/api/v1/accounts/:id/unmute.ts b/server/api/api/v1/accounts/:id/unmute.ts index 8e5c0c6b..dc267f40 100644 --- a/server/api/api/v1/accounts/:id/unmute.ts +++ b/server/api/api/v1/accounts/:id/unmute.ts @@ -2,12 +2,9 @@ import { applyConfig, auth, handleZodError } from "@/api"; import { errorResponse, jsonResponse } from "@/response"; import type { Hono } from "@hono/hono"; import { zValidator } from "@hono/zod-validator"; -import { eq } from "drizzle-orm"; import { z } from "zod"; -import { relationshipToApi } from "~/classes/functions/relationship"; -import { getRelationshipToOtherUser } from "~/classes/functions/user"; -import { db } from "~/drizzle/db"; -import { Relationships, RolePermissions } from "~/drizzle/schema"; +import { RolePermissions } from "~/drizzle/schema"; +import { Relationship } from "~/packages/database-interface/relationship"; import { User } from "~/packages/database-interface/user"; export const meta = applyConfig({ @@ -55,24 +52,18 @@ export default (app: Hono) => return errorResponse("User not found", 404); } - const foundRelationship = await getRelationshipToOtherUser( + const foundRelationship = await Relationship.fromOwnerAndSubject( self, user, ); - if (foundRelationship.muting) { - foundRelationship.muting = false; - foundRelationship.mutingNotifications = false; - - await db - .update(Relationships) - .set({ - muting: false, - mutingNotifications: false, - }) - .where(eq(Relationships.id, foundRelationship.id)); + if (foundRelationship.data.muting) { + await foundRelationship.update({ + muting: false, + mutingNotifications: false, + }); } - return jsonResponse(relationshipToApi(foundRelationship)); + return jsonResponse(foundRelationship.toApi()); }, ); diff --git a/server/api/api/v1/accounts/:id/unpin.ts b/server/api/api/v1/accounts/:id/unpin.ts index 04a97781..d4f4346b 100644 --- a/server/api/api/v1/accounts/:id/unpin.ts +++ b/server/api/api/v1/accounts/:id/unpin.ts @@ -2,12 +2,9 @@ import { applyConfig, auth, handleZodError } from "@/api"; import { errorResponse, jsonResponse } from "@/response"; import type { Hono } from "@hono/hono"; import { zValidator } from "@hono/zod-validator"; -import { eq } from "drizzle-orm"; import { z } from "zod"; -import { relationshipToApi } from "~/classes/functions/relationship"; -import { getRelationshipToOtherUser } from "~/classes/functions/user"; -import { db } from "~/drizzle/db"; -import { Relationships, RolePermissions } from "~/drizzle/schema"; +import { RolePermissions } from "~/drizzle/schema"; +import { Relationship } from "~/packages/database-interface/relationship"; import { User } from "~/packages/database-interface/user"; export const meta = applyConfig({ @@ -55,22 +52,17 @@ export default (app: Hono) => return errorResponse("User not found", 404); } - const foundRelationship = await getRelationshipToOtherUser( + const foundRelationship = await Relationship.fromOwnerAndSubject( self, otherUser, ); - if (foundRelationship.endorsed) { - foundRelationship.endorsed = false; - - await db - .update(Relationships) - .set({ - endorsed: false, - }) - .where(eq(Relationships.id, foundRelationship.id)); + if (foundRelationship.data.endorsed) { + await foundRelationship.update({ + endorsed: false, + }); } - return jsonResponse(relationshipToApi(foundRelationship)); + return jsonResponse(foundRelationship.toApi()); }, ); diff --git a/server/api/api/v1/accounts/relationships/index.test.ts b/server/api/api/v1/accounts/relationships/index.test.ts index 7dc96411..b12825b4 100644 --- a/server/api/api/v1/accounts/relationships/index.test.ts +++ b/server/api/api/v1/accounts/relationships/index.test.ts @@ -132,7 +132,7 @@ describe(meta.route, () => { expect.arrayContaining([ expect.objectContaining({ following: false, - followed_by: true, + followed_by: false, blocking: false, muting: false, muting_notifications: false, diff --git a/server/api/api/v1/accounts/relationships/index.ts b/server/api/api/v1/accounts/relationships/index.ts index f4dd264f..1eaa2ba9 100644 --- a/server/api/api/v1/accounts/relationships/index.ts +++ b/server/api/api/v1/accounts/relationships/index.ts @@ -3,13 +3,8 @@ import { errorResponse, jsonResponse } from "@/response"; import type { Hono } from "@hono/hono"; import { zValidator } from "@hono/zod-validator"; import { z } from "zod"; -import { - createNewRelationship, - relationshipToApi, -} from "~/classes/functions/relationship"; -import { db } from "~/drizzle/db"; import { RolePermissions } from "~/drizzle/schema"; -import { User } from "~/packages/database-interface/user"; +import { Relationship } from "~/packages/database-interface/relationship"; export const meta = applyConfig({ allowedMethods: ["GET"], @@ -50,32 +45,17 @@ export default (app: Hono) => return errorResponse("Unauthorized", 401); } - const relationships = await db.query.Relationships.findMany({ - where: (relationship, { inArray, and, eq }) => - and( - inArray(relationship.subjectId, ids), - eq(relationship.ownerId, self.id), - ), - }); - - const missingIds = ids.filter( - (id) => !relationships.some((r) => r.subjectId === id), + const relationships = await Relationship.fromOwnerAndSubjects( + self, + ids, ); - for (const id of missingIds) { - const user = await User.fromId(id); - if (!user) { - continue; - } - const relationship = await createNewRelationship(self, user); - - relationships.push(relationship); - } - relationships.sort( - (a, b) => ids.indexOf(a.subjectId) - ids.indexOf(b.subjectId), + (a, b) => + ids.indexOf(a.data.subjectId) - + ids.indexOf(b.data.subjectId), ); - return jsonResponse(relationships.map((r) => relationshipToApi(r))); + return jsonResponse(relationships.map((r) => r.toApi())); }, ); diff --git a/server/api/api/v1/follow_requests/:account_id/authorize.ts b/server/api/api/v1/follow_requests/:account_id/authorize.ts index db9a947b..24b505b0 100644 --- a/server/api/api/v1/follow_requests/:account_id/authorize.ts +++ b/server/api/api/v1/follow_requests/:account_id/authorize.ts @@ -2,18 +2,10 @@ import { applyConfig, auth, handleZodError } from "@/api"; import { errorResponse, jsonResponse } from "@/response"; import type { Hono } from "@hono/hono"; import { zValidator } from "@hono/zod-validator"; -import { and, eq } from "drizzle-orm"; import { z } from "zod"; -import { - checkForBidirectionalRelationships, - relationshipToApi, -} from "~/classes/functions/relationship"; -import { - getRelationshipToOtherUser, - sendFollowAccept, -} from "~/classes/functions/user"; -import { db } from "~/drizzle/db"; -import { Relationships, RolePermissions } from "~/drizzle/schema"; +import { sendFollowAccept } from "~/classes/functions/user"; +import { RolePermissions } from "~/drizzle/schema"; +import { Relationship } from "~/packages/database-interface/relationship"; import { User } from "~/packages/database-interface/user"; export const meta = applyConfig({ @@ -58,52 +50,27 @@ export default (app: Hono) => return errorResponse("Account not found", 404); } - // Check if there is a relationship on both sides - await checkForBidirectionalRelationships(user, account); + const oppositeRelationship = await Relationship.fromOwnerAndSubject( + account, + user, + ); - // Authorize follow request - await db - .update(Relationships) - .set({ - requested: false, - following: true, - }) - .where( - and( - eq(Relationships.subjectId, user.id), - eq(Relationships.ownerId, account.id), - ), - ); + await oppositeRelationship.update({ + requested: false, + following: true, + }); - // Update followedBy for other user - await db - .update(Relationships) - .set({ - followedBy: true, - requestedBy: false, - }) - .where( - and( - eq(Relationships.subjectId, account.id), - eq(Relationships.ownerId, user.id), - ), - ); - - const foundRelationship = await getRelationshipToOtherUser( + const foundRelationship = await Relationship.fromOwnerAndSubject( user, account, ); - if (!foundRelationship) { - return errorResponse("Relationship not found", 404); - } - // Check if accepting remote follow if (account.isRemote()) { // Federate follow accept await sendFollowAccept(account, user); } - return jsonResponse(relationshipToApi(foundRelationship)); + return jsonResponse(foundRelationship.toApi()); }, ); diff --git a/server/api/api/v1/follow_requests/:account_id/reject.ts b/server/api/api/v1/follow_requests/:account_id/reject.ts index d5c6b660..d34aadf9 100644 --- a/server/api/api/v1/follow_requests/:account_id/reject.ts +++ b/server/api/api/v1/follow_requests/:account_id/reject.ts @@ -2,18 +2,10 @@ import { applyConfig, auth, handleZodError } from "@/api"; import { errorResponse, jsonResponse } from "@/response"; import type { Hono } from "@hono/hono"; import { zValidator } from "@hono/zod-validator"; -import { and, eq } from "drizzle-orm"; import { z } from "zod"; -import { - checkForBidirectionalRelationships, - relationshipToApi, -} from "~/classes/functions/relationship"; -import { - getRelationshipToOtherUser, - sendFollowReject, -} from "~/classes/functions/user"; -import { db } from "~/drizzle/db"; -import { Relationships, RolePermissions } from "~/drizzle/schema"; +import { sendFollowReject } from "~/classes/functions/user"; +import { RolePermissions } from "~/drizzle/schema"; +import { Relationship } from "~/packages/database-interface/relationship"; import { User } from "~/packages/database-interface/user"; export const meta = applyConfig({ @@ -58,52 +50,27 @@ export default (app: Hono) => return errorResponse("Account not found", 404); } - // Check if there is a relationship on both sides - await checkForBidirectionalRelationships(user, account); + const oppositeRelationship = await Relationship.fromOwnerAndSubject( + account, + user, + ); - // Reject follow request - await db - .update(Relationships) - .set({ - requested: false, - following: false, - }) - .where( - and( - eq(Relationships.subjectId, user.id), - eq(Relationships.ownerId, account.id), - ), - ); + await oppositeRelationship.update({ + requested: false, + following: false, + }); - // Update followedBy for other user - await db - .update(Relationships) - .set({ - followedBy: false, - requestedBy: false, - }) - .where( - and( - eq(Relationships.subjectId, account.id), - eq(Relationships.ownerId, user.id), - ), - ); - - const foundRelationship = await getRelationshipToOtherUser( + const foundRelationship = await Relationship.fromOwnerAndSubject( user, account, ); - if (!foundRelationship) { - return errorResponse("Relationship not found", 404); - } - // Check if rejecting remote follow if (account.isRemote()) { // Federate follow reject await sendFollowReject(account, user); } - return jsonResponse(relationshipToApi(foundRelationship)); + return jsonResponse(foundRelationship.toApi()); }, ); diff --git a/server/api/users/:uuid/inbox/index.ts b/server/api/users/:uuid/inbox/index.ts index 656209f7..68e0c0b3 100644 --- a/server/api/users/:uuid/inbox/index.ts +++ b/server/api/users/:uuid/inbox/index.ts @@ -11,18 +11,16 @@ import { } from "@lysand-org/federation"; import type { Entity } from "@lysand-org/federation/types"; import type { SocketAddress } from "bun"; -import { and, eq } from "drizzle-orm"; +import { eq } from "drizzle-orm"; import { matches } from "ip-matching"; import { z } from "zod"; import { type ValidationError, isValidationError } from "zod-validation-error"; -import { - getRelationshipToOtherUser, - sendFollowAccept, -} from "~/classes/functions/user"; +import { sendFollowAccept } from "~/classes/functions/user"; import { db } from "~/drizzle/db"; -import { Notes, Notifications, Relationships } from "~/drizzle/schema"; +import { Notes, Notifications } from "~/drizzle/schema"; import { config } from "~/packages/config-manager"; import { Note } from "~/packages/database-interface/note"; +import { Relationship } from "~/packages/database-interface/relationship"; import { User } from "~/packages/database-interface/user"; export const meta = applyConfig({ @@ -228,35 +226,22 @@ export default (app: Hono) => } const foundRelationship = - await getRelationshipToOtherUser(account, user); + await Relationship.fromOwnerAndSubject( + account, + user, + ); - if (foundRelationship.following) { + if (foundRelationship.data.following) { return response("Already following", 200); } - await db - .update(Relationships) - .set({ - following: !user.data.isLocked, - requested: user.data.isLocked, - showingReblogs: true, - notifying: true, - languages: [], - }) - .where(eq(Relationships.id, foundRelationship.id)); - - // Update other user's requested_by - await db - .update(Relationships) - .set({ - requestedBy: user.data.isLocked, - }) - .where( - and( - eq(Relationships.subjectId, account.id), - eq(Relationships.ownerId, user.id), - ), - ); + await foundRelationship.update({ + following: !user.data.isLocked, + requested: user.data.isLocked, + showingReblogs: true, + notifying: true, + languages: [], + }); await db.insert(Notifications).values({ accountId: account.id, @@ -280,36 +265,22 @@ export default (app: Hono) => } const foundRelationship = - await getRelationshipToOtherUser(user, account); + await Relationship.fromOwnerAndSubject( + user, + account, + ); - if (!foundRelationship.requested) { + if (!foundRelationship.data.requested) { return response( "There is no follow request to accept", 200, ); } - await db - .update(Relationships) - .set({ - following: true, - requested: false, - }) - .where(eq(Relationships.id, foundRelationship.id)); - - // Update other user's data - await db - .update(Relationships) - .set({ - followedBy: true, - requestedBy: false, - }) - .where( - and( - eq(Relationships.subjectId, account.id), - eq(Relationships.ownerId, user.id), - ), - ); + await foundRelationship.update({ + requested: false, + following: true, + }); return response("Follow request accepted", 200); }, @@ -321,36 +292,22 @@ export default (app: Hono) => } const foundRelationship = - await getRelationshipToOtherUser(user, account); + await Relationship.fromOwnerAndSubject( + user, + account, + ); - if (!foundRelationship.requested) { + if (!foundRelationship.data.requested) { return response( "There is no follow request to reject", 200, ); } - await db - .update(Relationships) - .set({ - requested: false, - following: false, - }) - .where(eq(Relationships.id, foundRelationship.id)); - - // Update other user's data - await db - .update(Relationships) - .set({ - followedBy: false, - requestedBy: false, - }) - .where( - and( - eq(Relationships.subjectId, account.id), - eq(Relationships.ownerId, user.id), - ), - ); + await foundRelationship.update({ + requested: false, + following: false, + }); return response("Follow request rejected", 200); },