Commit Graph

10 Commits

Author SHA1 Message Date
Claude Owner
1d383d9a03 feat(prod-commands): wipe dev guild commands before global registration
When `Config.dev` is true, the operator typically registers slash
commands per-guild (Config.guildId) during iteration so they appear
instantly. Once those leak past dev, the same commands show up in
Discord twice — once from the dev guild registration and once from
the global registration.

Before doing the global PUT, also PUT an empty array against the
dev guild so any stale per-guild commands are cleared. Behavior in
production (Config.dev=false) is unchanged.
2026-05-27 10:20:41 +09:00
Claude Owner
1841828f7a feat(voice): per-user TTS voice selection via /목소리
Until now every TTS message was synthesized with VoiceType.가람
regardless of who spoke. This adds a user-scoped voice preference
persisted in SQLite so each member can pick their own voice and
keep it across bot restarts.

Changes
- db/schema.sql: add nullable `voice_type` column to `users`.
- src/utils/Database.ts:
  - run a PRAGMA-driven ALTER TABLE migration so existing DBs gain
    the column without dropping data.
  - add `DB.user.setVoice(guildId, userId, name, voiceType)` that
    upserts the row.
- src/classes/TTSClient.ts:
  - export `DEFAULT_VOICE` (= 가람).
  - resolve the speaking member's stored voice in `tts()` and
    thread it through `getSource()` instead of hardcoding 가람.
  - validate stored slug against `VoiceType` so stale/unknown
    values silently fall back to the default.
- src/commands/voice.ts (new):
  - `/목소리` slash command shows the user's current voice and a
    StringSelectMenu of all `VoiceType` entries (현재 + 이전 보이스
    모두). Selection writes to `users.voice_type` and confirms
    ephemerally.
  - defensively creates a guild row if `/tts channel register`
    hasn't run yet, to satisfy the ON DELETE CASCADE FK.

Deploy
Run `npm run prod` after pulling so Discord sees the new
`/목소리` command. No env or config changes required.
2026-05-26 22:50:53 +09:00
Claude Owner
2002b1cc12 docs(voice): document interrupt-latest as a design choice
VoiceSession.play() replaces the currently playing TTS on every new
message instead of queueing it. Without this comment a future reader
(or code review) is likely to file it as a missing-feature bug, but
the absence of a queue is intentional: in a many-user voice channel,
queueing causes a speaker's own message to lag far behind the chat
context. Document the policy at the call site.
2026-05-26 14:59:43 +09:00
Claude Owner
499852b2a7 fix(tts): avoid char-by-char split when signature list is empty
SignatureClient.regex returned new RegExp("()", "g") when nameSet
was empty (server unreachable, list not yet synced). text.split() with
that regex matches every zero-width position, so 'abc' became
['a','','b','','c',''] and TTSClient sent a separate Chzzk request
per character.

Two-layer fix:
- SignatureClient.regex returns /$^/g (never-match) when names is
  empty, so split() returns the original string as a single element.
- TTSClient.getSource explicitly skips split when nameSet.size === 0
  so the intent is obvious at the call site.
2026-05-26 14:48:24 +09:00
Claude Owner
acdaa4734f chore: harden logger TZ, ffmpeg lifecycle and fix package metadata
- Logger.Timestamp now formats via Intl with timeZone Asia/Seoul, so
  the timestamp is correct regardless of the container/host TZ. The
  previous setHours(+9) hack assumed the system clock was UTC.
- Transcode.mp3BufferToPcmStream now attaches error/stderr handlers
  to the ffmpeg child process and its streams, swallows EPIPE on
  early downstream close, and force-kills on spawn error so failed
  conversions can't leak processes. Log level bumped from 'quiet'
  to 'error' so real ffmpeg errors surface.
- package.json homepage/bugs/repository pointed at github.com/tkrmagid/bot.ts
  which doesn't reflect this repo. Repoint to the actual Gitea origin.
2026-05-26 14:41:29 +09:00
Claude Owner
35569ddd88 fix(handler): filter command files and guard non-command modules
Handler.ts iterated readdirSync(COMMANDS_PATH) without filtering, so
any .d.ts declaration file, .js.map source map, or non-class module
that landed in commands/ would crash startup with 'mod.default is not
a constructor'. Restrict loading to .ts/.js (excluding .d.ts and maps)
and skip modules without a default-exported constructor.
2026-05-26 14:40:29 +09:00
Claude Owner
fe10ed1bd9 feat(signature): make host configurable and add WS reconnect
- New optional env SIGNATURE_HOST overrides the hardcoded
  192.168.10.5:2967 (defaults preserved for back-compat).
- WebSocket now reconnects with exponential backoff (1s, 2s, 4s ...
  capped at 30s) on close/error. Previously a dropped signature
  server connection silently disabled signature playback until the
  bot was restarted.
2026-05-26 14:40:05 +09:00
Claude Owner
e22cb53ccf fix(tts): correct Discord snowflake regex and getSource typo
- Mention/emoji regexes used [(0-9)]{18}; '(' and ')' inside a character
  class are literal so this matched the bracket chars themselves and
  hard-fixed the ID length to 18. Discord snowflakes are 17-20 digits
  (older 2015-2017 accounts are 17, 2024+ trend toward 19-20). Switch
  to \\d{17,20} and capture the ID for member lookup.
- Tighten emoji regex name segment from .* (greedy, eats across emojis)
  to [^:]+ so adjacent emojis no longer collapse into one match.
- Rename getSorce -> getSource.
2026-05-26 14:38:46 +09:00
Claude Owner
7b56d2e055 fix(db): correct user.update target table
stmt.user.update was issuing UPDATE guilds instead of UPDATE users,
so any DB.user.update() call would silently corrupt guild rows that
happened to share the same WHERE clause shape and never touch the
intended user row.
2026-05-26 14:38:09 +09:00
d6b36c43c2 기존 2026-05-26 14:15:09 +09:00