clonegur/backend/routers/ImageRouter.ts

221 lines
9.6 KiB
TypeScript
Raw Normal View History

2023-06-29 14:31:25 +00:00
import { UUID } from "https://deno.land/x/web_bson@v0.3.0/mod.js";
import { auth, body, headers, jwt, page, rest, schema } from "../server/decorators.ts";
import { JWTPayload } from "../utils/JWT.ts";
import AppRouter from "./AppRouter.ts";
import HttpError from "../server/HttpError.ts";
import Image, { Visibility } from "../models/Image.ts";
import { Headers } from "../server/RestRequest.ts";
import { convert } from "../server/decorators/schema.ts";
import { now } from "../utils/utils.ts";
import AppDatabase from "../AppDatabase.ts";
2023-06-30 00:37:07 +00:00
import RestResponse from "../server/RestResponse.ts";
2023-06-30 20:01:08 +00:00
import { Page } from "../server/decorators/page.ts";
2023-06-29 14:31:25 +00:00
export default class ImageRouter extends AppRouter {
2023-06-30 19:04:13 +00:00
public static deserialize(image: Image, me?: string) {
2023-06-29 14:31:25 +00:00
return {
author: image.author,
created: image.created,
name: image.name,
visibility: image.visibility,
id: image._id,
2023-06-30 10:19:21 +00:00
file: image.file,
2023-06-30 19:04:13 +00:00
likes: image.likes?.length ?? 0,
liked: image.likes?.includes(me!) ?? false,
2023-06-29 14:31:25 +00:00
};
}
@rest('GET', '/')
2023-06-30 12:57:17 +00:00
async get(@schema('uuid') id: UUID, @jwt('salt', false) @auth() jwt?: JWTPayload) {
2023-06-29 14:31:25 +00:00
const image = await this.db.images.findOne({ _id: new UUID(id) });
if (
!image ||
image.visibility === Visibility.Private && image.author !== jwt?.name
) throw new HttpError("Image doesn't exist.");
2023-06-30 19:04:13 +00:00
return ImageRouter.deserialize(image, jwt?.name);
2023-06-29 14:31:25 +00:00
}
2023-06-30 00:37:07 +00:00
@rest('GET', '/img/:id')
2023-06-30 12:57:17 +00:00
async file(id: string, @jwt('salt', false) @auth() jwt?: JWTPayload) {
2023-06-30 00:37:07 +00:00
try {
const start = await Deno.realPath("images");
const file = await Deno.realPath(`images/${id}`);
if (!file.startsWith(start)) throw new HttpError("What the fuck are you doing?", 418);
}
catch (e) {
if (!(e instanceof HttpError)) throw new HttpError("File doesn't exist.", 404);
}
const img = await this.db.images.findOne({ _id: new UUID(id.split('.')[0]) }).catch(() => undefined);
if (img?.visibility === Visibility.Private && img.author !== jwt?.name) throw new HttpError("File doesn't exist.", 404);
return new RestResponse()
.body((await Deno.open(`images/${id}`)).readable)
.contentType(id.split('.')[1]);
}
2023-06-30 12:57:17 +00:00
@rest('GET')
2023-06-30 19:04:13 +00:00
async feed(@page() page: Page, @jwt('salt', false) @auth() jwt?: JWTPayload) {
2023-06-30 10:19:21 +00:00
const res = await page.apply(this.db.images.find({ visibility: Visibility.Public })).sort({ created: -1 }).toArray();
2023-06-29 14:31:25 +00:00
if (!res) throw new HttpError('User not found.');
2023-06-30 19:04:13 +00:00
return res.map(v => ImageRouter.deserialize(v, jwt?.name));
2023-06-29 14:31:25 +00:00
}
2023-06-30 12:57:17 +00:00
@rest('GET', '/feed/:username')
async userFeed(username: string, @page() page: Page, @jwt('salt', false) @auth() jwt?: JWTPayload) {
const user = await this.db.users.findOne({ username });
if (user === undefined) throw new HttpError("User not found.");
let cursor;
if (user.username === jwt?.name) cursor = this.db.images.find({ _id: { $in: user.images } });
else cursor = this.db.images.find({ _id: { $in: user.images }, visibility: Visibility.Public });
2023-06-30 19:04:13 +00:00
return (await page.apply(cursor.sort({ created: -1 })).toArray()).map(v => ImageRouter.deserialize(v, jwt?.name));
2023-06-30 12:57:17 +00:00
}
2023-06-30 22:48:24 +00:00
@rest('GET', '/likes/:username')
async likeFeed(username: string, @page() page: Page, @jwt('salt', false) @auth() jwt?: JWTPayload) {
const user = await this.db.users.findOne({ username });
if (user === undefined) throw new HttpError("User not found.");
const cursor = this.db.images.find({ $or: [
{ _id: { $in: user.likes }, visibility: Visibility.Public, },
{ _id: { $in: user.likes }, author: user.username, }
] });
return (await page.apply(cursor.sort({ created: -1 })).toArray()).map(v => ImageRouter.deserialize(v, jwt?.name));
}
2023-06-29 14:31:25 +00:00
@rest('POST', '/upload')
async upload(@body() body: Blob, @headers() headers: Headers, @jwt(v => v.salt, true) @auth() jwt: JWTPayload) {
const user = await this.db.users.findOne({ username: jwt.name });
if (!user) throw new HttpError("You don't exist.");
// Parse body
const contentType = headers['content-type'] + "";
if (!contentType.startsWith('multipart/form-data')) {
throw new HttpError("Expected a 'Content-type: multipart/form-data; ...' header.");
}
const data = await new Request('http://127.0.0.1', {
body, headers: [ ['content-type', contentType ] ],
method: 'post'
}).formData();
// Clean up data
if (!data.has('file')) throw new HttpError("Expected a 'file' entry in form data.");
if (!data.has('body')) throw new HttpError("Expected a 'body' entry in form data.");
let rawFile: File = data.get('file') as File;
let rawReq: string = data.get('body') as string;
if (typeof rawFile === 'string') rawFile = new File([rawFile], 'unknown');
if (typeof rawReq !== 'string') rawReq = await (rawReq as Blob).text();
if (rawFile.size > (1 << 20) * 2) throw new HttpError("File too large (max 2MB).");
// Extract (and check) extension
const pointI = rawFile.name.lastIndexOf(".");
if (pointI < 0) throw new HttpError("Given file has no extension.");
const ext = rawFile.name.substring(pointI + 1).trim();
if (ext === "") throw new HttpError("Given file has no extension.");
// Clean up request
const req = await convert(rawReq, { name: 'string?', visibility: 'number?' });
2023-06-30 20:52:08 +00:00
req.name ??= rawFile.name;
2023-06-30 12:57:17 +00:00
req.visibility ??= 0;
2023-06-29 14:31:25 +00:00
if (req.visibility < 0 || req.visibility > 2) throw new HttpError("body.visibility: Must be 0, 1, or 2");
2023-06-30 10:19:21 +00:00
const id = new UUID();
2023-06-29 14:31:25 +00:00
// Create file
2023-06-30 10:19:21 +00:00
const img: Image = {
_id: id,
author: user.username,
created: now(),
name: req.name!,
visibility: req.visibility,
2023-06-30 19:04:13 +00:00
file: `${id}.${ext}`,
likes: [],
2023-06-30 10:19:21 +00:00
};
2023-06-29 14:31:25 +00:00
await Deno.mkdir('images', { recursive: true });
2023-06-30 10:19:21 +00:00
const out = await Deno.open(`images/${id}.${ext}`, { write: true, create: true });
2023-06-29 14:31:25 +00:00
for await (const bit of rawFile.stream()) out.write(bit);
out.close();
// Write to DB
try {
await this.db.images.insertOne(img);
2023-06-30 10:19:21 +00:00
await this.db.users.updateOne({ username: user.username }, { $push: { images: id } });
2023-06-29 14:31:25 +00:00
return ImageRouter.deserialize(img);
}
catch (e) {
2023-06-30 10:19:21 +00:00
await Deno.remove(`images/${id}.${ext}`);
2023-06-29 14:31:25 +00:00
throw e;
}
}
@rest('POST', '/change')
2023-06-30 20:01:08 +00:00
async change(@schema('uuid') id: UUID, @body() raw: unknown, @jwt(v => v.salt, true) @auth() jwt: JWTPayload) {
2023-06-30 10:19:21 +00:00
const body = await convert(raw, { name: 'string?', visibility: 'number?' });
2023-06-29 14:31:25 +00:00
const user = await this.db.users.findOne({ username: jwt.name });
if (!user) throw new HttpError("You don't exist.");
2023-06-30 10:19:21 +00:00
const img = await this.db.images.findOne({ _id: id });
2023-06-29 14:31:25 +00:00
if (!img) throw new HttpError("Image doesn't exist.");
if (user.username !== img.author) throw new HttpError("You don't own the image.");
2023-06-30 10:19:21 +00:00
await this.db.images.updateOne({ _id: id }, { $set: { name: body.name, visibility: body.visibility } });
2023-06-29 14:31:25 +00:00
return ImageRouter.deserialize(img);
}
2023-06-30 20:52:08 +00:00
@rest('POST', '/delete')
async delete(@schema('uuid') id: UUID, @jwt(v => v.salt, true) @auth() jwt: JWTPayload) {
const user = await this.db.users.findOne({ username: jwt.name });
if (!user) throw new HttpError("You don't exist.");
const img = await this.db.images.findOne({ _id: id });
if (!img) throw new HttpError("Image doesn't exist.");
if (user.username !== img.author) throw new HttpError("You don't own the image.");
await this.db.images.deleteOne({ _id: id });
await this.db.users.updateMany({}, {
$pull: { images: id, likes: id }
});
return ImageRouter.deserialize(img);
}
2023-06-29 14:31:25 +00:00
2023-06-30 19:04:13 +00:00
@rest('POST', '/like')
2023-06-30 20:01:08 +00:00
async like(@schema('uuid') id: UUID, @jwt(v => v.salt, true) @auth() jwt: JWTPayload) {
2023-06-30 19:04:13 +00:00
const user = await this.db.users.findOne({ username: jwt.name });
if (!user) throw new HttpError("You don't exist.");
const res = await this.db.images.updateOne(
2023-06-30 22:48:24 +00:00
{ _id: id, visibility: { $not: { $eq: Visibility.Private } } },
2023-06-30 19:04:13 +00:00
{ $addToSet: { likes: jwt.name } }
);
if (res.matchedCount === 0) throw new HttpError("Image doesn't exist.");
await this.db.users.updateOne(
{ username: jwt.name },
{ $addToSet: { likes: id } }
);
2023-06-30 20:01:08 +00:00
return ImageRouter.deserialize((await this.db.images.findOne({ _id: id }))!, jwt?.name);
2023-06-30 19:04:13 +00:00
}
2023-06-30 20:01:08 +00:00
@rest('POST', '/unlike')
async unlike(@schema('uuid') id: UUID, @jwt(v => v.salt, true) @auth() jwt: JWTPayload) {
2023-06-30 19:04:13 +00:00
const user = await this.db.users.findOne({ username: jwt.name });
if (!user) throw new HttpError("You don't exist.");
const res = await this.db.images.updateOne(
2023-06-30 22:48:24 +00:00
{ _id: id, visibility: { $not: { $eq: Visibility.Private } } },
2023-06-30 19:04:13 +00:00
{ $pull: { likes: jwt.name } }
);
if (res.matchedCount === 0) throw new HttpError("Image doesn't exist.");
await this.db.users.updateOne(
{ username: jwt.name },
{ $pull: { likes: id } }
);
return ImageRouter.deserialize((await this.db.images.findOne({ _id: id }))!);
}
2023-06-29 14:31:25 +00:00
public constructor(private db: AppDatabase) {
super();
2023-06-30 10:19:21 +00:00
db.images.createIndexes({ indexes: [ { key: { created: -1 }, name: 'Image Order' } ] });
2023-06-29 14:31:25 +00:00
}
}