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