MahjongSoulAPI/index.js

362 lines
10 KiB
JavaScript

const commandLineArgs = require('command-line-args'),
commandLineUsage = require('command-line-usage'),
https = require('https'),
qstr = require('querystring'),
url = require('url'),
fs = require('fs'),
pbjs = require("protobufjs/cli/pbjs"),
WebSocketClient = require('websocket').client,
MS_HOST = 'mahjongsoul.game.yo-star.com',
opts = [{
name: 'help',
alias: 'h',
description: 'Display this usage guide',
type: Boolean
}, {
name: 'servers',
alias: 'l',
description: 'Get server list',
type: Boolean
}, {
name: 'server',
alias: 's',
description: 'Use custom server within check',
type: String
}, {
name: 'trueserver',
alias: 'S',
description: 'Use custom server without check',
type: String
}, {
name: 'update',
alias: 'u',
description: 'Update protocol',
type: Boolean
}, {
name: 'info',
alias: 'i',
description: 'Get info',
type: Boolean
}, {
name: 'docs',
alias: 'd',
description: 'Generate documentation from liqi.json',
type: Boolean
}],
sections = [{
header: 'Mahjong Soul RPC client',
content: 'RPC client for Mahjong Soul API'
}, {
header: 'Options',
optionList: opts
}],
args = commandLineArgs(opts);
let ProtoBuf = require("protobufjs");
const getServers = (serv = null, nocheck = false) => {
return new Promise((resolve, reject) => {
if (serv && nocheck) {
resolve([serv]);
} else {
let ms_opts = {
hostname: MS_HOST,
port: 443,
path: '/version.json',
method: 'GET'
};
// Запрос версии
const reqVersion = https.request(ms_opts, (resVersion) => {
resVersion.on('data', (dataVersion) => {
let resVers = JSON.parse(dataVersion);
ms_opts.path = `/v${resVers.version}/config.json`;
// Запрос конфигурации
const reqConfig = https.request(ms_opts, (resConfig) => {
resConfig.on('data', (dataConfig) => {
let resCfg = JSON.parse(dataConfig);
const mainland = url.parse(resCfg.ip[0].region_urls[0]);
ms_opts.hostname = mainland.hostname;
ms_opts.port = mainland.port;
ms_opts.data = {
service: 'ws-gateway',
protocol: 'ws',
ssl: 'true'
};
ms_opts.path = `${mainland.pathname}?${qstr.stringify(ms_opts.data)}`;
// Запрос серверов
const reqServers = https.request(ms_opts, (resServers) => {
resServers.on('data', (dataServers) => {
let resServs = JSON.parse(dataServers);
if (serv && !nocheck) {
if (resServs.servers.indexOf(serv) >= 0) {
resolve([serv]);
} else {
reject(`Wrong server: ${serv}`);
}
} else {
resolve(resServs.servers);
}
});
});
reqServers.on('error', (e) => {
reject(`Error: ${e.message}`);
});
reqServers.end();
});
});
reqConfig.on('error', (e) => {
reject(`Error: ${e.message}`);
});
reqConfig.end();
});
});
reqVersion.on('error', (e) => {
reject(`Error: ${e.message}`);
});
reqVersion.end();
}
});
};
// version URL https://mahjongsoul.game.yo-star.com/version.json
const getLastVersion = () => {
return new Promise((resolve, reject) => {
let ms_opts = {
hostname: MS_HOST,
port: 443,
path: '/version.json',
method: 'GET'
};
const reqVersion = https.request(ms_opts, (resVersion) => {
let dataVersion = '';
resVersion.on('data', (chunk) => {
dataVersion += chunk;
});
resVersion.on('end', () => {
let resVers = JSON.parse(dataVersion);
resolve(resVers.version);
});
});
reqVersion.on('error', (e) => {
reject(`Error: ${e.message}`);
});
reqVersion.end();
});
};
const sendRequest = (server, package_name, service_name, method_name, params, idx = 1) => {
return new Promise((resolve, reject) => {
const proto = ProtoBuf.loadSync('liqi.json'),
client = new WebSocketClient(),
service = proto.lookup(`${package_name}.${service_name}`),
method = service.methods[method_name].resolve(),
res = method.resolvedResponseType,
wrapper = proto.lookupType(`${package_name}.Wrapper`);
let meth_str = wrapper.create({
name: `${package_name}.${service_name}.${method_name}`,
data: wrapper.encode(params).finish()
});
let msg = wrapper.encode(meth_str).finish(),
pkt_type = [Number(2)],
idx_bytes = to_bytes(idx);
let header = pkt_type.concat(idx_bytes),
pkt = Buffer.concat([Buffer.from(header), Buffer.from(msg)]);
console.log(`method: .${package_name}.${service_name}.${method_name}`);
console.log(`request: ${JSON.stringify(params, null, 2)}`);
client.on('connect', function(connection) {
console.log('WebSocket Client Connected');
connection.on('error', function(error) {
reject(error.toString());
});
connection.on('close', function() {
console.log('Connection Closed');
});
connection.on('message', function(message) {
console.log(message);
if (message.type === 'binary') {
let r = message.binaryData.slice(3),
msg = res.decode(r);
resolve(msg);
connection.close(connection.CLOSE_REASON_NORMAL, 'Close');
}
});
const sendPacket = (packet) => {
if (connection.connected) {
console.log('Send data: ', packet.toString('hex'));
connection.sendBytes(packet);
}
};
sendPacket(pkt);
});
client.connect(`wss://${server}`);
});
};
const random = (arr) => {
return arr[Math.floor((Math.random() * arr.length))];
};
const to_bytes = (x) => {
return [(x & 255), x >> 8];
};
if (!args || args.help) {
console.log(commandLineUsage(sections));
} else {
if (args.update) {
console.log('Protocol update:');
getLastVersion().then(
resGameVersion => {
console.log(` Game version: ${resGameVersion}`);
// Resource version URL https://mahjongsoul.game.yo-star.com/resversion(version).json
const resvers_opts = {
hostname: MS_HOST,
port: 443,
path: `/resversion${resGameVersion}.json`,
method: 'GET'
};
const reqVersion = https.request(resvers_opts, (resVersion) => {
let dataProto = '';
resVersion.on('data', (chunk) => {
dataProto += chunk;
});
resVersion.on('end', () => {
const resJSON = JSON.parse(dataProto),
liqiVersion = resJSON.res['res/proto/liqi.json'].prefix;
let res_list = '';
Object.keys(resJSON.res).forEach((res_id) => {
res_list += `https://${MS_HOST}/${resJSON.res[res_id].prefix}/${res_id}\n`;
});
fs.writeFileSync(`${__dirname}/res_list.txt`, res_list);
console.log(' Save res_list.txt');
console.log(` Liqi version: ${liqiVersion}`);
// Resource version URL https://mahjongsoul.game.yo-star.com/(versionLiqi)/res/proto/liqi.json
const proto_opts = {
hostname: MS_HOST,
port: 443,
path: `/${liqiVersion}/res/proto/liqi.json`,
method: 'GET'
};
const reqProto = https.request(proto_opts, (resProto) => {
let dataProto = '';
resProto.on('data', (chunk) => {
dataProto += chunk;
});
resProto.on('end', () => {
let resJSON = JSON.parse(dataProto);
fs.writeFileSync(`${__dirname}/liqi.json`, JSON.stringify(resJSON, null, 2));
console.log(' Save liqi.json');
// Если нужен proto раскомментить
/*
pbjs.main(['-t', 'proto3', `${__dirname}/liqi.json`, '-o', `${__dirname}/liqi.proto`], function(err, output) {
if (err) throw err;
console.log(' Save to proto');
});
*/
});
});
reqProto.on('error', (e) => {
console.log(`Error: ${e.message}`);
});
reqProto.end();
});
});
reqVersion.on('error', (e) => {
console.log(`Error: ${e.message}`);
});
reqVersion.end();
},
errGameVersion => {
console.log(`Error: ${errGameVersion}`);
}
);
}
if (args.servers) {
getServers().then(
resServer => {
console.log('Server list:');
for (let i in resServer) {
console.log(` ${resServer[i]}`);
}
console.log(`Random server: ${random(resServer)}`);
},
errServer => {
console.error(errServer);
}
);
}
if (args.info) {
if (!fs.existsSync(`${__dirname}/liqi.json`)) {
console.log('Protocol file not found. Run update with option --update');
} else {
getServers((args.server ? args.server : args.trueserver), args.trueserver).then(
resServer => {
let server = random(resServer);
if (server) {
console.log(`Use server: ${server}`);
let idx = Math.floor(60007 * Math.random()),
params = {
code: '111111',
operation: 0
};
sendRequest(server, 'lq', 'Lobby', 'verfifyCodeForSecure', params, idx).then(
resSend => {
console.log(`Result: ${JSON.stringify(resSend, null, 2)}`);
},
errSend => {
console.error(`Error: ${errSend}`);
}
);
} else {
console.error(`Wrong server: ${args.server}`);
}
},
errServer => {
console.error(errServer);
}
);
}
}
if (args.docs) {
if (!fs.existsSync(`${__dirname}/liqi.json`)) {
console.log('Protocol file not found. Run update with option --update');
} else {
const liqi = require(`${__dirname}/liqi.json`),
types = new RegExp(/uint32|bool|string|int32/);
let doc = "# MahjongSoul protocol documentation\n\n"
items = liqi.nested.lq.nested;
for (const [key, item] of Object.entries(items)) {
doc += `## ${key}\n\n`;
if (item.fields) {
let fields = [];
for (let [name, field] of Object.entries(item.fields)) {
fields[field.id] = {
name: name,
type: field.type
};
}
if (fields.length > 0) {
doc += "| N | Field name | Field type |\n| --- | --- | --- |\n";
for (let i = 1; i <= fields.length; i++) {
if (fields[i]) {
doc += `| ${i} | ${fields[i].name} | ${(types.test(fields[i].type) ? fields[i].type : '['+fields[i].type+'](#'+fields[i].type.toLowerCase()+')')} |\n`;
}
}
} else {
doc += "No fields\n";
}
doc += "\n";
} else if (item.methods) {
const methods = item.methods;
for (const [key, method] of Object.entries(methods)) {
doc += `* ${key}([${method.responseType}](#${method.responseType.toLowerCase()}) return [${method.requestType}](#${method.requestType.toLowerCase()})\n`;
}
}
}
fs.writeFileSync(`${__dirname}/liqi.md`, doc);
console.log('Save liqi.md');
}
}
}