From 30132cf7529f9f4c7caf6c8b3a229c4fc2421fbd Mon Sep 17 00:00:00 2001 From: David Lindsay Date: Wed, 22 Apr 2026 12:40:36 -0700 Subject: [PATCH 1/2] Fix three !s command bugs in replacer - Split on first / only so search phrases containing / (e.g. AC/DC) work correctly - Guard isBlockedSearchPhrase against undefined replacement to prevent crash when no / is used - Fix message skip filter to use === 0 so only !s commands are skipped, not any message containing !s Co-Authored-By: Claude Sonnet 4.6 --- replacer.js | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/replacer.js b/replacer.js index 5dfed13..fef0ab2 100644 --- a/replacer.js +++ b/replacer.js @@ -107,7 +107,7 @@ function replaceFirstMessage(messages, regex, replacement, channel) { const lowerCaseSearch = regex.toLocaleLowerCase(); return messages.every(msg => { - if(msg.author.bot || msg.content.toString().indexOf('!s') > -1) { + if(msg.author.bot || msg.content.toString().indexOf('!s') === 0) { console.debug('Ignoring message from bot or search message'); return true; } @@ -144,13 +144,14 @@ function replaceFirstMessage(messages, regex, replacement, channel) { * @returns {{search:RegExp, isBlockedPhrase:boolean, replacement:string}} */ function splitReplaceCommand(replaceCommand) { - var response = replaceCommand.replace(/!s /, '').split('/'); - const search = response[0].unicodeToMerica(); - const replacement = response[1]; + const withoutCommand = replaceCommand.replace(/!s /, ''); + const slashIndex = withoutCommand.indexOf('/'); + const search = (slashIndex === -1 ? withoutCommand : withoutCommand.slice(0, slashIndex)).unicodeToMerica(); + const replacement = slashIndex === -1 ? undefined : withoutCommand.slice(slashIndex + 1); return { search, - isBlockedPhrase: isBlockedSearchPhrase(response[0]) || isBlockedSearchPhrase(replacement), + isBlockedPhrase: isBlockedSearchPhrase(search) || (replacement !== undefined && isBlockedSearchPhrase(replacement)), replacement }; } From faed2fe3e97206fc346c63d731522dde80d102c4 Mon Sep 17 00:00:00 2001 From: David Lindsay Date: Wed, 22 Apr 2026 12:44:25 -0700 Subject: [PATCH 2/2] Add !trending command showing top/bottom 5 scored phrases last 7 days Co-Authored-By: Claude Sonnet 4.6 --- __tests__/scoring.test.js | 36 ++++++++++++++++++++++++++++++++++++ index.js | 6 +++++- scoring.js | 30 ++++++++++++++++++++++++++++++ 3 files changed, 71 insertions(+), 1 deletion(-) diff --git a/__tests__/scoring.test.js b/__tests__/scoring.test.js index 45ea9df..53260ad 100644 --- a/__tests__/scoring.test.js +++ b/__tests__/scoring.test.js @@ -66,6 +66,42 @@ describe('Tests for the scoring module', () => { }); + it('getTrending returns formatted top and bottom phrases from the last 7 days', async () => { + const { processScores, getTrending } = require('../scoring'); + const phrases = [ + 'pizza++', 'pizza++', 'pizza++', + 'jazz++', 'jazz++', + 'mondays--', 'mondays--', 'mondays--', + 'meetings--', + ]; + phrases.forEach(p => processScores({ content: p })); + + const result = getTrending(2); + expect(result).toContain('pizza (+3)'); + expect(result).toContain('jazz (+2)'); + expect(result).toContain('mondays (-3)'); + expect(result).toContain('meetings (-1)'); + }); + + it('getTrending shows none when no data', async () => { + const { getTrending } = require('../scoring'); + const result = getTrending(5); + expect(result).toContain('Top 5: none'); + expect(result).toContain('Bottom 5: none'); + }); + + it('getTrending excludes scores older than 7 days', async () => { + const { processScores, getTrending } = require('../scoring'); + const { getDatabase } = require('../db'); + processScores({ content: 'pizza++' }); + const db = getDatabase(); + const oldTimestamp = Date.now() - 8 * 24 * 60 * 60 * 1000; + db.prepare('INSERT INTO scoring (timestamp, phrase, score) VALUES (?, ?, ?)').run(oldTimestamp, 'ancient history', 10); + + const result = getTrending(5); + expect(result).not.toContain('ancient history'); + }); + it('handles multiline scores', async () => { const { processScores, getScore } = require('../scoring'); await getScore('urch', score => { diff --git a/index.js b/index.js index 34b3a45..4a022d8 100644 --- a/index.js +++ b/index.js @@ -1,7 +1,7 @@ const { Client, GatewayIntentBits } = require('discord.js'); const config = require('./config').getConfig(); const { replaceFirstMessage, splitReplaceCommand } = require('./replacer'); -const { processScores, getScore } = require('./scoring'); +const { processScores, getScore, getTrending } = require('./scoring'); const { oneBlockedMessage } = require('./one-blocked-message'); /** @@ -46,6 +46,10 @@ client.on('messageCreate', async (initialQuery) => { } } } + else if (initialQuery.content.indexOf('!trending') === 0) + { + initialQuery.channel.send(getTrending(5)); + } else if (initialQuery.content.indexOf('!score ') == 0) { if (oneBlockedMessage(initialQuery)) { diff --git a/scoring.js b/scoring.js index 0c83c82..70b6ae7 100644 --- a/scoring.js +++ b/scoring.js @@ -74,7 +74,37 @@ function processScores(message) { } +/** + * Gets the top and bottom phrases by score delta over the last 7 days. + * + * @param {number} limit Number of phrases to return for each end. + * @returns {string} Formatted message ready to send to Discord. + */ +function getTrending(limit = 5) { + createSchema(db); + const since = Date.now() - 7 * 24 * 60 * 60 * 1000; + const rows = db.prepare(` + SELECT phrase, SUM(score) as total + FROM scoring + WHERE timestamp >= ? + GROUP BY phrase COLLATE NOCASE + HAVING total != 0 + ORDER BY total DESC + `).all(since); + + const top = rows.slice(0, limit); + const bottom = rows.slice(-limit).filter(r => !top.includes(r)).reverse(); + + const fmt = (rows, label) => { + if (rows.length === 0) return `*${label}: none*`; + return `**${label}**\n` + rows.map((r, i) => `${i + 1}. ${r.phrase} (${r.total > 0 ? '+' : ''}${r.total})`).join('\n'); + }; + + return `Trending last 7 days:\n${fmt(top, 'Top 5')}\n\n${fmt(bottom, 'Bottom 5')}`; +} + module.exports = { getScore, + getTrending, processScores }; \ No newline at end of file