Skip to content

Instantly share code, notes, and snippets.

@AlcaDesign
Created February 10, 2019 08:11
Show Gist options
  • Save AlcaDesign/da4a7e2b7f8dc28e51fa3dfd80c0d554 to your computer and use it in GitHub Desktop.
Save AlcaDesign/da4a7e2b7f8dc28e51fa3dfd80c0d554 to your computer and use it in GitHub Desktop.
Custom USERNOTICE events, ahead of 1.4.0. (Examples included in `index.js`)
const tmiParse = require('tmi.js/lib/parser');
const escapedIRCRegex = /\\([sn:r\\])/g;
const ircEscapedChars = { s: ' ', n: '', ':': ';', r: '' };
const booleanTagKeys = [ 'mod', 'subscriber', 'msg-param-should-share-streak' ];
const integerTagKeys = [
'tmi-sent-ts', 'msg-param-months', 'msg-param-cumulative-months',
'msg-param-streak-months', 'msg-param-sender-count',
'msg-param-mass-gift-count', 'msg-param-viewerCount', 'msg-param-threshold',
'msg-param-bits-amount', 'msg-param-min-cheer-amount',
'msg-param-selected-count'
];
const anonymousUserID = '274598607';
const anonymousUserLogin = 'ananonymousgifter';
function findIRCUnescapedChar(m, p) {
return p in ircEscapedChars ? ircEscapedChars[p] : p
}
function unescapeIRC(msg) {
if(!msg || !msg.includes('\\')) {
return msg;
}
return msg.replace(escapedIRCRegex, findIRCUnescapedChar);
}
function onRawMessage(str = '') {
const lines = str.split('\r\n');
for(let line of lines) {
if(!line) {
return;
}
onRawMessageLine.call(this, line);
}
}
function onRawMessageLine(line) {
const data = tmiParse.msg(line);
const { tags } = data;
// const msgID = tags['msg-id'];
if(
data.command !== 'USERNOTICE'
// || [ 'sub', 'resub', 'ritual' ].includes(msgID)
) {
return;
}
tmiParse.emotes(tags);
tmiParse.badges(tags);
for(const key in tags) {
const val = tags[key];
const type = typeof val;
if(type === 'boolean') {
tags[key] = null;
}
else if(type === 'string') {
if(integerTagKeys.includes(key)) {
tags[key] = parseInt(val);
}
else if(booleanTagKeys.includes(key)) {
tags[key] = val === '1';
}
else {
tags[key] = unescapeIRC(val);
}
}
}
onUserNotice.call(this, data);
}
function onUserNotice(data) {
const { tags } = data;
const param = p => {
const val = data.tags[`msg-param-${p}`];
return val !== undefined ? val : null;
};
const channel = data.params[0].slice(1);
const userMessage = data.params[1] || null;
const systemMsg = tags['system-msg'] || null;
const userID = tags['user-id'];
const roomID = tags['room-id'];
const msgID = tags['msg-id'];
const login = tags['login'];
const displayName = tags['display-name'];
const massGiftCount = param('mass-gift-count');
const senderTotalCount = param('sender-count');
const subPlan = param('sub-plan');
const subPlanName = param('sub-plan-name');
const tier = { Prime: 1, 1000: 1, 2000: 2, 3000: 3 }[subPlan] || null;
// const months = param('months'); // Deprecated
const shouldShareStreak = param('should-share-streak');
const streakMonths = param('streak-months');
const cumulativeMonths = param('cumulative-months');
const recipientDisplayName = param('recipient-display-name');
const recipientID = param('recipient-id');
const recipientUserName = param('user-name');
const senderLogin = param('sender-login') || login || null;
const senderDisplayName = param('sender-name') || displayName || null;
const anonymous = userID === anonymousUserID;
// const originID = param('origin-id'); // Not too useful
const params = { msgID, systemMsg };
switch(msgID) {
case 'sub':
Object.assign(params, {
shouldShareStreak,
cumulativeMonths,
sub: {
gift: false,
plan: subPlan,
planName: subPlanName,
tier
},
sender: null,
recipient: {
login,
id: userID,
displayName
},
channel: {
login: channel,
id: roomID,
displayName: null
}
});
break;
case 'resub':
Object.assign(params, {
shouldShareStreak,
streakMonths,
cumulativeMonths,
sub: {
gift: false,
plan: subPlan,
planName: subPlanName,
tier
},
sender: null,
recipient: {
login,
id: userID,
displayName
},
channel: {
login: channel,
id: roomID,
displayName: null
}
});
break;
case 'subgift':
Object.assign(params, {
totalCount: senderTotalCount,
sub: {
gift: true,
plan: subPlan,
planName: subPlanName,
tier
},
// originID,
// months,
sender: {
login: senderLogin,
id: userID,
displayName: senderDisplayName,
anonymous
},
recipient: {
login: recipientUserName,
id: recipientID,
displayName: recipientDisplayName
},
channel: {
login: channel,
id: roomID,
displayName: null
}
});
break;
case 'submysterygift':
Object.assign(params, {
giftCount: massGiftCount,
totalCount: senderTotalCount,
sub: {
gift: true,
plan: subPlan,
planName: null,
tier
},
// originID
sender: {
login: senderLogin,
id: userID,
displayName: senderDisplayName,
anonymous
},
recipient: null,
channel: {
login: channel,
id: roomID,
displayName: null
}
});
break;
case 'giftpaidupgrade':
Object.assign(params, {
sub: {
gift: false,
plan: null,
planName: null,
tier: null
},
sender: {
login: senderLogin,
id: null,
displayName: senderDisplayName,
anonymous: senderLogin === anonymousUserLogin
},
recipient: {
login,
id: userID,
displayName
},
channel: {
login: channel,
id: roomID,
displayName: null
}
});
break;
case 'ritual':
const ritualName = param('ritual-name');
params.ritualName = ritualName;
if(ritualName === 'new_chatter') {
const emoteKeys = Object.keys(tags.emotes || {});
const emoteID = emoteKeys.length === 1 ? emoteKeys[0] : null;
const emoteIndex = emoteID ?
tags.emotes[emoteID][0].split('-').map((n, i) => +n + i) :
null;
const emote = userMessage && emoteIndex ?
userMessage.slice(...emoteIndex) :
null;
Object.assign(params, {
emoteID,
emoteIndex,
emote
});
}
break;
case 'raid':
Object.assign(params, {
viewerCount: param('viewerCount'),
raider: {
login,
id: userID,
displayName,
profileImageURL: param('profileImageURL')
},
channel: {
login: channel,
id: roomID,
displayName: null
}
});
break;
case 'unraid':
Object.assign(params, {
viewerCount: null,
raider: {
login,
id: userID,
displayName,
profileImageURL: null
},
// Probably same as raider, not 100% same to assume
channel: {
login: channel,
id: roomID,
displayName
}
});
break;
case 'bitsbadgetier':
Object.assign(params, {
threshold: param('threshold'),
channel: {
login: channel,
id: roomID,
displayName: null
}
});
break;
case 'rewardgift':
Object.assign(params, {
bitsAmount: param('bits-amount'),
minCheerAmount: param('min-cheer-amount'),
selectedCount: param('selected-count'),
domain: param('domain'),
sender: {
login,
id: userID,
displayName
},
channel: {
login: channel,
id: roomID,
displayName: null
}
});
}
const baseEmitType = 'custom-usernotice';
const baseEmitValues = [ channel, params, userMessage, tags ];
this.emits(
[ baseEmitType, `${baseEmitType}-${msgID}` ],
[ [ msgID, ...baseEmitValues ], baseEmitValues ]
);
}
module.exports = {
handleMissingUserNotices(client) {
if(!client.ws) {
throw new Error('Client is not connecting/connected. ' +
'Call client.connect() before this.');
}
client.ws.on('message', onRawMessage.bind(client));
}
};
const tmi = require('tmi.js');
const client = tmi.client({
options: { debug: true },
channels: [ 'mychannel' ]
});
client.connect();
require('./custom-events').handleMissingUserNotices(client);
// All USERNOTICEs
client.on('custom-usernotice', (msgID, channel, params, userMessage, tags) => {});
// Indiviual USERNOTICEs, including duplicates from tmi.js.
client.on('custom-usernotice-sub', (channel, params, userMessage, tags) => {});
client.on('custom-usernotice-resub', (channel, params, userMessage, tags) => {});
client.on('custom-usernotice-subgift', (channel, params, userMessage, tags) => {});
client.on('custom-usernotice-submysterygift', (channel, params, userMessage, tags) => {});
client.on('custom-usernotice-giftpaidupgrade', (channel, params, userMessage, tags) => {});
client.on('custom-usernotice-ritual', (channel, params, userMessage, tags) => {});
client.on('custom-usernotice-raid', (channel, params, userMessage, tags) => {});
client.on('custom-usernotice-unraid', (channel, params, userMessage, tags) => {});
client.on('custom-usernotice-bitsbadgetier', (channel, params, userMessage, tags) => {});
client.on('custom-usernotice-rewardgift', (channel, params, userMessage, tags) => {});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment