mirror of
https://github.com/Discreetly/server.git
synced 2026-05-09 03:00:03 -04:00
255 lines
7.3 KiB
TypeScript
255 lines
7.3 KiB
TypeScript
import * as express from 'express';
|
|
import { Server } from 'http';
|
|
import { Server as SocketIOServer, Socket } from 'socket.io';
|
|
import * as cors from 'cors';
|
|
import { createClient } from 'redis';
|
|
import { serverConfig, rooms as defaultRooms, rooms } from '../config/rooms';
|
|
import type { MessageI, RoomI, RoomGroupI } from 'discreetly-interfaces';
|
|
import verifyProof from './verifier';
|
|
import ClaimCodeManager from 'discreetly-claimcodes';
|
|
import type { ClaimCodeStatus } from 'discreetly-claimcodes';
|
|
import { pp, addIdentityToRoom, createGroup } from './utils';
|
|
import { faker } from '@faker-js/faker';
|
|
|
|
// 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;
|
|
// Testing Mode
|
|
const TESTING = true;
|
|
|
|
// Deal with bigints in JSON
|
|
(BigInt.prototype as any).toJSON = function () {
|
|
return this.toString();
|
|
};
|
|
|
|
const app = express();
|
|
|
|
const socket_server = new Server(app);
|
|
app.use(express.json());
|
|
|
|
const io = new SocketIOServer(socket_server, {
|
|
cors: {
|
|
origin: '*'
|
|
}
|
|
});
|
|
|
|
interface userCountI {
|
|
[key: string]: number;
|
|
}
|
|
|
|
let userCount: userCountI = {};
|
|
let loadedRooms: RoomGroupI[];
|
|
let TESTGROUPID: BigInt;
|
|
// TODO get the claim code manager working with redis to store the state of the rooms and claim codes in a redis database that persists across server restarts
|
|
// Redis
|
|
|
|
const redisClient = createClient();
|
|
redisClient.connect().then(() => pp('Redis Connected'));
|
|
|
|
redisClient.get('rooms').then((rooms) => {
|
|
rooms = JSON.parse(rooms);
|
|
if (rooms) {
|
|
loadedRooms = rooms as RoomGroupI[];
|
|
} else {
|
|
loadedRooms = defaultRooms;
|
|
redisClient.set('rooms', JSON.stringify(loadedRooms));
|
|
}
|
|
pp({ 'Loaded Rooms': 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);
|
|
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, rooms, codes } = data;
|
|
if (password === process.env.PASSWORD) {
|
|
const roomGroups = createGroup(groupName, rooms, loadedRooms);
|
|
loadedRooms = roomGroups;
|
|
redisClient.set('rooms', JSON.stringify(loadedRooms));
|
|
res.status(201).json({ status: `Created group ${groupName}`, loadedRooms });
|
|
}
|
|
});
|
|
|
|
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 http://localhost:${HTTP_PORT}`);
|
|
});
|
|
|
|
socket_server.listen(SOCKET_PORT, () => {
|
|
pp(`SocketIO Server is running at http://localhost:${SOCKET_PORT}`);
|
|
});
|
|
|
|
// Disconnect from redis on exit
|
|
process.on('SIGINT', () => {
|
|
pp('disconnecting redis');
|
|
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(),
|
|
room: '7458174823225695762087107782399226439860424529052640186229953289032606624581',
|
|
message: picker.pick(),
|
|
timestamp: Date.now().toString(),
|
|
epoch: Math.floor(Date.now() / 10000)
|
|
};
|
|
console.log('SENDING TEST MESSAGE');
|
|
io.emit('messageBroadcast', message);
|
|
}, 10000);
|
|
}
|