// plugins/babble.js - Fresh Markov Chain Text Generator plugin with user-specific generation const path = require('path'); const fs = require('fs'); module.exports = { init(bot) { console.log('🎯 Babble plugin starting...'); this.bot = bot; // Try to load sqlite3 try { this.sqlite3 = require('sqlite3').verbose(); console.log('✅ SQLite3 loaded successfully'); } catch (error) { console.error('❌ SQLite3 failed to load:', error.message); this.sqlite3 = null; return; } // Database setup this.dbPath = path.join(__dirname, 'babble.db'); console.log(`📁 Database will be created at: ${this.dbPath}`); // Initialize database this.initDB(); // Simple settings this.settings = { minLength: 5, excludeBots: ['cancerbot', 'services'], excludeCommands: true }; console.log('✅ Babble plugin initialized'); }, cleanup(bot) { console.log('🎯 Babble plugin cleaning up'); if (this.db) { this.db.close(); } }, initDB() { try { this.db = new this.sqlite3.Database(this.dbPath, (err) => { if (err) { console.error('❌ Database creation failed:', err.message); this.db = null; return; } console.log('✅ Database created/opened successfully'); this.createTables(); }); } catch (error) { console.error('❌ Database init error:', error.message); this.db = null; } }, createTables() { if (!this.db) return; // Simple messages table this.db.run(` CREATE TABLE IF NOT EXISTS messages ( id INTEGER PRIMARY KEY AUTOINCREMENT, nick TEXT, channel TEXT, message TEXT, created DATETIME DEFAULT CURRENT_TIMESTAMP ) `, (err) => { if (err) { console.error('❌ Messages table creation failed:', err.message); } else { console.log('✅ Messages table ready'); } }); // Check if word_pairs table exists and if it has nick column this.db.get("PRAGMA table_info(word_pairs)", (err, result) => { if (err) { console.log('🔧 Creating new word_pairs table...'); this.createWordPairsTable(); } else { // Table exists, check if it has nick column this.db.all("PRAGMA table_info(word_pairs)", (err, columns) => { if (err) { console.error('❌ Error checking table structure:', err.message); return; } const hasNickColumn = columns.some(col => col.name === 'nick'); if (!hasNickColumn) { console.log('🔧 Migrating word_pairs table to add nick column...'); this.migrateWordPairsTable(); } else { console.log('✅ Word pairs table ready with nick column'); console.log('🎯 Database fully initialized'); } }); } }); }, createWordPairsTable() { this.db.run(` CREATE TABLE word_pairs ( id INTEGER PRIMARY KEY AUTOINCREMENT, word1 TEXT, word2 TEXT, count INTEGER DEFAULT 1, nick TEXT, UNIQUE(word1, word2, nick) ) `, (err) => { if (err) { console.error('❌ Word pairs table creation failed:', err.message); } else { console.log('✅ Word pairs table created with nick column'); console.log('🎯 Database fully initialized'); } }); }, migrateWordPairsTable() { // Rename old table this.db.run("ALTER TABLE word_pairs RENAME TO word_pairs_old", (err) => { if (err) { console.error('❌ Failed to rename old table:', err.message); return; } // Create new table with nick column this.db.run(` CREATE TABLE word_pairs ( id INTEGER PRIMARY KEY AUTOINCREMENT, word1 TEXT, word2 TEXT, count INTEGER DEFAULT 1, nick TEXT, UNIQUE(word1, word2, nick) ) `, (err) => { if (err) { console.error('❌ Failed to create new table:', err.message); return; } // Copy old data (nick will be NULL for old entries) this.db.run(` INSERT INTO word_pairs (word1, word2, count, nick) SELECT word1, word2, count, NULL FROM word_pairs_old `, (err) => { if (err) { console.error('❌ Failed to migrate data:', err.message); return; } // Drop old table this.db.run("DROP TABLE word_pairs_old", (err) => { if (err) { console.error('❌ Failed to drop old table:', err.message); } else { console.log('✅ Database migration completed successfully'); console.log('🎯 Database fully initialized'); } }); }); }); }); }, shouldLog(nick, message) { // Skip bots if (this.settings.excludeBots.includes(nick.toLowerCase())) { return false; } // Skip commands if (this.settings.excludeCommands && message.startsWith('!')) { return false; } // Skip short messages if (message.length < this.settings.minLength) { return false; } return true; }, cleanMessage(message) { // Remove IRC colors and formatting message = message.replace(/[\x02\x1D\x1F\x16\x0F]|\x03\d{0,2}(,\d{0,2})?/g, ''); // Basic cleanup message = message.toLowerCase().trim(); message = message.replace(/[^\w\s]/g, ' '); // Remove punctuation message = message.replace(/\s+/g, ' '); // Multiple spaces to single return message; }, storeMessage(nick, channel, message) { if (!this.db) { console.log('❌ No database available for storing message'); return; } if (!this.shouldLog(nick, message)) { console.log(`⏭️ Skipping message from ${nick}: ${message.substring(0, 20)}...`); return; } const cleaned = this.cleanMessage(message); if (cleaned.length < this.settings.minLength) { console.log('⏭️ Message too short after cleaning'); return; } console.log(`📝 Storing message from ${nick}: "${cleaned}"`); // Store the message this.db.run( 'INSERT INTO messages (nick, channel, message) VALUES (?, ?, ?)', [nick, channel, cleaned], (err) => { if (err) { console.error('❌ Failed to store message:', err.message); } else { console.log('✅ Message stored successfully'); this.buildPairs(cleaned, nick); } } ); }, buildPairs(message, nick) { if (!this.db) return; const words = message.split(' ').filter(w => w.length > 0); if (words.length < 2) return; // Add START and END markers const allWords = ['', ...words, '']; // Create word pairs for (let i = 0; i < allWords.length - 1; i++) { const word1 = allWords[i]; const word2 = allWords[i + 1]; this.db.run(` INSERT INTO word_pairs (word1, word2, count, nick) VALUES (?, ?, 1, ?) ON CONFLICT(word1, word2, nick) DO UPDATE SET count = count + 1 `, [word1, word2, nick], (err) => { if (err) { console.error('❌ Failed to store word pair:', err.message); } else { console.log(`🔗 Stored pair: "${word1}" → "${word2}" (${nick})`); } }); } }, generateText(callback, targetNick = null, creative = false) { if (!this.db) { callback('No database available'); return; } const mode = creative ? 'creative' : 'normal'; console.log(`🤖 Starting ${mode} text generation${targetNick ? ` for ${targetNick}` : ''}...`); // First check if target user exists and has enough data if (targetNick) { this.db.get( 'SELECT COUNT(*) as count FROM word_pairs WHERE nick = ?', [targetNick], (err, row) => { if (err || !row || row.count < 10) { callback(`Not enough data for user "${targetNick}" (need at least 10 word pairs)`); return; } this.performGeneration(callback, targetNick, creative); } ); } else { this.performGeneration(callback, null, creative); } }, performGeneration(callback, targetNick, creative = false) { const self = this; // Capture 'this' reference // Strategy: Start from a random word this.getRandomStartWord((err, startWord) => { if (err) { callback(err); return; } let currentWord = startWord; let result = []; let attempts = 0; const maxAttempts = creative ? 80 : 50; // More attempts for creative mode const maxLength = creative ? Math.floor(Math.random() * 15) + 8 : // Creative: 8-22 words Math.floor(Math.random() * 10) + 5; // Normal: 5-14 words console.log(`🎲 Starting with word: "${currentWord}" (${creative ? 'CREATIVE' : 'normal'} mode)`); // Add the starting word if it's not a marker if (currentWord !== '' && currentWord !== '') { result.push(currentWord); } const getNext = () => { if (attempts++ > maxAttempts || result.length >= maxLength) { if (result.length >= 3) { const prefix = targetNick ? `[${targetNick}] ` : ''; const emoji = creative ? '🎨 ' : '🤖 '; callback(null, prefix + emoji + result.join(' ')); } else { callback('Generation failed - too short'); } return; } // Build query based on whether we're targeting a specific user let query = 'SELECT word2, count FROM word_pairs WHERE word1 = ?'; let params = [currentWord]; if (targetNick) { query += ' AND nick = ?'; params.push(targetNick); } query += ' ORDER BY RANDOM() LIMIT 15'; // More options for creative mode this.db.all(query, params, (err, rows) => { if (err) { console.error('❌ Database error during generation:', err.message); callback('Database error: ' + err.message); return; } if (rows.length === 0) { // If stuck, try jumping to a random word console.log(`🔄 No next words for "${currentWord}", trying random jump`); // In creative mode, try mixing users more often const mixUsers = creative && Math.random() < 0.4; const jumpTargetNick = mixUsers ? null : targetNick; this.getRandomWord((err, randomWord) => { if (err || !randomWord || randomWord === '' || randomWord === '') { if (result.length >= 3) { const prefix = targetNick ? `[${targetNick}] ` : ''; const emoji = creative ? '🎨 ' : '🤖 '; callback(null, prefix + emoji + result.join(' ')); } else { callback('Generation incomplete'); } return; } result.push(randomWord); currentWord = randomWord; console.log(`🎲 Random jump to: "${randomWord}"${mixUsers ? ' (mixed user)' : ''}`); getNext(); }, jumpTargetNick); return; } // Different selection strategies for creative vs normal mode let nextWord; if (creative) { // Creative mode: Much more chaos and randomness const randomChoice = Math.random(); if (randomChoice < 0.6) { // 60% chance: Pick rare/uncommon words for weirdness const uncommon = rows.filter(r => r.count <= 2); if (uncommon.length > 0) { nextWord = uncommon[Math.floor(Math.random() * uncommon.length)].word2; console.log(`🎨 Weird choice: "${nextWord}"`); } else { // If no uncommon words, pick any random one nextWord = rows[Math.floor(Math.random() * rows.length)].word2; console.log(`🎨 Random fallback: "${nextWord}"`); } } else if (randomChoice < 0.8) { // 20% chance: Pick completely random word (ignore context) self.getRandomWord((err, randomWord) => { if (!err && randomWord && randomWord !== '' && randomWord !== '') { result.push(randomWord); currentWord = randomWord; console.log(`🎨 Context break: "${randomWord}"`); getNext(); return; } // Fallback to normal selection nextWord = rows[Math.floor(Math.random() * rows.length)].word2; console.log(`🎨 Fallback random: "${nextWord}"`); processNextWord(); }, null); // null = any user for maximum chaos return; } else { // 20% chance: Normal weighted selection for some coherence const totalWeight = rows.reduce((sum, row) => sum + row.count, 0); let random = Math.random() * totalWeight; nextWord = rows[0].word2; for (const row of rows) { random -= row.count; if (random <= 0) { nextWord = row.word2; break; } } console.log(`🎨 Weighted choice: "${nextWord}"`); } } else { // Normal mode: Original logic if (Math.random() < 0.3) { // 30% chance: Pick a less common word for creativity const lessCommon = rows.filter(r => r.count <= 2); if (lessCommon.length > 0) { nextWord = lessCommon[Math.floor(Math.random() * lessCommon.length)].word2; console.log(`🎨 Creative choice: "${nextWord}"`); } else { nextWord = rows[Math.floor(Math.random() * Math.min(5, rows.length))].word2; } } else { // 70% chance: Normal weighted selection const totalWeight = rows.reduce((sum, row) => sum + row.count, 0); let random = Math.random() * totalWeight; nextWord = rows[0].word2; for (const row of rows) { random -= row.count; if (random <= 0) { nextWord = row.word2; break; } } } } processNextWord(); function processNextWord() { console.log(`🔗 Selected next word: "${nextWord}"`); // Check for end if (nextWord === '') { if (result.length >= 3) { const prefix = targetNick ? `[${targetNick}] ` : ''; const emoji = creative ? '🎨 ' : '🤖 '; callback(null, prefix + emoji + result.join(' ')); } else { // Too short, try to continue with random word self.getRandomWord((err, randomWord) => { if (!err && randomWord && randomWord !== '' && randomWord !== '') { result.push(randomWord); currentWord = randomWord; getNext(); } else { const prefix = targetNick ? `[${targetNick}] ` : ''; const emoji = creative ? '🎨 ' : '🤖 '; callback(null, prefix + emoji + result.join(' ')); } }, targetNick); } return; } // Add word and continue if (nextWord !== '') { result.push(nextWord); } currentWord = nextWord; getNext(); } }); }; getNext(); }, targetNick); }, getRandomStartWord(callback, targetNick = null) { if (!this.db) { callback('No database available'); return; } // 50% chance to start with , 50% chance to start with random word if (Math.random() < 0.5) { callback(null, ''); return; } // Build query for random word that appears as word1 let query = 'SELECT word1 FROM word_pairs WHERE word1 != "" AND word1 != ""'; let params = []; if (targetNick) { query += ' AND nick = ?'; params.push(targetNick); } query += ' ORDER BY RANDOM() LIMIT 1'; this.db.get(query, params, (err, row) => { if (err || !row) { callback(null, ''); // Fallback } else { callback(null, row.word1); } }); }, getRandomWord(callback, targetNick = null) { if (!this.db) { callback('No database available'); return; } let query = 'SELECT word2 FROM word_pairs WHERE word2 != "" AND word2 != ""'; let params = []; if (targetNick) { query += ' AND nick = ?'; params.push(targetNick); } query += ' ORDER BY RANDOM() LIMIT 1'; this.db.get(query, params, (err, row) => { if (err || !row) { callback('No random word found'); } else { callback(null, row.word2); } }); }, getStats(callback) { if (!this.db) { callback('No database available'); return; } this.db.get('SELECT COUNT(*) as messages FROM messages', (err, msgRow) => { if (err) { callback('Database error: ' + err.message); return; } this.db.get('SELECT COUNT(*) as pairs FROM word_pairs', (err, pairRow) => { if (err) { callback('Database error: ' + err.message); return; } this.db.get('SELECT COUNT(DISTINCT nick) as users FROM messages', (err, userRow) => { if (err) { callback('Database error: ' + err.message); return; } callback(null, { messages: msgRow.messages, pairs: pairRow.pairs, users: userRow.users }); }); }); }); }, // This is the key - message event handler onMessage(data, bot) { console.log(`📨 Babble received message from ${data.nick}: "${data.message}"`); if (data.isChannel) { this.storeMessage(data.nick, data.target, data.message); } }, commands: [ { name: 'babble', description: 'Generate random text from learned messages, optionally like a specific user', execute: function(context, bot) { const plugin = module.exports; const target = context.replyTo; const from = context.nick; const args = context.args; // Check if user specified a target nick const targetNick = args.length > 0 ? args[0] : null; plugin.generateText((err, text) => { if (err) { bot.say(target, `${from}: ${err}`); } else { bot.say(target, text); } }, targetNick, false); } }, { name: 'babblecreative', description: 'Generate weird, funny, and strange text with maximum chaos', execute: function(context, bot) { const plugin = module.exports; const target = context.replyTo; const from = context.nick; const args = context.args; // Check if user specified a target nick const targetNick = args.length > 0 ? args[0] : null; plugin.generateText((err, text) => { if (err) { bot.say(target, `${from}: ${err}`); } else { bot.say(target, text); } }, targetNick, true); // true = creative mode } }, { name: 'babblestats', description: 'Show babble learning statistics', execute: function(context, bot) { const plugin = module.exports; const target = context.replyTo; plugin.getStats((err, stats) => { if (err) { bot.say(target, `Error: ${err}`); } else { bot.say(target, `🧠 Learned: ${stats.messages} messages, ${stats.pairs} word pairs, ${stats.users} users`); } }); } }, { name: 'babbleusers', description: 'Show users available for targeted babbling', execute: function(context, bot) { const plugin = module.exports; const target = context.replyTo; if (!plugin.db) { bot.say(target, 'No database available'); return; } plugin.db.all( 'SELECT nick, COUNT(*) as word_count FROM word_pairs WHERE nick IS NOT NULL GROUP BY nick ORDER BY word_count DESC LIMIT 10', (err, rows) => { if (err) { bot.say(target, 'Database error: ' + err.message); return; } if (rows.length === 0) { bot.say(target, '🤖 No users found in babble database yet.'); return; } const userList = rows.map(row => `${row.nick}(${row.word_count})`).join(', '); bot.say(target, `🎭 Available users: ${userList}`); bot.say(target, `💡 Usage: !babble or !babblecreative `); } ); } }, { name: 'babbleadd', description: 'Manually add a message to learning database', execute: function(context, bot) { const plugin = module.exports; const target = context.replyTo; const from = context.nick; const args = context.args; if (args.length === 0) { bot.say(target, `Usage: !babbleadd `); return; } const message = args.join(' '); plugin.storeMessage(from, target, message); bot.say(target, `${from}: Added message to learning database`); } }, { name: 'babbletest', description: 'Add test data to verify babble is working', execute: function(context, bot) { const plugin = module.exports; const target = context.replyTo; const from = context.nick; const testMessages = [ 'hello world how are you doing today', 'i love programming with javascript and node', 'the weather is really nice outside today', 'coding is fun but sometimes challenging', 'artificial intelligence is fascinating technology' ]; testMessages.forEach(msg => { plugin.storeMessage('testuser', target, msg); }); bot.say(target, `${from}: Added ${testMessages.length} test messages. Try !babble in a few seconds.`); } }, { name: 'babblereset', description: 'Reset all babble data (admin only)', execute: function(context, bot) { const plugin = module.exports; const target = context.replyTo; const from = context.nick; const adminNicks = ['admin', 'owner', 'cancerbot', 'megasconed']; if (!adminNicks.includes(from)) { bot.say(target, `${from}: Access denied - admin only command`); return; } if (!plugin.db) { bot.say(target, `${from}: No database available`); return; } plugin.db.run('DELETE FROM messages', (err) => { if (err) { bot.say(target, `${from}: Error clearing messages: ${err.message}`); return; } plugin.db.run('DELETE FROM word_pairs', (err) => { if (err) { bot.say(target, `${from}: Error clearing word pairs: ${err.message}`); } else { bot.say(target, `${from}: All babble data cleared`); } }); }); } } ] };