Compare commits
3
Commits
v0.5.7
..
9c64309c51
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9c64309c51 | ||
|
|
67ca472317 | ||
|
|
884e4517d8 |
No files matched your search
@@ -32,3 +32,6 @@ docs/handbuch/*.log
|
|||||||
docs/handbuch/*.out
|
docs/handbuch/*.out
|
||||||
docs/handbuch/*.toc
|
docs/handbuch/*.toc
|
||||||
docs/handbuch/*.png
|
docs/handbuch/*.png
|
||||||
|
|
||||||
|
# Mitschriften der Browser-Automatisierung
|
||||||
|
.playwright-mcp/
|
||||||
@@ -1,93 +0,0 @@
|
|||||||
[ 54050ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_RESET @ http://127.0.0.1:4791/events?quelle=pult:0
|
|
||||||
[ 54054ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:4791/soundboard/liste:0
|
|
||||||
[ 55062ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:4791/events?quelle=pult:0
|
|
||||||
[ 55068ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:4791/soundboard/liste:0
|
|
||||||
[ 56069ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:4791/events?quelle=pult:0
|
|
||||||
[ 56071ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:4791/soundboard/liste:0
|
|
||||||
[ 57087ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:4791/events?quelle=pult:0
|
|
||||||
[ 57089ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:4791/soundboard/liste:0
|
|
||||||
[ 58102ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:4791/events?quelle=pult:0
|
|
||||||
[ 58104ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:4791/soundboard/liste:0
|
|
||||||
[ 59105ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:4791/events?quelle=pult:0
|
|
||||||
[ 59107ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:4791/soundboard/liste:0
|
|
||||||
[ 60048ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:4791/soundboard/liste:0
|
|
||||||
[ 60109ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:4791/events?quelle=pult:0
|
|
||||||
[ 60111ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:4791/soundboard/liste:0
|
|
||||||
[ 61142ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:4791/events?quelle=pult:0
|
|
||||||
[ 61160ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:4791/soundboard/liste:0
|
|
||||||
[ 62276ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:4791/events?quelle=pult:0
|
|
||||||
[ 62524ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:4791/soundboard/liste:0
|
|
||||||
[ 63583ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:4791/events?quelle=pult:0
|
|
||||||
[ 63620ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:4791/soundboard/liste:0
|
|
||||||
[ 64985ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:4791/events?quelle=pult:0
|
|
||||||
[ 65532ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:4791/soundboard/liste:0
|
|
||||||
[ 66017ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:4791/events?quelle=pult:0
|
|
||||||
[ 66032ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:4791/soundboard/liste:0
|
|
||||||
[ 67490ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:4791/events?quelle=pult:0
|
|
||||||
[ 67498ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:4791/soundboard/liste:0
|
|
||||||
[ 68503ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:4791/events?quelle=pult:0
|
|
||||||
[ 68506ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:4791/soundboard/liste:0
|
|
||||||
[ 69510ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:4791/events?quelle=pult:0
|
|
||||||
[ 69512ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:4791/soundboard/liste:0
|
|
||||||
[ 70526ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:4791/events?quelle=pult:0
|
|
||||||
[ 70529ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:4791/soundboard/liste:0
|
|
||||||
[ 71536ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:4791/events?quelle=pult:0
|
|
||||||
[ 71538ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:4791/soundboard/liste:0
|
|
||||||
[ 72551ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:4791/events?quelle=pult:0
|
|
||||||
[ 72554ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:4791/soundboard/liste:0
|
|
||||||
[ 73562ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:4791/events?quelle=pult:0
|
|
||||||
[ 73565ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:4791/soundboard/liste:0
|
|
||||||
[ 74571ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:4791/events?quelle=pult:0
|
|
||||||
[ 74574ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:4791/soundboard/liste:0
|
|
||||||
[ 75587ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:4791/events?quelle=pult:0
|
|
||||||
[ 75590ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:4791/soundboard/liste:0
|
|
||||||
[ 76603ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:4791/events?quelle=pult:0
|
|
||||||
[ 76606ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:4791/soundboard/liste:0
|
|
||||||
[ 77607ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:4791/events?quelle=pult:0
|
|
||||||
[ 77608ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:4791/soundboard/liste:0
|
|
||||||
[ 78614ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:4791/events?quelle=pult:0
|
|
||||||
[ 78619ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:4791/soundboard/liste:0
|
|
||||||
[ 79627ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:4791/events?quelle=pult:0
|
|
||||||
[ 79629ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:4791/soundboard/liste:0
|
|
||||||
[ 80112ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:4791/soundboard/liste:0
|
|
||||||
[ 80649ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:4791/events?quelle=pult:0
|
|
||||||
[ 80663ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:4791/soundboard/liste:0
|
|
||||||
[ 82183ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:4791/events?quelle=pult:0
|
|
||||||
[ 82313ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:4791/soundboard/liste:0
|
|
||||||
[ 84634ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:4791/events?quelle=pult:0
|
|
||||||
[ 84970ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:4791/soundboard/liste:0
|
|
||||||
[ 85652ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:4791/events?quelle=pult:0
|
|
||||||
[ 85687ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:4791/soundboard/liste:0
|
|
||||||
[ 87032ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:4791/events?quelle=pult:0
|
|
||||||
[ 87063ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:4791/soundboard/liste:0
|
|
||||||
[ 88257ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:4791/events?quelle=pult:0
|
|
||||||
[ 88277ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:4791/soundboard/liste:0
|
|
||||||
[ 89301ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:4791/events?quelle=pult:0
|
|
||||||
[ 89323ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:4791/soundboard/liste:0
|
|
||||||
[ 90312ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:4791/events?quelle=pult:0
|
|
||||||
[ 90315ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:4791/soundboard/liste:0
|
|
||||||
[ 91330ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:4791/events?quelle=pult:0
|
|
||||||
[ 91333ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:4791/soundboard/liste:0
|
|
||||||
[ 93470ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:4791/events?quelle=pult:0
|
|
||||||
[ 94174ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:4791/soundboard/liste:0
|
|
||||||
[ 94469ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:4791/events?quelle=pult:0
|
|
||||||
[ 94472ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:4791/soundboard/liste:0
|
|
||||||
[ 95485ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:4791/events?quelle=pult:0
|
|
||||||
[ 95487ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:4791/soundboard/liste:0
|
|
||||||
[ 96489ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:4791/events?quelle=pult:0
|
|
||||||
[ 96491ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:4791/soundboard/liste:0
|
|
||||||
[ 97495ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:4791/events?quelle=pult:0
|
|
||||||
[ 97500ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:4791/soundboard/liste:0
|
|
||||||
[ 98504ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:4791/events?quelle=pult:0
|
|
||||||
[ 98507ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:4791/soundboard/liste:0
|
|
||||||
[ 99514ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:4791/events?quelle=pult:0
|
|
||||||
[ 99519ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:4791/soundboard/liste:0
|
|
||||||
[ 100093ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:4791/soundboard/liste:0
|
|
||||||
[ 100096ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:4791/events?quelle=pult:0
|
|
||||||
[ 100104ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:4791/soundboard/liste:0
|
|
||||||
[ 103134ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:4791/events?quelle=pult:0
|
|
||||||
[ 103167ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:4791/soundboard/liste:0
|
|
||||||
[ 106151ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:4791/events?quelle=pult:0
|
|
||||||
[ 106162ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:4791/soundboard/liste:0
|
|
||||||
[ 109164ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:4791/events?quelle=pult:0
|
|
||||||
[ 109166ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:4791/soundboard/liste:0
|
|
||||||
@@ -30,6 +30,7 @@ import { createBot } from '#discord/bot.ts';
|
|||||||
import { helix } from '#twitch/helix.ts';
|
import { helix } from '#twitch/helix.ts';
|
||||||
import { readSecrets } from '#twitch/secrets.ts';
|
import { readSecrets } from '#twitch/secrets.ts';
|
||||||
import { createEventSub } from '#twitch/eventsub.ts';
|
import { createEventSub } from '#twitch/eventsub.ts';
|
||||||
|
import { createBadges } from '#twitch/badges.ts';
|
||||||
import { createIrc, type UserNotice } from '#twitch/irc.ts';
|
import { createIrc, type UserNotice } from '#twitch/irc.ts';
|
||||||
import { streamWechsel } from '#twitch/stream-state.ts';
|
import { streamWechsel } from '#twitch/stream-state.ts';
|
||||||
import { piperFehlt, sprechen, stimmenPfad } from '#media/piper.ts';
|
import { piperFehlt, sprechen, stimmenPfad } from '#media/piper.ts';
|
||||||
@@ -245,6 +246,21 @@ export async function runHub(): Promise<void> {
|
|||||||
const bot = createBot();
|
const bot = createBot();
|
||||||
if (bot) void bot.run(abort.signal);
|
if (bot) void bot.run(abort.signal);
|
||||||
|
|
||||||
|
// ---- Abzeichen-Bilder fuer den Chat. Twitch aendert sie selten, ein
|
||||||
|
// neues Abo-Abzeichen des Kanals ist nach spaetestens sechs Stunden da.
|
||||||
|
const badges = createBadges();
|
||||||
|
void (async () => {
|
||||||
|
while (!abort.signal.aborted) {
|
||||||
|
if (helix.canReadStream()) {
|
||||||
|
const id = state.broadcasterId || cfg.channel.userId ||
|
||||||
|
(await helix.userByLogin(cfg.channel.login).catch(() => null))?.id || null;
|
||||||
|
if (id) state.broadcasterId = id;
|
||||||
|
await badges.laden(id);
|
||||||
|
}
|
||||||
|
try { await sleep(6 * 60 * 60_000, abort.signal); } catch { return; }
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
// ---- Chat
|
// ---- Chat
|
||||||
const irc = createIrc(cfg.channel.login, {
|
const irc = createIrc(cfg.channel.login, {
|
||||||
onConnected: (anonymous) => {
|
onConnected: (anonymous) => {
|
||||||
@@ -273,6 +289,7 @@ export async function runHub(): Promise<void> {
|
|||||||
mod: msg.isMod,
|
mod: msg.isMod,
|
||||||
sub: msg.isSub,
|
sub: msg.isSub,
|
||||||
broadcaster: msg.isBroadcaster,
|
broadcaster: msg.isBroadcaster,
|
||||||
|
badges: badges.aufloesen(msg.badges),
|
||||||
bits: msg.bits,
|
bits: msg.bits,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ function msg(text: string, userId: string, advanceMs = 0): ChatMessage {
|
|||||||
id: `m${clock}-${userId}`, ts: new Date(clock).toISOString(), userId,
|
id: `m${clock}-${userId}`, ts: new Date(clock).toISOString(), userId,
|
||||||
login: `u${userId}`, displayName: `U${userId}`, text, emotes: [], gifs: [],
|
login: `u${userId}`, displayName: `U${userId}`, text, emotes: [], gifs: [],
|
||||||
isFirst: false, isReturning: false, isCommand: text.startsWith('!'),
|
isFirst: false, isReturning: false, isCommand: text.startsWith('!'),
|
||||||
isMod: false, isSub: false, isBroadcaster: false, bits: 0, color: null,
|
isMod: false, isSub: false, isBroadcaster: false, badges: [], bits: 0, color: null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ function nachricht(over: Partial<ChatEvent> = {}): ChatEvent {
|
|||||||
return {
|
return {
|
||||||
kind: 'chat', id: 'm1', userId: 'u1', login: 'kevin', name: 'Kevin',
|
kind: 'chat', id: 'm1', userId: 'u1', login: 'kevin', name: 'Kevin',
|
||||||
text: 'moin', color: null, emotes: [], gifs: [], first: false, mod: false,
|
text: 'moin', color: null, emotes: [], gifs: [], first: false, mod: false,
|
||||||
sub: false, broadcaster: false, bits: 0,
|
sub: false, broadcaster: false, badges: [], bits: 0,
|
||||||
...over,
|
...over,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,6 +29,8 @@ export type ChatEvent = {
|
|||||||
mod: boolean;
|
mod: boolean;
|
||||||
sub: boolean;
|
sub: boolean;
|
||||||
broadcaster: boolean;
|
broadcaster: boolean;
|
||||||
|
/** Abzeichen als fertige Bilder, aufgeloest im Hub (#twitch/badges.ts). */
|
||||||
|
badges: Array<{ url: string; title: string }>;
|
||||||
bits: number;
|
bits: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -96,6 +96,23 @@ export const TEST_KNOEPFE: TestKnopf[] = [
|
|||||||
{ art: 'live-aus', label: 'Stand zurücksetzen', gruppe: 'Stand' },
|
{ art: 'live-aus', label: 'Stand zurücksetzen', gruppe: 'Stand' },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// Twitchs globale Abzeichen. Die Adressen sind fest, so zeigt die Vorfuehrung
|
||||||
|
// echte Bilder, ohne Helix fragen zu muessen.
|
||||||
|
const ABZEICHEN = 'https://static-cdn.jtvnw.net/badges/v1/';
|
||||||
|
const MOD = { url: `${ABZEICHEN}3267646d-33f0-4b17-b3df-f923a41db1d0/2`, title: 'Moderator' };
|
||||||
|
const VIP = { url: `${ABZEICHEN}b817aba4-fad8-49e2-b88a-7cc744dfa6ec/2`, title: 'VIP' };
|
||||||
|
const ABO = { url: `${ABZEICHEN}5d9f2208-5dd8-11e7-8513-2ff4adfae661/2`, title: 'Subscriber' };
|
||||||
|
|
||||||
|
function zufallsAbzeichen(): Pick<ChatEvent, 'mod' | 'sub' | 'badges'> {
|
||||||
|
const mod = Math.random() < 0.15;
|
||||||
|
const vip = !mod && Math.random() < 0.1;
|
||||||
|
const sub = Math.random() < 0.3;
|
||||||
|
return {
|
||||||
|
mod, sub,
|
||||||
|
badges: [...(mod ? [MOD] : []), ...(vip ? [VIP] : []), ...(sub ? [ABO] : [])],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function chat(over: Partial<ChatEvent> = {}): ChatEvent {
|
function chat(over: Partial<ChatEvent> = {}): ChatEvent {
|
||||||
const name = zufall(NAMEN);
|
const name = zufall(NAMEN);
|
||||||
return {
|
return {
|
||||||
@@ -109,8 +126,7 @@ function chat(over: Partial<ChatEvent> = {}): ChatEvent {
|
|||||||
emotes: [],
|
emotes: [],
|
||||||
gifs: [],
|
gifs: [],
|
||||||
first: false,
|
first: false,
|
||||||
mod: Math.random() < 0.15,
|
...zufallsAbzeichen(),
|
||||||
sub: Math.random() < 0.3,
|
|
||||||
broadcaster: false,
|
broadcaster: false,
|
||||||
bits: 0,
|
bits: 0,
|
||||||
...over,
|
...over,
|
||||||
|
|||||||
@@ -53,18 +53,15 @@
|
|||||||
background: rgba(0, 0, 0, .35);
|
background: rgba(0, 0, 0, .35);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Abzeichen statt Bilder: Twitchs Abzeichen-URLs haengen am Kanal und muessten
|
/* Twitchs eigene Abzeichen. Die Bilder loest der Hub ueber Helix auf und
|
||||||
* ueber Helix aufgeloest werden. Ein farbiger Punkt sagt dasselbe. */
|
* schickt sie mit jeder Zeile mit (#twitch/badges.ts). */
|
||||||
.abzeichen {
|
.abzeichen {
|
||||||
display: inline-block;
|
height: 1.05em;
|
||||||
width: .5em; height: .5em;
|
width: auto;
|
||||||
border-radius: 50%;
|
margin-right: .3em;
|
||||||
margin-right: .4em;
|
vertical-align: -.17em;
|
||||||
vertical-align: .08em;
|
|
||||||
}
|
}
|
||||||
.abzeichen.mod { background: var(--gruen); }
|
.abzeichen:last-of-type { margin-right: .45em; }
|
||||||
.abzeichen.abo { background: var(--lila); }
|
|
||||||
.abzeichen.host { background: var(--rot); }
|
|
||||||
|
|
||||||
/* Wer zum ersten Mal schreibt, wird hervorgehoben - das ist der Moment, in
|
/* Wer zum ersten Mal schreibt, wird hervorgehoben - das ist der Moment, in
|
||||||
* dem eine Begruessung am meisten bringt. Ohne den Balken an der linken Kante
|
* dem eine Begruessung am meisten bringt. Ohne den Balken an der linken Kante
|
||||||
@@ -152,9 +149,13 @@
|
|||||||
el.dataset.user = ev.userId;
|
el.dataset.user = ev.userId;
|
||||||
|
|
||||||
var abzeichen = '';
|
var abzeichen = '';
|
||||||
if (ev.broadcaster) abzeichen += '<span class="abzeichen host"></span>';
|
var bilder = ev.badges || [];
|
||||||
else if (ev.mod) abzeichen += '<span class="abzeichen mod"></span>';
|
for (var i = 0; i < bilder.length; i++) {
|
||||||
if (ev.sub) abzeichen += '<span class="abzeichen abo"></span>';
|
// Nur Twitchs eigene Bildadressen: was hier steht, laedt OBS.
|
||||||
|
if (!/^https:\/\/static-cdn\.jtvnw\.net\//.test(bilder[i].url)) continue;
|
||||||
|
abzeichen += '<img class="abzeichen" src="' + Overlay.esc(bilder[i].url) +
|
||||||
|
'" alt="' + Overlay.esc(bilder[i].title) + '">';
|
||||||
|
}
|
||||||
|
|
||||||
el.innerHTML =
|
el.innerHTML =
|
||||||
abzeichen +
|
abzeichen +
|
||||||
|
|||||||
+40
-6
@@ -1,6 +1,6 @@
|
|||||||
# Übergabe
|
# Übergabe
|
||||||
|
|
||||||
Stand **2026-10-26**, letztes gebautes Paket 0.5.7. Wer hier weitermacht,
|
Stand **2026-10-27**, letztes gebautes Paket 0.5.7. Wer hier weitermacht,
|
||||||
liest zuerst diese Seite, dann [`07-mvp-plan.md`](07-mvp-plan.md). Für Abdu
|
liest zuerst diese Seite, dann [`07-mvp-plan.md`](07-mvp-plan.md). Für Abdu
|
||||||
selbst gibt es das [Handbuch](handbuch/handbuch.pdf).
|
selbst gibt es das [Handbuch](handbuch/handbuch.pdf).
|
||||||
|
|
||||||
@@ -20,7 +20,7 @@ Auslöser bis zum fertigen Short steht im Code vollständig. Geprüft ist sie
|
|||||||
| S5 Onboarding + Sichtung | offen | |
|
| S5 Onboarding + Sichtung | offen | |
|
||||||
| S6 Erstchatter-Begrüßung | offen | |
|
| S6 Erstchatter-Begrüßung | offen | |
|
||||||
|
|
||||||
288 Tests, alle grün. Was noch fehlt, steht vollständig in
|
322 Tests, alle grün. Was noch fehlt, steht vollständig in
|
||||||
[`offene-punkte.md`](offene-punkte.md).
|
[`offene-punkte.md`](offene-punkte.md).
|
||||||
|
|
||||||
## Neu: der Dienst läuft zentral
|
## Neu: der Dienst läuft zentral
|
||||||
@@ -96,6 +96,15 @@ aus `theme.css`, `?probe=1` zum Ansehen im Browser. Die Ereignisse auf dem Bus
|
|||||||
(`apps/overlay-server/src/bus.ts`) sind die Schnittstelle, an der sich die
|
(`apps/overlay-server/src/bus.ts`) sind die Schnittstelle, an der sich die
|
||||||
neuen Seiten festhalten können.
|
neuen Seiten festhalten können.
|
||||||
|
|
||||||
|
**Chat-Abzeichen sind seit 2026-10-27 Twitchs echte Bilder** statt farbiger
|
||||||
|
Punkte. IRC liefert nur `moderator/1`, `subscriber/3012` usw.; der Hub löst das
|
||||||
|
über Helix (global und Kanal, Kanal gewinnt) zu Bildadressen auf und hängt sie
|
||||||
|
an jede Chatzeile (`packages/twitch/src/badges.ts`, Feld `badges` im
|
||||||
|
`ChatEvent`). Geladen beim Start und alle sechs Stunden, beim Ausrollen
|
||||||
|
kamen 552. Unbekanntes fällt weg, die Seite nimmt nur Adressen von
|
||||||
|
`static-cdn.jtvnw.net`. Im echten Stream noch nicht angesehen; zeigt OBS
|
||||||
|
weiter Punkte, die Browser-Source einmal aktualisieren.
|
||||||
|
|
||||||
Die Ereignisse kommen über Server-Sent Events statt WebSocket. Warum, und was
|
Die Ereignisse kommen über Server-Sent Events statt WebSocket. Warum, und was
|
||||||
gegen die tote Browser-Source nach einem Neustart getan ist, steht in
|
gegen die tote Browser-Source nach einem Neustart getan ist, steht in
|
||||||
[`apps/overlay-server/README.md`](../apps/overlay-server/README.md).
|
[`apps/overlay-server/README.md`](../apps/overlay-server/README.md).
|
||||||
@@ -121,8 +130,12 @@ Dauerbetrieb: `abdulive overlay --demo` oder der Knopf „Vorführung starten".
|
|||||||
|
|
||||||
## Das Pult: Handy als Stream Deck
|
## Das Pult: Handy als Stream Deck
|
||||||
|
|
||||||
Seit 2026-10-26 auf dem Hub ausgerollt und im Paket 0.5.7, **nicht an echtem
|
Seit 2026-10-26 auf dem Hub ausgerollt und im Paket 0.5.7. **Seit 2026-10-27
|
||||||
OBS geprüft**. Auf Abdus Rechner ist nichts auszuführen: das Update kommt über
|
hängt Abdus Stream-PC dran:** obs-websocket an, Passwort auf der
|
||||||
|
Einrichtungsseite hinterlegt, der PC (`DESKTOP-DGJ4HC6`) hat sich beim Hub
|
||||||
|
gemeldet und ist freigegeben, im Hub-Log steht `Stream-PC verbunden`. Ob
|
||||||
|
Szenenwechsel, Stummschalter, Sounds und das Anlegen der Quelle in OBS
|
||||||
|
greifen, ist damit noch nicht beobachtet. Auf Abdus Rechner ist nichts auszuführen: das Update kommt über
|
||||||
die tägliche Aufgabe, danach meldet sich der PC von selbst, Abdu tippt im Pult
|
die tägliche Aufgabe, danach meldet sich der PC von selbst, Abdu tippt im Pult
|
||||||
einmal „Freigeben“, und die Sound-Quelle legt `ingest` selbst in OBS an. `/pult` auf dem Hub ist eine installierbare Web-App (PWA) mit
|
einmal „Freigeben“, und die Sound-Quelle legt `ingest` selbst in OBS an. `/pult` auf dem Hub ist eine installierbare Web-App (PWA) mit
|
||||||
Soundboard, Szenenwechsel, Clip-Knopf, Stummschaltern, Liegestützen,
|
Soundboard, Szenenwechsel, Clip-Knopf, Stummschaltern, Liegestützen,
|
||||||
@@ -144,6 +157,13 @@ Vorlese-Freigabe und den Testknöpfen. Beschreibung in
|
|||||||
Sound-Quelle, Szenenwechsel, Stummschalten, Hochladen, Abspielen); 8 MB
|
Sound-Quelle, Szenenwechsel, Stummschalten, Hochladen, Abspielen); 8 MB
|
||||||
Upload durch den Proxy. Ungeprüft: echtes OBS (vor allem das Anlegen der
|
Upload durch den Proxy. Ungeprüft: echtes OBS (vor allem das Anlegen der
|
||||||
Quelle), iPhone/Android-Installation.
|
Quelle), iPhone/Android-Installation.
|
||||||
|
- **Die Adresse in der Anfragekarte** stimmt: bei Abdus echter Anfrage stand
|
||||||
|
seine öffentliche Adresse da, nicht die Docker-Brücke (`172.18.0.1` kam nur
|
||||||
|
bei der Probe aus dem eigenen Netz). Sie ist reine Anzeige - die Freigabe
|
||||||
|
hängt am Schlüssel, eine wechselnde IP stört nicht, eine feste braucht er
|
||||||
|
nicht.
|
||||||
|
- Auf dem Hub stand am 2026-10-26 kurz eine Probe-Anfrage „PRUEFUNG-CLAUDE“.
|
||||||
|
Sie ist nach zehn Minuten von selbst verfallen und wurde nicht freigegeben.
|
||||||
|
|
||||||
## Kanalpunkte: „Nachricht vorlesen" läuft
|
## Kanalpunkte: „Nachricht vorlesen" läuft
|
||||||
|
|
||||||
@@ -351,6 +371,11 @@ npx tsc --noEmit
|
|||||||
|
|
||||||
## Ausliefern
|
## Ausliefern
|
||||||
|
|
||||||
|
Zwei Wege, je nachdem was sich ändert: Hub-Teil (Overlay, Pult, Discord, Chat)
|
||||||
|
ist mit `deploy-hub.sh` sofort live, Abdu muss nichts tun. Der PC-Teil
|
||||||
|
(`ingest`) kommt nur mit einem neuen Paket an, also nach einem Versions-Tag;
|
||||||
|
ein Push auf `main` allein erreicht seinen Rechner nicht.
|
||||||
|
|
||||||
`build-package.ts` lädt Node, ffmpeg und whisper.cpp bei ihren Herausgebern,
|
`build-package.ts` lädt Node, ffmpeg und whisper.cpp bei ihren Herausgebern,
|
||||||
packt alles nach `dist/` und schreibt `abdulive.json`. Dieses Manifest fragt
|
packt alles nach `dist/` und schreibt `abdulive.json`. Dieses Manifest fragt
|
||||||
`abdulive update` auf Abdus Rechner ab.
|
`abdulive update` auf Abdus Rechner ab.
|
||||||
@@ -358,7 +383,7 @@ packt alles nach `dist/` und schreibt `abdulive.json`. Dieses Manifest fragt
|
|||||||
Die Veröffentlichung `latest` trägt beide Dateien und wird bei jedem Bau
|
Die Veröffentlichung `latest` trägt beide Dateien und wird bei jedem Bau
|
||||||
ersetzt, damit die Adresse gleich bleibt.
|
ersetzt, damit die Adresse gleich bleibt.
|
||||||
`.gitea/workflows/release.yml` macht das beim Tag automatisch. **Seit
|
`.gitea/workflows/release.yml` macht das beim Tag automatisch. **Seit
|
||||||
2026-10-26 läuft sie** (0.5.6). Der erste Lauf (`v0.5.5`) scheiterte daran,
|
2026-10-26 läuft sie** (0.5.6, danach 0.5.7). Der erste Lauf (`v0.5.5`) scheiterte daran,
|
||||||
dass whisper.cpp seine Windows-Programme nicht mehr an `latest` hängt, sondern
|
dass whisper.cpp seine Windows-Programme nicht mehr an `latest` hängt, sondern
|
||||||
an eigene `b…`-Veröffentlichungen; `githubAsset()` sucht jetzt die neueste
|
an eigene `b…`-Veröffentlichungen; `githubAsset()` sucht jetzt die neueste
|
||||||
mit passendem Archiv. Unter `v0.5.5` liegt kein Paket.
|
mit passendem Archiv. Unter `v0.5.5` liegt kein Paket.
|
||||||
@@ -456,7 +481,7 @@ Umlaute und das Trennzeichen `│` (U+2502) bleiben unverändert stehen.
|
|||||||
dieselbe Umbenennung erneut raus - und läuft nach zwei Läufen in das Limit von
|
dieselbe Umbenennung erneut raus - und läuft nach zwei Läufen in das Limit von
|
||||||
oben.
|
oben.
|
||||||
|
|
||||||
## Die drei Dinge, die Abdu tun muss
|
## Was Abdu tun muss
|
||||||
|
|
||||||
1. ~~**Broadcaster-Konto verbinden.**~~ Erledigt am 2026-09-23. Der Rest
|
1. ~~**Broadcaster-Konto verbinden.**~~ Erledigt am 2026-09-23. Der Rest
|
||||||
dieses Punkts bleibt als Nachschlag stehen, falls neu verbunden werden
|
dieses Punkts bleibt als Nachschlag stehen, falls neu verbunden werden
|
||||||
@@ -481,6 +506,9 @@ oben.
|
|||||||
Hotkey auf „Wiedergabepuffer speichern", getrennte Audiospuren.
|
Hotkey auf „Wiedergabepuffer speichern", getrennte Audiospuren.
|
||||||
3. **Layout umstellen** nach [`obs-layout.md`](obs-layout.md), danach
|
3. **Layout umstellen** nach [`obs-layout.md`](obs-layout.md), danach
|
||||||
`abdulive calibrate` zur Kontrolle.
|
`abdulive calibrate` zur Kontrolle.
|
||||||
|
4. ~~**Stream-PC im Pult freigeben.**~~ Erledigt am 2026-10-27, ebenso
|
||||||
|
obs-websocket und das Passwort aus Punkt 2. Offen aus Punkt 2 bleiben
|
||||||
|
Wiedergabepuffer, Hotkey und Audiospuren - nicht nachgeprüft.
|
||||||
|
|
||||||
Das Bot-Konto ist noch nicht verbunden: bis dahin liest das Toolkit den
|
Das Bot-Konto ist noch nicht verbunden: bis dahin liest das Toolkit den
|
||||||
Chat anonym mit und kann nichts senden.
|
Chat anonym mit und kann nichts senden.
|
||||||
@@ -509,6 +537,12 @@ in der ganzen Kette, und alles Weitere hängt daran. Seit es `abdulive sources`
|
|||||||
gibt, ist auch die Frage beantwortbar, welche OBS-Quelle fremden Text ins Bild
|
gibt, ist auch die Frage beantwortbar, welche OBS-Quelle fremden Text ins Bild
|
||||||
malt. Das war vorher nicht zu erraten.
|
malt. Das war vorher nicht zu erraten.
|
||||||
|
|
||||||
|
**Das Pult an Abdus OBS abnehmen.** Die Freigabe ist erteilt. Jetzt nachsehen, ob die
|
||||||
|
Szene `abdulive Ton` mit der Quelle `abdulive Sounds` angelegt und in jede
|
||||||
|
Szene eingehängt ist, ob ein Sound im Stream zu hören ist und ob Szenenwechsel
|
||||||
|
und Stummschalter greifen. Das fällt mit der Abnahme von S2 zusammen, beides
|
||||||
|
braucht nur ein laufendes OBS.
|
||||||
|
|
||||||
**Den Go-Live-Ping scharf schalten.** Er steht auf `draft`. Einmal
|
**Den Go-Live-Ping scharf schalten.** Er steht auf `draft`. Einmal
|
||||||
`abdulive golive:test --echt` laufen lassen, prüfen ob das Handy klingelt, dann
|
`abdulive golive:test --echt` laufen lassen, prüfen ob das Handy klingelt, dann
|
||||||
`mode: live`. Das ist der kleinste verbleibende Handgriff mit der größten
|
`mode: live`. Das ist der kleinste verbleibende Handgriff mit der größten
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ function msg(over: Partial<ChatMessage> = {}): ChatMessage {
|
|||||||
ts: '2026-09-09T20:00:00.000Z',
|
ts: '2026-09-09T20:00:00.000Z',
|
||||||
userId: '42', login: 'maxi_2k', displayName: 'maxi_2k',
|
userId: '42', login: 'maxi_2k', displayName: 'maxi_2k',
|
||||||
text: 'moin', emotes: [], gifs: [], isFirst: false, isReturning: false,
|
text: 'moin', emotes: [], gifs: [], isFirst: false, isReturning: false,
|
||||||
isCommand: false, isMod: false, isSub: false, isBroadcaster: false,
|
isCommand: false, isMod: false, isSub: false, isBroadcaster: false, badges: [],
|
||||||
bits: 0, color: null, ...over,
|
bits: 0, color: null, ...over,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,6 +41,8 @@ export type ChatMessage = {
|
|||||||
isMod: boolean;
|
isMod: boolean;
|
||||||
isSub: boolean;
|
isSub: boolean;
|
||||||
isBroadcaster: boolean;
|
isBroadcaster: boolean;
|
||||||
|
/** Twitchs Abzeichen in Anzeigereihenfolge, z. B. moderator/1, subscriber/3012. */
|
||||||
|
badges: Array<{ set: string; version: string }>;
|
||||||
bits: number;
|
bits: number;
|
||||||
color: string | null;
|
color: string | null;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { test } from 'node:test';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import { badgeKarte, badgesAufloesen } from './badges.ts';
|
||||||
|
|
||||||
|
const v = (id: string, url: string) =>
|
||||||
|
({ id, title: url, image_url_1x: '', image_url_2x: url, image_url_4x: '' });
|
||||||
|
|
||||||
|
test('Kanal-Abzeichen ueberdecken die globalen', () => {
|
||||||
|
const karte = badgeKarte(
|
||||||
|
[{ set_id: 'subscriber', versions: [v('0', 'global-sub')] }, { set_id: 'moderator', versions: [v('1', 'mod')] }],
|
||||||
|
[{ set_id: 'subscriber', versions: [v('0', 'kanal-sub'), v('3012', 'kanal-sub-t3')] }],
|
||||||
|
);
|
||||||
|
assert.deepEqual(
|
||||||
|
badgesAufloesen(karte, [{ set: 'moderator', version: '1' }, { set: 'subscriber', version: '0' }])
|
||||||
|
.map((b) => b.url),
|
||||||
|
['mod', 'kanal-sub'],
|
||||||
|
);
|
||||||
|
assert.equal(badgesAufloesen(karte, [{ set: 'subscriber', version: '3012' }])[0]?.url, 'kanal-sub-t3');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('unbekannte Abzeichen fallen weg', () => {
|
||||||
|
assert.deepEqual(badgesAufloesen(new Map(), [{ set: 'vip', version: '1' }]), []);
|
||||||
|
});
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
// Abzeichen im Chat als Bilder. IRC liefert nur `subscriber/3012`, das Bild
|
||||||
|
// dazu kennt nur Helix. Die Kanal-Abzeichen (eigene Abo-Stufen) ueberdecken
|
||||||
|
// die globalen gleichen Namens - so zeigt Twitch sie auch an.
|
||||||
|
|
||||||
|
import { logger } from '#core/logger.ts';
|
||||||
|
import { helix, type HelixBadgeSet } from './helix.ts';
|
||||||
|
|
||||||
|
const log = logger('badges');
|
||||||
|
|
||||||
|
export type BadgeBild = { url: string; title: string };
|
||||||
|
export type BadgeKarte = Map<string, BadgeBild>;
|
||||||
|
|
||||||
|
export function badgeKarte(global: HelixBadgeSet[], kanal: HelixBadgeSet[]): BadgeKarte {
|
||||||
|
const karte: BadgeKarte = new Map();
|
||||||
|
for (const set of [...global, ...kanal]) {
|
||||||
|
for (const v of set.versions) {
|
||||||
|
// 2x ist 36 px: bei 19 px Schrift im Overlay scharf, auch skaliert.
|
||||||
|
karte.set(`${set.set_id}/${v.id}`, { url: v.image_url_2x, title: v.title });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return karte;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Unbekannte Abzeichen fallen weg, statt als kaputtes Bild zu erscheinen. */
|
||||||
|
export function badgesAufloesen(
|
||||||
|
karte: BadgeKarte, badges: Array<{ set: string; version: string }>,
|
||||||
|
): BadgeBild[] {
|
||||||
|
return badges.flatMap((b) => {
|
||||||
|
const bild = karte.get(`${b.set}/${b.version}`);
|
||||||
|
return bild ? [bild] : [];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createBadges(): {
|
||||||
|
laden: (broadcasterId: string | null) => Promise<void>;
|
||||||
|
aufloesen: (badges: Array<{ set: string; version: string }>) => BadgeBild[];
|
||||||
|
} {
|
||||||
|
let karte: BadgeKarte = new Map();
|
||||||
|
return {
|
||||||
|
async laden(broadcasterId) {
|
||||||
|
try {
|
||||||
|
const [global, kanal] = await Promise.all([
|
||||||
|
helix.chatBadges(),
|
||||||
|
broadcasterId ? helix.chatBadges(broadcasterId) : Promise.resolve([]),
|
||||||
|
]);
|
||||||
|
karte = badgeKarte(global, kanal);
|
||||||
|
log.info(`Abzeichen geladen: ${karte.size}`);
|
||||||
|
} catch (err) {
|
||||||
|
// Die alte Karte bleibt stehen. Ohne Karte zeigt der Chat eben keine.
|
||||||
|
log.warn(`Abzeichen nicht geladen: ${(err as Error).message}`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
aufloesen: (badges) => badgesAufloesen(karte, badges),
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -47,6 +47,11 @@ export type HelixStream = {
|
|||||||
title: string; viewer_count: number; started_at: string; language: string; thumbnail_url: string;
|
title: string; viewer_count: number; started_at: string; language: string; thumbnail_url: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type HelixBadgeSet = {
|
||||||
|
set_id: string;
|
||||||
|
versions: Array<{ id: string; title: string; image_url_1x: string; image_url_2x: string; image_url_4x: string }>;
|
||||||
|
};
|
||||||
|
|
||||||
export type HelixUser = { id: string; login: string; display_name: string };
|
export type HelixUser = { id: string; login: string; display_name: string };
|
||||||
|
|
||||||
export type HelixReward = {
|
export type HelixReward = {
|
||||||
@@ -220,6 +225,18 @@ export const helix = {
|
|||||||
return (readSecrets().tokens.broadcaster?.scopes ?? []).includes(scope);
|
return (readSecrets().tokens.broadcaster?.scopes ?? []).includes(scope);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Abzeichen-Bilder, ohne `broadcasterId` die globalen (Moderator, VIP,
|
||||||
|
* Broadcaster ...), mit die des Kanals (Abo-Stufen, Bits). Ein App-Token
|
||||||
|
* reicht fuer beides.
|
||||||
|
*/
|
||||||
|
async chatBadges(broadcasterId?: string): Promise<HelixBadgeSet[]> {
|
||||||
|
const r = await call<{ data: HelixBadgeSet[] }>('broadcaster',
|
||||||
|
broadcasterId ? `/chat/badges?broadcaster_id=${broadcasterId}` : '/chat/badges/global',
|
||||||
|
{}, { appOk: true });
|
||||||
|
return r.data;
|
||||||
|
},
|
||||||
|
|
||||||
async createClip(broadcasterId: string): Promise<{ id: string; edit_url: string }> {
|
async createClip(broadcasterId: string): Promise<{ id: string; edit_url: string }> {
|
||||||
const r = await call<{ data: Array<{ id: string; edit_url: string }> }>(
|
const r = await call<{ data: Array<{ id: string; edit_url: string }> }>(
|
||||||
'broadcaster', `/clips?broadcaster_id=${broadcasterId}`, { method: 'POST' });
|
'broadcaster', `/clips?broadcaster_id=${broadcasterId}`, { method: 'POST' });
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { test } from 'node:test';
|
import { test } from 'node:test';
|
||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import { parseLine, toChatMessage, parseGifs } from './irc.ts';
|
import { parseLine, toChatMessage, parseGifs, parseBadges } from './irc.ts';
|
||||||
|
|
||||||
const LINE =
|
const LINE =
|
||||||
'@badge-info=subscriber/8;badges=broadcaster/1,subscriber/6;color=#1E90FF;' +
|
'@badge-info=subscriber/8;badges=broadcaster/1,subscriber/6;color=#1E90FF;' +
|
||||||
@@ -28,6 +28,9 @@ test('uebersetzt in eine ChatMessage', () => {
|
|||||||
assert.ok(msg);
|
assert.ok(msg);
|
||||||
assert.equal(msg.userId, '98765');
|
assert.equal(msg.userId, '98765');
|
||||||
assert.equal(msg.login, 'nic_ooo');
|
assert.equal(msg.login, 'nic_ooo');
|
||||||
|
assert.deepEqual(msg.badges, [
|
||||||
|
{ set: 'broadcaster', version: '1' }, { set: 'subscriber', version: '6' },
|
||||||
|
]);
|
||||||
assert.equal(msg.displayName, 'Nic_OOo');
|
assert.equal(msg.displayName, 'Nic_OOo');
|
||||||
assert.equal(msg.isBroadcaster, true);
|
assert.equal(msg.isBroadcaster, true);
|
||||||
assert.equal(msg.isSub, true);
|
assert.equal(msg.isSub, true);
|
||||||
@@ -104,3 +107,9 @@ test('unvollstaendige oder unsinnige Tags werden verworfen', () => {
|
|||||||
assert.deepEqual(parseGifs(kaputt), [], `"${kaputt}" haette verworfen gehoert`);
|
assert.deepEqual(parseGifs(kaputt), [], `"${kaputt}" haette verworfen gehoert`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('Abzeichen: leer und kaputt ergeben nichts', () => {
|
||||||
|
assert.deepEqual(parseBadges(undefined), []);
|
||||||
|
assert.deepEqual(parseBadges(''), []);
|
||||||
|
assert.deepEqual(parseBadges('vip/1,kaputt'), [{ set: 'vip', version: '1' }]);
|
||||||
|
});
|
||||||
@@ -111,6 +111,15 @@ function parseEmotes(tag: string | undefined, text: string): ChatMessage['emotes
|
|||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** `broadcaster/1,subscriber/3012` -> [{ set, version }], Reihenfolge wie bei Twitch. */
|
||||||
|
export function parseBadges(tag: string | undefined): Array<{ set: string; version: string }> {
|
||||||
|
if (!tag) return [];
|
||||||
|
return tag.split(',').flatMap((teil) => {
|
||||||
|
const [set, version] = teil.split('/');
|
||||||
|
return set && version ? [{ set, version }] : [];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export function toChatMessage(m: IrcMessage): ChatMessage | null {
|
export function toChatMessage(m: IrcMessage): ChatMessage | null {
|
||||||
if (m.command !== 'PRIVMSG') return null;
|
if (m.command !== 'PRIVMSG') return null;
|
||||||
const text = m.params[1] ?? '';
|
const text = m.params[1] ?? '';
|
||||||
@@ -133,6 +142,7 @@ export function toChatMessage(m: IrcMessage): ChatMessage | null {
|
|||||||
isMod: m.tags['mod'] === '1' || badges.includes('broadcaster'),
|
isMod: m.tags['mod'] === '1' || badges.includes('broadcaster'),
|
||||||
isSub: m.tags['subscriber'] === '1',
|
isSub: m.tags['subscriber'] === '1',
|
||||||
isBroadcaster: badges.includes('broadcaster'),
|
isBroadcaster: badges.includes('broadcaster'),
|
||||||
|
badges: parseBadges(badges),
|
||||||
bits: Number(m.tags['bits'] ?? 0),
|
bits: Number(m.tags['bits'] ?? 0),
|
||||||
color: m.tags['color'] || null,
|
color: m.tags['color'] || null,
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in new issue
Block a user