diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..aee7b56 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,21 @@ +version: '3' +services: + node: + build: . + ports: + - '3001:3001' + - '3002:3002' + volumes: + - .:/app + depends_on: + - redis + environment: + - REDIS_HOST=redis + - REDIS_PORT=6379 + - NODE_ENV=production + + redis: + image: "redis:alpine" + hostname: redis + ports: + - "6379:6379" diff --git a/dockerfile b/dockerfile new file mode 100644 index 0000000..e2c880a --- /dev/null +++ b/dockerfile @@ -0,0 +1,14 @@ +FROM node:18-alpine + +WORKDIR /app + +COPY package*.json ./ + +RUN npm install + +COPY . . + +EXPOSE 3001 +EXPOSE 3002 + +CMD [ "npm", "start" ] diff --git a/package-lock.json b/package-lock.json index 78b6879..41ba1cc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "discreetly-server", - "version": "0.1.0", + "version": "0.1.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "discreetly-server", - "version": "0.1.0", + "version": "0.1.1", "license": "ISC", "dependencies": { "@faker-js/faker": "^8.0.2", @@ -14,7 +14,7 @@ "body-parser": "^1.20.2", "cors": "^2.8.5", "discreetly-claimcodes": "^1.0.7", - "discreetly-interfaces": "^0.1.5", + "discreetly-interfaces": "^0.1.11", "dotenv": "^16.3.1", "express": "^4.18.2", "poseidon-lite": "^0.2.0", @@ -32,6 +32,7 @@ "nodemon": "^2.0.22", "rollup": "^3.26.2", "rollup-plugin-cleaner": "^1.0.0", + "rollup-plugin-include-sourcemaps": "^0.7.0", "rollup-plugin-typescript2": "^0.35.0", "typescript": "^5.1.6" }, @@ -1108,6 +1109,15 @@ "ms": "2.0.0" } }, + "node_modules/decode-uri-component": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz", + "integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==", + "dev": true, + "engines": { + "node": ">=0.10" + } + }, "node_modules/deepmerge": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", @@ -1140,10 +1150,11 @@ "integrity": "sha512-on8ZS7W1WlsY+J9hQmTU2IO18HQ2/pXbztaSCkJaqlpgMDjb2l2Q0MqGQtwshTtv9dfcoIuzeRzuXSk4T69NJg==" }, "node_modules/discreetly-interfaces": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/discreetly-interfaces/-/discreetly-interfaces-0.1.5.tgz", - "integrity": "sha512-RcdBvop3n8K4xlvD6/P9APD7nKhONNon++fh5UdDqa3bjWxLtZsaHna7pTfIuymjUmimDrqiMVB6JCS3ys4MFw==", + "version": "0.1.11", + "resolved": "https://registry.npmjs.org/discreetly-interfaces/-/discreetly-interfaces-0.1.11.tgz", + "integrity": "sha512-lHBIc6BVx2UbVXFqsXEs1aC0UTs9+FLE28v4K+dVVhN78sXY+aAyTbOELqv2+L1Fae5FMBVkexOqxJ6nDJAvPQ==", "dependencies": { + "atob": "^2.1.2", "poseidon-lite": "^0.2.0", "rlnjs": "^3.1.4" } @@ -2515,6 +2526,51 @@ "rollup": "> 1.0" } }, + "node_modules/rollup-plugin-include-sourcemaps": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/rollup-plugin-include-sourcemaps/-/rollup-plugin-include-sourcemaps-0.7.0.tgz", + "integrity": "sha512-zAlN2IkFSaptlHhuWVROZ5xrviEULRSInN9AzETsBD++Ab5aMKAtXhDH2aRHbE2cRW6cVT9FFrAQHwZXxqDCIQ==", + "dev": true, + "dependencies": { + "@rollup/pluginutils": "^5.0.2", + "atob": "^2.1.2", + "decode-uri-component": "^0.2.0" + }, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "@types/node": ">=10.0.0", + "rollup": ">=0.31.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/rollup-plugin-include-sourcemaps/node_modules/@rollup/pluginutils": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.0.2.tgz", + "integrity": "sha512-pTd9rIsP92h+B6wWwFbW8RkZv4hiR/xKsqre4SIuAOaOEQRxi0lqLke9k2/7WegC85GgUs9pjmOjCUi3In4vwA==", + "dev": true, + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, "node_modules/rollup-plugin-typescript2": { "version": "0.35.0", "resolved": "https://registry.npmjs.org/rollup-plugin-typescript2/-/rollup-plugin-typescript2-0.35.0.tgz", @@ -3863,6 +3919,12 @@ "ms": "2.0.0" } }, + "decode-uri-component": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz", + "integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==", + "dev": true + }, "deepmerge": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", @@ -3885,10 +3947,11 @@ "integrity": "sha512-on8ZS7W1WlsY+J9hQmTU2IO18HQ2/pXbztaSCkJaqlpgMDjb2l2Q0MqGQtwshTtv9dfcoIuzeRzuXSk4T69NJg==" }, "discreetly-interfaces": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/discreetly-interfaces/-/discreetly-interfaces-0.1.5.tgz", - "integrity": "sha512-RcdBvop3n8K4xlvD6/P9APD7nKhONNon++fh5UdDqa3bjWxLtZsaHna7pTfIuymjUmimDrqiMVB6JCS3ys4MFw==", + "version": "0.1.11", + "resolved": "https://registry.npmjs.org/discreetly-interfaces/-/discreetly-interfaces-0.1.11.tgz", + "integrity": "sha512-lHBIc6BVx2UbVXFqsXEs1aC0UTs9+FLE28v4K+dVVhN78sXY+aAyTbOELqv2+L1Fae5FMBVkexOqxJ6nDJAvPQ==", "requires": { + "atob": "^2.1.2", "poseidon-lite": "^0.2.0", "rlnjs": "^3.1.4" } @@ -4933,6 +4996,30 @@ "rimraf": "^2.6.3" } }, + "rollup-plugin-include-sourcemaps": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/rollup-plugin-include-sourcemaps/-/rollup-plugin-include-sourcemaps-0.7.0.tgz", + "integrity": "sha512-zAlN2IkFSaptlHhuWVROZ5xrviEULRSInN9AzETsBD++Ab5aMKAtXhDH2aRHbE2cRW6cVT9FFrAQHwZXxqDCIQ==", + "dev": true, + "requires": { + "@rollup/pluginutils": "^5.0.2", + "atob": "^2.1.2", + "decode-uri-component": "^0.2.0" + }, + "dependencies": { + "@rollup/pluginutils": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.0.2.tgz", + "integrity": "sha512-pTd9rIsP92h+B6wWwFbW8RkZv4hiR/xKsqre4SIuAOaOEQRxi0lqLke9k2/7WegC85GgUs9pjmOjCUi3In4vwA==", + "dev": true, + "requires": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^2.3.1" + } + } + } + }, "rollup-plugin-typescript2": { "version": "0.35.0", "resolved": "https://registry.npmjs.org/rollup-plugin-typescript2/-/rollup-plugin-typescript2-0.35.0.tgz", diff --git a/package.json b/package.json index 1db94c9..f139437 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "discreetly-server", - "version": "0.1.0", + "version": "0.1.1", "description": "", "main": "dist/server.cjs", "scripts": { @@ -8,7 +8,8 @@ "start": "node dist/server.cjs", "watch": "rollup --config rollup.config.mjs --watch", "serve": "nodemon -q dist/server.jcs", - "dev": "concurrently \"npm run watch\" \"npm run serve\"" + "dev": "concurrently \"npm run watch\" \"npm run serve\"", + "docker": "docker-compose up --build" }, "engines": { "node": "18.x.x" @@ -23,7 +24,7 @@ "body-parser": "^1.20.2", "cors": "^2.8.5", "discreetly-claimcodes": "^1.0.7", - "discreetly-interfaces": "^0.1.5", + "discreetly-interfaces": "^0.1.11", "dotenv": "^16.3.1", "express": "^4.18.2", "poseidon-lite": "^0.2.0", @@ -41,6 +42,7 @@ "nodemon": "^2.0.22", "rollup": "^3.26.2", "rollup-plugin-cleaner": "^1.0.0", + "rollup-plugin-include-sourcemaps": "^0.7.0", "rollup-plugin-typescript2": "^0.35.0", "typescript": "^5.1.6" } diff --git a/src/mock.ts b/src/mock.ts new file mode 100644 index 0000000..925aa70 --- /dev/null +++ b/src/mock.ts @@ -0,0 +1,47 @@ +import { faker } from '@faker-js/faker'; +import { MessageI } from 'discreetly-interfaces'; +import { Server as SocketIOServer } from 'socket.io'; + +export default function Mock(io: SocketIOServer) { + class randomMessagePicker { + values: any; + weightSums: any[]; + constructor(values, weights) { + this.values = values; + this.weightSums = []; + let sum = 0; + + for (let weight of weights) { + sum += weight; + this.weightSums.push(sum); + } + } + + pick() { + const rand = Math.random() * this.weightSums[this.weightSums.length - 1]; + let index = this.weightSums.findIndex((sum) => rand < sum); + return this.values[index](); + } + } + + const values = [ + faker.finance.ethereumAddress, + faker.company.buzzPhrase, + faker.lorem.sentence, + faker.hacker.phrase + ]; + const weights = [1, 3, 2, 8]; + const picker = new randomMessagePicker(values, weights); + + setInterval(() => { + const message: MessageI = { + id: faker.number.bigInt().toString(), + room: BigInt('7458174823225695762087107782399226439860424529052640186229953289032606624581'), + message: picker.pick(), + timestamp: Date.now().toString(), + epoch: Math.floor(Date.now() / 10000) + }; + console.log('SENDING TEST MESSAGE'); + io.emit('messageBroadcast', message); + }, 10000); +} diff --git a/src/server.ts b/src/server.ts index 0dff954..fc861d3 100644 --- a/src/server.ts +++ b/src/server.ts @@ -1,30 +1,27 @@ import express from 'express'; import { Server } from 'http'; -import { Server as SocketIOServer, Socket } from 'socket.io'; +import { Server as SocketIOServer } from 'socket.io'; import cors from 'cors'; import { createClient } from 'redis'; -import { serverConfig, rooms as defaultRooms, rooms } from './config/rooms.js'; -import type { MessageI, RoomI, RoomGroupI } from 'discreetly-interfaces'; -import verifyProof from './verifier.js'; -import { ClaimCodeManager } from 'discreetly-claimcodes'; -import type { ClaimCodeStatus } from 'discreetly-claimcodes'; -import { pp, addIdentityToRoom, createGroup, createRoom, findGroupById } from './utils.js'; -import { faker } from '@faker-js/faker'; - +import { pp, shim } from './utils.js'; +import { initRedisVariables, initSockets, initExpressEndpoints } from './startup.js'; +import mock from './mock.js'; // HTTP is to get info from the server about configuration, rooms, etc const HTTP_PORT = 3001; // Socket is to communicate chat room messages back and forth const SOCKET_PORT = 3002; -// Deal with bigints in JSON -(BigInt.prototype as any).toJSON = function () { - return this.toString(); -}; - const app = express(); - const socket_server = new Server(app); + +shim(); + app.use(express.json()); +app.use( + cors({ + origin: '*' + }) +); const io = new SocketIOServer(socket_server, { cors: { @@ -32,218 +29,17 @@ const io = new SocketIOServer(socket_server, { } }); -interface userCountI { - [key: string]: number; -} - -let userCount: userCountI = {}; -let loadedRooms: RoomGroupI[]; -let TESTGROUPID: BigInt; - let redisClient; -let TESTING = false; -if (!process.env.REDIS_URL) { - console.log('Connecting to redis at localhost'); - redisClient = createClient(); - TESTING = true; -} else { - console.log('Connecting to redis at: ' + process.env.REDIS_URL); - console.log(process.env.REDIS_TLS_URL); - redisClient = createClient({ - url: process.env.REDIS_URL, - socket: { - tls: true, - rejectUnauthorized: false - } +function initAppListeners() { + app.listen(HTTP_PORT, () => { + pp(`Express Http Server is running at port ${HTTP_PORT}`); + }); + + socket_server.listen(SOCKET_PORT, () => { + pp(`SocketIO Server is running at port ${SOCKET_PORT}`); }); } -redisClient.connect().then(() => pp('Redis Connected')); - -redisClient.get('rooms').then((rooms) => { - rooms = JSON.parse(rooms); - if (rooms) { - loadedRooms = rooms as unknown as RoomGroupI[]; - } else { - loadedRooms = defaultRooms; - redisClient.set('rooms', JSON.stringify(loadedRooms)); - } -}); - -let ccm: ClaimCodeManager; - -redisClient.get('ccm').then((cc) => { - TESTGROUPID = BigInt(loadedRooms[0].id); - if (!cc) { - ccm = new ClaimCodeManager(); - ccm.generateClaimCodeSet(10, TESTGROUPID, 'TEST'); - const ccs = ccm.getClaimCodeSets(); - redisClient.set('ccm', JSON.stringify(ccs)); - } else { - ccm = new ClaimCodeManager(JSON.parse(cc)); - - if (ccm.getUsedCount(TESTGROUPID).unusedCount < 5) { - ccm.generateClaimCodeSet(10, TESTGROUPID, 'TEST'); - const ccs = ccm.getClaimCodeSets(); - - redisClient.set('ccm', JSON.stringify(ccs)); - } - } - const ccs = ccm.getClaimCodeSets(); -}); - -redisClient.on('error', (err) => pp('Redis Client Error: ' + err, 'error')); - -io.on('connection', (socket: Socket) => { - pp('SocketIO: a user connected', 'debug'); - - socket.on('validateMessage', (msg: MessageI) => { - pp({ 'VALIDATING MESSAGE ID': msg.id.slice(0, 11), 'MSG:': msg.message }); - const valid = verifyProof(msg, loadedRooms); - if (!valid) { - pp('INVALID MESSAGE', 'warn'); - return; - } - io.emit('messageBroadcast', msg); - }); - - socket.on('disconnect', () => { - pp('SocketIO: user disconnected'); - }); - - socket.on('joinRoom', (roomID: bigint) => { - const id = roomID.toString(); - userCount[id] = userCount[id] ? userCount[id] + 1 : 1; - }); - - socket.on('leaveRoom', (roomID: bigint) => { - const id = roomID.toString(); - userCount[id] = userCount[id] ? userCount[id] - 1 : 0; - }); -}); - -app.use( - cors({ - origin: '*' - }) -); - -app.get(['/', '/api'], (req, res) => { - pp('Express: fetching server info'); - res.json(serverConfig); -}); - -app.get('/api/rooms', (req, res) => { - pp('Express: fetching rooms'); - res.json(loadedRooms); -}); - -app.get('/api/rooms/:id', (req, res) => { - // TODO This should return the room info for the given room ID - pp(String('Express: fetching room info for ' + req.params.id)); - const room = loadedRooms - .flatMap((rooms) => rooms.rooms) - .filter((room) => room.id === req.params.id); - res.json(room); -}); - -// TODO api endpoint that creates new rooms and generates invite codes for them -app.post('/join', (req, res) => { - const data = req.body; - const { code, idc } = data; - pp('Express[/join]: claiming code:' + code); - const result: ClaimCodeStatus = ccm.claimCode(code); - const groupID = result.groupID; - if (result.status === 'CLAIMED') { - let claimedRooms = []; - let alreadyAddedRooms = []; - loadedRooms.forEach((group) => { - if (group.id == groupID) { - group.rooms.forEach((room: RoomI) => { - let { status, roomGroups } = addIdentityToRoom(BigInt(room.id), BigInt(idc), loadedRooms); - loadedRooms = roomGroups; - redisClient.set('rooms', JSON.stringify(loadedRooms)); - if (status) { - claimedRooms.push(room); - } else { - alreadyAddedRooms.push(room); - } - }); - } - }); - let r = [...claimedRooms, ...alreadyAddedRooms]; - - if (claimedRooms.length > 0) { - res.status(200).json({ status: 'valid', rooms: r }); - } else if (alreadyAddedRooms.length > 0) { - res.status(200).json({ status: 'already-added', rooms: r }); - } else { - res.status(451).json({ status: 'invalid' }); - } - - // the DB should be updated after we successfully send a response - redisClient.set('ccm', JSON.stringify(ccm.getClaimCodeSets())); - } else { - res.status(451).json({ status: 'invalid' }); - } -}); - -// TODO we are going to need endpoints that take a password that will be in a .env file to generate new roomGroups, rooms, and claim codes -app.post('/group/add', (req, res) => { - const data = req.body; - const { password, groupName, roomNames, codes } = data; - if (password === process.env.PASSWORD) { - const result = createGroup(groupName, roomNames, loadedRooms); - loadedRooms = result.roomGroup; - redisClient.set('rooms', JSON.stringify(loadedRooms)); - if (codes.generate) { - codes.amount = codes.amount || 10; - ccm.generateClaimCodeSet(codes.amount, result.groupId, groupName); - const ccs = ccm.getClaimCodeSets(); - redisClient.set('ccm', JSON.stringify(ccs)); - } - res.status(201).json({ status: `Created group ${groupName}`, loadedRooms }); - } -}); - -app.post('/room/add', (req, res) => { - const data = req.body; - const { password, groupId, roomName } = data; - if (password === process.env.PASSWORD) { - const roomGroups = createRoom(groupId, roomName, loadedRooms); - loadedRooms = roomGroups; - redisClient.set('rooms', JSON.stringify(loadedRooms)); - res.status(201).json({ status: `Created room ${roomName}`, loadedRooms }); - } -}); - -app.post('/group/createcode', (req, res) => { - const data = req.body; - let { password, groupId, amount } = data; - if (password === process.env.PASSWORD) { - amount = amount || 10; - console.log(loadedRooms, groupId); - const group = findGroupById(loadedRooms, groupId); - const ccs = ccm.generateClaimCodeSet(amount, groupId, group.name); - redisClient.set('ccm', JSON.stringify(ccs)); - res.status(201).json({ stats: `Created ${amount} codes for ${group.name}`, ccm }); - } -}); - -app.get('/logclaimcodes', (req, res) => { - pp('-----CLAIMCODES-----', 'debug'); - pp(ccm.getClaimCodeSet(TESTGROUPID)); - pp('-----ENDOFCODES-----', 'debug'); - res.status(200).json({ status: 'ok' }); -}); - -app.listen(HTTP_PORT, () => { - pp(`Express Http Server is running at port ${HTTP_PORT}`); -}); - -socket_server.listen(SOCKET_PORT, () => { - pp(`SocketIO Server is running at port ${SOCKET_PORT}`); -}); // Disconnect from redis on exit process.on('SIGINT', () => { @@ -251,46 +47,46 @@ process.on('SIGINT', () => { redisClient.disconnect().then(process.exit()); }); -if (TESTING) { - class randomMessagePicker { - values: any; - weightSums: any[]; - constructor(values, weights) { - this.values = values; - this.weightSums = []; - let sum = 0; - - for (let weight of weights) { - sum += weight; - this.weightSums.push(sum); - } - } - - pick() { - const rand = Math.random() * this.weightSums[this.weightSums.length - 1]; - let index = this.weightSums.findIndex((sum) => rand < sum); - return this.values[index](); - } - } - - const values = [ - faker.finance.ethereumAddress, - faker.company.buzzPhrase, - faker.lorem.sentence, - faker.hacker.phrase - ]; - const weights = [1, 3, 2, 8]; - const picker = new randomMessagePicker(values, weights); - - setInterval(() => { - const message: MessageI = { - id: faker.number.bigInt().toString(), - room: BigInt('7458174823225695762087107782399226439860424529052640186229953289032606624581'), - message: picker.pick(), - timestamp: Date.now().toString(), - epoch: Math.floor(Date.now() / 10000) - }; - console.log('SENDING TEST MESSAGE'); - io.emit('messageBroadcast', message); - }, 10000); +/** + * This is the main entry point for the server + */ +if (!process.env.NODE_ENV || process.env.NODE_ENV === 'development') { + console.log('Creating Redis client on localhost'); + redisClient = createClient(); + redisClient.connect().then(() => { + pp('Redis Connected to localhost'); + }); + initRedisVariables(redisClient).then(({ loadedRooms, ccm, TESTGROUPID }) => { + initExpressEndpoints(app, redisClient, ccm, TESTGROUPID); + initSockets(io, loadedRooms); + initAppListeners(); + mock(io); + }); +} else { + console.log("Creating Redis client with socket: { host: 'redis', port: 6379 }"); + redisClient = createClient({ + socket: { + host: process.env.REDIS_HOST, + port: parseInt(process.env.REDIS_PORT) + }, + legacyMode: true + }); + console.log('Connecting to redis docker container'); + redisClient + .connect() + .then(() => { + pp('Redis Connected to redis docker container'); + }) + .catch((err) => { + pp('Redis Connection Error: ' + err, 'error'); + }); + console.log('Initializing Redis Variables'); + initRedisVariables(redisClient).then(({ loadedRooms, ccm, TESTGROUPID }) => { + console.log('Initializing Express Endpoints'); + initExpressEndpoints(app, redisClient, ccm, TESTGROUPID); + console.log('Initializing Sockets'); + initSockets(io, loadedRooms); + console.log('Initializing App Listeners'); + initAppListeners(); + }); } diff --git a/src/startup.ts b/src/startup.ts new file mode 100644 index 0000000..dcc7121 --- /dev/null +++ b/src/startup.ts @@ -0,0 +1,204 @@ +import { serverConfig, rooms as defaultRooms, rooms } from './config/rooms.js'; +import type { MessageI, RoomI, RoomGroupI } from 'discreetly-interfaces'; +import { ClaimCodeManager } from 'discreetly-claimcodes'; +import type { ClaimCodeStatus } from 'discreetly-claimcodes'; +import { pp, addIdentityToRoom, createGroup, createRoom, findGroupById } from './utils.js'; +import { Socket, Server as SocketIOServer } from 'socket.io'; +import { RedisClientType } from 'redis'; +import verifyProof from './verifier.js'; +import { userCountI } from './types.js'; + +export async function initRedisVariables(redisClient: RedisClientType): Promise<{ + loadedRooms: RoomGroupI[]; + ccm: ClaimCodeManager; + TESTGROUPID: bigint; +}> { + let loadedRooms: RoomGroupI[]; + + const _CachedRooms = await redisClient.get('rooms'); + if (rooms) { + console.log('Loading cached rooms'); + loadedRooms = JSON.parse(_CachedRooms) as unknown as RoomGroupI[]; + } else { + console.log('Using default rooms'); + loadedRooms = defaultRooms as RoomGroupI[]; + redisClient.set('rooms', JSON.stringify(loadedRooms)); + } + + let claimCodeManager: ClaimCodeManager; + let TESTGROUPID: bigint; + const _CachedClaimCodeManager = await redisClient.get('ccm'); + TESTGROUPID = BigInt(loadedRooms[0].id); + if (!_CachedClaimCodeManager) { + claimCodeManager = new ClaimCodeManager(); + claimCodeManager.generateClaimCodeSet(10, TESTGROUPID, 'TEST'); + const ccs = claimCodeManager.getClaimCodeSets(); + redisClient.set('ccm', JSON.stringify(ccs)); + } else { + claimCodeManager = new ClaimCodeManager(JSON.parse(_CachedClaimCodeManager)); + + if (claimCodeManager.getUsedCount(TESTGROUPID).unusedCount < 5) { + claimCodeManager.generateClaimCodeSet(10, TESTGROUPID, 'TEST'); + const ccs = claimCodeManager.getClaimCodeSets(); + + redisClient.set('ccm', JSON.stringify(ccs)); + } + } + + return { loadedRooms, ccm: claimCodeManager, TESTGROUPID }; +} + +export function initSockets(io: SocketIOServer, loadedRooms: RoomGroupI[]) { + let userCount: userCountI = {}; + io.on('connection', (socket: Socket) => { + pp('SocketIO: a user connected', 'debug'); + + socket.on('validateMessage', (msg: MessageI) => { + pp({ 'VALIDATING MESSAGE ID': msg.id?.slice(0, 11), 'MSG:': msg.message }); + const valid = verifyProof(msg, loadedRooms); + if (!valid) { + pp('INVALID MESSAGE', 'warn'); + return; + } + io.emit('messageBroadcast', msg); + }); + + socket.on('disconnect', () => { + pp('SocketIO: user disconnected'); + }); + + socket.on('joinRoom', (roomID: bigint) => { + const id = roomID.toString(); + userCount[id] = userCount[id] ? userCount[id] + 1 : 1; + }); + + socket.on('leaveRoom', (roomID: bigint) => { + const id = roomID.toString(); + userCount[id] = userCount[id] ? userCount[id] - 1 : 0; + }); + }); +} + +export function initExpressEndpoints( + app, + redisClient: RedisClientType, + ccm: ClaimCodeManager, + TESTGROUPID: bigint +) { + let loadedRooms: RoomGroupI[] = rooms; + app.get(['/', '/api'], (req, res) => { + pp('Express: fetching server info'); + res.json(serverConfig); + }); + + app.get('/api/rooms', (req, res) => { + pp('Express: fetching rooms'); + redisClient.get('rooms').then((rooms) => { + return res.json(rooms); + }); + }); + + app.get('/api/rooms/:id', (req, res) => { + // TODO This should return the room info for the given room ID + pp(String('Express: fetching room info for ' + req.params.id)); + const room = loadedRooms + .flatMap((rooms) => rooms.rooms) + .filter((room) => room.id === req.params.id); + res.json(room); + }); + + // TODO api endpoint that creates new rooms and generates invite codes for them + app.post('/join', (req, res) => { + const data = req.body; + const { code, idc } = data; + pp('Express[/join]: claiming code:' + code); + const result: ClaimCodeStatus = ccm.claimCode(code); + const groupID = result.groupID; + if (result.status === 'CLAIMED') { + let claimedRooms: any[] = []; + let alreadyAddedRooms: any[] = []; + loadedRooms.forEach((group) => { + if (group.id == groupID) { + group.rooms.forEach((room: RoomI) => { + let { status, roomGroups } = addIdentityToRoom( + BigInt(room.id), + BigInt(idc), + loadedRooms + ); + loadedRooms = roomGroups; + redisClient.set('rooms', JSON.stringify(loadedRooms)); + if (status) { + claimedRooms.push(room); + } else { + alreadyAddedRooms.push(room); + } + }); + } + }); + let r = [...claimedRooms, ...alreadyAddedRooms]; + + if (claimedRooms.length > 0) { + res.status(200).json({ status: 'valid', rooms: r }); + } else if (alreadyAddedRooms.length > 0) { + res.status(200).json({ status: 'already-added', rooms: r }); + } else { + res.status(451).json({ status: 'invalid' }); + } + + // the DB should be updated after we successfully send a response + redisClient.set('ccm', JSON.stringify(ccm.getClaimCodeSets())); + } else { + res.status(451).json({ status: 'invalid' }); + } + }); + + // TODO we are going to need endpoints that take a password that will be in a .env file to generate new roomGroups, rooms, and claim codes + app.post('/group/add', (req, res) => { + const data = req.body; + const { password, groupName, roomNames, codes } = data; + if (password === process.env.PASSWORD) { + const result = createGroup(groupName, roomNames, loadedRooms); + loadedRooms = result.roomGroup; + redisClient.set('rooms', JSON.stringify(loadedRooms)); + if (codes.generate) { + codes.amount = codes.amount || 10; + ccm.generateClaimCodeSet(codes.amount, result.groupId, groupName); + const ccs = ccm.getClaimCodeSets(); + redisClient.set('ccm', JSON.stringify(ccs)); + } + res.status(201).json({ status: `Created group ${groupName}`, loadedRooms }); + } + }); + + app.post('/room/add', (req, res) => { + const data = req.body; + const { password, groupId, roomName } = data; + if (password === process.env.PASSWORD) { + redisClient.get('rooms').then((loadedRooms) => { + const roomGroups = createRoom(groupId, roomName, JSON.parse(loadedRooms) as RoomGroupI[]); + redisClient.set('rooms', JSON.stringify(roomGroups)); + res.status(201).json({ status: `Created room ${roomName}`, roomGroups }); + }); + } + }); + + app.post('/group/createcode', (req, res) => { + const data = req.body; + let { password, groupId, amount } = data; + if (password === process.env.PASSWORD) { + amount = amount || 10; + console.log(loadedRooms, groupId); + const group = findGroupById(loadedRooms, groupId); + const ccs = ccm.generateClaimCodeSet(amount, groupId, group.name); + redisClient.set('ccm', JSON.stringify(ccs)); + res.status(201).json({ stats: `Created ${amount} codes for ${group.name}`, ccm }); + } + }); + + app.get('/logclaimcodes', (req, res) => { + pp('-----CLAIMCODES-----', 'debug'); + pp(ccm.getClaimCodeSet(TESTGROUPID)); + pp('-----ENDOFCODES-----', 'debug'); + res.status(200).json({ status: 'ok' }); + }); +} diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..1127d40 --- /dev/null +++ b/src/types.ts @@ -0,0 +1,3 @@ +export interface userCountI { + [key: string]: number; +} diff --git a/src/utils.ts b/src/utils.ts index a9e0522..f7cf459 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,9 +1,12 @@ -import { createClient } from 'redis'; import type { RoomI, RoomGroupI, ServerI } from 'discreetly-interfaces'; import { genId } from 'discreetly-interfaces'; -const redisClient = createClient(); -redisClient.connect(); +export function shim() { + // Deal with bigints in JSON + (BigInt.prototype as any).toJSON = function () { + return this.toString(); + }; +} export function findRoomById( roomGroups, @@ -55,7 +58,7 @@ export function createGroup( groupName: string, roomNames: string[], roomGroups: RoomGroupI[] -): { groupId: bigint, roomGroup: RoomGroupI[] } { +): { groupId: bigint; roomGroup: RoomGroupI[] } { const newGroup: RoomGroupI = { id: genId(BigInt(999), groupName).toString() as unknown as bigint, name: groupName, @@ -75,14 +78,15 @@ export function createGroup( export function createRoom( groupId: bigint, roomName: string, - roomGroups: RoomGroupI[]): RoomGroupI[] { + roomGroups: RoomGroupI[] +): RoomGroupI[] { const newRoom: RoomI = { id: genId(BigInt(999), roomName), name: roomName, membership: { identityCommitments: [] }, rateLimit: 1000 - } - const groupIndex = roomGroups.findIndex(group => group.id === groupId); + }; + const groupIndex = roomGroups.findIndex((group) => group.id === groupId); if (groupIndex !== -1) { roomGroups[groupIndex].rooms.push(newRoom); }