diff --git a/client/package-lock.json b/client/package-lock.json index 5e7a0103..75280788 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -23,6 +23,7 @@ "react-hook-form": "^7.53.0", "react-router-dom": "^6.26.2", "react-sigma": "^1.2.35", + "react-use-websocket": "^4.9.0", "sigma": "^3.0.0-beta.29", "vite": "^5.4.7", "vite-tsconfig-paths": "^5.0.1", @@ -3151,6 +3152,11 @@ "react-dom": ">=15.3" } }, + "node_modules/react-use-websocket": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/react-use-websocket/-/react-use-websocket-4.9.0.tgz", + "integrity": "sha512-/6OaCMggQCTnryCAsw/N+/wfH7bBfIXk5WXTMPdyf0x9HWJXLGUVttAT5hqAimRytD1dkHEJCUrFHAGzOAg1eg==" + }, "node_modules/redent": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", diff --git a/client/package.json b/client/package.json index bc8215e5..d845f2f9 100644 --- a/client/package.json +++ b/client/package.json @@ -18,6 +18,7 @@ "react-hook-form": "^7.53.0", "react-router-dom": "^6.26.2", "react-sigma": "^1.2.35", + "react-use-websocket": "^4.9.0", "sigma": "^3.0.0-beta.29", "vite": "^5.4.7", "vite-tsconfig-paths": "^5.0.1", diff --git a/client/src/App.jsx b/client/src/App.jsx index cc6507e2..880043b7 100644 --- a/client/src/App.jsx +++ b/client/src/App.jsx @@ -1,12 +1,12 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { BrowserRouter, Routes, Route } from 'react-router-dom'; import Landing from '/src/landing/Landing.jsx'; -import Page from '/src/page/Page.jsx'; -import PageWithSidebar from '/src/page/PageWithSidebar.jsx'; import HistoryView from '/src/page/HistoryView.jsx'; import LogIn from '/src/login/LogIn.jsx'; import Register from '/src/login/Register.jsx'; import Profile from '/src/login/Profile.jsx'; +import GhostPage from '/src/page/GhostPage.jsx'; +import Live from '/src/embodied/Live.jsx'; import AuthProvider from '/src/AuthProvider.jsx'; @@ -23,11 +23,12 @@ function App() { }/> }/> }/> - }/> - }/> + }/> + }/> }/> - }/> + }/> }/> + }/> diff --git a/client/src/apiTools.jsx b/client/src/apiTools.jsx index cd68c754..b12beeb3 100644 --- a/client/src/apiTools.jsx +++ b/client/src/apiTools.jsx @@ -3,7 +3,11 @@ import { LogInStatusUpdateEscapeTool } from './AuthProvider.jsx'; // This is wrapper functtions to do requests to the api, from the frontend. -export const apiUrl = `${window.location.origin}/api`; +const secure = false + +export const apiUrl = `http${secure?'s':''}://${window.location.host}/api`; + +export const wsUrl = `ws${secure?'s':''}://${window.location.host}/api` //lil helper to throw errorrs from the promise when we get not-ok results diff --git a/client/src/page/CommandEntry.css b/client/src/embodied/CommandEntry.css similarity index 91% rename from client/src/page/CommandEntry.css rename to client/src/embodied/CommandEntry.css index e720726d..6aa7cac0 100644 --- a/client/src/page/CommandEntry.css +++ b/client/src/embodied/CommandEntry.css @@ -14,7 +14,7 @@ font-size: 16pt; border: none; padding: 1rem; - width: calc(100% - 2rem); + width: calc(100% - 2rem); margin-left: 0; margin-right: 0; background: transparent; diff --git a/client/src/embodied/CommandEntry.jsx b/client/src/embodied/CommandEntry.jsx new file mode 100644 index 00000000..0ce2e322 --- /dev/null +++ b/client/src/embodied/CommandEntry.jsx @@ -0,0 +1,12 @@ +import './CommandEntry.css'; + +function CommandEntry({command, onChange, onSubmit, ...props}) { + return ( +
+ + +
+ ); +} + +export default CommandEntry; \ No newline at end of file diff --git a/client/src/embodied/Live.jsx b/client/src/embodied/Live.jsx new file mode 100644 index 00000000..8fe1f233 --- /dev/null +++ b/client/src/embodied/Live.jsx @@ -0,0 +1,60 @@ +import { useState, useEffect } from 'react'; +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; +import { useParams, useNavigate } from 'react-router-dom'; +import { apiUrl, wsUrl } from '../apiTools.jsx'; +import { useFixLinks } from '../clientStuff.jsx'; +import { useLoggedIn } from '../AuthProvider.jsx'; +import useWebSocket, { ReadyState } from 'react-use-websocket'; + +import Page from '../page/Page.jsx'; +import MessageFeed from './MessageFeed.jsx'; +import Sidebar from './Sidebar.jsx'; +import CommandEntry from './CommandEntry.jsx'; + +function Live({editing, ...props}) { + const navigate = useNavigate(); + const loggedIn = useLoggedIn(); + const { pagenumber } = useParams(); + const [command, setCommand] = useState(""); + const [ messageHistory, setMessageHistory ] = useState([]); + const [ currentNumber, setCurrentNumber ] = useState(1); + + const [ connecting, setConnecting ] = useState(true); + const { sendMessage, lastMessage, readyState } = useWebSocket(`${wsUrl}/embody`, { + onClose: () => setConnecting(false) + }, connecting); + + useEffect(() => { + if (lastMessage !== null) { + setMessageHistory((prev) => prev.concat(lastMessage)); + } + }, [lastMessage]); + + useEffect(() => { sendMessage("what the fuk is up"); }, []); + + function handleSendMessage() { + console.log("sending a message..."); + sendMessage("button got clicked"); + } + + return ( + <> + + + + + + + setCommand(e.target.value)} + onSubmit={(e) => { setCommand(""); sendMessage(e); }}/> + + ); +} + +export default Live; \ No newline at end of file diff --git a/client/src/embodied/MessageFeed.css b/client/src/embodied/MessageFeed.css new file mode 100644 index 00000000..b95a7033 --- /dev/null +++ b/client/src/embodied/MessageFeed.css @@ -0,0 +1,25 @@ +.messages-tray { + transition: all 0.1s linear; + margin: 0; + background: lightblue; + position: fixed; + width: 30ch; + height: 100%; + left: 0; + top: 0; +} +.messages-hidden { + transform: translateX(-20ch); +} + +.sidebar li { + list-style: none; +} + +.sidebar li button { + text-transform: uppercase; +} + +.sidebar-hidden li { + display: none; +} \ No newline at end of file diff --git a/client/src/embodied/MessageFeed.jsx b/client/src/embodied/MessageFeed.jsx new file mode 100644 index 00000000..acb4f511 --- /dev/null +++ b/client/src/embodied/MessageFeed.jsx @@ -0,0 +1,27 @@ +import { useEffect, useState, useCallback } from 'react'; +import { useForm } from "react-hook-form"; +import { useNavigate } from 'react-router-dom'; + +import './MessageFeed.css'; + +function MessageFeed({messages=[], children}) { + const [ open, setOpen ] = useState(false); + + return ( +
+ +

Message history:

+
    + {messages.map((message, idx) => +
  1. + {message.data} + {JSON.stringify(message)} +
  2. + )} +
+ {children} +
+ ); +} + +export default MessageFeed; \ No newline at end of file diff --git a/client/src/page/Sidebar.css b/client/src/embodied/Sidebar.css similarity index 100% rename from client/src/page/Sidebar.css rename to client/src/embodied/Sidebar.css diff --git a/client/src/page/Sidebar.jsx b/client/src/embodied/Sidebar.jsx similarity index 100% rename from client/src/page/Sidebar.jsx rename to client/src/embodied/Sidebar.jsx diff --git a/client/src/landing/Landing.jsx b/client/src/landing/Landing.jsx index 470a79af..feeb6942 100644 --- a/client/src/landing/Landing.jsx +++ b/client/src/landing/Landing.jsx @@ -48,6 +48,7 @@ function Landing() {


+
: <> diff --git a/client/src/login/Profile.jsx b/client/src/login/Profile.jsx index 595b585c..18885620 100644 --- a/client/src/login/Profile.jsx +++ b/client/src/login/Profile.jsx @@ -1,28 +1,28 @@ -import { useEffect } from 'react'; +import { useEffect, useState, useCallback } from 'react'; import { useForm } from "react-hook-form"; import { useNavigate } from 'react-router-dom'; import { fetchProfile } from '../apiTools.jsx'; import { useLoggedIn } from '../AuthProvider.jsx'; import { useQuery } from '@tanstack/react-query'; +import useWebSocket, { ReadyState } from 'react-use-websocket'; function Profile() { const { register, handleSubmit } = useForm(); const navigate = useNavigate(); const loggedIn = useLoggedIn(); + useEffect(() => { if (!loggedIn) navigate('/'); }, [loggedIn]); const { isPending, isError, error, data } = useQuery({ // fetch the currrent values queryKey: ['profile'], queryFn: fetchProfile, - retry: 1 }); - - useEffect(() => { if (!loggedIn) navigate('/'); }, [loggedIn]); - const { id, name, favoriteColor, leastFavoriteColor } = (data || {}); + const playerObject = 30; return (
+

Profile page

the page {isPending ? "is" : "isn't"} pending

there {isError ? "is" : "isn't"} an error

error is {error?.message}

@@ -47,6 +47,11 @@ function Profile() {
+
diff --git a/client/src/page/CommandEntry.jsx b/client/src/page/CommandEntry.jsx deleted file mode 100644 index d1f3d20b..00000000 --- a/client/src/page/CommandEntry.jsx +++ /dev/null @@ -1,13 +0,0 @@ -import { useState } from 'react'; - -import './CommandEntry.css'; - -function CommandEntry({command, onChange}) { - return ( -
- -
- ); -} - -export default CommandEntry; \ No newline at end of file diff --git a/client/src/page/GhostPage.jsx b/client/src/page/GhostPage.jsx new file mode 100644 index 00000000..d4187603 --- /dev/null +++ b/client/src/page/GhostPage.jsx @@ -0,0 +1,19 @@ +import { useState } from 'react'; +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; +import { useParams, useNavigate } from 'react-router-dom'; +import { apiUrl, fetchPage, fetchPageAtEdit, postPage, deletePage } from '../apiTools.jsx'; +import { useFixLinks } from '../clientStuff.jsx'; +import { useLoggedIn } from '../AuthProvider.jsx'; +import Page from './Page.jsx'; + +import './Pages.css'; + +function GhostPage({editing, ...props}) { + const { pagenumber } = useParams(); + + return ( + + ); +} + +export default GhostPage; \ No newline at end of file diff --git a/client/src/page/Page.jsx b/client/src/page/Page.jsx index 6986fd45..2ebe0dcc 100644 --- a/client/src/page/Page.jsx +++ b/client/src/page/Page.jsx @@ -6,10 +6,11 @@ import { useLoggedIn } from '../AuthProvider.jsx'; import './Pages.css'; -function Page({ editing }) { +function Page({ editing, number }) { const queryClient = useQueryClient(); const navigate = useNavigate(); - const { pagenumber, editid } = useParams(); + const { editid } = useParams(); + const pagenumber = number const loggedIn = useLoggedIn(); const noLoad = useFixLinks(); diff --git a/client/src/page/PageWithSidebar.jsx b/client/src/page/PageWithSidebar.jsx deleted file mode 100644 index 5b3323b0..00000000 --- a/client/src/page/PageWithSidebar.jsx +++ /dev/null @@ -1,128 +0,0 @@ -import { useState } from 'react'; -import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; -import { useParams, useNavigate } from 'react-router-dom'; -import { apiUrl, fetchPage, fetchPageAtEdit, postPage, deletePage } from '../apiTools.jsx'; -import { useFixLinks } from '../clientStuff.jsx'; -import { useLoggedIn } from '../AuthProvider.jsx'; -import Sidebar from './Sidebar.jsx'; -import CommandEntry from './CommandEntry.jsx'; - -import './Pages.css'; - -function PageWithSidebar({ editing }) { - const queryClient = useQueryClient(); - const navigate = useNavigate(); - const { pagenumber, editid } = useParams(); - const loggedIn = useLoggedIn(); - const noLoad = useFixLinks(); - const [command, setCommand] = useState(""); - - const fetchQuery = useQuery({ // fetch the currrent values - queryKey: ['page', pagenumber, editid], - queryFn: () => editid ? fetchPageAtEdit(pagenumber, editid) : fetchPage(pagenumber) - }) - - const postMutation = useMutation({ // for changing the value when we're done with it - mutationFn: ({id, title, description}) => postPage({id, title, description}), - onSettled: async (data, error, variables) => { - // Invalidate and refetch - await queryClient.invalidateQueries({ queryKey: ['page', variables.id, undefined] }) - }, - }); - - const deleteMutation = useMutation({ // for changing the value when we're done with it - mutationFn: (id) => deletePage(id), - onSettled: async (data, error, variables) => { - // Invalidate and refetch - await queryClient.invalidateQueries({ queryKey: ['pages'] }) - }, - }); - - const readyToShow = !(fetchQuery.error || fetchQuery.isPending); - - let {id, title, description, html, time, author} = fetchQuery.data || {}; - if (!title) title = "[no title]"; - if (!html) html = "[body missing]"; - if (!description) description = "[body missing]"; - - function submitChanges(e) { - const newTitle = document.querySelector('span').innerHTML; - const newText = document.querySelector('pre').innerHTML; - postMutation.mutate({ - id: pagenumber, - title: newTitle, - description: newText - }); - navigate(`/${pagenumber}`, {replace: true}) - } - - function submitDelete(e) { - e.preventDefault(); - deleteMutation.mutate(pagenumber); - navigate(`/`); - } - - return ( - <> -
-
-

- 🌳 - {pagenumber}.  - -

- {readyToShow && editid && `saved at ${time} by user #${author}`} -
-
-
- { readyToShow ? - ( - editing ? -
-                :
-                
- ) - : - "..." - } -
- - {editing && ( - )} - {!editing && !editid && ( - )} - {loggedIn && ( - )} -
- - setCommand(e.target.value)}/> - - ); -} - -export default PageWithSidebar; \ No newline at end of file diff --git a/server/authStuff.js b/server/authStuff.js index 3b211ddd..4636018c 100644 --- a/server/authStuff.js +++ b/server/authStuff.js @@ -1,5 +1,5 @@ function loginRequired(req, res, next) { - console.log("checkinig on req.session for auhetnticaion: ", req.session); + //console.log("checkinig on req.session for auhetnticaion: ", req.session); if (!req.session.name) { return res.status(401).json({"error": "need to be logged in for that bucko"}); } diff --git a/server/interpreter.js b/server/interpreter.js new file mode 100644 index 00000000..c5f2faa6 --- /dev/null +++ b/server/interpreter.js @@ -0,0 +1,47 @@ +const sqlite = require('better-sqlite3'); +const db = new sqlite('the_big_db.db', { verbose: console.log }); + +var sockets = new Map(); + +function interpret(context, subject, command) { + const words = command.split(' '); + + const verbs = findVerbs(context, subject); + // first word is either a subject or a verb. either way there must be a verb. + // check if the first word is in the list of verbs. + const [first, second, third, ...rest] = words; + if (second in verbs) { + executeVerb(verbs.get(second), first, third, ...rest); + } else { + executeVerb(verbs.get(first), subject, second, third, ...rest) + } +} + +function findVerbs(location, actor) { + // check for verbs on actor + // check for verbs on objects in location + // check for verbs on location + const out = new Map(); + out.set("look", 29); + return out; +} + +function executeVerb(verb, subject, object, ...rest) { + lookUpObject(verb).fn(subject, object, ...rest) +} + +const objectQuery = db.prepare('select * from pages where id=?'); +function lookUpObject(id) { + // return objectQuery.get(id); + if (id == 30) return {name: "shoofle", contents: "this is a shoofle", location: 1}; + if (id == 29) return { + name: "look", + contents: "send description of direct object to subject's socket", + fn: (subject, object, ...rest) => { + sockets.get(subject)?.send(`you looked around! subject: ${subject} object: ${object} args: ${rest}`); + console.log(`${subject} looked at ${object} with args ${rest}`); + } + }; +} + +module.exports = { interpret, sockets, lookUpObject }; \ No newline at end of file diff --git a/server/package-lock.json b/server/package-lock.json index e93604f5..d02dd7af 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -16,6 +16,7 @@ "client-sessions": "^0.8.0", "express": "^4.21.0", "express-session": "^1.18.0", + "express-ws": "^5.0.2", "graphology": "^0.25.4", "graphology-layout": "^0.6.1", "graphology-layout-force": "^0.2.4", @@ -815,6 +816,40 @@ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==" }, + "node_modules/express-ws": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/express-ws/-/express-ws-5.0.2.tgz", + "integrity": "sha512-0uvmuk61O9HXgLhGl3QhNSEtRsQevtmbL94/eILaliEADZBHZOQUAiHFrGPrgsjikohyrmSG5g+sCfASTt0lkQ==", + "dependencies": { + "ws": "^7.4.6" + }, + "engines": { + "node": ">=4.5.0" + }, + "peerDependencies": { + "express": "^4.0.0 || ^5.0.0-alpha.1" + } + }, + "node_modules/express-ws/node_modules/ws": { + "version": "7.5.10", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", + "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/fetch-blob": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", diff --git a/server/package.json b/server/package.json index c9d4d8c4..8a3139fd 100644 --- a/server/package.json +++ b/server/package.json @@ -21,6 +21,7 @@ "client-sessions": "^0.8.0", "express": "^4.21.0", "express-session": "^1.18.0", + "express-ws": "^5.0.2", "graphology": "^0.25.4", "graphology-layout": "^0.6.1", "graphology-layout-force": "^0.2.4", diff --git a/server/routes/api.js b/server/routes/api.js index 68a53a1e..225b0c27 100644 --- a/server/routes/api.js +++ b/server/routes/api.js @@ -12,6 +12,9 @@ app.use(page_routes); const user_routes = require('./users.js'); app.use(user_routes); +const socket_routes = require('./sockets.js'); +app.use(socket_routes); + const graphQuery = db.prepare(` select p.number, p.html, p.time from pages p diff --git a/server/routes/sockets.js b/server/routes/sockets.js new file mode 100644 index 00000000..a2b722b2 --- /dev/null +++ b/server/routes/sockets.js @@ -0,0 +1,42 @@ +const express = require('express'); +const app = express.Router(); +const expressWs = require('express-ws')(app); + +const sqlite = require('better-sqlite3'); +const db = new sqlite('the_big_db.db', { verbose: console.log }); + +const { loginRequired } = require('../authStuff.js'); + +const { interpret, sockets, lookUpObject } = require('../interpreter.js'); + +const clockListeners = new Set(); +const clock = setInterval(() => { + if (clockListeners.size == 0) return; + console.log(`sending a ping to all ${clockListeners.size} connections`); + clockListeners.forEach((x) => x()); +}, 5000); +console.log(`set up the clock: ${clock}`); + + + + + + +app.ws('/embody', (ws, req) => { + // const { playerObject } = db.prepare('select playerObject from users where id=?').get(req.session.userId) + const playerObjectId = 30; // mocked out for now! + sockets.set(playerObjectId, ws); + + ws.on('message', (msg) => { + const location = lookUpObject(playerObjectId).location; + + interpret(location, playerObjectId, msg); + }); + + ws.on('close', () => sockets.delete(playerObjectId)); +}); + + + + +module.exports = app; \ No newline at end of file diff --git a/server/routes/users.js b/server/routes/users.js index ecd16638..853442e4 100644 --- a/server/routes/users.js +++ b/server/routes/users.js @@ -29,6 +29,7 @@ app.post('/register', async (req, res) => { }); app.post('/login', async (req, res) => { + console.log(req.body); if (req.session.name) { return res.status(200).json({message: "already logged in", name: req.session.name}); } diff --git a/server/server.js b/server/server.js index 8870b07b..7a0c21b3 100644 --- a/server/server.js +++ b/server/server.js @@ -1,5 +1,6 @@ const express = require('express'); const app = express(); +const expressWs = require('express-ws')(app); const session = require('express-session'); @@ -8,7 +9,7 @@ const apiRoutes = require('./routes/api.js'); const port = process.env.PORT || 3001; // Use the port provided by the host or default to 3000 const sqlite = require('better-sqlite3'); -const db = new sqlite('the_big_db.db', { verbose: console.log }); +const db = new sqlite('the_big_db.db'); const SqliteStore = require('better-sqlite3-session-store')(session); app.use(express.json()); diff --git a/the-forest.nginx.conf b/the-forest.nginx.conf index 5fddc5ca..04c4a0ed 100644 --- a/the-forest.nginx.conf +++ b/the-forest.nginx.conf @@ -5,6 +5,11 @@ server { location /api/ { proxy_pass http://localhost:3001/api/; + + # WebSocket support + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; } location / { proxy_pass http://localhost:3000/;