Compare commits
No commits in common. "master" and "old-master" have entirely different histories.
master
...
old-master
46 changed files with 11705 additions and 4854 deletions
1
.envrc
1
.envrc
|
@ -1 +0,0 @@
|
||||||
use flake
|
|
11
.gitignore
vendored
11
.gitignore
vendored
|
@ -1,5 +1,6 @@
|
||||||
/target
|
.DS_Store
|
||||||
/node_modules
|
/node_modules/
|
||||||
/dist
|
/src/node_modules/@sapper/
|
||||||
/.direnv
|
yarn-error.log
|
||||||
/result
|
/__sapper__/
|
||||||
|
/test.db
|
2
.ignore
Normal file
2
.ignore
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
/package-lock.json
|
||||||
|
/rollup.config.js
|
|
@ -1 +0,0 @@
|
||||||
package-lock.json
|
|
1808
Cargo.lock
generated
1808
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
27
Cargo.toml
27
Cargo.toml
|
@ -1,27 +0,0 @@
|
||||||
[package]
|
|
||||||
name = "grub"
|
|
||||||
version = "0.1.0"
|
|
||||||
edition = "2021"
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
anyhow = "1.0.72"
|
|
||||||
automerge = "0.5.1"
|
|
||||||
axum = { version = "0.6.20", features = ["http2", "macros", "ws"] }
|
|
||||||
chrono = { version = "0.4.26", features = ["serde"] }
|
|
||||||
ciborium = "0.2.1"
|
|
||||||
clap = { version = "4.3.21", features = ["derive"] }
|
|
||||||
dashmap = "5.5.0"
|
|
||||||
derivative = "2.2.0"
|
|
||||||
flume = "0.10.14"
|
|
||||||
futures = "0.3.28"
|
|
||||||
mime_guess = "2.0.4"
|
|
||||||
rust-embed = "6.8.1"
|
|
||||||
serde = { version = "1.0.183", features = ["derive"] }
|
|
||||||
serde_bytes = "0.11.12"
|
|
||||||
serde_json = "1.0.104"
|
|
||||||
tokio = { version = "1.30.0", features = ["full"] }
|
|
||||||
uuid = "1.4.1"
|
|
||||||
|
|
||||||
[dependencies.mzlib]
|
|
||||||
git = "https://git.mzhang.io/michael/mzlib"
|
|
||||||
features = ["axum"]
|
|
16
README.md
16
README.md
|
@ -1,15 +1,11 @@
|
||||||
# grub
|
grub
|
||||||
|
====
|
||||||
To build:
|
|
||||||
|
|
||||||
```
|
```
|
||||||
npm ci
|
npm i
|
||||||
npm run build
|
npm run dev
|
||||||
cargo build --release
|
|
||||||
```
|
```
|
||||||
|
|
||||||
To run:
|
u know the deal
|
||||||
|
|
||||||
```
|
license: WTFPL
|
||||||
grub [--port 6106]
|
|
||||||
```
|
|
|
@ -1,21 +0,0 @@
|
||||||
import "bootstrap/dist/css/bootstrap.min.css";
|
|
||||||
import "./global.css";
|
|
||||||
|
|
||||||
import { v4 } from "uuid";
|
|
||||||
import Room from "./Room";
|
|
||||||
import { Container } from "react-bootstrap";
|
|
||||||
|
|
||||||
export default function App() {
|
|
||||||
const x = new URL(window.location.href);
|
|
||||||
const roomId = x.pathname.replace(/^\//, "");
|
|
||||||
|
|
||||||
if (!roomId) {
|
|
||||||
location.href = `/${v4()}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Container>
|
|
||||||
<Room roomId={roomId} />
|
|
||||||
</Container>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,70 +0,0 @@
|
||||||
import { useContext, useState } from "react";
|
|
||||||
import useWebSocket from "react-use-websocket";
|
|
||||||
import { wsUrl } from "./constants";
|
|
||||||
import CBOR from "cbor";
|
|
||||||
import * as uuid from "uuid";
|
|
||||||
import { RoomContext } from "./lib/roomContext";
|
|
||||||
|
|
||||||
export default function Chat() {
|
|
||||||
const { roomId, clientId } = useContext(RoomContext);
|
|
||||||
const [chats, setChats] = useState([]);
|
|
||||||
const [message, setMessage] = useState("");
|
|
||||||
|
|
||||||
const {
|
|
||||||
sendMessage,
|
|
||||||
lastJsonMessage,
|
|
||||||
readyState: newReadyState,
|
|
||||||
} = useWebSocket(wsUrl, {
|
|
||||||
share: true,
|
|
||||||
onOpen: ({}) => {
|
|
||||||
console.log("Shiet, connected.");
|
|
||||||
},
|
|
||||||
onMessage: async (event) => {
|
|
||||||
const data = CBOR.decode(await event.data.arrayBuffer());
|
|
||||||
console.log("received", data);
|
|
||||||
|
|
||||||
if (data.type === "ChatMessage") {
|
|
||||||
setChats([...chats, data]);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
function sendCborMessage(data) {
|
|
||||||
let cbor = CBOR.encode(data);
|
|
||||||
sendMessage(cbor);
|
|
||||||
}
|
|
||||||
|
|
||||||
function onSubmit(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
sendCborMessage({
|
|
||||||
type: "ChatMessage",
|
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
message_id: uuid.v4(),
|
|
||||||
room_id: roomId,
|
|
||||||
author: clientId,
|
|
||||||
content: message,
|
|
||||||
});
|
|
||||||
setMessage("");
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
Messages:
|
|
||||||
<ul>
|
|
||||||
{chats.map((x) => (
|
|
||||||
<li key={x.message_id}>
|
|
||||||
[{x.timestamp}] {uuid.stringify(x.author)}: {x.content}
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
<form onSubmit={onSubmit}>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={message}
|
|
||||||
onChange={(e) => setMessage(e.target.value)}
|
|
||||||
placeholder="Type a message..."
|
|
||||||
/>
|
|
||||||
</form>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,86 +0,0 @@
|
||||||
import * as Automerge from "@automerge/automerge";
|
|
||||||
import { useContext, useState } from "react";
|
|
||||||
import useWebSocket from "react-use-websocket";
|
|
||||||
import { wsUrl } from "./constants";
|
|
||||||
import CBOR from "cbor";
|
|
||||||
import { RoomContext } from "./lib/roomContext";
|
|
||||||
|
|
||||||
export default function Grub() {
|
|
||||||
const { roomId, clientId } = useContext(RoomContext);
|
|
||||||
const [doc, setDoc] = useState(Automerge.init());
|
|
||||||
const [syncState, setSyncState] = useState(Automerge.initSyncState());
|
|
||||||
const [addItemName, setAddItemName] = useState("");
|
|
||||||
|
|
||||||
function updateDoc(newDoc) {
|
|
||||||
setDoc(newDoc);
|
|
||||||
}
|
|
||||||
|
|
||||||
const { sendMessage } = useWebSocket(wsUrl, {
|
|
||||||
share: true,
|
|
||||||
onOpen: ({}) => {
|
|
||||||
console.log("Shiet, connected.");
|
|
||||||
},
|
|
||||||
onMessage: async (event) => {
|
|
||||||
const data = CBOR.decode(await event.data.arrayBuffer());
|
|
||||||
|
|
||||||
if (data.type === "Automerge") {
|
|
||||||
const [nextDoc, nextSyncState, patch] = Automerge.receiveSyncMessage(
|
|
||||||
doc,
|
|
||||||
syncState,
|
|
||||||
data.message[1]
|
|
||||||
);
|
|
||||||
setDoc(nextDoc);
|
|
||||||
setSyncState(nextSyncState);
|
|
||||||
console.log("patch", patch);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
function sendCborMessage(data) {
|
|
||||||
let cbor = CBOR.encode(data);
|
|
||||||
sendMessage(cbor);
|
|
||||||
}
|
|
||||||
|
|
||||||
function addItem(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
const newDoc = Automerge.change(doc, (doc) => {
|
|
||||||
if (!doc.items) doc.items = [];
|
|
||||||
doc.items.push({
|
|
||||||
id: uuid.v4(),
|
|
||||||
content: addItemName,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
updateDoc(newDoc);
|
|
||||||
const [syncMessage, binary] = Automerge.generateSyncMessage(doc, syncState);
|
|
||||||
if (syncMessage)
|
|
||||||
sendCborMessage({
|
|
||||||
type: "Automerge",
|
|
||||||
client_id: clientId,
|
|
||||||
room_id: roomId,
|
|
||||||
message: binary,
|
|
||||||
});
|
|
||||||
setAddItemName("");
|
|
||||||
}
|
|
||||||
|
|
||||||
const items = doc.items || [];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
Grubs:
|
|
||||||
<ul>
|
|
||||||
{items.map((x) => (
|
|
||||||
<li key={x.id}>{x.content}</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
<form onSubmit={addItem}>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={addItemName}
|
|
||||||
onChange={(e) => setAddItemName(e.target.value)}
|
|
||||||
placeholder="Type a message..."
|
|
||||||
/>
|
|
||||||
<button type="submit">add item</button>
|
|
||||||
</form>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
128
client/Room.tsx
128
client/Room.tsx
|
@ -1,128 +0,0 @@
|
||||||
import CBOR from "cbor";
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import useWebSocket, { ReadyState } from "react-use-websocket";
|
|
||||||
import * as Automerge from "@automerge/automerge";
|
|
||||||
import * as uuid from "uuid";
|
|
||||||
import { wsUrl } from "./constants";
|
|
||||||
import { Tab, Tabs } from "react-bootstrap";
|
|
||||||
import Upload from "./Upload";
|
|
||||||
import Chat from "./Chat";
|
|
||||||
import Grub from "./Grub";
|
|
||||||
import { RoomContext } from "./lib/roomContext";
|
|
||||||
import { Wifi } from "react-bootstrap-icons";
|
|
||||||
|
|
||||||
const connectionStatusMap = {
|
|
||||||
[ReadyState.CONNECTING]: "Connecting",
|
|
||||||
[ReadyState.OPEN]: "Open",
|
|
||||||
[ReadyState.CLOSING]: "Closing",
|
|
||||||
[ReadyState.CLOSED]: "Closed",
|
|
||||||
[ReadyState.UNINSTANTIATED]: "Uninstantiated",
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function Room({ roomId }) {
|
|
||||||
const [readyState, setReadyState] = useState(ReadyState.CLOSED);
|
|
||||||
const [connectedClients, setConnectedClients] = useState([]);
|
|
||||||
const [clientId, setClientId] = useState<string | null>(null);
|
|
||||||
|
|
||||||
const {
|
|
||||||
sendMessage,
|
|
||||||
lastJsonMessage,
|
|
||||||
readyState: newReadyState,
|
|
||||||
} = useWebSocket(wsUrl, {
|
|
||||||
share: true,
|
|
||||||
onOpen: ({}) => {
|
|
||||||
console.log("Shiet, connected.");
|
|
||||||
},
|
|
||||||
onMessage: async (event) => {
|
|
||||||
const data = CBOR.decode(await event.data.arrayBuffer());
|
|
||||||
console.log("received", data);
|
|
||||||
|
|
||||||
if (data.type === "ServerHello") {
|
|
||||||
setClientId(uuid.stringify(data.client_id));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data.type === "RoomClientList") {
|
|
||||||
setConnectedClients(data.clients.map((x) => uuid.stringify(x)));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data.type === "Automerge") {
|
|
||||||
const [nextDoc, nextSyncState, patch] = Automerge.receiveSyncMessage(
|
|
||||||
doc,
|
|
||||||
syncState,
|
|
||||||
data.message[1]
|
|
||||||
);
|
|
||||||
setDoc(nextDoc);
|
|
||||||
setSyncState(nextSyncState);
|
|
||||||
console.log("patch", patch);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
function sendWtfMessage(data) {
|
|
||||||
let cbor = CBOR.encode(data);
|
|
||||||
console.log(
|
|
||||||
"cbor-encoded",
|
|
||||||
[...new Uint8Array(cbor)]
|
|
||||||
.map((x) => x.toString(16).padStart(2, "0"))
|
|
||||||
.join(" ")
|
|
||||||
);
|
|
||||||
sendMessage(cbor);
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (
|
|
||||||
readyState === ReadyState.CONNECTING &&
|
|
||||||
newReadyState === ReadyState.OPEN
|
|
||||||
) {
|
|
||||||
// On Open
|
|
||||||
sendWtfMessage({ type: "JoinRoom", room_id: roomId });
|
|
||||||
console.log("Sent connection message");
|
|
||||||
}
|
|
||||||
setReadyState(newReadyState);
|
|
||||||
}, [newReadyState]);
|
|
||||||
|
|
||||||
const connectionStatus = connectionStatusMap[readyState];
|
|
||||||
|
|
||||||
if (newReadyState !== ReadyState.OPEN) return <>Connecting...</>;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<RoomContext.Provider value={{ roomId, clientId }}>
|
|
||||||
<p>Room Id: {roomId}</p>
|
|
||||||
<p>
|
|
||||||
Connection status: <ConnectionStatus readyState={readyState} />
|
|
||||||
</p>
|
|
||||||
Connected:
|
|
||||||
<ul>
|
|
||||||
{connectedClients.map((x) => (
|
|
||||||
<li key={x}>{x}</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
<Tabs
|
|
||||||
defaultActiveKey="file-transfer"
|
|
||||||
id="uncontrolled-tab-example"
|
|
||||||
className="mb-3"
|
|
||||||
>
|
|
||||||
<Tab eventKey="file-transfer" title="File Transfer">
|
|
||||||
<Upload />
|
|
||||||
</Tab>
|
|
||||||
<Tab eventKey="grocery-tracking" title="Grocery Tracking">
|
|
||||||
<Grub />
|
|
||||||
</Tab>
|
|
||||||
<Tab eventKey="chat" title="Chat">
|
|
||||||
<Chat />
|
|
||||||
</Tab>
|
|
||||||
</Tabs>
|
|
||||||
</RoomContext.Provider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ConnectionStatus({ readyState }) {
|
|
||||||
switch (readyState) {
|
|
||||||
case ReadyState.CONNECTING:
|
|
||||||
return <Wifi color="yellow" />;
|
|
||||||
case ReadyState.OPEN:
|
|
||||||
return <Wifi color="green" />;
|
|
||||||
default:
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,289 +0,0 @@
|
||||||
import { useCallback, useContext, useEffect, useState } from "react";
|
|
||||||
import * as sodium from "libsodium-wrappers-sumo";
|
|
||||||
import useWebSocket from "react-use-websocket";
|
|
||||||
import CBOR from "cbor";
|
|
||||||
import { wsUrl } from "./constants";
|
|
||||||
import { RoomContext } from "./lib/roomContext";
|
|
||||||
import { useDebouncedCallback } from "use-debounce";
|
|
||||||
import {
|
|
||||||
Row,
|
|
||||||
Col,
|
|
||||||
Button,
|
|
||||||
Form,
|
|
||||||
Badge,
|
|
||||||
OverlayTrigger,
|
|
||||||
Tooltip,
|
|
||||||
} from "react-bootstrap";
|
|
||||||
|
|
||||||
export default function Upload() {
|
|
||||||
const { roomId } = useContext(RoomContext);
|
|
||||||
const [selectedFile, setSelectedFile] = useState<File | null>(null);
|
|
||||||
const [encryptionKey, setEncryptionKey] = useState(null);
|
|
||||||
const [passphrase, setPassphrase] = useState("");
|
|
||||||
const [uploadProgress, setUploadProgress] = useState(null);
|
|
||||||
|
|
||||||
const setHashedPassphrase = useDebouncedCallback(
|
|
||||||
(passphrase) => {
|
|
||||||
if (passphrase) {
|
|
||||||
const salt = sodium.randombytes_buf(sodium.crypto_pwhash_SALTBYTES);
|
|
||||||
console.log("salt", salt);
|
|
||||||
setEncryptionKey(
|
|
||||||
sodium.crypto_pwhash(
|
|
||||||
sodium.crypto_box_SEEDBYTES,
|
|
||||||
passphrase,
|
|
||||||
salt,
|
|
||||||
sodium.crypto_pwhash_OPSLIMIT_INTERACTIVE,
|
|
||||||
sodium.crypto_pwhash_MEMLIMIT_INTERACTIVE,
|
|
||||||
sodium.crypto_pwhash_ALG_ARGON2ID13
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
// delay in ms
|
|
||||||
1000
|
|
||||||
);
|
|
||||||
|
|
||||||
const [decryptionKey, setDecryptionKey] = useState("");
|
|
||||||
const [selectedSaveFile, setSelectedSaveFile] = useState(null);
|
|
||||||
const [decryptState, setDecryptState] = useState(null);
|
|
||||||
const [pendingDecryption, setPendingDecryption] = useState([]);
|
|
||||||
|
|
||||||
let decryptionKeyInternal: Uint8Array;
|
|
||||||
if (decryptionKey) {
|
|
||||||
decryptionKeyInternal = hex2Bytes(decryptionKey);
|
|
||||||
}
|
|
||||||
const [writeStream, setWriteStream] = useState(null);
|
|
||||||
const readyToDownload =
|
|
||||||
!!decryptionKeyInternal && !!selectedSaveFile && !!writeStream;
|
|
||||||
|
|
||||||
const { sendMessage } = useWebSocket(wsUrl, {
|
|
||||||
share: true,
|
|
||||||
onMessage: async (event) => {
|
|
||||||
const message = CBOR.decode(await event.data.arrayBuffer());
|
|
||||||
|
|
||||||
if (message.type === "FileUploadBegin") {
|
|
||||||
const state = sodium.crypto_secretstream_xchacha20poly1305_init_pull(
|
|
||||||
message.header,
|
|
||||||
decryptionKeyInternal
|
|
||||||
);
|
|
||||||
setDecryptState(state);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (message.type === "FileUploadChunk") {
|
|
||||||
if (readyToDownload && decryptState) {
|
|
||||||
for (const { data } of [...pendingDecryption, message]) {
|
|
||||||
const { message } =
|
|
||||||
sodium.crypto_secretstream_xchacha20poly1305_pull(
|
|
||||||
decryptState,
|
|
||||||
data
|
|
||||||
);
|
|
||||||
const result = await writeStream.write(message);
|
|
||||||
}
|
|
||||||
setPendingDecryption([]);
|
|
||||||
} else {
|
|
||||||
setPendingDecryption([...pendingDecryption, message]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (message.type === "FileUploadComplete") {
|
|
||||||
await writeStream.close();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
function sendCborMessage(data) {
|
|
||||||
let cbor = CBOR.encode(data);
|
|
||||||
sendMessage(cbor);
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
(async () => {
|
|
||||||
await sodium.ready;
|
|
||||||
if (passphrase) {
|
|
||||||
setHashedPassphrase(passphrase);
|
|
||||||
} else {
|
|
||||||
const key = sodium.crypto_secretstream_xchacha20poly1305_keygen();
|
|
||||||
setEncryptionKey(key);
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
}, [passphrase]);
|
|
||||||
|
|
||||||
const uploadFile = useCallback(async () => {
|
|
||||||
if (!encryptionKey) return;
|
|
||||||
if (!selectedFile) return;
|
|
||||||
|
|
||||||
const { state, header } =
|
|
||||||
sodium.crypto_secretstream_xchacha20poly1305_init_push(encryptionKey);
|
|
||||||
sendCborMessage({
|
|
||||||
type: "FileUploadBegin",
|
|
||||||
room_id: roomId,
|
|
||||||
header,
|
|
||||||
});
|
|
||||||
|
|
||||||
const stream = selectedFile.stream();
|
|
||||||
const reader = stream.getReader();
|
|
||||||
|
|
||||||
try {
|
|
||||||
while (true) {
|
|
||||||
const { done, value } = await reader.read();
|
|
||||||
if (done) break;
|
|
||||||
|
|
||||||
// Encrypt this chunk
|
|
||||||
const encryptedValue =
|
|
||||||
sodium.crypto_secretstream_xchacha20poly1305_push(
|
|
||||||
state,
|
|
||||||
value,
|
|
||||||
null,
|
|
||||||
0
|
|
||||||
);
|
|
||||||
|
|
||||||
sendCborMessage({
|
|
||||||
type: "FileUploadChunk",
|
|
||||||
room_id: roomId,
|
|
||||||
data: encryptedValue,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
sendCborMessage({
|
|
||||||
type: "FileUploadComplete",
|
|
||||||
room_id: roomId,
|
|
||||||
});
|
|
||||||
} finally {
|
|
||||||
reader.releaseLock();
|
|
||||||
}
|
|
||||||
}, [encryptionKey, selectedFile]);
|
|
||||||
|
|
||||||
async function selectSaveFile() {
|
|
||||||
const newHandle = await window.showSaveFilePicker();
|
|
||||||
setSelectedSaveFile(newHandle);
|
|
||||||
|
|
||||||
const writableStream = await newHandle.createWritable();
|
|
||||||
setWriteStream(writableStream);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<h3>How does this work?</h3>
|
|
||||||
<p style={{ color: "darkred" }}>
|
|
||||||
<b>
|
|
||||||
RECEIVER MUST BE USING CHROME{" "}
|
|
||||||
<a
|
|
||||||
href="https://caniuse.com/mdn-api_window_showsavefilepicker"
|
|
||||||
target="_blank"
|
|
||||||
>
|
|
||||||
[?]
|
|
||||||
</a>
|
|
||||||
</b>
|
|
||||||
</p>
|
|
||||||
<ol>
|
|
||||||
<li>
|
|
||||||
<b>Uploader:</b> Copy the key and send it to whoever you're sending
|
|
||||||
files to
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<b>Receiver:</b> Paste the key into the "Paste key here" box
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<b>Receiver:</b> Press the "select save file" button to choose where
|
|
||||||
to save the file to
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<b>Receiver:</b> Tell the uploader to press "upload"
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<b>Uploader:</b> Press "upload"
|
|
||||||
</li>
|
|
||||||
</ol>
|
|
||||||
<p>Only one person can upload at a time.</p>
|
|
||||||
|
|
||||||
<Row>
|
|
||||||
<Col md={6}>
|
|
||||||
<h3>Send file</h3>
|
|
||||||
|
|
||||||
{/* <p>
|
|
||||||
Upload passphrase:
|
|
||||||
<Form.Control
|
|
||||||
value={passphrase}
|
|
||||||
onChange={(e) => {
|
|
||||||
setEncryptionKey(null);
|
|
||||||
setPassphrase(e.target.value);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</p> */}
|
|
||||||
|
|
||||||
<p>
|
|
||||||
Upload Key:{" "}
|
|
||||||
<Form.Control
|
|
||||||
type="text"
|
|
||||||
disabled
|
|
||||||
value={encryptionKey ? bytes2Hex(encryptionKey) : "generating..."}
|
|
||||||
/>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<input
|
|
||||||
type="file"
|
|
||||||
onChange={(e) => setSelectedFile(e.target.files[0])}
|
|
||||||
/>
|
|
||||||
<Button onClick={uploadFile} disabled={!encryptionKey}>
|
|
||||||
upload
|
|
||||||
</Button>
|
|
||||||
</Col>
|
|
||||||
|
|
||||||
<Col md={6}>
|
|
||||||
<h3>
|
|
||||||
Receive file
|
|
||||||
{readyToDownload ? (
|
|
||||||
<Badge bg="success">ready</Badge>
|
|
||||||
) : (
|
|
||||||
<OverlayTrigger
|
|
||||||
placement="top"
|
|
||||||
overlay={
|
|
||||||
<Tooltip>
|
|
||||||
you are not ready to download, make sure you enter the key
|
|
||||||
and select a place to save
|
|
||||||
</Tooltip>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Badge bg="warning">not ready</Badge>
|
|
||||||
</OverlayTrigger>
|
|
||||||
)}
|
|
||||||
</h3>
|
|
||||||
|
|
||||||
<ol>
|
|
||||||
<li>
|
|
||||||
<Form.Control
|
|
||||||
type="text"
|
|
||||||
value={decryptionKey}
|
|
||||||
onChange={(e) => setDecryptionKey(e.target.value)}
|
|
||||||
placeholder="Paste key here..."
|
|
||||||
/>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<Button onClick={selectSaveFile} disabled={!!selectedSaveFile}>
|
|
||||||
{selectedSaveFile
|
|
||||||
? `Saving to "${selectedSaveFile.name}"`
|
|
||||||
: "select save file"}
|
|
||||||
</Button>
|
|
||||||
</li>
|
|
||||||
</ol>
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function bytes2Hex(byteArray: Uint8Array): string {
|
|
||||||
return Array.prototype.map
|
|
||||||
.call(byteArray, function (byte: number) {
|
|
||||||
return ("0" + (byte & 0xff).toString(16)).slice(-2);
|
|
||||||
})
|
|
||||||
.join("");
|
|
||||||
}
|
|
||||||
|
|
||||||
function hex2Bytes(hexString: string): Uint8Array {
|
|
||||||
let result = [];
|
|
||||||
for (var i = 0; i < hexString.length; i += 2) {
|
|
||||||
result.push(parseInt(hexString.substr(i, 2), 16));
|
|
||||||
}
|
|
||||||
return new Uint8Array(result);
|
|
||||||
}
|
|
|
@ -1 +0,0 @@
|
||||||
export const wsUrl = `ws://${location.host}/ws`;
|
|
|
@ -1,4 +0,0 @@
|
||||||
.container {
|
|
||||||
max-width: 980px;
|
|
||||||
margin: auto;
|
|
||||||
}
|
|
|
@ -1,6 +0,0 @@
|
||||||
import { createRoot } from "react-dom/client";
|
|
||||||
import App from "./App";
|
|
||||||
|
|
||||||
const el = document.getElementById("app")!;
|
|
||||||
const root = createRoot(el);
|
|
||||||
root.render(App());
|
|
|
@ -1,8 +0,0 @@
|
||||||
import { createContext } from "react";
|
|
||||||
|
|
||||||
interface RoomContextProps {
|
|
||||||
roomId: string;
|
|
||||||
clientId: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const RoomContext = createContext<RoomContextProps>(undefined);
|
|
162
flake.lock
162
flake.lock
|
@ -1,162 +0,0 @@
|
||||||
{
|
|
||||||
"nodes": {
|
|
||||||
"fenix": {
|
|
||||||
"inputs": {
|
|
||||||
"nixpkgs": "nixpkgs",
|
|
||||||
"rust-analyzer-src": "rust-analyzer-src"
|
|
||||||
},
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1691648495,
|
|
||||||
"narHash": "sha256-JULr+eKL9rjfex17hZYn0K/fBxxfK/FM9TOCcxPQay4=",
|
|
||||||
"owner": "nix-community",
|
|
||||||
"repo": "fenix",
|
|
||||||
"rev": "6c9f0709358f212766cff5ce79f6e8300ec1eb91",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"id": "fenix",
|
|
||||||
"type": "indirect"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"flake-utils": {
|
|
||||||
"inputs": {
|
|
||||||
"systems": "systems"
|
|
||||||
},
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1689068808,
|
|
||||||
"narHash": "sha256-6ixXo3wt24N/melDWjq70UuHQLxGV8jZvooRanIHXw0=",
|
|
||||||
"owner": "numtide",
|
|
||||||
"repo": "flake-utils",
|
|
||||||
"rev": "919d646de7be200f3bf08cb76ae1f09402b6f9b4",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"id": "flake-utils",
|
|
||||||
"type": "indirect"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"flake-utils_2": {
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1659877975,
|
|
||||||
"narHash": "sha256-zllb8aq3YO3h8B/U0/J1WBgAL8EX5yWf5pMj3G0NAmc=",
|
|
||||||
"owner": "numtide",
|
|
||||||
"repo": "flake-utils",
|
|
||||||
"rev": "c0e246b9b83f637f4681389ecabcb2681b4f3af0",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "numtide",
|
|
||||||
"repo": "flake-utils",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"napalm": {
|
|
||||||
"inputs": {
|
|
||||||
"flake-utils": "flake-utils_2",
|
|
||||||
"nixpkgs": "nixpkgs_2"
|
|
||||||
},
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1672245824,
|
|
||||||
"narHash": "sha256-i596lbPiA/Rfx3DiJiCluxdgxWY7oGSgYMT7OmM+zik=",
|
|
||||||
"owner": "nix-community",
|
|
||||||
"repo": "napalm",
|
|
||||||
"rev": "7c25a05cef52dc405f4688422ce0046ca94aadcf",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "nix-community",
|
|
||||||
"repo": "napalm",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"nixpkgs": {
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1691472822,
|
|
||||||
"narHash": "sha256-XVfYZ2oB3lNPVq6sHCY9WkdQ8lHoIDzzbpg8bB6oBxA=",
|
|
||||||
"owner": "nixos",
|
|
||||||
"repo": "nixpkgs",
|
|
||||||
"rev": "41c7605718399dcfa53dd7083793b6ae3bc969ff",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "nixos",
|
|
||||||
"ref": "nixos-unstable",
|
|
||||||
"repo": "nixpkgs",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"nixpkgs_2": {
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1663087123,
|
|
||||||
"narHash": "sha256-cNIRkF/J4mRxDtNYw+9/fBNq/NOA2nCuPOa3EdIyeDs=",
|
|
||||||
"owner": "NixOS",
|
|
||||||
"repo": "nixpkgs",
|
|
||||||
"rev": "9608ace7009ce5bc3aeb940095e01553e635cbc7",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "NixOS",
|
|
||||||
"ref": "nixos-unstable",
|
|
||||||
"repo": "nixpkgs",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"nixpkgs_3": {
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1691709280,
|
|
||||||
"narHash": "sha256-zmfH2OlZEXwv572d0g8f6M5Ac6RiO8TxymOpY3uuqrM=",
|
|
||||||
"owner": "nixos",
|
|
||||||
"repo": "nixpkgs",
|
|
||||||
"rev": "cf73a86c35a84de0e2f3ba494327cf6fb51c0dfd",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "nixos",
|
|
||||||
"repo": "nixpkgs",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"root": {
|
|
||||||
"inputs": {
|
|
||||||
"fenix": "fenix",
|
|
||||||
"flake-utils": "flake-utils",
|
|
||||||
"napalm": "napalm",
|
|
||||||
"nixpkgs": "nixpkgs_3"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"rust-analyzer-src": {
|
|
||||||
"flake": false,
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1691604464,
|
|
||||||
"narHash": "sha256-nNc/c9r1O8ajE/LkMhGcvJGlyR6ykenR3aRkEkhutxA=",
|
|
||||||
"owner": "rust-lang",
|
|
||||||
"repo": "rust-analyzer",
|
|
||||||
"rev": "05b061205179dab9a5cd94ae66d1c0e9b8febe08",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "rust-lang",
|
|
||||||
"ref": "nightly",
|
|
||||||
"repo": "rust-analyzer",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"systems": {
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1681028828,
|
|
||||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
|
||||||
"owner": "nix-systems",
|
|
||||||
"repo": "default",
|
|
||||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "nix-systems",
|
|
||||||
"repo": "default",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"root": "root",
|
|
||||||
"version": 7
|
|
||||||
}
|
|
56
flake.nix
56
flake.nix
|
@ -1,56 +0,0 @@
|
||||||
{
|
|
||||||
description = "A very basic flake";
|
|
||||||
|
|
||||||
inputs.nixpkgs.url = "github:nixos/nixpkgs";
|
|
||||||
inputs.napalm.url = "github:nix-community/napalm";
|
|
||||||
|
|
||||||
outputs = { self, nixpkgs, flake-utils, fenix, napalm }:
|
|
||||||
flake-utils.lib.eachDefaultSystem (system:
|
|
||||||
let
|
|
||||||
pkgs = import nixpkgs {
|
|
||||||
inherit system;
|
|
||||||
overlays = [ fenix.overlays.default napalm.overlays.default ];
|
|
||||||
};
|
|
||||||
|
|
||||||
toolchain = pkgs.fenix.stable;
|
|
||||||
rustPlatform =
|
|
||||||
pkgs.makeRustPlatform { inherit (toolchain) cargo rustc; };
|
|
||||||
version = "0.1.0";
|
|
||||||
in rec {
|
|
||||||
defaultPackage = packages.grub;
|
|
||||||
|
|
||||||
packages = rec {
|
|
||||||
grub = rustPlatform.buildRustPackage {
|
|
||||||
name = "grub";
|
|
||||||
inherit version;
|
|
||||||
src = pkgs.symlinkJoin {
|
|
||||||
name = "grub-src";
|
|
||||||
paths = [
|
|
||||||
(pkgs.nix-gitignore.gitignoreSource [ ./.gitignore ] ./.)
|
|
||||||
frontend
|
|
||||||
];
|
|
||||||
};
|
|
||||||
cargoLock = {
|
|
||||||
lockFile = ./Cargo.lock;
|
|
||||||
outputHashes = {
|
|
||||||
"mzlib-0.1.0" =
|
|
||||||
"sha256-J7dEeCTPIoKmldpAkQadDBXhJP+Zv9hNlUdpMl2L5QM=";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
frontend = pkgs.buildNpmPackage {
|
|
||||||
name = "frontend";
|
|
||||||
inherit version;
|
|
||||||
src = pkgs.nix-gitignore.gitignoreSource [ ./.gitignore ] ./.;
|
|
||||||
npmDepsHash = "sha256-fmx7Ee+Idx91w9VwV/yeHgJ+4OHiVevfFJ46LTtmZl4=";
|
|
||||||
installPhase = ''
|
|
||||||
mkdir -p $out
|
|
||||||
mv dist $out
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
devShell = pkgs.mkShell { packages = with toolchain; [ cargo rustc ]; };
|
|
||||||
});
|
|
||||||
}
|
|
|
@ -1,7 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<body>
|
|
||||||
<div id="app"></div>
|
|
||||||
<script type="module" src="client/index.ts"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
12722
package-lock.json
generated
12722
package-lock.json
generated
File diff suppressed because it is too large
Load diff
69
package.json
69
package.json
|
@ -1,39 +1,42 @@
|
||||||
{
|
{
|
||||||
"name": "grub",
|
"name": "grubs",
|
||||||
"version": "0.1.0",
|
"description": "grubs",
|
||||||
|
"version": "0.0.1",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "sapper dev",
|
||||||
"build": "vite build"
|
"build": "sapper build --legacy",
|
||||||
},
|
"export": "sapper export --legacy",
|
||||||
"devDependencies": {
|
"start": "node __sapper__/build"
|
||||||
"@types/libsodium-wrappers": "^0.7.10",
|
|
||||||
"@types/react": "^18.2.20",
|
|
||||||
"@types/react-dom": "^18.2.7",
|
|
||||||
"@types/uuid": "^9.0.2",
|
|
||||||
"@vitejs/plugin-react": "^4.0.4",
|
|
||||||
"sass": "^1.65.1",
|
|
||||||
"typescript": "^5.1.6",
|
|
||||||
"vite": "^4.4.9",
|
|
||||||
"vite-plugin-top-level-await": "^1.3.1",
|
|
||||||
"vite-plugin-wasm": "^3.2.2"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@automerge/automerge": "^2.0.3",
|
"compression": "^1.7.1",
|
||||||
"bootstrap": "^5.3.1",
|
"express": "^4.17.1",
|
||||||
"cbor": "npm:@jprochazk/cbor@^0.5.0",
|
"knex": "^0.21.21",
|
||||||
"libsodium": "^0.7.11",
|
"sequelize": "^6.6.5",
|
||||||
"libsodium-wrappers": "^0.7.11",
|
"sirv": "^1.0.0",
|
||||||
"libsodium-wrappers-sumo": "^0.7.11",
|
"socket.io": "^4.1.3",
|
||||||
"localforage": "^1.10.0",
|
"sqlite3": "^5.0.2",
|
||||||
"match-sorter": "^6.3.1",
|
"uuid": "^8.3.2"
|
||||||
"react": "^18.2.0",
|
},
|
||||||
"react-bootstrap": "^2.8.0",
|
"devDependencies": {
|
||||||
"react-bootstrap-icons": "^1.10.3",
|
"@babel/core": "^7.0.0",
|
||||||
"react-dom": "^18.2.0",
|
"@babel/plugin-syntax-dynamic-import": "^7.0.0",
|
||||||
"react-router-dom": "^6.15.0",
|
"@babel/plugin-transform-runtime": "^7.0.0",
|
||||||
"react-use-websocket": "^4.3.1",
|
"@babel/preset-env": "^7.0.0",
|
||||||
"sort-by": "^1.2.0",
|
"@babel/runtime": "^7.0.0",
|
||||||
"use-debounce": "^9.0.4",
|
"@rollup/plugin-babel": "^5.0.0",
|
||||||
"uuid": "^9.0.0"
|
"@rollup/plugin-commonjs": "^20.0.0",
|
||||||
|
"@rollup/plugin-node-resolve": "^8.0.0",
|
||||||
|
"@rollup/plugin-replace": "^2.4.0",
|
||||||
|
"@rollup/plugin-url": "^5.0.0",
|
||||||
|
"bufferutil": "^4.0.3",
|
||||||
|
"rollup": "^2.3.4",
|
||||||
|
"rollup-plugin-svelte": "^7.0.0",
|
||||||
|
"rollup-plugin-terser": "^7.0.0",
|
||||||
|
"sapper": "^0.28.0",
|
||||||
|
"socket.io-client": "^4.1.3",
|
||||||
|
"svelte": "^3.17.3",
|
||||||
|
"sveltestrap": "^5.6.0",
|
||||||
|
"utf-8-validate": "^5.0.5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
127
rollup.config.js
Normal file
127
rollup.config.js
Normal file
|
@ -0,0 +1,127 @@
|
||||||
|
import path from 'path';
|
||||||
|
import resolve from '@rollup/plugin-node-resolve';
|
||||||
|
import replace from '@rollup/plugin-replace';
|
||||||
|
import commonjs from '@rollup/plugin-commonjs';
|
||||||
|
import url from '@rollup/plugin-url';
|
||||||
|
import svelte from 'rollup-plugin-svelte';
|
||||||
|
import babel from '@rollup/plugin-babel';
|
||||||
|
import { terser } from 'rollup-plugin-terser';
|
||||||
|
import config from 'sapper/config/rollup.js';
|
||||||
|
import pkg from './package.json';
|
||||||
|
|
||||||
|
const mode = process.env.NODE_ENV;
|
||||||
|
const dev = mode === 'development';
|
||||||
|
const legacy = !!process.env.SAPPER_LEGACY_BUILD;
|
||||||
|
|
||||||
|
const onwarn = (warning, onwarn) =>
|
||||||
|
(warning.code === 'MISSING_EXPORT' && /'preload'/.test(warning.message)) ||
|
||||||
|
(warning.code === 'CIRCULAR_DEPENDENCY' && /[/\\]@sapper[/\\]/.test(warning.message)) ||
|
||||||
|
onwarn(warning);
|
||||||
|
|
||||||
|
export default {
|
||||||
|
client: {
|
||||||
|
input: config.client.input(),
|
||||||
|
output: config.client.output(),
|
||||||
|
plugins: [
|
||||||
|
replace({
|
||||||
|
preventAssignment: true,
|
||||||
|
values:{
|
||||||
|
'process.browser': true,
|
||||||
|
'process.env.NODE_ENV': JSON.stringify(mode)
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
svelte({
|
||||||
|
compilerOptions: {
|
||||||
|
dev,
|
||||||
|
hydratable: true
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
url({
|
||||||
|
sourceDir: path.resolve(__dirname, 'src/node_modules/images'),
|
||||||
|
publicPath: '/client/'
|
||||||
|
}),
|
||||||
|
resolve({
|
||||||
|
browser: true,
|
||||||
|
dedupe: ['svelte']
|
||||||
|
}),
|
||||||
|
commonjs(),
|
||||||
|
|
||||||
|
legacy && babel({
|
||||||
|
extensions: ['.js', '.mjs', '.html', '.svelte'],
|
||||||
|
babelHelpers: 'runtime',
|
||||||
|
exclude: ['node_modules/@babel/**'],
|
||||||
|
presets: [
|
||||||
|
['@babel/preset-env', {
|
||||||
|
targets: '> 0.25%, not dead'
|
||||||
|
}]
|
||||||
|
],
|
||||||
|
plugins: [
|
||||||
|
'@babel/plugin-syntax-dynamic-import',
|
||||||
|
['@babel/plugin-transform-runtime', {
|
||||||
|
useESModules: true
|
||||||
|
}]
|
||||||
|
]
|
||||||
|
}),
|
||||||
|
|
||||||
|
!dev && terser({
|
||||||
|
module: true
|
||||||
|
})
|
||||||
|
],
|
||||||
|
|
||||||
|
preserveEntrySignatures: false,
|
||||||
|
onwarn,
|
||||||
|
},
|
||||||
|
|
||||||
|
server: {
|
||||||
|
input: config.server.input(),
|
||||||
|
output: config.server.output(),
|
||||||
|
plugins: [
|
||||||
|
replace({
|
||||||
|
preventAssignment: true,
|
||||||
|
values:{
|
||||||
|
'process.browser': false,
|
||||||
|
'process.env.NODE_ENV': JSON.stringify(mode)
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
svelte({
|
||||||
|
compilerOptions: {
|
||||||
|
dev,
|
||||||
|
generate: 'ssr',
|
||||||
|
hydratable: true
|
||||||
|
},
|
||||||
|
emitCss: false
|
||||||
|
}),
|
||||||
|
url({
|
||||||
|
sourceDir: path.resolve(__dirname, 'src/node_modules/images'),
|
||||||
|
publicPath: '/client/',
|
||||||
|
emitFiles: false // already emitted by client build
|
||||||
|
}),
|
||||||
|
resolve({
|
||||||
|
dedupe: ['svelte']
|
||||||
|
}),
|
||||||
|
commonjs()
|
||||||
|
],
|
||||||
|
external: Object.keys(pkg.dependencies).concat(require('module').builtinModules),
|
||||||
|
preserveEntrySignatures: 'strict',
|
||||||
|
onwarn,
|
||||||
|
},
|
||||||
|
|
||||||
|
serviceworker: {
|
||||||
|
input: config.serviceworker.input(),
|
||||||
|
output: config.serviceworker.output(),
|
||||||
|
plugins: [
|
||||||
|
resolve(),
|
||||||
|
replace({
|
||||||
|
preventAssignment: true,
|
||||||
|
values:{
|
||||||
|
'process.browser': true,
|
||||||
|
'process.env.NODE_ENV': JSON.stringify(mode)
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
commonjs(),
|
||||||
|
!dev && terser()
|
||||||
|
],
|
||||||
|
preserveEntrySignatures: false,
|
||||||
|
onwarn,
|
||||||
|
}
|
||||||
|
};
|
|
@ -1,4 +0,0 @@
|
||||||
max_width = 80
|
|
||||||
tab_spaces = 2
|
|
||||||
wrap_comments = true
|
|
||||||
fn_single_line = true
|
|
39
src/ambient.d.ts
vendored
Normal file
39
src/ambient.d.ts
vendored
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
/**
|
||||||
|
* These declarations tell TypeScript that we allow import of images, e.g.
|
||||||
|
* ```
|
||||||
|
<script lang='ts'>
|
||||||
|
import successkid from 'images/successkid.jpg';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<img src="{successkid}">
|
||||||
|
```
|
||||||
|
*/
|
||||||
|
declare module "*.gif" {
|
||||||
|
const value: string;
|
||||||
|
export default value;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "*.jpg" {
|
||||||
|
const value: string;
|
||||||
|
export default value;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "*.jpeg" {
|
||||||
|
const value: string;
|
||||||
|
export default value;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "*.png" {
|
||||||
|
const value: string;
|
||||||
|
export default value;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "*.svg" {
|
||||||
|
const value: string;
|
||||||
|
export default value;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "*.webp" {
|
||||||
|
const value: string;
|
||||||
|
export default value;
|
||||||
|
}
|
4
src/client.js
Normal file
4
src/client.js
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
import * as sapper from '@sapper/app';
|
||||||
|
sapper.start({
|
||||||
|
target: document.querySelector('#sapper')
|
||||||
|
});
|
67
src/components/GrubView.svelte
Normal file
67
src/components/GrubView.svelte
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
<script>
|
||||||
|
import { onMount } from "svelte";
|
||||||
|
import { Form, InputGroup, Input, Button, ListGroup, ListGroupItem } from "sveltestrap";
|
||||||
|
import { v4 as uuidv4 } from "uuid";
|
||||||
|
|
||||||
|
import { socket, clientText, clientShadow } from "../lib/stores";
|
||||||
|
import { emptyState, diffStates } from "../lib/state";
|
||||||
|
export let urlPath;
|
||||||
|
|
||||||
|
let text, shadow;
|
||||||
|
|
||||||
|
let connected = false;
|
||||||
|
$socket.on("connect", () => {
|
||||||
|
connected = true;
|
||||||
|
$socket.emit("fullSyncRequest", urlPath);
|
||||||
|
});
|
||||||
|
|
||||||
|
$socket.on("fullSync", newState => {
|
||||||
|
console.log("what the hell is this state", newState);
|
||||||
|
clientShadow.set(newState || emptyState());
|
||||||
|
});
|
||||||
|
|
||||||
|
clientText.subscribe(st => { text = st });
|
||||||
|
clientShadow.subscribe(st => { shadow = st });
|
||||||
|
|
||||||
|
let newGrubValue = "";
|
||||||
|
function newGrub() {
|
||||||
|
let s = newGrubValue.trim();
|
||||||
|
if (s == "") return false;
|
||||||
|
|
||||||
|
let key = `grub-${uuidv4()}`;
|
||||||
|
clientText.update(o => {
|
||||||
|
o.grubs[key] = s;
|
||||||
|
return o;
|
||||||
|
});
|
||||||
|
newGrubValue = "";
|
||||||
|
|
||||||
|
// compute the diff
|
||||||
|
console.log("text", text, "shadow", shadow);
|
||||||
|
let diff = diffStates(shadow, text);
|
||||||
|
console.log("diff", diff)
|
||||||
|
$socket.emit("diffs", diff);
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<h3>Grubs</h3>
|
||||||
|
|
||||||
|
<form on:submit|preventDefault|stopPropagation = {newGrub}>
|
||||||
|
<InputGroup>
|
||||||
|
<Input
|
||||||
|
bind:value = {newGrubValue}
|
||||||
|
placeholder = "What do you need?" />
|
||||||
|
<Button>Hellosu</Button>
|
||||||
|
</InputGroup>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{connected}
|
||||||
|
|
||||||
|
<ListGroup>
|
||||||
|
{#each Array.from(text.grubs) as [key, grub]}
|
||||||
|
<ListGroupItem>
|
||||||
|
{grub}
|
||||||
|
</ListGroupItem>
|
||||||
|
{/each}
|
||||||
|
</ListGroup>
|
28
src/lib/models.js
Normal file
28
src/lib/models.js
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
import { Sequelize, DataTypes } from "sequelize";
|
||||||
|
const sequelize = new Sequelize("sqlite:test.db");
|
||||||
|
|
||||||
|
const Grub = sequelize.define("Grubs", {
|
||||||
|
id: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
primaryKey: true,
|
||||||
|
},
|
||||||
|
tag: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
allowNull: false,
|
||||||
|
autoIncrement: true,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
type: DataTypes.JSON,
|
||||||
|
allowNull: false,
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
// Other model options go here
|
||||||
|
tableName: "grubs",
|
||||||
|
|
||||||
|
indexes: [
|
||||||
|
{ fields: ["tag"] },
|
||||||
|
{ fields: ["id", "tag"] },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
export { sequelize, Grub };
|
31
src/lib/server-sync.js
Normal file
31
src/lib/server-sync.js
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
import { Sequelize, Op } from "sequelize";
|
||||||
|
import { sequelize, Grub } from "./models";
|
||||||
|
import { emptyState, patchState } from "./state";
|
||||||
|
|
||||||
|
export function socketHandler(socket) {
|
||||||
|
socket.on("fullSyncRequest", async msg => {
|
||||||
|
// get the latest message for this id
|
||||||
|
let result = await Grub.findOne({
|
||||||
|
where: { id: { [Op.eq]: msg } },
|
||||||
|
order: [ ["tag", "DESC" ] ],
|
||||||
|
});
|
||||||
|
socket.emit("fullSync", result);
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on("diffs", async msg => {
|
||||||
|
try {
|
||||||
|
console.log("patch", msg);
|
||||||
|
let lastGrub = await Grub.findOne({
|
||||||
|
where: { id: { [Op.eq]: msg } },
|
||||||
|
order: [ ["tag", "DESC" ] ],
|
||||||
|
});
|
||||||
|
console.log("last", lastGrub);
|
||||||
|
let newGrub = patchState(lastGrub, msg);
|
||||||
|
console.log("New", newGrub);
|
||||||
|
// await newGrub.save();
|
||||||
|
console.log(result);
|
||||||
|
} catch(err) {
|
||||||
|
console.log("holy Fuck error...", err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
55
src/lib/state.js
Normal file
55
src/lib/state.js
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
function emptyState() {
|
||||||
|
return { tag: 0, grubs: {}, recipes: {} };
|
||||||
|
}
|
||||||
|
|
||||||
|
function diffMaps(a, b) {
|
||||||
|
let deleted = Object.assign({}, a);
|
||||||
|
let updated = {};
|
||||||
|
for (let [key, value] of Object.entries(b)) {
|
||||||
|
// delete keys that are still in the second map
|
||||||
|
if (key in deleted) delete deleted[key];
|
||||||
|
if (!(key in a)) updated[key] = value;
|
||||||
|
else if (a[key] != b[key]) updated[key] = value;
|
||||||
|
}
|
||||||
|
return { deleted, updated };
|
||||||
|
}
|
||||||
|
|
||||||
|
function emptyDiff(diff) {
|
||||||
|
return diff.deleted.size == 0 && diff.updated.size == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function diffStates(prev, next) {
|
||||||
|
let shadowTag = prev["tag"];
|
||||||
|
let tag = next["tag"];
|
||||||
|
let updated = {};
|
||||||
|
for (let key in prev) {
|
||||||
|
if (key === "tag") continue;
|
||||||
|
let diff = diffMaps(prev[key], next[key]);
|
||||||
|
if (!emptyDiff(diff)) updated[key] = diff;
|
||||||
|
}
|
||||||
|
updated["shadowTag"] = shadowTag;
|
||||||
|
updated["tag"] = tag;
|
||||||
|
return updated;
|
||||||
|
}
|
||||||
|
|
||||||
|
function patchMap(state, diff) {
|
||||||
|
let updated = Object.assign({}, state);
|
||||||
|
for (let key in diff.deleted) {
|
||||||
|
delete updated[key];
|
||||||
|
}
|
||||||
|
for (let [key, value] of Object.entries(diff.updated)) {
|
||||||
|
updated[key] = value;
|
||||||
|
}
|
||||||
|
return updated;
|
||||||
|
}
|
||||||
|
|
||||||
|
function patchState(state, diff) {
|
||||||
|
let updated = Object.assign({}, state);
|
||||||
|
for (let key in state) {
|
||||||
|
if (key === "tag") updated[key] = diff[key];
|
||||||
|
else if (key in diff) updated[key] = patchMap(state[key], diff[key]);
|
||||||
|
}
|
||||||
|
return updated;
|
||||||
|
}
|
||||||
|
|
||||||
|
export { emptyState, diffMaps, diffStates, emptyDiff, patchMap, patchState };
|
11
src/lib/stores.js
Normal file
11
src/lib/stores.js
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
import io from "socket.io-client";
|
||||||
|
import { readable, writable } from 'svelte/store';
|
||||||
|
import { emptyState } from "./state";
|
||||||
|
|
||||||
|
export const socket = readable(io(), (set) => {
|
||||||
|
// do nothing lol
|
||||||
|
});
|
||||||
|
|
||||||
|
export const clientShadow = writable(emptyState());
|
||||||
|
|
||||||
|
export const clientText = writable(emptyState());
|
363
src/main.rs
363
src/main.rs
|
@ -1,363 +0,0 @@
|
||||||
pub mod message;
|
|
||||||
|
|
||||||
#[macro_use]
|
|
||||||
extern crate derivative;
|
|
||||||
|
|
||||||
use std::{
|
|
||||||
io::Cursor,
|
|
||||||
mem,
|
|
||||||
net::{Ipv4Addr, Ipv6Addr, SocketAddr},
|
|
||||||
sync::Arc,
|
|
||||||
};
|
|
||||||
|
|
||||||
use automerge::{
|
|
||||||
sync::{State as SyncState, SyncDoc},
|
|
||||||
Automerge,
|
|
||||||
};
|
|
||||||
use axum::{
|
|
||||||
extract::{
|
|
||||||
ws::{Message as WsMessage, WebSocket, WebSocketUpgrade},
|
|
||||||
State,
|
|
||||||
},
|
|
||||||
response::Response,
|
|
||||||
routing::get,
|
|
||||||
Router, Server,
|
|
||||||
};
|
|
||||||
use chrono::Utc;
|
|
||||||
use clap::Parser;
|
|
||||||
use dashmap::{DashMap, DashSet};
|
|
||||||
use flume::r#async::SendSink;
|
|
||||||
use frontend::frontend;
|
|
||||||
use futures::{stream, FutureExt, SinkExt, StreamExt};
|
|
||||||
use message::Message;
|
|
||||||
use mzlib::axum_error::Result;
|
|
||||||
use tokio::join;
|
|
||||||
use uuid::Uuid;
|
|
||||||
|
|
||||||
#[derive(Debug, Parser)]
|
|
||||||
struct Opt {
|
|
||||||
#[clap(long, default_value = "6106")]
|
|
||||||
port: u16,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Default, Clone)]
|
|
||||||
struct AppState {
|
|
||||||
clients: Clients,
|
|
||||||
rooms: Rooms,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Default, Clone)]
|
|
||||||
struct Clients(Arc<DashMap<Uuid, Client>>);
|
|
||||||
|
|
||||||
struct Client {
|
|
||||||
writer: SendSink<'static, Result<axum::extract::ws::Message, axum::Error>>,
|
|
||||||
rooms: DashSet<Uuid>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Default, Clone)]
|
|
||||||
struct Rooms(Arc<DashMap<Uuid, Room>>);
|
|
||||||
|
|
||||||
#[derive(Default)]
|
|
||||||
struct Room {
|
|
||||||
document: Automerge,
|
|
||||||
sync_state: SyncState,
|
|
||||||
connected_clients: DashSet<Uuid>,
|
|
||||||
current_uploader: Option<Uuid>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Room {
|
|
||||||
pub fn split_borrow(&mut self) -> (&mut Automerge, &mut SyncState) {
|
|
||||||
(&mut self.document, &mut self.sync_state)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::main]
|
|
||||||
async fn main() -> Result<()> {
|
|
||||||
let opts = Opt::parse();
|
|
||||||
|
|
||||||
let state = AppState::default();
|
|
||||||
|
|
||||||
let app = Router::new()
|
|
||||||
.route("/ws", get(handler))
|
|
||||||
.fallback(frontend)
|
|
||||||
.with_state(state);
|
|
||||||
|
|
||||||
let service = app.into_make_service();
|
|
||||||
|
|
||||||
let addr6 = SocketAddr::from((Ipv6Addr::UNSPECIFIED, opts.port));
|
|
||||||
println!("Listening on {}...", addr6);
|
|
||||||
|
|
||||||
let server6 = Server::bind(&addr6).serve(service);
|
|
||||||
|
|
||||||
server6.await?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(non_upper_case_globals)]
|
|
||||||
mod frontend {
|
|
||||||
use axum::{
|
|
||||||
body::{Body, HttpBody},
|
|
||||||
http::{header::CONTENT_TYPE, HeaderMap, HeaderValue, StatusCode, Uri},
|
|
||||||
response::IntoResponse,
|
|
||||||
};
|
|
||||||
use rust_embed::RustEmbed;
|
|
||||||
|
|
||||||
#[derive(RustEmbed)]
|
|
||||||
#[folder = "dist/"]
|
|
||||||
struct Frontend;
|
|
||||||
|
|
||||||
pub async fn frontend(uri: Uri) -> impl IntoResponse {
|
|
||||||
let path = uri.path().trim_start_matches("/").to_owned();
|
|
||||||
|
|
||||||
match Frontend::get(&path).or_else(|| Frontend::get("index.html")) {
|
|
||||||
Some(file) => {
|
|
||||||
let guess = mime_guess::from_path(path);
|
|
||||||
let mut headers = HeaderMap::default();
|
|
||||||
if let Some(guess) = guess.first() {
|
|
||||||
headers.append(
|
|
||||||
CONTENT_TYPE,
|
|
||||||
HeaderValue::from_str(&guess.to_string()).unwrap(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
let data = file.data.as_ref();
|
|
||||||
let body = Body::from(data.to_owned());
|
|
||||||
let body = body.boxed();
|
|
||||||
(headers, body).into_response()
|
|
||||||
}
|
|
||||||
None => {
|
|
||||||
(StatusCode::NOT_FOUND, format!("No route for {}", uri)).into_response()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn handler(
|
|
||||||
ws: WebSocketUpgrade,
|
|
||||||
State(state): State<AppState>,
|
|
||||||
) -> Response {
|
|
||||||
ws.on_upgrade(|socket| handle_socket(state, socket).map(|res| res.unwrap()))
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn handle_socket(state: AppState, socket: WebSocket) -> Result<()> {
|
|
||||||
let (socket_tx, socket_rx) = socket.split();
|
|
||||||
|
|
||||||
// Generate an ID for this connection
|
|
||||||
let client_id = Uuid::new_v4();
|
|
||||||
println!("Connected client {client_id:?}");
|
|
||||||
|
|
||||||
// First, let's create multiplexed versions of these
|
|
||||||
let mut socket_tx = {
|
|
||||||
let (tx, rx) = flume::unbounded();
|
|
||||||
tokio::spawn(rx.into_stream().forward(socket_tx));
|
|
||||||
tx.into_sink()
|
|
||||||
};
|
|
||||||
let mut socket_rx = {
|
|
||||||
let (tx, rx) = flume::unbounded();
|
|
||||||
tokio::spawn(
|
|
||||||
socket_rx
|
|
||||||
.forward(tx.into_sink().sink_map_err(|err| axum::Error::new(err))),
|
|
||||||
);
|
|
||||||
rx.into_stream()
|
|
||||||
};
|
|
||||||
|
|
||||||
// Now register this client into the set of clients
|
|
||||||
let rooms = DashSet::default();
|
|
||||||
let client = Client {
|
|
||||||
writer: socket_tx.clone(),
|
|
||||||
rooms: rooms.clone(),
|
|
||||||
};
|
|
||||||
state.clients.0.insert(client_id, client);
|
|
||||||
|
|
||||||
// Send a hello message
|
|
||||||
socket_tx
|
|
||||||
.send(Ok(
|
|
||||||
Message::ServerHello {
|
|
||||||
timestamp: Utc::now(),
|
|
||||||
client_id: client_id.clone(),
|
|
||||||
}
|
|
||||||
.into_ws_message()?,
|
|
||||||
))
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
// Wait for messages
|
|
||||||
while let Some(msg) = socket_rx.next().await {
|
|
||||||
match msg {
|
|
||||||
WsMessage::Binary(ref bin) => {
|
|
||||||
let cursor = Cursor::new(bin);
|
|
||||||
let message = ciborium::from_reader::<Message, _>(cursor)?;
|
|
||||||
// println!("received message: {message:?}");
|
|
||||||
|
|
||||||
match &message {
|
|
||||||
Message::Automerge {
|
|
||||||
client_id,
|
|
||||||
room_id,
|
|
||||||
message: inner_message,
|
|
||||||
} => {
|
|
||||||
let mut room = state
|
|
||||||
.rooms
|
|
||||||
.0
|
|
||||||
.entry(room_id.clone())
|
|
||||||
.or_insert_with(|| Room::default());
|
|
||||||
rooms.insert(room_id.clone());
|
|
||||||
|
|
||||||
// Update local state
|
|
||||||
{
|
|
||||||
let (document, sync_state) = room.split_borrow();
|
|
||||||
document
|
|
||||||
.receive_sync_message(sync_state, inner_message.clone().0)?;
|
|
||||||
}
|
|
||||||
// println!("inner doc: {:?}", room.document);
|
|
||||||
|
|
||||||
// Remove current client, so send to everyone else
|
|
||||||
let connected_clients = room.connected_clients.clone();
|
|
||||||
connected_clients.remove(client_id);
|
|
||||||
|
|
||||||
// Send to everyone else
|
|
||||||
let message = message.clone();
|
|
||||||
broadcast(&state, connected_clients, message).await;
|
|
||||||
}
|
|
||||||
|
|
||||||
Message::JoinRoom { room_id } => {
|
|
||||||
let room = state
|
|
||||||
.rooms
|
|
||||||
.0
|
|
||||||
.entry(room_id.clone())
|
|
||||||
.or_insert_with(|| Room::default());
|
|
||||||
rooms.insert(room_id.clone());
|
|
||||||
|
|
||||||
// Add to the list of connected clients
|
|
||||||
room.connected_clients.insert(client_id);
|
|
||||||
let connected_clients = room.connected_clients.clone();
|
|
||||||
mem::drop(room);
|
|
||||||
println!("Added to room");
|
|
||||||
|
|
||||||
// Tell each clients which clients are connected
|
|
||||||
broadcast(
|
|
||||||
&state,
|
|
||||||
connected_clients.clone(),
|
|
||||||
Message::RoomClientList {
|
|
||||||
room_id: room_id.clone(),
|
|
||||||
clients: connected_clients.into_iter().collect(),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
}
|
|
||||||
|
|
||||||
Message::ChatMessage {
|
|
||||||
timestamp: _,
|
|
||||||
message_id: _,
|
|
||||||
room_id,
|
|
||||||
author: _,
|
|
||||||
content: _,
|
|
||||||
} => {
|
|
||||||
let room = state
|
|
||||||
.rooms
|
|
||||||
.0
|
|
||||||
.entry(room_id.clone())
|
|
||||||
.or_insert_with(|| Room::default());
|
|
||||||
|
|
||||||
// Add to the list of connected clients
|
|
||||||
let connected_clients = room.connected_clients.clone();
|
|
||||||
mem::drop(room);
|
|
||||||
println!("Added to room");
|
|
||||||
|
|
||||||
// Broadcast to the room
|
|
||||||
broadcast(&state, connected_clients.clone(), message).await;
|
|
||||||
}
|
|
||||||
|
|
||||||
Message::FileUploadBegin { room_id, .. }
|
|
||||||
| Message::FileUploadComplete { room_id }
|
|
||||||
| Message::FileUploadChunk { room_id, .. } => {
|
|
||||||
let mut room = state
|
|
||||||
.rooms
|
|
||||||
.0
|
|
||||||
.entry(room_id.clone())
|
|
||||||
.or_insert_with(|| Room::default());
|
|
||||||
|
|
||||||
let connected_clients = room.connected_clients.clone();
|
|
||||||
connected_clients.remove(&client_id);
|
|
||||||
|
|
||||||
match &message {
|
|
||||||
Message::FileUploadBegin { .. } => match room.current_uploader {
|
|
||||||
Some(v) if v == client_id => {}
|
|
||||||
Some(_v) => continue,
|
|
||||||
None => room.current_uploader = Some(client_id.clone()),
|
|
||||||
},
|
|
||||||
Message::FileUploadComplete { .. } => {
|
|
||||||
room.current_uploader = None;
|
|
||||||
}
|
|
||||||
Message::FileUploadChunk { .. } => {}
|
|
||||||
_ => {}
|
|
||||||
};
|
|
||||||
|
|
||||||
broadcast(&state, connected_clients, message).await;
|
|
||||||
}
|
|
||||||
|
|
||||||
_ => {}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
state.clients.0.remove(&client_id);
|
|
||||||
println!("Disconnecting client {client_id:?}");
|
|
||||||
|
|
||||||
// Tell other clients about the disconnection
|
|
||||||
stream::iter(rooms)
|
|
||||||
.for_each(|room_id| {
|
|
||||||
let room = state
|
|
||||||
.rooms
|
|
||||||
.0
|
|
||||||
.entry(room_id.clone())
|
|
||||||
.or_insert_with(|| Room::default());
|
|
||||||
|
|
||||||
room.connected_clients.remove(&client_id);
|
|
||||||
let connected_clients = room.connected_clients.clone();
|
|
||||||
mem::drop(room);
|
|
||||||
|
|
||||||
let room_id = room_id.clone();
|
|
||||||
{
|
|
||||||
let state_ref = &state;
|
|
||||||
async move {
|
|
||||||
broadcast(
|
|
||||||
state_ref,
|
|
||||||
connected_clients.clone(),
|
|
||||||
Message::RoomClientList {
|
|
||||||
room_id: room_id,
|
|
||||||
clients: connected_clients.into_iter().collect(),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.await;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn broadcast<I>(state: &AppState, client_ids: I, message: Message)
|
|
||||||
where
|
|
||||||
I: IntoIterator<Item = Uuid>,
|
|
||||||
{
|
|
||||||
let client_ids = client_ids.into_iter().collect::<Vec<_>>();
|
|
||||||
// println!("Broadcasting message to {} clients:", client_ids.len());
|
|
||||||
// println!(" - message: {message:?}");
|
|
||||||
|
|
||||||
stream::iter(
|
|
||||||
client_ids
|
|
||||||
.into_iter()
|
|
||||||
.filter_map(|client_id| state.clients.0.get(&client_id))
|
|
||||||
.map(|client| client.writer.clone()),
|
|
||||||
)
|
|
||||||
.for_each_concurrent(None, move |mut writer| {
|
|
||||||
let message = message.clone();
|
|
||||||
async move {
|
|
||||||
writer.send(Ok(message.into_ws_message().unwrap())).await;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.await;
|
|
||||||
}
|
|
104
src/message.rs
104
src/message.rs
|
@ -1,104 +0,0 @@
|
||||||
use std::collections::HashSet;
|
|
||||||
use std::fmt::Formatter;
|
|
||||||
use std::io::Cursor;
|
|
||||||
|
|
||||||
use axum::extract::ws;
|
|
||||||
use chrono::{DateTime, Utc};
|
|
||||||
|
|
||||||
use mzlib::axum_error::Result;
|
|
||||||
use serde::de::Visitor;
|
|
||||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
|
||||||
use uuid::Uuid;
|
|
||||||
|
|
||||||
#[derive(Derivative, Clone, Serialize, Deserialize)]
|
|
||||||
#[derivative(Debug)]
|
|
||||||
#[serde(tag = "type")]
|
|
||||||
pub enum Message {
|
|
||||||
ServerHello {
|
|
||||||
timestamp: DateTime<Utc>,
|
|
||||||
client_id: Uuid,
|
|
||||||
},
|
|
||||||
JoinRoom {
|
|
||||||
room_id: Uuid,
|
|
||||||
},
|
|
||||||
RoomClientList {
|
|
||||||
room_id: Uuid,
|
|
||||||
clients: HashSet<Uuid>,
|
|
||||||
},
|
|
||||||
ChatMessage {
|
|
||||||
timestamp: DateTime<Utc>,
|
|
||||||
message_id: Uuid,
|
|
||||||
room_id: Uuid,
|
|
||||||
author: Uuid,
|
|
||||||
content: String,
|
|
||||||
},
|
|
||||||
Automerge {
|
|
||||||
client_id: Uuid,
|
|
||||||
room_id: Uuid,
|
|
||||||
#[derivative(Debug = "ignore")]
|
|
||||||
message: AutomergeMessage,
|
|
||||||
},
|
|
||||||
FileUploadBegin {
|
|
||||||
room_id: Uuid,
|
|
||||||
#[serde(with = "serde_bytes")]
|
|
||||||
header: Vec<u8>,
|
|
||||||
},
|
|
||||||
FileUploadChunk {
|
|
||||||
room_id: Uuid,
|
|
||||||
#[serde(with = "serde_bytes")]
|
|
||||||
data: Vec<u8>,
|
|
||||||
},
|
|
||||||
FileUploadComplete {
|
|
||||||
room_id: Uuid,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Message {
|
|
||||||
pub fn into_ws_message(self) -> Result<ws::Message> {
|
|
||||||
let vec = Vec::new();
|
|
||||||
let mut cursor = Cursor::new(vec);
|
|
||||||
ciborium::into_writer(&self, &mut cursor)?;
|
|
||||||
let vec = cursor.into_inner();
|
|
||||||
Ok(ws::Message::Binary(vec))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct AutomergeMessage(pub automerge::sync::Message);
|
|
||||||
|
|
||||||
impl Serialize for AutomergeMessage {
|
|
||||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
|
||||||
where
|
|
||||||
S: Serializer,
|
|
||||||
{
|
|
||||||
let bytes = self.0.clone().encode();
|
|
||||||
serializer.serialize_bytes(&bytes)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'de> Deserialize<'de> for AutomergeMessage {
|
|
||||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
|
||||||
where
|
|
||||||
D: Deserializer<'de>,
|
|
||||||
{
|
|
||||||
struct ThisVisitor;
|
|
||||||
impl<'v> Visitor<'v> for ThisVisitor {
|
|
||||||
type Value = AutomergeMessage;
|
|
||||||
|
|
||||||
fn expecting(&self, f: &mut Formatter) -> std::fmt::Result {
|
|
||||||
write!(f, "an automerge sync message")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn visit_bytes<E>(self, v: &[u8]) -> Result<Self::Value, E>
|
|
||||||
where
|
|
||||||
E: serde::de::Error,
|
|
||||||
{
|
|
||||||
automerge::sync::Message::decode(v)
|
|
||||||
.map(AutomergeMessage)
|
|
||||||
.map_err(|_err| E::custom("invalid automerge sync message"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let visitor = ThisVisitor;
|
|
||||||
deserializer.deserialize_bytes(visitor)
|
|
||||||
}
|
|
||||||
}
|
|
14
src/routes/[grubs].svelte
Normal file
14
src/routes/[grubs].svelte
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
<script context="module">
|
||||||
|
export async function preload({ params }) {
|
||||||
|
return { grubs: params.grubs };
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { onMount } from "svelte";
|
||||||
|
import { socket } from "../lib/stores";
|
||||||
|
import GrubView from "../components/GrubView.svelte";
|
||||||
|
export let grubs;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<GrubView urlPath = {grubs} />
|
40
src/routes/_error.svelte
Normal file
40
src/routes/_error.svelte
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
<script>
|
||||||
|
export let status;
|
||||||
|
export let error;
|
||||||
|
|
||||||
|
const dev = process.env.NODE_ENV === 'development';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
h1, p {
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 2.8em;
|
||||||
|
font-weight: 700;
|
||||||
|
margin: 0 0 0.5em 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 1em auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 480px) {
|
||||||
|
h1 {
|
||||||
|
font-size: 4em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<title>{status}</title>
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
|
<h1>{status}</h1>
|
||||||
|
|
||||||
|
<p>{error.message}</p>
|
||||||
|
|
||||||
|
{#if dev && error.stack}
|
||||||
|
<pre>{error.stack}</pre>
|
||||||
|
{/if}
|
17
src/routes/_layout.svelte
Normal file
17
src/routes/_layout.svelte
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
<script>
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
main {
|
||||||
|
position: relative;
|
||||||
|
max-width: 56em;
|
||||||
|
background-color: white;
|
||||||
|
padding: 2em;
|
||||||
|
margin: 0 auto;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<slot></slot>
|
||||||
|
</main>
|
5
src/routes/index.svelte
Normal file
5
src/routes/index.svelte
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
<script>
|
||||||
|
import GrubView from "../components/GrubView.svelte";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
TODO: redirect to soemthing
|
31
src/server.js
Normal file
31
src/server.js
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
import express from "express";
|
||||||
|
import { Server } from "socket.io";
|
||||||
|
import http from "http";
|
||||||
|
import sirv from "sirv";
|
||||||
|
import * as sapper from "@sapper/server";
|
||||||
|
|
||||||
|
import { socketHandler } from "./lib/server-sync";
|
||||||
|
import { sequelize } from "./lib/models";
|
||||||
|
|
||||||
|
const { PORT, NODE_ENV } = process.env;
|
||||||
|
const dev = NODE_ENV === "development";
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
const server = http.createServer(app);
|
||||||
|
const io = new Server(server);
|
||||||
|
|
||||||
|
app.use(sirv("static", { dev }));
|
||||||
|
app.use(sapper.middleware());
|
||||||
|
|
||||||
|
io.on("connection", socketHandler);
|
||||||
|
|
||||||
|
async function start() {
|
||||||
|
await sequelize.sync();
|
||||||
|
|
||||||
|
const port = PORT || 3000;
|
||||||
|
server.listen(port, () => {
|
||||||
|
console.log("listening on ", port);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
start();
|
0
src/service-worker.js
Normal file
0
src/service-worker.js
Normal file
36
src/template.html
Normal file
36
src/template.html
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||||
|
<meta name="theme-color" content="#333333">
|
||||||
|
|
||||||
|
%sapper.base%
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="global.css">
|
||||||
|
<link rel="manifest" href="manifest.json" crossorigin="use-credentials">
|
||||||
|
<link rel="icon" type="image/png" href="favicon.png">
|
||||||
|
<link rel="apple-touch-icon" href="logo-192.png">
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.0/dist/css/bootstrap.min.css" integrity="sha384-KyZXEAg3QhqLMpG8r+8fhAXLRk2vvoC2f3B09zVXn8CA5QIVfZOJ3BCsw2P0p/We" crossorigin="anonymous">
|
||||||
|
|
||||||
|
<!-- Sapper creates a <script> tag containing `src/client.js`
|
||||||
|
and anything else it needs to hydrate the app and
|
||||||
|
initialise the router -->
|
||||||
|
%sapper.scripts%
|
||||||
|
|
||||||
|
<!-- Sapper generates a <style> tag containing critical CSS
|
||||||
|
for the current page. CSS for the rest of the app is
|
||||||
|
lazily loaded when it precaches secondary pages -->
|
||||||
|
%sapper.styles%
|
||||||
|
|
||||||
|
<!-- This contains the contents of the <svelte:head> component, if
|
||||||
|
the current page has one -->
|
||||||
|
%sapper.head%
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<!-- The application will be rendered inside this element,
|
||||||
|
because `src/client.js` references it -->
|
||||||
|
<div id="sapper">%sapper.html%</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
BIN
static/favicon.png
Normal file
BIN
static/favicon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.1 KiB |
36
static/global.css
Normal file
36
static/global.css
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
font-family: Roboto, -apple-system, BlinkMacSystemFont, Segoe UI, Oxygen, Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.5;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1, h2, h3, h4, h5, h6 {
|
||||||
|
margin: 0 0 0.5em 0;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
code {
|
||||||
|
font-family: menlo, inconsolata, monospace;
|
||||||
|
font-size: calc(1em - 2px);
|
||||||
|
color: #555;
|
||||||
|
background-color: #f0f0f0;
|
||||||
|
padding: 0.2em 0.4em;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 400px) {
|
||||||
|
body {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
}
|
BIN
static/logo-192.png
Normal file
BIN
static/logo-192.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.6 KiB |
BIN
static/logo-512.png
Normal file
BIN
static/logo-512.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 14 KiB |
22
static/manifest.json
Normal file
22
static/manifest.json
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
{
|
||||||
|
"background_color": "#ffffff",
|
||||||
|
"theme_color": "#333333",
|
||||||
|
"name": "TODO",
|
||||||
|
"short_name": "TODO",
|
||||||
|
"display": "minimal-ui",
|
||||||
|
"start_url": "/",
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": "logo-192.png",
|
||||||
|
"sizes": "192x192",
|
||||||
|
"type": "image/png",
|
||||||
|
"purpose": "any maskable"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "logo-512.png",
|
||||||
|
"sizes": "512x512",
|
||||||
|
"type": "image/png",
|
||||||
|
"purpose": "any maskable"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
|
@ -1,6 +0,0 @@
|
||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
"lib": ["ESNext", "DOM"],
|
|
||||||
"jsx": "react-jsx"
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,18 +0,0 @@
|
||||||
import { defineConfig } from "vite";
|
|
||||||
import wasm from "vite-plugin-wasm";
|
|
||||||
import topLevelAwait from "vite-plugin-top-level-await";
|
|
||||||
import react from "@vitejs/plugin-react";
|
|
||||||
|
|
||||||
export default defineConfig({
|
|
||||||
plugins: [react(), wasm(), topLevelAwait()],
|
|
||||||
server: {
|
|
||||||
proxy: {
|
|
||||||
"/ws": {
|
|
||||||
target: "ws://localhost:6106/ws",
|
|
||||||
changeOrigin: true,
|
|
||||||
rewrite: (path) => path.replace(/^\/ws/, ""),
|
|
||||||
ws: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
Loading…
Reference in a new issue