Skip to content

Commit d0a2a3a

Browse files
committed
Add Twitch bot account chatbot guide
1 parent 9117453 commit d0a2a3a

27 files changed

Lines changed: 1615 additions & 210 deletions

background.js

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5647,6 +5647,72 @@ chrome.runtime.onMessage.addListener(async function (request, sender, sendRespon
56475647
sendResponse({ ok: false, error: error && error.message ? error.message : "VPZone fetch failed" });
56485648
}
56495649
return true;
5650+
} else if (request.cmd && request.cmd === "joystickFetchJson") {
5651+
let parsedUrl;
5652+
try {
5653+
parsedUrl = new URL(request.url);
5654+
} catch (error) {
5655+
sendResponse({ ok: false, error: "Invalid Joystick API URL" });
5656+
return response;
5657+
}
5658+
if (parsedUrl.hostname !== "api.joystick.tv") {
5659+
sendResponse({ ok: false, error: "Joystick API URL not allowed" });
5660+
return response;
5661+
}
5662+
try {
5663+
const method = String(request.method || "GET").toUpperCase();
5664+
const isOAuthTokenPost = method === "POST" && parsedUrl.pathname === "/api/oauth/token";
5665+
const isStreamSettingsGet = method === "GET" && parsedUrl.pathname === "/api/users/stream-settings";
5666+
if (!isOAuthTokenPost && !isStreamSettingsGet) {
5667+
sendResponse({ ok: false, error: "Joystick API request not allowed" });
5668+
return response;
5669+
}
5670+
const headers = {
5671+
Accept: "application/json"
5672+
};
5673+
const authHeader = typeof request.auth === "string" ? request.auth.replace(/[\r\n]/g, "") : "";
5674+
if (isOAuthTokenPost) {
5675+
headers["Content-Type"] = "application/x-www-form-urlencoded";
5676+
if (request.authType === "basic" && authHeader.startsWith("Basic ")) {
5677+
headers.Authorization = authHeader;
5678+
}
5679+
} else if (isStreamSettingsGet) {
5680+
headers["Content-Type"] = "application/json";
5681+
if (request.authType === "bearer" && authHeader.startsWith("Bearer ")) {
5682+
headers.Authorization = authHeader;
5683+
}
5684+
}
5685+
const joystickResponse = await fetch(parsedUrl.toString(), {
5686+
method,
5687+
cache: "no-store",
5688+
credentials: "omit",
5689+
headers,
5690+
body: isOAuthTokenPost ? String(request.body || "") : undefined
5691+
});
5692+
const responseText = await joystickResponse.text();
5693+
let responseJson = {};
5694+
try {
5695+
responseJson = responseText ? JSON.parse(responseText) : {};
5696+
} catch (error) {
5697+
responseJson = null;
5698+
}
5699+
if (!joystickResponse.ok) {
5700+
sendResponse({
5701+
ok: false,
5702+
status: joystickResponse.status,
5703+
error: (responseJson && (responseJson.error_description || responseJson.message || (typeof responseJson.error === "string" ? responseJson.error : responseJson.error && responseJson.error.message))) || `HTTP ${joystickResponse.status}`
5704+
});
5705+
return response;
5706+
}
5707+
if (responseJson == null) {
5708+
sendResponse({ ok: false, status: joystickResponse.status, error: "Invalid JSON response" });
5709+
return response;
5710+
}
5711+
sendResponse({ ok: true, status: joystickResponse.status, data: responseJson });
5712+
} catch (error) {
5713+
sendResponse({ ok: false, error: error && error.message ? error.message : "Joystick API fetch failed" });
5714+
}
5715+
return true;
56505716
} else if (request.cmd && request.cmd === "rumbleFetchJson") {
56515717
try {
56525718
const rumbleResponse = await fetchRumbleJsonResponse(request.url);

credits.html

Lines changed: 37 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -671,15 +671,33 @@ <h2 class="instructions-title">Stream Credits Setup</h2>
671671
}
672672
}
673673

674-
function getOrCreateUser(name, type) {
675-
const key = `${name}-${type}`;
676-
if (!users.has(key)) {
677-
users.set(key, new User(name, type));
678-
}
679-
return users.get(key);
680-
}
681-
682-
let previewMode = false;
674+
function getOrCreateUser(name, type) {
675+
const key = `${name}-${type}`;
676+
if (!users.has(key)) {
677+
users.set(key, new User(name, type));
678+
}
679+
return users.get(key);
680+
}
681+
682+
function hasSupporterSignal(data) {
683+
return !!(data && (data.hasDonation || data.membership));
684+
}
685+
686+
function filterCreditsUsersForSupporters(userList) {
687+
if (urlParams.has('onlydonors')) {
688+
return userList.filter(function(user) {
689+
return user.donations > 0;
690+
});
691+
}
692+
if (urlParams.has('onlysupporters')) {
693+
return userList.filter(function(user) {
694+
return user.donations > 0 || user.isMember;
695+
});
696+
}
697+
return userList;
698+
}
699+
700+
let previewMode = false;
683701

684702
function processData(data) {
685703
if (data.content) {
@@ -704,14 +722,15 @@ <h2 class="instructions-title">Stream Credits Setup</h2>
704722
}
705723
return;
706724
}
707-
708-
if (!data.chatname || !data.type) return;
709-
710-
const onlydonors = urlParams.has('onlydonors');
711-
if (onlydonors && !data.hasDonation) return;
712-
713-
const user = getOrCreateUser(data.chatname, data.type);
714-
user.messageCount++;
725+
726+
if (!data.chatname || !data.type) return;
727+
728+
const onlydonors = urlParams.has('onlydonors');
729+
if (onlydonors && !data.hasDonation) return;
730+
if (!onlydonors && urlParams.has('onlysupporters') && !hasSupporterSignal(data)) return;
731+
732+
const user = getOrCreateUser(data.chatname, data.type);
733+
user.messageCount++;
715734

716735
// Store avatar URL if available
717736
if (data.chatimg) {
@@ -745,12 +764,7 @@ <h2 class="instructions-title">Stream Credits Setup</h2>
745764

746765
function generateCreditsContent() {
747766
let sortedUsers = Array.from(users.values());
748-
const onlydonors = urlParams.has('onlydonors');
749-
if (onlydonors) {
750-
sortedUsers = sortedUsers.filter(function(user) {
751-
return user.donations > 0;
752-
});
753-
}
767+
sortedUsers = filterCreditsUsersForSupporters(sortedUsers);
754768

755769
if (donationPriority) {
756770
// Separate users into donors and non-donors

docs/agents/07-overlays-and-pages/tipjar-credits.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ Bar/meter styling:
6767
| `fillend` / `barcolorend` | Full fill color. |
6868
| `fillmode` | `progress`, `gradient`, or `solid`. |
6969
| `barheight` | Track height for bar-based styles. |
70+
| `bartextsize` / `barfontsize` | Text size in px for the centered `bar` style amount text. |
7071
| `barradius` | Track corner radius; `0` gives square edges. |
7172
| `trackcolor` / `bartrackcolor` / `barbackground` | Bar track color. |
7273
| `noliquid` | Disables the animated liquid/bar effect. |
@@ -99,6 +100,7 @@ Hype mode:
99100
| `hidecompletions` | Hides completion count in hype mode. |
100101
| `completiondelay` | Delay before rollover/reset display. |
101102
| `levelsize` / `increment` | Repeating level size for level/band goals. |
103+
| `rollinggoal` / `cumulativegoal`, `goalstep` / `goalincrement` | Keeps the total cumulative and advances the displayed target to the next goal step, e.g. `$60 / $100` after passing a `$50` goal. |
102104
| `celebration` | `hearts`, `confetti`, `fireworks`, or `none`. |
103105

104106
## Tip Jar Commands
@@ -228,6 +230,7 @@ Support rule: credits needs the page open while messages arrive, unless `persist
228230
| `persistcredits` | Saves users to localStorage by session. |
229231
| `donationpriority` | Groups/sorts donors first, then members, then participants. |
230232
| `onlydonors` | Ignores users without donation payloads. |
233+
| `onlysupporters` | Ignores regular chat-only users and keeps donors plus members/subscribers. |
231234
| `hidecategories` | Hides category headers. |
232235
| `showavatar` / `showavatars` | Shows avatar images when available. |
233236
| `showamounts` | Shows donation totals next to donors. |

docs/agents/08-platform-sources/popout-chat-only-sources.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ That source is a chat-only/popout capture. Open the exact supported chat URL, ke
4848
| Mixcloud Live | `https://www.mixcloud.com/live/*/chat/` | Live chat rows with `data-testid="chatline"` or older `.mixcloud-live-chat-row-link` selectors | Extracts username from profile link; subscription rows can populate `hasDonation`; dedupes repeated JSON payloads | Selector paths support multiple layouts and need live validation. |
4949
| Nimo.TV | `https://www.nimo.tv/popout/chat/*`, `https://dashboard.nimo.tv/popout/chat/*` | `.nimo-room__chatroom__message-item`, `.nm-message-nickname`, `.content` | Badge images from level/decoration selectors; donation placeholder is inactive in inspected source | Uses popout chat; donation bits are not active in the inspected code. |
5050
| Odysee | `https://odysee.com/$/popout/*` | Main content mutations, author, livestream comment text, avatar | Rant/donation code is present only as commented-out notes in the inspected source | Treat as rendered chat capture; do not promise rants/donations from this pass. |
51-
| Parti | `https://parti.com/popout-chat?id=*` | Popout chat rows with `span.username`, avatar, badges, text | Tip parsing around `.bi-coin`; viewer count heartbeat every 60 seconds when `showviewercount` or `hypemode` is enabled | Viewer count requires `id` query parameter and platform heartbeat response; source sends no verified chat-back path. |
51+
| Parti | `https://parti.com/popout-chat?id=*` | Current `.creator-chat-stream > .ccs-row` rows with fallback support for older `span.username` rows | Tip rows emit `event: "donation"` with `hasDonation`, structured `meta.amount` / `meta.currency` when parsable, and USD `donoValue`; viewer count heartbeat uses a stable per-window token when `showviewercount` or `hypemode` is enabled | Viewer count requires `id` query parameter and platform heartbeat response; source sends no verified chat-back path. |
5252
| Picarto.tv | `https://picarto.tv/chatpopout/*`, `https://www.picarto.tv/chatpopout/*` | Chat popout rows with styled channel display name, message span, avatar | Handles image emotes inside message spans | Generated class names and two message-row layouts need live validation. |
5353
| Piczel.tv | `https://piczel.tv/chat/*` | `#PiczelChat` rows, buttons/avatar, `Message_` content | Handles inline images/emotes; focuses CodeMirror line for chat input | DOM traversal uses deep child-node indexing and is fragile. |
5454
| RokFin | `https://*.rokfin.com/popout/chat/*`, `https://rokfin.com/popout/chat/*` | Ant Design comment rows, author, badges, avatar, message body | Tip rows using `.ant-space-item mark` can set `hasDonation` | Requires popout chat URL; badges can be image or SVG descriptors. |

docs/agents/08-platform-sources/special-case-platform-and-helper-sources.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ This includes:
4747

4848
| Source | Manifest/Load Path | What It Captures Or Does | Source Identity | Support Notes |
4949
| --- | --- | --- | --- | --- |
50-
| Joystick DOM chat | `sources/joystick.js` on `https://joystick.tv/u/*/chat` | Watches `#chat-messages` for new `.chat-message` rows. Extracts name, message, streamer badge, notice rows, bot rows, and focuses `input[flow-id="chat-message-text-input"]`. | Payload `type: "joystick"` and `getSource` returns `joystick`. | This is rendered-page capture. Joystick bot Gateway/OAuth/send-back belongs to `sources/websocket/joystick.*` and `websocket-source-pages.md`. |
50+
| Joystick DOM chat | `sources/joystick.js` on `https://joystick.tv/u/*/chat` | Watches `#chat-messages` for new `.chat-message` rows. Extracts name, message, streamer badge, notice rows, bot rows, and focuses `input[flow-id="chat-message-text-input"]`. | Payload `type: "joystick"` and `getSource` returns `joystick`. | This is rendered-page capture and can fail before parsing if Joystick/Cloudflare blocks the app session. Joystick bot Gateway/OAuth/send-back belongs to `sources/websocket/joystick.*` and `websocket-source-pages.md`. |
5151
| Velora rendered site | `sources/velora.js` on `https://velora.tv/*` | Watches rendered chat rows, extracts badges before the username button, messages, name color, donation/Volts text, channel-points style "says" events, subscription rows, and viewer counts when viewer-count/hype settings are enabled. | Payload `type: "velora"` and `getSource` returns `velora`. | This DOM path is separate from the Velora OAuth/API source page. Do not promise send-back from the DOM script; send-back belongs to the source-page API path. |
5252
| Vercel demo launcher | `sources/vercel.js` on `https://maestro-launcher.vercel.app/` | Requests the current SSN stream/session ID from the extension, prompts the user unless `sharestreamid` is already allowed, writes the ID into `#roomId`, and calls the page's `updatedChatID` hook if present. | No chat payload source. | Treat as a demo/helper bridge for session access. It is not chat capture and not a supported social platform parser. |
5353
| Vertical Pixel Zone | `sources/verticalpixelzone.js` on `https://verticalpixelzone.com/*` | Attempts to watch a rendered chat container and extracts avatar, `.message-username`, and `.message-text-inner` content. Focuses `.input > input[type="text"][name^="chat_"]`. | `getSource` returns `verticalpixelzone`, but emitted payload `type` is `arena` in the inspected file. | Source identity mismatch is important for filters and support answers. The active observer selector looks fragile and needs live validation before promising current support. |
@@ -85,7 +85,8 @@ For support answers, ask which mode the user is using before troubleshooting. A
8585

8686
| Question | Short Answer |
8787
| --- | --- |
88-
| "Joystick works on the website but not the WebSocket source page." | Separate the modes. `sources/joystick.js` is rendered chat capture; `sources/websocket/joystick.*` needs bot credentials/OAuth and GatewayChannel connection. |
88+
| "Joystick works in Chrome but not the desktop app." | The rendered DOM path depends on a logged-in Joystick page and can be blocked by Joystick/Cloudflare in the app; use the Joystick Bot WebSocket source page when bot credentials are available. |
89+
| "Joystick works on the website but not the WebSocket source page." | Separate the modes. `sources/joystick.js` is rendered chat capture; `sources/websocket/joystick.*` needs bot credentials/OAuth and GatewayChannel connection through `api.joystick.tv`. |
8990
| "Velora chat appears but I cannot send replies." | The DOM source captures rendered rows. Reply/send-back is a Velora source-page/API feature and depends on OAuth, token state, and channel permissions. |
9091
| "VPZone duplicates messages." | Check whether `sources/inject/vpzone-ws.js` is active and whether `sources/vpzone.js` suppresses DOM rows after the first intercepted WebSocket frame. |
9192
| "Vertical Pixel Zone says source verticalpixelzone but filters see arena." | The inspected file returns `verticalpixelzone` for `getSource` but emits payload `type: "arena"`. Source filters and overlays can see the payload type. |

docs/agents/08-platform-sources/websocket-source-pages.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ Do not infer send-chat support from the page existing. Confirm the page has `SEN
5757
| --- | --- | --- | --- | --- |
5858
| `bilibili.html` / `bilibili.js` | Bilibili room ID; optional cookie/auth for sending | Chat, gifts, super chat, user-enter, follower updates, live/offline status | Yes, via `SEND_BILIBILI_MESSAGE` when credentials/session allow it | Uses Bilibili room APIs, socket packets, and a content-script bridge. If receive works but send fails, check cookie/auth and platform-side permission first. |
5959
| `irc.html` / `irc.js` | IRC server/channel/user fields in the page | IRC messages normalized through `IRCMessage` | Yes, via `SEND_IRC_MESSAGE` | `getSource` returns `irc`; `focusChat` targets `messageInput`. Useful for custom IRC-style workflows. |
60-
| `joystick.html` / `joystick.js` | Joystick bot client ID/secret, OAuth or external app auth, channel slug/ID | Chat messages, user presence, stream online/offline, follows, token/tip style donation events | Yes, when socket is connected/subscribed and channel ID is known | Requires bot credentials for the gateway socket. OAuth token storage/refresh is local to the source page. |
60+
| `joystick.html` / `joystick.js` | Joystick bot client ID/secret, OAuth or external app auth, channel slug/ID | Chat messages, user presence, stream online/offline, follows, token/tip style donation events | Yes, when socket is connected/subscribed and channel ID is known | Requires bot credentials for the gateway socket. Authorize starts on `joystick.tv`; token, REST, and cable use `api.joystick.tv`. Token/REST calls should use the extension background fetch or desktop `window.ninjafy.fetchJoystickJson` bridge because normal hosted-page fetch can hit CORS. Stream settings can hydrate channel ID, source name/image, live state, and follower count, but current Joystick docs do not expose live viewer count. OAuth token storage/refresh is local to the source page. |
6161
| `nostr.html` / `nostr.js` | Nostr relay/filter setup in the page | Nostr events forwarded as source messages | No documented send-back in the bridge | `getSource` deliberately returns false and `focusChat` returns false. Treat it as read-only unless source-checking proves otherwise. |
6262
| `socialstreamchat.html` / `socialstreamchat.js` | Social Stream chat room ID and optional token | Chat/event envelopes from `chat.socialstream.ninja` | Page has local send logic, but the extension bridge does not advertise `SEND_MESSAGE` handling in the inspected script | Internal/custom source path. It can mint a guest token for rooms that allow it; auth failures usually mean room token/permission setup, not overlay failure. |
6363
| `stageten.html` / `stageten.js` | StageTEN channel ID | PubNub chat messages from public chat access credentials | Page has local send logic; extension `SEND_MESSAGE` handling was not present in the inspected bridge | Fetches public chat access from StageTEN's plugin-service GraphQL endpoint and refreshes PubNub tokens. CORS failures are a known page-level setup issue. |

docs/commands.html

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -841,6 +841,10 @@ <h4>Detailed Documentation</h4>
841841
<i class="fas fa-volume-up"></i>
842842
<span>Local AI TTS Guide</span>
843843
</a>
844+
<a href="twitch-bot-account-chatbot.html" class="btn btn-secondary">
845+
<i class="fas fa-robot"></i>
846+
<span>Twitch Bot Account Replies</span>
847+
</a>
844848
<a href="https://github.com/steveseguin/social_stream/blob/master/ai.md" class="btn btn-secondary" target="_blank">
845849
<i class="fab fa-github"></i>
846850
<span>AI Setup Documentation on GitHub</span>

0 commit comments

Comments
 (0)