-
Notifications
You must be signed in to change notification settings - Fork 106
Shubham/waifu deal sniper #20
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Shubham/waifu deal sniper #20
Conversation
📝 WalkthroughWalkthroughAdds a new Discord bot project "waifu-deal-sniper" including: a production bot implementation (bot.js) that queries the TinyFish MINO SSE API across multiple marketplaces, a SQLite-backed persistence module (database.js) exporting CRUD and analytics functions, message templates (templates.js), package.json, README, .gitignore, and .env.example. Features include multi-site searches, rarity scoring, per-user watchlists with background checks/notifications, gacha/roast/copium modes, rate limiting, and persistent user/watch/notification storage. Sequence Diagram(s)sequenceDiagram
actor User as User/Discord
participant Bot as Discord Bot
participant Parser as Intent Parser
participant MINO as MINO API
participant DB as SQLite DB
User->>Bot: DM or mention (search/watch/gacha/roast)
Bot->>Parser: Parse intent & params
Parser-->>Bot: Intent & params
alt Search
Bot->>MINO: POST search request (SSE-like)
MINO-->>Bot: Stream JSON events / items
Bot->>Bot: Parse stream, compute grades & rarity
Bot->>DB: logSearch / increment stats
DB-->>Bot: Ack
Bot-->>User: Send results embed(s)
end
alt Add Watch
Bot->>DB: addToWatchlist / getUserWatchlist
DB-->>Bot: Ack
Bot-->>User: Confirm watch added
par Background Watch Loop
loop periodic
Bot->>MINO: Re-run search for active watches
MINO-->>Bot: Stream current listings
Bot->>DB: hasBeenNotified / markNotified
alt New deal found
Bot-->>User: Send DM notification
DB-->>Bot: Record notification
end
end
end
end
alt Gacha / Roast / Copium
Bot->>DB: getOrCreateUser / getUserStats / get lastSearchResults
DB-->>Bot: User & cached results
Bot->>Bot: Apply templates & randomize
Bot-->>User: Send formatted response
end
🚥 Pre-merge checks | ❌ 2❌ Failed checks (1 warning, 1 inconclusive)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 8
🤖 Fix all issues with AI agents
In `@waifu-deal-sniper/bot.js`:
- Around line 1415-1459: The runWatchChecker function can be invoked while a
previous execution is still running, causing overlapping runs and duplicate
notifications; add a reentrancy guard (e.g., a module-level boolean like
isWatchCheckerRunning) or switch to scheduling the next run after completion
(use setTimeout chaining instead of setInterval) so a new invocation returns
immediately if isWatchCheckerRunning is true; set the flag to true at the start
of runWatchChecker and ensure it is cleared in a finally block (or clear after
all watches processed) to avoid leaving it stuck, and update the interval
scheduling code (the setInterval/start call that triggers runWatchChecker) to
use the new non-overlapping pattern.
- Around line 1497-1499: The current console.log call printing message.content
(console.log(`📨 Message from ${message.author.username}:
"${message.content.slice(0, 50)}" (DM: ${isDM})`)) risks leaking PII; change it
so raw DM content is never logged in production by gating detailed logging
behind a DEBUG flag (e.g., process.env.DEBUG_LOGGING === 'true') or by logging
only metadata when the flag is false (author id, username, message length, isDM,
message.id) and, if DEBUG is true, include a safe redacted preview rather than
full content. Locate the console.log in the message handler (references:
message, isDM) and replace/wrap it with a conditional that checks the DEBUG flag
and falls back to metadata-only logging.
- Around line 374-380: The MAX price text in searchSite is hard-coded to "JPY"
causing wrong filtering for other sites; update the construction of the goal
string in async function searchSite (the site.goal + ... line) to use the site's
currency (e.g., site.currency) instead of "JPY", and if the app must compare
across sites either convert maxPrice into the site's currency before appending
or clearly state in the goal which site's currency is expected (e.g., "Only
items under {maxPrice} {site.currency}"). Ensure you reference the maxPrice
handling and site.goal so the message and any filtering logic use the correct
currency semantics.
- Around line 885-894: The main search path never updates lastSearchResults so
subsequent commands like gacha_last or roast can't access the latest results;
after computing result.items (before sending the summary/embed) update the
shared lastSearchResults store for this user (e.g., set
lastSearchResults[user.id] or the appropriate per-user key used by
gacha_last/roast to point to result.items) so the gacha_last and roast handlers
can read the most recent standard search results; place this update near the
block that computes deals (referencing result.items, user.id, and
lastSearchResults/gacha_last/roast).
- Around line 539-590: Sanitize external fields before embedding: in
createFigureEmbed() apply sanitizeForDisplay() to item.name,
item.item_grade/item.condition, item.box_grade (if considered condition), and
any item.manufacturer or item.seller used in the description/title/footer so the
embedded text cannot contain `@everyone/`@here; mirror the same sanitization in
createSiteEmbed() for item.name, item.condition, item.seller and
item.manufacturer referenced there. Also, when sending embeds (the message
send/interaction reply calls that include the EmbedBuilder from
createFigureEmbed/createSiteEmbed), add allowedMentions: { parse: [] } to the
send options to disable mention parsing as defense-in-depth. Ensure you only
transform display strings (don’t mutate raw item objects) and reference the
functions createFigureEmbed and createSiteEmbed when making the changes.
In `@waifu-deal-sniper/package.json`:
- Around line 6-26: The package.json currently sets "engines.node": ">=18.0.0"
while the "dev" script uses "node --watch" which is only stable in Node
>=20.13.0; either raise the engine requirement or remove the dependency on the
experimental flag: update "engines.node" to ">=20.13.0" if you want to keep
using "node --watch", or change the "scripts.dev" entry to use nodemon (e.g.,
"nodemon --watch bot.js bot.js") and add nodemon to devDependencies so
development works on older Node 18.x without forcing a node version bump; modify
the "scripts.dev" and "devDependencies" accordingly and ensure "scripts.start"
and "scripts.dev" remain consistent with the chosen approach.
In `@waifu-deal-sniper/README.md`:
- Line 3: Replace bare URLs in the README with proper Markdown link syntax to
satisfy MD034: change the literal OAuth2 link at the "Live Demo" line into a
labeled link like "Live Demo: [text](https://discord.com/...)" and similarly
convert the other bare URL noted at line 19 into a descriptive label with
bracketed text followed by the URL in parentheses; ensure each link uses the
form [label](URL) and keep descriptive labels (e.g., "Live Demo") for
readability.
- Around line 113-115: The README contains several fenced code blocks missing
language identifiers which fails linting; add a language tag (e.g., "text") to
each triple-backtick fence for the blocks that include the Discord OAuth URL
("https://discord.com/oauth2/authorize?client_id=YOUR_CLIENT_ID..."), the large
ASCII architecture diagram (the block starting with the DISCORD USER/ DISCORD
BOT diagram), and the repository file tree block that begins
"waifu-deal-sniper/"; ensure you update every matching fenced block (also the
other occurrences mentioned around the ASCII/art and file-tree sections) so each
opening ``` becomes ```text.
🧹 Nitpick comments (1)
waifu-deal-sniper/database.js (1)
104-124: Debounce disk writes to avoid blocking on every mutation.
run()triggerssaveDb()which exports the full DB and uses a sync write; frequent bot activity could stall the event loop. Consider batching writes or debouncing while keeping the periodic flush.♻️ Example debounce
+let saveScheduled = false; +function scheduleSave() { + if (saveScheduled) return; + saveScheduled = true; + setTimeout(() => { + saveScheduled = false; + saveDb(); + }, 1000); +} + function run(sql, params = []) { if (!db) throw new Error('Database not initialized'); db.run(sql, params); - saveDb(); + scheduleSave(); }
| function createFigureEmbed(item) { | ||
| const isGoodDeal = isDeal(item); | ||
| const price = parseInt(item.price) || 0; | ||
| const rarity = item.rarity?.tier || 'r'; | ||
| const rarityLabel = item.rarity?.label || ''; | ||
|
|
||
| // Color based on rarity or deal status | ||
| const rarityColors = { | ||
| ssr: 0xFFD700, // Gold | ||
| sr: 0xA855F7, // Purple | ||
| r: 0x3B82F6, // Blue | ||
| salt: 0x6B7280, // Gray | ||
| }; | ||
| const embedColor = isGoodDeal ? 0xFF6B6B : (rarityColors[rarity] || 0x6C5CE7); | ||
|
|
||
| // Title prefix based on rarity | ||
| const rarityPrefix = rarity === 'ssr' ? '🌈 ' : rarity === 'sr' ? '⭐ ' : ''; | ||
|
|
||
| const embed = new EmbedBuilder() | ||
| .setColor(embedColor) | ||
| .setTitle(`${isGoodDeal ? '🔥 ' : rarityPrefix}${(item.name || 'Figure').slice(0, 250)}`) | ||
| .setURL(item.url || 'https://www.amiami.com'); | ||
|
|
||
| // Only set thumbnail if it's a valid URL | ||
| if (item.image && item.image.startsWith('http')) { | ||
| embed.setThumbnail(item.image); | ||
| } | ||
|
|
||
| let desc = ''; | ||
| if (isGoodDeal) { | ||
| desc += `**${pick(TEMPLATES.deal_alert)}**\n\n`; | ||
| } else if (rarity === 'ssr') { | ||
| desc += `**${rarityLabel}**\n\n`; | ||
| } | ||
|
|
||
| desc += `💴 **¥${price.toLocaleString()}**\n`; | ||
| desc += `✨ Figure: **${item.item_grade || '?'}** | 📦 Box: **${item.box_grade || '?'}**\n`; | ||
| desc += `${item.in_stock !== false ? '✅ In Stock' : '❌ Sold Out'}`; | ||
|
|
||
| // Add rarity tags if present | ||
| if (item.rarityDetails && item.rarityDetails.length > 0) { | ||
| desc += `\n\n🏷️ ${item.rarityDetails.slice(0, 3).join(' • ')}`; | ||
| } | ||
|
|
||
| desc += `\n\n*${getConditionComment(item.item_grade, item.box_grade)}*`; | ||
|
|
||
| embed.setDescription(desc); | ||
|
|
||
| // Footer with site info if multi-site | ||
| const siteInfo = item.siteEmoji ? `${item.siteEmoji} ${item.siteName} • ` : ''; | ||
| embed.setFooter({ text: `${siteInfo}${getPriceReaction(price)} • Click title to buy!` }); | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
find . -name "bot.js" -type f | head -20Repository: tinyfish-io/TinyFish-cookbook
Length of output: 99
🏁 Script executed:
git ls-files | grep -E "bot\.js|\.js$" | head -30Repository: tinyfish-io/TinyFish-cookbook
Length of output: 158
🏁 Script executed:
wc -l waifu-deal-sniper/bot.jsRepository: tinyfish-io/TinyFish-cookbook
Length of output: 102
🏁 Script executed:
sed -n '539,590p' waifu-deal-sniper/bot.jsRepository: tinyfish-io/TinyFish-cookbook
Length of output: 1896
🏁 Script executed:
sed -n '1045,1085p' waifu-deal-sniper/bot.jsRepository: tinyfish-io/TinyFish-cookbook
Length of output: 1253
🏁 Script executed:
rg -n "sanitizeForDisplay" waifu-deal-sniper/Repository: tinyfish-io/TinyFish-cookbook
Length of output: 1477
🏁 Script executed:
rg -n "allowedMentions" waifu-deal-sniper/Repository: tinyfish-io/TinyFish-cookbook
Length of output: 55
🏁 Script executed:
sed -n '65,75p' waifu-deal-sniper/bot.jsRepository: tinyfish-io/TinyFish-cookbook
Length of output: 387
🏁 Script executed:
rg -n "\.send\(|\.reply\(" waifu-deal-sniper/bot.js | head -30Repository: tinyfish-io/TinyFish-cookbook
Length of output: 2559
🏁 Script executed:
sed -n '895,905p' waifu-deal-sniper/bot.jsRepository: tinyfish-io/TinyFish-cookbook
Length of output: 407
🏁 Script executed:
sed -n '965,975p' waifu-deal-sniper/bot.jsRepository: tinyfish-io/TinyFish-cookbook
Length of output: 401
🏁 Script executed:
sed -n '1033,1040p' waifu-deal-sniper/bot.jsRepository: tinyfish-io/TinyFish-cookbook
Length of output: 425
🏁 Script executed:
rg -n "parse:" waifu-deal-sniper/bot.jsRepository: tinyfish-io/TinyFish-cookbook
Length of output: 55
Sanitize external fields in embeds to prevent @mention abuse.
item.name, item.condition, item.seller, and item.manufacturer come from external sources and are embedded unsanitized—allowing @everyone/@here in listings to ping servers. Apply sanitizeForDisplay() to these fields in both createFigureEmbed() (line 559) and createSiteEmbed() (lines 1059, 1074, 1079, 1083). Additionally, set allowedMentions: { parse: [] } on all embed sends to block mention parsing as defense-in-depth.
🤖 Prompt for AI Agents
In `@waifu-deal-sniper/bot.js` around lines 539 - 590, Sanitize external fields
before embedding: in createFigureEmbed() apply sanitizeForDisplay() to
item.name, item.item_grade/item.condition, item.box_grade (if considered
condition), and any item.manufacturer or item.seller used in the
description/title/footer so the embedded text cannot contain `@everyone/`@here;
mirror the same sanitization in createSiteEmbed() for item.name, item.condition,
item.seller and item.manufacturer referenced there. Also, when sending embeds
(the message send/interaction reply calls that include the EmbedBuilder from
createFigureEmbed/createSiteEmbed), add allowedMentions: { parse: [] } to the
send options to disable mention parsing as defense-in-depth. Ensure you only
transform display strings (don’t mutate raw item objects) and reference the
functions createFigureEmbed and createSiteEmbed when making the changes.
| // Log & count deals | ||
| db.logSearch(user.id, cleanQuery, result.items.length); | ||
| const deals = result.items.filter(isDeal); | ||
| if (deals.length > 0) { | ||
| db.incrementDealsFound(user.id, deals.length); | ||
| } | ||
|
|
||
| // Send results | ||
| const summaryEmbed = createResultsSummaryEmbed(result.items, cleanQuery, spicy); | ||
| await statusMsg.edit({ content: searchMsg, embeds: [summaryEmbed] }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Store last results for standard searches.
lastSearchResults is updated for gacha and multi‑site searches but not the main search path, so gacha_last/roast won’t work after normal searches.
🧩 Suggested fix
// Log & count deals
db.logSearch(user.id, cleanQuery, result.items.length);
+ lastSearchResults.set(user.discord_id, { query: cleanQuery, items: result.items, timestamp: Date.now() });
const deals = result.items.filter(isDeal);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| // Log & count deals | |
| db.logSearch(user.id, cleanQuery, result.items.length); | |
| const deals = result.items.filter(isDeal); | |
| if (deals.length > 0) { | |
| db.incrementDealsFound(user.id, deals.length); | |
| } | |
| // Send results | |
| const summaryEmbed = createResultsSummaryEmbed(result.items, cleanQuery, spicy); | |
| await statusMsg.edit({ content: searchMsg, embeds: [summaryEmbed] }); | |
| // Log & count deals | |
| db.logSearch(user.id, cleanQuery, result.items.length); | |
| lastSearchResults.set(user.discord_id, { query: cleanQuery, items: result.items, timestamp: Date.now() }); | |
| const deals = result.items.filter(isDeal); | |
| if (deals.length > 0) { | |
| db.incrementDealsFound(user.id, deals.length); | |
| } | |
| // Send results | |
| const summaryEmbed = createResultsSummaryEmbed(result.items, cleanQuery, spicy); | |
| await statusMsg.edit({ content: searchMsg, embeds: [summaryEmbed] }); |
🤖 Prompt for AI Agents
In `@waifu-deal-sniper/bot.js` around lines 885 - 894, The main search path never
updates lastSearchResults so subsequent commands like gacha_last or roast can't
access the latest results; after computing result.items (before sending the
summary/embed) update the shared lastSearchResults store for this user (e.g.,
set lastSearchResults[user.id] or the appropriate per-user key used by
gacha_last/roast to point to result.items) so the gacha_last and roast handlers
can read the most recent standard search results; place this update near the
block that computes deals (referencing result.items, user.id, and
lastSearchResults/gacha_last/roast).
| // Debug logging | ||
| console.log(`📨 Message from ${message.author.username}: "${message.content.slice(0, 50)}" (DM: ${isDM})`); | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Avoid logging raw DM content in production.
Current logging includes message content, which can capture PII. Gate this behind a DEBUG flag or log only metadata (length, IDs).
🔒 Suggested change
- console.log(`📨 Message from ${message.author.username}: "${message.content.slice(0, 50)}" (DM: ${isDM})`);
+ if (process.env.DEBUG_LOGS === 'true') {
+ console.log(`📨 Message from ${message.author.username} (DM: ${isDM}) len=${message.content.length}`);
+ }🤖 Prompt for AI Agents
In `@waifu-deal-sniper/bot.js` around lines 1497 - 1499, The current console.log
call printing message.content (console.log(`📨 Message from
${message.author.username}: "${message.content.slice(0, 50)}" (DM: ${isDM})`))
risks leaking PII; change it so raw DM content is never logged in production by
gating detailed logging behind a DEBUG flag (e.g., process.env.DEBUG_LOGGING ===
'true') or by logging only metadata when the flag is false (author id, username,
message length, isDM, message.id) and, if DEBUG is true, include a safe redacted
preview rather than full content. Locate the console.log in the message handler
(references: message, isDM) and replace/wrap it with a conditional that checks
the DEBUG flag and falls back to metadata-only logging.
| "scripts": { | ||
| "start": "node bot.js", | ||
| "dev": "node --watch bot.js" | ||
| }, | ||
| "keywords": [ | ||
| "discord-bot", | ||
| "anime", | ||
| "figures", | ||
| "web-scraping", | ||
| "tinyfish", | ||
| "mino-api" | ||
| ], | ||
| "author": "Shubham Khandelwal", | ||
| "license": "MIT", | ||
| "dependencies": { | ||
| "discord.js": "^14.14.1", | ||
| "sql.js": "^1.10.3" | ||
| }, | ||
| "engines": { | ||
| "node": ">=18.0.0" | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🌐 Web query:
Node.js --watch flag introduced version stability history
💡 Result:
node --watch flag: introduction + stability timeline
-
Introduced (experimental):
- v18.11.0 (and backported to v16.19.0) —
--watch(and--watch-path) first added; initially Stability: 1 (Experimental). [1][2]
- v18.11.0 (and backported to v16.19.0) —
-
Still experimental in early v20:
- Example: v20.3.1 docs still mark
--watch/--watch-pathas Experimental. [2]
- Example: v20.3.1 docs still mark
-
Became stable:
- v20.13.0 — watch mode marked stable in the Node 20 line. [3]
- v22.0.0 — watch mode marked stable in the Node 22 line. [1]
Related flags (watch-mode family)
--watch-preserve-outputadded in v19.3.0 and v18.13.0. [4]
Sources: Node.js CLI documentation for various release lines. [1][2][3][4]
Align engines.node with node --watch availability.
npm run dev relies on node --watch, which was introduced in v18.11.0 but remained experimental until v20.13.0. The current requirement >=18.0.0 includes versions that lack this flag entirely (v18.0.0–v18.10.0). Either bump the engine floor to >=20.13.0 for stable watch mode support, or replace node --watch with nodemon to avoid tight version coupling.
♻️ Option A: require stable watch mode
- "node": ">=18.0.0"
+ "node": ">=20.13.0"📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| "scripts": { | |
| "start": "node bot.js", | |
| "dev": "node --watch bot.js" | |
| }, | |
| "keywords": [ | |
| "discord-bot", | |
| "anime", | |
| "figures", | |
| "web-scraping", | |
| "tinyfish", | |
| "mino-api" | |
| ], | |
| "author": "Shubham Khandelwal", | |
| "license": "MIT", | |
| "dependencies": { | |
| "discord.js": "^14.14.1", | |
| "sql.js": "^1.10.3" | |
| }, | |
| "engines": { | |
| "node": ">=18.0.0" | |
| } | |
| "scripts": { | |
| "start": "node bot.js", | |
| "dev": "node --watch bot.js" | |
| }, | |
| "keywords": [ | |
| "discord-bot", | |
| "anime", | |
| "figures", | |
| "web-scraping", | |
| "tinyfish", | |
| "mino-api" | |
| ], | |
| "author": "Shubham Khandelwal", | |
| "license": "MIT", | |
| "dependencies": { | |
| "discord.js": "^14.14.1", | |
| "sql.js": "^1.10.3" | |
| }, | |
| "engines": { | |
| "node": ">=20.13.0" | |
| } |
🤖 Prompt for AI Agents
In `@waifu-deal-sniper/package.json` around lines 6 - 26, The package.json
currently sets "engines.node": ">=18.0.0" while the "dev" script uses "node
--watch" which is only stable in Node >=20.13.0; either raise the engine
requirement or remove the dependency on the experimental flag: update
"engines.node" to ">=20.13.0" if you want to keep using "node --watch", or
change the "scripts.dev" entry to use nodemon (e.g., "nodemon --watch bot.js
bot.js") and add nodemon to devDependencies so development works on older Node
18.x without forcing a node version bump; modify the "scripts.dev" and
"devDependencies" accordingly and ensure "scripts.start" and "scripts.dev"
remain consistent with the chosen approach.
| @@ -0,0 +1,232 @@ | |||
| # 🎎 Waifu Deal Sniper | |||
|
|
|||
| **Live Demo:** [https://discord.com/oauth2/authorize?client_id=1465346765611077871&permissions=277025508352&scope=bot] | |||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Convert bare URLs to proper Markdown links.
This addresses the MD034 lint finding and improves readability.
📝 Suggested edit
-**Live Demo:** [https://discord.com/oauth2/authorize?client_id=1465346765611077871&permissions=277025508352&scope=bot]
+**Live Demo:** [Invite link](https://discord.com/oauth2/authorize?client_id=1465346765611077871&permissions=277025508352&scope=bot)
-https://github.com/user-attachments/assets/demo.mp4
+[Demo video](https://github.com/user-attachments/assets/demo.mp4)Also applies to: 19-19
🤖 Prompt for AI Agents
In `@waifu-deal-sniper/README.md` at line 3, Replace bare URLs in the README with
proper Markdown link syntax to satisfy MD034: change the literal OAuth2 link at
the "Live Demo" line into a labeled link like "Live Demo:
[text](https://discord.com/...)" and similarly convert the other bare URL noted
at line 19 into a descriptive label with bracketed text followed by the URL in
parentheses; ensure each link uses the form [label](URL) and keep descriptive
labels (e.g., "Live Demo") for readability.
| ``` | ||
| https://discord.com/oauth2/authorize?client_id=YOUR_CLIENT_ID&permissions=277025508352&scope=bot | ||
| ``` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add language identifiers to fenced blocks for lint compliance.
📝 Suggested edit
-```
+```text
https://discord.com/oauth2/authorize?client_id=YOUR_CLIENT_ID&permissions=277025508352&scope=bot- +text
┌─────────────────────────────────────────────────────────────────────────┐
│ DISCORD USER │
│ │
│ "mercari rem bunny" │
└─────────────────────────────────┬───────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────┐
│ DISCORD BOT (Node.js) │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌────────────────────────┐ │
│ │ Message │───▶│ Intent │───▶│ Site Router │ │
│ │ Parser │ │ Router │ │ (amiami/mercari/all) │ │
│ └──────────────┘ └──────────────┘ └───────────┬────────────┘ │
│ │ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────▼────────────┐ │
│ │ SQLite │◀──▶│ Rate │◀──▶│ Search Handler │ │
│ │ Database │ │ Limiter │ │ + Rarity Scoring │ │
│ └──────────────┘ └──────────────┘ └───────────┬────────────┘ │
│ │ │
└───────────────────────────────────────────────────────┼─────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────┐
│ TINYFISH MINO API │
│ │
│ POST /v1/automation/run-sse │
│ { url: "https://mercari.com/search?keyword=rem", goal: "..." } │
│ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ Headless Browser → Navigate → Extract → Return Structured JSON │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘
│
┌─────────────┼─────────────┐
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────┐
│ 🇯🇵 │ │ 🇺🇸 │ │ ☀️ │
│ AmiAmi │ │ Mercari │ │ Solaris │
│ (JPY) │ │ (USD) │ │ (USD) │
└──────────┘ └──────────┘ └──────────┘
-```
+```text
waifu-deal-sniper/
├── bot.js # Main bot logic (1,543 lines)
├── database.js # SQLite database layer
├── templates.js # 670+ personality responses
├── package.json # Dependencies
└── README.md # This file
</details>
Also applies to: 121-165, 185-192
<details>
<summary>🧰 Tools</summary>
<details>
<summary>🪛 markdownlint-cli2 (0.18.1)</summary>
113-113: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
</details>
</details>
<details>
<summary>🤖 Prompt for AI Agents</summary>
In @waifu-deal-sniper/README.md around lines 113 - 115, The README contains
several fenced code blocks missing language identifiers which fails linting; add
a language tag (e.g., "text") to each triple-backtick fence for the blocks that
include the Discord OAuth URL
("https://discord.com/oauth2/authorize?client_id=YOUR_CLIENT_ID..."), the large
ASCII architecture diagram (the block starting with the DISCORD USER/ DISCORD
BOT diagram), and the repository file tree block that begins
"waifu-deal-sniper/"; ensure you update every matching fenced block (also the
other occurrences mentioned around the ASCII/art and file-tree sections) so each
opening becomestext.
</details>
<!-- fingerprinting:phantom:medusa:eagle -->
<!-- This is an auto-generated comment by CodeRabbit -->
Fix natural language parsing and add USD conversion
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 2
🤖 Fix all issues with AI agents
In `@waifu-deal-sniper/bot.js`:
- Around line 1256-1265: The embed description uses chosen.name unsanitized;
call the existing sanitizeForDisplay function on the name before trimming and
inserting into the EmbedBuilder. Replace the current interpolation
`${(chosen.name || 'Mystery Figure').slice(0, 200)}` with a sanitized value
(e.g., const safeName = sanitizeForDisplay(chosen.name || 'Mystery Figure') and
then use safeName.slice(0,200)) so EmbedBuilder.setDescription uses the
sanitized, truncated string; update the code around EmbedBuilder (the
setDescription line) accordingly.
- Around line 1493-1499: The embed title currently interpolates untrusted
deal.name in embed.setTitle (within the block that calls createFigureEmbed and
discordUser.send), which can contain mentions or markdown—sanitize deal.name
before use; replace (deal.name || watch.query) with a sanitized value using the
existing sanitizeForDisplay utility (or an equivalent mention-escaping function)
and use that sanitized string in embed.setTitle and the discordUser.send content
to prevent accidental mentions/formatting.
🧹 Nitpick comments (1)
waifu-deal-sniper/bot.js (1)
711-711: Hardcoded USD→JPY conversion rate.The exchange rate of 150 will drift over time. Consider fetching a live rate periodically or making this configurable. For a hobby bot this is acceptable, but worth noting.
| const embed = new EmbedBuilder() | ||
| .setColor(rarityColors[rarity] || 0x6C5CE7) | ||
| .setTitle(`🎰 ${pick(GACHA_TEMPLATES.rarity[rarity])}`) | ||
| .setDescription(`${pick(GACHA_TEMPLATES.reveal)}\n\n**${(chosen.name || 'Mystery Figure').slice(0, 200)}**`) | ||
| .addFields( | ||
| { name: '💴 Price', value: `¥${price.toLocaleString()}`, inline: true }, | ||
| { name: '✨ Condition', value: `Item: ${chosen.item_grade || '?'} | Box: ${chosen.box_grade || '?'}`, inline: true }, | ||
| { name: '📦 Stock', value: chosen.in_stock !== false ? '✅ Available!' : '❌ Sold Out', inline: true } | ||
| ) | ||
| .setURL(chosen.url || 'https://www.amiami.com'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unsanitized item name in gacha embed.
chosen.name is used in embed description without sanitization. Apply sanitizeForDisplay() for consistency with the security model.
🛡️ Suggested fix
const embed = new EmbedBuilder()
.setColor(rarityColors[rarity] || 0x6C5CE7)
.setTitle(`🎰 ${pick(GACHA_TEMPLATES.rarity[rarity])}`)
- .setDescription(`${pick(GACHA_TEMPLATES.reveal)}\n\n**${(chosen.name || 'Mystery Figure').slice(0, 200)}**`)
+ .setDescription(`${pick(GACHA_TEMPLATES.reveal)}\n\n**${sanitizeForDisplay(chosen.name || 'Mystery Figure').slice(0, 200)}**`)🤖 Prompt for AI Agents
In `@waifu-deal-sniper/bot.js` around lines 1256 - 1265, The embed description
uses chosen.name unsanitized; call the existing sanitizeForDisplay function on
the name before trimming and inserting into the EmbedBuilder. Replace the
current interpolation `${(chosen.name || 'Mystery Figure').slice(0, 200)}` with
a sanitized value (e.g., const safeName = sanitizeForDisplay(chosen.name ||
'Mystery Figure') and then use safeName.slice(0,200)) so
EmbedBuilder.setDescription uses the sanitized, truncated string; update the
code around EmbedBuilder (the setDescription line) accordingly.
| const embed = createFigureEmbed(deal); | ||
| embed.setTitle(`🚨 DEAL: ${(deal.name || watch.query).slice(0, 200)}`); | ||
|
|
||
| await discordUser.send({ | ||
| content: `🔔 **Found a deal for "${sanitizeForDisplay(watch.query)}"!**`, | ||
| embeds: [embed] | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unsanitized deal.name in watch notification embed.
The embed title uses deal.name which comes from external API and could contain mention strings.
🛡️ Suggested fix
const embed = createFigureEmbed(deal);
- embed.setTitle(`🚨 DEAL: ${(deal.name || watch.query).slice(0, 200)}`);
+ embed.setTitle(`🚨 DEAL: ${sanitizeForDisplay(deal.name || watch.query).slice(0, 200)}`);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const embed = createFigureEmbed(deal); | |
| embed.setTitle(`🚨 DEAL: ${(deal.name || watch.query).slice(0, 200)}`); | |
| await discordUser.send({ | |
| content: `🔔 **Found a deal for "${sanitizeForDisplay(watch.query)}"!**`, | |
| embeds: [embed] | |
| }); | |
| const embed = createFigureEmbed(deal); | |
| embed.setTitle(`🚨 DEAL: ${sanitizeForDisplay(deal.name || watch.query).slice(0, 200)}`); | |
| await discordUser.send({ | |
| content: `🔔 **Found a deal for "${sanitizeForDisplay(watch.query)}"!**`, | |
| embeds: [embed] | |
| }); |
🤖 Prompt for AI Agents
In `@waifu-deal-sniper/bot.js` around lines 1493 - 1499, The embed title currently
interpolates untrusted deal.name in embed.setTitle (within the block that calls
createFigureEmbed and discordUser.send), which can contain mentions or
markdown—sanitize deal.name before use; replace (deal.name || watch.query) with
a sanitized value using the existing sanitizeForDisplay utility (or an
equivalent mention-escaping function) and use that sanitized string in
embed.setTitle and the discordUser.send content to prevent accidental
mentions/formatting.
Added Debug Logging
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 2
🤖 Fix all issues with AI agents
In `@waifu-deal-sniper/bot.js`:
- Around line 1318-1322: In handleGachaLast (same pattern as handleGacha) the
embed uses unsanitized chosen.name; update the EmbedBuilder description to call
sanitizeForDisplay(chosen.name) before using it (and keep the existing fallback
'Mystery Figure' and .slice(0, 200) behavior), so the display string is
sanitized prior to slicing/embedding; locate the EmbedBuilder construction in
handleGachaLast and replace direct uses of chosen.name with
sanitizeForDisplay(chosen.name).
- Around line 1587-1592: The SIGTERM handler in process.on('SIGTERM') currently
calls client.destroy() and exits without persisting the sql.js database; export
the existing saveDb function from the database module (ensure saveDb is added to
module.exports alongside other exports) and update the SIGTERM handler to
require/import that saveDb and call await saveDb() (or call it and wait for its
Promise) before client.destroy() and process.exit(0), handling errors from
saveDb so shutdown still completes.
🧹 Nitpick comments (2)
waifu-deal-sniper/bot.js (2)
940-942: Consider addingallowedMentions: { parse: [] }as defense-in-depth.Even with sanitization, setting
allowedMentionson embed sends provides an extra layer of protection against mention abuse if sanitization is ever bypassed or missed.🛡️ Example for embed sends
for (const item of toShow) { - await message.channel.send({ embeds: [createFigureEmbed(item)] }); + await message.channel.send({ + embeds: [createFigureEmbed(item)], + allowedMentions: { parse: [] } + }); }Apply similarly to other
message.reply()andmessage.channel.send()calls that include embeds with external data.
711-711: Hardcoded exchange rate may drift over time.
USD_TO_JPY = 150is static. Consider fetching from an API or making it configurable via environment variable for accuracy.
| const embed = new EmbedBuilder() | ||
| .setColor(rarityColors[rarity] || 0x6C5CE7) | ||
| .setTitle(`🎰 ${pick(GACHA_TEMPLATES.rarity[rarity])}`) | ||
| .setDescription(`**${(chosen.name || 'Mystery Figure').slice(0, 200)}**\n\n💴 ¥${price.toLocaleString()}`) | ||
| .setURL(chosen.url || 'https://www.amiami.com'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
handleGachaLast also uses unsanitized item name.
Same pattern as handleGacha — apply sanitizeForDisplay() to chosen.name.
🛡️ Suggested fix
const embed = new EmbedBuilder()
.setColor(rarityColors[rarity] || 0x6C5CE7)
.setTitle(`🎰 ${pick(GACHA_TEMPLATES.rarity[rarity])}`)
- .setDescription(`**${(chosen.name || 'Mystery Figure').slice(0, 200)}**\n\n💴 ¥${price.toLocaleString()}`)
+ .setDescription(`**${sanitizeForDisplay(chosen.name || 'Mystery Figure')}**\n\n💴 ¥${price.toLocaleString()}`)📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const embed = new EmbedBuilder() | |
| .setColor(rarityColors[rarity] || 0x6C5CE7) | |
| .setTitle(`🎰 ${pick(GACHA_TEMPLATES.rarity[rarity])}`) | |
| .setDescription(`**${(chosen.name || 'Mystery Figure').slice(0, 200)}**\n\n💴 ¥${price.toLocaleString()}`) | |
| .setURL(chosen.url || 'https://www.amiami.com'); | |
| const embed = new EmbedBuilder() | |
| .setColor(rarityColors[rarity] || 0x6C5CE7) | |
| .setTitle(`🎰 ${pick(GACHA_TEMPLATES.rarity[rarity])}`) | |
| .setDescription(`**${sanitizeForDisplay(chosen.name || 'Mystery Figure')}**\n\n💴 ¥${price.toLocaleString()}`) | |
| .setURL(chosen.url || 'https://www.amiami.com'); |
🤖 Prompt for AI Agents
In `@waifu-deal-sniper/bot.js` around lines 1318 - 1322, In handleGachaLast (same
pattern as handleGacha) the embed uses unsanitized chosen.name; update the
EmbedBuilder description to call sanitizeForDisplay(chosen.name) before using it
(and keep the existing fallback 'Mystery Figure' and .slice(0, 200) behavior),
so the display string is sanitized prior to slicing/embedding; locate the
EmbedBuilder construction in handleGachaLast and replace direct uses of
chosen.name with sanitizeForDisplay(chosen.name).
| // Graceful shutdown | ||
| process.on('SIGTERM', () => { | ||
| console.log('👋 Shutting down gracefully...'); | ||
| client.destroy(); | ||
| process.exit(0); | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# First, let's check if database.js exists and its structure
fd -t f "database.js" waifu-deal-sniper/Repository: tinyfish-io/tinyfish-cookbook
Length of output: 102
🏁 Script executed:
#!/bin/bash
# Check for close/shutdown/cleanup exports in database module
rg -n "exports\.(close|shutdown|cleanup|quit)" waifu-deal-sniper/ --type js
rg -n "module\.exports.*=" waifu-deal-sniper/database.jsRepository: tinyfish-io/tinyfish-cookbook
Length of output: 95
🏁 Script executed:
#!/bin/bash
# Check if db is imported or initialized in bot.js
rg -n "require.*database|import.*database|db\s*=" waifu-deal-sniper/bot.js | head -20Repository: tinyfish-io/tinyfish-cookbook
Length of output: 109
🏁 Script executed:
#!/bin/bash
# Get context around the SIGTERM handler to see if db is available
sed -n '1580,1600p' waifu-deal-sniper/bot.jsRepository: tinyfish-io/tinyfish-cookbook
Length of output: 484
🏁 Script executed:
#!/bin/bash
# Check the full exports of database.js
cat -n waifu-deal-sniper/database.js | tail -100Repository: tinyfish-io/tinyfish-cookbook
Length of output: 3005
🏁 Script executed:
#!/bin/bash
# Search for method definitions in database.js that might be cleanup-related
rg -n "\.close|\.shutdown|\.cleanup|\.quit" waifu-deal-sniper/database.jsRepository: tinyfish-io/tinyfish-cookbook
Length of output: 55
🏁 Script executed:
#!/bin/bash
# Get the database module structure - check what properties are exported
sed -n '299,350p' waifu-deal-sniper/database.jsRepository: tinyfish-io/tinyfish-cookbook
Length of output: 440
🏁 Script executed:
#!/bin/bash
# Check the beginning of database.js to see db initialization
head -50 waifu-deal-sniper/database.jsRepository: tinyfish-io/tinyfish-cookbook
Length of output: 1367
🏁 Script executed:
#!/bin/bash
# Search for db variable initialization pattern
rg -n "const db|let db|var db" waifu-deal-sniper/database.js | head -5Repository: tinyfish-io/tinyfish-cookbook
Length of output: 90
🏁 Script executed:
#!/bin/bash
# Search for save, export, flush, or persistence methods in database.js
rg -n "export|save|flush|toFile|writeFile|persist" waifu-deal-sniper/database.jsRepository: tinyfish-io/tinyfish-cookbook
Length of output: 294
🏁 Script executed:
#!/bin/bash
# Check if there's any persistence logic - search for fs.writeFile or similar
rg -n "fs\." waifu-deal-sniper/database.jsRepository: tinyfish-io/tinyfish-cookbook
Length of output: 285
Graceful shutdown doesn't save database before exiting.
The database uses sql.js and has an auto-save every 30 seconds via saveDb(), but this isn't called during graceful shutdown. Recent changes made after the last auto-save interval will be lost. The saveDb() function exists but isn't exported or invoked in the SIGTERM handler.
Export saveDb from the database module and call it before shutdown:
🔧 Suggested fix
In database.js, add saveDb to module.exports:
module.exports = {
initDb,
getOrCreateUser,
// ... other exports
+ saveDb,
get db() { return db; },
};In bot.js, call it during shutdown:
process.on('SIGTERM', () => {
console.log('👋 Shutting down gracefully...');
+ db.saveDb();
client.destroy();
process.exit(0);
});🤖 Prompt for AI Agents
In `@waifu-deal-sniper/bot.js` around lines 1587 - 1592, The SIGTERM handler in
process.on('SIGTERM') currently calls client.destroy() and exits without
persisting the sql.js database; export the existing saveDb function from the
database module (ensure saveDb is added to module.exports alongside other
exports) and update the SIGTERM handler to require/import that saveDb and call
await saveDb() (or call it and wait for its Promise) before client.destroy() and
process.exit(0), handling errors from saveDb so shutdown still completes.
demo.mp4