Finish rewrite of everything with Prisma

This commit is contained in:
Jesse Wierzbinski 2023-11-11 15:37:14 -10:00
parent 5eed8374cd
commit dc0ec47543
No known key found for this signature in database
GPG key ID: F9A1E418934E40B0
47 changed files with 1283 additions and 1036 deletions

View file

@ -1,8 +1,15 @@
import { errorResponse, jsonResponse } from "@response";
import { MatchedRoute } from "bun";
import { Relationship } from "~database/entities/Relationship";
import { UserAction, userRelations } from "~database/entities/User";
import {
createNewRelationship,
relationshipToAPI,
} from "~database/entities/Relationship";
import {
getFromRequest,
getRelationshipToOtherUser,
} from "~database/entities/User";
import { applyConfig } from "@api";
import { client } from "~database/datasource";
export const meta = applyConfig({
allowedMethods: ["POST"],
@ -25,29 +32,42 @@ export default async (
): Promise<Response> => {
const id = matchedRoute.params.id;
const { user: self } = await UserAction.getFromRequest(req);
const { user: self } = await getFromRequest(req);
if (!self) return errorResponse("Unauthorized", 401);
const user = await UserAction.findOne({
where: {
id,
const user = await client.user.findUnique({
where: { id },
include: {
relationships: {
include: {
owner: true,
subject: true,
},
},
},
relations: userRelations,
});
if (!user) return errorResponse("User not found", 404);
// Check if already following
let relationship = await self.getRelationshipToOtherUser(user);
let relationship = await getRelationshipToOtherUser(self, user);
if (!relationship) {
// Create new relationship
const newRelationship = await Relationship.createNew(self, user);
const newRelationship = await createNewRelationship(self, user);
self.relationships.push(newRelationship);
await self.save();
await client.user.update({
where: { id: self.id },
data: {
relationships: {
connect: {
id: newRelationship.id,
},
},
},
});
relationship = newRelationship;
}
@ -56,6 +76,12 @@ export default async (
relationship.blocking = true;
}
await relationship.save();
return jsonResponse(await relationship.toAPI());
await client.relationship.update({
where: { id: relationship.id },
data: {
blocking: true,
},
});
return jsonResponse(await relationshipToAPI(relationship));
};

View file

@ -1,9 +1,16 @@
import { parseRequest } from "@request";
import { errorResponse, jsonResponse } from "@response";
import { MatchedRoute } from "bun";
import { Relationship } from "~database/entities/Relationship";
import { UserAction, userRelations } from "~database/entities/User";
import {
createNewRelationship,
relationshipToAPI,
} from "~database/entities/Relationship";
import {
getFromRequest,
getRelationshipToOtherUser,
} from "~database/entities/User";
import { applyConfig } from "@api";
import { client } from "~database/datasource";
export const meta = applyConfig({
allowedMethods: ["POST"],
@ -26,7 +33,7 @@ export default async (
): Promise<Response> => {
const id = matchedRoute.params.id;
const { user: self } = await UserAction.getFromRequest(req);
const { user: self } = await getFromRequest(req);
if (!self) return errorResponse("Unauthorized", 401);
@ -36,25 +43,38 @@ export default async (
languages?: string[];
}>(req);
const user = await UserAction.findOne({
where: {
id,
const user = await client.user.findUnique({
where: { id },
include: {
relationships: {
include: {
owner: true,
subject: true,
},
},
},
relations: userRelations,
});
if (!user) return errorResponse("User not found", 404);
// Check if already following
let relationship = await self.getRelationshipToOtherUser(user);
let relationship = await getRelationshipToOtherUser(self, user);
if (!relationship) {
// Create new relationship
const newRelationship = await Relationship.createNew(self, user);
const newRelationship = await createNewRelationship(self, user);
self.relationships.push(newRelationship);
await self.save();
await client.user.update({
where: { id: self.id },
data: {
relationships: {
connect: {
id: newRelationship.id,
},
},
},
});
relationship = newRelationship;
}
@ -63,7 +83,7 @@ export default async (
relationship.following = true;
}
if (reblogs) {
relationship.showing_reblogs = true;
relationship.showingReblogs = true;
}
if (notify) {
relationship.notifying = true;
@ -72,6 +92,15 @@ export default async (
relationship.languages = languages;
}
await relationship.save();
return jsonResponse(await relationship.toAPI());
await client.relationship.update({
where: { id: relationship.id },
data: {
following: true,
showingReblogs: reblogs ?? false,
notifying: notify ?? false,
languages: languages ?? [],
},
});
return jsonResponse(relationshipToAPI(relationship));
};

View file

@ -1,7 +1,13 @@
import { errorResponse, jsonResponse } from "@response";
import { MatchedRoute } from "bun";
import { UserAction, userRelations } from "~database/entities/User";
import {
UserWithRelations,
getFromRequest,
userRelations,
userToAPI,
} from "~database/entities/User";
import { applyConfig } from "@api";
import { client } from "~database/datasource";
export const meta = applyConfig({
allowedMethods: ["GET"],
@ -24,15 +30,13 @@ export default async (
): Promise<Response> => {
const id = matchedRoute.params.id;
const { user } = await UserAction.getFromRequest(req);
const { user } = await getFromRequest(req);
let foundUser: UserAction | null;
let foundUser: UserWithRelations | null;
try {
foundUser = await UserAction.findOne({
where: {
id,
},
relations: userRelations,
foundUser = await client.user.findUnique({
where: { id },
include: userRelations,
});
} catch (e) {
return errorResponse("Invalid ID", 404);
@ -40,5 +44,5 @@ export default async (
if (!foundUser) return errorResponse("User not found", 404);
return jsonResponse(await foundUser.toAPI(user?.id === foundUser.id));
return jsonResponse(await userToAPI(foundUser, user?.id === foundUser.id));
};

View file

@ -1,9 +1,16 @@
import { parseRequest } from "@request";
import { errorResponse, jsonResponse } from "@response";
import { MatchedRoute } from "bun";
import { Relationship } from "~database/entities/Relationship";
import { UserAction, userRelations } from "~database/entities/User";
import {
createNewRelationship,
relationshipToAPI,
} from "~database/entities/Relationship";
import {
getFromRequest,
getRelationshipToOtherUser,
} from "~database/entities/User";
import { applyConfig } from "@api";
import { client } from "~database/datasource";
export const meta = applyConfig({
allowedMethods: ["POST"],
@ -26,7 +33,7 @@ export default async (
): Promise<Response> => {
const id = matchedRoute.params.id;
const { user: self } = await UserAction.getFromRequest(req);
const { user: self } = await getFromRequest(req);
if (!self) return errorResponse("Unauthorized", 401);
@ -36,25 +43,38 @@ export default async (
duration: number;
}>(req);
const user = await UserAction.findOne({
where: {
id,
const user = await client.user.findUnique({
where: { id },
include: {
relationships: {
include: {
owner: true,
subject: true,
},
},
},
relations: userRelations,
});
if (!user) return errorResponse("User not found", 404);
// Check if already following
let relationship = await self.getRelationshipToOtherUser(user);
let relationship = await getRelationshipToOtherUser(self, user);
if (!relationship) {
// Create new relationship
const newRelationship = await Relationship.createNew(self, user);
const newRelationship = await createNewRelationship(self, user);
self.relationships.push(newRelationship);
await self.save();
await client.user.update({
where: { id: self.id },
data: {
relationships: {
connect: {
id: newRelationship.id,
},
},
},
});
relationship = newRelationship;
}
@ -63,11 +83,18 @@ export default async (
relationship.muting = true;
}
if (notifications ?? true) {
relationship.muting_notifications = true;
relationship.mutingNotifications = true;
}
await client.relationship.update({
where: { id: relationship.id },
data: {
muting: true,
mutingNotifications: notifications ?? true,
},
});
// TODO: Implement duration
await relationship.save();
return jsonResponse(await relationship.toAPI());
return jsonResponse(await relationshipToAPI(relationship));
};

View file

@ -1,9 +1,16 @@
import { parseRequest } from "@request";
import { errorResponse, jsonResponse } from "@response";
import { MatchedRoute } from "bun";
import { Relationship } from "~database/entities/Relationship";
import { UserAction, userRelations } from "~database/entities/User";
import {
createNewRelationship,
relationshipToAPI,
} from "~database/entities/Relationship";
import {
getFromRequest,
getRelationshipToOtherUser,
} from "~database/entities/User";
import { applyConfig } from "@api";
import { client } from "~database/datasource";
export const meta = applyConfig({
allowedMethods: ["POST"],
@ -26,7 +33,7 @@ export default async (
): Promise<Response> => {
const id = matchedRoute.params.id;
const { user: self } = await UserAction.getFromRequest(req);
const { user: self } = await getFromRequest(req);
if (!self) return errorResponse("Unauthorized", 401);
@ -34,31 +41,50 @@ export default async (
comment: string;
}>(req);
const user = await UserAction.findOne({
where: {
id,
const user = await client.user.findUnique({
where: { id },
include: {
relationships: {
include: {
owner: true,
subject: true,
},
},
},
relations: userRelations,
});
if (!user) return errorResponse("User not found", 404);
// Check if already following
let relationship = await self.getRelationshipToOtherUser(user);
let relationship = await getRelationshipToOtherUser(self, user);
if (!relationship) {
// Create new relationship
const newRelationship = await Relationship.createNew(self, user);
const newRelationship = await createNewRelationship(self, user);
self.relationships.push(newRelationship);
await self.save();
await client.user.update({
where: { id: self.id },
data: {
relationships: {
connect: {
id: newRelationship.id,
},
},
},
});
relationship = newRelationship;
}
relationship.note = comment ?? "";
await relationship.save();
return jsonResponse(await relationship.toAPI());
await client.relationship.update({
where: { id: relationship.id },
data: {
note: relationship.note,
},
});
return jsonResponse(await relationshipToAPI(relationship));
};

View file

@ -1,8 +1,15 @@
import { errorResponse, jsonResponse } from "@response";
import { MatchedRoute } from "bun";
import { Relationship } from "~database/entities/Relationship";
import { UserAction, userRelations } from "~database/entities/User";
import {
createNewRelationship,
relationshipToAPI,
} from "~database/entities/Relationship";
import {
getFromRequest,
getRelationshipToOtherUser,
} from "~database/entities/User";
import { applyConfig } from "@api";
import { client } from "~database/datasource";
export const meta = applyConfig({
allowedMethods: ["POST"],
@ -25,29 +32,42 @@ export default async (
): Promise<Response> => {
const id = matchedRoute.params.id;
const { user: self } = await UserAction.getFromRequest(req);
const { user: self } = await getFromRequest(req);
if (!self) return errorResponse("Unauthorized", 401);
const user = await UserAction.findOne({
where: {
id,
const user = await client.user.findUnique({
where: { id },
include: {
relationships: {
include: {
owner: true,
subject: true,
},
},
},
relations: userRelations,
});
if (!user) return errorResponse("User not found", 404);
// Check if already following
let relationship = await self.getRelationshipToOtherUser(user);
let relationship = await getRelationshipToOtherUser(self, user);
if (!relationship) {
// Create new relationship
const newRelationship = await Relationship.createNew(self, user);
const newRelationship = await createNewRelationship(self, user);
self.relationships.push(newRelationship);
await self.save();
await client.user.update({
where: { id: self.id },
data: {
relationships: {
connect: {
id: newRelationship.id,
},
},
},
});
relationship = newRelationship;
}
@ -56,6 +76,12 @@ export default async (
relationship.endorsed = true;
}
await relationship.save();
return jsonResponse(await relationship.toAPI());
await client.relationship.update({
where: { id: relationship.id },
data: {
endorsed: true,
},
});
return jsonResponse(await relationshipToAPI(relationship));
};

View file

@ -1,8 +1,15 @@
import { errorResponse, jsonResponse } from "@response";
import { MatchedRoute } from "bun";
import { Relationship } from "~database/entities/Relationship";
import { UserAction, userRelations } from "~database/entities/User";
import {
createNewRelationship,
relationshipToAPI,
} from "~database/entities/Relationship";
import {
getFromRequest,
getRelationshipToOtherUser,
} from "~database/entities/User";
import { applyConfig } from "@api";
import { client } from "~database/datasource";
export const meta = applyConfig({
allowedMethods: ["POST"],
@ -25,37 +32,71 @@ export default async (
): Promise<Response> => {
const id = matchedRoute.params.id;
const { user: self } = await UserAction.getFromRequest(req);
const { user: self } = await getFromRequest(req);
if (!self) return errorResponse("Unauthorized", 401);
const user = await UserAction.findOne({
where: {
id,
const user = await client.user.findUnique({
where: { id },
include: {
relationships: {
include: {
owner: true,
subject: true,
},
},
},
relations: userRelations,
});
if (!user) return errorResponse("User not found", 404);
// Check if already following
let relationship = await self.getRelationshipToOtherUser(user);
let relationship = await getRelationshipToOtherUser(self, user);
if (!relationship) {
// Create new relationship
const newRelationship = await Relationship.createNew(self, user);
const newRelationship = await createNewRelationship(self, user);
self.relationships.push(newRelationship);
await self.save();
await client.user.update({
where: { id: self.id },
data: {
relationships: {
connect: {
id: newRelationship.id,
},
},
},
});
relationship = newRelationship;
}
if (relationship.followed_by) {
relationship.followed_by = false;
if (relationship.followedBy) {
relationship.followedBy = false;
}
await relationship.save();
return jsonResponse(await relationship.toAPI());
await client.relationship.update({
where: { id: relationship.id },
data: {
followedBy: false,
},
});
if (user.instanceId === null) {
// Also remove from followers list
await client.relationship.update({
// @ts-expect-error Idk why there's this error
where: {
ownerId: user.id,
subjectId: self.id,
following: true,
},
data: {
following: false,
},
});
}
return jsonResponse(await relationshipToAPI(relationship));
};

View file

@ -1,10 +1,10 @@
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
import { errorResponse, jsonResponse } from "@response";
import { MatchedRoute } from "bun";
import { Status, statusAndUserRelations } from "~database/entities/Status";
import { UserAction, userRelations } from "~database/entities/User";
import { statusAndUserRelations, statusToAPI } from "~database/entities/Status";
import { userRelations } from "~database/entities/User";
import { applyConfig } from "@api";
import { FindManyOptions } from "typeorm";
import { client } from "~database/datasource";
export const meta = applyConfig({
allowedMethods: ["GET"],
@ -47,79 +47,29 @@ export default async (
tagged?: string;
} = matchedRoute.query;
const user = await UserAction.findOne({
where: {
id,
},
relations: userRelations,
const user = await client.user.findUnique({
where: { id },
include: userRelations,
});
if (!user) return errorResponse("User not found", 404);
// Get list of boosts for this status
let query: FindManyOptions<Status> = {
const objects = await client.status.findMany({
where: {
account: {
id: user.id,
},
authorId: id,
isReblog: exclude_reblogs ? true : undefined,
id: {
lt: max_id,
gt: min_id,
gte: since_id,
},
},
relations: statusAndUserRelations,
include: statusAndUserRelations,
take: limit ?? 20,
order: {
id: "DESC",
orderBy: {
id: "desc",
},
};
if (max_id) {
const maxStatus = await Status.findOneBy({ id: max_id });
if (maxStatus) {
query = {
...query,
where: {
...query.where,
created_at: {
...(query.where as any)?.created_at,
$lt: maxStatus.created_at,
},
},
};
}
}
if (since_id) {
const sinceStatus = await Status.findOneBy({ id: since_id });
if (sinceStatus) {
query = {
...query,
where: {
...query.where,
created_at: {
...(query.where as any)?.created_at,
$gt: sinceStatus.created_at,
},
},
};
}
}
if (min_id) {
const minStatus = await Status.findOneBy({ id: min_id });
if (minStatus) {
query = {
...query,
where: {
...query.where,
created_at: {
...(query.where as any)?.created_at,
$gte: minStatus.created_at,
},
},
};
}
}
const objects = await Status.find(query);
});
// Constuct HTTP Link header (next and prev)
const linkHeader = [];
@ -129,14 +79,13 @@ export default async (
`<${urlWithoutQuery}?max_id=${objects[0].id}&limit=${limit}>; rel="next"`
);
linkHeader.push(
`<${urlWithoutQuery}?since_id=${
objects[objects.length - 1].id
}&limit=${limit}>; rel="prev"`
`<${urlWithoutQuery}?since_id=${objects.at(-1)
?.id}&limit=${limit}>; rel="prev"`
);
}
return jsonResponse(
await Promise.all(objects.map(async status => await status.toAPI())),
await Promise.all(objects.map(status => statusToAPI(status))),
200,
{
Link: linkHeader.join(", "),

View file

@ -1,8 +1,15 @@
import { errorResponse, jsonResponse } from "@response";
import { MatchedRoute } from "bun";
import { Relationship } from "~database/entities/Relationship";
import { UserAction, userRelations } from "~database/entities/User";
import {
createNewRelationship,
relationshipToAPI,
} from "~database/entities/Relationship";
import {
getFromRequest,
getRelationshipToOtherUser,
} from "~database/entities/User";
import { applyConfig } from "@api";
import { client } from "~database/datasource";
export const meta = applyConfig({
allowedMethods: ["POST"],
@ -25,29 +32,42 @@ export default async (
): Promise<Response> => {
const id = matchedRoute.params.id;
const { user: self } = await UserAction.getFromRequest(req);
const { user: self } = await getFromRequest(req);
if (!self) return errorResponse("Unauthorized", 401);
const user = await UserAction.findOne({
where: {
id,
const user = await client.user.findUnique({
where: { id },
include: {
relationships: {
include: {
owner: true,
subject: true,
},
},
},
relations: userRelations,
});
if (!user) return errorResponse("User not found", 404);
// Check if already following
let relationship = await self.getRelationshipToOtherUser(user);
let relationship = await getRelationshipToOtherUser(self, user);
if (!relationship) {
// Create new relationship
const newRelationship = await Relationship.createNew(self, user);
const newRelationship = await createNewRelationship(self, user);
self.relationships.push(newRelationship);
await self.save();
await client.user.update({
where: { id: self.id },
data: {
relationships: {
connect: {
id: newRelationship.id,
},
},
},
});
relationship = newRelationship;
}
@ -56,6 +76,12 @@ export default async (
relationship.blocking = false;
}
await relationship.save();
return jsonResponse(await relationship.toAPI());
await client.relationship.update({
where: { id: relationship.id },
data: {
blocking: false,
},
});
return jsonResponse(await relationshipToAPI(relationship));
};

View file

@ -1,8 +1,15 @@
import { errorResponse, jsonResponse } from "@response";
import { MatchedRoute } from "bun";
import { Relationship } from "~database/entities/Relationship";
import { UserAction, userRelations } from "~database/entities/User";
import {
createNewRelationship,
relationshipToAPI,
} from "~database/entities/Relationship";
import {
getFromRequest,
getRelationshipToOtherUser,
} from "~database/entities/User";
import { applyConfig } from "@api";
import { client } from "~database/datasource";
export const meta = applyConfig({
allowedMethods: ["POST"],
@ -25,29 +32,42 @@ export default async (
): Promise<Response> => {
const id = matchedRoute.params.id;
const { user: self } = await UserAction.getFromRequest(req);
const { user: self } = await getFromRequest(req);
if (!self) return errorResponse("Unauthorized", 401);
const user = await UserAction.findOne({
where: {
id,
const user = await client.user.findUnique({
where: { id },
include: {
relationships: {
include: {
owner: true,
subject: true,
},
},
},
relations: userRelations,
});
if (!user) return errorResponse("User not found", 404);
// Check if already following
let relationship = await self.getRelationshipToOtherUser(user);
let relationship = await getRelationshipToOtherUser(self, user);
if (!relationship) {
// Create new relationship
const newRelationship = await Relationship.createNew(self, user);
const newRelationship = await createNewRelationship(self, user);
self.relationships.push(newRelationship);
await self.save();
await client.user.update({
where: { id: self.id },
data: {
relationships: {
connect: {
id: newRelationship.id,
},
},
},
});
relationship = newRelationship;
}
@ -56,6 +76,12 @@ export default async (
relationship.following = false;
}
await relationship.save();
return jsonResponse(await relationship.toAPI());
await client.relationship.update({
where: { id: relationship.id },
data: {
following: false,
},
});
return jsonResponse(await relationshipToAPI(relationship));
};

View file

@ -1,8 +1,15 @@
import { errorResponse, jsonResponse } from "@response";
import { MatchedRoute } from "bun";
import { Relationship } from "~database/entities/Relationship";
import { UserAction, userRelations } from "~database/entities/User";
import {
createNewRelationship,
relationshipToAPI,
} from "~database/entities/Relationship";
import {
getFromRequest,
getRelationshipToOtherUser,
} from "~database/entities/User";
import { applyConfig } from "@api";
import { client } from "~database/datasource";
export const meta = applyConfig({
allowedMethods: ["POST"],
@ -25,29 +32,42 @@ export default async (
): Promise<Response> => {
const id = matchedRoute.params.id;
const { user: self } = await UserAction.getFromRequest(req);
const { user: self } = await getFromRequest(req);
if (!self) return errorResponse("Unauthorized", 401);
const user = await UserAction.findOne({
where: {
id,
const user = await client.user.findUnique({
where: { id },
include: {
relationships: {
include: {
owner: true,
subject: true,
},
},
},
relations: userRelations,
});
if (!user) return errorResponse("User not found", 404);
// Check if already following
let relationship = await self.getRelationshipToOtherUser(user);
let relationship = await getRelationshipToOtherUser(self, user);
if (!relationship) {
// Create new relationship
const newRelationship = await Relationship.createNew(self, user);
const newRelationship = await createNewRelationship(self, user);
self.relationships.push(newRelationship);
await self.save();
await client.user.update({
where: { id: self.id },
data: {
relationships: {
connect: {
id: newRelationship.id,
},
},
},
});
relationship = newRelationship;
}
@ -58,6 +78,12 @@ export default async (
// TODO: Implement duration
await relationship.save();
return jsonResponse(await relationship.toAPI());
await client.relationship.update({
where: { id: relationship.id },
data: {
muting: false,
},
});
return jsonResponse(await relationshipToAPI(relationship));
};

View file

@ -1,8 +1,15 @@
import { errorResponse, jsonResponse } from "@response";
import { MatchedRoute } from "bun";
import { Relationship } from "~database/entities/Relationship";
import { UserAction, userRelations } from "~database/entities/User";
import {
createNewRelationship,
relationshipToAPI,
} from "~database/entities/Relationship";
import {
getFromRequest,
getRelationshipToOtherUser,
} from "~database/entities/User";
import { applyConfig } from "@api";
import { client } from "~database/datasource";
export const meta = applyConfig({
allowedMethods: ["POST"],
@ -25,29 +32,42 @@ export default async (
): Promise<Response> => {
const id = matchedRoute.params.id;
const { user: self } = await UserAction.getFromRequest(req);
const { user: self } = await getFromRequest(req);
if (!self) return errorResponse("Unauthorized", 401);
const user = await UserAction.findOne({
where: {
id,
const user = await client.user.findUnique({
where: { id },
include: {
relationships: {
include: {
owner: true,
subject: true,
},
},
},
relations: userRelations,
});
if (!user) return errorResponse("User not found", 404);
// Check if already following
let relationship = await self.getRelationshipToOtherUser(user);
let relationship = await getRelationshipToOtherUser(self, user);
if (!relationship) {
// Create new relationship
const newRelationship = await Relationship.createNew(self, user);
const newRelationship = await createNewRelationship(self, user);
self.relationships.push(newRelationship);
await self.save();
await client.user.update({
where: { id: self.id },
data: {
relationships: {
connect: {
id: newRelationship.id,
},
},
},
});
relationship = newRelationship;
}
@ -56,6 +76,12 @@ export default async (
relationship.endorsed = false;
}
await relationship.save();
return jsonResponse(await relationship.toAPI());
await client.relationship.update({
where: { id: relationship.id },
data: {
endorsed: false,
},
});
return jsonResponse(await relationshipToAPI(relationship));
};

View file

@ -1,14 +1,18 @@
import { parseRequest } from "@request";
import { errorResponse, jsonResponse } from "@response";
import { UserAction } from "~database/entities/User";
import { APIAccount } from "~types/entities/account";
import {
getFromRequest,
userRelations,
userToAPI,
} from "~database/entities/User";
import { applyConfig } from "@api";
import { client } from "~database/datasource";
export const meta = applyConfig({
allowedMethods: ["GET"],
route: "/api/v1/accounts/familiar_followers",
ratelimits: {
max: 30,
max: 5,
duration: 60,
},
auth: {
@ -20,7 +24,7 @@ export const meta = applyConfig({
* Find familiar followers (followers of a user that you also follow)
*/
export default async (req: Request): Promise<Response> => {
const { user: self } = await UserAction.getFromRequest(req);
const { user: self } = await getFromRequest(req);
if (!self) return errorResponse("Unauthorized", 401);
@ -33,47 +37,34 @@ export default async (req: Request): Promise<Response> => {
return errorResponse("Number of ids must be between 1 and 10", 422);
}
const response = (
await Promise.all(
ids.map(async id => {
// Find followers of user that you also follow
// Get user
const user = await UserAction.findOne({
where: { id },
relations: {
relationships: {
subject: {
relationships: true,
},
},
const followersOfIds = await client.user.findMany({
where: {
relationships: {
some: {
subjectId: {
in: ids,
},
});
following: true,
},
},
},
});
if (!user) return null;
// Find users that you follow in followersOfIds
const output = await client.user.findMany({
where: {
relationships: {
some: {
ownerId: self.id,
subjectId: {
in: followersOfIds.map(u => u.id),
},
following: true,
},
},
},
include: userRelations,
});
// Map to user response
const response = user.relationships
.filter(r => r.following)
.map(r => r.subject)
.filter(u =>
u.relationships.some(
r => r.following && r.subject.id === self.id
)
);
return {
id: id,
accounts: await Promise.all(
response.map(async u => await u.toAPI())
),
};
})
)
).filter(r => r !== null) as {
id: string;
accounts: APIAccount[];
}[];
return jsonResponse(response);
return jsonResponse(output.map(o => userToAPI(o)));
};

View file

@ -2,8 +2,10 @@ import { getConfig } from "@config";
import { parseRequest } from "@request";
import { jsonResponse } from "@response";
import { tempmailDomains } from "@tempmail";
import { UserAction } from "~database/entities/User";
import { applyConfig } from "@api";
import { client } from "~database/datasource";
import { createNewLocalUser } from "~database/entities/User";
import ISO6391 from "iso-639-1";
export const meta = applyConfig({
allowedMethods: ["POST"],
@ -115,7 +117,7 @@ export default async (req: Request): Promise<Response> => {
});
// Check if username is taken
if (await UserAction.findOne({ where: { username: body.username } }))
if (await client.user.findFirst({ where: { username: body.username } }))
errors.details.username.push({
error: "ERR_TAKEN",
description: `is already taken`,
@ -150,6 +152,18 @@ export default async (req: Request): Promise<Response> => {
description: `must be accepted`,
});
if (!body.locale)
errors.details.locale.push({
error: "ERR_BLANK",
description: `can't be blank`,
});
if (!ISO6391.validate(body.locale ?? ""))
errors.details.locale.push({
error: "ERR_INVALID",
description: `must be a valid ISO 639-1 code`,
});
// If any errors are present, return them
if (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"
@ -168,14 +182,13 @@ export default async (req: Request): Promise<Response> => {
});
}
// TODO: Check if locale is valid
await UserAction.createNewLocal({
await createNewLocalUser({
username: body.username ?? "",
password: body.password ?? "",
email: body.email ?? "",
});
// TODO: Return access token
return new Response();
return new Response("", {
status: 200,
});
};

View file

@ -1,8 +1,12 @@
import { parseRequest } from "@request";
import { errorResponse, jsonResponse } from "@response";
import { Relationship } from "~database/entities/Relationship";
import { UserAction } from "~database/entities/User";
import {
createNewRelationship,
relationshipToAPI,
} from "~database/entities/Relationship";
import { getFromRequest } from "~database/entities/User";
import { applyConfig } from "@api";
import { client } from "~database/datasource";
export const meta = applyConfig({
allowedMethods: ["GET"],
@ -20,7 +24,7 @@ export const meta = applyConfig({
* Find relationships
*/
export default async (req: Request): Promise<Response> => {
const { user: self } = await UserAction.getFromRequest(req);
const { user: self } = await getFromRequest(req);
if (!self) return errorResponse("Unauthorized", 401);
@ -33,34 +37,35 @@ export default async (req: Request): Promise<Response> => {
return errorResponse("Number of ids must be between 1 and 10", 422);
}
// Check if already following
// TODO: Limit ID amount
const relationships = (
await Promise.all(
ids.map(async id => {
const user = await UserAction.findOneBy({ id });
if (!user) return null;
let relationship = await self.getRelationshipToOtherUser(user);
const relationships = await client.relationship.findMany({
where: {
ownerId: self.id,
subjectId: {
in: ids,
},
},
});
if (!relationship) {
// Create new relationship
// Find IDs that dont have a relationship
const missingIds = ids.filter(
id => !relationships.some(r => r.subjectId === id)
);
const newRelationship = await Relationship.createNew(
self,
user
);
// Create the missing relationships
for (const id of missingIds) {
const relationship = await createNewRelationship(self, { id } as any);
self.relationships.push(newRelationship);
await self.save();
relationships.push(relationship);
}
relationship = newRelationship;
}
return relationship;
})
)
).filter(relationship => relationship !== null) as Relationship[];
// Order in the same order as ids
relationships.sort(
(a, b) => ids.indexOf(a.subjectId) - ids.indexOf(b.subjectId)
);
return jsonResponse(
await Promise.all(relationships.map(async r => await r.toAPI()))
await Promise.all(
relationships.map(async r => await relationshipToAPI(r))
)
);
};

View file

@ -1,12 +1,14 @@
import { getConfig } from "@config";
import { parseRequest } from "@request";
import { errorResponse, jsonResponse } from "@response";
import { UserAction } from "~database/entities/User";
import { getFromRequest, userToAPI } from "~database/entities/User";
import { applyConfig } from "@api";
import { sanitize } from "isomorphic-dompurify";
import { sanitizeHtml } from "@sanitization";
import { uploadFile } from "~classes/media";
import { EmojiAction } from "~database/entities/Emoji";
import ISO6391 from "iso-639-1";
import { parseEmojis } from "~database/entities/Emoji";
import { client } from "~database/datasource";
export const meta = applyConfig({
allowedMethods: ["PATCH"],
@ -24,7 +26,7 @@ export const meta = applyConfig({
* Patches a user
*/
export default async (req: Request): Promise<Response> => {
const { user } = await UserAction.getFromRequest(req);
const { user } = await getFromRequest(req);
if (!user) return errorResponse("Unauthorized", 401);
@ -85,7 +87,7 @@ export default async (req: Request): Promise<Response> => {
// Remove emojis
user.emojis = [];
user.display_name = sanitizedDisplayName;
user.displayName = sanitizedDisplayName;
}
if (note) {
@ -112,7 +114,7 @@ export default async (req: Request): Promise<Response> => {
user.note = sanitizedNote;
}
if (source_privacy) {
if (source_privacy && user.source) {
// Check if within allowed privacy values
if (
!["public", "unlisted", "private", "direct"].includes(
@ -125,21 +127,30 @@ export default async (req: Request): Promise<Response> => {
);
}
user.source.privacy = source_privacy;
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
(user.source as any).privacy = source_privacy;
}
if (source_sensitive) {
if (source_sensitive && user.source) {
// Check if within allowed sensitive values
if (source_sensitive !== "true" && source_sensitive !== "false") {
return errorResponse("Sensitive must be a boolean", 422);
}
user.source.sensitive = source_sensitive === "true";
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
(user.source as any).sensitive = source_sensitive === "true";
}
if (source_language) {
// TODO: Check if proper ISO code
user.source.language = source_language;
if (source_language && user.source) {
if (!ISO6391.validate(source_language)) {
return errorResponse(
"Language must be a valid ISO 639-1 code",
422
);
}
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
(user.source as any).language = source_language;
}
if (avatar) {
@ -176,8 +187,7 @@ export default async (req: Request): Promise<Response> => {
return errorResponse("Locked must be a boolean", 422);
}
// TODO: Add a user value for Locked
// user.locked = locked === "true";
user.isLocked = locked === "true";
}
if (bot) {
@ -186,8 +196,7 @@ export default async (req: Request): Promise<Response> => {
return errorResponse("Bot must be a boolean", 422);
}
// TODO: Add a user value for bot
// user.bot = bot === "true";
user.isBot = bot === "true";
}
if (discoverable) {
@ -196,14 +205,13 @@ export default async (req: Request): Promise<Response> => {
return errorResponse("Discoverable must be a boolean", 422);
}
// TODO: Add a user value for discoverable
// user.discoverable = discoverable === "true";
user.isDiscoverable = discoverable === "true";
}
// Parse emojis
const displaynameEmojis = await EmojiAction.parseEmojis(sanitizedDisplayName);
const noteEmojis = await EmojiAction.parseEmojis(sanitizedNote);
const displaynameEmojis = await parseEmojis(sanitizedDisplayName);
const noteEmojis = await parseEmojis(sanitizedNote);
user.emojis = [...displaynameEmojis, ...noteEmojis];
@ -212,7 +220,31 @@ export default async (req: Request): Promise<Response> => {
(emoji, index, self) => self.findIndex(e => e.id === emoji.id) === index
);
await user.save();
await client.user.update({
where: { id: user.id },
data: {
displayName: user.displayName,
note: user.note,
avatar: user.avatar,
header: user.header,
isLocked: user.isLocked,
isBot: user.isBot,
isDiscoverable: user.isDiscoverable,
emojis: {
disconnect: user.emojis.map(e => ({
id: e.id,
})),
connect: user.emojis.map(e => ({
id: e.id,
})),
},
source: user.source
? {
update: user.source,
}
: undefined,
},
});
return jsonResponse(await user.toAPI());
return jsonResponse(await userToAPI(user));
};

View file

@ -1,5 +1,5 @@
import { errorResponse, jsonResponse } from "@response";
import { UserAction } from "~database/entities/User";
import { getFromRequest, userToAPI } from "~database/entities/User";
import { applyConfig } from "@api";
export const meta = applyConfig({
@ -17,12 +17,12 @@ export const meta = applyConfig({
export default async (req: Request): Promise<Response> => {
// TODO: Add checks for disabled or not email verified accounts
const { user } = await UserAction.getFromRequest(req);
const { user } = await getFromRequest(req);
if (!user) return errorResponse("Unauthorized", 401);
return jsonResponse({
...(await user.toAPI()),
...(await userToAPI(user)),
source: user.source,
// TODO: Add role support
role: {

View file

@ -2,7 +2,7 @@ import { applyConfig } from "@api";
import { parseRequest } from "@request";
import { errorResponse, jsonResponse } from "@response";
import { randomBytes } from "crypto";
import { ApplicationAction } from "~database/entities/Application";
import { client } from "~database/datasource";
export const meta = applyConfig({
allowedMethods: ["POST"],
@ -27,10 +27,6 @@ export default async (req: Request): Promise<Response> => {
website: string;
}>(req);
const application = new ApplicationAction();
application.name = client_name || "";
// Check if redirect URI is a valid URI, and also an absolute URI
if (redirect_uris) {
try {
@ -42,20 +38,20 @@ export default async (req: Request): Promise<Response> => {
422
);
}
application.redirect_uris = redirect_uris;
} catch {
return errorResponse("Redirect URI must be a valid URI", 422);
}
}
application.scopes = scopes || "read";
application.website = website || null;
application.client_id = randomBytes(32).toString("base64url");
application.secret = randomBytes(64).toString("base64url");
await application.save();
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,

View file

@ -1,7 +1,7 @@
import { applyConfig } from "@api";
import { errorResponse, jsonResponse } from "@response";
import { ApplicationAction } from "~database/entities/Application";
import { UserAction } from "~database/entities/User";
import { getFromToken } from "~database/entities/Application";
import { getFromRequest } from "~database/entities/User";
export const meta = applyConfig({
allowedMethods: ["GET"],
@ -19,8 +19,8 @@ export const meta = applyConfig({
* Returns OAuth2 credentials
*/
export default async (req: Request): Promise<Response> => {
const { user, token } = await UserAction.getFromRequest(req);
const application = await ApplicationAction.getFromToken(token);
const { user, token } = await getFromRequest(req);
const application = await getFromToken(token);
if (!user) return errorResponse("Unauthorized", 401);
if (!application) return errorResponse("Unauthorized", 401);

View file

@ -1,7 +1,7 @@
import { applyConfig } from "@api";
import { jsonResponse } from "@response";
import { IsNull } from "typeorm";
import { EmojiAction } from "~database/entities/Emoji";
import { client } from "~database/datasource";
import { emojiToAPI } from "~database/entities/Emoji";
export const meta = applyConfig({
allowedMethods: ["GET"],
@ -20,11 +20,13 @@ export const meta = applyConfig({
*/
// eslint-disable-next-line @typescript-eslint/require-await
export default async (): Promise<Response> => {
const emojis = await EmojiAction.findBy({
instance: IsNull(),
const emojis = await client.emoji.findMany({
where: {
instanceId: null,
},
});
return jsonResponse(
await Promise.all(emojis.map(async emoji => await emoji.toAPI()))
await Promise.all(emojis.map(emoji => emojiToAPI(emoji)))
);
};

View file

@ -1,8 +1,7 @@
import { applyConfig } from "@api";
import { getConfig } from "@config";
import { jsonResponse } from "@response";
import { Status } from "~database/entities/Status";
import { UserAction } from "~database/entities/User";
import { client } from "~database/datasource";
export const meta = applyConfig({
allowedMethods: ["GET"],
@ -23,8 +22,16 @@ export const meta = applyConfig({
export default async (): Promise<Response> => {
const config = getConfig();
const statusCount = await Status.count();
const userCount = await UserAction.count();
const statusCount = await client.status.count({
where: {
instanceId: null,
},
});
const userCount = await client.user.count({
where: {
instanceId: null,
},
});
// TODO: fill in more values
return jsonResponse({

View file

@ -1,14 +1,20 @@
import { applyConfig } from "@api";
import { errorResponse, jsonResponse } from "@response";
import { MatchedRoute } from "bun";
import { Status, statusAndUserRelations } from "~database/entities/Status";
import { UserAction } from "~database/entities/User";
import { client } from "~database/datasource";
import {
getAncestors,
getDescendants,
statusAndUserRelations,
statusToAPI,
} from "~database/entities/Status";
import { getFromRequest } from "~database/entities/User";
import { APIRouteMeta } from "~types/api";
export const meta: APIRouteMeta = applyConfig({
allowedMethods: ["GET"],
ratelimits: {
max: 100,
max: 8,
duration: 60,
},
route: "/api/v1/statuses/:id/context",
@ -28,30 +34,25 @@ export default async (
// User token + read:statuses for up to 4,096 ancestors, 4,096 descendants, unlimited depth, and private statuses.
const id = matchedRoute.params.id;
const { user } = await UserAction.getFromRequest(req);
const { user } = await getFromRequest(req);
let foundStatus: Status | null;
try {
foundStatus = await Status.findOne({
where: {
id,
},
relations: statusAndUserRelations,
});
} catch (e) {
return errorResponse("Invalid ID", 404);
}
const foundStatus = await client.status.findUnique({
where: { id },
include: statusAndUserRelations,
});
if (!foundStatus) return errorResponse("Record not found", 404);
// Get all ancestors
const ancestors = await foundStatus.getAncestors(user);
const descendants = await foundStatus.getDescendants(user);
const ancestors = await getAncestors(foundStatus, user);
const descendants = await getDescendants(foundStatus, user);
return jsonResponse({
ancestors: await Promise.all(ancestors.map(status => status.toAPI())),
ancestors: await Promise.all(
ancestors.map(status => statusToAPI(status))
),
descendants: await Promise.all(
descendants.map(status => status.toAPI())
descendants.map(status => statusToAPI(status))
),
});
};

View file

@ -2,10 +2,15 @@
import { applyConfig } from "@api";
import { errorResponse, jsonResponse } from "@response";
import { MatchedRoute } from "bun";
import { Like } from "~database/entities/Like";
import { Status, statusAndUserRelations } from "~database/entities/Status";
import { UserAction, userRelations } from "~database/entities/User";
import { client } from "~database/datasource";
import {
isViewableByUser,
statusAndUserRelations,
statusToAPI,
} from "~database/entities/Status";
import { getFromRequest } from "~database/entities/User";
import { APIRouteMeta } from "~types/api";
import { APIStatus } from "~types/entities/status";
export const meta: APIRouteMeta = applyConfig({
allowedMethods: ["POST"],
@ -28,51 +33,38 @@ export default async (
): Promise<Response> => {
const id = matchedRoute.params.id;
const { user } = await UserAction.getFromRequest(req);
const { user } = await getFromRequest(req);
if (!user) return errorResponse("Unauthorized", 401);
let foundStatus: Status | null;
try {
foundStatus = await Status.findOne({
where: {
id,
},
relations: statusAndUserRelations,
});
} catch (e) {
return errorResponse("Invalid ID", 404);
}
if (!foundStatus) return errorResponse("Record not found", 404);
const status = await client.status.findUnique({
where: { id },
include: statusAndUserRelations,
});
// Check if user is authorized to view this status (if it's private)
if (!foundStatus.isViewableByUser(user)) {
if (!status || !isViewableByUser(status, user))
return errorResponse("Record not found", 404);
}
// Check if user has already favourited this status
const existingLike = await Like.findOne({
const existingLike = await client.like.findFirst({
where: {
liked: {
id: foundStatus.id,
},
liker: {
id: user.id,
},
likedId: status.id,
likerId: user.id,
},
relations: [
...userRelations.map(r => `liker.${r}`),
...statusAndUserRelations.map(r => `liked.${r}`),
],
});
if (!existingLike) {
const like = new Like();
like.liker = user;
like.liked = foundStatus;
await like.save();
await client.like.create({
data: {
likedId: status.id,
likerId: user.id,
},
});
}
return jsonResponse(await foundStatus.toAPI());
return jsonResponse({
...(await statusToAPI(status, user)),
favourited: true,
favourites_count: status._count.likes + 1,
} as APIStatus);
};

View file

@ -3,10 +3,16 @@ import { applyConfig } from "@api";
import { parseRequest } from "@request";
import { errorResponse, jsonResponse } from "@response";
import { MatchedRoute } from "bun";
import { FindManyOptions } from "typeorm";
import { Like } from "~database/entities/Like";
import { Status, statusAndUserRelations } from "~database/entities/Status";
import { UserAction, userRelations } from "~database/entities/User";
import { client } from "~database/datasource";
import {
isViewableByUser,
statusAndUserRelations,
} from "~database/entities/Status";
import {
getFromRequest,
userRelations,
userToAPI,
} from "~database/entities/User";
import { APIRouteMeta } from "~types/api";
export const meta: APIRouteMeta = applyConfig({
@ -30,33 +36,25 @@ export default async (
): Promise<Response> => {
const id = matchedRoute.params.id;
const { user } = await UserAction.getFromRequest(req);
const { user } = await getFromRequest(req);
let foundStatus: Status | null;
try {
foundStatus = await Status.findOne({
where: {
id,
},
relations: statusAndUserRelations,
});
} catch (e) {
return errorResponse("Invalid ID", 404);
}
if (!foundStatus) return errorResponse("Record not found", 404);
const status = await client.status.findUnique({
where: { id },
include: statusAndUserRelations,
});
// Check if user is authorized to view this status (if it's private)
if (!foundStatus.isViewableByUser(user)) {
if (!status || !isViewableByUser(status, user))
return errorResponse("Record not found", 404);
}
const {
max_id = null,
min_id = null,
since_id = null,
limit = 40,
} = await parseRequest<{
max_id?: string;
min_id?: string;
since_id?: string;
limit?: number;
}>(req);
@ -65,53 +63,32 @@ export default async (
if (limit > 80) return errorResponse("Invalid limit (maximum is 80)", 400);
if (limit < 1) return errorResponse("Invalid limit", 400);
// Get list of boosts for this status
let query: FindManyOptions<Like> = {
const objects = await client.user.findMany({
where: {
liked: {
id,
likes: {
some: {
likedId: status.id,
},
},
id: {
lt: max_id ?? undefined,
gte: since_id ?? undefined,
gt: min_id ?? undefined,
},
},
relations: userRelations.map(r => `liker.${r}`),
take: limit,
order: {
id: "DESC",
include: {
...userRelations,
likes: {
where: {
likedId: status.id,
},
},
},
};
if (max_id) {
const maxLike = await Like.findOneBy({ id: max_id });
if (maxLike) {
query = {
...query,
where: {
...query.where,
created_at: {
...(query.where as any)?.created_at,
$lt: maxLike.created_at,
},
},
};
}
}
if (since_id) {
const sinceLike = await Like.findOneBy({ id: since_id });
if (sinceLike) {
query = {
...query,
where: {
...query.where,
created_at: {
...(query.where as any)?.created_at,
$gt: sinceLike.created_at,
},
},
};
}
}
const objects = await Like.find(query);
take: limit,
orderBy: {
id: "desc",
},
});
// Constuct HTTP Link header (next and prev)
const linkHeader = [];
@ -128,7 +105,7 @@ export default async (
}
return jsonResponse(
await Promise.all(objects.map(async like => await like.liker.toAPI())),
await Promise.all(objects.map(async user => userToAPI(user))),
200,
{
Link: linkHeader.join(", "),

View file

@ -1,8 +1,13 @@
import { applyConfig } from "@api";
import { errorResponse, jsonResponse } from "@response";
import { MatchedRoute } from "bun";
import { Status, statusAndUserRelations } from "~database/entities/Status";
import { UserAction } from "~database/entities/User";
import { client } from "~database/datasource";
import {
isViewableByUser,
statusAndUserRelations,
statusToAPI,
} from "~database/entities/Status";
import { getFromRequest } from "~database/entities/User";
import { APIRouteMeta } from "~types/api";
export const meta: APIRouteMeta = applyConfig({
@ -27,31 +32,21 @@ export default async (
): Promise<Response> => {
const id = matchedRoute.params.id;
const { user } = await UserAction.getFromRequest(req);
const { user } = await getFromRequest(req);
let foundStatus: Status | null;
try {
foundStatus = await Status.findOne({
where: {
id,
},
relations: statusAndUserRelations,
});
} catch (e) {
return errorResponse("Invalid ID", 404);
}
if (!foundStatus) return errorResponse("Record not found", 404);
const status = await client.status.findUnique({
where: { id },
include: statusAndUserRelations,
});
// Check if user is authorized to view this status (if it's private)
if (!foundStatus.isViewableByUser(user)) {
if (!status || isViewableByUser(status, user))
return errorResponse("Record not found", 404);
}
if (req.method === "GET") {
return jsonResponse(await foundStatus.toAPI());
return jsonResponse(await statusToAPI(status));
} else if (req.method === "DELETE") {
if (foundStatus.account.id !== user?.id) {
if (status.authorId !== user?.id) {
return errorResponse("Unauthorized", 401);
}
@ -60,11 +55,13 @@ export default async (
// Get associated Status object
// Delete status and all associated objects
await foundStatus.remove();
await client.status.delete({
where: { id },
});
return jsonResponse(
{
...(await foundStatus.toAPI()),
...(await statusToAPI(status)),
// TODO: Add
// text: Add source text
// poll: Add source poll

View file

@ -3,9 +3,16 @@ import { applyConfig } from "@api";
import { parseRequest } from "@request";
import { errorResponse, jsonResponse } from "@response";
import { MatchedRoute } from "bun";
import { FindManyOptions } from "typeorm";
import { Status, statusAndUserRelations } from "~database/entities/Status";
import { UserAction } from "~database/entities/User";
import { client } from "~database/datasource";
import {
isViewableByUser,
statusAndUserRelations,
} from "~database/entities/Status";
import {
getFromRequest,
userRelations,
userToAPI,
} from "~database/entities/User";
import { APIRouteMeta } from "~types/api";
export const meta: APIRouteMeta = applyConfig({
@ -29,33 +36,25 @@ export default async (
): Promise<Response> => {
const id = matchedRoute.params.id;
const { user } = await UserAction.getFromRequest(req);
const { user } = await getFromRequest(req);
let foundStatus: Status | null;
try {
foundStatus = await Status.findOne({
where: {
id,
},
relations: statusAndUserRelations,
});
} catch (e) {
return errorResponse("Invalid ID", 404);
}
if (!foundStatus) return errorResponse("Record not found", 404);
const status = await client.status.findUnique({
where: { id },
include: statusAndUserRelations,
});
// Check if user is authorized to view this status (if it's private)
if (!foundStatus.isViewableByUser(user)) {
if (!status || !isViewableByUser(status, user))
return errorResponse("Record not found", 404);
}
const {
max_id = null,
min_id = null,
since_id = null,
limit = 40,
} = await parseRequest<{
max_id?: string;
min_id?: string;
since_id?: string;
limit?: number;
}>(req);
@ -64,53 +63,33 @@ export default async (
if (limit > 80) return errorResponse("Invalid limit (maximum is 80)", 400);
if (limit < 1) return errorResponse("Invalid limit", 400);
// Get list of boosts for this status
let query: FindManyOptions<Status> = {
const objects = await client.user.findMany({
where: {
reblog: {
id,
statuses: {
some: {
reblogId: status.id,
},
},
id: {
lt: max_id ?? undefined,
gte: since_id ?? undefined,
gt: min_id ?? undefined,
},
},
relations: statusAndUserRelations,
take: limit,
order: {
id: "DESC",
include: {
...userRelations,
statuses: {
where: {
reblogId: status.id,
},
include: statusAndUserRelations,
},
},
};
if (max_id) {
const maxPost = await Status.findOneBy({ id: max_id });
if (maxPost) {
query = {
...query,
where: {
...query.where,
created_at: {
...(query.where as any)?.created_at,
$lt: maxPost.created_at,
},
},
};
}
}
if (since_id) {
const sincePost = await Status.findOneBy({ id: since_id });
if (sincePost) {
query = {
...query,
where: {
...query.where,
created_at: {
...(query.where as any)?.created_at,
$gt: sincePost.created_at,
},
},
};
}
}
const objects = await Status.find(query);
take: limit,
orderBy: {
id: "desc",
},
});
// Constuct HTTP Link header (next and prev)
const linkHeader = [];
@ -127,7 +106,7 @@ export default async (
}
return jsonResponse(
await Promise.all(objects.map(async object => await object.toAPI())),
await Promise.all(objects.map(async user => userToAPI(user))),
200,
{
Link: linkHeader.join(", "),

View file

@ -2,10 +2,15 @@
import { applyConfig } from "@api";
import { errorResponse, jsonResponse } from "@response";
import { MatchedRoute } from "bun";
import { Like } from "~database/entities/Like";
import { Status, statusAndUserRelations } from "~database/entities/Status";
import { UserAction } from "~database/entities/User";
import { client } from "~database/datasource";
import {
isViewableByUser,
statusAndUserRelations,
statusToAPI,
} from "~database/entities/Status";
import { getFromRequest } from "~database/entities/User";
import { APIRouteMeta } from "~types/api";
import { APIStatus } from "~types/entities/status";
export const meta: APIRouteMeta = applyConfig({
allowedMethods: ["POST"],
@ -28,37 +33,29 @@ export default async (
): Promise<Response> => {
const id = matchedRoute.params.id;
const { user } = await UserAction.getFromRequest(req);
const { user } = await getFromRequest(req);
if (!user) return errorResponse("Unauthorized", 401);
let foundStatus: Status | null;
try {
foundStatus = await Status.findOne({
where: {
id,
},
relations: statusAndUserRelations,
});
} catch (e) {
return errorResponse("Invalid ID", 404);
}
if (!foundStatus) return errorResponse("Record not found", 404);
const status = await client.status.findUnique({
where: { id },
include: statusAndUserRelations,
});
// Check if user is authorized to view this status (if it's private)
if (!foundStatus.isViewableByUser(user)) {
if (!status || !isViewableByUser(status, user))
return errorResponse("Record not found", 404);
}
await Like.delete({
liked: {
id: foundStatus.id,
},
liker: {
id: user.id,
await client.like.deleteMany({
where: {
likedId: status.id,
likerId: user.id,
},
});
return jsonResponse(await foundStatus.toAPI());
return jsonResponse({
...(await statusToAPI(status, user)),
favourited: false,
favourites_count: status._count.likes - 1,
} as APIStatus);
};

View file

@ -8,9 +8,15 @@ import { errorResponse, jsonResponse } from "@response";
import { sanitizeHtml } from "@sanitization";
import { MatchedRoute } from "bun";
import { parse } from "marked";
import { ApplicationAction } from "~database/entities/Application";
import { Status, statusRelations } from "~database/entities/Status";
import { AuthData, UserAction } from "~database/entities/User";
import { client } from "~database/datasource";
import { getFromToken } from "~database/entities/Application";
import {
StatusWithRelations,
createNewStatus,
statusAndUserRelations,
statusToAPI,
} from "~database/entities/Status";
import { AuthData, UserWithRelations } from "~database/entities/User";
import { APIRouteMeta } from "~types/api";
export const meta: APIRouteMeta = applyConfig({
@ -34,7 +40,7 @@ export default async (
authData: AuthData
): Promise<Response> => {
const { user, token } = authData;
const application = await ApplicationAction.getFromToken(token);
const application = await getFromToken(token);
if (!user) return errorResponse("Unauthorized", 401);
@ -126,18 +132,16 @@ export default async (
}
// Get reply account and status if exists
let replyStatus: Status | null = null;
let replyUser: UserAction | null = null;
let replyStatus: StatusWithRelations | null = null;
let replyUser: UserWithRelations | null = null;
if (in_reply_to_id) {
replyStatus = await Status.findOne({
where: {
id: in_reply_to_id,
},
relations: statusRelations,
replyStatus = await client.status.findUnique({
where: { id: in_reply_to_id },
include: statusAndUserRelations,
});
replyUser = replyStatus?.account || null;
replyUser = replyStatus?.author || null;
}
// Check if status body doesnt match filters
@ -145,8 +149,7 @@ export default async (
return errorResponse("Status contains blocked words", 422);
}
// Create status
const newStatus = await Status.createNew({
const newStatus = await createNewStatus({
account: user,
application,
content: sanitizedStatus,
@ -171,5 +174,5 @@ export default async (
// TODO: add database jobs to deliver the post
return jsonResponse(await newStatus.toAPI());
return jsonResponse(await statusToAPI(newStatus, user));
};

View file

@ -2,10 +2,9 @@
import { applyConfig } from "@api";
import { parseRequest } from "@request";
import { errorResponse, jsonResponse } from "@response";
import { MatchedRoute } from "bun";
import { FindManyOptions } from "typeorm";
import { Status, statusAndUserRelations } from "~database/entities/Status";
import { AuthData } from "~database/entities/User";
import { client } from "~database/datasource";
import { statusAndUserRelations, statusToAPI } from "~database/entities/Status";
import { getFromRequest } from "~database/entities/User";
import { APIRouteMeta } from "~types/api";
export const meta: APIRouteMeta = applyConfig({
@ -23,11 +22,9 @@ export const meta: APIRouteMeta = applyConfig({
/**
* Fetch home timeline statuses
*/
export default async (
req: Request,
matchedRoute: MatchedRoute,
authData: AuthData
): Promise<Response> => {
export default async (req: Request): Promise<Response> => {
const { user } = await getFromRequest(req);
const {
limit = 20,
max_id,
@ -40,85 +37,54 @@ export default async (
limit?: number;
}>(req);
const { user } = authData;
if (limit < 1 || limit > 40) {
return errorResponse("Limit must be between 1 and 40", 400);
}
let query: FindManyOptions<Status> = {
if (!user) return errorResponse("Unauthorized", 401);
const objects = await client.status.findMany({
where: {
visibility: "public",
account: [
{
relationships: {
id: user?.id,
followed_by: true,
id: {
lt: max_id ?? undefined,
gte: since_id ?? undefined,
gt: min_id ?? undefined,
},
author: {
relationships: {
some: {
subjectId: user.id,
following: true,
},
},
{
id: user?.id,
},
],
},
order: {
created_at: "DESC",
},
},
include: statusAndUserRelations,
take: limit,
relations: statusAndUserRelations,
};
orderBy: {
id: "desc",
},
});
if (max_id) {
const maxPost = await Status.findOneBy({ id: max_id });
if (maxPost) {
query = {
...query,
where: {
...query.where,
created_at: {
...(query.where as any)?.created_at,
$lt: maxPost.created_at,
},
},
};
}
// 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"`
);
}
if (min_id) {
const minPost = await Status.findOneBy({ id: min_id });
if (minPost) {
query = {
...query,
where: {
...query.where,
created_at: {
...(query.where as any)?.created_at,
$gt: minPost.created_at,
},
},
};
}
}
if (since_id) {
const sincePost = await Status.findOneBy({ id: since_id });
if (sincePost) {
query = {
...query,
where: {
...query.where,
created_at: {
...(query.where as any)?.created_at,
$gte: sincePost.created_at,
},
},
};
}
}
const objects = await Status.find(query);
return jsonResponse(
await Promise.all(objects.map(async object => await object.toAPI()))
await Promise.all(objects.map(async status => statusToAPI(status))),
200,
{
Link: linkHeader.join(", "),
}
);
};

View file

@ -1,8 +1,8 @@
import { applyConfig } from "@api";
import { parseRequest } from "@request";
import { errorResponse, jsonResponse } from "@response";
import { FindManyOptions, IsNull, Not } from "typeorm";
import { Status, statusAndUserRelations } from "~database/entities/Status";
import { client } from "~database/datasource";
import { statusAndUserRelations, statusToAPI } from "~database/entities/Status";
import { APIRouteMeta } from "~types/api";
export const meta: APIRouteMeta = applyConfig({
@ -17,36 +17,13 @@ export const meta: APIRouteMeta = applyConfig({
},
});
const updateQuery = async (
id: string | undefined,
operator: string,
query: FindManyOptions<Status>
) => {
if (!id) return query;
const post = await Status.findOneBy({ id });
if (post) {
query = {
...query,
where: {
...query.where,
created_at: {
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
...(query.where as any)?.created_at,
[operator]: post.created_at,
},
},
};
}
return query;
};
export default async (req: Request): Promise<Response> => {
const {
local,
limit = 20,
max_id,
min_id,
only_media,
// only_media,
remote,
since_id,
} = await parseRequest<{
@ -67,48 +44,47 @@ export default async (req: Request): Promise<Response> => {
return errorResponse("Cannot use both local and remote", 400);
}
let query: FindManyOptions<Status> = {
const objects = await client.status.findMany({
where: {
visibility: "public",
},
order: {
created_at: "DESC",
id: {
lt: max_id ?? undefined,
gte: since_id ?? undefined,
gt: min_id ?? undefined,
},
instanceId: remote
? {
not: null,
}
: local
? null
: undefined,
},
include: statusAndUserRelations,
take: limit,
relations: statusAndUserRelations,
};
orderBy: {
id: "desc",
},
});
query = await updateQuery(max_id, "$lt", query);
query = await updateQuery(min_id, "$gt", query);
query = await updateQuery(since_id, "$gte", query);
if (only_media) {
// TODO: add
// 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"`
);
}
if (local) {
query = {
...query,
where: {
...query.where,
instance: IsNull(),
},
};
}
if (remote) {
query = {
...query,
where: {
...query.where,
instance: Not(IsNull()),
},
};
}
const objects = await Status.find(query);
return jsonResponse(
await Promise.all(objects.map(async object => await object.toAPI()))
await Promise.all(objects.map(async status => statusToAPI(status))),
200,
{
Link: linkHeader.join(", "),
}
);
};

View file

@ -2,9 +2,8 @@ import { applyConfig } from "@api";
import { errorResponse } from "@response";
import { MatchedRoute } from "bun";
import { randomBytes } from "crypto";
import { ApplicationAction } from "~database/entities/Application";
import { Token } from "~database/entities/Token";
import { UserAction, userRelations } from "~database/entities/User";
import { client } from "~database/datasource";
import { userRelations } from "~database/entities/User";
import { APIRouteMeta } from "~types/api";
export const meta: APIRouteMeta = applyConfig({
@ -45,33 +44,44 @@ export default async (
return errorResponse("Missing username or password", 400);
// Get user
const user = await UserAction.findOne({
const user = await client.user.findFirst({
where: {
email,
},
relations: userRelations,
include: userRelations,
});
if (!user || !(await Bun.password.verify(password, user.password || "")))
return errorResponse("Invalid username or password", 401);
// Get application
const application = await ApplicationAction.findOneBy({
client_id,
const application = await client.application.findFirst({
where: {
client_id,
},
});
if (!application) return errorResponse("Invalid client_id", 404);
const token = new Token();
token.access_token = randomBytes(64).toString("base64url");
token.code = randomBytes(32).toString("hex");
token.application = application;
token.scope = scopes.join(" ");
token.user = user;
await token.save();
const token = await client.application.update({
where: { id: application.id },
data: {
tokens: {
create: {
access_token: randomBytes(64).toString("base64url"),
code: randomBytes(32).toString("hex"),
scope: scopes.join(" "),
token_type: "bearer",
user: {
connect: {
id: user.id,
},
},
},
},
},
});
// Redirect back to application
return Response.redirect(`${redirect_uri}?code=${token.code}`, 302);
return Response.redirect(`${redirect_uri}?code=${token.secret}`, 302);
};

View file

@ -1,7 +1,7 @@
import { applyConfig } from "@api";
import { parseRequest } from "@request";
import { errorResponse, jsonResponse } from "@response";
import { Token } from "~database/entities/Token";
import { client } from "~database/datasource";
export const meta = applyConfig({
allowedMethods: ["POST"],
@ -36,14 +36,19 @@ export default async (req: Request): Promise<Response> => {
);
// Get associated token
const token = await Token.findOneBy({
code,
application: {
client_id,
secret: client_secret,
redirect_uris: redirect_uri,
const token = await client.token.findFirst({
where: {
code,
application: {
client_id,
secret: client_secret,
redirect_uris: redirect_uri,
},
scope: scope?.replaceAll("+", " "),
},
include: {
application: true,
},
scope: scope?.replaceAll("+", " "),
});
if (!token)

View file

@ -5,17 +5,16 @@ import { getConfig } from "@config";
import { getBestContentType } from "@content_types";
import { errorResponse, jsonResponse } from "@response";
import { MatchedRoute } from "bun";
import { EmojiAction } from "~database/entities/Emoji";
import { LysandObject } from "~database/entities/Object";
import { Status } from "~database/entities/Status";
import { UserAction, userRelations } from "~database/entities/User";
import { client } from "~database/datasource";
import { parseEmojis } from "~database/entities/Emoji";
import { createFromObject } from "~database/entities/Object";
import {
ContentFormat,
LysandAction,
LysandObjectType,
LysandPublication,
Patch,
} from "~types/lysand/Object";
createNewStatus,
fetchFromRemote,
statusAndUserRelations,
} from "~database/entities/Status";
import { parseMentionsUris, userRelations } from "~database/entities/User";
import { LysandAction, LysandPublication, Patch } from "~types/lysand/Object";
export const meta = applyConfig({
allowedMethods: ["POST"],
@ -61,11 +60,11 @@ export default async (
// Process request body
const body = (await req.json()) as LysandPublication | LysandAction;
const author = await UserAction.findOne({
const author = await client.user.findUnique({
where: {
uri: body.author,
username,
},
relations: userRelations,
include: userRelations,
});
if (!author) {
@ -116,7 +115,7 @@ export default async (
// author.public_key is base64 encoded raw public key
const publicKey = await crypto.subtle.importKey(
"spki",
Buffer.from(author.public_key, "base64"),
Buffer.from(author.publicKey, "base64"),
"Ed25519",
false,
["verify"]
@ -141,13 +140,13 @@ export default async (
switch (type) {
case "Note": {
// Store the object in the LysandObject table
await LysandObject.createFromObject(body);
await createFromObject(body);
const content = getBestContentType(body.contents);
const emojis = await EmojiAction.parseEmojis(content?.content || "");
const emojis = await parseEmojis(content?.content || "");
const newStatus = await Status.createNew({
const newStatus = await createNewStatus({
account: author,
content: content?.content || "",
content_type: content?.content_type,
@ -158,39 +157,49 @@ export default async (
sensitive: body.is_sensitive,
uri: body.uri,
emojis: emojis,
mentions: await UserAction.parseMentions(body.mentions),
mentions: await parseMentionsUris(body.mentions),
});
// If there is a reply, fetch all the reply parents and add them to the database
if (body.replies_to.length > 0) {
newStatus.in_reply_to_post = await Status.fetchFromRemote(
body.replies_to[0]
);
newStatus.inReplyToPostId =
(await fetchFromRemote(body.replies_to[0]))?.id || null;
}
// Same for quotes
if (body.quotes.length > 0) {
newStatus.quoting_post = await Status.fetchFromRemote(
body.quotes[0]
);
newStatus.quotingPostId =
(await fetchFromRemote(body.quotes[0]))?.id || null;
}
await newStatus.save();
await client.status.update({
where: {
id: newStatus.id,
},
data: {
inReplyToPostId: newStatus.inReplyToPostId,
quotingPostId: newStatus.quotingPostId,
},
});
break;
}
case "Patch": {
const patch = body as Patch;
// Store the object in the LysandObject table
await LysandObject.createFromObject(patch);
await createFromObject(patch);
// Edit the status
const content = getBestContentType(patch.contents);
const emojis = await EmojiAction.parseEmojis(content?.content || "");
const emojis = await parseEmojis(content?.content || "");
const status = await Status.findOneBy({
id: patch.patched_id,
const status = await client.status.findUnique({
where: {
uri: patch.patched_id,
},
include: statusAndUserRelations,
});
if (!status) {
@ -198,64 +207,81 @@ export default async (
}
status.content = content?.content || "";
status.content_type = content?.content_type || "text/plain";
status.spoiler_text = patch.subject || "";
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.in_reply_to_post = await Status.fetchFromRemote(
body.replies_to[0]
);
status.inReplyToPostId =
(await fetchFromRemote(body.replies_to[0]))?.id || null;
}
// Same for quotes
if (body.quotes.length > 0) {
status.quoting_post = await Status.fetchFromRemote(
body.quotes[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": {
// Store the object in the LysandObject table
await LysandObject.createFromObject(body);
await createFromObject(body);
break;
}
case "Dislike": {
// Store the object in the LysandObject table
await LysandObject.createFromObject(body);
await createFromObject(body);
break;
}
case "Follow": {
// Store the object in the LysandObject table
await LysandObject.createFromObject(body);
await createFromObject(body);
break;
}
case "FollowAccept": {
// Store the object in the LysandObject table
await LysandObject.createFromObject(body);
await createFromObject(body);
break;
}
case "FollowReject": {
// Store the object in the LysandObject table
await LysandObject.createFromObject(body);
await createFromObject(body);
break;
}
case "Announce": {
// Store the object in the LysandObject table
await LysandObject.createFromObject(body);
await createFromObject(body);
break;
}
case "Undo": {
// Store the object in the LysandObject table
await LysandObject.createFromObject(body);
await createFromObject(body);
break;
}
case "Extension": {
// Store the object in the LysandObject table
await LysandObject.createFromObject(body);
await createFromObject(body);
break;
}
default: {

View file

@ -4,7 +4,8 @@ import { applyConfig } from "@api";
import { getConfig } from "@config";
import { errorResponse, jsonResponse } from "@response";
import { MatchedRoute } from "bun";
import { UserAction, userRelations } from "~database/entities/User";
import { client } from "~database/datasource";
import { userRelations, userToLysand } from "~database/entities/User";
export const meta = applyConfig({
allowedMethods: ["POST"],
@ -29,16 +30,16 @@ export default async (
const config = getConfig();
const user = await UserAction.findOne({
const user = await client.user.findUnique({
where: {
id: uuid,
},
relations: userRelations,
include: userRelations,
});
if (!user) {
return errorResponse("User not found", 404);
}
return jsonResponse(user.toLysand());
return jsonResponse(userToLysand(user));
};

View file

@ -1,10 +1,12 @@
import { jsonResponse } from "@response";
import { MatchedRoute } from "bun";
import { userRelations } from "~database/entities/User";
import { getConfig, getHost } from "@config";
import { applyConfig } from "@api";
import { Status } from "~database/entities/Status";
import { In } from "typeorm";
import {
statusAndUserRelations,
statusToLysand,
} from "~database/entities/Status";
import { client } from "~database/datasource";
export const meta = applyConfig({
allowedMethods: ["GET"],
@ -29,26 +31,25 @@ export default async (
const pageNumber = Number(matchedRoute.query.page) || 1;
const config = getConfig();
const statuses = await Status.find({
const statuses = await client.status.findMany({
where: {
account: {
id: uuid,
authorId: uuid,
visibility: {
in: ["public", "unlisted"],
},
visibility: In(["public", "unlisted"]),
},
relations: userRelations,
take: 20,
skip: 20 * (pageNumber - 1),
include: statusAndUserRelations,
});
const totalStatuses = await Status.count({
const totalStatuses = await client.status.count({
where: {
account: {
id: uuid,
authorId: uuid,
visibility: {
in: ["public", "unlisted"],
},
visibility: In(["public", "unlisted"]),
},
relations: userRelations,
});
return jsonResponse({
@ -65,6 +66,6 @@ export default async (
pageNumber > 1
? `${getHost()}/users/${uuid}/outbox?page=${pageNumber - 1}`
: undefined,
items: statuses.map(s => s.toLysand()),
items: statuses.map(s => statusToLysand(s)),
});
};