added web socket support

This commit is contained in:
slynn1324 2021-01-28 15:37:43 -06:00
parent 8cdf21ed55
commit b23208c0bf
12 changed files with 324 additions and 38 deletions

49
package-lock.json generated
View file

@ -12,6 +12,7 @@
"body-parser": "^1.19.0",
"cookie-parser": "^1.4.5",
"express": "^4.17.1",
"express-ws": "^4.0.0",
"node-fetch": "^2.6.1",
"sharp": "^0.27.0",
"yargs": "^16.2.0"
@ -86,6 +87,11 @@
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-3.0.0.tgz",
"integrity": "sha512-zPMVc3ZYlGLNk4mpK1NzP2wg0ml9t7fUgDsayR5Y5rSzxQilzR9FGu/EH2jQOcKSAeAfWeylyW8juy3OkWRvNA=="
},
"node_modules/async-limiter": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz",
"integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ=="
},
"node_modules/base64-js": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
@ -629,6 +635,20 @@
"node": ">= 0.10.0"
}
},
"node_modules/express-ws": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/express-ws/-/express-ws-4.0.0.tgz",
"integrity": "sha512-KEyUw8AwRET2iFjFsI1EJQrJ/fHeGiJtgpYgEWG3yDv4l/To/m3a2GaYfeGyB3lsWdvbesjF5XCMx+SVBgAAYw==",
"dependencies": {
"ws": "^5.2.0"
},
"engines": {
"node": ">=4.5.0"
},
"peerDependencies": {
"express": "^4.0.0 || ^5.0.0-alpha.1"
}
},
"node_modules/express/node_modules/array-flatten": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
@ -1522,6 +1542,14 @@
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
},
"node_modules/ws": {
"version": "5.2.2",
"resolved": "https://registry.npmjs.org/ws/-/ws-5.2.2.tgz",
"integrity": "sha512-jaHFD6PFv6UgoIVda6qZllptQsMlDEJkTQcybzzXDYM1XO9Y8em691FGMPmM46WGyLU4z9KMgQN+qrux/nhlHA==",
"dependencies": {
"async-limiter": "~1.0.0"
}
},
"node_modules/y18n": {
"version": "5.0.5",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.5.tgz",
@ -1658,6 +1686,11 @@
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-3.0.0.tgz",
"integrity": "sha512-zPMVc3ZYlGLNk4mpK1NzP2wg0ml9t7fUgDsayR5Y5rSzxQilzR9FGu/EH2jQOcKSAeAfWeylyW8juy3OkWRvNA=="
},
"async-limiter": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz",
"integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ=="
},
"base64-js": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
@ -2080,6 +2113,14 @@
}
}
},
"express-ws": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/express-ws/-/express-ws-4.0.0.tgz",
"integrity": "sha512-KEyUw8AwRET2iFjFsI1EJQrJ/fHeGiJtgpYgEWG3yDv4l/To/m3a2GaYfeGyB3lsWdvbesjF5XCMx+SVBgAAYw==",
"requires": {
"ws": "^5.2.0"
}
},
"file-uri-to-path": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
@ -2760,6 +2801,14 @@
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
},
"ws": {
"version": "5.2.2",
"resolved": "https://registry.npmjs.org/ws/-/ws-5.2.2.tgz",
"integrity": "sha512-jaHFD6PFv6UgoIVda6qZllptQsMlDEJkTQcybzzXDYM1XO9Y8em691FGMPmM46WGyLU4z9KMgQN+qrux/nhlHA==",
"requires": {
"async-limiter": "~1.0.0"
}
},
"y18n": {
"version": "5.0.5",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.5.tgz",

View file

@ -13,6 +13,7 @@
"body-parser": "^1.19.0",
"cookie-parser": "^1.4.5",
"express": "^4.17.1",
"express-ws": "^4.0.0",
"node-fetch": "^2.6.1",
"sharp": "^0.27.0",
"yargs": "^16.2.0"

View file

@ -86,12 +86,12 @@ const COOKIE_KEY = Buffer.from(db.prepare("SELECT value FROM properties WHERE ke
// express config
const app = express();
app.use(express.static('public'));
const expressWs = require('express-ws')(app);
app.use(express.static('public'));
app.use(bodyParser.raw({type: 'image/jpeg', limit: '25mb'}));
app.use(bodyParser.urlencoded({ extended: false }))
app.use(bodyParser.json());
app.set('json spaces', 2);
app.use(cookieParser());
@ -126,6 +126,20 @@ function decryptCookie(ciphertext){
return JSON.parse(deciphered);
}
// broadcast isn't user specific right now...
app.ws('/ws/:uid', (ws, req) => {
ws.on("message", (msg) => {
//console.log("received messsage: " + msg);
});
console.log("socket opened for user " + req.params.uid);
});
function broadcast(uid, msg){
for ( let socket of expressWs.getWss('/ws/' + uid).clients ){
socket.send(JSON.stringify(msg));
}
}
// handle auth
app.use ( async (req, res, next) => {
@ -283,7 +297,7 @@ const SERVER_ERROR = {status: "error", error: "server error"};
app.get("/api/whoami", (req, res) => {
res.send({name: req.user.name, version: VERSION});
res.send({name: req.user.name, version: VERSION, id: req.user.id});
});
// list boards
@ -335,7 +349,8 @@ app.post('/api/boards', (req, res) => {
board.titlePinId = 0;
res.send(board);
console.log(`Created board#${id} ${req.body.name}`);
broadcast(req.user.id, {b:id});
} catch (err){
console.log("Error creating board: " + err.message);
if ( err.message.includes('UNIQUE constraint failed:') ){
@ -383,6 +398,7 @@ app.delete("/api/boards/:boardId", async (req, res) => {
if ( result.changes == 1 ){
res.send(OK);
broadcast(req.user.id, {deleteBoard: req.params.boardId});
} else {
res.status(404).send(NOT_FOUND);
}
@ -411,6 +427,18 @@ app.get("/api/pins/:pinId", (req, res) => {
app.post("/api/pins", async (req, res) => {
try {
let boardId = req.body.boardId;
if ( boardId == "new" ){
try {
let result = db.prepare("INSERT INTO boards (name, userId, hidden, createDate) VALUES (@name, @userId, @hidden, @createDate)").run({name: req.body.newBoardName, userId: req.user.id, hidden: 0, createDate: new Date().toISOString()});
boardId = result.lastInsertRowid;
} catch (e){
console.log("error creating new board: ", err);
res.status(500).send(SERVER_ERROR);
}
}
// download the image first to make sure we can get it
let image = await downloadImage(req.body.imageUrl);
@ -439,7 +467,7 @@ app.post("/api/pins", async (req, res) => {
@userId,
@createDate)
`).run({
boardId: req.body.boardId,
boardId: boardId,
imageUrl: req.body.imageUrl,
siteUrl: req.body.siteUrl,
description: req.body.description,
@ -460,12 +488,15 @@ app.post("/api/pins", async (req, res) => {
let pin = db.prepare("SELECT * FROM pins WHERE userId = @userId and id = @pinId").get({userId: req.user.id, pinId: id});
res.send(pin);
broadcast(req.user.id, {updateBoard:boardId});
} catch (err) {
console.log(`Error creating pin: ${err.message}`, err);
res.status(500).send(SERVER_ERROR);
}
});
// create pin
app.post("/api/pins/:pinId", (req,res) => {
try {
@ -487,6 +518,7 @@ app.post("/api/pins/:pinId", (req,res) => {
if ( result.changes == 1 ){
console.log(`updated pin#${req.params.pinId}`)
res.send(OK);
broadcast(req.user.id, {updateBoard:req.body.boardId});
} else {
res.status(404).send(NOT_FOUND);
}
@ -497,9 +529,12 @@ app.post("/api/pins/:pinId", (req,res) => {
});
// delete pin
app.delete("/api/pins/:pinId", async (req, res) => {
try {
let pin = db.prepare('SELECT * FROM pins WHERE userId = @userId and id = @pinId').get({userId: req.user.id, pinId:req.params.pinId});
let result = db.prepare('DELETE FROM pins WHERE userId = @userId and id = @pinId').run({userId: req.user.id, pinId:req.params.pinId});
if ( result.changes == 1 ){
@ -512,6 +547,9 @@ app.delete("/api/pins/:pinId", async (req, res) => {
console.log(`deleted pin#${req.params.pinId}`);
res.send(OK);
broadcast(req.user.id, {updateBoard:pin.boardId});
} else {
res.status(404).send(NOT_FOUND);
}
@ -592,6 +630,10 @@ app.post("/up/", async (req, res) => {
return;
});
app.get("/otl/:l", (req, res) => {
});
// start listening

View file

@ -25,13 +25,56 @@ app.addSetter("load.boards", async (data) => {
store.do("loader.hide");
});
app.addSetter('load.board', async (data) => {
// handle update events
window.addEventListener("broadcast", async (e) => {
let data = store.data;
if ( e.detail.updateBoard ){
console.log("updating board");
let boardId = e.detail.updateBoard;
let boardExists = false;
for ( let i = 0; i < data.boards.length; ++i ){
if ( data.boards[i].id == boardId ){
boardExists = true;
}
}
// if it's a new board
if ( !boardExists ){
store.do("load.boards");
}
// if we are currently viewing this board, reload the pins
if ( data.board && boardId == data.board.id ){
store.do("load.board", true);
}
} else if ( e.detail.deleteBoard ) {
console.log("deleting board");
let boardId = e.detail.deleteBoard;
// reload the boards
store.do("load.boards");
// we're currently looking at this board... alert and error
if ( data.board && boardId == data.board.id ){
window.alert("this board has been deleted on another device");
window.location.hash = "#";
}
}
});
app.addSetter('load.board', async (data, force) => {
store.do("loader.show");
if ( !data.board || data.board.id != data.hash.board ){
let res = await fetch("/api/boards/" + data.hash.board);
data.board = await res.json();
}
if ( data.hash.board ){
if ( force || !data.board || data.board.id != data.hash.board ){
let res = await fetch("/api/boards/" + data.hash.board);
data.board = await res.json();
}
}
store.do("loader.hide");
});
@ -42,6 +85,9 @@ app.addSetter('load.user', async (data) => {
let res = await fetch("/api/whoami");
data.user = await res.json();
window.uid = data.user.id;
window.socketConnect();
store.do("loader.hide");
});
@ -225,4 +271,12 @@ store.do('load.user');
store.do('load.boards');
store.do('hash.update');
appComponent.render();
appComponent.render();
// refresh on focus
window.addEventListener("focus", () => {
store.do("load.boards");
store.do("load.board");
window.dispatchEvent(new CustomEvent("socket-connect"));
});

View file

@ -286,6 +286,21 @@
margin-top: -2px;
}
/* https://thenounproject.com/search/?q=connected&i=88200 */
#socketConnected {
background-image: url("data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9JzMwMHB4JyB3aWR0aD0nMzAwcHgnICBmaWxsPSIjMWExYTFhIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB2ZXJzaW9uPSIxLjEiIHg9IjBweCIgeT0iMHB4IiB2aWV3Qm94PSIwIDAgMTAwIDEwMCIgZW5hYmxlLWJhY2tncm91bmQ9Im5ldyAwIDAgMTAwIDEwMCIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHBhdGggZD0iTTkwLjMsNDguMUM5MC41LDYxLjksNzkuNCw3My41LDY1LjYsNzRjLTMuNywwLjEtNy4yLTAuNi0xMC40LTEuOWMtMS42LTAuNy0yLjMtMi42LTEuNS00LjJsMC0wLjFjMC43LTEuMywyLjMtMS45LDMuNi0xLjMgIGMzLDEuMyw2LjQsMS44LDEwLDEuM2M4LjktMS4yLDE1LjktOC41LDE2LjgtMTcuNGMxLjItMTIuMi05LTIyLjUtMjEuMy0yMS4zQzU0LDI5LjksNDYuNiwzNyw0NS41LDQ2Yy0wLjMsMi41LTAuMiw0LjksMC40LDcuMSAgYzAuMywxLjEtMC4xLDIuMi0wLjksMi45YzAsMCwwLDAsMCwwYy0xLjcsMS41LTQuNCwwLjctNC45LTEuNWMtMC44LTMuMy0xLTYuOS0wLjMtMTAuNmMyLTEwLjksMTEtMTkuNCwyMi0yMC43ICBDNzcsMjEuNCw5MC4xLDMzLjIsOTAuMyw0OC4xeiBNMzYuOSw3NC44YzExLTEuMywyMC05LjgsMjItMjAuN2MwLjctMy43LDAuNS03LjMtMC4zLTEwLjdjLTAuNS0yLjItMy4yLTMuMS00LjktMS41bDAsMCAgYy0wLjgsMC43LTEuMSwxLjgtMC45LDIuOWMwLjYsMi4zLDAuNyw0LjcsMC40LDcuMmMtMS4yLDguOS04LjUsMTYtMTcuNSwxNi45QzIzLjQsNzAsMTMuMiw1OS44LDE0LjQsNDcuNiAgYzAuOS04LjksNy45LTE2LjIsMTYuOC0xNy40YzMuNi0wLjUsNywwLjEsMTAsMS4zYzEuNCwwLjYsMi45LDAsMy42LTEuM2wwLTAuMWMwLjgtMS42LDAuMi0zLjUtMS41LTQuMmMtMy43LTEuNS03LjktMi4yLTEyLjMtMS43ICBDMTksMjUuNCw5LjQsMzUuMyw4LjQsNDcuNEM3LjEsNjMuNCwyMC43LDc2LjcsMzYuOSw3NC44eiI+PC9wYXRoPjwvc3ZnPg==");
background-size: 24px 24px;
background-repeat: no-repeat;
background-position: 6px 8px;
opacity: 0.1;
margin-left: 10px;
}
body.socketConnected #socketConnected {
opacity: 1;
}
.modal-card {
max-width: 95%;
}

View file

@ -48,26 +48,9 @@ app.addSetter('addPinModal.save', async (data) => {
let boardId = data.addPinModal.boardId;
let newBoard = null;
if ( boardId == "new" ){
let res = await fetch('api/boards', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
"name": data.addPinModal.newBoardName
})
});
if ( res.status == 200 ){
newBoard = await res.json();
boardId = newBoard.id;
data.boards.push(newBoard);
}
}
let postData = {
boardId: boardId,
newBoardName: data.addPinModal.newBoardName,
imageUrl: data.addPinModal.imageUrl,
siteUrl: data.addPinModal.siteUrl,
description: data.addPinModal.description
@ -80,20 +63,21 @@ app.addSetter('addPinModal.save', async (data) => {
},
body: JSON.stringify(postData)
});
if ( res.status == 200 ){
let body = await res.json();
if ( data.board && data.board.id == boardId ){
data.board.pins.push(body);
}
if ( newBoard ){
newBoard.titlePinId = body.id;
}
window.localStorage.addPinLastBoardId = boardId;
store.do("addPinModal.close");
// if we don't have a listening socket, we need to trigger our own update
if ( boardId == "new" && !window.socketConnected ){
store.do("load.boards");
}
}
store.do("loader.hide");

View file

@ -76,7 +76,9 @@ app.addComponent('navbar', (store) => { return new Reef("#navbar", {
${boardName}
<span id="loader-mobile" class="navbar-item" style="position: relative; margin-left: auto;">
<div id="loader" class="button is-text ${data.loading ? 'is-loading' : ''}"></div>
<div id="socketConnected" class="button is-text"></div>
</span>
<a role="button" class="navbar-burger ${data.menuOpen ? 'is-active' : ''}" aria-label="menu" aria-expanded="false" data-onclick="navbar.toggleMenu">
<span aria-hidden="true"></span>

View file

@ -11,7 +11,7 @@
<link rel="apple-touch-icon" sizes="180x180" href="pub/icons/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="pub/icons/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="pub/icons/favicon-16x16.png">
<link rel="manifest" href="pub/icons/site.webmanifest">
<!--<link rel="manifest" href="pub/icons/site.webmanifest"> not working -->
<link rel="mask-icon" href="pub/icons/safari-pinned-tab.svg" color="#5bbad5">
<link rel="shortcut icon" href="pub/icons/favicon.ico">
<meta name="msapplication-TileColor" content="#da532c">
@ -46,6 +46,7 @@
<script src="components/editpin.js"></script>
<script src="components/about.js"></script>
<script src="app.js"></script>
<script src="ws.js"></script>
</body>

View file

@ -2,7 +2,7 @@
<browserconfig>
<msapplication>
<tile>
<square150x150logo src="/mstile-150x150.png"/>
<square150x150logo src="/pub/icons/mstile-150x150.png"/>
<TileColor>#da532c</TileColor>
</tile>
</msapplication>

View file

@ -3,12 +3,12 @@
"short_name": "",
"icons": [
{
"src": "/android-chrome-192x192.png",
"src": "/pub/icons/android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/android-chrome-512x512.png",
"src": "/pub/icons/android-chrome-512x512.png",
"sizes": "512x512",
"type": "image/png"
}

51
static/ws.html Normal file
View file

@ -0,0 +1,51 @@
<!DOCTYPE html>
<html>
<head></head>
<body>
ws test
<button onclick="sendMessage()">send</button>
<script>
let socket = new WebSocket(getSocketUrl());
socket.onopen = (e) => {
console.log("[open] connection establised");
console.log("sending to server");
socket.send("hello");
};
socket.onmessage = (e) => {
console.log("got message: " + e.data);
}
socket.onclose = (e) => {
console.log("socket closed - wasClean=" + e.wasClean + " code=" + e.code + " reason=" + e.reason);
}
socket.onerror = (e) => {
console.log("error: " + e.message);
}
function sendMessage(){
socket.send("hello");
}
function getSocketUrl(){
var loc = window.location, new_uri;
if (loc.protocol === "https:") {
new_uri = "wss:";
} else {
new_uri = "ws:";
}
new_uri += "//" + loc.host;
// if ( loc.port ){
// new_uri += ":" + loc.port
// }
new_uri += "/ws";
return new_uri;
}
</script>
</body>
</html>

87
static/ws.js Normal file
View file

@ -0,0 +1,87 @@
// decorate on web socket functions. if web sockets fail to work, the app should keep working anyway.
// if you want to permanently disable websockets, just remove the src include
window.socketConnected = false;
window.socket = null;
// keep-alive
setInterval(() => {
if ( window.socket && window.socket.readyState != WebSocket.OPEN ){
console.log("web socket reconnect");
window.socket = socketConnect();
} else {
window.socket.send(".");
}
}, 30000);
window.addEventListener("socket-connect", () => {
socketConnect();
});
function socketConnect(){
if ( !window.uid ){
console.log("no user id, can't open a socket");
return;
}
if ( window.socketConnected ){
console.log("web socket already connected");
return;
}
window.socketConnected = false;
let s = new WebSocket(getSocketUrl());
s.onopen = (e) => {
console.log("web socket connected");
// wait 10ms to see if the socket stays connected
setTimeout( () => {
if ( s.readyState == WebSocket.OPEN ){
console.log("web socket appears operational");
document.body.classList.add("socketConnected");
window.socketConnected = true;
window.socketConnectFailureCount = 0;
store.do("load.boards");
store.do("load.board");
} else {
console.log("web socket connect failed");
}
}, 10);
};
s.onmessage = (e) => {
console.log("web socket message: " + e.data);
let msg = JSON.parse(e.data);
window.dispatchEvent(new CustomEvent("broadcast", {detail: msg}));
}
s.onclose = (e) => {
console.log("web socket closed");
document.body.classList.remove("socketConnected");
window.socketConnected = false;
}
s.onerror = (e) => {
console.log("web socket error: " + e.message);
}
window.socket = s;
}
function getSocketUrl(){
var loc = window.location, new_uri;
if (loc.protocol === "https:") {
new_uri = "wss:";
} else {
new_uri = "ws:";
}
new_uri += "//" + loc.host;
new_uri += "/ws/" + window.uid;
return new_uri;
}