Skip to content

Instantly share code, notes, and snippets.

@AlcaDesign
Last active May 2, 2022 10:52
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save AlcaDesign/14342b750623903906bf9f5c48189a57 to your computer and use it in GitHub Desktop.
Save AlcaDesign/14342b750623903906bf9f5c48189a57 to your computer and use it in GitHub Desktop.
An example for tracking mystery gift subs - https://github.com/tmijs/tmi.js/discussions/515
/**
* How long to wait in milliseconds if Twitch is taking too long to respond with
* all of the gifts.
*/
const MYSTERYGIFT_TIMEOUT_MS = 10 * 1000;
/**
* How long to wait in milliseconds if the detected gift sub is not part of a
* larger mystery gift initially.
*/
const GIFTSUB_ORPHAN_TIMEOUT_MS = 2 * 1000;
/**
* A list of all mystery gift groups.
* @type {Map<string, MysteryGift>}
*/
const mysteryGiftEvents = new Map();
/**
* A function that will be called when a mystery gift has finished getting all
* of the gifts.
* @param {MysteryGift} mysteryGift
*/
function mysteryGiftFinished(mysteryGift) {
mysteryGiftEvents.delete(mysteryGift.id);
mysteryGift.clearTimeout();
if(mysteryGift.registered) {
return;
}
mysteryGift.registered = true;
// Do something:
// addLife(points * mysteryGift.count, 'sub', mysteryGift.count);
}
class MysteryGift {
constructor(channel, tags) {
this.id = MysteryGift.getID(tags);
const channelLogin = channel.slice(1);
this.channel = {
id: tags['room-id'],
login: channelLogin,
displayName: MysteryGift.getChannelDisplayName(tags, channelLogin),
};
this.gifter = {
id: tags['user-id'],
login: tags.login,
displayName: tags['display-name'] || tags.login,
};
this.count = +(tags['msg-param-mass-gift-count'] ?? 1);
this.tier = +tags['msg-param-sub-plan'][0];
}
static getID(tags) {
return `${tags['room-id']}:${tags['user-id']}`;
}
/**
* Helper function to parse the display name of a channel from a
* submysterygift system message.
* @param {object} tags
* @param {string} channelLogin
*/
static getChannelDisplayName(tags, channelLogin = '') {
if(tags['msg-id'] !== 'submysterygift') {
return channelLogin;
}
const systemMsg = tags['system-msg'];
const channelDisplayNamePrefix = ' Subs to ';
const channelDisplayNamePrefixIndex = systemMsg.indexOf(channelDisplayNamePrefix);
return systemMsg.slice(
channelDisplayNamePrefixIndex + channelDisplayNamePrefix.length,
systemMsg.indexOf('\'s community', channelDisplayNamePrefixIndex)
);
}
/**
* Set a fallback timeout for the mystery gift.
* @returns {void}
*/
setTimeout(timeoutMs = MYSTERYGIFT_TIMEOUT_MS) {
this.clearTimeout();
this.timeout = setTimeout(() => mysteryGiftFinished(this), timeoutMs);
}
clearTimeout() {
clearTimeout(this.timeout);
this.timeout = null;
}
}
const client = new tmi.Client({ channels: [ 'some_channel' ] });
client.connect();
client.on('submysterygift', (channel, username, giftSubCount, methods, tags) => {
const mysteryGift = new MysteryGift(channel, tags);
if(mysteryGiftEvents.has(mysteryGift.id)) {
// Detected a mystery gift that is already being tracked as an orphan.
const orphan = mysteryGiftEvents.get(mysteryGift.id);
orphan.count = +tags['msg-param-mass-gift-count'];
return;
}
mysteryGift.setTimeout();
mysteryGiftEvents.set(mysteryGift.id, mysteryGift);
});
client.on('subgift', (channel, username, streakMonths, recipient, methods, tags) => {
const mysteryGiftID = MysteryGift.getID(tags);
const giftUser = {
id: tags['msg-param-recipient-id'],
login: tags['msg-param-recipient-user-name'],
displayName: recipient,
};
// An existing mystery gift exists.
if(mysteryGiftEvents.has(mysteryGiftID)) {
const mysteryGift = mysteryGiftEvents.get(mysteryGiftID);
mysteryGift.gifts.push(giftUser);
// The mystery gift has collected all of the gifts sucessfully.
if(mysteryGift.gifts.length === mysteryGift.count) {
mysteryGiftFinished(mysteryGift);
}
// If the mystery gift is not yet finished, refresh the timeout.
else {
mysteryGift.setTimeout();
}
}
// If the gift sub is not part of a mystery gift, add it as an orphan.
else {
const mysteryGift = new MysteryGift(channel, tags);
mysteryGift.gifts.push(giftUser);
// Set a shorter timeout for orphan gifts
mysteryGift.setTimeout(GIFTSUB_ORPHAN_TIMEOUT_MS);
}
});
interface TwitchUser {
/** The user's ID. */
id: string;
/** The user's login. */
login: string;
/** The user's display name. */
displayName: string;
}
interface MysteryGift {
new(channel: string, tags: object): MysteryGift;
/** The unique-ish ID of the mystery gift. */
id: string;
/** The channel the gift was sent to. */
channel: TwitchUser;
/** The user who gifted the mystery gift. */
gifter: TwitchUser;
/** The amount of gifts sent. */
count: number;
/** The tier of the mystery gift. */
tier: number;
/** The users who were gifted. */
gifts: TwitchUser[];
/** A fallback timeout to end the mystery gift. */
timeout: ReturnType<typeof setTimeout>;
/** Has the mystery gift been tallied by the finished function */
registered: boolean;
/** Generate the mystery gift's ID from the tags. */
getID(tags: object): string;
/**
* Helper function to parse the display name of a channel from a
* submysterygift system message.
*/
getChannelDisplayName(tags: object, channelLogin: string): string;
/** Set a fallback timeout */
setTimeout(): void;
/** Clear the fallback timeout */
clearTimeout(): void;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment