Chess Player Connection Scanner

Scan chess.com player profiles to find mutual opponents you've both played against

Size

13.4 KB

Version

1.1.1

Created

Nov 9, 2025

Updated

about 1 month ago

1// ==UserScript==
2// @name		Chess Player Connection Scanner
3// @description		Scan chess.com player profiles to find mutual opponents you've both played against
4// @version		1.1.1
5// @match		https://*.chess.com/*
6// @icon		https://www.chess.com/bundles/web/favicons/favicon.46041f2d.ico
7// @grant		GM.getValue
8// @grant		GM.setValue
9// @grant		GM.xmlhttpRequest
10// ==/UserScript==
11(function() {
12    'use strict';
13
14    let scannerActive = false;
15    let myGames = null;
16
17    // Debounce function for MutationObserver
18    function debounce(func, wait) {
19        let timeout;
20        return function executedFunction(...args) {
21            const later = () => {
22                clearTimeout(timeout);
23                func(...args);
24            };
25            clearTimeout(timeout);
26            timeout = setTimeout(later, wait);
27        };
28    }
29
30    // Create floating scan button
31    function createScanButton() {
32        const button = document.createElement('button');
33        button.id = 'chess-scanner-btn';
34        button.textContent = 'SCAN';
35        button.style.cssText = `
36            position: fixed;
37            top: 20px;
38            right: 20px;
39            z-index: 999999;
40            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
41            color: white;
42            border: none;
43            padding: 15px 30px;
44            font-size: 18px;
45            font-weight: bold;
46            border-radius: 50px;
47            cursor: pointer;
48            box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3);
49            transition: all 0.3s ease;
50            font-family: Arial, sans-serif;
51        `;
52
53        button.addEventListener('mouseenter', () => {
54            button.style.transform = 'scale(1.1)';
55            button.style.boxShadow = '0 6px 20px rgba(0, 0, 0, 0.4)';
56        });
57
58        button.addEventListener('mouseleave', () => {
59            button.style.transform = 'scale(1)';
60            button.style.boxShadow = '0 4px 15px rgba(0, 0, 0, 0.3)';
61        });
62
63        button.addEventListener('click', activateScanner);
64        document.body.appendChild(button);
65        console.log('Chess scanner button created');
66    }
67
68    // Show activation message
69    function showMessage(text, duration = 2000) {
70        const message = document.createElement('div');
71        message.textContent = text;
72        message.style.cssText = `
73            position: fixed;
74            top: 50%;
75            left: 50%;
76            transform: translate(-50%, -50%);
77            z-index: 1000000;
78            background: rgba(0, 0, 0, 0.9);
79            color: white;
80            padding: 30px 50px;
81            font-size: 24px;
82            font-weight: bold;
83            border-radius: 15px;
84            box-shadow: 0 8px 30px rgba(0, 0, 0, 0.5);
85            font-family: Arial, sans-serif;
86        `;
87        document.body.appendChild(message);
88
89        setTimeout(() => {
90            message.remove();
91        }, duration);
92    }
93
94    // Turn page green
95    function turnPageGreen() {
96        const overlay = document.createElement('div');
97        overlay.id = 'green-overlay';
98        overlay.style.cssText = `
99            position: fixed;
100            top: 0;
101            left: 0;
102            width: 100%;
103            height: 100%;
104            background: rgba(0, 255, 0, 0.3);
105            z-index: 999998;
106            pointer-events: none;
107            transition: opacity 0.5s ease;
108        `;
109        document.body.appendChild(overlay);
110        console.log('Green overlay added');
111    }
112
113    // Activate scanner
114    async function activateScanner() {
115        if (scannerActive) {
116            showMessage('Scanner already active!');
117            return;
118        }
119
120        scannerActive = true;
121        showMessage('SCANNER ACTIVATED');
122        
123        setTimeout(() => {
124            turnPageGreen();
125        }, 500);
126
127        // Get current user's username
128        const currentUser = await getCurrentUsername();
129        if (!currentUser) {
130            showMessage('Could not detect your username. Please log in.');
131            scannerActive = false;
132            return;
133        }
134
135        console.log('Current user:', currentUser);
136        showMessage('Loading your game history...', 3000);
137
138        // Fetch current user's games
139        myGames = await fetchPlayerGames(currentUser);
140        if (!myGames || myGames.length === 0) {
141            showMessage('Could not load your game history');
142            scannerActive = false;
143            return;
144        }
145
146        console.log(`Loaded ${myGames.length} games for ${currentUser}`);
147        showMessage(`Ready! Click any player to scan (${myGames.length} games loaded)`, 3000);
148
149        // Add click listeners to player links
150        addPlayerClickListeners();
151    }
152
153    // Get current username
154    async function getCurrentUsername() {
155        // Try to get from profile menu
156        const profileLink = document.querySelector('a[href*="/member/"]');
157        if (profileLink) {
158            const match = profileLink.href.match(/\/member\/([^\/]+)/);
159            if (match) return match[1];
160        }
161
162        // Try to get from URL if on profile page
163        const urlMatch = window.location.href.match(/\/member\/([^\/]+)/);
164        if (urlMatch) return urlMatch[1];
165
166        // Try to get from any visible username element
167        const usernameElement = document.querySelector('[class*="username"]');
168        if (usernameElement) {
169            return usernameElement.textContent.trim();
170        }
171
172        return null;
173    }
174
175    // Fetch player games from Chess.com API
176    async function fetchPlayerGames(username) {
177        try {
178            console.log(`Fetching games for ${username}...`);
179            
180            // Get current month's games
181            const now = new Date();
182            const year = now.getFullYear();
183            const month = String(now.getMonth() + 1).padStart(2, '0');
184            
185            const response = await GM.xmlhttpRequest({
186                method: 'GET',
187                url: `https://api.chess.com/pub/player/${username}/games/${year}/${month}`,
188                headers: {
189                    'Accept': 'application/json'
190                }
191            });
192
193            const data = JSON.parse(response.responseText);
194            console.log(`Fetched ${data.games?.length || 0} games`);
195            return data.games || [];
196        } catch (error) {
197            console.error('Error fetching games:', error);
198            return [];
199        }
200    }
201
202    // Extract opponents from games
203    function extractOpponents(games, myUsername) {
204        const opponents = new Set();
205        games.forEach(game => {
206            if (game.white?.username && game.white.username.toLowerCase() !== myUsername.toLowerCase()) {
207                opponents.add(game.white.username.toLowerCase());
208            }
209            if (game.black?.username && game.black.username.toLowerCase() !== myUsername.toLowerCase()) {
210                opponents.add(game.black.username.toLowerCase());
211            }
212        });
213        return Array.from(opponents);
214    }
215
216    // Add click listeners to player links
217    function addPlayerClickListeners() {
218        const debouncedAdd = debounce(() => {
219            const playerLinks = document.querySelectorAll('a[href*="/member/"], a[href*="/players/"], [class*="user-username"], [class*="username"]');
220            
221            playerLinks.forEach(link => {
222                if (!link.dataset.scannerListener) {
223                    link.dataset.scannerListener = 'true';
224                    link.style.cursor = 'crosshair';
225                    
226                    link.addEventListener('click', async (e) => {
227                        if (!scannerActive) return;
228                        
229                        e.preventDefault();
230                        e.stopPropagation();
231                        
232                        let username = null;
233                        
234                        // Try to extract username from href
235                        if (link.href) {
236                            const match = link.href.match(/\/member\/([^\/\?]+)/);
237                            if (match) username = match[1];
238                        }
239                        
240                        // Try to get from text content
241                        if (!username) {
242                            username = link.textContent.trim();
243                        }
244                        
245                        if (username) {
246                            await scanPlayer(username);
247                        }
248                    }, true);
249                }
250            });
251        }, 300);
252
253        debouncedAdd();
254
255        // Watch for new player links
256        const observer = new MutationObserver(debounce(() => {
257            if (scannerActive) {
258                debouncedAdd();
259            }
260        }, 300));
261
262        observer.observe(document.body, {
263            childList: true,
264            subtree: true
265        });
266    }
267
268    // Scan a player's profile
269    async function scanPlayer(username) {
270        showMessage(`Scanning ${username}...`, 2000);
271        console.log(`Scanning player: ${username}`);
272
273        const theirGames = await fetchPlayerGames(username);
274        if (!theirGames || theirGames.length === 0) {
275            showMessage(`No games found for ${username}`);
276            return;
277        }
278
279        console.log(`${username} has ${theirGames.length} games`);
280
281        // Get my opponents
282        const currentUser = await getCurrentUsername();
283        const myOpponents = extractOpponents(myGames, currentUser);
284        
285        // Get their opponents
286        const theirOpponents = extractOpponents(theirGames, username);
287
288        // Find mutual opponents
289        const mutualOpponents = myOpponents.filter(opponent => 
290            theirOpponents.includes(opponent)
291        );
292
293        console.log(`Found ${mutualOpponents.length} mutual opponents`);
294        displayResults(username, mutualOpponents);
295    }
296
297    // Display results
298    function displayResults(playerName, mutualOpponents) {
299        // Remove existing results
300        const existing = document.getElementById('scanner-results');
301        if (existing) existing.remove();
302
303        const resultsDiv = document.createElement('div');
304        resultsDiv.id = 'scanner-results';
305        resultsDiv.style.cssText = `
306            position: fixed;
307            top: 50%;
308            left: 50%;
309            transform: translate(-50%, -50%);
310            z-index: 1000001;
311            background: white;
312            color: black;
313            padding: 30px;
314            border-radius: 15px;
315            box-shadow: 0 10px 40px rgba(0, 0, 0, 0.5);
316            max-width: 500px;
317            max-height: 70vh;
318            overflow-y: auto;
319            font-family: Arial, sans-serif;
320        `;
321
322        const title = document.createElement('h2');
323        title.textContent = `Mutual Opponents with ${playerName}`;
324        title.style.cssText = 'margin: 0 0 20px 0; color: #333; font-size: 22px;';
325
326        const closeBtn = document.createElement('button');
327        closeBtn.textContent = '×';
328        closeBtn.style.cssText = `
329            position: absolute;
330            top: 10px;
331            right: 10px;
332            background: none;
333            border: none;
334            font-size: 30px;
335            cursor: pointer;
336            color: #666;
337            line-height: 1;
338        `;
339        closeBtn.addEventListener('click', () => resultsDiv.remove());
340
341        resultsDiv.appendChild(closeBtn);
342        resultsDiv.appendChild(title);
343
344        if (mutualOpponents.length === 0) {
345            const noResults = document.createElement('p');
346            noResults.textContent = 'No mutual opponents found in recent games.';
347            noResults.style.cssText = 'color: #666; font-size: 16px;';
348            resultsDiv.appendChild(noResults);
349        } else {
350            const count = document.createElement('p');
351            count.textContent = `Found ${mutualOpponents.length} mutual opponent(s):`;
352            count.style.cssText = 'color: #333; font-weight: bold; margin-bottom: 15px;';
353            resultsDiv.appendChild(count);
354
355            const list = document.createElement('ul');
356            list.style.cssText = 'list-style: none; padding: 0; margin: 0;';
357
358            mutualOpponents.forEach(opponent => {
359                const item = document.createElement('li');
360                item.style.cssText = `
361                    padding: 10px;
362                    margin: 5px 0;
363                    background: #f0f0f0;
364                    border-radius: 8px;
365                    font-size: 16px;
366                `;
367
368                const link = document.createElement('a');
369                link.href = `https://www.chess.com/member/${opponent}`;
370                link.textContent = opponent;
371                link.target = '_blank';
372                link.style.cssText = 'color: #769656; text-decoration: none; font-weight: bold;';
373                
374                item.appendChild(link);
375                list.appendChild(item);
376            });
377
378            resultsDiv.appendChild(list);
379        }
380
381        document.body.appendChild(resultsDiv);
382    }
383
384    // Initialize
385    function init() {
386        console.log('Chess Player Connection Scanner initialized');
387        
388        // Wait for body to be ready
389        if (document.body) {
390            createScanButton();
391        } else {
392            const observer = new MutationObserver(() => {
393                if (document.body) {
394                    createScanButton();
395                    observer.disconnect();
396                }
397            });
398            observer.observe(document.documentElement, { childList: true });
399        }
400    }
401
402    // Start the extension
403    init();
404})();
Chess Player Connection Scanner | Robomonkey