forked from Tromino/BeamLink-1.0
-
Notifications
You must be signed in to change notification settings - Fork 0
/
script.js
383 lines (330 loc) · 11.1 KB
/
script.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
require('node-limited')();
// Variables and requires and stuff.
var colors = require("colors");
var fs = require("fs");
var request = require("request");
var tmi = require("tmi.js");
var debug = false;
var Beam = require('beam-client-node');
var BeamSocket = require('beam-client-node/lib/ws');
//Beam messages should be decoded for safety
var ent = require('ent');
var accounts = JSON.parse(fs.readFileSync("accounts.json", "utf8"));
var channels = JSON.parse(fs.readFileSync("channels.json", "utf8"));
var supportedAccounts = JSON.parse(fs.readFileSync("supportedAccounts.json", "utf8"));["smblive"];
var beam = new Beam();
var twitch = new tmi.client({
connection: {
reconnect: true
},
identity: {
username: accounts.twitch.user,
password: accounts.twitch.pass
},
channels: (function() {
var i = [];
for(var j = 0; j < channels.length; j++) {
i.push(channels[j].twitch);
}
return i;
}())
});
var wttwitch = {};
var beamSockets = {};
var beamIDs = {};
var beamChannelNames = {};
function saveChannelsJson()
{
fs.rename("channels.json", "channels-bak-" + +new Date() + ".json", function() {
fs.writeFile("channels.json", JSON.stringify(channels), "utf8", function() {});
});
}
function closeBeamChat(channelName) {
if(beamSockets[channelName]) {
beamSockets[channelName].close();
beamSockets[channelName] = null;
}
}
function unlinkChannels(twitchChannel, beamChannel, channelIndex) {
twitch.say(twitchChannel, "Chats unlinked.");
sendBeamMessage(beamChannel, "Chat unlinked.");
console.log(("Unlinked channels: " + beamChannel + " / " + twitchChannel).green);
sendDebugBeamMessage("Unlinked channels: " + beamChannel + " / " + twitchChannel);
channels.splice(channelIndex, 1);
saveChannelsJson();
closeBeamChat(beamChannel);
twitch.part(twitchChannel);
}
function getChatLinkerInfo(site, channel) {
var username = site === "twitch" ? "@Irti" : "@IrtiPlays";
return "ChatLinker is a bot written by "+username+" that is linking the chats of this channel and "+site+" channel \""+channel.replace("#", "")+"\". Messages can be made more pretty in Chrome by using the ChatLinker Extension: \"http://bit.ly/2jE2gPM\".";
}
function onBeamMessage(channel, data) {
var nick = data.user_name;
var self = nick.toLowerCase() === accounts.beam.user.toLowerCase();
if(self) return;
var channelOwner = nick.toLowerCase() === channel.toLowerCase();
var message = flattenBeamMessage(data.message.message);
var beamIndex = chanIndex("beam", nick);
if(message.slice(0, 6) == "!link ") {
if(supportedAccounts.length > 0 && supportedAccounts.indexOf(nick.toLowerCase()) === -1) {
return sendBeamWhisper(channel, nick, "You are not permitted to perform this action.");
}
var twitchUser = message.slice(6).toLowerCase();
var twitchChannel = "#" + twitchUser;
if(!wttwitch[twitchChannel]) {
if(beamIndex == -1 && chanIndex("twitch", twitchChannel) == -1) {
wttwitch[twitchChannel] = nick;
sendBeamWhisper(channel, nick, "Watching " + twitchUser + "'s chat on Twitch. Confirmation instructions have been sent to " + twitchUser + ".");
twitch.whisper(twitchUser, "I have been asked to link your Twitch chat with " + nick + "'s Beam chat. Whisper \"!link\" if you wish to confirm.");
twitch.join(twitchChannel);
} else {
sendBeamWhisper(channel, nick, "One or both of your channels are already linked. Type \"!unlink\" on Beam or Twitch if you want to unlink them.");
}
}
return;
}
if(message === '!unlink' && beamIndex !== -1)
{
var twitchChannel = channels[beamIndex].twitch;
return unlinkChannels(twitchChannel, nick, beamIndex);
}
if(channel.toLowerCase() === accounts.beam.user.toLowerCase())
return console.log((" [beam#" + channel + "] " + nick + ": " + message).white);
beamIndex = chanIndex("beam", channel);
if(beamIndex == -1)
return;
var twitchChannel = channels[beamIndex].twitch;
if (message === "!chatlinker") {
return;
/*var infoMessage = getChatLinkerInfo("twitch", twitchChannel);
if(channelOwner) {
return sendBeamMessage(channel, infoMessage);
}
else
{
return sendBeamWhisper(channel, nick, infoMessage);
}*/
}
var displayName = (data.message.meta.me ? "/" : "") + nick;
twitch.say(twitchChannel, "[" + displayName + "] " + message);
console.log((" [beam#" + channel + "] " + displayName + ": " + message).grey);
sendDebugBeamMessage("[<beam#" + channel + "> " + displayName + "] " + message);
}
/**
* Called when a beam socket emits "Close"
* @param {String} channelName The closed Channel
*/
function onSocketClose(channelName) {
console.log(("Disconnected from Beam channel: " + channelName).yellow);
var i = chanIndex("beam", channelName);
//Check if we still actually care about this channel. We might catch "close"
//events on a channel that has been !unliked.
//We also check if this is the account we are running under aka "StreamLink_"
//If this is the case we don't want to stop here
if (i === -1 && channelName.toLowerCase() !== accounts.beam.user.toLowerCase()) {
return;
}
var socket = beamSockets[channelName];
//if we don't have a socket, just reconnect as though it was fresh channel
if (!socket) {
joinChannel(channelName);
return;
}
//BeamSockets emit error and close events but will
//automaticlaly try and reconnect on error but not on close.
//If this is the case we'll see the connecting status here
//As the socket is already reconnecting
if (socket.status === BeamSocket.CONNECTING) {
//Hey we are already reconnecting we don't need to do anything.
//if we ever see CLOSED here it means everything else has given up
return;
}
//This then re-spins up the websocket.
socket.boot();
}
function onTwitchMessage(channel, userstate, message) {
var nick = userstate.username
var channelOwner = channel.toLowerCase() === "#" + nick;
if (message === "!link" && channelOwner) {
var beamChannel = wttwitch[channel];
if (beamChannel) {
return joinChannel(beamChannel).then(function() {
sendBeamMessage(beamChannel, "Chats linked.");
twitch.say(channel, "Chats linked.");
console.log(("Linked channels: " + beamChannel + " / " + channel).green);
sendDebugBeamMessage("Linked channels: " + beamChannel + " / " + channel);
channels.push({beam: beamChannel, twitch: channel});
saveChannelsJson();
delete wttwitch[channel];
});
}
}
var twitchIndex = chanIndex("twitch", channel);
if(twitchIndex == -1) return;
var beamChannel = channels[twitchIndex].beam
if (message === "!unlink" && channelOwner) {
return unlinkChannels(channel, beamChannel, twitchIndex);
}
if (message === "!chatlinker") {
return;
/*var infoMessage = getChatLinkerInfo("Beam", beamChannel);
if(channelOwner) {
return twitch.say(channel, infoMessage);
}
else
{
return twitch.whisper(nick, infoMessage);
}*/
}
var displayName = userstate["display-name"];
if(userstate["message-type"] == "action") {
displayName = "/"+displayName;
}
sendBeamMessage(beamChannel, "[" + displayName + "] " + message);
console.log((" [twitch" + channel + "] " + displayName + ": " + message).grey);
sendDebugBeamMessage("[<twitch" + channel + "> " + displayName + "] " + message);
};
//To join a channel we need its id, then we need the ws address and an authkey.
//This handles them all
function joinChannel(channelName) {
//we need the channel id.
return beam.request('get', '/channels/' + channelName).bind(this)
.then(function(response) {
beamIDs[channelName] = response.body.id;
beamChannelNames[response.body.id] = channelName;
return beam.chat.join(response.body.id);
}).then(function(response){
beamSockets[channelName] = new BeamSocket(response.body.endpoints).boot();
beamSockets[channelName].on('ChatMessage', onBeamMessage.bind(this, channelName));
beamSockets[channelName].on('closed', onSocketClose.bind(this,channelName));
return beamSockets[channelName]
.call('auth', [beamIDs[channelName], accounts.beam.id, response.body.authkey])
.then(function(){
console.log(("Connected to Beam channel: " + channelName).cyan);
}).catch(function(err){
console.log(err);
throw err;
});
//Move all in one to here
}).catch(function(err) {
throw err;
});
}
//Connect and authenticate as a User on beam,
beam.use('password', {
username: accounts.beam.user,
password: accounts.beam.pass
}).attempt()
.then(function(response) {
console.log(('Connected to beam').cyan);
accounts.beam.id = response.body.id;
joinChannel(accounts.beam.user);
connectToChannels();
})
.catch(function(err){
//throw err;
if(err && err.message && err.message.body) {
console.log(err.message.body);
return;
}
console.log(err);
});
//Connect to twitch
twitch.connect();
// Functions for stuff.
function chanIndex(site, channel) {
var toret = -1;
for(var j = 0; j < channels.length; j++) {
if(channels[j][site] == channel) {
toret = j;
}
}
return toret;
}
//We need to delay this until after beam is logged in
function connectToChannels() {
// Connection logging and stuff.
for(var i = 0; i < channels.length; i++) {
console.log(("Trying to connect to channels: " + channels[i].beam + " / " + channels[i].twitch).white);
//Do manually connect to beam though because it does require some extra lifting
joinChannel(channels[i].beam);
}
}
twitch.on("join", function (channel, username, self) {
console.log(("Connected to Twitch channel: " + channel).magenta);
});
// Reconnect when disconnected
twitch.on("part", function(channel, username, self) {
if(chanIndex("twitch", channel) > -1) {
console.log(("Reconnect to twitch" + channel + "...").yellow);
twitch.join(channel);
}
});
function extractTextFromMessagePart(part) {
if (part == undefined) {
return '';
}
if (typeof part === "object") {
if (part.type != null && part.type === 'text') {
return part.data;
}
if(part.text != null) {
return ' ' + part.text;
}
return '';
}
return part;
}
//Flatten a beam message down into a string
function flattenBeamMessage(message) {
var result = '';
if (message.length !== undefined) {
if(message.length > 1 ) {
result = message.reduce(function (previous, current) {
if (!previous) {
previous = '';
}
if (typeof previous === 'object') {
previous = extractTextFromMessagePart(previous);
}
return previous + extractTextFromMessagePart(current);
});
} else if(message.length === 1) {
result = extractTextFromMessagePart(message[0]);
} else {
return '';
}
} else {
result = message;
}
return ent.decode(result);
}
//Beam.say is not longer valid, we need to find the correct socket and use that
function sendBeamMessage(channel, message) {
var socket = beamSockets[channel];
if(socket) {
socket.call('msg',[message]);
}
}
function sendBeamWhisper(channel, nick, message) {
var socket = beamSockets[channel];
if(socket) {
socket.call('whisper',[nick, message]);
}
}
function sendDebugBeamMessage(message) {
if(debug) {
sendBeamMessage(accounts.beam.user, message);
}
}
twitch.on("message", function (channel, userstate, message, self) {
if(self) return;
switch(userstate["message-type"]) {
case "action":
case "chat":
case "whisper":
onTwitchMessage(channel, userstate, message);
break;
}
});