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