mirror of
https://github.com/versia-pub/server.git
synced 2026-03-13 22:09:16 +01:00
Replace eslint and prettier with Biome
This commit is contained in:
parent
4a5a2ea590
commit
af0d627f19
199 changed files with 16493 additions and 16361 deletions
|
|
@ -1,23 +1,22 @@
|
|||
import { xmlResponse } from "@response";
|
||||
import { apiRoute, applyConfig } from "@api";
|
||||
import { xmlResponse } from "@response";
|
||||
|
||||
export const meta = applyConfig({
|
||||
allowedMethods: ["GET"],
|
||||
auth: {
|
||||
required: false,
|
||||
},
|
||||
ratelimits: {
|
||||
duration: 60,
|
||||
max: 60,
|
||||
},
|
||||
route: "/.well-known/host-meta",
|
||||
allowedMethods: ["GET"],
|
||||
auth: {
|
||||
required: false,
|
||||
},
|
||||
ratelimits: {
|
||||
duration: 60,
|
||||
max: 60,
|
||||
},
|
||||
route: "/.well-known/host-meta",
|
||||
});
|
||||
|
||||
|
||||
export default apiRoute(async (req, matchedRoute, extraData) => {
|
||||
const config = await extraData.configManager.getConfig();
|
||||
|
||||
return xmlResponse(`
|
||||
const config = await extraData.configManager.getConfig();
|
||||
|
||||
return xmlResponse(`
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<XRD xmlns="http://docs.oasis-open.org/ns/xri/xrd-1.0">
|
||||
<Link rel="lrdd" template="${config.http.base_url}/.well-known/webfinger?resource={uri}"/>
|
||||
|
|
|
|||
|
|
@ -1,43 +1,49 @@
|
|||
import { jsonResponse } from "@response";
|
||||
import { apiRoute, applyConfig } from "@api";
|
||||
import { jsonResponse } from "@response";
|
||||
|
||||
export const meta = applyConfig({
|
||||
allowedMethods: ["GET"],
|
||||
auth: {
|
||||
required: false,
|
||||
},
|
||||
ratelimits: {
|
||||
duration: 60,
|
||||
max: 60,
|
||||
},
|
||||
route: "/.well-known/lysand",
|
||||
allowedMethods: ["GET"],
|
||||
auth: {
|
||||
required: false,
|
||||
},
|
||||
ratelimits: {
|
||||
duration: 60,
|
||||
max: 60,
|
||||
},
|
||||
route: "/.well-known/lysand",
|
||||
});
|
||||
|
||||
export default apiRoute(async (req, matchedRoute, extraData) => {
|
||||
const config = await extraData.configManager.getConfig();
|
||||
|
||||
// In the format acct:name@example.com
|
||||
// In the format acct:name@example.com
|
||||
return jsonResponse({
|
||||
type: "ServerMetadata",
|
||||
name: config.instance.name,
|
||||
version: "0.0.1",
|
||||
description: config.instance.description,
|
||||
logo: config.instance.logo ? [
|
||||
{
|
||||
content: config.instance.logo,
|
||||
content_type: `image/${config.instance.logo.split(".")[1]}`,
|
||||
}
|
||||
] : undefined,
|
||||
banner: config.instance.banner ? [
|
||||
{
|
||||
content: config.instance.banner,
|
||||
content_type: `image/${config.instance.banner.split(".")[1]}`,
|
||||
}
|
||||
] : undefined,
|
||||
supported_extensions: [
|
||||
"org.lysand:custom_emojis"
|
||||
],
|
||||
logo: config.instance.logo
|
||||
? [
|
||||
{
|
||||
content: config.instance.logo,
|
||||
content_type: `image/${
|
||||
config.instance.logo.split(".")[1]
|
||||
}`,
|
||||
},
|
||||
]
|
||||
: undefined,
|
||||
banner: config.instance.banner
|
||||
? [
|
||||
{
|
||||
content: config.instance.banner,
|
||||
content_type: `image/${
|
||||
config.instance.banner.split(".")[1]
|
||||
}`,
|
||||
},
|
||||
]
|
||||
: undefined,
|
||||
supported_extensions: ["org.lysand:custom_emojis"],
|
||||
website: "https://lysand.org",
|
||||
// TODO: Add admins, moderators field
|
||||
})
|
||||
})
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,25 +1,24 @@
|
|||
import { apiRoute, applyConfig } from "@api";
|
||||
|
||||
export const meta = applyConfig({
|
||||
allowedMethods: ["GET"],
|
||||
auth: {
|
||||
required: false,
|
||||
},
|
||||
ratelimits: {
|
||||
duration: 60,
|
||||
max: 60,
|
||||
},
|
||||
route: "/.well-known/nodeinfo",
|
||||
allowedMethods: ["GET"],
|
||||
auth: {
|
||||
required: false,
|
||||
},
|
||||
ratelimits: {
|
||||
duration: 60,
|
||||
max: 60,
|
||||
},
|
||||
route: "/.well-known/nodeinfo",
|
||||
});
|
||||
|
||||
|
||||
export default apiRoute(async (req, matchedRoute, extraData) => {
|
||||
const config = await extraData.configManager.getConfig();
|
||||
const config = await extraData.configManager.getConfig();
|
||||
|
||||
return new Response("", {
|
||||
status: 301,
|
||||
headers: {
|
||||
Location: `${config.http.base_url}/.well-known/nodeinfo/2.0`,
|
||||
},
|
||||
});
|
||||
return new Response("", {
|
||||
status: 301,
|
||||
headers: {
|
||||
Location: `${config.http.base_url}/.well-known/nodeinfo/2.0`,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,59 +1,59 @@
|
|||
import { errorResponse, jsonResponse } from "@response";
|
||||
import { apiRoute, applyConfig } from "@api";
|
||||
import { errorResponse, jsonResponse } from "@response";
|
||||
import { client } from "~database/datasource";
|
||||
|
||||
export const meta = applyConfig({
|
||||
allowedMethods: ["GET"],
|
||||
auth: {
|
||||
required: false,
|
||||
},
|
||||
ratelimits: {
|
||||
duration: 60,
|
||||
max: 60,
|
||||
},
|
||||
route: "/.well-known/webfinger",
|
||||
allowedMethods: ["GET"],
|
||||
auth: {
|
||||
required: false,
|
||||
},
|
||||
ratelimits: {
|
||||
duration: 60,
|
||||
max: 60,
|
||||
},
|
||||
route: "/.well-known/webfinger",
|
||||
});
|
||||
|
||||
export default apiRoute(async (req, matchedRoute, extraData) => {
|
||||
// In the format acct:name@example.com
|
||||
const resource = matchedRoute.query.resource;
|
||||
const requestedUser = resource.split("acct:")[1];
|
||||
|
||||
const config = await extraData.configManager.getConfig();
|
||||
const host = new URL(config.http.base_url).hostname;
|
||||
|
||||
// Check if user is a local user
|
||||
if (requestedUser.split("@")[1] !== host) {
|
||||
return errorResponse("User is a remote user", 404);
|
||||
}
|
||||
|
||||
const user = await client.user.findUnique({
|
||||
where: { username: requestedUser.split("@")[0] },
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return errorResponse("User not found", 404);
|
||||
}
|
||||
|
||||
return jsonResponse({
|
||||
subject: `acct:${user.username}@${host}`,
|
||||
|
||||
links: [
|
||||
{
|
||||
rel: "self",
|
||||
type: "application/activity+json",
|
||||
href: `${config.http.base_url}/users/${user.username}/actor`
|
||||
},
|
||||
{
|
||||
rel: "https://webfinger.net/rel/profile-page",
|
||||
type: "text/html",
|
||||
href: `${config.http.base_url}/users/${user.username}`
|
||||
},
|
||||
{
|
||||
rel: "self",
|
||||
type: "application/activity+json; profile=\"https://www.w3.org/ns/activitystreams\"",
|
||||
href: `${config.http.base_url}/users/${user.username}/actor`
|
||||
}
|
||||
]
|
||||
})
|
||||
});
|
||||
// In the format acct:name@example.com
|
||||
const resource = matchedRoute.query.resource;
|
||||
const requestedUser = resource.split("acct:")[1];
|
||||
|
||||
const config = await extraData.configManager.getConfig();
|
||||
const host = new URL(config.http.base_url).hostname;
|
||||
|
||||
// Check if user is a local user
|
||||
if (requestedUser.split("@")[1] !== host) {
|
||||
return errorResponse("User is a remote user", 404);
|
||||
}
|
||||
|
||||
const user = await client.user.findUnique({
|
||||
where: { username: requestedUser.split("@")[0] },
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return errorResponse("User not found", 404);
|
||||
}
|
||||
|
||||
return jsonResponse({
|
||||
subject: `acct:${user.username}@${host}`,
|
||||
|
||||
links: [
|
||||
{
|
||||
rel: "self",
|
||||
type: "application/activity+json",
|
||||
href: `${config.http.base_url}/users/${user.username}/actor`,
|
||||
},
|
||||
{
|
||||
rel: "https://webfinger.net/rel/profile-page",
|
||||
type: "text/html",
|
||||
href: `${config.http.base_url}/users/${user.username}`,
|
||||
},
|
||||
{
|
||||
rel: "self",
|
||||
type: 'application/activity+json; profile="https://www.w3.org/ns/activitystreams"',
|
||||
href: `${config.http.base_url}/users/${user.username}/actor`,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -2,20 +2,20 @@ import { apiRoute, applyConfig } from "@api";
|
|||
import { errorResponse } from "@response";
|
||||
|
||||
export const meta = applyConfig({
|
||||
allowedMethods: ["POST", "GET", "PUT", "PATCH", "DELETE"],
|
||||
auth: {
|
||||
required: false,
|
||||
},
|
||||
ratelimits: {
|
||||
duration: 60,
|
||||
max: 100,
|
||||
},
|
||||
route: "/[...404]",
|
||||
allowedMethods: ["POST", "GET", "PUT", "PATCH", "DELETE"],
|
||||
auth: {
|
||||
required: false,
|
||||
},
|
||||
ratelimits: {
|
||||
duration: 60,
|
||||
max: 100,
|
||||
},
|
||||
route: "/[...404]",
|
||||
});
|
||||
|
||||
/**
|
||||
* Default catch-all route, returns a 404 error.
|
||||
*/
|
||||
export default apiRoute(() => {
|
||||
return errorResponse("This API route does not exist", 404);
|
||||
return errorResponse("This API route does not exist", 404);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,81 +1,81 @@
|
|||
import { apiRoute, applyConfig } from "@api";
|
||||
import { errorResponse, jsonResponse } from "@response";
|
||||
import { client } from "~database/datasource";
|
||||
import {
|
||||
createNewRelationship,
|
||||
relationshipToAPI,
|
||||
createNewRelationship,
|
||||
relationshipToAPI,
|
||||
} from "~database/entities/Relationship";
|
||||
import { getRelationshipToOtherUser } from "~database/entities/User";
|
||||
import { apiRoute, applyConfig } from "@api";
|
||||
import { client } from "~database/datasource";
|
||||
|
||||
export const meta = applyConfig({
|
||||
allowedMethods: ["POST"],
|
||||
ratelimits: {
|
||||
max: 30,
|
||||
duration: 60,
|
||||
},
|
||||
route: "/accounts/:id/block",
|
||||
auth: {
|
||||
required: true,
|
||||
oauthPermissions: ["write:blocks"],
|
||||
},
|
||||
allowedMethods: ["POST"],
|
||||
ratelimits: {
|
||||
max: 30,
|
||||
duration: 60,
|
||||
},
|
||||
route: "/accounts/:id/block",
|
||||
auth: {
|
||||
required: true,
|
||||
oauthPermissions: ["write:blocks"],
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Blocks a user
|
||||
*/
|
||||
export default apiRoute(async (req, matchedRoute, extraData) => {
|
||||
const id = matchedRoute.params.id;
|
||||
const id = matchedRoute.params.id;
|
||||
|
||||
const { user: self } = extraData.auth;
|
||||
const { user: self } = extraData.auth;
|
||||
|
||||
if (!self) return errorResponse("Unauthorized", 401);
|
||||
if (!self) return errorResponse("Unauthorized", 401);
|
||||
|
||||
const user = await client.user.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
relationships: {
|
||||
include: {
|
||||
owner: true,
|
||||
subject: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
const user = await client.user.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
relationships: {
|
||||
include: {
|
||||
owner: true,
|
||||
subject: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!user) return errorResponse("User not found", 404);
|
||||
if (!user) return errorResponse("User not found", 404);
|
||||
|
||||
// Check if already following
|
||||
let relationship = await getRelationshipToOtherUser(self, user);
|
||||
// Check if already following
|
||||
let relationship = await getRelationshipToOtherUser(self, user);
|
||||
|
||||
if (!relationship) {
|
||||
// Create new relationship
|
||||
if (!relationship) {
|
||||
// Create new relationship
|
||||
|
||||
const newRelationship = await createNewRelationship(self, user);
|
||||
const newRelationship = await createNewRelationship(self, user);
|
||||
|
||||
await client.user.update({
|
||||
where: { id: self.id },
|
||||
data: {
|
||||
relationships: {
|
||||
connect: {
|
||||
id: newRelationship.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
await client.user.update({
|
||||
where: { id: self.id },
|
||||
data: {
|
||||
relationships: {
|
||||
connect: {
|
||||
id: newRelationship.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
relationship = newRelationship;
|
||||
}
|
||||
relationship = newRelationship;
|
||||
}
|
||||
|
||||
if (!relationship.blocking) {
|
||||
relationship.blocking = true;
|
||||
}
|
||||
if (!relationship.blocking) {
|
||||
relationship.blocking = true;
|
||||
}
|
||||
|
||||
await client.relationship.update({
|
||||
where: { id: relationship.id },
|
||||
data: {
|
||||
blocking: true,
|
||||
},
|
||||
});
|
||||
await client.relationship.update({
|
||||
where: { id: relationship.id },
|
||||
data: {
|
||||
blocking: true,
|
||||
},
|
||||
});
|
||||
|
||||
return jsonResponse(relationshipToAPI(relationship));
|
||||
return jsonResponse(relationshipToAPI(relationship));
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,99 +1,99 @@
|
|||
import { apiRoute, applyConfig } from "@api";
|
||||
import { errorResponse, jsonResponse } from "@response";
|
||||
import { client } from "~database/datasource";
|
||||
import {
|
||||
createNewRelationship,
|
||||
relationshipToAPI,
|
||||
createNewRelationship,
|
||||
relationshipToAPI,
|
||||
} from "~database/entities/Relationship";
|
||||
import { getRelationshipToOtherUser } from "~database/entities/User";
|
||||
import { apiRoute, applyConfig } from "@api";
|
||||
import { client } from "~database/datasource";
|
||||
|
||||
export const meta = applyConfig({
|
||||
allowedMethods: ["POST"],
|
||||
ratelimits: {
|
||||
max: 30,
|
||||
duration: 60,
|
||||
},
|
||||
route: "/accounts/:id/follow",
|
||||
auth: {
|
||||
required: true,
|
||||
oauthPermissions: ["write:follows"],
|
||||
},
|
||||
allowedMethods: ["POST"],
|
||||
ratelimits: {
|
||||
max: 30,
|
||||
duration: 60,
|
||||
},
|
||||
route: "/accounts/:id/follow",
|
||||
auth: {
|
||||
required: true,
|
||||
oauthPermissions: ["write:follows"],
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Follow a user
|
||||
*/
|
||||
export default apiRoute<{
|
||||
reblogs?: boolean;
|
||||
notify?: boolean;
|
||||
languages?: string[];
|
||||
reblogs?: boolean;
|
||||
notify?: boolean;
|
||||
languages?: string[];
|
||||
}>(async (req, matchedRoute, extraData) => {
|
||||
const id = matchedRoute.params.id;
|
||||
const id = matchedRoute.params.id;
|
||||
|
||||
const { user: self } = extraData.auth;
|
||||
const { user: self } = extraData.auth;
|
||||
|
||||
if (!self) return errorResponse("Unauthorized", 401);
|
||||
if (!self) return errorResponse("Unauthorized", 401);
|
||||
|
||||
const { languages, notify, reblogs } = extraData.parsedRequest;
|
||||
const { languages, notify, reblogs } = extraData.parsedRequest;
|
||||
|
||||
const user = await client.user.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
relationships: {
|
||||
include: {
|
||||
owner: true,
|
||||
subject: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
const user = await client.user.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
relationships: {
|
||||
include: {
|
||||
owner: true,
|
||||
subject: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!user) return errorResponse("User not found", 404);
|
||||
if (!user) return errorResponse("User not found", 404);
|
||||
|
||||
// Check if already following
|
||||
let relationship = await getRelationshipToOtherUser(self, user);
|
||||
// Check if already following
|
||||
let relationship = await getRelationshipToOtherUser(self, user);
|
||||
|
||||
if (!relationship) {
|
||||
// Create new relationship
|
||||
if (!relationship) {
|
||||
// Create new relationship
|
||||
|
||||
const newRelationship = await createNewRelationship(self, user);
|
||||
const newRelationship = await createNewRelationship(self, user);
|
||||
|
||||
await client.user.update({
|
||||
where: { id: self.id },
|
||||
data: {
|
||||
relationships: {
|
||||
connect: {
|
||||
id: newRelationship.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
await client.user.update({
|
||||
where: { id: self.id },
|
||||
data: {
|
||||
relationships: {
|
||||
connect: {
|
||||
id: newRelationship.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
relationship = newRelationship;
|
||||
}
|
||||
relationship = newRelationship;
|
||||
}
|
||||
|
||||
if (!relationship.following) {
|
||||
relationship.following = true;
|
||||
}
|
||||
if (reblogs) {
|
||||
relationship.showingReblogs = true;
|
||||
}
|
||||
if (notify) {
|
||||
relationship.notifying = true;
|
||||
}
|
||||
if (languages) {
|
||||
relationship.languages = languages;
|
||||
}
|
||||
if (!relationship.following) {
|
||||
relationship.following = true;
|
||||
}
|
||||
if (reblogs) {
|
||||
relationship.showingReblogs = true;
|
||||
}
|
||||
if (notify) {
|
||||
relationship.notifying = true;
|
||||
}
|
||||
if (languages) {
|
||||
relationship.languages = languages;
|
||||
}
|
||||
|
||||
await client.relationship.update({
|
||||
where: { id: relationship.id },
|
||||
data: {
|
||||
following: true,
|
||||
showingReblogs: reblogs ?? false,
|
||||
notifying: notify ?? false,
|
||||
languages: languages ?? [],
|
||||
},
|
||||
});
|
||||
await client.relationship.update({
|
||||
where: { id: relationship.id },
|
||||
data: {
|
||||
following: true,
|
||||
showingReblogs: reblogs ?? false,
|
||||
notifying: notify ?? false,
|
||||
languages: languages ?? [],
|
||||
},
|
||||
});
|
||||
|
||||
return jsonResponse(relationshipToAPI(relationship));
|
||||
return jsonResponse(relationshipToAPI(relationship));
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,82 +1,82 @@
|
|||
import { apiRoute, applyConfig } from "@api";
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||
import { errorResponse, jsonResponse } from "@response";
|
||||
import { userToAPI } from "~database/entities/User";
|
||||
import { apiRoute, applyConfig } from "@api";
|
||||
import { client } from "~database/datasource";
|
||||
import { userToAPI } from "~database/entities/User";
|
||||
import { userRelations } from "~database/entities/relations";
|
||||
|
||||
export const meta = applyConfig({
|
||||
allowedMethods: ["GET"],
|
||||
ratelimits: {
|
||||
max: 60,
|
||||
duration: 60,
|
||||
},
|
||||
route: "/accounts/:id/followers",
|
||||
auth: {
|
||||
required: false,
|
||||
oauthPermissions: [],
|
||||
},
|
||||
allowedMethods: ["GET"],
|
||||
ratelimits: {
|
||||
max: 60,
|
||||
duration: 60,
|
||||
},
|
||||
route: "/accounts/:id/followers",
|
||||
auth: {
|
||||
required: false,
|
||||
oauthPermissions: [],
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Fetch all statuses for a user
|
||||
*/
|
||||
export default apiRoute<{
|
||||
max_id?: string;
|
||||
since_id?: string;
|
||||
min_id?: string;
|
||||
limit?: number;
|
||||
max_id?: string;
|
||||
since_id?: string;
|
||||
min_id?: string;
|
||||
limit?: number;
|
||||
}>(async (req, matchedRoute, extraData) => {
|
||||
const id = matchedRoute.params.id;
|
||||
const id = matchedRoute.params.id;
|
||||
|
||||
// TODO: Add pinned
|
||||
const { max_id, min_id, since_id, limit = 20 } = extraData.parsedRequest;
|
||||
// TODO: Add pinned
|
||||
const { max_id, min_id, since_id, limit = 20 } = extraData.parsedRequest;
|
||||
|
||||
const user = await client.user.findUnique({
|
||||
where: { id },
|
||||
include: userRelations,
|
||||
});
|
||||
const user = await client.user.findUnique({
|
||||
where: { id },
|
||||
include: userRelations,
|
||||
});
|
||||
|
||||
if (limit < 1 || limit > 40) return errorResponse("Invalid limit", 400);
|
||||
if (limit < 1 || limit > 40) return errorResponse("Invalid limit", 400);
|
||||
|
||||
if (!user) return errorResponse("User not found", 404);
|
||||
if (!user) return errorResponse("User not found", 404);
|
||||
|
||||
const objects = await client.user.findMany({
|
||||
where: {
|
||||
relationships: {
|
||||
some: {
|
||||
subjectId: user.id,
|
||||
following: true,
|
||||
},
|
||||
},
|
||||
id: {
|
||||
lt: max_id,
|
||||
gt: min_id,
|
||||
gte: since_id,
|
||||
},
|
||||
},
|
||||
include: userRelations,
|
||||
take: Number(limit),
|
||||
orderBy: {
|
||||
id: "desc",
|
||||
},
|
||||
});
|
||||
const objects = await client.user.findMany({
|
||||
where: {
|
||||
relationships: {
|
||||
some: {
|
||||
subjectId: user.id,
|
||||
following: true,
|
||||
},
|
||||
},
|
||||
id: {
|
||||
lt: max_id,
|
||||
gt: min_id,
|
||||
gte: since_id,
|
||||
},
|
||||
},
|
||||
include: userRelations,
|
||||
take: Number(limit),
|
||||
orderBy: {
|
||||
id: "desc",
|
||||
},
|
||||
});
|
||||
|
||||
// Constuct HTTP Link header (next and prev)
|
||||
const linkHeader = [];
|
||||
if (objects.length > 0) {
|
||||
const urlWithoutQuery = req.url.split("?")[0];
|
||||
linkHeader.push(
|
||||
`<${urlWithoutQuery}?max_id=${objects.at(-1)?.id}>; rel="next"`,
|
||||
`<${urlWithoutQuery}?min_id=${objects[0].id}>; rel="prev"`
|
||||
);
|
||||
}
|
||||
// Constuct HTTP Link header (next and prev)
|
||||
const linkHeader = [];
|
||||
if (objects.length > 0) {
|
||||
const urlWithoutQuery = req.url.split("?")[0];
|
||||
linkHeader.push(
|
||||
`<${urlWithoutQuery}?max_id=${objects.at(-1)?.id}>; rel="next"`,
|
||||
`<${urlWithoutQuery}?min_id=${objects[0].id}>; rel="prev"`,
|
||||
);
|
||||
}
|
||||
|
||||
return jsonResponse(
|
||||
await Promise.all(objects.map(object => userToAPI(object))),
|
||||
200,
|
||||
{
|
||||
Link: linkHeader.join(", "),
|
||||
}
|
||||
);
|
||||
return jsonResponse(
|
||||
await Promise.all(objects.map((object) => userToAPI(object))),
|
||||
200,
|
||||
{
|
||||
Link: linkHeader.join(", "),
|
||||
},
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,82 +1,82 @@
|
|||
import { apiRoute, applyConfig } from "@api";
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||
import { errorResponse, jsonResponse } from "@response";
|
||||
import { userToAPI } from "~database/entities/User";
|
||||
import { apiRoute, applyConfig } from "@api";
|
||||
import { client } from "~database/datasource";
|
||||
import { userToAPI } from "~database/entities/User";
|
||||
import { userRelations } from "~database/entities/relations";
|
||||
|
||||
export const meta = applyConfig({
|
||||
allowedMethods: ["GET"],
|
||||
ratelimits: {
|
||||
max: 60,
|
||||
duration: 60,
|
||||
},
|
||||
route: "/accounts/:id/following",
|
||||
auth: {
|
||||
required: false,
|
||||
oauthPermissions: [],
|
||||
},
|
||||
allowedMethods: ["GET"],
|
||||
ratelimits: {
|
||||
max: 60,
|
||||
duration: 60,
|
||||
},
|
||||
route: "/accounts/:id/following",
|
||||
auth: {
|
||||
required: false,
|
||||
oauthPermissions: [],
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Fetch all statuses for a user
|
||||
*/
|
||||
export default apiRoute<{
|
||||
max_id?: string;
|
||||
since_id?: string;
|
||||
min_id?: string;
|
||||
limit?: number;
|
||||
max_id?: string;
|
||||
since_id?: string;
|
||||
min_id?: string;
|
||||
limit?: number;
|
||||
}>(async (req, matchedRoute, extraData) => {
|
||||
const id = matchedRoute.params.id;
|
||||
const id = matchedRoute.params.id;
|
||||
|
||||
// TODO: Add pinned
|
||||
const { max_id, min_id, since_id, limit = 20 } = extraData.parsedRequest;
|
||||
// TODO: Add pinned
|
||||
const { max_id, min_id, since_id, limit = 20 } = extraData.parsedRequest;
|
||||
|
||||
const user = await client.user.findUnique({
|
||||
where: { id },
|
||||
include: userRelations,
|
||||
});
|
||||
const user = await client.user.findUnique({
|
||||
where: { id },
|
||||
include: userRelations,
|
||||
});
|
||||
|
||||
if (limit < 1 || limit > 40) return errorResponse("Invalid limit", 400);
|
||||
if (limit < 1 || limit > 40) return errorResponse("Invalid limit", 400);
|
||||
|
||||
if (!user) return errorResponse("User not found", 404);
|
||||
if (!user) return errorResponse("User not found", 404);
|
||||
|
||||
const objects = await client.user.findMany({
|
||||
where: {
|
||||
relationshipSubjects: {
|
||||
some: {
|
||||
ownerId: user.id,
|
||||
following: true,
|
||||
},
|
||||
},
|
||||
id: {
|
||||
lt: max_id,
|
||||
gt: min_id,
|
||||
gte: since_id,
|
||||
},
|
||||
},
|
||||
include: userRelations,
|
||||
take: Number(limit),
|
||||
orderBy: {
|
||||
id: "desc",
|
||||
},
|
||||
});
|
||||
const objects = await client.user.findMany({
|
||||
where: {
|
||||
relationshipSubjects: {
|
||||
some: {
|
||||
ownerId: user.id,
|
||||
following: true,
|
||||
},
|
||||
},
|
||||
id: {
|
||||
lt: max_id,
|
||||
gt: min_id,
|
||||
gte: since_id,
|
||||
},
|
||||
},
|
||||
include: userRelations,
|
||||
take: Number(limit),
|
||||
orderBy: {
|
||||
id: "desc",
|
||||
},
|
||||
});
|
||||
|
||||
// Constuct HTTP Link header (next and prev)
|
||||
const linkHeader = [];
|
||||
if (objects.length > 0) {
|
||||
const urlWithoutQuery = req.url.split("?")[0];
|
||||
linkHeader.push(
|
||||
`<${urlWithoutQuery}?max_id=${objects.at(-1)?.id}>; rel="next"`,
|
||||
`<${urlWithoutQuery}?min_id=${objects[0].id}>; rel="prev"`
|
||||
);
|
||||
}
|
||||
// Constuct HTTP Link header (next and prev)
|
||||
const linkHeader = [];
|
||||
if (objects.length > 0) {
|
||||
const urlWithoutQuery = req.url.split("?")[0];
|
||||
linkHeader.push(
|
||||
`<${urlWithoutQuery}?max_id=${objects.at(-1)?.id}>; rel="next"`,
|
||||
`<${urlWithoutQuery}?min_id=${objects[0].id}>; rel="prev"`,
|
||||
);
|
||||
}
|
||||
|
||||
return jsonResponse(
|
||||
await Promise.all(objects.map(object => userToAPI(object))),
|
||||
200,
|
||||
{
|
||||
Link: linkHeader.join(", "),
|
||||
}
|
||||
);
|
||||
return jsonResponse(
|
||||
await Promise.all(objects.map((object) => userToAPI(object))),
|
||||
200,
|
||||
{
|
||||
Link: linkHeader.join(", "),
|
||||
},
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,46 +1,46 @@
|
|||
import { apiRoute, applyConfig } from "@api";
|
||||
import { errorResponse, jsonResponse } from "@response";
|
||||
import { client } from "~database/datasource";
|
||||
import type { UserWithRelations } from "~database/entities/User";
|
||||
import { userToAPI } from "~database/entities/User";
|
||||
import { apiRoute, applyConfig } from "@api";
|
||||
import { client } from "~database/datasource";
|
||||
import { userRelations } from "~database/entities/relations";
|
||||
|
||||
export const meta = applyConfig({
|
||||
allowedMethods: ["GET"],
|
||||
ratelimits: {
|
||||
max: 30,
|
||||
duration: 60,
|
||||
},
|
||||
route: "/accounts/:id",
|
||||
auth: {
|
||||
required: true,
|
||||
oauthPermissions: [],
|
||||
},
|
||||
allowedMethods: ["GET"],
|
||||
ratelimits: {
|
||||
max: 30,
|
||||
duration: 60,
|
||||
},
|
||||
route: "/accounts/:id",
|
||||
auth: {
|
||||
required: true,
|
||||
oauthPermissions: [],
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Fetch a user
|
||||
*/
|
||||
export default apiRoute(async (req, matchedRoute, extraData) => {
|
||||
const id = matchedRoute.params.id;
|
||||
// Check if ID is valid UUID
|
||||
if (!id.match(/^[0-9a-fA-F]{24}$/)) {
|
||||
return errorResponse("Invalid ID", 404);
|
||||
}
|
||||
const id = matchedRoute.params.id;
|
||||
// Check if ID is valid UUID
|
||||
if (!id.match(/^[0-9a-fA-F]{24}$/)) {
|
||||
return errorResponse("Invalid ID", 404);
|
||||
}
|
||||
|
||||
const { user } = extraData.auth;
|
||||
const { user } = extraData.auth;
|
||||
|
||||
let foundUser: UserWithRelations | null;
|
||||
try {
|
||||
foundUser = await client.user.findUnique({
|
||||
where: { id },
|
||||
include: userRelations,
|
||||
});
|
||||
} catch (e) {
|
||||
return errorResponse("Invalid ID", 404);
|
||||
}
|
||||
let foundUser: UserWithRelations | null;
|
||||
try {
|
||||
foundUser = await client.user.findUnique({
|
||||
where: { id },
|
||||
include: userRelations,
|
||||
});
|
||||
} catch (e) {
|
||||
return errorResponse("Invalid ID", 404);
|
||||
}
|
||||
|
||||
if (!foundUser) return errorResponse("User not found", 404);
|
||||
if (!foundUser) return errorResponse("User not found", 404);
|
||||
|
||||
return jsonResponse(userToAPI(foundUser, user?.id === foundUser.id));
|
||||
return jsonResponse(userToAPI(foundUser, user?.id === foundUser.id));
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,93 +1,93 @@
|
|||
import { apiRoute, applyConfig } from "@api";
|
||||
import { errorResponse, jsonResponse } from "@response";
|
||||
import { client } from "~database/datasource";
|
||||
import {
|
||||
createNewRelationship,
|
||||
relationshipToAPI,
|
||||
createNewRelationship,
|
||||
relationshipToAPI,
|
||||
} from "~database/entities/Relationship";
|
||||
import { getRelationshipToOtherUser } from "~database/entities/User";
|
||||
import { apiRoute, applyConfig } from "@api";
|
||||
import { client } from "~database/datasource";
|
||||
|
||||
export const meta = applyConfig({
|
||||
allowedMethods: ["POST"],
|
||||
ratelimits: {
|
||||
max: 30,
|
||||
duration: 60,
|
||||
},
|
||||
route: "/accounts/:id/mute",
|
||||
auth: {
|
||||
required: true,
|
||||
oauthPermissions: ["write:mutes"],
|
||||
},
|
||||
allowedMethods: ["POST"],
|
||||
ratelimits: {
|
||||
max: 30,
|
||||
duration: 60,
|
||||
},
|
||||
route: "/accounts/:id/mute",
|
||||
auth: {
|
||||
required: true,
|
||||
oauthPermissions: ["write:mutes"],
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Mute a user
|
||||
*/
|
||||
export default apiRoute<{
|
||||
notifications: boolean;
|
||||
duration: number;
|
||||
notifications: boolean;
|
||||
duration: number;
|
||||
}>(async (req, matchedRoute, extraData) => {
|
||||
const id = matchedRoute.params.id;
|
||||
const id = matchedRoute.params.id;
|
||||
|
||||
const { user: self } = extraData.auth;
|
||||
const { user: self } = extraData.auth;
|
||||
|
||||
if (!self) return errorResponse("Unauthorized", 401);
|
||||
if (!self) return errorResponse("Unauthorized", 401);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { notifications, duration } = extraData.parsedRequest;
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { notifications, duration } = extraData.parsedRequest;
|
||||
|
||||
const user = await client.user.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
relationships: {
|
||||
include: {
|
||||
owner: true,
|
||||
subject: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
const user = await client.user.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
relationships: {
|
||||
include: {
|
||||
owner: true,
|
||||
subject: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!user) return errorResponse("User not found", 404);
|
||||
if (!user) return errorResponse("User not found", 404);
|
||||
|
||||
// Check if already following
|
||||
let relationship = await getRelationshipToOtherUser(self, user);
|
||||
// Check if already following
|
||||
let relationship = await getRelationshipToOtherUser(self, user);
|
||||
|
||||
if (!relationship) {
|
||||
// Create new relationship
|
||||
if (!relationship) {
|
||||
// Create new relationship
|
||||
|
||||
const newRelationship = await createNewRelationship(self, user);
|
||||
const newRelationship = await createNewRelationship(self, user);
|
||||
|
||||
await client.user.update({
|
||||
where: { id: self.id },
|
||||
data: {
|
||||
relationships: {
|
||||
connect: {
|
||||
id: newRelationship.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
await client.user.update({
|
||||
where: { id: self.id },
|
||||
data: {
|
||||
relationships: {
|
||||
connect: {
|
||||
id: newRelationship.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
relationship = newRelationship;
|
||||
}
|
||||
relationship = newRelationship;
|
||||
}
|
||||
|
||||
if (!relationship.muting) {
|
||||
relationship.muting = true;
|
||||
}
|
||||
if (notifications ?? true) {
|
||||
relationship.mutingNotifications = true;
|
||||
}
|
||||
if (!relationship.muting) {
|
||||
relationship.muting = true;
|
||||
}
|
||||
if (notifications ?? true) {
|
||||
relationship.mutingNotifications = true;
|
||||
}
|
||||
|
||||
await client.relationship.update({
|
||||
where: { id: relationship.id },
|
||||
data: {
|
||||
muting: true,
|
||||
mutingNotifications: notifications ?? true,
|
||||
},
|
||||
});
|
||||
await client.relationship.update({
|
||||
where: { id: relationship.id },
|
||||
data: {
|
||||
muting: true,
|
||||
mutingNotifications: notifications ?? true,
|
||||
},
|
||||
});
|
||||
|
||||
// TODO: Implement duration
|
||||
// TODO: Implement duration
|
||||
|
||||
return jsonResponse(relationshipToAPI(relationship));
|
||||
return jsonResponse(relationshipToAPI(relationship));
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,83 +1,83 @@
|
|||
import { apiRoute, applyConfig } from "@api";
|
||||
import { errorResponse, jsonResponse } from "@response";
|
||||
import { client } from "~database/datasource";
|
||||
import {
|
||||
createNewRelationship,
|
||||
relationshipToAPI,
|
||||
createNewRelationship,
|
||||
relationshipToAPI,
|
||||
} from "~database/entities/Relationship";
|
||||
import { getRelationshipToOtherUser } from "~database/entities/User";
|
||||
import { apiRoute, applyConfig } from "@api";
|
||||
import { client } from "~database/datasource";
|
||||
|
||||
export const meta = applyConfig({
|
||||
allowedMethods: ["POST"],
|
||||
ratelimits: {
|
||||
max: 30,
|
||||
duration: 60,
|
||||
},
|
||||
route: "/accounts/:id/note",
|
||||
auth: {
|
||||
required: true,
|
||||
oauthPermissions: ["write:accounts"],
|
||||
},
|
||||
allowedMethods: ["POST"],
|
||||
ratelimits: {
|
||||
max: 30,
|
||||
duration: 60,
|
||||
},
|
||||
route: "/accounts/:id/note",
|
||||
auth: {
|
||||
required: true,
|
||||
oauthPermissions: ["write:accounts"],
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Sets a user note
|
||||
*/
|
||||
export default apiRoute<{
|
||||
comment: string;
|
||||
comment: string;
|
||||
}>(async (req, matchedRoute, extraData) => {
|
||||
const id = matchedRoute.params.id;
|
||||
const id = matchedRoute.params.id;
|
||||
|
||||
const { user: self } = extraData.auth;
|
||||
const { user: self } = extraData.auth;
|
||||
|
||||
if (!self) return errorResponse("Unauthorized", 401);
|
||||
if (!self) return errorResponse("Unauthorized", 401);
|
||||
|
||||
const { comment } = extraData.parsedRequest;
|
||||
const { comment } = extraData.parsedRequest;
|
||||
|
||||
const user = await client.user.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
relationships: {
|
||||
include: {
|
||||
owner: true,
|
||||
subject: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
const user = await client.user.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
relationships: {
|
||||
include: {
|
||||
owner: true,
|
||||
subject: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!user) return errorResponse("User not found", 404);
|
||||
if (!user) return errorResponse("User not found", 404);
|
||||
|
||||
// Check if already following
|
||||
let relationship = await getRelationshipToOtherUser(self, user);
|
||||
// Check if already following
|
||||
let relationship = await getRelationshipToOtherUser(self, user);
|
||||
|
||||
if (!relationship) {
|
||||
// Create new relationship
|
||||
if (!relationship) {
|
||||
// Create new relationship
|
||||
|
||||
const newRelationship = await createNewRelationship(self, user);
|
||||
const newRelationship = await createNewRelationship(self, user);
|
||||
|
||||
await client.user.update({
|
||||
where: { id: self.id },
|
||||
data: {
|
||||
relationships: {
|
||||
connect: {
|
||||
id: newRelationship.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
await client.user.update({
|
||||
where: { id: self.id },
|
||||
data: {
|
||||
relationships: {
|
||||
connect: {
|
||||
id: newRelationship.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
relationship = newRelationship;
|
||||
}
|
||||
relationship = newRelationship;
|
||||
}
|
||||
|
||||
relationship.note = comment ?? "";
|
||||
relationship.note = comment ?? "";
|
||||
|
||||
await client.relationship.update({
|
||||
where: { id: relationship.id },
|
||||
data: {
|
||||
note: relationship.note,
|
||||
},
|
||||
});
|
||||
await client.relationship.update({
|
||||
where: { id: relationship.id },
|
||||
data: {
|
||||
note: relationship.note,
|
||||
},
|
||||
});
|
||||
|
||||
return jsonResponse(relationshipToAPI(relationship));
|
||||
return jsonResponse(relationshipToAPI(relationship));
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,81 +1,81 @@
|
|||
import { apiRoute, applyConfig } from "@api";
|
||||
import { errorResponse, jsonResponse } from "@response";
|
||||
import { client } from "~database/datasource";
|
||||
import {
|
||||
createNewRelationship,
|
||||
relationshipToAPI,
|
||||
createNewRelationship,
|
||||
relationshipToAPI,
|
||||
} from "~database/entities/Relationship";
|
||||
import { getRelationshipToOtherUser } from "~database/entities/User";
|
||||
import { apiRoute, applyConfig } from "@api";
|
||||
import { client } from "~database/datasource";
|
||||
|
||||
export const meta = applyConfig({
|
||||
allowedMethods: ["POST"],
|
||||
ratelimits: {
|
||||
max: 30,
|
||||
duration: 60,
|
||||
},
|
||||
route: "/accounts/:id/pin",
|
||||
auth: {
|
||||
required: true,
|
||||
oauthPermissions: ["write:accounts"],
|
||||
},
|
||||
allowedMethods: ["POST"],
|
||||
ratelimits: {
|
||||
max: 30,
|
||||
duration: 60,
|
||||
},
|
||||
route: "/accounts/:id/pin",
|
||||
auth: {
|
||||
required: true,
|
||||
oauthPermissions: ["write:accounts"],
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Pin a user
|
||||
*/
|
||||
export default apiRoute(async (req, matchedRoute, extraData) => {
|
||||
const id = matchedRoute.params.id;
|
||||
const id = matchedRoute.params.id;
|
||||
|
||||
const { user: self } = extraData.auth;
|
||||
const { user: self } = extraData.auth;
|
||||
|
||||
if (!self) return errorResponse("Unauthorized", 401);
|
||||
if (!self) return errorResponse("Unauthorized", 401);
|
||||
|
||||
const user = await client.user.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
relationships: {
|
||||
include: {
|
||||
owner: true,
|
||||
subject: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
const user = await client.user.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
relationships: {
|
||||
include: {
|
||||
owner: true,
|
||||
subject: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!user) return errorResponse("User not found", 404);
|
||||
if (!user) return errorResponse("User not found", 404);
|
||||
|
||||
// Check if already following
|
||||
let relationship = await getRelationshipToOtherUser(self, user);
|
||||
// Check if already following
|
||||
let relationship = await getRelationshipToOtherUser(self, user);
|
||||
|
||||
if (!relationship) {
|
||||
// Create new relationship
|
||||
if (!relationship) {
|
||||
// Create new relationship
|
||||
|
||||
const newRelationship = await createNewRelationship(self, user);
|
||||
const newRelationship = await createNewRelationship(self, user);
|
||||
|
||||
await client.user.update({
|
||||
where: { id: self.id },
|
||||
data: {
|
||||
relationships: {
|
||||
connect: {
|
||||
id: newRelationship.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
await client.user.update({
|
||||
where: { id: self.id },
|
||||
data: {
|
||||
relationships: {
|
||||
connect: {
|
||||
id: newRelationship.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
relationship = newRelationship;
|
||||
}
|
||||
relationship = newRelationship;
|
||||
}
|
||||
|
||||
if (!relationship.endorsed) {
|
||||
relationship.endorsed = true;
|
||||
}
|
||||
if (!relationship.endorsed) {
|
||||
relationship.endorsed = true;
|
||||
}
|
||||
|
||||
await client.relationship.update({
|
||||
where: { id: relationship.id },
|
||||
data: {
|
||||
endorsed: true,
|
||||
},
|
||||
});
|
||||
await client.relationship.update({
|
||||
where: { id: relationship.id },
|
||||
data: {
|
||||
endorsed: true,
|
||||
},
|
||||
});
|
||||
|
||||
return jsonResponse(relationshipToAPI(relationship));
|
||||
return jsonResponse(relationshipToAPI(relationship));
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,95 +1,95 @@
|
|||
import { apiRoute, applyConfig } from "@api";
|
||||
import { errorResponse, jsonResponse } from "@response";
|
||||
import { client } from "~database/datasource";
|
||||
import {
|
||||
createNewRelationship,
|
||||
relationshipToAPI,
|
||||
createNewRelationship,
|
||||
relationshipToAPI,
|
||||
} from "~database/entities/Relationship";
|
||||
import { getRelationshipToOtherUser } from "~database/entities/User";
|
||||
import { apiRoute, applyConfig } from "@api";
|
||||
import { client } from "~database/datasource";
|
||||
|
||||
export const meta = applyConfig({
|
||||
allowedMethods: ["POST"],
|
||||
ratelimits: {
|
||||
max: 30,
|
||||
duration: 60,
|
||||
},
|
||||
route: "/accounts/:id/remove_from_followers",
|
||||
auth: {
|
||||
required: true,
|
||||
oauthPermissions: ["write:follows"],
|
||||
},
|
||||
allowedMethods: ["POST"],
|
||||
ratelimits: {
|
||||
max: 30,
|
||||
duration: 60,
|
||||
},
|
||||
route: "/accounts/:id/remove_from_followers",
|
||||
auth: {
|
||||
required: true,
|
||||
oauthPermissions: ["write:follows"],
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Removes an account from your followers list
|
||||
*/
|
||||
export default apiRoute(async (req, matchedRoute, extraData) => {
|
||||
const id = matchedRoute.params.id;
|
||||
const id = matchedRoute.params.id;
|
||||
|
||||
const { user: self } = extraData.auth;
|
||||
const { user: self } = extraData.auth;
|
||||
|
||||
if (!self) return errorResponse("Unauthorized", 401);
|
||||
if (!self) return errorResponse("Unauthorized", 401);
|
||||
|
||||
const user = await client.user.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
relationships: {
|
||||
include: {
|
||||
owner: true,
|
||||
subject: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
const user = await client.user.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
relationships: {
|
||||
include: {
|
||||
owner: true,
|
||||
subject: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!user) return errorResponse("User not found", 404);
|
||||
if (!user) return errorResponse("User not found", 404);
|
||||
|
||||
// Check if already following
|
||||
let relationship = await getRelationshipToOtherUser(self, user);
|
||||
// Check if already following
|
||||
let relationship = await getRelationshipToOtherUser(self, user);
|
||||
|
||||
if (!relationship) {
|
||||
// Create new relationship
|
||||
if (!relationship) {
|
||||
// Create new relationship
|
||||
|
||||
const newRelationship = await createNewRelationship(self, user);
|
||||
const newRelationship = await createNewRelationship(self, user);
|
||||
|
||||
await client.user.update({
|
||||
where: { id: self.id },
|
||||
data: {
|
||||
relationships: {
|
||||
connect: {
|
||||
id: newRelationship.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
await client.user.update({
|
||||
where: { id: self.id },
|
||||
data: {
|
||||
relationships: {
|
||||
connect: {
|
||||
id: newRelationship.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
relationship = newRelationship;
|
||||
}
|
||||
relationship = newRelationship;
|
||||
}
|
||||
|
||||
if (relationship.followedBy) {
|
||||
relationship.followedBy = false;
|
||||
}
|
||||
if (relationship.followedBy) {
|
||||
relationship.followedBy = false;
|
||||
}
|
||||
|
||||
await client.relationship.update({
|
||||
where: { id: relationship.id },
|
||||
data: {
|
||||
followedBy: false,
|
||||
},
|
||||
});
|
||||
await client.relationship.update({
|
||||
where: { id: relationship.id },
|
||||
data: {
|
||||
followedBy: false,
|
||||
},
|
||||
});
|
||||
|
||||
if (user.instanceId === null) {
|
||||
// Also remove from followers list
|
||||
await client.relationship.updateMany({
|
||||
where: {
|
||||
ownerId: user.id,
|
||||
subjectId: self.id,
|
||||
following: true,
|
||||
},
|
||||
data: {
|
||||
following: false,
|
||||
},
|
||||
});
|
||||
}
|
||||
if (user.instanceId === null) {
|
||||
// Also remove from followers list
|
||||
await client.relationship.updateMany({
|
||||
where: {
|
||||
ownerId: user.id,
|
||||
subjectId: self.id,
|
||||
following: true,
|
||||
},
|
||||
data: {
|
||||
following: false,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return jsonResponse(relationshipToAPI(relationship));
|
||||
return jsonResponse(relationshipToAPI(relationship));
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,134 +1,136 @@
|
|||
import { apiRoute, applyConfig } from "@api";
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||
import { errorResponse, jsonResponse } from "@response";
|
||||
import { statusToAPI } from "~database/entities/Status";
|
||||
import { apiRoute, applyConfig } from "@api";
|
||||
import { client } from "~database/datasource";
|
||||
import { statusToAPI } from "~database/entities/Status";
|
||||
import {
|
||||
userRelations,
|
||||
statusAndUserRelations,
|
||||
statusAndUserRelations,
|
||||
userRelations,
|
||||
} from "~database/entities/relations";
|
||||
|
||||
export const meta = applyConfig({
|
||||
allowedMethods: ["GET"],
|
||||
ratelimits: {
|
||||
max: 30,
|
||||
duration: 60,
|
||||
},
|
||||
route: "/accounts/:id/statuses",
|
||||
auth: {
|
||||
required: false,
|
||||
oauthPermissions: ["read:statuses"],
|
||||
},
|
||||
allowedMethods: ["GET"],
|
||||
ratelimits: {
|
||||
max: 30,
|
||||
duration: 60,
|
||||
},
|
||||
route: "/accounts/:id/statuses",
|
||||
auth: {
|
||||
required: false,
|
||||
oauthPermissions: ["read:statuses"],
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Fetch all statuses for a user
|
||||
*/
|
||||
export default apiRoute<{
|
||||
max_id?: string;
|
||||
since_id?: string;
|
||||
min_id?: string;
|
||||
limit?: string;
|
||||
only_media?: boolean;
|
||||
exclude_replies?: boolean;
|
||||
exclude_reblogs?: boolean;
|
||||
// TODO: Add with_muted
|
||||
pinned?: boolean;
|
||||
tagged?: string;
|
||||
max_id?: string;
|
||||
since_id?: string;
|
||||
min_id?: string;
|
||||
limit?: string;
|
||||
only_media?: boolean;
|
||||
exclude_replies?: boolean;
|
||||
exclude_reblogs?: boolean;
|
||||
// TODO: Add with_muted
|
||||
pinned?: boolean;
|
||||
tagged?: string;
|
||||
}>(async (req, matchedRoute, extraData) => {
|
||||
const id = matchedRoute.params.id;
|
||||
const id = matchedRoute.params.id;
|
||||
|
||||
// TODO: Add pinned
|
||||
const {
|
||||
max_id,
|
||||
min_id,
|
||||
since_id,
|
||||
limit = "20",
|
||||
exclude_reblogs,
|
||||
pinned,
|
||||
} = extraData.parsedRequest;
|
||||
// TODO: Add pinned
|
||||
const {
|
||||
max_id,
|
||||
min_id,
|
||||
since_id,
|
||||
limit = "20",
|
||||
exclude_reblogs,
|
||||
pinned,
|
||||
} = extraData.parsedRequest;
|
||||
|
||||
const user = await client.user.findUnique({
|
||||
where: { id },
|
||||
include: userRelations,
|
||||
});
|
||||
const user = await client.user.findUnique({
|
||||
where: { id },
|
||||
include: userRelations,
|
||||
});
|
||||
|
||||
if (!user) return errorResponse("User not found", 404);
|
||||
if (!user) return errorResponse("User not found", 404);
|
||||
|
||||
if (pinned) {
|
||||
const objects = await client.status.findMany({
|
||||
where: {
|
||||
authorId: id,
|
||||
isReblog: false,
|
||||
pinnedBy: {
|
||||
some: {
|
||||
id: user.id,
|
||||
},
|
||||
},
|
||||
id: {
|
||||
lt: max_id,
|
||||
gt: min_id,
|
||||
gte: since_id,
|
||||
},
|
||||
},
|
||||
include: statusAndUserRelations,
|
||||
take: Number(limit),
|
||||
orderBy: {
|
||||
id: "desc",
|
||||
},
|
||||
});
|
||||
if (pinned) {
|
||||
const objects = await client.status.findMany({
|
||||
where: {
|
||||
authorId: id,
|
||||
isReblog: false,
|
||||
pinnedBy: {
|
||||
some: {
|
||||
id: user.id,
|
||||
},
|
||||
},
|
||||
id: {
|
||||
lt: max_id,
|
||||
gt: min_id,
|
||||
gte: since_id,
|
||||
},
|
||||
},
|
||||
include: statusAndUserRelations,
|
||||
take: Number(limit),
|
||||
orderBy: {
|
||||
id: "desc",
|
||||
},
|
||||
});
|
||||
|
||||
// Constuct HTTP Link header (next and prev)
|
||||
const linkHeader = [];
|
||||
if (objects.length > 0) {
|
||||
const urlWithoutQuery = req.url.split("?")[0];
|
||||
linkHeader.push(
|
||||
`<${urlWithoutQuery}?max_id=${objects.at(-1)?.id}>; rel="next"`,
|
||||
`<${urlWithoutQuery}?min_id=${objects[0].id}>; rel="prev"`
|
||||
);
|
||||
}
|
||||
// Constuct HTTP Link header (next and prev)
|
||||
const linkHeader = [];
|
||||
if (objects.length > 0) {
|
||||
const urlWithoutQuery = req.url.split("?")[0];
|
||||
linkHeader.push(
|
||||
`<${urlWithoutQuery}?max_id=${objects.at(-1)?.id}>; rel="next"`,
|
||||
`<${urlWithoutQuery}?min_id=${objects[0].id}>; rel="prev"`,
|
||||
);
|
||||
}
|
||||
|
||||
return jsonResponse(
|
||||
await Promise.all(objects.map(status => statusToAPI(status, user))),
|
||||
200,
|
||||
{
|
||||
Link: linkHeader.join(", "),
|
||||
}
|
||||
);
|
||||
}
|
||||
return jsonResponse(
|
||||
await Promise.all(
|
||||
objects.map((status) => statusToAPI(status, user)),
|
||||
),
|
||||
200,
|
||||
{
|
||||
Link: linkHeader.join(", "),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
const objects = await client.status.findMany({
|
||||
where: {
|
||||
authorId: id,
|
||||
isReblog: exclude_reblogs ? true : undefined,
|
||||
id: {
|
||||
lt: max_id,
|
||||
gt: min_id,
|
||||
gte: since_id,
|
||||
},
|
||||
},
|
||||
include: statusAndUserRelations,
|
||||
take: Number(limit),
|
||||
orderBy: {
|
||||
id: "desc",
|
||||
},
|
||||
});
|
||||
const objects = await client.status.findMany({
|
||||
where: {
|
||||
authorId: id,
|
||||
isReblog: exclude_reblogs ? true : undefined,
|
||||
id: {
|
||||
lt: max_id,
|
||||
gt: min_id,
|
||||
gte: since_id,
|
||||
},
|
||||
},
|
||||
include: statusAndUserRelations,
|
||||
take: Number(limit),
|
||||
orderBy: {
|
||||
id: "desc",
|
||||
},
|
||||
});
|
||||
|
||||
// Constuct HTTP Link header (next and prev)
|
||||
const linkHeader = [];
|
||||
if (objects.length > 0) {
|
||||
const urlWithoutQuery = req.url.split("?")[0];
|
||||
linkHeader.push(
|
||||
`<${urlWithoutQuery}?max_id=${objects.at(-1)?.id}>; rel="next"`,
|
||||
`<${urlWithoutQuery}?min_id=${objects[0].id}>; rel="prev"`
|
||||
);
|
||||
}
|
||||
// Constuct HTTP Link header (next and prev)
|
||||
const linkHeader = [];
|
||||
if (objects.length > 0) {
|
||||
const urlWithoutQuery = req.url.split("?")[0];
|
||||
linkHeader.push(
|
||||
`<${urlWithoutQuery}?max_id=${objects.at(-1)?.id}>; rel="next"`,
|
||||
`<${urlWithoutQuery}?min_id=${objects[0].id}>; rel="prev"`,
|
||||
);
|
||||
}
|
||||
|
||||
return jsonResponse(
|
||||
await Promise.all(objects.map(status => statusToAPI(status, user))),
|
||||
200,
|
||||
{
|
||||
Link: linkHeader.join(", "),
|
||||
}
|
||||
);
|
||||
return jsonResponse(
|
||||
await Promise.all(objects.map((status) => statusToAPI(status, user))),
|
||||
200,
|
||||
{
|
||||
Link: linkHeader.join(", "),
|
||||
},
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,81 +1,81 @@
|
|||
import { apiRoute, applyConfig } from "@api";
|
||||
import { errorResponse, jsonResponse } from "@response";
|
||||
import { client } from "~database/datasource";
|
||||
import {
|
||||
createNewRelationship,
|
||||
relationshipToAPI,
|
||||
createNewRelationship,
|
||||
relationshipToAPI,
|
||||
} from "~database/entities/Relationship";
|
||||
import { getRelationshipToOtherUser } from "~database/entities/User";
|
||||
import { apiRoute, applyConfig } from "@api";
|
||||
import { client } from "~database/datasource";
|
||||
|
||||
export const meta = applyConfig({
|
||||
allowedMethods: ["POST"],
|
||||
ratelimits: {
|
||||
max: 30,
|
||||
duration: 60,
|
||||
},
|
||||
route: "/accounts/:id/unblock",
|
||||
auth: {
|
||||
required: true,
|
||||
oauthPermissions: ["write:blocks"],
|
||||
},
|
||||
allowedMethods: ["POST"],
|
||||
ratelimits: {
|
||||
max: 30,
|
||||
duration: 60,
|
||||
},
|
||||
route: "/accounts/:id/unblock",
|
||||
auth: {
|
||||
required: true,
|
||||
oauthPermissions: ["write:blocks"],
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Blocks a user
|
||||
*/
|
||||
export default apiRoute(async (req, matchedRoute, extraData) => {
|
||||
const id = matchedRoute.params.id;
|
||||
const id = matchedRoute.params.id;
|
||||
|
||||
const { user: self } = extraData.auth;
|
||||
const { user: self } = extraData.auth;
|
||||
|
||||
if (!self) return errorResponse("Unauthorized", 401);
|
||||
if (!self) return errorResponse("Unauthorized", 401);
|
||||
|
||||
const user = await client.user.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
relationships: {
|
||||
include: {
|
||||
owner: true,
|
||||
subject: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
const user = await client.user.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
relationships: {
|
||||
include: {
|
||||
owner: true,
|
||||
subject: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!user) return errorResponse("User not found", 404);
|
||||
if (!user) return errorResponse("User not found", 404);
|
||||
|
||||
// Check if already following
|
||||
let relationship = await getRelationshipToOtherUser(self, user);
|
||||
// Check if already following
|
||||
let relationship = await getRelationshipToOtherUser(self, user);
|
||||
|
||||
if (!relationship) {
|
||||
// Create new relationship
|
||||
if (!relationship) {
|
||||
// Create new relationship
|
||||
|
||||
const newRelationship = await createNewRelationship(self, user);
|
||||
const newRelationship = await createNewRelationship(self, user);
|
||||
|
||||
await client.user.update({
|
||||
where: { id: self.id },
|
||||
data: {
|
||||
relationships: {
|
||||
connect: {
|
||||
id: newRelationship.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
await client.user.update({
|
||||
where: { id: self.id },
|
||||
data: {
|
||||
relationships: {
|
||||
connect: {
|
||||
id: newRelationship.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
relationship = newRelationship;
|
||||
}
|
||||
relationship = newRelationship;
|
||||
}
|
||||
|
||||
if (relationship.blocking) {
|
||||
relationship.blocking = false;
|
||||
}
|
||||
if (relationship.blocking) {
|
||||
relationship.blocking = false;
|
||||
}
|
||||
|
||||
await client.relationship.update({
|
||||
where: { id: relationship.id },
|
||||
data: {
|
||||
blocking: false,
|
||||
},
|
||||
});
|
||||
await client.relationship.update({
|
||||
where: { id: relationship.id },
|
||||
data: {
|
||||
blocking: false,
|
||||
},
|
||||
});
|
||||
|
||||
return jsonResponse(relationshipToAPI(relationship));
|
||||
return jsonResponse(relationshipToAPI(relationship));
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,81 +1,81 @@
|
|||
import { apiRoute, applyConfig } from "@api";
|
||||
import { errorResponse, jsonResponse } from "@response";
|
||||
import { client } from "~database/datasource";
|
||||
import {
|
||||
createNewRelationship,
|
||||
relationshipToAPI,
|
||||
createNewRelationship,
|
||||
relationshipToAPI,
|
||||
} from "~database/entities/Relationship";
|
||||
import { getRelationshipToOtherUser } from "~database/entities/User";
|
||||
import { apiRoute, applyConfig } from "@api";
|
||||
import { client } from "~database/datasource";
|
||||
|
||||
export const meta = applyConfig({
|
||||
allowedMethods: ["POST"],
|
||||
ratelimits: {
|
||||
max: 30,
|
||||
duration: 60,
|
||||
},
|
||||
route: "/accounts/:id/unfollow",
|
||||
auth: {
|
||||
required: true,
|
||||
oauthPermissions: ["write:follows"],
|
||||
},
|
||||
allowedMethods: ["POST"],
|
||||
ratelimits: {
|
||||
max: 30,
|
||||
duration: 60,
|
||||
},
|
||||
route: "/accounts/:id/unfollow",
|
||||
auth: {
|
||||
required: true,
|
||||
oauthPermissions: ["write:follows"],
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Unfollows a user
|
||||
*/
|
||||
export default apiRoute(async (req, matchedRoute, extraData) => {
|
||||
const id = matchedRoute.params.id;
|
||||
const id = matchedRoute.params.id;
|
||||
|
||||
const { user: self } = extraData.auth;
|
||||
const { user: self } = extraData.auth;
|
||||
|
||||
if (!self) return errorResponse("Unauthorized", 401);
|
||||
if (!self) return errorResponse("Unauthorized", 401);
|
||||
|
||||
const user = await client.user.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
relationships: {
|
||||
include: {
|
||||
owner: true,
|
||||
subject: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
const user = await client.user.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
relationships: {
|
||||
include: {
|
||||
owner: true,
|
||||
subject: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!user) return errorResponse("User not found", 404);
|
||||
if (!user) return errorResponse("User not found", 404);
|
||||
|
||||
// Check if already following
|
||||
let relationship = await getRelationshipToOtherUser(self, user);
|
||||
// Check if already following
|
||||
let relationship = await getRelationshipToOtherUser(self, user);
|
||||
|
||||
if (!relationship) {
|
||||
// Create new relationship
|
||||
if (!relationship) {
|
||||
// Create new relationship
|
||||
|
||||
const newRelationship = await createNewRelationship(self, user);
|
||||
const newRelationship = await createNewRelationship(self, user);
|
||||
|
||||
await client.user.update({
|
||||
where: { id: self.id },
|
||||
data: {
|
||||
relationships: {
|
||||
connect: {
|
||||
id: newRelationship.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
await client.user.update({
|
||||
where: { id: self.id },
|
||||
data: {
|
||||
relationships: {
|
||||
connect: {
|
||||
id: newRelationship.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
relationship = newRelationship;
|
||||
}
|
||||
relationship = newRelationship;
|
||||
}
|
||||
|
||||
if (relationship.following) {
|
||||
relationship.following = false;
|
||||
}
|
||||
if (relationship.following) {
|
||||
relationship.following = false;
|
||||
}
|
||||
|
||||
await client.relationship.update({
|
||||
where: { id: relationship.id },
|
||||
data: {
|
||||
following: false,
|
||||
},
|
||||
});
|
||||
await client.relationship.update({
|
||||
where: { id: relationship.id },
|
||||
data: {
|
||||
following: false,
|
||||
},
|
||||
});
|
||||
|
||||
return jsonResponse(relationshipToAPI(relationship));
|
||||
return jsonResponse(relationshipToAPI(relationship));
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,83 +1,83 @@
|
|||
import { apiRoute, applyConfig } from "@api";
|
||||
import { errorResponse, jsonResponse } from "@response";
|
||||
import { client } from "~database/datasource";
|
||||
import {
|
||||
createNewRelationship,
|
||||
relationshipToAPI,
|
||||
createNewRelationship,
|
||||
relationshipToAPI,
|
||||
} from "~database/entities/Relationship";
|
||||
import { getRelationshipToOtherUser } from "~database/entities/User";
|
||||
import { apiRoute, applyConfig } from "@api";
|
||||
import { client } from "~database/datasource";
|
||||
|
||||
export const meta = applyConfig({
|
||||
allowedMethods: ["POST"],
|
||||
ratelimits: {
|
||||
max: 30,
|
||||
duration: 60,
|
||||
},
|
||||
route: "/accounts/:id/unmute",
|
||||
auth: {
|
||||
required: true,
|
||||
oauthPermissions: ["write:mutes"],
|
||||
},
|
||||
allowedMethods: ["POST"],
|
||||
ratelimits: {
|
||||
max: 30,
|
||||
duration: 60,
|
||||
},
|
||||
route: "/accounts/:id/unmute",
|
||||
auth: {
|
||||
required: true,
|
||||
oauthPermissions: ["write:mutes"],
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Unmute a user
|
||||
*/
|
||||
export default apiRoute(async (req, matchedRoute, extraData) => {
|
||||
const id = matchedRoute.params.id;
|
||||
const id = matchedRoute.params.id;
|
||||
|
||||
const { user: self } = extraData.auth;
|
||||
const { user: self } = extraData.auth;
|
||||
|
||||
if (!self) return errorResponse("Unauthorized", 401);
|
||||
if (!self) return errorResponse("Unauthorized", 401);
|
||||
|
||||
const user = await client.user.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
relationships: {
|
||||
include: {
|
||||
owner: true,
|
||||
subject: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
const user = await client.user.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
relationships: {
|
||||
include: {
|
||||
owner: true,
|
||||
subject: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!user) return errorResponse("User not found", 404);
|
||||
if (!user) return errorResponse("User not found", 404);
|
||||
|
||||
// Check if already following
|
||||
let relationship = await getRelationshipToOtherUser(self, user);
|
||||
// Check if already following
|
||||
let relationship = await getRelationshipToOtherUser(self, user);
|
||||
|
||||
if (!relationship) {
|
||||
// Create new relationship
|
||||
if (!relationship) {
|
||||
// Create new relationship
|
||||
|
||||
const newRelationship = await createNewRelationship(self, user);
|
||||
const newRelationship = await createNewRelationship(self, user);
|
||||
|
||||
await client.user.update({
|
||||
where: { id: self.id },
|
||||
data: {
|
||||
relationships: {
|
||||
connect: {
|
||||
id: newRelationship.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
await client.user.update({
|
||||
where: { id: self.id },
|
||||
data: {
|
||||
relationships: {
|
||||
connect: {
|
||||
id: newRelationship.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
relationship = newRelationship;
|
||||
}
|
||||
relationship = newRelationship;
|
||||
}
|
||||
|
||||
if (relationship.muting) {
|
||||
relationship.muting = false;
|
||||
}
|
||||
if (relationship.muting) {
|
||||
relationship.muting = false;
|
||||
}
|
||||
|
||||
// TODO: Implement duration
|
||||
// TODO: Implement duration
|
||||
|
||||
await client.relationship.update({
|
||||
where: { id: relationship.id },
|
||||
data: {
|
||||
muting: false,
|
||||
},
|
||||
});
|
||||
await client.relationship.update({
|
||||
where: { id: relationship.id },
|
||||
data: {
|
||||
muting: false,
|
||||
},
|
||||
});
|
||||
|
||||
return jsonResponse(relationshipToAPI(relationship));
|
||||
return jsonResponse(relationshipToAPI(relationship));
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,81 +1,81 @@
|
|||
import { apiRoute, applyConfig } from "@api";
|
||||
import { errorResponse, jsonResponse } from "@response";
|
||||
import { client } from "~database/datasource";
|
||||
import {
|
||||
createNewRelationship,
|
||||
relationshipToAPI,
|
||||
createNewRelationship,
|
||||
relationshipToAPI,
|
||||
} from "~database/entities/Relationship";
|
||||
import { getRelationshipToOtherUser } from "~database/entities/User";
|
||||
import { apiRoute, applyConfig } from "@api";
|
||||
import { client } from "~database/datasource";
|
||||
|
||||
export const meta = applyConfig({
|
||||
allowedMethods: ["POST"],
|
||||
ratelimits: {
|
||||
max: 30,
|
||||
duration: 60,
|
||||
},
|
||||
route: "/accounts/:id/unpin",
|
||||
auth: {
|
||||
required: true,
|
||||
oauthPermissions: ["write:accounts"],
|
||||
},
|
||||
allowedMethods: ["POST"],
|
||||
ratelimits: {
|
||||
max: 30,
|
||||
duration: 60,
|
||||
},
|
||||
route: "/accounts/:id/unpin",
|
||||
auth: {
|
||||
required: true,
|
||||
oauthPermissions: ["write:accounts"],
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Unpin a user
|
||||
*/
|
||||
export default apiRoute(async (req, matchedRoute, extraData) => {
|
||||
const id = matchedRoute.params.id;
|
||||
const id = matchedRoute.params.id;
|
||||
|
||||
const { user: self } = extraData.auth;
|
||||
const { user: self } = extraData.auth;
|
||||
|
||||
if (!self) return errorResponse("Unauthorized", 401);
|
||||
if (!self) return errorResponse("Unauthorized", 401);
|
||||
|
||||
const user = await client.user.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
relationships: {
|
||||
include: {
|
||||
owner: true,
|
||||
subject: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
const user = await client.user.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
relationships: {
|
||||
include: {
|
||||
owner: true,
|
||||
subject: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!user) return errorResponse("User not found", 404);
|
||||
if (!user) return errorResponse("User not found", 404);
|
||||
|
||||
// Check if already following
|
||||
let relationship = await getRelationshipToOtherUser(self, user);
|
||||
// Check if already following
|
||||
let relationship = await getRelationshipToOtherUser(self, user);
|
||||
|
||||
if (!relationship) {
|
||||
// Create new relationship
|
||||
if (!relationship) {
|
||||
// Create new relationship
|
||||
|
||||
const newRelationship = await createNewRelationship(self, user);
|
||||
const newRelationship = await createNewRelationship(self, user);
|
||||
|
||||
await client.user.update({
|
||||
where: { id: self.id },
|
||||
data: {
|
||||
relationships: {
|
||||
connect: {
|
||||
id: newRelationship.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
await client.user.update({
|
||||
where: { id: self.id },
|
||||
data: {
|
||||
relationships: {
|
||||
connect: {
|
||||
id: newRelationship.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
relationship = newRelationship;
|
||||
}
|
||||
relationship = newRelationship;
|
||||
}
|
||||
|
||||
if (relationship.endorsed) {
|
||||
relationship.endorsed = false;
|
||||
}
|
||||
if (relationship.endorsed) {
|
||||
relationship.endorsed = false;
|
||||
}
|
||||
|
||||
await client.relationship.update({
|
||||
where: { id: relationship.id },
|
||||
data: {
|
||||
endorsed: false,
|
||||
},
|
||||
});
|
||||
await client.relationship.update({
|
||||
where: { id: relationship.id },
|
||||
data: {
|
||||
endorsed: false,
|
||||
},
|
||||
});
|
||||
|
||||
return jsonResponse(relationshipToAPI(relationship));
|
||||
return jsonResponse(relationshipToAPI(relationship));
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,67 +1,67 @@
|
|||
import { errorResponse, jsonResponse } from "@response";
|
||||
import { userToAPI } from "~database/entities/User";
|
||||
import { apiRoute, applyConfig } from "@api";
|
||||
import { errorResponse, jsonResponse } from "@response";
|
||||
import { client } from "~database/datasource";
|
||||
import { userToAPI } from "~database/entities/User";
|
||||
import { userRelations } from "~database/entities/relations";
|
||||
|
||||
export const meta = applyConfig({
|
||||
allowedMethods: ["GET"],
|
||||
route: "/api/v1/accounts/familiar_followers",
|
||||
ratelimits: {
|
||||
max: 5,
|
||||
duration: 60,
|
||||
},
|
||||
auth: {
|
||||
required: true,
|
||||
oauthPermissions: ["read:follows"],
|
||||
},
|
||||
allowedMethods: ["GET"],
|
||||
route: "/api/v1/accounts/familiar_followers",
|
||||
ratelimits: {
|
||||
max: 5,
|
||||
duration: 60,
|
||||
},
|
||||
auth: {
|
||||
required: true,
|
||||
oauthPermissions: ["read:follows"],
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Find familiar followers (followers of a user that you also follow)
|
||||
*/
|
||||
export default apiRoute<{
|
||||
id: string[];
|
||||
id: string[];
|
||||
}>(async (req, matchedRoute, extraData) => {
|
||||
const { user: self } = extraData.auth;
|
||||
const { user: self } = extraData.auth;
|
||||
|
||||
if (!self) return errorResponse("Unauthorized", 401);
|
||||
if (!self) return errorResponse("Unauthorized", 401);
|
||||
|
||||
const { id: ids } = extraData.parsedRequest;
|
||||
const { id: ids } = extraData.parsedRequest;
|
||||
|
||||
// Minimum id count 1, maximum 10
|
||||
if (!ids || ids.length < 1 || ids.length > 10) {
|
||||
return errorResponse("Number of ids must be between 1 and 10", 422);
|
||||
}
|
||||
// Minimum id count 1, maximum 10
|
||||
if (!ids || ids.length < 1 || ids.length > 10) {
|
||||
return errorResponse("Number of ids must be between 1 and 10", 422);
|
||||
}
|
||||
|
||||
const followersOfIds = await client.user.findMany({
|
||||
where: {
|
||||
relationships: {
|
||||
some: {
|
||||
subjectId: {
|
||||
in: ids,
|
||||
},
|
||||
following: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
const followersOfIds = await client.user.findMany({
|
||||
where: {
|
||||
relationships: {
|
||||
some: {
|
||||
subjectId: {
|
||||
in: ids,
|
||||
},
|
||||
following: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Find users that you follow in followersOfIds
|
||||
const output = await client.user.findMany({
|
||||
where: {
|
||||
relationships: {
|
||||
some: {
|
||||
ownerId: self.id,
|
||||
subjectId: {
|
||||
in: followersOfIds.map(f => f.id),
|
||||
},
|
||||
following: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
include: userRelations,
|
||||
});
|
||||
// Find users that you follow in followersOfIds
|
||||
const output = await client.user.findMany({
|
||||
where: {
|
||||
relationships: {
|
||||
some: {
|
||||
ownerId: self.id,
|
||||
subjectId: {
|
||||
in: followersOfIds.map((f) => f.id),
|
||||
},
|
||||
following: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
include: userRelations,
|
||||
});
|
||||
|
||||
return jsonResponse(output.map(o => userToAPI(o)));
|
||||
return jsonResponse(output.map((o) => userToAPI(o)));
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,202 +1,206 @@
|
|||
import { apiRoute, applyConfig } from "@api";
|
||||
import { jsonResponse } from "@response";
|
||||
import { tempmailDomains } from "@tempmail";
|
||||
import { apiRoute, applyConfig } from "@api";
|
||||
import ISO6391 from "iso-639-1";
|
||||
import { client } from "~database/datasource";
|
||||
import { createNewLocalUser } from "~database/entities/User";
|
||||
import ISO6391 from "iso-639-1";
|
||||
|
||||
export const meta = applyConfig({
|
||||
allowedMethods: ["POST"],
|
||||
route: "/api/v1/accounts",
|
||||
ratelimits: {
|
||||
max: 2,
|
||||
duration: 60,
|
||||
},
|
||||
auth: {
|
||||
required: false,
|
||||
oauthPermissions: ["write:accounts"],
|
||||
},
|
||||
allowedMethods: ["POST"],
|
||||
route: "/api/v1/accounts",
|
||||
ratelimits: {
|
||||
max: 2,
|
||||
duration: 60,
|
||||
},
|
||||
auth: {
|
||||
required: false,
|
||||
oauthPermissions: ["write:accounts"],
|
||||
},
|
||||
});
|
||||
|
||||
export default apiRoute<{
|
||||
username: string;
|
||||
email: string;
|
||||
password: string;
|
||||
agreement: boolean;
|
||||
locale: string;
|
||||
reason: string;
|
||||
username: string;
|
||||
email: string;
|
||||
password: string;
|
||||
agreement: boolean;
|
||||
locale: string;
|
||||
reason: string;
|
||||
}>(async (req, matchedRoute, extraData) => {
|
||||
// TODO: Add Authorization check
|
||||
// TODO: Add Authorization check
|
||||
|
||||
const body = extraData.parsedRequest;
|
||||
const body = extraData.parsedRequest;
|
||||
|
||||
const config = await extraData.configManager.getConfig();
|
||||
const config = await extraData.configManager.getConfig();
|
||||
|
||||
if (!config.signups.registration) {
|
||||
return jsonResponse(
|
||||
{
|
||||
error: "Registration is disabled",
|
||||
},
|
||||
422
|
||||
);
|
||||
}
|
||||
if (!config.signups.registration) {
|
||||
return jsonResponse(
|
||||
{
|
||||
error: "Registration is disabled",
|
||||
},
|
||||
422,
|
||||
);
|
||||
}
|
||||
|
||||
const errors: {
|
||||
details: Record<
|
||||
string,
|
||||
{
|
||||
error:
|
||||
| "ERR_BLANK"
|
||||
| "ERR_INVALID"
|
||||
| "ERR_TOO_LONG"
|
||||
| "ERR_TOO_SHORT"
|
||||
| "ERR_BLOCKED"
|
||||
| "ERR_TAKEN"
|
||||
| "ERR_RESERVED"
|
||||
| "ERR_ACCEPTED"
|
||||
| "ERR_INCLUSION";
|
||||
description: string;
|
||||
}[]
|
||||
>;
|
||||
} = {
|
||||
details: {
|
||||
password: [],
|
||||
username: [],
|
||||
email: [],
|
||||
agreement: [],
|
||||
locale: [],
|
||||
reason: [],
|
||||
},
|
||||
};
|
||||
const errors: {
|
||||
details: Record<
|
||||
string,
|
||||
{
|
||||
error:
|
||||
| "ERR_BLANK"
|
||||
| "ERR_INVALID"
|
||||
| "ERR_TOO_LONG"
|
||||
| "ERR_TOO_SHORT"
|
||||
| "ERR_BLOCKED"
|
||||
| "ERR_TAKEN"
|
||||
| "ERR_RESERVED"
|
||||
| "ERR_ACCEPTED"
|
||||
| "ERR_INCLUSION";
|
||||
description: string;
|
||||
}[]
|
||||
>;
|
||||
} = {
|
||||
details: {
|
||||
password: [],
|
||||
username: [],
|
||||
email: [],
|
||||
agreement: [],
|
||||
locale: [],
|
||||
reason: [],
|
||||
},
|
||||
};
|
||||
|
||||
// Check if fields are blank
|
||||
["username", "email", "password", "agreement", "locale", "reason"].forEach(
|
||||
value => {
|
||||
// @ts-expect-error Value is always valid
|
||||
if (!body[value])
|
||||
errors.details[value].push({
|
||||
error: "ERR_BLANK",
|
||||
description: `can't be blank`,
|
||||
});
|
||||
}
|
||||
);
|
||||
// Check if fields are blank
|
||||
for (const value of [
|
||||
"username",
|
||||
"email",
|
||||
"password",
|
||||
"agreement",
|
||||
"locale",
|
||||
"reason",
|
||||
]) {
|
||||
// @ts-expect-error We don't care about typing here
|
||||
if (!body[value]) {
|
||||
errors.details[value].push({
|
||||
error: "ERR_BLANK",
|
||||
description: `can't be blank`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Check if username is valid
|
||||
if (!body.username?.match(/^[a-zA-Z0-9_]+$/))
|
||||
errors.details.username.push({
|
||||
error: "ERR_INVALID",
|
||||
description: `must only contain letters, numbers, and underscores`,
|
||||
});
|
||||
// Check if username is valid
|
||||
if (!body.username?.match(/^[a-zA-Z0-9_]+$/))
|
||||
errors.details.username.push({
|
||||
error: "ERR_INVALID",
|
||||
description: "must only contain letters, numbers, and underscores",
|
||||
});
|
||||
|
||||
// Check if username doesnt match filters
|
||||
if (
|
||||
config.filters.username_filters.some(filter =>
|
||||
body.username?.match(filter)
|
||||
)
|
||||
) {
|
||||
errors.details.username.push({
|
||||
error: "ERR_INVALID",
|
||||
description: `contains blocked words`,
|
||||
});
|
||||
}
|
||||
// Check if username doesnt match filters
|
||||
if (
|
||||
config.filters.username.some((filter) => body.username?.match(filter))
|
||||
) {
|
||||
errors.details.username.push({
|
||||
error: "ERR_INVALID",
|
||||
description: "contains blocked words",
|
||||
});
|
||||
}
|
||||
|
||||
// Check if username is too long
|
||||
if ((body.username?.length ?? 0) > config.validation.max_username_size)
|
||||
errors.details.username.push({
|
||||
error: "ERR_TOO_LONG",
|
||||
description: `is too long (maximum is ${config.validation.max_username_size} characters)`,
|
||||
});
|
||||
// Check if username is too long
|
||||
if ((body.username?.length ?? 0) > config.validation.max_username_size)
|
||||
errors.details.username.push({
|
||||
error: "ERR_TOO_LONG",
|
||||
description: `is too long (maximum is ${config.validation.max_username_size} characters)`,
|
||||
});
|
||||
|
||||
// Check if username is too short
|
||||
if ((body.username?.length ?? 0) < 3)
|
||||
errors.details.username.push({
|
||||
error: "ERR_TOO_SHORT",
|
||||
description: `is too short (minimum is 3 characters)`,
|
||||
});
|
||||
// Check if username is too short
|
||||
if ((body.username?.length ?? 0) < 3)
|
||||
errors.details.username.push({
|
||||
error: "ERR_TOO_SHORT",
|
||||
description: "is too short (minimum is 3 characters)",
|
||||
});
|
||||
|
||||
// Check if username is reserved
|
||||
if (config.validation.username_blacklist.includes(body.username ?? ""))
|
||||
errors.details.username.push({
|
||||
error: "ERR_RESERVED",
|
||||
description: `is reserved`,
|
||||
});
|
||||
// Check if username is reserved
|
||||
if (config.validation.username_blacklist.includes(body.username ?? ""))
|
||||
errors.details.username.push({
|
||||
error: "ERR_RESERVED",
|
||||
description: "is reserved",
|
||||
});
|
||||
|
||||
// Check if username is taken
|
||||
if (await client.user.findFirst({ where: { username: body.username } }))
|
||||
errors.details.username.push({
|
||||
error: "ERR_TAKEN",
|
||||
description: `is already taken`,
|
||||
});
|
||||
// Check if username is taken
|
||||
if (await client.user.findFirst({ where: { username: body.username } }))
|
||||
errors.details.username.push({
|
||||
error: "ERR_TAKEN",
|
||||
description: "is already taken",
|
||||
});
|
||||
|
||||
// Check if email is valid
|
||||
if (
|
||||
!body.email?.match(
|
||||
/^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
|
||||
)
|
||||
)
|
||||
errors.details.email.push({
|
||||
error: "ERR_INVALID",
|
||||
description: `must be a valid email address`,
|
||||
});
|
||||
// Check if email is valid
|
||||
if (
|
||||
!body.email?.match(
|
||||
/^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/,
|
||||
)
|
||||
)
|
||||
errors.details.email.push({
|
||||
error: "ERR_INVALID",
|
||||
description: "must be a valid email address",
|
||||
});
|
||||
|
||||
// Check if email is blocked
|
||||
if (
|
||||
config.validation.email_blacklist.includes(body.email ?? "") ||
|
||||
(config.validation.blacklist_tempmail &&
|
||||
tempmailDomains.domains.includes((body.email ?? "").split("@")[1]))
|
||||
)
|
||||
errors.details.email.push({
|
||||
error: "ERR_BLOCKED",
|
||||
description: `is from a blocked email provider`,
|
||||
});
|
||||
// Check if email is blocked
|
||||
if (
|
||||
config.validation.email_blacklist.includes(body.email ?? "") ||
|
||||
(config.validation.blacklist_tempmail &&
|
||||
tempmailDomains.domains.includes((body.email ?? "").split("@")[1]))
|
||||
)
|
||||
errors.details.email.push({
|
||||
error: "ERR_BLOCKED",
|
||||
description: "is from a blocked email provider",
|
||||
});
|
||||
|
||||
// Check if agreement is accepted
|
||||
if (!body.agreement)
|
||||
errors.details.agreement.push({
|
||||
error: "ERR_ACCEPTED",
|
||||
description: `must be accepted`,
|
||||
});
|
||||
// Check if agreement is accepted
|
||||
if (!body.agreement)
|
||||
errors.details.agreement.push({
|
||||
error: "ERR_ACCEPTED",
|
||||
description: "must be accepted",
|
||||
});
|
||||
|
||||
if (!body.locale)
|
||||
errors.details.locale.push({
|
||||
error: "ERR_BLANK",
|
||||
description: `can't be blank`,
|
||||
});
|
||||
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 (!ISO6391.validate(body.locale ?? ""))
|
||||
errors.details.locale.push({
|
||||
error: "ERR_INVALID",
|
||||
description: "must be a valid ISO 639-1 code",
|
||||
});
|
||||
|
||||
// If any errors are present, return them
|
||||
if (Object.values(errors.details).some(value => value.length > 0)) {
|
||||
// Error is something like "Validation failed: Password can't be blank, Username must contain only letters, numbers and underscores, Agreement must be accepted"
|
||||
// If any errors are present, return them
|
||||
if (Object.values(errors.details).some((value) => value.length > 0)) {
|
||||
// Error is something like "Validation failed: Password can't be blank, Username must contain only letters, numbers and underscores, Agreement must be accepted"
|
||||
|
||||
const errorsText = Object.entries(errors.details)
|
||||
.map(
|
||||
([name, errors]) =>
|
||||
`${name} ${errors
|
||||
.map(error => error.description)
|
||||
.join(", ")}`
|
||||
)
|
||||
.join(", ");
|
||||
return jsonResponse(
|
||||
{
|
||||
error: `Validation failed: ${errorsText}`,
|
||||
details: errors.details,
|
||||
},
|
||||
422
|
||||
);
|
||||
}
|
||||
const errorsText = Object.entries(errors.details)
|
||||
.map(
|
||||
([name, errors]) =>
|
||||
`${name} ${errors
|
||||
.map((error) => error.description)
|
||||
.join(", ")}`,
|
||||
)
|
||||
.join(", ");
|
||||
return jsonResponse(
|
||||
{
|
||||
error: `Validation failed: ${errorsText}`,
|
||||
details: errors.details,
|
||||
},
|
||||
422,
|
||||
);
|
||||
}
|
||||
|
||||
await createNewLocalUser({
|
||||
username: body.username ?? "",
|
||||
password: body.password ?? "",
|
||||
email: body.email ?? "",
|
||||
});
|
||||
await createNewLocalUser({
|
||||
username: body.username ?? "",
|
||||
password: body.password ?? "",
|
||||
email: body.email ?? "",
|
||||
});
|
||||
|
||||
return new Response("", {
|
||||
status: 200,
|
||||
});
|
||||
return new Response("", {
|
||||
status: 200,
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,66 +1,67 @@
|
|||
import { errorResponse, jsonResponse } from "@response";
|
||||
import {
|
||||
createNewRelationship,
|
||||
relationshipToAPI,
|
||||
} from "~database/entities/Relationship";
|
||||
import { apiRoute, applyConfig } from "@api";
|
||||
import type { User } from "@prisma/client";
|
||||
import { errorResponse, jsonResponse } from "@response";
|
||||
import { client } from "~database/datasource";
|
||||
import {
|
||||
createNewRelationship,
|
||||
relationshipToAPI,
|
||||
} from "~database/entities/Relationship";
|
||||
|
||||
export const meta = applyConfig({
|
||||
allowedMethods: ["GET"],
|
||||
route: "/api/v1/accounts/relationships",
|
||||
ratelimits: {
|
||||
max: 30,
|
||||
duration: 60,
|
||||
},
|
||||
auth: {
|
||||
required: true,
|
||||
oauthPermissions: ["read:follows"],
|
||||
},
|
||||
allowedMethods: ["GET"],
|
||||
route: "/api/v1/accounts/relationships",
|
||||
ratelimits: {
|
||||
max: 30,
|
||||
duration: 60,
|
||||
},
|
||||
auth: {
|
||||
required: true,
|
||||
oauthPermissions: ["read:follows"],
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Find relationships
|
||||
*/
|
||||
export default apiRoute<{
|
||||
id: string[];
|
||||
id: string[];
|
||||
}>(async (req, matchedRoute, extraData) => {
|
||||
const { user: self } = extraData.auth;
|
||||
const { user: self } = extraData.auth;
|
||||
|
||||
if (!self) return errorResponse("Unauthorized", 401);
|
||||
if (!self) return errorResponse("Unauthorized", 401);
|
||||
|
||||
const { id: ids } = extraData.parsedRequest;
|
||||
const { id: ids } = extraData.parsedRequest;
|
||||
|
||||
// Minimum id count 1, maximum 10
|
||||
if (!ids || ids.length < 1 || ids.length > 10) {
|
||||
return errorResponse("Number of ids must be between 1 and 10", 422);
|
||||
}
|
||||
// Minimum id count 1, maximum 10
|
||||
if (!ids || ids.length < 1 || ids.length > 10) {
|
||||
return errorResponse("Number of ids must be between 1 and 10", 422);
|
||||
}
|
||||
|
||||
const relationships = await client.relationship.findMany({
|
||||
where: {
|
||||
ownerId: self.id,
|
||||
subjectId: {
|
||||
in: ids,
|
||||
},
|
||||
},
|
||||
});
|
||||
const relationships = await client.relationship.findMany({
|
||||
where: {
|
||||
ownerId: self.id,
|
||||
subjectId: {
|
||||
in: ids,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Find IDs that dont have a relationship
|
||||
const missingIds = ids.filter(
|
||||
id => !relationships.some(r => r.subjectId === id)
|
||||
);
|
||||
// Find IDs that dont have a relationship
|
||||
const missingIds = ids.filter(
|
||||
(id) => !relationships.some((r) => r.subjectId === id),
|
||||
);
|
||||
|
||||
// Create the missing relationships
|
||||
for (const id of missingIds) {
|
||||
const relationship = await createNewRelationship(self, { id } as any);
|
||||
// Create the missing relationships
|
||||
for (const id of missingIds) {
|
||||
const relationship = await createNewRelationship(self, { id } as User);
|
||||
|
||||
relationships.push(relationship);
|
||||
}
|
||||
relationships.push(relationship);
|
||||
}
|
||||
|
||||
// Order in the same order as ids
|
||||
relationships.sort(
|
||||
(a, b) => ids.indexOf(a.subjectId) - ids.indexOf(b.subjectId)
|
||||
);
|
||||
// Order in the same order as ids
|
||||
relationships.sort(
|
||||
(a, b) => ids.indexOf(a.subjectId) - ids.indexOf(b.subjectId),
|
||||
);
|
||||
|
||||
return jsonResponse(relationships.map(r => relationshipToAPI(r)));
|
||||
return jsonResponse(relationships.map((r) => relationshipToAPI(r)));
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,75 +1,75 @@
|
|||
import { errorResponse, jsonResponse } from "@response";
|
||||
import { userToAPI } from "~database/entities/User";
|
||||
import { apiRoute, applyConfig } from "@api";
|
||||
import { errorResponse, jsonResponse } from "@response";
|
||||
import { client } from "~database/datasource";
|
||||
import { userToAPI } from "~database/entities/User";
|
||||
import { userRelations } from "~database/entities/relations";
|
||||
|
||||
export const meta = applyConfig({
|
||||
allowedMethods: ["GET"],
|
||||
route: "/api/v1/accounts/search",
|
||||
ratelimits: {
|
||||
max: 100,
|
||||
duration: 60,
|
||||
},
|
||||
auth: {
|
||||
required: true,
|
||||
oauthPermissions: ["read:accounts"],
|
||||
},
|
||||
allowedMethods: ["GET"],
|
||||
route: "/api/v1/accounts/search",
|
||||
ratelimits: {
|
||||
max: 100,
|
||||
duration: 60,
|
||||
},
|
||||
auth: {
|
||||
required: true,
|
||||
oauthPermissions: ["read:accounts"],
|
||||
},
|
||||
});
|
||||
|
||||
export default apiRoute<{
|
||||
q?: string;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
resolve?: boolean;
|
||||
following?: boolean;
|
||||
q?: string;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
resolve?: boolean;
|
||||
following?: boolean;
|
||||
}>(async (req, matchedRoute, extraData) => {
|
||||
// TODO: Add checks for disabled or not email verified accounts
|
||||
// TODO: Add checks for disabled or not email verified accounts
|
||||
|
||||
const { user } = extraData.auth;
|
||||
const { user } = extraData.auth;
|
||||
|
||||
if (!user) return errorResponse("Unauthorized", 401);
|
||||
if (!user) return errorResponse("Unauthorized", 401);
|
||||
|
||||
const {
|
||||
following = false,
|
||||
limit = 40,
|
||||
offset,
|
||||
q,
|
||||
} = extraData.parsedRequest;
|
||||
const {
|
||||
following = false,
|
||||
limit = 40,
|
||||
offset,
|
||||
q,
|
||||
} = extraData.parsedRequest;
|
||||
|
||||
if (limit < 1 || limit > 80) {
|
||||
return errorResponse("Limit must be between 1 and 80", 400);
|
||||
}
|
||||
if (limit < 1 || limit > 80) {
|
||||
return errorResponse("Limit must be between 1 and 80", 400);
|
||||
}
|
||||
|
||||
// TODO: Add WebFinger resolve
|
||||
// TODO: Add WebFinger resolve
|
||||
|
||||
const accounts = await client.user.findMany({
|
||||
where: {
|
||||
OR: [
|
||||
{
|
||||
displayName: {
|
||||
contains: q,
|
||||
},
|
||||
},
|
||||
{
|
||||
username: {
|
||||
contains: q,
|
||||
},
|
||||
},
|
||||
],
|
||||
relationshipSubjects: following
|
||||
? {
|
||||
some: {
|
||||
ownerId: user.id,
|
||||
following,
|
||||
},
|
||||
}
|
||||
: undefined,
|
||||
},
|
||||
take: Number(limit),
|
||||
skip: Number(offset || 0),
|
||||
include: userRelations,
|
||||
});
|
||||
const accounts = await client.user.findMany({
|
||||
where: {
|
||||
OR: [
|
||||
{
|
||||
displayName: {
|
||||
contains: q,
|
||||
},
|
||||
},
|
||||
{
|
||||
username: {
|
||||
contains: q,
|
||||
},
|
||||
},
|
||||
],
|
||||
relationshipSubjects: following
|
||||
? {
|
||||
some: {
|
||||
ownerId: user.id,
|
||||
following,
|
||||
},
|
||||
}
|
||||
: undefined,
|
||||
},
|
||||
take: Number(limit),
|
||||
skip: Number(offset || 0),
|
||||
include: userRelations,
|
||||
});
|
||||
|
||||
return jsonResponse(accounts.map(acct => userToAPI(acct)));
|
||||
return jsonResponse(accounts.map((acct) => userToAPI(acct)));
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,72 +1,72 @@
|
|||
import { errorResponse, jsonResponse } from "@response";
|
||||
import { userToAPI } from "~database/entities/User";
|
||||
import { apiRoute, applyConfig } from "@api";
|
||||
import { sanitize } from "isomorphic-dompurify";
|
||||
import { convertTextToHtml } from "@formatting";
|
||||
import { errorResponse, jsonResponse } from "@response";
|
||||
import { sanitizeHtml } from "@sanitization";
|
||||
import ISO6391 from "iso-639-1";
|
||||
import { parseEmojis } from "~database/entities/Emoji";
|
||||
import { client } from "~database/datasource";
|
||||
import type { APISource } from "~types/entities/source";
|
||||
import { convertTextToHtml } from "@formatting";
|
||||
import { sanitize } from "isomorphic-dompurify";
|
||||
import { MediaBackendType } from "media-manager";
|
||||
import type { MediaBackend } from "media-manager";
|
||||
import { client } from "~database/datasource";
|
||||
import { getUrl } from "~database/entities/Attachment";
|
||||
import { parseEmojis } from "~database/entities/Emoji";
|
||||
import { userToAPI } from "~database/entities/User";
|
||||
import { userRelations } from "~database/entities/relations";
|
||||
import { LocalMediaBackend } from "~packages/media-manager/backends/local";
|
||||
import { S3MediaBackend } from "~packages/media-manager/backends/s3";
|
||||
import { getUrl } from "~database/entities/Attachment";
|
||||
import { userRelations } from "~database/entities/relations";
|
||||
import type { APISource } from "~types/entities/source";
|
||||
|
||||
export const meta = applyConfig({
|
||||
allowedMethods: ["PATCH"],
|
||||
route: "/api/v1/accounts/update_credentials",
|
||||
ratelimits: {
|
||||
max: 2,
|
||||
duration: 60,
|
||||
},
|
||||
auth: {
|
||||
required: true,
|
||||
oauthPermissions: ["write:accounts"],
|
||||
},
|
||||
allowedMethods: ["PATCH"],
|
||||
route: "/api/v1/accounts/update_credentials",
|
||||
ratelimits: {
|
||||
max: 2,
|
||||
duration: 60,
|
||||
},
|
||||
auth: {
|
||||
required: true,
|
||||
oauthPermissions: ["write:accounts"],
|
||||
},
|
||||
});
|
||||
|
||||
export default apiRoute<{
|
||||
display_name: string;
|
||||
note: string;
|
||||
avatar: File;
|
||||
header: File;
|
||||
locked: string;
|
||||
bot: string;
|
||||
discoverable: string;
|
||||
"source[privacy]": string;
|
||||
"source[sensitive]": string;
|
||||
"source[language]": string;
|
||||
display_name: string;
|
||||
note: string;
|
||||
avatar: File;
|
||||
header: File;
|
||||
locked: string;
|
||||
bot: string;
|
||||
discoverable: string;
|
||||
"source[privacy]": string;
|
||||
"source[sensitive]": string;
|
||||
"source[language]": string;
|
||||
}>(async (req, matchedRoute, extraData) => {
|
||||
const { user } = extraData.auth;
|
||||
const { user } = extraData.auth;
|
||||
|
||||
if (!user) return errorResponse("Unauthorized", 401);
|
||||
if (!user) return errorResponse("Unauthorized", 401);
|
||||
|
||||
const config = await extraData.configManager.getConfig();
|
||||
const config = await extraData.configManager.getConfig();
|
||||
|
||||
const {
|
||||
display_name,
|
||||
note,
|
||||
avatar,
|
||||
header,
|
||||
locked,
|
||||
bot,
|
||||
discoverable,
|
||||
"source[privacy]": source_privacy,
|
||||
"source[sensitive]": source_sensitive,
|
||||
"source[language]": source_language,
|
||||
} = extraData.parsedRequest;
|
||||
const {
|
||||
display_name,
|
||||
note,
|
||||
avatar,
|
||||
header,
|
||||
locked,
|
||||
bot,
|
||||
discoverable,
|
||||
"source[privacy]": source_privacy,
|
||||
"source[sensitive]": source_sensitive,
|
||||
"source[language]": source_language,
|
||||
} = extraData.parsedRequest;
|
||||
|
||||
const sanitizedNote = await sanitizeHtml(note ?? "");
|
||||
const sanitizedNote = await sanitizeHtml(note ?? "");
|
||||
|
||||
const sanitizedDisplayName = sanitize(display_name ?? "", {
|
||||
ALLOWED_TAGS: [],
|
||||
ALLOWED_ATTR: [],
|
||||
});
|
||||
const sanitizedDisplayName = sanitize(display_name ?? "", {
|
||||
ALLOWED_TAGS: [],
|
||||
ALLOWED_ATTR: [],
|
||||
});
|
||||
|
||||
/* if (!user.source) {
|
||||
/* if (!user.source) {
|
||||
user.source = {
|
||||
privacy: "public",
|
||||
sensitive: false,
|
||||
|
|
@ -75,191 +75,192 @@ export default apiRoute<{
|
|||
};
|
||||
} */
|
||||
|
||||
let mediaManager: MediaBackend;
|
||||
let mediaManager: MediaBackend;
|
||||
|
||||
switch (config.media.backend as MediaBackendType) {
|
||||
case MediaBackendType.LOCAL:
|
||||
mediaManager = new LocalMediaBackend(config);
|
||||
break;
|
||||
case MediaBackendType.S3:
|
||||
mediaManager = new S3MediaBackend(config);
|
||||
break;
|
||||
default:
|
||||
// TODO: Replace with logger
|
||||
throw new Error("Invalid media backend");
|
||||
}
|
||||
switch (config.media.backend as MediaBackendType) {
|
||||
case MediaBackendType.LOCAL:
|
||||
mediaManager = new LocalMediaBackend(config);
|
||||
break;
|
||||
case MediaBackendType.S3:
|
||||
mediaManager = new S3MediaBackend(config);
|
||||
break;
|
||||
default:
|
||||
// TODO: Replace with logger
|
||||
throw new Error("Invalid media backend");
|
||||
}
|
||||
|
||||
if (display_name) {
|
||||
// Check if within allowed display name lengths
|
||||
if (
|
||||
sanitizedDisplayName.length < 3 ||
|
||||
sanitizedDisplayName.length > config.validation.max_displayname_size
|
||||
) {
|
||||
return errorResponse(
|
||||
`Display name must be between 3 and ${config.validation.max_displayname_size} characters`,
|
||||
422
|
||||
);
|
||||
}
|
||||
if (display_name) {
|
||||
// Check if within allowed display name lengths
|
||||
if (
|
||||
sanitizedDisplayName.length < 3 ||
|
||||
sanitizedDisplayName.length > config.validation.max_displayname_size
|
||||
) {
|
||||
return errorResponse(
|
||||
`Display name must be between 3 and ${config.validation.max_displayname_size} characters`,
|
||||
422,
|
||||
);
|
||||
}
|
||||
|
||||
// Check if display name doesnt match filters
|
||||
if (
|
||||
config.filters.displayname.some(filter =>
|
||||
sanitizedDisplayName.match(filter)
|
||||
)
|
||||
) {
|
||||
return errorResponse("Display name contains blocked words", 422);
|
||||
}
|
||||
// Check if display name doesnt match filters
|
||||
if (
|
||||
config.filters.displayname.some((filter) =>
|
||||
sanitizedDisplayName.match(filter),
|
||||
)
|
||||
) {
|
||||
return errorResponse("Display name contains blocked words", 422);
|
||||
}
|
||||
|
||||
// Remove emojis
|
||||
user.emojis = [];
|
||||
// Remove emojis
|
||||
user.emojis = [];
|
||||
|
||||
user.displayName = sanitizedDisplayName;
|
||||
}
|
||||
user.displayName = sanitizedDisplayName;
|
||||
}
|
||||
|
||||
if (note && user.source) {
|
||||
// Check if within allowed note length
|
||||
if (sanitizedNote.length > config.validation.max_note_size) {
|
||||
return errorResponse(
|
||||
`Note must be less than ${config.validation.max_note_size} characters`,
|
||||
422
|
||||
);
|
||||
}
|
||||
if (note && user.source) {
|
||||
// Check if within allowed note length
|
||||
if (sanitizedNote.length > config.validation.max_note_size) {
|
||||
return errorResponse(
|
||||
`Note must be less than ${config.validation.max_note_size} characters`,
|
||||
422,
|
||||
);
|
||||
}
|
||||
|
||||
// Check if bio doesnt match filters
|
||||
if (config.filters.bio.some(filter => sanitizedNote.match(filter))) {
|
||||
return errorResponse("Bio contains blocked words", 422);
|
||||
}
|
||||
// Check if bio doesnt match filters
|
||||
if (config.filters.bio.some((filter) => sanitizedNote.match(filter))) {
|
||||
return errorResponse("Bio contains blocked words", 422);
|
||||
}
|
||||
|
||||
(user.source as APISource).note = sanitizedNote;
|
||||
// TODO: Convert note to HTML
|
||||
user.note = await convertTextToHtml(sanitizedNote);
|
||||
}
|
||||
(user.source as APISource).note = sanitizedNote;
|
||||
// TODO: Convert note to HTML
|
||||
user.note = await convertTextToHtml(sanitizedNote);
|
||||
}
|
||||
|
||||
if (source_privacy && user.source) {
|
||||
// Check if within allowed privacy values
|
||||
if (
|
||||
!["public", "unlisted", "private", "direct"].includes(
|
||||
source_privacy
|
||||
)
|
||||
) {
|
||||
return errorResponse(
|
||||
"Privacy must be one of public, unlisted, private, or direct",
|
||||
422
|
||||
);
|
||||
}
|
||||
if (source_privacy && user.source) {
|
||||
// Check if within allowed privacy values
|
||||
if (
|
||||
!["public", "unlisted", "private", "direct"].includes(
|
||||
source_privacy,
|
||||
)
|
||||
) {
|
||||
return errorResponse(
|
||||
"Privacy must be one of public, unlisted, private, or direct",
|
||||
422,
|
||||
);
|
||||
}
|
||||
|
||||
(user.source as APISource).privacy = source_privacy;
|
||||
}
|
||||
(user.source as APISource).privacy = source_privacy;
|
||||
}
|
||||
|
||||
if (source_sensitive && user.source) {
|
||||
// Check if within allowed sensitive values
|
||||
if (source_sensitive !== "true" && source_sensitive !== "false") {
|
||||
return errorResponse("Sensitive must be a boolean", 422);
|
||||
}
|
||||
if (source_sensitive && user.source) {
|
||||
// Check if within allowed sensitive values
|
||||
if (source_sensitive !== "true" && source_sensitive !== "false") {
|
||||
return errorResponse("Sensitive must be a boolean", 422);
|
||||
}
|
||||
|
||||
(user.source as APISource).sensitive = source_sensitive === "true";
|
||||
}
|
||||
(user.source as APISource).sensitive = source_sensitive === "true";
|
||||
}
|
||||
|
||||
if (source_language && user.source) {
|
||||
if (!ISO6391.validate(source_language)) {
|
||||
return errorResponse(
|
||||
"Language must be a valid ISO 639-1 code",
|
||||
422
|
||||
);
|
||||
}
|
||||
if (source_language && user.source) {
|
||||
if (!ISO6391.validate(source_language)) {
|
||||
return errorResponse(
|
||||
"Language must be a valid ISO 639-1 code",
|
||||
422,
|
||||
);
|
||||
}
|
||||
|
||||
(user.source as APISource).language = source_language;
|
||||
}
|
||||
(user.source as APISource).language = source_language;
|
||||
}
|
||||
|
||||
if (avatar) {
|
||||
// Check if within allowed avatar length (avatar is an image)
|
||||
if (avatar.size > config.validation.max_avatar_size) {
|
||||
return errorResponse(
|
||||
`Avatar must be less than ${config.validation.max_avatar_size} bytes`,
|
||||
422
|
||||
);
|
||||
}
|
||||
if (avatar) {
|
||||
// Check if within allowed avatar length (avatar is an image)
|
||||
if (avatar.size > config.validation.max_avatar_size) {
|
||||
return errorResponse(
|
||||
`Avatar must be less than ${config.validation.max_avatar_size} bytes`,
|
||||
422,
|
||||
);
|
||||
}
|
||||
|
||||
const { uploadedFile } = await mediaManager.addFile(avatar);
|
||||
const { uploadedFile } = await mediaManager.addFile(avatar);
|
||||
|
||||
user.avatar = getUrl(uploadedFile.name, config);
|
||||
}
|
||||
user.avatar = getUrl(uploadedFile.name, config);
|
||||
}
|
||||
|
||||
if (header) {
|
||||
// Check if within allowed header length (header is an image)
|
||||
if (header.size > config.validation.max_header_size) {
|
||||
return errorResponse(
|
||||
`Header must be less than ${config.validation.max_avatar_size} bytes`,
|
||||
422
|
||||
);
|
||||
}
|
||||
if (header) {
|
||||
// Check if within allowed header length (header is an image)
|
||||
if (header.size > config.validation.max_header_size) {
|
||||
return errorResponse(
|
||||
`Header must be less than ${config.validation.max_avatar_size} bytes`,
|
||||
422,
|
||||
);
|
||||
}
|
||||
|
||||
const { uploadedFile } = await mediaManager.addFile(header);
|
||||
const { uploadedFile } = await mediaManager.addFile(header);
|
||||
|
||||
user.header = getUrl(uploadedFile.name, config);
|
||||
}
|
||||
user.header = getUrl(uploadedFile.name, config);
|
||||
}
|
||||
|
||||
if (locked) {
|
||||
// Check if locked is a boolean
|
||||
if (locked !== "true" && locked !== "false") {
|
||||
return errorResponse("Locked must be a boolean", 422);
|
||||
}
|
||||
if (locked) {
|
||||
// Check if locked is a boolean
|
||||
if (locked !== "true" && locked !== "false") {
|
||||
return errorResponse("Locked must be a boolean", 422);
|
||||
}
|
||||
|
||||
user.isLocked = locked === "true";
|
||||
}
|
||||
user.isLocked = locked === "true";
|
||||
}
|
||||
|
||||
if (bot) {
|
||||
// Check if bot is a boolean
|
||||
if (bot !== "true" && bot !== "false") {
|
||||
return errorResponse("Bot must be a boolean", 422);
|
||||
}
|
||||
if (bot) {
|
||||
// Check if bot is a boolean
|
||||
if (bot !== "true" && bot !== "false") {
|
||||
return errorResponse("Bot must be a boolean", 422);
|
||||
}
|
||||
|
||||
user.isBot = bot === "true";
|
||||
}
|
||||
user.isBot = bot === "true";
|
||||
}
|
||||
|
||||
if (discoverable) {
|
||||
// Check if discoverable is a boolean
|
||||
if (discoverable !== "true" && discoverable !== "false") {
|
||||
return errorResponse("Discoverable must be a boolean", 422);
|
||||
}
|
||||
if (discoverable) {
|
||||
// Check if discoverable is a boolean
|
||||
if (discoverable !== "true" && discoverable !== "false") {
|
||||
return errorResponse("Discoverable must be a boolean", 422);
|
||||
}
|
||||
|
||||
user.isDiscoverable = discoverable === "true";
|
||||
}
|
||||
user.isDiscoverable = discoverable === "true";
|
||||
}
|
||||
|
||||
// Parse emojis
|
||||
// Parse emojis
|
||||
|
||||
const displaynameEmojis = await parseEmojis(sanitizedDisplayName);
|
||||
const noteEmojis = await parseEmojis(sanitizedNote);
|
||||
const displaynameEmojis = await parseEmojis(sanitizedDisplayName);
|
||||
const noteEmojis = await parseEmojis(sanitizedNote);
|
||||
|
||||
user.emojis = [...displaynameEmojis, ...noteEmojis];
|
||||
user.emojis = [...displaynameEmojis, ...noteEmojis];
|
||||
|
||||
// Deduplicate emojis
|
||||
user.emojis = user.emojis.filter(
|
||||
(emoji, index, self) => self.findIndex(e => e.id === emoji.id) === index
|
||||
);
|
||||
// Deduplicate emojis
|
||||
user.emojis = user.emojis.filter(
|
||||
(emoji, index, self) =>
|
||||
self.findIndex((e) => e.id === emoji.id) === index,
|
||||
);
|
||||
|
||||
const output = 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 || undefined,
|
||||
},
|
||||
include: userRelations,
|
||||
});
|
||||
const output = 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 || undefined,
|
||||
},
|
||||
include: userRelations,
|
||||
});
|
||||
|
||||
return jsonResponse(userToAPI(output));
|
||||
return jsonResponse(userToAPI(output));
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,28 +1,28 @@
|
|||
import { apiRoute, applyConfig } from "@api";
|
||||
import { errorResponse, jsonResponse } from "@response";
|
||||
import { userToAPI } from "~database/entities/User";
|
||||
import { apiRoute, applyConfig } from "@api";
|
||||
|
||||
export const meta = applyConfig({
|
||||
allowedMethods: ["GET"],
|
||||
route: "/api/v1/accounts/verify_credentials",
|
||||
ratelimits: {
|
||||
max: 100,
|
||||
duration: 60,
|
||||
},
|
||||
auth: {
|
||||
required: true,
|
||||
oauthPermissions: ["read:accounts"],
|
||||
},
|
||||
allowedMethods: ["GET"],
|
||||
route: "/api/v1/accounts/verify_credentials",
|
||||
ratelimits: {
|
||||
max: 100,
|
||||
duration: 60,
|
||||
},
|
||||
auth: {
|
||||
required: true,
|
||||
oauthPermissions: ["read:accounts"],
|
||||
},
|
||||
});
|
||||
|
||||
export default apiRoute((req, matchedRoute, extraData) => {
|
||||
// TODO: Add checks for disabled or not email verified accounts
|
||||
// TODO: Add checks for disabled or not email verified accounts
|
||||
|
||||
const { user } = extraData.auth;
|
||||
const { user } = extraData.auth;
|
||||
|
||||
if (!user) return errorResponse("Unauthorized", 401);
|
||||
if (!user) return errorResponse("Unauthorized", 401);
|
||||
|
||||
return jsonResponse({
|
||||
...userToAPI(user, true),
|
||||
});
|
||||
return jsonResponse({
|
||||
...userToAPI(user, true),
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,65 +1,65 @@
|
|||
import { randomBytes } from "node:crypto";
|
||||
import { apiRoute, applyConfig } from "@api";
|
||||
import { errorResponse, jsonResponse } from "@response";
|
||||
import { randomBytes } from "crypto";
|
||||
import { client } from "~database/datasource";
|
||||
|
||||
export const meta = applyConfig({
|
||||
allowedMethods: ["POST"],
|
||||
route: "/api/v1/apps",
|
||||
ratelimits: {
|
||||
max: 2,
|
||||
duration: 60,
|
||||
},
|
||||
auth: {
|
||||
required: false,
|
||||
},
|
||||
allowedMethods: ["POST"],
|
||||
route: "/api/v1/apps",
|
||||
ratelimits: {
|
||||
max: 2,
|
||||
duration: 60,
|
||||
},
|
||||
auth: {
|
||||
required: false,
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Creates a new application to obtain OAuth 2 credentials
|
||||
*/
|
||||
export default apiRoute<{
|
||||
client_name: string;
|
||||
redirect_uris: string;
|
||||
scopes: string;
|
||||
website: string;
|
||||
client_name: string;
|
||||
redirect_uris: string;
|
||||
scopes: string;
|
||||
website: string;
|
||||
}>(async (req, matchedRoute, extraData) => {
|
||||
const { client_name, redirect_uris, scopes, website } =
|
||||
extraData.parsedRequest;
|
||||
const { client_name, redirect_uris, scopes, website } =
|
||||
extraData.parsedRequest;
|
||||
|
||||
// Check if redirect URI is a valid URI, and also an absolute URI
|
||||
if (redirect_uris) {
|
||||
try {
|
||||
const redirect_uri = new URL(redirect_uris);
|
||||
// Check if redirect URI is a valid URI, and also an absolute URI
|
||||
if (redirect_uris) {
|
||||
try {
|
||||
const redirect_uri = new URL(redirect_uris);
|
||||
|
||||
if (!redirect_uri.protocol.startsWith("http")) {
|
||||
return errorResponse(
|
||||
"Redirect URI must be an absolute URI",
|
||||
422
|
||||
);
|
||||
}
|
||||
} catch {
|
||||
return errorResponse("Redirect URI must be a valid URI", 422);
|
||||
}
|
||||
}
|
||||
const application = await client.application.create({
|
||||
data: {
|
||||
name: client_name || "",
|
||||
redirect_uris: redirect_uris || "",
|
||||
scopes: scopes || "read",
|
||||
website: website || null,
|
||||
client_id: randomBytes(32).toString("base64url"),
|
||||
secret: randomBytes(64).toString("base64url"),
|
||||
},
|
||||
});
|
||||
if (!redirect_uri.protocol.startsWith("http")) {
|
||||
return errorResponse(
|
||||
"Redirect URI must be an absolute URI",
|
||||
422,
|
||||
);
|
||||
}
|
||||
} catch {
|
||||
return errorResponse("Redirect URI must be a valid URI", 422);
|
||||
}
|
||||
}
|
||||
const application = await client.application.create({
|
||||
data: {
|
||||
name: client_name || "",
|
||||
redirect_uris: redirect_uris || "",
|
||||
scopes: scopes || "read",
|
||||
website: website || null,
|
||||
client_id: randomBytes(32).toString("base64url"),
|
||||
secret: randomBytes(64).toString("base64url"),
|
||||
},
|
||||
});
|
||||
|
||||
return jsonResponse({
|
||||
id: application.id,
|
||||
name: application.name,
|
||||
website: application.website,
|
||||
client_id: application.client_id,
|
||||
client_secret: application.secret,
|
||||
redirect_uri: application.redirect_uris,
|
||||
vapid_link: application.vapid_key,
|
||||
});
|
||||
return jsonResponse({
|
||||
id: application.id,
|
||||
name: application.name,
|
||||
website: application.website,
|
||||
client_id: application.client_id,
|
||||
client_secret: application.secret,
|
||||
redirect_uri: application.redirect_uris,
|
||||
vapid_link: application.vapid_key,
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -3,32 +3,32 @@ import { errorResponse, jsonResponse } from "@response";
|
|||
import { getFromToken } from "~database/entities/Application";
|
||||
|
||||
export const meta = applyConfig({
|
||||
allowedMethods: ["GET"],
|
||||
route: "/api/v1/apps/verify_credentials",
|
||||
ratelimits: {
|
||||
max: 100,
|
||||
duration: 60,
|
||||
},
|
||||
auth: {
|
||||
required: true,
|
||||
},
|
||||
allowedMethods: ["GET"],
|
||||
route: "/api/v1/apps/verify_credentials",
|
||||
ratelimits: {
|
||||
max: 100,
|
||||
duration: 60,
|
||||
},
|
||||
auth: {
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Returns OAuth2 credentials
|
||||
*/
|
||||
export default apiRoute(async (req, matchedRoute, extraData) => {
|
||||
const { user, token } = extraData.auth;
|
||||
const application = await getFromToken(token);
|
||||
const { user, token } = extraData.auth;
|
||||
const application = await getFromToken(token);
|
||||
|
||||
if (!user) return errorResponse("Unauthorized", 401);
|
||||
if (!application) return errorResponse("Unauthorized", 401);
|
||||
if (!user) return errorResponse("Unauthorized", 401);
|
||||
if (!application) return errorResponse("Unauthorized", 401);
|
||||
|
||||
return jsonResponse({
|
||||
name: application.name,
|
||||
website: application.website,
|
||||
vapid_key: application.vapid_key,
|
||||
redirect_uris: application.redirect_uris,
|
||||
scopes: application.scopes,
|
||||
});
|
||||
return jsonResponse({
|
||||
name: application.name,
|
||||
website: application.website,
|
||||
vapid_key: application.vapid_key,
|
||||
redirect_uris: application.redirect_uris,
|
||||
scopes: application.scopes,
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,37 +1,37 @@
|
|||
import { errorResponse, jsonResponse } from "@response";
|
||||
import { userToAPI } from "~database/entities/User";
|
||||
import { apiRoute, applyConfig } from "@api";
|
||||
import { errorResponse, jsonResponse } from "@response";
|
||||
import { client } from "~database/datasource";
|
||||
import { userToAPI } from "~database/entities/User";
|
||||
import { userRelations } from "~database/entities/relations";
|
||||
|
||||
export const meta = applyConfig({
|
||||
allowedMethods: ["GET"],
|
||||
route: "/api/v1/blocks",
|
||||
ratelimits: {
|
||||
max: 100,
|
||||
duration: 60,
|
||||
},
|
||||
auth: {
|
||||
required: true,
|
||||
},
|
||||
allowedMethods: ["GET"],
|
||||
route: "/api/v1/blocks",
|
||||
ratelimits: {
|
||||
max: 100,
|
||||
duration: 60,
|
||||
},
|
||||
auth: {
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
export default apiRoute(async (req, matchedRoute, extraData) => {
|
||||
const { user } = extraData.auth;
|
||||
const { user } = extraData.auth;
|
||||
|
||||
if (!user) return errorResponse("Unauthorized", 401);
|
||||
if (!user) return errorResponse("Unauthorized", 401);
|
||||
|
||||
const blocks = await client.user.findMany({
|
||||
where: {
|
||||
relationshipSubjects: {
|
||||
some: {
|
||||
ownerId: user.id,
|
||||
blocking: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
include: userRelations,
|
||||
});
|
||||
const blocks = await client.user.findMany({
|
||||
where: {
|
||||
relationshipSubjects: {
|
||||
some: {
|
||||
ownerId: user.id,
|
||||
blocking: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
include: userRelations,
|
||||
});
|
||||
|
||||
return jsonResponse(blocks.map(u => userToAPI(u)));
|
||||
return jsonResponse(blocks.map((u) => userToAPI(u)));
|
||||
});
|
||||
|
|
|
|||
|
|
@ -4,25 +4,25 @@ import { client } from "~database/datasource";
|
|||
import { emojiToAPI } from "~database/entities/Emoji";
|
||||
|
||||
export const meta = applyConfig({
|
||||
allowedMethods: ["GET"],
|
||||
route: "/api/v1/custom_emojis",
|
||||
ratelimits: {
|
||||
max: 100,
|
||||
duration: 60,
|
||||
},
|
||||
auth: {
|
||||
required: false,
|
||||
},
|
||||
allowedMethods: ["GET"],
|
||||
route: "/api/v1/custom_emojis",
|
||||
ratelimits: {
|
||||
max: 100,
|
||||
duration: 60,
|
||||
},
|
||||
auth: {
|
||||
required: false,
|
||||
},
|
||||
});
|
||||
|
||||
export default apiRoute(async () => {
|
||||
const emojis = await client.emoji.findMany({
|
||||
where: {
|
||||
instanceId: null,
|
||||
},
|
||||
});
|
||||
const emojis = await client.emoji.findMany({
|
||||
where: {
|
||||
instanceId: null,
|
||||
},
|
||||
});
|
||||
|
||||
return jsonResponse(
|
||||
await Promise.all(emojis.map(emoji => emojiToAPI(emoji)))
|
||||
);
|
||||
return jsonResponse(
|
||||
await Promise.all(emojis.map((emoji) => emojiToAPI(emoji))),
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,74 +1,74 @@
|
|||
import { errorResponse, jsonResponse } from "@response";
|
||||
import { apiRoute, applyConfig } from "@api";
|
||||
import { errorResponse, jsonResponse } from "@response";
|
||||
import { client } from "~database/datasource";
|
||||
import { statusAndUserRelations } from "~database/entities/relations";
|
||||
import { statusToAPI } from "~database/entities/Status";
|
||||
import { statusAndUserRelations } from "~database/entities/relations";
|
||||
|
||||
export const meta = applyConfig({
|
||||
allowedMethods: ["GET"],
|
||||
route: "/api/v1/favourites",
|
||||
ratelimits: {
|
||||
max: 100,
|
||||
duration: 60,
|
||||
},
|
||||
auth: {
|
||||
required: true,
|
||||
},
|
||||
allowedMethods: ["GET"],
|
||||
route: "/api/v1/favourites",
|
||||
ratelimits: {
|
||||
max: 100,
|
||||
duration: 60,
|
||||
},
|
||||
auth: {
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
export default apiRoute<{
|
||||
max_id?: string;
|
||||
since_id?: string;
|
||||
min_id?: string;
|
||||
limit?: number;
|
||||
max_id?: string;
|
||||
since_id?: string;
|
||||
min_id?: string;
|
||||
limit?: number;
|
||||
}>(async (req, matchedRoute, extraData) => {
|
||||
const { user } = extraData.auth;
|
||||
const { user } = extraData.auth;
|
||||
|
||||
const { limit = 20, max_id, min_id, since_id } = extraData.parsedRequest;
|
||||
const { limit = 20, max_id, min_id, since_id } = extraData.parsedRequest;
|
||||
|
||||
if (limit < 1 || limit > 40) {
|
||||
return errorResponse("Limit must be between 1 and 40", 400);
|
||||
}
|
||||
if (limit < 1 || limit > 40) {
|
||||
return errorResponse("Limit must be between 1 and 40", 400);
|
||||
}
|
||||
|
||||
if (!user) return errorResponse("Unauthorized", 401);
|
||||
if (!user) return errorResponse("Unauthorized", 401);
|
||||
|
||||
const objects = await client.status.findMany({
|
||||
where: {
|
||||
id: {
|
||||
lt: max_id ?? undefined,
|
||||
gte: since_id ?? undefined,
|
||||
gt: min_id ?? undefined,
|
||||
},
|
||||
likes: {
|
||||
some: {
|
||||
likerId: user.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
include: statusAndUserRelations,
|
||||
take: limit,
|
||||
orderBy: {
|
||||
id: "desc",
|
||||
},
|
||||
});
|
||||
const objects = await client.status.findMany({
|
||||
where: {
|
||||
id: {
|
||||
lt: max_id ?? undefined,
|
||||
gte: since_id ?? undefined,
|
||||
gt: min_id ?? undefined,
|
||||
},
|
||||
likes: {
|
||||
some: {
|
||||
likerId: user.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
include: statusAndUserRelations,
|
||||
take: limit,
|
||||
orderBy: {
|
||||
id: "desc",
|
||||
},
|
||||
});
|
||||
|
||||
// Constuct HTTP Link header (next and prev)
|
||||
const linkHeader = [];
|
||||
if (objects.length > 0) {
|
||||
const urlWithoutQuery = req.url.split("?")[0];
|
||||
linkHeader.push(
|
||||
`<${urlWithoutQuery}?max_id=${objects.at(-1)?.id}>; rel="next"`,
|
||||
`<${urlWithoutQuery}?min_id=${objects[0].id}>; rel="prev"`
|
||||
);
|
||||
}
|
||||
// Constuct HTTP Link header (next and prev)
|
||||
const linkHeader = [];
|
||||
if (objects.length > 0) {
|
||||
const urlWithoutQuery = req.url.split("?")[0];
|
||||
linkHeader.push(
|
||||
`<${urlWithoutQuery}?max_id=${objects.at(-1)?.id}>; rel="next"`,
|
||||
`<${urlWithoutQuery}?min_id=${objects[0].id}>; rel="prev"`,
|
||||
);
|
||||
}
|
||||
|
||||
return jsonResponse(
|
||||
await Promise.all(
|
||||
objects.map(async status => statusToAPI(status, user))
|
||||
),
|
||||
200,
|
||||
{
|
||||
Link: linkHeader.join(", "),
|
||||
}
|
||||
);
|
||||
return jsonResponse(
|
||||
await Promise.all(
|
||||
objects.map(async (status) => statusToAPI(status, user)),
|
||||
),
|
||||
200,
|
||||
{
|
||||
Link: linkHeader.join(", "),
|
||||
},
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,75 +1,75 @@
|
|||
import { errorResponse, jsonResponse } from "@response";
|
||||
import { apiRoute, applyConfig } from "@api";
|
||||
import { errorResponse, jsonResponse } from "@response";
|
||||
import { client } from "~database/datasource";
|
||||
import {
|
||||
checkForBidirectionalRelationships,
|
||||
relationshipToAPI,
|
||||
checkForBidirectionalRelationships,
|
||||
relationshipToAPI,
|
||||
} from "~database/entities/Relationship";
|
||||
import { userRelations } from "~database/entities/relations";
|
||||
|
||||
export const meta = applyConfig({
|
||||
allowedMethods: ["POST"],
|
||||
route: "/api/v1/follow_requests/:account_id/authorize",
|
||||
ratelimits: {
|
||||
max: 100,
|
||||
duration: 60,
|
||||
},
|
||||
auth: {
|
||||
required: true,
|
||||
},
|
||||
allowedMethods: ["POST"],
|
||||
route: "/api/v1/follow_requests/:account_id/authorize",
|
||||
ratelimits: {
|
||||
max: 100,
|
||||
duration: 60,
|
||||
},
|
||||
auth: {
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
export default apiRoute(async (req, matchedRoute, extraData) => {
|
||||
const { user } = extraData.auth;
|
||||
const { user } = extraData.auth;
|
||||
|
||||
if (!user) return errorResponse("Unauthorized", 401);
|
||||
if (!user) return errorResponse("Unauthorized", 401);
|
||||
|
||||
const { account_id } = matchedRoute.params;
|
||||
const { account_id } = matchedRoute.params;
|
||||
|
||||
const account = await client.user.findUnique({
|
||||
where: {
|
||||
id: account_id,
|
||||
},
|
||||
include: userRelations,
|
||||
});
|
||||
const account = await client.user.findUnique({
|
||||
where: {
|
||||
id: account_id,
|
||||
},
|
||||
include: userRelations,
|
||||
});
|
||||
|
||||
if (!account) return errorResponse("Account not found", 404);
|
||||
if (!account) return errorResponse("Account not found", 404);
|
||||
|
||||
// Check if there is a relationship on both sides
|
||||
await checkForBidirectionalRelationships(user, account);
|
||||
// Check if there is a relationship on both sides
|
||||
await checkForBidirectionalRelationships(user, account);
|
||||
|
||||
// Authorize follow request
|
||||
await client.relationship.updateMany({
|
||||
where: {
|
||||
subjectId: user.id,
|
||||
ownerId: account.id,
|
||||
requested: true,
|
||||
},
|
||||
data: {
|
||||
requested: false,
|
||||
following: true,
|
||||
},
|
||||
});
|
||||
// Authorize follow request
|
||||
await client.relationship.updateMany({
|
||||
where: {
|
||||
subjectId: user.id,
|
||||
ownerId: account.id,
|
||||
requested: true,
|
||||
},
|
||||
data: {
|
||||
requested: false,
|
||||
following: true,
|
||||
},
|
||||
});
|
||||
|
||||
// Update followedBy for other user
|
||||
await client.relationship.updateMany({
|
||||
where: {
|
||||
subjectId: account.id,
|
||||
ownerId: user.id,
|
||||
},
|
||||
data: {
|
||||
followedBy: true,
|
||||
},
|
||||
});
|
||||
// Update followedBy for other user
|
||||
await client.relationship.updateMany({
|
||||
where: {
|
||||
subjectId: account.id,
|
||||
ownerId: user.id,
|
||||
},
|
||||
data: {
|
||||
followedBy: true,
|
||||
},
|
||||
});
|
||||
|
||||
const relationship = await client.relationship.findFirst({
|
||||
where: {
|
||||
subjectId: account.id,
|
||||
ownerId: user.id,
|
||||
},
|
||||
});
|
||||
const relationship = await client.relationship.findFirst({
|
||||
where: {
|
||||
subjectId: account.id,
|
||||
ownerId: user.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (!relationship) return errorResponse("Relationship not found", 404);
|
||||
if (!relationship) return errorResponse("Relationship not found", 404);
|
||||
|
||||
return jsonResponse(relationshipToAPI(relationship));
|
||||
return jsonResponse(relationshipToAPI(relationship));
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,63 +1,63 @@
|
|||
import { errorResponse, jsonResponse } from "@response";
|
||||
import { apiRoute, applyConfig } from "@api";
|
||||
import { errorResponse, jsonResponse } from "@response";
|
||||
import { client } from "~database/datasource";
|
||||
import {
|
||||
checkForBidirectionalRelationships,
|
||||
relationshipToAPI,
|
||||
checkForBidirectionalRelationships,
|
||||
relationshipToAPI,
|
||||
} from "~database/entities/Relationship";
|
||||
import { userRelations } from "~database/entities/relations";
|
||||
|
||||
export const meta = applyConfig({
|
||||
allowedMethods: ["POST"],
|
||||
route: "/api/v1/follow_requests/:account_id/reject",
|
||||
ratelimits: {
|
||||
max: 100,
|
||||
duration: 60,
|
||||
},
|
||||
auth: {
|
||||
required: true,
|
||||
},
|
||||
allowedMethods: ["POST"],
|
||||
route: "/api/v1/follow_requests/:account_id/reject",
|
||||
ratelimits: {
|
||||
max: 100,
|
||||
duration: 60,
|
||||
},
|
||||
auth: {
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
export default apiRoute(async (req, matchedRoute, extraData) => {
|
||||
const { user } = extraData.auth;
|
||||
const { user } = extraData.auth;
|
||||
|
||||
if (!user) return errorResponse("Unauthorized", 401);
|
||||
if (!user) return errorResponse("Unauthorized", 401);
|
||||
|
||||
const { account_id } = matchedRoute.params;
|
||||
const { account_id } = matchedRoute.params;
|
||||
|
||||
const account = await client.user.findUnique({
|
||||
where: {
|
||||
id: account_id,
|
||||
},
|
||||
include: userRelations,
|
||||
});
|
||||
const account = await client.user.findUnique({
|
||||
where: {
|
||||
id: account_id,
|
||||
},
|
||||
include: userRelations,
|
||||
});
|
||||
|
||||
if (!account) return errorResponse("Account not found", 404);
|
||||
if (!account) return errorResponse("Account not found", 404);
|
||||
|
||||
// Check if there is a relationship on both sides
|
||||
await checkForBidirectionalRelationships(user, account);
|
||||
// Check if there is a relationship on both sides
|
||||
await checkForBidirectionalRelationships(user, account);
|
||||
|
||||
// Reject follow request
|
||||
await client.relationship.updateMany({
|
||||
where: {
|
||||
subjectId: user.id,
|
||||
ownerId: account.id,
|
||||
requested: true,
|
||||
},
|
||||
data: {
|
||||
requested: false,
|
||||
},
|
||||
});
|
||||
// Reject follow request
|
||||
await client.relationship.updateMany({
|
||||
where: {
|
||||
subjectId: user.id,
|
||||
ownerId: account.id,
|
||||
requested: true,
|
||||
},
|
||||
data: {
|
||||
requested: false,
|
||||
},
|
||||
});
|
||||
|
||||
const relationship = await client.relationship.findFirst({
|
||||
where: {
|
||||
subjectId: account.id,
|
||||
ownerId: user.id,
|
||||
},
|
||||
});
|
||||
const relationship = await client.relationship.findFirst({
|
||||
where: {
|
||||
subjectId: account.id,
|
||||
ownerId: user.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (!relationship) return errorResponse("Relationship not found", 404);
|
||||
if (!relationship) return errorResponse("Relationship not found", 404);
|
||||
|
||||
return jsonResponse(relationshipToAPI(relationship));
|
||||
return jsonResponse(relationshipToAPI(relationship));
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,73 +1,73 @@
|
|||
import { errorResponse, jsonResponse } from "@response";
|
||||
import { userToAPI } from "~database/entities/User";
|
||||
import { apiRoute, applyConfig } from "@api";
|
||||
import { errorResponse, jsonResponse } from "@response";
|
||||
import { client } from "~database/datasource";
|
||||
import { userToAPI } from "~database/entities/User";
|
||||
import { userRelations } from "~database/entities/relations";
|
||||
|
||||
export const meta = applyConfig({
|
||||
allowedMethods: ["GET"],
|
||||
route: "/api/v1/follow_requests",
|
||||
ratelimits: {
|
||||
max: 100,
|
||||
duration: 60,
|
||||
},
|
||||
auth: {
|
||||
required: true,
|
||||
},
|
||||
allowedMethods: ["GET"],
|
||||
route: "/api/v1/follow_requests",
|
||||
ratelimits: {
|
||||
max: 100,
|
||||
duration: 60,
|
||||
},
|
||||
auth: {
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
export default apiRoute<{
|
||||
max_id?: string;
|
||||
since_id?: string;
|
||||
min_id?: string;
|
||||
limit?: number;
|
||||
max_id?: string;
|
||||
since_id?: string;
|
||||
min_id?: string;
|
||||
limit?: number;
|
||||
}>(async (req, matchedRoute, extraData) => {
|
||||
const { user } = extraData.auth;
|
||||
const { user } = extraData.auth;
|
||||
|
||||
const { limit = 20, max_id, min_id, since_id } = extraData.parsedRequest;
|
||||
const { limit = 20, max_id, min_id, since_id } = extraData.parsedRequest;
|
||||
|
||||
if (limit < 1 || limit > 40) {
|
||||
return errorResponse("Limit must be between 1 and 40", 400);
|
||||
}
|
||||
if (limit < 1 || limit > 40) {
|
||||
return errorResponse("Limit must be between 1 and 40", 400);
|
||||
}
|
||||
|
||||
if (!user) return errorResponse("Unauthorized", 401);
|
||||
if (!user) return errorResponse("Unauthorized", 401);
|
||||
|
||||
const objects = await client.user.findMany({
|
||||
where: {
|
||||
id: {
|
||||
lt: max_id ?? undefined,
|
||||
gte: since_id ?? undefined,
|
||||
gt: min_id ?? undefined,
|
||||
},
|
||||
relationships: {
|
||||
some: {
|
||||
subjectId: user.id,
|
||||
requested: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
include: userRelations,
|
||||
take: limit,
|
||||
orderBy: {
|
||||
id: "desc",
|
||||
},
|
||||
});
|
||||
const objects = await client.user.findMany({
|
||||
where: {
|
||||
id: {
|
||||
lt: max_id ?? undefined,
|
||||
gte: since_id ?? undefined,
|
||||
gt: min_id ?? undefined,
|
||||
},
|
||||
relationships: {
|
||||
some: {
|
||||
subjectId: user.id,
|
||||
requested: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
include: userRelations,
|
||||
take: limit,
|
||||
orderBy: {
|
||||
id: "desc",
|
||||
},
|
||||
});
|
||||
|
||||
// Constuct HTTP Link header (next and prev)
|
||||
const linkHeader = [];
|
||||
if (objects.length > 0) {
|
||||
const urlWithoutQuery = req.url.split("?")[0];
|
||||
linkHeader.push(
|
||||
`<${urlWithoutQuery}?max_id=${objects.at(-1)?.id}>; rel="next"`,
|
||||
`<${urlWithoutQuery}?min_id=${objects[0].id}>; rel="prev"`
|
||||
);
|
||||
}
|
||||
// Constuct HTTP Link header (next and prev)
|
||||
const linkHeader = [];
|
||||
if (objects.length > 0) {
|
||||
const urlWithoutQuery = req.url.split("?")[0];
|
||||
linkHeader.push(
|
||||
`<${urlWithoutQuery}?max_id=${objects.at(-1)?.id}>; rel="next"`,
|
||||
`<${urlWithoutQuery}?min_id=${objects[0].id}>; rel="prev"`,
|
||||
);
|
||||
}
|
||||
|
||||
return jsonResponse(
|
||||
objects.map(user => userToAPI(user)),
|
||||
200,
|
||||
{
|
||||
Link: linkHeader.join(", "),
|
||||
}
|
||||
);
|
||||
return jsonResponse(
|
||||
objects.map((user) => userToAPI(user)),
|
||||
200,
|
||||
{
|
||||
Link: linkHeader.join(", "),
|
||||
},
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -2,157 +2,157 @@ import { apiRoute, applyConfig } from "@api";
|
|||
import { jsonResponse } from "@response";
|
||||
import { client } from "~database/datasource";
|
||||
import { userToAPI } from "~database/entities/User";
|
||||
import type { APIInstance } from "~types/entities/instance";
|
||||
import manifest from "~package.json";
|
||||
import { userRelations } from "~database/entities/relations";
|
||||
import manifest from "~package.json";
|
||||
import type { APIInstance } from "~types/entities/instance";
|
||||
|
||||
export const meta = applyConfig({
|
||||
allowedMethods: ["GET"],
|
||||
route: "/api/v1/instance",
|
||||
ratelimits: {
|
||||
max: 300,
|
||||
duration: 60,
|
||||
},
|
||||
auth: {
|
||||
required: false,
|
||||
},
|
||||
allowedMethods: ["GET"],
|
||||
route: "/api/v1/instance",
|
||||
ratelimits: {
|
||||
max: 300,
|
||||
duration: 60,
|
||||
},
|
||||
auth: {
|
||||
required: false,
|
||||
},
|
||||
});
|
||||
|
||||
export default apiRoute(async (req, matchedRoute, extraData) => {
|
||||
const config = await extraData.configManager.getConfig();
|
||||
const config = await extraData.configManager.getConfig();
|
||||
|
||||
// Get software version from package.json
|
||||
const version = manifest.version;
|
||||
// Get software version from package.json
|
||||
const version = manifest.version;
|
||||
|
||||
const statusCount = await client.status.count({
|
||||
where: {
|
||||
instanceId: null,
|
||||
},
|
||||
});
|
||||
const userCount = await client.user.count({
|
||||
where: {
|
||||
instanceId: null,
|
||||
},
|
||||
});
|
||||
const statusCount = await client.status.count({
|
||||
where: {
|
||||
instanceId: null,
|
||||
},
|
||||
});
|
||||
const userCount = await client.user.count({
|
||||
where: {
|
||||
instanceId: null,
|
||||
},
|
||||
});
|
||||
|
||||
// Get the first created admin user
|
||||
const contactAccount = await client.user.findFirst({
|
||||
where: {
|
||||
instanceId: null,
|
||||
isAdmin: true,
|
||||
},
|
||||
orderBy: {
|
||||
id: "asc",
|
||||
},
|
||||
include: userRelations,
|
||||
});
|
||||
// Get the first created admin user
|
||||
const contactAccount = await client.user.findFirst({
|
||||
where: {
|
||||
instanceId: null,
|
||||
isAdmin: true,
|
||||
},
|
||||
orderBy: {
|
||||
id: "asc",
|
||||
},
|
||||
include: userRelations,
|
||||
});
|
||||
|
||||
// Get user that have posted once in the last 30 days
|
||||
const monthlyActiveUsers = await client.user.count({
|
||||
where: {
|
||||
instanceId: null,
|
||||
statuses: {
|
||||
some: {
|
||||
createdAt: {
|
||||
gte: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
// Get user that have posted once in the last 30 days
|
||||
const monthlyActiveUsers = await client.user.count({
|
||||
where: {
|
||||
instanceId: null,
|
||||
statuses: {
|
||||
some: {
|
||||
createdAt: {
|
||||
gte: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const knownDomainsCount = await client.instance.count();
|
||||
const knownDomainsCount = await client.instance.count();
|
||||
|
||||
// TODO: fill in more values
|
||||
return jsonResponse({
|
||||
approval_required: false,
|
||||
configuration: {
|
||||
media_attachments: {
|
||||
image_matrix_limit: config.validation.max_media_attachments,
|
||||
image_size_limit: config.validation.max_media_size,
|
||||
supported_mime_types: config.validation.allowed_mime_types,
|
||||
video_frame_limit: 60,
|
||||
video_matrix_limit: 10,
|
||||
video_size_limit: config.validation.max_media_size,
|
||||
},
|
||||
polls: {
|
||||
max_characters_per_option:
|
||||
config.validation.max_poll_option_size,
|
||||
max_expiration: config.validation.max_poll_duration,
|
||||
max_options: config.validation.max_poll_options,
|
||||
min_expiration: 60,
|
||||
},
|
||||
statuses: {
|
||||
characters_reserved_per_url: 0,
|
||||
max_characters: config.validation.max_note_size,
|
||||
max_media_attachments: config.validation.max_media_attachments,
|
||||
supported_mime_types: [
|
||||
"text/plain",
|
||||
"text/markdown",
|
||||
"text/html",
|
||||
"text/x.misskeymarkdown",
|
||||
],
|
||||
},
|
||||
},
|
||||
description: "A test instance",
|
||||
email: "",
|
||||
invites_enabled: false,
|
||||
registrations: config.signups.registration,
|
||||
languages: ["en"],
|
||||
rules: config.signups.rules.map((r, index) => ({
|
||||
id: String(index),
|
||||
text: r,
|
||||
})),
|
||||
stats: {
|
||||
domain_count: knownDomainsCount,
|
||||
status_count: statusCount,
|
||||
user_count: userCount,
|
||||
},
|
||||
thumbnail: "",
|
||||
tos_url: config.signups.tos_url,
|
||||
title: "Test Instance",
|
||||
uri: new URL(config.http.base_url).hostname,
|
||||
urls: {
|
||||
streaming_api: "",
|
||||
},
|
||||
version: `4.2.0+glitch (compatible; Lysand ${version}})`,
|
||||
max_toot_chars: config.validation.max_note_size,
|
||||
pleroma: {
|
||||
metadata: {
|
||||
// account_activation_required: false,
|
||||
features: [
|
||||
"pleroma_api",
|
||||
"akkoma_api",
|
||||
"mastodon_api",
|
||||
// "mastodon_api_streaming",
|
||||
// "polls",
|
||||
// "v2_suggestions",
|
||||
// "pleroma_explicit_addressing",
|
||||
// "shareable_emoji_packs",
|
||||
// "multifetch",
|
||||
// "pleroma:api/v1/notifications:include_types_filter",
|
||||
"quote_posting",
|
||||
"editing",
|
||||
// "bubble_timeline",
|
||||
// "relay",
|
||||
// "pleroma_emoji_reactions",
|
||||
// "exposable_reactions",
|
||||
// "profile_directory",
|
||||
// "custom_emoji_reactions",
|
||||
// "pleroma:get:main/ostatus",
|
||||
],
|
||||
post_formats: [
|
||||
"text/plain",
|
||||
"text/html",
|
||||
"text/markdown",
|
||||
"text/x.misskeymarkdown",
|
||||
],
|
||||
privileged_staff: false,
|
||||
},
|
||||
stats: {
|
||||
mau: monthlyActiveUsers,
|
||||
},
|
||||
},
|
||||
contact_account: contactAccount ? userToAPI(contactAccount) : null,
|
||||
} as APIInstance);
|
||||
// TODO: fill in more values
|
||||
return jsonResponse({
|
||||
approval_required: false,
|
||||
configuration: {
|
||||
media_attachments: {
|
||||
image_matrix_limit: config.validation.max_media_attachments,
|
||||
image_size_limit: config.validation.max_media_size,
|
||||
supported_mime_types: config.validation.allowed_mime_types,
|
||||
video_frame_limit: 60,
|
||||
video_matrix_limit: 10,
|
||||
video_size_limit: config.validation.max_media_size,
|
||||
},
|
||||
polls: {
|
||||
max_characters_per_option:
|
||||
config.validation.max_poll_option_size,
|
||||
max_expiration: config.validation.max_poll_duration,
|
||||
max_options: config.validation.max_poll_options,
|
||||
min_expiration: 60,
|
||||
},
|
||||
statuses: {
|
||||
characters_reserved_per_url: 0,
|
||||
max_characters: config.validation.max_note_size,
|
||||
max_media_attachments: config.validation.max_media_attachments,
|
||||
supported_mime_types: [
|
||||
"text/plain",
|
||||
"text/markdown",
|
||||
"text/html",
|
||||
"text/x.misskeymarkdown",
|
||||
],
|
||||
},
|
||||
},
|
||||
description: "A test instance",
|
||||
email: "",
|
||||
invites_enabled: false,
|
||||
registrations: config.signups.registration,
|
||||
languages: ["en"],
|
||||
rules: config.signups.rules.map((r, index) => ({
|
||||
id: String(index),
|
||||
text: r,
|
||||
})),
|
||||
stats: {
|
||||
domain_count: knownDomainsCount,
|
||||
status_count: statusCount,
|
||||
user_count: userCount,
|
||||
},
|
||||
thumbnail: "",
|
||||
tos_url: config.signups.tos_url,
|
||||
title: "Test Instance",
|
||||
uri: new URL(config.http.base_url).hostname,
|
||||
urls: {
|
||||
streaming_api: "",
|
||||
},
|
||||
version: `4.2.0+glitch (compatible; Lysand ${version}})`,
|
||||
max_toot_chars: config.validation.max_note_size,
|
||||
pleroma: {
|
||||
metadata: {
|
||||
// account_activation_required: false,
|
||||
features: [
|
||||
"pleroma_api",
|
||||
"akkoma_api",
|
||||
"mastodon_api",
|
||||
// "mastodon_api_streaming",
|
||||
// "polls",
|
||||
// "v2_suggestions",
|
||||
// "pleroma_explicit_addressing",
|
||||
// "shareable_emoji_packs",
|
||||
// "multifetch",
|
||||
// "pleroma:api/v1/notifications:include_types_filter",
|
||||
"quote_posting",
|
||||
"editing",
|
||||
// "bubble_timeline",
|
||||
// "relay",
|
||||
// "pleroma_emoji_reactions",
|
||||
// "exposable_reactions",
|
||||
// "profile_directory",
|
||||
// "custom_emoji_reactions",
|
||||
// "pleroma:get:main/ostatus",
|
||||
],
|
||||
post_formats: [
|
||||
"text/plain",
|
||||
"text/html",
|
||||
"text/markdown",
|
||||
"text/x.misskeymarkdown",
|
||||
],
|
||||
privileged_staff: false,
|
||||
},
|
||||
stats: {
|
||||
mau: monthlyActiveUsers,
|
||||
},
|
||||
},
|
||||
contact_account: contactAccount ? userToAPI(contactAccount) : null,
|
||||
} as APIInstance);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,109 +1,108 @@
|
|||
import { apiRoute, applyConfig } from "@api";
|
||||
import { errorResponse, jsonResponse } from "@response";
|
||||
import { client } from "~database/datasource";
|
||||
import { attachmentToAPI, getUrl } from "~database/entities/Attachment";
|
||||
import type { MediaBackend } from "media-manager";
|
||||
import { MediaBackendType } from "media-manager";
|
||||
import { client } from "~database/datasource";
|
||||
import { attachmentToAPI, getUrl } from "~database/entities/Attachment";
|
||||
import { LocalMediaBackend } from "~packages/media-manager/backends/local";
|
||||
import { S3MediaBackend } from "~packages/media-manager/backends/s3";
|
||||
|
||||
export const meta = applyConfig({
|
||||
allowedMethods: ["GET", "PUT"],
|
||||
ratelimits: {
|
||||
max: 10,
|
||||
duration: 60,
|
||||
},
|
||||
route: "/api/v1/media/:id",
|
||||
auth: {
|
||||
required: true,
|
||||
oauthPermissions: ["write:media"],
|
||||
},
|
||||
allowedMethods: ["GET", "PUT"],
|
||||
ratelimits: {
|
||||
max: 10,
|
||||
duration: 60,
|
||||
},
|
||||
route: "/api/v1/media/:id",
|
||||
auth: {
|
||||
required: true,
|
||||
oauthPermissions: ["write:media"],
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Get media information
|
||||
*/
|
||||
export default apiRoute<{
|
||||
thumbnail?: File;
|
||||
description?: string;
|
||||
focus?: string;
|
||||
thumbnail?: File;
|
||||
description?: string;
|
||||
focus?: string;
|
||||
}>(async (req, matchedRoute, extraData) => {
|
||||
const { user } = extraData.auth;
|
||||
const { user } = extraData.auth;
|
||||
|
||||
if (!user) {
|
||||
return errorResponse("Unauthorized", 401);
|
||||
}
|
||||
if (!user) {
|
||||
return errorResponse("Unauthorized", 401);
|
||||
}
|
||||
|
||||
const id = matchedRoute.params.id;
|
||||
const id = matchedRoute.params.id;
|
||||
|
||||
const attachment = await client.attachment.findUnique({
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
});
|
||||
const attachment = await client.attachment.findUnique({
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
});
|
||||
|
||||
if (!attachment) {
|
||||
return errorResponse("Media not found", 404);
|
||||
}
|
||||
if (!attachment) {
|
||||
return errorResponse("Media not found", 404);
|
||||
}
|
||||
|
||||
const config = await extraData.configManager.getConfig();
|
||||
const config = await extraData.configManager.getConfig();
|
||||
|
||||
switch (req.method) {
|
||||
case "GET": {
|
||||
if (attachment.url) {
|
||||
return jsonResponse(attachmentToAPI(attachment));
|
||||
} else {
|
||||
return new Response(null, {
|
||||
status: 206,
|
||||
});
|
||||
}
|
||||
}
|
||||
case "PUT": {
|
||||
const { description, thumbnail } = extraData.parsedRequest;
|
||||
switch (req.method) {
|
||||
case "GET": {
|
||||
if (attachment.url) {
|
||||
return jsonResponse(attachmentToAPI(attachment));
|
||||
}
|
||||
return new Response(null, {
|
||||
status: 206,
|
||||
});
|
||||
}
|
||||
case "PUT": {
|
||||
const { description, thumbnail } = extraData.parsedRequest;
|
||||
|
||||
let thumbnailUrl = attachment.thumbnail_url;
|
||||
let thumbnailUrl = attachment.thumbnail_url;
|
||||
|
||||
let mediaManager: MediaBackend;
|
||||
let mediaManager: MediaBackend;
|
||||
|
||||
switch (config.media.backend as MediaBackendType) {
|
||||
case MediaBackendType.LOCAL:
|
||||
mediaManager = new LocalMediaBackend(config);
|
||||
break;
|
||||
case MediaBackendType.S3:
|
||||
mediaManager = new S3MediaBackend(config);
|
||||
break;
|
||||
default:
|
||||
// TODO: Replace with logger
|
||||
throw new Error("Invalid media backend");
|
||||
}
|
||||
switch (config.media.backend as MediaBackendType) {
|
||||
case MediaBackendType.LOCAL:
|
||||
mediaManager = new LocalMediaBackend(config);
|
||||
break;
|
||||
case MediaBackendType.S3:
|
||||
mediaManager = new S3MediaBackend(config);
|
||||
break;
|
||||
default:
|
||||
// TODO: Replace with logger
|
||||
throw new Error("Invalid media backend");
|
||||
}
|
||||
|
||||
if (thumbnail) {
|
||||
const { uploadedFile } = await mediaManager.addFile(thumbnail);
|
||||
thumbnailUrl = getUrl(uploadedFile.name, config);
|
||||
}
|
||||
if (thumbnail) {
|
||||
const { uploadedFile } = await mediaManager.addFile(thumbnail);
|
||||
thumbnailUrl = getUrl(uploadedFile.name, config);
|
||||
}
|
||||
|
||||
const descriptionText = description || attachment.description;
|
||||
const descriptionText = description || attachment.description;
|
||||
|
||||
if (
|
||||
descriptionText !== attachment.description ||
|
||||
thumbnailUrl !== attachment.thumbnail_url
|
||||
) {
|
||||
const newAttachment = await client.attachment.update({
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
data: {
|
||||
description: descriptionText,
|
||||
thumbnail_url: thumbnailUrl,
|
||||
},
|
||||
});
|
||||
if (
|
||||
descriptionText !== attachment.description ||
|
||||
thumbnailUrl !== attachment.thumbnail_url
|
||||
) {
|
||||
const newAttachment = await client.attachment.update({
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
data: {
|
||||
description: descriptionText,
|
||||
thumbnail_url: thumbnailUrl,
|
||||
},
|
||||
});
|
||||
|
||||
return jsonResponse(attachmentToAPI(newAttachment));
|
||||
}
|
||||
return jsonResponse(attachmentToAPI(newAttachment));
|
||||
}
|
||||
|
||||
return jsonResponse(attachmentToAPI(attachment));
|
||||
}
|
||||
}
|
||||
return jsonResponse(attachmentToAPI(attachment));
|
||||
}
|
||||
}
|
||||
|
||||
return errorResponse("Method not allowed", 405);
|
||||
return errorResponse("Method not allowed", 405);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,136 +1,136 @@
|
|||
import { apiRoute, applyConfig } from "@api";
|
||||
import { errorResponse, jsonResponse } from "@response";
|
||||
import { client } from "~database/datasource";
|
||||
import { encode } from "blurhash";
|
||||
import sharp from "sharp";
|
||||
import { attachmentToAPI, getUrl } from "~database/entities/Attachment";
|
||||
import { MediaBackendType } from "media-manager";
|
||||
import type { MediaBackend } from "media-manager";
|
||||
import sharp from "sharp";
|
||||
import { client } from "~database/datasource";
|
||||
import { attachmentToAPI, getUrl } from "~database/entities/Attachment";
|
||||
import { LocalMediaBackend } from "~packages/media-manager/backends/local";
|
||||
import { S3MediaBackend } from "~packages/media-manager/backends/s3";
|
||||
|
||||
export const meta = applyConfig({
|
||||
allowedMethods: ["POST"],
|
||||
ratelimits: {
|
||||
max: 10,
|
||||
duration: 60,
|
||||
},
|
||||
route: "/api/v1/media",
|
||||
auth: {
|
||||
required: true,
|
||||
oauthPermissions: ["write:media"],
|
||||
},
|
||||
allowedMethods: ["POST"],
|
||||
ratelimits: {
|
||||
max: 10,
|
||||
duration: 60,
|
||||
},
|
||||
route: "/api/v1/media",
|
||||
auth: {
|
||||
required: true,
|
||||
oauthPermissions: ["write:media"],
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Upload new media
|
||||
*/
|
||||
export default apiRoute<{
|
||||
file: File;
|
||||
thumbnail?: File;
|
||||
description?: string;
|
||||
// TODO: Add focus
|
||||
focus?: string;
|
||||
file: File;
|
||||
thumbnail?: File;
|
||||
description?: string;
|
||||
// TODO: Add focus
|
||||
focus?: string;
|
||||
}>(async (req, matchedRoute, extraData) => {
|
||||
const { user } = extraData.auth;
|
||||
const { user } = extraData.auth;
|
||||
|
||||
if (!user) {
|
||||
return errorResponse("Unauthorized", 401);
|
||||
}
|
||||
if (!user) {
|
||||
return errorResponse("Unauthorized", 401);
|
||||
}
|
||||
|
||||
const { file, thumbnail, description } = extraData.parsedRequest;
|
||||
const { file, thumbnail, description } = extraData.parsedRequest;
|
||||
|
||||
if (!file) {
|
||||
return errorResponse("No file provided", 400);
|
||||
}
|
||||
if (!file) {
|
||||
return errorResponse("No file provided", 400);
|
||||
}
|
||||
|
||||
const config = await extraData.configManager.getConfig();
|
||||
const config = await extraData.configManager.getConfig();
|
||||
|
||||
if (file.size > config.validation.max_media_size) {
|
||||
return errorResponse(
|
||||
`File too large, max size is ${config.validation.max_media_size} bytes`,
|
||||
413
|
||||
);
|
||||
}
|
||||
if (file.size > config.validation.max_media_size) {
|
||||
return errorResponse(
|
||||
`File too large, max size is ${config.validation.max_media_size} bytes`,
|
||||
413,
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
config.validation.enforce_mime_types &&
|
||||
!config.validation.allowed_mime_types.includes(file.type)
|
||||
) {
|
||||
return errorResponse("Invalid file type", 415);
|
||||
}
|
||||
if (
|
||||
config.validation.enforce_mime_types &&
|
||||
!config.validation.allowed_mime_types.includes(file.type)
|
||||
) {
|
||||
return errorResponse("Invalid file type", 415);
|
||||
}
|
||||
|
||||
if (
|
||||
description &&
|
||||
description.length > config.validation.max_media_description_size
|
||||
) {
|
||||
return errorResponse(
|
||||
`Description too long, max length is ${config.validation.max_media_description_size} characters`,
|
||||
413
|
||||
);
|
||||
}
|
||||
if (
|
||||
description &&
|
||||
description.length > config.validation.max_media_description_size
|
||||
) {
|
||||
return errorResponse(
|
||||
`Description too long, max length is ${config.validation.max_media_description_size} characters`,
|
||||
413,
|
||||
);
|
||||
}
|
||||
|
||||
const sha256 = new Bun.SHA256();
|
||||
const sha256 = new Bun.SHA256();
|
||||
|
||||
const isImage = file.type.startsWith("image/");
|
||||
const isImage = file.type.startsWith("image/");
|
||||
|
||||
const metadata = isImage
|
||||
? await sharp(await file.arrayBuffer()).metadata()
|
||||
: null;
|
||||
const metadata = isImage
|
||||
? await sharp(await file.arrayBuffer()).metadata()
|
||||
: null;
|
||||
|
||||
const blurhash = isImage
|
||||
? encode(
|
||||
new Uint8ClampedArray(await file.arrayBuffer()),
|
||||
metadata?.width ?? 0,
|
||||
metadata?.height ?? 0,
|
||||
4,
|
||||
4
|
||||
)
|
||||
: null;
|
||||
const blurhash = isImage
|
||||
? encode(
|
||||
new Uint8ClampedArray(await file.arrayBuffer()),
|
||||
metadata?.width ?? 0,
|
||||
metadata?.height ?? 0,
|
||||
4,
|
||||
4,
|
||||
)
|
||||
: null;
|
||||
|
||||
let url = "";
|
||||
let url = "";
|
||||
|
||||
let mediaManager: MediaBackend;
|
||||
let mediaManager: MediaBackend;
|
||||
|
||||
switch (config.media.backend as MediaBackendType) {
|
||||
case MediaBackendType.LOCAL:
|
||||
mediaManager = new LocalMediaBackend(config);
|
||||
break;
|
||||
case MediaBackendType.S3:
|
||||
mediaManager = new S3MediaBackend(config);
|
||||
break;
|
||||
default:
|
||||
// TODO: Replace with logger
|
||||
throw new Error("Invalid media backend");
|
||||
}
|
||||
switch (config.media.backend as MediaBackendType) {
|
||||
case MediaBackendType.LOCAL:
|
||||
mediaManager = new LocalMediaBackend(config);
|
||||
break;
|
||||
case MediaBackendType.S3:
|
||||
mediaManager = new S3MediaBackend(config);
|
||||
break;
|
||||
default:
|
||||
// TODO: Replace with logger
|
||||
throw new Error("Invalid media backend");
|
||||
}
|
||||
|
||||
const { uploadedFile } = await mediaManager.addFile(file);
|
||||
const { uploadedFile } = await mediaManager.addFile(file);
|
||||
|
||||
url = getUrl(uploadedFile.name, config);
|
||||
url = getUrl(uploadedFile.name, config);
|
||||
|
||||
let thumbnailUrl = "";
|
||||
let thumbnailUrl = "";
|
||||
|
||||
if (thumbnail) {
|
||||
const { uploadedFile } = await mediaManager.addFile(thumbnail);
|
||||
if (thumbnail) {
|
||||
const { uploadedFile } = await mediaManager.addFile(thumbnail);
|
||||
|
||||
thumbnailUrl = getUrl(uploadedFile.name, config);
|
||||
}
|
||||
thumbnailUrl = getUrl(uploadedFile.name, config);
|
||||
}
|
||||
|
||||
const newAttachment = await client.attachment.create({
|
||||
data: {
|
||||
url,
|
||||
thumbnail_url: thumbnailUrl,
|
||||
sha256: sha256.update(await file.arrayBuffer()).digest("hex"),
|
||||
mime_type: file.type,
|
||||
description: description ?? "",
|
||||
size: file.size,
|
||||
blurhash: blurhash ?? undefined,
|
||||
width: metadata?.width ?? undefined,
|
||||
height: metadata?.height ?? undefined,
|
||||
},
|
||||
});
|
||||
const newAttachment = await client.attachment.create({
|
||||
data: {
|
||||
url,
|
||||
thumbnail_url: thumbnailUrl,
|
||||
sha256: sha256.update(await file.arrayBuffer()).digest("hex"),
|
||||
mime_type: file.type,
|
||||
description: description ?? "",
|
||||
size: file.size,
|
||||
blurhash: blurhash ?? undefined,
|
||||
width: metadata?.width ?? undefined,
|
||||
height: metadata?.height ?? undefined,
|
||||
},
|
||||
});
|
||||
|
||||
// TODO: Add job to process videos and other media
|
||||
// TODO: Add job to process videos and other media
|
||||
|
||||
return jsonResponse(attachmentToAPI(newAttachment));
|
||||
return jsonResponse(attachmentToAPI(newAttachment));
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,37 +1,37 @@
|
|||
import { errorResponse, jsonResponse } from "@response";
|
||||
import { userToAPI } from "~database/entities/User";
|
||||
import { apiRoute, applyConfig } from "@api";
|
||||
import { errorResponse, jsonResponse } from "@response";
|
||||
import { client } from "~database/datasource";
|
||||
import { userToAPI } from "~database/entities/User";
|
||||
import { userRelations } from "~database/entities/relations";
|
||||
|
||||
export const meta = applyConfig({
|
||||
allowedMethods: ["GET"],
|
||||
route: "/api/v1/mutes",
|
||||
ratelimits: {
|
||||
max: 100,
|
||||
duration: 60,
|
||||
},
|
||||
auth: {
|
||||
required: true,
|
||||
},
|
||||
allowedMethods: ["GET"],
|
||||
route: "/api/v1/mutes",
|
||||
ratelimits: {
|
||||
max: 100,
|
||||
duration: 60,
|
||||
},
|
||||
auth: {
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
export default apiRoute(async (req, matchedRoute, extraData) => {
|
||||
const { user } = extraData.auth;
|
||||
const { user } = extraData.auth;
|
||||
|
||||
if (!user) return errorResponse("Unauthorized", 401);
|
||||
if (!user) return errorResponse("Unauthorized", 401);
|
||||
|
||||
const blocks = await client.user.findMany({
|
||||
where: {
|
||||
relationshipSubjects: {
|
||||
some: {
|
||||
ownerId: user.id,
|
||||
muting: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
include: userRelations,
|
||||
});
|
||||
const blocks = await client.user.findMany({
|
||||
where: {
|
||||
relationshipSubjects: {
|
||||
some: {
|
||||
ownerId: user.id,
|
||||
muting: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
include: userRelations,
|
||||
});
|
||||
|
||||
return jsonResponse(blocks.map(u => userToAPI(u)));
|
||||
return jsonResponse(blocks.map((u) => userToAPI(u)));
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,102 +1,102 @@
|
|||
import { errorResponse, jsonResponse } from "@response";
|
||||
import { apiRoute, applyConfig } from "@api";
|
||||
import { errorResponse, jsonResponse } from "@response";
|
||||
import { client } from "~database/datasource";
|
||||
import { notificationToAPI } from "~database/entities/Notification";
|
||||
import {
|
||||
userRelations,
|
||||
statusAndUserRelations,
|
||||
statusAndUserRelations,
|
||||
userRelations,
|
||||
} from "~database/entities/relations";
|
||||
|
||||
export const meta = applyConfig({
|
||||
allowedMethods: ["GET"],
|
||||
route: "/api/v1/notifications",
|
||||
ratelimits: {
|
||||
max: 100,
|
||||
duration: 60,
|
||||
},
|
||||
auth: {
|
||||
required: true,
|
||||
},
|
||||
allowedMethods: ["GET"],
|
||||
route: "/api/v1/notifications",
|
||||
ratelimits: {
|
||||
max: 100,
|
||||
duration: 60,
|
||||
},
|
||||
auth: {
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
export default apiRoute<{
|
||||
max_id?: string;
|
||||
since_id?: string;
|
||||
min_id?: string;
|
||||
limit?: number;
|
||||
exclude_types?: string[];
|
||||
types?: string[];
|
||||
account_id?: string;
|
||||
max_id?: string;
|
||||
since_id?: string;
|
||||
min_id?: string;
|
||||
limit?: number;
|
||||
exclude_types?: string[];
|
||||
types?: string[];
|
||||
account_id?: string;
|
||||
}>(async (req, matchedRoute, extraData) => {
|
||||
const { user } = extraData.auth;
|
||||
const { user } = extraData.auth;
|
||||
|
||||
if (!user) return errorResponse("Unauthorized", 401);
|
||||
if (!user) return errorResponse("Unauthorized", 401);
|
||||
|
||||
const {
|
||||
account_id,
|
||||
exclude_types,
|
||||
limit = 15,
|
||||
max_id,
|
||||
min_id,
|
||||
since_id,
|
||||
types,
|
||||
} = extraData.parsedRequest;
|
||||
const {
|
||||
account_id,
|
||||
exclude_types,
|
||||
limit = 15,
|
||||
max_id,
|
||||
min_id,
|
||||
since_id,
|
||||
types,
|
||||
} = extraData.parsedRequest;
|
||||
|
||||
if (limit > 30) return errorResponse("Limit too high", 400);
|
||||
if (limit > 30) return errorResponse("Limit too high", 400);
|
||||
|
||||
if (limit <= 0) return errorResponse("Limit too low", 400);
|
||||
if (limit <= 0) return errorResponse("Limit too low", 400);
|
||||
|
||||
if (types && exclude_types) {
|
||||
return errorResponse("Can't use both types and exclude_types", 400);
|
||||
}
|
||||
if (types && exclude_types) {
|
||||
return errorResponse("Can't use both types and exclude_types", 400);
|
||||
}
|
||||
|
||||
const objects = await client.notification.findMany({
|
||||
where: {
|
||||
notifiedId: user.id,
|
||||
id: {
|
||||
lt: max_id,
|
||||
gt: min_id,
|
||||
gte: since_id,
|
||||
},
|
||||
type: {
|
||||
in: types,
|
||||
notIn: exclude_types,
|
||||
},
|
||||
accountId: account_id,
|
||||
},
|
||||
include: {
|
||||
account: {
|
||||
include: userRelations,
|
||||
},
|
||||
status: {
|
||||
include: statusAndUserRelations,
|
||||
},
|
||||
},
|
||||
orderBy: {
|
||||
id: "desc",
|
||||
},
|
||||
take: limit,
|
||||
});
|
||||
const objects = await client.notification.findMany({
|
||||
where: {
|
||||
notifiedId: user.id,
|
||||
id: {
|
||||
lt: max_id,
|
||||
gt: min_id,
|
||||
gte: since_id,
|
||||
},
|
||||
type: {
|
||||
in: types,
|
||||
notIn: exclude_types,
|
||||
},
|
||||
accountId: account_id,
|
||||
},
|
||||
include: {
|
||||
account: {
|
||||
include: userRelations,
|
||||
},
|
||||
status: {
|
||||
include: statusAndUserRelations,
|
||||
},
|
||||
},
|
||||
orderBy: {
|
||||
id: "desc",
|
||||
},
|
||||
take: limit,
|
||||
});
|
||||
|
||||
// Constuct HTTP Link header (next and prev)
|
||||
const linkHeader = [];
|
||||
if (objects.length > 0) {
|
||||
const urlWithoutQuery = req.url.split("?")[0];
|
||||
linkHeader.push(
|
||||
`<${urlWithoutQuery}?max_id=${objects[0].id}&limit=${limit}>; rel="next"`
|
||||
);
|
||||
linkHeader.push(
|
||||
`<${urlWithoutQuery}?since_id=${
|
||||
objects.at(-1)?.id
|
||||
}&limit=${limit}>; rel="prev"`
|
||||
);
|
||||
}
|
||||
// Constuct HTTP Link header (next and prev)
|
||||
const linkHeader = [];
|
||||
if (objects.length > 0) {
|
||||
const urlWithoutQuery = req.url.split("?")[0];
|
||||
linkHeader.push(
|
||||
`<${urlWithoutQuery}?max_id=${objects[0].id}&limit=${limit}>; rel="next"`,
|
||||
);
|
||||
linkHeader.push(
|
||||
`<${urlWithoutQuery}?since_id=${
|
||||
objects.at(-1)?.id
|
||||
}&limit=${limit}>; rel="prev"`,
|
||||
);
|
||||
}
|
||||
|
||||
return jsonResponse(
|
||||
await Promise.all(objects.map(n => notificationToAPI(n))),
|
||||
200,
|
||||
{
|
||||
Link: linkHeader.join(", "),
|
||||
}
|
||||
);
|
||||
return jsonResponse(
|
||||
await Promise.all(objects.map((n) => notificationToAPI(n))),
|
||||
200,
|
||||
{
|
||||
Link: linkHeader.join(", "),
|
||||
},
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -5,35 +5,35 @@ import { userToAPI } from "~database/entities/User";
|
|||
import { userRelations } from "~database/entities/relations";
|
||||
|
||||
export const meta = applyConfig({
|
||||
allowedMethods: ["DELETE"],
|
||||
ratelimits: {
|
||||
max: 10,
|
||||
duration: 60,
|
||||
},
|
||||
route: "/api/v1/profile/avatar",
|
||||
auth: {
|
||||
required: true,
|
||||
},
|
||||
allowedMethods: ["DELETE"],
|
||||
ratelimits: {
|
||||
max: 10,
|
||||
duration: 60,
|
||||
},
|
||||
route: "/api/v1/profile/avatar",
|
||||
auth: {
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Deletes a user avatar
|
||||
*/
|
||||
export default apiRoute(async (req, matchedRoute, extraData) => {
|
||||
const { user } = extraData.auth;
|
||||
const { user } = extraData.auth;
|
||||
|
||||
if (!user) return errorResponse("Unauthorized", 401);
|
||||
if (!user) return errorResponse("Unauthorized", 401);
|
||||
|
||||
// Delete user avatar
|
||||
const newUser = await client.user.update({
|
||||
where: {
|
||||
id: user.id,
|
||||
},
|
||||
data: {
|
||||
avatar: "",
|
||||
},
|
||||
include: userRelations,
|
||||
});
|
||||
// Delete user avatar
|
||||
const newUser = await client.user.update({
|
||||
where: {
|
||||
id: user.id,
|
||||
},
|
||||
data: {
|
||||
avatar: "",
|
||||
},
|
||||
include: userRelations,
|
||||
});
|
||||
|
||||
return jsonResponse(userToAPI(newUser));
|
||||
return jsonResponse(userToAPI(newUser));
|
||||
});
|
||||
|
|
|
|||
|
|
@ -5,35 +5,35 @@ import { userToAPI } from "~database/entities/User";
|
|||
import { userRelations } from "~database/entities/relations";
|
||||
|
||||
export const meta = applyConfig({
|
||||
allowedMethods: ["DELETE"],
|
||||
ratelimits: {
|
||||
max: 10,
|
||||
duration: 60,
|
||||
},
|
||||
route: "/api/v1/profile/header",
|
||||
auth: {
|
||||
required: true,
|
||||
},
|
||||
allowedMethods: ["DELETE"],
|
||||
ratelimits: {
|
||||
max: 10,
|
||||
duration: 60,
|
||||
},
|
||||
route: "/api/v1/profile/header",
|
||||
auth: {
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Deletes a user header
|
||||
*/
|
||||
export default apiRoute(async (req, matchedRoute, extraData) => {
|
||||
const { user } = extraData.auth;
|
||||
const { user } = extraData.auth;
|
||||
|
||||
if (!user) return errorResponse("Unauthorized", 401);
|
||||
if (!user) return errorResponse("Unauthorized", 401);
|
||||
|
||||
// Delete user header
|
||||
const newUser = await client.user.update({
|
||||
where: {
|
||||
id: user.id,
|
||||
},
|
||||
data: {
|
||||
header: "",
|
||||
},
|
||||
include: userRelations,
|
||||
});
|
||||
// Delete user header
|
||||
const newUser = await client.user.update({
|
||||
where: {
|
||||
id: user.id,
|
||||
},
|
||||
data: {
|
||||
header: "",
|
||||
},
|
||||
include: userRelations,
|
||||
});
|
||||
|
||||
return jsonResponse(userToAPI(newUser));
|
||||
return jsonResponse(userToAPI(newUser));
|
||||
});
|
||||
|
|
|
|||
|
|
@ -2,51 +2,51 @@ import { apiRoute, applyConfig } from "@api";
|
|||
import { errorResponse, jsonResponse } from "@response";
|
||||
import { client } from "~database/datasource";
|
||||
import {
|
||||
getAncestors,
|
||||
getDescendants,
|
||||
statusToAPI,
|
||||
getAncestors,
|
||||
getDescendants,
|
||||
statusToAPI,
|
||||
} from "~database/entities/Status";
|
||||
import { statusAndUserRelations } from "~database/entities/relations";
|
||||
|
||||
export const meta = applyConfig({
|
||||
allowedMethods: ["GET"],
|
||||
ratelimits: {
|
||||
max: 8,
|
||||
duration: 60,
|
||||
},
|
||||
route: "/api/v1/statuses/:id/context",
|
||||
auth: {
|
||||
required: false,
|
||||
},
|
||||
allowedMethods: ["GET"],
|
||||
ratelimits: {
|
||||
max: 8,
|
||||
duration: 60,
|
||||
},
|
||||
route: "/api/v1/statuses/:id/context",
|
||||
auth: {
|
||||
required: false,
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Fetch a user
|
||||
*/
|
||||
export default apiRoute(async (req, matchedRoute, extraData) => {
|
||||
// Public for public statuses limited to 40 ancestors and 60 descendants with a maximum depth of 20.
|
||||
// User token + read:statuses for up to 4,096 ancestors, 4,096 descendants, unlimited depth, and private statuses.
|
||||
const id = matchedRoute.params.id;
|
||||
// Public for public statuses limited to 40 ancestors and 60 descendants with a maximum depth of 20.
|
||||
// User token + read:statuses for up to 4,096 ancestors, 4,096 descendants, unlimited depth, and private statuses.
|
||||
const id = matchedRoute.params.id;
|
||||
|
||||
const { user } = extraData.auth;
|
||||
const { user } = extraData.auth;
|
||||
|
||||
const foundStatus = await client.status.findUnique({
|
||||
where: { id },
|
||||
include: statusAndUserRelations,
|
||||
});
|
||||
const foundStatus = await client.status.findUnique({
|
||||
where: { id },
|
||||
include: statusAndUserRelations,
|
||||
});
|
||||
|
||||
if (!foundStatus) return errorResponse("Record not found", 404);
|
||||
if (!foundStatus) return errorResponse("Record not found", 404);
|
||||
|
||||
// Get all ancestors
|
||||
const ancestors = await getAncestors(foundStatus, user);
|
||||
const descendants = await getDescendants(foundStatus, user);
|
||||
// Get all ancestors
|
||||
const ancestors = await getAncestors(foundStatus, user);
|
||||
const descendants = await getDescendants(foundStatus, user);
|
||||
|
||||
return jsonResponse({
|
||||
ancestors: await Promise.all(
|
||||
ancestors.map(status => statusToAPI(status, user || undefined))
|
||||
),
|
||||
descendants: await Promise.all(
|
||||
descendants.map(status => statusToAPI(status, user || undefined))
|
||||
),
|
||||
});
|
||||
return jsonResponse({
|
||||
ancestors: await Promise.all(
|
||||
ancestors.map((status) => statusToAPI(status, user || undefined)),
|
||||
),
|
||||
descendants: await Promise.all(
|
||||
descendants.map((status) => statusToAPI(status, user || undefined)),
|
||||
),
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -8,50 +8,50 @@ import { statusAndUserRelations } from "~database/entities/relations";
|
|||
import type { APIStatus } from "~types/entities/status";
|
||||
|
||||
export const meta = applyConfig({
|
||||
allowedMethods: ["POST"],
|
||||
ratelimits: {
|
||||
max: 100,
|
||||
duration: 60,
|
||||
},
|
||||
route: "/api/v1/statuses/:id/favourite",
|
||||
auth: {
|
||||
required: true,
|
||||
},
|
||||
allowedMethods: ["POST"],
|
||||
ratelimits: {
|
||||
max: 100,
|
||||
duration: 60,
|
||||
},
|
||||
route: "/api/v1/statuses/:id/favourite",
|
||||
auth: {
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Favourite a post
|
||||
*/
|
||||
export default apiRoute(async (req, matchedRoute, extraData) => {
|
||||
const id = matchedRoute.params.id;
|
||||
const id = matchedRoute.params.id;
|
||||
|
||||
const { user } = extraData.auth;
|
||||
const { user } = extraData.auth;
|
||||
|
||||
if (!user) return errorResponse("Unauthorized", 401);
|
||||
if (!user) return errorResponse("Unauthorized", 401);
|
||||
|
||||
const status = await client.status.findUnique({
|
||||
where: { id },
|
||||
include: statusAndUserRelations,
|
||||
});
|
||||
const status = await client.status.findUnique({
|
||||
where: { id },
|
||||
include: statusAndUserRelations,
|
||||
});
|
||||
|
||||
// Check if user is authorized to view this status (if it's private)
|
||||
if (!status || !isViewableByUser(status, user))
|
||||
return errorResponse("Record not found", 404);
|
||||
// Check if user is authorized to view this status (if it's private)
|
||||
if (!status || !isViewableByUser(status, user))
|
||||
return errorResponse("Record not found", 404);
|
||||
|
||||
const existingLike = await client.like.findFirst({
|
||||
where: {
|
||||
likedId: status.id,
|
||||
likerId: user.id,
|
||||
},
|
||||
});
|
||||
const existingLike = await client.like.findFirst({
|
||||
where: {
|
||||
likedId: status.id,
|
||||
likerId: user.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (!existingLike) {
|
||||
await createLike(user, status);
|
||||
}
|
||||
if (!existingLike) {
|
||||
await createLike(user, status);
|
||||
}
|
||||
|
||||
return jsonResponse({
|
||||
...(await statusToAPI(status, user)),
|
||||
favourited: true,
|
||||
favourites_count: status._count.likes + 1,
|
||||
} as APIStatus);
|
||||
return jsonResponse({
|
||||
...(await statusToAPI(status, user)),
|
||||
favourited: true,
|
||||
favourites_count: status._count.likes + 1,
|
||||
} as APIStatus);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -4,101 +4,101 @@ import { client } from "~database/datasource";
|
|||
import { isViewableByUser } from "~database/entities/Status";
|
||||
import { userToAPI } from "~database/entities/User";
|
||||
import {
|
||||
statusAndUserRelations,
|
||||
userRelations,
|
||||
statusAndUserRelations,
|
||||
userRelations,
|
||||
} from "~database/entities/relations";
|
||||
|
||||
export const meta = applyConfig({
|
||||
allowedMethods: ["GET"],
|
||||
ratelimits: {
|
||||
max: 100,
|
||||
duration: 60,
|
||||
},
|
||||
route: "/api/v1/statuses/:id/favourited_by",
|
||||
auth: {
|
||||
required: true,
|
||||
},
|
||||
allowedMethods: ["GET"],
|
||||
ratelimits: {
|
||||
max: 100,
|
||||
duration: 60,
|
||||
},
|
||||
route: "/api/v1/statuses/:id/favourited_by",
|
||||
auth: {
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Fetch users who favourited the post
|
||||
*/
|
||||
export default apiRoute<{
|
||||
max_id?: string;
|
||||
min_id?: string;
|
||||
since_id?: string;
|
||||
limit?: number;
|
||||
max_id?: string;
|
||||
min_id?: string;
|
||||
since_id?: string;
|
||||
limit?: number;
|
||||
}>(async (req, matchedRoute, extraData) => {
|
||||
const id = matchedRoute.params.id;
|
||||
const id = matchedRoute.params.id;
|
||||
|
||||
const { user } = extraData.auth;
|
||||
const { user } = extraData.auth;
|
||||
|
||||
const status = await client.status.findUnique({
|
||||
where: { id },
|
||||
include: statusAndUserRelations,
|
||||
});
|
||||
const status = await client.status.findUnique({
|
||||
where: { id },
|
||||
include: statusAndUserRelations,
|
||||
});
|
||||
|
||||
// Check if user is authorized to view this status (if it's private)
|
||||
if (!status || !isViewableByUser(status, user))
|
||||
return errorResponse("Record not found", 404);
|
||||
// Check if user is authorized to view this status (if it's private)
|
||||
if (!status || !isViewableByUser(status, user))
|
||||
return errorResponse("Record not found", 404);
|
||||
|
||||
const {
|
||||
max_id = null,
|
||||
min_id = null,
|
||||
since_id = null,
|
||||
limit = 40,
|
||||
} = extraData.parsedRequest;
|
||||
const {
|
||||
max_id = null,
|
||||
min_id = null,
|
||||
since_id = null,
|
||||
limit = 40,
|
||||
} = extraData.parsedRequest;
|
||||
|
||||
// Check for limit limits
|
||||
if (limit > 80) return errorResponse("Invalid limit (maximum is 80)", 400);
|
||||
if (limit < 1) return errorResponse("Invalid limit", 400);
|
||||
// Check for limit limits
|
||||
if (limit > 80) return errorResponse("Invalid limit (maximum is 80)", 400);
|
||||
if (limit < 1) return errorResponse("Invalid limit", 400);
|
||||
|
||||
const objects = await client.user.findMany({
|
||||
where: {
|
||||
likes: {
|
||||
some: {
|
||||
likedId: status.id,
|
||||
},
|
||||
},
|
||||
id: {
|
||||
lt: max_id ?? undefined,
|
||||
gte: since_id ?? undefined,
|
||||
gt: min_id ?? undefined,
|
||||
},
|
||||
},
|
||||
include: {
|
||||
...userRelations,
|
||||
likes: {
|
||||
where: {
|
||||
likedId: status.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
take: limit,
|
||||
orderBy: {
|
||||
id: "desc",
|
||||
},
|
||||
});
|
||||
const objects = await client.user.findMany({
|
||||
where: {
|
||||
likes: {
|
||||
some: {
|
||||
likedId: status.id,
|
||||
},
|
||||
},
|
||||
id: {
|
||||
lt: max_id ?? undefined,
|
||||
gte: since_id ?? undefined,
|
||||
gt: min_id ?? undefined,
|
||||
},
|
||||
},
|
||||
include: {
|
||||
...userRelations,
|
||||
likes: {
|
||||
where: {
|
||||
likedId: status.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
take: limit,
|
||||
orderBy: {
|
||||
id: "desc",
|
||||
},
|
||||
});
|
||||
|
||||
// Constuct HTTP Link header (next and prev)
|
||||
const linkHeader = [];
|
||||
if (objects.length > 0) {
|
||||
const urlWithoutQuery = req.url.split("?")[0];
|
||||
linkHeader.push(
|
||||
`<${urlWithoutQuery}?max_id=${objects[0].id}&limit=${limit}>; rel="next"`
|
||||
);
|
||||
linkHeader.push(
|
||||
`<${urlWithoutQuery}?since_id=${
|
||||
objects[objects.length - 1].id
|
||||
}&limit=${limit}>; rel="prev"`
|
||||
);
|
||||
}
|
||||
// Constuct HTTP Link header (next and prev)
|
||||
const linkHeader = [];
|
||||
if (objects.length > 0) {
|
||||
const urlWithoutQuery = req.url.split("?")[0];
|
||||
linkHeader.push(
|
||||
`<${urlWithoutQuery}?max_id=${objects[0].id}&limit=${limit}>; rel="next"`,
|
||||
);
|
||||
linkHeader.push(
|
||||
`<${urlWithoutQuery}?since_id=${
|
||||
objects[objects.length - 1].id
|
||||
}&limit=${limit}>; rel="prev"`,
|
||||
);
|
||||
}
|
||||
|
||||
return jsonResponse(
|
||||
objects.map(user => userToAPI(user)),
|
||||
200,
|
||||
{
|
||||
Link: linkHeader.join(", "),
|
||||
}
|
||||
);
|
||||
return jsonResponse(
|
||||
objects.map((user) => userToAPI(user)),
|
||||
200,
|
||||
{
|
||||
Link: linkHeader.join(", "),
|
||||
},
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -4,215 +4,217 @@ import { sanitizeHtml } from "@sanitization";
|
|||
import { parse } from "marked";
|
||||
import { client } from "~database/datasource";
|
||||
import {
|
||||
editStatus,
|
||||
isViewableByUser,
|
||||
statusToAPI,
|
||||
editStatus,
|
||||
isViewableByUser,
|
||||
statusToAPI,
|
||||
} from "~database/entities/Status";
|
||||
import { statusAndUserRelations } from "~database/entities/relations";
|
||||
|
||||
export const meta = applyConfig({
|
||||
allowedMethods: ["GET", "DELETE", "PUT"],
|
||||
ratelimits: {
|
||||
max: 100,
|
||||
duration: 60,
|
||||
},
|
||||
route: "/api/v1/statuses/:id",
|
||||
auth: {
|
||||
required: false,
|
||||
requiredOnMethods: ["DELETE", "PUT"],
|
||||
},
|
||||
allowedMethods: ["GET", "DELETE", "PUT"],
|
||||
ratelimits: {
|
||||
max: 100,
|
||||
duration: 60,
|
||||
},
|
||||
route: "/api/v1/statuses/:id",
|
||||
auth: {
|
||||
required: false,
|
||||
requiredOnMethods: ["DELETE", "PUT"],
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Fetch a user
|
||||
*/
|
||||
export default apiRoute<{
|
||||
status?: string;
|
||||
spoiler_text?: string;
|
||||
sensitive?: boolean;
|
||||
language?: string;
|
||||
content_type?: string;
|
||||
media_ids?: string[];
|
||||
"poll[options]"?: string[];
|
||||
"poll[expires_in]"?: number;
|
||||
"poll[multiple]"?: boolean;
|
||||
"poll[hide_totals]"?: boolean;
|
||||
status?: string;
|
||||
spoiler_text?: string;
|
||||
sensitive?: boolean;
|
||||
language?: string;
|
||||
content_type?: string;
|
||||
media_ids?: string[];
|
||||
"poll[options]"?: string[];
|
||||
"poll[expires_in]"?: number;
|
||||
"poll[multiple]"?: boolean;
|
||||
"poll[hide_totals]"?: boolean;
|
||||
}>(async (req, matchedRoute, extraData) => {
|
||||
const id = matchedRoute.params.id;
|
||||
const id = matchedRoute.params.id;
|
||||
|
||||
const { user } = extraData.auth;
|
||||
const { user } = extraData.auth;
|
||||
|
||||
const status = await client.status.findUnique({
|
||||
where: { id },
|
||||
include: statusAndUserRelations,
|
||||
});
|
||||
const status = await client.status.findUnique({
|
||||
where: { id },
|
||||
include: statusAndUserRelations,
|
||||
});
|
||||
|
||||
const config = await extraData.configManager.getConfig();
|
||||
const config = await extraData.configManager.getConfig();
|
||||
|
||||
// Check if user is authorized to view this status (if it's private)
|
||||
if (!status || !isViewableByUser(status, user))
|
||||
return errorResponse("Record not found", 404);
|
||||
// Check if user is authorized to view this status (if it's private)
|
||||
if (!status || !isViewableByUser(status, user))
|
||||
return errorResponse("Record not found", 404);
|
||||
|
||||
if (req.method === "GET") {
|
||||
return jsonResponse(await statusToAPI(status));
|
||||
} else if (req.method === "DELETE") {
|
||||
if (status.authorId !== user?.id) {
|
||||
return errorResponse("Unauthorized", 401);
|
||||
}
|
||||
if (req.method === "GET") {
|
||||
return jsonResponse(await statusToAPI(status));
|
||||
}
|
||||
if (req.method === "DELETE") {
|
||||
if (status.authorId !== user?.id) {
|
||||
return errorResponse("Unauthorized", 401);
|
||||
}
|
||||
|
||||
// TODO: Implement delete and redraft functionality
|
||||
// TODO: Implement delete and redraft functionality
|
||||
|
||||
// Get associated Status object
|
||||
// Get associated Status object
|
||||
|
||||
// Delete status and all associated objects
|
||||
await client.status.delete({
|
||||
where: { id },
|
||||
});
|
||||
// Delete status and all associated objects
|
||||
await client.status.delete({
|
||||
where: { id },
|
||||
});
|
||||
|
||||
return jsonResponse(
|
||||
{
|
||||
...(await statusToAPI(status, user)),
|
||||
// TODO: Add
|
||||
// text: Add source text
|
||||
// poll: Add source poll
|
||||
// media_attachments
|
||||
},
|
||||
200
|
||||
);
|
||||
} else if (req.method == "PUT") {
|
||||
if (status.authorId !== user?.id) {
|
||||
return errorResponse("Unauthorized", 401);
|
||||
}
|
||||
return jsonResponse(
|
||||
{
|
||||
...(await statusToAPI(status, user)),
|
||||
// TODO: Add
|
||||
// text: Add source text
|
||||
// poll: Add source poll
|
||||
// media_attachments
|
||||
},
|
||||
200,
|
||||
);
|
||||
}
|
||||
if (req.method === "PUT") {
|
||||
if (status.authorId !== user?.id) {
|
||||
return errorResponse("Unauthorized", 401);
|
||||
}
|
||||
|
||||
const {
|
||||
status: statusText,
|
||||
content_type,
|
||||
"poll[expires_in]": expires_in,
|
||||
"poll[options]": options,
|
||||
media_ids,
|
||||
spoiler_text,
|
||||
sensitive,
|
||||
} = extraData.parsedRequest;
|
||||
const {
|
||||
status: statusText,
|
||||
content_type,
|
||||
"poll[expires_in]": expires_in,
|
||||
"poll[options]": options,
|
||||
media_ids,
|
||||
spoiler_text,
|
||||
sensitive,
|
||||
} = extraData.parsedRequest;
|
||||
|
||||
// TODO: Add Poll support
|
||||
// Validate status
|
||||
if (!statusText && !(media_ids && media_ids.length > 0)) {
|
||||
return errorResponse(
|
||||
"Status is required unless media is attached",
|
||||
422
|
||||
);
|
||||
}
|
||||
// TODO: Add Poll support
|
||||
// Validate status
|
||||
if (!statusText && !(media_ids && media_ids.length > 0)) {
|
||||
return errorResponse(
|
||||
"Status is required unless media is attached",
|
||||
422,
|
||||
);
|
||||
}
|
||||
|
||||
// Validate media_ids
|
||||
if (media_ids && !Array.isArray(media_ids)) {
|
||||
return errorResponse("Media IDs must be an array", 422);
|
||||
}
|
||||
// Validate media_ids
|
||||
if (media_ids && !Array.isArray(media_ids)) {
|
||||
return errorResponse("Media IDs must be an array", 422);
|
||||
}
|
||||
|
||||
// Validate poll options
|
||||
if (options && !Array.isArray(options)) {
|
||||
return errorResponse("Poll options must be an array", 422);
|
||||
}
|
||||
// Validate poll options
|
||||
if (options && !Array.isArray(options)) {
|
||||
return errorResponse("Poll options must be an array", 422);
|
||||
}
|
||||
|
||||
if (options && options.length > 4) {
|
||||
return errorResponse("Poll options must be less than 5", 422);
|
||||
}
|
||||
if (options && options.length > 4) {
|
||||
return errorResponse("Poll options must be less than 5", 422);
|
||||
}
|
||||
|
||||
if (media_ids && media_ids.length > 0) {
|
||||
// Disallow poll
|
||||
if (options) {
|
||||
return errorResponse("Cannot attach poll to media", 422);
|
||||
}
|
||||
if (media_ids.length > 4) {
|
||||
return errorResponse("Media IDs must be less than 5", 422);
|
||||
}
|
||||
}
|
||||
if (media_ids && media_ids.length > 0) {
|
||||
// Disallow poll
|
||||
if (options) {
|
||||
return errorResponse("Cannot attach poll to media", 422);
|
||||
}
|
||||
if (media_ids.length > 4) {
|
||||
return errorResponse("Media IDs must be less than 5", 422);
|
||||
}
|
||||
}
|
||||
|
||||
if (options && options.length > config.validation.max_poll_options) {
|
||||
return errorResponse(
|
||||
`Poll options must be less than ${config.validation.max_poll_options}`,
|
||||
422
|
||||
);
|
||||
}
|
||||
if (options && options.length > config.validation.max_poll_options) {
|
||||
return errorResponse(
|
||||
`Poll options must be less than ${config.validation.max_poll_options}`,
|
||||
422,
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
options &&
|
||||
options.some(
|
||||
option => option.length > config.validation.max_poll_option_size
|
||||
)
|
||||
) {
|
||||
return errorResponse(
|
||||
`Poll options must be less than ${config.validation.max_poll_option_size} characters`,
|
||||
422
|
||||
);
|
||||
}
|
||||
if (
|
||||
options?.some(
|
||||
(option) =>
|
||||
option.length > config.validation.max_poll_option_size,
|
||||
)
|
||||
) {
|
||||
return errorResponse(
|
||||
`Poll options must be less than ${config.validation.max_poll_option_size} characters`,
|
||||
422,
|
||||
);
|
||||
}
|
||||
|
||||
if (expires_in && expires_in < config.validation.min_poll_duration) {
|
||||
return errorResponse(
|
||||
`Poll duration must be greater than ${config.validation.min_poll_duration} seconds`,
|
||||
422
|
||||
);
|
||||
}
|
||||
if (expires_in && expires_in < config.validation.min_poll_duration) {
|
||||
return errorResponse(
|
||||
`Poll duration must be greater than ${config.validation.min_poll_duration} seconds`,
|
||||
422,
|
||||
);
|
||||
}
|
||||
|
||||
if (expires_in && expires_in > config.validation.max_poll_duration) {
|
||||
return errorResponse(
|
||||
`Poll duration must be less than ${config.validation.max_poll_duration} seconds`,
|
||||
422
|
||||
);
|
||||
}
|
||||
if (expires_in && expires_in > config.validation.max_poll_duration) {
|
||||
return errorResponse(
|
||||
`Poll duration must be less than ${config.validation.max_poll_duration} seconds`,
|
||||
422,
|
||||
);
|
||||
}
|
||||
|
||||
let sanitizedStatus: string;
|
||||
let sanitizedStatus: string;
|
||||
|
||||
if (content_type === "text/markdown") {
|
||||
sanitizedStatus = await sanitizeHtml(await parse(statusText ?? ""));
|
||||
} else if (content_type === "text/x.misskeymarkdown") {
|
||||
// Parse as MFM
|
||||
// TODO: Parse as MFM
|
||||
sanitizedStatus = await sanitizeHtml(await parse(statusText ?? ""));
|
||||
} else {
|
||||
sanitizedStatus = await sanitizeHtml(statusText ?? "");
|
||||
}
|
||||
if (content_type === "text/markdown") {
|
||||
sanitizedStatus = await sanitizeHtml(await parse(statusText ?? ""));
|
||||
} else if (content_type === "text/x.misskeymarkdown") {
|
||||
// Parse as MFM
|
||||
// TODO: Parse as MFM
|
||||
sanitizedStatus = await sanitizeHtml(await parse(statusText ?? ""));
|
||||
} else {
|
||||
sanitizedStatus = await sanitizeHtml(statusText ?? "");
|
||||
}
|
||||
|
||||
if (sanitizedStatus.length > config.validation.max_note_size) {
|
||||
return errorResponse(
|
||||
`Status must be less than ${config.validation.max_note_size} characters`,
|
||||
400
|
||||
);
|
||||
}
|
||||
if (sanitizedStatus.length > config.validation.max_note_size) {
|
||||
return errorResponse(
|
||||
`Status must be less than ${config.validation.max_note_size} characters`,
|
||||
400,
|
||||
);
|
||||
}
|
||||
|
||||
// Check if status body doesnt match filters
|
||||
if (
|
||||
config.filters.note_content.some(filter =>
|
||||
statusText?.match(filter)
|
||||
)
|
||||
) {
|
||||
return errorResponse("Status contains blocked words", 422);
|
||||
}
|
||||
// Check if status body doesnt match filters
|
||||
if (
|
||||
config.filters.note_content.some((filter) =>
|
||||
statusText?.match(filter),
|
||||
)
|
||||
) {
|
||||
return errorResponse("Status contains blocked words", 422);
|
||||
}
|
||||
|
||||
// Check if media attachments are all valid
|
||||
// Check if media attachments are all valid
|
||||
|
||||
const foundAttachments = await client.attachment.findMany({
|
||||
where: {
|
||||
id: {
|
||||
in: media_ids ?? [],
|
||||
},
|
||||
},
|
||||
});
|
||||
const foundAttachments = await client.attachment.findMany({
|
||||
where: {
|
||||
id: {
|
||||
in: media_ids ?? [],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (foundAttachments.length !== (media_ids ?? []).length) {
|
||||
return errorResponse("Invalid media IDs", 422);
|
||||
}
|
||||
if (foundAttachments.length !== (media_ids ?? []).length) {
|
||||
return errorResponse("Invalid media IDs", 422);
|
||||
}
|
||||
|
||||
// Update status
|
||||
const newStatus = await editStatus(status, {
|
||||
content: sanitizedStatus,
|
||||
content_type,
|
||||
media_attachments: media_ids,
|
||||
spoiler_text: spoiler_text ?? "",
|
||||
sensitive: sensitive ?? false,
|
||||
});
|
||||
// Update status
|
||||
const newStatus = await editStatus(status, {
|
||||
content: sanitizedStatus,
|
||||
content_type,
|
||||
media_attachments: media_ids,
|
||||
spoiler_text: spoiler_text ?? "",
|
||||
sensitive: sensitive ?? false,
|
||||
});
|
||||
|
||||
return jsonResponse(await statusToAPI(newStatus, user));
|
||||
}
|
||||
return jsonResponse(await statusToAPI(newStatus, user));
|
||||
}
|
||||
|
||||
return jsonResponse({});
|
||||
return jsonResponse({});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -6,55 +6,55 @@ import { statusToAPI } from "~database/entities/Status";
|
|||
import { statusAndUserRelations } from "~database/entities/relations";
|
||||
|
||||
export const meta = applyConfig({
|
||||
allowedMethods: ["POST"],
|
||||
ratelimits: {
|
||||
max: 100,
|
||||
duration: 60,
|
||||
},
|
||||
route: "/api/v1/statuses/:id/pin",
|
||||
auth: {
|
||||
required: true,
|
||||
},
|
||||
allowedMethods: ["POST"],
|
||||
ratelimits: {
|
||||
max: 100,
|
||||
duration: 60,
|
||||
},
|
||||
route: "/api/v1/statuses/:id/pin",
|
||||
auth: {
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Pin a post
|
||||
*/
|
||||
export default apiRoute(async (req, matchedRoute, extraData) => {
|
||||
const id = matchedRoute.params.id;
|
||||
const id = matchedRoute.params.id;
|
||||
|
||||
const { user } = extraData.auth;
|
||||
const { user } = extraData.auth;
|
||||
|
||||
if (!user) return errorResponse("Unauthorized", 401);
|
||||
if (!user) return errorResponse("Unauthorized", 401);
|
||||
|
||||
let status = await client.status.findUnique({
|
||||
where: { id },
|
||||
include: statusAndUserRelations,
|
||||
});
|
||||
let status = await client.status.findUnique({
|
||||
where: { id },
|
||||
include: statusAndUserRelations,
|
||||
});
|
||||
|
||||
// Check if status exists
|
||||
if (!status) return errorResponse("Record not found", 404);
|
||||
// Check if status exists
|
||||
if (!status) return errorResponse("Record not found", 404);
|
||||
|
||||
// Check if status is user's
|
||||
if (status.authorId !== user.id) return errorResponse("Unauthorized", 401);
|
||||
// Check if status is user's
|
||||
if (status.authorId !== user.id) return errorResponse("Unauthorized", 401);
|
||||
|
||||
await client.user.update({
|
||||
where: { id: user.id },
|
||||
data: {
|
||||
pinnedNotes: {
|
||||
connect: {
|
||||
id: status.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
await client.user.update({
|
||||
where: { id: user.id },
|
||||
data: {
|
||||
pinnedNotes: {
|
||||
connect: {
|
||||
id: status.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
status = await client.status.findUnique({
|
||||
where: { id },
|
||||
include: statusAndUserRelations,
|
||||
});
|
||||
status = await client.status.findUnique({
|
||||
where: { id },
|
||||
include: statusAndUserRelations,
|
||||
});
|
||||
|
||||
if (!status) return errorResponse("Record not found", 404);
|
||||
if (!status) return errorResponse("Record not found", 404);
|
||||
|
||||
return jsonResponse(statusToAPI(status, user));
|
||||
return jsonResponse(statusToAPI(status, user));
|
||||
});
|
||||
|
|
|
|||
|
|
@ -3,95 +3,95 @@ import { apiRoute, applyConfig } from "@api";
|
|||
import { errorResponse, jsonResponse } from "@response";
|
||||
import { client } from "~database/datasource";
|
||||
import { isViewableByUser, statusToAPI } from "~database/entities/Status";
|
||||
import { type UserWithRelations } from "~database/entities/User";
|
||||
import type { UserWithRelations } from "~database/entities/User";
|
||||
import { statusAndUserRelations } from "~database/entities/relations";
|
||||
|
||||
export const meta = applyConfig({
|
||||
allowedMethods: ["POST"],
|
||||
ratelimits: {
|
||||
max: 100,
|
||||
duration: 60,
|
||||
},
|
||||
route: "/api/v1/statuses/:id/reblog",
|
||||
auth: {
|
||||
required: true,
|
||||
},
|
||||
allowedMethods: ["POST"],
|
||||
ratelimits: {
|
||||
max: 100,
|
||||
duration: 60,
|
||||
},
|
||||
route: "/api/v1/statuses/:id/reblog",
|
||||
auth: {
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Reblogs a post
|
||||
*/
|
||||
export default apiRoute<{
|
||||
visibility: "public" | "unlisted" | "private";
|
||||
visibility: "public" | "unlisted" | "private";
|
||||
}>(async (req, matchedRoute, extraData) => {
|
||||
const id = matchedRoute.params.id;
|
||||
const config = await extraData.configManager.getConfig();
|
||||
const id = matchedRoute.params.id;
|
||||
const config = await extraData.configManager.getConfig();
|
||||
|
||||
const { user } = extraData.auth;
|
||||
const { user } = extraData.auth;
|
||||
|
||||
if (!user) return errorResponse("Unauthorized", 401);
|
||||
if (!user) return errorResponse("Unauthorized", 401);
|
||||
|
||||
const { visibility = "public" } = extraData.parsedRequest;
|
||||
const { visibility = "public" } = extraData.parsedRequest;
|
||||
|
||||
const status = await client.status.findUnique({
|
||||
where: { id },
|
||||
include: statusAndUserRelations,
|
||||
});
|
||||
const status = await client.status.findUnique({
|
||||
where: { id },
|
||||
include: statusAndUserRelations,
|
||||
});
|
||||
|
||||
// Check if user is authorized to view this status (if it's private)
|
||||
if (!status || !isViewableByUser(status, user))
|
||||
return errorResponse("Record not found", 404);
|
||||
// Check if user is authorized to view this status (if it's private)
|
||||
if (!status || !isViewableByUser(status, user))
|
||||
return errorResponse("Record not found", 404);
|
||||
|
||||
const existingReblog = await client.status.findFirst({
|
||||
where: {
|
||||
authorId: user.id,
|
||||
reblogId: status.id,
|
||||
},
|
||||
});
|
||||
const existingReblog = await client.status.findFirst({
|
||||
where: {
|
||||
authorId: user.id,
|
||||
reblogId: status.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (existingReblog) {
|
||||
return errorResponse("Already reblogged", 422);
|
||||
}
|
||||
if (existingReblog) {
|
||||
return errorResponse("Already reblogged", 422);
|
||||
}
|
||||
|
||||
const newReblog = await client.status.create({
|
||||
data: {
|
||||
authorId: user.id,
|
||||
reblogId: status.id,
|
||||
isReblog: true,
|
||||
uri: `${config.http.base_url}/statuses/FAKE-${crypto.randomUUID()}`,
|
||||
visibility,
|
||||
sensitive: false,
|
||||
},
|
||||
include: statusAndUserRelations,
|
||||
});
|
||||
const newReblog = await client.status.create({
|
||||
data: {
|
||||
authorId: user.id,
|
||||
reblogId: status.id,
|
||||
isReblog: true,
|
||||
uri: `${config.http.base_url}/statuses/FAKE-${crypto.randomUUID()}`,
|
||||
visibility,
|
||||
sensitive: false,
|
||||
},
|
||||
include: statusAndUserRelations,
|
||||
});
|
||||
|
||||
await client.status.update({
|
||||
where: { id: newReblog.id },
|
||||
data: {
|
||||
uri: `${config.http.base_url}/statuses/${newReblog.id}`,
|
||||
},
|
||||
include: statusAndUserRelations,
|
||||
});
|
||||
await client.status.update({
|
||||
where: { id: newReblog.id },
|
||||
data: {
|
||||
uri: `${config.http.base_url}/statuses/${newReblog.id}`,
|
||||
},
|
||||
include: statusAndUserRelations,
|
||||
});
|
||||
|
||||
// Create notification for reblog if reblogged user is on the same instance
|
||||
if ((status.author as UserWithRelations).instanceId === user.instanceId) {
|
||||
await client.notification.create({
|
||||
data: {
|
||||
accountId: user.id,
|
||||
notifiedId: status.authorId,
|
||||
type: "reblog",
|
||||
statusId: status.reblogId,
|
||||
},
|
||||
});
|
||||
}
|
||||
// Create notification for reblog if reblogged user is on the same instance
|
||||
if ((status.author as UserWithRelations).instanceId === user.instanceId) {
|
||||
await client.notification.create({
|
||||
data: {
|
||||
accountId: user.id,
|
||||
notifiedId: status.authorId,
|
||||
type: "reblog",
|
||||
statusId: status.reblogId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return jsonResponse(
|
||||
await statusToAPI(
|
||||
{
|
||||
...newReblog,
|
||||
uri: `${config.http.base_url}/statuses/${newReblog.id}`,
|
||||
},
|
||||
user
|
||||
)
|
||||
);
|
||||
return jsonResponse(
|
||||
await statusToAPI(
|
||||
{
|
||||
...newReblog,
|
||||
uri: `${config.http.base_url}/statuses/${newReblog.id}`,
|
||||
},
|
||||
user,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -4,102 +4,102 @@ import { client } from "~database/datasource";
|
|||
import { isViewableByUser } from "~database/entities/Status";
|
||||
import { userToAPI } from "~database/entities/User";
|
||||
import {
|
||||
statusAndUserRelations,
|
||||
userRelations,
|
||||
statusAndUserRelations,
|
||||
userRelations,
|
||||
} from "~database/entities/relations";
|
||||
|
||||
export const meta = applyConfig({
|
||||
allowedMethods: ["GET"],
|
||||
ratelimits: {
|
||||
max: 100,
|
||||
duration: 60,
|
||||
},
|
||||
route: "/api/v1/statuses/:id/reblogged_by",
|
||||
auth: {
|
||||
required: true,
|
||||
},
|
||||
allowedMethods: ["GET"],
|
||||
ratelimits: {
|
||||
max: 100,
|
||||
duration: 60,
|
||||
},
|
||||
route: "/api/v1/statuses/:id/reblogged_by",
|
||||
auth: {
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Fetch users who reblogged the post
|
||||
*/
|
||||
export default apiRoute<{
|
||||
max_id?: string;
|
||||
min_id?: string;
|
||||
since_id?: string;
|
||||
limit?: number;
|
||||
max_id?: string;
|
||||
min_id?: string;
|
||||
since_id?: string;
|
||||
limit?: number;
|
||||
}>(async (req, matchedRoute, extraData) => {
|
||||
const id = matchedRoute.params.id;
|
||||
const id = matchedRoute.params.id;
|
||||
|
||||
const { user } = extraData.auth;
|
||||
const { user } = extraData.auth;
|
||||
|
||||
const status = await client.status.findUnique({
|
||||
where: { id },
|
||||
include: statusAndUserRelations,
|
||||
});
|
||||
const status = await client.status.findUnique({
|
||||
where: { id },
|
||||
include: statusAndUserRelations,
|
||||
});
|
||||
|
||||
// Check if user is authorized to view this status (if it's private)
|
||||
if (!status || !isViewableByUser(status, user))
|
||||
return errorResponse("Record not found", 404);
|
||||
// Check if user is authorized to view this status (if it's private)
|
||||
if (!status || !isViewableByUser(status, user))
|
||||
return errorResponse("Record not found", 404);
|
||||
|
||||
const {
|
||||
max_id = null,
|
||||
min_id = null,
|
||||
since_id = null,
|
||||
limit = 40,
|
||||
} = extraData.parsedRequest;
|
||||
const {
|
||||
max_id = null,
|
||||
min_id = null,
|
||||
since_id = null,
|
||||
limit = 40,
|
||||
} = extraData.parsedRequest;
|
||||
|
||||
// Check for limit limits
|
||||
if (limit > 80) return errorResponse("Invalid limit (maximum is 80)", 400);
|
||||
if (limit < 1) return errorResponse("Invalid limit", 400);
|
||||
// Check for limit limits
|
||||
if (limit > 80) return errorResponse("Invalid limit (maximum is 80)", 400);
|
||||
if (limit < 1) return errorResponse("Invalid limit", 400);
|
||||
|
||||
const objects = await client.user.findMany({
|
||||
where: {
|
||||
statuses: {
|
||||
some: {
|
||||
reblogId: status.id,
|
||||
},
|
||||
},
|
||||
id: {
|
||||
lt: max_id ?? undefined,
|
||||
gte: since_id ?? undefined,
|
||||
gt: min_id ?? undefined,
|
||||
},
|
||||
},
|
||||
include: {
|
||||
...userRelations,
|
||||
statuses: {
|
||||
where: {
|
||||
reblogId: status.id,
|
||||
},
|
||||
include: statusAndUserRelations,
|
||||
},
|
||||
},
|
||||
take: limit,
|
||||
orderBy: {
|
||||
id: "desc",
|
||||
},
|
||||
});
|
||||
const objects = await client.user.findMany({
|
||||
where: {
|
||||
statuses: {
|
||||
some: {
|
||||
reblogId: status.id,
|
||||
},
|
||||
},
|
||||
id: {
|
||||
lt: max_id ?? undefined,
|
||||
gte: since_id ?? undefined,
|
||||
gt: min_id ?? undefined,
|
||||
},
|
||||
},
|
||||
include: {
|
||||
...userRelations,
|
||||
statuses: {
|
||||
where: {
|
||||
reblogId: status.id,
|
||||
},
|
||||
include: statusAndUserRelations,
|
||||
},
|
||||
},
|
||||
take: limit,
|
||||
orderBy: {
|
||||
id: "desc",
|
||||
},
|
||||
});
|
||||
|
||||
// Constuct HTTP Link header (next and prev)
|
||||
const linkHeader = [];
|
||||
if (objects.length > 0) {
|
||||
const urlWithoutQuery = req.url.split("?")[0];
|
||||
linkHeader.push(
|
||||
`<${urlWithoutQuery}?max_id=${objects[0].id}&limit=${limit}>; rel="next"`
|
||||
);
|
||||
linkHeader.push(
|
||||
`<${urlWithoutQuery}?since_id=${
|
||||
objects[objects.length - 1].id
|
||||
}&limit=${limit}>; rel="prev"`
|
||||
);
|
||||
}
|
||||
// Constuct HTTP Link header (next and prev)
|
||||
const linkHeader = [];
|
||||
if (objects.length > 0) {
|
||||
const urlWithoutQuery = req.url.split("?")[0];
|
||||
linkHeader.push(
|
||||
`<${urlWithoutQuery}?max_id=${objects[0].id}&limit=${limit}>; rel="next"`,
|
||||
);
|
||||
linkHeader.push(
|
||||
`<${urlWithoutQuery}?since_id=${
|
||||
objects[objects.length - 1].id
|
||||
}&limit=${limit}>; rel="prev"`,
|
||||
);
|
||||
}
|
||||
|
||||
return jsonResponse(
|
||||
objects.map(user => userToAPI(user)),
|
||||
200,
|
||||
{
|
||||
Link: linkHeader.join(", "),
|
||||
}
|
||||
);
|
||||
return jsonResponse(
|
||||
objects.map((user) => userToAPI(user)),
|
||||
200,
|
||||
{
|
||||
Link: linkHeader.join(", "),
|
||||
},
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -5,35 +5,35 @@ import { isViewableByUser } from "~database/entities/Status";
|
|||
import { statusAndUserRelations } from "~database/entities/relations";
|
||||
|
||||
export const meta = applyConfig({
|
||||
allowedMethods: ["GET"],
|
||||
ratelimits: {
|
||||
max: 100,
|
||||
duration: 60,
|
||||
},
|
||||
route: "/api/v1/statuses/:id/source",
|
||||
auth: {
|
||||
required: true,
|
||||
},
|
||||
allowedMethods: ["GET"],
|
||||
ratelimits: {
|
||||
max: 100,
|
||||
duration: 60,
|
||||
},
|
||||
route: "/api/v1/statuses/:id/source",
|
||||
auth: {
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Favourite a post
|
||||
*/
|
||||
export default apiRoute(async (req, matchedRoute, extraData) => {
|
||||
const id = matchedRoute.params.id;
|
||||
const id = matchedRoute.params.id;
|
||||
|
||||
const { user } = extraData.auth;
|
||||
const { user } = extraData.auth;
|
||||
|
||||
if (!user) return errorResponse("Unauthorized", 401);
|
||||
if (!user) return errorResponse("Unauthorized", 401);
|
||||
|
||||
const status = await client.status.findUnique({
|
||||
where: { id },
|
||||
include: statusAndUserRelations,
|
||||
});
|
||||
const status = await client.status.findUnique({
|
||||
where: { id },
|
||||
include: statusAndUserRelations,
|
||||
});
|
||||
|
||||
// Check if user is authorized to view this status (if it's private)
|
||||
if (!status || !isViewableByUser(status, user))
|
||||
return errorResponse("Record not found", 404);
|
||||
// Check if user is authorized to view this status (if it's private)
|
||||
if (!status || !isViewableByUser(status, user))
|
||||
return errorResponse("Record not found", 404);
|
||||
|
||||
return errorResponse("Not implemented yet");
|
||||
return errorResponse("Not implemented yet");
|
||||
});
|
||||
|
|
|
|||
|
|
@ -8,41 +8,41 @@ import { statusAndUserRelations } from "~database/entities/relations";
|
|||
import type { APIStatus } from "~types/entities/status";
|
||||
|
||||
export const meta = applyConfig({
|
||||
allowedMethods: ["POST"],
|
||||
ratelimits: {
|
||||
max: 100,
|
||||
duration: 60,
|
||||
},
|
||||
route: "/api/v1/statuses/:id/unfavourite",
|
||||
auth: {
|
||||
required: true,
|
||||
},
|
||||
allowedMethods: ["POST"],
|
||||
ratelimits: {
|
||||
max: 100,
|
||||
duration: 60,
|
||||
},
|
||||
route: "/api/v1/statuses/:id/unfavourite",
|
||||
auth: {
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Unfavourite a post
|
||||
*/
|
||||
export default apiRoute(async (req, matchedRoute, extraData) => {
|
||||
const id = matchedRoute.params.id;
|
||||
const id = matchedRoute.params.id;
|
||||
|
||||
const { user } = extraData.auth;
|
||||
const { user } = extraData.auth;
|
||||
|
||||
if (!user) return errorResponse("Unauthorized", 401);
|
||||
if (!user) return errorResponse("Unauthorized", 401);
|
||||
|
||||
const status = await client.status.findUnique({
|
||||
where: { id },
|
||||
include: statusAndUserRelations,
|
||||
});
|
||||
const status = await client.status.findUnique({
|
||||
where: { id },
|
||||
include: statusAndUserRelations,
|
||||
});
|
||||
|
||||
// Check if user is authorized to view this status (if it's private)
|
||||
if (!status || !isViewableByUser(status, user))
|
||||
return errorResponse("Record not found", 404);
|
||||
// Check if user is authorized to view this status (if it's private)
|
||||
if (!status || !isViewableByUser(status, user))
|
||||
return errorResponse("Record not found", 404);
|
||||
|
||||
await deleteLike(user, status);
|
||||
await deleteLike(user, status);
|
||||
|
||||
return jsonResponse({
|
||||
...(await statusToAPI(status, user)),
|
||||
favourited: false,
|
||||
favourites_count: status._count.likes - 1,
|
||||
} as APIStatus);
|
||||
return jsonResponse({
|
||||
...(await statusToAPI(status, user)),
|
||||
favourited: false,
|
||||
favourites_count: status._count.likes - 1,
|
||||
} as APIStatus);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -5,55 +5,55 @@ import { statusToAPI } from "~database/entities/Status";
|
|||
import { statusAndUserRelations } from "~database/entities/relations";
|
||||
|
||||
export const meta = applyConfig({
|
||||
allowedMethods: ["POST"],
|
||||
ratelimits: {
|
||||
max: 100,
|
||||
duration: 60,
|
||||
},
|
||||
route: "/api/v1/statuses/:id/unpin",
|
||||
auth: {
|
||||
required: true,
|
||||
},
|
||||
allowedMethods: ["POST"],
|
||||
ratelimits: {
|
||||
max: 100,
|
||||
duration: 60,
|
||||
},
|
||||
route: "/api/v1/statuses/:id/unpin",
|
||||
auth: {
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Unpins a post
|
||||
*/
|
||||
export default apiRoute(async (req, matchedRoute, extraData) => {
|
||||
const id = matchedRoute.params.id;
|
||||
const id = matchedRoute.params.id;
|
||||
|
||||
const { user } = extraData.auth;
|
||||
const { user } = extraData.auth;
|
||||
|
||||
if (!user) return errorResponse("Unauthorized", 401);
|
||||
if (!user) return errorResponse("Unauthorized", 401);
|
||||
|
||||
let status = await client.status.findUnique({
|
||||
where: { id },
|
||||
include: statusAndUserRelations,
|
||||
});
|
||||
let status = await client.status.findUnique({
|
||||
where: { id },
|
||||
include: statusAndUserRelations,
|
||||
});
|
||||
|
||||
// Check if status exists
|
||||
if (!status) return errorResponse("Record not found", 404);
|
||||
// Check if status exists
|
||||
if (!status) return errorResponse("Record not found", 404);
|
||||
|
||||
// Check if status is user's
|
||||
if (status.authorId !== user.id) return errorResponse("Unauthorized", 401);
|
||||
// Check if status is user's
|
||||
if (status.authorId !== user.id) return errorResponse("Unauthorized", 401);
|
||||
|
||||
await client.user.update({
|
||||
where: { id: user.id },
|
||||
data: {
|
||||
pinnedNotes: {
|
||||
disconnect: {
|
||||
id: status.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
await client.user.update({
|
||||
where: { id: user.id },
|
||||
data: {
|
||||
pinnedNotes: {
|
||||
disconnect: {
|
||||
id: status.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
status = await client.status.findUnique({
|
||||
where: { id },
|
||||
include: statusAndUserRelations,
|
||||
});
|
||||
status = await client.status.findUnique({
|
||||
where: { id },
|
||||
include: statusAndUserRelations,
|
||||
});
|
||||
|
||||
if (!status) return errorResponse("Record not found", 404);
|
||||
if (!status) return errorResponse("Record not found", 404);
|
||||
|
||||
return jsonResponse(statusToAPI(status, user));
|
||||
return jsonResponse(statusToAPI(status, user));
|
||||
});
|
||||
|
|
|
|||
|
|
@ -6,54 +6,54 @@ import { statusAndUserRelations } from "~database/entities/relations";
|
|||
import type { APIStatus } from "~types/entities/status";
|
||||
|
||||
export const meta = applyConfig({
|
||||
allowedMethods: ["POST"],
|
||||
ratelimits: {
|
||||
max: 100,
|
||||
duration: 60,
|
||||
},
|
||||
route: "/api/v1/statuses/:id/unreblog",
|
||||
auth: {
|
||||
required: true,
|
||||
},
|
||||
allowedMethods: ["POST"],
|
||||
ratelimits: {
|
||||
max: 100,
|
||||
duration: 60,
|
||||
},
|
||||
route: "/api/v1/statuses/:id/unreblog",
|
||||
auth: {
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Unreblogs a post
|
||||
*/
|
||||
export default apiRoute(async (req, matchedRoute, extraData) => {
|
||||
const id = matchedRoute.params.id;
|
||||
const id = matchedRoute.params.id;
|
||||
|
||||
const { user } = extraData.auth;
|
||||
const { user } = extraData.auth;
|
||||
|
||||
if (!user) return errorResponse("Unauthorized", 401);
|
||||
if (!user) return errorResponse("Unauthorized", 401);
|
||||
|
||||
const status = await client.status.findUnique({
|
||||
where: { id },
|
||||
include: statusAndUserRelations,
|
||||
});
|
||||
const status = await client.status.findUnique({
|
||||
where: { id },
|
||||
include: statusAndUserRelations,
|
||||
});
|
||||
|
||||
// Check if user is authorized to view this status (if it's private)
|
||||
if (!status || !isViewableByUser(status, user))
|
||||
return errorResponse("Record not found", 404);
|
||||
// Check if user is authorized to view this status (if it's private)
|
||||
if (!status || !isViewableByUser(status, user))
|
||||
return errorResponse("Record not found", 404);
|
||||
|
||||
const existingReblog = await client.status.findFirst({
|
||||
where: {
|
||||
authorId: user.id,
|
||||
reblogId: status.id,
|
||||
},
|
||||
});
|
||||
const existingReblog = await client.status.findFirst({
|
||||
where: {
|
||||
authorId: user.id,
|
||||
reblogId: status.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (!existingReblog) {
|
||||
return errorResponse("Not already reblogged", 422);
|
||||
}
|
||||
if (!existingReblog) {
|
||||
return errorResponse("Not already reblogged", 422);
|
||||
}
|
||||
|
||||
await client.status.delete({
|
||||
where: { id: existingReblog.id },
|
||||
});
|
||||
await client.status.delete({
|
||||
where: { id: existingReblog.id },
|
||||
});
|
||||
|
||||
return jsonResponse({
|
||||
...(await statusToAPI(status, user)),
|
||||
reblogged: false,
|
||||
reblogs_count: status._count.reblogs - 1,
|
||||
} as APIStatus);
|
||||
return jsonResponse({
|
||||
...(await statusToAPI(status, user)),
|
||||
reblogged: false,
|
||||
reblogs_count: status._count.reblogs - 1,
|
||||
} as APIStatus);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -10,234 +10,233 @@ import type { UserWithRelations } from "~database/entities/User";
|
|||
import { statusAndUserRelations } from "~database/entities/relations";
|
||||
|
||||
export const meta = applyConfig({
|
||||
allowedMethods: ["POST"],
|
||||
ratelimits: {
|
||||
max: 300,
|
||||
duration: 60,
|
||||
},
|
||||
route: "/api/v1/statuses",
|
||||
auth: {
|
||||
required: true,
|
||||
},
|
||||
allowedMethods: ["POST"],
|
||||
ratelimits: {
|
||||
max: 300,
|
||||
duration: 60,
|
||||
},
|
||||
route: "/api/v1/statuses",
|
||||
auth: {
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Post new status
|
||||
*/
|
||||
export default apiRoute<{
|
||||
status: string;
|
||||
media_ids?: string[];
|
||||
"poll[options]"?: string[];
|
||||
"poll[expires_in]"?: number;
|
||||
"poll[multiple]"?: boolean;
|
||||
"poll[hide_totals]"?: boolean;
|
||||
in_reply_to_id?: string;
|
||||
quote_id?: string;
|
||||
sensitive?: boolean;
|
||||
spoiler_text?: string;
|
||||
visibility?: "public" | "unlisted" | "private" | "direct";
|
||||
language?: string;
|
||||
scheduled_at?: string;
|
||||
local_only?: boolean;
|
||||
content_type?: string;
|
||||
status: string;
|
||||
media_ids?: string[];
|
||||
"poll[options]"?: string[];
|
||||
"poll[expires_in]"?: number;
|
||||
"poll[multiple]"?: boolean;
|
||||
"poll[hide_totals]"?: boolean;
|
||||
in_reply_to_id?: string;
|
||||
quote_id?: string;
|
||||
sensitive?: boolean;
|
||||
spoiler_text?: string;
|
||||
visibility?: "public" | "unlisted" | "private" | "direct";
|
||||
language?: string;
|
||||
scheduled_at?: string;
|
||||
local_only?: boolean;
|
||||
content_type?: string;
|
||||
}>(async (req, matchedRoute, extraData) => {
|
||||
const { user, token } = extraData.auth;
|
||||
const application = await getFromToken(token);
|
||||
const { user, token } = extraData.auth;
|
||||
const application = await getFromToken(token);
|
||||
|
||||
if (!user) return errorResponse("Unauthorized", 401);
|
||||
if (!user) return errorResponse("Unauthorized", 401);
|
||||
|
||||
const config = await extraData.configManager.getConfig();
|
||||
const config = await extraData.configManager.getConfig();
|
||||
|
||||
const {
|
||||
status,
|
||||
media_ids,
|
||||
"poll[expires_in]": expires_in,
|
||||
// "poll[hide_totals]": hide_totals,
|
||||
// "poll[multiple]": multiple,
|
||||
"poll[options]": options,
|
||||
in_reply_to_id,
|
||||
quote_id,
|
||||
// language,
|
||||
scheduled_at,
|
||||
sensitive,
|
||||
spoiler_text,
|
||||
visibility,
|
||||
content_type,
|
||||
} = extraData.parsedRequest;
|
||||
const {
|
||||
status,
|
||||
media_ids,
|
||||
"poll[expires_in]": expires_in,
|
||||
// "poll[hide_totals]": hide_totals,
|
||||
// "poll[multiple]": multiple,
|
||||
"poll[options]": options,
|
||||
in_reply_to_id,
|
||||
quote_id,
|
||||
// language,
|
||||
scheduled_at,
|
||||
sensitive,
|
||||
spoiler_text,
|
||||
visibility,
|
||||
content_type,
|
||||
} = extraData.parsedRequest;
|
||||
|
||||
// Validate status
|
||||
if (!status && !(media_ids && media_ids.length > 0)) {
|
||||
return errorResponse(
|
||||
"Status is required unless media is attached",
|
||||
422
|
||||
);
|
||||
}
|
||||
// Validate status
|
||||
if (!status && !(media_ids && media_ids.length > 0)) {
|
||||
return errorResponse(
|
||||
"Status is required unless media is attached",
|
||||
422,
|
||||
);
|
||||
}
|
||||
|
||||
// Validate media_ids
|
||||
if (media_ids && !Array.isArray(media_ids)) {
|
||||
return errorResponse("Media IDs must be an array", 422);
|
||||
}
|
||||
// Validate media_ids
|
||||
if (media_ids && !Array.isArray(media_ids)) {
|
||||
return errorResponse("Media IDs must be an array", 422);
|
||||
}
|
||||
|
||||
// Validate poll options
|
||||
if (options && !Array.isArray(options)) {
|
||||
return errorResponse("Poll options must be an array", 422);
|
||||
}
|
||||
// Validate poll options
|
||||
if (options && !Array.isArray(options)) {
|
||||
return errorResponse("Poll options must be an array", 422);
|
||||
}
|
||||
|
||||
if (options && options.length > 4) {
|
||||
return errorResponse("Poll options must be less than 5", 422);
|
||||
}
|
||||
if (options && options.length > 4) {
|
||||
return errorResponse("Poll options must be less than 5", 422);
|
||||
}
|
||||
|
||||
if (media_ids && media_ids.length > 0) {
|
||||
// Disallow poll
|
||||
if (options) {
|
||||
return errorResponse("Cannot attach poll to media", 422);
|
||||
}
|
||||
if (media_ids.length > 4) {
|
||||
return errorResponse("Media IDs must be less than 5", 422);
|
||||
}
|
||||
}
|
||||
if (media_ids && media_ids.length > 0) {
|
||||
// Disallow poll
|
||||
if (options) {
|
||||
return errorResponse("Cannot attach poll to media", 422);
|
||||
}
|
||||
if (media_ids.length > 4) {
|
||||
return errorResponse("Media IDs must be less than 5", 422);
|
||||
}
|
||||
}
|
||||
|
||||
if (options && options.length > config.validation.max_poll_options) {
|
||||
return errorResponse(
|
||||
`Poll options must be less than ${config.validation.max_poll_options}`,
|
||||
422
|
||||
);
|
||||
}
|
||||
if (options && options.length > config.validation.max_poll_options) {
|
||||
return errorResponse(
|
||||
`Poll options must be less than ${config.validation.max_poll_options}`,
|
||||
422,
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
options &&
|
||||
options.some(
|
||||
option => option.length > config.validation.max_poll_option_size
|
||||
)
|
||||
) {
|
||||
return errorResponse(
|
||||
`Poll options must be less than ${config.validation.max_poll_option_size} characters`,
|
||||
422
|
||||
);
|
||||
}
|
||||
if (
|
||||
options?.some(
|
||||
(option) => option.length > config.validation.max_poll_option_size,
|
||||
)
|
||||
) {
|
||||
return errorResponse(
|
||||
`Poll options must be less than ${config.validation.max_poll_option_size} characters`,
|
||||
422,
|
||||
);
|
||||
}
|
||||
|
||||
if (expires_in && expires_in < config.validation.min_poll_duration) {
|
||||
return errorResponse(
|
||||
`Poll duration must be greater than ${config.validation.min_poll_duration} seconds`,
|
||||
422
|
||||
);
|
||||
}
|
||||
if (expires_in && expires_in < config.validation.min_poll_duration) {
|
||||
return errorResponse(
|
||||
`Poll duration must be greater than ${config.validation.min_poll_duration} seconds`,
|
||||
422,
|
||||
);
|
||||
}
|
||||
|
||||
if (expires_in && expires_in > config.validation.max_poll_duration) {
|
||||
return errorResponse(
|
||||
`Poll duration must be less than ${config.validation.max_poll_duration} seconds`,
|
||||
422
|
||||
);
|
||||
}
|
||||
if (expires_in && expires_in > config.validation.max_poll_duration) {
|
||||
return errorResponse(
|
||||
`Poll duration must be less than ${config.validation.max_poll_duration} seconds`,
|
||||
422,
|
||||
);
|
||||
}
|
||||
|
||||
if (scheduled_at) {
|
||||
if (new Date(scheduled_at).getTime() < Date.now()) {
|
||||
return errorResponse("Scheduled time must be in the future", 422);
|
||||
}
|
||||
}
|
||||
if (scheduled_at) {
|
||||
if (new Date(scheduled_at).getTime() < Date.now()) {
|
||||
return errorResponse("Scheduled time must be in the future", 422);
|
||||
}
|
||||
}
|
||||
|
||||
let sanitizedStatus: string;
|
||||
let sanitizedStatus: string;
|
||||
|
||||
if (content_type === "text/markdown") {
|
||||
sanitizedStatus = await sanitizeHtml(parse(status ?? "") as any);
|
||||
} else if (content_type === "text/x.misskeymarkdown") {
|
||||
// Parse as MFM
|
||||
// TODO: Parse as MFM
|
||||
sanitizedStatus = await sanitizeHtml(parse(status ?? "") as any);
|
||||
} else {
|
||||
sanitizedStatus = await sanitizeHtml(status ?? "");
|
||||
}
|
||||
if (content_type === "text/markdown") {
|
||||
sanitizedStatus = await sanitizeHtml(parse(status ?? "") as string);
|
||||
} else if (content_type === "text/x.misskeymarkdown") {
|
||||
// Parse as MFM
|
||||
// TODO: Parse as MFM
|
||||
sanitizedStatus = await sanitizeHtml(parse(status ?? "") as string);
|
||||
} else {
|
||||
sanitizedStatus = await sanitizeHtml(status ?? "");
|
||||
}
|
||||
|
||||
if (sanitizedStatus.length > config.validation.max_note_size) {
|
||||
return errorResponse(
|
||||
`Status must be less than ${config.validation.max_note_size} characters`,
|
||||
400
|
||||
);
|
||||
}
|
||||
if (sanitizedStatus.length > config.validation.max_note_size) {
|
||||
return errorResponse(
|
||||
`Status must be less than ${config.validation.max_note_size} characters`,
|
||||
400,
|
||||
);
|
||||
}
|
||||
|
||||
// Validate visibility
|
||||
if (
|
||||
visibility &&
|
||||
!["public", "unlisted", "private", "direct"].includes(visibility)
|
||||
) {
|
||||
return errorResponse("Invalid visibility", 422);
|
||||
}
|
||||
// Validate visibility
|
||||
if (
|
||||
visibility &&
|
||||
!["public", "unlisted", "private", "direct"].includes(visibility)
|
||||
) {
|
||||
return errorResponse("Invalid visibility", 422);
|
||||
}
|
||||
|
||||
// Get reply account and status if exists
|
||||
let replyStatus: StatusWithRelations | null = null;
|
||||
let replyUser: UserWithRelations | null = null;
|
||||
let quote: StatusWithRelations | null = null;
|
||||
// Get reply account and status if exists
|
||||
let replyStatus: StatusWithRelations | null = null;
|
||||
let replyUser: UserWithRelations | null = null;
|
||||
let quote: StatusWithRelations | null = null;
|
||||
|
||||
if (in_reply_to_id) {
|
||||
replyStatus = await client.status.findUnique({
|
||||
where: { id: in_reply_to_id },
|
||||
include: statusAndUserRelations,
|
||||
});
|
||||
if (in_reply_to_id) {
|
||||
replyStatus = await client.status.findUnique({
|
||||
where: { id: in_reply_to_id },
|
||||
include: statusAndUserRelations,
|
||||
});
|
||||
|
||||
if (!replyStatus) {
|
||||
return errorResponse("Reply status not found", 404);
|
||||
}
|
||||
if (!replyStatus) {
|
||||
return errorResponse("Reply status not found", 404);
|
||||
}
|
||||
|
||||
// @ts-expect-error Prisma Typescript doesn't include relations
|
||||
replyUser = replyStatus.author;
|
||||
}
|
||||
// @ts-expect-error Prisma Typescript doesn't include relations
|
||||
replyUser = replyStatus.author;
|
||||
}
|
||||
|
||||
if (quote_id) {
|
||||
quote = await client.status.findUnique({
|
||||
where: { id: quote_id },
|
||||
include: statusAndUserRelations,
|
||||
});
|
||||
if (quote_id) {
|
||||
quote = await client.status.findUnique({
|
||||
where: { id: quote_id },
|
||||
include: statusAndUserRelations,
|
||||
});
|
||||
|
||||
if (!quote) {
|
||||
return errorResponse("Quote status not found", 404);
|
||||
}
|
||||
}
|
||||
if (!quote) {
|
||||
return errorResponse("Quote status not found", 404);
|
||||
}
|
||||
}
|
||||
|
||||
// Check if status body doesnt match filters
|
||||
if (config.filters.note_content.some(filter => status?.match(filter))) {
|
||||
return errorResponse("Status contains blocked words", 422);
|
||||
}
|
||||
// Check if status body doesnt match filters
|
||||
if (config.filters.note_content.some((filter) => status?.match(filter))) {
|
||||
return errorResponse("Status contains blocked words", 422);
|
||||
}
|
||||
|
||||
// Check if media attachments are all valid
|
||||
// Check if media attachments are all valid
|
||||
|
||||
const foundAttachments = await client.attachment.findMany({
|
||||
where: {
|
||||
id: {
|
||||
in: media_ids ?? [],
|
||||
},
|
||||
},
|
||||
});
|
||||
const foundAttachments = await client.attachment.findMany({
|
||||
where: {
|
||||
id: {
|
||||
in: media_ids ?? [],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (foundAttachments.length !== (media_ids ?? []).length) {
|
||||
return errorResponse("Invalid media IDs", 422);
|
||||
}
|
||||
if (foundAttachments.length !== (media_ids ?? []).length) {
|
||||
return errorResponse("Invalid media IDs", 422);
|
||||
}
|
||||
|
||||
const newStatus = await createNewStatus({
|
||||
account: user,
|
||||
application,
|
||||
content: sanitizedStatus,
|
||||
visibility:
|
||||
visibility ||
|
||||
(config.defaults.visibility as
|
||||
| "public"
|
||||
| "unlisted"
|
||||
| "private"
|
||||
| "direct"),
|
||||
sensitive: sensitive || false,
|
||||
spoiler_text: spoiler_text || "",
|
||||
emojis: [],
|
||||
media_attachments: media_ids,
|
||||
reply:
|
||||
replyStatus && replyUser
|
||||
? {
|
||||
user: replyUser,
|
||||
status: replyStatus,
|
||||
}
|
||||
: undefined,
|
||||
quote: quote || undefined,
|
||||
});
|
||||
const newStatus = await createNewStatus({
|
||||
account: user,
|
||||
application,
|
||||
content: sanitizedStatus,
|
||||
visibility:
|
||||
visibility ||
|
||||
(config.defaults.visibility as
|
||||
| "public"
|
||||
| "unlisted"
|
||||
| "private"
|
||||
| "direct"),
|
||||
sensitive: sensitive || false,
|
||||
spoiler_text: spoiler_text || "",
|
||||
emojis: [],
|
||||
media_attachments: media_ids,
|
||||
reply:
|
||||
replyStatus && replyUser
|
||||
? {
|
||||
user: replyUser,
|
||||
status: replyStatus,
|
||||
}
|
||||
: undefined,
|
||||
quote: quote || undefined,
|
||||
});
|
||||
|
||||
// TODO: add database jobs to deliver the post
|
||||
// TODO: add database jobs to deliver the post
|
||||
|
||||
return jsonResponse(await statusToAPI(newStatus, user));
|
||||
return jsonResponse(await statusToAPI(newStatus, user));
|
||||
});
|
||||
|
|
|
|||
|
|
@ -5,95 +5,95 @@ import { statusToAPI } from "~database/entities/Status";
|
|||
import { statusAndUserRelations } from "~database/entities/relations";
|
||||
|
||||
export const meta = applyConfig({
|
||||
allowedMethods: ["GET"],
|
||||
ratelimits: {
|
||||
max: 200,
|
||||
duration: 60,
|
||||
},
|
||||
route: "/api/v1/timelines/home",
|
||||
auth: {
|
||||
required: true,
|
||||
},
|
||||
allowedMethods: ["GET"],
|
||||
ratelimits: {
|
||||
max: 200,
|
||||
duration: 60,
|
||||
},
|
||||
route: "/api/v1/timelines/home",
|
||||
auth: {
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Fetch home timeline statuses
|
||||
*/
|
||||
export default apiRoute<{
|
||||
max_id?: string;
|
||||
since_id?: string;
|
||||
min_id?: string;
|
||||
limit?: number;
|
||||
max_id?: string;
|
||||
since_id?: string;
|
||||
min_id?: string;
|
||||
limit?: number;
|
||||
}>(async (req, matchedRoute, extraData) => {
|
||||
const { user } = extraData.auth;
|
||||
const { user } = extraData.auth;
|
||||
|
||||
const { limit = 20, max_id, min_id, since_id } = extraData.parsedRequest;
|
||||
const { limit = 20, max_id, min_id, since_id } = extraData.parsedRequest;
|
||||
|
||||
if (limit < 1 || limit > 40) {
|
||||
return errorResponse("Limit must be between 1 and 40", 400);
|
||||
}
|
||||
if (limit < 1 || limit > 40) {
|
||||
return errorResponse("Limit must be between 1 and 40", 400);
|
||||
}
|
||||
|
||||
if (!user) return errorResponse("Unauthorized", 401);
|
||||
if (!user) return errorResponse("Unauthorized", 401);
|
||||
|
||||
const objects = await client.status.findMany({
|
||||
where: {
|
||||
id: {
|
||||
lt: max_id ?? undefined,
|
||||
gte: since_id ?? undefined,
|
||||
gt: min_id ?? undefined,
|
||||
},
|
||||
OR: [
|
||||
{
|
||||
author: {
|
||||
OR: [
|
||||
{
|
||||
relationshipSubjects: {
|
||||
some: {
|
||||
ownerId: user.id,
|
||||
following: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: user.id,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
// Include posts where the user is mentioned in addition to posts by followed users
|
||||
mentions: {
|
||||
some: {
|
||||
id: user.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
include: statusAndUserRelations,
|
||||
take: limit,
|
||||
orderBy: {
|
||||
id: "desc",
|
||||
},
|
||||
});
|
||||
const objects = await client.status.findMany({
|
||||
where: {
|
||||
id: {
|
||||
lt: max_id ?? undefined,
|
||||
gte: since_id ?? undefined,
|
||||
gt: min_id ?? undefined,
|
||||
},
|
||||
OR: [
|
||||
{
|
||||
author: {
|
||||
OR: [
|
||||
{
|
||||
relationshipSubjects: {
|
||||
some: {
|
||||
ownerId: user.id,
|
||||
following: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: user.id,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
// Include posts where the user is mentioned in addition to posts by followed users
|
||||
mentions: {
|
||||
some: {
|
||||
id: user.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
include: statusAndUserRelations,
|
||||
take: limit,
|
||||
orderBy: {
|
||||
id: "desc",
|
||||
},
|
||||
});
|
||||
|
||||
// Constuct HTTP Link header (next and prev)
|
||||
const linkHeader = [];
|
||||
if (objects.length > 0) {
|
||||
const urlWithoutQuery = req.url.split("?")[0];
|
||||
linkHeader.push(
|
||||
`<${urlWithoutQuery}?max_id=${objects.at(-1)?.id}>; rel="next"`,
|
||||
`<${urlWithoutQuery}?min_id=${objects[0].id}>; rel="prev"`
|
||||
);
|
||||
}
|
||||
// Constuct HTTP Link header (next and prev)
|
||||
const linkHeader = [];
|
||||
if (objects.length > 0) {
|
||||
const urlWithoutQuery = req.url.split("?")[0];
|
||||
linkHeader.push(
|
||||
`<${urlWithoutQuery}?max_id=${objects.at(-1)?.id}>; rel="next"`,
|
||||
`<${urlWithoutQuery}?min_id=${objects[0].id}>; rel="prev"`,
|
||||
);
|
||||
}
|
||||
|
||||
return jsonResponse(
|
||||
await Promise.all(
|
||||
objects.map(async status => statusToAPI(status, user))
|
||||
),
|
||||
200,
|
||||
{
|
||||
Link: linkHeader.join(", "),
|
||||
}
|
||||
);
|
||||
return jsonResponse(
|
||||
await Promise.all(
|
||||
objects.map(async (status) => statusToAPI(status, user)),
|
||||
),
|
||||
200,
|
||||
{
|
||||
Link: linkHeader.join(", "),
|
||||
},
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -5,84 +5,86 @@ import { statusToAPI } from "~database/entities/Status";
|
|||
import { statusAndUserRelations } from "~database/entities/relations";
|
||||
|
||||
export const meta = applyConfig({
|
||||
allowedMethods: ["GET"],
|
||||
ratelimits: {
|
||||
max: 200,
|
||||
duration: 60,
|
||||
},
|
||||
route: "/api/v1/timelines/public",
|
||||
auth: {
|
||||
required: false,
|
||||
},
|
||||
allowedMethods: ["GET"],
|
||||
ratelimits: {
|
||||
max: 200,
|
||||
duration: 60,
|
||||
},
|
||||
route: "/api/v1/timelines/public",
|
||||
auth: {
|
||||
required: false,
|
||||
},
|
||||
});
|
||||
|
||||
export default apiRoute<{
|
||||
local?: boolean;
|
||||
only_media?: boolean;
|
||||
remote?: boolean;
|
||||
max_id?: string;
|
||||
since_id?: string;
|
||||
min_id?: string;
|
||||
limit?: number;
|
||||
local?: boolean;
|
||||
only_media?: boolean;
|
||||
remote?: boolean;
|
||||
max_id?: string;
|
||||
since_id?: string;
|
||||
min_id?: string;
|
||||
limit?: number;
|
||||
}>(async (req, matchedRoute, extraData) => {
|
||||
const { user } = extraData.auth;
|
||||
const {
|
||||
local,
|
||||
limit = 20,
|
||||
max_id,
|
||||
min_id,
|
||||
// only_media,
|
||||
remote,
|
||||
since_id,
|
||||
} = extraData.parsedRequest;
|
||||
const { user } = extraData.auth;
|
||||
const {
|
||||
local,
|
||||
limit = 20,
|
||||
max_id,
|
||||
min_id,
|
||||
// only_media,
|
||||
remote,
|
||||
since_id,
|
||||
} = extraData.parsedRequest;
|
||||
|
||||
if (limit < 1 || limit > 40) {
|
||||
return errorResponse("Limit must be between 1 and 40", 400);
|
||||
}
|
||||
if (limit < 1 || limit > 40) {
|
||||
return errorResponse("Limit must be between 1 and 40", 400);
|
||||
}
|
||||
|
||||
if (local && remote) {
|
||||
return errorResponse("Cannot use both local and remote", 400);
|
||||
}
|
||||
if (local && remote) {
|
||||
return errorResponse("Cannot use both local and remote", 400);
|
||||
}
|
||||
|
||||
const objects = await client.status.findMany({
|
||||
where: {
|
||||
id: {
|
||||
lt: max_id ?? undefined,
|
||||
gte: since_id ?? undefined,
|
||||
gt: min_id ?? undefined,
|
||||
},
|
||||
instanceId: remote
|
||||
? {
|
||||
not: null,
|
||||
}
|
||||
: local
|
||||
? null
|
||||
: undefined,
|
||||
},
|
||||
include: statusAndUserRelations,
|
||||
take: limit,
|
||||
orderBy: {
|
||||
id: "desc",
|
||||
},
|
||||
});
|
||||
const objects = await client.status.findMany({
|
||||
where: {
|
||||
id: {
|
||||
lt: max_id ?? undefined,
|
||||
gte: since_id ?? undefined,
|
||||
gt: min_id ?? undefined,
|
||||
},
|
||||
instanceId: remote
|
||||
? {
|
||||
not: null,
|
||||
}
|
||||
: local
|
||||
? null
|
||||
: undefined,
|
||||
},
|
||||
include: statusAndUserRelations,
|
||||
take: limit,
|
||||
orderBy: {
|
||||
id: "desc",
|
||||
},
|
||||
});
|
||||
|
||||
// Constuct HTTP Link header (next and prev)
|
||||
const linkHeader = [];
|
||||
if (objects.length > 0) {
|
||||
const urlWithoutQuery = req.url.split("?")[0];
|
||||
linkHeader.push(
|
||||
`<${urlWithoutQuery}?max_id=${objects.at(-1)?.id}>; rel="next"`,
|
||||
`<${urlWithoutQuery}?min_id=${objects[0].id}>; rel="prev"`
|
||||
);
|
||||
}
|
||||
// Constuct HTTP Link header (next and prev)
|
||||
const linkHeader = [];
|
||||
if (objects.length > 0) {
|
||||
const urlWithoutQuery = req.url.split("?")[0];
|
||||
linkHeader.push(
|
||||
`<${urlWithoutQuery}?max_id=${objects.at(-1)?.id}>; rel="next"`,
|
||||
`<${urlWithoutQuery}?min_id=${objects[0].id}>; rel="prev"`,
|
||||
);
|
||||
}
|
||||
|
||||
return jsonResponse(
|
||||
await Promise.all(
|
||||
objects.map(async status => statusToAPI(status, user || undefined))
|
||||
),
|
||||
200,
|
||||
{
|
||||
Link: linkHeader.join(", "),
|
||||
}
|
||||
);
|
||||
return jsonResponse(
|
||||
await Promise.all(
|
||||
objects.map(async (status) =>
|
||||
statusToAPI(status, user || undefined),
|
||||
),
|
||||
),
|
||||
200,
|
||||
{
|
||||
Link: linkHeader.join(", "),
|
||||
},
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,148 +1,148 @@
|
|||
import { apiRoute, applyConfig } from "@api";
|
||||
import { errorResponse, jsonResponse } from "@response";
|
||||
import { client } from "~database/datasource";
|
||||
import { encode } from "blurhash";
|
||||
import sharp from "sharp";
|
||||
import { attachmentToAPI, getUrl } from "~database/entities/Attachment";
|
||||
import type { MediaBackend } from "media-manager";
|
||||
import { MediaBackendType } from "media-manager";
|
||||
import sharp from "sharp";
|
||||
import { client } from "~database/datasource";
|
||||
import { attachmentToAPI, getUrl } from "~database/entities/Attachment";
|
||||
import { LocalMediaBackend } from "~packages/media-manager/backends/local";
|
||||
import { S3MediaBackend } from "~packages/media-manager/backends/s3";
|
||||
|
||||
export const meta = applyConfig({
|
||||
allowedMethods: ["POST"],
|
||||
ratelimits: {
|
||||
max: 10,
|
||||
duration: 60,
|
||||
},
|
||||
route: "/api/v2/media",
|
||||
auth: {
|
||||
required: true,
|
||||
oauthPermissions: ["write:media"],
|
||||
},
|
||||
allowedMethods: ["POST"],
|
||||
ratelimits: {
|
||||
max: 10,
|
||||
duration: 60,
|
||||
},
|
||||
route: "/api/v2/media",
|
||||
auth: {
|
||||
required: true,
|
||||
oauthPermissions: ["write:media"],
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Upload new media
|
||||
*/
|
||||
export default apiRoute<{
|
||||
file: File;
|
||||
thumbnail: File;
|
||||
description: string;
|
||||
// TODO: Implement focus storage
|
||||
focus: string;
|
||||
file: File;
|
||||
thumbnail: File;
|
||||
description: string;
|
||||
// TODO: Implement focus storage
|
||||
focus: string;
|
||||
}>(async (req, matchedRoute, extraData) => {
|
||||
const { user } = extraData.auth;
|
||||
const { user } = extraData.auth;
|
||||
|
||||
if (!user) {
|
||||
return errorResponse("Unauthorized", 401);
|
||||
}
|
||||
if (!user) {
|
||||
return errorResponse("Unauthorized", 401);
|
||||
}
|
||||
|
||||
const { file, thumbnail, description } = extraData.parsedRequest;
|
||||
const { file, thumbnail, description } = extraData.parsedRequest;
|
||||
|
||||
if (!file) {
|
||||
return errorResponse("No file provided", 400);
|
||||
}
|
||||
if (!file) {
|
||||
return errorResponse("No file provided", 400);
|
||||
}
|
||||
|
||||
const config = await extraData.configManager.getConfig();
|
||||
const config = await extraData.configManager.getConfig();
|
||||
|
||||
if (file.size > config.validation.max_media_size) {
|
||||
return errorResponse(
|
||||
`File too large, max size is ${config.validation.max_media_size} bytes`,
|
||||
413
|
||||
);
|
||||
}
|
||||
if (file.size > config.validation.max_media_size) {
|
||||
return errorResponse(
|
||||
`File too large, max size is ${config.validation.max_media_size} bytes`,
|
||||
413,
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
config.validation.enforce_mime_types &&
|
||||
!config.validation.allowed_mime_types.includes(file.type)
|
||||
) {
|
||||
return errorResponse("Invalid file type", 415);
|
||||
}
|
||||
if (
|
||||
config.validation.enforce_mime_types &&
|
||||
!config.validation.allowed_mime_types.includes(file.type)
|
||||
) {
|
||||
return errorResponse("Invalid file type", 415);
|
||||
}
|
||||
|
||||
if (
|
||||
description &&
|
||||
description.length > config.validation.max_media_description_size
|
||||
) {
|
||||
return errorResponse(
|
||||
`Description too long, max length is ${config.validation.max_media_description_size} characters`,
|
||||
413
|
||||
);
|
||||
}
|
||||
if (
|
||||
description &&
|
||||
description.length > config.validation.max_media_description_size
|
||||
) {
|
||||
return errorResponse(
|
||||
`Description too long, max length is ${config.validation.max_media_description_size} characters`,
|
||||
413,
|
||||
);
|
||||
}
|
||||
|
||||
const sha256 = new Bun.SHA256();
|
||||
const sha256 = new Bun.SHA256();
|
||||
|
||||
const isImage = file.type.startsWith("image/");
|
||||
const isImage = file.type.startsWith("image/");
|
||||
|
||||
const metadata = isImage
|
||||
? await sharp(await file.arrayBuffer()).metadata()
|
||||
: null;
|
||||
const metadata = isImage
|
||||
? await sharp(await file.arrayBuffer()).metadata()
|
||||
: null;
|
||||
|
||||
const blurhash = isImage
|
||||
? encode(
|
||||
new Uint8ClampedArray(await file.arrayBuffer()),
|
||||
metadata?.width ?? 0,
|
||||
metadata?.height ?? 0,
|
||||
4,
|
||||
4
|
||||
)
|
||||
: null;
|
||||
const blurhash = isImage
|
||||
? encode(
|
||||
new Uint8ClampedArray(await file.arrayBuffer()),
|
||||
metadata?.width ?? 0,
|
||||
metadata?.height ?? 0,
|
||||
4,
|
||||
4,
|
||||
)
|
||||
: null;
|
||||
|
||||
let url = "";
|
||||
let url = "";
|
||||
|
||||
let mediaManager: MediaBackend;
|
||||
let mediaManager: MediaBackend;
|
||||
|
||||
switch (config.media.backend as MediaBackendType) {
|
||||
case MediaBackendType.LOCAL:
|
||||
mediaManager = new LocalMediaBackend(config);
|
||||
break;
|
||||
case MediaBackendType.S3:
|
||||
mediaManager = new S3MediaBackend(config);
|
||||
break;
|
||||
default:
|
||||
// TODO: Replace with logger
|
||||
throw new Error("Invalid media backend");
|
||||
}
|
||||
switch (config.media.backend as MediaBackendType) {
|
||||
case MediaBackendType.LOCAL:
|
||||
mediaManager = new LocalMediaBackend(config);
|
||||
break;
|
||||
case MediaBackendType.S3:
|
||||
mediaManager = new S3MediaBackend(config);
|
||||
break;
|
||||
default:
|
||||
// TODO: Replace with logger
|
||||
throw new Error("Invalid media backend");
|
||||
}
|
||||
|
||||
if (isImage) {
|
||||
const { uploadedFile } = await mediaManager.addFile(file);
|
||||
if (isImage) {
|
||||
const { uploadedFile } = await mediaManager.addFile(file);
|
||||
|
||||
url = getUrl(uploadedFile.name, config);
|
||||
}
|
||||
url = getUrl(uploadedFile.name, config);
|
||||
}
|
||||
|
||||
let thumbnailUrl = "";
|
||||
let thumbnailUrl = "";
|
||||
|
||||
if (thumbnail) {
|
||||
const { uploadedFile } = await mediaManager.addFile(thumbnail);
|
||||
if (thumbnail) {
|
||||
const { uploadedFile } = await mediaManager.addFile(thumbnail);
|
||||
|
||||
thumbnailUrl = getUrl(uploadedFile.name, config);
|
||||
}
|
||||
thumbnailUrl = getUrl(uploadedFile.name, config);
|
||||
}
|
||||
|
||||
const newAttachment = await client.attachment.create({
|
||||
data: {
|
||||
url,
|
||||
thumbnail_url: thumbnailUrl,
|
||||
sha256: sha256.update(await file.arrayBuffer()).digest("hex"),
|
||||
mime_type: file.type,
|
||||
description: description ?? "",
|
||||
size: file.size,
|
||||
blurhash: blurhash ?? undefined,
|
||||
width: metadata?.width ?? undefined,
|
||||
height: metadata?.height ?? undefined,
|
||||
},
|
||||
});
|
||||
const newAttachment = await client.attachment.create({
|
||||
data: {
|
||||
url,
|
||||
thumbnail_url: thumbnailUrl,
|
||||
sha256: sha256.update(await file.arrayBuffer()).digest("hex"),
|
||||
mime_type: file.type,
|
||||
description: description ?? "",
|
||||
size: file.size,
|
||||
blurhash: blurhash ?? undefined,
|
||||
width: metadata?.width ?? undefined,
|
||||
height: metadata?.height ?? undefined,
|
||||
},
|
||||
});
|
||||
|
||||
// TODO: Add job to process videos and other media
|
||||
// TODO: Add job to process videos and other media
|
||||
|
||||
if (isImage) {
|
||||
return jsonResponse(attachmentToAPI(newAttachment));
|
||||
} else {
|
||||
return jsonResponse(
|
||||
{
|
||||
...attachmentToAPI(newAttachment),
|
||||
url: null,
|
||||
},
|
||||
202
|
||||
);
|
||||
}
|
||||
if (isImage) {
|
||||
return jsonResponse(attachmentToAPI(newAttachment));
|
||||
}
|
||||
|
||||
return jsonResponse(
|
||||
{
|
||||
...attachmentToAPI(newAttachment),
|
||||
url: null,
|
||||
},
|
||||
202,
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -5,139 +5,139 @@ import { client } from "~database/datasource";
|
|||
import { statusToAPI } from "~database/entities/Status";
|
||||
import { userToAPI } from "~database/entities/User";
|
||||
import {
|
||||
statusAndUserRelations,
|
||||
userRelations,
|
||||
statusAndUserRelations,
|
||||
userRelations,
|
||||
} from "~database/entities/relations";
|
||||
|
||||
export const meta = applyConfig({
|
||||
allowedMethods: ["GET"],
|
||||
ratelimits: {
|
||||
max: 10,
|
||||
duration: 60,
|
||||
},
|
||||
route: "/api/v2/search",
|
||||
auth: {
|
||||
required: false,
|
||||
oauthPermissions: ["read:search"],
|
||||
},
|
||||
allowedMethods: ["GET"],
|
||||
ratelimits: {
|
||||
max: 10,
|
||||
duration: 60,
|
||||
},
|
||||
route: "/api/v2/search",
|
||||
auth: {
|
||||
required: false,
|
||||
oauthPermissions: ["read:search"],
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Upload new media
|
||||
*/
|
||||
export default apiRoute<{
|
||||
q?: string;
|
||||
type?: string;
|
||||
resolve?: boolean;
|
||||
following?: boolean;
|
||||
account_id?: string;
|
||||
max_id?: string;
|
||||
min_id?: string;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
q?: string;
|
||||
type?: string;
|
||||
resolve?: boolean;
|
||||
following?: boolean;
|
||||
account_id?: string;
|
||||
max_id?: string;
|
||||
min_id?: string;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
}>(async (req, matchedRoute, extraData) => {
|
||||
const { user } = extraData.auth;
|
||||
const { user } = extraData.auth;
|
||||
|
||||
const {
|
||||
q,
|
||||
type,
|
||||
resolve,
|
||||
following,
|
||||
account_id,
|
||||
// max_id,
|
||||
// min_id,
|
||||
limit = 20,
|
||||
offset,
|
||||
} = extraData.parsedRequest;
|
||||
const {
|
||||
q,
|
||||
type,
|
||||
resolve,
|
||||
following,
|
||||
account_id,
|
||||
// max_id,
|
||||
// min_id,
|
||||
limit = 20,
|
||||
offset,
|
||||
} = extraData.parsedRequest;
|
||||
|
||||
const config = await extraData.configManager.getConfig();
|
||||
const config = await extraData.configManager.getConfig();
|
||||
|
||||
if (!config.meilisearch.enabled) {
|
||||
return errorResponse("Meilisearch is not enabled", 501);
|
||||
}
|
||||
if (!config.meilisearch.enabled) {
|
||||
return errorResponse("Meilisearch is not enabled", 501);
|
||||
}
|
||||
|
||||
if (!user && (resolve || offset)) {
|
||||
return errorResponse(
|
||||
"Cannot use resolve or offset without being authenticated",
|
||||
401
|
||||
);
|
||||
}
|
||||
if (!user && (resolve || offset)) {
|
||||
return errorResponse(
|
||||
"Cannot use resolve or offset without being authenticated",
|
||||
401,
|
||||
);
|
||||
}
|
||||
|
||||
if (limit < 1 || limit > 40) {
|
||||
return errorResponse("Limit must be between 1 and 40", 400);
|
||||
}
|
||||
if (limit < 1 || limit > 40) {
|
||||
return errorResponse("Limit must be between 1 and 40", 400);
|
||||
}
|
||||
|
||||
let accountResults: { id: string }[] = [];
|
||||
let statusResults: { id: string }[] = [];
|
||||
let accountResults: { id: string }[] = [];
|
||||
let statusResults: { id: string }[] = [];
|
||||
|
||||
if (!type || type === "accounts") {
|
||||
accountResults = (
|
||||
await meilisearch.index(MeiliIndexType.Accounts).search<{
|
||||
id: string;
|
||||
}>(q, {
|
||||
limit: Number(limit) || 10,
|
||||
offset: Number(offset) || 0,
|
||||
sort: ["createdAt:desc"],
|
||||
})
|
||||
).hits;
|
||||
}
|
||||
if (!type || type === "accounts") {
|
||||
accountResults = (
|
||||
await meilisearch.index(MeiliIndexType.Accounts).search<{
|
||||
id: string;
|
||||
}>(q, {
|
||||
limit: Number(limit) || 10,
|
||||
offset: Number(offset) || 0,
|
||||
sort: ["createdAt:desc"],
|
||||
})
|
||||
).hits;
|
||||
}
|
||||
|
||||
if (!type || type === "statuses") {
|
||||
statusResults = (
|
||||
await meilisearch.index(MeiliIndexType.Statuses).search<{
|
||||
id: string;
|
||||
}>(q, {
|
||||
limit: Number(limit) || 10,
|
||||
offset: Number(offset) || 0,
|
||||
sort: ["createdAt:desc"],
|
||||
})
|
||||
).hits;
|
||||
}
|
||||
if (!type || type === "statuses") {
|
||||
statusResults = (
|
||||
await meilisearch.index(MeiliIndexType.Statuses).search<{
|
||||
id: string;
|
||||
}>(q, {
|
||||
limit: Number(limit) || 10,
|
||||
offset: Number(offset) || 0,
|
||||
sort: ["createdAt:desc"],
|
||||
})
|
||||
).hits;
|
||||
}
|
||||
|
||||
const accounts = await client.user.findMany({
|
||||
where: {
|
||||
id: {
|
||||
in: accountResults.map(hit => hit.id),
|
||||
},
|
||||
relationshipSubjects: {
|
||||
some: {
|
||||
subjectId: user?.id,
|
||||
following: following ? true : undefined,
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: "desc",
|
||||
},
|
||||
include: userRelations,
|
||||
});
|
||||
const accounts = await client.user.findMany({
|
||||
where: {
|
||||
id: {
|
||||
in: accountResults.map((hit) => hit.id),
|
||||
},
|
||||
relationshipSubjects: {
|
||||
some: {
|
||||
subjectId: user?.id,
|
||||
following: following ? true : undefined,
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: "desc",
|
||||
},
|
||||
include: userRelations,
|
||||
});
|
||||
|
||||
const statuses = await client.status.findMany({
|
||||
where: {
|
||||
id: {
|
||||
in: statusResults.map(hit => hit.id),
|
||||
},
|
||||
author: {
|
||||
relationshipSubjects: {
|
||||
some: {
|
||||
subjectId: user?.id,
|
||||
following: following ? true : undefined,
|
||||
},
|
||||
},
|
||||
},
|
||||
authorId: account_id ? account_id : undefined,
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: "desc",
|
||||
},
|
||||
include: statusAndUserRelations,
|
||||
});
|
||||
const statuses = await client.status.findMany({
|
||||
where: {
|
||||
id: {
|
||||
in: statusResults.map((hit) => hit.id),
|
||||
},
|
||||
author: {
|
||||
relationshipSubjects: {
|
||||
some: {
|
||||
subjectId: user?.id,
|
||||
following: following ? true : undefined,
|
||||
},
|
||||
},
|
||||
},
|
||||
authorId: account_id ? account_id : undefined,
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: "desc",
|
||||
},
|
||||
include: statusAndUserRelations,
|
||||
});
|
||||
|
||||
return jsonResponse({
|
||||
accounts: accounts.map(account => userToAPI(account)),
|
||||
statuses: await Promise.all(
|
||||
statuses.map(status => statusToAPI(status))
|
||||
),
|
||||
hashtags: [],
|
||||
});
|
||||
return jsonResponse({
|
||||
accounts: accounts.map((account) => userToAPI(account)),
|
||||
statuses: await Promise.all(
|
||||
statuses.map((status) => statusToAPI(status)),
|
||||
),
|
||||
hashtags: [],
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,105 +1,103 @@
|
|||
import { randomBytes } from "node:crypto";
|
||||
import { apiRoute, applyConfig } from "@api";
|
||||
import { randomBytes } from "crypto";
|
||||
import { client } from "~database/datasource";
|
||||
import { TokenType } from "~database/entities/Token";
|
||||
import { userRelations } from "~database/entities/relations";
|
||||
|
||||
export const meta = applyConfig({
|
||||
allowedMethods: ["POST"],
|
||||
ratelimits: {
|
||||
max: 4,
|
||||
duration: 60,
|
||||
},
|
||||
route: "/auth/login",
|
||||
auth: {
|
||||
required: false,
|
||||
},
|
||||
allowedMethods: ["POST"],
|
||||
ratelimits: {
|
||||
max: 4,
|
||||
duration: 60,
|
||||
},
|
||||
route: "/auth/login",
|
||||
auth: {
|
||||
required: false,
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* OAuth Code flow
|
||||
*/
|
||||
export default apiRoute<{
|
||||
email: string;
|
||||
password: string;
|
||||
email: string;
|
||||
password: string;
|
||||
}>(async (req, matchedRoute, extraData) => {
|
||||
const scopes = (matchedRoute.query.scope || "")
|
||||
.replaceAll("+", " ")
|
||||
.split(" ");
|
||||
const redirect_uri = matchedRoute.query.redirect_uri;
|
||||
const response_type = matchedRoute.query.response_type;
|
||||
const client_id = matchedRoute.query.client_id;
|
||||
const scopes = (matchedRoute.query.scope || "")
|
||||
.replaceAll("+", " ")
|
||||
.split(" ");
|
||||
const redirect_uri = matchedRoute.query.redirect_uri;
|
||||
const response_type = matchedRoute.query.response_type;
|
||||
const client_id = matchedRoute.query.client_id;
|
||||
|
||||
const { email, password } = extraData.parsedRequest;
|
||||
const { email, password } = extraData.parsedRequest;
|
||||
|
||||
const redirectToLogin = (error: string) =>
|
||||
Response.redirect(
|
||||
`/oauth/authorize?` +
|
||||
new URLSearchParams({
|
||||
...matchedRoute.query,
|
||||
error: encodeURIComponent(error),
|
||||
}).toString(),
|
||||
302
|
||||
);
|
||||
const redirectToLogin = (error: string) =>
|
||||
Response.redirect(
|
||||
`/oauth/authorize?${new URLSearchParams({
|
||||
...matchedRoute.query,
|
||||
error: encodeURIComponent(error),
|
||||
}).toString()}`,
|
||||
302,
|
||||
);
|
||||
|
||||
if (response_type !== "code")
|
||||
return redirectToLogin("Invalid response_type");
|
||||
if (response_type !== "code")
|
||||
return redirectToLogin("Invalid response_type");
|
||||
|
||||
if (!email || !password)
|
||||
return redirectToLogin("Invalid username or password");
|
||||
if (!email || !password)
|
||||
return redirectToLogin("Invalid username or password");
|
||||
|
||||
// Get user
|
||||
const user = await client.user.findFirst({
|
||||
where: {
|
||||
email,
|
||||
},
|
||||
include: userRelations,
|
||||
});
|
||||
// Get user
|
||||
const user = await client.user.findFirst({
|
||||
where: {
|
||||
email,
|
||||
},
|
||||
include: userRelations,
|
||||
});
|
||||
|
||||
if (!user || !(await Bun.password.verify(password, user.password || "")))
|
||||
return redirectToLogin("Invalid username or password");
|
||||
if (!user || !(await Bun.password.verify(password, user.password || "")))
|
||||
return redirectToLogin("Invalid username or password");
|
||||
|
||||
// Get application
|
||||
const application = await client.application.findFirst({
|
||||
where: {
|
||||
client_id,
|
||||
},
|
||||
});
|
||||
// Get application
|
||||
const application = await client.application.findFirst({
|
||||
where: {
|
||||
client_id,
|
||||
},
|
||||
});
|
||||
|
||||
if (!application) return redirectToLogin("Invalid client_id");
|
||||
if (!application) return redirectToLogin("Invalid client_id");
|
||||
|
||||
const code = randomBytes(32).toString("hex");
|
||||
const code = randomBytes(32).toString("hex");
|
||||
|
||||
await client.application.update({
|
||||
where: { id: application.id },
|
||||
data: {
|
||||
tokens: {
|
||||
create: {
|
||||
access_token: randomBytes(64).toString("base64url"),
|
||||
code: code,
|
||||
scope: scopes.join(" "),
|
||||
token_type: TokenType.BEARER,
|
||||
user: {
|
||||
connect: {
|
||||
id: user.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
await client.application.update({
|
||||
where: { id: application.id },
|
||||
data: {
|
||||
tokens: {
|
||||
create: {
|
||||
access_token: randomBytes(64).toString("base64url"),
|
||||
code: code,
|
||||
scope: scopes.join(" "),
|
||||
token_type: TokenType.BEARER,
|
||||
user: {
|
||||
connect: {
|
||||
id: user.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Redirect to OAuth confirmation screen
|
||||
return Response.redirect(
|
||||
`/oauth/redirect?` +
|
||||
new URLSearchParams({
|
||||
redirect_uri,
|
||||
code,
|
||||
client_id,
|
||||
application: application.name,
|
||||
website: application.website ?? "",
|
||||
scope: scopes.join(" "),
|
||||
}).toString(),
|
||||
302
|
||||
);
|
||||
// Redirect to OAuth confirmation screen
|
||||
return Response.redirect(
|
||||
`/oauth/redirect?${new URLSearchParams({
|
||||
redirect_uri,
|
||||
code,
|
||||
client_id,
|
||||
application: application.name,
|
||||
website: application.website ?? "",
|
||||
scope: scopes.join(" "),
|
||||
}).toString()}`,
|
||||
302,
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -3,56 +3,55 @@ import { client } from "~database/datasource";
|
|||
import { userRelations } from "~database/entities/relations";
|
||||
|
||||
export const meta = applyConfig({
|
||||
allowedMethods: ["POST"],
|
||||
ratelimits: {
|
||||
max: 4,
|
||||
duration: 60,
|
||||
},
|
||||
route: "/auth/redirect",
|
||||
auth: {
|
||||
required: false,
|
||||
},
|
||||
allowedMethods: ["POST"],
|
||||
ratelimits: {
|
||||
max: 4,
|
||||
duration: 60,
|
||||
},
|
||||
route: "/auth/redirect",
|
||||
auth: {
|
||||
required: false,
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* OAuth Code flow
|
||||
*/
|
||||
export default apiRoute<{
|
||||
email: string;
|
||||
password: string;
|
||||
email: string;
|
||||
password: string;
|
||||
}>(async (req, matchedRoute) => {
|
||||
const redirect_uri = decodeURIComponent(matchedRoute.query.redirect_uri);
|
||||
const client_id = matchedRoute.query.client_id;
|
||||
const code = matchedRoute.query.code;
|
||||
const redirect_uri = decodeURIComponent(matchedRoute.query.redirect_uri);
|
||||
const client_id = matchedRoute.query.client_id;
|
||||
const code = matchedRoute.query.code;
|
||||
|
||||
const redirectToLogin = (error: string) =>
|
||||
Response.redirect(
|
||||
`/oauth/authorize?` +
|
||||
new URLSearchParams({
|
||||
...matchedRoute.query,
|
||||
error: encodeURIComponent(error),
|
||||
}).toString(),
|
||||
302
|
||||
);
|
||||
const redirectToLogin = (error: string) =>
|
||||
Response.redirect(
|
||||
`/oauth/authorize?${new URLSearchParams({
|
||||
...matchedRoute.query,
|
||||
error: encodeURIComponent(error),
|
||||
}).toString()}`,
|
||||
302,
|
||||
);
|
||||
|
||||
// Get token
|
||||
const token = await client.token.findFirst({
|
||||
where: {
|
||||
code,
|
||||
application: {
|
||||
client_id,
|
||||
},
|
||||
},
|
||||
include: {
|
||||
user: {
|
||||
include: userRelations,
|
||||
},
|
||||
application: true,
|
||||
},
|
||||
});
|
||||
// Get token
|
||||
const token = await client.token.findFirst({
|
||||
where: {
|
||||
code,
|
||||
application: {
|
||||
client_id,
|
||||
},
|
||||
},
|
||||
include: {
|
||||
user: {
|
||||
include: userRelations,
|
||||
},
|
||||
application: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!token) return redirectToLogin("Invalid code");
|
||||
if (!token) return redirectToLogin("Invalid code");
|
||||
|
||||
// Redirect back to application
|
||||
return Response.redirect(`${redirect_uri}?code=${code}`, 302);
|
||||
// Redirect back to application
|
||||
return Response.redirect(`${redirect_uri}?code=${code}`, 302);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,45 +1,45 @@
|
|||
import { errorResponse } from "@response";
|
||||
import { apiRoute, applyConfig } from "@api";
|
||||
import { errorResponse } from "@response";
|
||||
|
||||
export const meta = applyConfig({
|
||||
allowedMethods: ["GET"],
|
||||
route: "/media/:id",
|
||||
ratelimits: {
|
||||
max: 100,
|
||||
duration: 60,
|
||||
},
|
||||
auth: {
|
||||
required: false,
|
||||
},
|
||||
allowedMethods: ["GET"],
|
||||
route: "/media/:id",
|
||||
ratelimits: {
|
||||
max: 100,
|
||||
duration: 60,
|
||||
},
|
||||
auth: {
|
||||
required: false,
|
||||
},
|
||||
});
|
||||
|
||||
export default apiRoute(async (req, matchedRoute) => {
|
||||
// TODO: Add checks for disabled or not email verified accounts
|
||||
// TODO: Add checks for disabled or not email verified accounts
|
||||
|
||||
const id = matchedRoute.params.id;
|
||||
const id = matchedRoute.params.id;
|
||||
|
||||
// parse `Range` header
|
||||
const [start = 0, end = Infinity] = (
|
||||
(req.headers.get("Range") || "")
|
||||
.split("=") // ["Range: bytes", "0-100"]
|
||||
.at(-1) || ""
|
||||
) // "0-100"
|
||||
.split("-") // ["0", "100"]
|
||||
.map(Number); // [0, 100]
|
||||
// parse `Range` header
|
||||
const [start = 0, end = Number.POSITIVE_INFINITY] = (
|
||||
(req.headers.get("Range") || "")
|
||||
.split("=") // ["Range: bytes", "0-100"]
|
||||
.at(-1) || ""
|
||||
) // "0-100"
|
||||
.split("-") // ["0", "100"]
|
||||
.map(Number); // [0, 100]
|
||||
|
||||
// Serve file from filesystem
|
||||
const file = Bun.file(`./uploads/${id}`);
|
||||
// Serve file from filesystem
|
||||
const file = Bun.file(`./uploads/${id}`);
|
||||
|
||||
const buffer = await file.arrayBuffer();
|
||||
const buffer = await file.arrayBuffer();
|
||||
|
||||
if (!(await file.exists())) return errorResponse("File not found", 404);
|
||||
if (!(await file.exists())) return errorResponse("File not found", 404);
|
||||
|
||||
// Can't directly copy file into Response because this crashes Bun for now
|
||||
return new Response(buffer, {
|
||||
headers: {
|
||||
"Content-Type": file.type || "application/octet-stream",
|
||||
"Content-Length": `${file.size - start}`,
|
||||
"Content-Range": `bytes ${start}-${end}/${file.size}`,
|
||||
},
|
||||
});
|
||||
// Can't directly copy file into Response because this crashes Bun for now
|
||||
return new Response(buffer, {
|
||||
headers: {
|
||||
"Content-Type": file.type || "application/octet-stream",
|
||||
"Content-Length": `${file.size - start}`,
|
||||
"Content-Range": `bytes ${start}-${end}/${file.size}`,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -2,32 +2,32 @@ import { apiRoute, applyConfig } from "@api";
|
|||
import { jsonResponse } from "@response";
|
||||
|
||||
export const meta = applyConfig({
|
||||
allowedMethods: ["GET"],
|
||||
auth: {
|
||||
required: false,
|
||||
},
|
||||
ratelimits: {
|
||||
duration: 60,
|
||||
max: 500,
|
||||
},
|
||||
route: "/nodeinfo/2.0",
|
||||
allowedMethods: ["GET"],
|
||||
auth: {
|
||||
required: false,
|
||||
},
|
||||
ratelimits: {
|
||||
duration: 60,
|
||||
max: 500,
|
||||
},
|
||||
route: "/nodeinfo/2.0",
|
||||
});
|
||||
|
||||
/**
|
||||
* ActivityPub nodeinfo 2.0 endpoint
|
||||
*/
|
||||
export default apiRoute(() => {
|
||||
// TODO: Implement this
|
||||
return jsonResponse({
|
||||
version: "2.0",
|
||||
software: { name: "lysand", version: "0.0.1" },
|
||||
protocols: ["activitypub"],
|
||||
services: { outbound: [], inbound: [] },
|
||||
usage: {
|
||||
users: { total: 0, activeMonth: 0, activeHalfyear: 0 },
|
||||
localPosts: 0,
|
||||
},
|
||||
openRegistrations: false,
|
||||
metadata: {},
|
||||
});
|
||||
// TODO: Implement this
|
||||
return jsonResponse({
|
||||
version: "2.0",
|
||||
software: { name: "lysand", version: "0.0.1" },
|
||||
protocols: ["activitypub"],
|
||||
services: { outbound: [], inbound: [] },
|
||||
usage: {
|
||||
users: { total: 0, activeMonth: 0, activeHalfyear: 0 },
|
||||
localPosts: 0,
|
||||
},
|
||||
openRegistrations: false,
|
||||
metadata: {},
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,95 +1,91 @@
|
|||
import { apiRoute, applyConfig } from "@api";
|
||||
import { oauthRedirectUri } from "@constants";
|
||||
import {
|
||||
calculatePKCECodeChallenge,
|
||||
discoveryRequest,
|
||||
generateRandomCodeVerifier,
|
||||
processDiscoveryResponse,
|
||||
calculatePKCECodeChallenge,
|
||||
discoveryRequest,
|
||||
generateRandomCodeVerifier,
|
||||
processDiscoveryResponse,
|
||||
} from "oauth4webapi";
|
||||
import { client } from "~database/datasource";
|
||||
|
||||
export const meta = applyConfig({
|
||||
allowedMethods: ["GET"],
|
||||
auth: {
|
||||
required: false,
|
||||
},
|
||||
ratelimits: {
|
||||
duration: 60,
|
||||
max: 20,
|
||||
},
|
||||
route: "/oauth/authorize-external",
|
||||
allowedMethods: ["GET"],
|
||||
auth: {
|
||||
required: false,
|
||||
},
|
||||
ratelimits: {
|
||||
duration: 60,
|
||||
max: 20,
|
||||
},
|
||||
route: "/oauth/authorize-external",
|
||||
});
|
||||
|
||||
/**
|
||||
* Redirects the user to the external OAuth provider
|
||||
*/
|
||||
export default apiRoute(async (req, matchedRoute, extraData) => {
|
||||
const redirectToLogin = (error: string) =>
|
||||
Response.redirect(
|
||||
`/oauth/authorize?` +
|
||||
new URLSearchParams({
|
||||
...matchedRoute.query,
|
||||
error: encodeURIComponent(error),
|
||||
}).toString(),
|
||||
302
|
||||
);
|
||||
const redirectToLogin = (error: string) =>
|
||||
Response.redirect(
|
||||
`/oauth/authorize?${new URLSearchParams({
|
||||
...matchedRoute.query,
|
||||
error: encodeURIComponent(error),
|
||||
}).toString()}`,
|
||||
302,
|
||||
);
|
||||
|
||||
const issuerId = matchedRoute.query.issuer;
|
||||
const issuerId = matchedRoute.query.issuer;
|
||||
|
||||
// This is the Lysand client's client_id, not the external OAuth provider's client_id
|
||||
const clientId = matchedRoute.query.clientId;
|
||||
// This is the Lysand client's client_id, not the external OAuth provider's client_id
|
||||
const clientId = matchedRoute.query.clientId;
|
||||
|
||||
if (!clientId || clientId === "undefined") {
|
||||
return redirectToLogin("Missing client_id");
|
||||
}
|
||||
if (!clientId || clientId === "undefined") {
|
||||
return redirectToLogin("Missing client_id");
|
||||
}
|
||||
|
||||
const config = await extraData.configManager.getConfig();
|
||||
const config = await extraData.configManager.getConfig();
|
||||
|
||||
const issuer = config.oidc.providers.find(
|
||||
provider => provider.id === issuerId
|
||||
);
|
||||
const issuer = config.oidc.providers.find(
|
||||
(provider) => provider.id === issuerId,
|
||||
);
|
||||
|
||||
if (!issuer) {
|
||||
return redirectToLogin("Invalid issuer");
|
||||
}
|
||||
if (!issuer) {
|
||||
return redirectToLogin("Invalid issuer");
|
||||
}
|
||||
|
||||
const issuerUrl = new URL(issuer.url);
|
||||
const issuerUrl = new URL(issuer.url);
|
||||
|
||||
const authServer = await discoveryRequest(issuerUrl, {
|
||||
algorithm: "oidc",
|
||||
}).then(res => processDiscoveryResponse(issuerUrl, res));
|
||||
const authServer = await discoveryRequest(issuerUrl, {
|
||||
algorithm: "oidc",
|
||||
}).then((res) => processDiscoveryResponse(issuerUrl, res));
|
||||
|
||||
const codeVerifier = generateRandomCodeVerifier();
|
||||
const codeVerifier = generateRandomCodeVerifier();
|
||||
|
||||
// Store into database
|
||||
// Store into database
|
||||
|
||||
const newFlow = await client.openIdLoginFlow.create({
|
||||
data: {
|
||||
codeVerifier,
|
||||
application: {
|
||||
connect: {
|
||||
client_id: clientId,
|
||||
},
|
||||
},
|
||||
issuerId,
|
||||
},
|
||||
});
|
||||
const newFlow = await client.openIdLoginFlow.create({
|
||||
data: {
|
||||
codeVerifier,
|
||||
application: {
|
||||
connect: {
|
||||
client_id: clientId,
|
||||
},
|
||||
},
|
||||
issuerId,
|
||||
},
|
||||
});
|
||||
|
||||
const codeChallenge = await calculatePKCECodeChallenge(codeVerifier);
|
||||
const codeChallenge = await calculatePKCECodeChallenge(codeVerifier);
|
||||
|
||||
return Response.redirect(
|
||||
authServer.authorization_endpoint +
|
||||
"?" +
|
||||
new URLSearchParams({
|
||||
client_id: issuer.client_id,
|
||||
redirect_uri:
|
||||
oauthRedirectUri(issuerId) + `?flow=${newFlow.id}`,
|
||||
response_type: "code",
|
||||
scope: "openid profile email",
|
||||
// PKCE
|
||||
code_challenge_method: "S256",
|
||||
code_challenge: codeChallenge,
|
||||
}).toString(),
|
||||
302
|
||||
);
|
||||
return Response.redirect(
|
||||
`${authServer.authorization_endpoint}?${new URLSearchParams({
|
||||
client_id: issuer.client_id,
|
||||
redirect_uri: `${oauthRedirectUri(issuerId)}?flow=${newFlow.id}`,
|
||||
response_type: "code",
|
||||
scope: "openid profile email",
|
||||
// PKCE
|
||||
code_challenge_method: "S256",
|
||||
code_challenge: codeChallenge,
|
||||
}).toString()}`,
|
||||
302,
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,198 +1,196 @@
|
|||
import { randomBytes } from "node:crypto";
|
||||
import { apiRoute, applyConfig } from "@api";
|
||||
import { oauthRedirectUri } from "@constants";
|
||||
import { randomBytes } from "crypto";
|
||||
import {
|
||||
authorizationCodeGrantRequest,
|
||||
discoveryRequest,
|
||||
expectNoState,
|
||||
isOAuth2Error,
|
||||
processDiscoveryResponse,
|
||||
validateAuthResponse,
|
||||
userInfoRequest,
|
||||
processAuthorizationCodeOpenIDResponse,
|
||||
processUserInfoResponse,
|
||||
getValidatedIdTokenClaims,
|
||||
authorizationCodeGrantRequest,
|
||||
discoveryRequest,
|
||||
expectNoState,
|
||||
getValidatedIdTokenClaims,
|
||||
isOAuth2Error,
|
||||
processAuthorizationCodeOpenIDResponse,
|
||||
processDiscoveryResponse,
|
||||
processUserInfoResponse,
|
||||
userInfoRequest,
|
||||
validateAuthResponse,
|
||||
} from "oauth4webapi";
|
||||
import { client } from "~database/datasource";
|
||||
import { TokenType } from "~database/entities/Token";
|
||||
|
||||
export const meta = applyConfig({
|
||||
allowedMethods: ["GET"],
|
||||
auth: {
|
||||
required: false,
|
||||
},
|
||||
ratelimits: {
|
||||
duration: 60,
|
||||
max: 20,
|
||||
},
|
||||
route: "/oauth/callback/:issuer",
|
||||
allowedMethods: ["GET"],
|
||||
auth: {
|
||||
required: false,
|
||||
},
|
||||
ratelimits: {
|
||||
duration: 60,
|
||||
max: 20,
|
||||
},
|
||||
route: "/oauth/callback/:issuer",
|
||||
});
|
||||
|
||||
/**
|
||||
* Redirects the user to the external OAuth provider
|
||||
*/
|
||||
export default apiRoute(async (req, matchedRoute, extraData) => {
|
||||
const redirectToLogin = (error: string) =>
|
||||
Response.redirect(
|
||||
`/oauth/authorize?` +
|
||||
new URLSearchParams({
|
||||
client_id: matchedRoute.query.clientId,
|
||||
error: encodeURIComponent(error),
|
||||
}).toString(),
|
||||
302
|
||||
);
|
||||
const redirectToLogin = (error: string) =>
|
||||
Response.redirect(
|
||||
`/oauth/authorize?${new URLSearchParams({
|
||||
client_id: matchedRoute.query.clientId,
|
||||
error: encodeURIComponent(error),
|
||||
}).toString()}`,
|
||||
302,
|
||||
);
|
||||
|
||||
const currentUrl = new URL(req.url);
|
||||
const currentUrl = new URL(req.url);
|
||||
|
||||
// Remove state query parameter from URL
|
||||
currentUrl.searchParams.delete("state");
|
||||
const issuerParam = matchedRoute.params.issuer;
|
||||
const flow = await client.openIdLoginFlow.findFirst({
|
||||
where: {
|
||||
id: matchedRoute.query.flow,
|
||||
},
|
||||
include: {
|
||||
application: true,
|
||||
},
|
||||
});
|
||||
// Remove state query parameter from URL
|
||||
currentUrl.searchParams.delete("state");
|
||||
const issuerParam = matchedRoute.params.issuer;
|
||||
const flow = await client.openIdLoginFlow.findFirst({
|
||||
where: {
|
||||
id: matchedRoute.query.flow,
|
||||
},
|
||||
include: {
|
||||
application: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!flow) {
|
||||
return redirectToLogin("Invalid flow");
|
||||
}
|
||||
if (!flow) {
|
||||
return redirectToLogin("Invalid flow");
|
||||
}
|
||||
|
||||
const config = await extraData.configManager.getConfig();
|
||||
const config = await extraData.configManager.getConfig();
|
||||
|
||||
const issuer = config.oidc.providers.find(
|
||||
provider => provider.id === issuerParam
|
||||
);
|
||||
const issuer = config.oidc.providers.find(
|
||||
(provider) => provider.id === issuerParam,
|
||||
);
|
||||
|
||||
if (!issuer) {
|
||||
return redirectToLogin("Invalid issuer");
|
||||
}
|
||||
if (!issuer) {
|
||||
return redirectToLogin("Invalid issuer");
|
||||
}
|
||||
|
||||
const issuerUrl = new URL(issuer.url);
|
||||
const issuerUrl = new URL(issuer.url);
|
||||
|
||||
const authServer = await discoveryRequest(issuerUrl, {
|
||||
algorithm: "oidc",
|
||||
}).then(res => processDiscoveryResponse(issuerUrl, res));
|
||||
const authServer = await discoveryRequest(issuerUrl, {
|
||||
algorithm: "oidc",
|
||||
}).then((res) => processDiscoveryResponse(issuerUrl, res));
|
||||
|
||||
const parameters = validateAuthResponse(
|
||||
authServer,
|
||||
{
|
||||
client_id: issuer.client_id,
|
||||
client_secret: issuer.client_secret,
|
||||
},
|
||||
currentUrl,
|
||||
// Whether to expect state or not
|
||||
expectNoState
|
||||
);
|
||||
const parameters = validateAuthResponse(
|
||||
authServer,
|
||||
{
|
||||
client_id: issuer.client_id,
|
||||
client_secret: issuer.client_secret,
|
||||
},
|
||||
currentUrl,
|
||||
// Whether to expect state or not
|
||||
expectNoState,
|
||||
);
|
||||
|
||||
if (isOAuth2Error(parameters)) {
|
||||
return redirectToLogin(
|
||||
parameters.error_description || parameters.error
|
||||
);
|
||||
}
|
||||
if (isOAuth2Error(parameters)) {
|
||||
return redirectToLogin(
|
||||
parameters.error_description || parameters.error,
|
||||
);
|
||||
}
|
||||
|
||||
const response = await authorizationCodeGrantRequest(
|
||||
authServer,
|
||||
{
|
||||
client_id: issuer.client_id,
|
||||
client_secret: issuer.client_secret,
|
||||
},
|
||||
parameters,
|
||||
oauthRedirectUri(issuerParam) + `?flow=${flow.id}`,
|
||||
flow.codeVerifier
|
||||
);
|
||||
const response = await authorizationCodeGrantRequest(
|
||||
authServer,
|
||||
{
|
||||
client_id: issuer.client_id,
|
||||
client_secret: issuer.client_secret,
|
||||
},
|
||||
parameters,
|
||||
`${oauthRedirectUri(issuerParam)}?flow=${flow.id}`,
|
||||
flow.codeVerifier,
|
||||
);
|
||||
|
||||
const result = await processAuthorizationCodeOpenIDResponse(
|
||||
authServer,
|
||||
{
|
||||
client_id: issuer.client_id,
|
||||
client_secret: issuer.client_secret,
|
||||
},
|
||||
response
|
||||
);
|
||||
const result = await processAuthorizationCodeOpenIDResponse(
|
||||
authServer,
|
||||
{
|
||||
client_id: issuer.client_id,
|
||||
client_secret: issuer.client_secret,
|
||||
},
|
||||
response,
|
||||
);
|
||||
|
||||
if (isOAuth2Error(result)) {
|
||||
return redirectToLogin(result.error_description || result.error);
|
||||
}
|
||||
if (isOAuth2Error(result)) {
|
||||
return redirectToLogin(result.error_description || result.error);
|
||||
}
|
||||
|
||||
const { access_token } = result;
|
||||
const { access_token } = result;
|
||||
|
||||
const claims = getValidatedIdTokenClaims(result);
|
||||
const { sub } = claims;
|
||||
const claims = getValidatedIdTokenClaims(result);
|
||||
const { sub } = claims;
|
||||
|
||||
// Validate `sub`
|
||||
// Later, we'll use this to automatically set the user's data
|
||||
await userInfoRequest(
|
||||
authServer,
|
||||
{
|
||||
client_id: issuer.client_id,
|
||||
client_secret: issuer.client_secret,
|
||||
},
|
||||
access_token
|
||||
).then(res =>
|
||||
processUserInfoResponse(
|
||||
authServer,
|
||||
{
|
||||
client_id: issuer.client_id,
|
||||
client_secret: issuer.client_secret,
|
||||
},
|
||||
sub,
|
||||
res
|
||||
)
|
||||
);
|
||||
// Validate `sub`
|
||||
// Later, we'll use this to automatically set the user's data
|
||||
await userInfoRequest(
|
||||
authServer,
|
||||
{
|
||||
client_id: issuer.client_id,
|
||||
client_secret: issuer.client_secret,
|
||||
},
|
||||
access_token,
|
||||
).then((res) =>
|
||||
processUserInfoResponse(
|
||||
authServer,
|
||||
{
|
||||
client_id: issuer.client_id,
|
||||
client_secret: issuer.client_secret,
|
||||
},
|
||||
sub,
|
||||
res,
|
||||
),
|
||||
);
|
||||
|
||||
const user = await client.user.findFirst({
|
||||
where: {
|
||||
linkedOpenIdAccounts: {
|
||||
some: {
|
||||
serverId: sub,
|
||||
issuerId: issuer.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
const user = await client.user.findFirst({
|
||||
where: {
|
||||
linkedOpenIdAccounts: {
|
||||
some: {
|
||||
serverId: sub,
|
||||
issuerId: issuer.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return redirectToLogin("No user found with that account");
|
||||
}
|
||||
if (!user) {
|
||||
return redirectToLogin("No user found with that account");
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
if (!flow.application) return redirectToLogin("Invalid client_id");
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
if (!flow.application) return redirectToLogin("Invalid client_id");
|
||||
|
||||
const code = randomBytes(32).toString("hex");
|
||||
const code = randomBytes(32).toString("hex");
|
||||
|
||||
await client.application.update({
|
||||
where: { id: flow.application.id },
|
||||
data: {
|
||||
tokens: {
|
||||
create: {
|
||||
access_token: randomBytes(64).toString("base64url"),
|
||||
code: code,
|
||||
scope: flow.application.scopes,
|
||||
token_type: TokenType.BEARER,
|
||||
user: {
|
||||
connect: {
|
||||
id: user.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
await client.application.update({
|
||||
where: { id: flow.application.id },
|
||||
data: {
|
||||
tokens: {
|
||||
create: {
|
||||
access_token: randomBytes(64).toString("base64url"),
|
||||
code: code,
|
||||
scope: flow.application.scopes,
|
||||
token_type: TokenType.BEARER,
|
||||
user: {
|
||||
connect: {
|
||||
id: user.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Redirect back to application
|
||||
return Response.redirect(
|
||||
`/oauth/redirect?` +
|
||||
new URLSearchParams({
|
||||
redirect_uri: flow.application.redirect_uris,
|
||||
code,
|
||||
client_id: flow.application.client_id,
|
||||
application: flow.application.name,
|
||||
website: flow.application.website ?? "",
|
||||
scope: flow.application.scopes,
|
||||
}).toString(),
|
||||
302
|
||||
);
|
||||
// Redirect back to application
|
||||
return Response.redirect(
|
||||
`/oauth/redirect?${new URLSearchParams({
|
||||
redirect_uri: flow.application.redirect_uris,
|
||||
code,
|
||||
client_id: flow.application.client_id,
|
||||
application: flow.application.name,
|
||||
website: flow.application.website ?? "",
|
||||
scope: flow.application.scopes,
|
||||
}).toString()}`,
|
||||
302,
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -2,28 +2,28 @@ import { apiRoute, applyConfig } from "@api";
|
|||
import { jsonResponse } from "@response";
|
||||
|
||||
export const meta = applyConfig({
|
||||
allowedMethods: ["GET"],
|
||||
auth: {
|
||||
required: false,
|
||||
},
|
||||
ratelimits: {
|
||||
duration: 60,
|
||||
max: 10,
|
||||
},
|
||||
route: "/oauth/providers",
|
||||
allowedMethods: ["GET"],
|
||||
auth: {
|
||||
required: false,
|
||||
},
|
||||
ratelimits: {
|
||||
duration: 60,
|
||||
max: 10,
|
||||
},
|
||||
route: "/oauth/providers",
|
||||
});
|
||||
|
||||
/**
|
||||
* Lists available OAuth providers
|
||||
*/
|
||||
export default apiRoute(async (req, matchedRoute, extraData) => {
|
||||
const config = await extraData.configManager.getConfig();
|
||||
const config = await extraData.configManager.getConfig();
|
||||
|
||||
return jsonResponse(
|
||||
config.oidc.providers.map(p => ({
|
||||
name: p.name,
|
||||
icon: p.icon,
|
||||
id: p.id,
|
||||
}))
|
||||
);
|
||||
return jsonResponse(
|
||||
config.oidc.providers.map((p) => ({
|
||||
name: p.name,
|
||||
icon: p.icon,
|
||||
id: p.id,
|
||||
})),
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -3,61 +3,61 @@ import { errorResponse, jsonResponse } from "@response";
|
|||
import { client } from "~database/datasource";
|
||||
|
||||
export const meta = applyConfig({
|
||||
allowedMethods: ["POST"],
|
||||
auth: {
|
||||
required: false,
|
||||
},
|
||||
ratelimits: {
|
||||
duration: 60,
|
||||
max: 10,
|
||||
},
|
||||
route: "/oauth/token",
|
||||
allowedMethods: ["POST"],
|
||||
auth: {
|
||||
required: false,
|
||||
},
|
||||
ratelimits: {
|
||||
duration: 60,
|
||||
max: 10,
|
||||
},
|
||||
route: "/oauth/token",
|
||||
});
|
||||
|
||||
/**
|
||||
* Allows getting token from OAuth code
|
||||
*/
|
||||
export default apiRoute<{
|
||||
grant_type: string;
|
||||
code: string;
|
||||
redirect_uri: string;
|
||||
client_id: string;
|
||||
client_secret: string;
|
||||
scope: string;
|
||||
grant_type: string;
|
||||
code: string;
|
||||
redirect_uri: string;
|
||||
client_id: string;
|
||||
client_secret: string;
|
||||
scope: string;
|
||||
}>(async (req, matchedRoute, extraData) => {
|
||||
const { grant_type, code, redirect_uri, client_id, client_secret, scope } =
|
||||
extraData.parsedRequest;
|
||||
const { grant_type, code, redirect_uri, client_id, client_secret, scope } =
|
||||
extraData.parsedRequest;
|
||||
|
||||
if (grant_type !== "authorization_code")
|
||||
return errorResponse(
|
||||
"Invalid grant type (try 'authorization_code')",
|
||||
400
|
||||
);
|
||||
if (grant_type !== "authorization_code")
|
||||
return errorResponse(
|
||||
"Invalid grant type (try 'authorization_code')",
|
||||
400,
|
||||
);
|
||||
|
||||
// Get associated token
|
||||
const token = await client.token.findFirst({
|
||||
where: {
|
||||
code,
|
||||
application: {
|
||||
client_id,
|
||||
secret: client_secret,
|
||||
redirect_uris: redirect_uri,
|
||||
scopes: scope?.replaceAll("+", " "),
|
||||
},
|
||||
scope: scope?.replaceAll("+", " "),
|
||||
},
|
||||
include: {
|
||||
application: true,
|
||||
},
|
||||
});
|
||||
// Get associated token
|
||||
const token = await client.token.findFirst({
|
||||
where: {
|
||||
code,
|
||||
application: {
|
||||
client_id,
|
||||
secret: client_secret,
|
||||
redirect_uris: redirect_uri,
|
||||
scopes: scope?.replaceAll("+", " "),
|
||||
},
|
||||
scope: scope?.replaceAll("+", " "),
|
||||
},
|
||||
include: {
|
||||
application: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!token)
|
||||
return errorResponse("Invalid access token or client credentials", 401);
|
||||
if (!token)
|
||||
return errorResponse("Invalid access token or client credentials", 401);
|
||||
|
||||
return jsonResponse({
|
||||
access_token: token.access_token,
|
||||
token_type: token.token_type,
|
||||
scope: token.scope,
|
||||
created_at: token.created_at,
|
||||
});
|
||||
return jsonResponse({
|
||||
access_token: token.access_token,
|
||||
token_type: token.token_type,
|
||||
scope: token.scope,
|
||||
created_at: token.created_at,
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -2,17 +2,17 @@ import { apiRoute, applyConfig } from "@api";
|
|||
import { jsonResponse } from "@response";
|
||||
|
||||
export const meta = applyConfig({
|
||||
allowedMethods: ["GET"],
|
||||
auth: {
|
||||
required: false,
|
||||
},
|
||||
ratelimits: {
|
||||
duration: 60,
|
||||
max: 500,
|
||||
},
|
||||
route: "/object/:id",
|
||||
allowedMethods: ["GET"],
|
||||
auth: {
|
||||
required: false,
|
||||
},
|
||||
ratelimits: {
|
||||
duration: 60,
|
||||
max: 500,
|
||||
},
|
||||
route: "/object/:id",
|
||||
});
|
||||
|
||||
export default apiRoute(() => {
|
||||
return jsonResponse({});
|
||||
return jsonResponse({});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -3,13 +3,13 @@ import type { Config } from "config-manager";
|
|||
import type { AuthData } from "~database/entities/User";
|
||||
|
||||
export type RouteHandler<T> = (
|
||||
req: Request,
|
||||
matchedRoute: MatchedRoute,
|
||||
extraData: {
|
||||
auth: AuthData;
|
||||
parsedRequest: Partial<T>;
|
||||
configManager: {
|
||||
getConfig: () => Promise<Config>;
|
||||
};
|
||||
}
|
||||
req: Request,
|
||||
matchedRoute: MatchedRoute,
|
||||
extraData: {
|
||||
auth: AuthData;
|
||||
parsedRequest: Partial<T>;
|
||||
configManager: {
|
||||
getConfig: () => Promise<Config>;
|
||||
};
|
||||
},
|
||||
) => Response | Promise<Response>;
|
||||
|
|
|
|||
|
|
@ -9,394 +9,393 @@ import { createFromObject } from "~database/entities/Object";
|
|||
import { createNewStatus, fetchFromRemote } from "~database/entities/Status";
|
||||
import { parseMentionsUris } from "~database/entities/User";
|
||||
import {
|
||||
userRelations,
|
||||
statusAndUserRelations,
|
||||
statusAndUserRelations,
|
||||
userRelations,
|
||||
} from "~database/entities/relations";
|
||||
import type {
|
||||
Announce,
|
||||
Like,
|
||||
LysandAction,
|
||||
LysandPublication,
|
||||
Patch,
|
||||
Undo,
|
||||
Announce,
|
||||
Like,
|
||||
LysandAction,
|
||||
LysandPublication,
|
||||
Patch,
|
||||
Undo,
|
||||
} from "~types/lysand/Object";
|
||||
|
||||
export const meta = applyConfig({
|
||||
allowedMethods: ["POST"],
|
||||
auth: {
|
||||
required: false,
|
||||
},
|
||||
ratelimits: {
|
||||
duration: 60,
|
||||
max: 500,
|
||||
},
|
||||
route: "/users/:username/inbox",
|
||||
allowedMethods: ["POST"],
|
||||
auth: {
|
||||
required: false,
|
||||
},
|
||||
ratelimits: {
|
||||
duration: 60,
|
||||
max: 500,
|
||||
},
|
||||
route: "/users/:username/inbox",
|
||||
});
|
||||
|
||||
/**
|
||||
* ActivityPub user inbox endpoint
|
||||
*/
|
||||
export default apiRoute(async (req, matchedRoute, extraData) => {
|
||||
const username = matchedRoute.params.username;
|
||||
const username = matchedRoute.params.username;
|
||||
|
||||
const config = await extraData.configManager.getConfig();
|
||||
const config = await extraData.configManager.getConfig();
|
||||
|
||||
try {
|
||||
if (
|
||||
config.activitypub.reject_activities.includes(
|
||||
new URL(req.headers.get("Origin") ?? "").hostname
|
||||
)
|
||||
) {
|
||||
// Discard request
|
||||
return jsonResponse({});
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(
|
||||
`[-] Error parsing Origin header of incoming Activity from ${req.headers.get(
|
||||
"Origin"
|
||||
)}`
|
||||
);
|
||||
console.error(e);
|
||||
}
|
||||
/* try {
|
||||
if (
|
||||
config.activitypub.reject_activities.includes(
|
||||
new URL(req.headers.get("Origin") ?? "").hostname,
|
||||
)
|
||||
) {
|
||||
// Discard request
|
||||
return jsonResponse({});
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(
|
||||
`[-] Error parsing Origin header of incoming Activity from ${req.headers.get(
|
||||
"Origin",
|
||||
)}`,
|
||||
);
|
||||
console.error(e);
|
||||
} */
|
||||
|
||||
// Process request body
|
||||
const body = (await req.json()) as LysandPublication | LysandAction;
|
||||
// Process request body
|
||||
const body = (await req.json()) as LysandPublication | LysandAction;
|
||||
|
||||
const author = await client.user.findUnique({
|
||||
where: {
|
||||
username,
|
||||
},
|
||||
include: userRelations,
|
||||
});
|
||||
const author = await client.user.findUnique({
|
||||
where: {
|
||||
username,
|
||||
},
|
||||
include: userRelations,
|
||||
});
|
||||
|
||||
if (!author) {
|
||||
// TODO: Add new author to database
|
||||
return errorResponse("Author not found", 404);
|
||||
}
|
||||
if (!author) {
|
||||
// TODO: Add new author to database
|
||||
return errorResponse("Author not found", 404);
|
||||
}
|
||||
|
||||
// Verify HTTP signature
|
||||
if (config.activitypub.authorized_fetch) {
|
||||
// Check if date is older than 30 seconds
|
||||
const origin = req.headers.get("Origin");
|
||||
// Verify HTTP signature
|
||||
/* if (config.activitypub.authorized_fetch) {
|
||||
// Check if date is older than 30 seconds
|
||||
const origin = req.headers.get("Origin");
|
||||
|
||||
if (!origin) {
|
||||
return errorResponse("Origin header is required", 401);
|
||||
}
|
||||
if (!origin) {
|
||||
return errorResponse("Origin header is required", 401);
|
||||
}
|
||||
|
||||
const date = req.headers.get("Date");
|
||||
const date = req.headers.get("Date");
|
||||
|
||||
if (!date) {
|
||||
return errorResponse("Date header is required", 401);
|
||||
}
|
||||
if (!date) {
|
||||
return errorResponse("Date header is required", 401);
|
||||
}
|
||||
|
||||
if (new Date(date).getTime() < Date.now() - 30000) {
|
||||
return errorResponse("Date is too old (max 30 seconds)", 401);
|
||||
}
|
||||
if (new Date(date).getTime() < Date.now() - 30000) {
|
||||
return errorResponse("Date is too old (max 30 seconds)", 401);
|
||||
}
|
||||
|
||||
const signatureHeader = req.headers.get("Signature");
|
||||
const signatureHeader = req.headers.get("Signature");
|
||||
|
||||
if (!signatureHeader) {
|
||||
return errorResponse("Signature header is required", 401);
|
||||
}
|
||||
if (!signatureHeader) {
|
||||
return errorResponse("Signature header is required", 401);
|
||||
}
|
||||
|
||||
const signature = signatureHeader
|
||||
.split("signature=")[1]
|
||||
.replace(/"/g, "");
|
||||
const signature = signatureHeader
|
||||
.split("signature=")[1]
|
||||
.replace(/"/g, "");
|
||||
|
||||
const digest = await crypto.subtle.digest(
|
||||
"SHA-256",
|
||||
new TextEncoder().encode(await req.text())
|
||||
);
|
||||
const digest = await crypto.subtle.digest(
|
||||
"SHA-256",
|
||||
new TextEncoder().encode(await req.text()),
|
||||
);
|
||||
|
||||
const expectedSignedString =
|
||||
`(request-target): ${req.method.toLowerCase()} ${req.url}\n` +
|
||||
`host: ${req.url}\n` +
|
||||
`date: ${date}\n` +
|
||||
`digest: SHA-256=${Buffer.from(digest).toString("base64")}`;
|
||||
const expectedSignedString =
|
||||
`(request-target): ${req.method.toLowerCase()} ${req.url}\n` +
|
||||
`host: ${req.url}\n` +
|
||||
`date: ${date}\n` +
|
||||
`digest: SHA-256=${Buffer.from(digest).toString("base64")}`;
|
||||
|
||||
// author.public_key is base64 encoded raw public key
|
||||
const publicKey = await crypto.subtle.importKey(
|
||||
"spki",
|
||||
Buffer.from(author.publicKey, "base64"),
|
||||
"Ed25519",
|
||||
false,
|
||||
["verify"]
|
||||
);
|
||||
// author.public_key is base64 encoded raw public key
|
||||
const publicKey = await crypto.subtle.importKey(
|
||||
"spki",
|
||||
Buffer.from(author.publicKey, "base64"),
|
||||
"Ed25519",
|
||||
false,
|
||||
["verify"],
|
||||
);
|
||||
|
||||
// Check if signed string is valid
|
||||
const isValid = await crypto.subtle.verify(
|
||||
"Ed25519",
|
||||
publicKey,
|
||||
Buffer.from(signature, "base64"),
|
||||
new TextEncoder().encode(expectedSignedString)
|
||||
);
|
||||
// Check if signed string is valid
|
||||
const isValid = await crypto.subtle.verify(
|
||||
"Ed25519",
|
||||
publicKey,
|
||||
Buffer.from(signature, "base64"),
|
||||
new TextEncoder().encode(expectedSignedString),
|
||||
);
|
||||
|
||||
if (!isValid) {
|
||||
return errorResponse("Invalid signature", 401);
|
||||
}
|
||||
}
|
||||
if (!isValid) {
|
||||
return errorResponse("Invalid signature", 401);
|
||||
}
|
||||
} */
|
||||
|
||||
// Get the object's ActivityPub type
|
||||
const type = body.type;
|
||||
// Get the object's ActivityPub type
|
||||
const type = body.type;
|
||||
|
||||
switch (type) {
|
||||
case "Note": {
|
||||
// Store the object in the LysandObject table
|
||||
await createFromObject(body);
|
||||
switch (type) {
|
||||
case "Note": {
|
||||
// Store the object in the LysandObject table
|
||||
await createFromObject(body);
|
||||
|
||||
const content = getBestContentType(body.contents);
|
||||
const content = getBestContentType(body.contents);
|
||||
|
||||
const emojis = await parseEmojis(content?.content || "");
|
||||
const emojis = await parseEmojis(content?.content || "");
|
||||
|
||||
const newStatus = await createNewStatus({
|
||||
account: author,
|
||||
content: content?.content || "",
|
||||
content_type: content?.content_type,
|
||||
application: null,
|
||||
// TODO: Add visibility
|
||||
visibility: "public",
|
||||
spoiler_text: body.subject || "",
|
||||
sensitive: body.is_sensitive,
|
||||
uri: body.uri,
|
||||
emojis: emojis,
|
||||
mentions: await parseMentionsUris(body.mentions),
|
||||
});
|
||||
const newStatus = await createNewStatus({
|
||||
account: author,
|
||||
content: content?.content || "",
|
||||
content_type: content?.content_type,
|
||||
application: null,
|
||||
// TODO: Add visibility
|
||||
visibility: "public",
|
||||
spoiler_text: body.subject || "",
|
||||
sensitive: body.is_sensitive,
|
||||
uri: body.uri,
|
||||
emojis: emojis,
|
||||
mentions: await parseMentionsUris(body.mentions),
|
||||
});
|
||||
|
||||
// If there is a reply, fetch all the reply parents and add them to the database
|
||||
if (body.replies_to.length > 0) {
|
||||
newStatus.inReplyToPostId =
|
||||
(await fetchFromRemote(body.replies_to[0]))?.id || null;
|
||||
}
|
||||
// If there is a reply, fetch all the reply parents and add them to the database
|
||||
if (body.replies_to.length > 0) {
|
||||
newStatus.inReplyToPostId =
|
||||
(await fetchFromRemote(body.replies_to[0]))?.id || null;
|
||||
}
|
||||
|
||||
// Same for quotes
|
||||
if (body.quotes.length > 0) {
|
||||
newStatus.quotingPostId =
|
||||
(await fetchFromRemote(body.quotes[0]))?.id || null;
|
||||
}
|
||||
// Same for quotes
|
||||
if (body.quotes.length > 0) {
|
||||
newStatus.quotingPostId =
|
||||
(await fetchFromRemote(body.quotes[0]))?.id || null;
|
||||
}
|
||||
|
||||
await client.status.update({
|
||||
where: {
|
||||
id: newStatus.id,
|
||||
},
|
||||
data: {
|
||||
inReplyToPostId: newStatus.inReplyToPostId,
|
||||
quotingPostId: newStatus.quotingPostId,
|
||||
},
|
||||
});
|
||||
await client.status.update({
|
||||
where: {
|
||||
id: newStatus.id,
|
||||
},
|
||||
data: {
|
||||
inReplyToPostId: newStatus.inReplyToPostId,
|
||||
quotingPostId: newStatus.quotingPostId,
|
||||
},
|
||||
});
|
||||
|
||||
break;
|
||||
}
|
||||
case "Patch": {
|
||||
const patch = body as Patch;
|
||||
// Store the object in the LysandObject table
|
||||
await createFromObject(patch);
|
||||
break;
|
||||
}
|
||||
case "Patch": {
|
||||
const patch = body as Patch;
|
||||
// Store the object in the LysandObject table
|
||||
await createFromObject(patch);
|
||||
|
||||
// Edit the status
|
||||
// Edit the status
|
||||
|
||||
const content = getBestContentType(patch.contents);
|
||||
const content = getBestContentType(patch.contents);
|
||||
|
||||
const emojis = await parseEmojis(content?.content || "");
|
||||
const emojis = await parseEmojis(content?.content || "");
|
||||
|
||||
const status = await client.status.findUnique({
|
||||
where: {
|
||||
uri: patch.patched_id,
|
||||
},
|
||||
include: statusAndUserRelations,
|
||||
});
|
||||
const status = await client.status.findUnique({
|
||||
where: {
|
||||
uri: patch.patched_id,
|
||||
},
|
||||
include: statusAndUserRelations,
|
||||
});
|
||||
|
||||
if (!status) {
|
||||
return errorResponse("Status not found", 404);
|
||||
}
|
||||
if (!status) {
|
||||
return errorResponse("Status not found", 404);
|
||||
}
|
||||
|
||||
status.content = content?.content || "";
|
||||
status.contentType = content?.content_type || "text/plain";
|
||||
status.spoilerText = patch.subject || "";
|
||||
status.sensitive = patch.is_sensitive;
|
||||
status.emojis = emojis;
|
||||
status.content = content?.content || "";
|
||||
status.contentType = content?.content_type || "text/plain";
|
||||
status.spoilerText = patch.subject || "";
|
||||
status.sensitive = patch.is_sensitive;
|
||||
status.emojis = emojis;
|
||||
|
||||
// If there is a reply, fetch all the reply parents and add them to the database
|
||||
if (body.replies_to.length > 0) {
|
||||
status.inReplyToPostId =
|
||||
(await fetchFromRemote(body.replies_to[0]))?.id || null;
|
||||
}
|
||||
// If there is a reply, fetch all the reply parents and add them to the database
|
||||
if (body.replies_to.length > 0) {
|
||||
status.inReplyToPostId =
|
||||
(await fetchFromRemote(body.replies_to[0]))?.id || null;
|
||||
}
|
||||
|
||||
// Same for quotes
|
||||
if (body.quotes.length > 0) {
|
||||
status.quotingPostId =
|
||||
(await fetchFromRemote(body.quotes[0]))?.id || null;
|
||||
}
|
||||
// Same for quotes
|
||||
if (body.quotes.length > 0) {
|
||||
status.quotingPostId =
|
||||
(await fetchFromRemote(body.quotes[0]))?.id || null;
|
||||
}
|
||||
|
||||
await client.status.update({
|
||||
where: {
|
||||
id: status.id,
|
||||
},
|
||||
data: {
|
||||
content: status.content,
|
||||
contentType: status.contentType,
|
||||
spoilerText: status.spoilerText,
|
||||
sensitive: status.sensitive,
|
||||
emojis: {
|
||||
connect: status.emojis.map(emoji => ({
|
||||
id: emoji.id,
|
||||
})),
|
||||
},
|
||||
inReplyToPostId: status.inReplyToPostId,
|
||||
quotingPostId: status.quotingPostId,
|
||||
},
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "Like": {
|
||||
const like = body as Like;
|
||||
// Store the object in the LysandObject table
|
||||
await createFromObject(body);
|
||||
await client.status.update({
|
||||
where: {
|
||||
id: status.id,
|
||||
},
|
||||
data: {
|
||||
content: status.content,
|
||||
contentType: status.contentType,
|
||||
spoilerText: status.spoilerText,
|
||||
sensitive: status.sensitive,
|
||||
emojis: {
|
||||
connect: status.emojis.map((emoji) => ({
|
||||
id: emoji.id,
|
||||
})),
|
||||
},
|
||||
inReplyToPostId: status.inReplyToPostId,
|
||||
quotingPostId: status.quotingPostId,
|
||||
},
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "Like": {
|
||||
const like = body as Like;
|
||||
// Store the object in the LysandObject table
|
||||
await createFromObject(body);
|
||||
|
||||
const likedStatus = await client.status.findUnique({
|
||||
where: {
|
||||
uri: like.object,
|
||||
},
|
||||
include: statusAndUserRelations,
|
||||
});
|
||||
const likedStatus = await client.status.findUnique({
|
||||
where: {
|
||||
uri: like.object,
|
||||
},
|
||||
include: statusAndUserRelations,
|
||||
});
|
||||
|
||||
if (!likedStatus) {
|
||||
return errorResponse("Status not found", 404);
|
||||
}
|
||||
if (!likedStatus) {
|
||||
return errorResponse("Status not found", 404);
|
||||
}
|
||||
|
||||
await createLike(author, likedStatus);
|
||||
await createLike(author, likedStatus);
|
||||
|
||||
break;
|
||||
}
|
||||
case "Dislike": {
|
||||
// Store the object in the LysandObject table
|
||||
await createFromObject(body);
|
||||
break;
|
||||
}
|
||||
case "Dislike": {
|
||||
// Store the object in the LysandObject table
|
||||
await createFromObject(body);
|
||||
|
||||
return jsonResponse({
|
||||
info: "Dislikes are not supported by this software",
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "Follow": {
|
||||
// Store the object in the LysandObject table
|
||||
await createFromObject(body);
|
||||
break;
|
||||
}
|
||||
case "FollowAccept": {
|
||||
// Store the object in the LysandObject table
|
||||
await createFromObject(body);
|
||||
break;
|
||||
}
|
||||
case "FollowReject": {
|
||||
// Store the object in the LysandObject table
|
||||
await createFromObject(body);
|
||||
break;
|
||||
}
|
||||
case "Announce": {
|
||||
const announce = body as Announce;
|
||||
// Store the object in the LysandObject table
|
||||
await createFromObject(body);
|
||||
return jsonResponse({
|
||||
info: "Dislikes are not supported by this software",
|
||||
});
|
||||
}
|
||||
case "Follow": {
|
||||
// Store the object in the LysandObject table
|
||||
await createFromObject(body);
|
||||
break;
|
||||
}
|
||||
case "FollowAccept": {
|
||||
// Store the object in the LysandObject table
|
||||
await createFromObject(body);
|
||||
break;
|
||||
}
|
||||
case "FollowReject": {
|
||||
// Store the object in the LysandObject table
|
||||
await createFromObject(body);
|
||||
break;
|
||||
}
|
||||
case "Announce": {
|
||||
const announce = body as Announce;
|
||||
// Store the object in the LysandObject table
|
||||
await createFromObject(body);
|
||||
|
||||
const rebloggedStatus = await client.status.findUnique({
|
||||
where: {
|
||||
uri: announce.object,
|
||||
},
|
||||
include: statusAndUserRelations,
|
||||
});
|
||||
const rebloggedStatus = await client.status.findUnique({
|
||||
where: {
|
||||
uri: announce.object,
|
||||
},
|
||||
include: statusAndUserRelations,
|
||||
});
|
||||
|
||||
if (!rebloggedStatus) {
|
||||
return errorResponse("Status not found", 404);
|
||||
}
|
||||
if (!rebloggedStatus) {
|
||||
return errorResponse("Status not found", 404);
|
||||
}
|
||||
|
||||
// Create new reblog
|
||||
await client.status.create({
|
||||
data: {
|
||||
authorId: author.id,
|
||||
reblogId: rebloggedStatus.id,
|
||||
isReblog: true,
|
||||
uri: body.uri,
|
||||
visibility: rebloggedStatus.visibility,
|
||||
sensitive: false,
|
||||
},
|
||||
include: statusAndUserRelations,
|
||||
});
|
||||
// Create new reblog
|
||||
await client.status.create({
|
||||
data: {
|
||||
authorId: author.id,
|
||||
reblogId: rebloggedStatus.id,
|
||||
isReblog: true,
|
||||
uri: body.uri,
|
||||
visibility: rebloggedStatus.visibility,
|
||||
sensitive: false,
|
||||
},
|
||||
include: statusAndUserRelations,
|
||||
});
|
||||
|
||||
// Create notification
|
||||
await client.notification.create({
|
||||
data: {
|
||||
accountId: author.id,
|
||||
notifiedId: rebloggedStatus.authorId,
|
||||
type: "reblog",
|
||||
statusId: rebloggedStatus.id,
|
||||
},
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "Undo": {
|
||||
const undo = body as Undo;
|
||||
// Store the object in the LysandObject table
|
||||
await createFromObject(body);
|
||||
// Create notification
|
||||
await client.notification.create({
|
||||
data: {
|
||||
accountId: author.id,
|
||||
notifiedId: rebloggedStatus.authorId,
|
||||
type: "reblog",
|
||||
statusId: rebloggedStatus.id,
|
||||
},
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "Undo": {
|
||||
const undo = body as Undo;
|
||||
// Store the object in the LysandObject table
|
||||
await createFromObject(body);
|
||||
|
||||
const object = await client.lysandObject.findUnique({
|
||||
where: {
|
||||
uri: undo.object,
|
||||
},
|
||||
});
|
||||
const object = await client.lysandObject.findUnique({
|
||||
where: {
|
||||
uri: undo.object,
|
||||
},
|
||||
});
|
||||
|
||||
if (!object) {
|
||||
return errorResponse("Object not found", 404);
|
||||
}
|
||||
if (!object) {
|
||||
return errorResponse("Object not found", 404);
|
||||
}
|
||||
|
||||
switch (object.type) {
|
||||
case "Like": {
|
||||
const status = await client.status.findUnique({
|
||||
where: {
|
||||
uri: undo.object,
|
||||
authorId: author.id,
|
||||
},
|
||||
include: statusAndUserRelations,
|
||||
});
|
||||
switch (object.type) {
|
||||
case "Like": {
|
||||
const status = await client.status.findUnique({
|
||||
where: {
|
||||
uri: undo.object,
|
||||
authorId: author.id,
|
||||
},
|
||||
include: statusAndUserRelations,
|
||||
});
|
||||
|
||||
if (!status) {
|
||||
return errorResponse("Status not found", 404);
|
||||
}
|
||||
if (!status) {
|
||||
return errorResponse("Status not found", 404);
|
||||
}
|
||||
|
||||
await deleteLike(author, status);
|
||||
break;
|
||||
}
|
||||
case "Announce": {
|
||||
await client.status.delete({
|
||||
where: {
|
||||
uri: undo.object,
|
||||
authorId: author.id,
|
||||
},
|
||||
include: statusAndUserRelations,
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "Note": {
|
||||
await client.status.delete({
|
||||
where: {
|
||||
uri: undo.object,
|
||||
authorId: author.id,
|
||||
},
|
||||
include: statusAndUserRelations,
|
||||
});
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
return errorResponse("Invalid object type", 400);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "Extension": {
|
||||
// Store the object in the LysandObject table
|
||||
await createFromObject(body);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
return errorResponse("Invalid type", 400);
|
||||
}
|
||||
}
|
||||
await deleteLike(author, status);
|
||||
break;
|
||||
}
|
||||
case "Announce": {
|
||||
await client.status.delete({
|
||||
where: {
|
||||
uri: undo.object,
|
||||
authorId: author.id,
|
||||
},
|
||||
include: statusAndUserRelations,
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "Note": {
|
||||
await client.status.delete({
|
||||
where: {
|
||||
uri: undo.object,
|
||||
authorId: author.id,
|
||||
},
|
||||
include: statusAndUserRelations,
|
||||
});
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
return errorResponse("Invalid object type", 400);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "Extension": {
|
||||
// Store the object in the LysandObject table
|
||||
await createFromObject(body);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
return errorResponse("Invalid type", 400);
|
||||
}
|
||||
}
|
||||
|
||||
return jsonResponse({});
|
||||
return jsonResponse({});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -5,33 +5,33 @@ import { userToLysand } from "~database/entities/User";
|
|||
import { userRelations } from "~database/entities/relations";
|
||||
|
||||
export const meta = applyConfig({
|
||||
allowedMethods: ["POST"],
|
||||
auth: {
|
||||
required: false,
|
||||
},
|
||||
ratelimits: {
|
||||
duration: 60,
|
||||
max: 500,
|
||||
},
|
||||
route: "/users/:uuid",
|
||||
allowedMethods: ["POST"],
|
||||
auth: {
|
||||
required: false,
|
||||
},
|
||||
ratelimits: {
|
||||
duration: 60,
|
||||
max: 500,
|
||||
},
|
||||
route: "/users/:uuid",
|
||||
});
|
||||
|
||||
/**
|
||||
* ActivityPub user inbox endpoint
|
||||
*/
|
||||
export default apiRoute(async (req, matchedRoute) => {
|
||||
const uuid = matchedRoute.params.uuid;
|
||||
const uuid = matchedRoute.params.uuid;
|
||||
|
||||
const user = await client.user.findUnique({
|
||||
where: {
|
||||
id: uuid,
|
||||
},
|
||||
include: userRelations,
|
||||
});
|
||||
const user = await client.user.findUnique({
|
||||
where: {
|
||||
id: uuid,
|
||||
},
|
||||
include: userRelations,
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return errorResponse("User not found", 404);
|
||||
}
|
||||
if (!user) {
|
||||
return errorResponse("User not found", 404);
|
||||
}
|
||||
|
||||
return jsonResponse(userToLysand(user));
|
||||
return jsonResponse(userToLysand(user));
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,65 +1,65 @@
|
|||
import { jsonResponse } from "@response";
|
||||
import { apiRoute, applyConfig } from "@api";
|
||||
import { statusToLysand } from "~database/entities/Status";
|
||||
import { jsonResponse } from "@response";
|
||||
import { client } from "~database/datasource";
|
||||
import { statusToLysand } from "~database/entities/Status";
|
||||
import { statusAndUserRelations } from "~database/entities/relations";
|
||||
|
||||
export const meta = applyConfig({
|
||||
allowedMethods: ["GET"],
|
||||
auth: {
|
||||
required: false,
|
||||
},
|
||||
ratelimits: {
|
||||
duration: 60,
|
||||
max: 500,
|
||||
},
|
||||
route: "/users/:uuid/outbox",
|
||||
allowedMethods: ["GET"],
|
||||
auth: {
|
||||
required: false,
|
||||
},
|
||||
ratelimits: {
|
||||
duration: 60,
|
||||
max: 500,
|
||||
},
|
||||
route: "/users/:uuid/outbox",
|
||||
});
|
||||
|
||||
/**
|
||||
* ActivityPub user outbox endpoint
|
||||
*/
|
||||
export default apiRoute(async (req, matchedRoute, extraData) => {
|
||||
const uuid = matchedRoute.params.uuid;
|
||||
const pageNumber = Number(matchedRoute.query.page) || 1;
|
||||
const config = await extraData.configManager.getConfig();
|
||||
const host = new URL(config.http.base_url).hostname;
|
||||
const uuid = matchedRoute.params.uuid;
|
||||
const pageNumber = Number(matchedRoute.query.page) || 1;
|
||||
const config = await extraData.configManager.getConfig();
|
||||
const host = new URL(config.http.base_url).hostname;
|
||||
|
||||
const statuses = await client.status.findMany({
|
||||
where: {
|
||||
authorId: uuid,
|
||||
visibility: {
|
||||
in: ["public", "unlisted"],
|
||||
},
|
||||
},
|
||||
take: 20,
|
||||
skip: 20 * (pageNumber - 1),
|
||||
include: statusAndUserRelations,
|
||||
});
|
||||
const statuses = await client.status.findMany({
|
||||
where: {
|
||||
authorId: uuid,
|
||||
visibility: {
|
||||
in: ["public", "unlisted"],
|
||||
},
|
||||
},
|
||||
take: 20,
|
||||
skip: 20 * (pageNumber - 1),
|
||||
include: statusAndUserRelations,
|
||||
});
|
||||
|
||||
const totalStatuses = await client.status.count({
|
||||
where: {
|
||||
authorId: uuid,
|
||||
visibility: {
|
||||
in: ["public", "unlisted"],
|
||||
},
|
||||
},
|
||||
});
|
||||
const totalStatuses = await client.status.count({
|
||||
where: {
|
||||
authorId: uuid,
|
||||
visibility: {
|
||||
in: ["public", "unlisted"],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return jsonResponse({
|
||||
first: `${host}/users/${uuid}/outbox?page=1`,
|
||||
last: `${host}/users/${uuid}/outbox?page=1`,
|
||||
total_items: totalStatuses,
|
||||
// Server actor
|
||||
author: `${config.http.base_url}/users/actor`,
|
||||
next:
|
||||
statuses.length === 20
|
||||
? `${host}/users/${uuid}/outbox?page=${pageNumber + 1}`
|
||||
: undefined,
|
||||
prev:
|
||||
pageNumber > 1
|
||||
? `${host}/users/${uuid}/outbox?page=${pageNumber - 1}`
|
||||
: undefined,
|
||||
items: statuses.map(s => statusToLysand(s)),
|
||||
});
|
||||
return jsonResponse({
|
||||
first: `${host}/users/${uuid}/outbox?page=1`,
|
||||
last: `${host}/users/${uuid}/outbox?page=1`,
|
||||
total_items: totalStatuses,
|
||||
// Server actor
|
||||
author: `${config.http.base_url}/users/actor`,
|
||||
next:
|
||||
statuses.length === 20
|
||||
? `${host}/users/${uuid}/outbox?page=${pageNumber + 1}`
|
||||
: undefined,
|
||||
prev:
|
||||
pageNumber > 1
|
||||
? `${host}/users/${uuid}/outbox?page=${pageNumber - 1}`
|
||||
: undefined,
|
||||
items: statuses.map((s) => statusToLysand(s)),
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue