add basic trovo support
parent
0f59c71243
commit
055920c18e
@ -0,0 +1,512 @@
|
||||
if(typeof process === 'object') {
|
||||
// node.js only
|
||||
var Chat = require('./Chat');
|
||||
var TrovoSocket = require('./trovo.js/lib/socket/TrovoSocket');
|
||||
var TrovoApollo = require('./trovo.js/lib/api/TrovoApollo');
|
||||
var EventEmitter = require('events');
|
||||
var fetch = require('node-fetch');
|
||||
}
|
||||
|
||||
class TrovoChat extends Chat {
|
||||
constructor(settings) {
|
||||
super(settings);
|
||||
|
||||
this.settings = settings || {};
|
||||
|
||||
this.gifts = {};
|
||||
this.emotes = {};
|
||||
|
||||
this.client = new TrovoApollo();
|
||||
if(!this.settings.email || !this.settings.password) {
|
||||
this.connect();
|
||||
return;
|
||||
}
|
||||
|
||||
this.log('Logging in with email and password', this.settings.email);
|
||||
this.client.login(this.settings.email, this.settings.password).then((login) => {
|
||||
this.emit('auth', login);
|
||||
this.log('AUTH', login);
|
||||
this.connect();
|
||||
}).catch((e) => {
|
||||
this.error('Error logging in', e);
|
||||
});
|
||||
}
|
||||
|
||||
loadEmotes() {
|
||||
var emotesUrl = 'https://console.trovo.live/api/public/object?appid=madcat&schemaid=emotes&appkey=884aa07fbdbb42ffbd6a851f6cae43d6&size=total';
|
||||
var fetchData = {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-type': 'application/json',
|
||||
}
|
||||
};
|
||||
|
||||
fetch(emotesUrl, fetchData)
|
||||
.then((response) => {
|
||||
return response.json();
|
||||
})
|
||||
.then((json) => {
|
||||
this.emotes = {};
|
||||
for(var i in json.data) {
|
||||
this.emotes[json.data[i].hold] = json.data[i];
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
reject(error);
|
||||
})
|
||||
;
|
||||
}
|
||||
|
||||
getPlatformName() { return 'trovo'; }
|
||||
|
||||
createChannelWS(channel) {
|
||||
var testEvents = {
|
||||
userFollowed: {
|
||||
unknownInt: 101379536,
|
||||
identifier: '1611412852879554538_101588242_101379536_2886928683_1',
|
||||
user: 'konic',
|
||||
content: 'just followed channel!',
|
||||
id: 1611412852879558000,
|
||||
chatType: 5003,
|
||||
anotherIdentifier: '101588242_101588242_1611401808',
|
||||
iconURL: '2dwqubqaaaaaazbwhlpmzdc2cy.jpeg?t=1',
|
||||
accountName: 'konic40',
|
||||
'follow.chatViewerThreshold': '100',
|
||||
'follow.chatShowAuth': '100000|100001',
|
||||
'live.viewers': '8'
|
||||
},
|
||||
giftReceived: {
|
||||
unknownInt: 101528826,
|
||||
identifier: '1611412000411439538_101588242_101528826_2886928683_1',
|
||||
user: 'Rainpipe',
|
||||
content: { id: 520010002, num: 1, name: 'Stay Safe', type: 'MANA', value: 100 },
|
||||
id: 1611412000411441000,
|
||||
chatType: 5,
|
||||
anotherIdentifier: '101588242_101588242_1611401808',
|
||||
iconURL: '7i2a2bqaaaaabuwpez2xrz2zcy.jpeg?t=0',
|
||||
accountName: 'Rainpipe'
|
||||
},
|
||||
giftReceived2: {
|
||||
unknownInt: 101379536,
|
||||
identifier: '1611414744841871626_101588242_101379536_2886928683_1',
|
||||
user: 'konic',
|
||||
content: { id: 520010003, num: 1, name: 'On Fire', type: 'MANA', value: 500 },
|
||||
id: 1611414744841873200,
|
||||
chatType: 5,
|
||||
anotherIdentifier: '101588242_101588242_1611401808',
|
||||
iconURL: '2dwqubqaaaaaazbwhlpmzdc2cy.jpeg?t=1',
|
||||
accountName: 'konic40'
|
||||
},
|
||||
giftReceived3: {
|
||||
unknownInt: 101488110,
|
||||
identifier: '1611430885375436015_101588242_101488110_2886928683_1',
|
||||
user: 'comfyfren',
|
||||
content: 'Poopoo https://www.youtube.com/watch?v=lzv0crh5tWM Pee Pee https://www.youtube.com/watch?v=o-ITxecOAlA doo doo',
|
||||
id: 1611430885375437300,
|
||||
chatType: 6,
|
||||
anotherIdentifier: '101588242_101588242_1611401808',
|
||||
iconURL: '5.png',
|
||||
accountName: 'comfyfren',
|
||||
'magicChat.magicChatGiftID': '520000995',
|
||||
webmsgid: '98955522'
|
||||
},
|
||||
userJoined: {
|
||||
unknownInt: 101518072,
|
||||
identifier: '1611419853281181128_101588242_101518072_2886928017_1',
|
||||
user: 'innsmouthlook',
|
||||
content: 'just joined channel!',
|
||||
id: 1611419853281186300,
|
||||
chatType: 5004,
|
||||
anotherIdentifier: '101588242_101588242_1611401808',
|
||||
iconURL: '7afa2bqaaaaab5nyypujbm2zcy.jpeg?t=0',
|
||||
accountName: 'innsmouthlook',
|
||||
'welcome.chatViewerThreshold': '100',
|
||||
'welcome.chatShowAuth': '100000|100001',
|
||||
'live.viewers': '6'
|
||||
},
|
||||
};
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
this.log('Opening WebSocket', channel.channelName);
|
||||
|
||||
this.client.GetLiveInfo(channel.channelName).then((data) => {
|
||||
// get gift data
|
||||
this.client.GetGiftShop({
|
||||
"channelID": data.getLiveInfo.channelInfo.id,
|
||||
"channelOnly": false,
|
||||
"includeOffShelf": true,
|
||||
"customGift": true
|
||||
}).then((data) => {
|
||||
this.gifts = {};
|
||||
for(let info of data.getGiftShop.shopItemInfo) {
|
||||
this.gifts[info.giftInfo.giftID] = info;
|
||||
}
|
||||
}).catch((e) => {
|
||||
this.error('Could not get gift shop', e);
|
||||
});
|
||||
|
||||
var emitter = new EventEmitter();
|
||||
var ws = new TrovoSocket(data.getLiveInfo.channelInfo.id, emitter);
|
||||
emitter.on('socketMessage', (event) => {
|
||||
this.log('RECV', event.data);
|
||||
});
|
||||
emitter.on('chatMessage', (message) => {
|
||||
this.log('chatMessage', JSON.stringify(message));
|
||||
if(message.user == channel.channelName && testEvents[message.content]) {
|
||||
this.handleChatEvent(channel, message.content.replace(/[0-9]+$/, ''), testEvents[message.content]);
|
||||
return;
|
||||
}
|
||||
this.handleChatMessage(channel, message);
|
||||
});
|
||||
emitter.on('chatEvent', (type, data) => {
|
||||
this.handleChatEvent(channel, type, data);
|
||||
});
|
||||
|
||||
ws.connect();
|
||||
channel.ws = ws;
|
||||
}).catch((e) => {
|
||||
this.error(`Could not get live info for ${channel.channelName}`, e);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
getAvatarUrl(iconURL) {
|
||||
return 'https://headicon.trovo.live/user/'+iconURL+'?max_age=31536000&imageView2/2/w/100/h/100/format/webp'
|
||||
}
|
||||
|
||||
handleChatMessage(channel, message) {
|
||||
var spans = [];
|
||||
var state = 'none', emote = '', span = '';
|
||||
|
||||
var eatChar = (c) => {
|
||||
switch(state) {
|
||||
case 'none':
|
||||
if(c == ':') {
|
||||
if(span) spans.push(span);
|
||||
span = '';
|
||||
state = 'emote';
|
||||
emote = '';
|
||||
} else if(c === null) {
|
||||
spans.push(span);
|
||||
} else {
|
||||
span += c;
|
||||
}
|
||||
break;
|
||||
case 'emote':
|
||||
if(c == ' ' || c === null) {
|
||||
if(this.emotes[emote]) {
|
||||
console.log('emote', emote, this.emotes[emote]);
|
||||
spans.push({
|
||||
type: 'image',
|
||||
size: 'small',
|
||||
class: 'emote',
|
||||
src: this.emotes[emote].name,
|
||||
alt: this.emotes[emote].desc||emote
|
||||
});
|
||||
span = (c || '');
|
||||
} else {
|
||||
span += ':' + emote + (c||'');
|
||||
}
|
||||
state = 'none';
|
||||
} else {
|
||||
emote += c;
|
||||
}
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
// we use a simple state machine to parse the emotes
|
||||
for(let i = 0; i < message.content.length; i++) {
|
||||
eatChar(message.content[i]);
|
||||
}
|
||||
// process state machine once more after end of string
|
||||
eatChar(null);
|
||||
|
||||
this.emit('message', {
|
||||
type: 'chat',
|
||||
client: this,
|
||||
source: { channelName: channel.channelName, data: channel.data },
|
||||
id: message.id,
|
||||
original: message,
|
||||
text: message.content,
|
||||
spans: spans,
|
||||
username: message.accountName,
|
||||
displayName: message.user,
|
||||
avatar: this.getAvatarUrl(message.iconURL)
|
||||
});
|
||||
}
|
||||
|
||||
handleChatEvent(channel, type, data) {
|
||||
switch(type) {
|
||||
case 'giftReceived':
|
||||
var message, giftId, currency, value, amount, giftName, icon;
|
||||
if(data.chatType == 5) {
|
||||
message = '';
|
||||
giftId = data.content.id.toString();
|
||||
let giftData = this.gifts[giftId];
|
||||
if(giftData) {
|
||||
currency = giftData.priceInfo.currencyType.replace(/^EM_CURRENCY_TYPE_/, '');
|
||||
value = giftData.priceInfo.number;
|
||||
giftName = giftData.giftInfo.name;
|
||||
icon = giftData.giftInfo.icon;
|
||||
}
|
||||
amount = data.content.num;
|
||||
} else {
|
||||
message = data.content;
|
||||
giftId = data['magicChat.magicChatGiftID'];
|
||||
}
|
||||
let giftData = this.gifts[giftId];
|
||||
if(giftData) {
|
||||
currency = giftData.priceInfo.currencyType.replace(/^EM_CURRENCY_TYPE_/, '');
|
||||
value = giftData.priceInfo.number;
|
||||
amount = giftData.giftInfo.number;
|
||||
giftName = giftData.giftInfo.name;
|
||||
icon = giftData.giftInfo.icon;
|
||||
}
|
||||
|
||||
this.emit('message', {
|
||||
type: 'gift',
|
||||
client: this,
|
||||
source: { channelName: channel.channelName, data: channel.data },
|
||||
id: data.id,
|
||||
original: data,
|
||||
|
||||
message: message,
|
||||
giftType: giftName,
|
||||
currency: currency,
|
||||
value: value,
|
||||
amount: amount,
|
||||
icon: icon,
|
||||
|
||||
username: data.accountName,
|
||||
displayName: data.user,
|
||||
avatar: this.getAvatarUrl(data.iconURL)
|
||||
});
|
||||
break;
|
||||
case 'userFollowed':
|
||||
this.emit('message', {
|
||||
type: 'follow',
|
||||
client: this,
|
||||
source: { channelName: channel.channelName, data: channel.data },
|
||||
id: data.id,
|
||||
original: data,
|
||||
username: data.accountName,
|
||||
displayName: data.user,
|
||||
avatar: this.getAvatarUrl(data.iconURL)
|
||||
});
|
||||
break;
|
||||
case 'userJoined':
|
||||
this.emit('message', {
|
||||
type: 'join',
|
||||
client: this,
|
||||
source: { channelName: channel.channelName, data: channel.data },
|
||||
id: data.id,
|
||||
original: data,
|
||||
username: data.accountName,
|
||||
displayName: data.user,
|
||||
avatar: this.getAvatarUrl(data.iconURL)
|
||||
});
|
||||
break;
|
||||
case 'userSubbed':
|
||||
break;
|
||||
case 'raidUser':
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
connect() {
|
||||
this.log('CONNECT');
|
||||
this.state = 'connected';
|
||||
|
||||
this.emotes = {};
|
||||
this.loadEmotes();
|
||||
|
||||
for(let i in this.channels) {
|
||||
this.createChannelWS(this.channels[i]).then((ws) => {
|
||||
this.channels[i].ws = ws;
|
||||
}).catch((e) => {
|
||||
this.error('Could not create channel WebSocket', e);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
joinChannel(channelName, data) {
|
||||
var channelData = {
|
||||
channelName: channelName,
|
||||
data: data,
|
||||
joined: false
|
||||
};
|
||||
|
||||
if(this.state == 'connected') {
|
||||
this.createChannelWS(channelData).then((ws) => {
|
||||
channelData.ws = ws;
|
||||
}).catch((e) => {
|
||||
this.error('Could not create channel WebSocket', e);
|
||||
});
|
||||
}
|
||||
|
||||
this.channels.push(channelData);
|
||||
}
|
||||
|
||||
leaveChannel(channelName) {
|
||||
return new Promise((resolve, reject) => {
|
||||
var channel = this.getChannelByName(channelName);
|
||||
|
||||
if(!channel) reject(new Error('Channel unrecognized'));
|
||||
|
||||
var data = {
|
||||
id: user.id,
|
||||
type:'stop'
|
||||
};
|
||||
this.send(data);
|
||||
|
||||
resolve(true);
|
||||
});
|
||||
}
|
||||
|
||||
getChannelById(id) {
|
||||
for(let i in this.channels) {
|
||||
if(this.channels[i].id == id)
|
||||
return this.channels[i];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
getChannelByName(channelName) {
|
||||
for(let i in this.channels) {
|
||||
if(this.channels[i].channelName == channelName)
|
||||
return this.channels[i];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
getChannelData(channelName) {
|
||||
var channel = this.getChannelByName(channelName);
|
||||
if(!channel) return null;
|
||||
|
||||
return channel.data;
|
||||
}
|
||||
|
||||
isJoined(channelName) {
|
||||
var channel = this.getChannelByName();
|
||||
|
||||
if(!channel) return null;
|
||||
|
||||
return channel.joined;
|
||||
}
|
||||
|
||||
getJoinedChannels() {
|
||||
var ret = [];
|
||||
|
||||
for(let i in this.channels) {
|
||||
if(this.channels[i].joined)
|
||||
ret.push(this.channels[i].channelName);
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
isConnected() {
|
||||
return this.state == 'connected';
|
||||
}
|
||||
|
||||
sendStreamChatMessage(message, streamer) {
|
||||
return new Promise((resolve, reject) => {
|
||||
reject('Not implemented');
|
||||
if(!streamer && !this.loginUser) {
|
||||
reject(new Error('No streamer or login user specified'));
|
||||
return;
|
||||
}
|
||||
|
||||
streamer = streamer || this.loginUser;
|
||||
|
||||
this.post({
|
||||
"operationName":"SendStreamChatMessage",
|
||||
"variables":{
|
||||
"input":{
|
||||
"streamer":streamer,
|
||||
"message":message,
|
||||
"roomRole":"Member",
|
||||
"subscribing":true
|
||||
}
|
||||
},
|
||||
"query": `
|
||||
mutation SendStreamChatMessage($input: SendStreamchatMessageInput!) {
|
||||
sendStreamchatMessage(input: $input) {
|
||||
err {
|
||||
message
|
||||
code
|
||||
}
|
||||
message {
|
||||
type
|
||||
... on ChatText {
|
||||
id
|
||||
emojis
|
||||
content
|
||||
createdAt
|
||||
subLength
|
||||
subscribing
|
||||
role
|
||||
roomRole
|
||||
sender {
|
||||
id
|
||||
username
|
||||
displayname
|
||||
avatar
|
||||
partnerStatus
|
||||
badges
|
||||
effect
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
}).then((data) => {
|
||||
if(!data || !data.sendStreamchatMessage) {
|
||||
reject(data);
|
||||
return;
|
||||
}
|
||||
|
||||
if(data.sendStreamchatMessage.err) {
|
||||
reject(data.sendStreamchatMessage.err);
|
||||
} else {
|
||||
resolve(data.sendStreamchatMessage.message);
|
||||
}
|
||||
}).catch((err) => {
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
livestreamChatroomInfo(displayname) {
|
||||
let isLoading = true;
|
||||
|
||||
return this.post({
|
||||
operationName: 'LivestreamChatroomInfo',
|
||||
variables: {
|
||||
displayname: displayname
|
||||
},
|
||||
query: `
|
||||
query LivestreamChatroomInfo(
|
||||
$displayname: String!
|
||||
) {
|
||||
userByDisplayName(displayname: $displayname) {
|
||||
id
|
||||
username
|
||||
displayname
|
||||
avatar
|
||||
partnerStatus
|
||||
myChatBadges
|
||||
}
|
||||
}
|
||||
`
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if(typeof process === 'object') {
|
||||
module.exports = TrovoChat;
|
||||
}
|
Loading…
Reference in New Issue