@allior/wmake-streamelements-events 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +46 -0
- package/README_RU.md +46 -0
- package/dist/react/hooks/index.d.ts +4 -0
- package/dist/react/hooks/index.d.ts.map +1 -0
- package/dist/react/hooks/use-event-listener.d.ts +4 -0
- package/dist/react/hooks/use-event-listener.d.ts.map +1 -0
- package/dist/react/hooks/use-on-event-received.d.ts +44 -0
- package/dist/react/hooks/use-on-event-received.d.ts.map +1 -0
- package/dist/react/hooks/use-on-widget-load.d.ts +4 -0
- package/dist/react/hooks/use-on-widget-load.d.ts.map +1 -0
- package/dist/react/index.d.ts +4 -0
- package/dist/react/index.d.ts.map +1 -0
- package/dist/react/index.iife.js +2 -0
- package/dist/react/index.iife.js.map +1 -0
- package/dist/react/index.js +179 -0
- package/dist/react/index.js.map +1 -0
- package/dist/react/types/index.d.ts +2 -0
- package/dist/react/types/index.d.ts.map +1 -0
- package/dist/react/types/window-events.d.ts +3 -0
- package/dist/react/types/window-events.d.ts.map +1 -0
- package/dist/root/aggregate.d.ts +12 -0
- package/dist/root/aggregate.d.ts.map +1 -0
- package/dist/root/classifier.d.ts +9 -0
- package/dist/root/classifier.d.ts.map +1 -0
- package/dist/root/commands.d.ts +9 -0
- package/dist/root/commands.d.ts.map +1 -0
- package/dist/root/data/event-detail.d.ts +10 -0
- package/dist/root/data/event-detail.d.ts.map +1 -0
- package/dist/root/data/field-value.d.ts +3 -0
- package/dist/root/data/field-value.d.ts.map +1 -0
- package/dist/root/data/index.d.ts +3 -0
- package/dist/root/data/index.d.ts.map +1 -0
- package/dist/root/data/widget-load.d.ts +5 -0
- package/dist/root/data/widget-load.d.ts.map +1 -0
- package/dist/root/index.d.ts +10 -0
- package/dist/root/index.d.ts.map +1 -0
- package/dist/root/index.iife.js +2 -0
- package/dist/root/index.iife.js.map +1 -0
- package/dist/root/index.js +2692 -0
- package/dist/root/index.js.map +1 -0
- package/dist/root/keys.d.ts +6 -0
- package/dist/root/keys.d.ts.map +1 -0
- package/dist/root/message/event.d.ts +7 -0
- package/dist/root/message/event.d.ts.map +1 -0
- package/dist/root/message/index.d.ts +5 -0
- package/dist/root/message/index.d.ts.map +1 -0
- package/dist/root/message/message.d.ts +37 -0
- package/dist/root/message/message.d.ts.map +1 -0
- package/dist/root/message/roles.d.ts +4 -0
- package/dist/root/message/roles.d.ts.map +1 -0
- package/dist/root/message/tags.d.ts +69 -0
- package/dist/root/message/tags.d.ts.map +1 -0
- package/dist/root/sources/alerts.d.ts +162 -0
- package/dist/root/sources/alerts.d.ts.map +1 -0
- package/dist/root/sources/index.d.ts +5 -0
- package/dist/root/sources/index.d.ts.map +1 -0
- package/dist/root/sources/messages.d.ts +963 -0
- package/dist/root/sources/messages.d.ts.map +1 -0
- package/dist/root/sources/on-widget-load-detail.d.ts +542 -0
- package/dist/root/sources/on-widget-load-detail.d.ts.map +1 -0
- package/dist/root/types/index.d.ts +3 -0
- package/dist/root/types/index.d.ts.map +1 -0
- package/dist/root/types/on-event-received.d.ts +170 -0
- package/dist/root/types/on-event-received.d.ts.map +1 -0
- package/dist/root/types/on-widget-load.d.ts +135 -0
- package/dist/root/types/on-widget-load.d.ts.map +1 -0
- package/dist/root/window-event-map.d.ts +10 -0
- package/dist/root/window-event-map.d.ts.map +1 -0
- package/package.json +48 -0
- package/src/react/hooks/index.ts +3 -0
- package/src/react/hooks/use-event-listener.ts +17 -0
- package/src/react/hooks/use-on-event-received.ts +206 -0
- package/src/react/hooks/use-on-widget-load.ts +15 -0
- package/src/react/index.ts +3 -0
- package/src/react/types/index.ts +1 -0
- package/src/react/types/window-events.ts +6 -0
- package/src/root/aggregate.ts +258 -0
- package/src/root/classifier.ts +208 -0
- package/src/root/commands.ts +33 -0
- package/src/root/data/event-detail.ts +14 -0
- package/src/root/data/field-value.ts +2 -0
- package/src/root/data/index.ts +2 -0
- package/src/root/data/widget-load.ts +5 -0
- package/src/root/index.ts +9 -0
- package/src/root/keys.ts +14 -0
- package/src/root/message/event.ts +7 -0
- package/src/root/message/index.ts +4 -0
- package/src/root/message/message.ts +40 -0
- package/src/root/message/roles.ts +43 -0
- package/src/root/message/tags.ts +119 -0
- package/src/root/sources/alerts.ts +163 -0
- package/src/root/sources/index.ts +5 -0
- package/src/root/sources/messages.ts +1245 -0
- package/src/root/sources/on-widget-load-detail.ts +969 -0
- package/src/root/types/index.ts +2 -0
- package/src/root/types/on-event-received.ts +198 -0
- package/src/root/types/on-widget-load.ts +171 -0
- package/src/root/window-event-map.ts +11 -0
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Event normalizer — aggregate mode: buffer events by activityGroup,
|
|
3
|
+
* merge into one normalized event per action (timeout 2.5s).
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { classifyEvent, getCacheKey } from "./classifier.js";
|
|
7
|
+
import type {
|
|
8
|
+
IncomingDetail,
|
|
9
|
+
NormalizerOptions,
|
|
10
|
+
CacheSubsByType,
|
|
11
|
+
ClassifiedEvent,
|
|
12
|
+
} from "./types/on-event-received.js";
|
|
13
|
+
|
|
14
|
+
const AGGREGATE_TIMEOUT_MS = 2500;
|
|
15
|
+
|
|
16
|
+
interface BufferEntry {
|
|
17
|
+
type: string;
|
|
18
|
+
subscribers: Array<{
|
|
19
|
+
recipient?: string;
|
|
20
|
+
username?: string;
|
|
21
|
+
name?: string;
|
|
22
|
+
sender?: string;
|
|
23
|
+
}>;
|
|
24
|
+
tier: number;
|
|
25
|
+
tierText: string;
|
|
26
|
+
totalAmount: number;
|
|
27
|
+
purchase?: ClassifiedEvent;
|
|
28
|
+
sender?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const bufferByGroup: Record<string, BufferEntry> = {};
|
|
32
|
+
const bufferTimers: Record<string, ReturnType<typeof setTimeout>> = {};
|
|
33
|
+
|
|
34
|
+
function getCacheForType(
|
|
35
|
+
cacheSubs: CacheSubsByType | undefined,
|
|
36
|
+
type: string,
|
|
37
|
+
): {
|
|
38
|
+
_has: (k: string) => boolean;
|
|
39
|
+
_set: (k: string, v: boolean) => void;
|
|
40
|
+
} | null {
|
|
41
|
+
if (!cacheSubs) return null;
|
|
42
|
+
if (
|
|
43
|
+
typeof cacheSubs._has === "function" &&
|
|
44
|
+
typeof cacheSubs._set === "function"
|
|
45
|
+
) {
|
|
46
|
+
return cacheSubs as unknown as {
|
|
47
|
+
_has: (k: string) => boolean;
|
|
48
|
+
_set: (k: string, v: boolean) => void;
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
const c = cacheSubs[type];
|
|
52
|
+
return c && typeof (c as { _has?: unknown })._has === "function"
|
|
53
|
+
? (c as {
|
|
54
|
+
_has: (k: string) => boolean;
|
|
55
|
+
_set: (k: string, v: boolean) => void;
|
|
56
|
+
})
|
|
57
|
+
: null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function emitAggregated(
|
|
61
|
+
groupKey: string,
|
|
62
|
+
merged: Record<string, unknown>,
|
|
63
|
+
cacheSubs: CacheSubsByType | undefined,
|
|
64
|
+
onNormalized: (event: ClassifiedEvent | null) => void,
|
|
65
|
+
): void {
|
|
66
|
+
const cache = getCacheForType(cacheSubs, merged.type as string);
|
|
67
|
+
if (cache && cache._has(groupKey)) return;
|
|
68
|
+
if (cache) cache._set(groupKey, true);
|
|
69
|
+
onNormalized(merged as ClassifiedEvent);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function flushBuffer(
|
|
73
|
+
groupKey: string,
|
|
74
|
+
cacheSubs: CacheSubsByType | undefined,
|
|
75
|
+
onNormalized: (event: ClassifiedEvent | null) => void,
|
|
76
|
+
broadcaster: string,
|
|
77
|
+
): void {
|
|
78
|
+
const buf = bufferByGroup[groupKey];
|
|
79
|
+
if (!buf) return;
|
|
80
|
+
delete bufferByGroup[groupKey];
|
|
81
|
+
if (bufferTimers[groupKey]) {
|
|
82
|
+
clearTimeout(bufferTimers[groupKey]);
|
|
83
|
+
delete bufferTimers[groupKey];
|
|
84
|
+
}
|
|
85
|
+
const purchase = buf.purchase as Record<string, unknown> | undefined;
|
|
86
|
+
const subscribers = buf.subscribers ?? [];
|
|
87
|
+
const type = buf.type;
|
|
88
|
+
const tier = buf.tier;
|
|
89
|
+
const tierText = buf.tierText;
|
|
90
|
+
const totalAmount = buf.totalAmount ?? subscribers.length;
|
|
91
|
+
const recipients = subscribers
|
|
92
|
+
.map((s) => s.recipient ?? s.username ?? s.name)
|
|
93
|
+
.filter(Boolean) as string[];
|
|
94
|
+
const firstSub = subscribers[0];
|
|
95
|
+
const senderFromSub = firstSub?.sender;
|
|
96
|
+
const sender = purchase?.sender ?? buf.sender ?? senderFromSub;
|
|
97
|
+
if (type === "community-gift" && broadcaster && sender === broadcaster) {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
if (type === "community-gift-anonymous") {
|
|
101
|
+
emitAggregated(
|
|
102
|
+
groupKey,
|
|
103
|
+
{
|
|
104
|
+
type: "community-gift-anonymous",
|
|
105
|
+
tier,
|
|
106
|
+
tierText,
|
|
107
|
+
totalAmount,
|
|
108
|
+
recipients,
|
|
109
|
+
anonymousDisplayName: purchase?.anonymousDisplayName,
|
|
110
|
+
},
|
|
111
|
+
cacheSubs,
|
|
112
|
+
onNormalized,
|
|
113
|
+
);
|
|
114
|
+
} else {
|
|
115
|
+
emitAggregated(
|
|
116
|
+
groupKey,
|
|
117
|
+
{
|
|
118
|
+
type: "community-gift",
|
|
119
|
+
sender,
|
|
120
|
+
tier,
|
|
121
|
+
tierText,
|
|
122
|
+
totalAmount,
|
|
123
|
+
recipients,
|
|
124
|
+
},
|
|
125
|
+
cacheSubs,
|
|
126
|
+
onNormalized,
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Normalizes an incoming subscription event (StreamElements/Twitch).
|
|
133
|
+
* Buffers community-gift recipients by activityGroup, then emits one normalized event per group.
|
|
134
|
+
* For self-sub, solo-sub, sub-renewal emits immediately (with optional cache dedup).
|
|
135
|
+
*/
|
|
136
|
+
export function normalizeIncomingEvent(
|
|
137
|
+
detail: IncomingDetail,
|
|
138
|
+
options: NormalizerOptions | undefined,
|
|
139
|
+
onNormalized: (event: ClassifiedEvent | null) => void,
|
|
140
|
+
): void {
|
|
141
|
+
const cacheSubs = options?.cacheSubs;
|
|
142
|
+
const broadcaster = options?.broadcasterLogin ?? "";
|
|
143
|
+
|
|
144
|
+
const classified = classifyEvent(detail);
|
|
145
|
+
if (!classified) {
|
|
146
|
+
onNormalized(null);
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const cache = getCacheForType(cacheSubs, classified.type);
|
|
151
|
+
const cacheKey = getCacheKey(detail);
|
|
152
|
+
const event = detail.event ?? {};
|
|
153
|
+
const data = (event.data ?? event) as Record<string, unknown>;
|
|
154
|
+
|
|
155
|
+
if ((classified as Record<string, unknown>)._role === "recipient") {
|
|
156
|
+
const ag =
|
|
157
|
+
((classified as Record<string, unknown>).activityGroup as string) ??
|
|
158
|
+
cacheKey;
|
|
159
|
+
if (!bufferByGroup[ag]) {
|
|
160
|
+
bufferByGroup[ag] = {
|
|
161
|
+
type: classified.type,
|
|
162
|
+
subscribers: [],
|
|
163
|
+
tier: classified.tier,
|
|
164
|
+
tierText: classified.tierText,
|
|
165
|
+
totalAmount: 0,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
if (bufferTimers[ag]) clearTimeout(bufferTimers[ag]);
|
|
169
|
+
bufferTimers[ag] = setTimeout(() => {
|
|
170
|
+
flushBuffer(ag, cacheSubs, onNormalized, broadcaster);
|
|
171
|
+
}, AGGREGATE_TIMEOUT_MS);
|
|
172
|
+
bufferByGroup[ag].subscribers.push({
|
|
173
|
+
recipient: (classified as Record<string, unknown>).recipient as string,
|
|
174
|
+
username: data.username as string,
|
|
175
|
+
name: data.displayName as string,
|
|
176
|
+
sender: data.sender as string,
|
|
177
|
+
});
|
|
178
|
+
if (bufferByGroup[ag].totalAmount === 0) bufferByGroup[ag].totalAmount = 1;
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (
|
|
183
|
+
classified.type === "community-gift-anonymous" ||
|
|
184
|
+
classified.type === "community-gift"
|
|
185
|
+
) {
|
|
186
|
+
const isPurchase =
|
|
187
|
+
detail.listener === "event" && event.type === "communityGiftPurchase";
|
|
188
|
+
if (isPurchase) {
|
|
189
|
+
const ag = (event.activityGroup ??
|
|
190
|
+
(data && data.activityGroup) ??
|
|
191
|
+
cacheKey) as string;
|
|
192
|
+
if (!bufferByGroup[ag]) {
|
|
193
|
+
bufferByGroup[ag] = {
|
|
194
|
+
type: classified.type,
|
|
195
|
+
purchase: classified,
|
|
196
|
+
subscribers: [],
|
|
197
|
+
tier: classified.tier,
|
|
198
|
+
tierText: classified.tierText,
|
|
199
|
+
totalAmount:
|
|
200
|
+
Number((classified as Record<string, unknown>).totalAmount) || 0,
|
|
201
|
+
sender: (classified as Record<string, unknown>).sender as string,
|
|
202
|
+
};
|
|
203
|
+
bufferTimers[ag] = setTimeout(() => {
|
|
204
|
+
flushBuffer(ag, cacheSubs, onNormalized, broadcaster);
|
|
205
|
+
}, AGGREGATE_TIMEOUT_MS);
|
|
206
|
+
} else {
|
|
207
|
+
bufferByGroup[ag].purchase = classified;
|
|
208
|
+
bufferByGroup[ag].tier = classified.tier;
|
|
209
|
+
bufferByGroup[ag].tierText = classified.tierText;
|
|
210
|
+
bufferByGroup[ag].totalAmount =
|
|
211
|
+
((classified as Record<string, unknown>).totalAmount as number) ??
|
|
212
|
+
bufferByGroup[ag].totalAmount;
|
|
213
|
+
bufferByGroup[ag].sender = (classified as Record<string, unknown>)
|
|
214
|
+
.sender as string;
|
|
215
|
+
if (bufferTimers[ag]) clearTimeout(bufferTimers[ag]);
|
|
216
|
+
bufferTimers[ag] = setTimeout(() => {
|
|
217
|
+
flushBuffer(ag, cacheSubs, onNormalized, broadcaster);
|
|
218
|
+
}, AGGREGATE_TIMEOUT_MS);
|
|
219
|
+
}
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
if (detail.listener === "subscriber-latest") {
|
|
223
|
+
const c = classified as Record<string, unknown>;
|
|
224
|
+
if (
|
|
225
|
+
classified.type === "community-gift" &&
|
|
226
|
+
broadcaster &&
|
|
227
|
+
c.sender === broadcaster
|
|
228
|
+
) {
|
|
229
|
+
onNormalized(null);
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (
|
|
238
|
+
classified.type === "self-sub" ||
|
|
239
|
+
classified.type === "solo-sub-to-someone" ||
|
|
240
|
+
classified.type === "sub-renewal"
|
|
241
|
+
) {
|
|
242
|
+
if (cache?._has(cacheKey)) {
|
|
243
|
+
onNormalized(null);
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
if (cache) cache._set(cacheKey, true);
|
|
247
|
+
const c = classified as Record<string, unknown>;
|
|
248
|
+
if (
|
|
249
|
+
classified.type === "solo-sub-to-someone" &&
|
|
250
|
+
broadcaster &&
|
|
251
|
+
c.sender === broadcaster
|
|
252
|
+
) {
|
|
253
|
+
onNormalized(null);
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
onNormalized(classified);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared classification and tier parsing for Twitch/StreamElements subscription events.
|
|
3
|
+
* Used by aggregate normalizer.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type {
|
|
7
|
+
IncomingDetail,
|
|
8
|
+
TierInfo,
|
|
9
|
+
ClassifiedEvent,
|
|
10
|
+
} from "./types/on-event-received.js";
|
|
11
|
+
|
|
12
|
+
export function parseTier(raw: string | number | null | undefined): TierInfo {
|
|
13
|
+
if (raw == null) return { tier: 0, tierText: "" };
|
|
14
|
+
const tierText = typeof raw === "string" ? raw : String(raw);
|
|
15
|
+
const num = parseInt(tierText, 10);
|
|
16
|
+
if (isNaN(num)) return { tier: 0, tierText };
|
|
17
|
+
const tier = num >= 3000 ? 3 : num >= 2000 ? 2 : num >= 1000 ? 1 : 0;
|
|
18
|
+
return { tier, tierText };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function getCacheKey(detail: IncomingDetail): string {
|
|
22
|
+
const listener = detail.listener;
|
|
23
|
+
const event = detail.event ?? {};
|
|
24
|
+
const ag = event.activityGroup;
|
|
25
|
+
const aid = event.activityId ?? event._id;
|
|
26
|
+
if (ag) return String(ag);
|
|
27
|
+
if (aid) return String(aid);
|
|
28
|
+
if (listener === "subscriber-latest" && event.bulkGifted) {
|
|
29
|
+
return (
|
|
30
|
+
"sl-bulk-" +
|
|
31
|
+
(event.sender ?? "") +
|
|
32
|
+
"-" +
|
|
33
|
+
(event.amount ?? "") +
|
|
34
|
+
"-" +
|
|
35
|
+
(event.tier ?? "") +
|
|
36
|
+
"-" +
|
|
37
|
+
(event._id ?? "")
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
return "sl-" + (event._id ?? event.activityId ?? "");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function classifyEvent(detail: IncomingDetail): ClassifiedEvent | null {
|
|
44
|
+
const listener = detail.listener;
|
|
45
|
+
const event = detail.event ?? {};
|
|
46
|
+
const data = (event.data ?? event) as Record<string, unknown>;
|
|
47
|
+
const tierInfo = parseTier(
|
|
48
|
+
(data.tier ?? event.tier) as string | number | undefined,
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
if (listener === "event") {
|
|
52
|
+
if (event.type === "communityGiftPurchase") {
|
|
53
|
+
const isAnon =
|
|
54
|
+
(data.username &&
|
|
55
|
+
String(data.username).toLowerCase() === "anonymous") ||
|
|
56
|
+
data.sender === "Anonymous";
|
|
57
|
+
return {
|
|
58
|
+
type: isAnon ? "community-gift-anonymous" : "community-gift",
|
|
59
|
+
tier: tierInfo.tier,
|
|
60
|
+
tierText: tierInfo.tierText,
|
|
61
|
+
totalAmount: parseInt(String(data.amount), 10) || 1,
|
|
62
|
+
recipients: [],
|
|
63
|
+
sender: isAnon
|
|
64
|
+
? undefined
|
|
65
|
+
: ((data.sender ?? data.displayName ?? data.username) as string),
|
|
66
|
+
anonymousDisplayName: isAnon
|
|
67
|
+
? ((data.displayName as string) ?? "Anonymous")
|
|
68
|
+
: undefined,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
if (event.type === "subscriber") {
|
|
72
|
+
const gifted = data.gifted === true;
|
|
73
|
+
const sender = data.sender as string | undefined;
|
|
74
|
+
const amount = parseInt(String(data.amount), 10) || 1;
|
|
75
|
+
const username = (data.username ?? data.displayName) as
|
|
76
|
+
| string
|
|
77
|
+
| undefined;
|
|
78
|
+
const activityGroup = event.activityGroup as string | undefined;
|
|
79
|
+
|
|
80
|
+
if (sender === "Anonymous" && activityGroup) {
|
|
81
|
+
return {
|
|
82
|
+
type: "community-gift-anonymous",
|
|
83
|
+
_role: "recipient",
|
|
84
|
+
activityGroup,
|
|
85
|
+
recipient: (username ?? data.displayName) as string,
|
|
86
|
+
tier: tierInfo.tier,
|
|
87
|
+
tierText: tierInfo.tierText,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
if (gifted && sender && sender !== "Anonymous") {
|
|
91
|
+
const isCommunity =
|
|
92
|
+
data.communityGifted === true ||
|
|
93
|
+
(activityGroup != null && activityGroup !== "");
|
|
94
|
+
if (!isCommunity) {
|
|
95
|
+
const firstGift =
|
|
96
|
+
String(data.message ?? "").indexOf("first Gift Sub") !== -1;
|
|
97
|
+
return {
|
|
98
|
+
type: "solo-sub-to-someone",
|
|
99
|
+
sender,
|
|
100
|
+
recipient: (username ?? data.displayName) as string,
|
|
101
|
+
tier: tierInfo.tier,
|
|
102
|
+
tierText: tierInfo.tierText,
|
|
103
|
+
firstGiftInChannel: firstGift,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
return {
|
|
107
|
+
type: "community-gift",
|
|
108
|
+
_role: "recipient",
|
|
109
|
+
activityGroup: activityGroup!,
|
|
110
|
+
recipient: (username ?? data.displayName) as string,
|
|
111
|
+
tier: tierInfo.tier,
|
|
112
|
+
tierText: tierInfo.tierText,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
if (!gifted && amount > 1) {
|
|
116
|
+
return {
|
|
117
|
+
type: "sub-renewal",
|
|
118
|
+
name: (username ?? data.displayName) as string,
|
|
119
|
+
months: amount,
|
|
120
|
+
tier: tierInfo.tier,
|
|
121
|
+
tierText: tierInfo.tierText,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
if (!gifted && amount === 1) {
|
|
125
|
+
return {
|
|
126
|
+
type: "self-sub",
|
|
127
|
+
name: (username ?? data.displayName) as string,
|
|
128
|
+
tier: tierInfo.tier,
|
|
129
|
+
tierText: tierInfo.tierText,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
if (activityGroup) {
|
|
133
|
+
return {
|
|
134
|
+
type: "community-gift",
|
|
135
|
+
_role: "recipient",
|
|
136
|
+
activityGroup,
|
|
137
|
+
recipient: (username ?? data.displayName) as string,
|
|
138
|
+
tier: tierInfo.tier,
|
|
139
|
+
tierText: tierInfo.tierText,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (listener === "subscriber-latest") {
|
|
148
|
+
const bulkGifted = event.bulkGifted === true;
|
|
149
|
+
const sender = event.sender as string | undefined;
|
|
150
|
+
const name = event.name as string | undefined;
|
|
151
|
+
const amount = parseInt(String(event.amount), 10) || 1;
|
|
152
|
+
const gifted = event.gifted === true;
|
|
153
|
+
const isCommunityGift =
|
|
154
|
+
event.isCommunityGift === true || event.communityGifted === true;
|
|
155
|
+
|
|
156
|
+
if (bulkGifted && sender === "Anonymous") {
|
|
157
|
+
return {
|
|
158
|
+
type: "community-gift-anonymous",
|
|
159
|
+
tier: tierInfo.tier,
|
|
160
|
+
tierText: tierInfo.tierText,
|
|
161
|
+
totalAmount: amount,
|
|
162
|
+
recipients: [],
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
if (bulkGifted && sender) {
|
|
166
|
+
return {
|
|
167
|
+
type: "community-gift",
|
|
168
|
+
sender,
|
|
169
|
+
tier: tierInfo.tier,
|
|
170
|
+
tierText: tierInfo.tierText,
|
|
171
|
+
totalAmount: amount,
|
|
172
|
+
recipients: [],
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
if (gifted && !isCommunityGift && sender) {
|
|
176
|
+
const firstGift =
|
|
177
|
+
String(event.message ?? "").indexOf("first Gift Sub") !== -1;
|
|
178
|
+
return {
|
|
179
|
+
type: "solo-sub-to-someone",
|
|
180
|
+
sender,
|
|
181
|
+
recipient: name ?? "",
|
|
182
|
+
tier: tierInfo.tier,
|
|
183
|
+
tierText: tierInfo.tierText,
|
|
184
|
+
firstGiftInChannel: firstGift,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
if (!gifted && amount > 1) {
|
|
188
|
+
return {
|
|
189
|
+
type: "sub-renewal",
|
|
190
|
+
name: name ?? "",
|
|
191
|
+
months: amount,
|
|
192
|
+
tier: tierInfo.tier,
|
|
193
|
+
tierText: tierInfo.tierText,
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
if (!gifted && amount === 1 && (sender == null || name === sender)) {
|
|
197
|
+
return {
|
|
198
|
+
type: "self-sub",
|
|
199
|
+
name: name ?? "",
|
|
200
|
+
tier: tierInfo.tier,
|
|
201
|
+
tierText: tierInfo.tierText,
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return null;
|
|
208
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Команды, вызывающие onEventReceived с тестовыми сообщением или алертом.
|
|
3
|
+
* Работают только в браузере (требуется window).
|
|
4
|
+
*/
|
|
5
|
+
import { testMessages, testAlerts } from "./sources";
|
|
6
|
+
|
|
7
|
+
const EVENT_NAME = "onEventReceived";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Отправляет тестовое сообщение по ключу: диспатчит CustomEvent onEventReceived.
|
|
11
|
+
*/
|
|
12
|
+
export function testMessage(key: string): void {
|
|
13
|
+
if (typeof window === "undefined") {
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
const msg = testMessages[key as keyof typeof testMessages];
|
|
17
|
+
if (msg) {
|
|
18
|
+
window.dispatchEvent(new CustomEvent(EVENT_NAME, { detail: msg }));
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Отправляет тестовый алерт по ключу: диспатчит CustomEvent onEventReceived.
|
|
24
|
+
*/
|
|
25
|
+
export function testAlert(key: string): void {
|
|
26
|
+
if (typeof window === "undefined") {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
const alert = testAlerts[key as keyof typeof testAlerts];
|
|
30
|
+
if (alert) {
|
|
31
|
+
window.dispatchEvent(new CustomEvent(EVENT_NAME, { detail: alert }));
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface EventDetail<T = string, R = unknown> {
|
|
2
|
+
listener: T;
|
|
3
|
+
event: R;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export type FollowerLatestDetail = EventDetail<"follower-latest">;
|
|
7
|
+
|
|
8
|
+
export type WidgetButtonDetail = EventDetail<
|
|
9
|
+
"event:test",
|
|
10
|
+
{
|
|
11
|
+
listener: "widget-button";
|
|
12
|
+
field: string;
|
|
13
|
+
}
|
|
14
|
+
>;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export * from "./classifier.js";
|
|
2
|
+
export * from "./aggregate.js";
|
|
3
|
+
export * from "./types";
|
|
4
|
+
import "./window-event-map";
|
|
5
|
+
export * from "./data";
|
|
6
|
+
export * from "./sources/index";
|
|
7
|
+
export * from "./commands";
|
|
8
|
+
export * from "./keys";
|
|
9
|
+
export * from "./message";
|
package/src/root/keys.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Преобразование fieldId виджета в ключ тестовых данных.
|
|
3
|
+
*/
|
|
4
|
+
export function getTestMessageKey(fieldId: string): string | null {
|
|
5
|
+
if (!fieldId.startsWith("testMessage")) return null;
|
|
6
|
+
const name = fieldId.replace("testMessage", "");
|
|
7
|
+
return name.charAt(0).toLowerCase() + name.slice(1);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function getTestAlertKey(fieldId: string): string | null {
|
|
11
|
+
if (!fieldId.startsWith("testAlert")) return null;
|
|
12
|
+
const name = fieldId.replace("testAlert", "");
|
|
13
|
+
return name.charAt(0).toLowerCase() + name.slice(1);
|
|
14
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { TwitchMessageTags } from "./tags";
|
|
2
|
+
|
|
3
|
+
export namespace Message {
|
|
4
|
+
export interface Event {
|
|
5
|
+
service: "twitch";
|
|
6
|
+
data: Data;
|
|
7
|
+
renderedText: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface Data {
|
|
11
|
+
tags: TwitchMessageTags;
|
|
12
|
+
nick: string;
|
|
13
|
+
userId: string;
|
|
14
|
+
displayName: string;
|
|
15
|
+
displayColor?: string;
|
|
16
|
+
badges: Array<Badge>;
|
|
17
|
+
channel: string;
|
|
18
|
+
text: string;
|
|
19
|
+
isAction: boolean;
|
|
20
|
+
emotes: Array<Emote>;
|
|
21
|
+
msgId: string;
|
|
22
|
+
time?: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface Badge {
|
|
26
|
+
type: string;
|
|
27
|
+
version?: string;
|
|
28
|
+
url?: string;
|
|
29
|
+
description?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface Emote {
|
|
33
|
+
type: string;
|
|
34
|
+
name: string;
|
|
35
|
+
id: string;
|
|
36
|
+
urls: Record<string, string>;
|
|
37
|
+
start?: number;
|
|
38
|
+
end?: number;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import {
|
|
2
|
+
isArtist,
|
|
3
|
+
isBroadcaster,
|
|
4
|
+
isModerator,
|
|
5
|
+
isSubscriber,
|
|
6
|
+
isTurbo,
|
|
7
|
+
isVip,
|
|
8
|
+
type TwitchMessageTags,
|
|
9
|
+
} from "./tags";
|
|
10
|
+
|
|
11
|
+
export type UserRole =
|
|
12
|
+
| "broadcaster"
|
|
13
|
+
| "moderator"
|
|
14
|
+
| "vip"
|
|
15
|
+
| "artist"
|
|
16
|
+
| "subscriber"
|
|
17
|
+
| "turbo"
|
|
18
|
+
| "default";
|
|
19
|
+
|
|
20
|
+
export function determineUserRoles(
|
|
21
|
+
tags: TwitchMessageTags | Record<string, string>,
|
|
22
|
+
): UserRole[] {
|
|
23
|
+
const roles: UserRole[] = ["default"];
|
|
24
|
+
if (isBroadcaster(tags)) {
|
|
25
|
+
roles.push("broadcaster");
|
|
26
|
+
}
|
|
27
|
+
if (isModerator(tags)) {
|
|
28
|
+
roles.push("moderator");
|
|
29
|
+
}
|
|
30
|
+
if (isVip(tags)) {
|
|
31
|
+
roles.push("vip");
|
|
32
|
+
}
|
|
33
|
+
if (isArtist(tags)) {
|
|
34
|
+
roles.push("artist");
|
|
35
|
+
}
|
|
36
|
+
if (isSubscriber(tags)) {
|
|
37
|
+
roles.push("subscriber");
|
|
38
|
+
}
|
|
39
|
+
if (isTurbo(tags)) {
|
|
40
|
+
roles.push("turbo");
|
|
41
|
+
}
|
|
42
|
+
return roles;
|
|
43
|
+
}
|