diff --git a/bun.lockb b/bun.lockb new file mode 100755 index 0000000..bf0fa41 Binary files /dev/null and b/bun.lockb differ diff --git a/config/channels.yml b/config/channels.yml index 15cdb33..cbf3088 100644 --- a/config/channels.yml +++ b/config/channels.yml @@ -6,9 +6,20 @@ lobbySettings: lobby: true chat: true crownsolo: false + color: "#eeeeee" + color2: "#888888" + visible: true + +defaultSettings: + chat: true + crownsolo: false + color: "#480505" + color2: "#000000" + visible: true lobbyRegexes: - - "^lobby\\d\\d$" - - "^test/.*$" + - "^lobby[1-9]?[1-9]?$" + - "^test/.+$" lobbyBackdoor: "lolwutsecretlobbybackdoor" +fullChannel: "test/awkward" diff --git a/package.json b/package.json index 2dfb353..9c1cba1 100644 --- a/package.json +++ b/package.json @@ -4,9 +4,9 @@ "description": "Hri7566's MPP Server", "main": "out/index.js", "scripts": { - "start": "node .", - "build": "npx tsc", - "dev": "pnpm build && pnpm start" + "start": "bun .", + "build": "bun build ./src/index.ts --outdir=out", + "dev": "bun run src/index.ts" }, "keywords": [], "author": "Hri7566", @@ -14,13 +14,14 @@ "dependencies": { "@prisma/client": "5.2.0", "@t3-oss/env-core": "^0.6.1", + "bun": "^1.0.0", + "bun-types": "^1.0.1", "date-holidays": "^3.21.5", "dotenv": "^8.6.0", "events": "^3.3.0", "fancy-text-converter": "^1.0.9", "keccak": "^2.1.0", "mppclone-client": "^1.1.3", - "uWebSockets.js": "uNetworking/uWebSockets.js#v20.31.0", "unique-names-generator": "^4.7.1", "yaml": "^2.3.2", "zod": "^3.22.2" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml deleted file mode 100644 index 43a5f97..0000000 --- a/pnpm-lock.yaml +++ /dev/null @@ -1,292 +0,0 @@ -lockfileVersion: '6.0' - -settings: - autoInstallPeers: true - excludeLinksFromLockfile: false - -dependencies: - '@prisma/client': - specifier: 5.2.0 - version: 5.2.0(prisma@5.2.0) - '@t3-oss/env-core': - specifier: ^0.6.1 - version: 0.6.1(typescript@5.2.2)(zod@3.22.2) - date-holidays: - specifier: ^3.21.5 - version: 3.21.5 - dotenv: - specifier: ^8.6.0 - version: 8.6.0 - events: - specifier: ^3.3.0 - version: 3.3.0 - fancy-text-converter: - specifier: ^1.0.9 - version: 1.0.9 - keccak: - specifier: ^2.1.0 - version: 2.1.0 - mppclone-client: - specifier: ^1.1.3 - version: 1.1.3 - uWebSockets.js: - specifier: github:uNetworking/uWebSockets.js#v20.31.0 - version: github.com/uNetworking/uWebSockets.js/809b99d2d7d12e2cbf89b7135041e9b41ff84084 - unique-names-generator: - specifier: ^4.7.1 - version: 4.7.1 - yaml: - specifier: ^2.3.2 - version: 2.3.2 - zod: - specifier: ^3.22.2 - version: 3.22.2 - -devDependencies: - '@types/node': - specifier: ^20.5.9 - version: 20.5.9 - prisma: - specifier: ^5.2.0 - version: 5.2.0 - typescript: - specifier: ^5.2.2 - version: 5.2.2 - -packages: - - /@prisma/client@5.2.0(prisma@5.2.0): - resolution: {integrity: sha512-AiTjJwR4J5Rh6Z/9ZKrBBLel3/5DzUNntMohOy7yObVnVoTNVFi2kvpLZlFuKO50d7yDspOtW6XBpiAd0BVXbQ==} - engines: {node: '>=16.13'} - requiresBuild: true - peerDependencies: - prisma: '*' - peerDependenciesMeta: - prisma: - optional: true - dependencies: - '@prisma/engines-version': 5.2.0-25.2804dc98259d2ea960602aca6b8e7fdc03c1758f - prisma: 5.2.0 - dev: false - - /@prisma/engines-version@5.2.0-25.2804dc98259d2ea960602aca6b8e7fdc03c1758f: - resolution: {integrity: sha512-jsnKT5JIDIE01lAeCj2ghY9IwxkedhKNvxQeoyLs6dr4ZXynetD0vTy7u6wMJt8vVPv8I5DPy/I4CFaoXAgbtg==} - dev: false - - /@prisma/engines@5.2.0: - resolution: {integrity: sha512-dT7FOLUCdZmq+AunLqB1Iz+ZH/IIS1Fz2THmKZQ6aFONrQD/BQ5ecJ7g2wGS2OgyUFf4OaLam6/bxmgdOBDqig==} - requiresBuild: true - - /@t3-oss/env-core@0.6.1(typescript@5.2.2)(zod@3.22.2): - resolution: {integrity: sha512-KQD7qEDJtkWIWWmTVjNvk0wnHpkvAQ6CRbUxbWMFNG/fiosBQDQvtRpBNu6USxBscJCoC4z6y7P9MN52/mLOzw==} - peerDependencies: - typescript: '>=4.7.2' - zod: ^3.0.0 - dependencies: - typescript: 5.2.2 - zod: 3.22.2 - dev: false - - /@types/node@20.5.9: - resolution: {integrity: sha512-PcGNd//40kHAS3sTlzKB9C9XL4K0sTup8nbG5lC14kzEteTNuAFh9u5nA0o5TWnSG2r/JNPRXFVcHJIIeRlmqQ==} - dev: true - - /argparse@2.0.1: - resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} - dev: false - - /astronomia@4.1.1: - resolution: {integrity: sha512-TcJD9lUC5eAo0/Ji7rnQauX/yQbi0yZWM+JsNr77W3OA5fsrgvuFgubLMFwfw4VlZ29cu9dG/yfJbfvuTSftjg==} - engines: {node: '>=12.0.0'} - dev: false - - /bindings@1.5.0: - resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==} - dependencies: - file-uri-to-path: 1.0.0 - dev: false - - /caldate@2.0.5: - resolution: {integrity: sha512-JndhrUuDuE975KUhFqJaVR1OQkCHZqpOrJur/CFXEIEhWhBMjxO85cRSK8q4FW+B+yyPq6GYua2u4KvNzTcq0w==} - engines: {node: '>=12.0.0'} - dependencies: - moment-timezone: 0.5.43 - dev: false - - /date-bengali-revised@2.0.2: - resolution: {integrity: sha512-q9iDru4+TSA9k4zfm0CFHJj6nBsxP7AYgWC/qodK/i7oOIlj5K2z5IcQDtESfs/Qwqt/xJYaP86tkazd/vRptg==} - engines: {node: '>=12.0.0'} - dev: false - - /date-chinese@2.1.4: - resolution: {integrity: sha512-WY+6+Qw92ZGWFvGtStmNQHEYpNa87b8IAQ5T8VKt4wqrn24lBXyyBnWI5jAIyy7h/KVwJZ06bD8l/b7yss82Ww==} - engines: {node: '>=12.0.0'} - dependencies: - astronomia: 4.1.1 - dev: false - - /date-easter@1.0.2: - resolution: {integrity: sha512-mpC1izx7lUSLYl4B88V2W57eNB4xS2ic+ahxK2AYUsaBTsCeHzT6K5ymUKzL9YPFf/GlygFqpiD4/NO1hxDsLw==} - engines: {node: '>=12.0.0'} - dev: false - - /date-holidays-parser@3.4.4: - resolution: {integrity: sha512-R5aO4oT8H51ZKdvApqHrqYEiNBrqT6tRj2PFXNcZfqMI4nxY7KKKly0ZsmquR5gY+x9ldKR8SAMdozzIInaoXg==} - engines: {node: '>=12.0.0'} - dependencies: - astronomia: 4.1.1 - caldate: 2.0.5 - date-bengali-revised: 2.0.2 - date-chinese: 2.1.4 - date-easter: 1.0.2 - deepmerge: 4.3.1 - jalaali-js: 1.2.6 - moment-timezone: 0.5.43 - dev: false - - /date-holidays@3.21.5: - resolution: {integrity: sha512-5X/UK7FunfIiM/q7CwepNfzh1XkkukdZNfTPyKlD5kx01MQzJ9DqKyTcFNzlQJ+HgpAxqUqSs3+F8mwV9bzo/w==} - engines: {node: '>=12.0.0'} - hasBin: true - dependencies: - date-holidays-parser: 3.4.4 - js-yaml: 4.1.0 - lodash.omit: 4.5.0 - lodash.pick: 4.4.0 - prepin: 1.0.3 - dev: false - - /deepmerge@4.3.1: - resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} - engines: {node: '>=0.10.0'} - dev: false - - /dotenv@8.6.0: - resolution: {integrity: sha512-IrPdXQsk2BbzvCBGBOTmmSH5SodmqZNt4ERAZDmW4CT+tL8VtvinqywuANaFu4bOMWki16nqf0e4oC0QIaDr/g==} - engines: {node: '>=10'} - dev: false - - /events@3.3.0: - resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} - engines: {node: '>=0.8.x'} - dev: false - - /fancy-text-converter@1.0.9: - resolution: {integrity: sha512-tFUAWpEfZOYhdsjILVu7c0PL9Ud9pTQmonm/2mdvFC7WcEHIYi9NYS5irJYFdBJDIRSqi64XV+IhHPc/ngxtyw==} - dev: false - - /file-uri-to-path@1.0.0: - resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==} - dev: false - - /inherits@2.0.4: - resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} - dev: false - - /jalaali-js@1.2.6: - resolution: {integrity: sha512-io974va+Qyu+UfuVX3UIAgJlxLhAMx9Y8VMfh+IG00Js7hXQo1qNQuwSiSa0xxco0SVgx5HWNkaiCcV+aZ8WPw==} - dev: false - - /js-yaml@4.1.0: - resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} - hasBin: true - dependencies: - argparse: 2.0.1 - dev: false - - /keccak@2.1.0: - resolution: {integrity: sha512-m1wbJRTo+gWbctZWay9i26v5fFnYkOn7D5PCxJ3fZUGUEb49dE1Pm4BREUYCt/aoO6di7jeoGmhvqN9Nzylm3Q==} - engines: {node: '>=5.12.0'} - requiresBuild: true - dependencies: - bindings: 1.5.0 - inherits: 2.0.4 - nan: 2.17.0 - safe-buffer: 5.2.1 - dev: false - - /lodash.omit@4.5.0: - resolution: {integrity: sha512-XeqSp49hNGmlkj2EJlfrQFIzQ6lXdNro9sddtQzcJY8QaoC2GO0DT7xaIokHeyM+mIT0mPMlPvkYzg2xCuHdZg==} - dev: false - - /lodash.pick@4.4.0: - resolution: {integrity: sha512-hXt6Ul/5yWjfklSGvLQl8vM//l3FtyHZeuelpzK6mm99pNvN9yTDruNZPEJZD1oWrqo+izBmB7oUfWgcCX7s4Q==} - dev: false - - /moment-timezone@0.5.43: - resolution: {integrity: sha512-72j3aNyuIsDxdF1i7CEgV2FfxM1r6aaqJyLB2vwb33mXYyoyLly+F1zbWqhA3/bVIoJ4szlUoMbUnVdid32NUQ==} - dependencies: - moment: 2.29.4 - dev: false - - /moment@2.29.4: - resolution: {integrity: sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==} - dev: false - - /mppclone-client@1.1.3: - resolution: {integrity: sha512-5DSkQmZOj823/BPwi6CQa4UWkoAX7itfNxf6L26NJS/qj9AljuKoqnIZxhtSKdak75qZd5Jgx+zD1aXflRNxHg==} - dependencies: - ws: 8.14.0 - transitivePeerDependencies: - - bufferutil - - utf-8-validate - dev: false - - /nan@2.17.0: - resolution: {integrity: sha512-2ZTgtl0nJsO0KQCjEpxcIr5D+Yv90plTitZt9JBfQvVJDS5seMl3FOvsh3+9CoYWXf/1l5OaZzzF6nDm4cagaQ==} - dev: false - - /prepin@1.0.3: - resolution: {integrity: sha512-0XL2hreherEEvUy0fiaGEfN/ioXFV+JpImqIzQjxk6iBg4jQ2ARKqvC4+BmRD8w/pnpD+lbxvh0Ub+z7yBEjvA==} - hasBin: true - dev: false - - /prisma@5.2.0: - resolution: {integrity: sha512-FfFlpjVCkZwrqxDnP4smlNYSH1so+CbfjgdpioFzGGqlQAEm6VHAYSzV7jJgC3ebtY9dNOhDMS2+4/1DDSM7bQ==} - engines: {node: '>=16.13'} - hasBin: true - requiresBuild: true - dependencies: - '@prisma/engines': 5.2.0 - - /safe-buffer@5.2.1: - resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} - dev: false - - /typescript@5.2.2: - resolution: {integrity: sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==} - engines: {node: '>=14.17'} - hasBin: true - - /unique-names-generator@4.7.1: - resolution: {integrity: sha512-lMx9dX+KRmG8sq6gulYYpKWZc9RlGsgBR6aoO8Qsm3qvkSJ+3rAymr+TnV8EDMrIrwuFJ4kruzMWM/OpYzPoow==} - engines: {node: '>=8'} - dev: false - - /ws@8.14.0: - resolution: {integrity: sha512-WR0RJE9Ehsio6U4TuM+LmunEsjQ5ncHlw4sn9ihD6RoJKZrVyH9FWV3dmnwu8B2aNib1OvG2X6adUCyFpQyWcg==} - engines: {node: '>=10.0.0'} - peerDependencies: - bufferutil: ^4.0.1 - utf-8-validate: '>=5.0.2' - peerDependenciesMeta: - bufferutil: - optional: true - utf-8-validate: - optional: true - dev: false - - /yaml@2.3.2: - resolution: {integrity: sha512-N/lyzTPaJasoDmfV7YTrYCI0G/3ivm/9wdG0aHuheKowWQwGTsK0Eoiw6utmzAnI6pkJa0DUVygvp3spqqEKXg==} - engines: {node: '>= 14'} - dev: false - - /zod@3.22.2: - resolution: {integrity: sha512-wvWkphh5WQsJbVk1tbx1l1Ly4yg+XecD+Mq280uBGt9wa5BKSWf4Mhp6GmrkPixhMxmabYY7RbzlwVP32pbGCg==} - dev: false - - github.com/uNetworking/uWebSockets.js/809b99d2d7d12e2cbf89b7135041e9b41ff84084: - resolution: {tarball: https://codeload.github.com/uNetworking/uWebSockets.js/tar.gz/809b99d2d7d12e2cbf89b7135041e9b41ff84084} - name: uWebSockets.js - version: 20.31.0 - dev: false diff --git a/src/channel/Channel.ts b/src/channel/Channel.ts index a8ccb48..a36f398 100644 --- a/src/channel/Channel.ts +++ b/src/channel/Channel.ts @@ -1,11 +1,218 @@ -// TODO Load channel config file +import { Logger } from "../util/Logger"; +import { loadConfig } from "../util/config"; +import { + ChannelSettingValue, + ChannelSettings, + Participant +} from "../util/types"; +import { Socket } from "../ws/Socket"; +import { app, findSocketByPartID } from "../ws/server"; +import { validateChannelSettings } from "./settings"; + +interface ChannelConfig { + forceLoad: string[]; + lobbySettings: Partial; + defaultSettings: Partial; + lobbyRegexes: string[]; + lobbyBackdoor: string; + fullChannel: string; +} + +export const config = loadConfig("config/channels.yml", { + forceLoad: ["lobby", "test/awkward"], + lobbySettings: { + lobby: true, + chat: true, + crownsolo: false, + visible: true + }, + defaultSettings: { + chat: true, + crownsolo: false, + color: "#480505", + color2: "#000000", + visible: true + }, + // TODO Test this regex + lobbyRegexes: ["^lobby[1-9]?[1-9]?$", "^test/.+$"], + lobbyBackdoor: "lolwutsecretlobbybackdoor", + fullChannel: "test/awkward" +}); + +export const channelList = new Array(); export class Channel { - constructor(private _id: string) {} + private settings: Partial = config.defaultSettings; + private ppl = new Array(); - getID() { + public logger: Logger; + + // TODO Add the crown + + constructor( + private _id: string, + set: Partial = config.defaultSettings + ) { + this.logger = new Logger("Channel - " + _id); + // Verify default settings just in case + this.changeSettings(this.settings, true); + + if (this.isLobby()) { + set = config.lobbySettings; + } + + this.changeSettings(set); + } + + public getID() { return this._id; } - isLobby() {} + public isLobby() { + for (const reg of config.lobbyRegexes) { + let exp = new RegExp(reg, "g"); + + if (this.getID().match(exp)) { + return true; + } + } + + return false; + } + + public changeSettings( + set: Partial, + admin: boolean = false + ) { + if (!admin) { + if (set.lobby) set.lobby = undefined; + if (set.owner_id) set.owner_id = undefined; + } + + // Verify settings + const validSettings = validateChannelSettings(set); + + for (const key of Object.keys(validSettings)) { + // Setting is valid? + if ((validSettings as Record)[key]) { + // Change setting + (this.settings as Record)[key] = ( + set as Record + )[key]; + } + } + } + + public join(socket: Socket) { + const part = socket.getParticipant(); + + // Unknown side-effects, but for type safety... + if (!part) return; + + let hasChangedChannel = false; + + this.logger.debug("Has user?", this.hasUser(part)); + + // Is user in this channel? + if (this.hasUser(part)) { + // Alreay in channel, disconnect old + + const oldSocket = findSocketByPartID(part.id); + + if (oldSocket) { + oldSocket.destroy(); + } + + // Add to channel + this.ppl.push(part); + hasChangedChannel = true; + } else { + // Are we full? + if (!this.isFull()) { + // Add to channel + this.ppl.push(part); + hasChangedChannel = true; + } else { + // Put us in full channel + socket.setChannel(config.fullChannel); + } + } + + if (hasChangedChannel) { + // Is user in any channel that isn't this one? + for (const ch of channelList) { + if (ch == this) continue; + if (ch.hasUser(part)) { + ch.leave(socket); + } + } + } + + this.logger.debug("Participant list:", this.ppl); + + // Send our data back + socket.sendArray([ + { + m: "ch", + ch: this.getInfo(), + p: part.id, + ppl: this.getParticipantList() + } + ]); + + // TODO Broadcast channel update + } + + public leave(socket: Socket) { + this.logger.debug("Leave called"); + const part = socket.getParticipant(); + + // Same as above... + if (!part) return; + + if (this.hasUser(part)) { + this.ppl.splice(this.ppl.indexOf(part), 1); + } + // TODO Broadcast channel update + } + + public isFull() { + // TODO Use limit setting + + if (this.isLobby() && this.ppl.length >= 20) { + return true; + } + + return false; + } + + public getInfo() { + return { + _id: this.getID(), + id: this.getID(), + count: this.ppl.length, + settings: this.settings + }; + } + + public getParticipantList() { + return this.ppl; + } + + public hasUser(part: Participant) { + const foundPart = this.ppl.find(p => p._id == part._id); + return !!foundPart; + } +} + +// Forceloader +let hasFullChannel = false; + +for (const id of config.forceLoad) { + channelList.push(new Channel(id)); + if (id == config.fullChannel) hasFullChannel = true; +} + +if (!hasFullChannel) { + channelList.push(new Channel(config.fullChannel)); } diff --git a/src/index.ts b/src/index.ts index 7e0587a..d902f96 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,10 +1,6 @@ import env from "./util/env"; -import { app } from "./ws/server"; +// import { app } from "./ws/server"; +import "./ws/server"; import { Logger } from "./util/Logger"; const logger = new Logger("Main"); - -// No IPv6 (yet) -app.listen("0.0.0.0", env.PORT, () => { - logger.info("Listening on :" + env.PORT); -}); diff --git a/src/util/types.d.ts b/src/util/types.d.ts index 67c8c22..bdd8cc6 100644 --- a/src/util/types.d.ts +++ b/src/util/types.d.ts @@ -100,9 +100,10 @@ declare interface ChannelInfo { id: string; _id: string; crown?: Crown; - settings: ChannelSettings; + settings: Partial; } +// Events copied from Hri7566/mppclone-client typedefs declare interface ServerEvents { a: { m: "a"; @@ -246,6 +247,7 @@ declare interface ClientEvents { m: "ch"; p: string; ch: ChannelInfo; + ppl: Participant[]; }; custom: { @@ -284,6 +286,7 @@ declare interface ClientEvents { }; notification: { + m: "notification"; duration?: number; class?: string; id?: string; diff --git a/src/ws/Socket.ts b/src/ws/Socket.ts index 5d5f40d..2185d4c 100644 --- a/src/ws/Socket.ts +++ b/src/ws/Socket.ts @@ -1,4 +1,3 @@ -import { WebSocket } from "uWebSockets.js"; import { createColor, createID, createUserID } from "../util/id"; import { decoder, encoder } from "../util/helpers"; import EventEmitter from "events"; @@ -8,6 +7,8 @@ import { createUser, readUser } from "../data/user"; import { eventGroups } from "./events"; import { loadConfig } from "../util/config"; import { Gateway } from "./Gateway"; +import { Channel, channelList } from "../channel/Channel"; +import { ServerWebSocket } from "bun"; interface UsersConfig { defaultName: string; @@ -31,17 +32,17 @@ export class Socket extends EventEmitter { public desiredChannel: { _id: string | undefined; - set: Partial; + set: Partial | undefined; } = { _id: undefined, set: {} }; - constructor(private ws: WebSocket) { - super(); - this.ip = decoder.decode(this.ws.getRemoteAddressAsText()); + public currentChannelID: string | undefined; - // Participant ID + constructor(private ws: ServerWebSocket) { + super(); + this.ip = ws.remoteAddress; // Participant ID this.id = createID(); // User ID @@ -61,9 +62,40 @@ export class Socket extends EventEmitter { return this._id; } - public setChannel(_id: string, set: Partial) { + public getParticipantID() { + return this.id; + } + + public setChannel(_id: string, set?: Partial) { + if (this.isDestroyed()) return; + this.desiredChannel._id = _id; this.desiredChannel.set = set; + + let channel; + try { + for (const ch of channelList.values()) { + if (ch.getID() == this.desiredChannel._id) { + channel = ch; + break; + } + } + } catch (err) {} + + // Does channel exist? + if (channel) { + // Exists, join normally + channel.join(this); + } else { + // Doesn't exist, join with crown + channel = new Channel( + this.desiredChannel._id, + this.desiredChannel.set + ); + + channel.join(this); + // TODO Give the crown upon joining + } } private bindEventListeners() { @@ -80,7 +112,8 @@ export class Socket extends EventEmitter { public sendArray( arr: ClientEvents[EventID][] ) { - this.ws.send(encoder.encode(JSON.stringify(arr))); + if (this.isDestroyed()) return; + this.ws.send(JSON.stringify(arr)); } private async loadUser() { @@ -142,7 +175,27 @@ export class Socket extends EventEmitter { } } - public getParticipantID() { - return this.id; + private destroyed = false; + + public destroy() { + // Socket was closed or should be closed, clear data + + // Simulate closure + try { + this.ws.close(); + } catch (err) {} + + if (this.currentChannelID) { + const foundCh = channelList.find( + ch => ch.getID() == this.currentChannelID + ); + if (foundCh) foundCh.leave(this); + } + + this.destroyed = true; + } + + public isDestroyed() { + return this.destroyed == true; } } diff --git a/src/ws/events.inc.ts b/src/ws/events.inc.ts index 0ff6c3c..26c35a1 100644 --- a/src/ws/events.inc.ts +++ b/src/ws/events.inc.ts @@ -1,2 +1,3 @@ -import "./events/user"; -import "./events/admin"; +// Bun hoists import, but not require? +require("./events/user"); +require("./events/admin"); diff --git a/src/ws/events/user/handlers/ch.ts b/src/ws/events/user/handlers/ch.ts new file mode 100644 index 0000000..303bd1b --- /dev/null +++ b/src/ws/events/user/handlers/ch.ts @@ -0,0 +1,11 @@ +import { ServerEventListener } from "../../../../util/types"; +import { eventGroups } from "../../../events"; + +export const ch: ServerEventListener<"ch"> = { + id: "ch", + callback: (msg, socket) => { + // Switch channel + if (!msg._id) return; + socket.setChannel(msg._id, msg.set); + } +}; diff --git a/src/ws/events/user/handlers/hi.ts b/src/ws/events/user/handlers/hi.ts index 8cdab68..8c18cc1 100644 --- a/src/ws/events/user/handlers/hi.ts +++ b/src/ws/events/user/handlers/hi.ts @@ -12,7 +12,7 @@ export const hi: ServerEventListener<"hi"> = { _id: socket.getUserID(), name: "Anonymous", color: "#777", - id: socket.getParticipantID() + id: socket.getUserID() }; } diff --git a/src/ws/events/user/index.ts b/src/ws/events/user/index.ts index 795b762..0220091 100644 --- a/src/ws/events/user/index.ts +++ b/src/ws/events/user/index.ts @@ -4,8 +4,10 @@ export const EVENTGROUP_USER = new EventGroup("user"); import { hi } from "./handlers/hi"; import { devices } from "./handlers/devices"; +import { ch } from "./handlers/ch"; EVENTGROUP_USER.add(hi); EVENTGROUP_USER.add(devices); +EVENTGROUP_USER.add(ch); eventGroups.push(EVENTGROUP_USER); diff --git a/src/ws/message.ts b/src/ws/message.ts index f9931a2..841a96b 100644 --- a/src/ws/message.ts +++ b/src/ws/message.ts @@ -1,4 +1,3 @@ -import { WebSocket } from "uWebSockets.js"; import { Logger } from "../util/Logger"; import { Socket } from "./Socket"; import { hasOwn } from "../util/helpers"; diff --git a/src/ws/server.ts b/src/ws/server.ts index 4a9aebb..4f495b9 100644 --- a/src/ws/server.ts +++ b/src/ws/server.ts @@ -1,97 +1,165 @@ -import { - App, - DEDICATED_COMPRESSOR_8KB, - HttpRequest, - HttpResponse, - WebSocket -} from "uWebSockets.js"; +// import { +// App, +// DEDICATED_COMPRESSOR_8KB, +// HttpRequest, +// HttpResponse, +// WebSocket +// } from "uWebSockets.js"; import { Logger } from "../util/Logger"; import { createUserID } from "../util/id"; -import { readFileSync, lstatSync } from "fs"; -import { join } from "path/posix"; +import fs from "fs"; +// import { join } from "path"; +import path from "path"; import { handleMessage } from "./message"; import { decoder } from "../util/helpers"; import { Socket } from "./Socket"; +import { serve, file } from "bun"; +import env from "../util/env"; const logger = new Logger("WebSocket Server"); -export const app = App() - .get("/*", async (res, req) => { - const url = req.getUrl(); - const ip = decoder.decode(res.getRemoteAddressAsText()); - // logger.debug(`${req.getMethod()} ${url} ${ip}`); - // res.writeStatus(`200 OK`).end("HI!"); - const file = join("./public/", url); +const usersByPartID = new Map(); - // TODO Cleaner file serving - try { - const stats = lstatSync(file); +export function findSocketByPartID(id: string) { + for (const key of usersByPartID.keys()) { + if (key == id) return usersByPartID.get(key); + } +} - let data; - if (!stats.isDirectory()) { - data = readFileSync(file); - } +// Original uWebSockets code +// export const app = App() +// .get("/*", async (res, req) => { +// const url = req.getUrl(); +// const ip = decoder.decode(res.getRemoteAddressAsText()); +// // logger.debug(`${req.getMethod()} ${url} ${ip}`); +// // res.writeStatus(`200 OK`).end("HI!"); +// const file = join("./public/", url); - // logger.debug(filename); +// // TODO Cleaner file serving +// try { +// const stats = lstatSync(file); - if (!data) { - const index = readFileSync("./public/index.html"); +// let data; +// if (!stats.isDirectory()) { +// data = readFileSync(file); +// } - if (!index) { - return void res - .writeStatus(`404 Not Found`) - .end("uh oh :("); +// // logger.debug(filename); + +// if (!data) { +// const index = readFileSync("./public/index.html"); + +// if (!index) { +// return void res +// .writeStatus(`404 Not Found`) +// .end("uh oh :("); +// } else { +// return void res.writeStatus(`200 OK`).end(index); +// } +// } + +// res.writeStatus(`200 OK`).end(data); +// } catch (err) { +// logger.warn("Unable to serve file at", file); +// logger.error(err); +// const index = readFileSync("./public/index.html"); + +// if (!index) { +// return void res.writeStatus(`404 Not Found`).end("uh oh :("); +// } else { +// return void res.writeStatus(`200 OK`).end(index); +// } +// } +// }) +// .ws("/*", { +// idleTimeout: 25, +// maxBackpressure: 1024, +// maxPayloadLength: 8192, +// compression: DEDICATED_COMPRESSOR_8KB, + +// open: ((ws: WebSocket & { socket: Socket }) => { +// ws.socket = new Socket(ws); +// // logger.debug("Connection at " + ws.socket.getIP()); + +// usersByPartID.set(ws.socket.getParticipantID(), ws.socket); +// }) as (ws: WebSocket) => void, + +// message: (( +// ws: WebSocket & { socket: Socket }, +// message, +// isBinary +// ) => { +// const msg = decoder.decode(message); +// handleMessage(ws.socket, msg); +// }) as ( +// ws: WebSocket, +// message: ArrayBuffer, +// isBinary: boolean +// ) => void, + +// close: (( +// ws: WebSocket & { socket: Socket }, +// code: number, +// message: ArrayBuffer +// ) => { +// logger.debug("Close called"); +// ws.socket.destroy(); +// usersByPartID.delete(ws.socket.getParticipantID()); +// }) as ( +// ws: WebSocket, +// code: number, +// message: ArrayBuffer +// ) => void +// }); + +export const app = Bun.serve({ + port: env.PORT, + fetch: (req, server) => { + if (server.upgrade(req)) { + return; + } else { + const url = new URL(req.url).pathname; + // const ip = decoder.decode(res.getRemoteAddressAsText()); + // logger.debug(`${req.getMethod()} ${url} ${ip}`); + // res.writeStatus(`200 OK`).end("HI!"); + const file = path.join("./public/", url); + + try { + if (fs.lstatSync(file).isFile()) { + const data = Bun.file(file); + + if (data) { + return new Response(data); + } else { + return new Response(Bun.file("./public/index.html")); + } } else { - return void res.writeStatus(`200 OK`).end(index); + return new Response(Bun.file("./public/index.html")); } - } - - res.writeStatus(`200 OK`).end(data); - } catch (err) { - logger.warn("Unable to serve file at", file); - logger.error(err); - const index = readFileSync("./public/index.html"); - - if (!index) { - return void res.writeStatus(`404 Not Found`).end("uh oh :("); - } else { - return void res.writeStatus(`200 OK`).end(index); + } catch (err) { + return new Response(Bun.file("./public/index.html")); } } - }) - .ws("/*", { - idleTimeout: 180, - maxBackpressure: 1024, - maxPayloadLength: 8192, - compression: DEDICATED_COMPRESSOR_8KB, + }, + websocket: { + open: ws => { + const socket = new Socket(ws); + (ws as unknown as any).socket = socket; + logger.debug("Connection at " + socket.getIP()); - open: ((ws: WebSocket & { socket: Socket }) => { - ws.socket = new Socket(ws); - // logger.debug("Connection at " + ws.socket.getIP()); - }) as (ws: WebSocket) => void, + usersByPartID.set(socket.getParticipantID(), socket); + }, - message: (( - ws: WebSocket & { socket: Socket }, - message, - isBinary - ) => { - const msg = decoder.decode(message); - handleMessage(ws.socket, msg); - }) as ( - ws: WebSocket, - message: ArrayBuffer, - isBinary: boolean - ) => void, + message: (ws, message) => { + const msg = message.toString(); + handleMessage((ws as unknown as any).socket, msg); + }, - close: (( - ws: WebSocket & { socket: Socket }, - code: number, - message: ArrayBuffer - ) => { - // TODO handle close event - }) as ( - ws: WebSocket, - code: number, - message: ArrayBuffer - ) => void - }); + close: (ws, code, message) => { + logger.debug("Close called"); + const socket = (ws as unknown as any).socket as Socket; + socket.destroy(); + usersByPartID.delete(socket.getParticipantID()); + } + } +}); diff --git a/tsconfig.json b/tsconfig.json index ab7739d..8afd498 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -11,8 +11,10 @@ // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ /* Language and Environment */ - "target": "es2016" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, + // "target": "es2016" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, + "target": "ESNext", // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ + "lib": ["ESNext"], // "jsx": "preserve", /* Specify what JSX code is generated. */ // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ @@ -23,20 +25,25 @@ // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ + "moduleDetection": "force", /* Modules */ - "module": "commonjs" /* Specify what module code is generated. */, + // "module": "commonjs" /* Specify what module code is generated. */, + "module": "ESNext", // "rootDir": "./", /* Specify the root folder within your source files. */ "rootDir": "./src/", // "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */ + "moduleResolution": "Bundler", // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ // "types": [], /* Specify type package names to be included without being referenced in a source file. */ + "types": ["bun-types"], // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */ + "allowImportingTsExtensions": true, // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ @@ -60,6 +67,7 @@ "outDir": "./out/", // "removeComments": true, /* Disable emitting comments. */ // "noEmit": true, /* Disable emitting files from a compilation. */ + "noEmit": true, // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */