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"; import RestResponse from "../server/RestResponse.ts"; import { Page } from "../server/decorators/page.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('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)); } @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 ??= rawFile.name; 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(@schema('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', '/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); } @rest('POST', '/like') async like(@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 res = await this.db.images.updateOne( { _id: id, visibility: { $not: { $eq: Visibility.Private } } }, { $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 }))!, jwt?.name); } @rest('POST', '/unlike') async unlike(@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 res = await this.db.images.updateOne( { _id: id, visibility: { $not: { $eq: Visibility.Private } } }, { $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' } ] }); } }