├── .gitignore
├── .vscode
└── settings.json
├── LICENSE
├── README.md
├── assets
├── Classroom+Chair.jpg
├── add.gif
├── arch-shadow.png
├── arch.png
├── bubble.png
├── chat.gif
├── close.png
├── delete.ico
├── drag.png
├── dragArea.png
├── duplicate.png
├── ear_sit.gif
├── ear_stand.gif
├── ear_stand.png
├── ear_walk.gif
├── evil_stand.gif
├── evil_walk.gif
├── favicon.ico
├── fern.jpg
├── flower.png
├── gear.png
├── locked.png
├── move.gif
├── spiral.png
├── spiral.svg
├── stand1.gif
├── subtract.gif
├── text.png
├── unlocked.png
├── upload.png
├── walk1.gif
└── walk2.gif
├── index.html
├── index.tsx
├── main.css
├── package.json
├── scraper
├── ambient_sounds.json
└── scrape.js
├── server
├── config
│ └── index.ts
├── cors.json
├── database.ts
├── email.ts
├── endpoints.ts
├── helpers.ts
├── package.json
├── serve.ts
├── tsconfig.json
├── yarn.lock
└── yjs
│ ├── callback.js
│ ├── utils.ts
│ └── ws-server.ts
├── src
├── Entity.tsx
├── SpaceSettings.tsx
├── audio.ts
├── camera.tsx
├── client.ts
├── entity.css
├── hooks.ts
├── imageUpload.ts
├── input.ts
├── movement.ts
├── render.tsx
├── settings.css
├── state.ts
├── tools.css
├── types.ts
├── ui.tsx
├── useAnimationFrame.tsx
└── utils.ts
├── tsconfig.json
└── yarn.lock
/.gitignore:
--------------------------------------------------------------------------------
1 | server/uploads/
2 | server/db.txt
3 | server/db.sqlite
4 | server/dbDir/
5 | server/config/keys.json
6 | # Logs
7 | logs
8 | *.log
9 | npm-debug.log*
10 | yarn-debug.log*
11 | yarn-error.log*
12 | lerna-debug.log*
13 | docs
14 |
15 | # Diagnostic reports (https://nodejs.org/api/report.html)
16 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
17 |
18 | # Runtime data
19 | pids
20 | *.pid
21 | *.seed
22 | *.pid.lock
23 |
24 | # Directory for instrumented libs generated by jscoverage/JSCover
25 | lib-cov
26 |
27 | # Coverage directory used by tools like istanbul
28 | coverage
29 | *.lcov
30 |
31 | # nyc test coverage
32 | .nyc_output
33 |
34 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
35 | .grunt
36 |
37 | # Bower dependency directory (https://bower.io/)
38 | bower_components
39 |
40 | # node-waf configuration
41 | .lock-wscript
42 |
43 | # Compiled binary addons (https://nodejs.org/api/addons.html)
44 | build/Release
45 |
46 | # Dependency directories
47 | node_modules/
48 | jspm_packages/
49 |
50 | # TypeScript v1 declaration files
51 | typings/
52 |
53 | # TypeScript cache
54 | *.tsbuildinfo
55 |
56 | # Optional npm cache directory
57 | .npm
58 |
59 | # Optional eslint cache
60 | .eslintcache
61 |
62 | # Microbundle cache
63 | .rpt2_cache/
64 | .rts2_cache_cjs/
65 | .rts2_cache_es/
66 | .rts2_cache_umd/
67 |
68 | # Optional REPL history
69 | .node_repl_history
70 |
71 | # Output of 'npm pack'
72 | *.tgz
73 |
74 | # Yarn Integrity file
75 | .yarn-integrity
76 |
77 | # dotenv environment variables file
78 | .env
79 | .env.test
80 |
81 | # parcel-bundler cache (https://parceljs.org/)
82 | .cache
83 |
84 | # Next.js build output
85 | .next
86 |
87 | # Nuxt.js build / generate output
88 | .nuxt
89 | dist
90 |
91 | # Gatsby files
92 | .cache/
93 | # Comment in the public line in if your project uses Gatsby and *not* Next.js
94 | # https://nextjs.org/blog/next-9-1#public-directory-support
95 | # public
96 |
97 | # vuepress build output
98 | .vuepress/dist
99 |
100 | # Serverless directories
101 | .serverless/
102 |
103 | # FuseBox cache
104 | .fusebox/
105 |
106 | # DynamoDB Local files
107 | .dynamodb/
108 |
109 | # TernJS port file
110 | .tern-port
111 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "cSpell.enabledLanguageIds": [
3 | "asciidoc",
4 | "c",
5 | "cpp",
6 | "csharp",
7 | "git-commit",
8 | "go",
9 | "handlebars",
10 | "haskell",
11 | "html",
12 | "jade",
13 | "java",
14 | "javascript",
15 | "javascriptreact",
16 | "json",
17 | "jsonc",
18 | "latex",
19 | "less",
20 | "markdown",
21 | "php",
22 | "plaintext",
23 | "pug",
24 | "restructuredtext",
25 | "rust",
26 | "scala",
27 | "scss",
28 | "text",
29 | "yaml",
30 | "yml"
31 | ]
32 | }
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Max Bittker
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # walk & collage
2 |
3 | collage tool, flat game, MMO
4 |
5 | http://harmonyzone.org/ballwithfeet.html
6 | https://www.gabrielgambetta.com/client-server-game-architecture.html
7 |
--------------------------------------------------------------------------------
/assets/Classroom+Chair.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MaxBittker/walky/4f0d179750d48193608d229d61421afea8f8d912/assets/Classroom+Chair.jpg
--------------------------------------------------------------------------------
/assets/add.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MaxBittker/walky/4f0d179750d48193608d229d61421afea8f8d912/assets/add.gif
--------------------------------------------------------------------------------
/assets/arch-shadow.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MaxBittker/walky/4f0d179750d48193608d229d61421afea8f8d912/assets/arch-shadow.png
--------------------------------------------------------------------------------
/assets/arch.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MaxBittker/walky/4f0d179750d48193608d229d61421afea8f8d912/assets/arch.png
--------------------------------------------------------------------------------
/assets/bubble.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MaxBittker/walky/4f0d179750d48193608d229d61421afea8f8d912/assets/bubble.png
--------------------------------------------------------------------------------
/assets/chat.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MaxBittker/walky/4f0d179750d48193608d229d61421afea8f8d912/assets/chat.gif
--------------------------------------------------------------------------------
/assets/close.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MaxBittker/walky/4f0d179750d48193608d229d61421afea8f8d912/assets/close.png
--------------------------------------------------------------------------------
/assets/delete.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MaxBittker/walky/4f0d179750d48193608d229d61421afea8f8d912/assets/delete.ico
--------------------------------------------------------------------------------
/assets/drag.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MaxBittker/walky/4f0d179750d48193608d229d61421afea8f8d912/assets/drag.png
--------------------------------------------------------------------------------
/assets/dragArea.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MaxBittker/walky/4f0d179750d48193608d229d61421afea8f8d912/assets/dragArea.png
--------------------------------------------------------------------------------
/assets/duplicate.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MaxBittker/walky/4f0d179750d48193608d229d61421afea8f8d912/assets/duplicate.png
--------------------------------------------------------------------------------
/assets/ear_sit.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MaxBittker/walky/4f0d179750d48193608d229d61421afea8f8d912/assets/ear_sit.gif
--------------------------------------------------------------------------------
/assets/ear_stand.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MaxBittker/walky/4f0d179750d48193608d229d61421afea8f8d912/assets/ear_stand.gif
--------------------------------------------------------------------------------
/assets/ear_stand.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MaxBittker/walky/4f0d179750d48193608d229d61421afea8f8d912/assets/ear_stand.png
--------------------------------------------------------------------------------
/assets/ear_walk.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MaxBittker/walky/4f0d179750d48193608d229d61421afea8f8d912/assets/ear_walk.gif
--------------------------------------------------------------------------------
/assets/evil_stand.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MaxBittker/walky/4f0d179750d48193608d229d61421afea8f8d912/assets/evil_stand.gif
--------------------------------------------------------------------------------
/assets/evil_walk.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MaxBittker/walky/4f0d179750d48193608d229d61421afea8f8d912/assets/evil_walk.gif
--------------------------------------------------------------------------------
/assets/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MaxBittker/walky/4f0d179750d48193608d229d61421afea8f8d912/assets/favicon.ico
--------------------------------------------------------------------------------
/assets/fern.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MaxBittker/walky/4f0d179750d48193608d229d61421afea8f8d912/assets/fern.jpg
--------------------------------------------------------------------------------
/assets/flower.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MaxBittker/walky/4f0d179750d48193608d229d61421afea8f8d912/assets/flower.png
--------------------------------------------------------------------------------
/assets/gear.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MaxBittker/walky/4f0d179750d48193608d229d61421afea8f8d912/assets/gear.png
--------------------------------------------------------------------------------
/assets/locked.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MaxBittker/walky/4f0d179750d48193608d229d61421afea8f8d912/assets/locked.png
--------------------------------------------------------------------------------
/assets/move.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MaxBittker/walky/4f0d179750d48193608d229d61421afea8f8d912/assets/move.gif
--------------------------------------------------------------------------------
/assets/spiral.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MaxBittker/walky/4f0d179750d48193608d229d61421afea8f8d912/assets/spiral.png
--------------------------------------------------------------------------------
/assets/spiral.svg:
--------------------------------------------------------------------------------
1 |
2 |
9 |
--------------------------------------------------------------------------------
/assets/stand1.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MaxBittker/walky/4f0d179750d48193608d229d61421afea8f8d912/assets/stand1.gif
--------------------------------------------------------------------------------
/assets/subtract.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MaxBittker/walky/4f0d179750d48193608d229d61421afea8f8d912/assets/subtract.gif
--------------------------------------------------------------------------------
/assets/text.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MaxBittker/walky/4f0d179750d48193608d229d61421afea8f8d912/assets/text.png
--------------------------------------------------------------------------------
/assets/unlocked.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MaxBittker/walky/4f0d179750d48193608d229d61421afea8f8d912/assets/unlocked.png
--------------------------------------------------------------------------------
/assets/upload.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MaxBittker/walky/4f0d179750d48193608d229d61421afea8f8d912/assets/upload.png
--------------------------------------------------------------------------------
/assets/walk1.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MaxBittker/walky/4f0d179750d48193608d229d61421afea8f8d912/assets/walk1.gif
--------------------------------------------------------------------------------
/assets/walk2.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MaxBittker/walky/4f0d179750d48193608d229d61421afea8f8d912/assets/walk2.gif
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | walky
5 |
10 |
11 |
12 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
31 |
36 |
37 |
38 |
39 |
44 |
50 |
51 |
--------------------------------------------------------------------------------
/index.tsx:
--------------------------------------------------------------------------------
1 | import { Render } from "./src/render";
2 | import * as React from "react";
3 | import ReactDOM = require("react-dom");
4 |
5 | import { startUI } from "./src/ui";
6 | import "regenerator-runtime/runtime";
7 |
8 | import { startInput } from "./src/input";
9 | import { updateCamera } from "./src/camera";
10 | import { updateAgent } from "./src/movement";
11 | import { sendUpdate } from "./src/client";
12 | import { getState } from "./src/state";
13 |
14 | startInput();
15 | startUI();
16 | let i = 0;
17 |
18 | const rootElement = document.getElementById("window");
19 | ReactDOM.render(
20 |
21 |
22 | ,
23 | rootElement
24 | );
25 |
26 | // // let debug = document.getElementById("debug");
27 |
28 | let lasttick = Date.now();
29 | function tick() {
30 | let state = getState();
31 | let { me, agents } = state;
32 |
33 | let elapsedMillis = Date.now() - lasttick;
34 | let millisPerTick = 1000 / 60;
35 | let elapsedTicks = elapsedMillis / millisPerTick;
36 |
37 | state.me = updateAgent(me, elapsedTicks);
38 | state.agents = agents.map((agent) => updateAgent(agent, elapsedTicks));
39 |
40 | // debug.innerHTML = state.tick;
41 | updateCamera(elapsedTicks);
42 | if (i % 10 == 0) {
43 | sendUpdate();
44 | }
45 |
46 | i++;
47 |
48 | lasttick = Date.now();
49 | }
50 |
--------------------------------------------------------------------------------
/main.css:
--------------------------------------------------------------------------------
1 | html,
2 | body {
3 | font-family: "Libre Franklin", sans-serif;
4 |
5 | width: 100vw;
6 | height: 100vh;
7 | overflow: hidden;
8 | overscroll-behavior-x: none;
9 | overscroll-behavior-y: none;
10 | user-select: none;
11 | box-sizing: border-box;
12 | touch-action: none;
13 | -webkit-touch-callout: none;
14 |
15 | height: 100vh;
16 | height: -webkit-fill-available;
17 | height: fill-available;
18 | }
19 | textarea {
20 | font-family: "Libre Franklin", sans-serif;
21 |
22 | }
23 | * {
24 | -webkit-user-select: none; /* Safari */
25 | user-select: none !important;
26 | touch-action: manipulation;
27 | }
28 | #window {
29 | overscroll-behavior-y: none;
30 | overscroll-behavior-x: none;
31 | position: absolute;
32 | overflow: hidden;
33 |
34 | left: 0;
35 | right: 0;
36 | top: 0;
37 | bottom: 0;
38 | height: 100vh;
39 | height: -webkit-fill-available;
40 | height: fill-available;
41 | /* background-color: bisque; */
42 | }
43 | @media screen and (max-width: 600px) {
44 | #window {
45 | zoom: 0.6;
46 | /* overflow: visible; */
47 |
48 | /* overflow: hidden; */
49 | /* overscroll-behavior-y: none; */
50 | }
51 | }
52 |
53 | #background {
54 | background-size: 20px 20px;
55 | position: absolute;
56 | /* overflow: hidden; */
57 | left: 0;
58 | right: 0;
59 | top: 0;
60 | bottom: 0;
61 | background-image: linear-gradient(to right, #aaa 1px, transparent 1px),
62 | linear-gradient(to bottom, #aaa 1px, transparent 1px);
63 | }
64 |
65 | #entities {
66 | transform: translateZ(0);
67 |
68 | /* transition: transform 100ms; */
69 | will-change: transform;
70 | }
71 |
72 | #entities img {
73 | /* box-shadow: 2px 2px 10px black; */
74 | /* filter: drop-shadow(4px 4px 7px black); */
75 | /* opacity: 0.5; */
76 | /* will-change: filter; */
77 | /* box-shadow: inset 0 0 0 0.01px white; */
78 | /* mix-blend-mode: normal; */
79 | /* image-rendering: pixelated; */
80 | }
81 |
82 | * {
83 | color: #333;
84 | margin: 0;
85 | /* border: 1px red solid; */
86 | }
87 |
88 | img {
89 | /* position: absolute; */
90 | z-index: 1;
91 | /* mix-blend-mode: difference; */
92 | /* mix-blend-mode: exclusion; */
93 | pointer-events: none;
94 | user-select: none;
95 | /* transition: transform 0.05s; */
96 | /* transform: translate(-50%, -50%); */
97 | }
98 |
99 | .deleting img {
100 | pointer-events: all;
101 | }
102 | .deleting {
103 | z-index: 1;
104 |
105 | cursor: url(./assets/delete.ico), auto;
106 | }
107 |
108 | .deleting #entities img:hover {
109 | border: 3px red dashed;
110 | filter: drop-shadow(0px 0px 5px red);
111 | }
112 |
113 | .moving img {
114 | pointer-events: all;
115 | }
116 | .moving {
117 | z-index: 1;
118 |
119 | cursor: move;
120 | }
121 |
122 | .moving #entities img:hover {
123 | /* border: 3px greenyellow dashed; */
124 | /* filter: drop-shadow(0px 0px 5px greenyellow); */
125 | }
126 |
127 | #walker {
128 | position: absolute;
129 | width: 100px;
130 | height: 100px;
131 | z-index: 10;
132 | filter: contrast(120%);
133 | mix-blend-mode: normal;
134 | }
135 | .speech {
136 | font-size: 50px;
137 | position: absolute;
138 | z-index: 10;
139 | transform: translate(-50%, -130px);
140 | /* filter: drop-shadow(0px 0px 2px #fff); */
141 | }
142 | .bubble {
143 | width: 120px;
144 | height: 120px;
145 | font-size: 50px;
146 | position: absolute;
147 | z-index: 9;
148 | filter: contrast(1.9);
149 | transform: translate(-50%, -150px);
150 | mix-blend-mode: normal;
151 | }
152 |
153 | #spawner{
154 | position: absolute;
155 | z-index: 10006;
156 | top: -50px;
157 | /* transform: translate(-50%, -50%); */
158 | width: 150px;
159 | height: 150px;
160 | /* border-radius: 100%; */
161 | /* opacity: 0.5; */
162 | /* border: 2px dashed rgba(100, 100, 100, 0.561) ; */
163 | }
164 | #spawner.shadow{
165 | z-index: 10;
166 | }
167 |
168 | .rotate{
169 | animation: rotate 10s linear infinite;
170 | }
171 | @keyframes rotate{
172 | to{ transform: rotate(360deg); }
173 | }
174 |
175 | #close {
176 | position: absolute;
177 | right: 1.5em;
178 | top: 1.5em;
179 | width: 32px;
180 | height: 32px;
181 | pointer-events: all;
182 | cursor: pointer;
183 | }
184 |
185 | .audio {
186 | position: absolute;
187 | opacity: 0.2;
188 | font-size: 60px;
189 | filter: blur(25px);
190 | position: "absolute";
191 | }
192 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "walky",
3 | "version": "1.0.0",
4 | "description": "walk and collage",
5 | "main": "index.js",
6 | "dependencies": {
7 | "@graph-ts/vector2": "^1.3.0",
8 | "@stytch/stytch-react": "^3.0.7",
9 | "@types/classnames": "^2.3.1",
10 | "@types/react": "^16.9.41",
11 | "@types/react-dom": "^16.9.8",
12 | "@types/uuid": "^8.3.0",
13 | "assets": "^3.0.1",
14 | "browser-image-resizer": "github:ericnograles/browser-image-resizer",
15 | "classnames": "^2.2.6",
16 | "jotai": "^1.6.1",
17 | "morphdom": "^2.6.1",
18 | "parcel": "^1.12.4",
19 | "query-string": "^7.1.1",
20 | "react": "^16.13.1",
21 | "react-dom": "^16.13.1",
22 | "react-router-dom": "^6.3.0",
23 | "seedrandom": "^3.0.5",
24 | "xml2js": "^0.4.23",
25 | "y-websocket": "^1.4.0",
26 | "yjs": "^13.5.31"
27 | },
28 | "devDependencies": {
29 | "@babel/core": "^7.10.4",
30 | "@babel/preset-env": "^7.10.4",
31 | "@babel/preset-react": "^7.10.4",
32 | "@babel/preset-typescript": "^7.10.4",
33 | "babel-core": "^6.26.3",
34 | "babel-preset-env": "^1.7.0",
35 | "babel-preset-react": "^6.24.1",
36 | "parcel-bundler": "^1.12.4",
37 | "typescript": "^3.8.2"
38 | },
39 | "scripts": {
40 | "start": "parcel develop index.html",
41 | "build": "parcel build index.html -d docs ",
42 | "watch": "parcel watch index.html -d docs "
43 | },
44 | "repository": {
45 | "type": "git",
46 | "url": "git+https://github.com/MaxBittker/walky.git"
47 | },
48 | "author": "",
49 | "license": "ISC",
50 | "bugs": {}
51 | }
52 |
--------------------------------------------------------------------------------
/scraper/scrape.js:
--------------------------------------------------------------------------------
1 | const https = require("https");
2 | const fs = require("fs");
3 | const xml2js = require("xml2js");
4 | var parser = new xml2js.Parser();
5 | let queue = [];
6 | let files = [];
7 |
8 | function scrapeURL() {
9 | let url = queue.pop();
10 | console.log(url);
11 | https
12 | .get(url, function(res) {
13 | // res.setEncoding("utf8");
14 |
15 | var body = "";
16 | res.on("data", function(chunk) {
17 | body += chunk;
18 | });
19 | res.on("end", function() {
20 | parser.parseString(body, function(err, result) {
21 | console.log(err);
22 | if (result) {
23 | let audio_files = result["audio_files"];
24 | console.dir(JSON.stringify(result));
25 | if (audio_files) {
26 | let list = audio_files["audio_file"];
27 | console.log(list);
28 | files = files.concat(list);
29 | }
30 | }
31 |
32 | if (queue.length > 0) {
33 | scrapeURL();
34 | } else {
35 | let data = JSON.stringify(files);
36 | fs.writeFileSync("ambient_sounds.json", data);
37 | }
38 |
39 | console.log("Done");
40 | });
41 | });
42 | })
43 | .on("error", function(e) {
44 | console.log("Got error: " + e.message);
45 | });
46 | }
47 |
48 | let url_stem = "https://xml.ambient-mixer.com/get-audio?id_category=";
49 | for (var i = 0; i < 300; i++) {
50 | let url = url_stem + i;
51 | queue.push(url);
52 | }
53 | console.log(queue);
54 | scrapeURL();
55 |
--------------------------------------------------------------------------------
/server/config/index.ts:
--------------------------------------------------------------------------------
1 | import { Storage } from "@google-cloud/storage";
2 | import path from "path";
3 | const serviceKey = path.join(__dirname, "./keys.json");
4 |
5 | const storage = new Storage({
6 | keyFilename: serviceKey,
7 | projectId: "walky-345702"
8 | });
9 |
10 | export default storage;
11 |
--------------------------------------------------------------------------------
/server/cors.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "origin": ["*"],
4 | "method": ["*"],
5 | "responseHeader": ["*"],
6 | "maxAgeSeconds": 3600
7 | }
8 | ]
--------------------------------------------------------------------------------
/server/database.ts:
--------------------------------------------------------------------------------
1 | import sqlite3 from "sqlite3";
2 |
3 | const sqlite = sqlite3.verbose();
4 |
5 | const DBSOURCE = "db.sqlite";
6 |
7 | let db = new sqlite.Database(DBSOURCE, (err) => {
8 | if (err) {
9 | // Cannot open database
10 | console.error(err.message);
11 | throw err;
12 | } else {
13 | console.log("Connected to the SQLite database.");
14 | db.run(
15 | `CREATE TABLE IF NOT EXISTS space (
16 | id INTEGER PRIMARY KEY AUTOINCREMENT,
17 | path text UNIQUE,
18 | email text,
19 | code text,
20 | opt INTEGER,
21 | CONSTRAINT path_unique UNIQUE (path)
22 | )`,
23 | (err) => {
24 | console.log(err);
25 | }
26 | );
27 | }
28 | });
29 |
30 | export default db;
31 |
--------------------------------------------------------------------------------
/server/email.ts:
--------------------------------------------------------------------------------
1 | import path from "path";
2 | import fs from "fs";
3 |
4 | const serviceKey = JSON.parse(
5 | fs.readFileSync(path.join(__dirname, "./config/keys.json")).toString()
6 | );
7 |
8 | const mailjet = require("node-mailjet").connect(
9 | (serviceKey as any)["mailjet_key"],
10 | (serviceKey as any)["mailjet_secret"]
11 | );
12 |
13 | function sendEmail({
14 | email,
15 | path,
16 | code,
17 | }: {
18 | email: string;
19 | path: string;
20 | code: string;
21 | }): void {
22 | const request = mailjet.post("send", { version: "v3.1" }).request({
23 | Messages: [
24 | {
25 | From: {
26 | Email: "noreply@walky.space",
27 | Name: "walky.space",
28 | },
29 | To: [
30 | {
31 | Email: email,
32 | Name: email,
33 | },
34 | ],
35 | Subject: `You claimed walky.space/${path}`,
36 | TextPart: `Here's your edit code: ${code} `,
37 | HTMLPart: `
38 | You're now the gardener of walky.space/${path}!
39 | Edit code: ${code}
40 |
41 |
42 |
43 |
44 | If you have any questions or comments, you can send them to maxbittker@gmail.com!
45 |
46 |
47 |
`,
48 | },
49 | ],
50 | });
51 | request
52 | .then((result: { body: any }) => {
53 | console.log(result.body);
54 | })
55 | .catch((err: { statusCode: any }) => {
56 | console.log(err.statusCode);
57 | });
58 | }
59 | export default sendEmail;
60 |
--------------------------------------------------------------------------------
/server/endpoints.ts:
--------------------------------------------------------------------------------
1 | import * as path from "path";
2 | import * as http from "http";
3 | import * as https from "https";
4 | import express from "express";
5 | import multer from "multer";
6 | import fs from "fs";
7 | import db from "./database";
8 | import dotenv from "dotenv";
9 | dotenv.config({ path: __dirname + "/.env" });
10 |
11 | // Certificate
12 | const privateKey = fs.readFileSync(
13 | "/etc/letsencrypt/live/walky.space/privkey.pem",
14 | "utf8"
15 | );
16 | const certificate = fs.readFileSync(
17 | "/etc/letsencrypt/live/walky.space/cert.pem",
18 | "utf8"
19 | );
20 | const ca = fs.readFileSync(
21 | "/etc/letsencrypt/live/walky.space/chain.pem",
22 | "utf8"
23 | );
24 |
25 | const credentials = {
26 | key: privateKey,
27 | cert: certificate,
28 | ca: ca,
29 | };
30 |
31 | import { uploadImage, uploadImageURl } from "./helpers";
32 | import sendEmail from "./email";
33 |
34 | function startEndpoints(PORT: number): https.Server {
35 | const app = express();
36 | app.use(express.json());
37 | app.use(express.urlencoded({ extended: true })); //Parse URL-encoded bodies
38 |
39 | const httpServer = http.createServer(app);
40 | const httpsServer = https.createServer(credentials, app);
41 |
42 | app.use(express.static("../docs"));
43 | app.use("/files", express.static("./uploads"));
44 |
45 | app.get("/claimed/:path?", async function (req, res) {
46 | let path = req.params.path ?? "";
47 | const code = req.header("code");
48 |
49 | // Query the space by path
50 | const query = "SELECT id, email, path, code FROM space WHERE path = ?";
51 | const params = [path];
52 |
53 | db.get(query, params, (err: any, row: object) => {
54 | if (err) return res.status(400).send(err);
55 |
56 | if (!row) {
57 | return res.status(201).send({ claimed: false, access: "public" });
58 | } else {
59 | if ((row as any)["code"] === "") {
60 | res.status(200).send({ claimed: true, access: "public" });
61 | return;
62 | }
63 | if ((row as any)["code"] === code) {
64 | res.status(200).send({ claimed: true, access: "editor" });
65 | return;
66 | }
67 |
68 | // space was already saved in database.
69 | res.status(200).send({ claimed: true, access: "none" });
70 | }
71 | });
72 | });
73 |
74 | app.post("/claim/:path?", async function (req, res) {
75 | let path = req.params.path ?? "";
76 | const email = req.body["email"];
77 | const code = req.body["code"];
78 | const opt = req.body["opt"];
79 |
80 | // Query the space by path
81 | const query = "SELECT id, email, path, code FROM space WHERE path = ?";
82 | const params = [path];
83 |
84 | console.log("claiming space: " + path);
85 | db.all(query, params, (err: any, rows: string | any[]) => {
86 | if (err) return res.status(400).send(err);
87 |
88 | // If space is not found, create an entry
89 | if (rows.length === 0) {
90 | const insertQuery =
91 | "INSERT INTO space (path, email, code, opt) VALUES (?, ?, ?, ?)";
92 | const params = [path, email, code, opt];
93 | db.run(insertQuery, params, (result: any, err: any) => {
94 | if (err) {
95 | return res.status(400).send(err);
96 | } else {
97 | console.log("space created");
98 | sendEmail({ code, path, email });
99 | return res.status(201).send({ path, email, code, opt });
100 | }
101 | });
102 | } else {
103 | // User was already saved in database.
104 | console.log("User retrieved");
105 | res.status(200).send(rows[0]);
106 | }
107 | });
108 | });
109 |
110 | app.get("*", function (request, response) {
111 | response.sendFile(path.resolve(__dirname, "../docs/index.html"));
112 | });
113 |
114 | httpServer.listen(80, () => {
115 | console.log(`Server is listening on port ${80}`);
116 | });
117 | httpsServer.listen(443, () => {
118 | console.log(`Server is listening on port ${443}`);
119 | });
120 |
121 | const handleError = (err: any, res: any) => {
122 | console.log(err);
123 | res
124 | .status(500)
125 | .contentType("text/plain")
126 | .end("Oops! Something went wrong!");
127 | };
128 |
129 | const upload = multer({
130 | storage: multer.memoryStorage(),
131 | limits: {
132 | fileSize: 6 * 1024 * 1024,
133 | },
134 | });
135 |
136 | app.post(
137 | "/upload",
138 | upload.single(
139 | "image-upload" /* name attribute of element in your form */
140 | ),
141 | async (req, res) => {
142 | let owner_uuid = req.body["owner"];
143 | let name = req.body["name"];
144 | let position = JSON.parse(req.body["position"]);
145 | let size = req.body["size"] && JSON.parse(req.body["size"]);
146 |
147 | const uuid = Math.random().toString().slice(2, 7);
148 | try {
149 | let imageUrl;
150 | if (req.file) {
151 | imageUrl = await uploadImage(req.file, uuid + name);
152 | } else {
153 | let url = req.body["url"];
154 | let resp = (await uploadImageURl(url)) as any;
155 | size = resp.size;
156 | imageUrl = resp.publicUrl;
157 | }
158 | res
159 | .status(200)
160 | .contentType("text/json")
161 | .end(
162 | JSON.stringify({
163 | uuid,
164 | value: imageUrl,
165 | type: "img",
166 | position,
167 | size,
168 | owner_uuid,
169 | })
170 | );
171 | } catch (error) {
172 | return handleError(error, res);
173 | }
174 | }
175 | );
176 |
177 | return httpsServer;
178 | }
179 |
180 | export { startEndpoints };
181 |
--------------------------------------------------------------------------------
/server/helpers.ts:
--------------------------------------------------------------------------------
1 | import util from "util";
2 | import gc from "./config/";
3 | const bucket = gc.bucket("walky-uploads"); // should be your bucket name
4 | import axios, { AxiosRequestConfig } from "axios";
5 | import sizeOf from "image-size";
6 |
7 | /**
8 | *
9 | * @param { File } object file object that will be uploaded
10 | * @description - This function does the following
11 | * - It uploads a file to the image bucket on Google Cloud
12 | * - It accepts an object as an argument with the
13 | * "originalname" and "buffer" as keys
14 | */
15 |
16 | export const uploadImage = (
17 | file: { originalname: any; buffer: any },
18 | originalname: string
19 | ) =>
20 | new Promise((resolve, reject) => {
21 | const { buffer } = file;
22 | let name = originalname.replace(/ /g, "_");
23 | const blob = bucket.file(name);
24 | const blobStream = blob.createWriteStream({
25 | resumable: false
26 | });
27 | blobStream
28 | .on("finish", () => {
29 | const publicUrl = util.format(
30 | `https://storage.googleapis.com/${bucket.name}/${name}`
31 | );
32 | resolve(publicUrl);
33 | })
34 | .on("error", (e: any) => {
35 | console.log(e);
36 | reject(`Unable to upload image, something went wrong`);
37 | })
38 | .end(buffer);
39 | });
40 |
41 | export const uploadImageURl = (url: string) =>
42 | new Promise(async (resolve, reject): Promise => {
43 | const config = { responseType: "arraybuffer" };
44 |
45 | const resp = await axios.get(url, config as AxiosRequestConfig);
46 | resp.data;
47 | const name = encodeURI(url.substring(url.lastIndexOf("/") + 1));
48 |
49 | const blob = bucket.file(name);
50 | const blobStream = blob.createWriteStream({
51 | resumable: false
52 | });
53 | const dimensions = await sizeOf(resp.data);
54 | const size = { x: dimensions.width, y: dimensions.height };
55 |
56 | blobStream
57 | .on("finish", () => {
58 | const publicUrl = util.format(
59 | `https://storage.googleapis.com/${bucket.name}/${name}`
60 | );
61 | resolve({ publicUrl, size });
62 | })
63 | .on("error", (e: any) => {
64 | console.log(e);
65 | reject(`Unable to upload image, something went wrong`);
66 | })
67 | .end(resp.data);
68 | });
69 |
--------------------------------------------------------------------------------
/server/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "server",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "serve.ts",
6 | "scripts": {
7 | "start": "ts-node serve.ts",
8 | "test": "echo \"Error: no test specified\" && exit 1"
9 | },
10 | "author": "",
11 | "license": "ISC",
12 | "dependencies": {
13 | "@google-cloud/storage": "^5.18.3",
14 | "@types/express": "^4.17.7",
15 | "@types/multer": "^1.4.4",
16 | "@types/uuid": "^8.3.0",
17 | "@types/websocket": "^1.0.1",
18 | "axios": "^0.26.1",
19 | "express": "^4.17.1",
20 | "image-size": "^1.0.1",
21 | "multer": "^1.4.2",
22 | "node-mailjet": "^3.3.7",
23 | "sqlite3": "^5.0.2",
24 | "stytch": "^3.13.0",
25 | "ts-node": "^10.7.0",
26 | "typescript": "^4.0.2",
27 | "websocket": "^1.0.31",
28 | "y-websocket": "^1.4.0"
29 | },
30 | "devDependencies": {
31 | "@types/dotenv": "^8.2.0",
32 | "@types/sqlite3": "^3.1.8",
33 | "@types/ws": "^8.5.3"
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/server/serve.ts:
--------------------------------------------------------------------------------
1 | // Node.js WebSocket server script
2 | import { startEndpoints } from "./endpoints";
3 | import { startWsServer } from "./yjs/ws-server";
4 |
5 | let basePort: number = parseInt(process.env?.PORT || "4000", 10);
6 |
7 | let server = startEndpoints(basePort);
8 | startWsServer(server);
9 |
--------------------------------------------------------------------------------
/server/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "ts-node/node16/tsconfig.json",
3 | // Or install directly with `npm i -D @tsconfig/node16`
4 | "extends": "@tsconfig/node16/tsconfig.json",
5 | "compilerOptions": {
6 | "esModuleInterop": true,
7 | "downlevelIteration": true
8 |
9 | },
10 | }
--------------------------------------------------------------------------------
/server/yjs/callback.js:
--------------------------------------------------------------------------------
1 | const http = require('http')
2 |
3 | const CALLBACK_URL = process.env.CALLBACK_URL ? new URL(process.env.CALLBACK_URL) : null
4 | const CALLBACK_TIMEOUT = process.env.CALLBACK_TIMEOUT || 5000
5 | const CALLBACK_OBJECTS = process.env.CALLBACK_OBJECTS ? JSON.parse(process.env.CALLBACK_OBJECTS) : {}
6 |
7 | exports.isCallbackSet = !!CALLBACK_URL
8 |
9 | /**
10 | * @param {Uint8Array} update
11 | * @param {any} origin
12 | * @param {WSSharedDoc} doc
13 | */
14 | exports.callbackHandler = (update, origin, doc) => {
15 | const room = doc.name
16 | const dataToSend = {
17 | room: room,
18 | data: {}
19 | }
20 | const sharedObjectList = Object.keys(CALLBACK_OBJECTS)
21 | sharedObjectList.forEach(sharedObjectName => {
22 | const sharedObjectType = CALLBACK_OBJECTS[sharedObjectName]
23 | dataToSend.data[sharedObjectName] = {
24 | type: sharedObjectType,
25 | content: getContent(sharedObjectName, sharedObjectType, doc).toJSON()
26 | }
27 | })
28 | callbackRequest(CALLBACK_URL, CALLBACK_TIMEOUT, dataToSend)
29 | }
30 |
31 | /**
32 | * @param {URL} url
33 | * @param {number} timeout
34 | * @param {Object} data
35 | */
36 | const callbackRequest = (url, timeout, data) => {
37 | data = JSON.stringify(data)
38 | const options = {
39 | hostname: url.hostname,
40 | port: url.port,
41 | path: url.pathname,
42 | timeout: timeout,
43 | method: 'POST',
44 | headers: {
45 | 'Content-Type': 'application/json',
46 | 'Content-Length': data.length
47 | }
48 | }
49 | const req = http.request(options)
50 | req.on('timeout', () => {
51 | console.warn('Callback request timed out.')
52 | req.abort()
53 | })
54 | req.on('error', (e) => {
55 | console.error('Callback request error.', e)
56 | req.abort()
57 | })
58 | req.write(data)
59 | req.end()
60 | }
61 |
62 | /**
63 | * @param {string} objName
64 | * @param {string} objType
65 | * @param {WSSharedDoc} doc
66 | */
67 | const getContent = (objName, objType, doc) => {
68 | switch (objType) {
69 | case 'Array': return doc.getArray(objName)
70 | case 'Map': return doc.getMap(objName)
71 | case 'Text': return doc.getText(objName)
72 | case 'XmlFragment': return doc.getXmlFragment(objName)
73 | case 'XmlElement': return doc.getXmlElement(objName)
74 | default : return {}
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/server/yjs/utils.ts:
--------------------------------------------------------------------------------
1 | import WebSocket from "ws";
2 | import { IncomingMessage } from "http";
3 | const querystring = require("querystring");
4 | const Y = require("yjs");
5 | const syncProtocol = require("y-protocols/dist/sync.cjs");
6 | const awarenessProtocol = require("y-protocols/dist/awareness.cjs");
7 |
8 | const encoding = require("lib0/dist/encoding.cjs");
9 | const decoding = require("lib0/dist/decoding.cjs");
10 | const mutex = require("lib0/dist/mutex.cjs");
11 | const map = require("lib0/dist/map.cjs");
12 |
13 | const debounce = require("lodash.debounce");
14 |
15 | import db from "../database";
16 |
17 | const callbackHandler = require("./callback.js").callbackHandler;
18 | const isCallbackSet = require("./callback.js").isCallbackSet;
19 |
20 | const CALLBACK_DEBOUNCE_WAIT =
21 | parseInt(process.env.CALLBACK_DEBOUNCE_WAIT!) || 2000;
22 | const CALLBACK_DEBOUNCE_MAXWAIT =
23 | parseInt(process.env.CALLBACK_DEBOUNCE_MAXWAIT!) || 10000;
24 |
25 | const wsReadyStateConnecting = 0;
26 | const wsReadyStateOpen = 1;
27 | const wsReadyStateClosing = 2; // eslint-disable-line
28 | const wsReadyStateClosed = 3; // eslint-disable-line
29 |
30 | // disable gc when using snapshots!
31 | const gcEnabled = process.env.GC !== "false" && process.env.GC !== "0";
32 | const persistenceDir = process.env.YPERSISTENCE;
33 | /**
34 | * @type {{bindState: function(string,WSSharedDoc):void, writeState:function(string,WSSharedDoc):Promise, provider: any}|null}
35 | */
36 | let persistence: {
37 | bindState: (arg0: string, arg1: WSSharedDoc) => void;
38 | writeState: (arg0: string, arg1: WSSharedDoc) => Promise;
39 | provider: any;
40 | } | null = null;
41 | if (typeof persistenceDir === "string") {
42 | console.info('Persisting documents to "' + persistenceDir + '"');
43 | // @ts-ignore
44 | const LeveldbPersistence = require("y-leveldb").LeveldbPersistence;
45 | const ldb = new LeveldbPersistence(persistenceDir);
46 | persistence = {
47 | provider: ldb,
48 | bindState: async (docName, ydoc) => {
49 | const persistedYdoc = await ldb.getYDoc(docName);
50 | const newUpdates = Y.encodeStateAsUpdate(ydoc);
51 | ldb.storeUpdate(docName, newUpdates);
52 | Y.applyUpdate(ydoc, Y.encodeStateAsUpdate(persistedYdoc));
53 | ydoc.on("update", (update: any) => {
54 | ldb.storeUpdate(docName, update);
55 | });
56 | },
57 | writeState: async (docName, ydoc) => {},
58 | };
59 | }
60 |
61 | /**
62 | * @param {{bindState: function(string,WSSharedDoc):void,
63 | * writeState:function(string,WSSharedDoc):Promise,provider:any}|null} persistence_
64 | */
65 | exports.setPersistence = (
66 | persistence_: {
67 | bindState: (arg0: string, arg1: WSSharedDoc) => void;
68 | writeState: (arg0: string, arg1: WSSharedDoc) => Promise;
69 | provider: any;
70 | } | null
71 | ) => {
72 | persistence = persistence_;
73 | };
74 |
75 | /**
76 | * @return {null|{bindState: function(string,WSSharedDoc):void,
77 | * writeState:function(string,WSSharedDoc):Promise}|null} used persistence layer
78 | */
79 | exports.getPersistence = (): null | {
80 | bindState: (arg0: string, arg1: WSSharedDoc) => void;
81 | writeState: (arg0: string, arg1: WSSharedDoc) => Promise;
82 | } | null => persistence;
83 |
84 | /**
85 | * @type {Map}
86 | */
87 | const docs: Map = new Map();
88 | // exporting docs so that others can use it
89 | exports.docs = docs;
90 |
91 | const messageSync = 0;
92 | const messageAwareness = 1;
93 | // const messageAuth = 2
94 |
95 | /**
96 | * @param {Uint8Array} update
97 | * @param {any} origin
98 | * @param {WSSharedDoc} doc
99 | */
100 | const updateHandler = (update: Uint8Array, origin: any, doc: WSSharedDoc) => {
101 | const encoder = encoding.createEncoder();
102 | encoding.writeVarUint(encoder, messageSync);
103 | syncProtocol.writeUpdate(encoder, update);
104 | const message = encoding.toUint8Array(encoder);
105 | doc.conns.forEach((_: any, conn: any) => send(doc, conn, message));
106 | };
107 |
108 | class WSSharedDoc extends Y.Doc {
109 | /**
110 | * @param {string} name
111 | */
112 | constructor(name: string) {
113 | super({ gc: gcEnabled });
114 | this.name = name;
115 | this.mux = mutex.createMutex();
116 | /**
117 | * Maps from conn to set of controlled user ids. Delete all user ids from awareness when this conn is closed
118 | * @type {Map