mirror of
https://github.com/versia-pub/server.git
synced 2025-12-06 16:38:19 +01:00
Fix timeline rendering
This commit is contained in:
parent
73cb7db6b3
commit
440e994576
|
|
@ -37,6 +37,33 @@ export const createNewRelationship = async (
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const checkForBidirectionalRelationships = async (
|
||||||
|
user1: User,
|
||||||
|
user2: User,
|
||||||
|
createIfNotExists = true
|
||||||
|
): Promise<boolean> => {
|
||||||
|
const relationship1 = await client.relationship.findFirst({
|
||||||
|
where: {
|
||||||
|
ownerId: user1.id,
|
||||||
|
subjectId: user2.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const relationship2 = await client.relationship.findFirst({
|
||||||
|
where: {
|
||||||
|
ownerId: user2.id,
|
||||||
|
subjectId: user1.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!relationship1 && !relationship2 && createIfNotExists) {
|
||||||
|
await createNewRelationship(user1, user2);
|
||||||
|
await createNewRelationship(user2, user1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return !!relationship1 && !!relationship2;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Converts the relationship to an API-friendly format.
|
* Converts the relationship to an API-friendly format.
|
||||||
* @returns The API-friendly relationship.
|
* @returns The API-friendly relationship.
|
||||||
|
|
|
||||||
|
|
@ -55,7 +55,9 @@ export const statusAndUserRelations: Prisma.StatusInclude = {
|
||||||
},
|
},
|
||||||
attachments: true,
|
attachments: true,
|
||||||
instance: true,
|
instance: true,
|
||||||
mentions: true,
|
mentions: {
|
||||||
|
include: userRelations,
|
||||||
|
},
|
||||||
pinnedBy: true,
|
pinnedBy: true,
|
||||||
_count: {
|
_count: {
|
||||||
select: {
|
select: {
|
||||||
|
|
@ -307,12 +309,9 @@ export const createNewStatus = async (data: {
|
||||||
};
|
};
|
||||||
quote?: Status;
|
quote?: Status;
|
||||||
}) => {
|
}) => {
|
||||||
// Get people mentioned in the content
|
// Get people mentioned in the content (match @username or @username@domain.com mentions)
|
||||||
const mentionedPeople = [...data.content.matchAll(/@([a-zA-Z0-9_]+)/g)].map(
|
const mentionedPeople =
|
||||||
match => {
|
data.content.match(/@[a-zA-Z0-9_]+(@[a-zA-Z0-9_]+)?/g) ?? [];
|
||||||
return `${config.http.base_url}/users/${match[1]}`;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
let mentions = data.mentions || [];
|
let mentions = data.mentions || [];
|
||||||
|
|
||||||
|
|
@ -438,7 +437,8 @@ export const statusToAPI = async (
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||||
favourites_count: (status.likes ?? []).length,
|
favourites_count: (status.likes ?? []).length,
|
||||||
media_attachments: [],
|
media_attachments: [],
|
||||||
mentions: [],
|
// @ts-expect-error Prisma TypeScript types dont include relations
|
||||||
|
mentions: status.mentions.map(mention => userToAPI(mention)),
|
||||||
language: null,
|
language: null,
|
||||||
muted: user
|
muted: user
|
||||||
? user.relationships.find(r => r.subjectId == status.authorId)
|
? user.relationships.find(r => r.subjectId == status.authorId)
|
||||||
|
|
|
||||||
72
server/api/api/v1/accounts/search/index.ts
Normal file
72
server/api/api/v1/accounts/search/index.ts
Normal file
|
|
@ -0,0 +1,72 @@
|
||||||
|
import { errorResponse, jsonResponse } from "@response";
|
||||||
|
import {
|
||||||
|
getFromRequest,
|
||||||
|
userRelations,
|
||||||
|
userToAPI,
|
||||||
|
} from "~database/entities/User";
|
||||||
|
import { applyConfig } from "@api";
|
||||||
|
import { parseRequest } from "@request";
|
||||||
|
import { client } from "~database/datasource";
|
||||||
|
|
||||||
|
export const meta = applyConfig({
|
||||||
|
allowedMethods: ["GET"],
|
||||||
|
route: "/api/v1/accounts/search",
|
||||||
|
ratelimits: {
|
||||||
|
max: 100,
|
||||||
|
duration: 60,
|
||||||
|
},
|
||||||
|
auth: {
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default async (req: Request): Promise<Response> => {
|
||||||
|
// TODO: Add checks for disabled or not email verified accounts
|
||||||
|
|
||||||
|
const { user } = await getFromRequest(req);
|
||||||
|
|
||||||
|
if (!user) return errorResponse("Unauthorized", 401);
|
||||||
|
|
||||||
|
const {
|
||||||
|
following = false,
|
||||||
|
limit = 40,
|
||||||
|
offset,
|
||||||
|
q,
|
||||||
|
} = await parseRequest<{
|
||||||
|
q?: string;
|
||||||
|
limit?: number;
|
||||||
|
offset?: number;
|
||||||
|
resolve?: boolean;
|
||||||
|
following?: boolean;
|
||||||
|
}>(req);
|
||||||
|
|
||||||
|
if (limit < 1 || limit > 80) {
|
||||||
|
return errorResponse("Limit must be between 1 and 80", 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Add WebFinger resolve
|
||||||
|
|
||||||
|
const accounts = await client.user.findMany({
|
||||||
|
where: {
|
||||||
|
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)));
|
||||||
|
};
|
||||||
79
server/api/api/v1/follow_requests/[account_id]/authorize.ts
Normal file
79
server/api/api/v1/follow_requests/[account_id]/authorize.ts
Normal file
|
|
@ -0,0 +1,79 @@
|
||||||
|
import { errorResponse, jsonResponse } from "@response";
|
||||||
|
import { getFromRequest, userRelations } from "~database/entities/User";
|
||||||
|
import { applyConfig } from "@api";
|
||||||
|
import { client } from "~database/datasource";
|
||||||
|
import type { MatchedRoute } from "bun";
|
||||||
|
import {
|
||||||
|
checkForBidirectionalRelationships,
|
||||||
|
relationshipToAPI,
|
||||||
|
} from "~database/entities/Relationship";
|
||||||
|
|
||||||
|
export const meta = applyConfig({
|
||||||
|
allowedMethods: ["POST"],
|
||||||
|
route: "/api/v1/follow_requests/:account_id/authorize",
|
||||||
|
ratelimits: {
|
||||||
|
max: 100,
|
||||||
|
duration: 60,
|
||||||
|
},
|
||||||
|
auth: {
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default async (
|
||||||
|
req: Request,
|
||||||
|
matchedRoute: MatchedRoute
|
||||||
|
): Promise<Response> => {
|
||||||
|
const { user } = await getFromRequest(req);
|
||||||
|
|
||||||
|
if (!user) return errorResponse("Unauthorized", 401);
|
||||||
|
|
||||||
|
const { account_id } = matchedRoute.params;
|
||||||
|
|
||||||
|
const account = await client.user.findUnique({
|
||||||
|
where: {
|
||||||
|
id: account_id,
|
||||||
|
},
|
||||||
|
include: userRelations,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!account) return errorResponse("Account not found", 404);
|
||||||
|
|
||||||
|
// 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,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// 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,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!relationship) return errorResponse("Relationship not found", 404);
|
||||||
|
|
||||||
|
return jsonResponse(relationshipToAPI(relationship));
|
||||||
|
};
|
||||||
67
server/api/api/v1/follow_requests/[account_id]/reject.ts
Normal file
67
server/api/api/v1/follow_requests/[account_id]/reject.ts
Normal file
|
|
@ -0,0 +1,67 @@
|
||||||
|
import { errorResponse, jsonResponse } from "@response";
|
||||||
|
import { getFromRequest, userRelations } from "~database/entities/User";
|
||||||
|
import { applyConfig } from "@api";
|
||||||
|
import { client } from "~database/datasource";
|
||||||
|
import type { MatchedRoute } from "bun";
|
||||||
|
import {
|
||||||
|
checkForBidirectionalRelationships,
|
||||||
|
relationshipToAPI,
|
||||||
|
} from "~database/entities/Relationship";
|
||||||
|
|
||||||
|
export const meta = applyConfig({
|
||||||
|
allowedMethods: ["POST"],
|
||||||
|
route: "/api/v1/follow_requests/:account_id/reject",
|
||||||
|
ratelimits: {
|
||||||
|
max: 100,
|
||||||
|
duration: 60,
|
||||||
|
},
|
||||||
|
auth: {
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default async (
|
||||||
|
req: Request,
|
||||||
|
matchedRoute: MatchedRoute
|
||||||
|
): Promise<Response> => {
|
||||||
|
const { user } = await getFromRequest(req);
|
||||||
|
|
||||||
|
if (!user) return errorResponse("Unauthorized", 401);
|
||||||
|
|
||||||
|
const { account_id } = matchedRoute.params;
|
||||||
|
|
||||||
|
const account = await client.user.findUnique({
|
||||||
|
where: {
|
||||||
|
id: account_id,
|
||||||
|
},
|
||||||
|
include: userRelations,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!account) return errorResponse("Account not found", 404);
|
||||||
|
|
||||||
|
// 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,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const relationship = await client.relationship.findFirst({
|
||||||
|
where: {
|
||||||
|
subjectId: account.id,
|
||||||
|
ownerId: user.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!relationship) return errorResponse("Relationship not found", 404);
|
||||||
|
|
||||||
|
return jsonResponse(relationshipToAPI(relationship));
|
||||||
|
};
|
||||||
82
server/api/api/v1/follow_requests/index.ts
Normal file
82
server/api/api/v1/follow_requests/index.ts
Normal file
|
|
@ -0,0 +1,82 @@
|
||||||
|
import { errorResponse, jsonResponse } from "@response";
|
||||||
|
import {
|
||||||
|
getFromRequest,
|
||||||
|
userRelations,
|
||||||
|
userToAPI,
|
||||||
|
} from "~database/entities/User";
|
||||||
|
import { applyConfig } from "@api";
|
||||||
|
import { client } from "~database/datasource";
|
||||||
|
import { parseRequest } from "@request";
|
||||||
|
|
||||||
|
export const meta = applyConfig({
|
||||||
|
allowedMethods: ["GET"],
|
||||||
|
route: "/api/v1/follow_requests",
|
||||||
|
ratelimits: {
|
||||||
|
max: 100,
|
||||||
|
duration: 60,
|
||||||
|
},
|
||||||
|
auth: {
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default async (req: Request): Promise<Response> => {
|
||||||
|
const { user } = await getFromRequest(req);
|
||||||
|
|
||||||
|
const {
|
||||||
|
limit = 20,
|
||||||
|
max_id,
|
||||||
|
min_id,
|
||||||
|
since_id,
|
||||||
|
} = await parseRequest<{
|
||||||
|
max_id?: string;
|
||||||
|
since_id?: string;
|
||||||
|
min_id?: string;
|
||||||
|
limit?: number;
|
||||||
|
}>(req);
|
||||||
|
|
||||||
|
if (limit < 1 || limit > 40) {
|
||||||
|
return errorResponse("Limit must be between 1 and 40", 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
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",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// 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(", "),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -50,21 +50,33 @@ export default async (req: Request): Promise<Response> => {
|
||||||
gte: since_id ?? undefined,
|
gte: since_id ?? undefined,
|
||||||
gt: min_id ?? undefined,
|
gt: min_id ?? undefined,
|
||||||
},
|
},
|
||||||
author: {
|
OR: [
|
||||||
OR: [
|
{
|
||||||
{
|
author: {
|
||||||
relationships: {
|
OR: [
|
||||||
some: {
|
{
|
||||||
subjectId: user.id,
|
relationshipSubjects: {
|
||||||
following: true,
|
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,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
},
|
||||||
id: user.id,
|
],
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
include: statusAndUserRelations,
|
include: statusAndUserRelations,
|
||||||
take: limit,
|
take: limit,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue