chzzk
Version:
네이버 라이브 스트리밍 서비스 CHZZK의 비공식 API 라이브러리
336 lines (335 loc) • 13.6 kB
JavaScript
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.ChzzkChat = void 0;
const isomorphic_ws_1 = __importDefault(require("isomorphic-ws"));
const types_1 = require("./types");
const client_1 = require("../client");
const const_1 = require("../const");
class ChzzkChat {
constructor(options) {
var _a, _b;
this.handlers = [];
this.defaults = {};
this.pingTimeoutId = null;
this.pollIntervalId = null;
this.isReconnect = false;
this._connected = false;
if (options.pollInterval && !options.channelId) {
throw new Error('channelId is required for polling');
}
if (!options.chatChannelId && !options.channelId) {
throw new Error('channelId or chatChannelId is required');
}
if (const_1.IS_BROWSER && options.baseUrls == const_1.DEFAULT_BASE_URLS) {
if (options.pollInterval) {
throw new Error('Custom baseUrls are required for polling in browser');
}
if (!options.chatChannelId) {
throw new Error('chatChannelId is required in browser if not using custom baseUrls');
}
if (!options.accessToken) {
throw new Error('accessToken is required in browser if not using custom baseUrls');
}
}
this.options = options;
this.options.baseUrls = (_a = options.baseUrls) !== null && _a !== void 0 ? _a : const_1.DEFAULT_BASE_URLS;
this.client = (_b = options.client) !== null && _b !== void 0 ? _b : new client_1.ChzzkClient({ baseUrls: options.baseUrls });
}
get connected() {
return this._connected;
}
get chatChannelId() {
return this.options.chatChannelId;
}
static fromClient(chatChannelId, client) {
return new ChzzkChat({
chatChannelId,
client,
baseUrls: client.options.baseUrls
});
}
static fromAccessToken(chatChannelId, accessToken, uid, baseUrls) {
const chzzkChat = new ChzzkChat({
chatChannelId,
accessToken,
baseUrls
});
chzzkChat.uid = uid;
return chzzkChat;
}
connect() {
return __awaiter(this, void 0, void 0, function* () {
if (this._connected) {
throw new Error('Already connected');
}
if (this.options.channelId && !this.options.chatChannelId) {
const status = yield this.client.live.status(this.options.channelId);
this.options.chatChannelId = status.chatChannelId;
}
if (this.options.chatChannelId && !this.options.accessToken) {
this.uid = this.client.hasAuth ?
yield this.client.user().then(user => user.userIdHash) :
null;
this.options.accessToken = yield this.client.chat.accessToken(this.options.chatChannelId)
.then(token => token.accessToken);
}
this.defaults = {
cid: this.options.chatChannelId,
svcid: "game",
ver: "2"
};
const serverId = Math.abs(this.options.chatChannelId.split("")
.map(c => c.charCodeAt(0))
.reduce((a, b) => a + b)) % 9 + 1;
this.ws = new isomorphic_ws_1.default(`wss://kr-ss${serverId}.chat.naver.com/chat`);
this.ws.onopen = () => {
this.ws.send(JSON.stringify(Object.assign({ bdy: {
accTkn: this.options.accessToken,
auth: this.uid ? "SEND" : "READ",
devType: 2001,
uid: this.uid
}, cmd: types_1.ChatCmd.CONNECT, tid: 1 }, this.defaults)));
if (!this.isReconnect) {
this.startPolling();
}
};
this.ws.onmessage = this.handleMessage.bind(this);
this.ws.onclose = () => {
if (!this.isReconnect) {
this.emit('disconnect', this.options.chatChannelId);
this.stopPolling();
this.options.chatChannelId = null;
}
this.stopPingTimer();
this.ws = null;
if (this._connected) {
this.disconnect();
}
};
});
}
disconnect() {
var _a;
return __awaiter(this, void 0, void 0, function* () {
if (!this._connected) {
throw new Error('Not connected');
}
(_a = this.ws) === null || _a === void 0 ? void 0 : _a.close();
this.ws = null;
this.sid = null;
if (this.client) {
this.options.accessToken = null;
this.uid = null;
}
this._connected = false;
});
}
reconnect() {
return __awaiter(this, void 0, void 0, function* () {
this.isReconnect = true;
if (this._connected) {
yield this.disconnect();
yield this.connect();
}
});
}
requestRecentChat(count = 50) {
if (!this._connected) {
throw new Error('Not connected');
}
this.ws.send(JSON.stringify(Object.assign({ bdy: { recentMessageCount: count }, cmd: types_1.ChatCmd.REQUEST_RECENT_CHAT, sid: this.sid, tid: 2 }, this.defaults)));
}
sendChat(message, emojis = {}) {
if (!this._connected) {
throw new Error('Not connected');
}
if (!this.uid) {
throw new Error('Not logged in');
}
const extras = {
chatType: "STREAMING",
emojis,
osType: "PC",
streamingChannelId: this.options.chatChannelId
};
this.ws.send(JSON.stringify(Object.assign({ bdy: {
extras: JSON.stringify(extras),
msg: message,
msgTime: Date.now(),
msgTypeCode: types_1.ChatType.TEXT
}, retry: false, cmd: types_1.ChatCmd.SEND_CHAT, sid: this.sid, tid: 3 }, this.defaults)));
}
selfProfile() {
return __awaiter(this, void 0, void 0, function* () {
if (!this.uid) {
throw new Error('Not logged in');
}
return yield this.profile(this.uid);
});
}
profile(uid) {
return __awaiter(this, void 0, void 0, function* () {
if (!this._connected) {
throw new Error('Not connected');
}
return yield this.client.chat.profileCard(this.options.chatChannelId, uid);
});
}
emit(event, data) {
if (this.handlers[event]) {
for (const handler of this.handlers[event]) {
handler(data);
}
}
}
on(event, handler) {
const e = event;
this.handlers[e] = this.handlers[e] || [];
this.handlers[e].push(handler);
}
handleMessage(data) {
return __awaiter(this, void 0, void 0, function* () {
const json = JSON.parse(data.data);
const body = json['bdy'];
this.emit('raw', json);
switch (json.cmd) {
case types_1.ChatCmd.CONNECTED:
this._connected = true;
this.sid = body['sid'];
if (this.isReconnect) {
this.emit('reconnect', this.options.chatChannelId);
this.isReconnect = false;
}
else {
this.emit('connect', null);
}
break;
case types_1.ChatCmd.PING:
this.ws.send(JSON.stringify({
cmd: types_1.ChatCmd.PONG,
ver: "2"
}));
break;
case types_1.ChatCmd.CHAT:
case types_1.ChatCmd.RECENT_CHAT:
case types_1.ChatCmd.DONATION:
const isRecent = json.cmd == types_1.ChatCmd.RECENT_CHAT;
const chats = body['messageList'] || body;
const notice = body['notice'];
if (notice) {
this.emit('notice', this.parseChat(notice, isRecent));
}
for (const chat of chats) {
const type = chat['msgTypeCode'] || chat['messageTypeCode'] || '';
const parsed = this.parseChat(chat, isRecent);
switch (type) {
case types_1.ChatType.TEXT:
this.emit('chat', parsed);
break;
case types_1.ChatType.DONATION:
this.emit('donation', parsed);
break;
case types_1.ChatType.SUBSCRIPTION:
this.emit('subscription', parsed);
break;
case types_1.ChatType.SYSTEM_MESSAGE:
this.emit('systemMessage', parsed);
break;
}
}
break;
case types_1.ChatCmd.NOTICE:
this.emit('notice', Object.keys(body).length != 0 ? this.parseChat(body) : null);
break;
case types_1.ChatCmd.BLIND:
this.emit('blind', body);
}
if (json.cmd != types_1.ChatCmd.PONG) {
this.startPingTimer();
}
});
}
parseChat(chat, isRecent = false) {
const profile = JSON.parse(chat['profile']);
const extras = chat['extras'] ? JSON.parse(chat['extras']) : null;
const params = extras === null || extras === void 0 ? void 0 : extras['params'];
const registerChatProfileJson = params === null || params === void 0 ? void 0 : params['registerChatProfileJson'];
const targetChatProfileJson = params === null || params === void 0 ? void 0 : params['targetChatProfileJson'];
if (registerChatProfileJson && targetChatProfileJson) {
params['registerChatProfile'] = JSON.parse(registerChatProfileJson);
params['targetChatProfile'] = JSON.parse(targetChatProfileJson);
delete params['registerChatProfileJson'];
delete params['targetChatProfileJson'];
extras['params'] = params;
}
const message = chat['msg'] || chat['content'];
const memberCount = chat['mbrCnt'] || chat['memberCount'];
const time = chat['msgTime'] || chat['messageTime'];
const hidden = (chat['msgStatusType'] || chat['messageStatusType']) == "HIDDEN";
const parsed = {
profile,
extras,
hidden,
message,
time,
isRecent
};
if (memberCount) {
parsed['memberCount'] = memberCount;
}
return parsed;
}
startPolling() {
if (!this.options.pollInterval || this.pollIntervalId)
return;
this.pollIntervalId = setInterval(() => __awaiter(this, void 0, void 0, function* () {
const chatChannelId = yield this.client.live.status(this.options.channelId)
.then(status => status === null || status === void 0 ? void 0 : status.chatChannelId)
.catch(() => null);
if (chatChannelId && chatChannelId != this.options.chatChannelId) {
this.options.chatChannelId = chatChannelId;
yield this.reconnect();
}
}), this.options.pollInterval);
}
stopPolling() {
if (this.pollIntervalId) {
clearInterval(this.pollIntervalId);
}
this.pollIntervalId = null;
}
startPingTimer() {
if (this.pingTimeoutId) {
clearTimeout(this.pingTimeoutId);
}
this.pingTimeoutId = setTimeout(() => this.sendPing(), 20000);
}
stopPingTimer() {
if (this.pingTimeoutId) {
clearTimeout(this.pingTimeoutId);
}
this.pingTimeoutId = null;
}
sendPing() {
if (!this.ws)
return;
this.ws.send(JSON.stringify({
cmd: types_1.ChatCmd.PING,
ver: "2"
}));
this.pingTimeoutId = setTimeout(() => this.sendPing(), 20000);
}
}
exports.ChzzkChat = ChzzkChat;