Skip to content

Commit

Permalink
Implement client/server API (#5)
Browse files Browse the repository at this point in the history
* Add API and database

* Store room data in database

* Get stuff working

* Remove hardcoded stuff

* Move api to bot process

Co-authored-by: Porasjeet Singh <[email protected]>
  • Loading branch information
rjt-rockx and mmkiir authored Jul 8, 2022
1 parent c1d3224 commit 3ce260c
Show file tree
Hide file tree
Showing 20 changed files with 799 additions and 62 deletions.
1 change: 1 addition & 0 deletions .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 61,7 @@
"always"
],
"comma-dangle": [
"error",
"always-multiline"
],
"no-return-await": "warn",
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -135,3 135,4 @@ config.json
.vscode/
.env
dist/
database.db
5 changes: 3 additions & 2 deletions bot/commands/ping.ts
Original file line number Diff line number Diff line change
@@ -1,10 1,11 @@
import { SlashCommand, SlashCreator, CommandContext } from "slash-create";
import { BotClient } from "../types";

export default class Ping extends SlashCommand {
export default class Ping extends SlashCommand<BotClient> {
constructor(creator: SlashCreator) {
super(creator, {
name: "ping",
description: "Replies with pong!"
description: "Replies with pong!",
});
this.filePath = __filename;
}
Expand Down
42 changes: 24 additions & 18 deletions bot/commands/start.ts
Original file line number Diff line number Diff line change
@@ -1,6 1,8 @@
import { SlashCommand, SlashCreator, CommandContext, CommandOptionType } from "slash-create";
import { nanoid } from "nanoid";
import { CommandContext, CommandOptionType, SlashCommand, SlashCreator } from "slash-create";
import { BotClient } from "../types";

export default class Start extends SlashCommand {
export default class Start extends SlashCommand<BotClient> {
constructor(creator: SlashCreator) {
super(creator, {
name: "start",
Expand All @@ -9,7 11,7 @@ export default class Start extends SlashCommand {
{
type: CommandOptionType.STRING,
name: "start_url",
description: "The initial URL that is set in the browser"
description: "The initial URL that is set in the browser",
},
{
type: CommandOptionType.STRING,
Expand All @@ -18,30 20,29 @@ export default class Start extends SlashCommand {
choices: [
{
name: "North America",
value: "NA"
value: "NA",
},
{
name: "Europe",
value: "EU"
value: "EU",
},
{
name: "Asia",
value: "AS"
}
]
}
]
value: "AS",
},
],
},
],
});
this.filePath = __filename;
}

async run(ctx: CommandContext) {
const start_url = ctx.options.start_url;

const response = await fetch("https://enginetest.hyperbeam.com/v0/vm", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.HYPERBEAM_API_KEY}`
Authorization: `Bearer ${process.env.HYPERBEAM_API_KEY}`,
},
body: JSON.stringify({
start_url: start_url
Expand All @@ -50,13 51,18 @@ export default class Start extends SlashCommand {
: `https://duckduckgo.com/?q=${encodeURIComponent(start_url)}`
: "https://duckduckgo.com",
offline_timeout: 300,
region: ctx.options.region || "NA"
})
region: ctx.options.region || "NA",
}),
});

const { embed_url } = (await response.json()) as { embed_url: string; };
if (embed_url) return ctx.send(`Started a multiplayer browser session at ${embed_url}`);
else return ctx.send("Something went wrong! Please try again.", { ephemeral: true });
if (!response.ok) {
return ctx.send("Something went wrong! Please try again.", { ephemeral: true });
}
const room_id = nanoid();
const hb_session_id: string = await response.json().then(data => data.session_id);
await this.client.db.room.create({ data: { room_id, hb_session_id } });
return ctx.send(
`Started a multiplayer browser session at ${process.env.VITE_CLIENT_BASE_URL}/rooms/${room_id}`,
);
}

hasProtocol(s: string) {
Expand Down
14 changes: 11 additions & 3 deletions bot/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 2,18 @@ import dotenv from "dotenv";
import { SlashCreator, GatewayServer } from "slash-create";
import { Client, Intents } from "discord.js";
import path from "path";
import { PrismaClient } from "@prisma/client";
import apiServer from "./server/api";
import { BotClient } from "./types";

dotenv.config({ path: path.join(__dirname, "../../.env") });
const db = new PrismaClient();

const client = new Client({ intents: [Intents.FLAGS.GUILDS, Intents.FLAGS.GUILD_MESSAGES] });
const port = parseInt(process.env.API_SERVER_PORT || "3000", 10);
apiServer(db).listen(port, () => console.log(`API server listening on port ${port}`));

const client = new Client({ intents: [Intents.FLAGS.GUILDS, Intents.FLAGS.GUILD_MESSAGES] }) as BotClient;
client.db = db;

client.once("ready", () => {
console.log("Ready!");
Expand All @@ -14,14 22,14 @@ client.once("ready", () => {
const creator = new SlashCreator({
applicationID: process.env.DISCORD_CLIENT_ID!,
token: process.env.DISCORD_BOT_TOKEN!,
client
client,
});

creator.on("warn", (message) => console.warn(message));
creator.on("error", (error) => console.error(error));
creator.on("synced", () => console.info("Commands synced!"));
creator.on("commandRun", (command, _, ctx) =>
console.info(`${ctx.user.username}#${ctx.user.discriminator} (${ctx.user.id}) ran command ${command.commandName}`)
console.info(`${ctx.user.username}#${ctx.user.discriminator} (${ctx.user.id}) ran command ${command.commandName}`),
);
creator.on("commandRegister", (command) => console.info(`Registered command ${command.commandName}`));
creator.on("commandError", (command, error) => console.error(`Command ${command.commandName}:`, error));
Expand Down
14 changes: 14 additions & 0 deletions bot/prisma/schema.prisma
Original file line number Diff line number Diff line change
@@ -0,0 1,14 @@
generator client {
provider = "prisma-client-js"
}

datasource db {
provider = "sqlite"
url = env("DATABASE_URL")
}

model Room {
id Int @id @default(autoincrement())
room_id String @unique
hb_session_id String @unique
}
34 changes: 34 additions & 0 deletions bot/server/api.ts
Original file line number Diff line number Diff line change
@@ -0,0 1,34 @@
import { PrismaClient } from "@prisma/client";
import express from "express";

export default function apiServer(db: PrismaClient) {
const app = express();

app.use(function (_req, res, next) {
res.header("Access-Control-Allow-Origin", process.env.VITE_CLIENT_BASE_URL);
res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
next();
});

app.get("/api/rooms/:id", async (req, res) => {
const hyperbeam_session_id = await db.room.findFirst({ where: { room_id: req.params.id } }).then(room => room?.hb_session_id);
if (!hyperbeam_session_id)
return res.status(404).send("Room not found");
const hbResponse = await fetch(
`https://enginetest.hyperbeam.com/v0/vm/${hyperbeam_session_id}`,
{
headers: {
Authorization: `Bearer ${process.env.HYPERBEAM_API_KEY}`,
},
},
);
if (!hbResponse.ok) {
return res.status(500).send("Internal Server Error");
}
res.setHeader("Content-Type", "application/json");
const { embed_url } = await hbResponse.json();
res.send({ embed_url });
});

return app;
}
6 changes: 6 additions & 0 deletions bot/types.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 1,6 @@
import { PrismaClient } from "@prisma/client";
import { Client } from "discord.js";

export interface BotClient extends Client {
db: PrismaClient;
}
5 changes: 4 additions & 1 deletion client/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,9 1,11 @@
import {
BrowserRouter as Router,
Route,
Routes,
Route
} from "react-router-dom";

import OauthHandler from "./components/OAuthHandler";
import VM from "./components/VM";

function App() {

Expand All @@ -13,6 15,7 @@ function App() {
<Router>
<Routes>
<Route path="/authorize" element={<OauthHandler />} />
<Route path="/rooms/:id" element={<VM />} />
</Routes>
</Router>
</div>
Expand Down
10 changes: 6 additions & 4 deletions client/src/components/OAuthHandler.tsx
Original file line number Diff line number Diff line change
@@ -1,5 1,6 @@
import React from "react";
import { ReactNode } from "react";

import User, { UserData } from "./User";

interface IProps { }
Expand All @@ -17,11 18,12 @@ export default class OAuthHandler extends React.Component<IProps, IState> {

async componentDidMount() {
const searchParams = new URLSearchParams(window.location.hash.slice(1));
if (!searchParams.has('access_token') || !searchParams.has('token_type')) {
window.location.href = import.meta.env.VITE_OAUTH_URL;
if (!searchParams.has("access_token") || !searchParams.has("token_type")) {
const oAuthUrl = `https://discord.com/oauth2/authorize?client_id=${process.env.VITE_CLIENT_ID!}&redirect_uri=${encodeURIComponent(process.env.VITE_CLIENT_BASE_URL!)}/authorize&response_type=token&scope=identify email`;
window.location.href = oAuthUrl;
}
const [accessToken, tokenType] = [searchParams.get('access_token'), searchParams.get('token_type')];
const response = await fetch('https://discord.com/api/users/@me', {
const [accessToken, tokenType] = [searchParams.get("access_token"), searchParams.get("token_type")];
const response = await fetch("https://discord.com/api/users/@me", {
headers: {
authorization: `${tokenType} ${accessToken}`,
},
Expand Down
43 changes: 43 additions & 0 deletions client/src/components/VM.tsx
Original file line number Diff line number Diff line change
@@ -0,0 1,43 @@
import Hyperbeam from "@hyperbeam/iframe";
import React from "react";
import { ReactNode } from "react";
import { useParams } from "react-router-dom";

interface IProps {
params: { id: string | null | undefined; };
}

interface IState {
}

class VMElement extends React.Component<IProps, IState> {
constructor(props) {
super(props);
this.state = {};
}

async componentDidMount() {
const { id } = this.props.params;
const apiResponse = await fetch(`http://localhost:3000/api/rooms/${id}`).then(response => {
console.log(response);
return response.json();
});
const hbiframe = document.getElementById("hyperbeam") as HTMLIFrameElement | null;
if (hbiframe) {
await Hyperbeam(hbiframe, apiResponse.embed_url);
}
}

render(): ReactNode {
return <div className="OAuth">
<h2>VM</h2>
<iframe id="hyperbeam" title="Hyperbeam" />
</div>;
}
}

const VM = (props) => {
return <VMElement {...props} params={useParams()} />;
};

export default VM;
2 changes: 1 addition & 1 deletion client/src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 8,5 @@ import App from "./App";
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
<React.StrictMode>
<App />
</React.StrictMode>
</React.StrictMode>,
);
7 changes: 5 additions & 2 deletions env.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 6,13 @@ declare global {
DISCORD_BOT_TOKEN: string;
DISCORD_DEVELOPMENT_GUILD_ID: string;
HYPERBEAM_API_KEY: string;
VITE_CLIENT_ID: string;
VITE_CLIENT_PORT: string;
VITE_OAUTH_URL: string;
VITE_CLIENT_BASE_URL: string;
API_SERVER_PORT: string;
DATABASE_URL: string;
}
}
}

export { };
export {};
16 changes: 14 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 7,27 @@
"scripts": {
"start": "pm2 start ./pm2.config.js",
"bot": "pnpm run build:bot && pnpm run launch:bot",
"client": "pnpm run launch:client",
"client": "pnpm run build:client && pnpm run launch:client",
"listcommands": "slash-up local",
"build:bot": "tsc --project tsconfig.bot.json",
"launch:bot": "cd dist/bot && node index.js",
"lint": "eslint .",
"lint:fix": "eslint . --fix",
"launch:client": "vite",
"build:client": "vite build",
"preview:client": "vite preview"
"preview:client": "vite preview",
"envtypes": "pnpm exec dotenv-types-generator --file ./.env"
},
"dependencies": {
"@hyperbeam/iframe": "^0.0.10",
"@prisma/client": "^4.0.0",
"@types/better-sqlite3": "^7.5.0",
"@vitejs/plugin-react": "^1.3.2",
"better-sqlite3": "^7.5.3",
"discord.js": "^13.8.0",
"dotenv": "^16.0.1",
"express": "^4.18.1",
"nanoid": "3.3.4",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^6.3.0",
Expand All @@ -34,12 41,17 @@
"@types/react-dom": "^18.0.5",
"@typescript-eslint/eslint-plugin": "^5.28.0",
"@typescript-eslint/parser": "^5.28.0",
"dotenv-types-generator": "^1.1.2",
"eslint": "^8.18.0",
"eslint-plugin-jsx-a11y": "^6.6.0",
"eslint-plugin-react": "^7.30.1",
"eslint-plugin-simple-import-sort": "^7.0.0",
"prisma": "^4.0.0",
"slash-up": "^1.2.0",
"typescript": "^4.7.4",
"vite-plugin-mkcert": "^1.7.2"
},
"prisma": {
"schema": "bot/prisma/schema.prisma"
}
}
8 changes: 4 additions & 4 deletions pm2.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 4,12 @@ module.exports = {
name: "hyperbeam-bot",
script: "node",
args: "dist/bot/index.js",
max_restarts: 10
max_restarts: 10,
},
{
name: "hyperbeam-client",
script: "pnpm",
args: "run launch:client"
}
]
args: "run launch:client",
},
],
};
Loading

0 comments on commit 3ce260c

Please sign in to comment.