Skip to content
This repository was archived by the owner on Apr 13, 2025. It is now read-only.

Commit 4992c0c

Browse files
authored
Merge pull request #709 from SteffoSpieler/feat/streamelements-sub-bomb-detection
feat: streamelements sub bomb detection
2 parents 0590003 + 889b710 commit 4992c0c

File tree

4 files changed

+324
-176
lines changed

4 files changed

+324
-176
lines changed

samples/streamelements-events/extension/index.ts

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -32,32 +32,52 @@ module.exports = function (nodecg: NodeCG) {
3232
});
3333

3434
client.onSubscriber((data) => {
35-
nodecg.log.info(`${data.data.displayName} just subscribed for ${data.data.amount} months (${formatSubTier(data.data.tier)}).`);
35+
nodecg.log.info(
36+
`${data.data.displayName} just subscribed for ${data.data.amount} months (${formatSubTier(
37+
data.data.tier,
38+
)}).`,
39+
);
3640
});
3741

3842
client.onTestSubscriber((data) => {
39-
nodecg.log.info(`${data.event.displayName} just subscribed for ${data.event.amount} months (${formatSubTier(data.event.tier)}).`);
40-
})
43+
nodecg.log.info(
44+
`${data.event.displayName} just subscribed for ${data.event.amount} months (${formatSubTier(
45+
data.event.tier,
46+
)}).`,
47+
);
48+
});
49+
50+
client.onSubscriberBomb((data) => {
51+
nodecg.log.info(`${data.gifterUsername} just gifted ${data.subscribers.length} subs.`);
52+
});
53+
54+
client.onTestSubscriberBomb((data) => {
55+
nodecg.log.info(`${data.gifterUsername} just gifted ${data.subscribers.length} subs.`);
56+
});
4157

4258
client.onGift((data) => {
4359
nodecg.log.info(
44-
`${data.data.displayName} just got a tier ${formatSubTier(data.data.tier)} subscription from ${data.data.sender ?? "anonymous"}! It's ${data.data.displayName}'s ${data.data.amount} month.`,
60+
`${data.data.displayName} just got a tier ${formatSubTier(data.data.tier)} subscription from ${
61+
data.data.sender ?? "anonymous"
62+
}! It's ${data.data.displayName}'s ${data.data.amount} month.`,
4563
);
4664
});
4765

4866
client.onTestGift((data) => {
4967
nodecg.log.info(
50-
`${data.event.displayName} just got a tier ${formatSubTier(data.event.tier)} subscription from ${data.event.sender ?? "anonymous"}! It's ${data.event.displayName}'s ${data.event.amount} month.`,
68+
`${data.event.displayName} just got a tier ${formatSubTier(data.event.tier)} subscription from ${
69+
data.event.sender ?? "anonymous"
70+
}! It's ${data.event.displayName}'s ${data.event.amount} month.`,
5171
);
52-
})
72+
});
5373

5474
client.onHost((data) => {
5575
nodecg.log.info(`${data.data.displayName} just hosted the stream for ${data.data.amount} viewer(s).`);
5676
});
5777

5878
client.onTestHost((data) => {
5979
nodecg.log.info(`${data.event.displayName} just hosted the stream for ${data.event.amount} viewer(s).`);
60-
})
80+
});
6181

6282
client.onRaid((data) => {
6383
nodecg.log.info(`${data.data.displayName} just raided the stream with ${data.data.amount} viewers.`);
@@ -92,8 +112,7 @@ module.exports = function (nodecg: NodeCG) {
92112
};
93113

94114
function formatSubTier(tier: "1000" | "2000" | "3000" | "prime"): string {
95-
if (tier === "prime")
96-
return "Twitch Prime";
115+
if (tier === "prime") return "Twitch Prime";
97116

98117
// We want to display the tier as 1, 2, 3
99118
// However StreamElements stores the sub tiers as 1000, 2000 and 3000.

samples/streamelements-events/graphics/index.html

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -21,47 +21,55 @@ <h1>streamelements-events sample bundle</h1>
2121
<p>
2222
Last subscriber:
2323
<span v-if="streamElementsReplicant?.lastSubscriber">
24-
{{ streamElementsReplicant?.lastSubscriber?.data.displayName }} subscribed for {{
25-
streamElementsReplicant?.lastSubscriber?.data.amount }} months ({{ subTier }}).
24+
{{ streamElementsReplicant?.lastSubscriber?.data.displayName }} subscribed for
25+
{{ streamElementsReplicant?.lastSubscriber?.data.amount }} months ({{ subTier }}).
26+
</span>
27+
<span v-else>none</span>
28+
</p>
29+
<p>
30+
Last subbomb:
31+
<span v-if="streamElementsReplicant?.lastSubBomb">
32+
{{ streamElementsReplicant?.lastSubBomb.gifterUsername }} gifted
33+
{{ streamElementsReplicant?.subscribers.subs.length }}.
2634
</span>
2735
<span v-else>none</span>
2836
</p>
2937
<p>
3038
Last tip:
3139
<span v-if="streamElementsReplicant?.lastTip">
32-
{{ streamElementsReplicant?.lastSubscriber?.data.displayName }} subscribed for {{
33-
streamElementsReplicant?.lastSubscriber?.data.amount }} months.
40+
{{ streamElementsReplicant?.lastSubscriber?.data.displayName }} subscribed for
41+
{{ streamElementsReplicant?.lastSubscriber?.data.amount }} months.
3442
</span>
3543
<span v-else>none</span>
3644
</p>
3745
<p>
3846
Last cheer:
3947
<span v-if="streamElementsReplicant?.lastCheer">
40-
{{ streamElementsReplicant?.lastCheer?.data.amount }} bits by {{
41-
streamElementsReplicant?.lastCheer?.data.displayName }}
48+
{{ streamElementsReplicant?.lastCheer?.data.amount }} bits by
49+
{{ streamElementsReplicant?.lastCheer?.data.displayName }}.
4250
</span>
4351
<span v-else>none</span>
4452
</p>
4553
<p>
4654
Last follow:
4755
<span v-if="streamElementsReplicant?.lastFollow">
48-
{{ streamElementsReplicant?.lastFollow?.data.displayName }}
56+
{{ streamElementsReplicant?.lastFollow?.data.displayName }}.
4957
</span>
5058
<span v-else>none</span>
5159
</p>
5260
<p>
5361
Last raid:
5462
<span v-if="streamElementsReplicant?.lastRaid">
55-
{{ streamElementsReplicant?.lastRaid?.data.displayName }} raided with {{
56-
streamElementsReplicant?.lastRaid?.data.amount }} viewers
63+
{{ streamElementsReplicant?.lastRaid?.data.displayName }} raided with
64+
{{ streamElementsReplicant?.lastRaid?.data.amount }} viewers.
5765
</span>
5866
<span v-else>none</span>
5967
</p>
6068
<p>
6169
Last host:
6270
<span v-if="streamElementsReplicant?.lastHost">
63-
{{ streamElementsReplicant?.lastHost?.data.displayName }} hosted with {{
64-
streamElementsReplicant?.lastHost?.data.viewers }} viewers
71+
{{ streamElementsReplicant?.lastHost?.data.displayName }} hosted with
72+
{{ streamElementsReplicant?.lastHost?.data.viewers }} viewers.
6573
</span>
6674
<span v-else>none</span>
6775
</p>

services/nodecg-io-streamelements/extension/StreamElements.ts

Lines changed: 102 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,28 @@
11
import io = require("socket.io-client");
22
import { Result, emptySuccess, error } from "nodecg-io-core";
33
import {
4-
StreamElementsCheerEvent, StreamElementsEvent,
4+
StreamElementsCheerEvent,
5+
StreamElementsEvent,
56
StreamElementsFollowEvent,
67
StreamElementsHostEvent,
78
StreamElementsRaidEvent,
9+
StreamElementsSubBombEvent,
810
StreamElementsSubscriberEvent,
9-
StreamElementsTestCheerEvent, StreamElementsTestEvent,
11+
StreamElementsTestCheerEvent,
12+
StreamElementsTestEvent,
1013
StreamElementsTestFollowEvent,
11-
StreamElementsTestHostEvent, StreamElementsTestRaidEvent,
12-
StreamElementsTestSubscriberEvent, StreamElementsTestTipEvent,
13-
StreamElementsTipEvent
14+
StreamElementsTestHostEvent,
15+
StreamElementsTestRaidEvent,
16+
StreamElementsTestSubscriberEvent,
17+
StreamElementsTestTipEvent,
18+
StreamElementsTipEvent,
1419
} from "./StreamElementsEvent";
1520
import { EventEmitter } from "events";
1621
import { Replicant } from "nodecg-types/types/server";
1722

1823
export interface StreamElementsReplicant {
1924
lastSubscriber?: StreamElementsSubscriberEvent;
25+
lastSubBomb?: StreamElementsSubBombEvent<StreamElementsSubscriberEvent>;
2026
lastTip?: StreamElementsTipEvent;
2127
lastCheer?: StreamElementsCheerEvent;
2228
lastGift?: StreamElementsSubscriberEvent;
@@ -25,8 +31,17 @@ export interface StreamElementsReplicant {
2531
lastHost?: StreamElementsHostEvent;
2632
}
2733

34+
/**
35+
* Internal utility interface for tracking sub-bombs.
36+
*/
37+
interface SubBomb {
38+
timeout: NodeJS.Timeout;
39+
subs: Array<StreamElementsSubscriberEvent | StreamElementsTestSubscriberEvent>;
40+
}
41+
2842
export class StreamElementsServiceClient extends EventEmitter {
2943
private socket: SocketIOClient.Socket;
44+
private subBombDetectionMap: Map<string, SubBomb> = new Map();
3045

3146
constructor(private jwtToken: string, private handleTestEvents: boolean) {
3247
super();
@@ -47,21 +62,73 @@ export class StreamElementsServiceClient extends EventEmitter {
4762
this.onEvent((data: StreamElementsEvent) => {
4863
if (data.type === "subscriber") {
4964
if (data.data.gifted) {
50-
this.emit("gift", data);
65+
this.handleSubGift(
66+
data.data.sender,
67+
data,
68+
(subBomb) => this.emit("subbomb", subBomb),
69+
(gift) => this.emit("gift", gift),
70+
);
5171
}
5272
}
5373
this.emit(data.type, data);
5474
});
75+
5576
if (this.handleTestEvents) {
5677
this.onTestEvent((data: StreamElementsTestEvent) => {
5778
if (data.listener) {
5879
this.emit("test", data);
5980
this.emit("test:" + data.listener, data);
6081
}
6182
});
83+
84+
this.onTestSubscriber((data) => {
85+
if (data.event.gifted) {
86+
this.handleSubGift(
87+
data.event.sender,
88+
data,
89+
(subBomb) => this.emit("test:subbomb", subBomb),
90+
(gift) => this.emit("test:gift", gift),
91+
);
92+
}
93+
});
6294
}
6395
}
6496

97+
private handleSubGift<T extends StreamElementsSubscriberEvent | StreamElementsTestSubscriberEvent>(
98+
subGifter: string | undefined,
99+
gift: T,
100+
handlerSubBomb: (data: StreamElementsSubBombEvent<T>) => void,
101+
handlerGift: (data: T) => void,
102+
) {
103+
const gifter = subGifter ?? "anonymous";
104+
105+
const subBomb = this.subBombDetectionMap.get(gifter) ?? {
106+
subs: [],
107+
timeout: setTimeout(() => {
108+
this.subBombDetectionMap.delete(gifter);
109+
110+
// Only fire sub bomb event if more than one sub were gifted.
111+
// Otherwise, this is just a single gifted sub.
112+
if (subBomb.subs.length > 1) {
113+
const subBombEvent = {
114+
gifterUsername: gifter,
115+
subscribers: subBomb.subs as T[],
116+
};
117+
handlerSubBomb(subBombEvent);
118+
}
119+
120+
subBomb.subs.forEach(handlerGift);
121+
}, 1000),
122+
};
123+
124+
subBomb.subs.push(gift);
125+
126+
// New subs in this sub bomb. Refresh timeout in case another one follows.
127+
subBomb.timeout.refresh();
128+
129+
this.subBombDetectionMap.set(gifter, subBomb);
130+
}
131+
65132
async connect(): Promise<void> {
66133
return new Promise((resolve, _reject) => {
67134
this.createSocket();
@@ -123,8 +190,15 @@ export class StreamElementsServiceClient extends EventEmitter {
123190
});
124191
}
125192

126-
public onSubscriber(handler: (data: StreamElementsSubscriberEvent) => void): void {
127-
this.on("subscriber", handler);
193+
public onSubscriber(handler: (data: StreamElementsSubscriberEvent) => void, includeSubGifts = true): void {
194+
this.on("subscriber", (data) => {
195+
if (data.data.gifted && !includeSubGifts) return;
196+
handler(data);
197+
});
198+
}
199+
200+
public onSubscriberBomb(handler: (data: StreamElementsSubBombEvent<StreamElementsSubscriberEvent>) => void): void {
201+
this.on("subbomb", handler);
128202
}
129203

130204
public onTip(handler: (data: StreamElementsTipEvent) => void): void {
@@ -155,16 +229,21 @@ export class StreamElementsServiceClient extends EventEmitter {
155229
this.on("test", handler);
156230
}
157231

158-
public onTestSubscriber(handler: (data: StreamElementsTestSubscriberEvent) => void): void {
159-
this.on("test:subscriber-latest", handler);
232+
public onTestSubscriber(handler: (data: StreamElementsTestSubscriberEvent) => void, includeSubGifts = true): void {
233+
this.on("test:subscriber-latest", (data) => {
234+
if (data.event.gifted && !includeSubGifts) return;
235+
handler(data);
236+
});
237+
}
238+
239+
public onTestSubscriberBomb(
240+
handler: (data: StreamElementsSubBombEvent<StreamElementsTestSubscriberEvent>) => void,
241+
): void {
242+
this.on("test:subbomb", handler);
160243
}
161244

162245
public onTestGift(handler: (data: StreamElementsTestSubscriberEvent) => void): void {
163-
this.on("test:subscriber-latest", d => {
164-
if(d.data.gifted) {
165-
handler(d);
166-
}
167-
});
246+
this.on("test:gift", handler);
168247
}
169248

170249
public onTestCheer(handler: (data: StreamElementsTestCheerEvent) => void): void {
@@ -192,12 +271,13 @@ export class StreamElementsServiceClient extends EventEmitter {
192271
rep.value = {};
193272
}
194273

195-
this.on("subscriber", (data) => (rep.value.lastSubscriber = data));
196-
this.on("tip", (data) => (rep.value.lastTip = data));
197-
this.on("cheer", (data) => (rep.value.lastCheer = data));
198-
this.on("gift", (data) => (rep.value.lastGift = data));
199-
this.on("follow", (data) => (rep.value.lastFollow = data));
200-
this.on("raid", (data) => (rep.value.lastRaid = data));
201-
this.on("host", (data) => (rep.value.lastHost = data));
274+
this.onSubscriber((data) => (rep.value.lastSubscriber = data));
275+
this.onSubscriberBomb((data) => (rep.value.lastSubBomb = data));
276+
this.onTip((data) => (rep.value.lastTip = data));
277+
this.onCheer((data) => (rep.value.lastCheer = data));
278+
this.onGift((data) => (rep.value.lastGift = data));
279+
this.onFollow((data) => (rep.value.lastFollow = data));
280+
this.onRaid((data) => (rep.value.lastRaid = data));
281+
this.onHost((data) => (rep.value.lastHost = data));
202282
}
203283
}

0 commit comments

Comments
 (0)