Skip to content
This repository was archived by the owner on Jan 29, 2025. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
.env

# WebStorm
.idea
137 changes: 85 additions & 52 deletions communication/nats.ts
Original file line number Diff line number Diff line change
@@ -1,70 +1,73 @@
import type { NatsConnection, JetStreamClient, KV } from "https://deno.land/x/nats@v1.13.0/src/mod.ts";
import type {
JetStreamClient,
KV,
NatsConnection,
} from "https://deno.land/x/nats@v1.13.0/src/mod.ts";
import {
connect, jwtAuthenticator
} from "../lib/nats.js";
DiscardPolicy,
RetentionPolicy,
} from "https://deno.land/x/nats@v1.13.0/src/mod.ts";
import { connect, jwtAuthenticator } from "../lib/nats.js";
import { createUser } from "https://deno.land/x/nkeys.js@v1.0.5/src/nkeys.ts";
import { encodeUser } from "https://raw.githubusercontent.com/nats-io/jwt.js/main/src/jwt.ts";
import { denoHelper } from "https://deno.land/x/nkeys.js@v1.0.5/modules/esm/deps.ts";
import { setEd25519Helper } from "https://deno.land/x/nkeys.js@v1.0.5/src/helper.ts";

const enc = new TextEncoder()
const enc = new TextEncoder();
export function encodeToBuf(x: any) {
return enc.encode(JSON.stringify(x))
return enc.encode(JSON.stringify(x));
}

const dec = new TextDecoder()
const dec = new TextDecoder();
export function decodeFromBuf<T>(buf: Uint8Array) {
const str = dec.decode(buf)
const str = dec.decode(buf);
const t: T = JSON.parse(str) as T;
return t
return t;
}

export class NatsCon {
nc!: NatsConnection
js!: JetStreamClient
roomBucket!: KV
nc: NatsConnection;
username: string;
private js?: JetStreamClient;
private roomBucket?: KV;

async createConnection() {
if (!this.nc) {
const res = await fetch('/api/creds');
const { jwt, seed } = await res.json();

this.nc = await connect({
servers: 'wss://connect.ngs.global',
authenticator: jwtAuthenticator(jwt, new TextEncoder().encode(seed))
})

const jsm = await this.nc.jetstreamManager();
await jsm.streams.add({ name: "rooms", subjects: ["rooms.*"], max_bytes: 100000000});
}

return this.nc
}

async createServerSideConnection(jwt: string, seed: string) {
if (!this.nc) {
this.nc = await connect({
servers: 'wss://connect.ngs.global',
authenticator: jwtAuthenticator(jwt, new TextEncoder().encode(seed))
})

const jsm = await this.nc.jetstreamManager();
await jsm.streams.add({ name: "rooms", subjects: ["rooms.*"], max_bytes: 100000000});
}

return this.nc
constructor(nc: NatsConnection, username: string) {
this.nc = nc;
this.username = username;
}

async getJetstreamClient() {
if (!this.js) {
const nc = await this.createConnection();
this.js = await nc.jetstream();
this.js = await this.nc.jetstream();
}
return this.js
return this.js;
}

async getKVClient() {
if (!this.roomBucket) {
const js = await this.getJetstreamClient();
this.roomBucket = await js.views.kv("bucketOfRooms", { maxBucketSize: 100000000, maxValueSize: 131072 });
this.roomBucket = await js.views.kv("bucketOfRooms", {
max_bytes: 100000000,
maxValueSize: 131072,
});
}
return this.roomBucket;
}

async ensureRoomsStreamCreated(): Promise<void> {
const jsm = await this.nc.jetstreamManager();
try {
await jsm.streams.find("rooms.>");
} catch {
await jsm.streams.add({
name: "rooms",
subjects: ["rooms.>"],
max_bytes: 100000000,
max_msg_size: 10000,
retention: RetentionPolicy.Limits,
discard: DiscardPolicy.Old,
});
}
return this.roomBucket
}

drain() {
Expand All @@ -74,13 +77,43 @@ export class NatsCon {
}
}

let serverNC: NatsCon;
// creates a client connection that should be used with an island
// the client connection will have strict permissions associated with it
// that are tied to the User ID
export async function createClientNatsConnection() {
const res = await fetch("/api/creds");
const { jwt, seed, inboxPrefix, username } = await res.json();

export function makeNC() {
if (!serverNC) {
serverNC = new NatsCon;
}
const nc = await connect({
servers: "wss://connect.ngs.global",
authenticator: jwtAuthenticator(jwt, new TextEncoder().encode(seed)),
inboxPrefix: inboxPrefix,
noEcho: true,
});

return new NatsCon(nc, username);
}

export { serverNC }
export const natsCon = new NatsCon();
let serverNC: Promise<NatsCon> | undefined;

// singleton with the server-side NATS connection
// should only be used in routes. the connection has no permissions set,
// so it must never be exposed to the client
export function getServerNatsConnection() {
return serverNC ??= (async () => {
const accountSeed = Deno.env.get("ACCOUNT_SEED") || "";
setEd25519Helper(denoHelper);
const natsUser = createUser();
const seed = new TextDecoder().decode(natsUser.getSeed());
const jwt = await encodeUser("server", natsUser, accountSeed);

return new NatsCon(
await connect({
servers: "wss://connect.ngs.global",
authenticator: jwtAuthenticator(jwt, new TextEncoder().encode(seed)),
noEcho: true,
}),
"server",
);
})();
}
2 changes: 1 addition & 1 deletion communication/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,4 @@ export interface MessageView {
export interface UserView {
name: string;
avatarURL: string;
}
}
2 changes: 1 addition & 1 deletion deno.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,4 @@
"$std/": "https://deno.land/std@0.144.0/",
"emojify": "https://esm.sh/@twuni/emojify@1.0.2"
}
}
}
26 changes: 26 additions & 0 deletions helpers/ClientNatsCon.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { useEffect, useState } from "preact/hooks";
import { createClientNatsConnection, NatsCon } from "../communication/nats.ts";

// react hook for client nats con
export function useClientNatsCon(): { natsCon: NatsCon | undefined } {
const [natsCon, setNatsCon] = useState<NatsCon>(undefined);

// set up the natsConn
useEffect(() => {
let localNatsCon: NatsCon | undefined = undefined;

createClientNatsConnection()
.then((res) => {
localNatsCon = res;
setNatsCon(res);
})
.catch((err) => alert(`Cannot connect to NATS: ${err}`));

return () => {
console.log("nats con drained");
localNatsCon?.drain();
};
}, []);

return { natsCon };
}
2 changes: 1 addition & 1 deletion helpers/bad_words.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,4 +75,4 @@ class TextBadWordsCleaner implements BadWordsCleaner {
}
}

export const badWordsCleanerLoader = new BadWordsCleanerLoader();
export const badWordsCleanerLoader = new BadWordsCleanerLoader();
6 changes: 3 additions & 3 deletions helpers/github.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,15 @@ export class GitHubApi {
);
if (!response.ok) {
console.log("response wasn't ok");

throw new Error(await response.text());
}
const data = await response.json();

const accessToken = data["access_token"];
if (typeof accessToken !== "string") {
console.log(accessToken);

throw new Error("Access token was not a string.");
}
return accessToken;
Expand Down
75 changes: 41 additions & 34 deletions islands/AddRoom.tsx
Original file line number Diff line number Diff line change
@@ -1,44 +1,52 @@
import { useState } from "preact/hooks";
import { encodeToBuf, natsCon } from "../communication/nats.ts";
import { badWordsCleanerLoader } from "../helpers/bad_words.ts"
import { RoomView } from "../communication/types.ts";
import { useCallback, useState } from "preact/hooks";
import * as xxhash64 from "https://deno.land/x/xxhash64@1.0.0/mod.ts";
import { badWordsCleanerLoader } from "../helpers/bad_words.ts";
import { RoomView } from "../communication/types.ts";
import { encodeToBuf } from "../communication/nats.ts";
import { useClientNatsCon } from "../helpers/ClientNatsCon.ts";

export default function AddRoom() {
const [roomName, setRoomName] = useState("");
const { natsCon } = useClientNatsCon();

return (
<form
onSubmit={async (e) => {
e.preventDefault();
const create = xxhash64.create();
try {
// create hash based on the room name
const roomHasher = await create;
const roomHash = roomHasher.hash(roomName, 'hex').toString();
const onSubmit = useCallback(async (e) => {
e.preventDefault();
if (!natsCon) {
// wait until the natsCon connection has been made
alert(`Cannot create room: NATS not connected`);
return;
}

const create = xxhash64.create();
try {
// create hash based on the room name
const roomHasher = await create;
const roomHash = roomHasher.hash(roomName, "hex").toString();

const badWordsCleaner = await badWordsCleanerLoader.getInstance();
const cleanedRoomName = badWordsCleaner.clean(roomName);
const roomMsg: RoomView = {
name: cleanedRoomName,
lastMessageAt: "",
}
const badWordsCleaner = await badWordsCleanerLoader.getInstance();
const cleanedRoomName = badWordsCleaner.clean(roomName);
const roomMsg: RoomView = {
name: cleanedRoomName,
lastMessageAt: "",
};

const roomBucket = await natsCon.getKVClient();
const roomBucket = await natsCon.getKVClient();
const roomKey = `${roomHash}.${natsCon.username}`;

// if room doesn't exist, create it
const getRoom = await roomBucket.get(roomHash);
if (!getRoom) {
await roomBucket.put(roomHash, encodeToBuf(roomMsg));
}
natsCon.drain();
// if room doesn't exist, create it
const getRoom = await roomBucket.get(roomKey);
if (!getRoom) {
await roomBucket.put(roomKey, encodeToBuf(roomMsg));
}

location.pathname = "/" + roomHash;
} catch (err) {
alert(`Cannot create room: ${err.message}`);
}
}}
>
location.pathname = "/" + roomKey;
} catch (err) {
alert(`Cannot create room: ${err.message}`);
}
}, [natsCon, roomName]);

return (
<form onSubmit={onSubmit}>
<label>
<div class="mb-2.5">
<p class="font-semibold">Name</p>
Expand All @@ -55,14 +63,13 @@ export default function AddRoom() {
onChange={(e) => setRoomName(e.currentTarget.value)}
/>
</label>

<button
class="mt-7 flex flex items-center rounded-md h-8 py-2 px-4 bg-gray-800 font-medium text-sm text-white"
type="submit"
>
create
</button>

</form>
);
}
Loading