Skip to content

Commit d821f00

Browse files
Merge pull request #97 from SkullGaming31/feature/electron-setup
Feature/electron setup
2 parents 3adea8e + 06890a6 commit d821f00

83 files changed

Lines changed: 7425 additions & 723 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.env.example‎

Lines changed: 15 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,60 +1,40 @@
1-
# Example environment variables for OpenDevBot
2-
# Copy this file to .env and fill in secrets. DO NOT commit your real .env to source control.
3-
4-
# Twitch
51
TWITCH_CLIENT_ID=
62
TWITCH_CLIENT_SECRET=
7-
TWITCH_EVENTSUB_SECRET= # random secret for EventSub signature verification
8-
TWITCH_REDIRECT_URL=http://localhost:3000/api/v1/auth/twitch/callback
3+
TWITCH_EVENTSUB_SECRET=
4+
TWITCH_REDIRECT_URL=http://localhost:3001/api/v1/auth/twitch/callback
95

10-
# Discord / webhooks
116
DEV_DISCORD_BOT_TOKEN=
127
DEV_DISCORD_CLIENT_ID=
138
DEV_DISCORD_CLIENT_SECRET=
149
DEV_DISCORD_GUILD_ID=
15-
DEV_DISCORD_WEBHOOK_ID=
16-
DEV_DISCORD_WEBHOOK_TOKEN=
17-
DEV_DISCORD_WEBHOOK_URL=
18-
DEV_DISCORD_ERROR_LOGS_ID=
19-
DEV_DISCORD_ERROR_WEBHOOK=
10+
2011
DEV_DISCORD_PROMOTE_WEBHOOK_ID=
2112
DEV_DISCORD_PROMOTE_WEBHOOK_TOKEN=
13+
2214
DEV_DISCORD_TWITCH_ACTIVITY_ID=
2315
DEV_DISCORD_TWITCH_ACTIVITY_TOKEN=
2416
DEV_DISCORD_FEATURE_REQUEST=
25-
DEV_DISCORD_FEATURE_REQUEST_ID=
26-
DEV_DISCORD_FEATURE_REQUEST_TOKEN=
17+
2718
DISCORD_COMMAND_USAGE_ID=
2819
DISCORD_COMMAND_USAGE_TOKEN=
20+
2921
BUG_REPORT_WEBHOOK_ID=
3022
BUG_REPORT_WEBHOOK_TOKEN=
23+
DEV_DISCORD_FEATURE_REQUEST_ID=
24+
DEV_DISCORD_FEATURE_REQUEST_TOKEN=
25+
PORT=
3126

32-
# Server & logging
33-
PORT=3000
34-
PROD_LOG_FILE=./logs/prod.log
35-
DEV_LOG_FILE=./logs/dev.log
36-
37-
# MongoDB (choose one)
38-
# For production (when ENVIRONMENT=prod) set MONGO_URI
3927
MONGO_USER=
4028
MONGO_PASS=
4129
MONGO_DB=
42-
MONGO_URI=
30+
MONGO_URI=full mongodb connection string
4331

44-
# For local Docker Compose (dev), use DOCKER_URI pointing to the published mongo port
4532
DOCKER_URI=mongodb://localhost:27017/opendevbot
46-
47-
# Feature flags / environment (note the project uses the misspelled 'ENVIRONMENT')
48-
ENVIRONMENT=dev # allowed: dev | debug | prod
33+
PROD_LOG_FILE=where you want the prod logs stored too
34+
DEV_LOG_FILE=where you want the dev logs stored too
35+
ENVIRONMENT=prod | debug | dev
4936
ENABLE_CHAT=true
5037
ENABLE_EVENTSUB=true
5138

52-
# Optional third-party keys
53-
GUILDED_TOKEN=
54-
NEXON_API_KEY=
55-
DEV_NEXON_API_KEY=
56-
NITRADO_LONGLIFE_TOKEN=
57-
58-
# Notes:
59-
# - Keep this file as a template. Never commit your real .env with secrets.
60-
# - If you run the app inside Docker Compose as a service, prefer DOCKER_URI=mongodb://mongodb:27017/opendevbot
39+
ADMIN_API_TOKEN=can be anything
40+
ADMIN_SETUP_TOKEN=can be anything

‎.gitignore‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
node_modules/
22
dist/
3+
dist-electron/
34
.env
45
/node_modules
56
.env
@@ -11,5 +12,4 @@ NOTES.txt
1112
/logs
1213
/coverage
1314
.tsbuildinfo
14-
/.agents
15-
*.agent.md
15+
bot-status.diff

‎CHANGELOG.md‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,13 @@
3838

3939
All notable changes to this project are documented in this file.
4040

41+
### 2026-07-15 — Electron / Admin dashboard
42+
43+
- Added: Electron admin shell support improvements — masked `ADMIN_API_TOKEN` is now logged in the main process for diagnostics (masked, not the full secret). File: `electron/adminProxy.ts`.
44+
- Added: Health indicator to the local admin dashboard UI at the top header. The dashboard now polls `/health` and shows a green "API OK" badge on 200 responses and a red "API ERROR" badge for failures. File: `public/admin/dashboard.html`.
45+
- Changed: Built and pushed these changes to the `feature/electron-setup` branch for further testing; no PR opened.
46+
47+
4148
## 2025-12-09 — Test hygiene & Jest/TypeScript fixes
4249

4350
- Re-enabled strict TypeScript unused checks (`noUnusedLocals` / `noUnusedParameters`) and fixed the resulting errors across the codebase to restore strict type hygiene.

‎CODE-REVIEW-PRIORITY.md‎

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
# OpenDevBot — Core Architecture Code Review by ClaudeAI
2+
3+
Scope: `authProvider.ts`, `chat.ts`, `createApp.ts`, `EventSubEvents.ts`, `index.ts`
4+
Date: 2026-07-13
5+
6+
---
7+
8+
## 🔴 Critical
9+
10+
### 1. `chat.ts:289` — `setInterval` created on every single chat message, never cleared
11+
12+
Inside `commandHandler` (which runs on **every** incoming chat message), there's:
13+
14+
```ts
15+
void setInterval(async () => {
16+
if (isIntervalRunning) {
17+
await intervalHandler();
18+
}
19+
}, intervalDuration);
20+
```
21+
22+
This has no guard flag and no stored handle. Compare this to `periodicSaveIntervalId` and
23+
`socialIntervalId`, which *are* correctly guarded/cleared elsewhere in this same file (e.g. the
24+
`periodicSocialTimerStarted` flag around line 454).
25+
26+
**Impact:** every chat message in every joined channel spins up a brand-new interval that fetches
27+
chatters, checks live status, and credits wallets — and it runs forever. With any real chat
28+
activity this is an unbounded interval leak:
29+
- growing memory use over time
30+
- growing Twitch API call volume (rate-limit risk)
31+
- duplicate/overlapping wallet credits for the same users each interval tick
32+
33+
**This is almost certainly the top production risk right now.**
34+
35+
**Fix direction:** this "periodically credit chatters while live" block shouldn't live inside the
36+
message handler at all. Pull it out into a one-time setup step (same pattern you already used
37+
correctly for the periodic social-message timer), guarded by a module-level flag so it only starts
38+
once per process, and store/clear its interval handle like `periodicSaveIntervalId`.
39+
40+
---
41+
42+
## 🟠 High
43+
44+
### 2. `createApp.ts` — admin token handling
45+
46+
- Token comparisons use plain `!==` (`provided !== token`, `token !== expected`) instead of a
47+
constant-time comparison. Low real-world risk for a personal bot, but cheap to harden with
48+
`crypto.timingSafeEqual`.
49+
- The admin token can be supplied via `?admin_token=` query param, and the setup/login tokens via
50+
`?setup_token=` / `?token=` query params. These end up in server access logs, any reverse-proxy
51+
logs, and browser history. Recommend dropping query-param support for tokens entirely and
52+
requiring the header/cookie only.
53+
54+
### 3. `/api/v1/admin/setup` writes directly to `.env` on disk
55+
56+
`fs.writeFileSync` is used to persist `ADMIN_API_TOKEN` whenever the endpoint is hit with a valid
57+
`ADMIN_SETUP_TOKEN`. If that setup token ever leaks, or is left set in a prod `.env` after initial
58+
setup, anyone who finds it gets full admin API access. Confirm `ADMIN_SETUP_TOKEN` is unset in prod
59+
once initial setup is complete — maybe add a startup warning if it's still set alongside
60+
`ENVIRONMENT=prod`.
61+
62+
---
63+
64+
## 🟡 Medium
65+
66+
### 4. Chatter-fetch/crediting logic duplicated in two places
67+
68+
Once as the (broken) interval inside `commandHandler`, and again in the `onJoin` watch-time
69+
interval logic. Worth consolidating into a single periodic service so there's one source of truth
70+
for "credit points every N minutes while live."
71+
72+
### 5. `chat.ts:623-624` — dead code
73+
74+
```ts
75+
if (stream === null) clearInterval(intervalId);
76+
```
77+
78+
This runs right after the function already `return`s earlier when `stream === null` (~line 557),
79+
so this branch can never be reached. Harmless, but confusing — safe to delete.
80+
81+
### 6. `chat.ts` `onJoin` — potential interval leak on reconnect without `part`
82+
83+
If a user rejoins without a `part` event firing first (reconnect edge case), `viewerWatchTimes.set(user, ...)`
84+
overwrites the map entry without clearing the *previous* interval tied to that user — a smaller-scale
85+
version of the same leak pattern as issue #1. Worth clearing any existing interval for `user` before
86+
overwriting the map entry.
87+
88+
### 7. `authProvider.ts` — fragile Twurple SDK fallback chain
89+
90+
`getChatAuthProvider()`'s intent-registration logic (~lines 120–193) tries four different fallback
91+
paths wrapped in nested try/catch to work around Twurple SDK version differences between
92+
`addUserForToken`, `addUser` with options, `addUser`, and `addIntentsToUser`. It works, but it's
93+
fragile — any Twurple upgrade could silently change which path fires and you might not notice until
94+
chat auth breaks. Worth a code comment noting the exact Twurple version this was validated against.
95+
96+
---
97+
98+
## 🟢 Low / cleanup
99+
100+
### 8. `index.ts` — `printEnvironmentVariables()` logs secrets, but is dead code
101+
102+
Not called anywhere in the codebase currently, but if it's ever wired up it will `logger.debug`
103+
every `.env` key/value pair verbatim — including `TWITCH_CLIENT_SECRET`, Discord webhook tokens,
104+
`MONGO_PASS`, `NEXON_API_KEY`, etc. Either delete it, or if you want to keep it for future
105+
debugging, redact any key containing `TOKEN`, `SECRET`, `PASS`, or `KEY` before logging.
106+
107+
### 9. Silent error swallowing in `authProvider.ts`
108+
109+
Multiple `catch (e) { /* ignore */ }` blocks around the SDK-fallback probing in
110+
`getChatAuthProvider()`. Reasonable for probing which SDK method exists, but worth double-checking
111+
none of these are hiding a genuine auth failure in prod — consider at least a `logger.debug` in
112+
each ignored catch so there's a breadcrumb if chat auth ever silently stops working.
113+
114+
---
115+
116+
## Suggested order of attack
117+
118+
1. Fix #1 (interval leak) — this is the one actually degrading the running bot over time.
119+
2. Fix #2/#3 (admin auth hardening) — quick wins, closes real exposure.
120+
3. #4–#7 as time allows — quality-of-life and future-proofing.
121+
4. #8/#9 — cleanup whenever convenient.

‎Dockerfile‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,11 +42,11 @@ RUN mkdir -p /app/logs && chown -R nodejs:nodejs /app/logs
4242
USER nodejs
4343

4444
# Expose port (adjust if needed)
45-
EXPOSE 3000
45+
EXPOSE 3001
4646

4747
# Health check
4848
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
49-
CMD node -e "require('http').get('http://localhost:3000', (r) => {if (r.statusCode !== 200) throw new Error(r.statusCode)})"
49+
CMD node -e "require('http').get('http://localhost:3001/health', (r) => {if (r.statusCode !== 200) throw new Error(r.statusCode)})"
5050

5151
# Start application
5252
CMD ["node", "./dist/index.js"]

‎README.md‎

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -129,9 +129,13 @@ This is an example of how to list things you need to use the software and how to
129129
<p align="right">(<a href="#top">back to top</a>)</p>
130130

131131
<!-- USAGE EXAMPLES -->
132-
<!-- ## Usage
132+
## Usage
133+
<p>electron:dev will build the electron app and start it</p>
133134

134-
<p align="right">(<a href="#top">back to top</a>)</p> -->
135+
```sh
136+
npm run electron:dev
137+
```
138+
<p align="right">(<a href="#top">back to top</a>)</p>
135139

136140
<!-- ROADMAP -->
137141
## Roadmap
@@ -147,7 +151,7 @@ This is an example of how to list things you need to use the software and how to
147151
* [x] add word detection to send commands without prefix
148152
* [x] create channelPoints with userToken to modify the channel points with commands [Hard Coded ChannelPoints]
149153
* [x] Advanced Lurk Command
150-
* [ ] Timer Commands
154+
* [X] Timer Command
151155
* [x] Counters
152156
* [x] Quotes System?
153157
* [x] Viewer Watch Time?

‎TODO.md‎

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,15 @@
2626
* [x] change channelPoints Message to display only on the console when channelpoints rewardId is not found.
2727
* [x] Que/delay webhooks being sent to avoid being rate limited by the Discord API
2828

29+
## Remaining work
30+
31+
* [ ] Finish the test-fix sweep and make the full test suite (`npm test`) pass deterministically.
32+
* [ ] Remove temporary diagnostic `console.log` lines added during debugging and re-run tests.
33+
* [ ] Harden mirroring logic: avoid upserts with null keys, add guards before mirror writes, and make `MIRROR_TO_USERMODEL` explicitly toggleable (default OFF for migrations).
34+
* [ ] Finalize and validate migration tooling for canonicalizing user IDs (dry-run + `--apply`, batching/limit, backups).
35+
* [ ] Ensure all commands use the correct API: wallet ops via `balanceAdapter` (short-lived/game bets) and bank ops via `economyService` (persistent ops), and update any remaining command code/tests.
36+
* [ ] Add/expand integration tests for transactional flows (heist/replica-set transactions) and outbox/migration scenarios.
37+
* [ ] Fix remaining TypeScript/Jest issues (unused imports like in `src/Commands/Fun/rps.ts`) and run `tsc`/lint to clean up errors.
38+
* [ ] Update docs: `NOTES.txt`, `README.md`, and migration instructions showing `MIRROR_TO_USERMODEL` usage and rollback guidance.
39+
* [ ] Run formatting and prepare a tidy commit/PR with descriptive changes and test results.
2940

30-
FIX:
31-
* [x] (node:2288) [MONGOOSE] Warning: mongoose: the `new` option for `findOneAndUpdate()` and `findOneAndReplace()` is deprecated. Use `returnDocument: 'after'` instead.

‎docker-compose.yml‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,11 @@ services:
44
app:
55
build: .
66
ports:
7-
- "3000:3000"
7+
- "3001:3001"
88
environment:
99
- ENVIRONMENT=prod
1010
- MONGO_URI=mongodb://mongodb:27017/opendevbot
11-
- PORT=3000
11+
- PORT=3001
1212
env_file:
1313
- .env.prod
1414
depends_on:

‎electron/adminProxy.ts‎

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import { ipcMain } from 'electron';
2+
import logger from '../src/util/logger';
3+
4+
interface AdminFetchOptions {
5+
method?: string;
6+
body?: string;
7+
}
8+
9+
/**
10+
* Proxies admin API calls from the renderer to the bot's local Express
11+
* server, attaching `x-admin-token` on the main-process side so the
12+
* renderer (and dashboard.html) never sees the token.
13+
*/
14+
export function registerAdminProxy(port: number): void {
15+
const token = process.env.ADMIN_API_TOKEN || '';
16+
logger.info('[electron] adminProxy configured');
17+
18+
ipcMain.handle('admin:fetch', async (_event, path: string, opts: AdminFetchOptions = {}) => {
19+
const maxAttempts = 3;
20+
let attempt = 0;
21+
const token = process.env.ADMIN_API_TOKEN || '';
22+
23+
while (true) {
24+
attempt++;
25+
try {
26+
const res = await fetch(`http://localhost:${port}${path}`, {
27+
method: opts.method || 'GET',
28+
headers: {
29+
'Content-Type': 'application/json',
30+
'x-admin-token': token
31+
},
32+
body: opts.body
33+
});
34+
35+
if (res.ok) {
36+
const ct = (res.headers.get('content-type') || '').toLowerCase();
37+
if (ct.includes('application/json')) return await res.json();
38+
// Try to parse body as JSON, otherwise return raw text
39+
const text = await res.text();
40+
try { return JSON.parse(text); } catch { return text; }
41+
}
42+
43+
// Handle 429 with retry/backoff when possible
44+
if (res.status === 429 && attempt < maxAttempts) {
45+
const retryAfter = res.headers.get('retry-after');
46+
let wait = 1000 * Math.pow(2, attempt - 1); // 1s, 2s, 4s
47+
if (retryAfter) {
48+
const ra = Number(retryAfter);
49+
if (!Number.isNaN(ra)) wait = ra * 1000;
50+
}
51+
logger.warn(`admin:fetch ${path} returned 429; retrying in ${wait}ms (attempt ${attempt}/${maxAttempts})`);
52+
await new Promise((r) => setTimeout(r, wait));
53+
continue;
54+
}
55+
56+
// Non-OK response: read body and surface a clear error
57+
const bodyText = await res.text();
58+
// If body is JSON, return parsed object to preserve existing behaviour
59+
try {
60+
const parsed = JSON.parse(bodyText);
61+
return parsed;
62+
} catch {
63+
logger.warn(`admin:fetch ${path} returned ${res.status} ${res.statusText}`);
64+
throw new Error(`HTTP ${res.status} ${res.statusText}: ${bodyText}`);
65+
}
66+
} catch (err) {
67+
// Network or parsing error — retry a few times before failing
68+
if (attempt >= maxAttempts) {
69+
logger.error(`admin:fetch proxy failed for ${path}`, err as Error);
70+
throw err;
71+
}
72+
const wait = 500 * attempt;
73+
logger.warn(`admin:fetch transient error for ${path}, retrying in ${wait}ms (attempt ${attempt}/${maxAttempts})`, err as Error);
74+
await new Promise((r) => setTimeout(r, wait));
75+
}
76+
}
77+
});
78+
}

0 commit comments

Comments
 (0)