Merge branch 'return-codes' into pixelmap-support

This commit is contained in:
AtHeartEngineer
2023-08-24 23:08:41 -04:00
committed by GitHub
4 changed files with 259 additions and 45 deletions

View File

@@ -19,12 +19,19 @@ interface RoomsFromClaimCode {
roomIds: string[];
}
/**
* Gets a room by id
* @param {string} id The id of the room to get
* @returns {Promise<RoomI | null>}The room, or null if it doesn't exist
*/
export async function getRoomByID(id: string): Promise<RoomI | null> {
const room = await prisma.rooms
.findUnique({
where: {
roomId: id
},
// Filter out the information we want from the room
select: {
id: true,
roomId: true,
@@ -54,13 +61,20 @@ export async function getRoomByID(id: string): Promise<RoomI | null> {
});
}
export async function getRoomsByIdentity(identity: string): Promise<string[]> {
/* TODO Need to create a system here where the client needs to provide a
proof they know the secrets to some Identity Commitment with a unix epoch
time stamp to prevent replay attacks
/* TODO Need to create a system here where the client needs to provide a
proof they know the secrets to some Identity Commitment with a unix epoch
time stamp to prevent replay attacks
https://github.com/Discreetly/IdentityCommitmentNullifierCircuit <- Circuit and JS to do this
*/
https://github.com/Discreetly/IdentityCommitmentNullifierCircuit <- Circuit and JS to do this
*/
/**
* This function takes in an identity and returns the rooms the identity is in.
* @param identity - the identity of a user
* @returns an array of roomIds
*/
export async function getRoomsByIdentity(identity: string): Promise<string[]> {
const r: string[] = [];
try {
const rooms = await prisma.rooms.findMany({
@@ -80,12 +94,26 @@ export async function getRoomsByIdentity(identity: string): Promise<string[]> {
}
}
/**
* Finds a claim code in the database.
*
* @param {string} code - The code to find.
* @returns {Promise<CodeStatus | null>} - The claim code, if found.
*/
export function findClaimCode(code: string): Promise<CodeStatus | null> {
return prisma.claimCodes.findUnique({
where: { claimcode: code }
});
}
/**
* Update the claim_code table to mark the given code as claimed.
* @param {string} code - The code to update
* @returns {Promise<RoomsFromClaimCode>} - The rooms associated with the claim code
*/
export function updateClaimCode(code: string): Promise<RoomsFromClaimCode> {
return prisma.claimCodes.update({
where: { claimcode: code },
@@ -93,6 +121,14 @@ export function updateClaimCode(code: string): Promise<RoomsFromClaimCode> {
});
}
/*
The sanitizeIDC function takes a string and returns a string.
The string is converted to a BigInt and then back to a string.
If the string has no loss of precision, it is returned.
Otherwise, an error is thrown.
*/
function sanitizeIDC(idc: string): string {
try {
const tempBigInt = BigInt(idc);
@@ -107,6 +143,16 @@ function sanitizeIDC(idc: string): string {
}
}
/**
* This code updates the identity commitments of a list of rooms.
* It adds the identity commitment to the identity list of each room,
* and also adds it to the bandada of each room. The identity commitment is
* sanitized before being added to the database.
* @param idc - The identity commitment of the user
* @param roomIds - The list of roomIds that the user is in
* @returns {Promise<void>} - A promise that resolves when the update is complete
*/
export async function updateRoomIdentities(
idc: string,
roomIds: string[]
@@ -125,6 +171,11 @@ export async function updateRoomIdentities(
});
}
/**
* Adds a user's identity commitment to the semaphoreIdentities list and adds their rate commitment to the identities list for each of the identity list rooms that they are in.
* @param {rooms} - The list of rooms that the user is in
* @param {string} identityCommitment - The user's identity commitment
*/
function addIdentityToIdentityListRooms(
rooms,
identityCommitment: string
@@ -163,6 +214,19 @@ function addIdentityToIdentityListRooms(
}
}
}
/**
* This code adds a new identity commitment to the list of identities in a bandada room.
* First we get the list of bandada rooms that contain the identity commitment.
* Then we iterate over the list of rooms and add the identity commitment to each room.
* After that we update the list of identities in each room in the database.
* Finally, we send a POST request to the bandada server to add the identity to the group.
* @param {RoomI[]} rooms - The list of rooms that contain the identity commitment.
* @param {string} identityCommitment - The identity commitment to be added to the bandada room.
* @return {void} Nothing.
*/
function addIdentityToBandadaRooms(rooms, identityCommitment: string): void {
const bandadaGroupRooms = rooms
.filter(
@@ -214,6 +278,14 @@ function addIdentityToBandadaRooms(rooms, identityCommitment: string): void {
}
}
/**
* This function is used to find rooms that have been updated
* It is used in the findUpdatedRooms function
* It is important because it allows the user to see which rooms have been updated
* @param {string[]} roomIds - The list of roomIds that the user is in
* @returns {Promise<RoomI[]>} - A promise that resolves to a list of rooms
*/
export async function findUpdatedRooms(roomIds: string[]): Promise<RoomI[]> {
const rooms = await prisma.rooms.findMany({
where: { id: { in: roomIds } }
@@ -226,11 +298,19 @@ export async function findUpdatedRooms(roomIds: string[]): Promise<RoomI[]> {
});
}
// TODO: Make interface for this return type; which is like a MessageI
/**
* This function creates a system message in a room.
* The message will be the same in all rooms if no roomId is passed.
* If a roomId is passed, the message will be created in that room.
* @param {string} message - The message to be created
* @param {string} roomId - The roomId to create the message in
*/
export function createSystemMessages(
message: string,
roomId?: string
): Promise<unknown> {
): Promise<unknown> {
const query = roomId ? { where: { roomId } } : undefined;
return prisma.rooms
.findMany(query)
@@ -257,19 +337,27 @@ export function createSystemMessages(
});
}
/**
* This function takes in an identity and a room and removes the identity from the room
* by setting its semaphoreIdentities to 0n and identities to 0n
* @param {string} idc - The identity of the user
* @param {RoomI} room - The room to remove the identity from
* @returns {Promise<void | RoomI>} - A promise that resolves to the room
*/
export function removeIdentityFromRoom(
idc: string,
room: RoomI
): Promise<void | RoomI> {
const updateSemaphoreIdentities = room.semaphoreIdentities?.map((identity) =>
identity === idc ? '0n' : identity as string
)!;
) ?? [];
const rateCommitmentsToUpdate = getRateCommitmentHash(BigInt(idc), BigInt(room.userMessageLimit!)).toString()
const updatedRateCommitments = room.identities?.map((limiter) =>
limiter == rateCommitmentsToUpdate ? '0n' : limiter as string
)
) ?? []
return prisma.rooms
.update({
@@ -294,6 +382,12 @@ export function removeIdentityFromRoom(
* @param {number} [userMessageLimit=1] - The message limit per user per epoch
* @param {number} [numClaimCodes=0] - The number of claim codes to generate for the room.
* @param {number} [approxNumMockUsers=20] - The approximate number of mock users to generate for the room.
* @param {string} [type='IDENTITY_LIST'] - The type of room to create.
* @param {string} [bandadaAddress] - The address of the bandada server.
* @param {string} [bandadaGroupId] - The id of the bandada group.
* @param {string} [bandadaAPIKey] - The API key for the bandada server.
* @param {string} [membershipType] - The membership type of the room.
* @returns {Promise<boolean>} - A promise that resolves to true if the room was created successfully.
*/
export async function createRoom(
roomName: string,

View File

@@ -15,6 +15,13 @@ interface CollisionCheckResult {
oldMessage?: MessageI;
}
/**
* This code is used to check if there is a collision in the room, and if there is, to recover the secret.
* It does this by checking if the message already exists in the DB, and if it does, it uses the secret recovery algorithm to recover the secret.
* @param {string} roomId - The ID of the room to check for collisions in
* @param {MessageI} message - The message to check for collisions with
* @returns {Promise<CollisionCheckResult>} - Returns a promise that resolves to a CollisionCheckResult
*/
async function checkRLNCollision(
roomId: string,
@@ -36,12 +43,15 @@ async function checkRLNCollision(
}
})
.then((oldMessage) => {
if (!message.proof) {
throw new Error('Proof not provided');
}
if (!oldMessage || !oldMessage?.epochs[0]?.messages) {
res({ collision: false } as CollisionCheckResult);
} else {
const oldMessageProof = JSON.parse(
oldMessage.epochs[0].messages[0].proof
) as RLNFullProof;
@@ -78,6 +88,14 @@ async function checkRLNCollision(
});
}
/**
* Adds a message to a room.
* @param {string} roomId - The ID of the room to add the message to.
* @param {MessageI} message - The message to add to the room.
* @returns {Promise<unknown>} - A promise that resolves when the message has been added to the room.
*/
function addMessageToRoom(roomId: string, message: MessageI): Promise<unknown> {
if (!message.epoch) {
throw new Error('Epoch not provided');
@@ -103,12 +121,19 @@ function addMessageToRoom(roomId: string, message: MessageI): Promise<unknown> {
}
});
}
export interface createMessageResult {
success: boolean;
message?: MessageI;
idc?: string | bigint;
}
/**
* Creates a message in a room
* @param {string} roomId - The ID of the room in which the message will be added
* @param {MessageI} message - The message to be created
* @returns {Promise<createMessageResult>} - A result object which contains a boolean indicating whether the operation was successful
*/
export function createMessage(
roomId: string,
message: MessageI
@@ -117,12 +142,10 @@ export function createMessage(
getRoomByID(roomId)
.then(async (room) => {
if (room) {
// Todo This should check that there is no duplicate messageId with in this room and epoch,
// if there is, we need to return an error and
// reconstruct the secret from both messages, and ban the user
await checkRLNCollision(roomId, message)
.then((collisionResult) => {
console.log('HERE', collisionResult);
if (!collisionResult.collision) {
addMessageToRoom(roomId, message)
.then(() => {
@@ -134,6 +157,7 @@ export function createMessage(
return reject({ success: false });
});
} else {
console.debug('Collision found');
const identityCommitment = getIdentityCommitmentFromSecret(
collisionResult.secret!

View File

@@ -30,11 +30,17 @@ function asyncHandler(fn: {
}
export function initEndpoints(app: Express, adminAuth: RequestHandler) {
// This code is used to fetch the server info from the api
// This is used to display the server info on the client side
app.get(['/', '/api'], (req, res) => {
pp('Express: fetching server info');
res.status(200).json(serverConfig);
});
// This code gets a room by its ID, and then checks if room is null.
// If room is null, it returns a 500 error.
// Otherwise, it returns a 200 status code and the room object.
app.get(['/room/:id', '/api/room/:id'], (req, res) => {
if (!req.params.id) {
res.status(400).json({ error: 'Bad Request' });
@@ -86,11 +92,15 @@ export function initEndpoints(app: Express, adminAuth: RequestHandler) {
}
});
/** This function gets the rooms that a user is a member of.
* @param {string} idc - The id of the identity to get rooms for.
* @returns {Array} - An array of room objects.
*/
app.get(
['/rooms/:idc', '/api/rooms/:idc'],
asyncHandler(async (req: Request, res: Response) => {
try {
pp(String('Express: fetching rooms by identityCommitment ' + req.params.idc));
res.status(200).json(await getRoomsByIdentity(req.params.idc));
} catch (error) {
console.error(error);
@@ -104,6 +114,19 @@ export function initEndpoints(app: Express, adminAuth: RequestHandler) {
idc: string;
}
/**
* This code is used to update the room identities of the rooms that the user is joining.
* The code updates the claim code and sets it to be claimed.
* It then updates the room identities of the user joining.
* Finally, it finds the rooms that have been updated and returns them.
* @param {string} code - The claim code to be updated
* @param {string} idc - The id of the identity to be added to the room
* @returns {Array} - An array of room objects
* @example {
* "code": "string",
* "idc": "string"
* }
*/
app.post(
['/join', '/api/join'],
asyncHandler(async (req: Request, res: Response) => {
@@ -115,21 +138,17 @@ export function initEndpoints(app: Express, adminAuth: RequestHandler) {
const { code, idc } = parsedBody;
console.debug('Invite Code:', code);
// Check if claim code is valid and not used before
const codeStatus = await findClaimCode(code);
if (!codeStatus || codeStatus.claimed) {
res.status(400).json({ message: 'Claim code already used' });
return;
}
// Update claim code
const claimCode = await updateClaimCode(code);
const roomIds = claimCode.roomIds;
// Update Room Identities
await updateRoomIdentities(idc, roomIds);
// Find updated rooms
const updatedRooms: RoomI[] = await findUpdatedRooms(roomIds);
// Return the room ids of the updated rooms
@@ -154,6 +173,33 @@ export function initEndpoints(app: Express, adminAuth: RequestHandler) {
}
/* ~~~~ ADMIN ENDPOINTS ~~~~ */
/** createRoom is used to create a new room in the database
* @param {string} roomName - The name of the room
* @param {number} rateLimit - The rate limit of the room
* @param {number} userMessageLimit - The user message limit of the room
* @param {number} numClaimCodes - The number of claim codes to generate
* @param {number} approxNumMockUsers - The approximate number of mock users to generate
* @param {string} type - The type of room
* @param {string} bandadaAddress - The address of the Bandada group
* @param {string} bandadaGroupId - The id of the Bandada group
* @param {string} bandadaAPIKey - The API key of the Bandada group
* @param {string} membershipType - The type of membership
* @returns {void}
* @example {
* "roomName": "string",
* "rateLimit": number,
* "userMessageLimit": number,
* "numClaimCodes": number, // optional
* "approxNumMockUsers": number, // optional
* "roomType": "string", // optional
* "bandadaAddress": "string", // optional
* "bandadaGroupId": "string", // optional
* "bandadaAPIKey": "string", // optional
* "membershipType": "string" // optional if not an IDENTITY_LIST
* }
*/
app.post(['/room/add', '/api/room/add'], adminAuth, (req, res) => {
const roomMetadata = req.body as addRoomData;
const roomName = roomMetadata.roomName;
@@ -192,6 +238,11 @@ export function initEndpoints(app: Express, adminAuth: RequestHandler) {
});
});
/*
This code handles the get request to get a list of messages for a particular room.
It uses the Prisma client to query the database and return the messages for a particular room.
It also parses the proof from a string to a JSON object.
*/
app.get('/api/room/:id/messages', (req, res) => {
const { id } = req.params;
prisma.messages
@@ -226,6 +277,26 @@ export function initEndpoints(app: Express, adminAuth: RequestHandler) {
res.status(500).send('Error fetching messages');
});
});
/**
* Endpoint to add claim codes to all rooms or a subset of rooms
* This code adds claim codes to the database.
* It is used by the admin panel to create claim codes.
* It takes in the number of codes to create, the rooms to add them to,
* and whether to add them to all rooms or just the selected ones.
* It generates the codes, then creates the ClaimCode objects in the database.
* The codes are added to the specified rooms, and are not claimed.
* @param {number} numCodes - The number of codes to add to the room
* @param {string[]} rooms - The ids of the rooms to add codes to
* @param {boolean} all - Whether to add codes to all rooms or just the selected ones
* @returns {void}
* @example {
* "numCodes": number,
* "rooms": string[], // optional
* "all": boolean
* }
*/
app.post(
['/addcode', '/api/addcode'],
adminAuth,
@@ -235,10 +306,12 @@ export function initEndpoints(app: Express, adminAuth: RequestHandler) {
rooms: string[];
all: boolean;
};
const query = all ? undefined : { where: { roomId: { in: rooms } } };
const codes = genClaimCodeArray(numCodes);
return await prisma.rooms.findMany(query).then((rooms) => {
const roomIds = rooms.map((room) => room.id);
const createCodes = codes.map(async (code, index) => {
return await prisma.claimCodes.create({
data: {
@@ -255,7 +328,7 @@ export function initEndpoints(app: Express, adminAuth: RequestHandler) {
});
return Promise.all(createCodes)
.then(() => {
res.status(200).json({ message: 'Claim codes added successfully' });
res.status(200).json({ message: 'Claim codes added successfully', codes });
})
.catch((err) => {
console.error(err);
@@ -264,6 +337,18 @@ export function initEndpoints(app: Express, adminAuth: RequestHandler) {
});
})
);
/**
* Adds claim codes to a room
*
* @param {number} numCodes The number of codes to add to the room
* @param {string} roomId The id of the room to add codes to
* @returns {void}
* @example {
* "numCodes": number
* }
*/
app.post(['/room/:roomId/addcode', '/api/room/:roomId/addcode'], adminAuth, (req, res) => {
const { roomId } = req.params;
const { numCodes } = req.body as { numCodes: number };
@@ -279,7 +364,7 @@ export function initEndpoints(app: Express, adminAuth: RequestHandler) {
res.status(404).json({ error: 'Room not found' });
return;
}
// Map over the codes array and create a claim code for each code
const createCodes = codes.map((code) => {
return prisma.claimCodes.create({
data: {
@@ -305,6 +390,8 @@ export function initEndpoints(app: Express, adminAuth: RequestHandler) {
});
});
// This code fetches the claim codes from the database.
app.get(['/logclaimcodes', '/api/logclaimcodes'], adminAuth, (req, res) => {
pp('Express: fetching claim codes');
prisma.claimCodes
@@ -318,6 +405,7 @@ export function initEndpoints(app: Express, adminAuth: RequestHandler) {
});
});
// GET all rooms from the database and return them as JSON
app.get(['/rooms', '/api/rooms'], adminAuth, (req, res) => {
pp(String('Express: fetching all rooms'));
prisma.rooms
@@ -331,32 +419,34 @@ export function initEndpoints(app: Express, adminAuth: RequestHandler) {
});
});
/**
* Sends system messages to the specified room, or all rooms if no room is specified
* @params {string} message - The message to send
* @params {string} roomId - The id of the room to send the message to
* @returns {void}
* @example {
* "message": "string",
* "roomId": "string" // optional
* }
*/
app.post(
'/admin/message',
adminAuth,
asyncHandler(async (req: Request, res: Response) => {
const { message } = req.body as { message: string };
pp(String('Express: sending system message: ' + message));
try {
await createSystemMessages(message);
res.status(200).json({ message: 'Messages sent to all rooms' });
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Internal Server Error' });
}
})
);
const { message, roomId } = req.body as { message: string; roomId?: string };
app.post(
'/admin/message/:roomId',
adminAuth,
asyncHandler(async (req: Request, res: Response) => {
const { roomId } = req.params;
const { message } = req.body as { message: string };
pp(String('Express: sending system message: ' + message + ' to ' + roomId));
try {
// Function to send system messages
await createSystemMessages(message, roomId);
res.status(200).json({ message: 'Message sent to room ' + roomId });
if (roomId) {
pp(`Express: sending system message: ${message} to ${roomId}`);
res.status(200).json({ message: `Message sent to room ${roomId}` });
} else {
pp(`Express: sending system message: ${message}`);
res.status(200).json({ message: 'Messages sent to all rooms' });
}
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Internal Server Error' });

View File

@@ -3,8 +3,8 @@ import { Socket, Server as SocketIOServer } from 'socket.io';
import verifyProof from '../crypto/verifier';
import { getRoomByID } from '../data/db';
import { pp } from '../utils';
import { createMessage, createMessageResult } from '../data/messages';
import { createMessage } from '../data/messages';
import type { createMessageResult } from '../data/messages';
const userCount: Record<string, number> = {};
export function websocketSetup(io: SocketIOServer) {
@@ -12,7 +12,10 @@ export function websocketSetup(io: SocketIOServer) {
pp('SocketIO: a user connected', 'debug');
socket.on('validateMessage', async (msg: MessageI) => {
pp({ 'VALIDATING MESSAGE ID': String(msg.roomId).slice(0, 11), 'MSG:': msg.message });
pp({
'VALIDATING MESSAGE ID': String(msg.roomId).slice(0, 11),
'MSG:': msg.message
});
let validProof: boolean;
await getRoomByID(String(msg.roomId))
.then((room: RoomI) => {
@@ -21,11 +24,14 @@ export function websocketSetup(io: SocketIOServer) {
return;
}
verifyProof(msg, room)
.then(async (v) => {
console.log('validProof', v)
.then(async (v) => {
console.log('validProof', v);
validProof = v;
// TODO import createMessageResult, and broadcast the idc and message ID that were removed to those room users
const validMessage: createMessageResult = await createMessage(String(msg.roomId), msg);
const validMessage: createMessageResult = await createMessage(
String(msg.roomId),
msg
);
if (!validProof || !validMessage.success) {
pp('INVALID MESSAGE', 'warn');
return;