move environment variables to their own file, also massive features

This commit is contained in:
Shoofle 2024-10-25 17:48:12 -04:00
parent 7d650ef424
commit 41d0f148de
23 changed files with 547 additions and 510 deletions

View File

@ -1,6 +1,6 @@
body { html {
background: rgb(0,131,77); background: rgb(0,131,77);
background: radial-gradient(circle, rgba(0,131,77,1) 0%, rgba(17,66,0,1) 100%); /*background: radial-gradient(circle, rgba(0,131,77,1) 0%, rgba(17,66,0,1) 100%);*/
font-family: sans-serif; font-family: sans-serif;
} }
@ -42,3 +42,13 @@ header hr {
padding: 1rem; padding: 1rem;
border-radius: 1rem; border-radius: 1rem;
} }
input {
background: none;
border: none;
padding: 0;
font-size: unset;
color: unset;
width: auto;
overflow: visible;
}

View File

@ -8,6 +8,8 @@ import Profile from '/src/login/Profile.jsx';
import GhostPage from '/src/page/GhostPage.jsx'; import GhostPage from '/src/page/GhostPage.jsx';
import Live from '/src/embodied/Live.jsx'; import Live from '/src/embodied/Live.jsx';
//import Browsing from '/src/entwined/Browsing.jsx';
import AuthProvider from '/src/AuthProvider.jsx'; import AuthProvider from '/src/AuthProvider.jsx';
import './App.css'; import './App.css';

View File

@ -42,6 +42,10 @@ export async function fetchPage(number) {
return shoofetch(`${apiUrl}/page/${number}`, {method: 'GET'}); return shoofetch(`${apiUrl}/page/${number}`, {method: 'GET'});
} }
export async function fetchPageAttributes(number) {
return shoofetch(`${apiUrl}/page/${number}/attributes`, {method: 'GET'});
}
export async function fetchPageHistory(number) { export async function fetchPageHistory(number) {
return shoofetch(`${apiUrl}/page/${number}/history`, {method: 'GET'}); return shoofetch(`${apiUrl}/page/${number}/history`, {method: 'GET'});
} }
@ -50,10 +54,10 @@ export async function fetchPageAtEdit(number, id) {
return shoofetch(`${apiUrl}/page/${number}/${id}`, {method: 'GET'}); return shoofetch(`${apiUrl}/page/${number}/${id}`, {method: 'GET'});
} }
export async function postPage({id, title, description, type}) { export async function postPage({number, title, description, type}) {
return shoofetch(`${apiUrl}/page/${id}`, { return shoofetch(`${apiUrl}/page/${number}`, {
method: 'POST', method: 'POST',
body: JSON.stringify({id: id, title: title, description: description, type: type}), body: JSON.stringify({number: number, title: title, description: description, type: type}),
}) })
} }

View File

@ -10,6 +10,7 @@
left: 20%; left: 20%;
right: 20%; right: 20%;
border-radius: 1rem; border-radius: 1rem;
border: 1px solid gray;
} }
.commandline form { .commandline form {
display: flex; display: flex;
@ -21,7 +22,7 @@
font-size: 16pt; font-size: 16pt;
border: none; border: none;
padding: 2rem; padding: 2rem;
border-radius: 1rem;
background: transparent; background: transparent;
} }

View File

@ -1,18 +1,30 @@
import { forwardRef } from 'react';
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import './CommandEntry.css'; import './CommandEntry.css';
function CommandEntry({ onSubmit }) { const CommandEntry = forwardRef(({ setCommand, onSubmitCommand, command }, ref) => {
const { register, handleSubmit, setValue } = useForm(); const { register, handleSubmit, setValue } = useForm();
return ( return (
<div className="commandline"> <div className="commandline">
<form onSubmit={handleSubmit((data) => { setValue('command', ""); onSubmit(data.command); })}> <form onSubmit={
<input {...register("command")} autoFocus type="text" placeholder="Enter a command!" /> handleSubmit((data) => { onSubmitCommand(data.command); })
}>
<input {...register("command")}
ref={ref}
onChange={(e) => {
setCommand(e.target.value);
setValue("command", e.target.value);
}}
autoFocus
type="text"
placeholder="Enter a command!"
value={command} />
<button type="submit">Do</button> <button type="submit">Do</button>
</form> </form>
</div> </div>
); );
} });
export default CommandEntry; export default CommandEntry;

View File

@ -1,53 +1,132 @@
import { useState, useEffect } from 'react'; import { useState, useEffect, useRef } from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { useParams, useNavigate } from 'react-router-dom'; import { useParams, useNavigate } from 'react-router-dom';
import { apiUrl, wsUrl } from '../apiTools.jsx'; import { apiUrl, wsUrl, fetchPage, fetchPageAttributes, fetchPageAtEdit, postPage, fetchCurrentVerbs } from '../apiTools.jsx';
import { useFixLinks } from '../clientStuff.jsx'; import { useFixLinks } from '../clientStuff.jsx';
import { useLoggedIn } from '../AuthProvider.jsx'; import { useLoggedIn } from '../AuthProvider.jsx';
import useWebSocket, { ReadyState } from 'react-use-websocket'; import useWebSocket, { ReadyState } from 'react-use-websocket';
import Page from '../page/Page.jsx'; import Page from '../page/Page.jsx';
import ExtendedAttributes from '../page/ExtendedAttributes.jsx';
import MessageFeed from './MessageFeed.jsx'; import MessageFeed from './MessageFeed.jsx';
import Sidebar from './Sidebar.jsx'; import Sidebar from './Sidebar.jsx';
import CommandEntry from './CommandEntry.jsx'; import CommandEntry from './CommandEntry.jsx';
function Live({editing, ...props}) { function Live({...props}) {
const navigate = useNavigate(); const navigate = useNavigate();
const loggedIn = useLoggedIn(); const loggedIn = useLoggedIn();
const queryClient = useQueryClient();
const [ command, setCommand ] = useState('');
const [ messageHistory, setMessageHistory ] = useState([]); const [ messageHistory, setMessageHistory ] = useState([]);
const [ currentNumber, setCurrentNumber ] = useState(1); const [ currentNumber, setCurrentNumber ] = useState(1);
const [ editing, setEditing ] = useState(false);
const [ connecting, setConnecting ] = useState(true); const [ connecting, setConnecting ] = useState(true);
const commandEntryRef = useRef(null);
//setting up the websocket and using its data!
const { sendMessage, lastMessage, lastJsonMessage, readyState } = useWebSocket(`${wsUrl}/embody`, { const { sendMessage, lastMessage, lastJsonMessage, readyState } = useWebSocket(`${wsUrl}/embody`, {
onClose: () => setConnecting(false) onClose: () => console.log("broke connection!"),
onOpen: () => console.log("opened connection"),
shouldReconnect: (closeEvent) => true,
}, connecting); }, connecting);
useEffect(() => { useEffect(() => console.log(lastMessage), [lastMessage]);
if (lastMessage !== null) {
console.log(lastMessage);
setMessageHistory((prev) => prev.concat(lastMessage));
if (lastMessage.data.startsWith("location change to: ")) { useEffect(() => {
const num = Number(lastMessage.data.replace("location change to: #", "")); if (!(lastJsonMessage === null || lastJsonMessage === undefined)) {
setCurrentNumber(num); setMessageHistory((prev) => prev.concat(lastJsonMessage));
//if (lastJsonMessage["error"]) alert(lastJsonMessage["error"]);
if (lastJsonMessage["setPageNumber"])
setCurrentNumber(Number(lastJsonMessage["setPageNumber"]));
} }
}, [lastJsonMessage, lastMessage]);
// first class object data values!
const fetchPageQuery = useQuery({
queryKey: ['page', currentNumber, null],
queryFn: () => fetchPage(currentNumber),
});
// setting up for editing and suchlike
const [title, setTitle] = useState(null);
useEffect(() => {
setTitle(fetchPageQuery.data?.title);
}, [fetchPageQuery.data?.title]);
const [text, setText] = useState(null);
useEffect(() => {
setText(fetchPageQuery.data?.description);
}, [fetchPageQuery.data?.description]);
const [type, setType] = useState(false);
useEffect(() => {
setType(fetchPageQuery.data?.type == 1);
}, [fetchPageQuery.data?.type]);
// extended attribute store!
const fetchAttributesQuery = useQuery({
queryKey: ['attributes', currentNumber],
queryFn: () => fetchPageAttributes(currentNumber)
});
// verbs available to us
const fetchVerbsQuery = useQuery({
queryKey: ['my verbs'],
queryFn: fetchCurrentVerbs,
});
let verbs = fetchVerbsQuery.data || [];
if (!editing) verbs = ["edit this"].concat(verbs);
else verbs = ["save changes"].concat(verbs);
const postMutation = useMutation({ // for changing the value when we're done with it
mutationFn: postPage,
onSettled: async (data, error, variables) => {
queryClient.invalidateQueries(['page', variables.number, null])
},
});
function saveChanges() {
postMutation.mutate({
number: currentNumber,
title: title,
description: text,
type: type ? 1 : 0,
});
}
function handleSubmitCommand() {
const c = command.trim();
if (c == "edit this") setEditing(true);
else if (c == "save changes") {
saveChanges();
setEditing(false);
}
else sendMessage(c);
setCommand("");
} }
}, [lastMessage]);
// spread this return value on <a/> elements in order to make them navigate // spread this return value on <a/> elements in order to make them navigate
const commandLinkClick = (e) => { const commandLinkClick = (e) => {
if (e.target.tagName != "A") { return; } if (e.target.tagName != "A") return;
if (!e.target.href.includes(window.location.origin)) { return; } if (!e.target.href.includes(window.location.origin)) return;
if (e.ctrlKey || e.metaKey || e.shiftKey || e.altKey) return;
// if it was an <a> tag, if it's to a subdomain of this website, and a modifier wasn't held
// then we should prevent default behavior and execute our own.
e.preventDefault(); e.preventDefault();
const localLink = e.target.href.replace(window.location.origin, ""); const localLink = e.target.href.replace(window.location.origin, "");
const targetString = localLink.replace("/", ""); const targetString = localLink.replace("/", "");
const targetNumber = Number(targetString); const targetNumber = Number(targetString);
if (targetNumber != 0 && targetNumber != NaN) { if (targetNumber != 0 && !isNaN(targetNumber)) {
const warpMessage = `warp #${targetString}`; const warpMessage = `warp #${targetNumber}`;
console.log(`clicked a link, executing "warp #${targetString}"`); console.log(`clicked a link, executing "warp #${targetNumber}"`);
sendMessage(warpMessage); sendMessage(warpMessage);
return; return;
} }
@ -55,16 +134,46 @@ function Live({editing, ...props}) {
navigate(e.target.href.replace(window.location.origin, "")); navigate(e.target.href.replace(window.location.origin, ""));
}; };
useEffect(() => {
window.history.replaceState(null, "", window.location.origin + "/" + currentNumber);
}, [currentNumber]);
return ( return (
<> <>
<Page editing={editing} number={currentNumber} linkClick={commandLinkClick} {...props} /> <div className="main-column">
<Page
page={{...fetchPageQuery.data, description: text, title: title, type: type }}
editing={editing}
linkClick={commandLinkClick}
onChangeTitle={(e) => setTitle(e.target.value)}
onChangeText={setText}
onChangeType={() => setType(!type)}
{...props} />
<ExtendedAttributes
attributes={fetchAttributesQuery.data} />
</div>
<MessageFeed messages={messageHistory}> <MessageFeed messages={messageHistory}>
<button disabled={connecting} onClick={() => setConnecting(true)}>Reconnect</button> <button disabled={connecting} onClick={() => setConnecting(true)}>
{{
[ReadyState.CONNECTING]: 'Connecting',
[ReadyState.OPEN]: 'Open',
[ReadyState.CLOSING]: 'Closing',
[ReadyState.CLOSED]: 'Closed',
[ReadyState.UNINSTANTIATED]: 'Uninstantiated',
}[readyState]}</button>
<button onClick={()=> setMessageHistory([])}>Clear History</button> <button onClick={()=> setMessageHistory([])}>Clear History</button>
</MessageFeed> </MessageFeed>
<Sidebar pagenumber={currentNumber} hidden="true" sendWord={(word) => setCommand((command + " " + word).trim())}> <Sidebar verbs={verbs} sendWord={(word) => {
setCommand((command + " " + word).trim());
commandEntryRef.current.focus();
// maybe set focus to the command entry?
}}>
</Sidebar> </Sidebar>
<CommandEntry onSubmit={(w) => { console.log(w); sendMessage(w); } }/> <CommandEntry
ref={commandEntryRef}
setCommand={(val) => {setCommand(val)}}
onSubmitCommand={handleSubmitCommand}
command={command} />
</> </>
); );
} }

View File

@ -17,7 +17,7 @@ function MessageFeed({messages=[], children}) {
<ol ref={listContainer}> <ol ref={listContainer}>
{messages.map((message, idx) => {messages.map((message, idx) =>
<li key={idx}> <li key={idx}>
{message.data} {JSON.stringify(message)}
</li> </li>
)} )}
</ol> </ol>

View File

@ -5,24 +5,17 @@ import { fetchCurrentVerbs } from '../apiTools.jsx';
import './Sidebar.css'; import './Sidebar.css';
function Sidebar({children, pagenumber, hidden=false, sendWord=(()=>null)}) { function Sidebar({children, verbs, hidden=true, sendWord=(()=>null)}) {
const [open, setOpen] = useState(!hidden); const [open, setOpen] = useState(!hidden);
const loggedIn = useLoggedIn(); const loggedIn = useLoggedIn();
const { isPending, isError, error, data } = useQuery({ // fetch the currrent values
queryKey: ['my verbs'],
queryFn: fetchCurrentVerbs,
});
const verbs = data;
return ( return (
<div className={`sidebar ${!open && "sidebar-hidden"}`}> <div className={`sidebar ${!open && "sidebar-hidden"}`}>
<button onClick={() => setOpen(!open)}>{open ? "hide" : "show"}</button> <button onClick={() => setOpen(!open)}>{open ? "hide" : "show"}</button>
<ul> <ul>
{(verbs || []).map( (name) => ( {verbs.map( (name) => (
<li key={name}> <li key={name}>
<button onClick={() => sendWord(name)}> <button onClick={() => { console.log("clicked button for", name); sendWord(name);}}>
{name} {name}
</button> </button>
</li> </li>

View File

@ -1,7 +1,9 @@
import { useEffect } from 'react'; import { useEffect } from 'react';
import { SigmaContainer } from "@react-sigma/core"; import { SigmaContainer, useRegisterEvents } from "@react-sigma/core";
import "@react-sigma/core/lib/react-sigma.min.css"; import "@react-sigma/core/lib/react-sigma.min.css";
import { useNavigate } from 'react-router-dom';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { fetchGraph } from '../apiTools.jsx'; import { fetchGraph } from '../apiTools.jsx';
@ -22,7 +24,21 @@ export default function GraphRender() {
return () => supervisor.stop(); return () => supervisor.stop();
} }
}, [data]);
/*
const navigate = useNavigate();
const registerEvents = useRegisterEvents();
useEffect(() => {
console.log("register events");
// Register the events
registerEvents({
// node events
clickNode: (event) => navigate(`/${event.node}`),
}); });
}, [registerEvents]);
*/
if (isPending) return "Loading..."; if (isPending) return "Loading...";
else if (error) return `Error encountered: ${error}`; else if (error) return `Error encountered: ${error}`;

View File

@ -0,0 +1,37 @@
function ExtendedAttributes({attributes, ...props}) {
if (!attributes) return;
return <>
<section className="page-contents">
{attributes.parent ?
`The parent of this object is #${attributes.parent}.`
:
"This object has no parent."}
</section>
{attributes.location && <section className="page-contents">
This object lives in #{JSON.stringify(attributes.location)}.
</section>}
{attributes.contents && typeof attributes.contents.map === 'function' && <section className="page-contents">
<h4>Contents of this location:</h4>
<ul>
{attributes.contents.map((objectNum) => <li key={objectNum}>{objectNum}</li>)}
</ul>
</section>}
{attributes.verbs && <section className="page-contents">
<h4>Verbs defined on this object:</h4>
<ul>
{attributes.verbs.map((verbNum) => <li key={verbNum}>{verbNum}</li>)}
</ul>
</section>}
{attributes.prepositions && <section className="page-contents">
<h4>Prepositions this verb expects:</h4>
<ul>
{attributes.prepositions.map((prep) => <li key={prep}>{prep}</li>)}
</ul>
</section>}
</>
}
export default ExtendedAttributes;

View File

@ -1,29 +1,98 @@
import { useState } from 'react'; import { useState, useEffect } from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { useParams, useNavigate } from 'react-router-dom'; import { useParams, useNavigate } from 'react-router-dom';
import { apiUrl, fetchPage, fetchPageAtEdit, postPage, deletePage } from '../apiTools.jsx'; import { apiUrl, fetchPage, fetchPageAttributes, postPage } from '../apiTools.jsx';
import { useLoggedIn } from '../AuthProvider.jsx'; import { useLoggedIn } from '../AuthProvider.jsx';
import Page from './Page.jsx'; import Page from './Page.jsx';
import ExtendedAttributes from './ExtendedAttributes.jsx';
import './Pages.css'; import './Pages.css';
function GhostPage({editing, ...props}) { function GhostPage({editing, ...props}) {
const { pagenumber, editid } = useParams(); const { pagenumber, editid } = useParams();
const navigate = useNavigate(); const navigate = useNavigate();
const loggedIn = useLoggedIn();
const queryClient = useQueryClient();
const linkClick = (e) => { const linkClick = (e) => {
if (e.target.tagName != "A") { return; } if (e.target.tagName != "A") { return; }
if (!e.target.href.includes(window.location.origin)) { return; } if (!e.target.href.includes(window.location.origin)) return;
e.preventDefault(); e.preventDefault();
navigate(e.target.href.replace(window.location.origin, "")); navigate(e.target.href.replace(window.location.origin, ""));
}; };
return <Page const fetchPageQuery = useQuery({
number={pagenumber} queryKey: ['page', pagenumber, null],
editid={editid} queryFn: () => fetchPage(pagenumber),
});
const fetchAttributesQuery = useQuery({
queryKey: ['attributes', pagenumber],
queryFn: () => fetchPageAttributes(pagenumber)
});
const [title, setTitle] = useState(null);
useEffect(() => {
setTitle(fetchPageQuery.data?.title);
}, [fetchPageQuery.data?.title]);
const [text, setText] = useState(null);
useEffect(() => {
setText(fetchPageQuery.data?.description);
}, [fetchPageQuery.data?.description]);
const [type, setType] = useState(false);
useEffect(() => {
setType(fetchPageQuery.data?.type == 1);
}, [fetchPageQuery.data?.type]);
const postMutation = useMutation({ // for changing the value when we're done with it
mutationFn: postPage,
onSettled: async (data, error, variables) => {
// Invalidate and refetch
await queryClient.invalidateQueries(['page', variables.number, null])
console.log("shoulda just invalidated the thing");
navigate(`/${pagenumber}`);
},
});
function submitChanges(e) {
postMutation.mutate({
number: pagenumber,
title: title,
description: text,
type: type ? 1 : 0,
});
}
return <div className="main-column">
<Page
page={{...fetchPageQuery.data, description: text, title: title, type: type }}
editing={editing} editing={editing}
linkClick={linkClick} linkClick={linkClick}
{...props}/>; onChangeTitle={(e) => setTitle(e.target.value)}
onChangeText={setText}
onChangeType={() => setType(!type)}
{...props}/>
<ExtendedAttributes
attributes={fetchAttributesQuery.data} />
<button
onClick={() => navigate(`/${pagenumber}/history`)}>
History
</button>
{editing && (
<button
disabled={postMutation.isPending}
onClick={submitChanges}>
{postMutation.isPending ? "Updating..." : "Update"}
</button>)}
{!editing && !editid && (
<button
disabled={!loggedIn}
onClick={() => navigate(`/${pagenumber}/edit`, {replace: true})}>
Edit Page
</button>)}
</div>;
} }
export default GhostPage; export default GhostPage;

View File

@ -1,121 +1,73 @@
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { useParams, useNavigate } from 'react-router-dom'; import { apiUrl, fetchPage, fetchPageAtEdit, postPage } from '../apiTools.jsx';
import { apiUrl, fetchPage, fetchPageAtEdit, postPage, deletePage } from '../apiTools.jsx';
import { useFixLinks } from '../clientStuff.jsx'; import { useFixLinks } from '../clientStuff.jsx';
import { useLoggedIn } from '../AuthProvider.jsx';
import { MDXEditor, headingsPlugin, quotePlugin, linkPlugin, diffSourcePlugin } from '@mdxeditor/editor'; import { MDXEditor,
headingsPlugin, quotePlugin, listsPlugin,
thematicBreakPlugin, linkPlugin, diffSourcePlugin } from '@mdxeditor/editor';
import './Pages.css'; import './Pages.css';
import 'highlight.js/styles/a11y-dark.min.css'; import 'highlight.js/styles/a11y-dark.min.css';
import '@mdxeditor/editor/style.css'; import '@mdxeditor/editor/style.css';
function Page({ editing, number, editid=null, linkClick=()=>{} }) { function Page({
const queryClient = useQueryClient(); page=null,
const navigate = useNavigate(); editing, historical,
const loggedIn = useLoggedIn(); linkClick=()=>{},
onChangeTitle=()=>{},
const fetchQuery = useQuery({ // fetch the currrent values onChangeType=()=>{},
queryKey: ['page', number, editid], onChangeText=()=>{},
queryFn: () => editid ? fetchPageAtEdit(number, editid) : fetchPage(number) }) {
}) let {title, description, html, lua, time, author, type} = page || {};
if (!title) title = "";
const postMutation = useMutation({ // for changing the value when we're done with it
mutationFn: ({id, title, description, type}) => postPage({id, title, description, type}),
onSettled: async (data, error, variables) => {
// Invalidate and refetch
queryClient.invalidateQueries({ queryKey: ['page', variables.id, null] })
},
});
const readyToShow = !(fetchQuery.error || fetchQuery.isPending);
let {id, title, description, html, lua, time, author, type} = fetchQuery.data || {};
const [verb, setVerb] = useState(false);
const [enteredText, setText] = useState("...");
if (!title) title = "[no title]";
if (!html) html = "[body missing]"; if (!html) html = "[body missing]";
if (!lua) lua = "[no definition]"; if (!lua) lua = "[no definition]";
if (!description) description = "[body missing]"; if (!description) description = "[body missing]";
useEffect(() => {
setVerb(type == 1);
setText(description);
}, [type, description])
function submitChanges(e) { return (<>
const newTitle = document.querySelector('span').innerHTML;
postMutation.mutate({
id: number,
title: newTitle,
description: enteredText,
type: verb ? 1 : 0,
});
navigate(`/${number}`, {replace: true})
}
return (
<div className="main-column">
<header> <header>
<h1> <h1>
<a href="/" onClick={linkClick}>🌳</a> <a href="/" onClick={linkClick}>🌳</a>
{number}.&nbsp; {page?.number}.&nbsp;
<span <input disabled={!editing} value={title} onChange={onChangeTitle}/>
contentEditable={editing}
dangerouslySetInnerHTML={{__html: readyToShow ? title : "..." }} />
</h1> </h1>
<p>{readyToShow && editid && `saved at ${time} by user #${author}`}</p> { historical && <p>saved at {time} by user #{author}</p> }
{ editing ? <label> { editing ?
<input type="checkbox" checked={verb} onChange={() => setVerb(!verb)}/> <label>
{verb ? "verb" : "noun"} <input type="checkbox" checked={type} onChange={onChangeType}/>
</label> : <br/> } {type ? "verb" : "noun"}
</label>
:
<br/>
}
<hr/> <hr/>
</header> </header>
<section className="page-contents"> <section className="page-contents">
{ readyToShow ? { editing ?
(
editing ?
<MDXEditor <MDXEditor
markdown={description} markdown={description}
plugins={[ plugins={[
headingsPlugin(), headingsPlugin(),
quotePlugin(), quotePlugin(),
listsPlugin(),
thematicBreakPlugin(),
linkPlugin(), linkPlugin(),
diffSourcePlugin({ diffMarkdown: 'ahhhh do not look upon me!', viewMode: 'source' }), diffSourcePlugin({ diffMarkdown: 'ahhhh do not look upon me!', viewMode: 'source' }),
]} ]}
onChange={(md) => setText(md)} onChange={onChangeText}
/> />
: :
( ( type ?
verb ?
<pre><code dangerouslySetInnerHTML={{__html: lua.replace(/\n/g, "<br>").replace(/ /g, "&nbsp;&nbsp;")}}></code></pre> <pre><code dangerouslySetInnerHTML={{__html: lua.replace(/\n/g, "<br>").replace(/ /g, "&nbsp;&nbsp;")}}></code></pre>
: :
<div <div
dangerouslySetInnerHTML={{__html: html}} dangerouslySetInnerHTML={{__html: html}}
onClick={linkClick} /> onClick={linkClick} />
) )
)
:
"..."
} }
</section> </section>
<button </>
onClick={() => navigate(`/${number}/history`)}>
History
</button>
{editing && (
<button
disabled={postMutation.isPending}
onClick={submitChanges}>
{postMutation.isPending ? "Updating..." : "Update"}
</button>)}
{!editing && !editid && (
<button
disabled={!loggedIn}
onClick={() => navigate(`/${number}/edit`)}>
Edit Page
</button>)}
</div>
); );
} }

View File

@ -2,9 +2,12 @@
I think `npm start` in each directory should run the app I think `npm start` in each directory should run the app
# Environoment variables!
- `SESSION_DATA_PASSWORD` for encrypting session cookies
- `USER_CREATION_PASSWORD` to set the extra password required for account creation
# build log # build log
- `brew tap libsql/sqld`
- `brew install sqld-beta` - `brew install sqld-beta`
- to run cast `npm start` in each directory - to run cast `npm start` in each directory
- `node initialize_db.js` `node populate_db.js` - `node initialize_db.js` `node populate_db.js`

2
server/.env Normal file
View File

@ -0,0 +1,2 @@
USER_CREATION_PASSWORD="a softer birdsong"
SESSION_DATA_SECRET="dontcheckmeintoversioncontrolpleeeeasealskdfjsdf"

View File

@ -2,19 +2,44 @@ const { JSDOM } = require("jsdom");
const graphology = require("graphology"); const graphology = require("graphology");
const { circular } = require('graphology-layout'); const { circular } = require('graphology-layout');
const graphQueryString = `
select p.number, p.html, p.time, a.contents
from pages p
left join attributes a on a.number = p.number
where time >= (
select max(s.time)
from pages s
where s.number = p.number
)
order by p.time desc
`;
function graphFromList(allTheStuff) { function graphFromList(allTheStuff) {
const graph = new graphology.Graph(); const graph = new graphology.Graph();
for (const {number, html} of allTheStuff) { for (const {number, html} of allTheStuff) {
if (!graph.hasNode(number)) graph.addNode(number); if (!graph.hasNode(number)) graph.addNode(number);
} }
for (const {number, html} of allTheStuff) { for (const {number, html, contents} of allTheStuff) {
const { document } = (new JSDOM(html)).window; const { document } = (new JSDOM(html)).window;
const links = document.querySelectorAll('a'); const links = document.querySelectorAll('a');
links.forEach((link) => { links.forEach((link) => {
const referent = link.href.replace("/",""); const referent = link.href.replace("/","");
graph.mergeEdge(number, referent); graph.mergeEdge(number, referent, {color: "white"});
}); });
if (typeof contents?.verbs?.forEach === 'function') {
contents?.verbs?.forEach((verbNumber) => {
graph.mergeEdge(number, verbNumber, {color: "red"})
});
}
if (typeof contents?.contents?.forEach === 'function') {
contents?.contents?.forEach((nounNumber) => {
graph.mergeEdge(number, nounNumber, {color: "green"});
});
}
} }
circular.assign(graph); circular.assign(graph);
@ -22,4 +47,4 @@ function graphFromList(allTheStuff) {
return graph; return graph;
} }
module.exports = { graphFromList: graphFromList }; module.exports = { graphFromList, graphQueryString };

View File

@ -18,6 +18,10 @@ async function makeLua() {
lua.global.set('lookUpObject', luaSafe(lookUpObject)); lua.global.set('lookUpObject', luaSafe(lookUpObject));
lua.global.set('lookUpObjectAttributes', luaSafe(lookUpObjectAttributes)); lua.global.set('lookUpObjectAttributes', luaSafe(lookUpObjectAttributes));
lua.global.set('interpret', interpret); lua.global.set('interpret', interpret);
// let's get cheeky with it
lua.global.set('console_log', console.log);
} }
makeLua(); makeLua();
@ -33,6 +37,7 @@ function interpret(context, player, command) {
// may? be null if it's executing from a script? used for output. // may? be null if it's executing from a script? used for output.
// so arguably // so arguably
// command: the full string typed in by the player // command: the full string typed in by the player
console.log(context, player, command);
const socket = sockets.get(player) const socket = sockets.get(player)
const wordsAndQuotes = tokenizeQuotes(command.trim()); const wordsAndQuotes = tokenizeQuotes(command.trim());
@ -55,12 +60,12 @@ function interpret(context, player, command) {
const [first, second, third, ...rest] = words; const [first, second, third, ...rest] = words;
executeVerb(command, player, verb, prepMap, player, second, third, ...rest) return executeVerb(command, player, verb, prepMap, context, player, second, third, ...rest);
} else { } else {
interpret(1,1, `system_send_message '{"error": "verb ${verbId} not found in ${fullCommand}"}' to ${player}`); interpret(1,1, `system_send_message '{"error": "verb ${verbId} not found in ${fullCommand}"}' to ${player}`);
} }
} catch (error) { } catch (error) {
interpret(1,1, `system_send_message '{"error": "error found: ${error}"}' to ${player}`); executeVerb('', player, 2, {to: player}, null, `{"error": "error found: ${error}"}`);
} }
/* /*
@ -75,22 +80,16 @@ function interpret(context, player, command) {
*/ */
} }
async function executeVerb(fullCommand, outputObject, verbId, prepositions, subject, object, ...rest) { async function executeVerb(fullCommand, outputObject, verbId, prepositions, context, subject, object, ...rest) {
if (verbId == 2) { if (verbId == 2) {
// todo: make this more intelligently get the rright thing to send // todo: make this more intelligently get the rright thing to send
let theObject;
try {
theObject = verifyObjectReference(object);
} catch (error) {
theObject = verifyObjectReference(outputObject);
}
if (prepositions["to"]) { if (prepositions["to"]) {
let destination = verifyObjectReference(prepositions["to"]) let destination = verifyObjectReference(prepositions["to"])
return sockets.get(destination)?.send(object); return sockets.get(destination)?.send(object);
} }
return sockets return sockets
.get(theObject) .get(outputObject)
?.send(`full command: ${fullCommand}. verb id: ${verbId}. subject: ${subject} object: ${object}`); ?.send(`{"message": "missing \"to\" clause trying to execute command: ${fullCommand}"}`);
} }
const fullVerb = lookUpObject(verbId); const fullVerb = lookUpObject(verbId);
@ -102,19 +101,36 @@ async function executeVerb(fullCommand, outputObject, verbId, prepositions, subj
const verbName = "verb" + Math.random().toString(36).substring(2); const verbName = "verb" + Math.random().toString(36).substring(2);
const body = fullVerb.description.replace(/&nbsp;/g, " "); const body = fullVerb.description.replace(/&nbsp;/g, " ");
const verbDeclaration = ` const verbDeclaration = `
function ${verbName} (fullCommand, outputObject, prepositionMap, subject, object, ...) function ${verbName} (fullCommand, outputObject, prepositionMap, context, subject, object, ...)
${body} ${body}
end`; end`;
console.log("verb we're running:"); console.log("verb we're running:");
console.log(verbDeclaration); console.log(verbDeclaration);
await lua.doString(verbDeclaration); await lua.doString(verbDeclaration).catch((e) => {
console.log("found an error heyyoyyoyoyoyo", e);
let msg = {"error": "syntax error defining a verb!", "errorObject": e};
sockets.get(outputObject)?.send(JSON.stringify(msg));
})
let returnValue = await lua.global.get(verbName)(fullCommand, outputObject, prepositions, subject, object, ...rest); let func = lua.global.get(verbName);
if (typeof func === "function") {
let returnValue;
try {
returnValue =
lua.global.get(verbName)(fullCommand, outputObject, prepositions, context, subject, object, ...rest)
} catch (error) {
sockets.get(outputObject)?.send(JSON.stringify({"error": "error executing a verb!"}));
}
// maybe unset it so we dno't have an ever-growing set of functions cluttering up the space? // maybe unset it so we dno't have an ever-growing set of functions cluttering up the space?
//lua.global.set(verbName, null); //lua.global.set(verbName, null);
return returnValue; return returnValue;
} else {
sockets.get(outputObject)?.send(JSON.stringify({"error": "error defining a verb!"}));
}
return null;
} }
const objectQuery = db.prepare('select * from pages where number=? order by time desc'); const objectQuery = db.prepare('select * from pages where number=? order by time desc');
@ -173,7 +189,7 @@ function findVerbOnObject(word, object) {
if (!fullVerb) continue; if (!fullVerb) continue;
// test our word against // test our word against each verb in turn
if (word.toLowerCase() == fullVerb.title.toLowerCase()) { if (word.toLowerCase() == fullVerb.title.toLowerCase()) {
return verbId; return verbId;
} }
@ -189,8 +205,7 @@ function findAllVerbsOnObject(object) {
let focus = object; let focus = object;
while (focus) { while (focus) {
const newVerbs = (getAttribute(focus, "verbs") || []).filter((verb) => !(verb in verbs)); verbs = concatWithoutDuplicates(verbs, getAttribute(focus, "verbs"));
verbs = verbs.concat(newVerbs);
focus = getAttribute(focus, "parent"); focus = getAttribute(focus, "parent");
} }
@ -225,6 +240,7 @@ function getAttribute(obj, attributeName) {
let attributeStore = pullAttribute.get(verifyObjectReference(obj)); let attributeStore = pullAttribute.get(verifyObjectReference(obj));
if (!attributeStore || !attributeStore.contents) return undefined;
let contents = JSON.parse(attributeStore.contents); let contents = JSON.parse(attributeStore.contents);
if (contents.hasOwnProperty(attributeName)) { if (contents.hasOwnProperty(attributeName)) {
@ -256,11 +272,29 @@ function setAttribute(obj, attributeName, value) {
const attributeStore = pullAttribute.get(verifyObjectReference(obj)); const attributeStore = pullAttribute.get(verifyObjectReference(obj));
const contents = JSON.parse(attributeStore.contents); const contents = JSON.parse(attributeStore.contents);
if (isEmptyObject(value) && isArray(contents[attributes]))
contents[attributeName] = [];
else
contents[attributeName] = value; contents[attributeName] = value;
// hacky fix so that an empty array can't be replaced with an empty object
insertAttribute.run({...attributeStore, contents: JSON.stringify(contents)}); insertAttribute.run({...attributeStore, contents: JSON.stringify(contents)});
} }
function isArray(obj) {
return Object.prototype.toString.apply(value) === '[object Array]';
}
function isEmptyObject(obj) {
if (typeof obj !== "obj") return false;
for (const prop in obj) {
if (Object.hasOwn(obj, prop)) return true;
}
return true;
}
function deleteAttribute(obj, attributeName) { function deleteAttribute(obj, attributeName) {
if (!verifyObjectReference(obj)) return undefined; if (!verifyObjectReference(obj)) return undefined;
@ -288,8 +322,10 @@ function verifyObjectReference(obj) {
} }
function concatWithoutDuplicates(a, b) { function concatWithoutDuplicates(a, b) {
let b2 = b.filter((x) => !(x in a)); let c = []
return a.concat(b2); a?.forEach((x) => { if (!c.includes(x)) c.push(x) })
b?.forEach((x) => { if (!c.includes(x)) c.push(x) });
return c;
} }
function luaSafe(func) { function luaSafe(func) {
return (...all) => { return (...all) => {

342
server/package-lock.json generated
View File

@ -1,15 +1,14 @@
{ {
"name": "forest-server", "name": "forest-server",
"version": "1.0.0", "version": "0.0.1",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "forest-server", "name": "forest-server",
"version": "1.0.0", "version": "0.0.1",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"@libsql/client": "^0.14.0",
"argon2": "^0.41.1", "argon2": "^0.41.1",
"better-sqlite3": "^11.3.0", "better-sqlite3": "^11.3.0",
"better-sqlite3-session-store": "^0.1.0", "better-sqlite3-session-store": "^0.1.0",
@ -23,148 +22,12 @@
"graphology-layout-forceatlas2": "^0.10.1", "graphology-layout-forceatlas2": "^0.10.1",
"highlight.js": "^11.10.0", "highlight.js": "^11.10.0",
"jsdom": "^25.0.0", "jsdom": "^25.0.0",
"node": "^22.10.0",
"nodemon": "^3.1.5", "nodemon": "^3.1.5",
"showdown": "^2.1.0", "showdown": "^2.1.0",
"wasmoon": "^1.16.0" "wasmoon": "^1.16.0"
} }
}, },
"node_modules/@libsql/client": {
"version": "0.14.0",
"resolved": "https://registry.npmjs.org/@libsql/client/-/client-0.14.0.tgz",
"integrity": "sha512-/9HEKfn6fwXB5aTEEoMeFh4CtG0ZzbncBb1e++OCdVpgKZ/xyMsIVYXm0w7Pv4RUel803vE6LwniB3PqD72R0Q==",
"dependencies": {
"@libsql/core": "^0.14.0",
"@libsql/hrana-client": "^0.7.0",
"js-base64": "^3.7.5",
"libsql": "^0.4.4",
"promise-limit": "^2.7.0"
}
},
"node_modules/@libsql/core": {
"version": "0.14.0",
"resolved": "https://registry.npmjs.org/@libsql/core/-/core-0.14.0.tgz",
"integrity": "sha512-nhbuXf7GP3PSZgdCY2Ecj8vz187ptHlZQ0VRc751oB2C1W8jQUXKKklvt7t1LJiUTQBVJuadF628eUk+3cRi4Q==",
"dependencies": {
"js-base64": "^3.7.5"
}
},
"node_modules/@libsql/darwin-arm64": {
"version": "0.4.5",
"resolved": "https://registry.npmjs.org/@libsql/darwin-arm64/-/darwin-arm64-0.4.5.tgz",
"integrity": "sha512-xLdnn0NrgSk6OMi716FFs/27Hs33jtSd2fkKi/72Ey/qBtPWcB1BMurDQekzi0yAcfQTjGqIz7tpOibyjiEPyQ==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"darwin"
]
},
"node_modules/@libsql/darwin-x64": {
"version": "0.4.5",
"resolved": "https://registry.npmjs.org/@libsql/darwin-x64/-/darwin-x64-0.4.5.tgz",
"integrity": "sha512-rZsEWj0H7oCqd5Y2pe0RzKmuQXC2OB1RbnFy4CvjeAjT6MP6mFp+Vx9mTCAUuJMhuoSVMsFPUJRpAQznl9E3Tg==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"darwin"
]
},
"node_modules/@libsql/hrana-client": {
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/@libsql/hrana-client/-/hrana-client-0.7.0.tgz",
"integrity": "sha512-OF8fFQSkbL7vJY9rfuegK1R7sPgQ6kFMkDamiEccNUvieQ+3urzfDFI616oPl8V7T9zRmnTkSjMOImYCAVRVuw==",
"dependencies": {
"@libsql/isomorphic-fetch": "^0.3.1",
"@libsql/isomorphic-ws": "^0.1.5",
"js-base64": "^3.7.5",
"node-fetch": "^3.3.2"
}
},
"node_modules/@libsql/isomorphic-fetch": {
"version": "0.3.1",
"resolved": "https://registry.npmjs.org/@libsql/isomorphic-fetch/-/isomorphic-fetch-0.3.1.tgz",
"integrity": "sha512-6kK3SUK5Uu56zPq/Las620n5aS9xJq+jMBcNSOmjhNf/MUvdyji4vrMTqD7ptY7/4/CAVEAYDeotUz60LNQHtw==",
"engines": {
"node": ">=18.0.0"
}
},
"node_modules/@libsql/isomorphic-ws": {
"version": "0.1.5",
"resolved": "https://registry.npmjs.org/@libsql/isomorphic-ws/-/isomorphic-ws-0.1.5.tgz",
"integrity": "sha512-DtLWIH29onUYR00i0GlQ3UdcTRC6EP4u9w/h9LxpUZJWRMARk6dQwZ6Jkd+QdwVpuAOrdxt18v0K2uIYR3fwFg==",
"dependencies": {
"@types/ws": "^8.5.4",
"ws": "^8.13.0"
}
},
"node_modules/@libsql/linux-arm64-gnu": {
"version": "0.4.5",
"resolved": "https://registry.npmjs.org/@libsql/linux-arm64-gnu/-/linux-arm64-gnu-0.4.5.tgz",
"integrity": "sha512-VR09iu6KWGJ6fauCn59u/jJ9OA+/A2yQ0dr2HDN2zkRueLC6D2oGYt4gPfLZPFKf+WJpVMtIhNfd+Ru9MMaFkA==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"linux"
]
},
"node_modules/@libsql/linux-arm64-musl": {
"version": "0.4.5",
"resolved": "https://registry.npmjs.org/@libsql/linux-arm64-musl/-/linux-arm64-musl-0.4.5.tgz",
"integrity": "sha512-74hvD5ej4rBshhxFGNYU16a3m8B/NjIPvhlZ/flG1Oeydfo6AuUXSSNFi+H5+zi9/uWuzyz5TLVeQcraoUV10A==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"linux"
]
},
"node_modules/@libsql/linux-x64-gnu": {
"version": "0.4.5",
"resolved": "https://registry.npmjs.org/@libsql/linux-x64-gnu/-/linux-x64-gnu-0.4.5.tgz",
"integrity": "sha512-gb5WObGO3+rbuG8h9font1N02iF+zgYAgY0wNa8BNiZ5A9UolZKFxiqGFS7eHaAYfemHJKKTT+aAt3X2p5TibA==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"linux"
]
},
"node_modules/@libsql/linux-x64-musl": {
"version": "0.4.5",
"resolved": "https://registry.npmjs.org/@libsql/linux-x64-musl/-/linux-x64-musl-0.4.5.tgz",
"integrity": "sha512-JfyE6OVC5X4Nr4cFF77VhB1o+hBRxAqYT9YdeqnWdAQSYc/ASi5HnRALLAQEsGacFPZZ32pixfraQmPE3iJFfw==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"linux"
]
},
"node_modules/@libsql/win32-x64-msvc": {
"version": "0.4.5",
"resolved": "https://registry.npmjs.org/@libsql/win32-x64-msvc/-/win32-x64-msvc-0.4.5.tgz",
"integrity": "sha512-57GGurNJhOhq3XIopLdGnCoQ4kQAcmbmzzFoC4tpvDE/KSbwZ/13zqJWhQA41nMGk/PKM1XKfKmbIybKx1+eqA==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"win32"
]
},
"node_modules/@neon-rs/load": {
"version": "0.0.4",
"resolved": "https://registry.npmjs.org/@neon-rs/load/-/load-0.0.4.tgz",
"integrity": "sha512-kTPhdZyTQxB+2wpiRcFWrDcejc4JI6tkPuS7UZCG4l6Zvc5kU/gGQ/ozvHTh1XR5tS+UlfAfGuPajjzQjCiHCw=="
},
"node_modules/@phc/format": { "node_modules/@phc/format": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/@phc/format/-/format-1.0.0.tgz", "resolved": "https://registry.npmjs.org/@phc/format/-/format-1.0.0.tgz",
@ -178,22 +41,6 @@
"resolved": "https://registry.npmjs.org/@types/emscripten/-/emscripten-1.39.10.tgz", "resolved": "https://registry.npmjs.org/@types/emscripten/-/emscripten-1.39.10.tgz",
"integrity": "sha512-TB/6hBkYQJxsZHSqyeuO1Jt0AB/bW6G7rHt9g7lML7SOF6lbgcHvw/Lr+69iqN0qxgXLhWKScAon73JNnptuDw==" "integrity": "sha512-TB/6hBkYQJxsZHSqyeuO1Jt0AB/bW6G7rHt9g7lML7SOF6lbgcHvw/Lr+69iqN0qxgXLhWKScAon73JNnptuDw=="
}, },
"node_modules/@types/node": {
"version": "22.7.3",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.3.tgz",
"integrity": "sha512-qXKfhXXqGTyBskvWEzJZPUxSslAiLaB6JGP1ic/XTH9ctGgzdgYguuLP1C601aRTSDNlLb0jbKqXjZ48GNraSA==",
"dependencies": {
"undici-types": "~6.19.2"
}
},
"node_modules/@types/ws": {
"version": "8.5.12",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.12.tgz",
"integrity": "sha512-3tPRkv1EtkDpzlgyKyI8pGsGZAGPEaXeu0DOj5DI25Ja91bdAYddYHbADRYVrZMRbfW+1l5YwXVDKohDJNQxkQ==",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/accepts": { "node_modules/accepts": {
"version": "1.3.8", "version": "1.3.8",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
@ -520,9 +367,9 @@
} }
}, },
"node_modules/cookie": { "node_modules/cookie": {
"version": "0.6.0", "version": "0.7.1",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz",
"integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==",
"engines": { "engines": {
"node": ">= 0.6" "node": ">= 0.6"
} }
@ -563,14 +410,6 @@
"node": ">=18" "node": ">=18"
} }
}, },
"node_modules/data-uri-to-buffer": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz",
"integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==",
"engines": {
"node": ">= 12"
}
},
"node_modules/data-urls": { "node_modules/data-urls": {
"version": "5.0.0", "version": "5.0.0",
"resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz",
@ -760,16 +599,16 @@
} }
}, },
"node_modules/express": { "node_modules/express": {
"version": "4.21.0", "version": "4.21.1",
"resolved": "https://registry.npmjs.org/express/-/express-4.21.0.tgz", "resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz",
"integrity": "sha512-VqcNGcj/Id5ZT1LZ/cfihi3ttTn+NJmkli2eZADigjq29qTlWi/hAQ43t/VLPq8+UX06FCEx3ByOYet6ZFblng==", "integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==",
"dependencies": { "dependencies": {
"accepts": "~1.3.8", "accepts": "~1.3.8",
"array-flatten": "1.1.1", "array-flatten": "1.1.1",
"body-parser": "1.20.3", "body-parser": "1.20.3",
"content-disposition": "0.5.4", "content-disposition": "0.5.4",
"content-type": "~1.0.4", "content-type": "~1.0.4",
"cookie": "0.6.0", "cookie": "0.7.1",
"cookie-signature": "1.0.6", "cookie-signature": "1.0.6",
"debug": "2.6.9", "debug": "2.6.9",
"depd": "2.0.0", "depd": "2.0.0",
@ -801,11 +640,11 @@
} }
}, },
"node_modules/express-session": { "node_modules/express-session": {
"version": "1.18.0", "version": "1.18.1",
"resolved": "https://registry.npmjs.org/express-session/-/express-session-1.18.0.tgz", "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.18.1.tgz",
"integrity": "sha512-m93QLWr0ju+rOwApSsyso838LQwgfs44QtOP/WBiwtAgPIo/SAh1a5c6nn2BR6mFNZehTpqKDESzP+fRHVbxwQ==", "integrity": "sha512-a5mtTqEaZvBCL9A9aqkrtfz+3SMDhOVUnjafjo+s7A9Txkq+SVX2DLvSp1Zrv4uCXa3lMSK3viWnh9Gg07PBUA==",
"dependencies": { "dependencies": {
"cookie": "0.6.0", "cookie": "0.7.2",
"cookie-signature": "1.0.7", "cookie-signature": "1.0.7",
"debug": "2.6.9", "debug": "2.6.9",
"depd": "~2.0.0", "depd": "~2.0.0",
@ -818,6 +657,14 @@
"node": ">= 0.8.0" "node": ">= 0.8.0"
} }
}, },
"node_modules/express-session/node_modules/cookie": {
"version": "0.7.2",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
"integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/express-session/node_modules/cookie-signature": { "node_modules/express-session/node_modules/cookie-signature": {
"version": "1.0.7", "version": "1.0.7",
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz",
@ -857,28 +704,6 @@
} }
} }
}, },
"node_modules/fetch-blob": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz",
"integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/jimmywarting"
},
{
"type": "paypal",
"url": "https://paypal.me/jimmywarting"
}
],
"dependencies": {
"node-domexception": "^1.0.0",
"web-streams-polyfill": "^3.0.3"
},
"engines": {
"node": "^12.20 || >= 14.13"
}
},
"node_modules/file-uri-to-path": { "node_modules/file-uri-to-path": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
@ -925,17 +750,6 @@
"node": ">= 6" "node": ">= 6"
} }
}, },
"node_modules/formdata-polyfill": {
"version": "4.0.10",
"resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz",
"integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==",
"dependencies": {
"fetch-blob": "^3.1.2"
},
"engines": {
"node": ">=12.20.0"
}
},
"node_modules/forwarded": { "node_modules/forwarded": {
"version": "0.2.0", "version": "0.2.0",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
@ -1331,11 +1145,6 @@
"resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz",
"integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==" "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ=="
}, },
"node_modules/js-base64": {
"version": "3.7.7",
"resolved": "https://registry.npmjs.org/js-base64/-/js-base64-3.7.7.tgz",
"integrity": "sha512-7rCnleh0z2CkXhH67J8K1Ytz0b2Y+yxTPL+/KOJoa20hfnVQ/3/T6W/KflYI4bRHRagNeXeU2bkNGI3v1oS/lw=="
},
"node_modules/jsdom": { "node_modules/jsdom": {
"version": "25.0.0", "version": "25.0.0",
"resolved": "https://registry.npmjs.org/jsdom/-/jsdom-25.0.0.tgz", "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-25.0.0.tgz",
@ -1383,42 +1192,6 @@
"node": ">= 0.6" "node": ">= 0.6"
} }
}, },
"node_modules/libsql": {
"version": "0.4.5",
"resolved": "https://registry.npmjs.org/libsql/-/libsql-0.4.5.tgz",
"integrity": "sha512-sorTJV6PNt94Wap27Sai5gtVLIea4Otb2LUiAUyr3p6BPOScGMKGt5F1b5X/XgkNtcsDKeX5qfeBDj+PdShclQ==",
"cpu": [
"x64",
"arm64",
"wasm32"
],
"os": [
"darwin",
"linux",
"win32"
],
"dependencies": {
"@neon-rs/load": "^0.0.4",
"detect-libc": "2.0.2"
},
"optionalDependencies": {
"@libsql/darwin-arm64": "0.4.5",
"@libsql/darwin-x64": "0.4.5",
"@libsql/linux-arm64-gnu": "0.4.5",
"@libsql/linux-arm64-musl": "0.4.5",
"@libsql/linux-x64-gnu": "0.4.5",
"@libsql/linux-x64-musl": "0.4.5",
"@libsql/win32-x64-msvc": "0.4.5"
}
},
"node_modules/libsql/node_modules/detect-libc": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.2.tgz",
"integrity": "sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw==",
"engines": {
"node": ">=8"
}
},
"node_modules/media-typer": { "node_modules/media-typer": {
"version": "0.3.0", "version": "0.3.0",
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
@ -1534,6 +1307,21 @@
"node": ">= 0.6" "node": ">= 0.6"
} }
}, },
"node_modules/node": {
"version": "22.10.0",
"resolved": "https://registry.npmjs.org/node/-/node-22.10.0.tgz",
"integrity": "sha512-xiXybf2ElhQcbN3DXfUNKxxHK3fsQrxqNS6m7fu9u5E2YaolF2GfuNJV1+xR2CBraTPtvGablAocjKQ8Ey8gyQ==",
"hasInstallScript": true,
"dependencies": {
"node-bin-setup": "^1.0.0"
},
"bin": {
"node": "bin/node"
},
"engines": {
"npm": ">=5.0.0"
}
},
"node_modules/node-abi": { "node_modules/node-abi": {
"version": "3.68.0", "version": "3.68.0",
"resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.68.0.tgz", "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.68.0.tgz",
@ -1553,40 +1341,10 @@
"node": "^18 || ^20 || >= 21" "node": "^18 || ^20 || >= 21"
} }
}, },
"node_modules/node-domexception": { "node_modules/node-bin-setup": {
"version": "1.0.0", "version": "1.1.3",
"resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", "resolved": "https://registry.npmjs.org/node-bin-setup/-/node-bin-setup-1.1.3.tgz",
"integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", "integrity": "sha512-opgw9iSCAzT2+6wJOETCpeRYAQxSopqQ2z+N6BXwIMsQQ7Zj5M8MaafQY8JMlolRR6R1UXg2WmhKp0p9lSOivg=="
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/jimmywarting"
},
{
"type": "github",
"url": "https://paypal.me/jimmywarting"
}
],
"engines": {
"node": ">=10.5.0"
}
},
"node_modules/node-fetch": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz",
"integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==",
"dependencies": {
"data-uri-to-buffer": "^4.0.0",
"fetch-blob": "^3.1.4",
"formdata-polyfill": "^4.0.10"
},
"engines": {
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/node-fetch"
}
}, },
"node_modules/node-gyp-build": { "node_modules/node-gyp-build": {
"version": "4.8.2", "version": "4.8.2",
@ -1770,11 +1528,6 @@
"node": ">=10" "node": ">=10"
} }
}, },
"node_modules/promise-limit": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/promise-limit/-/promise-limit-2.7.0.tgz",
"integrity": "sha512-7nJ6v5lnJsXwGprnGXga4wx6d1POjvi5Qmf1ivTRxTjH4Z/9Czja/UCMLVmB9N93GeWOU93XaFaEt6jbuoagNw=="
},
"node_modules/proxy-addr": { "node_modules/proxy-addr": {
"version": "2.0.7", "version": "2.0.7",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
@ -2271,11 +2024,6 @@
"resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz",
"integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==" "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA=="
}, },
"node_modules/undici-types": {
"version": "6.19.8",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz",
"integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw=="
},
"node_modules/universalify": { "node_modules/universalify": {
"version": "0.2.0", "version": "0.2.0",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz",
@ -2344,14 +2092,6 @@
"wasmoon": "bin/wasmoon" "wasmoon": "bin/wasmoon"
} }
}, },
"node_modules/web-streams-polyfill": {
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz",
"integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==",
"engines": {
"node": ">= 8"
}
},
"node_modules/webidl-conversions": { "node_modules/webidl-conversions": {
"version": "7.0.0", "version": "7.0.0",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",

View File

@ -1,10 +1,10 @@
{ {
"name": "forest-server", "name": "forest-server",
"version": "1.0.0", "version": "0.0.1",
"description": "the backend for a wiki/mud", "description": "the backend for a wiki/mud",
"main": "server.js", "main": "server.js",
"scripts": { "scripts": {
"start": "nodemon server.js", "start": "node --watch --env-file=.env server.js",
"test": "echo \"Error: no test specified\" && exit 1" "test": "echo \"Error: no test specified\" && exit 1"
}, },
"repository": { "repository": {
@ -14,7 +14,6 @@
"author": "", "author": "",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"@libsql/client": "^0.14.0",
"argon2": "^0.41.1", "argon2": "^0.41.1",
"better-sqlite3": "^11.3.0", "better-sqlite3": "^11.3.0",
"better-sqlite3-session-store": "^0.1.0", "better-sqlite3-session-store": "^0.1.0",
@ -28,6 +27,7 @@
"graphology-layout-forceatlas2": "^0.10.1", "graphology-layout-forceatlas2": "^0.10.1",
"highlight.js": "^11.10.0", "highlight.js": "^11.10.0",
"jsdom": "^25.0.0", "jsdom": "^25.0.0",
"node": "^22.10.0",
"nodemon": "^3.1.5", "nodemon": "^3.1.5",
"showdown": "^2.1.0", "showdown": "^2.1.0",
"wasmoon": "^1.16.0" "wasmoon": "^1.16.0"

View File

@ -1,4 +1,4 @@
const { graphFromList } = require('../graphStuff.js'); const { graphFromList, graphQueryString } = require('../graphStuff.js');
const sqlite = require('better-sqlite3'); const sqlite = require('better-sqlite3');
const db = new sqlite('the_big_db.db', { verbose: console.log }); const db = new sqlite('the_big_db.db', { verbose: console.log });
@ -15,22 +15,18 @@ app.use(user_routes);
const live_connection_routes = require('./live.js'); const live_connection_routes = require('./live.js');
app.use(live_connection_routes); app.use(live_connection_routes);
const graphQuery = db.prepare(`
select p.number, p.html, p.time
from pages p
where time >= (
select max(s.time)
from pages s
where s.number = p.number
)
order by p.time desc
`);
app.get('/graph', (req, res) => { app.get('/graph', (req, res) => {
try { try {
const rows = graphQuery.all(); const rows = db.prepare(graphQueryString).all();
const graph = graphFromList(rows); const parsedRows = rows.map((row) => {
let newRow = row;
newRow.contents = JSON.parse(row.contents);
return newRow;
});
const graph = graphFromList(parsedRows);
res.status(200).json(graph); res.status(200).json(graph);
} catch (error) { } catch (error) {
console.log("error:", error);
res.status(500).json({"error": error}); res.status(500).json({"error": error});
} }
}); });

View File

@ -11,8 +11,6 @@ const { interpret, sockets, lookUpObject,
getAttribute, setAttribute, hasOwnAttribute, deleteAttribute, getAttribute, setAttribute, hasOwnAttribute, deleteAttribute,
findAllVerbsInArea } = require('../interpreter.js'); findAllVerbsInArea } = require('../interpreter.js');
app.ws('/embody', (ws, req) => { app.ws('/embody', (ws, req) => {
const character = req.session.characterId; const character = req.session.characterId;
if (!character) { if (!character) {
@ -23,9 +21,10 @@ app.ws('/embody', (ws, req) => {
sockets.set(character, ws); sockets.set(character, ws);
console.log("sending location change, should get attribute for 30"); console.log("sending location change, should get attribute for 30");
ws.send(`location change to: #${getAttribute(character, "location")}`); ws.send(JSON.stringify({setPageNumber: getAttribute(character, "location")}));
ws.on('message', (msg) => { ws.on('message', (msg) => {
console.log("received message: ", msg);
const location = getAttribute(character, "location"); const location = getAttribute(character, "location");
interpret(location, character, msg); interpret(location, character, msg);

View File

@ -46,23 +46,42 @@ app.post('/page/new', loginRequired, (req, res) => {
} }
}); });
const getPageQuery = db.prepare('select * from pages where number=? order by time desc');
app.get('/page/:number', (req, res) => { app.get('/page/:number', (req, res) => {
try { try {
const page = db.prepare('select * from pages where number=:number order by time desc').get(req.params); const page = getPageQuery.get(req.params.number);
if (page === undefined) res.status(404).json({"error": "page not found"}); if (!page) res.status(404).json({"error": "page not found"});
else res.status(200).json(page); else res.status(200).json(page);
} catch (error) { } catch (error) {
res.status(500).json({"error": error}); res.status(500).json({"error": error});
} }
}); });
const getAttributesQuery = db.prepare('select * from attributes where number=?');
app.get('/page/:number/attributes', (req, res) => {
try {
const page = getPageQuery.get(req.params.number);
if (!page) {
res.status(404).json({"error": "page not found"});
return;
}
const attributes = getAttributesQuery.get(req.params.number);
if (!attributes) res.status(200).json({});
else if (attributes.contents) res.status(200).json(JSON.parse(attributes.contents));
else res.status(200).json({});
} catch (error) {
res.status(500).json({"error": error});
}
});
const pageInsertQuery = db.prepare('insert into pages (number, title, description, html, lua, author, type) values (?, ?, ?, ?, ?, ?, ?)')
app.post('/page/:number', loginRequired, (req, res) => { app.post('/page/:number', loginRequired, (req, res) => {
try { try {
const html = converter.makeHtml(req.body.description); const html = converter.makeHtml(req.body.description);
const lua = hljs.highlight(req.body.description, {language: 'lua'}).value; const lua = hljs.highlight(req.body.description, {language: 'lua'}).value;
console.log(lua);
const changes = db.prepare('insert into pages (number, title, description, html, lua, author, type) values (?, ?, ?, ?, ?, ?, ?)') const changes = pageInsertQuery.run(req.params.number, req.body.title, req.body.description, html, lua, req.session.userId, req.body.type ? 1 : 0);
.run(req.params.number, req.body.title, req.body.description, html, lua, req.session.userId, req.body.type ? 1 : 0);
res.status(200).json(changes); res.status(200).json(changes);
} catch (error) { } catch (error) {
console.log(error); console.log(error);
@ -70,6 +89,16 @@ app.post('/page/:number', loginRequired, (req, res) => {
} }
}); });
const attributesUpdateQuery = db.prepare('update attributes set contents=? where number=?')
app.post('/page/:number/attributes', loginRequired, (req, res) => {
try {
const changes = attributesUpdateQuery.run(req.body, req.params.number);
res.status(200).json(changes);
} catch (error) {
res.status(500).json({"error": error});
}
})
app.delete('/page/:number', loginRequired, (req, res) => { app.delete('/page/:number', loginRequired, (req, res) => {
try { try {
db.prepare('delete from pages where number = ?').run(req.params.number); db.prepare('delete from pages where number = ?').run(req.params.number);

View File

@ -23,6 +23,7 @@ app.post('/register', async (req, res) => {
const hash = await argon2.hash(password); const hash = await argon2.hash(password);
const inserted = db.prepare('insert into users (name, password) values (?, ?)').run(name, hash); const inserted = db.prepare('insert into users (name, password) values (?, ?)').run(name, hash);
res.status(200).json(inserted); res.status(200).json(inserted);
// todo: create an object associated with that players
} catch (error) { } catch (error) {
res.status(500).json({"error": error}); res.status(500).json({"error": error});
} }

View File

@ -22,8 +22,9 @@ app.use(session({
intervalMs: 15*60*1000 intervalMs: 15*60*1000
} }
}), }),
secret: "dno'tt check me into versino control", secret: process.env.SESSION_DATA_SECRET,
resave: false resave: false,
saveUninitialized: false
})); }));
app.use("/api", apiRoutes); app.use("/api", apiRoutes);