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.
|
||||
* @returns The API-friendly relationship.
|
||||
|
|
|
|||
|
|
@ -55,7 +55,9 @@ export const statusAndUserRelations: Prisma.StatusInclude = {
|
|||
},
|
||||
attachments: true,
|
||||
instance: true,
|
||||
mentions: true,
|
||||
mentions: {
|
||||
include: userRelations,
|
||||
},
|
||||
pinnedBy: true,
|
||||
_count: {
|
||||
select: {
|
||||
|
|
@ -307,12 +309,9 @@ export const createNewStatus = async (data: {
|
|||
};
|
||||
quote?: Status;
|
||||
}) => {
|
||||
// Get people mentioned in the content
|
||||
const mentionedPeople = [...data.content.matchAll(/@([a-zA-Z0-9_]+)/g)].map(
|
||||
match => {
|
||||
return `${config.http.base_url}/users/${match[1]}`;
|
||||
}
|
||||
);
|
||||
// Get people mentioned in the content (match @username or @username@domain.com mentions)
|
||||
const mentionedPeople =
|
||||
data.content.match(/@[a-zA-Z0-9_]+(@[a-zA-Z0-9_]+)?/g) ?? [];
|
||||
|
||||
let mentions = data.mentions || [];
|
||||
|
||||
|
|
@ -438,7 +437,8 @@ export const statusToAPI = async (
|
|||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
favourites_count: (status.likes ?? []).length,
|
||||
media_attachments: [],
|
||||
mentions: [],
|
||||
// @ts-expect-error Prisma TypeScript types dont include relations
|
||||
mentions: status.mentions.map(mention => userToAPI(mention)),
|
||||
language: null,
|
||||
muted: user
|
||||
? 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,12 +50,14 @@ export default async (req: Request): Promise<Response> => {
|
|||
gte: since_id ?? undefined,
|
||||
gt: min_id ?? undefined,
|
||||
},
|
||||
OR: [
|
||||
{
|
||||
author: {
|
||||
OR: [
|
||||
{
|
||||
relationships: {
|
||||
relationshipSubjects: {
|
||||
some: {
|
||||
subjectId: user.id,
|
||||
ownerId: user.id,
|
||||
following: true,
|
||||
},
|
||||
},
|
||||
|
|
@ -66,6 +68,16 @@ export default async (req: Request): Promise<Response> => {
|
|||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
// Include posts where the user is mentioned in addition to posts by followed users
|
||||
mentions: {
|
||||
some: {
|
||||
id: user.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
include: statusAndUserRelations,
|
||||
take: limit,
|
||||
orderBy: {
|
||||
|
|
|
|||
Loading…
Reference in a new issue