serverquery
Version:
Low level TeamSpeak™ 3 ServerQuery protocol implementation
162 lines (125 loc) • 3.62 kB
JavaScript
const LIST_DELIMETER = '|';
const LIST_DELIMETER_REGEX = /\|/g;
const PARAMS_DELIMETER = ' ';
const PARAMS_DELIMETER_REGEX = / +/g;
const ASSIGNMENT = '=';
const ASSIGNMENT_REGEX = /^(\w+)(?:=(.*))?$/;
const INTEGER_REGEX = /^[0-9]+$/;
const FLOAT_REGEX = /^[0-9]+.[0-9]+$/;
/**
* Escape a string.
* @param {String} str
* Adapted from: https://github.com/gwTumm/node-teamspeak/blob/master/index.js
*/
export function escape(str) {
return String(str)
.replace(/\\/g, '\\\\') // Backslash
.replace(/\//g, '\\/') // Slash
.replace(/\|/g, '\\p') // Pipe
.replace(/\n/g, '\\n') // Newline
.replace(/\r/g, '\\r') // Carriage Return
.replace(/\t/g, '\\t') // Tab
.replace(/\v/g, '\\v') // Vertical Tab
.replace(/\f/g, '\\f') // Formfeed
.replace(/ /g, '\\s'); // Whitespace
}
/**
* Unescape a string.
* @param {String} str
* Adapted from: https://github.com/gwTumm/node-teamspeak/blob/master/index.js
*/
export function unescape(str) {
return String(str)
.replace(/\\s/g, ' ') // Whitespace
.replace(/\\p/g, '|') // Pipe
.replace(/\\n/g, '\n') // Newline
.replace(/\\f/g, '\f') // Formfeed
.replace(/\\r/g, '\r') // Carriage Return
.replace(/\\t/g, '\t') // Tab
.replace(/\\v/g, '\v') // Vertical Tab
.replace(/\\\//g, '\/') // Slash
.replace(/\\\\/g, '\\'); // Backslash
}
/**
* Serialize a key: camelCase -> snake_case
* @param {String} key
*/
export function serializeKey(key) {
return key
.replace(/[a-z][A-Z]/g, match => match[0] + '_' + match[1].toLowerCase());
}
/**
* Deserialize a key: snake_case -> camelCase
* @param {String} key
*/
export function deserializeKey(key) {
return key
.replace(/_[a-z]/g, match => match[1].toUpperCase());
}
/**
* Serialize a value.
* @param {String|[String]} value
*/
export function serializeValue(value) {
if (value instanceof Array) {
return value.map(escape).join('|');
}
return escape(value);
}
/**
* Deserialize a value.
* @param {String} value
*/
export function deserializeValue(value) {
// parse as number, if it is number like
if (INTEGER_REGEX.test(value)) {
return Number.parseInt(value, 10);
}
if (FLOAT_REGEX.test(value)) {
return Number.parseFloat(value);
}
return unescape(value);
}
/**
* Serialize a command.
* @param {String} cmd
* @param {[String]} flags
* @param {Object} params
*/
export function serializeCommand(cmd, flags, params) {
if (flags.length) {
cmd += ' -' + flags.join(' -');
}
params = Object.keys(params)
.map(k => `${serializeKey(k)}=${serializeValue(params[k])}`)
.join(' ');
if (params) {
cmd += ' ' + params;
}
return cmd + '\r\n';
}
/**
* Parse a response.
* @param {String} response
* @param {Boolean} skipListCheck
*/
export function parseResponse(response, skipListCheck = false) {
// got a list as response
if (!skipListCheck && LIST_DELIMETER_REGEX.test(response)) {
return response
.split(LIST_DELIMETER_REGEX)
.map(item => parseResponse(item, true));
}
let fields = Object.create(null);
response
.split(PARAMS_DELIMETER_REGEX)
.forEach(pair => {
let [, key, value] = pair.match(ASSIGNMENT_REGEX);
if (value) {
fields[deserializeKey(key)] = deserializeValue(value);
} else {
fields[deserializeKey(key)] = null;
}
});
return fields;
}