@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.
Files changed (98) hide show
  1. package/README.md +46 -0
  2. package/README_RU.md +46 -0
  3. package/dist/react/hooks/index.d.ts +4 -0
  4. package/dist/react/hooks/index.d.ts.map +1 -0
  5. package/dist/react/hooks/use-event-listener.d.ts +4 -0
  6. package/dist/react/hooks/use-event-listener.d.ts.map +1 -0
  7. package/dist/react/hooks/use-on-event-received.d.ts +44 -0
  8. package/dist/react/hooks/use-on-event-received.d.ts.map +1 -0
  9. package/dist/react/hooks/use-on-widget-load.d.ts +4 -0
  10. package/dist/react/hooks/use-on-widget-load.d.ts.map +1 -0
  11. package/dist/react/index.d.ts +4 -0
  12. package/dist/react/index.d.ts.map +1 -0
  13. package/dist/react/index.iife.js +2 -0
  14. package/dist/react/index.iife.js.map +1 -0
  15. package/dist/react/index.js +179 -0
  16. package/dist/react/index.js.map +1 -0
  17. package/dist/react/types/index.d.ts +2 -0
  18. package/dist/react/types/index.d.ts.map +1 -0
  19. package/dist/react/types/window-events.d.ts +3 -0
  20. package/dist/react/types/window-events.d.ts.map +1 -0
  21. package/dist/root/aggregate.d.ts +12 -0
  22. package/dist/root/aggregate.d.ts.map +1 -0
  23. package/dist/root/classifier.d.ts +9 -0
  24. package/dist/root/classifier.d.ts.map +1 -0
  25. package/dist/root/commands.d.ts +9 -0
  26. package/dist/root/commands.d.ts.map +1 -0
  27. package/dist/root/data/event-detail.d.ts +10 -0
  28. package/dist/root/data/event-detail.d.ts.map +1 -0
  29. package/dist/root/data/field-value.d.ts +3 -0
  30. package/dist/root/data/field-value.d.ts.map +1 -0
  31. package/dist/root/data/index.d.ts +3 -0
  32. package/dist/root/data/index.d.ts.map +1 -0
  33. package/dist/root/data/widget-load.d.ts +5 -0
  34. package/dist/root/data/widget-load.d.ts.map +1 -0
  35. package/dist/root/index.d.ts +10 -0
  36. package/dist/root/index.d.ts.map +1 -0
  37. package/dist/root/index.iife.js +2 -0
  38. package/dist/root/index.iife.js.map +1 -0
  39. package/dist/root/index.js +2692 -0
  40. package/dist/root/index.js.map +1 -0
  41. package/dist/root/keys.d.ts +6 -0
  42. package/dist/root/keys.d.ts.map +1 -0
  43. package/dist/root/message/event.d.ts +7 -0
  44. package/dist/root/message/event.d.ts.map +1 -0
  45. package/dist/root/message/index.d.ts +5 -0
  46. package/dist/root/message/index.d.ts.map +1 -0
  47. package/dist/root/message/message.d.ts +37 -0
  48. package/dist/root/message/message.d.ts.map +1 -0
  49. package/dist/root/message/roles.d.ts +4 -0
  50. package/dist/root/message/roles.d.ts.map +1 -0
  51. package/dist/root/message/tags.d.ts +69 -0
  52. package/dist/root/message/tags.d.ts.map +1 -0
  53. package/dist/root/sources/alerts.d.ts +162 -0
  54. package/dist/root/sources/alerts.d.ts.map +1 -0
  55. package/dist/root/sources/index.d.ts +5 -0
  56. package/dist/root/sources/index.d.ts.map +1 -0
  57. package/dist/root/sources/messages.d.ts +963 -0
  58. package/dist/root/sources/messages.d.ts.map +1 -0
  59. package/dist/root/sources/on-widget-load-detail.d.ts +542 -0
  60. package/dist/root/sources/on-widget-load-detail.d.ts.map +1 -0
  61. package/dist/root/types/index.d.ts +3 -0
  62. package/dist/root/types/index.d.ts.map +1 -0
  63. package/dist/root/types/on-event-received.d.ts +170 -0
  64. package/dist/root/types/on-event-received.d.ts.map +1 -0
  65. package/dist/root/types/on-widget-load.d.ts +135 -0
  66. package/dist/root/types/on-widget-load.d.ts.map +1 -0
  67. package/dist/root/window-event-map.d.ts +10 -0
  68. package/dist/root/window-event-map.d.ts.map +1 -0
  69. package/package.json +48 -0
  70. package/src/react/hooks/index.ts +3 -0
  71. package/src/react/hooks/use-event-listener.ts +17 -0
  72. package/src/react/hooks/use-on-event-received.ts +206 -0
  73. package/src/react/hooks/use-on-widget-load.ts +15 -0
  74. package/src/react/index.ts +3 -0
  75. package/src/react/types/index.ts +1 -0
  76. package/src/react/types/window-events.ts +6 -0
  77. package/src/root/aggregate.ts +258 -0
  78. package/src/root/classifier.ts +208 -0
  79. package/src/root/commands.ts +33 -0
  80. package/src/root/data/event-detail.ts +14 -0
  81. package/src/root/data/field-value.ts +2 -0
  82. package/src/root/data/index.ts +2 -0
  83. package/src/root/data/widget-load.ts +5 -0
  84. package/src/root/index.ts +9 -0
  85. package/src/root/keys.ts +14 -0
  86. package/src/root/message/event.ts +7 -0
  87. package/src/root/message/index.ts +4 -0
  88. package/src/root/message/message.ts +40 -0
  89. package/src/root/message/roles.ts +43 -0
  90. package/src/root/message/tags.ts +119 -0
  91. package/src/root/sources/alerts.ts +163 -0
  92. package/src/root/sources/index.ts +5 -0
  93. package/src/root/sources/messages.ts +1245 -0
  94. package/src/root/sources/on-widget-load-detail.ts +969 -0
  95. package/src/root/types/index.ts +2 -0
  96. package/src/root/types/on-event-received.ts +198 -0
  97. package/src/root/types/on-widget-load.ts +171 -0
  98. 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,2 @@
1
+ /** Минимальный тип для fieldData в WidgetLoadDetail (без зависимости от streamelements). */
2
+ export type FieldValue = string | number | boolean | undefined | null;
@@ -0,0 +1,2 @@
1
+ export * from "./event-detail";
2
+ export * from "./widget-load";
@@ -0,0 +1,5 @@
1
+ import type { FieldValue } from "./field-value.js";
2
+
3
+ export interface WidgetLoadDetail {
4
+ fieldData: Record<string, FieldValue>;
5
+ }
@@ -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";
@@ -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,7 @@
1
+ import type { Message } from "./message";
2
+
3
+ /** Событие сообщения чата (формат onEventReceived, listener "message"). */
4
+ export interface MessageEvent {
5
+ listener: "message";
6
+ event: Message.Event;
7
+ }
@@ -0,0 +1,4 @@
1
+ export * from "./event"
2
+ export * from "./message"
3
+ export * from "./roles"
4
+ export * from "./tags"
@@ -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
+ }