├── .github
└── FUNDING.yml
├── .gitignore
├── LICENSE
├── README.md
├── SECURITY.md
├── aftman.toml
├── default.project.json
├── package.json
├── selene.toml
├── server
├── index.js
├── package-lock.json
└── package.json
├── sourcemap.json
├── src
├── Maid.lua
├── Signal.lua
├── Signature.lua
├── init.lua
└── reader
│ ├── Dictionary.lua
│ ├── Errors.lua
│ └── init.lua
└── wally.toml
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | github: [sturmgeisty]
2 | custom: https://paypal.me/ajihaede1
3 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.md
2 | .github/
3 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 RoSocket
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 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | ---
13 |
14 |
15 | API documentation •
16 | Examples •
17 | Self-hosting
18 |
19 |
20 | ## Installation
21 |
22 | Wally:
23 |
24 | ```toml
25 | [dependencies]
26 | Socket = "RoSocket/rosocket@1.0.1"
27 | ```
28 |
29 | Roblox Model:
30 | Click [here](https://create.roblox.com/store/asset/17132752732/RoSocket) or
31 | Download from [Releases](https://github.com/RoSocket/rosocket/releases)
32 | (we recommend you get the marketplace one which will always be the latest one)
33 |
34 | ## API
35 |
36 | For RoSocket to work correctly, you must enable in-game HTTP requests & self-host the server!
37 | If you want faster replies, then navigate to the **reader module** > **SOCKET_SERVER_UPDATES**, set it to 0.10 or less, minimum is 0.02 before ratelimits start to appear.
38 |
39 | **Functions:**
40 |
41 | ```Lua
42 | function RoSocket.Connect(socket: string): (any?) -> (table)
43 | ```
44 |
45 | **Keys:**
46 |
47 | ```Lua
48 | string RoSocket.Version
49 | ```
50 |
51 | **Socket:**
52 | ```Lua
53 | function socket.Disconnect(...: any): (boolean) -> (boolean)
54 | function socket.Send(msg: string?): (boolean) -> (boolean)
55 | RBXScriptSignal socket.OnDisconnect()
56 | RBXScriptSignal socket.OnMessageReceived(msg: string?)
57 | RBXScriptSignal socket.OnErrorReceived(err: string?)
58 | string socket.UUID -- Universal Unique Identifier
59 | string socket.Socket -- Socket link (e.g: wss://hello.com)
60 | string socket.binaryType -- buffer (doesn't modify way of requests)
61 | string socket.readyState -- OPEN/CLOSED
62 | object socket.Messages
63 | object socket.Errors
64 | ```
65 |
66 | ## Simple Example
67 |
68 | ```Lua
69 | local RoSocket = require(script.RoSocket)
70 |
71 | -- Http service requests should be enabled for this to work, and a correct server should be set in the Reader module.
72 | local Success, Socket = pcall(RoSocket.Connect, "wss://echo.websocket.org")
73 | if Success ~= false then
74 | print(`Socket's Universal Unique Identifier: {Socket.UUID}`) -- ...
75 | print(`Socket's URL is: {Socket.Socket}`) -- wss://echo.websocket.org
76 | print(`Socket's state is: {Socket.readyState}`) -- OPEN
77 | print(`Socket's binary Type is: {Socket.binaryType}`) -- buffer (read-only)
78 | print(`Socket's amount of messages: {#Socket.Messages}`)
79 | print(`Socket's amount of errors: {#Socket.Errors}`)
80 | Socket.OnDisconnect:Connect(function(...: any?)
81 | warn(`Socket {Socket.Socket} was disconnected!`)
82 | end)
83 | Socket.OnMessageReceived:Connect(function(msg: string?)
84 | warn(`Message from {Socket.Socket}: {tostring(msg)}`)
85 | end)
86 | Socket.OnErrorReceived:Connect(function(err: string?)
87 | error(`Error from {Socket.Socket}: {tostring(err)}`)
88 | end)
89 | local Suc1 = Socket.Send("Hello World!") -- First message
90 | print(`Socket first message {Suc1 == true and "has been sent successfully!" or "has failed to send!"}`)
91 | local Suc2 = Socket.Send("Hello World!") -- Repeated message
92 | print(`Socket repeated message {Suc2 == true and "has been sent successfully!" or "has failed to send!"}`)
93 | local Suc3 = Socket.Send("Goodbye World!") -- Second message
94 | print(`Socket second message {Suc3 == true and "has been sent successfully!" or "has failed to send!"}`)
95 | Socket.Disconnect()
96 | Socket.Send("Hello World!") -- Throws a warning in the output saying you can't send messages to a disconnected socket
97 | print(`Socket's state is: {Socket.readyState}`) -- CLOSED
98 | print(`Socket's amount of messages: {#Socket.Messages}`)
99 | else
100 | warn("Failed to connect to websocket!")
101 | end
102 | ```
103 |
104 | ## Self-hosting the server
105 | 1. Download the entire RoSocket repository by clicking on **Code** > **Download ZIP**
106 | 2. Extract the ZIP file, and cut the "server" folder. Paste the contents of the folder inside a directory of your choice/folder.
107 | 3. Open a shell and run:
108 | ```npm
109 | npm install express ws
110 | ```
111 | 4. You're good to go! Optional is to change the default port & default host.
112 |
113 | [(Back to top)](#installation)
114 |
--------------------------------------------------------------------------------
/SECURITY.md:
--------------------------------------------------------------------------------
1 | # Security Policy
2 |
3 | ## Supported Versions
4 |
5 | | Version | Supported |
6 | | ------- | ------------------ |
7 | | 1.0.x | :white_check_mark: |
8 |
9 | ## Reporting a Vulnerability / Issue
10 |
11 | Open a new issue.
12 | Arrival time for a response from our team is around 1 up to 2 days, or even a few hours. Additionally, contact us on discord for faster responses!
13 |
--------------------------------------------------------------------------------
/aftman.toml:
--------------------------------------------------------------------------------
1 | # This file lists tools managed by Aftman, a cross-platform toolchain manager.
2 | # For more information, see https://github.com/LPGhatguy/aftman
3 |
4 | # To add a new tool, add an entry to this table.
5 | [tools]
6 | rojo = "rojo-rbx/rojo@7.3.0"
7 | run-in-roblox = "rojo-rbx/run-in-roblox@0.3.0"
8 | wally = "upliftgames/wally@0.3.2"
9 | selene = "Kampfkarren/selene@0.25.0"
10 | stylua = "JohnnyMorganz/stylua@0.18.1"
11 |
--------------------------------------------------------------------------------
/default.project.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "RoSocket",
3 | "tree": {
4 | "$path": "src"
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "RoSocket/rosocket",
3 | "version": "1.0.1",
4 | "license": "MIT",
5 | "repository": {
6 | "type": "git",
7 | "url": "https://github.com/RoSocket/rosocket.git"
8 | },
9 | "contributors": [
10 | "sturmgeisty"
11 | ],
12 | "bugs": {
13 | "url": "https://github.com/RoSocket/rosocket/issues"
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/selene.toml:
--------------------------------------------------------------------------------
1 | std = "roblox"
2 |
--------------------------------------------------------------------------------
/server/index.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const WebSocket = require('ws');
3 |
4 | const app = express();
5 | app.use(express.json());
6 | const port = 6214;
7 |
8 | const connections = {};
9 |
10 | function isValidWebSocketURL(url) {
11 | return url.startsWith("wss://");
12 | }
13 |
14 | function generateUUID() {
15 | return Math.random().toString(36).substring(2, 10);
16 | } // TODO: remove this and use uuid npm package
17 |
18 | function handleWebSocketConnection(UUID, socket) {
19 | socket.on('message', (message) => {
20 | if (connections[UUID]) {
21 | const messageString = message.toString();
22 | connections[UUID].messages.push({
23 | id: generateUUID(),
24 | message: messageString,
25 | step: connections[UUID].messages.length + 1
26 | });
27 | }
28 | });
29 | socket.on('error', (error) => {
30 | console.error(`WebSocket error for UUID: ${UUID}`, error);
31 | if (connections[UUID]) {
32 | connections[UUID].errors.push({
33 | id: generateUUID(),
34 | message: error,
35 | step: connections[UUID].errors.length + 1
36 | });
37 | }
38 | });
39 | }
40 |
41 |
42 | app.post('/connect', async (req, res) => {
43 | const { Socket } = req.body;
44 | if (!Socket) {
45 | return res.status(400).json({ success: false, error: "No WebSocket URL provided!" });
46 | }
47 | if (!isValidWebSocketURL(Socket)) {
48 | return res.status(400).json({ success: false, error: "Invalid WebSocket URL" });
49 | }
50 |
51 | const UUID = generateUUID();
52 | const socket = new WebSocket(Socket);
53 |
54 | try {
55 | await new Promise((resolve, reject) => {
56 | socket.on('error', (error) => {
57 | console.error(`WebSocket error for UUID: ${UUID}`, error);
58 | reject(error);
59 | });
60 | socket.on('open', () => {
61 | resolve();
62 | });
63 | });
64 | } catch (error) {
65 | return res.status(500).json({ success: false, error: "WebSocket connection error" });
66 | }
67 |
68 | connections[UUID] = { socket: socket, messages: [] };
69 | handleWebSocketConnection(UUID, socket);
70 |
71 | res.json({ UUID, Socket, success: true });
72 | });
73 |
74 |
75 | app.post('/disconnect', (req, res) => {
76 | const { UUID } = req.body;
77 | if (!UUID) {
78 | return res.status(400).json({ success: false, error: "No UUID provided!" });
79 | }
80 | if (!connections[UUID]) {
81 | return res.status(404).json({ success: false, error: "UUID not found" });
82 | }
83 |
84 | connections[UUID].socket.close();
85 | delete connections[UUID];
86 |
87 | res.json({ UUID, success: true });
88 | });
89 |
90 | app.post('/send', (req, res) => {
91 | const { UUID, Message } = req.body;
92 | if (!UUID || !Message) {
93 | return res.status(400).json({ success: false, error: "UUID or Message not provided!" });
94 | }
95 | if (!connections[UUID] || connections[UUID].socket.readyState !== WebSocket.OPEN) {
96 | return res.status(404).json({ success: false, error: "Invalid UUID or WebSocket connection closed" });
97 | }
98 |
99 | connections[UUID].socket.send(Message);
100 | res.json(true);
101 | });
102 |
103 | app.post('/get', (req, res) => {
104 | const { UUID } = req.body;
105 | if (!UUID) {
106 | return res.status(400).json({ success: false, error: "No UUID provided!" });
107 | }
108 | if (!connections[UUID]) {
109 | return res.status(404).json({ success: false, error: "Invalid UUID" });
110 | }
111 |
112 | res.json(connections[UUID].messages);
113 | });
114 | app.post('/errors', (req, res) => {
115 | const { UUID } = req.body;
116 | if (!UUID) {
117 | return res.status(400).json({ success: false, error: "No UUID provided!" });
118 | }
119 | if (!connections[UUID]) {
120 | return res.status(404).json({ success: false, error: "Invalid UUID" });
121 | }
122 |
123 | res.json(connections[UUID].errors);
124 | });
125 |
126 | app.listen(port, () => {
127 | console.log(`Server running on port ${port}`);
128 | });
129 |
--------------------------------------------------------------------------------
/server/package-lock.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "rosocket-server",
3 | "version": "1.0.0",
4 | "lockfileVersion": 3,
5 | "requires": true,
6 | "packages": {
7 | "": {
8 | "name": "rosocket-server",
9 | "version": "1.0.0",
10 | "license": "MIT",
11 | "dependencies": {
12 | "express": "^4.19.2",
13 | "ws": "^8.17.1"
14 | }
15 | },
16 | "node_modules/accepts": {
17 | "version": "1.3.8",
18 | "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
19 | "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
20 | "dependencies": {
21 | "mime-types": "~2.1.34",
22 | "negotiator": "0.6.3"
23 | },
24 | "engines": {
25 | "node": ">= 0.6"
26 | }
27 | },
28 | "node_modules/array-flatten": {
29 | "version": "1.1.1",
30 | "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
31 | "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg=="
32 | },
33 | "node_modules/body-parser": {
34 | "version": "1.20.2",
35 | "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz",
36 | "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==",
37 | "dependencies": {
38 | "bytes": "3.1.2",
39 | "content-type": "~1.0.5",
40 | "debug": "2.6.9",
41 | "depd": "2.0.0",
42 | "destroy": "1.2.0",
43 | "http-errors": "2.0.0",
44 | "iconv-lite": "0.4.24",
45 | "on-finished": "2.4.1",
46 | "qs": "6.11.0",
47 | "raw-body": "2.5.2",
48 | "type-is": "~1.6.18",
49 | "unpipe": "1.0.0"
50 | },
51 | "engines": {
52 | "node": ">= 0.8",
53 | "npm": "1.2.8000 || >= 1.4.16"
54 | }
55 | },
56 | "node_modules/bytes": {
57 | "version": "3.1.2",
58 | "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
59 | "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
60 | "engines": {
61 | "node": ">= 0.8"
62 | }
63 | },
64 | "node_modules/call-bind": {
65 | "version": "1.0.7",
66 | "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz",
67 | "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==",
68 | "dependencies": {
69 | "es-define-property": "^1.0.0",
70 | "es-errors": "^1.3.0",
71 | "function-bind": "^1.1.2",
72 | "get-intrinsic": "^1.2.4",
73 | "set-function-length": "^1.2.1"
74 | },
75 | "engines": {
76 | "node": ">= 0.4"
77 | },
78 | "funding": {
79 | "url": "https://github.com/sponsors/ljharb"
80 | }
81 | },
82 | "node_modules/content-disposition": {
83 | "version": "0.5.4",
84 | "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
85 | "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
86 | "dependencies": {
87 | "safe-buffer": "5.2.1"
88 | },
89 | "engines": {
90 | "node": ">= 0.6"
91 | }
92 | },
93 | "node_modules/content-type": {
94 | "version": "1.0.5",
95 | "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
96 | "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
97 | "engines": {
98 | "node": ">= 0.6"
99 | }
100 | },
101 | "node_modules/cookie": {
102 | "version": "0.6.0",
103 | "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz",
104 | "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==",
105 | "engines": {
106 | "node": ">= 0.6"
107 | }
108 | },
109 | "node_modules/cookie-signature": {
110 | "version": "1.0.6",
111 | "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
112 | "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ=="
113 | },
114 | "node_modules/debug": {
115 | "version": "2.6.9",
116 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
117 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
118 | "dependencies": {
119 | "ms": "2.0.0"
120 | }
121 | },
122 | "node_modules/define-data-property": {
123 | "version": "1.1.4",
124 | "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
125 | "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
126 | "dependencies": {
127 | "es-define-property": "^1.0.0",
128 | "es-errors": "^1.3.0",
129 | "gopd": "^1.0.1"
130 | },
131 | "engines": {
132 | "node": ">= 0.4"
133 | },
134 | "funding": {
135 | "url": "https://github.com/sponsors/ljharb"
136 | }
137 | },
138 | "node_modules/depd": {
139 | "version": "2.0.0",
140 | "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
141 | "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
142 | "engines": {
143 | "node": ">= 0.8"
144 | }
145 | },
146 | "node_modules/destroy": {
147 | "version": "1.2.0",
148 | "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
149 | "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
150 | "engines": {
151 | "node": ">= 0.8",
152 | "npm": "1.2.8000 || >= 1.4.16"
153 | }
154 | },
155 | "node_modules/ee-first": {
156 | "version": "1.1.1",
157 | "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
158 | "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="
159 | },
160 | "node_modules/encodeurl": {
161 | "version": "1.0.2",
162 | "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
163 | "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==",
164 | "engines": {
165 | "node": ">= 0.8"
166 | }
167 | },
168 | "node_modules/es-define-property": {
169 | "version": "1.0.0",
170 | "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz",
171 | "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==",
172 | "dependencies": {
173 | "get-intrinsic": "^1.2.4"
174 | },
175 | "engines": {
176 | "node": ">= 0.4"
177 | }
178 | },
179 | "node_modules/es-errors": {
180 | "version": "1.3.0",
181 | "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
182 | "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
183 | "engines": {
184 | "node": ">= 0.4"
185 | }
186 | },
187 | "node_modules/escape-html": {
188 | "version": "1.0.3",
189 | "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
190 | "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="
191 | },
192 | "node_modules/etag": {
193 | "version": "1.8.1",
194 | "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
195 | "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
196 | "engines": {
197 | "node": ">= 0.6"
198 | }
199 | },
200 | "node_modules/express": {
201 | "version": "4.19.2",
202 | "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz",
203 | "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==",
204 | "dependencies": {
205 | "accepts": "~1.3.8",
206 | "array-flatten": "1.1.1",
207 | "body-parser": "1.20.2",
208 | "content-disposition": "0.5.4",
209 | "content-type": "~1.0.4",
210 | "cookie": "0.6.0",
211 | "cookie-signature": "1.0.6",
212 | "debug": "2.6.9",
213 | "depd": "2.0.0",
214 | "encodeurl": "~1.0.2",
215 | "escape-html": "~1.0.3",
216 | "etag": "~1.8.1",
217 | "finalhandler": "1.2.0",
218 | "fresh": "0.5.2",
219 | "http-errors": "2.0.0",
220 | "merge-descriptors": "1.0.1",
221 | "methods": "~1.1.2",
222 | "on-finished": "2.4.1",
223 | "parseurl": "~1.3.3",
224 | "path-to-regexp": "0.1.7",
225 | "proxy-addr": "~2.0.7",
226 | "qs": "6.11.0",
227 | "range-parser": "~1.2.1",
228 | "safe-buffer": "5.2.1",
229 | "send": "0.18.0",
230 | "serve-static": "1.15.0",
231 | "setprototypeof": "1.2.0",
232 | "statuses": "2.0.1",
233 | "type-is": "~1.6.18",
234 | "utils-merge": "1.0.1",
235 | "vary": "~1.1.2"
236 | },
237 | "engines": {
238 | "node": ">= 0.10.0"
239 | }
240 | },
241 | "node_modules/finalhandler": {
242 | "version": "1.2.0",
243 | "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz",
244 | "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==",
245 | "dependencies": {
246 | "debug": "2.6.9",
247 | "encodeurl": "~1.0.2",
248 | "escape-html": "~1.0.3",
249 | "on-finished": "2.4.1",
250 | "parseurl": "~1.3.3",
251 | "statuses": "2.0.1",
252 | "unpipe": "~1.0.0"
253 | },
254 | "engines": {
255 | "node": ">= 0.8"
256 | }
257 | },
258 | "node_modules/forwarded": {
259 | "version": "0.2.0",
260 | "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
261 | "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
262 | "engines": {
263 | "node": ">= 0.6"
264 | }
265 | },
266 | "node_modules/fresh": {
267 | "version": "0.5.2",
268 | "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
269 | "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
270 | "engines": {
271 | "node": ">= 0.6"
272 | }
273 | },
274 | "node_modules/function-bind": {
275 | "version": "1.1.2",
276 | "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
277 | "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
278 | "funding": {
279 | "url": "https://github.com/sponsors/ljharb"
280 | }
281 | },
282 | "node_modules/get-intrinsic": {
283 | "version": "1.2.4",
284 | "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz",
285 | "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==",
286 | "dependencies": {
287 | "es-errors": "^1.3.0",
288 | "function-bind": "^1.1.2",
289 | "has-proto": "^1.0.1",
290 | "has-symbols": "^1.0.3",
291 | "hasown": "^2.0.0"
292 | },
293 | "engines": {
294 | "node": ">= 0.4"
295 | },
296 | "funding": {
297 | "url": "https://github.com/sponsors/ljharb"
298 | }
299 | },
300 | "node_modules/gopd": {
301 | "version": "1.0.1",
302 | "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz",
303 | "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==",
304 | "dependencies": {
305 | "get-intrinsic": "^1.1.3"
306 | },
307 | "funding": {
308 | "url": "https://github.com/sponsors/ljharb"
309 | }
310 | },
311 | "node_modules/has-property-descriptors": {
312 | "version": "1.0.2",
313 | "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
314 | "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
315 | "dependencies": {
316 | "es-define-property": "^1.0.0"
317 | },
318 | "funding": {
319 | "url": "https://github.com/sponsors/ljharb"
320 | }
321 | },
322 | "node_modules/has-proto": {
323 | "version": "1.0.3",
324 | "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz",
325 | "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==",
326 | "engines": {
327 | "node": ">= 0.4"
328 | },
329 | "funding": {
330 | "url": "https://github.com/sponsors/ljharb"
331 | }
332 | },
333 | "node_modules/has-symbols": {
334 | "version": "1.0.3",
335 | "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
336 | "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==",
337 | "engines": {
338 | "node": ">= 0.4"
339 | },
340 | "funding": {
341 | "url": "https://github.com/sponsors/ljharb"
342 | }
343 | },
344 | "node_modules/hasown": {
345 | "version": "2.0.2",
346 | "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
347 | "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
348 | "dependencies": {
349 | "function-bind": "^1.1.2"
350 | },
351 | "engines": {
352 | "node": ">= 0.4"
353 | }
354 | },
355 | "node_modules/http-errors": {
356 | "version": "2.0.0",
357 | "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
358 | "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
359 | "dependencies": {
360 | "depd": "2.0.0",
361 | "inherits": "2.0.4",
362 | "setprototypeof": "1.2.0",
363 | "statuses": "2.0.1",
364 | "toidentifier": "1.0.1"
365 | },
366 | "engines": {
367 | "node": ">= 0.8"
368 | }
369 | },
370 | "node_modules/iconv-lite": {
371 | "version": "0.4.24",
372 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
373 | "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
374 | "dependencies": {
375 | "safer-buffer": ">= 2.1.2 < 3"
376 | },
377 | "engines": {
378 | "node": ">=0.10.0"
379 | }
380 | },
381 | "node_modules/inherits": {
382 | "version": "2.0.4",
383 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
384 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
385 | },
386 | "node_modules/ipaddr.js": {
387 | "version": "1.9.1",
388 | "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
389 | "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
390 | "engines": {
391 | "node": ">= 0.10"
392 | }
393 | },
394 | "node_modules/media-typer": {
395 | "version": "0.3.0",
396 | "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
397 | "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
398 | "engines": {
399 | "node": ">= 0.6"
400 | }
401 | },
402 | "node_modules/merge-descriptors": {
403 | "version": "1.0.1",
404 | "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
405 | "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w=="
406 | },
407 | "node_modules/methods": {
408 | "version": "1.1.2",
409 | "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
410 | "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
411 | "engines": {
412 | "node": ">= 0.6"
413 | }
414 | },
415 | "node_modules/mime": {
416 | "version": "1.6.0",
417 | "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
418 | "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
419 | "bin": {
420 | "mime": "cli.js"
421 | },
422 | "engines": {
423 | "node": ">=4"
424 | }
425 | },
426 | "node_modules/mime-db": {
427 | "version": "1.52.0",
428 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
429 | "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
430 | "engines": {
431 | "node": ">= 0.6"
432 | }
433 | },
434 | "node_modules/mime-types": {
435 | "version": "2.1.35",
436 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
437 | "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
438 | "dependencies": {
439 | "mime-db": "1.52.0"
440 | },
441 | "engines": {
442 | "node": ">= 0.6"
443 | }
444 | },
445 | "node_modules/ms": {
446 | "version": "2.0.0",
447 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
448 | "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
449 | },
450 | "node_modules/negotiator": {
451 | "version": "0.6.3",
452 | "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
453 | "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
454 | "engines": {
455 | "node": ">= 0.6"
456 | }
457 | },
458 | "node_modules/object-inspect": {
459 | "version": "1.13.1",
460 | "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz",
461 | "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==",
462 | "funding": {
463 | "url": "https://github.com/sponsors/ljharb"
464 | }
465 | },
466 | "node_modules/on-finished": {
467 | "version": "2.4.1",
468 | "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
469 | "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
470 | "dependencies": {
471 | "ee-first": "1.1.1"
472 | },
473 | "engines": {
474 | "node": ">= 0.8"
475 | }
476 | },
477 | "node_modules/parseurl": {
478 | "version": "1.3.3",
479 | "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
480 | "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
481 | "engines": {
482 | "node": ">= 0.8"
483 | }
484 | },
485 | "node_modules/path-to-regexp": {
486 | "version": "0.1.7",
487 | "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
488 | "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ=="
489 | },
490 | "node_modules/proxy-addr": {
491 | "version": "2.0.7",
492 | "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
493 | "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
494 | "dependencies": {
495 | "forwarded": "0.2.0",
496 | "ipaddr.js": "1.9.1"
497 | },
498 | "engines": {
499 | "node": ">= 0.10"
500 | }
501 | },
502 | "node_modules/qs": {
503 | "version": "6.11.0",
504 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz",
505 | "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==",
506 | "dependencies": {
507 | "side-channel": "^1.0.4"
508 | },
509 | "engines": {
510 | "node": ">=0.6"
511 | },
512 | "funding": {
513 | "url": "https://github.com/sponsors/ljharb"
514 | }
515 | },
516 | "node_modules/range-parser": {
517 | "version": "1.2.1",
518 | "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
519 | "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
520 | "engines": {
521 | "node": ">= 0.6"
522 | }
523 | },
524 | "node_modules/raw-body": {
525 | "version": "2.5.2",
526 | "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz",
527 | "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==",
528 | "dependencies": {
529 | "bytes": "3.1.2",
530 | "http-errors": "2.0.0",
531 | "iconv-lite": "0.4.24",
532 | "unpipe": "1.0.0"
533 | },
534 | "engines": {
535 | "node": ">= 0.8"
536 | }
537 | },
538 | "node_modules/safe-buffer": {
539 | "version": "5.2.1",
540 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
541 | "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
542 | "funding": [
543 | {
544 | "type": "github",
545 | "url": "https://github.com/sponsors/feross"
546 | },
547 | {
548 | "type": "patreon",
549 | "url": "https://www.patreon.com/feross"
550 | },
551 | {
552 | "type": "consulting",
553 | "url": "https://feross.org/support"
554 | }
555 | ]
556 | },
557 | "node_modules/safer-buffer": {
558 | "version": "2.1.2",
559 | "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
560 | "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
561 | },
562 | "node_modules/send": {
563 | "version": "0.18.0",
564 | "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz",
565 | "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==",
566 | "dependencies": {
567 | "debug": "2.6.9",
568 | "depd": "2.0.0",
569 | "destroy": "1.2.0",
570 | "encodeurl": "~1.0.2",
571 | "escape-html": "~1.0.3",
572 | "etag": "~1.8.1",
573 | "fresh": "0.5.2",
574 | "http-errors": "2.0.0",
575 | "mime": "1.6.0",
576 | "ms": "2.1.3",
577 | "on-finished": "2.4.1",
578 | "range-parser": "~1.2.1",
579 | "statuses": "2.0.1"
580 | },
581 | "engines": {
582 | "node": ">= 0.8.0"
583 | }
584 | },
585 | "node_modules/send/node_modules/ms": {
586 | "version": "2.1.3",
587 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
588 | "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
589 | },
590 | "node_modules/serve-static": {
591 | "version": "1.15.0",
592 | "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz",
593 | "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==",
594 | "dependencies": {
595 | "encodeurl": "~1.0.2",
596 | "escape-html": "~1.0.3",
597 | "parseurl": "~1.3.3",
598 | "send": "0.18.0"
599 | },
600 | "engines": {
601 | "node": ">= 0.8.0"
602 | }
603 | },
604 | "node_modules/set-function-length": {
605 | "version": "1.2.2",
606 | "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
607 | "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==",
608 | "dependencies": {
609 | "define-data-property": "^1.1.4",
610 | "es-errors": "^1.3.0",
611 | "function-bind": "^1.1.2",
612 | "get-intrinsic": "^1.2.4",
613 | "gopd": "^1.0.1",
614 | "has-property-descriptors": "^1.0.2"
615 | },
616 | "engines": {
617 | "node": ">= 0.4"
618 | }
619 | },
620 | "node_modules/setprototypeof": {
621 | "version": "1.2.0",
622 | "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
623 | "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="
624 | },
625 | "node_modules/side-channel": {
626 | "version": "1.0.6",
627 | "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz",
628 | "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==",
629 | "dependencies": {
630 | "call-bind": "^1.0.7",
631 | "es-errors": "^1.3.0",
632 | "get-intrinsic": "^1.2.4",
633 | "object-inspect": "^1.13.1"
634 | },
635 | "engines": {
636 | "node": ">= 0.4"
637 | },
638 | "funding": {
639 | "url": "https://github.com/sponsors/ljharb"
640 | }
641 | },
642 | "node_modules/statuses": {
643 | "version": "2.0.1",
644 | "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
645 | "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
646 | "engines": {
647 | "node": ">= 0.8"
648 | }
649 | },
650 | "node_modules/toidentifier": {
651 | "version": "1.0.1",
652 | "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
653 | "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
654 | "engines": {
655 | "node": ">=0.6"
656 | }
657 | },
658 | "node_modules/type-is": {
659 | "version": "1.6.18",
660 | "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
661 | "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
662 | "dependencies": {
663 | "media-typer": "0.3.0",
664 | "mime-types": "~2.1.24"
665 | },
666 | "engines": {
667 | "node": ">= 0.6"
668 | }
669 | },
670 | "node_modules/unpipe": {
671 | "version": "1.0.0",
672 | "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
673 | "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
674 | "engines": {
675 | "node": ">= 0.8"
676 | }
677 | },
678 | "node_modules/utils-merge": {
679 | "version": "1.0.1",
680 | "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
681 | "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
682 | "engines": {
683 | "node": ">= 0.4.0"
684 | }
685 | },
686 | "node_modules/vary": {
687 | "version": "1.1.2",
688 | "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
689 | "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
690 | "engines": {
691 | "node": ">= 0.8"
692 | }
693 | },
694 | "node_modules/ws": {
695 | "version": "8.17.1",
696 | "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
697 | "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==",
698 | "engines": {
699 | "node": ">=10.0.0"
700 | },
701 | "peerDependencies": {
702 | "bufferutil": "^4.0.1",
703 | "utf-8-validate": ">=5.0.2"
704 | },
705 | "peerDependenciesMeta": {
706 | "bufferutil": {
707 | "optional": true
708 | },
709 | "utf-8-validate": {
710 | "optional": true
711 | }
712 | }
713 | }
714 | }
715 | }
716 |
--------------------------------------------------------------------------------
/server/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "rosocket-server",
3 | "version": "1.0.0",
4 | "description": "Node backend server for RoSocket.",
5 | "main": "index.js",
6 | "scripts": {
7 | "test": "echo Hello World!"
8 | },
9 | "keywords": [
10 | "server",
11 | "node.js server",
12 | "rosocket"
13 | ],
14 | "author": "sturmgeisty",
15 | "license": "MIT",
16 | "dependencies": {
17 | "express": "^4.19.2",
18 | "ws": "^8.17.1"
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/sourcemap.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "RoSocket",
3 | "className": "ModuleScript",
4 | "filePaths": [
5 | "src\\init.lua",
6 | "default.project.json"
7 | ],
8 | "children": [
9 | {
10 | "name": "Reader",
11 | "className": "ModuleScript",
12 | "filePaths": [
13 | "src\\reader\\init.lua"
14 | ],
15 | "children": [
16 | {
17 | "name": "Dictionary",
18 | "className": "ModuleScript",
19 | "filePaths": [
20 | "src\\reader\\Dictionary.lua"
21 | ]
22 | },
23 | {
24 | "name": "Errors",
25 | "className": "ModuleScript",
26 | "filePaths": [
27 | "src\\reader\\Errors.lua"
28 | ]
29 | }
30 | ]
31 | },
32 | {
33 | "name": "Maid",
34 | "className": "ModuleScript",
35 | "filePaths": [
36 | "src\\Maid.lua"
37 | ]
38 | },
39 | {
40 | "name": "Signal",
41 | "className": "ModuleScript",
42 | "filePaths": [
43 | "src\\Signal.lua"
44 | ]
45 | },
46 | {
47 | "name": "Signature",
48 | "className": "ModuleScript",
49 | "filePaths": [
50 | "src\\Signature.lua"
51 | ]
52 | }
53 | ]
54 | }
--------------------------------------------------------------------------------
/src/Maid.lua:
--------------------------------------------------------------------------------
1 | --- Manages the cleaning of events and other things.
2 | -- Useful for encapsulating state and make deconstructors easy
3 | -- @classmod Maid
4 | -- @see Signal
5 |
6 | local Maid = {}
7 | Maid.ClassName = "Maid"
8 |
9 | --- Returns a new Maid object
10 | -- @constructor Maid.new()
11 | -- @treturn Maid
12 | function Maid.new()
13 | return setmetatable({
14 | _tasks = {}
15 | }, Maid)
16 | end
17 |
18 | function Maid.isMaid(value)
19 | return type(value) == "table" and value.ClassName == "Maid"
20 | end
21 |
22 | --- Returns Maid[key] if not part of Maid metatable
23 | -- @return Maid[key] value
24 | function Maid:__index(index)
25 | if Maid[index] then
26 | return Maid[index]
27 | else
28 | return self._tasks[index]
29 | end
30 | end
31 |
32 | --- Add a task to clean up. Tasks given to a maid will be cleaned when
33 | -- maid[index] is set to a different value.
34 | -- @usage
35 | -- Maid[key] = (function) Adds a task to perform
36 | -- Maid[key] = (event connection) Manages an event connection
37 | -- Maid[key] = (Maid) Maids can act as an event connection, allowing a Maid to have other maids to clean up.
38 | -- Maid[key] = (Object) Maids can cleanup objects with a `Destroy` method
39 | -- Maid[key] = nil Removes a named task. If the task is an event, it is disconnected. If it is an object,
40 | -- it is destroyed.
41 | function Maid:__newindex(index, newTask)
42 | if Maid[index] ~= nil then
43 | error(("'%s' is reserved"):format(tostring(index)), 2)
44 | end
45 |
46 | local tasks = self._tasks
47 | local oldTask = tasks[index]
48 |
49 | if oldTask == newTask then
50 | return
51 | end
52 |
53 | tasks[index] = newTask
54 |
55 | if oldTask then
56 | if type(oldTask) == "function" then
57 | oldTask()
58 | elseif typeof(oldTask) == "RBXScriptConnection" then
59 | oldTask:Disconnect()
60 | elseif oldTask.Destroy then
61 | oldTask:Destroy()
62 | end
63 | end
64 | end
65 |
66 | --- Same as indexing, but uses an incremented number as a key.
67 | -- @param task An item to clean
68 | -- @treturn number taskId
69 | function Maid:GiveTask(task)
70 | if not task then
71 | error("Task cannot be false or nil", 2)
72 | end
73 |
74 | local taskId = #self._tasks+1
75 | self[taskId] = task
76 |
77 | if type(task) == "table" and (not task.Destroy) then
78 | warn("[Maid.GiveTask] - Gave table task without .Destroy\n\n" .. debug.traceback())
79 | end
80 |
81 | return taskId
82 | end
83 |
84 | function Maid:GivePromise(promise)
85 | if not promise:IsPending() then
86 | return promise
87 | end
88 |
89 | local newPromise = promise.resolved(promise)
90 | local id = self:GiveTask(newPromise)
91 |
92 | -- Ensure GC
93 | newPromise:Finally(function()
94 | self[id] = nil
95 | end)
96 |
97 | return newPromise
98 | end
99 |
100 | --- Cleans up all tasks.
101 | -- @alias Destroy
102 | function Maid:DoCleaning()
103 | local tasks = self._tasks
104 |
105 | -- Disconnect all events first as we know this is safe
106 | for index, task in pairs(tasks) do
107 | if typeof(task) == "RBXScriptConnection" then
108 | tasks[index] = nil
109 | task:Disconnect()
110 | end
111 | end
112 |
113 | -- Clear out tasks table completely, even if clean up tasks add more tasks to the maid
114 | local index, task = next(tasks)
115 | while task ~= nil do
116 | tasks[index] = nil
117 | if type(task) == "function" then
118 | task()
119 | elseif typeof(task) == "RBXScriptConnection" then
120 | task:Disconnect()
121 | elseif task.Destroy then
122 | task:Destroy()
123 | end
124 | index, task = next(tasks)
125 | end
126 | end
127 |
128 | --- Alias for DoCleaning()
129 | -- @function Destroy
130 | Maid.Destroy = Maid.DoCleaning
131 |
132 | return Maid
--------------------------------------------------------------------------------
/src/Signal.lua:
--------------------------------------------------------------------------------
1 | --------------------------------------------------------------------------------
2 | -- Batched Yield-Safe Signal Implementation --
3 | -- This is a Signal class which has effectively identical behavior to a --
4 | -- normal RBXScriptSignal, with the only difference being a couple extra --
5 | -- stack frames at the bottom of the stack trace when an error is thrown. --
6 | -- This implementation caches runner coroutines, so the ability to yield in --
7 | -- the signal handlers comes at minimal extra cost over a naive signal --
8 | -- implementation that either always or never spawns a thread. --
9 | -- --
10 | -- API: --
11 | -- local Signal = require(THIS MODULE) --
12 | -- local sig = Signal.new() --
13 | -- local connection = sig:Connect(function(arg1, arg2, ...) ... end) --
14 | -- sig:Fire(arg1, arg2, ...) --
15 | -- connection:Disconnect() --
16 | -- sig:DisconnectAll() --
17 | -- local arg1, arg2, ... = sig:Wait() --
18 | -- --
19 | -- Licence: --
20 | -- Licenced under the MIT licence. --
21 | -- --
22 | -- Authors: --
23 | -- stravant - July 31st, 2021 - Created the file. --
24 | --------------------------------------------------------------------------------
25 |
26 | -- The currently idle thread to run the next handler on
27 | local freeRunnerThread = nil
28 |
29 | -- Function which acquires the currently idle handler runner thread, runs the
30 | -- function fn on it, and then releases the thread, returning it to being the
31 | -- currently idle one.
32 | -- If there was a currently idle runner thread already, that's okay, that old
33 | -- one will just get thrown and eventually GCed.
34 | local function acquireRunnerThreadAndCallEventHandler(fn, ...)
35 | local acquiredRunnerThread = freeRunnerThread
36 | freeRunnerThread = nil
37 | fn(...)
38 | -- The handler finished running, this runner thread is free again.
39 | freeRunnerThread = acquiredRunnerThread
40 | end
41 |
42 | -- Coroutine runner that we create coroutines of. The coroutine can be
43 | -- repeatedly resumed with functions to run followed by the argument to run
44 | -- them with.
45 | local function runEventHandlerInFreeThread(...)
46 | acquireRunnerThreadAndCallEventHandler(...)
47 | while true do
48 | acquireRunnerThreadAndCallEventHandler(coroutine.yield())
49 | end
50 | end
51 |
52 | -- Connection class
53 | local Connection = {}
54 | Connection.__index = Connection
55 |
56 | function Connection.new(signal, fn)
57 | return setmetatable({
58 | _connected = true,
59 | _signal = signal,
60 | _fn = fn,
61 | _next = false,
62 | }, Connection)
63 | end
64 |
65 | function Connection:Disconnect()
66 | assert(self._connected, "Can't disconnect a connection twice.", 2)
67 | self._connected = false
68 |
69 | -- Unhook the node, but DON'T clear it. That way any fire calls that are
70 | -- currently sitting on this node will be able to iterate forwards off of
71 | -- it, but any subsequent fire calls will not hit it, and it will be GCed
72 | -- when no more fire calls are sitting on it.
73 | if self._signal._handlerListHead == self then
74 | self._signal._handlerListHead = self._next
75 | else
76 | local prev = self._signal._handlerListHead
77 | while prev and prev._next ~= self do
78 | prev = prev._next
79 | end
80 | if prev then
81 | prev._next = self._next
82 | end
83 | end
84 | end
85 |
86 | -- Make Connection strict
87 | setmetatable(Connection, {
88 | __index = function(tb, key)
89 | error(("Attempt to get Connection::%s (not a valid member)"):format(tostring(key)), 2)
90 | end,
91 | __newindex = function(tb, key, value)
92 | error(("Attempt to set Connection::%s (not a valid member)"):format(tostring(key)), 2)
93 | end
94 | })
95 |
96 | -- Signal class
97 | local Signal = {}
98 | Signal.__index = Signal
99 |
100 | function Signal.new()
101 | return setmetatable({
102 | _handlerListHead = false,
103 | }, Signal)
104 | end
105 |
106 | function Signal:Connect(fn)
107 | local connection = Connection.new(self, fn)
108 | if self._handlerListHead then
109 | connection._next = self._handlerListHead
110 | self._handlerListHead = connection
111 | else
112 | self._handlerListHead = connection
113 | end
114 | return connection
115 | end
116 |
117 | -- Disconnect all handlers. Since we use a linked list it suffices to clear the
118 | -- reference to the head handler.
119 | function Signal:DisconnectAll()
120 | self._handlerListHead = false
121 | end
122 |
123 | -- Signal:Fire(...) implemented by running the handler functions on the
124 | -- coRunnerThread, and any time the resulting thread yielded without returning
125 | -- to us, that means that it yielded to the Roblox scheduler and has been taken
126 | -- over by Roblox scheduling, meaning we have to make a new coroutine runner.
127 | function Signal:Fire(...)
128 | local item = self._handlerListHead
129 | while item do
130 | if item._connected then
131 | if not freeRunnerThread then
132 | freeRunnerThread = coroutine.create(runEventHandlerInFreeThread)
133 | end
134 | task.spawn(freeRunnerThread, item._fn, ...)
135 | end
136 | item = item._next
137 | end
138 | end
139 |
140 | -- Implement Signal:Wait() in terms of a temporary connection using
141 | -- a Signal:Connect() which disconnects itself.
142 | function Signal:Wait()
143 | local waitingCoroutine = coroutine.running()
144 | local cn;
145 | cn = self:Connect(function(...)
146 | cn:Disconnect()
147 | task.spawn(waitingCoroutine, ...)
148 | end)
149 | return coroutine.yield()
150 | end
151 |
152 | -- Make signal strict
153 | setmetatable(Signal, {
154 | __index = function(tb, key)
155 | error(("Attempt to get Signal::%s (not a valid member)"):format(tostring(key)), 2)
156 | end,
157 | __newindex = function(tb, key, value)
158 | error(("Attempt to set Signal::%s (not a valid member)"):format(tostring(key)), 2)
159 | end
160 | })
161 |
162 | return Signal
--------------------------------------------------------------------------------
/src/Signature.lua:
--------------------------------------------------------------------------------
1 | --[[
2 | - RO SOCKET -
3 | Module responsible to return the signature for standard outputting.
4 | You can modify to your liking, such as: the roblox websocket -> Connection successfull!
5 |
6 | • Creator: @binarychunk
7 | ]]--
8 |
9 | return {
10 | Signature = "[RoSocket]",
11 | Splitter = "::"
12 | }
--------------------------------------------------------------------------------
/src/init.lua:
--------------------------------------------------------------------------------
1 | --[[
2 | - RO SOCKET -
3 | A blazing fast implementation of WebSockets in roblox, similar to the "ws" library in Node.
4 | Supports client and server implementation.
5 | Backend uses the "ws" library aswell, providing proper socket support, and is coded in Node.
6 |
7 | • Creator: @binarychunk
8 |
9 | - CHANGELOG -
10 |
11 | v1.0.0:
12 | Initial release
13 | v1.0.1:
14 | Improved code readability
15 | Improved code speed by removing some useless portions
16 | Made it send all variable arguments from inside the Send function to the Reader
17 | Added more intelli sense stuff
18 | Added custom error messages when trying to:
19 | - send messages to a disconnected socket
20 | - disconnect a already-disconnected socket
21 | ]]--
22 |
23 | local RoSocket = {}
24 | local Reader = require(script.Reader)
25 | local Errors = require(script.Reader.Errors)
26 | local Signal = require(script.Signal)
27 | local Maid = require(script.Maid)
28 |
29 | local HttpService = game:GetService("HttpService")
30 | local RunService = game:GetService("RunService")
31 | local Players = game:GetService("Players")
32 | local StarterGui = game:GetService("StarterGui")
33 |
34 | local SOCKET_SERVER_UPDATES = 0.10
35 |
36 | if RunService:IsServer() == false then
37 | error(Reader:FormatText(Errors.INVALID_REQUIREMENT_CONTEXT))
38 | end
39 | if not HttpService.HttpEnabled then
40 | error(Reader:FormatText(Errors.HTTP_SERVICE_DISABLED))
41 | end
42 |
43 | local MaidSocket = Maid.new()
44 | local Sockets = {}
45 | RoSocket.Version = "1.0.1"
46 | RoSocket.Connect = function(socket: string): (any?) -> (table)
47 | local validsocket = true
48 |
49 | if validsocket ~= false then
50 | local data = Reader:Connect(socket)
51 | if data.success ~= false then
52 | local dis = false
53 | local uuid = data.UUID
54 | local localmsgs = {}
55 | local localerrors = {}
56 | local tbl = {}
57 | tbl.readyState = dis
58 | coroutine.resume(coroutine.create(function()
59 | while tbl do
60 | tbl.readyState = dis and "CLOSED" or "OPEN"
61 | task.wait(0.05)
62 | end
63 | end))
64 | tbl.binaryType = "buffer"
65 | local OnDisconnect : RBXScriptSignal = Signal.new()
66 | tbl.OnDisconnect = OnDisconnect
67 | local OnMessageReceived : RBXScriptSignal = Signal.new()
68 | tbl.OnMessageReceived = OnMessageReceived
69 | local OnErrorReceived : RBXScriptSignal = Signal.new()
70 | tbl.OnErrorReceived = OnErrorReceived
71 |
72 | local elapsedTimer = Sockets[uuid] and Sockets[uuid].elapsedtimer or 0
73 |
74 | MaidSocket[uuid] = RunService.Heartbeat:Connect(function(deltaTime)
75 |
76 | if elapsedTimer >= SOCKET_SERVER_UPDATES then
77 | elapsedTimer = 0
78 | end
79 | elapsedTimer += deltaTime
80 | if elapsedTimer >= SOCKET_SERVER_UPDATES then
81 | if dis == false then
82 | -- messages
83 | local suc, Msgs = pcall(Reader.Get, Reader, uuid)
84 | if typeof(Msgs) == "table" then
85 | for _, msgobj in ipairs(Msgs) do
86 | local existsAlready = false
87 | for i,msg in ipairs(Sockets[uuid].msgs) do
88 | if msg.id == msgobj.id then
89 | existsAlready = true
90 | break
91 | end
92 | end
93 |
94 | if existsAlready == false then
95 | tbl.OnMessageReceived:Fire(msgobj.message)
96 | table.insert(Sockets[uuid].msgs, {
97 | id = msgobj.id,
98 | message = msgobj.message,
99 | })
100 | table.insert(localmsgs, {
101 | id = msgobj.id,
102 | message = msgobj.message,
103 | })
104 | end
105 | end
106 | end
107 | -- errors
108 | local suc, Msgs = pcall(Reader.GetErrors, Reader, uuid)
109 | if typeof(Msgs) == "table" then
110 | for _, msgobj in ipairs(Msgs) do
111 | local existsAlready = false
112 | for i,msg in ipairs(Sockets[uuid].errors) do
113 | if msg.id == msgobj.id then
114 | existsAlready = true
115 | break
116 | end
117 | end
118 |
119 | if existsAlready == false then
120 | tbl.OnErrorReceived:Fire(msgobj.message)
121 | table.insert(Sockets[uuid].errors, {
122 | id = msgobj.id,
123 | message = msgobj.message,
124 | })
125 | table.insert(localerrors, {
126 | id = msgobj.id,
127 | message = msgobj.message,
128 | })
129 | end
130 | end
131 | end
132 | else
133 |
134 | end
135 | end
136 | end)
137 |
138 | tbl.UUID = uuid
139 | tbl.Socket = data.Socket
140 | tbl.Disconnect = function(...)
141 | if dis == true then
142 | warn(Reader:FormatText("You cannot disconnect a disconnected socket!"))
143 | return false
144 | else
145 | local success = Reader:Disconnect(uuid)
146 | Sockets[uuid] = nil
147 | MaidSocket[uuid] = nil
148 | tbl.OnDisconnect:Fire()
149 | dis = true
150 | return true
151 | end
152 |
153 | end
154 | tbl.Send = function(...)
155 | if dis == false then
156 | local success = Reader:Send(uuid, ...)
157 | return success
158 | else
159 | warn(Reader:FormatText("You cannot send messages to a disconnected socket!"))
160 | return false
161 | end
162 | end
163 | tbl.Messages = localmsgs or {}
164 | tbl.Errors = localerrors or {}
165 |
166 | setmetatable(tbl, {
167 | __call = function(self, index, ...)
168 | return tbl[index](...)
169 | end,
170 | __metatable = "This is a protected metatable!"
171 | })
172 | Sockets[uuid] = {
173 | sockettbl = tbl,
174 | msgs = {},
175 | errors = {},
176 | elapsedtimer = 0
177 | }
178 |
179 | return tbl
180 | end
181 | else
182 | return {}
183 | end
184 | end
185 | setmetatable(RoSocket, {
186 | __call = function(self, ...)
187 | return RoSocket.Connect(...)
188 | end
189 | })
190 | table.freeze(RoSocket)
191 | -----------------------------------------------
192 | return RoSocket
193 |
--------------------------------------------------------------------------------
/src/reader/Dictionary.lua:
--------------------------------------------------------------------------------
1 | --[[
2 | - RO SOCKET -
3 | Module responsible for helping reader module with API endpoints.
4 |
5 | • Creator: @binarychunk
6 | ]]--
7 |
8 | return {
9 | Connection = "/connect",
10 | Disconnection = "/disconnect",
11 |
12 | Validation = "/validation",
13 | Send = "/send",
14 | Get = "/get",
15 | GetErrors = "/errors"
16 | }
--------------------------------------------------------------------------------
/src/reader/Errors.lua:
--------------------------------------------------------------------------------
1 | --[[
2 | - RO SOCKET -
3 | Module responsible for helping reader module to throw out errors.
4 |
5 | • Creator: @binarychunk
6 | ]]--
7 |
8 | return {
9 | INVALID_ARGUMENT_TYPE = "Argument \"%s\" expected to be type %s, instead got %s!",
10 |
11 | EMPTY_WSS_LINK_TO_VALIDATE = "wss link expected, received none. Regex pattern can't be executed.",
12 | EMPTY_TEXT_TO_FORMAT = "text expected to be formatted, received none. Text formatting can't operate further more.",
13 | EMPTY_ID_TO_VALIDATE = "id expected, received none. ID validation can't operate further more.",
14 |
15 | INVALID_WSS_LINK = "invalid wss link received. Try connecting with a proper link!",
16 |
17 | HTTP_SERVICE_DISABLED = "RoSocket must have HTTP enabled for it to operate correctly. To resolve this problem, navigate to the top left corner, select FILE ➡ Game Settings ➡ Security ➡ = {
18 | Success: boolean?,
19 | StatusCode: number?,
20 | StatusMessage: string?,
21 | Headers: {[string?]: string},
22 | Body: T
23 | }
24 | export type ConnectionData = {
25 | UUID: string?,
26 | Socket: string?,
27 | Success: boolean?
28 | }
29 | export type DisconnectionData = {
30 | UUID: string?,
31 | Socket: string?
32 | }
33 | -----------------------------------------------
34 | local SOCKET_SERVER_URL = "" --You can change this if you self host
35 | -----------------------------------------------
36 | local WSS_PATTERN = "^wss://[%w%.]"
37 | -----------------------------------------------
38 | function Reader:FormatText(text: string?, ...: any?): string
39 | assert(text, Errors.EMPTY_TEXT_TO_FORMAT)
40 | assert(typeof(text) == "string", string.format(Errors.INVALID_ARGUMENT_TYPE, "text", "string", typeof(text)))
41 |
42 | return tostring(`{Signature.Signature} {Signature.Splitter} {text}`)
43 | end
44 | assert(SOCKET_SERVER_URL ~= "", Reader:FormatText("Invalid backend URL. You can use heroku or replit to deploy the node backend!"))
45 | function Reader:ValidateWSSLink(link: string?, ...: any?): boolean
46 | assert(link, Errors.EMPTY_WSS_LINK_TO_VALIDATE)
47 | assert(typeof(link) == "string", string.format(Errors.INVALID_ARGUMENT_TYPE, "link", "string", typeof(link)))
48 |
49 | return string.match(link, WSS_PATTERN) and true or false
50 | end
51 |
52 | function Reader:Connect(socket: string?, ...: any?)
53 | local ValidLink = self:ValidateWSSLink(tostring(socket))
54 | if ValidLink == false then
55 | return error(self:FormatText(`Invalid socket link passed. Their format is: wss://hostname/path.`))
56 | end
57 | local Response : RequestResponse = HttpService:RequestAsync({
58 | Url = `{SOCKET_SERVER_URL}{Dictionary.Connection}`,
59 | Method = "POST",
60 | Headers = {
61 | ["Content-Type"] = "application/json",
62 | },
63 | Body = HttpService:JSONEncode({Socket = tostring(socket)})
64 | })
65 |
66 | if Response.Success == true then
67 | warn(self:FormatText(`Successfully connected!`))
68 | local DecodedSuccess, DecodedResult : ConnectionData = pcall(function()
69 | return HttpService:JSONDecode(Response.Body)
70 | end)
71 |
72 | if DecodedSuccess == true then
73 | return DecodedResult
74 | elseif DecodedSuccess == false then
75 | return error(self:FormatText(`Failed to decode response | Error {tostring(DecodedResult)}`))
76 | end
77 | elseif Response.Success == false then
78 | return error(self:FormatText(`Failed to connect to socket {tostring(socket)} | Status Code {tostring(Response.StatusCode)}`))
79 | end
80 | end
81 | function Reader:Disconnect(id: string?, ...: any?): boolean
82 | local ValidID = true
83 |
84 | local Response : RequestResponse = HttpService:RequestAsync({
85 | Url = `{SOCKET_SERVER_URL}{Dictionary.Disconnection}`,
86 | Method = "POST",
87 | Headers = {
88 | ["Content-Type"] = "application/json",
89 | },
90 | Body = HttpService:JSONEncode({UUID = tostring(id)})
91 | })
92 |
93 | if Response.Success == true then
94 | local DecodedSuccess, DecodedResult : DisconnectionData = pcall(function()
95 | return HttpService:JSONDecode(Response.Body)
96 | end)
97 |
98 | if DecodedSuccess == true then
99 | return DecodedResult
100 | elseif DecodedSuccess == false then
101 | return error(self:FormatText(`Failed to decode response | Error {tostring(DecodedResult)}`))
102 | end
103 | elseif Response.Success == false then
104 | return error(self:FormatText(`Failed to disconnect ID {tostring(id)} | Status Code {tostring(Response.StatusCode)}`))
105 | end
106 | end
107 | function Reader:Send(id: string?, message: string?, ...): boolean
108 | local Response : RequestResponse = HttpService:RequestAsync({
109 | Url = `{SOCKET_SERVER_URL}{Dictionary.Send}`,
110 | Method = "POST",
111 | Headers = {
112 | ["Content-Type"] = "application/json",
113 | },
114 | Body = HttpService:JSONEncode({UUID = tostring(id), Message = tostring(message)})
115 | })
116 |
117 | if Response.Success == true then
118 | local DecodedSuccess, DecodedResult = pcall(function()
119 | return HttpService:JSONDecode(Response.Body)
120 | end)
121 |
122 | if DecodedSuccess == true then
123 | return DecodedResult
124 | elseif DecodedSuccess == false then
125 | return error(self:FormatText(`Failed to decode response | Error {tostring(DecodedResult)}`))
126 | end
127 | elseif Response.Success == false then
128 | return error(self:FormatText(`Failed to send message! This socket is probably disconnected.`))
129 | end
130 | end
131 | function Reader:Get(id: string?, ...): any
132 | local Response : RequestResponse = HttpService:RequestAsync({
133 | Url = `{SOCKET_SERVER_URL}{Dictionary.Get}`,
134 | Method = "POST",
135 | Headers = {
136 | ["Content-Type"] = "application/json",
137 | },
138 | Body = HttpService:JSONEncode({UUID = tostring(id)})
139 | })
140 |
141 | if Response.Success == true then
142 | local DecodedSuccess, DecodedResult = pcall(function()
143 | return HttpService:JSONDecode(Response.Body)
144 | end)
145 |
146 | if DecodedSuccess == true then
147 | return DecodedResult
148 | elseif DecodedSuccess == false then
149 | return error(self:FormatText(`Failed to decode response | Error {tostring(DecodedResult)}`))
150 | end
151 | elseif Response.Success == false then
152 | return error(self:FormatText(`Failed to get messages! This socket is probably disconnected.`))
153 | end
154 | end
155 | function Reader:GetErrors(id: string?, ...): any
156 | local Response : RequestResponse = HttpService:RequestAsync({
157 | Url = `{SOCKET_SERVER_URL}{Dictionary.GetErrors}`,
158 | Method = "POST",
159 | Headers = {
160 | ["Content-Type"] = "application/json",
161 | },
162 | Body = HttpService:JSONEncode({UUID = tostring(id)})
163 | })
164 |
165 | if Response.Success == true then
166 | local DecodedSuccess, DecodedResult = pcall(function()
167 | return HttpService:JSONDecode(Response.Body)
168 | end)
169 |
170 | if DecodedSuccess == true then
171 | return DecodedResult
172 | elseif DecodedSuccess == false then
173 | return error(self:FormatText(`Failed to decode response | Error {tostring(DecodedResult)}`))
174 | end
175 | elseif Response.Success == false then
176 | return error(self:FormatText(`Failed to get errors! This socket is probably disconnected.`))
177 | end
178 | end
179 |
180 | return Reader
181 |
--------------------------------------------------------------------------------
/wally.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "RoSocket/rosocket"
3 | description = "Roblox websocket support in 1 module"
4 | version = "1.0.1"
5 | license = "MIT"
6 | authors = ["sturmgeisty"]
7 | registry = "https://github.com/upliftgames/wally-index"
8 | realm = "shared"
9 | include = ["src", "src/**", "wally.toml", "wally.lock", "default.project.json"]
10 | exclude = ["**"]
11 |
--------------------------------------------------------------------------------