Compare commits
5 Commits
daed4245fd
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| f3d84f3c55 | |||
| 19e0ae4605 | |||
| 6313364ba4 | |||
| 064528b178 | |||
| f0e522b039 |
@@ -1,11 +1,12 @@
|
|||||||
import { Link, useParams } from "react-router-dom";
|
import { Link, useParams } from "react-router-dom";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { css } from "@emotion/css";
|
import { css } from "@emotion/css";
|
||||||
|
import { Game } from "../server/util/types";
|
||||||
// import { type Pico8Player } from "@athingperday/react-pico-player";
|
// import { type Pico8Player } from "@athingperday/react-pico-player";
|
||||||
|
|
||||||
type Info = {
|
type Info = {
|
||||||
author: string | null;
|
author: string | null;
|
||||||
games: string[];
|
games: Game[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const AuthorPage = () => {
|
export const AuthorPage = () => {
|
||||||
@@ -44,11 +45,23 @@ export const AuthorPage = () => {
|
|||||||
`}
|
`}
|
||||||
>
|
>
|
||||||
<h1>{author}</h1>
|
<h1>{author}</h1>
|
||||||
|
<div
|
||||||
|
className={css`
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
`}
|
||||||
|
>
|
||||||
{info.games.map((game) => (
|
{info.games.map((game) => (
|
||||||
<Link key={game} to={`/u/${author}/${game}`}>
|
<Link
|
||||||
<h3>{game}</h3>
|
key={game.carts[0].name}
|
||||||
|
to={`/u/${author}/${game.carts[0].name}`}
|
||||||
|
>
|
||||||
|
<h3>
|
||||||
|
<img src={game.carts[0].src} />
|
||||||
|
</h3>
|
||||||
</Link>
|
</Link>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
+52
-1
@@ -15,6 +15,22 @@ type Game = {
|
|||||||
carts: Parameters<typeof Pico8Player>["0"]["carts"];
|
carts: Parameters<typeof Pico8Player>["0"]["carts"];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getPatch = (before: number[], after: number[]) => {
|
||||||
|
const diff: Record<number, number> = {};
|
||||||
|
for (const i in after) {
|
||||||
|
if (after[i] !== before[i]) {
|
||||||
|
diff[i] = after[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return diff;
|
||||||
|
};
|
||||||
|
|
||||||
|
const applyPatch = (before: number[], patch: Record<number, number>) => {
|
||||||
|
for (const i in patch) {
|
||||||
|
before[i] = patch[i];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export const GamePage = () => {
|
export const GamePage = () => {
|
||||||
const { author, slug } = useParams();
|
const { author, slug } = useParams();
|
||||||
// const [text, setText] = useState("");
|
// const [text, setText] = useState("");
|
||||||
@@ -46,6 +62,16 @@ export const GamePage = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (msg.gpioPatch) {
|
||||||
|
if (picoRef.current) {
|
||||||
|
const handle = picoRef.current;
|
||||||
|
if (handle) {
|
||||||
|
// console.log("updating pico gpio");
|
||||||
|
applyPatch(handle.gpio, msg.gpioPatch);
|
||||||
|
setPrevGpio([...handle.gpio]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const [game, setGame] = useState<Game | null>(null);
|
const [game, setGame] = useState<Game | null>(null);
|
||||||
@@ -70,7 +96,9 @@ export const GamePage = () => {
|
|||||||
if (JSON.stringify(handle.gpio) !== JSON.stringify(prevGpio)) {
|
if (JSON.stringify(handle.gpio) !== JSON.stringify(prevGpio)) {
|
||||||
if (prevGpio) {
|
if (prevGpio) {
|
||||||
setPrevGpio([...handle.gpio]);
|
setPrevGpio([...handle.gpio]);
|
||||||
socket.sendMessage({ gpio: handle.gpio });
|
socket.sendMessage({
|
||||||
|
gpioPatch: getPatch(prevGpio, handle.gpio),
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
socket.sendMessage({ getGpio: true });
|
socket.sendMessage({ getGpio: true });
|
||||||
setPrevGpio([...handle.gpio]);
|
setPrevGpio([...handle.gpio]);
|
||||||
@@ -127,6 +155,29 @@ export const GamePage = () => {
|
|||||||
<Pico8Player consoleRef={picoRef} carts={game.carts} />
|
<Pico8Player consoleRef={picoRef} carts={game.carts} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
className={css`
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
{game.carts.map((cart) =>
|
||||||
|
"src" in cart ? (
|
||||||
|
<div
|
||||||
|
key={cart.name}
|
||||||
|
className={css`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
<img src={cart.src} />
|
||||||
|
<a href={cart.src} download={`${cart.name}.p8.png`}>
|
||||||
|
Download
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
) : null,
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
{/* <div>
|
{/* <div>
|
||||||
<input onChange={(x) => setText(x.target.value)} />
|
<input onChange={(x) => setText(x.target.value)} />
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import { FirRouteInput, FirRouteOptions } from "../util/routewrap.ts";
|
|||||||
import { authors } from "../../data/authors.ts";
|
import { authors } from "../../data/authors.ts";
|
||||||
import { Octokit } from "octokit";
|
import { Octokit } from "octokit";
|
||||||
import { decrypt } from "../util/crypt.ts";
|
import { decrypt } from "../util/crypt.ts";
|
||||||
|
import { Game } from "../util/types.ts";
|
||||||
|
import { getGame, getGameFromSha } from "../util/getData.ts";
|
||||||
|
|
||||||
const method = "GET";
|
const method = "GET";
|
||||||
const url = "/api/author";
|
const url = "/api/author";
|
||||||
@@ -20,7 +22,7 @@ const handler = async ({ payload }: FirRouteInput<typeof payloadT>) => {
|
|||||||
games: [],
|
games: [],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
console.log("author", authorData);
|
// console.log("author", authorData);
|
||||||
|
|
||||||
const pat = decrypt({
|
const pat = decrypt({
|
||||||
password: process.env.ENCRYPTION_PASSWORD!,
|
password: process.env.ENCRYPTION_PASSWORD!,
|
||||||
@@ -38,16 +40,21 @@ const handler = async ({ payload }: FirRouteInput<typeof payloadT>) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// TODO: get the games.
|
// TODO: get the games.
|
||||||
const games: string[] = [];
|
const games: Game[] = [];
|
||||||
|
|
||||||
if (Array.isArray(gameStuff.data)) {
|
if (Array.isArray(gameStuff.data)) {
|
||||||
games.push(
|
const newGames = await Promise.all(
|
||||||
...gameStuff.data
|
gameStuff.data
|
||||||
.map((x) => x.name)
|
.filter((x) => x.name.endsWith(".p8.png") && x.type === "file")
|
||||||
.filter((x) => x.endsWith(".p8.png"))
|
.flatMap(async (x) => {
|
||||||
.map((x) => x.slice(0, -".p8.png".length)),
|
const name = x.name.slice(0, -".p8.png".length);
|
||||||
|
const game =
|
||||||
|
(await getGameFromSha(x.sha)) ??
|
||||||
|
(await getGame(authorData.username, name));
|
||||||
|
return game ? [game] : [];
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
console.log("hi");
|
games.push(...newGames.flat());
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ const handler = async ({ payload }: FirRouteInput<typeof payloadT>) => {
|
|||||||
if (!authorData) {
|
if (!authorData) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
console.log("author", authorData);
|
// console.log("author", authorData);
|
||||||
|
|
||||||
const pat = decrypt({
|
const pat = decrypt({
|
||||||
password: process.env.ENCRYPTION_PASSWORD!,
|
password: process.env.ENCRYPTION_PASSWORD!,
|
||||||
@@ -36,7 +36,7 @@ const handler = async ({ payload }: FirRouteInput<typeof payloadT>) => {
|
|||||||
})
|
})
|
||||||
).data;
|
).data;
|
||||||
|
|
||||||
console.log(gameData);
|
// console.log(gameData);
|
||||||
|
|
||||||
if (Array.isArray(gameData)) {
|
if (Array.isArray(gameData)) {
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -1,19 +0,0 @@
|
|||||||
import { Type } from "@sinclair/typebox";
|
|
||||||
import { FirRouteInput, FirRouteOptions } from "../util/routewrap.js";
|
|
||||||
|
|
||||||
const method = "POST";
|
|
||||||
const url = "/api/webhook-gh";
|
|
||||||
|
|
||||||
const payloadT = Type.Any();
|
|
||||||
|
|
||||||
const handler = ({payload}: FirRouteInput<typeof payloadT>) => {
|
|
||||||
console.log(payload);
|
|
||||||
return {};
|
|
||||||
};
|
|
||||||
|
|
||||||
export default {
|
|
||||||
method,
|
|
||||||
url,
|
|
||||||
payloadT,
|
|
||||||
handler,
|
|
||||||
} as const satisfies FirRouteOptions<typeof payloadT>;
|
|
||||||
+1
-1
@@ -9,7 +9,7 @@ const server = Fastify({
|
|||||||
logger: true,
|
logger: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
server.register(fastifyWebsocket);
|
server.register<any>(fastifyWebsocket, { server: server.server });
|
||||||
|
|
||||||
server.register(fastifyStatic, {
|
server.register(fastifyStatic, {
|
||||||
root: new URL("public", import.meta.url).toString().slice("file://".length),
|
root: new URL("public", import.meta.url).toString().slice("file://".length),
|
||||||
|
|||||||
@@ -2,8 +2,7 @@ import echo from "./api/echo.ts";
|
|||||||
import getAuthor from "./api/getAuthor.ts";
|
import getAuthor from "./api/getAuthor.ts";
|
||||||
import getGame from "./api/getGame.ts";
|
import getGame from "./api/getGame.ts";
|
||||||
import room from "./api/room.ts";
|
import room from "./api/room.ts";
|
||||||
import webhook from "./api/webhook.ts";
|
|
||||||
|
|
||||||
export const routeList = [echo, webhook, getAuthor, room, getGame];
|
export const routeList = [echo, getAuthor, room, getGame];
|
||||||
|
|
||||||
export type RouteList = typeof routeList;
|
export type RouteList = typeof routeList;
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
import { Octokit } from "octokit";
|
||||||
|
import { decrypt } from "./crypt";
|
||||||
|
import { authors } from "../../data/authors";
|
||||||
|
|
||||||
|
type Game = { carts: { name: string; src: string }[] };
|
||||||
|
|
||||||
|
const gameCache: Record<string, Game> = {};
|
||||||
|
|
||||||
|
export const getGame = async (
|
||||||
|
author: string,
|
||||||
|
name: string,
|
||||||
|
): Promise<Game | null> => {
|
||||||
|
const authorData = authors.find((x) => x.username === author);
|
||||||
|
|
||||||
|
if (!authorData) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const pat = decrypt({
|
||||||
|
password: process.env.ENCRYPTION_PASSWORD!,
|
||||||
|
cyphertext: authorData.auth.pat,
|
||||||
|
});
|
||||||
|
|
||||||
|
const octokit = new Octokit({
|
||||||
|
auth: pat,
|
||||||
|
});
|
||||||
|
|
||||||
|
const gameData = (
|
||||||
|
await octokit.rest.repos.getContent({
|
||||||
|
owner: authorData.username,
|
||||||
|
repo: authorData.repo,
|
||||||
|
path: `.picobook/${name}.p8.png`,
|
||||||
|
})
|
||||||
|
).data;
|
||||||
|
|
||||||
|
if (Array.isArray(gameData)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (gameData.type !== "file") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(gameData.sha in gameCache)) {
|
||||||
|
gameCache[gameData.sha] = {
|
||||||
|
carts: [{ name, src: `data:image/png;base64,${gameData.content}` }],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return gameCache[gameData.sha];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getGameFromSha = async (sha: string): Promise<Game | null> => {
|
||||||
|
if (gameCache[sha]) {
|
||||||
|
console.log("cache hit");
|
||||||
|
}
|
||||||
|
return gameCache[sha] ?? null;
|
||||||
|
};
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Static, TSchema } from "@sinclair/typebox";
|
import { Static, TSchema } from "@sinclair/typebox";
|
||||||
import { Value } from "@sinclair/typebox/value";
|
import { Value } from "@sinclair/typebox/value";
|
||||||
import { FastifyInstance, FastifyRequest, HTTPMethods } from "fastify"
|
import { FastifyInstance, FastifyRequest, HTTPMethods } from "fastify";
|
||||||
import { RouteOptions } from "fastify/types/route.js";
|
import { RouteOptions } from "fastify/types/route.js";
|
||||||
import { type WebSocket } from "@fastify/websocket";
|
import { type WebSocket } from "@fastify/websocket";
|
||||||
|
|
||||||
@@ -9,63 +9,80 @@ type WebsocketConnection = Parameters<Defined<RouteOptions["wsHandler"]>>[0];
|
|||||||
type URLString = string;
|
type URLString = string;
|
||||||
|
|
||||||
export type FirRouteInput<TPayloadSchema extends TSchema> = {
|
export type FirRouteInput<TPayloadSchema extends TSchema> = {
|
||||||
payload: Static<TPayloadSchema>,
|
payload: Static<TPayloadSchema>;
|
||||||
}
|
|
||||||
|
|
||||||
export type FirWebsocketInput<TPayloadSchema extends TSchema> = {
|
|
||||||
socket: WebsocketConnection,
|
|
||||||
req: FastifyRequest,
|
|
||||||
payload: Static<TPayloadSchema>,
|
|
||||||
}
|
|
||||||
|
|
||||||
export type FirWebsocketHandler<TIn extends TSchema = TSchema> = {
|
|
||||||
onMessage?(input: FirWebsocketInput<TIn>): void,
|
|
||||||
onOpen?(input: {socket: WebSocket, req: FastifyRequest}): void,
|
|
||||||
onClose?(input: {socket: WebSocket, req: FastifyRequest}): void,
|
|
||||||
onError?(input: {socket: WebSocket, req: FastifyRequest, error: unknown}): void,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type FirRouteOptions<TIn extends TSchema = TSchema, TOut extends TSchema = TSchema> = {
|
export type FirWebsocketInput<TPayloadSchema extends TSchema> = {
|
||||||
method: HTTPMethods,
|
socket: WebsocketConnection;
|
||||||
url: URLString,
|
req: FastifyRequest;
|
||||||
payloadT: TIn,
|
payload: Static<TPayloadSchema>;
|
||||||
responseT?: TOut,
|
};
|
||||||
} & ({
|
|
||||||
handler: (input: FirRouteInput<TIn>) => Static<TOut> | Promise<Static<TOut>>,
|
export type FirWebsocketHandler<TIn extends TSchema = TSchema> = {
|
||||||
} | {
|
onMessage?(input: FirWebsocketInput<TIn>): void;
|
||||||
websocket: FirWebsocketHandler<TIn>,
|
onOpen?(input: { socket: WebSocket; req: FastifyRequest }): void;
|
||||||
})
|
onClose?(input: { socket: WebSocket; req: FastifyRequest }): void;
|
||||||
|
onError?(input: {
|
||||||
|
socket: WebSocket;
|
||||||
|
req: FastifyRequest;
|
||||||
|
error: unknown;
|
||||||
|
}): void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type FirRouteOptions<
|
||||||
|
TIn extends TSchema = TSchema,
|
||||||
|
TOut extends TSchema = TSchema,
|
||||||
|
> = {
|
||||||
|
method: HTTPMethods;
|
||||||
|
url: URLString;
|
||||||
|
payloadT: TIn;
|
||||||
|
responseT?: TOut;
|
||||||
|
} & (
|
||||||
|
| {
|
||||||
|
handler: (
|
||||||
|
input: FirRouteInput<TIn>,
|
||||||
|
) => Static<TOut> | Promise<Static<TOut>>;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
websocket: FirWebsocketHandler<TIn>;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
type Defined<T> = T extends undefined ? never : T;
|
type Defined<T> = T extends undefined ? never : T;
|
||||||
|
|
||||||
export const attachRoute = <TIn extends TSchema, TOut extends TSchema>(server: FastifyInstance, routeOptions: FirRouteOptions<TIn, TOut>) => {
|
export const attachRoute = <TIn extends TSchema, TOut extends TSchema>(
|
||||||
const {
|
server: FastifyInstance,
|
||||||
method,
|
routeOptions: FirRouteOptions<TIn, TOut>,
|
||||||
url,
|
) => {
|
||||||
payloadT,
|
const { method, url, payloadT } = routeOptions;
|
||||||
} = routeOptions;
|
|
||||||
|
|
||||||
if ("websocket" in routeOptions) {
|
if ("websocket" in routeOptions) {
|
||||||
console.log('SETTING UP WS');
|
console.log("SETTING UP WS");
|
||||||
const {websocket} = routeOptions;
|
const { websocket } = routeOptions;
|
||||||
server.register(async function(fastify: FastifyInstance) {
|
server.register(async function (fastify: FastifyInstance) {
|
||||||
fastify.get('/api/ws/room', { websocket: true }, (socket: WebSocket, req: FastifyRequest) => {
|
fastify.get(
|
||||||
websocket.onOpen && websocket.onOpen({socket, req});
|
"/api/ws/room",
|
||||||
socket.on('message', (message: any) => {
|
{ websocket: true },
|
||||||
|
(socket: WebSocket, req: FastifyRequest) => {
|
||||||
|
websocket.onOpen && websocket.onOpen({ socket, req });
|
||||||
|
socket.on("message", (message: any) => {
|
||||||
const payload = JSON.parse(message.toString());
|
const payload = JSON.parse(message.toString());
|
||||||
if (Value.Check(payloadT, payload)) {
|
if (Value.Check(payloadT, payload)) {
|
||||||
websocket.onMessage && websocket.onMessage({socket, payload, req});
|
websocket.onMessage &&
|
||||||
|
websocket.onMessage({ socket, payload, req });
|
||||||
} else {
|
} else {
|
||||||
throw new Error("Payload wrong shape.");
|
throw new Error("Payload wrong shape.");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
socket.on('close', () => {
|
socket.on("close", () => {
|
||||||
websocket.onClose && websocket.onClose({socket, req});
|
websocket.onClose && websocket.onClose({ socket, req });
|
||||||
});
|
});
|
||||||
socket.on('error', (error: any) => {
|
socket.on("error", (error: any) => {
|
||||||
websocket.onError && websocket.onError({socket, error, req});
|
websocket.onError &&
|
||||||
|
websocket.onError({ socket, error, req });
|
||||||
});
|
});
|
||||||
})
|
},
|
||||||
|
);
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
|
|
||||||
@@ -99,23 +116,22 @@ export const attachRoute = <TIn extends TSchema, TOut extends TSchema>(server: F
|
|||||||
// }
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
const {handler} = routeOptions;
|
const { handler } = routeOptions;
|
||||||
const augmentedHandler = (request: Parameters<RouteOptions["handler"]>[0]) => {
|
const augmentedHandler = (
|
||||||
const {
|
request: Parameters<RouteOptions["handler"]>[0],
|
||||||
body,
|
) => {
|
||||||
query,
|
const { body, query } = request;
|
||||||
} = request;
|
|
||||||
const payload = body ?? query;
|
const payload = body ?? query;
|
||||||
if (Value.Check(payloadT, payload)) {
|
if (Value.Check(payloadT, payload)) {
|
||||||
return handler({payload});
|
return handler({ payload });
|
||||||
} else {
|
} else {
|
||||||
throw new Error("Payload wrong shape.");
|
throw new Error("Payload wrong shape.");
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
server.route({
|
server.route({
|
||||||
method,
|
method,
|
||||||
url,
|
url,
|
||||||
handler: augmentedHandler,
|
handler: augmentedHandler,
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
export type Game = { carts: { name: string; src: string }[] };
|
||||||
Reference in New Issue
Block a user