diff --git a/README.md b/README.md index f9cb63f..da83b52 100644 --- a/README.md +++ b/README.md @@ -5,13 +5,13 @@ Поставь зависимости и запусти сервер. Для этого перейди в директорию задачи и выполни команду `npm install`. После установки зависимостей, выполни команду `npm run dev`. После запуска, перейди по адресу [localhost:5000](http://localhost:5000) -1. Сейчас цвета палитры захардкожены в файле `/client/picker.mjs`, cделай так, чтобы цвета запрашивались с сервера в функции `drawPalette` +--1. Сейчас цвета палитры захардкожены в файле `/client/picker.mjs`, cделай так, чтобы цвета запрашивались с сервера в функции `drawPalette` -2. На сервере мы будем использовать библиотеку [ws](https://github.com/websockets/ws) для подключения по протоколу [WebSocket](https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API). Она уже подключена, `WebSocket`-сервер уже правильно интегрирован с `express`, запущен и ждёт подключений по тому же порту, что и `express`. +--2. На сервере мы будем использовать библиотеку [ws](https://github.com/websockets/ws) для подключения по протоколу [WebSocket](https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API). Она уже подключена, `WebSocket`-сервер уже правильно интегрирован с `express`, запущен и ждёт подключений по тому же порту, что и `express`. Сделай так, чтобы `WebSocket`-сервер начал обрабатывать подключения, т.е. события `connection`, как [в примере](https://github.com/websockets/ws#simple-server) -3. Сделай так, чтобы после подключения, новому клиенту присылался текущий массив состояния поля. Так как от сервера к клиенту будут передаваться разные сообщения, то удобно, чтобы каждое сообщение имело следующую структуру: +--3. Сделай так, чтобы после подключения, новому клиенту присылался текущий массив состояния поля. Так как от сервера к клиенту будут передаваться разные сообщения, то удобно, чтобы каждое сообщение имело следующую структуру: ```javascript { @@ -28,15 +28,15 @@ Как принимать и отправлять строковые сообщения уже было показано [в примере](https://github.com/websockets/ws#simple-server) -4. Сделай так, чтобы на клиенте при получении сообщения из `WebSocket` с начальным состоянием поля, заполнялось поле. Для этого измени обработчик события на `ws` в файле `/client/index.mjs`. В него сейчас приходят события [MessageEvent](https://developer.mozilla.org/en-US/docs/Web/API/MessageEvent). Для десериализации данных в событии используй `JSON.parse()`. Для отрисовки начального состояния используй `drawer.putArray()` +--4. Сделай так, чтобы на клиенте при получении сообщения из `WebSocket` с начальным состоянием поля, заполнялось поле. Для этого измени обработчик события на `ws` в файле `/client/index.mjs`. В него сейчас приходят события [MessageEvent](https://developer.mozilla.org/en-US/docs/Web/API/MessageEvent). Для десериализации данных в событии используй `JSON.parse()`. Для отрисовки начального состояния используй `drawer.putArray()` -5. Сделай так, чтобы при клике на поле, на сервер по `WebSocket` передавалось сообщение с координатами и цветом. Посылай сообщения с помощью метода [send()](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/send). Для удобства, используй тот же формат, что и в сообщениях, посылаемых с сервера +--5. Сделай так, чтобы при клике на поле, на сервер по `WebSocket` передавалось сообщение с координатами и цветом. Посылай сообщения с помощью метода [send()](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/send). Для удобства, используй тот же формат, что и в сообщениях, посылаемых с сервера -6. Сделай так, чтобы `WebSocket`-сервер при получении сообщения с координатами и цветом валидировал его на правильность координат и цвета, а затем рассылал всем активным клиентам. Как сделать `broadcast` посмотри [в примере](https://github.com/websockets/ws#server-broadcast) +--6. Сделай так, чтобы `WebSocket`-сервер при получении сообщения с координатами и цветом валидировал его на правильность координат и цвета, а затем рассылал всем активным клиентам. Как сделать `broadcast` посмотри [в примере](https://github.com/websockets/ws#server-broadcast) -7. Сделай так, чтобы новые пиксели не только рассылались остальным клиентам, но и добавлялись в поле, хранимое на стороне сервера. Это нужно, чтобы новые клиенты получали текущее изображение, а не начальное. Как сделаешь — проверь: открой приложение с одной вкладки браузера, нарисуй что-нибудь, затем открой со второй вкладки и убедись, что изображения совпадают +--7. Сделай так, чтобы новые пиксели не только рассылались остальным клиентам, но и добавлялись в поле, хранимое на стороне сервера. Это нужно, чтобы новые клиенты получали текущее изображение, а не начальное. Как сделаешь — проверь: открой приложение с одной вкладки браузера, нарисуй что-нибудь, затем открой со второй вкладки и убедись, что изображения совпадают -8. Удали из обработчика клика на поле отрисовку пикселя. Для этого сделай так, чтобы при `broadcast`-е текущему клиенту тоже отправлялось сообщение. А затем сделай так, чтобы отрисовка пикселя происходила только при получении клиентом сообщения из `WebSocket`. После этого сервер будет полностью контролировать целостность поля для рисования +--8. Удали из обработчика клика на поле отрисовку пикселя. Для этого сделай так, чтобы при `broadcast`-е текущему клиенту тоже отправлялось сообщение. А затем сделай так, чтобы отрисовка пикселя происходила только при получении клиентом сообщения из `WebSocket`. После этого сервер будет полностью контролировать целостность поля для рисования 9. Опубликуй своё приложение на [Heroku](https://id.heroku.com/login). Для этого потребуются `heroku-cli`, и `git`, если чего-то нет, пройди [вот эти шаги](https://devcenter.heroku.com/articles/getting-started-with-nodejs#set-up). @@ -52,13 +52,13 @@ - Чтобы запускать приложение локально можно использовать `heroku local web` - Чтобы деплоить новую версию приложения можно повторять последние два шага -10. Сделай так, что `WebSocket` сервер принимал только те подключения, которые передали в параметрах правильный `apiKey`. Для этого измени обработчик события `upgrade`. Подробнее о нём можно прочитать [здесь](https://nodejs.org/api/http.html#http_event_upgrade). +--10. Сделай так, что `WebSocket` сервер принимал только те подключения, которые передали в параметрах правильный `apiKey`. Для этого измени обработчик события `upgrade`. Подробнее о нём можно прочитать [здесь](https://nodejs.org/api/http.html#http_event_upgrade). В обработчике уже используется объект `URL`, благодаря которому можно получить значение `apiKey` из `query string`. Как это сделать посмотри [тут](https://nodejs.org/api/url.html#url_class_urlsearchparams). Для закрытия соединения, которое произошло с невалидным `apiKey` можно использовать `socket.destroy()`. Подробнее можно прочитать [здесь](https://nodejs.org/api/stream.html#stream_writable_destroy_error) -11. \* Сделай так, чтобы после успешного `upgrade` происходила связь `ws` клиента и его `apiKey`. Сделать это можно внутри в `handleUpgrade()`. Для этого используй [WeakMap](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakMap), где ключами будут объекты `ws`, а значениями — связанные с ними `apiKey`. `WeakMap` не будет препятствовать сборщику мусора очищать `ws`-соединения, когда они будут закрыты +--11. \* Сделай так, чтобы после успешного `upgrade` происходила связь `ws` клиента и его `apiKey`. Сделать это можно внутри в `handleUpgrade()`. Для этого используй [WeakMap](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakMap), где ключами будут объекты `ws`, а значениями — связанные с ними `apiKey`. `WeakMap` не будет препятствовать сборщику мусора очищать `ws`-соединения, когда они будут закрыты -12. \* Сделай так, чтобы у каждого `apiKey` был свой таймаут. При подключении нового `WebSocket` клиента, посылай ему сообщение с временем, когда он в следующий раз может нарисовать на поле. Текущее время можно получить с помощью `new Date()`. Для отправки сериализуй дату с помощью `.toISOString()`. +--12. \* Сделай так, чтобы у каждого `apiKey` был свой таймаут. При подключении нового `WebSocket` клиента, посылай ему сообщение с временем, когда он в следующий раз может нарисовать на поле. Текущее время можно получить с помощью `new Date()`. Для отправки сериализуй дату с помощью `.toISOString()`. На клиенте при получении сообщения, обновляй таймер с помощью `timeout.next =`. Десериализуй полученную дату с помощью `new Date(isoDateString)`. На сервере, при получении сообщения с координатами проверяй таймаут `apiKey` `ws` клиента и не принимай сообщения, которые произошли до истечения этого таймаута. diff --git a/client/index.mjs b/client/index.mjs index 3836cb0..a552a13 100644 --- a/client/index.mjs +++ b/client/index.mjs @@ -3,22 +3,46 @@ import drawer from "./drawer.mjs"; import picker from "./picker.mjs"; document.querySelector("#start").addEventListener("submit", e => { - e.preventDefault(); - main(new FormData(e.currentTarget).get("apiKey")); - document.querySelector(".container").classList.add("ready"); + e.preventDefault(); + main(new FormData(e.currentTarget).get("apiKey")); + document.querySelector(".container").classList.add("ready"); }); +const onFieldRecieved = (message) => { + console.log(message); + + const data = JSON.parse(message?.['data']) + console.log(data); + + switch (data['type']) { + case "place": + drawer.putArray(data['payload']['place']); + break; + case "click": + drawer.put(data.payload.x, data.payload.y, data.payload.color); + break; + case "timeout": + timeout.next = new Date(data.payload); + break; + } +} + const main = apiKey => { - const ws = connect(apiKey); - ws.addEventListener("message", console.log); + const ws = connect(apiKey); + ws.addEventListener("message", onFieldRecieved); - timeout.next = new Date(); - drawer.onClick = (x, y) => { - drawer.put(x, y, picker.color); - }; + timeout.next = new Date(); + drawer.onClick = (x, y) => { + // отправляем на сервер + ws.send(JSON.stringify({ + type: "click", + payload: { x: x, y: y, color: picker.color } + })); + }; }; const connect = apiKey => { - const url = `${location.origin.replace(/^http/, "ws")}?apiKey=${apiKey}`; - return new WebSocket(url); + const url = `${location.origin.replace(/^http/, "ws")}?apiKey=${apiKey}`; + return new WebSocket(url); }; + diff --git a/client/picker.mjs b/client/picker.mjs index 6a49c82..bed32df 100644 --- a/client/picker.mjs +++ b/client/picker.mjs @@ -5,7 +5,12 @@ const setAttributes = (element, object) => { }; const drawPalette = async () => { - const colors = hardcodedColors; + const response = await fetch("/api/getColors"); + + console.log(response); + + const colors = await response.json(); + pickedColor = colors[0]; const palette = document.querySelector("#palette"); const fragment = document.createDocumentFragment(); diff --git a/package-lock.json b/package-lock.json index 72c62c9..7b3bbc0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -35,33 +35,14 @@ } }, "ansi-align": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.0.tgz", - "integrity": "sha512-ZpClVKqXN3RGBmKibdfWzqCY4lnjEuoNzU5T0oEFpfd/z5qJHVarukridD4juLO2FXMiwUQxr9WqQtaYa8XRYw==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.1.tgz", + "integrity": "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==", "dev": true, "requires": { - "string-width": "^3.0.0" - }, - "dependencies": { - "string-width": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", - "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", - "dev": true, - "requires": { - "emoji-regex": "^7.0.1", - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^5.1.0" - } - } + "string-width": "^4.1.0" } }, - "ansi-regex": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", - "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", - "dev": true - }, "ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -380,12 +361,6 @@ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" }, - "emoji-regex": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", - "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", - "dev": true - }, "encodeurl": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", @@ -503,9 +478,9 @@ } }, "glob-parent": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.1.tgz", - "integrity": "sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==", + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", "dev": true, "requires": { "is-glob": "^4.0.1" @@ -607,9 +582,9 @@ "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" }, "ini": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", - "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==", + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", "dev": true }, "ipaddr.js": { @@ -641,12 +616,6 @@ "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", "dev": true }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", - "dev": true - }, "is-glob": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", @@ -864,9 +833,9 @@ "dev": true }, "normalize-url": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-4.5.0.tgz", - "integrity": "sha512-2s47yzUxdexf1OhyRi4Em83iQk0aPvwTddtFz4hnSSw9dCEsLEGf6SwIO8ss/19S9iBb5sJaOuTvTGDeZI00BQ==", + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-4.5.1.tgz", + "integrity": "sha512-9UZCFRHQdNrfTpGg8+1INIg93B6zE0aXMVFkw1WFwvO4SlZywU6aLg5Of0Ap/PgcbSw4LNxvMWXMeugwMCX0AA==", "dev": true }, "on-finished": { @@ -1136,9 +1105,9 @@ }, "dependencies": { "ansi-regex": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", - "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true }, "emoji-regex": { @@ -1164,15 +1133,6 @@ } } }, - "strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", - "dev": true, - "requires": { - "ansi-regex": "^4.1.0" - } - }, "strip-json-comments": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", @@ -1338,9 +1298,9 @@ } }, "ws": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.1.tgz", - "integrity": "sha512-pTsP8UAfhy3sk1lSk/O/s4tjD0CRwvMnzvwr4OKGX7ZvqZtUyx4KIJB5JWbkykPoc55tixMGgTNoh3k4FkNGFQ==" + "version": "7.5.6", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.6.tgz", + "integrity": "sha512-6GLgCqo2cy2A2rjCNFlxQS6ZljG/coZfZXclldI8FB/1G3CCI36Zd8xy2HrFVACi8tfk5XrgLQEk+P0Tnz9UcA==" }, "xdg-basedir": { "version": "4.0.0", diff --git a/package.json b/package.json index f1ac7cc..d5e7d32 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ }, "dependencies": { "express": "^4.17.1", - "ws": "^7.4.1" + "ws": "^7.5.6" }, "devDependencies": { "nodemon": "^2.0.6" diff --git a/server.mjs b/server.mjs index 02a05b5..4733f09 100644 --- a/server.mjs +++ b/server.mjs @@ -2,61 +2,134 @@ import * as path from "path"; import express from "express"; import WebSocket from "ws"; -const port = process.env.PORT || 5000; +const port = process.env.PORT || 3000; const apiKeys = new Set([ - "4a83051d-aad4-483e-8fc8-693273d15dc7", - "c08c9038-693d-4669-98cd-9f0dd5ef06bf", - "4b1545c4-4a70-4727-9ea1-152ed4c84ae2", - "4a226908-aa3e-4a34-a57d-1f3d1f6cba84", + "4a83051d-aad4-483e-8fc8-693273d15dc7", + "c08c9038-693d-4669-98cd-9f0dd5ef06bf", + "4b1545c4-4a70-4727-9ea1-152ed4c84ae2", + "4a226908-aa3e-4a34-a57d-1f3d1f6cba84", ]); const colors = [ - "#140c1c", - "#442434", - "#30346d", - "#4e4a4e", - "#854c30", - "#346524", - "#d04648", - "#757161", - "#597dce", - "#d27d2c", - "#8595a1", - "#6daa2c", - "#d2aa99", - "#6dc2ca", - "#dad45e", - "#deeed6", + "#140c1c", + "#442434", + "#30346d", + "#4e4a4e", + "#854c30", + "#346524", + "#d04648", + "#757161", + "#597dce", + "#d27d2c", + "#8595a1", + "#6daa2c", + "#d2aa99", + "#6dc2ca", + "#dad45e", + "#deeed6", ]; +const timeoutLength = 30; +let timeouts = {}; + +for (let key of apiKeys) { + //console.log(key) + timeouts[key] = new Date(); +} +console.log(timeouts); + const size = 256; -// place(x, y) := place[x + y * size] const place = Array(size * size).fill(null); + for (const [colorIndex, colorValue] of colors.entries()) { - for (let dx = 0; dx < size; dx++) { - place[dx + colorIndex * size] = colorValue; - } + for (let dx = 0; dx < size; dx++) { + place[dx + colorIndex * size] = colorValue; + } } const app = express(); app.use(express.static(path.join(process.cwd(), "client"))); +app.get("/api/getColors", (req, res) => { + console.log(colors); + res.json(colors); +}); + app.get("/*", (_, res) => { - res.send("Place(holder)"); + res.send("Place(holder)"); }); -const server = app.listen(port); +const server = app.listen(port, () => console.log(`App listening on port ${port}`)); const wss = new WebSocket.Server({ - noServer: true, + noServer: true, }); +let keyMap = new WeakMap(); + server.on("upgrade", (req, socket, head) => { - const url = new URL(req.url, req.headers.origin); - console.log(url); - wss.handleUpgrade(req, socket, head, (ws) => { - wss.emit("connection", ws, req); - }); + const url = new URL(req.url, req.headers.origin); + console.log(url); + const apiKey = url.searchParams.get('apiKey'); + + wss.handleUpgrade(req, socket, head, (ws) => { + if (!apiKeys.has(apiKey)) { + socket.destroy(new Error("Invalid API key!")); + return; + } + keyMap.set(ws, apiKey); + wss.emit("connection", ws, req); + }); }); + +function insertIntoPlace(ws, payload, apiKey) { + const date = new Date(); + + let [x, y, color] = [payload.x, payload.y, payload.color]; + + if (x >= 0 && x <= 256 && + y >= 0 && y <= 256 && + colors.includes(color)) { + + if (date > timeouts[apiKey]) { + timeouts[apiKey] = new Date(date.valueOf() + timeoutLength * 1000); + + place[x + size * y] = color; + + wss.clients.forEach((ws) => { + if (ws.readyState === WebSocket.OPEN) { + sendField(ws); + } + }); + } + sendField(ws, "timeout", timeouts[apiKey].toISOString()); + } +} + +function sendField(ws, type = "place", payload = { place }) { + const result = { + type, + payload, + } + ws.send(JSON.stringify(result)); +} + +wss.on('connection', function connection(ws) { + const apiKey = keyMap.get(ws); + + ws.on('message', function message(message) { + // я ловлю + const data = JSON.parse(message); + console.log('received: %s', data); + + switch (data['type']) { + case ("click"): { + insertIntoPlace(ws, data['payload'], apiKey); + }; + }; + }); + sendField(ws, "timeout", timeouts[apiKey].toISOString()); + sendField(ws); +}); \ No newline at end of file