oicq
Version:
QQ protocol!
579 lines (525 loc) • 18.1 kB
JavaScript
/**
* api
*/
"use strict";
const version = require("../package.json");
version.app_name = version.name;
version.app_version = version.version;
version.protocol_version = "v11";
const { EventEmitter } = require("events");
const fs = require("fs");
const path = require("path");
const os = require("os");
const { exec } = require("child_process");
const { randomBytes } = require("crypto");
const log4js = require("log4js");
const Network = require("./client-net");
const { getApkInfo, getDeviceInfo } = require("./device");
const { timestamp, md5, BUF0 } = require("./common");
const { onlineListener, offlineListener, packetListener, networkErrorListener } = require("./oicq");
const frdlst = require("./core/friendlist");
const sysmsg = require("./core/sysmsg");
const troop = require("./core/troop");
const nessy = require("./core/nessy");
const wt = require("./wtlogin/wt");
const chat = require("./message/chat");
const { Gfs } = require("./message/file");
const { getErrorMessage, TimeoutError } = require("./exception");
function buildApiRet(retcode, data = null, error = null) {
data = data ? data : null;
error = error ? error : null;
const status = retcode ? (retcode === 1 ? "async" : "failed") : "ok";
return {
retcode, data, status, error
};
}
const platforms = {
1: "Android",
2: "aPad",
3: "Watch",
4: "MacOS",
5: "iPad",
};
/** 客户端已上线状态 */
const STATUS_ONLINE = Symbol("ONLINE");
/** socket未连接状态 */
const STATUS_OFFLINE = Symbol("OFFLINE");
/** socket已连接,但客户端未上线状态 */
const STATUS_PENDING = Symbol("PENDING");
class Client extends EventEmitter {
logining = false;
status = STATUS_OFFLINE;
online_status = 0;
nickname = "";
age = 0;
sex = "unknown";
fl = new Map; //friendList
sl = new Map; //strangerList
gl = new Map; //groupList
gml = new Map; //groupMemberList
seq_id = 0;
handlers = new Map; //存放响应包的回调
seq_cache = new Map;
sig = {
srm_token: BUF0,
tgt: BUF0,
tgt_key: BUF0,
st_key: BUF0,
st_web_sig: BUF0,
skey: BUF0,
d2: BUF0,
d2key: BUF0,
sig_key: BUF0,
ticket_key: BUF0,
device_token: BUF0,
emp_time: timestamp(),
};
cookies = {};
sync_finished = false;
sync_cookie;
const1 = randomBytes(4).readUInt32BE();
const2 = randomBytes(4).readUInt32BE();
const3 = randomBytes(1)[0];
stat = {
start_time: timestamp(),
lost_times: 0,
recv_pkt_cnt: 0,
sent_pkt_cnt: 0,
lost_pkt_cnt: 0,
recv_msg_cnt: 0,
sent_msg_cnt: 0,
};
storage = {};
_socket = new Network(this);
/**
* @param {number} uin
* @param {import("./ref").ConfBot} config
*/
constructor(uin, config) {
super();
this.uin = uin;
config = {
platform: 1,
log_level: "info",
kickoff: false,
brief: false,
ignore_self: true,
resend: true,
reconn_interval: 5,
internal_cache_life: 3600,
auto_server: true,
data_dir: path.join(require.main ? require.main.path : process.cwd(), "data"),
...config
};
this.config = config;
this.dir = createDataDir(config.data_dir, uin);
this.logger = log4js.getLogger(`[${platforms[config.platform]||"Android"}:${uin}]`);
this.logger.level = config.log_level;
this.logger.mark("----------");
this.logger.mark(`Package Version: oicq@${version.version} (Released on ${version.upday})`);
this.logger.mark("View Changelogs:https://github.com/takayama-lily/oicq/releases");
this.logger.mark("----------");
const filepath = path.join(this.dir, `device-${uin}.json`);
if (!fs.existsSync(filepath))
this.logger.mark("创建了新的设备文件:" + filepath);
this.device = getDeviceInfo(filepath, this.uin);
this.apk = getApkInfo(config.platform);
this.ksid = Buffer.from(`|${this.device.imei}|` + this.apk.name);
this.on("internal.offline", offlineListener);
this.on("internal.login", onlineListener);
this.on("internal.packet", packetListener);
this.on("internal.network", networkErrorListener);
}
/**
* 连接服务器并执行回调
* 如果已经连接则立刻执行回调
* @private
* @param {Function} cb
*/
_connect(cb) {
if (this.status !== STATUS_OFFLINE)
return cb();
this._socket.join(() => {
this.status = STATUS_PENDING;
cb();
});
}
/**
* 调用api前的一层封装
* @private
* @param {Function} fn
* @param {Array} params
*/
async _useProtocol(fn, params) {
if (!this.isOnline() || !this.sync_finished)
return buildApiRet(104, null, { code: -1, message: "client not online" });
try {
const rsp = await fn.apply(this, params);
if (!rsp)
return buildApiRet(1);
if (rsp.result !== 0)
return buildApiRet(102, null,
{
code: rsp.result,
message: rsp.emsg ? rsp.emsg : getErrorMessage(fn, rsp.result)
}
);
else
return buildApiRet(0, rsp.data);
} catch (e) {
if (e instanceof TimeoutError)
return buildApiRet(103, null, { code: -1, message: "packet timeout" });
this.logger.debug(e);
return buildApiRet(100, null, { code: -1, message: e.message });
}
}
/**
* 计算每分钟消息数
* @private
*/
_calcMsgCnt() {
let cnt = 0;
for (let [time, set] of this.seq_cache) {
if (timestamp() - time >= 60)
this.seq_cache.delete(time);
else
cnt += set.size;
}
return cnt;
}
///////////////////////////////////////////////////
login(password) {
if (this.isOnline() || this.logining)
return;
if (password || !this.password_md5) {
if (password === undefined)
throw new Error("No password input.");
let password_md5;
if (typeof password === "string")
password_md5 = Buffer.from(password, "hex");
else if (password instanceof Uint8Array)
password_md5 = Buffer.from(password);
if (password_md5 && password_md5.length === 16)
this.password_md5 = password_md5;
else
this.password_md5 = md5(String(password));
}
this._connect(() => {
this.session_id = randomBytes(4);
this.random_key = randomBytes(16);
wt.passwordLogin.call(this);
});
}
captchaLogin() { }
sliderLogin(ticket) {
if (!this.t104)
return this.logger.warn("未收到滑动验证码或已过期,你不能调用sliderLogin函数。");
this._connect(() => {
wt.sliderLogin.call(this, ticket);
});
}
sendSMSCode() {
if (!this.t104 || !this.t174)
return this.logger.warn("未收到设备锁验证要求,你不能调用sendSMSCode函数。");
this._connect(() => {
wt.sendSMS.call(this);
});
}
submitSMSCode(code) {
if (!this.t104 || !this.t174)
return this.logger.warn("未发送短信验证码,你不能调用submitSMSCode函数。");
this._connect(() => {
wt.smsLogin.call(this, code);
});
}
terminate() {
if (this.status === STATUS_ONLINE)
this.status = STATUS_PENDING;
this._socket.destroy();
}
async logout() {
if (this.isOnline()) {
try {
await wt.register.call(this, true);
} catch { }
}
this.terminate();
}
isOnline() {
return this.status === STATUS_ONLINE;
}
///////////////////////////////////////////////////
setOnlineStatus(status) {
return this._useProtocol(nessy.setStatus, arguments);
}
getFriendList() {
return buildApiRet(0, this.fl);
}
getStrangerList() {
return buildApiRet(0, this.sl);
}
getGroupList() {
return buildApiRet(0, this.gl);
}
async reloadFriendList() {
const ret = await this._useProtocol(frdlst.initFL, arguments);
this.sync_finished = true;
this.pbGetMsg();
return ret;
}
async reloadGroupList() {
const ret = await this._useProtocol(frdlst.initGL, arguments);
this.sync_finished = true;
this.pbGetMsg();
return ret;
}
getGroupMemberList(group_id, no_cache = false) {
return this._useProtocol(frdlst.getGML, arguments);
}
getStrangerInfo(user_id, no_cache = false) {
return this._useProtocol(frdlst.getSI, arguments);
}
getGroupInfo(group_id, no_cache = false) {
return this._useProtocol(frdlst.getGI, arguments);
}
getGroupMemberInfo(group_id, user_id, no_cache = false) {
return this._useProtocol(frdlst.getGMI, arguments);
}
///////////////////////////////////////////////////
sendPrivateMsg(user_id, message = "", auto_escape = false) {
return this._useProtocol(chat.sendMsg, [user_id, message, auto_escape, 0]);
}
sendGroupMsg(group_id, message = "", auto_escape = false) {
return this._useProtocol(chat.sendMsg, [group_id, message, auto_escape, 1]);
}
sendDiscussMsg(discuss_id, message = "", auto_escape = false) {
return this._useProtocol(chat.sendMsg, [discuss_id, message, auto_escape, 2]);
}
sendTempMsg(group_id, user_id, message = "", auto_escape = false) {
return this._useProtocol(chat.sendTempMsg, arguments);
}
deleteMsg(message_id) {
return this._useProtocol(chat.recallMsg, arguments);
}
getMsg(message_id) {
return this._useProtocol(chat.getOneMsg, arguments);
}
getChatHistory(message_id, count = 10) {
return this._useProtocol(chat.getMsgs, arguments);
}
getForwardMsg(id) {
return this._useProtocol(chat.getForwardMsg, arguments);
}
///////////////////////////////////////////////////
setGroupAnonymousBan(group_id, flag, duration = 1800) {
return this._useProtocol(troop.muteAnonymous, arguments);
}
setGroupAnonymous(group_id, enable = true) {
return this._useProtocol(troop.setAnonymous, arguments);
}
setGroupWholeBan(group_id, enable = true) {
return this.setGroupSetting(group_id, "shutupTime", enable ? 0xffffffff : 0);
}
setGroupName(group_id, group_name) {
return this.setGroupSetting(group_id, "ingGroupName", String(group_name));
}
sendGroupNotice(group_id, content) {
return this.setGroupSetting(group_id, "ingGroupMemo", String(content));
}
setGroupSetting(group_id, k, v) {
return this._useProtocol(troop.setting, arguments);
}
setGroupAdmin(group_id, user_id, enable = true) {
return this._useProtocol(troop.setAdmin, arguments);
}
setGroupSpecialTitle(group_id, user_id, special_title = "", duration = -1) {
return this._useProtocol(troop.setTitle, arguments);
}
setGroupCard(group_id, user_id, card = "") {
return this._useProtocol(troop.setCard, arguments);
}
setGroupKick(group_id, user_id, reject_add_request = false) {
return this._useProtocol(troop.kickMember, arguments);
}
setGroupBan(group_id, user_id, duration = 1800) {
return this._useProtocol(troop.muteMember, arguments);
}
setGroupLeave(group_id, is_dismiss = false) {
return this._useProtocol(troop.quitGroup, arguments);
}
sendGroupPoke(group_id, user_id) {
return this._useProtocol(troop.pokeMember, arguments);
}
///////////////////////////////////////////////////
setFriendAddRequest(flag, approve = true, remark = "", block = false) {
return this._useProtocol(sysmsg.friendAction, arguments);
}
setGroupAddRequest(flag, approve = true, reason = "", block = false) {
return this._useProtocol(sysmsg.groupAction, arguments);
}
getSystemMsg() {
return this._useProtocol(sysmsg.getSysMsg, arguments);
}
addGroup(group_id, comment = "") {
return this._useProtocol(troop.addGroup, arguments);
}
addFriend(group_id, user_id, comment = "") {
return this._useProtocol(troop.addFriend, arguments);
}
deleteFriend(user_id, block = true) {
return this._useProtocol(troop.delFriend, arguments);
}
inviteFriend(group_id, user_id) {
return this._useProtocol(troop.inviteFriend, arguments);
}
sendLike(user_id, times = 1) {
return this._useProtocol(nessy.sendLike, arguments);
}
setNickname(nickname) {
return this._useProtocol(troop.setProfile, [0x14E22, String(nickname)]);
}
setDescription(description = "") {
return this._useProtocol(troop.setProfile, [0x14E33, String(description)]);
}
setGender(gender) {
gender = parseInt(gender);
if (![0, 1, 2].includes(gender))
return buildApiRet(100);
return this._useProtocol(troop.setProfile, [0x14E29, Buffer.from([gender])]);
}
async setBirthday(birthday) {
try {
birthday = String(birthday).replace(/[^\d]/g, "");
const buf = Buffer.alloc(4);
buf.writeUInt16BE(parseInt(birthday.substr(0, 4)));
buf.writeUInt8(parseInt(birthday.substr(4, 2)), 2);
buf.writeUInt8(parseInt(birthday.substr(6, 2)), 3);
return this._useProtocol(troop.setProfile, [0x16593, buf]);
} catch (e) {
return buildApiRet(100);
}
}
setSignature(signature = "") {
return this._useProtocol(troop.setSign, arguments);
}
setPortrait(file) {
return this._useProtocol(troop.setPortrait, arguments);
}
setGroupPortrait(group_id, file) {
return this._useProtocol(troop.setGroupPortrait, arguments);
}
getLevelInfo(user_id) {
return this._useProtocol(nessy.getLevelInfo, arguments);
}
getRoamingStamp(no_cache = false) {
return this._useProtocol(nessy.getRoamingStamp, arguments);
}
getGroupNotice(group_id) {
return this._useProtocol(troop.getGroupNotice, arguments);
}
preloadImages(files) {
return this._useProtocol(chat.preloadImages, arguments);
}
///////////////////////////////////////////////////
async getCookies(domain) {
await wt.exchangeEMP.call(this);
if (domain && !this.cookies[domain])
return buildApiRet(100, null, { code: -1, message: "unknown domain" });
let cookies = `uin=o${this.uin}; skey=${this.sig.skey};`;
if (domain)
cookies = `${cookies} p_uin=o${this.uin}; p_skey=${this.cookies[domain]};`;
return buildApiRet(0, { cookies });
}
async getCsrfToken() {
await wt.exchangeEMP.call(this);
let token = 5381;
for (let v of this.sig.skey)
token = token + (token << 5) + v;
token &= 2147483647;
return buildApiRet(0, { token });
}
/**
* @param {String} type "image" or "record" or undefined
*/
async cleanCache(type = "") {
let file, cmd;
switch (type) {
case "image":
case "record":
file = path.join(this.dir, "..", type, "*");
cmd = os.platform().includes("win") ? "del /q " : "rm -f ";
exec(cmd + file, (err, stdout, stderr) => {
if (err)
return this.logger.error(err);
if (stderr)
return this.logger.error(stderr);
this.logger.info(type + " cache clear");
});
break;
case "":
this.cleanCache("image");
this.cleanCache("record");
break;
default:
return buildApiRet(100, null, { code: -1, message: "unknown type (image, record, or undefined)" });
}
return buildApiRet(1);
}
canSendImage() {
return buildApiRet(0, { yes: true });
}
canSendRecord() {
return buildApiRet(0, { yes: true });
}
getVersionInfo() {
return buildApiRet(0, version);
}
getStatus() {
return buildApiRet(0, {
online: this.isOnline(),
status: this.online_status,
remote_ip: this._socket.remoteAddress,
remote_port: this._socket.remotePort,
msg_cnt_per_min: this._calcMsgCnt(),
statistics: this.stat,
config: this.config
});
}
getLoginInfo() {
return buildApiRet(0, {
user_id: this.uin,
nickname: this.nickname,
age: this.age, sex: this.sex
});
}
acquireGfs(group_id) {
return new Gfs(this, group_id);
}
}
/**
* @deprecated
*/
const logger = log4js.getLogger("[SYSTEM]");
logger.level = "info";
process.OICQ = {
logger
};
function createDataDir(dir, uin) {
if (!fs.existsSync(dir))
fs.mkdirSync(dir, { mode: 0o755, recursive: true });
const img_path = path.join(dir, "image");
const ptt_path = path.join(dir, "record");
const uin_path = path.join(dir, String(uin));
if (!fs.existsSync(img_path))
fs.mkdirSync(img_path);
if (!fs.existsSync(ptt_path))
fs.mkdirSync(ptt_path);
if (!fs.existsSync(uin_path))
fs.mkdirSync(uin_path, { mode: 0o755 });
return uin_path;
}
module.exports = {
Client,
STATUS_ONLINE, STATUS_OFFLINE, STATUS_PENDING
};
require("./client-ext");