diff --git a/README.md b/README.md index 9d624485..f275fb45 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ ## What is this? -This is a project to create a federated social network based on the [ActivityPub](https://www.w3.org/TR/activitypub/) standard. It is currently in early alpha phase, with very basic federation and API support. +This is a project to create a federated social network based on the [Lysand](https://lysand.org) protocol. It is currently in alpha phase, with basic federation and API support. This project aims to be a fully featured social network, with a focus on privacy and security. It will implement the Mastodon API for support with clients that already support Mastodon or Pleroma. @@ -15,7 +15,7 @@ This project aims to be a fully featured social network, with a focus on privacy ### Requirements -- The [Bun Runtime](https://bun.sh), version 0.8 or later (use of the latest version is recommended) +- The [Bun Runtime](https://bun.sh), version 1.0.5 or later (usage of the latest version is recommended) - A PostgreSQL database - (Optional but recommended) A Linux-based operating system @@ -60,11 +60,8 @@ Contributions are welcome! Please see the [CONTRIBUTING.md](CONTRIBUTING.md) fil > **Warning**: Federation has not been tested outside of automated tests. It is not recommended to use this software in production. -Lysand is currently able to federate basic `Note` objects with `Create`, `Update` and `Delete` activities supported. (as well as `Accept` and `Reject`, but with no tests) - -Planned federation features are: -- Activities: `Follow`, `Block`, `Undo`, `Announce`, `Like`, `Dislike`, `Flag`, `Ignore` and more -- Objects: `Emoji` and more +The following extensions are currently supported or being worked on: +- `org.lysand:custom_emojis`: Custom emojis ## API @@ -186,6 +183,8 @@ Configuration can be found inside the `config.toml` file. The following values a ### ActivityPub +> **Note**: These options do nothing and date back to when Lysand had ActivityPub support. They will be removed in a future version. + - `use_tombstones`: Whether to use ActivityPub Tombstones instead of deleting objects. Example: `true` - `fetch_all_collection_members`: Whether to fetch all members of collections (followers, following, etc) when receiving them. Example: `false` - `reject_activities`: An array of instance domain names without "https" or glob patterns. Rejects all activities from these instances, simply doesn't save them at all. Example: `[ "mastodon.social" ]` diff --git a/bun.lockb b/bun.lockb index 50befd6c..c97db9f8 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/database/entities/Emoji.ts b/database/entities/Emoji.ts index c2359dfc..04fad36e 100644 --- a/database/entities/Emoji.ts +++ b/database/entities/Emoji.ts @@ -21,6 +21,7 @@ export const parseEmojis = async (text: string): Promise => { shortcode: { in: matches.map(match => match.replace(/:/g, "")), }, + instanceId: null, }, include: { instance: true, diff --git a/database/entities/Relationship.ts b/database/entities/Relationship.ts index 7b3162c3..fb56fa85 100644 --- a/database/entities/Relationship.ts +++ b/database/entities/Relationship.ts @@ -12,7 +12,7 @@ import { client } from "~database/datasource"; * @param other The user who is the subject of the relationship. * @returns The newly created relationship. */ -export const createNew = async ( +export const createNewRelationship = async ( owner: User, other: User ): Promise => { @@ -41,8 +41,10 @@ export const createNew = async ( * Converts the relationship to an API-friendly format. * @returns The API-friendly relationship. */ -// eslint-disable-next-line @typescript-eslint/require-await -export const toAPI = async (rel: Relationship): Promise => { +export const relationshipToAPI = async ( + rel: Relationship + // eslint-disable-next-line @typescript-eslint/require-await +): Promise => { return { blocked_by: rel.blockedBy, blocking: rel.blocking, diff --git a/database/entities/Status.ts b/database/entities/Status.ts index 2cecbe3c..8ead79e8 100644 --- a/database/entities/Status.ts +++ b/database/entities/Status.ts @@ -47,9 +47,9 @@ export const statusAndUserRelations = { instance: true, mentions: true, pinnedBy: true, - replies: { - include: { - _count: true, + _count: { + select: { + replies: true, }, }, }, @@ -57,9 +57,10 @@ export const statusAndUserRelations = { instance: true, mentions: true, pinnedBy: true, - replies: { - include: { - _count: true, + _count: { + select: { + replies: true, + likes: true, }, }, reblog: { @@ -77,9 +78,9 @@ export const statusAndUserRelations = { instance: true, mentions: true, pinnedBy: true, - replies: { - include: { - _count: true, + _count: { + select: { + replies: true, }, }, }, @@ -99,9 +100,9 @@ export const statusAndUserRelations = { instance: true, mentions: true, pinnedBy: true, - replies: { - include: { - _count: true, + _count: { + select: { + replies: true, }, }, }, @@ -113,7 +114,7 @@ export const statusAndUserRelations = { }, }; -type StatusWithRelations = Status & { +export type StatusWithRelations = Status & { author: UserWithRelations; application: Application | null; emojis: Emoji[]; @@ -126,16 +127,17 @@ type StatusWithRelations = Status & { instance: Instance | null; mentions: User[]; pinnedBy: User[]; - replies: Status[] & { - _count: number; + _count: { + replies: number; }; }) | null; instance: Instance | null; mentions: User[]; pinnedBy: User[]; - replies: Status[] & { - _count: number; + _count: { + replies: number; + likes: number; }; reblog: | (Status & { @@ -146,8 +148,8 @@ type StatusWithRelations = Status & { instance: Instance | null; mentions: User[]; pinnedBy: User[]; - replies: Status[] & { - _count: number; + _count: { + replies: number; }; }) | null; @@ -160,8 +162,8 @@ type StatusWithRelations = Status & { instance: Instance | null; mentions: User[]; pinnedBy: User[]; - replies: Status[] & { - _count: number; + _count: { + replies: number; }; }) | null; @@ -196,12 +198,13 @@ export const isViewableByUser = (status: Status, user: User | null) => { export const fetchFromRemote = async (uri: string): Promise => { // Check if already in database - const existingStatus = await client.status.findFirst({ - where: { - uri: uri, - }, - include: statusAndUserRelations, - }); + const existingStatus: StatusWithRelations | null = + await client.status.findFirst({ + where: { + uri: uri, + }, + include: statusAndUserRelations, + }); if (existingStatus) return existingStatus; @@ -228,7 +231,7 @@ export const fetchFromRemote = async (uri: string): Promise => { quotingStatus = await fetchFromRemote(body.quotes[0]); } - return await createNew({ + return await createNewStatus({ account: author, content: content?.content || "", content_type: content?.content_type, @@ -254,18 +257,79 @@ export const fetchFromRemote = async (uri: string): Promise => { * Return all the ancestors of this post, */ // eslint-disable-next-line @typescript-eslint/require-await, @typescript-eslint/no-unused-vars -export const getAncestors = async (fetcher: UserWithRelations | null) => { - // TODO: Implement - return []; +export const getAncestors = async ( + status: StatusWithRelations, + fetcher: UserWithRelations | null +) => { + const ancestors: StatusWithRelations[] = []; + + let currentStatus = status; + + while (currentStatus.inReplyToPostId) { + const parent = await client.status.findFirst({ + where: { + id: currentStatus.inReplyToPostId, + }, + include: statusAndUserRelations, + }); + + if (!parent) break; + + ancestors.push(parent); + + currentStatus = parent; + } + + // Filter for posts that are viewable by the user + + const viewableAncestors = ancestors.filter(ancestor => + isViewableByUser(ancestor, fetcher) + ); + return viewableAncestors; }; /** - * Return all the descendants of this post, + * Return all the descendants of this post (recursive) + * Temporary implementation, will be replaced with a recursive SQL query when Prisma adds support for it */ // eslint-disable-next-line @typescript-eslint/require-await, @typescript-eslint/no-unused-vars -export const getDescendants = async (fetcher: UserWithRelations | null) => { - // TODO: Implement - return []; +export const getDescendants = async ( + status: StatusWithRelations, + fetcher: UserWithRelations | null, + depth = 0 +) => { + const descendants: StatusWithRelations[] = []; + + const currentStatus = status; + + // Fetch all children of children of children recursively calling getDescendants + + const children = await client.status.findMany({ + where: { + inReplyToPostId: currentStatus.id, + }, + include: statusAndUserRelations, + }); + + for (const child of children) { + descendants.push(child); + + if (depth < 20) { + const childDescendants = await getDescendants( + child, + fetcher, + depth + 1 + ); + descendants.push(...childDescendants); + } + } + + // Filter for posts that are viewable by the user + + const viewableDescendants = descendants.filter(descendant => + isViewableByUser(descendant, fetcher) + ); + return viewableDescendants; }; /** @@ -273,7 +337,7 @@ export const getDescendants = async (fetcher: UserWithRelations | null) => { * @param data The data for the new status. * @returns A promise that resolves with the new status. */ -const createNew = async (data: { +export const createNewStatus = async (data: { account: User; application: Application | null; content: string; @@ -408,7 +472,7 @@ export const statusToAPI = async ( reblogId: status.id, }, }), - replies_count: status.replies._count, + replies_count: status._count.replies, sensitive: status.sensitive, spoiler_text: status.spoilerText, tags: [], diff --git a/database/entities/User.ts b/database/entities/User.ts index 13c31bf9..8a57b2c9 100644 --- a/database/entities/User.ts +++ b/database/entities/User.ts @@ -32,9 +32,10 @@ export const userRelations = { relationships: true, relationshipSubjects: true, pinnedNotes: true, - statuses: { + _count: { select: { - _count: true, + statuses: true, + likes: true, }, }, }; @@ -46,8 +47,9 @@ export type UserWithRelations = User & { relationships: Relationship[]; relationshipSubjects: Relationship[]; pinnedNotes: Status[]; - statuses: { - length: number; + _count: { + statuses: number; + likes: number; }; }; @@ -353,7 +355,7 @@ export const userToAPI = async ( followers_count: user.relationshipSubjects.filter(r => r.following) .length, following_count: user.relationships.filter(r => r.following).length, - statuses_count: user.statuses.length, + statuses_count: user._count.statuses, emojis: await Promise.all(user.emojis.map(emoji => emojiToAPI(emoji))), // TODO: Add fields fields: [], diff --git a/package.json b/package.json index e854ea14..7e024233 100644 --- a/package.json +++ b/package.json @@ -66,12 +66,14 @@ "chalk": "^5.3.0", "html-to-text": "^9.0.5", "ip-matching": "^2.1.2", + "iso-639-1": "^3.1.0", "isomorphic-dompurify": "^1.9.0", "jsonld": "^8.3.1", "marked": "^9.1.2", "pg": "^8.11.3", "prisma": "^5.5.2", "reflect-metadata": "^0.1.13", + "sharp": "^0.32.6", "typeorm": "^0.3.17" } } \ No newline at end of file diff --git a/prisma/schema.prisma b/prisma/schema.prisma index a3b2bc0d..dcf058ff 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -151,6 +151,10 @@ model User { header String createdAt DateTime @default(now()) updatedAt DateTime @updatedAt + isBot Boolean @default(false) + isLocked Boolean @default(false) + isDiscoverable Boolean @default(false) + sanctions String[] @default([]) publicKey String privateKey String? // Nullable relationships Relationship[] @relation("OwnerToRelationship") // One to many relation with Relationship diff --git a/server/api/api/v1/accounts/[id]/block.ts b/server/api/api/v1/accounts/[id]/block.ts index 583ce511..291866a7 100644 --- a/server/api/api/v1/accounts/[id]/block.ts +++ b/server/api/api/v1/accounts/[id]/block.ts @@ -1,8 +1,15 @@ import { errorResponse, jsonResponse } from "@response"; import { MatchedRoute } from "bun"; -import { Relationship } from "~database/entities/Relationship"; -import { UserAction, userRelations } from "~database/entities/User"; +import { + createNewRelationship, + relationshipToAPI, +} from "~database/entities/Relationship"; +import { + getFromRequest, + getRelationshipToOtherUser, +} from "~database/entities/User"; import { applyConfig } from "@api"; +import { client } from "~database/datasource"; export const meta = applyConfig({ allowedMethods: ["POST"], @@ -25,29 +32,42 @@ export default async ( ): Promise => { const id = matchedRoute.params.id; - const { user: self } = await UserAction.getFromRequest(req); + const { user: self } = await getFromRequest(req); if (!self) return errorResponse("Unauthorized", 401); - const user = await UserAction.findOne({ - where: { - id, + const user = await client.user.findUnique({ + where: { id }, + include: { + relationships: { + include: { + owner: true, + subject: true, + }, + }, }, - relations: userRelations, }); if (!user) return errorResponse("User not found", 404); // Check if already following - let relationship = await self.getRelationshipToOtherUser(user); + let relationship = await getRelationshipToOtherUser(self, user); if (!relationship) { // Create new relationship - const newRelationship = await Relationship.createNew(self, user); + const newRelationship = await createNewRelationship(self, user); - self.relationships.push(newRelationship); - await self.save(); + await client.user.update({ + where: { id: self.id }, + data: { + relationships: { + connect: { + id: newRelationship.id, + }, + }, + }, + }); relationship = newRelationship; } @@ -56,6 +76,12 @@ export default async ( relationship.blocking = true; } - await relationship.save(); - return jsonResponse(await relationship.toAPI()); + await client.relationship.update({ + where: { id: relationship.id }, + data: { + blocking: true, + }, + }); + + return jsonResponse(await relationshipToAPI(relationship)); }; diff --git a/server/api/api/v1/accounts/[id]/follow.ts b/server/api/api/v1/accounts/[id]/follow.ts index bc5f1e70..62da3cf8 100644 --- a/server/api/api/v1/accounts/[id]/follow.ts +++ b/server/api/api/v1/accounts/[id]/follow.ts @@ -1,9 +1,16 @@ import { parseRequest } from "@request"; import { errorResponse, jsonResponse } from "@response"; import { MatchedRoute } from "bun"; -import { Relationship } from "~database/entities/Relationship"; -import { UserAction, userRelations } from "~database/entities/User"; +import { + createNewRelationship, + relationshipToAPI, +} from "~database/entities/Relationship"; +import { + getFromRequest, + getRelationshipToOtherUser, +} from "~database/entities/User"; import { applyConfig } from "@api"; +import { client } from "~database/datasource"; export const meta = applyConfig({ allowedMethods: ["POST"], @@ -26,7 +33,7 @@ export default async ( ): Promise => { const id = matchedRoute.params.id; - const { user: self } = await UserAction.getFromRequest(req); + const { user: self } = await getFromRequest(req); if (!self) return errorResponse("Unauthorized", 401); @@ -36,25 +43,38 @@ export default async ( languages?: string[]; }>(req); - const user = await UserAction.findOne({ - where: { - id, + const user = await client.user.findUnique({ + where: { id }, + include: { + relationships: { + include: { + owner: true, + subject: true, + }, + }, }, - relations: userRelations, }); if (!user) return errorResponse("User not found", 404); // Check if already following - let relationship = await self.getRelationshipToOtherUser(user); + let relationship = await getRelationshipToOtherUser(self, user); if (!relationship) { // Create new relationship - const newRelationship = await Relationship.createNew(self, user); + const newRelationship = await createNewRelationship(self, user); - self.relationships.push(newRelationship); - await self.save(); + await client.user.update({ + where: { id: self.id }, + data: { + relationships: { + connect: { + id: newRelationship.id, + }, + }, + }, + }); relationship = newRelationship; } @@ -63,7 +83,7 @@ export default async ( relationship.following = true; } if (reblogs) { - relationship.showing_reblogs = true; + relationship.showingReblogs = true; } if (notify) { relationship.notifying = true; @@ -72,6 +92,15 @@ export default async ( relationship.languages = languages; } - await relationship.save(); - return jsonResponse(await relationship.toAPI()); + await client.relationship.update({ + where: { id: relationship.id }, + data: { + following: true, + showingReblogs: reblogs ?? false, + notifying: notify ?? false, + languages: languages ?? [], + }, + }); + + return jsonResponse(relationshipToAPI(relationship)); }; diff --git a/server/api/api/v1/accounts/[id]/index.ts b/server/api/api/v1/accounts/[id]/index.ts index f9d52e20..f6eadb3c 100644 --- a/server/api/api/v1/accounts/[id]/index.ts +++ b/server/api/api/v1/accounts/[id]/index.ts @@ -1,7 +1,13 @@ import { errorResponse, jsonResponse } from "@response"; import { MatchedRoute } from "bun"; -import { UserAction, userRelations } from "~database/entities/User"; +import { + UserWithRelations, + getFromRequest, + userRelations, + userToAPI, +} from "~database/entities/User"; import { applyConfig } from "@api"; +import { client } from "~database/datasource"; export const meta = applyConfig({ allowedMethods: ["GET"], @@ -24,15 +30,13 @@ export default async ( ): Promise => { const id = matchedRoute.params.id; - const { user } = await UserAction.getFromRequest(req); + const { user } = await getFromRequest(req); - let foundUser: UserAction | null; + let foundUser: UserWithRelations | null; try { - foundUser = await UserAction.findOne({ - where: { - id, - }, - relations: userRelations, + foundUser = await client.user.findUnique({ + where: { id }, + include: userRelations, }); } catch (e) { return errorResponse("Invalid ID", 404); @@ -40,5 +44,5 @@ export default async ( if (!foundUser) return errorResponse("User not found", 404); - return jsonResponse(await foundUser.toAPI(user?.id === foundUser.id)); + return jsonResponse(await userToAPI(foundUser, user?.id === foundUser.id)); }; diff --git a/server/api/api/v1/accounts/[id]/mute.ts b/server/api/api/v1/accounts/[id]/mute.ts index eabacd52..b1b85f06 100644 --- a/server/api/api/v1/accounts/[id]/mute.ts +++ b/server/api/api/v1/accounts/[id]/mute.ts @@ -1,9 +1,16 @@ import { parseRequest } from "@request"; import { errorResponse, jsonResponse } from "@response"; import { MatchedRoute } from "bun"; -import { Relationship } from "~database/entities/Relationship"; -import { UserAction, userRelations } from "~database/entities/User"; +import { + createNewRelationship, + relationshipToAPI, +} from "~database/entities/Relationship"; +import { + getFromRequest, + getRelationshipToOtherUser, +} from "~database/entities/User"; import { applyConfig } from "@api"; +import { client } from "~database/datasource"; export const meta = applyConfig({ allowedMethods: ["POST"], @@ -26,7 +33,7 @@ export default async ( ): Promise => { const id = matchedRoute.params.id; - const { user: self } = await UserAction.getFromRequest(req); + const { user: self } = await getFromRequest(req); if (!self) return errorResponse("Unauthorized", 401); @@ -36,25 +43,38 @@ export default async ( duration: number; }>(req); - const user = await UserAction.findOne({ - where: { - id, + const user = await client.user.findUnique({ + where: { id }, + include: { + relationships: { + include: { + owner: true, + subject: true, + }, + }, }, - relations: userRelations, }); if (!user) return errorResponse("User not found", 404); // Check if already following - let relationship = await self.getRelationshipToOtherUser(user); + let relationship = await getRelationshipToOtherUser(self, user); if (!relationship) { // Create new relationship - const newRelationship = await Relationship.createNew(self, user); + const newRelationship = await createNewRelationship(self, user); - self.relationships.push(newRelationship); - await self.save(); + await client.user.update({ + where: { id: self.id }, + data: { + relationships: { + connect: { + id: newRelationship.id, + }, + }, + }, + }); relationship = newRelationship; } @@ -63,11 +83,18 @@ export default async ( relationship.muting = true; } if (notifications ?? true) { - relationship.muting_notifications = true; + relationship.mutingNotifications = true; } + await client.relationship.update({ + where: { id: relationship.id }, + data: { + muting: true, + mutingNotifications: notifications ?? true, + }, + }); + // TODO: Implement duration - await relationship.save(); - return jsonResponse(await relationship.toAPI()); + return jsonResponse(await relationshipToAPI(relationship)); }; diff --git a/server/api/api/v1/accounts/[id]/note.ts b/server/api/api/v1/accounts/[id]/note.ts index 54f8378d..5aa005af 100644 --- a/server/api/api/v1/accounts/[id]/note.ts +++ b/server/api/api/v1/accounts/[id]/note.ts @@ -1,9 +1,16 @@ import { parseRequest } from "@request"; import { errorResponse, jsonResponse } from "@response"; import { MatchedRoute } from "bun"; -import { Relationship } from "~database/entities/Relationship"; -import { UserAction, userRelations } from "~database/entities/User"; +import { + createNewRelationship, + relationshipToAPI, +} from "~database/entities/Relationship"; +import { + getFromRequest, + getRelationshipToOtherUser, +} from "~database/entities/User"; import { applyConfig } from "@api"; +import { client } from "~database/datasource"; export const meta = applyConfig({ allowedMethods: ["POST"], @@ -26,7 +33,7 @@ export default async ( ): Promise => { const id = matchedRoute.params.id; - const { user: self } = await UserAction.getFromRequest(req); + const { user: self } = await getFromRequest(req); if (!self) return errorResponse("Unauthorized", 401); @@ -34,31 +41,50 @@ export default async ( comment: string; }>(req); - const user = await UserAction.findOne({ - where: { - id, + const user = await client.user.findUnique({ + where: { id }, + include: { + relationships: { + include: { + owner: true, + subject: true, + }, + }, }, - relations: userRelations, }); if (!user) return errorResponse("User not found", 404); // Check if already following - let relationship = await self.getRelationshipToOtherUser(user); + let relationship = await getRelationshipToOtherUser(self, user); if (!relationship) { // Create new relationship - const newRelationship = await Relationship.createNew(self, user); + const newRelationship = await createNewRelationship(self, user); - self.relationships.push(newRelationship); - await self.save(); + await client.user.update({ + where: { id: self.id }, + data: { + relationships: { + connect: { + id: newRelationship.id, + }, + }, + }, + }); relationship = newRelationship; } relationship.note = comment ?? ""; - await relationship.save(); - return jsonResponse(await relationship.toAPI()); + await client.relationship.update({ + where: { id: relationship.id }, + data: { + note: relationship.note, + }, + }); + + return jsonResponse(await relationshipToAPI(relationship)); }; diff --git a/server/api/api/v1/accounts/[id]/pin.ts b/server/api/api/v1/accounts/[id]/pin.ts index 82ee42cb..84b76611 100644 --- a/server/api/api/v1/accounts/[id]/pin.ts +++ b/server/api/api/v1/accounts/[id]/pin.ts @@ -1,8 +1,15 @@ import { errorResponse, jsonResponse } from "@response"; import { MatchedRoute } from "bun"; -import { Relationship } from "~database/entities/Relationship"; -import { UserAction, userRelations } from "~database/entities/User"; +import { + createNewRelationship, + relationshipToAPI, +} from "~database/entities/Relationship"; +import { + getFromRequest, + getRelationshipToOtherUser, +} from "~database/entities/User"; import { applyConfig } from "@api"; +import { client } from "~database/datasource"; export const meta = applyConfig({ allowedMethods: ["POST"], @@ -25,29 +32,42 @@ export default async ( ): Promise => { const id = matchedRoute.params.id; - const { user: self } = await UserAction.getFromRequest(req); + const { user: self } = await getFromRequest(req); if (!self) return errorResponse("Unauthorized", 401); - const user = await UserAction.findOne({ - where: { - id, + const user = await client.user.findUnique({ + where: { id }, + include: { + relationships: { + include: { + owner: true, + subject: true, + }, + }, }, - relations: userRelations, }); if (!user) return errorResponse("User not found", 404); // Check if already following - let relationship = await self.getRelationshipToOtherUser(user); + let relationship = await getRelationshipToOtherUser(self, user); if (!relationship) { // Create new relationship - const newRelationship = await Relationship.createNew(self, user); + const newRelationship = await createNewRelationship(self, user); - self.relationships.push(newRelationship); - await self.save(); + await client.user.update({ + where: { id: self.id }, + data: { + relationships: { + connect: { + id: newRelationship.id, + }, + }, + }, + }); relationship = newRelationship; } @@ -56,6 +76,12 @@ export default async ( relationship.endorsed = true; } - await relationship.save(); - return jsonResponse(await relationship.toAPI()); + await client.relationship.update({ + where: { id: relationship.id }, + data: { + endorsed: true, + }, + }); + + return jsonResponse(await relationshipToAPI(relationship)); }; 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 9bf94a9c..ce321933 100644 --- a/server/api/api/v1/accounts/[id]/remove_from_followers.ts +++ b/server/api/api/v1/accounts/[id]/remove_from_followers.ts @@ -1,8 +1,15 @@ import { errorResponse, jsonResponse } from "@response"; import { MatchedRoute } from "bun"; -import { Relationship } from "~database/entities/Relationship"; -import { UserAction, userRelations } from "~database/entities/User"; +import { + createNewRelationship, + relationshipToAPI, +} from "~database/entities/Relationship"; +import { + getFromRequest, + getRelationshipToOtherUser, +} from "~database/entities/User"; import { applyConfig } from "@api"; +import { client } from "~database/datasource"; export const meta = applyConfig({ allowedMethods: ["POST"], @@ -25,37 +32,71 @@ export default async ( ): Promise => { const id = matchedRoute.params.id; - const { user: self } = await UserAction.getFromRequest(req); + const { user: self } = await getFromRequest(req); if (!self) return errorResponse("Unauthorized", 401); - const user = await UserAction.findOne({ - where: { - id, + const user = await client.user.findUnique({ + where: { id }, + include: { + relationships: { + include: { + owner: true, + subject: true, + }, + }, }, - relations: userRelations, }); if (!user) return errorResponse("User not found", 404); // Check if already following - let relationship = await self.getRelationshipToOtherUser(user); + let relationship = await getRelationshipToOtherUser(self, user); if (!relationship) { // Create new relationship - const newRelationship = await Relationship.createNew(self, user); + const newRelationship = await createNewRelationship(self, user); - self.relationships.push(newRelationship); - await self.save(); + await client.user.update({ + where: { id: self.id }, + data: { + relationships: { + connect: { + id: newRelationship.id, + }, + }, + }, + }); relationship = newRelationship; } - if (relationship.followed_by) { - relationship.followed_by = false; + if (relationship.followedBy) { + relationship.followedBy = false; } - await relationship.save(); - return jsonResponse(await relationship.toAPI()); + await client.relationship.update({ + where: { id: relationship.id }, + data: { + followedBy: false, + }, + }); + + if (user.instanceId === null) { + // Also remove from followers list + await client.relationship.update({ + // @ts-expect-error Idk why there's this error + where: { + ownerId: user.id, + subjectId: self.id, + following: true, + }, + data: { + following: false, + }, + }); + } + + return jsonResponse(await relationshipToAPI(relationship)); }; diff --git a/server/api/api/v1/accounts/[id]/statuses.ts b/server/api/api/v1/accounts/[id]/statuses.ts index 8b663b5a..220ef2f7 100644 --- a/server/api/api/v1/accounts/[id]/statuses.ts +++ b/server/api/api/v1/accounts/[id]/statuses.ts @@ -1,10 +1,10 @@ /* eslint-disable @typescript-eslint/no-unsafe-member-access */ import { errorResponse, jsonResponse } from "@response"; import { MatchedRoute } from "bun"; -import { Status, statusAndUserRelations } from "~database/entities/Status"; -import { UserAction, userRelations } from "~database/entities/User"; +import { statusAndUserRelations, statusToAPI } from "~database/entities/Status"; +import { userRelations } from "~database/entities/User"; import { applyConfig } from "@api"; -import { FindManyOptions } from "typeorm"; +import { client } from "~database/datasource"; export const meta = applyConfig({ allowedMethods: ["GET"], @@ -47,79 +47,29 @@ export default async ( tagged?: string; } = matchedRoute.query; - const user = await UserAction.findOne({ - where: { - id, - }, - relations: userRelations, + const user = await client.user.findUnique({ + where: { id }, + include: userRelations, }); if (!user) return errorResponse("User not found", 404); - // Get list of boosts for this status - let query: FindManyOptions = { + const objects = await client.status.findMany({ where: { - account: { - id: user.id, - }, + authorId: id, isReblog: exclude_reblogs ? true : undefined, + id: { + lt: max_id, + gt: min_id, + gte: since_id, + }, }, - relations: statusAndUserRelations, + include: statusAndUserRelations, take: limit ?? 20, - order: { - id: "DESC", + orderBy: { + id: "desc", }, - }; - - if (max_id) { - const maxStatus = await Status.findOneBy({ id: max_id }); - if (maxStatus) { - query = { - ...query, - where: { - ...query.where, - created_at: { - ...(query.where as any)?.created_at, - $lt: maxStatus.created_at, - }, - }, - }; - } - } - - if (since_id) { - const sinceStatus = await Status.findOneBy({ id: since_id }); - if (sinceStatus) { - query = { - ...query, - where: { - ...query.where, - created_at: { - ...(query.where as any)?.created_at, - $gt: sinceStatus.created_at, - }, - }, - }; - } - } - - if (min_id) { - const minStatus = await Status.findOneBy({ id: min_id }); - if (minStatus) { - query = { - ...query, - where: { - ...query.where, - created_at: { - ...(query.where as any)?.created_at, - $gte: minStatus.created_at, - }, - }, - }; - } - } - - const objects = await Status.find(query); + }); // Constuct HTTP Link header (next and prev) const linkHeader = []; @@ -129,14 +79,13 @@ export default async ( `<${urlWithoutQuery}?max_id=${objects[0].id}&limit=${limit}>; rel="next"` ); linkHeader.push( - `<${urlWithoutQuery}?since_id=${ - objects[objects.length - 1].id - }&limit=${limit}>; rel="prev"` + `<${urlWithoutQuery}?since_id=${objects.at(-1) + ?.id}&limit=${limit}>; rel="prev"` ); } return jsonResponse( - await Promise.all(objects.map(async status => await status.toAPI())), + await Promise.all(objects.map(status => statusToAPI(status))), 200, { Link: linkHeader.join(", "), diff --git a/server/api/api/v1/accounts/[id]/unblock.ts b/server/api/api/v1/accounts/[id]/unblock.ts index d28f16a4..0aeb007d 100644 --- a/server/api/api/v1/accounts/[id]/unblock.ts +++ b/server/api/api/v1/accounts/[id]/unblock.ts @@ -1,8 +1,15 @@ import { errorResponse, jsonResponse } from "@response"; import { MatchedRoute } from "bun"; -import { Relationship } from "~database/entities/Relationship"; -import { UserAction, userRelations } from "~database/entities/User"; +import { + createNewRelationship, + relationshipToAPI, +} from "~database/entities/Relationship"; +import { + getFromRequest, + getRelationshipToOtherUser, +} from "~database/entities/User"; import { applyConfig } from "@api"; +import { client } from "~database/datasource"; export const meta = applyConfig({ allowedMethods: ["POST"], @@ -25,29 +32,42 @@ export default async ( ): Promise => { const id = matchedRoute.params.id; - const { user: self } = await UserAction.getFromRequest(req); + const { user: self } = await getFromRequest(req); if (!self) return errorResponse("Unauthorized", 401); - const user = await UserAction.findOne({ - where: { - id, + const user = await client.user.findUnique({ + where: { id }, + include: { + relationships: { + include: { + owner: true, + subject: true, + }, + }, }, - relations: userRelations, }); if (!user) return errorResponse("User not found", 404); // Check if already following - let relationship = await self.getRelationshipToOtherUser(user); + let relationship = await getRelationshipToOtherUser(self, user); if (!relationship) { // Create new relationship - const newRelationship = await Relationship.createNew(self, user); + const newRelationship = await createNewRelationship(self, user); - self.relationships.push(newRelationship); - await self.save(); + await client.user.update({ + where: { id: self.id }, + data: { + relationships: { + connect: { + id: newRelationship.id, + }, + }, + }, + }); relationship = newRelationship; } @@ -56,6 +76,12 @@ export default async ( relationship.blocking = false; } - await relationship.save(); - return jsonResponse(await relationship.toAPI()); + await client.relationship.update({ + where: { id: relationship.id }, + data: { + blocking: false, + }, + }); + + return jsonResponse(await relationshipToAPI(relationship)); }; diff --git a/server/api/api/v1/accounts/[id]/unfollow.ts b/server/api/api/v1/accounts/[id]/unfollow.ts index 0d189824..1ca9ea20 100644 --- a/server/api/api/v1/accounts/[id]/unfollow.ts +++ b/server/api/api/v1/accounts/[id]/unfollow.ts @@ -1,8 +1,15 @@ import { errorResponse, jsonResponse } from "@response"; import { MatchedRoute } from "bun"; -import { Relationship } from "~database/entities/Relationship"; -import { UserAction, userRelations } from "~database/entities/User"; +import { + createNewRelationship, + relationshipToAPI, +} from "~database/entities/Relationship"; +import { + getFromRequest, + getRelationshipToOtherUser, +} from "~database/entities/User"; import { applyConfig } from "@api"; +import { client } from "~database/datasource"; export const meta = applyConfig({ allowedMethods: ["POST"], @@ -25,29 +32,42 @@ export default async ( ): Promise => { const id = matchedRoute.params.id; - const { user: self } = await UserAction.getFromRequest(req); + const { user: self } = await getFromRequest(req); if (!self) return errorResponse("Unauthorized", 401); - const user = await UserAction.findOne({ - where: { - id, + const user = await client.user.findUnique({ + where: { id }, + include: { + relationships: { + include: { + owner: true, + subject: true, + }, + }, }, - relations: userRelations, }); if (!user) return errorResponse("User not found", 404); // Check if already following - let relationship = await self.getRelationshipToOtherUser(user); + let relationship = await getRelationshipToOtherUser(self, user); if (!relationship) { // Create new relationship - const newRelationship = await Relationship.createNew(self, user); + const newRelationship = await createNewRelationship(self, user); - self.relationships.push(newRelationship); - await self.save(); + await client.user.update({ + where: { id: self.id }, + data: { + relationships: { + connect: { + id: newRelationship.id, + }, + }, + }, + }); relationship = newRelationship; } @@ -56,6 +76,12 @@ export default async ( relationship.following = false; } - await relationship.save(); - return jsonResponse(await relationship.toAPI()); + await client.relationship.update({ + where: { id: relationship.id }, + data: { + following: false, + }, + }); + + return jsonResponse(await relationshipToAPI(relationship)); }; diff --git a/server/api/api/v1/accounts/[id]/unmute.ts b/server/api/api/v1/accounts/[id]/unmute.ts index 7a79bae0..ed46b8ad 100644 --- a/server/api/api/v1/accounts/[id]/unmute.ts +++ b/server/api/api/v1/accounts/[id]/unmute.ts @@ -1,8 +1,15 @@ import { errorResponse, jsonResponse } from "@response"; import { MatchedRoute } from "bun"; -import { Relationship } from "~database/entities/Relationship"; -import { UserAction, userRelations } from "~database/entities/User"; +import { + createNewRelationship, + relationshipToAPI, +} from "~database/entities/Relationship"; +import { + getFromRequest, + getRelationshipToOtherUser, +} from "~database/entities/User"; import { applyConfig } from "@api"; +import { client } from "~database/datasource"; export const meta = applyConfig({ allowedMethods: ["POST"], @@ -25,29 +32,42 @@ export default async ( ): Promise => { const id = matchedRoute.params.id; - const { user: self } = await UserAction.getFromRequest(req); + const { user: self } = await getFromRequest(req); if (!self) return errorResponse("Unauthorized", 401); - const user = await UserAction.findOne({ - where: { - id, + const user = await client.user.findUnique({ + where: { id }, + include: { + relationships: { + include: { + owner: true, + subject: true, + }, + }, }, - relations: userRelations, }); if (!user) return errorResponse("User not found", 404); // Check if already following - let relationship = await self.getRelationshipToOtherUser(user); + let relationship = await getRelationshipToOtherUser(self, user); if (!relationship) { // Create new relationship - const newRelationship = await Relationship.createNew(self, user); + const newRelationship = await createNewRelationship(self, user); - self.relationships.push(newRelationship); - await self.save(); + await client.user.update({ + where: { id: self.id }, + data: { + relationships: { + connect: { + id: newRelationship.id, + }, + }, + }, + }); relationship = newRelationship; } @@ -58,6 +78,12 @@ export default async ( // TODO: Implement duration - await relationship.save(); - return jsonResponse(await relationship.toAPI()); + await client.relationship.update({ + where: { id: relationship.id }, + data: { + muting: false, + }, + }); + + return jsonResponse(await relationshipToAPI(relationship)); }; diff --git a/server/api/api/v1/accounts/[id]/unpin.ts b/server/api/api/v1/accounts/[id]/unpin.ts index 1462f145..c941ba0f 100644 --- a/server/api/api/v1/accounts/[id]/unpin.ts +++ b/server/api/api/v1/accounts/[id]/unpin.ts @@ -1,8 +1,15 @@ import { errorResponse, jsonResponse } from "@response"; import { MatchedRoute } from "bun"; -import { Relationship } from "~database/entities/Relationship"; -import { UserAction, userRelations } from "~database/entities/User"; +import { + createNewRelationship, + relationshipToAPI, +} from "~database/entities/Relationship"; +import { + getFromRequest, + getRelationshipToOtherUser, +} from "~database/entities/User"; import { applyConfig } from "@api"; +import { client } from "~database/datasource"; export const meta = applyConfig({ allowedMethods: ["POST"], @@ -25,29 +32,42 @@ export default async ( ): Promise => { const id = matchedRoute.params.id; - const { user: self } = await UserAction.getFromRequest(req); + const { user: self } = await getFromRequest(req); if (!self) return errorResponse("Unauthorized", 401); - const user = await UserAction.findOne({ - where: { - id, + const user = await client.user.findUnique({ + where: { id }, + include: { + relationships: { + include: { + owner: true, + subject: true, + }, + }, }, - relations: userRelations, }); if (!user) return errorResponse("User not found", 404); // Check if already following - let relationship = await self.getRelationshipToOtherUser(user); + let relationship = await getRelationshipToOtherUser(self, user); if (!relationship) { // Create new relationship - const newRelationship = await Relationship.createNew(self, user); + const newRelationship = await createNewRelationship(self, user); - self.relationships.push(newRelationship); - await self.save(); + await client.user.update({ + where: { id: self.id }, + data: { + relationships: { + connect: { + id: newRelationship.id, + }, + }, + }, + }); relationship = newRelationship; } @@ -56,6 +76,12 @@ export default async ( relationship.endorsed = false; } - await relationship.save(); - return jsonResponse(await relationship.toAPI()); + await client.relationship.update({ + where: { id: relationship.id }, + data: { + endorsed: false, + }, + }); + + return jsonResponse(await relationshipToAPI(relationship)); }; diff --git a/server/api/api/v1/accounts/familiar_followers/index.ts b/server/api/api/v1/accounts/familiar_followers/index.ts index c012d85f..92c2d50a 100644 --- a/server/api/api/v1/accounts/familiar_followers/index.ts +++ b/server/api/api/v1/accounts/familiar_followers/index.ts @@ -1,14 +1,18 @@ import { parseRequest } from "@request"; import { errorResponse, jsonResponse } from "@response"; -import { UserAction } from "~database/entities/User"; -import { APIAccount } from "~types/entities/account"; +import { + getFromRequest, + userRelations, + userToAPI, +} from "~database/entities/User"; import { applyConfig } from "@api"; +import { client } from "~database/datasource"; export const meta = applyConfig({ allowedMethods: ["GET"], route: "/api/v1/accounts/familiar_followers", ratelimits: { - max: 30, + max: 5, duration: 60, }, auth: { @@ -20,7 +24,7 @@ export const meta = applyConfig({ * Find familiar followers (followers of a user that you also follow) */ export default async (req: Request): Promise => { - const { user: self } = await UserAction.getFromRequest(req); + const { user: self } = await getFromRequest(req); if (!self) return errorResponse("Unauthorized", 401); @@ -33,47 +37,34 @@ export default async (req: Request): Promise => { return errorResponse("Number of ids must be between 1 and 10", 422); } - const response = ( - await Promise.all( - ids.map(async id => { - // Find followers of user that you also follow - - // Get user - const user = await UserAction.findOne({ - where: { id }, - relations: { - relationships: { - subject: { - relationships: true, - }, - }, + const followersOfIds = await client.user.findMany({ + where: { + relationships: { + some: { + subjectId: { + in: ids, }, - }); + following: true, + }, + }, + }, + }); - if (!user) return null; + // Find users that you follow in followersOfIds + const output = await client.user.findMany({ + where: { + relationships: { + some: { + ownerId: self.id, + subjectId: { + in: followersOfIds.map(u => u.id), + }, + following: true, + }, + }, + }, + include: userRelations, + }); - // Map to user response - const response = user.relationships - .filter(r => r.following) - .map(r => r.subject) - .filter(u => - u.relationships.some( - r => r.following && r.subject.id === self.id - ) - ); - - return { - id: id, - accounts: await Promise.all( - response.map(async u => await u.toAPI()) - ), - }; - }) - ) - ).filter(r => r !== null) as { - id: string; - accounts: APIAccount[]; - }[]; - - return jsonResponse(response); + return jsonResponse(output.map(o => userToAPI(o))); }; diff --git a/server/api/api/v1/accounts/index.ts b/server/api/api/v1/accounts/index.ts index 69d997cf..678384db 100644 --- a/server/api/api/v1/accounts/index.ts +++ b/server/api/api/v1/accounts/index.ts @@ -2,8 +2,10 @@ import { getConfig } from "@config"; import { parseRequest } from "@request"; import { jsonResponse } from "@response"; import { tempmailDomains } from "@tempmail"; -import { UserAction } from "~database/entities/User"; import { applyConfig } from "@api"; +import { client } from "~database/datasource"; +import { createNewLocalUser } from "~database/entities/User"; +import ISO6391 from "iso-639-1"; export const meta = applyConfig({ allowedMethods: ["POST"], @@ -115,7 +117,7 @@ export default async (req: Request): Promise => { }); // Check if username is taken - if (await UserAction.findOne({ where: { username: body.username } })) + if (await client.user.findFirst({ where: { username: body.username } })) errors.details.username.push({ error: "ERR_TAKEN", description: `is already taken`, @@ -150,6 +152,18 @@ export default async (req: Request): Promise => { description: `must be accepted`, }); + if (!body.locale) + errors.details.locale.push({ + error: "ERR_BLANK", + description: `can't be blank`, + }); + + if (!ISO6391.validate(body.locale ?? "")) + errors.details.locale.push({ + error: "ERR_INVALID", + description: `must be a valid ISO 639-1 code`, + }); + // If any errors are present, return them if (Object.values(errors.details).some(value => value.length > 0)) { // Error is something like "Validation failed: Password can't be blank, Username must contain only letters, numbers and underscores, Agreement must be accepted" @@ -168,14 +182,13 @@ export default async (req: Request): Promise => { }); } - // TODO: Check if locale is valid - - await UserAction.createNewLocal({ + await createNewLocalUser({ username: body.username ?? "", password: body.password ?? "", email: body.email ?? "", }); - // TODO: Return access token - return new Response(); + return new Response("", { + status: 200, + }); }; diff --git a/server/api/api/v1/accounts/relationships/index.ts b/server/api/api/v1/accounts/relationships/index.ts index b9dde030..b32628a5 100644 --- a/server/api/api/v1/accounts/relationships/index.ts +++ b/server/api/api/v1/accounts/relationships/index.ts @@ -1,8 +1,12 @@ import { parseRequest } from "@request"; import { errorResponse, jsonResponse } from "@response"; -import { Relationship } from "~database/entities/Relationship"; -import { UserAction } from "~database/entities/User"; +import { + createNewRelationship, + relationshipToAPI, +} from "~database/entities/Relationship"; +import { getFromRequest } from "~database/entities/User"; import { applyConfig } from "@api"; +import { client } from "~database/datasource"; export const meta = applyConfig({ allowedMethods: ["GET"], @@ -20,7 +24,7 @@ export const meta = applyConfig({ * Find relationships */ export default async (req: Request): Promise => { - const { user: self } = await UserAction.getFromRequest(req); + const { user: self } = await getFromRequest(req); if (!self) return errorResponse("Unauthorized", 401); @@ -33,34 +37,35 @@ export default async (req: Request): Promise => { return errorResponse("Number of ids must be between 1 and 10", 422); } - // Check if already following - // TODO: Limit ID amount - const relationships = ( - await Promise.all( - ids.map(async id => { - const user = await UserAction.findOneBy({ id }); - if (!user) return null; - let relationship = await self.getRelationshipToOtherUser(user); + const relationships = await client.relationship.findMany({ + where: { + ownerId: self.id, + subjectId: { + in: ids, + }, + }, + }); - if (!relationship) { - // Create new relationship + // Find IDs that dont have a relationship + const missingIds = ids.filter( + id => !relationships.some(r => r.subjectId === id) + ); - const newRelationship = await Relationship.createNew( - self, - user - ); + // Create the missing relationships + for (const id of missingIds) { + const relationship = await createNewRelationship(self, { id } as any); - self.relationships.push(newRelationship); - await self.save(); + relationships.push(relationship); + } - relationship = newRelationship; - } - return relationship; - }) - ) - ).filter(relationship => relationship !== null) as Relationship[]; + // Order in the same order as ids + relationships.sort( + (a, b) => ids.indexOf(a.subjectId) - ids.indexOf(b.subjectId) + ); return jsonResponse( - await Promise.all(relationships.map(async r => await r.toAPI())) + await Promise.all( + relationships.map(async r => await relationshipToAPI(r)) + ) ); }; diff --git a/server/api/api/v1/accounts/update_credentials/index.ts b/server/api/api/v1/accounts/update_credentials/index.ts index 8ac804c6..4b85924b 100644 --- a/server/api/api/v1/accounts/update_credentials/index.ts +++ b/server/api/api/v1/accounts/update_credentials/index.ts @@ -1,12 +1,14 @@ import { getConfig } from "@config"; import { parseRequest } from "@request"; import { errorResponse, jsonResponse } from "@response"; -import { UserAction } from "~database/entities/User"; +import { getFromRequest, userToAPI } from "~database/entities/User"; import { applyConfig } from "@api"; import { sanitize } from "isomorphic-dompurify"; import { sanitizeHtml } from "@sanitization"; import { uploadFile } from "~classes/media"; -import { EmojiAction } from "~database/entities/Emoji"; +import ISO6391 from "iso-639-1"; +import { parseEmojis } from "~database/entities/Emoji"; +import { client } from "~database/datasource"; export const meta = applyConfig({ allowedMethods: ["PATCH"], @@ -24,7 +26,7 @@ export const meta = applyConfig({ * Patches a user */ export default async (req: Request): Promise => { - const { user } = await UserAction.getFromRequest(req); + const { user } = await getFromRequest(req); if (!user) return errorResponse("Unauthorized", 401); @@ -85,7 +87,7 @@ export default async (req: Request): Promise => { // Remove emojis user.emojis = []; - user.display_name = sanitizedDisplayName; + user.displayName = sanitizedDisplayName; } if (note) { @@ -112,7 +114,7 @@ export default async (req: Request): Promise => { user.note = sanitizedNote; } - if (source_privacy) { + if (source_privacy && user.source) { // Check if within allowed privacy values if ( !["public", "unlisted", "private", "direct"].includes( @@ -125,21 +127,30 @@ export default async (req: Request): Promise => { ); } - user.source.privacy = source_privacy; + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + (user.source as any).privacy = source_privacy; } - if (source_sensitive) { + if (source_sensitive && user.source) { // Check if within allowed sensitive values if (source_sensitive !== "true" && source_sensitive !== "false") { return errorResponse("Sensitive must be a boolean", 422); } - user.source.sensitive = source_sensitive === "true"; + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + (user.source as any).sensitive = source_sensitive === "true"; } - if (source_language) { - // TODO: Check if proper ISO code - user.source.language = source_language; + if (source_language && user.source) { + if (!ISO6391.validate(source_language)) { + return errorResponse( + "Language must be a valid ISO 639-1 code", + 422 + ); + } + + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + (user.source as any).language = source_language; } if (avatar) { @@ -176,8 +187,7 @@ export default async (req: Request): Promise => { return errorResponse("Locked must be a boolean", 422); } - // TODO: Add a user value for Locked - // user.locked = locked === "true"; + user.isLocked = locked === "true"; } if (bot) { @@ -186,8 +196,7 @@ export default async (req: Request): Promise => { return errorResponse("Bot must be a boolean", 422); } - // TODO: Add a user value for bot - // user.bot = bot === "true"; + user.isBot = bot === "true"; } if (discoverable) { @@ -196,14 +205,13 @@ export default async (req: Request): Promise => { return errorResponse("Discoverable must be a boolean", 422); } - // TODO: Add a user value for discoverable - // user.discoverable = discoverable === "true"; + user.isDiscoverable = discoverable === "true"; } // Parse emojis - const displaynameEmojis = await EmojiAction.parseEmojis(sanitizedDisplayName); - const noteEmojis = await EmojiAction.parseEmojis(sanitizedNote); + const displaynameEmojis = await parseEmojis(sanitizedDisplayName); + const noteEmojis = await parseEmojis(sanitizedNote); user.emojis = [...displaynameEmojis, ...noteEmojis]; @@ -212,7 +220,31 @@ export default async (req: Request): Promise => { (emoji, index, self) => self.findIndex(e => e.id === emoji.id) === index ); - await user.save(); + await client.user.update({ + where: { id: user.id }, + data: { + displayName: user.displayName, + note: user.note, + avatar: user.avatar, + header: user.header, + isLocked: user.isLocked, + isBot: user.isBot, + isDiscoverable: user.isDiscoverable, + emojis: { + disconnect: user.emojis.map(e => ({ + id: e.id, + })), + connect: user.emojis.map(e => ({ + id: e.id, + })), + }, + source: user.source + ? { + update: user.source, + } + : undefined, + }, + }); - return jsonResponse(await user.toAPI()); + return jsonResponse(await userToAPI(user)); }; diff --git a/server/api/api/v1/accounts/verify_credentials/index.ts b/server/api/api/v1/accounts/verify_credentials/index.ts index 102d49fd..2fd46dc2 100644 --- a/server/api/api/v1/accounts/verify_credentials/index.ts +++ b/server/api/api/v1/accounts/verify_credentials/index.ts @@ -1,5 +1,5 @@ import { errorResponse, jsonResponse } from "@response"; -import { UserAction } from "~database/entities/User"; +import { getFromRequest, userToAPI } from "~database/entities/User"; import { applyConfig } from "@api"; export const meta = applyConfig({ @@ -17,12 +17,12 @@ export const meta = applyConfig({ export default async (req: Request): Promise => { // TODO: Add checks for disabled or not email verified accounts - const { user } = await UserAction.getFromRequest(req); + const { user } = await getFromRequest(req); if (!user) return errorResponse("Unauthorized", 401); return jsonResponse({ - ...(await user.toAPI()), + ...(await userToAPI(user)), source: user.source, // TODO: Add role support role: { diff --git a/server/api/api/v1/apps/index.ts b/server/api/api/v1/apps/index.ts index c1b06dec..175599f8 100644 --- a/server/api/api/v1/apps/index.ts +++ b/server/api/api/v1/apps/index.ts @@ -2,7 +2,7 @@ import { applyConfig } from "@api"; import { parseRequest } from "@request"; import { errorResponse, jsonResponse } from "@response"; import { randomBytes } from "crypto"; -import { ApplicationAction } from "~database/entities/Application"; +import { client } from "~database/datasource"; export const meta = applyConfig({ allowedMethods: ["POST"], @@ -27,10 +27,6 @@ export default async (req: Request): Promise => { website: string; }>(req); - const application = new ApplicationAction(); - - application.name = client_name || ""; - // Check if redirect URI is a valid URI, and also an absolute URI if (redirect_uris) { try { @@ -42,20 +38,20 @@ export default async (req: Request): Promise => { 422 ); } - - application.redirect_uris = redirect_uris; } catch { return errorResponse("Redirect URI must be a valid URI", 422); } } - - application.scopes = scopes || "read"; - application.website = website || null; - - application.client_id = randomBytes(32).toString("base64url"); - application.secret = randomBytes(64).toString("base64url"); - - await application.save(); + const application = await client.application.create({ + data: { + name: client_name || "", + redirect_uris: redirect_uris || "", + scopes: scopes || "read", + website: website || null, + client_id: randomBytes(32).toString("base64url"), + secret: randomBytes(64).toString("base64url"), + }, + }); return jsonResponse({ id: application.id, diff --git a/server/api/api/v1/apps/verify_credentials/index.ts b/server/api/api/v1/apps/verify_credentials/index.ts index bd606cfb..a281be66 100644 --- a/server/api/api/v1/apps/verify_credentials/index.ts +++ b/server/api/api/v1/apps/verify_credentials/index.ts @@ -1,7 +1,7 @@ import { applyConfig } from "@api"; import { errorResponse, jsonResponse } from "@response"; -import { ApplicationAction } from "~database/entities/Application"; -import { UserAction } from "~database/entities/User"; +import { getFromToken } from "~database/entities/Application"; +import { getFromRequest } from "~database/entities/User"; export const meta = applyConfig({ allowedMethods: ["GET"], @@ -19,8 +19,8 @@ export const meta = applyConfig({ * Returns OAuth2 credentials */ export default async (req: Request): Promise => { - const { user, token } = await UserAction.getFromRequest(req); - const application = await ApplicationAction.getFromToken(token); + const { user, token } = await getFromRequest(req); + const application = await getFromToken(token); if (!user) return errorResponse("Unauthorized", 401); if (!application) return errorResponse("Unauthorized", 401); diff --git a/server/api/api/v1/custom_emojis/index.ts b/server/api/api/v1/custom_emojis/index.ts index b19e24dc..1590bbc0 100644 --- a/server/api/api/v1/custom_emojis/index.ts +++ b/server/api/api/v1/custom_emojis/index.ts @@ -1,7 +1,7 @@ import { applyConfig } from "@api"; import { jsonResponse } from "@response"; -import { IsNull } from "typeorm"; -import { EmojiAction } from "~database/entities/Emoji"; +import { client } from "~database/datasource"; +import { emojiToAPI } from "~database/entities/Emoji"; export const meta = applyConfig({ allowedMethods: ["GET"], @@ -20,11 +20,13 @@ export const meta = applyConfig({ */ // eslint-disable-next-line @typescript-eslint/require-await export default async (): Promise => { - const emojis = await EmojiAction.findBy({ - instance: IsNull(), + const emojis = await client.emoji.findMany({ + where: { + instanceId: null, + }, }); return jsonResponse( - await Promise.all(emojis.map(async emoji => await emoji.toAPI())) + await Promise.all(emojis.map(emoji => emojiToAPI(emoji))) ); }; diff --git a/server/api/api/v1/instance/index.ts b/server/api/api/v1/instance/index.ts index 75237801..1904aa31 100644 --- a/server/api/api/v1/instance/index.ts +++ b/server/api/api/v1/instance/index.ts @@ -1,8 +1,7 @@ import { applyConfig } from "@api"; import { getConfig } from "@config"; import { jsonResponse } from "@response"; -import { Status } from "~database/entities/Status"; -import { UserAction } from "~database/entities/User"; +import { client } from "~database/datasource"; export const meta = applyConfig({ allowedMethods: ["GET"], @@ -23,8 +22,16 @@ export const meta = applyConfig({ export default async (): Promise => { const config = getConfig(); - const statusCount = await Status.count(); - const userCount = await UserAction.count(); + const statusCount = await client.status.count({ + where: { + instanceId: null, + }, + }); + const userCount = await client.user.count({ + where: { + instanceId: null, + }, + }); // TODO: fill in more values return jsonResponse({ diff --git a/server/api/api/v1/statuses/[id]/context.ts b/server/api/api/v1/statuses/[id]/context.ts index f62d30b0..d780de2a 100644 --- a/server/api/api/v1/statuses/[id]/context.ts +++ b/server/api/api/v1/statuses/[id]/context.ts @@ -1,14 +1,20 @@ import { applyConfig } from "@api"; import { errorResponse, jsonResponse } from "@response"; import { MatchedRoute } from "bun"; -import { Status, statusAndUserRelations } from "~database/entities/Status"; -import { UserAction } from "~database/entities/User"; +import { client } from "~database/datasource"; +import { + getAncestors, + getDescendants, + statusAndUserRelations, + statusToAPI, +} from "~database/entities/Status"; +import { getFromRequest } from "~database/entities/User"; import { APIRouteMeta } from "~types/api"; export const meta: APIRouteMeta = applyConfig({ allowedMethods: ["GET"], ratelimits: { - max: 100, + max: 8, duration: 60, }, route: "/api/v1/statuses/:id/context", @@ -28,30 +34,25 @@ export default async ( // User token + read:statuses for up to 4,096 ancestors, 4,096 descendants, unlimited depth, and private statuses. const id = matchedRoute.params.id; - const { user } = await UserAction.getFromRequest(req); + const { user } = await getFromRequest(req); - let foundStatus: Status | null; - try { - foundStatus = await Status.findOne({ - where: { - id, - }, - relations: statusAndUserRelations, - }); - } catch (e) { - return errorResponse("Invalid ID", 404); - } + const foundStatus = await client.status.findUnique({ + where: { id }, + include: statusAndUserRelations, + }); if (!foundStatus) return errorResponse("Record not found", 404); // Get all ancestors - const ancestors = await foundStatus.getAncestors(user); - const descendants = await foundStatus.getDescendants(user); + const ancestors = await getAncestors(foundStatus, user); + const descendants = await getDescendants(foundStatus, user); return jsonResponse({ - ancestors: await Promise.all(ancestors.map(status => status.toAPI())), + ancestors: await Promise.all( + ancestors.map(status => statusToAPI(status)) + ), descendants: await Promise.all( - descendants.map(status => status.toAPI()) + descendants.map(status => statusToAPI(status)) ), }); }; diff --git a/server/api/api/v1/statuses/[id]/favourite.ts b/server/api/api/v1/statuses/[id]/favourite.ts index 348a57c1..e8e60162 100644 --- a/server/api/api/v1/statuses/[id]/favourite.ts +++ b/server/api/api/v1/statuses/[id]/favourite.ts @@ -2,10 +2,15 @@ import { applyConfig } from "@api"; import { errorResponse, jsonResponse } from "@response"; import { MatchedRoute } from "bun"; -import { Like } from "~database/entities/Like"; -import { Status, statusAndUserRelations } from "~database/entities/Status"; -import { UserAction, userRelations } from "~database/entities/User"; +import { client } from "~database/datasource"; +import { + isViewableByUser, + statusAndUserRelations, + statusToAPI, +} from "~database/entities/Status"; +import { getFromRequest } from "~database/entities/User"; import { APIRouteMeta } from "~types/api"; +import { APIStatus } from "~types/entities/status"; export const meta: APIRouteMeta = applyConfig({ allowedMethods: ["POST"], @@ -28,51 +33,38 @@ export default async ( ): Promise => { const id = matchedRoute.params.id; - const { user } = await UserAction.getFromRequest(req); + const { user } = await getFromRequest(req); if (!user) return errorResponse("Unauthorized", 401); - let foundStatus: Status | null; - try { - foundStatus = await Status.findOne({ - where: { - id, - }, - relations: statusAndUserRelations, - }); - } catch (e) { - return errorResponse("Invalid ID", 404); - } - - if (!foundStatus) return errorResponse("Record not found", 404); + const status = await client.status.findUnique({ + where: { id }, + include: statusAndUserRelations, + }); // Check if user is authorized to view this status (if it's private) - if (!foundStatus.isViewableByUser(user)) { + if (!status || !isViewableByUser(status, user)) return errorResponse("Record not found", 404); - } - // Check if user has already favourited this status - const existingLike = await Like.findOne({ + const existingLike = await client.like.findFirst({ where: { - liked: { - id: foundStatus.id, - }, - liker: { - id: user.id, - }, + likedId: status.id, + likerId: user.id, }, - relations: [ - ...userRelations.map(r => `liker.${r}`), - ...statusAndUserRelations.map(r => `liked.${r}`), - ], }); if (!existingLike) { - const like = new Like(); - like.liker = user; - like.liked = foundStatus; - await like.save(); + await client.like.create({ + data: { + likedId: status.id, + likerId: user.id, + }, + }); } - return jsonResponse(await foundStatus.toAPI()); + return jsonResponse({ + ...(await statusToAPI(status, user)), + favourited: true, + favourites_count: status._count.likes + 1, + } as APIStatus); }; diff --git a/server/api/api/v1/statuses/[id]/favourited_by.ts b/server/api/api/v1/statuses/[id]/favourited_by.ts index c64b5de9..ba24be4f 100644 --- a/server/api/api/v1/statuses/[id]/favourited_by.ts +++ b/server/api/api/v1/statuses/[id]/favourited_by.ts @@ -3,10 +3,16 @@ import { applyConfig } from "@api"; import { parseRequest } from "@request"; import { errorResponse, jsonResponse } from "@response"; import { MatchedRoute } from "bun"; -import { FindManyOptions } from "typeorm"; -import { Like } from "~database/entities/Like"; -import { Status, statusAndUserRelations } from "~database/entities/Status"; -import { UserAction, userRelations } from "~database/entities/User"; +import { client } from "~database/datasource"; +import { + isViewableByUser, + statusAndUserRelations, +} from "~database/entities/Status"; +import { + getFromRequest, + userRelations, + userToAPI, +} from "~database/entities/User"; import { APIRouteMeta } from "~types/api"; export const meta: APIRouteMeta = applyConfig({ @@ -30,33 +36,25 @@ export default async ( ): Promise => { const id = matchedRoute.params.id; - const { user } = await UserAction.getFromRequest(req); + const { user } = await getFromRequest(req); - let foundStatus: Status | null; - try { - foundStatus = await Status.findOne({ - where: { - id, - }, - relations: statusAndUserRelations, - }); - } catch (e) { - return errorResponse("Invalid ID", 404); - } - - if (!foundStatus) return errorResponse("Record not found", 404); + const status = await client.status.findUnique({ + where: { id }, + include: statusAndUserRelations, + }); // Check if user is authorized to view this status (if it's private) - if (!foundStatus.isViewableByUser(user)) { + if (!status || !isViewableByUser(status, user)) return errorResponse("Record not found", 404); - } const { max_id = null, + min_id = null, since_id = null, limit = 40, } = await parseRequest<{ max_id?: string; + min_id?: string; since_id?: string; limit?: number; }>(req); @@ -65,53 +63,32 @@ export default async ( if (limit > 80) return errorResponse("Invalid limit (maximum is 80)", 400); if (limit < 1) return errorResponse("Invalid limit", 400); - // Get list of boosts for this status - let query: FindManyOptions = { + const objects = await client.user.findMany({ where: { - liked: { - id, + likes: { + some: { + likedId: status.id, + }, + }, + id: { + lt: max_id ?? undefined, + gte: since_id ?? undefined, + gt: min_id ?? undefined, }, }, - relations: userRelations.map(r => `liker.${r}`), - take: limit, - order: { - id: "DESC", + include: { + ...userRelations, + likes: { + where: { + likedId: status.id, + }, + }, }, - }; - - if (max_id) { - const maxLike = await Like.findOneBy({ id: max_id }); - if (maxLike) { - query = { - ...query, - where: { - ...query.where, - created_at: { - ...(query.where as any)?.created_at, - $lt: maxLike.created_at, - }, - }, - }; - } - } - - if (since_id) { - const sinceLike = await Like.findOneBy({ id: since_id }); - if (sinceLike) { - query = { - ...query, - where: { - ...query.where, - created_at: { - ...(query.where as any)?.created_at, - $gt: sinceLike.created_at, - }, - }, - }; - } - } - - const objects = await Like.find(query); + take: limit, + orderBy: { + id: "desc", + }, + }); // Constuct HTTP Link header (next and prev) const linkHeader = []; @@ -128,7 +105,7 @@ export default async ( } return jsonResponse( - await Promise.all(objects.map(async like => await like.liker.toAPI())), + await Promise.all(objects.map(async user => userToAPI(user))), 200, { Link: linkHeader.join(", "), diff --git a/server/api/api/v1/statuses/[id]/index.ts b/server/api/api/v1/statuses/[id]/index.ts index ae0058e4..f91d4a1f 100644 --- a/server/api/api/v1/statuses/[id]/index.ts +++ b/server/api/api/v1/statuses/[id]/index.ts @@ -1,8 +1,13 @@ import { applyConfig } from "@api"; import { errorResponse, jsonResponse } from "@response"; import { MatchedRoute } from "bun"; -import { Status, statusAndUserRelations } from "~database/entities/Status"; -import { UserAction } from "~database/entities/User"; +import { client } from "~database/datasource"; +import { + isViewableByUser, + statusAndUserRelations, + statusToAPI, +} from "~database/entities/Status"; +import { getFromRequest } from "~database/entities/User"; import { APIRouteMeta } from "~types/api"; export const meta: APIRouteMeta = applyConfig({ @@ -27,31 +32,21 @@ export default async ( ): Promise => { const id = matchedRoute.params.id; - const { user } = await UserAction.getFromRequest(req); + const { user } = await getFromRequest(req); - let foundStatus: Status | null; - try { - foundStatus = await Status.findOne({ - where: { - id, - }, - relations: statusAndUserRelations, - }); - } catch (e) { - return errorResponse("Invalid ID", 404); - } - - if (!foundStatus) return errorResponse("Record not found", 404); + const status = await client.status.findUnique({ + where: { id }, + include: statusAndUserRelations, + }); // Check if user is authorized to view this status (if it's private) - if (!foundStatus.isViewableByUser(user)) { + if (!status || isViewableByUser(status, user)) return errorResponse("Record not found", 404); - } if (req.method === "GET") { - return jsonResponse(await foundStatus.toAPI()); + return jsonResponse(await statusToAPI(status)); } else if (req.method === "DELETE") { - if (foundStatus.account.id !== user?.id) { + if (status.authorId !== user?.id) { return errorResponse("Unauthorized", 401); } @@ -60,11 +55,13 @@ export default async ( // Get associated Status object // Delete status and all associated objects - await foundStatus.remove(); + await client.status.delete({ + where: { id }, + }); return jsonResponse( { - ...(await foundStatus.toAPI()), + ...(await statusToAPI(status)), // TODO: Add // text: Add source text // poll: Add source poll diff --git a/server/api/api/v1/statuses/[id]/reblogged_by.ts b/server/api/api/v1/statuses/[id]/reblogged_by.ts index cf30e4ff..0754a276 100644 --- a/server/api/api/v1/statuses/[id]/reblogged_by.ts +++ b/server/api/api/v1/statuses/[id]/reblogged_by.ts @@ -3,9 +3,16 @@ import { applyConfig } from "@api"; import { parseRequest } from "@request"; import { errorResponse, jsonResponse } from "@response"; import { MatchedRoute } from "bun"; -import { FindManyOptions } from "typeorm"; -import { Status, statusAndUserRelations } from "~database/entities/Status"; -import { UserAction } from "~database/entities/User"; +import { client } from "~database/datasource"; +import { + isViewableByUser, + statusAndUserRelations, +} from "~database/entities/Status"; +import { + getFromRequest, + userRelations, + userToAPI, +} from "~database/entities/User"; import { APIRouteMeta } from "~types/api"; export const meta: APIRouteMeta = applyConfig({ @@ -29,33 +36,25 @@ export default async ( ): Promise => { const id = matchedRoute.params.id; - const { user } = await UserAction.getFromRequest(req); + const { user } = await getFromRequest(req); - let foundStatus: Status | null; - try { - foundStatus = await Status.findOne({ - where: { - id, - }, - relations: statusAndUserRelations, - }); - } catch (e) { - return errorResponse("Invalid ID", 404); - } - - if (!foundStatus) return errorResponse("Record not found", 404); + const status = await client.status.findUnique({ + where: { id }, + include: statusAndUserRelations, + }); // Check if user is authorized to view this status (if it's private) - if (!foundStatus.isViewableByUser(user)) { + if (!status || !isViewableByUser(status, user)) return errorResponse("Record not found", 404); - } const { max_id = null, + min_id = null, since_id = null, limit = 40, } = await parseRequest<{ max_id?: string; + min_id?: string; since_id?: string; limit?: number; }>(req); @@ -64,53 +63,33 @@ export default async ( if (limit > 80) return errorResponse("Invalid limit (maximum is 80)", 400); if (limit < 1) return errorResponse("Invalid limit", 400); - // Get list of boosts for this status - let query: FindManyOptions = { + const objects = await client.user.findMany({ where: { - reblog: { - id, + statuses: { + some: { + reblogId: status.id, + }, + }, + id: { + lt: max_id ?? undefined, + gte: since_id ?? undefined, + gt: min_id ?? undefined, }, }, - relations: statusAndUserRelations, - take: limit, - order: { - id: "DESC", + include: { + ...userRelations, + statuses: { + where: { + reblogId: status.id, + }, + include: statusAndUserRelations, + }, }, - }; - - if (max_id) { - const maxPost = await Status.findOneBy({ id: max_id }); - if (maxPost) { - query = { - ...query, - where: { - ...query.where, - created_at: { - ...(query.where as any)?.created_at, - $lt: maxPost.created_at, - }, - }, - }; - } - } - - if (since_id) { - const sincePost = await Status.findOneBy({ id: since_id }); - if (sincePost) { - query = { - ...query, - where: { - ...query.where, - created_at: { - ...(query.where as any)?.created_at, - $gt: sincePost.created_at, - }, - }, - }; - } - } - - const objects = await Status.find(query); + take: limit, + orderBy: { + id: "desc", + }, + }); // Constuct HTTP Link header (next and prev) const linkHeader = []; @@ -127,7 +106,7 @@ export default async ( } return jsonResponse( - await Promise.all(objects.map(async object => await object.toAPI())), + await Promise.all(objects.map(async user => userToAPI(user))), 200, { Link: linkHeader.join(", "), diff --git a/server/api/api/v1/statuses/[id]/unfavourite.ts b/server/api/api/v1/statuses/[id]/unfavourite.ts index 7d349d94..01038312 100644 --- a/server/api/api/v1/statuses/[id]/unfavourite.ts +++ b/server/api/api/v1/statuses/[id]/unfavourite.ts @@ -2,10 +2,15 @@ import { applyConfig } from "@api"; import { errorResponse, jsonResponse } from "@response"; import { MatchedRoute } from "bun"; -import { Like } from "~database/entities/Like"; -import { Status, statusAndUserRelations } from "~database/entities/Status"; -import { UserAction } from "~database/entities/User"; +import { client } from "~database/datasource"; +import { + isViewableByUser, + statusAndUserRelations, + statusToAPI, +} from "~database/entities/Status"; +import { getFromRequest } from "~database/entities/User"; import { APIRouteMeta } from "~types/api"; +import { APIStatus } from "~types/entities/status"; export const meta: APIRouteMeta = applyConfig({ allowedMethods: ["POST"], @@ -28,37 +33,29 @@ export default async ( ): Promise => { const id = matchedRoute.params.id; - const { user } = await UserAction.getFromRequest(req); + const { user } = await getFromRequest(req); if (!user) return errorResponse("Unauthorized", 401); - let foundStatus: Status | null; - try { - foundStatus = await Status.findOne({ - where: { - id, - }, - relations: statusAndUserRelations, - }); - } catch (e) { - return errorResponse("Invalid ID", 404); - } - - if (!foundStatus) return errorResponse("Record not found", 404); + const status = await client.status.findUnique({ + where: { id }, + include: statusAndUserRelations, + }); // Check if user is authorized to view this status (if it's private) - if (!foundStatus.isViewableByUser(user)) { + if (!status || !isViewableByUser(status, user)) return errorResponse("Record not found", 404); - } - await Like.delete({ - liked: { - id: foundStatus.id, - }, - liker: { - id: user.id, + await client.like.deleteMany({ + where: { + likedId: status.id, + likerId: user.id, }, }); - return jsonResponse(await foundStatus.toAPI()); + return jsonResponse({ + ...(await statusToAPI(status, user)), + favourited: false, + favourites_count: status._count.likes - 1, + } as APIStatus); }; diff --git a/server/api/api/v1/statuses/index.ts b/server/api/api/v1/statuses/index.ts index 0c9865d6..5c9f81ff 100644 --- a/server/api/api/v1/statuses/index.ts +++ b/server/api/api/v1/statuses/index.ts @@ -8,9 +8,15 @@ import { errorResponse, jsonResponse } from "@response"; import { sanitizeHtml } from "@sanitization"; import { MatchedRoute } from "bun"; import { parse } from "marked"; -import { ApplicationAction } from "~database/entities/Application"; -import { Status, statusRelations } from "~database/entities/Status"; -import { AuthData, UserAction } from "~database/entities/User"; +import { client } from "~database/datasource"; +import { getFromToken } from "~database/entities/Application"; +import { + StatusWithRelations, + createNewStatus, + statusAndUserRelations, + statusToAPI, +} from "~database/entities/Status"; +import { AuthData, UserWithRelations } from "~database/entities/User"; import { APIRouteMeta } from "~types/api"; export const meta: APIRouteMeta = applyConfig({ @@ -34,7 +40,7 @@ export default async ( authData: AuthData ): Promise => { const { user, token } = authData; - const application = await ApplicationAction.getFromToken(token); + const application = await getFromToken(token); if (!user) return errorResponse("Unauthorized", 401); @@ -126,18 +132,16 @@ export default async ( } // Get reply account and status if exists - let replyStatus: Status | null = null; - let replyUser: UserAction | null = null; + let replyStatus: StatusWithRelations | null = null; + let replyUser: UserWithRelations | null = null; if (in_reply_to_id) { - replyStatus = await Status.findOne({ - where: { - id: in_reply_to_id, - }, - relations: statusRelations, + replyStatus = await client.status.findUnique({ + where: { id: in_reply_to_id }, + include: statusAndUserRelations, }); - replyUser = replyStatus?.account || null; + replyUser = replyStatus?.author || null; } // Check if status body doesnt match filters @@ -145,8 +149,7 @@ export default async ( return errorResponse("Status contains blocked words", 422); } - // Create status - const newStatus = await Status.createNew({ + const newStatus = await createNewStatus({ account: user, application, content: sanitizedStatus, @@ -171,5 +174,5 @@ export default async ( // TODO: add database jobs to deliver the post - return jsonResponse(await newStatus.toAPI()); + return jsonResponse(await statusToAPI(newStatus, user)); }; diff --git a/server/api/api/v1/timelines/home.ts b/server/api/api/v1/timelines/home.ts index 6a571189..fa9408e5 100644 --- a/server/api/api/v1/timelines/home.ts +++ b/server/api/api/v1/timelines/home.ts @@ -2,10 +2,9 @@ import { applyConfig } from "@api"; import { parseRequest } from "@request"; import { errorResponse, jsonResponse } from "@response"; -import { MatchedRoute } from "bun"; -import { FindManyOptions } from "typeorm"; -import { Status, statusAndUserRelations } from "~database/entities/Status"; -import { AuthData } from "~database/entities/User"; +import { client } from "~database/datasource"; +import { statusAndUserRelations, statusToAPI } from "~database/entities/Status"; +import { getFromRequest } from "~database/entities/User"; import { APIRouteMeta } from "~types/api"; export const meta: APIRouteMeta = applyConfig({ @@ -23,11 +22,9 @@ export const meta: APIRouteMeta = applyConfig({ /** * Fetch home timeline statuses */ -export default async ( - req: Request, - matchedRoute: MatchedRoute, - authData: AuthData -): Promise => { +export default async (req: Request): Promise => { + const { user } = await getFromRequest(req); + const { limit = 20, max_id, @@ -40,85 +37,54 @@ export default async ( limit?: number; }>(req); - const { user } = authData; - if (limit < 1 || limit > 40) { return errorResponse("Limit must be between 1 and 40", 400); } - let query: FindManyOptions = { + if (!user) return errorResponse("Unauthorized", 401); + + const objects = await client.status.findMany({ where: { - visibility: "public", - account: [ - { - relationships: { - id: user?.id, - followed_by: true, + id: { + lt: max_id ?? undefined, + gte: since_id ?? undefined, + gt: min_id ?? undefined, + }, + author: { + relationships: { + some: { + subjectId: user.id, + following: true, }, }, - { - id: user?.id, - }, - ], - }, - order: { - created_at: "DESC", + }, }, + include: statusAndUserRelations, take: limit, - relations: statusAndUserRelations, - }; + orderBy: { + id: "desc", + }, + }); - if (max_id) { - const maxPost = await Status.findOneBy({ id: max_id }); - if (maxPost) { - query = { - ...query, - where: { - ...query.where, - created_at: { - ...(query.where as any)?.created_at, - $lt: maxPost.created_at, - }, - }, - }; - } + // Constuct HTTP Link header (next and prev) + const linkHeader = []; + if (objects.length > 0) { + const urlWithoutQuery = req.url.split("?")[0]; + linkHeader.push( + `<${urlWithoutQuery}?max_id=${objects[0].id}&limit=${limit}>; rel="next"` + ); + linkHeader.push( + `<${urlWithoutQuery}?since_id=${ + objects[objects.length - 1].id + }&limit=${limit}>; rel="prev"` + ); } - if (min_id) { - const minPost = await Status.findOneBy({ id: min_id }); - if (minPost) { - query = { - ...query, - where: { - ...query.where, - created_at: { - ...(query.where as any)?.created_at, - $gt: minPost.created_at, - }, - }, - }; - } - } - - if (since_id) { - const sincePost = await Status.findOneBy({ id: since_id }); - if (sincePost) { - query = { - ...query, - where: { - ...query.where, - created_at: { - ...(query.where as any)?.created_at, - $gte: sincePost.created_at, - }, - }, - }; - } - } - - const objects = await Status.find(query); - return jsonResponse( - await Promise.all(objects.map(async object => await object.toAPI())) + await Promise.all(objects.map(async status => statusToAPI(status))), + 200, + { + Link: linkHeader.join(", "), + } ); }; diff --git a/server/api/api/v1/timelines/public.ts b/server/api/api/v1/timelines/public.ts index ad69380a..b1bf33bb 100644 --- a/server/api/api/v1/timelines/public.ts +++ b/server/api/api/v1/timelines/public.ts @@ -1,8 +1,8 @@ import { applyConfig } from "@api"; import { parseRequest } from "@request"; import { errorResponse, jsonResponse } from "@response"; -import { FindManyOptions, IsNull, Not } from "typeorm"; -import { Status, statusAndUserRelations } from "~database/entities/Status"; +import { client } from "~database/datasource"; +import { statusAndUserRelations, statusToAPI } from "~database/entities/Status"; import { APIRouteMeta } from "~types/api"; export const meta: APIRouteMeta = applyConfig({ @@ -17,36 +17,13 @@ export const meta: APIRouteMeta = applyConfig({ }, }); -const updateQuery = async ( - id: string | undefined, - operator: string, - query: FindManyOptions -) => { - if (!id) return query; - const post = await Status.findOneBy({ id }); - if (post) { - query = { - ...query, - where: { - ...query.where, - created_at: { - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - ...(query.where as any)?.created_at, - [operator]: post.created_at, - }, - }, - }; - } - return query; -}; - export default async (req: Request): Promise => { const { local, limit = 20, max_id, min_id, - only_media, + // only_media, remote, since_id, } = await parseRequest<{ @@ -67,48 +44,47 @@ export default async (req: Request): Promise => { return errorResponse("Cannot use both local and remote", 400); } - let query: FindManyOptions = { + const objects = await client.status.findMany({ where: { - visibility: "public", - }, - order: { - created_at: "DESC", + id: { + lt: max_id ?? undefined, + gte: since_id ?? undefined, + gt: min_id ?? undefined, + }, + instanceId: remote + ? { + not: null, + } + : local + ? null + : undefined, }, + include: statusAndUserRelations, take: limit, - relations: statusAndUserRelations, - }; + orderBy: { + id: "desc", + }, + }); - query = await updateQuery(max_id, "$lt", query); - query = await updateQuery(min_id, "$gt", query); - query = await updateQuery(since_id, "$gte", query); - - if (only_media) { - // TODO: add + // Constuct HTTP Link header (next and prev) + const linkHeader = []; + if (objects.length > 0) { + const urlWithoutQuery = req.url.split("?")[0]; + linkHeader.push( + `<${urlWithoutQuery}?max_id=${objects[0].id}&limit=${limit}>; rel="next"` + ); + linkHeader.push( + `<${urlWithoutQuery}?since_id=${ + objects[objects.length - 1].id + }&limit=${limit}>; rel="prev"` + ); } - if (local) { - query = { - ...query, - where: { - ...query.where, - instance: IsNull(), - }, - }; - } - - if (remote) { - query = { - ...query, - where: { - ...query.where, - instance: Not(IsNull()), - }, - }; - } - - const objects = await Status.find(query); - return jsonResponse( - await Promise.all(objects.map(async object => await object.toAPI())) + await Promise.all(objects.map(async status => statusToAPI(status))), + 200, + { + Link: linkHeader.join(", "), + } ); }; diff --git a/server/api/auth/login/index.ts b/server/api/auth/login/index.ts index 3fe279e7..ee58c44e 100644 --- a/server/api/auth/login/index.ts +++ b/server/api/auth/login/index.ts @@ -2,9 +2,8 @@ import { applyConfig } from "@api"; import { errorResponse } from "@response"; import { MatchedRoute } from "bun"; import { randomBytes } from "crypto"; -import { ApplicationAction } from "~database/entities/Application"; -import { Token } from "~database/entities/Token"; -import { UserAction, userRelations } from "~database/entities/User"; +import { client } from "~database/datasource"; +import { userRelations } from "~database/entities/User"; import { APIRouteMeta } from "~types/api"; export const meta: APIRouteMeta = applyConfig({ @@ -45,33 +44,44 @@ export default async ( return errorResponse("Missing username or password", 400); // Get user - const user = await UserAction.findOne({ + const user = await client.user.findFirst({ where: { email, }, - relations: userRelations, + include: userRelations, }); if (!user || !(await Bun.password.verify(password, user.password || ""))) return errorResponse("Invalid username or password", 401); // Get application - const application = await ApplicationAction.findOneBy({ - client_id, + const application = await client.application.findFirst({ + where: { + client_id, + }, }); if (!application) return errorResponse("Invalid client_id", 404); - const token = new Token(); - - token.access_token = randomBytes(64).toString("base64url"); - token.code = randomBytes(32).toString("hex"); - token.application = application; - token.scope = scopes.join(" "); - token.user = user; - - await token.save(); + const token = await client.application.update({ + where: { id: application.id }, + data: { + tokens: { + create: { + access_token: randomBytes(64).toString("base64url"), + code: randomBytes(32).toString("hex"), + scope: scopes.join(" "), + token_type: "bearer", + user: { + connect: { + id: user.id, + }, + }, + }, + }, + }, + }); // Redirect back to application - return Response.redirect(`${redirect_uri}?code=${token.code}`, 302); + return Response.redirect(`${redirect_uri}?code=${token.secret}`, 302); }; diff --git a/server/api/oauth/token/index.ts b/server/api/oauth/token/index.ts index 7e3fa9f0..a0c9d8ae 100644 --- a/server/api/oauth/token/index.ts +++ b/server/api/oauth/token/index.ts @@ -1,7 +1,7 @@ import { applyConfig } from "@api"; import { parseRequest } from "@request"; import { errorResponse, jsonResponse } from "@response"; -import { Token } from "~database/entities/Token"; +import { client } from "~database/datasource"; export const meta = applyConfig({ allowedMethods: ["POST"], @@ -36,14 +36,19 @@ export default async (req: Request): Promise => { ); // Get associated token - const token = await Token.findOneBy({ - code, - application: { - client_id, - secret: client_secret, - redirect_uris: redirect_uri, + const token = await client.token.findFirst({ + where: { + code, + application: { + client_id, + secret: client_secret, + redirect_uris: redirect_uri, + }, + scope: scope?.replaceAll("+", " "), + }, + include: { + application: true, }, - scope: scope?.replaceAll("+", " "), }); if (!token) diff --git a/server/api/users/[uuid]/inbox/index.ts b/server/api/users/[uuid]/inbox/index.ts index 1a143b7c..062cbae9 100644 --- a/server/api/users/[uuid]/inbox/index.ts +++ b/server/api/users/[uuid]/inbox/index.ts @@ -5,17 +5,16 @@ import { getConfig } from "@config"; import { getBestContentType } from "@content_types"; import { errorResponse, jsonResponse } from "@response"; import { MatchedRoute } from "bun"; -import { EmojiAction } from "~database/entities/Emoji"; -import { LysandObject } from "~database/entities/Object"; -import { Status } from "~database/entities/Status"; -import { UserAction, userRelations } from "~database/entities/User"; +import { client } from "~database/datasource"; +import { parseEmojis } from "~database/entities/Emoji"; +import { createFromObject } from "~database/entities/Object"; import { - ContentFormat, - LysandAction, - LysandObjectType, - LysandPublication, - Patch, -} from "~types/lysand/Object"; + createNewStatus, + fetchFromRemote, + statusAndUserRelations, +} from "~database/entities/Status"; +import { parseMentionsUris, userRelations } from "~database/entities/User"; +import { LysandAction, LysandPublication, Patch } from "~types/lysand/Object"; export const meta = applyConfig({ allowedMethods: ["POST"], @@ -61,11 +60,11 @@ export default async ( // Process request body const body = (await req.json()) as LysandPublication | LysandAction; - const author = await UserAction.findOne({ + const author = await client.user.findUnique({ where: { - uri: body.author, + username, }, - relations: userRelations, + include: userRelations, }); if (!author) { @@ -116,7 +115,7 @@ export default async ( // author.public_key is base64 encoded raw public key const publicKey = await crypto.subtle.importKey( "spki", - Buffer.from(author.public_key, "base64"), + Buffer.from(author.publicKey, "base64"), "Ed25519", false, ["verify"] @@ -141,13 +140,13 @@ export default async ( switch (type) { case "Note": { // Store the object in the LysandObject table - await LysandObject.createFromObject(body); + await createFromObject(body); const content = getBestContentType(body.contents); - const emojis = await EmojiAction.parseEmojis(content?.content || ""); + const emojis = await parseEmojis(content?.content || ""); - const newStatus = await Status.createNew({ + const newStatus = await createNewStatus({ account: author, content: content?.content || "", content_type: content?.content_type, @@ -158,39 +157,49 @@ export default async ( sensitive: body.is_sensitive, uri: body.uri, emojis: emojis, - mentions: await UserAction.parseMentions(body.mentions), + mentions: await parseMentionsUris(body.mentions), }); // If there is a reply, fetch all the reply parents and add them to the database if (body.replies_to.length > 0) { - newStatus.in_reply_to_post = await Status.fetchFromRemote( - body.replies_to[0] - ); + newStatus.inReplyToPostId = + (await fetchFromRemote(body.replies_to[0]))?.id || null; } // Same for quotes if (body.quotes.length > 0) { - newStatus.quoting_post = await Status.fetchFromRemote( - body.quotes[0] - ); + newStatus.quotingPostId = + (await fetchFromRemote(body.quotes[0]))?.id || null; } - await newStatus.save(); + await client.status.update({ + where: { + id: newStatus.id, + }, + data: { + inReplyToPostId: newStatus.inReplyToPostId, + quotingPostId: newStatus.quotingPostId, + }, + }); + break; } case "Patch": { const patch = body as Patch; // Store the object in the LysandObject table - await LysandObject.createFromObject(patch); + await createFromObject(patch); // Edit the status const content = getBestContentType(patch.contents); - const emojis = await EmojiAction.parseEmojis(content?.content || ""); + const emojis = await parseEmojis(content?.content || ""); - const status = await Status.findOneBy({ - id: patch.patched_id, + const status = await client.status.findUnique({ + where: { + uri: patch.patched_id, + }, + include: statusAndUserRelations, }); if (!status) { @@ -198,64 +207,81 @@ export default async ( } status.content = content?.content || ""; - status.content_type = content?.content_type || "text/plain"; - status.spoiler_text = patch.subject || ""; + status.contentType = content?.content_type || "text/plain"; + status.spoilerText = patch.subject || ""; status.sensitive = patch.is_sensitive; status.emojis = emojis; // If there is a reply, fetch all the reply parents and add them to the database if (body.replies_to.length > 0) { - status.in_reply_to_post = await Status.fetchFromRemote( - body.replies_to[0] - ); + status.inReplyToPostId = + (await fetchFromRemote(body.replies_to[0]))?.id || null; } // Same for quotes if (body.quotes.length > 0) { - status.quoting_post = await Status.fetchFromRemote( - body.quotes[0] - ); + status.quotingPostId = + (await fetchFromRemote(body.quotes[0]))?.id || null; } + + await client.status.update({ + where: { + id: status.id, + }, + data: { + content: status.content, + contentType: status.contentType, + spoilerText: status.spoilerText, + sensitive: status.sensitive, + emojis: { + connect: status.emojis.map(emoji => ({ + id: emoji.id, + })), + }, + inReplyToPostId: status.inReplyToPostId, + quotingPostId: status.quotingPostId, + }, + }); break; } case "Like": { // Store the object in the LysandObject table - await LysandObject.createFromObject(body); + await createFromObject(body); break; } case "Dislike": { // Store the object in the LysandObject table - await LysandObject.createFromObject(body); + await createFromObject(body); break; } case "Follow": { // Store the object in the LysandObject table - await LysandObject.createFromObject(body); + await createFromObject(body); break; } case "FollowAccept": { // Store the object in the LysandObject table - await LysandObject.createFromObject(body); + await createFromObject(body); break; } case "FollowReject": { // Store the object in the LysandObject table - await LysandObject.createFromObject(body); + await createFromObject(body); break; } case "Announce": { // Store the object in the LysandObject table - await LysandObject.createFromObject(body); + await createFromObject(body); break; } case "Undo": { // Store the object in the LysandObject table - await LysandObject.createFromObject(body); + await createFromObject(body); break; } case "Extension": { // Store the object in the LysandObject table - await LysandObject.createFromObject(body); + await createFromObject(body); break; } default: { diff --git a/server/api/users/[uuid]/index.ts b/server/api/users/[uuid]/index.ts index f80eca54..8d37794b 100644 --- a/server/api/users/[uuid]/index.ts +++ b/server/api/users/[uuid]/index.ts @@ -4,7 +4,8 @@ import { applyConfig } from "@api"; import { getConfig } from "@config"; import { errorResponse, jsonResponse } from "@response"; import { MatchedRoute } from "bun"; -import { UserAction, userRelations } from "~database/entities/User"; +import { client } from "~database/datasource"; +import { userRelations, userToLysand } from "~database/entities/User"; export const meta = applyConfig({ allowedMethods: ["POST"], @@ -29,16 +30,16 @@ export default async ( const config = getConfig(); - const user = await UserAction.findOne({ + const user = await client.user.findUnique({ where: { id: uuid, }, - relations: userRelations, + include: userRelations, }); if (!user) { return errorResponse("User not found", 404); } - return jsonResponse(user.toLysand()); + return jsonResponse(userToLysand(user)); }; diff --git a/server/api/users/[uuid]/outbox/index.ts b/server/api/users/[uuid]/outbox/index.ts index 813bbfc5..bc3d3559 100644 --- a/server/api/users/[uuid]/outbox/index.ts +++ b/server/api/users/[uuid]/outbox/index.ts @@ -1,10 +1,12 @@ import { jsonResponse } from "@response"; import { MatchedRoute } from "bun"; -import { userRelations } from "~database/entities/User"; import { getConfig, getHost } from "@config"; import { applyConfig } from "@api"; -import { Status } from "~database/entities/Status"; -import { In } from "typeorm"; +import { + statusAndUserRelations, + statusToLysand, +} from "~database/entities/Status"; +import { client } from "~database/datasource"; export const meta = applyConfig({ allowedMethods: ["GET"], @@ -29,26 +31,25 @@ export default async ( const pageNumber = Number(matchedRoute.query.page) || 1; const config = getConfig(); - const statuses = await Status.find({ + const statuses = await client.status.findMany({ where: { - account: { - id: uuid, + authorId: uuid, + visibility: { + in: ["public", "unlisted"], }, - visibility: In(["public", "unlisted"]), }, - relations: userRelations, take: 20, skip: 20 * (pageNumber - 1), + include: statusAndUserRelations, }); - const totalStatuses = await Status.count({ + const totalStatuses = await client.status.count({ where: { - account: { - id: uuid, + authorId: uuid, + visibility: { + in: ["public", "unlisted"], }, - visibility: In(["public", "unlisted"]), }, - relations: userRelations, }); return jsonResponse({ @@ -65,6 +66,6 @@ export default async ( pageNumber > 1 ? `${getHost()}/users/${uuid}/outbox?page=${pageNumber - 1}` : undefined, - items: statuses.map(s => s.toLysand()), + items: statuses.map(s => statusToLysand(s)), }); }; diff --git a/tests/api.test.ts b/tests/api.test.ts index a7f939f8..56e9e21e 100644 --- a/tests/api.test.ts +++ b/tests/api.test.ts @@ -1,71 +1,63 @@ /* eslint-disable @typescript-eslint/no-unsafe-member-access */ /* eslint-disable @typescript-eslint/no-explicit-any */ import { getConfig } from "@config"; +import { Token } from "@prisma/client"; import { afterAll, beforeAll, describe, expect, test } from "bun:test"; -import { AppDataSource } from "~database/datasource"; -import { ApplicationAction } from "~database/entities/Application"; -import { EmojiAction } from "~database/entities/Emoji"; -import { Token, TokenType } from "~database/entities/Token"; -import { UserAction } from "~database/entities/User"; +import { client } from "~database/datasource"; +import { TokenType } from "~database/entities/Token"; +import { UserWithRelations, createNewLocalUser } from "~database/entities/User"; import { APIEmoji } from "~types/entities/emoji"; import { APIInstance } from "~types/entities/instance"; const config = getConfig(); let token: Token; -let user: UserAction; -let user2: UserAction; +let user: UserWithRelations; describe("API Tests", () => { beforeAll(async () => { - if (!AppDataSource.isInitialized) await AppDataSource.initialize(); - // Initialize test user - user = await UserAction.createNewLocal({ + user = await createNewLocalUser({ email: "test@test.com", username: "test", password: "test", display_name: "", }); - // Initialize second test user - user2 = await UserAction.createNewLocal({ - email: "test2@test.com", - username: "test2", - password: "test2", - display_name: "", + token = await client.token.create({ + data: { + access_token: "test", + application: { + create: { + client_id: "test", + name: "Test Application", + redirect_uris: "https://example.com", + scopes: "read write", + secret: "test", + website: "https://example.com", + vapid_key: null, + }, + }, + code: "test", + scope: "read write", + token_type: TokenType.BEARER, + user: { + connect: { + id: user.id, + }, + }, + }, }); - - const app = new ApplicationAction(); - - app.name = "Test Application"; - app.website = "https://example.com"; - app.client_id = "test"; - app.redirect_uris = "https://example.com"; - app.scopes = "read write"; - app.secret = "test"; - app.vapid_key = null; - - await app.save(); - - // Initialize test token - token = new Token(); - - token.access_token = "test"; - token.application = app; - token.code = "test"; - token.scope = "read write"; - token.token_type = TokenType.BEARER; - token.user = user; - - token = await token.save(); }); afterAll(async () => { - await user.remove(); - await user2.remove(); - - await AppDataSource.destroy(); + await client.user.deleteMany({ + where: { + username: { + in: ["test", "test2"], + }, + }, + }); }); describe("GET /api/v1/instance", () => { @@ -106,15 +98,15 @@ describe("API Tests", () => { describe("GET /api/v1/custom_emojis", () => { beforeAll(async () => { - const emoji = new EmojiAction(); - - emoji.instance = null; - emoji.url = "https://example.com/test.png"; - emoji.content_type = "image/png"; - emoji.shortcode = "test"; - emoji.visible_in_picker = true; - - await emoji.save(); + await client.emoji.create({ + data: { + instanceId: null, + url: "https://example.com/test.png", + content_type: "image/png", + shortcode: "test", + visible_in_picker: true, + }, + }); }); test("should return an array of at least one custom emoji", async () => { const response = await fetch( @@ -139,7 +131,11 @@ describe("API Tests", () => { expect(emojis[0].url).toBe("https://example.com/test.png"); }); afterAll(async () => { - await EmojiAction.delete({ shortcode: "test" }); + await client.emoji.deleteMany({ + where: { + shortcode: "test", + }, + }); }); }); }); diff --git a/tests/api/accounts.test.ts b/tests/api/accounts.test.ts index 44af9feb..fae15c4d 100644 --- a/tests/api/accounts.test.ts +++ b/tests/api/accounts.test.ts @@ -1,11 +1,11 @@ /* eslint-disable @typescript-eslint/no-unsafe-member-access */ /* eslint-disable @typescript-eslint/no-explicit-any */ import { getConfig } from "@config"; +import { Token } from "@prisma/client"; import { afterAll, beforeAll, describe, expect, test } from "bun:test"; -import { AppDataSource } from "~database/datasource"; -import { ApplicationAction } from "~database/entities/Application"; -import { Token, TokenType } from "~database/entities/Token"; -import { UserAction } from "~database/entities/User"; +import { client } from "~database/datasource"; +import { TokenType } from "~database/entities/Token"; +import { UserWithRelations, createNewLocalUser } from "~database/entities/User"; import { APIAccount } from "~types/entities/account"; import { APIRelationship } from "~types/entities/relationship"; import { APIStatus } from "~types/entities/status"; @@ -13,59 +13,59 @@ import { APIStatus } from "~types/entities/status"; const config = getConfig(); let token: Token; -let user: UserAction; -let user2: UserAction; +let user: UserWithRelations; +let user2: UserWithRelations; describe("API Tests", () => { beforeAll(async () => { - if (!AppDataSource.isInitialized) await AppDataSource.initialize(); - - // Initialize test user - user = await UserAction.createNewLocal({ + user = await createNewLocalUser({ email: "test@test.com", username: "test", password: "test", display_name: "", }); - // Initialize second test user - user2 = await UserAction.createNewLocal({ + user2 = await createNewLocalUser({ email: "test2@test.com", username: "test2", password: "test2", display_name: "", }); - const app = new ApplicationAction(); - - app.name = "Test Application"; - app.website = "https://example.com"; - app.client_id = "test"; - app.redirect_uris = "https://example.com"; - app.scopes = "read write"; - app.secret = "test"; - app.vapid_key = null; - - await app.save(); - - // Initialize test token - token = new Token(); - - token.access_token = "test"; - token.application = app; - token.code = "test"; - token.scope = "read write"; - token.token_type = TokenType.BEARER; - token.user = user; - - token = await token.save(); + token = await client.token.create({ + data: { + access_token: "test", + application: { + create: { + client_id: "test", + name: "Test Application", + redirect_uris: "https://example.com", + scopes: "read write", + secret: "test", + website: "https://example.com", + vapid_key: null, + }, + }, + code: "test", + scope: "read write", + token_type: TokenType.BEARER, + user: { + connect: { + id: user.id, + }, + }, + }, + }); }); afterAll(async () => { - await user.remove(); - await user2.remove(); - - await AppDataSource.destroy(); + await client.user.deleteMany({ + where: { + username: { + in: ["test", "test2"], + }, + }, + }); }); describe("POST /api/v1/accounts/:id", () => { diff --git a/tests/api/statuses.test.ts b/tests/api/statuses.test.ts index 168a9b27..3c82a160 100644 --- a/tests/api/statuses.test.ts +++ b/tests/api/statuses.test.ts @@ -1,72 +1,65 @@ /* eslint-disable @typescript-eslint/no-unsafe-member-access */ /* eslint-disable @typescript-eslint/no-explicit-any */ import { getConfig } from "@config"; +import { Token } from "@prisma/client"; import { afterAll, beforeAll, describe, expect, test } from "bun:test"; -import { AppDataSource } from "~database/datasource"; -import { ApplicationAction } from "~database/entities/Application"; -import { Token, TokenType } from "~database/entities/Token"; -import { UserAction } from "~database/entities/User"; +import { client } from "~database/datasource"; +import { TokenType } from "~database/entities/Token"; +import { UserWithRelations, createNewLocalUser } from "~database/entities/User"; +import { APIAccount } from "~types/entities/account"; import { APIContext } from "~types/entities/context"; import { APIStatus } from "~types/entities/status"; const config = getConfig(); let token: Token; -let user: UserAction; -let user2: UserAction; +let user: UserWithRelations; let status: APIStatus | null = null; let status2: APIStatus | null = null; describe("API Tests", () => { beforeAll(async () => { - if (!AppDataSource.isInitialized) await AppDataSource.initialize(); - - // Initialize test user - user = await UserAction.createNewLocal({ + user = await createNewLocalUser({ email: "test@test.com", username: "test", password: "test", display_name: "", }); - // Initialize second test user - user2 = await UserAction.createNewLocal({ - email: "test2@test.com", - username: "test2", - password: "test2", - display_name: "", + token = await client.token.create({ + data: { + access_token: "test", + application: { + create: { + client_id: "test", + name: "Test Application", + redirect_uris: "https://example.com", + scopes: "read write", + secret: "test", + website: "https://example.com", + vapid_key: null, + }, + }, + code: "test", + scope: "read write", + token_type: TokenType.BEARER, + user: { + connect: { + id: user.id, + }, + }, + }, }); - - const app = new ApplicationAction(); - - app.name = "Test Application"; - app.website = "https://example.com"; - app.client_id = "test"; - app.redirect_uris = "https://example.com"; - app.scopes = "read write"; - app.secret = "test"; - app.vapid_key = null; - - await app.save(); - - // Initialize test token - token = new Token(); - - token.access_token = "test"; - token.application = app; - token.code = "test"; - token.scope = "read write"; - token.token_type = TokenType.BEARER; - token.user = user; - - token = await token.save(); }); afterAll(async () => { - await user.remove(); - await user2.remove(); - - await AppDataSource.destroy(); + await client.user.deleteMany({ + where: { + username: { + in: ["test", "test2"], + }, + }, + }); }); describe("POST /api/v1/statuses", () => { @@ -322,7 +315,7 @@ describe("API Tests", () => { "application/json" ); - const users = (await response.json()) as UserAction[]; + const users = (await response.json()) as APIAccount[]; expect(users.length).toBe(1); expect(users[0].id).toBe(user.id); diff --git a/tests/oauth.test.ts b/tests/oauth.test.ts index a0d132d9..13adc8d9 100644 --- a/tests/oauth.test.ts +++ b/tests/oauth.test.ts @@ -1,9 +1,8 @@ import { getConfig } from "@config"; +import { Application, Token } from "@prisma/client"; import { afterAll, beforeAll, describe, expect, test } from "bun:test"; -import { AppDataSource } from "~database/datasource"; -import { ApplicationAction } from "~database/entities/Application"; -import { Token } from "~database/entities/Token"; -import { UserAction, userRelations } from "~database/entities/User"; +import { client } from "~database/datasource"; +import { createNewLocalUser } from "~database/entities/User"; const config = getConfig(); @@ -13,10 +12,8 @@ let code: string; let token: Token; beforeAll(async () => { - if (!AppDataSource.isInitialized) await AppDataSource.initialize(); - - // Initialize test user - await UserAction.createNewLocal({ + // Init test user + await createNewLocalUser({ email: "test@test.com", username: "test", password: "test", @@ -139,7 +136,7 @@ describe("GET /api/v1/apps/verify_credentials", () => { expect(response.status).toBe(200); expect(response.headers.get("content-type")).toBe("application/json"); - const credentials = (await response.json()) as Partial; + const credentials = (await response.json()) as Partial; expect(credentials.name).toBe("Test Application"); expect(credentials.website).toBe("https://example.com"); @@ -150,31 +147,9 @@ describe("GET /api/v1/apps/verify_credentials", () => { afterAll(async () => { // Clean up user - const user = await UserAction.findOne({ + await client.user.delete({ where: { username: "test", }, - relations: userRelations, }); - - // Clean up tokens - const tokens = await Token.findBy({ - user: { - username: "test", - }, - }); - - const applications = await ApplicationAction.findBy({ - client_id, - secret: client_secret, - }); - - await Promise.all(tokens.map(async token => await token.remove())); - await Promise.all( - applications.map(async application => await application.remove()) - ); - - if (user) await user.remove(); - - await AppDataSource.destroy(); });