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})();