various trovo fixes

master
vampi 9 months ago
parent 03d829c3f6
commit ed15a5bd3a

@ -0,0 +1,39 @@
const fetch = require('node-fetch');
class TrovoApi {
constructor(clientId, clientSecret) {
if(!clientId)
throw "TrovoApi needs clientId";
this.clientId = clientId;
this.clientSecret = clientSecret || '';
}
getChatToken(channelId) {
return new Promise((resolve, reject) => {
let url = `https://open-api.trovo.live/openplatform/chat/channel-token/${channelId}`;
let headers = {
'Accept': 'application/json',
'Client-ID': this.clientId,
};
fetch(url, { headers }).then(e => e.json()).then(response => {
if(response.error)
throw response.message||response.status;
resolve(response.token);
}).catch(e => reject(e));
});
}
generateRandomString(length) {
var result = '';
var randomChars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
for(var i = 0; i < length; i++) {
result += randomChars.charAt(Math.floor(Math.random() * randomChars.length));
}
return result;
}
}
module.exports = TrovoApi;

@ -2,7 +2,8 @@ if(typeof process === 'object') {
// node.js only
var Channel = require('./Channel');
var TrovoPubSubSocket = require('./TrovoPubSubSocket');
var TrovoSocket = require('./trovo.js/lib/socket/TrovoSocket');
// var TrovoSocket = require('./trovo.js/lib/socket/TrovoSocket');
var TrovoSocket = require('./TrovoSocket');
var EventEmitter = require('events');
}
@ -23,12 +24,14 @@ class TrovoChannel extends Channel {
_loadGiftData() {
this.gifts = {};
console.log('loadGiftData', this.channelID);
this.chat.client.GetGiftShop({
"channelID": this.channelID,
"channelOnly": false,
"includeOffShelf": true,
"customGift": true
"customGift": false
}).then((data) => {
console.log('giftShop', JSON.stringify(data));
for(let info of data.getGiftShop.shopItemInfo) {
this.gifts[info.giftInfo.giftID] = {
giftName: info.giftInfo.name,
@ -55,6 +58,14 @@ class TrovoChannel extends Channel {
}
_getTestEvents() {
return {
SuperCap: {"type":"CHAT","channel_info":{"channel_id":"101588242"},"data":{"eid":"1629482589166379_101588242_2886926797_1","chats":[{"type":6,"content":"balls","nick_name":"vampirefrog","avatar":"https://headicon.trovo.live/user/cioq4bqaaaaaabbllzj43qc7cy.jpeg?t=0","medals":["creator"],"roles":["streamer"],"message_id":"1629482589139681695_101588242_101588242_2886927975_1","sender_id":101588242,"uid":101588242,"user_name":"vampirefrog","content_data":{"gift_display_name":"Super Cap","gift_num":1,"magicChatCurrencyInfo":"2,1500","magicChatGiftID":520000995,"normal_emote_enabled":"","user_time":1629482588720},"custom_role":"[{\"roleName\":\"streamer\",\"roleType\":100000}]"}]}},
StaySafe: {"type":"CHAT","data":{"eid":"1629482969285073_0_2886926853_1","chats":[{"type":5,"content":"{\"gift_id\":520010002,\"gift\":\"Stay Safe\",\"num\":1,\"gift_value\":100,\"value_type\":\"Mana\"}","nick_name":"XSENKU","avatar":"10.png","roles":["follower"],"message_id":"1629480612852370586_100039313_104364132_2886927975_1","sender_id":104364132}]}},
Join: {"type":"CHAT","channel_info":{"channel_id":"101301301"},"data":{"eid":"1629483241736358_101301301_2887057659_1","chats":[{"type":5004,"content":"just joined channel!","nick_name":"t2kANONYMOUS","avatar":"https://headicon.trovo.live/user/fqwrybqaaaaaayhef65cuwlxcy.jpeg?t=0","roles":["follower"],"message_id":"1629483241418427441_101301301_102509868_2887057787_1","sender_id":102509868,"uid":102509868,"user_name":"t2kAN0NYN0US","content_data":{"chatShowAuth":"100000|100001","chatViewerThreshold":100,"normal_emote_enabled":"","viewers":69},"custom_role":"[{\"roleName\":\"follower\",\"roleType\":100006}]"}]}},
Follow: {"type":"CHAT","channel_info":{"channel_id":"101301301"},"data":{"eid":"1629483281504129_101301301_2887057659_1","chats":[{"type":5003,"content":"just followed channel!","nick_name":"mvpIMMORTAL","avatar":"https://headicon.trovo.live/default/1.png","roles":["follower"],"message_id":"1629483281187938299_101301301_103085572_2887057787_1","sender_id":103085572,"uid":103085572,"user_name":"mvpIMMORTAL","content_data":{"chatShowAuth":"100000|100001","chatViewerThreshold":100,"normal_emote_enabled":"","viewers":70},"custom_role":"[{\"roleName\":\"follower\",\"roleType\":100006}]"}]}},
Boost: {"type":"CHAT","data":{"eid":"1629483226522550_0_2886926853_1","chats":[{"type":5007,"content":"{title} has casted a spell rocket and the channel will be boosted to front page!","nick_name":"","message_id":"1629482666829442093_101301301_0_2886927750_1"}]}},
};
return {
FOLLOW: {
unknownInt: 101379536,
@ -318,32 +329,133 @@ class TrovoChannel extends Channel {
*/
}
_handleChat(chat) {
switch(chat.type) {
case 5003:
this.emit('message', {
type: 'follow',
id: chat.message_id,
original: chat,
username: chat.user_name||chat.nick_name,
displayName: chat.nick_name,
avatar: chat.avatar,
});
break;
case 5004:
this.emit('message', {
type: 'join',
id: chat.message_id,
original: chat,
username: chat.user_name||chat.nick_name,
displayName: chat.nick_name,
avatar: chat.avatar,
});
break;
case 5009:
break;
case 6:
this.emit('message', {
type: 'gift',
id: chat.message_id,
original: chat,
message: chat.content,
spans: this._parseEmotes(chat.content),
giftType: chat.content_data.gift_display_name || 'Super Cap',
currency: 'ELIXIR',
value: 1,
amount: chat.content_data.gift_num,
icon: null,
username: chat.user_name||chat.nick_name,
displayName: chat.nick_name,
avatar: chat.avatar,
});
break;
case 5:
let data = JSON.parse(chat.content);
let giftData;
if(data.gift_id && (data.gift_id in this.gifts))
giftData = this.gifts[data.gift_id];
let message = '';
this.emit('message', {
type: 'gift',
id: chat.message_id,
original: chat,
message: message,
spans: this._parseEmotes(message),
giftType: data.gift,
currency: data.value_type,
value: data.gift_value,
amount: data.num,
icon: giftData&&giftData.icon,
username: chat.user_name||chat.nick_name,
displayName: chat.nick_name,
avatar: chat.avatar,
});
break;
default:
var testEvents = this._getTestEvents();
if(chat.content in testEvents && testEvents[chat.content].data && testEvents[chat.content].data.chats) {
for(let testChat of testEvents[chat.content].data.chats) {
this._handleChat(testChat);
}
} else {
this.emit('message', {
type: 'chat',
id: chat.message_id,
original: chat,
text: chat.content,
spans: this._parseEmotes(chat.content),
username: chat.user_name||chat.nick_name,
displayName: chat.nick_name,
avatar: chat.avatar,
});
}
}
}
_createChannelWS() {
var testEvents = this._getTestEvents();
console.log('_createChannelWS');
this.emit('verbose', 'Opening WebSocket ' + this.channelName);
var emitter = new EventEmitter();
this.ws = new TrovoSocket(this.channelID, emitter);
emitter.on('socketMessage', (event) => {
this.emit('verbose', 'RECV ' + Buffer.from(event.data).toString('base64'));
});
emitter.on('chatMessage', (message) => {
this.emit('verbose', 'chatMessage ' + JSON.stringify(message));
if(message.user == this.channelName && testEvents[message.content]) {
this._handleChatEvent(message.content.replace(/[0-9]+$/, ''), testEvents[message.content]);
this.ws = new TrovoSocket(this.chat.settings.clientId, this.channelID);
this.ws.on('verbose', (msg) => { console.log('TROVO SOCKET', msg); });
this.ws.on('CHAT', (data) => {
if(!data || !data.chats)
return;
for(let chat of data.chats) {
this._handleChat(chat);
}
this._handleChatMessage(message);
});
emitter.on('chatEvent', (type, data) => {
this._handleChatEvent(type, data);
});
emitter.on('socketOpen', (event) => {
this.state = 'joined';
this.emit('joined', {});
});
this.ws.connect();
// var emitter = new EventEmitter();
// this.ws = new TrovoSocket(this.channelID, emitter);
// emitter.on('socketMessage', (event) => {
// this.emit('verbose', 'RECV ' + Buffer.from(event.data).toString('base64'));
// });
// emitter.on('chatMessage', (message) => {
// this.emit('verbose', 'chatMessage ' + JSON.stringify(message));
// if(message.user == this.channelName && testEvents[message.content]) {
// this._handleChatEvent(message.content.replace(/[0-9]+$/, ''), testEvents[message.content]);
// return;
// }
// this._handleChatMessage(message);
// });
// emitter.on('chatEvent', (type, data) => {
// this._handleChatEvent(type, data);
// });
// emitter.on('socketOpen', (event) => {
// this.state = 'joined';
// this.emit('joined', {});
// });
this.pubWs = new TrovoPubSubSocket(this.channelID);
this.pubWs.on('MESSAGE', (data) => {
if(data && data.data && data.data.message) {
@ -396,6 +508,7 @@ class TrovoChannel extends Channel {
}
_parseEmotes(str) {
str = str || '';
var spans = [];
var state = 'none', emote = '', span = '';
@ -457,22 +570,24 @@ class TrovoChannel extends Channel {
return 'https://headicon.trovo.live/user/'+iconURL+'?max_age=31536000&imageView2/2/w/100/h/100/format/webp'
}
_handleChatMessage(message) {
this.emit('message', {
type: 'chat',
id: message.identifier,
original: message,
text: message.content,
spans: this._parseEmotes(message.content),
username: message.accountName,
displayName: message.user,
avatar: this._getAvatarUrl(message.iconURL)
});
}
_handleChatEvent(type, data) {
console.log('chatEvent', type, data);
this.emit('verbose', 'handleChatEvent ' + type + ' ' + JSON.stringify(data));
switch(type) {
case 'CHAT':
for(let chat in data.chats) {
this.emit('message', {
type: 'chat',
id: chat.uid,
original: chat,
text: chat.content,
spans: this._parseEmotes(chat.content),
username: chat.user_name,
displayName: chat.nick_name,
avatar: chat.avatar,
});
}
break;
case 'GIFT':
case 'CUSTOM_SPELL':
var message, giftId, currency, value, amount, giftName, icon;
@ -542,24 +657,24 @@ class TrovoChannel extends Channel {
break;
case 'GIFT_SUBS':
// this.emit('message', {
// type: 'giftSub',
// id: msg.id,
// original: msg,
// username: msg.sender.username,
// displayName: msg.sender.displayname,
// avatar: msg.sender.avatar,
// receiver: msg.receiver
// type: 'giftSub',
// id: msg.id,
// original: msg,
// username: msg.sender.username,
// displayName: msg.sender.displayname,
// avatar: msg.sender.avatar,
// receiver: msg.receiver
// });
break;
case 'RECEIVE_SUB':
// this.emit('message', {
// type: 'giftSubReceive',
// id: data.id,
// original: data,
// username: data.accountName,
// displayName: data.user,
// avatar: this._getAvatarUrl(data.iconURL),
// gifter: msg.gifter
// type: 'giftSubReceive',
// id: data.id,
// original: data,
// username: data.accountName,
// displayName: data.user,
// avatar: this._getAvatarUrl(data.iconURL),
// gifter: msg.gifter
// });
break;
case 'SUBSCRIBE_CHANNEL':
@ -574,13 +689,13 @@ class TrovoChannel extends Channel {
break;
case 'RAID':
// this.emit('message', {
// type: 'host',
// id: data.id,
// original: data,
// username: data.accountName,
// displayName: data.user,
// avatar: this._getAvatarUrl(data.iconURL),
// viewers: null // TODO get this?
// type: 'host',
// id: data.id,
// original: data,
// username: data.accountName,
// displayName: data.user,
// avatar: this._getAvatarUrl(data.iconURL),
// viewers: null // TODO get this?
// });
break;
}
@ -596,9 +711,72 @@ class TrovoChannel extends Channel {
});
}
getStatus() {
getStatus(original) {
return new Promise((resolve, reject) => {
resolve({ viewers: this.viewerCount, platform: this.chat.getPlatformName() });
this.chat.client.GetLiveInfo(this.channelName).then((result) => {
if(original) {
resolve(result);
return;
}
let audienceType = null;
let audienceTypeOptions = [{
value: "CHANNEL_AUDIENCE_TYPE_FAMILYFRIENDLY",
label: "For all viewers"
}, {
value: "CHANNEL_AUDIENCE_TYPE_TEEN",
label: "13+ content"
}, {
value: "CHANNEL_AUDIENCE_TYPE_EIGHTEENPLUS",
label: "18+ content"
}, {
value: "CHANNEL_AUDIENCE_TYPE_PERSONALVIEWS",
label: "Personal views"
}];
for(let a of audienceTypeOptions) {
if(result.getLiveInfo.channelInfo.audiType == a.value) {
audienceType = a.label;
}
}
let ret = {
platform: this.chat.getPlatformName(),
clientId: this.chat.id,
channelId: this.id,
user: {
avatar: result.getLiveInfo.streamerInfo.faceUrl,
displayName: result.getLiveInfo.streamerInfo.nickName
},
channel: {
description: result.getLiveInfo.streamerInfo.info
},
stream: {
isLive: result.getLiveInfo.isLive,
startTime: result.getLiveInfo.programInfo.startTm,
viewers: result.getLiveInfo.channelInfo.viewers,
title: result.getLiveInfo.programInfo.title,
preview: result.getLiveInfo.programInfo.coverUrl,
language: result.getLiveInfo.channelInfo.languageName,
audienceType: audienceType,
category: {
id: result.getLiveInfo.categoryInfo.id,
name: result.getLiveInfo.categoryInfo.name,
}
},
chat: {
}
};
this.chat.client.GetUserFollowCount(result.getLiveInfo.streamerInfo.uid).then((followCountResult) => {
ret.user.followers = followCountResult.getUserFollowCount.followerCount;
ret.user.following = followCountResult.getUserFollowCount.followingCount;
resolve(ret);
}, (err) => {
resolve(ret);
});
}, reject);
});
}
}

@ -19,15 +19,20 @@ class TrovoChat extends Chat {
this.liveInfoCache = {};
this.client = new TrovoApollo();
if(this.settings.email && this.settings.password) {
if(this.settings.clientId) {
setTimeout(() => this.emit('ready'));
} else if(this.settings.email && this.settings.password) {
console.log('logging in!');
this.client.login(this.settings.email, this.settings.password).then((login) => {
console.log('logged in', login);
this.log('AUTH', login);
this.emit('ready');
}).catch((e) => {
console.error(e);
throw new Error('Error logging in: ' + e.toString());
});
} else {
this.emit('ready');
setTimeout(() => this.emit('ready'));
}
this._loadEmotes();
@ -61,10 +66,12 @@ class TrovoChat extends Chat {
getPlatformName() { return 'trovo'; }
_joinChannelByChannelID(channelData) {
var channel = new TrovoChannel(this, channelData);
let channel = new TrovoChannel(this, channelData);
channel.addListener('verbose', (str) => this.log(str));
this.channels.push(channel);
channel.id = channelData.channelID + '-' + this._generateRandomString(8);
this.channels[channel.id] = channel;
return channel;
}

@ -0,0 +1,100 @@
if(typeof process === 'object') {
var WebSocket = require('isomorphic-ws');
var EventEmitter = require('events');
var TrovoApi = require('./TrovoApi');
}
class TrovoSocket extends EventEmitter {
constructor(clientId, channelID, settings) {
super();
this.channelID = channelID;
settings = settings || {};
this.wsUrl = settings && settings.wsUrl || 'wss://open-chat.trovo.live/chat';
this.verbose = settings.verbose || false;
this.reconnectDelay = settings.reconnectDelay || 2000;
this.state = 'none';
console.log('instantiating trovo api', clientId);
this.trovoApi = new TrovoApi(clientId);
this.nonces = {};
this.pingTimeout = 30000;
}
connect() {
if(this.webSocket) {
this.webSocket.removeAllListeners('message');
this.state = 'none';
this.webSocket = null;
}
this.emit('verbose', 'Opening WebSocket ' + this.wsUrl);
this.webSocket = new WebSocket(this.wsUrl);
this.webSocket.once('open', () => {
this.emit('verbose', 'websocket open');
console.log('getting chat token', this.channelID);
this.trovoApi.getChatToken(this.channelID).then((token) => {
console.log('got token', token);
this.send('AUTH', { token: token }).then((response) => {
this.emit('open');
this.sendPing();
});
})
});
this.webSocket.once('error', (err) => {
this.emit('verbose', 'websocket error: ' + err.toString());
});
this.webSocket.on('close', (err, message) => {
this.emit('verbose', 'websocket close: ' + err + ' ' + message);
setTimeout(() => { this.connect(); }, this.reconnectDelay);
});
this.webSocket.on('message', (text) => {
var data = JSON.parse(text);
if(data.type == 'RESPONSE' && data.nonce) {
if(this.nonces && this.nonces[data.nonce]) {
this.nonces[data.nonce].resolve(data.data);
delete this.nonces[data.nonce];
}
} else if(data.type == 'PONG') {
setTimeout(() => { this.sendPing(); }, this.pingTimeout)
}
this.emit('verbose', 'message: '+JSON.stringify(data));
this.emit(data.type, data.data);
});
}
sendPing() {
this.send('PING');
}
send(type, data) {
if(this.webSocket && this.webSocket.readyState == 1) {
var nonce = type + '_' + Date.now();
var message = {
type: type,
nonce: nonce,
data: data
};
var str = JSON.stringify(message);
this.webSocket.send(str);
this.emit('verbose', 'SEND ' + str);
return new Promise((resolve, reject) => {
this.nonces[nonce] = { resolve: resolve, reject: reject };
});
}
}
isConnected() {
return this.state === 'connected';
}
}
if(typeof process === 'object') {
module.exports = TrovoSocket;
}
Loading…
Cancel
Save