
- Complete NestJS TypeScript implementation with WebSocket support - Direct messaging (DM) and group chat functionality - End-to-end encryption with AES encryption and key pairs - Media file support (images, videos, audio, documents) up to 100MB - Push notifications with Firebase Cloud Messaging integration - Mention alerts and real-time typing indicators - User authentication with JWT and Passport - SQLite database with TypeORM entities and relationships - Comprehensive API documentation with Swagger/OpenAPI - File upload handling with secure access control - Online/offline status tracking and presence management - Message editing, deletion, and reply functionality - Notification management with automatic cleanup - Health check endpoint for monitoring - CORS configuration for cross-origin requests - Environment-based configuration management - Structured for Flutter SDK integration Features implemented: ✅ Real-time messaging with Socket.IO ✅ User registration and authentication ✅ Direct messages and group chats ✅ Media file uploads and management ✅ End-to-end encryption ✅ Push notifications ✅ Mention alerts ✅ Typing indicators ✅ Message read receipts ✅ Online status tracking ✅ File access control ✅ Comprehensive API documentation Ready for Flutter SDK development and production deployment.
322 lines
7.0 KiB
JavaScript
322 lines
7.0 KiB
JavaScript
const objectToString = Object.prototype.toString;
|
|
const uint8ArrayStringified = '[object Uint8Array]';
|
|
const arrayBufferStringified = '[object ArrayBuffer]';
|
|
|
|
function isType(value, typeConstructor, typeStringified) {
|
|
if (!value) {
|
|
return false;
|
|
}
|
|
|
|
if (value.constructor === typeConstructor) {
|
|
return true;
|
|
}
|
|
|
|
return objectToString.call(value) === typeStringified;
|
|
}
|
|
|
|
export function isUint8Array(value) {
|
|
return isType(value, Uint8Array, uint8ArrayStringified);
|
|
}
|
|
|
|
function isArrayBuffer(value) {
|
|
return isType(value, ArrayBuffer, arrayBufferStringified);
|
|
}
|
|
|
|
function isUint8ArrayOrArrayBuffer(value) {
|
|
return isUint8Array(value) || isArrayBuffer(value);
|
|
}
|
|
|
|
export function assertUint8Array(value) {
|
|
if (!isUint8Array(value)) {
|
|
throw new TypeError(`Expected \`Uint8Array\`, got \`${typeof value}\``);
|
|
}
|
|
}
|
|
|
|
export function assertUint8ArrayOrArrayBuffer(value) {
|
|
if (!isUint8ArrayOrArrayBuffer(value)) {
|
|
throw new TypeError(`Expected \`Uint8Array\` or \`ArrayBuffer\`, got \`${typeof value}\``);
|
|
}
|
|
}
|
|
|
|
export function toUint8Array(value) {
|
|
if (value instanceof ArrayBuffer) {
|
|
return new Uint8Array(value);
|
|
}
|
|
|
|
if (ArrayBuffer.isView(value)) {
|
|
return new Uint8Array(value.buffer, value.byteOffset, value.byteLength);
|
|
}
|
|
|
|
throw new TypeError(`Unsupported value, got \`${typeof value}\`.`);
|
|
}
|
|
|
|
export function concatUint8Arrays(arrays, totalLength) {
|
|
if (arrays.length === 0) {
|
|
return new Uint8Array(0);
|
|
}
|
|
|
|
totalLength ??= arrays.reduce((accumulator, currentValue) => accumulator + currentValue.length, 0);
|
|
|
|
const returnValue = new Uint8Array(totalLength);
|
|
|
|
let offset = 0;
|
|
for (const array of arrays) {
|
|
assertUint8Array(array);
|
|
returnValue.set(array, offset);
|
|
offset += array.length;
|
|
}
|
|
|
|
return returnValue;
|
|
}
|
|
|
|
export function areUint8ArraysEqual(a, b) {
|
|
assertUint8Array(a);
|
|
assertUint8Array(b);
|
|
|
|
if (a === b) {
|
|
return true;
|
|
}
|
|
|
|
if (a.length !== b.length) {
|
|
return false;
|
|
}
|
|
|
|
// eslint-disable-next-line unicorn/no-for-loop
|
|
for (let index = 0; index < a.length; index++) {
|
|
if (a[index] !== b[index]) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
export function compareUint8Arrays(a, b) {
|
|
assertUint8Array(a);
|
|
assertUint8Array(b);
|
|
|
|
const length = Math.min(a.length, b.length);
|
|
|
|
for (let index = 0; index < length; index++) {
|
|
const diff = a[index] - b[index];
|
|
if (diff !== 0) {
|
|
return Math.sign(diff);
|
|
}
|
|
}
|
|
|
|
// At this point, all the compared elements are equal.
|
|
// The shorter array should come first if the arrays are of different lengths.
|
|
return Math.sign(a.length - b.length);
|
|
}
|
|
|
|
const cachedDecoders = {
|
|
utf8: new globalThis.TextDecoder('utf8'),
|
|
};
|
|
|
|
export function uint8ArrayToString(array, encoding = 'utf8') {
|
|
assertUint8ArrayOrArrayBuffer(array);
|
|
cachedDecoders[encoding] ??= new globalThis.TextDecoder(encoding);
|
|
return cachedDecoders[encoding].decode(array);
|
|
}
|
|
|
|
function assertString(value) {
|
|
if (typeof value !== 'string') {
|
|
throw new TypeError(`Expected \`string\`, got \`${typeof value}\``);
|
|
}
|
|
}
|
|
|
|
const cachedEncoder = new globalThis.TextEncoder();
|
|
|
|
export function stringToUint8Array(string) {
|
|
assertString(string);
|
|
return cachedEncoder.encode(string);
|
|
}
|
|
|
|
function base64ToBase64Url(base64) {
|
|
return base64.replaceAll('+', '-').replaceAll('/', '_').replace(/=+$/, '');
|
|
}
|
|
|
|
function base64UrlToBase64(base64url) {
|
|
return base64url.replaceAll('-', '+').replaceAll('_', '/');
|
|
}
|
|
|
|
// Reference: https://phuoc.ng/collection/this-vs-that/concat-vs-push/
|
|
const MAX_BLOCK_SIZE = 65_535;
|
|
|
|
export function uint8ArrayToBase64(array, {urlSafe = false} = {}) {
|
|
assertUint8Array(array);
|
|
|
|
let base64;
|
|
|
|
if (array.length < MAX_BLOCK_SIZE) {
|
|
// Required as `btoa` and `atob` don't properly support Unicode: https://developer.mozilla.org/en-US/docs/Glossary/Base64#the_unicode_problem
|
|
base64 = globalThis.btoa(String.fromCodePoint.apply(this, array));
|
|
} else {
|
|
base64 = '';
|
|
for (const value of array) {
|
|
base64 += String.fromCodePoint(value);
|
|
}
|
|
|
|
base64 = globalThis.btoa(base64);
|
|
}
|
|
|
|
return urlSafe ? base64ToBase64Url(base64) : base64;
|
|
}
|
|
|
|
export function base64ToUint8Array(base64String) {
|
|
assertString(base64String);
|
|
return Uint8Array.from(globalThis.atob(base64UrlToBase64(base64String)), x => x.codePointAt(0));
|
|
}
|
|
|
|
export function stringToBase64(string, {urlSafe = false} = {}) {
|
|
assertString(string);
|
|
return uint8ArrayToBase64(stringToUint8Array(string), {urlSafe});
|
|
}
|
|
|
|
export function base64ToString(base64String) {
|
|
assertString(base64String);
|
|
return uint8ArrayToString(base64ToUint8Array(base64String));
|
|
}
|
|
|
|
const byteToHexLookupTable = Array.from({length: 256}, (_, index) => index.toString(16).padStart(2, '0'));
|
|
|
|
export function uint8ArrayToHex(array) {
|
|
assertUint8Array(array);
|
|
|
|
// Concatenating a string is faster than using an array.
|
|
let hexString = '';
|
|
|
|
// eslint-disable-next-line unicorn/no-for-loop -- Max performance is critical.
|
|
for (let index = 0; index < array.length; index++) {
|
|
hexString += byteToHexLookupTable[array[index]];
|
|
}
|
|
|
|
return hexString;
|
|
}
|
|
|
|
const hexToDecimalLookupTable = {
|
|
0: 0,
|
|
1: 1,
|
|
2: 2,
|
|
3: 3,
|
|
4: 4,
|
|
5: 5,
|
|
6: 6,
|
|
7: 7,
|
|
8: 8,
|
|
9: 9,
|
|
a: 10,
|
|
b: 11,
|
|
c: 12,
|
|
d: 13,
|
|
e: 14,
|
|
f: 15,
|
|
A: 10,
|
|
B: 11,
|
|
C: 12,
|
|
D: 13,
|
|
E: 14,
|
|
F: 15,
|
|
};
|
|
|
|
export function hexToUint8Array(hexString) {
|
|
assertString(hexString);
|
|
|
|
if (hexString.length % 2 !== 0) {
|
|
throw new Error('Invalid Hex string length.');
|
|
}
|
|
|
|
const resultLength = hexString.length / 2;
|
|
const bytes = new Uint8Array(resultLength);
|
|
|
|
for (let index = 0; index < resultLength; index++) {
|
|
const highNibble = hexToDecimalLookupTable[hexString[index * 2]];
|
|
const lowNibble = hexToDecimalLookupTable[hexString[(index * 2) + 1]];
|
|
|
|
if (highNibble === undefined || lowNibble === undefined) {
|
|
throw new Error(`Invalid Hex character encountered at position ${index * 2}`);
|
|
}
|
|
|
|
bytes[index] = (highNibble << 4) | lowNibble; // eslint-disable-line no-bitwise
|
|
}
|
|
|
|
return bytes;
|
|
}
|
|
|
|
/**
|
|
@param {DataView} view
|
|
@returns {number}
|
|
*/
|
|
export function getUintBE(view) {
|
|
const {byteLength} = view;
|
|
|
|
if (byteLength === 6) {
|
|
return (view.getUint16(0) * (2 ** 32)) + view.getUint32(2);
|
|
}
|
|
|
|
if (byteLength === 5) {
|
|
return (view.getUint8(0) * (2 ** 32)) + view.getUint32(1);
|
|
}
|
|
|
|
if (byteLength === 4) {
|
|
return view.getUint32(0);
|
|
}
|
|
|
|
if (byteLength === 3) {
|
|
return (view.getUint8(0) * (2 ** 16)) + view.getUint16(1);
|
|
}
|
|
|
|
if (byteLength === 2) {
|
|
return view.getUint16(0);
|
|
}
|
|
|
|
if (byteLength === 1) {
|
|
return view.getUint8(0);
|
|
}
|
|
}
|
|
|
|
/**
|
|
@param {Uint8Array} array
|
|
@param {Uint8Array} value
|
|
@returns {number}
|
|
*/
|
|
export function indexOf(array, value) {
|
|
const arrayLength = array.length;
|
|
const valueLength = value.length;
|
|
|
|
if (valueLength === 0) {
|
|
return -1;
|
|
}
|
|
|
|
if (valueLength > arrayLength) {
|
|
return -1;
|
|
}
|
|
|
|
const validOffsetLength = arrayLength - valueLength;
|
|
|
|
for (let index = 0; index <= validOffsetLength; index++) {
|
|
let isMatch = true;
|
|
for (let index2 = 0; index2 < valueLength; index2++) {
|
|
if (array[index + index2] !== value[index2]) {
|
|
isMatch = false;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (isMatch) {
|
|
return index;
|
|
}
|
|
}
|
|
|
|
return -1;
|
|
}
|
|
|
|
/**
|
|
@param {Uint8Array} array
|
|
@param {Uint8Array} value
|
|
@returns {boolean}
|
|
*/
|
|
export function includes(array, value) {
|
|
return indexOf(array, value) !== -1;
|
|
}
|