diff --git a/__tests__/triggers/remove-inactive-friends.js b/__tests__/triggers/remove-inactive-friends.js new file mode 100644 index 0000000..02f1198 --- /dev/null +++ b/__tests__/triggers/remove-inactive-friends.js @@ -0,0 +1,187 @@ +const { trigger: removeInactiveFriendsTrigger } = require('../../triggers/remove-inactive-friends'); +const { getAllFriends, loadAllFriends } = require('../../friends-cache'); +const { priorityFriendIds } = require('../../utils'); + +jest.mock('../../friends-cache'); +jest.mock('../../utils', () => ({ + ...jest.requireActual('../../utils'), + sleep: jest.fn(() => Promise.resolve()), +})); + +const triggerDescription = 'remove inactive friends trigger'; + +describe(triggerDescription, () => { + let mockVk; + + beforeEach(() => { + jest.clearAllMocks(); + mockVk = { + api: { + friends: { + getRequests: jest.fn(), + delete: jest.fn(), + }, + }, + }; + }); + + test('should not remove friends when there are no incoming requests', async () => { + mockVk.api.friends.getRequests.mockResolvedValue({ count: 0, items: [] }); + + await removeInactiveFriendsTrigger.action({ vk: mockVk }); + + expect(mockVk.api.friends.getRequests).toHaveBeenCalledWith({ count: 1, out: 0 }); + expect(mockVk.api.friends.delete).not.toHaveBeenCalled(); + }); + + test('should not remove friends when current count is below maximum', async () => { + mockVk.api.friends.getRequests.mockResolvedValue({ count: 5, items: [1, 2, 3, 4, 5] }); + getAllFriends.mockResolvedValue([ + { id: 1000, last_seen: { time: 1000000000 } }, + { id: 1001, last_seen: { time: 1000000001 } }, + ]); + + await removeInactiveFriendsTrigger.action({ vk: mockVk }); + + expect(mockVk.api.friends.delete).not.toHaveBeenCalled(); + }); + + test('should remove most inactive friends when at maximum capacity', async () => { + const friends = []; + for (let i = 0; i < 10000; i++) { + friends.push({ + id: 10000 + i, + last_seen: { time: 1000000000 + i }, + online: 0, + }); + } + + mockVk.api.friends.getRequests.mockResolvedValue({ count: 3, items: [1, 2, 3] }); + getAllFriends.mockResolvedValue(friends); + loadAllFriends.mockResolvedValue(friends.slice(3)); + mockVk.api.friends.delete.mockResolvedValue({ success: 1 }); + + await removeInactiveFriendsTrigger.action({ vk: mockVk }); + + expect(mockVk.api.friends.delete).toHaveBeenCalledTimes(3); + expect(mockVk.api.friends.delete).toHaveBeenCalledWith({ user_id: 10000 }); // Most inactive + expect(mockVk.api.friends.delete).toHaveBeenCalledWith({ user_id: 10001 }); + expect(mockVk.api.friends.delete).toHaveBeenCalledWith({ user_id: 10002 }); + expect(loadAllFriends).toHaveBeenCalled(); + }); + + test('should not remove priority friends', async () => { + const friends = [ + { id: priorityFriendIds[0], last_seen: { time: 1000000000 }, online: 0 }, // Priority friend, very inactive + { id: 10001, last_seen: { time: 1000000001 }, online: 0 }, + { id: 10002, last_seen: { time: 1000000002 }, online: 0 }, + ]; + + // Add more friends to reach 10000 + for (let i = 3; i < 10000; i++) { + friends.push({ + id: 10000 + i, + last_seen: { time: 1000000000 + i }, + online: 0, + }); + } + + mockVk.api.friends.getRequests.mockResolvedValue({ count: 2, items: [1, 2] }); + getAllFriends.mockResolvedValue(friends); + loadAllFriends.mockResolvedValue(friends.slice(2)); + mockVk.api.friends.delete.mockResolvedValue({ success: 1 }); + + await removeInactiveFriendsTrigger.action({ vk: mockVk }); + + expect(mockVk.api.friends.delete).toHaveBeenCalledTimes(2); + expect(mockVk.api.friends.delete).not.toHaveBeenCalledWith({ user_id: priorityFriendIds[0] }); + expect(mockVk.api.friends.delete).toHaveBeenCalledWith({ user_id: 10001 }); // Most inactive non-priority + expect(mockVk.api.friends.delete).toHaveBeenCalledWith({ user_id: 10002 }); + }); + + test('should not remove deactivated friends', async () => { + const friends = [ + { id: 10000, deactivated: 'deleted', last_seen: { time: 1000000000 } }, // Deactivated, very inactive + { id: 10001, last_seen: { time: 1000000001 }, online: 0 }, + { id: 10002, last_seen: { time: 1000000002 }, online: 0 }, + ]; + + // Add more friends to reach 10000 + for (let i = 3; i < 10000; i++) { + friends.push({ + id: 10000 + i, + last_seen: { time: 1000000000 + i }, + online: 0, + }); + } + + mockVk.api.friends.getRequests.mockResolvedValue({ count: 2, items: [1, 2] }); + getAllFriends.mockResolvedValue(friends); + loadAllFriends.mockResolvedValue(friends.slice(2)); + mockVk.api.friends.delete.mockResolvedValue({ success: 1 }); + + await removeInactiveFriendsTrigger.action({ vk: mockVk }); + + expect(mockVk.api.friends.delete).toHaveBeenCalledTimes(2); + expect(mockVk.api.friends.delete).not.toHaveBeenCalledWith({ user_id: 10000 }); // Deactivated + expect(mockVk.api.friends.delete).toHaveBeenCalledWith({ user_id: 10001 }); // Most inactive non-deactivated + expect(mockVk.api.friends.delete).toHaveBeenCalledWith({ user_id: 10002 }); + }); + + test('should prioritize removing friends without last_seen data', async () => { + const friends = [ + { id: 10000, online: 0 }, // No last_seen data + { id: 10001, last_seen: { time: 1000000001 }, online: 0 }, + { id: 10002, last_seen: { time: 1000000002 }, online: 0 }, + ]; + + // Add more friends to reach 10000 + for (let i = 3; i < 10000; i++) { + friends.push({ + id: 10000 + i, + last_seen: { time: 1000000000 + i }, + online: 0, + }); + } + + mockVk.api.friends.getRequests.mockResolvedValue({ count: 2, items: [1, 2] }); + getAllFriends.mockResolvedValue(friends); + loadAllFriends.mockResolvedValue(friends.slice(2)); + mockVk.api.friends.delete.mockResolvedValue({ success: 1 }); + + await removeInactiveFriendsTrigger.action({ vk: mockVk }); + + expect(mockVk.api.friends.delete).toHaveBeenCalledTimes(2); + expect(mockVk.api.friends.delete).toHaveBeenCalledWith({ user_id: 10000 }); // No last_seen + expect(mockVk.api.friends.delete).toHaveBeenCalledWith({ user_id: 10001 }); // Oldest last_seen + }); + + test('should prioritize removing offline friends over online friends', async () => { + const friends = [ + { id: 10000, last_seen: { time: 1000000000 }, online: 0 }, // Offline + { id: 10001, last_seen: { time: 999999999 }, online: 1 }, // Online but older last_seen + { id: 10002, last_seen: { time: 1000000001 }, online: 0 }, // Offline + ]; + + // Add more friends to reach 10000 + for (let i = 3; i < 10000; i++) { + friends.push({ + id: 10000 + i, + last_seen: { time: 1000000000 + i }, + online: 0, + }); + } + + mockVk.api.friends.getRequests.mockResolvedValue({ count: 2, items: [1, 2] }); + getAllFriends.mockResolvedValue(friends); + loadAllFriends.mockResolvedValue(friends.slice(2)); + mockVk.api.friends.delete.mockResolvedValue({ success: 1 }); + + await removeInactiveFriendsTrigger.action({ vk: mockVk }); + + expect(mockVk.api.friends.delete).toHaveBeenCalledTimes(2); + expect(mockVk.api.friends.delete).toHaveBeenCalledWith({ user_id: 10000 }); // Offline + expect(mockVk.api.friends.delete).toHaveBeenCalledWith({ user_id: 10002 }); // Offline + expect(mockVk.api.friends.delete).not.toHaveBeenCalledWith({ user_id: 10001 }); // Online, should be preserved + }); +}); diff --git a/experiments/test-remove-inactive-friends.js b/experiments/test-remove-inactive-friends.js new file mode 100644 index 0000000..7d879b3 --- /dev/null +++ b/experiments/test-remove-inactive-friends.js @@ -0,0 +1,27 @@ +const { VK } = require('vk-io'); +const { getToken } = require('../utils'); +const { trigger: removeInactiveFriendsTrigger } = require('../triggers/remove-inactive-friends'); + +const token = getToken(); +const vk = new VK({ token }); + +(async () => { + console.log('Testing remove-inactive-friends trigger...'); + + try { + // Check current friends count + const allFriendsResponse = await vk.api.friends.get({ count: 1 }); + console.log(`Current friends count: ${allFriendsResponse.count}`); + + // Check incoming friend requests + const requestsResponse = await vk.api.friends.getRequests({ count: 1, out: 0 }); + console.log(`Incoming friend requests count: ${requestsResponse.count}`); + + // Run the trigger + await removeInactiveFriendsTrigger.action({ vk }); + + console.log('Trigger execution completed successfully.'); + } catch (error) { + console.error('Error during trigger execution:', error); + } +})(); diff --git a/index.js b/index.js index 4226830..58dc2fd 100644 --- a/index.js +++ b/index.js @@ -86,6 +86,11 @@ const setOnlineStatusInterval = setInterval(async () => { await executeTrigger(setOnlineStatusTrigger, { vk }); }, (14 * minute) / ms); +const { trigger: removeInactiveFriendsTrigger } = require('./triggers/remove-inactive-friends'); +const removeInactiveFriendsInterval = setInterval(async () => { + await executeTrigger(removeInactiveFriendsTrigger, { vk }); +}, (18 * minute) / ms); + const { trigger: acceptFriendRequestsTrigger } = require('./triggers/accept-friend-requests'); const acceptFriendRequestsInterval = setInterval(async () => { await executeTrigger(acceptFriendRequestsTrigger, { vk }); diff --git a/triggers/remove-inactive-friends.js b/triggers/remove-inactive-friends.js new file mode 100644 index 0000000..387718b --- /dev/null +++ b/triggers/remove-inactive-friends.js @@ -0,0 +1,108 @@ +const { getAllFriends, loadAllFriends } = require('../friends-cache'); +const { sleep, priorityFriendIds, second, minute, ms } = require('../utils'); + +const maxFriends = 10000; + +async function removeInactiveFriends({ vk }) { + try { + // Get incoming friend requests count + const requests = await vk.api.friends.getRequests({ count: 1, out: 0 }); + await sleep((2 * second) / ms); + + const incomingRequestsCount = requests?.count || 0; + if (incomingRequestsCount === 0) { + console.log('No incoming friend requests. No need to remove inactive friends.'); + return; + } + + console.log(`Found ${incomingRequestsCount} incoming friend requests.`); + + // Get all current friends + const allFriends = await getAllFriends({ context: { vk } }); + const currentFriendsCount = allFriends.length; + + console.log(`Current friends count: ${currentFriendsCount}`); + + // Check if we're at or near the limit + if (currentFriendsCount < maxFriends) { + console.log(`Friends count (${currentFriendsCount}) is below maximum (${maxFriends}). No need to remove inactive friends.`); + return; + } + + // Calculate how many friends we need to remove + const friendsToRemoveCount = Math.min(incomingRequestsCount, currentFriendsCount - maxFriends + incomingRequestsCount); + + if (friendsToRemoveCount <= 0) { + console.log('No friends need to be removed.'); + return; + } + + console.log(`Need to remove ${friendsToRemoveCount} inactive friends to make room for incoming requests.`); + + // Filter out priority friends and deactivated friends + const removableFriends = allFriends.filter(friend => + !priorityFriendIds.includes(friend.id) && + !friend.deactivated + ); + + // Sort friends by activity (last_seen timestamp) + // Friends without last_seen or who are offline for the longest time come first + const sortedByInactivity = removableFriends.sort((a, b) => { + // If friend doesn't have last_seen data, consider them most inactive + if (!a.last_seen && !b.last_seen) return 0; + if (!a.last_seen) return -1; + if (!b.last_seen) return 1; + + // If one is online (online === 1), they should be sorted last (more active) + if (a.online && !b.online) return 1; + if (!a.online && b.online) return -1; + + // Otherwise sort by last_seen timestamp (earlier timestamp = more inactive) + const aTime = a.last_seen?.time || 0; + const bTime = b.last_seen?.time || 0; + return aTime - bTime; + }); + + // Get the friends to remove (most inactive ones) + const friendsToRemove = sortedByInactivity.slice(0, friendsToRemoveCount); + + if (friendsToRemove.length === 0) { + console.log('No removable friends found (all friends are either priority or deactivated).'); + return; + } + + console.log(`Removing ${friendsToRemove.length} most inactive friends...`); + + let removedCount = 0; + for (const friend of friendsToRemove) { + try { + await vk.api.friends.delete({ user_id: friend.id }); + removedCount++; + console.log(`Removed inactive friend ${friend.id}. Last seen: ${friend.last_seen?.time ? new Date(friend.last_seen.time * 1000).toISOString() : 'never'}`); + } catch (error) { + console.error(`Failed to remove friend ${friend.id}:`, error); + } + await sleep((5 * second) / ms); + } + + console.log(`Successfully removed ${removedCount} inactive friends to make room for ${incomingRequestsCount} incoming friend requests.`); + + // Reload friends cache after removals + if (removedCount > 0) { + await loadAllFriends({ context: { vk } }); + } + } catch (error) { + console.error('Could not remove inactive friends:', error); + } +} + +const trigger = { + name: "RemoveInactiveFriends", + action: async (context) => { + return await removeInactiveFriends(context); + } +}; + +module.exports = { + trigger +};