Skip to content

Commit

Permalink
feat: more stuff lol
Browse files Browse the repository at this point in the history
  • Loading branch information
redraskal committed Feb 24, 2024
1 parent 805f189 commit 24b9da8
Show file tree
Hide file tree
Showing 20 changed files with 122 additions and 91 deletions.
Binary file modified bun.lockb
Binary file not shown.
5 changes: 2 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,11 @@
"dev": "bunx gateway dev",
"gen": "bunx gateway gen",
"prettier": "prettier --write .",
"migrate:gen": "drizzle-kit generate:sqlite --schema src/schema",
"migrate": "bun ./src/migrate"
"drizzle": "drizzle-kit generate:sqlite --schema src/schema"
},
"dependencies": {
"drizzle-orm": "^0.29.0",
"gateway": "link:gateway",
"gateway": "redraskal/gateway#cce37eb",
"nanoid": "^5.0.3",
"zod": "^3.22.4"
}
Expand Down
24 changes: 11 additions & 13 deletions pages/[slug].ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { MatchedRoute } from "bun";
import { Data, Route, html, meta } from "gateway";
import type { MatchedRoute } from "bun";
import { type Data, Route, html, meta } from "gateway";
import { database } from "../src/database";
import { inferAccount } from "../src/middleware/auth";
import { Clips } from "../src/schema/clips";
Expand Down Expand Up @@ -57,27 +57,25 @@ export default class implements Route {
head(data: Data<this>) {
return (
meta({
title: data ? data.account.username + " | Clips" : "404 | Clips",
title: `${data?.account.username || "404"} | Clips`,
}) + style
);
}

body(data: Data<this>) {
if (!data) return Response.redirect("/404");
if (!data) return Response.redirect("/404", 302);

return site({
path: `/@${data.account.username}`,
account: data._account,
body: html`
${data.account.discord_avatar_hash
? html`
<br />
<img
src="https://cdn.discordapp.com/avatars/${data.account.discord_id}/${data.account
.discord_avatar_hash}.png"
/>
`
: ""}
${data.account.discord_avatar_hash &&
html`
<br />
<img
src="https://cdn.discordapp.com/avatars/${data.account.discord_id}/${data.account.discord_avatar_hash}.png"
/>
`}
<h2>${data.account.username}</h2>
<p>Registered ${dateTimeFormat.format(snowflakeToDate(BigInt(data.account.id)))}</p>
${data.clips.length > 0 ? clipPreviews(data.clips) : html`<p>No clips found.</p>`}
Expand Down
33 changes: 28 additions & 5 deletions pages/content/[scope]/[file].ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,42 @@
import { MatchedRoute } from "bun";
import { Data, Route } from "gateway";
import type { MatchedRoute } from "bun";
import type { Data, Route } from "gateway";
import { join } from "path";

const cacheHeader = {
"Cache-Control": "max-age=3600",
};

export default class implements Route {
data(_: Request, route: MatchedRoute) {
data(req: Request, route: MatchedRoute) {
const [_start = 0, _end = Infinity] = req.headers.get("Range")?.split("=").at(-1)?.split("-").map(Number) || [];

return {
_start: Math.max(0, _start),
_end: Math.max(_start + 3e7, _end), // at least a 30MB chunk
_path: join(process.env.STORAGE_PATH!, `${route.params.scope}/${route.params.file}`),
};
}

body(data: Data<this>) {
return new Response(Bun.file(data._path).stream(), {
const file = Bun.file(data._path);

if (file.type.indexOf("video/") != 0) {
return new Response(file, {
headers: cacheHeader,
});
}

data._end = Math.min(file.size - 1, data._end);
const incomplete = data._end < file.size - 1;

return new Response(file.slice(data._start, data._end), {
headers: {
"Cache-Control": "max-age=3600",
...cacheHeader,
...{
"Content-Range": `bytes ${data._start}-${data._end}/${file.size - 1}`,
},
},
status: incomplete ? 206 : undefined,
});
}
}
16 changes: 7 additions & 9 deletions pages/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Data, Route, cache, html, meta } from "gateway";
import { type Data, Route, cache, html, meta } from "gateway";
import { inferAccount } from "../src/middleware/auth";
import { database } from "../src/database";
import { Clips } from "../src/schema/clips";
Expand Down Expand Up @@ -46,7 +46,6 @@ export default class implements Route {
const account = inferAccount(req);

const recentlyUploaded = account ? selectRecentlyUploadedClips.all({ uploader_id: account.id }) : [];

const fromFriends = selectClipsFromFriends.all({ account_id: account?.id || 0 });

return {
Expand All @@ -69,13 +68,12 @@ export default class implements Route {
path: "/",
account: data._account,
body: html`
${data.recentlyUploaded.length > 0
? html`
<h2>Recently uploaded</h2>
${clipPreviews(data.recentlyUploaded)}
<a href="/@${data._account?.username}">More clips -></a>
`
: ""}
${data.recentlyUploaded.length > 0 &&
html`
<h2>Recently uploaded</h2>
${clipPreviews(data.recentlyUploaded)}
<a href="/@${data._account?.username}">More clips -></a>
`}
<h2>From friends</h2>
${clipPreviews(data.fromFriends)}
<script>
Expand Down
17 changes: 10 additions & 7 deletions pages/login.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Data, Route, cache, html, meta } from "gateway";
import { type Data, Route, cache, html, meta } from "gateway";
import { ensureSignedOut } from "../src/middleware/auth";
import { MatchedRoute } from "bun";
import type { MatchedRoute } from "bun";
import { Accounts } from "../src/schema/accounts";
import { database } from "../src/database";
import { Sessions } from "../src/schema/sessions";
Expand Down Expand Up @@ -105,12 +105,15 @@ export default class implements Route {

body(data: Data<this>, err?: Error) {
if (data?.token) {
const response = Response.redirect("/", 302);
const expiresAt = new Date(Date.now() + 365 * 24 * 60 * 60 * 1000).toUTCString();
return Response.redirect("/", {
headers: {
"set-cookie": `clips=${encodeURIComponent(data.token)}; Expires=${expiresAt}; SameSite: none; Secure; HttpOnly`,
},
});

response.headers.set(
"Set-Cookie",
`clips=${encodeURIComponent(data.token)}; Expires=${expiresAt}; SameSite: none; Secure; HttpOnly`
);

return response;
}

return site({
Expand Down
13 changes: 8 additions & 5 deletions pages/logout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,19 @@ export default class implements Route {
@ensureSignedIn()
async data(req: Request) {
const token = sessionToken(req);

if (!token) return {};

deleteSession.run({ token });

return {};
}

body() {
return Response.redirect("/login", {
headers: {
"Set-Cookie": "clips=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT",
},
});
const response = Response.redirect("/login", 302);

response.headers.set("Set-Cookie", "clips=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT");

return response;
}
}
6 changes: 3 additions & 3 deletions pages/search.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { MatchedRoute } from "bun";
import { Data, Route, cache, html, meta } from "gateway";
import type { MatchedRoute } from "bun";
import { type Data, Route, cache, html, meta } from "gateway";
import { sqlite } from "../src/database";
import { site } from "../src/templates/site";
import { inferAccount } from "../src/middleware/auth";
import { ClipPreview, clipPreviews } from "../src/templates/clipPreviews";
import { type ClipPreview, clipPreviews } from "../src/templates/clipPreviews";
import { z } from "zod";
import { style } from "../src/templates/style";

Expand Down
2 changes: 1 addition & 1 deletion pages/upload.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Data, Route, cache, html, meta } from "gateway";
import { type Data, Route, cache, html, meta } from "gateway";
import { ensureSignedIn, inferAccount } from "../src/middleware/auth";
import { mkdir } from "fs/promises";
import { join, parse } from "path";
Expand Down
27 changes: 15 additions & 12 deletions pages/watch/[id].ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,12 +56,12 @@ export default class implements Route {
// TODO: this does not work well as a prepared statement on drizzle with a dynamic body
database.update(Clips).set(body).where(eq(Clips.id, route.params.id)).run();

return { edited: true };
return {};
} else if (req.method == "DELETE" || (await req.formData()).get("_method") == "DELETE") {
const root = join(process.env.STORAGE_PATH!, data.clips.uploader_id);

for (const ext of ["mp4", "jpg"]) {
await unlink(join(root, data.clips.id + "." + ext));
await unlink(join(root, `${data.clips.id}.${ext}`));
}

database.delete(Clips).where(eq(Clips.id, route.params.id)).run();
Expand All @@ -80,34 +80,37 @@ export default class implements Route {
}

head(data: Data<this>) {
if (!data.clip) return "";
return (
meta({
title: data.clip!.title + " | Clips",
description: data.clip!.description || undefined,
title: data.clip.title + " | Clips",
description: data.clip.description,
imageURL: `${process.env.WEBSITE_ROOT || ""}${data._root}.jpg`,
}) + style
);
}

body(data: Data<this>) {
if (data.deleted) return Response.redirect("/", 302);
if (!data.clip) throw new Error("Clip not available in body.");

incrementViews.run({ id: data.clip!.id });
// only increment views when clip is seen in a browser
incrementViews.run({ id: data.clip.id });
data.clip!.views += 1;

const views = pluralize(data.clip!.views, "view");
const views = pluralize(data.clip.views, "view");

// description must contain no whitespace because css :empty is fun
// prettier-ignore
return site({
path: `/watch/${data.clip!.id}`,
path: `/watch/${data.clip.id}`,
account: data._account,
body: html`
<video src="${data._root}.mp4" poster="${data._root}.jpg" autoplay muted controls loop download></video>
<h1 ${data.editable ? "contenteditable" : ""}>${data.clip!.title}</h1>
<p><a href="/@${data.uploader!.username}">${data.uploader!.username}</a>${views}</p>
<p ${data.editable && 'placeholder="Click to edit description." contenteditable'}>${data.clip!.description || ""}</p>
<a href="${data._root}.mp4" download="${data.clip!.title}.mp4"><button>Download</button></a>
<video src="${data._root}.mp4" poster="${data._root}.jpg" autoplay muted controls loop></video>
<h1 ${data.editable && "contenteditable"}>${data.clip.title}</h1>
<p><a href="/@${data.uploader!.username}">${data.uploader.username}</a>${views}</p>
<p ${data.editable && 'placeholder="Click to edit description." contenteditable'}>${data.clip.description}</p>
<a href="${data._root}.mp4" download="${data.clip.title}.mp4"><button>Download</button></a>
${data.editable
&& html`
<form method="POST">
Expand Down
2 changes: 1 addition & 1 deletion public/js/clips/edit.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ function save() {
}

function shouldFocus() {
return !document.activeElement.getAttribute("contenteditable") && document.activeElement.tagName !== "INPUT";
return !document.activeElement.hasAttribute("contenteditable") && document.activeElement.tagName !== "INPUT";
}

title.addEventListener("keydown", (e) => {
Expand Down
7 changes: 4 additions & 3 deletions public/js/clips/upload.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,15 @@ const progress = document.getElementById("progress");

drag.addEventListener("dragover", (e) => e.preventDefault());
drag.addEventListener("click", () => file.click());
file.addEventListener("change", async (e) => {
await upload(e.target.files);
});
drag.addEventListener("drop", async (e) => {
e.preventDefault();
await upload(e.dataTransfer.files);
});

file.addEventListener("change", async (e) => {
await upload(e.target.files);
});

function pushProgress(text) {
progress.innerText += text;
progress.innerHTML += "<br />";
Expand Down
2 changes: 1 addition & 1 deletion public/js/clips/watch.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ video.addEventListener("seeked", () => {
const url = new URL(location.href);
seeked = true;

url.searchParams.set("t", video.currentTime);
url.searchParams.set("t", video.currentTime.toFixed(2));
history.pushState(null, document.title, url);
});

Expand Down
7 changes: 6 additions & 1 deletion src/database.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import { drizzle } from "drizzle-orm/bun-sqlite";
import Database from "bun:sqlite";
import { drizzle } from "drizzle-orm/bun-sqlite";
import { migrate } from "drizzle-orm/bun-sqlite/migrator";

export const sqlite = new Database(process.env.DATABASE || "clips.sqlite");
export const database = drizzle(sqlite);

if (process.env.DATABASE_MIGRATE) {
migrate(database, { migrationsFolder: "./drizzle" });
}
5 changes: 0 additions & 5 deletions src/index.ts

This file was deleted.

2 changes: 1 addition & 1 deletion src/middleware/auth.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { MatchedRoute } from "bun";
import type { MatchedRoute } from "bun";
import { RouteError } from "gateway";
import { database } from "../database";
import { Sessions } from "../schema/sessions";
Expand Down
4 changes: 0 additions & 4 deletions src/migrate.ts

This file was deleted.

37 changes: 22 additions & 15 deletions src/templates/clipPreviews.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,23 +12,30 @@ export type ClipPreview = {
};

export function clipPreviews(clips: ClipPreview[]) {
// prettier-ignore
return html`
<ul class="clips">
${clips.map(
(clip) => html`
<li>
<div onclick="watch('${clip.id}')">
<video src="/content/${clip.uploader_id}/${clip.id}.mp4" poster="/content/${clip.uploader_id}/${clip.id}.jpg" muted loop preload="none" onmouseover="this.play()" onmouseout="this.pause()"></video>
<span>${formatDuration(clip.video_duration)}</span>
<span>${formatViews(clip.views)}</span>
<span>${dateTimeFormat.format(snowflakeToDate(BigInt(clip.id)))}</span>
</div>
<b>${clip.title}</b>
<p><a href="/@${clip.username}">${clip.username}</a></p>
</li>
`
)}
${clips.map(
(clip) => html`
<li>
<div onclick="watch('${clip.id}')">
<video
src="/content/${clip.uploader_id}/${clip.id}.mp4"
poster="/content/${clip.uploader_id}/${clip.id}.jpg"
muted
loop
preload="none"
onmouseover="this.play()"
onmouseout="this.pause()"
></video>
<span>${formatDuration(clip.video_duration)}</span>
<span>${formatViews(clip.views)}</span>
<span>${dateTimeFormat.format(snowflakeToDate(BigInt(clip.id)))}</span>
</div>
<b>${clip.title}</b>
<p><a href="/@${clip.username}">${clip.username}</a></p>
</li>
`
)}
</ul>
`;
}
2 changes: 1 addition & 1 deletion src/templates/site.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export function site(params: SiteParams) {
/>
</form>
<ul>
<li>${params.account ? html`<a href="/logout">Logout</a>` : html`<a href="/login">Login</a>`}</li>
<li><a href="/${params.account ? "logout" : "login"}">${params.account ? "Logout" : "Login"}</a></li>
</ul>
</nav>
<main>${params.body}</main>
Expand Down
Loading

0 comments on commit 24b9da8

Please sign in to comment.