Finish rewrite of everything with Prisma

This commit is contained in:
Jesse Wierzbinski 2023-11-11 15:37:14 -10:00
parent 5eed8374cd
commit dc0ec47543
No known key found for this signature in database
GPG key ID: F9A1E418934E40B0
47 changed files with 1283 additions and 1036 deletions

View file

@ -5,7 +5,7 @@
## What is this? ## 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. 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 ### 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 - A PostgreSQL database
- (Optional but recommended) A Linux-based operating system - (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. > **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) The following extensions are currently supported or being worked on:
- `org.lysand:custom_emojis`: Custom emojis
Planned federation features are:
- Activities: `Follow`, `Block`, `Undo`, `Announce`, `Like`, `Dislike`, `Flag`, `Ignore` and more
- Objects: `Emoji` and more
## API ## API
@ -186,6 +183,8 @@ Configuration can be found inside the `config.toml` file. The following values a
### ActivityPub ### 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` - `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` - `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" ]` - `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" ]`

BIN
bun.lockb

Binary file not shown.

View file

@ -21,6 +21,7 @@ export const parseEmojis = async (text: string): Promise<Emoji[]> => {
shortcode: { shortcode: {
in: matches.map(match => match.replace(/:/g, "")), in: matches.map(match => match.replace(/:/g, "")),
}, },
instanceId: null,
}, },
include: { include: {
instance: true, instance: true,

View file

@ -12,7 +12,7 @@ import { client } from "~database/datasource";
* @param other The user who is the subject of the relationship. * @param other The user who is the subject of the relationship.
* @returns The newly created relationship. * @returns The newly created relationship.
*/ */
export const createNew = async ( export const createNewRelationship = async (
owner: User, owner: User,
other: User other: User
): Promise<Relationship> => { ): Promise<Relationship> => {
@ -41,8 +41,10 @@ export const createNew = async (
* Converts the relationship to an API-friendly format. * Converts the relationship to an API-friendly format.
* @returns The API-friendly relationship. * @returns The API-friendly relationship.
*/ */
// eslint-disable-next-line @typescript-eslint/require-await export const relationshipToAPI = async (
export const toAPI = async (rel: Relationship): Promise<APIRelationship> => { rel: Relationship
// eslint-disable-next-line @typescript-eslint/require-await
): Promise<APIRelationship> => {
return { return {
blocked_by: rel.blockedBy, blocked_by: rel.blockedBy,
blocking: rel.blocking, blocking: rel.blocking,

View file

@ -47,9 +47,9 @@ export const statusAndUserRelations = {
instance: true, instance: true,
mentions: true, mentions: true,
pinnedBy: true, pinnedBy: true,
replies: { _count: {
include: { select: {
_count: true, replies: true,
}, },
}, },
}, },
@ -57,9 +57,10 @@ export const statusAndUserRelations = {
instance: true, instance: true,
mentions: true, mentions: true,
pinnedBy: true, pinnedBy: true,
replies: { _count: {
include: { select: {
_count: true, replies: true,
likes: true,
}, },
}, },
reblog: { reblog: {
@ -77,9 +78,9 @@ export const statusAndUserRelations = {
instance: true, instance: true,
mentions: true, mentions: true,
pinnedBy: true, pinnedBy: true,
replies: { _count: {
include: { select: {
_count: true, replies: true,
}, },
}, },
}, },
@ -99,9 +100,9 @@ export const statusAndUserRelations = {
instance: true, instance: true,
mentions: true, mentions: true,
pinnedBy: true, pinnedBy: true,
replies: { _count: {
include: { select: {
_count: true, replies: true,
}, },
}, },
}, },
@ -113,7 +114,7 @@ export const statusAndUserRelations = {
}, },
}; };
type StatusWithRelations = Status & { export type StatusWithRelations = Status & {
author: UserWithRelations; author: UserWithRelations;
application: Application | null; application: Application | null;
emojis: Emoji[]; emojis: Emoji[];
@ -126,16 +127,17 @@ type StatusWithRelations = Status & {
instance: Instance | null; instance: Instance | null;
mentions: User[]; mentions: User[];
pinnedBy: User[]; pinnedBy: User[];
replies: Status[] & { _count: {
_count: number; replies: number;
}; };
}) })
| null; | null;
instance: Instance | null; instance: Instance | null;
mentions: User[]; mentions: User[];
pinnedBy: User[]; pinnedBy: User[];
replies: Status[] & { _count: {
_count: number; replies: number;
likes: number;
}; };
reblog: reblog:
| (Status & { | (Status & {
@ -146,8 +148,8 @@ type StatusWithRelations = Status & {
instance: Instance | null; instance: Instance | null;
mentions: User[]; mentions: User[];
pinnedBy: User[]; pinnedBy: User[];
replies: Status[] & { _count: {
_count: number; replies: number;
}; };
}) })
| null; | null;
@ -160,8 +162,8 @@ type StatusWithRelations = Status & {
instance: Instance | null; instance: Instance | null;
mentions: User[]; mentions: User[];
pinnedBy: User[]; pinnedBy: User[];
replies: Status[] & { _count: {
_count: number; replies: number;
}; };
}) })
| null; | null;
@ -196,12 +198,13 @@ export const isViewableByUser = (status: Status, user: User | null) => {
export const fetchFromRemote = async (uri: string): Promise<Status | null> => { export const fetchFromRemote = async (uri: string): Promise<Status | null> => {
// Check if already in database // Check if already in database
const existingStatus = await client.status.findFirst({ const existingStatus: StatusWithRelations | null =
where: { await client.status.findFirst({
uri: uri, where: {
}, uri: uri,
include: statusAndUserRelations, },
}); include: statusAndUserRelations,
});
if (existingStatus) return existingStatus; if (existingStatus) return existingStatus;
@ -228,7 +231,7 @@ export const fetchFromRemote = async (uri: string): Promise<Status | null> => {
quotingStatus = await fetchFromRemote(body.quotes[0]); quotingStatus = await fetchFromRemote(body.quotes[0]);
} }
return await createNew({ return await createNewStatus({
account: author, account: author,
content: content?.content || "", content: content?.content || "",
content_type: content?.content_type, content_type: content?.content_type,
@ -254,18 +257,79 @@ export const fetchFromRemote = async (uri: string): Promise<Status | null> => {
* Return all the ancestors of this post, * Return all the ancestors of this post,
*/ */
// eslint-disable-next-line @typescript-eslint/require-await, @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/require-await, @typescript-eslint/no-unused-vars
export const getAncestors = async (fetcher: UserWithRelations | null) => { export const getAncestors = async (
// TODO: Implement status: StatusWithRelations,
return []; 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 // eslint-disable-next-line @typescript-eslint/require-await, @typescript-eslint/no-unused-vars
export const getDescendants = async (fetcher: UserWithRelations | null) => { export const getDescendants = async (
// TODO: Implement status: StatusWithRelations,
return []; 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. * @param data The data for the new status.
* @returns A promise that resolves with the new status. * @returns A promise that resolves with the new status.
*/ */
const createNew = async (data: { export const createNewStatus = async (data: {
account: User; account: User;
application: Application | null; application: Application | null;
content: string; content: string;
@ -408,7 +472,7 @@ export const statusToAPI = async (
reblogId: status.id, reblogId: status.id,
}, },
}), }),
replies_count: status.replies._count, replies_count: status._count.replies,
sensitive: status.sensitive, sensitive: status.sensitive,
spoiler_text: status.spoilerText, spoiler_text: status.spoilerText,
tags: [], tags: [],

View file

@ -32,9 +32,10 @@ export const userRelations = {
relationships: true, relationships: true,
relationshipSubjects: true, relationshipSubjects: true,
pinnedNotes: true, pinnedNotes: true,
statuses: { _count: {
select: { select: {
_count: true, statuses: true,
likes: true,
}, },
}, },
}; };
@ -46,8 +47,9 @@ export type UserWithRelations = User & {
relationships: Relationship[]; relationships: Relationship[];
relationshipSubjects: Relationship[]; relationshipSubjects: Relationship[];
pinnedNotes: Status[]; pinnedNotes: Status[];
statuses: { _count: {
length: number; statuses: number;
likes: number;
}; };
}; };
@ -353,7 +355,7 @@ export const userToAPI = async (
followers_count: user.relationshipSubjects.filter(r => r.following) followers_count: user.relationshipSubjects.filter(r => r.following)
.length, .length,
following_count: user.relationships.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))), emojis: await Promise.all(user.emojis.map(emoji => emojiToAPI(emoji))),
// TODO: Add fields // TODO: Add fields
fields: [], fields: [],

View file

@ -66,12 +66,14 @@
"chalk": "^5.3.0", "chalk": "^5.3.0",
"html-to-text": "^9.0.5", "html-to-text": "^9.0.5",
"ip-matching": "^2.1.2", "ip-matching": "^2.1.2",
"iso-639-1": "^3.1.0",
"isomorphic-dompurify": "^1.9.0", "isomorphic-dompurify": "^1.9.0",
"jsonld": "^8.3.1", "jsonld": "^8.3.1",
"marked": "^9.1.2", "marked": "^9.1.2",
"pg": "^8.11.3", "pg": "^8.11.3",
"prisma": "^5.5.2", "prisma": "^5.5.2",
"reflect-metadata": "^0.1.13", "reflect-metadata": "^0.1.13",
"sharp": "^0.32.6",
"typeorm": "^0.3.17" "typeorm": "^0.3.17"
} }
} }

View file

@ -151,6 +151,10 @@ model User {
header String header String
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
isBot Boolean @default(false)
isLocked Boolean @default(false)
isDiscoverable Boolean @default(false)
sanctions String[] @default([])
publicKey String publicKey String
privateKey String? // Nullable privateKey String? // Nullable
relationships Relationship[] @relation("OwnerToRelationship") // One to many relation with Relationship relationships Relationship[] @relation("OwnerToRelationship") // One to many relation with Relationship

View file

@ -1,8 +1,15 @@
import { errorResponse, jsonResponse } from "@response"; import { errorResponse, jsonResponse } from "@response";
import { MatchedRoute } from "bun"; import { MatchedRoute } from "bun";
import { Relationship } from "~database/entities/Relationship"; import {
import { UserAction, userRelations } from "~database/entities/User"; createNewRelationship,
relationshipToAPI,
} from "~database/entities/Relationship";
import {
getFromRequest,
getRelationshipToOtherUser,
} from "~database/entities/User";
import { applyConfig } from "@api"; import { applyConfig } from "@api";
import { client } from "~database/datasource";
export const meta = applyConfig({ export const meta = applyConfig({
allowedMethods: ["POST"], allowedMethods: ["POST"],
@ -25,29 +32,42 @@ export default async (
): Promise<Response> => { ): Promise<Response> => {
const id = matchedRoute.params.id; const id = matchedRoute.params.id;
const { user: self } = await UserAction.getFromRequest(req); const { user: self } = await getFromRequest(req);
if (!self) return errorResponse("Unauthorized", 401); if (!self) return errorResponse("Unauthorized", 401);
const user = await UserAction.findOne({ const user = await client.user.findUnique({
where: { where: { id },
id, include: {
relationships: {
include: {
owner: true,
subject: true,
},
},
}, },
relations: userRelations,
}); });
if (!user) return errorResponse("User not found", 404); if (!user) return errorResponse("User not found", 404);
// Check if already following // Check if already following
let relationship = await self.getRelationshipToOtherUser(user); let relationship = await getRelationshipToOtherUser(self, user);
if (!relationship) { if (!relationship) {
// Create new relationship // Create new relationship
const newRelationship = await Relationship.createNew(self, user); const newRelationship = await createNewRelationship(self, user);
self.relationships.push(newRelationship); await client.user.update({
await self.save(); where: { id: self.id },
data: {
relationships: {
connect: {
id: newRelationship.id,
},
},
},
});
relationship = newRelationship; relationship = newRelationship;
} }
@ -56,6 +76,12 @@ export default async (
relationship.blocking = true; relationship.blocking = true;
} }
await relationship.save(); await client.relationship.update({
return jsonResponse(await relationship.toAPI()); where: { id: relationship.id },
data: {
blocking: true,
},
});
return jsonResponse(await relationshipToAPI(relationship));
}; };

View file

@ -1,9 +1,16 @@
import { parseRequest } from "@request"; import { parseRequest } from "@request";
import { errorResponse, jsonResponse } from "@response"; import { errorResponse, jsonResponse } from "@response";
import { MatchedRoute } from "bun"; import { MatchedRoute } from "bun";
import { Relationship } from "~database/entities/Relationship"; import {
import { UserAction, userRelations } from "~database/entities/User"; createNewRelationship,
relationshipToAPI,
} from "~database/entities/Relationship";
import {
getFromRequest,
getRelationshipToOtherUser,
} from "~database/entities/User";
import { applyConfig } from "@api"; import { applyConfig } from "@api";
import { client } from "~database/datasource";
export const meta = applyConfig({ export const meta = applyConfig({
allowedMethods: ["POST"], allowedMethods: ["POST"],
@ -26,7 +33,7 @@ export default async (
): Promise<Response> => { ): Promise<Response> => {
const id = matchedRoute.params.id; const id = matchedRoute.params.id;
const { user: self } = await UserAction.getFromRequest(req); const { user: self } = await getFromRequest(req);
if (!self) return errorResponse("Unauthorized", 401); if (!self) return errorResponse("Unauthorized", 401);
@ -36,25 +43,38 @@ export default async (
languages?: string[]; languages?: string[];
}>(req); }>(req);
const user = await UserAction.findOne({ const user = await client.user.findUnique({
where: { where: { id },
id, include: {
relationships: {
include: {
owner: true,
subject: true,
},
},
}, },
relations: userRelations,
}); });
if (!user) return errorResponse("User not found", 404); if (!user) return errorResponse("User not found", 404);
// Check if already following // Check if already following
let relationship = await self.getRelationshipToOtherUser(user); let relationship = await getRelationshipToOtherUser(self, user);
if (!relationship) { if (!relationship) {
// Create new relationship // Create new relationship
const newRelationship = await Relationship.createNew(self, user); const newRelationship = await createNewRelationship(self, user);
self.relationships.push(newRelationship); await client.user.update({
await self.save(); where: { id: self.id },
data: {
relationships: {
connect: {
id: newRelationship.id,
},
},
},
});
relationship = newRelationship; relationship = newRelationship;
} }
@ -63,7 +83,7 @@ export default async (
relationship.following = true; relationship.following = true;
} }
if (reblogs) { if (reblogs) {
relationship.showing_reblogs = true; relationship.showingReblogs = true;
} }
if (notify) { if (notify) {
relationship.notifying = true; relationship.notifying = true;
@ -72,6 +92,15 @@ export default async (
relationship.languages = languages; relationship.languages = languages;
} }
await relationship.save(); await client.relationship.update({
return jsonResponse(await relationship.toAPI()); where: { id: relationship.id },
data: {
following: true,
showingReblogs: reblogs ?? false,
notifying: notify ?? false,
languages: languages ?? [],
},
});
return jsonResponse(relationshipToAPI(relationship));
}; };

View file

@ -1,7 +1,13 @@
import { errorResponse, jsonResponse } from "@response"; import { errorResponse, jsonResponse } from "@response";
import { MatchedRoute } from "bun"; 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 { applyConfig } from "@api";
import { client } from "~database/datasource";
export const meta = applyConfig({ export const meta = applyConfig({
allowedMethods: ["GET"], allowedMethods: ["GET"],
@ -24,15 +30,13 @@ export default async (
): Promise<Response> => { ): Promise<Response> => {
const id = matchedRoute.params.id; 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 { try {
foundUser = await UserAction.findOne({ foundUser = await client.user.findUnique({
where: { where: { id },
id, include: userRelations,
},
relations: userRelations,
}); });
} catch (e) { } catch (e) {
return errorResponse("Invalid ID", 404); return errorResponse("Invalid ID", 404);
@ -40,5 +44,5 @@ export default async (
if (!foundUser) return errorResponse("User not found", 404); 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));
}; };

View file

@ -1,9 +1,16 @@
import { parseRequest } from "@request"; import { parseRequest } from "@request";
import { errorResponse, jsonResponse } from "@response"; import { errorResponse, jsonResponse } from "@response";
import { MatchedRoute } from "bun"; import { MatchedRoute } from "bun";
import { Relationship } from "~database/entities/Relationship"; import {
import { UserAction, userRelations } from "~database/entities/User"; createNewRelationship,
relationshipToAPI,
} from "~database/entities/Relationship";
import {
getFromRequest,
getRelationshipToOtherUser,
} from "~database/entities/User";
import { applyConfig } from "@api"; import { applyConfig } from "@api";
import { client } from "~database/datasource";
export const meta = applyConfig({ export const meta = applyConfig({
allowedMethods: ["POST"], allowedMethods: ["POST"],
@ -26,7 +33,7 @@ export default async (
): Promise<Response> => { ): Promise<Response> => {
const id = matchedRoute.params.id; const id = matchedRoute.params.id;
const { user: self } = await UserAction.getFromRequest(req); const { user: self } = await getFromRequest(req);
if (!self) return errorResponse("Unauthorized", 401); if (!self) return errorResponse("Unauthorized", 401);
@ -36,25 +43,38 @@ export default async (
duration: number; duration: number;
}>(req); }>(req);
const user = await UserAction.findOne({ const user = await client.user.findUnique({
where: { where: { id },
id, include: {
relationships: {
include: {
owner: true,
subject: true,
},
},
}, },
relations: userRelations,
}); });
if (!user) return errorResponse("User not found", 404); if (!user) return errorResponse("User not found", 404);
// Check if already following // Check if already following
let relationship = await self.getRelationshipToOtherUser(user); let relationship = await getRelationshipToOtherUser(self, user);
if (!relationship) { if (!relationship) {
// Create new relationship // Create new relationship
const newRelationship = await Relationship.createNew(self, user); const newRelationship = await createNewRelationship(self, user);
self.relationships.push(newRelationship); await client.user.update({
await self.save(); where: { id: self.id },
data: {
relationships: {
connect: {
id: newRelationship.id,
},
},
},
});
relationship = newRelationship; relationship = newRelationship;
} }
@ -63,11 +83,18 @@ export default async (
relationship.muting = true; relationship.muting = true;
} }
if (notifications ?? 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 // TODO: Implement duration
await relationship.save(); return jsonResponse(await relationshipToAPI(relationship));
return jsonResponse(await relationship.toAPI());
}; };

View file

@ -1,9 +1,16 @@
import { parseRequest } from "@request"; import { parseRequest } from "@request";
import { errorResponse, jsonResponse } from "@response"; import { errorResponse, jsonResponse } from "@response";
import { MatchedRoute } from "bun"; import { MatchedRoute } from "bun";
import { Relationship } from "~database/entities/Relationship"; import {
import { UserAction, userRelations } from "~database/entities/User"; createNewRelationship,
relationshipToAPI,
} from "~database/entities/Relationship";
import {
getFromRequest,
getRelationshipToOtherUser,
} from "~database/entities/User";
import { applyConfig } from "@api"; import { applyConfig } from "@api";
import { client } from "~database/datasource";
export const meta = applyConfig({ export const meta = applyConfig({
allowedMethods: ["POST"], allowedMethods: ["POST"],
@ -26,7 +33,7 @@ export default async (
): Promise<Response> => { ): Promise<Response> => {
const id = matchedRoute.params.id; const id = matchedRoute.params.id;
const { user: self } = await UserAction.getFromRequest(req); const { user: self } = await getFromRequest(req);
if (!self) return errorResponse("Unauthorized", 401); if (!self) return errorResponse("Unauthorized", 401);
@ -34,31 +41,50 @@ export default async (
comment: string; comment: string;
}>(req); }>(req);
const user = await UserAction.findOne({ const user = await client.user.findUnique({
where: { where: { id },
id, include: {
relationships: {
include: {
owner: true,
subject: true,
},
},
}, },
relations: userRelations,
}); });
if (!user) return errorResponse("User not found", 404); if (!user) return errorResponse("User not found", 404);
// Check if already following // Check if already following
let relationship = await self.getRelationshipToOtherUser(user); let relationship = await getRelationshipToOtherUser(self, user);
if (!relationship) { if (!relationship) {
// Create new relationship // Create new relationship
const newRelationship = await Relationship.createNew(self, user); const newRelationship = await createNewRelationship(self, user);
self.relationships.push(newRelationship); await client.user.update({
await self.save(); where: { id: self.id },
data: {
relationships: {
connect: {
id: newRelationship.id,
},
},
},
});
relationship = newRelationship; relationship = newRelationship;
} }
relationship.note = comment ?? ""; relationship.note = comment ?? "";
await relationship.save(); await client.relationship.update({
return jsonResponse(await relationship.toAPI()); where: { id: relationship.id },
data: {
note: relationship.note,
},
});
return jsonResponse(await relationshipToAPI(relationship));
}; };

View file

@ -1,8 +1,15 @@
import { errorResponse, jsonResponse } from "@response"; import { errorResponse, jsonResponse } from "@response";
import { MatchedRoute } from "bun"; import { MatchedRoute } from "bun";
import { Relationship } from "~database/entities/Relationship"; import {
import { UserAction, userRelations } from "~database/entities/User"; createNewRelationship,
relationshipToAPI,
} from "~database/entities/Relationship";
import {
getFromRequest,
getRelationshipToOtherUser,
} from "~database/entities/User";
import { applyConfig } from "@api"; import { applyConfig } from "@api";
import { client } from "~database/datasource";
export const meta = applyConfig({ export const meta = applyConfig({
allowedMethods: ["POST"], allowedMethods: ["POST"],
@ -25,29 +32,42 @@ export default async (
): Promise<Response> => { ): Promise<Response> => {
const id = matchedRoute.params.id; const id = matchedRoute.params.id;
const { user: self } = await UserAction.getFromRequest(req); const { user: self } = await getFromRequest(req);
if (!self) return errorResponse("Unauthorized", 401); if (!self) return errorResponse("Unauthorized", 401);
const user = await UserAction.findOne({ const user = await client.user.findUnique({
where: { where: { id },
id, include: {
relationships: {
include: {
owner: true,
subject: true,
},
},
}, },
relations: userRelations,
}); });
if (!user) return errorResponse("User not found", 404); if (!user) return errorResponse("User not found", 404);
// Check if already following // Check if already following
let relationship = await self.getRelationshipToOtherUser(user); let relationship = await getRelationshipToOtherUser(self, user);
if (!relationship) { if (!relationship) {
// Create new relationship // Create new relationship
const newRelationship = await Relationship.createNew(self, user); const newRelationship = await createNewRelationship(self, user);
self.relationships.push(newRelationship); await client.user.update({
await self.save(); where: { id: self.id },
data: {
relationships: {
connect: {
id: newRelationship.id,
},
},
},
});
relationship = newRelationship; relationship = newRelationship;
} }
@ -56,6 +76,12 @@ export default async (
relationship.endorsed = true; relationship.endorsed = true;
} }
await relationship.save(); await client.relationship.update({
return jsonResponse(await relationship.toAPI()); where: { id: relationship.id },
data: {
endorsed: true,
},
});
return jsonResponse(await relationshipToAPI(relationship));
}; };

View file

@ -1,8 +1,15 @@
import { errorResponse, jsonResponse } from "@response"; import { errorResponse, jsonResponse } from "@response";
import { MatchedRoute } from "bun"; import { MatchedRoute } from "bun";
import { Relationship } from "~database/entities/Relationship"; import {
import { UserAction, userRelations } from "~database/entities/User"; createNewRelationship,
relationshipToAPI,
} from "~database/entities/Relationship";
import {
getFromRequest,
getRelationshipToOtherUser,
} from "~database/entities/User";
import { applyConfig } from "@api"; import { applyConfig } from "@api";
import { client } from "~database/datasource";
export const meta = applyConfig({ export const meta = applyConfig({
allowedMethods: ["POST"], allowedMethods: ["POST"],
@ -25,37 +32,71 @@ export default async (
): Promise<Response> => { ): Promise<Response> => {
const id = matchedRoute.params.id; const id = matchedRoute.params.id;
const { user: self } = await UserAction.getFromRequest(req); const { user: self } = await getFromRequest(req);
if (!self) return errorResponse("Unauthorized", 401); if (!self) return errorResponse("Unauthorized", 401);
const user = await UserAction.findOne({ const user = await client.user.findUnique({
where: { where: { id },
id, include: {
relationships: {
include: {
owner: true,
subject: true,
},
},
}, },
relations: userRelations,
}); });
if (!user) return errorResponse("User not found", 404); if (!user) return errorResponse("User not found", 404);
// Check if already following // Check if already following
let relationship = await self.getRelationshipToOtherUser(user); let relationship = await getRelationshipToOtherUser(self, user);
if (!relationship) { if (!relationship) {
// Create new relationship // Create new relationship
const newRelationship = await Relationship.createNew(self, user); const newRelationship = await createNewRelationship(self, user);
self.relationships.push(newRelationship); await client.user.update({
await self.save(); where: { id: self.id },
data: {
relationships: {
connect: {
id: newRelationship.id,
},
},
},
});
relationship = newRelationship; relationship = newRelationship;
} }
if (relationship.followed_by) { if (relationship.followedBy) {
relationship.followed_by = false; relationship.followedBy = false;
} }
await relationship.save(); await client.relationship.update({
return jsonResponse(await relationship.toAPI()); 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));
}; };

View file

@ -1,10 +1,10 @@
/* eslint-disable @typescript-eslint/no-unsafe-member-access */ /* eslint-disable @typescript-eslint/no-unsafe-member-access */
import { errorResponse, jsonResponse } from "@response"; import { errorResponse, jsonResponse } from "@response";
import { MatchedRoute } from "bun"; import { MatchedRoute } from "bun";
import { Status, statusAndUserRelations } from "~database/entities/Status"; import { statusAndUserRelations, statusToAPI } from "~database/entities/Status";
import { UserAction, userRelations } from "~database/entities/User"; import { userRelations } from "~database/entities/User";
import { applyConfig } from "@api"; import { applyConfig } from "@api";
import { FindManyOptions } from "typeorm"; import { client } from "~database/datasource";
export const meta = applyConfig({ export const meta = applyConfig({
allowedMethods: ["GET"], allowedMethods: ["GET"],
@ -47,79 +47,29 @@ export default async (
tagged?: string; tagged?: string;
} = matchedRoute.query; } = matchedRoute.query;
const user = await UserAction.findOne({ const user = await client.user.findUnique({
where: { where: { id },
id, include: userRelations,
},
relations: userRelations,
}); });
if (!user) return errorResponse("User not found", 404); if (!user) return errorResponse("User not found", 404);
// Get list of boosts for this status const objects = await client.status.findMany({
let query: FindManyOptions<Status> = {
where: { where: {
account: { authorId: id,
id: user.id,
},
isReblog: exclude_reblogs ? true : undefined, isReblog: exclude_reblogs ? true : undefined,
id: {
lt: max_id,
gt: min_id,
gte: since_id,
},
}, },
relations: statusAndUserRelations, include: statusAndUserRelations,
take: limit ?? 20, take: limit ?? 20,
order: { orderBy: {
id: "DESC", 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) // Constuct HTTP Link header (next and prev)
const linkHeader = []; const linkHeader = [];
@ -129,14 +79,13 @@ export default async (
`<${urlWithoutQuery}?max_id=${objects[0].id}&limit=${limit}>; rel="next"` `<${urlWithoutQuery}?max_id=${objects[0].id}&limit=${limit}>; rel="next"`
); );
linkHeader.push( linkHeader.push(
`<${urlWithoutQuery}?since_id=${ `<${urlWithoutQuery}?since_id=${objects.at(-1)
objects[objects.length - 1].id ?.id}&limit=${limit}>; rel="prev"`
}&limit=${limit}>; rel="prev"`
); );
} }
return jsonResponse( return jsonResponse(
await Promise.all(objects.map(async status => await status.toAPI())), await Promise.all(objects.map(status => statusToAPI(status))),
200, 200,
{ {
Link: linkHeader.join(", "), Link: linkHeader.join(", "),

View file

@ -1,8 +1,15 @@
import { errorResponse, jsonResponse } from "@response"; import { errorResponse, jsonResponse } from "@response";
import { MatchedRoute } from "bun"; import { MatchedRoute } from "bun";
import { Relationship } from "~database/entities/Relationship"; import {
import { UserAction, userRelations } from "~database/entities/User"; createNewRelationship,
relationshipToAPI,
} from "~database/entities/Relationship";
import {
getFromRequest,
getRelationshipToOtherUser,
} from "~database/entities/User";
import { applyConfig } from "@api"; import { applyConfig } from "@api";
import { client } from "~database/datasource";
export const meta = applyConfig({ export const meta = applyConfig({
allowedMethods: ["POST"], allowedMethods: ["POST"],
@ -25,29 +32,42 @@ export default async (
): Promise<Response> => { ): Promise<Response> => {
const id = matchedRoute.params.id; const id = matchedRoute.params.id;
const { user: self } = await UserAction.getFromRequest(req); const { user: self } = await getFromRequest(req);
if (!self) return errorResponse("Unauthorized", 401); if (!self) return errorResponse("Unauthorized", 401);
const user = await UserAction.findOne({ const user = await client.user.findUnique({
where: { where: { id },
id, include: {
relationships: {
include: {
owner: true,
subject: true,
},
},
}, },
relations: userRelations,
}); });
if (!user) return errorResponse("User not found", 404); if (!user) return errorResponse("User not found", 404);
// Check if already following // Check if already following
let relationship = await self.getRelationshipToOtherUser(user); let relationship = await getRelationshipToOtherUser(self, user);
if (!relationship) { if (!relationship) {
// Create new relationship // Create new relationship
const newRelationship = await Relationship.createNew(self, user); const newRelationship = await createNewRelationship(self, user);
self.relationships.push(newRelationship); await client.user.update({
await self.save(); where: { id: self.id },
data: {
relationships: {
connect: {
id: newRelationship.id,
},
},
},
});
relationship = newRelationship; relationship = newRelationship;
} }
@ -56,6 +76,12 @@ export default async (
relationship.blocking = false; relationship.blocking = false;
} }
await relationship.save(); await client.relationship.update({
return jsonResponse(await relationship.toAPI()); where: { id: relationship.id },
data: {
blocking: false,
},
});
return jsonResponse(await relationshipToAPI(relationship));
}; };

View file

@ -1,8 +1,15 @@
import { errorResponse, jsonResponse } from "@response"; import { errorResponse, jsonResponse } from "@response";
import { MatchedRoute } from "bun"; import { MatchedRoute } from "bun";
import { Relationship } from "~database/entities/Relationship"; import {
import { UserAction, userRelations } from "~database/entities/User"; createNewRelationship,
relationshipToAPI,
} from "~database/entities/Relationship";
import {
getFromRequest,
getRelationshipToOtherUser,
} from "~database/entities/User";
import { applyConfig } from "@api"; import { applyConfig } from "@api";
import { client } from "~database/datasource";
export const meta = applyConfig({ export const meta = applyConfig({
allowedMethods: ["POST"], allowedMethods: ["POST"],
@ -25,29 +32,42 @@ export default async (
): Promise<Response> => { ): Promise<Response> => {
const id = matchedRoute.params.id; const id = matchedRoute.params.id;
const { user: self } = await UserAction.getFromRequest(req); const { user: self } = await getFromRequest(req);
if (!self) return errorResponse("Unauthorized", 401); if (!self) return errorResponse("Unauthorized", 401);
const user = await UserAction.findOne({ const user = await client.user.findUnique({
where: { where: { id },
id, include: {
relationships: {
include: {
owner: true,
subject: true,
},
},
}, },
relations: userRelations,
}); });
if (!user) return errorResponse("User not found", 404); if (!user) return errorResponse("User not found", 404);
// Check if already following // Check if already following
let relationship = await self.getRelationshipToOtherUser(user); let relationship = await getRelationshipToOtherUser(self, user);
if (!relationship) { if (!relationship) {
// Create new relationship // Create new relationship
const newRelationship = await Relationship.createNew(self, user); const newRelationship = await createNewRelationship(self, user);
self.relationships.push(newRelationship); await client.user.update({
await self.save(); where: { id: self.id },
data: {
relationships: {
connect: {
id: newRelationship.id,
},
},
},
});
relationship = newRelationship; relationship = newRelationship;
} }
@ -56,6 +76,12 @@ export default async (
relationship.following = false; relationship.following = false;
} }
await relationship.save(); await client.relationship.update({
return jsonResponse(await relationship.toAPI()); where: { id: relationship.id },
data: {
following: false,
},
});
return jsonResponse(await relationshipToAPI(relationship));
}; };

View file

@ -1,8 +1,15 @@
import { errorResponse, jsonResponse } from "@response"; import { errorResponse, jsonResponse } from "@response";
import { MatchedRoute } from "bun"; import { MatchedRoute } from "bun";
import { Relationship } from "~database/entities/Relationship"; import {
import { UserAction, userRelations } from "~database/entities/User"; createNewRelationship,
relationshipToAPI,
} from "~database/entities/Relationship";
import {
getFromRequest,
getRelationshipToOtherUser,
} from "~database/entities/User";
import { applyConfig } from "@api"; import { applyConfig } from "@api";
import { client } from "~database/datasource";
export const meta = applyConfig({ export const meta = applyConfig({
allowedMethods: ["POST"], allowedMethods: ["POST"],
@ -25,29 +32,42 @@ export default async (
): Promise<Response> => { ): Promise<Response> => {
const id = matchedRoute.params.id; const id = matchedRoute.params.id;
const { user: self } = await UserAction.getFromRequest(req); const { user: self } = await getFromRequest(req);
if (!self) return errorResponse("Unauthorized", 401); if (!self) return errorResponse("Unauthorized", 401);
const user = await UserAction.findOne({ const user = await client.user.findUnique({
where: { where: { id },
id, include: {
relationships: {
include: {
owner: true,
subject: true,
},
},
}, },
relations: userRelations,
}); });
if (!user) return errorResponse("User not found", 404); if (!user) return errorResponse("User not found", 404);
// Check if already following // Check if already following
let relationship = await self.getRelationshipToOtherUser(user); let relationship = await getRelationshipToOtherUser(self, user);
if (!relationship) { if (!relationship) {
// Create new relationship // Create new relationship
const newRelationship = await Relationship.createNew(self, user); const newRelationship = await createNewRelationship(self, user);
self.relationships.push(newRelationship); await client.user.update({
await self.save(); where: { id: self.id },
data: {
relationships: {
connect: {
id: newRelationship.id,
},
},
},
});
relationship = newRelationship; relationship = newRelationship;
} }
@ -58,6 +78,12 @@ export default async (
// TODO: Implement duration // TODO: Implement duration
await relationship.save(); await client.relationship.update({
return jsonResponse(await relationship.toAPI()); where: { id: relationship.id },
data: {
muting: false,
},
});
return jsonResponse(await relationshipToAPI(relationship));
}; };

View file

@ -1,8 +1,15 @@
import { errorResponse, jsonResponse } from "@response"; import { errorResponse, jsonResponse } from "@response";
import { MatchedRoute } from "bun"; import { MatchedRoute } from "bun";
import { Relationship } from "~database/entities/Relationship"; import {
import { UserAction, userRelations } from "~database/entities/User"; createNewRelationship,
relationshipToAPI,
} from "~database/entities/Relationship";
import {
getFromRequest,
getRelationshipToOtherUser,
} from "~database/entities/User";
import { applyConfig } from "@api"; import { applyConfig } from "@api";
import { client } from "~database/datasource";
export const meta = applyConfig({ export const meta = applyConfig({
allowedMethods: ["POST"], allowedMethods: ["POST"],
@ -25,29 +32,42 @@ export default async (
): Promise<Response> => { ): Promise<Response> => {
const id = matchedRoute.params.id; const id = matchedRoute.params.id;
const { user: self } = await UserAction.getFromRequest(req); const { user: self } = await getFromRequest(req);
if (!self) return errorResponse("Unauthorized", 401); if (!self) return errorResponse("Unauthorized", 401);
const user = await UserAction.findOne({ const user = await client.user.findUnique({
where: { where: { id },
id, include: {
relationships: {
include: {
owner: true,
subject: true,
},
},
}, },
relations: userRelations,
}); });
if (!user) return errorResponse("User not found", 404); if (!user) return errorResponse("User not found", 404);
// Check if already following // Check if already following
let relationship = await self.getRelationshipToOtherUser(user); let relationship = await getRelationshipToOtherUser(self, user);
if (!relationship) { if (!relationship) {
// Create new relationship // Create new relationship
const newRelationship = await Relationship.createNew(self, user); const newRelationship = await createNewRelationship(self, user);
self.relationships.push(newRelationship); await client.user.update({
await self.save(); where: { id: self.id },
data: {
relationships: {
connect: {
id: newRelationship.id,
},
},
},
});
relationship = newRelationship; relationship = newRelationship;
} }
@ -56,6 +76,12 @@ export default async (
relationship.endorsed = false; relationship.endorsed = false;
} }
await relationship.save(); await client.relationship.update({
return jsonResponse(await relationship.toAPI()); where: { id: relationship.id },
data: {
endorsed: false,
},
});
return jsonResponse(await relationshipToAPI(relationship));
}; };

View file

@ -1,14 +1,18 @@
import { parseRequest } from "@request"; import { parseRequest } from "@request";
import { errorResponse, jsonResponse } from "@response"; import { errorResponse, jsonResponse } from "@response";
import { UserAction } from "~database/entities/User"; import {
import { APIAccount } from "~types/entities/account"; getFromRequest,
userRelations,
userToAPI,
} from "~database/entities/User";
import { applyConfig } from "@api"; import { applyConfig } from "@api";
import { client } from "~database/datasource";
export const meta = applyConfig({ export const meta = applyConfig({
allowedMethods: ["GET"], allowedMethods: ["GET"],
route: "/api/v1/accounts/familiar_followers", route: "/api/v1/accounts/familiar_followers",
ratelimits: { ratelimits: {
max: 30, max: 5,
duration: 60, duration: 60,
}, },
auth: { auth: {
@ -20,7 +24,7 @@ export const meta = applyConfig({
* Find familiar followers (followers of a user that you also follow) * Find familiar followers (followers of a user that you also follow)
*/ */
export default async (req: Request): Promise<Response> => { export default async (req: Request): Promise<Response> => {
const { user: self } = await UserAction.getFromRequest(req); const { user: self } = await getFromRequest(req);
if (!self) return errorResponse("Unauthorized", 401); if (!self) return errorResponse("Unauthorized", 401);
@ -33,47 +37,34 @@ export default async (req: Request): Promise<Response> => {
return errorResponse("Number of ids must be between 1 and 10", 422); return errorResponse("Number of ids must be between 1 and 10", 422);
} }
const response = ( const followersOfIds = await client.user.findMany({
await Promise.all( where: {
ids.map(async id => { relationships: {
// Find followers of user that you also follow some: {
subjectId: {
// Get user in: ids,
const user = await UserAction.findOne({
where: { id },
relations: {
relationships: {
subject: {
relationships: true,
},
},
}, },
}); 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 return jsonResponse(output.map(o => userToAPI(o)));
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);
}; };

View file

@ -2,8 +2,10 @@ import { getConfig } from "@config";
import { parseRequest } from "@request"; import { parseRequest } from "@request";
import { jsonResponse } from "@response"; import { jsonResponse } from "@response";
import { tempmailDomains } from "@tempmail"; import { tempmailDomains } from "@tempmail";
import { UserAction } from "~database/entities/User";
import { applyConfig } from "@api"; 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({ export const meta = applyConfig({
allowedMethods: ["POST"], allowedMethods: ["POST"],
@ -115,7 +117,7 @@ export default async (req: Request): Promise<Response> => {
}); });
// Check if username is taken // 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({ errors.details.username.push({
error: "ERR_TAKEN", error: "ERR_TAKEN",
description: `is already taken`, description: `is already taken`,
@ -150,6 +152,18 @@ export default async (req: Request): Promise<Response> => {
description: `must be accepted`, 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 any errors are present, return them
if (Object.values(errors.details).some(value => value.length > 0)) { 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" // 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<Response> => {
}); });
} }
// TODO: Check if locale is valid await createNewLocalUser({
await UserAction.createNewLocal({
username: body.username ?? "", username: body.username ?? "",
password: body.password ?? "", password: body.password ?? "",
email: body.email ?? "", email: body.email ?? "",
}); });
// TODO: Return access token return new Response("", {
return new Response(); status: 200,
});
}; };

View file

@ -1,8 +1,12 @@
import { parseRequest } from "@request"; import { parseRequest } from "@request";
import { errorResponse, jsonResponse } from "@response"; import { errorResponse, jsonResponse } from "@response";
import { Relationship } from "~database/entities/Relationship"; import {
import { UserAction } from "~database/entities/User"; createNewRelationship,
relationshipToAPI,
} from "~database/entities/Relationship";
import { getFromRequest } from "~database/entities/User";
import { applyConfig } from "@api"; import { applyConfig } from "@api";
import { client } from "~database/datasource";
export const meta = applyConfig({ export const meta = applyConfig({
allowedMethods: ["GET"], allowedMethods: ["GET"],
@ -20,7 +24,7 @@ export const meta = applyConfig({
* Find relationships * Find relationships
*/ */
export default async (req: Request): Promise<Response> => { export default async (req: Request): Promise<Response> => {
const { user: self } = await UserAction.getFromRequest(req); const { user: self } = await getFromRequest(req);
if (!self) return errorResponse("Unauthorized", 401); if (!self) return errorResponse("Unauthorized", 401);
@ -33,34 +37,35 @@ export default async (req: Request): Promise<Response> => {
return errorResponse("Number of ids must be between 1 and 10", 422); return errorResponse("Number of ids must be between 1 and 10", 422);
} }
// Check if already following const relationships = await client.relationship.findMany({
// TODO: Limit ID amount where: {
const relationships = ( ownerId: self.id,
await Promise.all( subjectId: {
ids.map(async id => { in: ids,
const user = await UserAction.findOneBy({ id }); },
if (!user) return null; },
let relationship = await self.getRelationshipToOtherUser(user); });
if (!relationship) { // Find IDs that dont have a relationship
// Create new relationship const missingIds = ids.filter(
id => !relationships.some(r => r.subjectId === id)
);
const newRelationship = await Relationship.createNew( // Create the missing relationships
self, for (const id of missingIds) {
user const relationship = await createNewRelationship(self, { id } as any);
);
self.relationships.push(newRelationship); relationships.push(relationship);
await self.save(); }
relationship = newRelationship; // Order in the same order as ids
} relationships.sort(
return relationship; (a, b) => ids.indexOf(a.subjectId) - ids.indexOf(b.subjectId)
}) );
)
).filter(relationship => relationship !== null) as Relationship[];
return jsonResponse( return jsonResponse(
await Promise.all(relationships.map(async r => await r.toAPI())) await Promise.all(
relationships.map(async r => await relationshipToAPI(r))
)
); );
}; };

View file

@ -1,12 +1,14 @@
import { getConfig } from "@config"; import { getConfig } from "@config";
import { parseRequest } from "@request"; import { parseRequest } from "@request";
import { errorResponse, jsonResponse } from "@response"; import { errorResponse, jsonResponse } from "@response";
import { UserAction } from "~database/entities/User"; import { getFromRequest, userToAPI } from "~database/entities/User";
import { applyConfig } from "@api"; import { applyConfig } from "@api";
import { sanitize } from "isomorphic-dompurify"; import { sanitize } from "isomorphic-dompurify";
import { sanitizeHtml } from "@sanitization"; import { sanitizeHtml } from "@sanitization";
import { uploadFile } from "~classes/media"; 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({ export const meta = applyConfig({
allowedMethods: ["PATCH"], allowedMethods: ["PATCH"],
@ -24,7 +26,7 @@ export const meta = applyConfig({
* Patches a user * Patches a user
*/ */
export default async (req: Request): Promise<Response> => { export default async (req: Request): Promise<Response> => {
const { user } = await UserAction.getFromRequest(req); const { user } = await getFromRequest(req);
if (!user) return errorResponse("Unauthorized", 401); if (!user) return errorResponse("Unauthorized", 401);
@ -85,7 +87,7 @@ export default async (req: Request): Promise<Response> => {
// Remove emojis // Remove emojis
user.emojis = []; user.emojis = [];
user.display_name = sanitizedDisplayName; user.displayName = sanitizedDisplayName;
} }
if (note) { if (note) {
@ -112,7 +114,7 @@ export default async (req: Request): Promise<Response> => {
user.note = sanitizedNote; user.note = sanitizedNote;
} }
if (source_privacy) { if (source_privacy && user.source) {
// Check if within allowed privacy values // Check if within allowed privacy values
if ( if (
!["public", "unlisted", "private", "direct"].includes( !["public", "unlisted", "private", "direct"].includes(
@ -125,21 +127,30 @@ export default async (req: Request): Promise<Response> => {
); );
} }
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 // Check if within allowed sensitive values
if (source_sensitive !== "true" && source_sensitive !== "false") { if (source_sensitive !== "true" && source_sensitive !== "false") {
return errorResponse("Sensitive must be a boolean", 422); 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) { if (source_language && user.source) {
// TODO: Check if proper ISO code if (!ISO6391.validate(source_language)) {
user.source.language = 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) { if (avatar) {
@ -176,8 +187,7 @@ export default async (req: Request): Promise<Response> => {
return errorResponse("Locked must be a boolean", 422); return errorResponse("Locked must be a boolean", 422);
} }
// TODO: Add a user value for Locked user.isLocked = locked === "true";
// user.locked = locked === "true";
} }
if (bot) { if (bot) {
@ -186,8 +196,7 @@ export default async (req: Request): Promise<Response> => {
return errorResponse("Bot must be a boolean", 422); return errorResponse("Bot must be a boolean", 422);
} }
// TODO: Add a user value for bot user.isBot = bot === "true";
// user.bot = bot === "true";
} }
if (discoverable) { if (discoverable) {
@ -196,14 +205,13 @@ export default async (req: Request): Promise<Response> => {
return errorResponse("Discoverable must be a boolean", 422); return errorResponse("Discoverable must be a boolean", 422);
} }
// TODO: Add a user value for discoverable user.isDiscoverable = discoverable === "true";
// user.discoverable = discoverable === "true";
} }
// Parse emojis // Parse emojis
const displaynameEmojis = await EmojiAction.parseEmojis(sanitizedDisplayName); const displaynameEmojis = await parseEmojis(sanitizedDisplayName);
const noteEmojis = await EmojiAction.parseEmojis(sanitizedNote); const noteEmojis = await parseEmojis(sanitizedNote);
user.emojis = [...displaynameEmojis, ...noteEmojis]; user.emojis = [...displaynameEmojis, ...noteEmojis];
@ -212,7 +220,31 @@ export default async (req: Request): Promise<Response> => {
(emoji, index, self) => self.findIndex(e => e.id === emoji.id) === index (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));
}; };

View file

@ -1,5 +1,5 @@
import { errorResponse, jsonResponse } from "@response"; import { errorResponse, jsonResponse } from "@response";
import { UserAction } from "~database/entities/User"; import { getFromRequest, userToAPI } from "~database/entities/User";
import { applyConfig } from "@api"; import { applyConfig } from "@api";
export const meta = applyConfig({ export const meta = applyConfig({
@ -17,12 +17,12 @@ export const meta = applyConfig({
export default async (req: Request): Promise<Response> => { export default async (req: Request): Promise<Response> => {
// TODO: Add checks for disabled or not email verified accounts // 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); if (!user) return errorResponse("Unauthorized", 401);
return jsonResponse({ return jsonResponse({
...(await user.toAPI()), ...(await userToAPI(user)),
source: user.source, source: user.source,
// TODO: Add role support // TODO: Add role support
role: { role: {

View file

@ -2,7 +2,7 @@ import { applyConfig } from "@api";
import { parseRequest } from "@request"; import { parseRequest } from "@request";
import { errorResponse, jsonResponse } from "@response"; import { errorResponse, jsonResponse } from "@response";
import { randomBytes } from "crypto"; import { randomBytes } from "crypto";
import { ApplicationAction } from "~database/entities/Application"; import { client } from "~database/datasource";
export const meta = applyConfig({ export const meta = applyConfig({
allowedMethods: ["POST"], allowedMethods: ["POST"],
@ -27,10 +27,6 @@ export default async (req: Request): Promise<Response> => {
website: string; website: string;
}>(req); }>(req);
const application = new ApplicationAction();
application.name = client_name || "";
// Check if redirect URI is a valid URI, and also an absolute URI // Check if redirect URI is a valid URI, and also an absolute URI
if (redirect_uris) { if (redirect_uris) {
try { try {
@ -42,20 +38,20 @@ export default async (req: Request): Promise<Response> => {
422 422
); );
} }
application.redirect_uris = redirect_uris;
} catch { } catch {
return errorResponse("Redirect URI must be a valid URI", 422); return errorResponse("Redirect URI must be a valid URI", 422);
} }
} }
const application = await client.application.create({
application.scopes = scopes || "read"; data: {
application.website = website || null; name: client_name || "",
redirect_uris: redirect_uris || "",
application.client_id = randomBytes(32).toString("base64url"); scopes: scopes || "read",
application.secret = randomBytes(64).toString("base64url"); website: website || null,
client_id: randomBytes(32).toString("base64url"),
await application.save(); secret: randomBytes(64).toString("base64url"),
},
});
return jsonResponse({ return jsonResponse({
id: application.id, id: application.id,

View file

@ -1,7 +1,7 @@
import { applyConfig } from "@api"; import { applyConfig } from "@api";
import { errorResponse, jsonResponse } from "@response"; import { errorResponse, jsonResponse } from "@response";
import { ApplicationAction } from "~database/entities/Application"; import { getFromToken } from "~database/entities/Application";
import { UserAction } from "~database/entities/User"; import { getFromRequest } from "~database/entities/User";
export const meta = applyConfig({ export const meta = applyConfig({
allowedMethods: ["GET"], allowedMethods: ["GET"],
@ -19,8 +19,8 @@ export const meta = applyConfig({
* Returns OAuth2 credentials * Returns OAuth2 credentials
*/ */
export default async (req: Request): Promise<Response> => { export default async (req: Request): Promise<Response> => {
const { user, token } = await UserAction.getFromRequest(req); const { user, token } = await getFromRequest(req);
const application = await ApplicationAction.getFromToken(token); const application = await getFromToken(token);
if (!user) return errorResponse("Unauthorized", 401); if (!user) return errorResponse("Unauthorized", 401);
if (!application) return errorResponse("Unauthorized", 401); if (!application) return errorResponse("Unauthorized", 401);

View file

@ -1,7 +1,7 @@
import { applyConfig } from "@api"; import { applyConfig } from "@api";
import { jsonResponse } from "@response"; import { jsonResponse } from "@response";
import { IsNull } from "typeorm"; import { client } from "~database/datasource";
import { EmojiAction } from "~database/entities/Emoji"; import { emojiToAPI } from "~database/entities/Emoji";
export const meta = applyConfig({ export const meta = applyConfig({
allowedMethods: ["GET"], allowedMethods: ["GET"],
@ -20,11 +20,13 @@ export const meta = applyConfig({
*/ */
// eslint-disable-next-line @typescript-eslint/require-await // eslint-disable-next-line @typescript-eslint/require-await
export default async (): Promise<Response> => { export default async (): Promise<Response> => {
const emojis = await EmojiAction.findBy({ const emojis = await client.emoji.findMany({
instance: IsNull(), where: {
instanceId: null,
},
}); });
return jsonResponse( return jsonResponse(
await Promise.all(emojis.map(async emoji => await emoji.toAPI())) await Promise.all(emojis.map(emoji => emojiToAPI(emoji)))
); );
}; };

View file

@ -1,8 +1,7 @@
import { applyConfig } from "@api"; import { applyConfig } from "@api";
import { getConfig } from "@config"; import { getConfig } from "@config";
import { jsonResponse } from "@response"; import { jsonResponse } from "@response";
import { Status } from "~database/entities/Status"; import { client } from "~database/datasource";
import { UserAction } from "~database/entities/User";
export const meta = applyConfig({ export const meta = applyConfig({
allowedMethods: ["GET"], allowedMethods: ["GET"],
@ -23,8 +22,16 @@ export const meta = applyConfig({
export default async (): Promise<Response> => { export default async (): Promise<Response> => {
const config = getConfig(); const config = getConfig();
const statusCount = await Status.count(); const statusCount = await client.status.count({
const userCount = await UserAction.count(); where: {
instanceId: null,
},
});
const userCount = await client.user.count({
where: {
instanceId: null,
},
});
// TODO: fill in more values // TODO: fill in more values
return jsonResponse({ return jsonResponse({

View file

@ -1,14 +1,20 @@
import { applyConfig } from "@api"; import { applyConfig } from "@api";
import { errorResponse, jsonResponse } from "@response"; import { errorResponse, jsonResponse } from "@response";
import { MatchedRoute } from "bun"; import { MatchedRoute } from "bun";
import { Status, statusAndUserRelations } from "~database/entities/Status"; import { client } from "~database/datasource";
import { UserAction } from "~database/entities/User"; import {
getAncestors,
getDescendants,
statusAndUserRelations,
statusToAPI,
} from "~database/entities/Status";
import { getFromRequest } from "~database/entities/User";
import { APIRouteMeta } from "~types/api"; import { APIRouteMeta } from "~types/api";
export const meta: APIRouteMeta = applyConfig({ export const meta: APIRouteMeta = applyConfig({
allowedMethods: ["GET"], allowedMethods: ["GET"],
ratelimits: { ratelimits: {
max: 100, max: 8,
duration: 60, duration: 60,
}, },
route: "/api/v1/statuses/:id/context", 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. // User token + read:statuses for up to 4,096 ancestors, 4,096 descendants, unlimited depth, and private statuses.
const id = matchedRoute.params.id; const id = matchedRoute.params.id;
const { user } = await UserAction.getFromRequest(req); const { user } = await getFromRequest(req);
let foundStatus: Status | null; const foundStatus = await client.status.findUnique({
try { where: { id },
foundStatus = await Status.findOne({ include: statusAndUserRelations,
where: { });
id,
},
relations: statusAndUserRelations,
});
} catch (e) {
return errorResponse("Invalid ID", 404);
}
if (!foundStatus) return errorResponse("Record not found", 404); if (!foundStatus) return errorResponse("Record not found", 404);
// Get all ancestors // Get all ancestors
const ancestors = await foundStatus.getAncestors(user); const ancestors = await getAncestors(foundStatus, user);
const descendants = await foundStatus.getDescendants(user); const descendants = await getDescendants(foundStatus, user);
return jsonResponse({ 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: await Promise.all(
descendants.map(status => status.toAPI()) descendants.map(status => statusToAPI(status))
), ),
}); });
}; };

View file

@ -2,10 +2,15 @@
import { applyConfig } from "@api"; import { applyConfig } from "@api";
import { errorResponse, jsonResponse } from "@response"; import { errorResponse, jsonResponse } from "@response";
import { MatchedRoute } from "bun"; import { MatchedRoute } from "bun";
import { Like } from "~database/entities/Like"; import { client } from "~database/datasource";
import { Status, statusAndUserRelations } from "~database/entities/Status"; import {
import { UserAction, userRelations } from "~database/entities/User"; isViewableByUser,
statusAndUserRelations,
statusToAPI,
} from "~database/entities/Status";
import { getFromRequest } from "~database/entities/User";
import { APIRouteMeta } from "~types/api"; import { APIRouteMeta } from "~types/api";
import { APIStatus } from "~types/entities/status";
export const meta: APIRouteMeta = applyConfig({ export const meta: APIRouteMeta = applyConfig({
allowedMethods: ["POST"], allowedMethods: ["POST"],
@ -28,51 +33,38 @@ export default async (
): Promise<Response> => { ): Promise<Response> => {
const id = matchedRoute.params.id; const id = matchedRoute.params.id;
const { user } = await UserAction.getFromRequest(req); const { user } = await getFromRequest(req);
if (!user) return errorResponse("Unauthorized", 401); if (!user) return errorResponse("Unauthorized", 401);
let foundStatus: Status | null; const status = await client.status.findUnique({
try { where: { id },
foundStatus = await Status.findOne({ include: statusAndUserRelations,
where: { });
id,
},
relations: statusAndUserRelations,
});
} catch (e) {
return errorResponse("Invalid ID", 404);
}
if (!foundStatus) return errorResponse("Record not found", 404);
// Check if user is authorized to view this status (if it's private) // 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); return errorResponse("Record not found", 404);
}
// Check if user has already favourited this status const existingLike = await client.like.findFirst({
const existingLike = await Like.findOne({
where: { where: {
liked: { likedId: status.id,
id: foundStatus.id, likerId: user.id,
},
liker: {
id: user.id,
},
}, },
relations: [
...userRelations.map(r => `liker.${r}`),
...statusAndUserRelations.map(r => `liked.${r}`),
],
}); });
if (!existingLike) { if (!existingLike) {
const like = new Like(); await client.like.create({
like.liker = user; data: {
like.liked = foundStatus; likedId: status.id,
await like.save(); likerId: user.id,
},
});
} }
return jsonResponse(await foundStatus.toAPI()); return jsonResponse({
...(await statusToAPI(status, user)),
favourited: true,
favourites_count: status._count.likes + 1,
} as APIStatus);
}; };

View file

@ -3,10 +3,16 @@ import { applyConfig } from "@api";
import { parseRequest } from "@request"; import { parseRequest } from "@request";
import { errorResponse, jsonResponse } from "@response"; import { errorResponse, jsonResponse } from "@response";
import { MatchedRoute } from "bun"; import { MatchedRoute } from "bun";
import { FindManyOptions } from "typeorm"; import { client } from "~database/datasource";
import { Like } from "~database/entities/Like"; import {
import { Status, statusAndUserRelations } from "~database/entities/Status"; isViewableByUser,
import { UserAction, userRelations } from "~database/entities/User"; statusAndUserRelations,
} from "~database/entities/Status";
import {
getFromRequest,
userRelations,
userToAPI,
} from "~database/entities/User";
import { APIRouteMeta } from "~types/api"; import { APIRouteMeta } from "~types/api";
export const meta: APIRouteMeta = applyConfig({ export const meta: APIRouteMeta = applyConfig({
@ -30,33 +36,25 @@ export default async (
): Promise<Response> => { ): Promise<Response> => {
const id = matchedRoute.params.id; const id = matchedRoute.params.id;
const { user } = await UserAction.getFromRequest(req); const { user } = await getFromRequest(req);
let foundStatus: Status | null; const status = await client.status.findUnique({
try { where: { id },
foundStatus = await Status.findOne({ include: statusAndUserRelations,
where: { });
id,
},
relations: statusAndUserRelations,
});
} catch (e) {
return errorResponse("Invalid ID", 404);
}
if (!foundStatus) return errorResponse("Record not found", 404);
// Check if user is authorized to view this status (if it's private) // 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); return errorResponse("Record not found", 404);
}
const { const {
max_id = null, max_id = null,
min_id = null,
since_id = null, since_id = null,
limit = 40, limit = 40,
} = await parseRequest<{ } = await parseRequest<{
max_id?: string; max_id?: string;
min_id?: string;
since_id?: string; since_id?: string;
limit?: number; limit?: number;
}>(req); }>(req);
@ -65,53 +63,32 @@ export default async (
if (limit > 80) return errorResponse("Invalid limit (maximum is 80)", 400); if (limit > 80) return errorResponse("Invalid limit (maximum is 80)", 400);
if (limit < 1) return errorResponse("Invalid limit", 400); if (limit < 1) return errorResponse("Invalid limit", 400);
// Get list of boosts for this status const objects = await client.user.findMany({
let query: FindManyOptions<Like> = {
where: { where: {
liked: { likes: {
id, some: {
likedId: status.id,
},
},
id: {
lt: max_id ?? undefined,
gte: since_id ?? undefined,
gt: min_id ?? undefined,
}, },
}, },
relations: userRelations.map(r => `liker.${r}`), include: {
take: limit, ...userRelations,
order: { likes: {
id: "DESC", where: {
likedId: status.id,
},
},
}, },
}; take: limit,
orderBy: {
if (max_id) { id: "desc",
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);
// Constuct HTTP Link header (next and prev) // Constuct HTTP Link header (next and prev)
const linkHeader = []; const linkHeader = [];
@ -128,7 +105,7 @@ export default async (
} }
return jsonResponse( return jsonResponse(
await Promise.all(objects.map(async like => await like.liker.toAPI())), await Promise.all(objects.map(async user => userToAPI(user))),
200, 200,
{ {
Link: linkHeader.join(", "), Link: linkHeader.join(", "),

View file

@ -1,8 +1,13 @@
import { applyConfig } from "@api"; import { applyConfig } from "@api";
import { errorResponse, jsonResponse } from "@response"; import { errorResponse, jsonResponse } from "@response";
import { MatchedRoute } from "bun"; import { MatchedRoute } from "bun";
import { Status, statusAndUserRelations } from "~database/entities/Status"; import { client } from "~database/datasource";
import { UserAction } from "~database/entities/User"; import {
isViewableByUser,
statusAndUserRelations,
statusToAPI,
} from "~database/entities/Status";
import { getFromRequest } from "~database/entities/User";
import { APIRouteMeta } from "~types/api"; import { APIRouteMeta } from "~types/api";
export const meta: APIRouteMeta = applyConfig({ export const meta: APIRouteMeta = applyConfig({
@ -27,31 +32,21 @@ export default async (
): Promise<Response> => { ): Promise<Response> => {
const id = matchedRoute.params.id; const id = matchedRoute.params.id;
const { user } = await UserAction.getFromRequest(req); const { user } = await getFromRequest(req);
let foundStatus: Status | null; const status = await client.status.findUnique({
try { where: { id },
foundStatus = await Status.findOne({ include: statusAndUserRelations,
where: { });
id,
},
relations: statusAndUserRelations,
});
} catch (e) {
return errorResponse("Invalid ID", 404);
}
if (!foundStatus) return errorResponse("Record not found", 404);
// Check if user is authorized to view this status (if it's private) // 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); return errorResponse("Record not found", 404);
}
if (req.method === "GET") { if (req.method === "GET") {
return jsonResponse(await foundStatus.toAPI()); return jsonResponse(await statusToAPI(status));
} else if (req.method === "DELETE") { } else if (req.method === "DELETE") {
if (foundStatus.account.id !== user?.id) { if (status.authorId !== user?.id) {
return errorResponse("Unauthorized", 401); return errorResponse("Unauthorized", 401);
} }
@ -60,11 +55,13 @@ export default async (
// Get associated Status object // Get associated Status object
// Delete status and all associated objects // Delete status and all associated objects
await foundStatus.remove(); await client.status.delete({
where: { id },
});
return jsonResponse( return jsonResponse(
{ {
...(await foundStatus.toAPI()), ...(await statusToAPI(status)),
// TODO: Add // TODO: Add
// text: Add source text // text: Add source text
// poll: Add source poll // poll: Add source poll

View file

@ -3,9 +3,16 @@ import { applyConfig } from "@api";
import { parseRequest } from "@request"; import { parseRequest } from "@request";
import { errorResponse, jsonResponse } from "@response"; import { errorResponse, jsonResponse } from "@response";
import { MatchedRoute } from "bun"; import { MatchedRoute } from "bun";
import { FindManyOptions } from "typeorm"; import { client } from "~database/datasource";
import { Status, statusAndUserRelations } from "~database/entities/Status"; import {
import { UserAction } from "~database/entities/User"; isViewableByUser,
statusAndUserRelations,
} from "~database/entities/Status";
import {
getFromRequest,
userRelations,
userToAPI,
} from "~database/entities/User";
import { APIRouteMeta } from "~types/api"; import { APIRouteMeta } from "~types/api";
export const meta: APIRouteMeta = applyConfig({ export const meta: APIRouteMeta = applyConfig({
@ -29,33 +36,25 @@ export default async (
): Promise<Response> => { ): Promise<Response> => {
const id = matchedRoute.params.id; const id = matchedRoute.params.id;
const { user } = await UserAction.getFromRequest(req); const { user } = await getFromRequest(req);
let foundStatus: Status | null; const status = await client.status.findUnique({
try { where: { id },
foundStatus = await Status.findOne({ include: statusAndUserRelations,
where: { });
id,
},
relations: statusAndUserRelations,
});
} catch (e) {
return errorResponse("Invalid ID", 404);
}
if (!foundStatus) return errorResponse("Record not found", 404);
// Check if user is authorized to view this status (if it's private) // 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); return errorResponse("Record not found", 404);
}
const { const {
max_id = null, max_id = null,
min_id = null,
since_id = null, since_id = null,
limit = 40, limit = 40,
} = await parseRequest<{ } = await parseRequest<{
max_id?: string; max_id?: string;
min_id?: string;
since_id?: string; since_id?: string;
limit?: number; limit?: number;
}>(req); }>(req);
@ -64,53 +63,33 @@ export default async (
if (limit > 80) return errorResponse("Invalid limit (maximum is 80)", 400); if (limit > 80) return errorResponse("Invalid limit (maximum is 80)", 400);
if (limit < 1) return errorResponse("Invalid limit", 400); if (limit < 1) return errorResponse("Invalid limit", 400);
// Get list of boosts for this status const objects = await client.user.findMany({
let query: FindManyOptions<Status> = {
where: { where: {
reblog: { statuses: {
id, some: {
reblogId: status.id,
},
},
id: {
lt: max_id ?? undefined,
gte: since_id ?? undefined,
gt: min_id ?? undefined,
}, },
}, },
relations: statusAndUserRelations, include: {
take: limit, ...userRelations,
order: { statuses: {
id: "DESC", where: {
reblogId: status.id,
},
include: statusAndUserRelations,
},
}, },
}; take: limit,
orderBy: {
if (max_id) { id: "desc",
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);
// Constuct HTTP Link header (next and prev) // Constuct HTTP Link header (next and prev)
const linkHeader = []; const linkHeader = [];
@ -127,7 +106,7 @@ export default async (
} }
return jsonResponse( return jsonResponse(
await Promise.all(objects.map(async object => await object.toAPI())), await Promise.all(objects.map(async user => userToAPI(user))),
200, 200,
{ {
Link: linkHeader.join(", "), Link: linkHeader.join(", "),

View file

@ -2,10 +2,15 @@
import { applyConfig } from "@api"; import { applyConfig } from "@api";
import { errorResponse, jsonResponse } from "@response"; import { errorResponse, jsonResponse } from "@response";
import { MatchedRoute } from "bun"; import { MatchedRoute } from "bun";
import { Like } from "~database/entities/Like"; import { client } from "~database/datasource";
import { Status, statusAndUserRelations } from "~database/entities/Status"; import {
import { UserAction } from "~database/entities/User"; isViewableByUser,
statusAndUserRelations,
statusToAPI,
} from "~database/entities/Status";
import { getFromRequest } from "~database/entities/User";
import { APIRouteMeta } from "~types/api"; import { APIRouteMeta } from "~types/api";
import { APIStatus } from "~types/entities/status";
export const meta: APIRouteMeta = applyConfig({ export const meta: APIRouteMeta = applyConfig({
allowedMethods: ["POST"], allowedMethods: ["POST"],
@ -28,37 +33,29 @@ export default async (
): Promise<Response> => { ): Promise<Response> => {
const id = matchedRoute.params.id; const id = matchedRoute.params.id;
const { user } = await UserAction.getFromRequest(req); const { user } = await getFromRequest(req);
if (!user) return errorResponse("Unauthorized", 401); if (!user) return errorResponse("Unauthorized", 401);
let foundStatus: Status | null; const status = await client.status.findUnique({
try { where: { id },
foundStatus = await Status.findOne({ include: statusAndUserRelations,
where: { });
id,
},
relations: statusAndUserRelations,
});
} catch (e) {
return errorResponse("Invalid ID", 404);
}
if (!foundStatus) return errorResponse("Record not found", 404);
// Check if user is authorized to view this status (if it's private) // 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); return errorResponse("Record not found", 404);
}
await Like.delete({ await client.like.deleteMany({
liked: { where: {
id: foundStatus.id, likedId: status.id,
}, likerId: user.id,
liker: {
id: user.id,
}, },
}); });
return jsonResponse(await foundStatus.toAPI()); return jsonResponse({
...(await statusToAPI(status, user)),
favourited: false,
favourites_count: status._count.likes - 1,
} as APIStatus);
}; };

View file

@ -8,9 +8,15 @@ import { errorResponse, jsonResponse } from "@response";
import { sanitizeHtml } from "@sanitization"; import { sanitizeHtml } from "@sanitization";
import { MatchedRoute } from "bun"; import { MatchedRoute } from "bun";
import { parse } from "marked"; import { parse } from "marked";
import { ApplicationAction } from "~database/entities/Application"; import { client } from "~database/datasource";
import { Status, statusRelations } from "~database/entities/Status"; import { getFromToken } from "~database/entities/Application";
import { AuthData, UserAction } from "~database/entities/User"; import {
StatusWithRelations,
createNewStatus,
statusAndUserRelations,
statusToAPI,
} from "~database/entities/Status";
import { AuthData, UserWithRelations } from "~database/entities/User";
import { APIRouteMeta } from "~types/api"; import { APIRouteMeta } from "~types/api";
export const meta: APIRouteMeta = applyConfig({ export const meta: APIRouteMeta = applyConfig({
@ -34,7 +40,7 @@ export default async (
authData: AuthData authData: AuthData
): Promise<Response> => { ): Promise<Response> => {
const { user, token } = authData; const { user, token } = authData;
const application = await ApplicationAction.getFromToken(token); const application = await getFromToken(token);
if (!user) return errorResponse("Unauthorized", 401); if (!user) return errorResponse("Unauthorized", 401);
@ -126,18 +132,16 @@ export default async (
} }
// Get reply account and status if exists // Get reply account and status if exists
let replyStatus: Status | null = null; let replyStatus: StatusWithRelations | null = null;
let replyUser: UserAction | null = null; let replyUser: UserWithRelations | null = null;
if (in_reply_to_id) { if (in_reply_to_id) {
replyStatus = await Status.findOne({ replyStatus = await client.status.findUnique({
where: { where: { id: in_reply_to_id },
id: in_reply_to_id, include: statusAndUserRelations,
},
relations: statusRelations,
}); });
replyUser = replyStatus?.account || null; replyUser = replyStatus?.author || null;
} }
// Check if status body doesnt match filters // Check if status body doesnt match filters
@ -145,8 +149,7 @@ export default async (
return errorResponse("Status contains blocked words", 422); return errorResponse("Status contains blocked words", 422);
} }
// Create status const newStatus = await createNewStatus({
const newStatus = await Status.createNew({
account: user, account: user,
application, application,
content: sanitizedStatus, content: sanitizedStatus,
@ -171,5 +174,5 @@ export default async (
// TODO: add database jobs to deliver the post // TODO: add database jobs to deliver the post
return jsonResponse(await newStatus.toAPI()); return jsonResponse(await statusToAPI(newStatus, user));
}; };

View file

@ -2,10 +2,9 @@
import { applyConfig } from "@api"; import { applyConfig } from "@api";
import { parseRequest } from "@request"; import { parseRequest } from "@request";
import { errorResponse, jsonResponse } from "@response"; import { errorResponse, jsonResponse } from "@response";
import { MatchedRoute } from "bun"; import { client } from "~database/datasource";
import { FindManyOptions } from "typeorm"; import { statusAndUserRelations, statusToAPI } from "~database/entities/Status";
import { Status, statusAndUserRelations } from "~database/entities/Status"; import { getFromRequest } from "~database/entities/User";
import { AuthData } from "~database/entities/User";
import { APIRouteMeta } from "~types/api"; import { APIRouteMeta } from "~types/api";
export const meta: APIRouteMeta = applyConfig({ export const meta: APIRouteMeta = applyConfig({
@ -23,11 +22,9 @@ export const meta: APIRouteMeta = applyConfig({
/** /**
* Fetch home timeline statuses * Fetch home timeline statuses
*/ */
export default async ( export default async (req: Request): Promise<Response> => {
req: Request, const { user } = await getFromRequest(req);
matchedRoute: MatchedRoute,
authData: AuthData
): Promise<Response> => {
const { const {
limit = 20, limit = 20,
max_id, max_id,
@ -40,85 +37,54 @@ export default async (
limit?: number; limit?: number;
}>(req); }>(req);
const { user } = authData;
if (limit < 1 || limit > 40) { if (limit < 1 || limit > 40) {
return errorResponse("Limit must be between 1 and 40", 400); return errorResponse("Limit must be between 1 and 40", 400);
} }
let query: FindManyOptions<Status> = { if (!user) return errorResponse("Unauthorized", 401);
const objects = await client.status.findMany({
where: { where: {
visibility: "public", id: {
account: [ lt: max_id ?? undefined,
{ gte: since_id ?? undefined,
relationships: { gt: min_id ?? undefined,
id: user?.id, },
followed_by: true, author: {
relationships: {
some: {
subjectId: user.id,
following: true,
}, },
}, },
{ },
id: user?.id,
},
],
},
order: {
created_at: "DESC",
}, },
include: statusAndUserRelations,
take: limit, take: limit,
relations: statusAndUserRelations, orderBy: {
}; id: "desc",
},
});
if (max_id) { // Constuct HTTP Link header (next and prev)
const maxPost = await Status.findOneBy({ id: max_id }); const linkHeader = [];
if (maxPost) { if (objects.length > 0) {
query = { const urlWithoutQuery = req.url.split("?")[0];
...query, linkHeader.push(
where: { `<${urlWithoutQuery}?max_id=${objects[0].id}&limit=${limit}>; rel="next"`
...query.where, );
created_at: { linkHeader.push(
...(query.where as any)?.created_at, `<${urlWithoutQuery}?since_id=${
$lt: maxPost.created_at, 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( 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(", "),
}
); );
}; };

View file

@ -1,8 +1,8 @@
import { applyConfig } from "@api"; import { applyConfig } from "@api";
import { parseRequest } from "@request"; import { parseRequest } from "@request";
import { errorResponse, jsonResponse } from "@response"; import { errorResponse, jsonResponse } from "@response";
import { FindManyOptions, IsNull, Not } from "typeorm"; import { client } from "~database/datasource";
import { Status, statusAndUserRelations } from "~database/entities/Status"; import { statusAndUserRelations, statusToAPI } from "~database/entities/Status";
import { APIRouteMeta } from "~types/api"; import { APIRouteMeta } from "~types/api";
export const meta: APIRouteMeta = applyConfig({ export const meta: APIRouteMeta = applyConfig({
@ -17,36 +17,13 @@ export const meta: APIRouteMeta = applyConfig({
}, },
}); });
const updateQuery = async (
id: string | undefined,
operator: string,
query: FindManyOptions<Status>
) => {
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<Response> => { export default async (req: Request): Promise<Response> => {
const { const {
local, local,
limit = 20, limit = 20,
max_id, max_id,
min_id, min_id,
only_media, // only_media,
remote, remote,
since_id, since_id,
} = await parseRequest<{ } = await parseRequest<{
@ -67,48 +44,47 @@ export default async (req: Request): Promise<Response> => {
return errorResponse("Cannot use both local and remote", 400); return errorResponse("Cannot use both local and remote", 400);
} }
let query: FindManyOptions<Status> = { const objects = await client.status.findMany({
where: { where: {
visibility: "public", id: {
}, lt: max_id ?? undefined,
order: { gte: since_id ?? undefined,
created_at: "DESC", gt: min_id ?? undefined,
},
instanceId: remote
? {
not: null,
}
: local
? null
: undefined,
}, },
include: statusAndUserRelations,
take: limit, take: limit,
relations: statusAndUserRelations, orderBy: {
}; id: "desc",
},
});
query = await updateQuery(max_id, "$lt", query); // Constuct HTTP Link header (next and prev)
query = await updateQuery(min_id, "$gt", query); const linkHeader = [];
query = await updateQuery(since_id, "$gte", query); if (objects.length > 0) {
const urlWithoutQuery = req.url.split("?")[0];
if (only_media) { linkHeader.push(
// TODO: add `<${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( 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(", "),
}
); );
}; };

View file

@ -2,9 +2,8 @@ import { applyConfig } from "@api";
import { errorResponse } from "@response"; import { errorResponse } from "@response";
import { MatchedRoute } from "bun"; import { MatchedRoute } from "bun";
import { randomBytes } from "crypto"; import { randomBytes } from "crypto";
import { ApplicationAction } from "~database/entities/Application"; import { client } from "~database/datasource";
import { Token } from "~database/entities/Token"; import { userRelations } from "~database/entities/User";
import { UserAction, userRelations } from "~database/entities/User";
import { APIRouteMeta } from "~types/api"; import { APIRouteMeta } from "~types/api";
export const meta: APIRouteMeta = applyConfig({ export const meta: APIRouteMeta = applyConfig({
@ -45,33 +44,44 @@ export default async (
return errorResponse("Missing username or password", 400); return errorResponse("Missing username or password", 400);
// Get user // Get user
const user = await UserAction.findOne({ const user = await client.user.findFirst({
where: { where: {
email, email,
}, },
relations: userRelations, include: userRelations,
}); });
if (!user || !(await Bun.password.verify(password, user.password || ""))) if (!user || !(await Bun.password.verify(password, user.password || "")))
return errorResponse("Invalid username or password", 401); return errorResponse("Invalid username or password", 401);
// Get application // Get application
const application = await ApplicationAction.findOneBy({ const application = await client.application.findFirst({
client_id, where: {
client_id,
},
}); });
if (!application) return errorResponse("Invalid client_id", 404); if (!application) return errorResponse("Invalid client_id", 404);
const token = new Token(); const token = await client.application.update({
where: { id: application.id },
token.access_token = randomBytes(64).toString("base64url"); data: {
token.code = randomBytes(32).toString("hex"); tokens: {
token.application = application; create: {
token.scope = scopes.join(" "); access_token: randomBytes(64).toString("base64url"),
token.user = user; code: randomBytes(32).toString("hex"),
scope: scopes.join(" "),
await token.save(); token_type: "bearer",
user: {
connect: {
id: user.id,
},
},
},
},
},
});
// Redirect back to application // Redirect back to application
return Response.redirect(`${redirect_uri}?code=${token.code}`, 302); return Response.redirect(`${redirect_uri}?code=${token.secret}`, 302);
}; };

View file

@ -1,7 +1,7 @@
import { applyConfig } from "@api"; import { applyConfig } from "@api";
import { parseRequest } from "@request"; import { parseRequest } from "@request";
import { errorResponse, jsonResponse } from "@response"; import { errorResponse, jsonResponse } from "@response";
import { Token } from "~database/entities/Token"; import { client } from "~database/datasource";
export const meta = applyConfig({ export const meta = applyConfig({
allowedMethods: ["POST"], allowedMethods: ["POST"],
@ -36,14 +36,19 @@ export default async (req: Request): Promise<Response> => {
); );
// Get associated token // Get associated token
const token = await Token.findOneBy({ const token = await client.token.findFirst({
code, where: {
application: { code,
client_id, application: {
secret: client_secret, client_id,
redirect_uris: redirect_uri, secret: client_secret,
redirect_uris: redirect_uri,
},
scope: scope?.replaceAll("+", " "),
},
include: {
application: true,
}, },
scope: scope?.replaceAll("+", " "),
}); });
if (!token) if (!token)

View file

@ -5,17 +5,16 @@ import { getConfig } from "@config";
import { getBestContentType } from "@content_types"; import { getBestContentType } from "@content_types";
import { errorResponse, jsonResponse } from "@response"; import { errorResponse, jsonResponse } from "@response";
import { MatchedRoute } from "bun"; import { MatchedRoute } from "bun";
import { EmojiAction } from "~database/entities/Emoji"; import { client } from "~database/datasource";
import { LysandObject } from "~database/entities/Object"; import { parseEmojis } from "~database/entities/Emoji";
import { Status } from "~database/entities/Status"; import { createFromObject } from "~database/entities/Object";
import { UserAction, userRelations } from "~database/entities/User";
import { import {
ContentFormat, createNewStatus,
LysandAction, fetchFromRemote,
LysandObjectType, statusAndUserRelations,
LysandPublication, } from "~database/entities/Status";
Patch, import { parseMentionsUris, userRelations } from "~database/entities/User";
} from "~types/lysand/Object"; import { LysandAction, LysandPublication, Patch } from "~types/lysand/Object";
export const meta = applyConfig({ export const meta = applyConfig({
allowedMethods: ["POST"], allowedMethods: ["POST"],
@ -61,11 +60,11 @@ export default async (
// Process request body // Process request body
const body = (await req.json()) as LysandPublication | LysandAction; const body = (await req.json()) as LysandPublication | LysandAction;
const author = await UserAction.findOne({ const author = await client.user.findUnique({
where: { where: {
uri: body.author, username,
}, },
relations: userRelations, include: userRelations,
}); });
if (!author) { if (!author) {
@ -116,7 +115,7 @@ export default async (
// author.public_key is base64 encoded raw public key // author.public_key is base64 encoded raw public key
const publicKey = await crypto.subtle.importKey( const publicKey = await crypto.subtle.importKey(
"spki", "spki",
Buffer.from(author.public_key, "base64"), Buffer.from(author.publicKey, "base64"),
"Ed25519", "Ed25519",
false, false,
["verify"] ["verify"]
@ -141,13 +140,13 @@ export default async (
switch (type) { switch (type) {
case "Note": { case "Note": {
// Store the object in the LysandObject table // Store the object in the LysandObject table
await LysandObject.createFromObject(body); await createFromObject(body);
const content = getBestContentType(body.contents); 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, account: author,
content: content?.content || "", content: content?.content || "",
content_type: content?.content_type, content_type: content?.content_type,
@ -158,39 +157,49 @@ export default async (
sensitive: body.is_sensitive, sensitive: body.is_sensitive,
uri: body.uri, uri: body.uri,
emojis: emojis, 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 there is a reply, fetch all the reply parents and add them to the database
if (body.replies_to.length > 0) { if (body.replies_to.length > 0) {
newStatus.in_reply_to_post = await Status.fetchFromRemote( newStatus.inReplyToPostId =
body.replies_to[0] (await fetchFromRemote(body.replies_to[0]))?.id || null;
);
} }
// Same for quotes // Same for quotes
if (body.quotes.length > 0) { if (body.quotes.length > 0) {
newStatus.quoting_post = await Status.fetchFromRemote( newStatus.quotingPostId =
body.quotes[0] (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; break;
} }
case "Patch": { case "Patch": {
const patch = body as Patch; const patch = body as Patch;
// Store the object in the LysandObject table // Store the object in the LysandObject table
await LysandObject.createFromObject(patch); await createFromObject(patch);
// Edit the status // Edit the status
const content = getBestContentType(patch.contents); const content = getBestContentType(patch.contents);
const emojis = await EmojiAction.parseEmojis(content?.content || ""); const emojis = await parseEmojis(content?.content || "");
const status = await Status.findOneBy({ const status = await client.status.findUnique({
id: patch.patched_id, where: {
uri: patch.patched_id,
},
include: statusAndUserRelations,
}); });
if (!status) { if (!status) {
@ -198,64 +207,81 @@ export default async (
} }
status.content = content?.content || ""; status.content = content?.content || "";
status.content_type = content?.content_type || "text/plain"; status.contentType = content?.content_type || "text/plain";
status.spoiler_text = patch.subject || ""; status.spoilerText = patch.subject || "";
status.sensitive = patch.is_sensitive; status.sensitive = patch.is_sensitive;
status.emojis = emojis; status.emojis = emojis;
// If there is a reply, fetch all the reply parents and add them to the database // If there is a reply, fetch all the reply parents and add them to the database
if (body.replies_to.length > 0) { if (body.replies_to.length > 0) {
status.in_reply_to_post = await Status.fetchFromRemote( status.inReplyToPostId =
body.replies_to[0] (await fetchFromRemote(body.replies_to[0]))?.id || null;
);
} }
// Same for quotes // Same for quotes
if (body.quotes.length > 0) { if (body.quotes.length > 0) {
status.quoting_post = await Status.fetchFromRemote( status.quotingPostId =
body.quotes[0] (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; break;
} }
case "Like": { case "Like": {
// Store the object in the LysandObject table // Store the object in the LysandObject table
await LysandObject.createFromObject(body); await createFromObject(body);
break; break;
} }
case "Dislike": { case "Dislike": {
// Store the object in the LysandObject table // Store the object in the LysandObject table
await LysandObject.createFromObject(body); await createFromObject(body);
break; break;
} }
case "Follow": { case "Follow": {
// Store the object in the LysandObject table // Store the object in the LysandObject table
await LysandObject.createFromObject(body); await createFromObject(body);
break; break;
} }
case "FollowAccept": { case "FollowAccept": {
// Store the object in the LysandObject table // Store the object in the LysandObject table
await LysandObject.createFromObject(body); await createFromObject(body);
break; break;
} }
case "FollowReject": { case "FollowReject": {
// Store the object in the LysandObject table // Store the object in the LysandObject table
await LysandObject.createFromObject(body); await createFromObject(body);
break; break;
} }
case "Announce": { case "Announce": {
// Store the object in the LysandObject table // Store the object in the LysandObject table
await LysandObject.createFromObject(body); await createFromObject(body);
break; break;
} }
case "Undo": { case "Undo": {
// Store the object in the LysandObject table // Store the object in the LysandObject table
await LysandObject.createFromObject(body); await createFromObject(body);
break; break;
} }
case "Extension": { case "Extension": {
// Store the object in the LysandObject table // Store the object in the LysandObject table
await LysandObject.createFromObject(body); await createFromObject(body);
break; break;
} }
default: { default: {

View file

@ -4,7 +4,8 @@ import { applyConfig } from "@api";
import { getConfig } from "@config"; import { getConfig } from "@config";
import { errorResponse, jsonResponse } from "@response"; import { errorResponse, jsonResponse } from "@response";
import { MatchedRoute } from "bun"; 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({ export const meta = applyConfig({
allowedMethods: ["POST"], allowedMethods: ["POST"],
@ -29,16 +30,16 @@ export default async (
const config = getConfig(); const config = getConfig();
const user = await UserAction.findOne({ const user = await client.user.findUnique({
where: { where: {
id: uuid, id: uuid,
}, },
relations: userRelations, include: userRelations,
}); });
if (!user) { if (!user) {
return errorResponse("User not found", 404); return errorResponse("User not found", 404);
} }
return jsonResponse(user.toLysand()); return jsonResponse(userToLysand(user));
}; };

View file

@ -1,10 +1,12 @@
import { jsonResponse } from "@response"; import { jsonResponse } from "@response";
import { MatchedRoute } from "bun"; import { MatchedRoute } from "bun";
import { userRelations } from "~database/entities/User";
import { getConfig, getHost } from "@config"; import { getConfig, getHost } from "@config";
import { applyConfig } from "@api"; import { applyConfig } from "@api";
import { Status } from "~database/entities/Status"; import {
import { In } from "typeorm"; statusAndUserRelations,
statusToLysand,
} from "~database/entities/Status";
import { client } from "~database/datasource";
export const meta = applyConfig({ export const meta = applyConfig({
allowedMethods: ["GET"], allowedMethods: ["GET"],
@ -29,26 +31,25 @@ export default async (
const pageNumber = Number(matchedRoute.query.page) || 1; const pageNumber = Number(matchedRoute.query.page) || 1;
const config = getConfig(); const config = getConfig();
const statuses = await Status.find({ const statuses = await client.status.findMany({
where: { where: {
account: { authorId: uuid,
id: uuid, visibility: {
in: ["public", "unlisted"],
}, },
visibility: In(["public", "unlisted"]),
}, },
relations: userRelations,
take: 20, take: 20,
skip: 20 * (pageNumber - 1), skip: 20 * (pageNumber - 1),
include: statusAndUserRelations,
}); });
const totalStatuses = await Status.count({ const totalStatuses = await client.status.count({
where: { where: {
account: { authorId: uuid,
id: uuid, visibility: {
in: ["public", "unlisted"],
}, },
visibility: In(["public", "unlisted"]),
}, },
relations: userRelations,
}); });
return jsonResponse({ return jsonResponse({
@ -65,6 +66,6 @@ export default async (
pageNumber > 1 pageNumber > 1
? `${getHost()}/users/${uuid}/outbox?page=${pageNumber - 1}` ? `${getHost()}/users/${uuid}/outbox?page=${pageNumber - 1}`
: undefined, : undefined,
items: statuses.map(s => s.toLysand()), items: statuses.map(s => statusToLysand(s)),
}); });
}; };

View file

@ -1,71 +1,63 @@
/* eslint-disable @typescript-eslint/no-unsafe-member-access */ /* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
import { getConfig } from "@config"; import { getConfig } from "@config";
import { Token } from "@prisma/client";
import { afterAll, beforeAll, describe, expect, test } from "bun:test"; import { afterAll, beforeAll, describe, expect, test } from "bun:test";
import { AppDataSource } from "~database/datasource"; import { client } from "~database/datasource";
import { ApplicationAction } from "~database/entities/Application"; import { TokenType } from "~database/entities/Token";
import { EmojiAction } from "~database/entities/Emoji"; import { UserWithRelations, createNewLocalUser } from "~database/entities/User";
import { Token, TokenType } from "~database/entities/Token";
import { UserAction } from "~database/entities/User";
import { APIEmoji } from "~types/entities/emoji"; import { APIEmoji } from "~types/entities/emoji";
import { APIInstance } from "~types/entities/instance"; import { APIInstance } from "~types/entities/instance";
const config = getConfig(); const config = getConfig();
let token: Token; let token: Token;
let user: UserAction; let user: UserWithRelations;
let user2: UserAction;
describe("API Tests", () => { describe("API Tests", () => {
beforeAll(async () => { beforeAll(async () => {
if (!AppDataSource.isInitialized) await AppDataSource.initialize();
// Initialize test user // Initialize test user
user = await UserAction.createNewLocal({ user = await createNewLocalUser({
email: "test@test.com", email: "test@test.com",
username: "test", username: "test",
password: "test", password: "test",
display_name: "", display_name: "",
}); });
// Initialize second test user token = await client.token.create({
user2 = await UserAction.createNewLocal({ data: {
email: "test2@test.com", access_token: "test",
username: "test2", application: {
password: "test2", create: {
display_name: "", 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 () => { afterAll(async () => {
await user.remove(); await client.user.deleteMany({
await user2.remove(); where: {
username: {
await AppDataSource.destroy(); in: ["test", "test2"],
},
},
});
}); });
describe("GET /api/v1/instance", () => { describe("GET /api/v1/instance", () => {
@ -106,15 +98,15 @@ describe("API Tests", () => {
describe("GET /api/v1/custom_emojis", () => { describe("GET /api/v1/custom_emojis", () => {
beforeAll(async () => { beforeAll(async () => {
const emoji = new EmojiAction(); await client.emoji.create({
data: {
emoji.instance = null; instanceId: null,
emoji.url = "https://example.com/test.png"; url: "https://example.com/test.png",
emoji.content_type = "image/png"; content_type: "image/png",
emoji.shortcode = "test"; shortcode: "test",
emoji.visible_in_picker = true; visible_in_picker: true,
},
await emoji.save(); });
}); });
test("should return an array of at least one custom emoji", async () => { test("should return an array of at least one custom emoji", async () => {
const response = await fetch( const response = await fetch(
@ -139,7 +131,11 @@ describe("API Tests", () => {
expect(emojis[0].url).toBe("https://example.com/test.png"); expect(emojis[0].url).toBe("https://example.com/test.png");
}); });
afterAll(async () => { afterAll(async () => {
await EmojiAction.delete({ shortcode: "test" }); await client.emoji.deleteMany({
where: {
shortcode: "test",
},
});
}); });
}); });
}); });

View file

@ -1,11 +1,11 @@
/* eslint-disable @typescript-eslint/no-unsafe-member-access */ /* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
import { getConfig } from "@config"; import { getConfig } from "@config";
import { Token } from "@prisma/client";
import { afterAll, beforeAll, describe, expect, test } from "bun:test"; import { afterAll, beforeAll, describe, expect, test } from "bun:test";
import { AppDataSource } from "~database/datasource"; import { client } from "~database/datasource";
import { ApplicationAction } from "~database/entities/Application"; import { TokenType } from "~database/entities/Token";
import { Token, TokenType } from "~database/entities/Token"; import { UserWithRelations, createNewLocalUser } from "~database/entities/User";
import { UserAction } from "~database/entities/User";
import { APIAccount } from "~types/entities/account"; import { APIAccount } from "~types/entities/account";
import { APIRelationship } from "~types/entities/relationship"; import { APIRelationship } from "~types/entities/relationship";
import { APIStatus } from "~types/entities/status"; import { APIStatus } from "~types/entities/status";
@ -13,59 +13,59 @@ import { APIStatus } from "~types/entities/status";
const config = getConfig(); const config = getConfig();
let token: Token; let token: Token;
let user: UserAction; let user: UserWithRelations;
let user2: UserAction; let user2: UserWithRelations;
describe("API Tests", () => { describe("API Tests", () => {
beforeAll(async () => { beforeAll(async () => {
if (!AppDataSource.isInitialized) await AppDataSource.initialize(); user = await createNewLocalUser({
// Initialize test user
user = await UserAction.createNewLocal({
email: "test@test.com", email: "test@test.com",
username: "test", username: "test",
password: "test", password: "test",
display_name: "", display_name: "",
}); });
// Initialize second test user user2 = await createNewLocalUser({
user2 = await UserAction.createNewLocal({
email: "test2@test.com", email: "test2@test.com",
username: "test2", username: "test2",
password: "test2", password: "test2",
display_name: "", display_name: "",
}); });
const app = new ApplicationAction(); token = await client.token.create({
data: {
app.name = "Test Application"; access_token: "test",
app.website = "https://example.com"; application: {
app.client_id = "test"; create: {
app.redirect_uris = "https://example.com"; client_id: "test",
app.scopes = "read write"; name: "Test Application",
app.secret = "test"; redirect_uris: "https://example.com",
app.vapid_key = null; scopes: "read write",
secret: "test",
await app.save(); website: "https://example.com",
vapid_key: null,
// Initialize test token },
token = new Token(); },
code: "test",
token.access_token = "test"; scope: "read write",
token.application = app; token_type: TokenType.BEARER,
token.code = "test"; user: {
token.scope = "read write"; connect: {
token.token_type = TokenType.BEARER; id: user.id,
token.user = user; },
},
token = await token.save(); },
});
}); });
afterAll(async () => { afterAll(async () => {
await user.remove(); await client.user.deleteMany({
await user2.remove(); where: {
username: {
await AppDataSource.destroy(); in: ["test", "test2"],
},
},
});
}); });
describe("POST /api/v1/accounts/:id", () => { describe("POST /api/v1/accounts/:id", () => {

View file

@ -1,72 +1,65 @@
/* eslint-disable @typescript-eslint/no-unsafe-member-access */ /* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
import { getConfig } from "@config"; import { getConfig } from "@config";
import { Token } from "@prisma/client";
import { afterAll, beforeAll, describe, expect, test } from "bun:test"; import { afterAll, beforeAll, describe, expect, test } from "bun:test";
import { AppDataSource } from "~database/datasource"; import { client } from "~database/datasource";
import { ApplicationAction } from "~database/entities/Application"; import { TokenType } from "~database/entities/Token";
import { Token, TokenType } from "~database/entities/Token"; import { UserWithRelations, createNewLocalUser } from "~database/entities/User";
import { UserAction } from "~database/entities/User"; import { APIAccount } from "~types/entities/account";
import { APIContext } from "~types/entities/context"; import { APIContext } from "~types/entities/context";
import { APIStatus } from "~types/entities/status"; import { APIStatus } from "~types/entities/status";
const config = getConfig(); const config = getConfig();
let token: Token; let token: Token;
let user: UserAction; let user: UserWithRelations;
let user2: UserAction;
let status: APIStatus | null = null; let status: APIStatus | null = null;
let status2: APIStatus | null = null; let status2: APIStatus | null = null;
describe("API Tests", () => { describe("API Tests", () => {
beforeAll(async () => { beforeAll(async () => {
if (!AppDataSource.isInitialized) await AppDataSource.initialize(); user = await createNewLocalUser({
// Initialize test user
user = await UserAction.createNewLocal({
email: "test@test.com", email: "test@test.com",
username: "test", username: "test",
password: "test", password: "test",
display_name: "", display_name: "",
}); });
// Initialize second test user token = await client.token.create({
user2 = await UserAction.createNewLocal({ data: {
email: "test2@test.com", access_token: "test",
username: "test2", application: {
password: "test2", create: {
display_name: "", 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 () => { afterAll(async () => {
await user.remove(); await client.user.deleteMany({
await user2.remove(); where: {
username: {
await AppDataSource.destroy(); in: ["test", "test2"],
},
},
});
}); });
describe("POST /api/v1/statuses", () => { describe("POST /api/v1/statuses", () => {
@ -322,7 +315,7 @@ describe("API Tests", () => {
"application/json" "application/json"
); );
const users = (await response.json()) as UserAction[]; const users = (await response.json()) as APIAccount[];
expect(users.length).toBe(1); expect(users.length).toBe(1);
expect(users[0].id).toBe(user.id); expect(users[0].id).toBe(user.id);

View file

@ -1,9 +1,8 @@
import { getConfig } from "@config"; import { getConfig } from "@config";
import { Application, Token } from "@prisma/client";
import { afterAll, beforeAll, describe, expect, test } from "bun:test"; import { afterAll, beforeAll, describe, expect, test } from "bun:test";
import { AppDataSource } from "~database/datasource"; import { client } from "~database/datasource";
import { ApplicationAction } from "~database/entities/Application"; import { createNewLocalUser } from "~database/entities/User";
import { Token } from "~database/entities/Token";
import { UserAction, userRelations } from "~database/entities/User";
const config = getConfig(); const config = getConfig();
@ -13,10 +12,8 @@ let code: string;
let token: Token; let token: Token;
beforeAll(async () => { beforeAll(async () => {
if (!AppDataSource.isInitialized) await AppDataSource.initialize(); // Init test user
await createNewLocalUser({
// Initialize test user
await UserAction.createNewLocal({
email: "test@test.com", email: "test@test.com",
username: "test", username: "test",
password: "test", password: "test",
@ -139,7 +136,7 @@ describe("GET /api/v1/apps/verify_credentials", () => {
expect(response.status).toBe(200); expect(response.status).toBe(200);
expect(response.headers.get("content-type")).toBe("application/json"); expect(response.headers.get("content-type")).toBe("application/json");
const credentials = (await response.json()) as Partial<ApplicationAction>; const credentials = (await response.json()) as Partial<Application>;
expect(credentials.name).toBe("Test Application"); expect(credentials.name).toBe("Test Application");
expect(credentials.website).toBe("https://example.com"); expect(credentials.website).toBe("https://example.com");
@ -150,31 +147,9 @@ describe("GET /api/v1/apps/verify_credentials", () => {
afterAll(async () => { afterAll(async () => {
// Clean up user // Clean up user
const user = await UserAction.findOne({ await client.user.delete({
where: { where: {
username: "test", 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();
}); });