194 lines
8.2 KiB
TypeScript
194 lines
8.2 KiB
TypeScript
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 uuid, { Page } from "../server/decorators/page.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";
|
|
import RestResponse from "../server/RestResponse.ts";
|
|
|
|
export default class ImageRouter extends AppRouter {
|
|
public static deserialize(image: Image, me?: string) {
|
|
return {
|
|
author: image.author,
|
|
created: image.created,
|
|
name: image.name,
|
|
visibility: image.visibility,
|
|
id: image._id,
|
|
file: image.file,
|
|
likes: image.likes?.length ?? 0,
|
|
liked: image.likes?.includes(me!) ?? false,
|
|
};
|
|
}
|
|
|
|
@rest('GET', '/')
|
|
async get(@schema('uuid') id: UUID, @jwt('salt', false) @auth() jwt?: JWTPayload) {
|
|
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.");
|
|
|
|
return ImageRouter.deserialize(image, jwt?.name);
|
|
}
|
|
@rest('GET', '/img/:id')
|
|
async file(id: string, @jwt('salt', false) @auth() jwt?: JWTPayload) {
|
|
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]);
|
|
}
|
|
@rest('GET')
|
|
async feed(@page() page: Page, @jwt('salt', false) @auth() jwt?: JWTPayload) {
|
|
const res = await page.apply(this.db.images.find({ visibility: Visibility.Public })).sort({ created: -1 }).toArray();
|
|
if (!res) throw new HttpError('User not found.');
|
|
return res.map(v => ImageRouter.deserialize(v, jwt?.name));
|
|
}
|
|
@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 });
|
|
|
|
return (await page.apply(cursor.sort({ created: -1 })).toArray()).map(v => ImageRouter.deserialize(v, jwt?.name));
|
|
}
|
|
|
|
@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?' });
|
|
req.name ??= new UUID().toString();
|
|
req.visibility ??= 0;
|
|
if (req.visibility < 0 || req.visibility > 2) throw new HttpError("body.visibility: Must be 0, 1, or 2");
|
|
const id = new UUID();
|
|
// Create file
|
|
const img: Image = {
|
|
_id: id,
|
|
author: user.username,
|
|
created: now(),
|
|
name: req.name!,
|
|
visibility: req.visibility,
|
|
file: `${id}.${ext}`,
|
|
likes: [],
|
|
};
|
|
await Deno.mkdir('images', { recursive: true });
|
|
const out = await Deno.open(`images/${id}.${ext}`, { write: true, create: true });
|
|
|
|
for await (const bit of rawFile.stream()) out.write(bit);
|
|
out.close();
|
|
|
|
// Write to DB
|
|
try {
|
|
await this.db.images.insertOne(img);
|
|
await this.db.users.updateOne({ username: user.username }, { $push: { images: id } });
|
|
return ImageRouter.deserialize(img);
|
|
}
|
|
catch (e) {
|
|
await Deno.remove(`images/${id}.${ext}`);
|
|
throw e;
|
|
}
|
|
}
|
|
@rest('POST', '/change')
|
|
async change(@uuid() id: UUID, @body() raw: unknown, @jwt(v => v.salt, true) @auth() jwt: JWTPayload) {
|
|
const body = await convert(raw, { name: 'string?', visibility: 'number?' });
|
|
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.updateOne({ _id: id }, { $set: { name: body.name, visibility: body.visibility } });
|
|
return ImageRouter.deserialize(img);
|
|
}
|
|
|
|
@rest('POST', '/like')
|
|
async like(@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 res = await this.db.images.updateOne(
|
|
{ _id: id },
|
|
{ $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 } }
|
|
);
|
|
|
|
|
|
return ImageRouter.deserialize((await this.db.images.findOne({ _id: id }))!);
|
|
}
|
|
@rest('POST', '/dislike')
|
|
async dislike(@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 res = await this.db.images.updateOne(
|
|
{ _id: id },
|
|
{ $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 }))!);
|
|
}
|
|
|
|
public constructor(private db: AppDatabase) {
|
|
super();
|
|
db.images.createIndexes({ indexes: [ { key: { created: -1 }, name: 'Image Order' } ] });
|
|
}
|
|
} |