Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
187 changes: 187 additions & 0 deletions __tests__/triggers/remove-inactive-friends.js
Original file line number Diff line number Diff line change
@@ -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
});
});
27 changes: 27 additions & 0 deletions experiments/test-remove-inactive-friends.js
Original file line number Diff line number Diff line change
@@ -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);
}
})();
5 changes: 5 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
Expand Down
108 changes: 108 additions & 0 deletions triggers/remove-inactive-friends.js
Original file line number Diff line number Diff line change
@@ -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
};