diff --git a/__tests__/detect-programmer.test.js b/__tests__/detect-programmer.test.js new file mode 100644 index 0000000..6c0f55d --- /dev/null +++ b/__tests__/detect-programmer.test.js @@ -0,0 +1,197 @@ +const { detectProgrammer, programmingKeywords } = require('../detect-programmer'); + +describe('detectProgrammer', () => { + test('should detect programmer from programming keywords in Russian', () => { + const messages = [ + { id: 1, text: 'Привет! Я занимаюсь программированием на Python' }, + { id: 2, text: 'Изучаю React и Node.js' }, + { id: 3, text: 'Работаю разработчиком' }, + ]; + + const result = detectProgrammer(messages); + + expect(result.isProgrammer).toBe(true); + expect(result.confidence).toBeGreaterThan(0); + expect(result.indicators.length).toBeGreaterThan(0); + expect(result.stats.totalMessages).toBe(3); + }); + + test('should detect programmer from programming keywords in English', () => { + const messages = [ + { id: 1, text: 'I am a software engineer' }, + { id: 2, text: 'Working with JavaScript and TypeScript' }, + { id: 3, text: 'Love coding in Python' }, + ]; + + const result = detectProgrammer(messages); + + expect(result.isProgrammer).toBe(true); + expect(result.confidence).toBeGreaterThan(0); + expect(result.indicators.length).toBeGreaterThan(0); + }); + + test('should detect programmer from code patterns', () => { + const messages = [ + { id: 1, text: 'function test() { return true; }' }, + { id: 2, text: 'const myVar = 123;' }, + { id: 3, text: 'let x = await getData();' }, + ]; + + const result = detectProgrammer(messages); + + expect(result.isProgrammer).toBe(true); + expect(result.confidence).toBeGreaterThan(0); + expect(result.stats.codePatternMatches).toBeGreaterThan(0); + }); + + test('should detect programmer from technology mentions', () => { + const messages = [ + { id: 1, text: 'Запускаю docker контейнер' }, + { id: 2, text: 'Использую git для версионирования' }, + { id: 3, text: 'Деплою на kubernetes' }, + { id: 4, text: 'Работаю с PostgreSQL базой данных' }, + ]; + + const result = detectProgrammer(messages); + + expect(result.isProgrammer).toBe(true); + expect(result.confidence).toBeGreaterThan(0); + }); + + test('should not detect programmer from general conversation', () => { + const messages = [ + { id: 1, text: 'Привет! Как дела?' }, + { id: 2, text: 'Что делаешь сегодня?' }, + { id: 3, text: 'Пойдём гулять?' }, + { id: 4, text: 'Хорошая погода' }, + { id: 5, text: 'Как твои дела?' }, + ]; + + const result = detectProgrammer(messages); + + expect(result.isProgrammer).toBe(false); + expect(result.confidence).toBe(0); + expect(result.indicators.length).toBe(0); + }); + + test('should handle empty messages array', () => { + const messages = []; + + const result = detectProgrammer(messages); + + expect(result.isProgrammer).toBe(false); + expect(result.confidence).toBe(0); + expect(result.indicators.length).toBe(0); + }); + + test('should handle null messages', () => { + const result = detectProgrammer(null); + + expect(result.isProgrammer).toBe(false); + expect(result.confidence).toBe(0); + }); + + test('should handle messages without text', () => { + const messages = [ + { id: 1 }, + { id: 2, text: null }, + { id: 3, text: '' }, + ]; + + const result = detectProgrammer(messages); + + expect(result.isProgrammer).toBe(false); + }); + + test('should detect programmer with mixed programming and casual messages', () => { + const messages = [ + { id: 1, text: 'Привет!' }, + { id: 2, text: 'Работаю над API на Node.js' }, + { id: 3, text: 'Как дела?' }, + { id: 4, text: 'Изучаю алгоритмы' }, + { id: 5, text: 'Спасибо!' }, + ]; + + const result = detectProgrammer(messages); + + expect(result.isProgrammer).toBe(true); + expect(result.stats.totalMessages).toBe(5); + expect(result.stats.keywordMatches).toBeGreaterThan(0); + }); + + test('should detect programmer from framework mentions', () => { + const messages = [ + { id: 1, text: 'Использую React для фронтенда' }, + { id: 2, text: 'Django отличный фреймворк' }, + { id: 3, text: 'Express.js для сервера' }, + ]; + + const result = detectProgrammer(messages); + + expect(result.isProgrammer).toBe(true); + expect(result.confidence).toBeGreaterThan(0); + }); + + test('should detect programmer from GitHub/Git mentions', () => { + const messages = [ + { id: 1, text: 'Залил коммит на GitHub' }, + { id: 2, text: 'Сделал пулреквест' }, + { id: 3, text: 'Смержил ветку' }, + ]; + + const result = detectProgrammer(messages); + + expect(result.isProgrammer).toBe(true); + }); + + test('should have correct confidence calculation', () => { + const messages = [ + { id: 1, text: 'Я программист' }, + { id: 2, text: 'Пишу код на JavaScript' }, + { id: 3, text: 'function test() {}' }, + ]; + + const result = detectProgrammer(messages); + + expect(result.isProgrammer).toBe(true); + expect(result.confidence).toBeGreaterThanOrEqual(0); + expect(result.confidence).toBeLessThanOrEqual(100); + expect(result.stats.matchRatio).toBeGreaterThan(0); + }); + + test('should limit indicators to top 10', () => { + const messages = []; + for (let i = 0; i < 20; i++) { + messages.push({ id: i, text: 'I am a programmer working with JavaScript' }); + } + + const result = detectProgrammer(messages); + + expect(result.isProgrammer).toBe(true); + expect(result.indicators.length).toBeLessThanOrEqual(10); + }); + + test('should detect from LeetCode/HackerRank mentions', () => { + const messages = [ + { id: 1, text: 'Решаю задачи на LeetCode' }, + { id: 2, text: 'Прошёл тест на HackerRank' }, + ]; + + const result = detectProgrammer(messages); + + expect(result.isProgrammer).toBe(true); + }); + + test('should be case insensitive', () => { + const messages = [ + { id: 1, text: 'JAVASCRIPT' }, + { id: 2, text: 'PyThOn' }, + { id: 3, text: 'ReAcT' }, + ]; + + const result = detectProgrammer(messages); + + expect(result.isProgrammer).toBe(true); + expect(result.stats.keywordMatches).toBeGreaterThan(0); + }); +}); diff --git a/detect-programmer.js b/detect-programmer.js new file mode 100644 index 0000000..d3144fa --- /dev/null +++ b/detect-programmer.js @@ -0,0 +1,121 @@ +// Programming-related keywords and patterns to detect programmers +const programmingKeywords = [ + // Programming languages + 'javascript', 'python', 'java', 'c++', 'c#', 'php', 'ruby', 'go', 'rust', 'swift', + 'kotlin', 'typescript', 'scala', 'perl', 'haskell', 'dart', 'elixir', 'clojure', + + // Technologies and frameworks + 'react', 'angular', 'vue', 'node', 'django', 'flask', 'spring', 'laravel', + 'express', 'fastapi', 'rails', 'asp.net', 'nextjs', 'nuxt', 'svelte', + + // Development concepts + 'api', 'backend', 'frontend', 'fullstack', 'database', 'sql', 'nosql', + 'mongodb', 'postgresql', 'mysql', 'redis', 'docker', 'kubernetes', + 'git', 'github', 'gitlab', 'bitbucket', 'ci/cd', 'devops', + 'algorithm', 'data structure', 'leetcode', 'hackerrank', + + // Programming terms (Russian) + 'программирование', 'программист', 'разработчик', 'разработка', + 'код', 'кодинг', 'баг', 'дебаг', 'репозиторий', 'коммит', + 'пулреквест', 'деплой', 'бэкенд', 'фронтенд', 'фулстек', + + // Programming terms (English) + 'programming', 'programmer', 'developer', 'development', 'coding', + 'code', 'bug', 'debug', 'repository', 'commit', 'pull request', + 'deploy', 'deployment', 'software', 'engineer', 'engineering', + + // Common development phrases + 'npm install', 'pip install', 'yarn add', 'import ', 'require(', + 'function ', 'const ', 'let ', 'var ', 'class ', 'async ', 'await ', +]; + +// Regular expressions for code patterns +const codePatterns = [ + /\bfunction\s+\w+\s*\(/i, + /\bconst\s+\w+\s*=/i, + /\blet\s+\w+\s*=/i, + /\bvar\s+\w+\s*=/i, + /\bimport\s+.*\s+from\s+/i, + /\brequire\s*\(/i, + /\bclass\s+\w+/i, + /\basync\s+function/i, + /=>/, // Arrow functions + /\{\s*\.\.\.\w+\s*\}/, // Spread operator + /\w+\.\w+\(.*\)/, // Method calls + /\/\/ .+/, // Single-line comments + /\/\*.+\*\//, // Multi-line comments + /```[\s\S]*```/, // Code blocks in markdown +]; + +/** + * Detects if a person is a programmer based on their message history + * @param {Array} messages - Array of message objects from VK API + * @returns {Object} - { isProgrammer: boolean, confidence: number, indicators: Array } + */ +function detectProgrammer(messages) { + if (!messages || messages.length === 0) { + return { isProgrammer: false, confidence: 0, indicators: [] }; + } + + const indicators = []; + let keywordMatches = 0; + let codePatternMatches = 0; + + for (const message of messages) { + const text = message.text?.toLowerCase() || ''; + + // Skip very short messages + if (text.length < 3) { + continue; + } + + // Check for programming keywords + for (const keyword of programmingKeywords) { + if (text.includes(keyword.toLowerCase())) { + keywordMatches++; + indicators.push({ type: 'keyword', value: keyword, messageId: message.id }); + break; // Count only once per message + } + } + + // Check for code patterns + for (const pattern of codePatterns) { + if (pattern.test(message.text || '')) { + codePatternMatches++; + indicators.push({ type: 'pattern', value: pattern.toString(), messageId: message.id }); + break; // Count only once per message + } + } + } + + // Calculate confidence based on matches + const totalMatches = keywordMatches + (codePatternMatches * 2); // Code patterns weighted more + const messagesChecked = messages.length; + const matchRatio = totalMatches / messagesChecked; + + // Determine if programmer + // If at least 3 matches or 5% of messages contain programming content + const isProgrammer = totalMatches >= 3 || matchRatio >= 0.05; + + // Confidence score (0-100) + const confidence = Math.min(100, Math.round(matchRatio * 1000 + totalMatches * 10)); + + return { + isProgrammer, + confidence, + indicators: indicators.slice(0, 10), // Return top 10 indicators + stats: { + totalMessages: messagesChecked, + keywordMatches, + codePatternMatches, + totalMatches, + matchRatio: Math.round(matchRatio * 10000) / 100, // Percentage + } + }; +} + +module.exports = { + detectProgrammer, + programmingKeywords, + codePatterns, +}; diff --git a/experiments/test-programmer-detection.js b/experiments/test-programmer-detection.js new file mode 100644 index 0000000..f456955 --- /dev/null +++ b/experiments/test-programmer-detection.js @@ -0,0 +1,93 @@ +const { VK } = require('vk-io'); +const { getToken } = require('../utils'); +const { detectProgrammer } = require('../detect-programmer'); +const { getOrLoadMessages } = require('../messages-cache'); +const { + setProgrammerStatus, + getProgrammerStatus, + getAllProgrammerStatuses, +} = require('../programmer-status-cache'); + +const token = getToken(); +const vk = new VK({ token }); + +/** + * Test programmer detection on a specific friend + * Usage: node experiments/test-programmer-detection.js [friendId] + */ +async function testProgrammerDetection() { + const friendId = process.argv[2]; + + if (!friendId) { + console.error('Usage: node experiments/test-programmer-detection.js [friendId]'); + process.exit(1); + } + + console.log(`Testing programmer detection for friend ID: ${friendId}`); + console.log('='.repeat(60)); + + try { + // Load messages + console.log('\n1. Loading message history...'); + const messages = await getOrLoadMessages({ context: { vk }, friendId: parseInt(friendId) }); + console.log(` Loaded ${messages?.length || 0} messages`); + + if (!messages || messages.length === 0) { + console.log('\n ⚠️ No messages found. Unable to detect.'); + return; + } + + // Show sample messages + console.log('\n2. Sample messages (first 5):'); + messages.slice(0, 5).forEach((msg, idx) => { + const text = msg.text || '[no text]'; + const preview = text.length > 80 ? text.substring(0, 80) + '...' : text; + console.log(` ${idx + 1}. ${preview}`); + }); + + // Detect programmer + console.log('\n3. Running programmer detection...'); + const detection = detectProgrammer(messages); + + console.log('\n4. Detection Results:'); + console.log(' ='.repeat(58)); + console.log(` Is Programmer: ${detection.isProgrammer ? '✓ YES' : '✗ NO'}`); + console.log(` Confidence: ${detection.confidence}%`); + console.log(` Total Messages Analyzed: ${detection.stats.totalMessages}`); + console.log(` Keyword Matches: ${detection.stats.keywordMatches}`); + console.log(` Code Pattern Matches: ${detection.stats.codePatternMatches}`); + console.log(` Total Matches: ${detection.stats.totalMatches}`); + console.log(` Match Ratio: ${detection.stats.matchRatio}%`); + + if (detection.indicators.length > 0) { + console.log('\n5. Programming Indicators Found:'); + detection.indicators.forEach((indicator, idx) => { + console.log(` ${idx + 1}. Type: ${indicator.type}, Value: ${indicator.value}`); + }); + } + + // Check current status in cache + console.log('\n6. Current Cache Status:'); + const currentStatus = await getProgrammerStatus(friendId); + if (currentStatus) { + console.log(' Cached Status:', JSON.stringify(currentStatus, null, 2)); + } else { + console.log(' No cached status found.'); + } + + // Ask if user wants to save the result + console.log('\n7. Would you like to save this detection to cache?'); + console.log(' Run the following command to save:'); + console.log(` node -e "require('./programmer-status-cache').setProgrammerStatus(${friendId}, { isProgrammer: ${detection.isProgrammer}, confidence: ${detection.confidence}, method: 'manual-test', checkedAt: new Date().toISOString() })"`); + + console.log('\n' + '='.repeat(60)); + console.log('Test completed successfully!'); + + } catch (error) { + console.error('\n❌ Error during test:', error); + process.exit(1); + } +} + +// Run the test +testProgrammerDetection(); diff --git a/experiments/view-programmer-statuses.js b/experiments/view-programmer-statuses.js new file mode 100644 index 0000000..5fa835c --- /dev/null +++ b/experiments/view-programmer-statuses.js @@ -0,0 +1,86 @@ +const { + getAllProgrammerStatuses, + getProgrammerStatus, +} = require('../programmer-status-cache'); + +/** + * View all programmer statuses or a specific friend's status + * Usage: + * node experiments/view-programmer-statuses.js # View all + * node experiments/view-programmer-statuses.js [friendId] # View specific + */ +async function viewProgrammerStatuses() { + const friendId = process.argv[2]; + + try { + if (friendId) { + // View specific friend's status + console.log(`Viewing programmer status for friend ID: ${friendId}`); + console.log('='.repeat(60)); + + const status = await getProgrammerStatus(friendId); + + if (status) { + console.log('\nStatus:', JSON.stringify(status, null, 2)); + } else { + console.log('\n⚠️ No status found for this friend.'); + } + } else { + // View all statuses + console.log('Viewing all programmer statuses:'); + console.log('='.repeat(60)); + + const allStatuses = await getAllProgrammerStatuses(); + const statusEntries = Object.entries(allStatuses); + + if (statusEntries.length === 0) { + console.log('\n⚠️ No statuses found in cache.'); + return; + } + + console.log(`\nTotal friends with status: ${statusEntries.length}\n`); + + // Count by category + let programmers = 0; + let nonProgrammers = 0; + let unknown = 0; + let asked = 0; + + statusEntries.forEach(([friendId, status]) => { + if (status.isProgrammer === true) programmers++; + else if (status.isProgrammer === false) nonProgrammers++; + else unknown++; + + if (status.askedAt) asked++; + }); + + console.log('Summary:'); + console.log(` ✓ Confirmed Programmers: ${programmers}`); + console.log(` ✗ Confirmed Non-Programmers: ${nonProgrammers}`); + console.log(` ? Unknown: ${unknown}`); + console.log(` 📧 Asked (pending response): ${asked}`); + + console.log('\n' + '-'.repeat(60)); + console.log('Detailed List:\n'); + + statusEntries.forEach(([friendId, status]) => { + const isProg = status.isProgrammer === true ? '✓' : + status.isProgrammer === false ? '✗' : '?'; + const asked = status.askedAt ? '📧' : ' '; + const confidence = status.confidence || 0; + const method = status.method || 'unknown'; + + console.log(`${isProg} ${asked} Friend ${friendId}: confidence=${confidence}%, method=${method}`); + }); + } + + console.log('\n' + '='.repeat(60)); + + } catch (error) { + console.error('❌ Error viewing statuses:', error); + process.exit(1); + } +} + +// Run the viewer +viewProgrammerStatuses(); diff --git a/index.js b/index.js index 4226830..6fcd942 100644 --- a/index.js +++ b/index.js @@ -111,6 +111,11 @@ const deleteOutgoingFriendRequestsInterval = setInterval(async () => { await executeTrigger(deleteOutgoingFriendRequestsTrigger, { vk, options: { maxRequests: 20 } }); }, (8 * minute) / ms); +const { trigger: filterProgrammersTrigger } = require('./triggers/filter-programmers'); +const filterProgrammersInterval = setInterval(async () => { + await executeTrigger(filterProgrammersTrigger, { vk, options: { maxFriendsToCheck: 5 } }); +}, (45 * minute) / ms); + const { trigger: sendInvitationPostsForFriendsTrigger } = require('./triggers/send-invitation-posts-for-friends'); const sendInvitationPostsForFriendsIntervalAction = async () => { await executeTrigger(sendInvitationPostsForFriendsTrigger, { vk }); diff --git a/programmer-status-cache.js b/programmer-status-cache.js new file mode 100644 index 0000000..a5e84be --- /dev/null +++ b/programmer-status-cache.js @@ -0,0 +1,127 @@ +const { createCache } = require('cache-manager'); +const jsonStore = require('./json-store'); +const { clean, year, second } = require('./utils'); + +const TTL_SECONDS = (year / second); // Store programmer status for 1 year +const targetPath = './data/friends/programmer-status.json'; +let cache = null; + +async function getCache() { + if (cache) { + return cache; + } + console.log('Initializing programmer status cache with jsonStore'); + const store = await jsonStore({ filePath: targetPath }); + + cache = createCache({ + stores: [store], + }); + + return cache; +} + +/** + * Set programmer status for a friend + * @param {number} friendId - Friend ID + * @param {Object} status - Status object with properties: + * - isProgrammer: boolean + * - confidence: number (0-100) + * - method: string ('auto-detected', 'user-confirmed', 'user-denied') + * - checkedAt: Date + * - askedAt: Date (optional) + * - indicators: Array (optional) + */ +async function setProgrammerStatus(friendId, status) { + console.log(`Setting programmer status for friendId ${friendId}:`, status); + const cleanedStatus = clean({ + ...status, + updatedAt: new Date().toISOString(), + }); + const cacheInstance = await getCache(); + await cacheInstance.set(String(friendId), cleanedStatus, { ttl: TTL_SECONDS }); + console.log(`Programmer status set for friendId ${friendId}`); + return cleanedStatus; +} + +/** + * Get programmer status for a friend + * @param {number} friendId - Friend ID + * @returns {Object|null} Status object or null if not found + */ +async function getProgrammerStatus(friendId) { + const cacheInstance = await getCache(); + const status = await cacheInstance.get(String(friendId)); + return status || null; +} + +/** + * Get all programmer statuses + * @returns {Object} Object with friendId as keys and status as values + */ +async function getAllProgrammerStatuses() { + const cacheInstance = await getCache(); + const store = cacheInstance.store; + + // Access the underlying store data + if (store && store.data) { + return store.data; + } + + return {}; +} + +/** + * Mark that a friend has been asked about being a programmer + * @param {number} friendId - Friend ID + */ +async function markAsAsked(friendId) { + const existingStatus = await getProgrammerStatus(friendId); + const status = { + ...(existingStatus || { isProgrammer: null, confidence: 0 }), + askedAt: new Date().toISOString(), + }; + return await setProgrammerStatus(friendId, status); +} + +/** + * Check if a friend has been asked about being a programmer + * @param {number} friendId - Friend ID + * @returns {boolean} + */ +async function hasBeenAsked(friendId) { + const status = await getProgrammerStatus(friendId); + return status?.askedAt != null; +} + +/** + * Check if a friend needs to be checked + * Returns true if: + * - No status exists + * - Status exists but isProgrammer is null and hasn't been asked + * @param {number} friendId - Friend ID + * @returns {boolean} + */ +async function needsCheck(friendId) { + const status = await getProgrammerStatus(friendId); + + // No status exists + if (!status) { + return true; + } + + // Status exists but isProgrammer is null and hasn't been asked + if (status.isProgrammer === null && !status.askedAt) { + return true; + } + + return false; +} + +module.exports = { + setProgrammerStatus, + getProgrammerStatus, + getAllProgrammerStatuses, + markAsAsked, + hasBeenAsked, + needsCheck, +}; diff --git a/triggers/delete-deactivated-friends.js b/triggers/delete-deactivated-friends.js index d1c9eec..0eeb2e9 100644 --- a/triggers/delete-deactivated-friends.js +++ b/triggers/delete-deactivated-friends.js @@ -1,4 +1,5 @@ const { getAllFriends } = require('../friends-cache'); +const { getProgrammerStatus } = require('../programmer-status-cache'); const { sleep, priorityFriendIds, second, ms } = require('../utils'); async function deleteDeactivatedFriends({ vk }) { @@ -13,6 +14,13 @@ async function deleteDeactivatedFriends({ vk }) { console.log(`Skipping deletion of deactivated friend ${friend.id} because it is in priority friends list.`); continue; } + + // Check if friend is a confirmed programmer (safe list) + const programmerStatus = await getProgrammerStatus(friend.id); + if (programmerStatus?.isProgrammer === true) { + console.log(`Skipping deletion of deactivated friend ${friend.id} because they are a confirmed programmer (safe list).`); + continue; + } try { deletedFriendsIds.push(friend.id); await vk.api.friends.delete({ user_id: friend.id }); diff --git a/triggers/filter-programmers.js b/triggers/filter-programmers.js new file mode 100644 index 0000000..916ae60 --- /dev/null +++ b/triggers/filter-programmers.js @@ -0,0 +1,165 @@ +const { getAllFriends } = require('../friends-cache'); +const { getOrLoadMessages } = require('../messages-cache'); +const { detectProgrammer } = require('../detect-programmer'); +const { + setProgrammerStatus, + getProgrammerStatus, + needsCheck, + markAsAsked, +} = require('../programmer-status-cache'); +const { enqueueMessage } = require('../outgoing-messages'); +const { sleep, priorityFriendIds, second, ms } = require('../utils'); + +// Maximum number of friends to check per run +const MAX_FRIENDS_TO_CHECK_PER_RUN = 5; + +// Message to ask if they're a programmer (in Russian) +const PROGRAMMER_QUESTION = 'Привет! Подскажи, пожалуйста, ты программист? Это поможет мне лучше организовать список друзей.'; + +// Alternative question in English +const PROGRAMMER_QUESTION_EN = 'Hi! Could you please tell me, are you a programmer? This will help me better organize my friends list.'; + +async function filterProgrammers({ vk, options = {} }) { + try { + const maxFriendsToCheck = options.maxFriendsToCheck || MAX_FRIENDS_TO_CHECK_PER_RUN; + + console.log('Starting programmer filtering process...'); + + // Get all friends + const allFriends = await getAllFriends({ context: { vk } }); + console.log(`Total friends: ${allFriends.length}`); + + // Filter friends that need to be checked + const friendsToCheck = []; + for (const friend of allFriends) { + // Skip priority friends - they're always safe + if (priorityFriendIds.includes(friend.id)) { + console.log(`Skipping priority friend ${friend.id}`); + continue; + } + + // Skip deactivated friends + if (friend.deactivated) { + console.log(`Skipping deactivated friend ${friend.id}`); + continue; + } + + // Check if friend needs to be checked + if (await needsCheck(friend.id)) { + friendsToCheck.push(friend); + } + + // Limit the number of friends to check + if (friendsToCheck.length >= maxFriendsToCheck) { + break; + } + } + + console.log(`Friends to check in this run: ${friendsToCheck.length}`); + + if (friendsToCheck.length === 0) { + console.log('No friends need to be checked at this time.'); + return; + } + + // Process each friend + for (const friend of friendsToCheck) { + try { + console.log(`Checking friend ${friend.id}...`); + + // Load message history + const messages = await getOrLoadMessages({ context: { vk }, friendId: friend.id }); + console.log(`Loaded ${messages?.length || 0} messages for friend ${friend.id}`); + + if (!messages || messages.length === 0) { + console.log(`No messages found for friend ${friend.id}, will need to ask directly.`); + + // Ask directly since there's no history + await askIfProgrammer(vk, friend); + await markAsAsked(friend.id); + + // Sleep to avoid rate limiting + await sleep((10 * second) / ms); + continue; + } + + // Detect if programmer based on messages + const detection = detectProgrammer(messages); + console.log(`Detection result for friend ${friend.id}:`, { + isProgrammer: detection.isProgrammer, + confidence: detection.confidence, + stats: detection.stats, + }); + + if (detection.isProgrammer) { + // Mark as programmer + await setProgrammerStatus(friend.id, { + isProgrammer: true, + confidence: detection.confidence, + method: 'auto-detected', + checkedAt: new Date().toISOString(), + indicators: detection.indicators, + }); + console.log(`Friend ${friend.id} identified as programmer (confidence: ${detection.confidence}%)`); + } else { + // No clear indication - ask directly + console.log(`No clear indication for friend ${friend.id}, asking directly...`); + await askIfProgrammer(vk, friend); + await markAsAsked(friend.id); + } + + // Sleep to avoid rate limiting + await sleep((10 * second) / ms); + + } catch (error) { + console.error(`Error processing friend ${friend.id}:`, error); + // Continue with next friend + } + } + + console.log('Programmer filtering process completed.'); + + } catch (error) { + console.error('Error in filterProgrammers trigger:', error); + } +} + +/** + * Send a message asking if the person is a programmer + * @param {Object} vk - VK API instance + * @param {Object} friend - Friend object + */ +async function askIfProgrammer(vk, friend) { + try { + // Determine language based on friend's language field if available + const message = PROGRAMMER_QUESTION; // Default to Russian + + console.log(`Asking friend ${friend.id} if they're a programmer...`); + + // Enqueue the message + enqueueMessage({ + vk, + request: { peerId: friend.id }, + response: { message }, + }); + + console.log(`Question enqueued for friend ${friend.id}`); + } catch (error) { + console.error(`Error asking friend ${friend.id} if they're a programmer:`, error); + } +} + +const trigger = { + name: "FilterProgrammers", + action: async (context) => { + return await filterProgrammers(context); + } +}; + +module.exports = { + trigger, + filterProgrammers, + askIfProgrammer, + PROGRAMMER_QUESTION, + PROGRAMMER_QUESTION_EN, +};