Fix timeline rendering

This commit is contained in:
Jesse Wierzbinski 2023-11-28 12:57:48 -10:00
parent 73cb7db6b3
commit 440e994576
No known key found for this signature in database
7 changed files with 359 additions and 20 deletions

View file

@ -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.

View file

@ -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)

View 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)));
};

View 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));
};

View 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));
};

View 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(", "),
}
);
};

View file

@ -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,