您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Find all duels played against specific users (single or CSV batch)
// ==UserScript== // @name GeoGuessr Duels Finder 1.2 // @namespace http://tampermonkey.net/ // @version 1.2 // @description Find all duels played against specific users (single or CSV batch) // @author Flykii (@flykii), Att (attx_) and Tweek (@member0001) // @match https://www.geoguessr.com/* // @grant none // @license Proprietary Source Available // ==/UserScript== (function() { 'use strict'; class GeoGuessrDuelFinder { constructor() { this.baseUrl = 'https://www.geoguessr.com/api'; this.gameServerUrl = 'https://game-server.geoguessr.com/api'; this.myUserId = null; this.duelsFound = []; this.processedGameIds = new Set(); } async apiRequest(url, options = {}) { const requestOptions = { ...options, credentials: 'include', headers: { 'Accept': 'application/json', 'Content-Type': 'application/json', ...options.headers } }; const response = await fetch(url, requestOptions); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } return await response.json(); } async getMyProfile() { if (!this.myUserId) { const profile = await this.apiRequest(`${this.baseUrl}/v3/profiles`); this.myUserId = profile.id; } return this.myUserId; } async getActivities(count = 50, paginationToken = null) { let url = `${this.baseUrl}/v4/feed/private?count=${count}`; if (paginationToken) { url += `&paginationToken=${paginationToken}`; } const response = await this.apiRequest(url); let entries = []; if (Array.isArray(response)) { entries = response; } else if (response.entries && Array.isArray(response.entries)) { entries = response.entries; } else if (response.data && Array.isArray(response.data)) { entries = response.data; } return { entries: entries, paginationToken: response.paginationToken || null }; } async getDuelDetails(gameId) { return await this.apiRequest(`${this.gameServerUrl}/duels/${gameId}`); } extractGameIds(activity) { const gameIds = []; if (!activity.payload) return gameIds; try { const payload = JSON.parse(activity.payload); if (Array.isArray(payload)) { payload.forEach(event => { if (event.payload && event.payload.gameId) { const gameMode = event.payload.gameMode; if (gameMode === 'Duels' || gameMode === 'TeamDuels') { gameIds.push({ gameId: event.payload.gameId, gameMode: gameMode, time: event.time || activity.time }); } } }); } else if (payload.gameId) { const gameMode = payload.gameMode; if (gameMode === 'Duels' || gameMode === 'TeamDuels') { gameIds.push({ gameId: payload.gameId, gameMode: gameMode, time: payload.time || activity.time }); } } } catch (error) { console.error('Error parsing payload:', error); } return gameIds; } async checkUserInDuel(gameId, targetUserIds, includeParty = false, includeTeamDuels = false, includeNormalDuels = true) { if (this.processedGameIds.has(gameId)) { return false; } this.processedGameIds.add(gameId); try { const duelData = await this.getDuelDetails(gameId); if (!duelData.teams || !Array.isArray(duelData.teams)) { return false; } console.log(`Checking game ${gameId}:`, { teams: duelData.teams?.map(team => ({ name: team.name, playerCount: team.players?.length })), options: duelData.options, includeTeamDuels, includeParty, includeNormalDuels }); const isTeamDuel = duelData.teams?.some(team => team.players && team.players.length > 1); if (isTeamDuel && !includeTeamDuels) { console.log(`❌ Skipping Team Duel: ${gameId} (detected ${duelData.teams.map(t => t.players?.length).join(' vs ')} players)`); return false; } const isPartyGame = this.detectPartyGame(duelData); if (isPartyGame && !includeParty) { console.log(`❌ Skipping Party game: ${gameId} (party detection: ${isPartyGame})`); return false; } const isNormalDuel = !isTeamDuel && !isPartyGame; if (isNormalDuel && !includeNormalDuels) { console.log(`❌ Skipping Normal Duel: ${gameId} (normal duels excluded)`); return false; } let foundUsers = []; for (const team of duelData.teams) { if (team.players && Array.isArray(team.players)) { for (const player of team.players) { if (targetUserIds.includes(player.playerId)) { foundUsers.push(player.playerId); } } } } if (foundUsers.length > 0) { const gameType = this.determineGameType(duelData); console.log(`✅ Found valid duel: ${gameId} (Type: ${gameType})`); return { found: true, duelData: duelData, gameId: gameId, gameLink: `https://www.geoguessr.com/duels/${gameId}/summary`, foundUsers: foundUsers, gameType: gameType }; } console.log(`⚪ No target users found in duel: ${gameId}`); return false; } catch (error) { console.error(`Error checking duel ${gameId}:`, error); return false; } } detectPartyGame(duelData) { if (duelData.options) { const partyIndicators = [ 'partyMode', 'isParty', 'gameMode', 'lobbyId' ]; for (let indicator of partyIndicators) { if (duelData.options[indicator]) { return `options.${indicator}: ${duelData.options[indicator]}`; } } } if (duelData.lobbyId || duelData.partyId) { return `lobbyId/partyId detected`; } return false; } determineGameType(duelData) { const isTeamDuel = duelData.teams?.some(team => team.players && team.players.length > 1); if (isTeamDuel) { const teamSizes = duelData.teams.map(t => t.players?.length || 0); return `Team Duel (${teamSizes.join('v')})`; } const partyDetection = this.detectPartyGame(duelData); if (partyDetection) { return `Party (${partyDetection})`; } return 'Normal Duel'; } async findDuelsAgainstUsers(targetUserIds, maxPages = 20, progressCallback = null, includeParty = false, includeTeamDuels = false, includeNormalDuels = true) { await this.getMyProfile(); this.duelsFound = []; this.processedGameIds.clear(); let currentPage = 0; let consecutiveEmptyPages = 0; let totalActivities = 0; let totalDuelsChecked = 0; let paginationToken = null; console.log(`Starting search with filters: includeParty=${includeParty}, includeTeamDuels=${includeTeamDuels}, includeNormalDuels=${includeNormalDuels}`); while (currentPage < maxPages && consecutiveEmptyPages < 3) { if (progressCallback) { progressCallback(`Processing page ${currentPage + 1}/${maxPages}...`); } try { const result = await this.getActivities(50, paginationToken); const activities = result.entries; paginationToken = result.paginationToken; totalActivities += activities.length; if (!activities || activities.length === 0) { consecutiveEmptyPages++; if (consecutiveEmptyPages >= 3) break; currentPage++; continue; } if (!paginationToken) { if (progressCallback) { progressCallback(`Reached end of activities (no more pages)`); } } consecutiveEmptyPages = 0; const allGameIds = []; for (const activity of activities) { const gameIds = this.extractGameIds(activity); allGameIds.push(...gameIds.map(g => ({...g, activity}))); } const batchSize = 5; for (let i = 0; i < allGameIds.length; i += batchSize) { const batch = allGameIds.slice(i, i + batchSize); const promises = batch.map(gameInfo => this.checkUserInDuel(gameInfo.gameId, targetUserIds, includeParty, includeTeamDuels, includeNormalDuels) ); const results = await Promise.allSettled(promises); for (let j = 0; j < results.length; j++) { const result = results[j]; const gameInfo = batch[j]; if (result.status === 'fulfilled' && result.value && result.value.found) { totalDuelsChecked++; const duelData = { gameId: gameInfo.gameId, gameMode: gameInfo.gameMode, time: gameInfo.time, activity: gameInfo.activity, duelDetails: result.value.duelData, foundUsers: result.value.foundUsers, gameLink: result.value.gameLink, gameType: result.value.gameType }; this.duelsFound.push(duelData); if (progressCallback) { progressCallback(`Found ${this.duelsFound.length} duel(s) so far...`); } } } await new Promise(resolve => setTimeout(resolve, 100)); } currentPage++; if (!paginationToken) { break; } await new Promise(resolve => setTimeout(resolve, 200)); } catch (error) { console.error(`Error on page ${currentPage}:`, error); break; } } return this.duelsFound; } parseCsv(csvText, filterType = 'all') { const lines = csvText.split('\n'); const allUserIds = []; const allUserInfo = {}; const filteredUserIds = []; for (let i = 1; i < lines.length; i++) { const line = lines[i].trim(); if (!line) continue; const columns = this.parseCsvLine(line); if (columns.length >= 3) { const username = columns[1]; const userId = columns[2]; const actionType = columns[7] || ''; if (userId && userId.length > 10) { allUserIds.push(userId); allUserInfo[userId] = { username: username, date: columns[0], profileUrl: columns[3] || '', countryCode: columns[4] || '', elo: columns[5] || '', position: columns[6] || '', actionType: actionType, suspendedUntil: columns[8] || '' }; if (filterType === 'all') { filteredUserIds.push(userId); } else if (filterType === 'banned') { if (actionType.toUpperCase().includes('BANNED')) { filteredUserIds.push(userId); } } } } } return { userIds: filteredUserIds, userInfo: allUserInfo, totalUsers: allUserIds.length, filteredCount: filteredUserIds.length }; } parseCsvLine(line) { const result = []; let current = ''; let inQuotes = false; for (let i = 0; i < line.length; i++) { const char = line[i]; if (char === '"') { inQuotes = !inQuotes; } else if (char === ',' && !inQuotes) { result.push(current.trim()); current = ''; } else { current += char; } } result.push(current.trim()); return result; } formatResults(duels, userInfo = {}) { if (duels.length === 0) { return 'No duels found against these users.'; } let result = `Found ${duels.length} duel(s):\n\n`; duels.forEach((duel, index) => { const date = new Date(duel.time).toLocaleString('en-US'); const duelDetails = duel.duelDetails; const state = duelDetails.state || 'N/A'; const rounds = duelDetails.rounds ? duelDetails.rounds.length : 'N/A'; result += `${index + 1}. ${date}\n`; result += ` Mode: ${duel.gameMode}\n`; result += ` Type: ${duel.gameType || 'Unknown'}\n`; result += ` Rounds: ${rounds}\n`; if (duel.foundUsers && duel.foundUsers.length > 0) { result += ` Opponents: `; const opponentNames = duel.foundUsers.map(userId => { if (userInfo[userId]) { return `${userInfo[userId].username} (${userId})`; } return userId; }).join(', '); result += `${opponentNames}\n`; } result += ` ${duel.gameLink}\n\n`; }); return result; } formatLinksOnly(duels) { if (duels.length === 0) { return 'No duels found against these users.'; } return duels.map(duel => duel.gameLink).join('\n'); } async copyToClipboard(text) { try { await navigator.clipboard.writeText(text); return true; } catch (err) { console.error('Clipboard error:', err); return false; } } downloadJSON(data, filename) { const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = filename; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); } } function createUI() { if (document.getElementById('duel-finder-ui')) return; const ui = document.createElement('div'); ui.id = 'duel-finder-ui'; ui.style.cssText = ` position: fixed; top: 20px; right: 20px; width: 50vw; min-width: 500px; max-width: 800px; max-height: 90vh; background: #2c3e50; color: white; padding: 20px; border-radius: 10px; box-shadow: 0 4px 20px rgba(0,0,0,0.3); z-index: 10000; font-family: Arial, sans-serif; font-size: 14px; overflow-y: auto; display: flex; flex-direction: column; `; ui.innerHTML = ` <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px;"> <h3 style="margin: 0; color: #3498db;">Duel Finder</h3> <button id="close-duel-finder" style="background: #e74c3c; color: white; border: none; padding: 5px 10px; border-radius: 5px; cursor: pointer;">×</button> </div> <div style="margin-bottom: 15px;"> <label style="display: block; margin-bottom: 5px;">Search Mode:</label> <select id="search-mode" style="width: 100%; padding: 8px; border: none; border-radius: 5px; background: #34495e; color: white; box-sizing: border-box;"> <option value="single">Single User</option> <option value="csv">CSV File Upload</option> </select> </div> <div id="single-user-section" style="margin-bottom: 15px;"> <label style="display: block; margin-bottom: 5px;">User UUID:</label> <input type="text" id="target-user-id" placeholder="Enter UUID or profile URL" style="width: 100%; padding: 8px; border: none; border-radius: 5px; background: #34495e; color: white; box-sizing: border-box;"> </div> <div id="csv-upload-section" style="margin-bottom: 15px; display: none;"> <label style="display: block; margin-bottom: 5px;">CSV File:</label> <input type="file" id="csv-file" accept=".csv" style="width: 100%; padding: 8px; border: none; border-radius: 5px; background: #34495e; color: white; box-sizing: border-box;"> <div id="csv-info" style="margin-top: 5px; font-size: 12px; color: #bdc3c7;"></div> <label style="display: block; margin-bottom: 5px; margin-top: 10px;">Filter users by status:</label> <select id="user-filter" style="width: 100%; padding: 8px; border: none; border-radius: 5px; background: #34495e; color: white; box-sizing: border-box;"> <option value="all">All users</option> <option value="banned">Only banned users</option> </select> <div id="filter-info" style="margin-top: 5px; font-size: 12px; color: #bdc3c7;"></div> </div> <div style="margin-bottom: 15px;"> <label style="display: block; margin-bottom: 5px;">Game Type Filters:</label> <div style="margin-bottom: 10px;"> <label style="display: flex; align-items: center; margin-bottom: 5px;"> <input type="checkbox" id="include-normal-duels" checked style="margin-right: 8px;"> Include Normal Duels (1v1) </label> <label style="display: flex; align-items: center; margin-bottom: 5px;"> <input type="checkbox" id="include-party" style="margin-right: 8px;"> Include Party games </label> <label style="display: flex; align-items: center;"> <input type="checkbox" id="include-team-duels" style="margin-right: 8px;"> Include Team Duels </label> </div> <div style="font-size: 11px; color: #95a5a6; font-style: italic;"> Uncheck options to exclude those game types from results </div> </div> <div style="margin-bottom: 15px;"> <label style="display: block; margin-bottom: 5px;">Max pages to analyze:</label> <input type="number" id="max-pages" value="5" min="1" max="50" style="width: 100%; padding: 8px; border: none; border-radius: 5px; background: #34495e; color: white; box-sizing: border-box;"> </div> <button id="search-duels" style="width: 100%; padding: 10px; background: #27ae60; color: white; border: none; border-radius: 5px; cursor: pointer; margin-bottom: 10px;"> Search Duels </button> <div id="progress" style="display: none; margin-bottom: 10px; padding: 10px; background: #34495e; border-radius: 5px; font-size: 12px;"></div> <div id="results" style="flex: 1; overflow-y: auto; background: #34495e; padding: 10px; border-radius: 5px; font-size: 12px; white-space: pre-wrap; display: none; min-height: 100px;"></div> <div id="actions" style="display: none; margin-top: 10px; flex-shrink: 0;"> <div style="display: flex; gap: 2%; margin-bottom: 5px;"> <button id="copy-full" style="flex: 1; padding: 8px; background: #3498db; color: white; border: none; border-radius: 5px; cursor: pointer;"> Copy Full </button> <button id="copy-links" style="flex: 1; padding: 8px; background: #f39c12; color: white; border: none; border-radius: 5px; cursor: pointer;"> Copy Links </button> </div> <button id="download-json" style="width: 100%; padding: 8px; background: #9b59b6; color: white; border: none; border-radius: 5px; cursor: pointer;"> Download JSON </button> </div> `; document.body.appendChild(ui); const finder = new GeoGuessrDuelFinder(); let currentResults = []; let currentUserInfo = {}; document.getElementById('search-mode').onchange = (e) => { const mode = e.target.value; const singleSection = document.getElementById('single-user-section'); const csvSection = document.getElementById('csv-upload-section'); if (mode === 'single') { singleSection.style.display = 'block'; csvSection.style.display = 'none'; } else { singleSection.style.display = 'none'; csvSection.style.display = 'block'; } }; document.getElementById('csv-file').onchange = (e) => { const file = e.target.files[0]; if (file) { const reader = new FileReader(); reader.onload = (event) => { const csvText = event.target.result; updateCsvInfo(csvText); }; reader.readAsText(file); } }; document.getElementById('user-filter').onchange = (e) => { const csvFile = document.getElementById('csv-file').files[0]; if (csvFile) { const reader = new FileReader(); reader.onload = (event) => { const csvText = event.target.result; updateCsvInfo(csvText); }; reader.readAsText(csvFile); } }; function updateCsvInfo(csvText) { const filterType = document.getElementById('user-filter').value; const { userIds, userInfo, totalUsers, filteredCount } = finder.parseCsv(csvText, filterType); currentUserInfo = userInfo; const infoDiv = document.getElementById('csv-info'); const filterInfoDiv = document.getElementById('filter-info'); infoDiv.textContent = `Total users in CSV: ${totalUsers}`; if (filterType === 'all') { filterInfoDiv.textContent = `Will search duels against all ${filteredCount} users`; } else { filterInfoDiv.textContent = `Will search duels against ${filteredCount} banned users`; } } document.getElementById('close-duel-finder').onclick = () => { document.body.removeChild(ui); }; document.getElementById('search-duels').onclick = async () => { const searchMode = document.getElementById('search-mode').value; const maxPages = parseInt(document.getElementById('max-pages').value); let targetUserIds = []; if (searchMode === 'single') { const targetUserInput = document.getElementById('target-user-id').value.trim(); if (!targetUserInput) { alert('Please enter a user UUID or profile URL!'); return; } let targetUserId = targetUserInput; if (targetUserInput.includes('/user/')) { targetUserId = targetUserInput.split('/user/')[1].split('?')[0].split('#')[0]; } targetUserIds = [targetUserId]; currentUserInfo = {}; } else { const csvFile = document.getElementById('csv-file').files[0]; if (!csvFile) { alert('Please select a CSV file!'); return; } const reader = new FileReader(); reader.onload = async (event) => { const csvText = event.target.result; const filterType = document.getElementById('user-filter').value; const { userIds, userInfo } = finder.parseCsv(csvText, filterType); targetUserIds = userIds; currentUserInfo = userInfo; if (targetUserIds.length === 0) { if (filterType === 'banned') { alert('No banned users found in CSV file!'); } else { alert('No valid user IDs found in CSV file!'); } return; } await performSearch(targetUserIds, maxPages); }; reader.readAsText(csvFile); return; } await performSearch(targetUserIds, maxPages); }; async function performSearch(targetUserIds, maxPages) { const progressDiv = document.getElementById('progress'); const resultsDiv = document.getElementById('results'); const actionsDiv = document.getElementById('actions'); const searchBtn = document.getElementById('search-duels'); const includeNormalDuels = document.getElementById('include-normal-duels').checked; const includeParty = document.getElementById('include-party').checked; const includeTeamDuels = document.getElementById('include-team-duels').checked; progressDiv.style.display = 'block'; resultsDiv.style.display = 'none'; actionsDiv.style.display = 'none'; searchBtn.disabled = true; searchBtn.textContent = 'Searching...'; try { const duels = await finder.findDuelsAgainstUsers(targetUserIds, maxPages, (message) => { progressDiv.textContent = `${message} (Searching ${targetUserIds.length} users)`; }, includeParty, includeTeamDuels, includeNormalDuels); currentResults = duels; const formattedResults = finder.formatResults(duels, currentUserInfo); resultsDiv.textContent = formattedResults; resultsDiv.style.display = 'block'; actionsDiv.style.display = 'block'; progressDiv.textContent = `Complete! ${duels.length} duel(s) found against ${targetUserIds.length} users`; } catch (error) { console.error('Search error:', error); progressDiv.textContent = `Error: ${error.message}`; resultsDiv.style.display = 'none'; actionsDiv.style.display = 'none'; } searchBtn.disabled = false; searchBtn.textContent = 'Search Duels'; } document.getElementById('copy-full').onclick = async () => { const results = document.getElementById('results').textContent; const success = await finder.copyToClipboard(results); const btn = document.getElementById('copy-full'); const originalText = btn.textContent; btn.textContent = success ? 'Copied!' : 'Error'; setTimeout(() => btn.textContent = originalText, 2000); }; document.getElementById('copy-links').onclick = async () => { const linksOnly = finder.formatLinksOnly(currentResults); const success = await finder.copyToClipboard(linksOnly); const btn = document.getElementById('copy-links'); const originalText = btn.textContent; btn.textContent = success ? 'Copied!' : 'Error'; setTimeout(() => btn.textContent = originalText, 2000); }; document.getElementById('download-json').onclick = () => { const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); const filename = `duels_${timestamp}.json`; const exportData = { searchResults: currentResults, userInfo: currentUserInfo, searchTimestamp: new Date().toISOString(), totalUsers: Object.keys(currentUserInfo).length, totalDuels: currentResults.length }; finder.downloadJSON(exportData, filename); }; document.addEventListener('keydown', (e) => { if (e.key === 'Escape' && document.getElementById('duel-finder-ui')) { document.body.removeChild(ui); } }); } function addTriggerButton() { const button = document.createElement('button'); button.textContent = 'Duel Finder'; button.style.cssText = ` position: fixed; bottom: 20px; right: 20px; background: #3498db; color: white; border: none; padding: 10px 15px; border-radius: 25px; cursor: pointer; z-index: 9999; font-weight: bold; box-shadow: 0 2px 10px rgba(0,0,0,0.2); `; button.onclick = () => { createUI(); }; document.body.appendChild(button); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', addTriggerButton); } else { addTriggerButton(); } })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址