@fnlb-project/fnbr
Version:
A library to interact with Epic Games' Fortnite HTTP and XMPP services
322 lines • 11.4 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.getUUID = exports.signCrossPlatformMessage = exports.generateCrossPlatformKeyPair = exports.decodeSTOMPMessageBody = exports.chunk = exports.resolveAuthObject = exports.resolveAuthString = exports.parseStatKey = exports.createDefaultInputTypeStats = exports.parseM3U8File = exports.parseBlurlStream = exports.createPartyInvitation = exports.getRandomDefaultCharacter = exports.makeSnakeCase = exports.makeCamelCase = void 0;
const tslib_1 = require("tslib");
/* eslint-disable no-restricted-syntax */
const zlib_1 = tslib_1.__importDefault(require("zlib"));
const fs_1 = require("fs");
const crypto_1 = tslib_1.__importDefault(require("crypto"));
const defaultCharacters = [
'CID_A_272_Athena_Commando_F_Prime',
'CID_A_273_Athena_Commando_F_Prime_B',
'CID_A_274_Athena_Commando_F_Prime_C',
'CID_A_275_Athena_Commando_F_Prime_D',
'CID_A_276_Athena_Commando_F_Prime_E',
'CID_A_277_Athena_Commando_F_Prime_F',
'CID_A_278_Athena_Commando_F_Prime_G',
'CID_A_279_Athena_Commando_M_Prime',
'CID_A_280_Athena_Commando_M_Prime_B',
'CID_A_281_Athena_Commando_M_Prime_C',
'CID_A_282_Athena_Commando_M_Prime_D',
'CID_A_283_Athena_Commando_M_Prime_E',
'CID_A_284_Athena_Commando_M_Prime_F',
'CID_A_285_Athena_Commando_M_Prime_G',
];
const makeCamelCase = (obj) => {
const returnObj = {};
Object.keys(obj).forEach((k) => {
returnObj[k.split('_').map((s, i) => (i > 0 ? `${s.charAt(0).toUpperCase()}${s.slice(1)}` : s)).join('')] = obj[k];
});
return returnObj;
};
exports.makeCamelCase = makeCamelCase;
const makeSnakeCase = (obj) => {
const returnObj = {};
Object.keys(obj).forEach((k) => {
returnObj[k.replace(/[A-Z]/g, (l) => `_${l.toLowerCase()}`)] = obj[k];
});
return returnObj;
};
exports.makeSnakeCase = makeSnakeCase;
const getRandomDefaultCharacter = () => defaultCharacters[Math.floor(Math.random() * defaultCharacters.length)];
exports.getRandomDefaultCharacter = getRandomDefaultCharacter;
const createPartyInvitation = (clientUserId, pingerId, data) => {
const member = data.members.find((m) => m.account_id === pingerId);
const partyMeta = data.meta;
const memberMeta = member.meta;
const meta = {
'urn:epic:conn:type_s': 'game',
'urn:epic:cfg:build-id_s': partyMeta['urn:epic:cfg:build-id_s'],
'urn:epic:invite:platformdata_s': '',
};
if (memberMeta.Platform_j) {
meta['Platform_j'] = JSON.parse(memberMeta.Platform_j).Platform.platformStr;
}
if (memberMeta['urn:epic:member:dn_s'])
meta['urn:epic:member:dn_s'] = memberMeta['urn:epic:member:dn_s'];
return {
party_id: data.id,
sent_by: pingerId,
sent_to: clientUserId,
sent_at: data.sent,
updated_at: data.sent,
expires_at: data.expies_at,
status: 'SENT',
meta,
};
};
exports.createPartyInvitation = createPartyInvitation;
const parseBlurlStream = (stream) => new Promise((res) => {
zlib_1.default.inflate(stream.slice(8), (_err, buffer) => {
const data = JSON.parse(buffer.toString());
res(data);
});
});
exports.parseBlurlStream = parseBlurlStream;
const parseM3U8FileLine = (line) => {
const [key, value] = line.replace(/^#EXT-X-/, '').split(/:(.+)/);
let output;
if (value.includes(',')) {
output = {};
let store = '';
let isString = false;
for (const char of value.split('')) {
if (char === '"') {
isString = !isString;
}
else if (char === ',' && !isString) {
const [vK, vV] = store.split(/=(.+)/);
output[vK] = vV.replace(/(^"|"$)/g, '');
store = '';
}
else {
store += char;
}
}
}
else {
output = value;
}
return [key, output];
};
const parseM3U8File = (data) => {
const output = {
streams: [],
};
let streamInf;
for (const line of data.split(/\n/).slice(1)) {
if (line.startsWith('#EXT-X-STREAM-INF:')) {
[, streamInf] = parseM3U8FileLine(line);
}
else if (line.startsWith('#EXT-X-')) {
const [key, value] = parseM3U8FileLine(line);
output[key] = value;
}
else if (!line.startsWith('#') && streamInf && line.length > 0) {
output.streams.push({
data: streamInf,
url: line,
});
streamInf = undefined;
}
}
return output;
};
exports.parseM3U8File = parseM3U8File;
const defaultStats = {
score: 0,
scorePerMin: 0,
scorePerMatch: 0,
wins: 0,
top3: 0,
top5: 0,
top6: 0,
top10: 0,
top12: 0,
top25: 0,
kills: 0,
killsPerMin: 0,
killsPerMatch: 0,
deaths: 0,
kd: 0,
matches: 0,
winRate: 0,
minutesPlayed: 0,
playersOutlived: 0,
lastModified: undefined,
};
const createDefaultInputTypeStats = () => ({
overall: { ...defaultStats },
solo: { ...defaultStats },
duo: { ...defaultStats },
squad: { ...defaultStats },
ltm: { ...defaultStats },
});
exports.createDefaultInputTypeStats = createDefaultInputTypeStats;
const parseStatKey = (key, value) => {
switch (key) {
case 'lastmodified':
return ['lastModified', new Date(value * 1000)];
case 'placetop25':
return ['top25', value];
case 'placetop12':
return ['top12', value];
case 'placetop10':
return ['top10', value];
case 'placetop6':
return ['top6', value];
case 'placetop5':
return ['top5', value];
case 'placetop3':
return ['top3', value];
case 'placetop1':
return ['wins', value];
case 'playersoutlived':
return ['playersOutlived', value];
case 'minutesplayed':
return ['minutesPlayed', value];
case 'matchesplayed':
return ['matches', value];
default:
return [key, value];
}
};
exports.parseStatKey = parseStatKey;
const resolveAuthString = async (str) => {
switch (typeof str) {
case 'function':
return str();
case 'string':
if (str.length === 32 || str.startsWith('eg1')) {
return str;
}
return fs_1.promises.readFile(str, 'utf8');
default:
throw new TypeError(`The type "${typeof str}" does not resolve to a valid auth string`);
}
};
exports.resolveAuthString = resolveAuthString;
const resolveAuthObject = async (obj) => {
switch (typeof obj) {
case 'function':
return obj();
case 'string':
return JSON.parse(await fs_1.promises.readFile(obj, 'utf8'));
case 'object':
return obj;
default:
throw new TypeError(`The type "${typeof obj}" does not resolve to a valid auth object`);
}
};
exports.resolveAuthObject = resolveAuthObject;
const chunk = (array, maxSize) => {
const chunkedArray = [];
for (let i = 0; i < array.length; i += maxSize) {
chunkedArray.push(array.slice(i, i + maxSize));
}
return chunkedArray;
};
exports.chunk = chunk;
const decodeSTOMPMessageBody = (body) => {
var _a;
if (!body)
return '';
try {
const decoded = Buffer.from(body, 'base64')
.toString('utf-8')
.replace(/\0+$/, '');
try {
return (_a = JSON.parse(decoded).msg) !== null && _a !== void 0 ? _a : decoded;
}
catch {
return decoded;
}
}
catch {
return '';
}
};
exports.decodeSTOMPMessageBody = decodeSTOMPMessageBody;
const getSubtleCrypto = () => {
if (typeof globalThis !== 'undefined' && globalThis.crypto && globalThis.crypto.subtle) {
return globalThis.crypto.subtle;
}
return undefined;
};
const arrayBufferToBase64 = (buffer) => {
const bytes = new Uint8Array(buffer);
if (typeof btoa === 'function') {
let binary = '';
const len = bytes.byteLength;
for (let i = 0; i < len; i++) {
binary += String.fromCharCode(bytes[i]);
}
return btoa(binary);
}
if (typeof Buffer !== 'undefined') {
return Buffer.from(bytes).toString('base64');
}
throw new Error('Base64 encoding not supported in this environment');
};
const generateCrossPlatformKeyPair = async () => {
if (crypto_1.default && typeof crypto_1.default.generateKeyPairSync === 'function') {
const { privateKey, publicKey } = crypto_1.default.generateKeyPairSync('ed25519');
const spkiDer = publicKey.export({ type: 'spki', format: 'der' });
const publicKeyB64 = spkiDer.subarray(spkiDer.length - 32).toString('base64');
return {
privateKey,
publicKeyB64,
};
}
const subtle = getSubtleCrypto();
if (subtle) {
const keyPair = (await subtle.generateKey({ name: 'Ed25519' }, true, ['sign', 'verify']));
const rawKeyBuffer = await subtle.exportKey('spki', keyPair.publicKey);
const publicKeyB64 = arrayBufferToBase64(rawKeyBuffer);
return {
privateKey: keyPair.privateKey,
publicKeyB64,
};
}
throw new Error('No cryptographic key generation API is available in this environment.');
};
exports.generateCrossPlatformKeyPair = generateCrossPlatformKeyPair;
const signCrossPlatformMessage = async (privateKey, data) => {
if (typeof CryptoKey !== 'undefined' && privateKey instanceof CryptoKey) {
const subtle = getSubtleCrypto();
if (!subtle) {
throw new Error('Web Crypto subtle API not found');
}
const signBuffer = new Uint8Array(data);
const signatureBuffer = await subtle.sign({ name: 'Ed25519' }, privateKey, signBuffer);
return arrayBufferToBase64(signatureBuffer);
}
if (crypto_1.default && typeof crypto_1.default.sign === 'function') {
const signature = crypto_1.default.sign(null, data, privateKey);
return signature.toString('base64');
}
if (privateKey && typeof privateKey === 'object' && privateKey.type === 'private' && privateKey.algorithm) {
const subtle = getSubtleCrypto();
if (subtle) {
const signatureBuffer = await subtle.sign({ name: 'Ed25519' }, privateKey, data);
return arrayBufferToBase64(signatureBuffer);
}
}
throw new Error('No cryptographic signing API is available or private key type is unsupported.');
};
exports.signCrossPlatformMessage = signCrossPlatformMessage;
const getUUID = () => {
if (typeof crypto_1.default !== 'undefined' && typeof crypto_1.default.randomUUID === 'function') {
return crypto_1.default.randomUUID();
}
if (typeof globalThis !== 'undefined' && globalThis.crypto && typeof globalThis.crypto.randomUUID === 'function') {
return globalThis.crypto.randomUUID();
}
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
const r = Math.random() * 16 | 0;
const v = c === 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
});
};
exports.getUUID = getUUID;
//# sourceMappingURL=Util.js.map