您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Show only pretest result when participating in virtual contest in Codeforces
// ==UserScript== // @name CF-virtual-pretest // @version 0.1.1 // @description Show only pretest result when participating in virtual contest in Codeforces // @match *://codeforces.com/* // @grant GM_getValue // @grant GM_setValue // @run-at document-start // @namespace https://gf.zukizuki.org/users/410786 // ==/UserScript== (function(){ //OK:1, //COMPILATION_ERROR:1, //CRASHED:1, // example: https://codeforces.com/contest/566/submission/42421894 //FAILED:1, // example: https://codeforces.com/contest/566/submission/16877130 /* const wrong_verdicts={ WRONG_ANSWER:1, TIME_LIMIT_EXCEEDED:1, RUNTIME_ERROR:1, MEMORY_LIMIT_EXCEEDED:1, IDLENESS_LIMIT_EXCEEDED:1, 0:0}*/ const pretest_passed_verdicts={ SKIPPED:1, CHALLENGED:1, // aka hacked 0:0} function getPassedTestCount(x){ return x.passedTestCount } //let csrf_token=Codeforces.getCsrfToken() //let csrf_token=document.getElementsByName('X-Csrf-Token')[0].content let csrf_token=undefined // TODO function parseCsrfToken(html){ if(typeof(html)==='string') html=$.parseHTML(html) return html.find(z=>z.name=='X-Csrf-Token').content } let logged_out=false // When the user is taking part in a virtual contest, it // isn't possible to get submission result directly. async function isValid(submissionId){ //return true; // HACK TODO 403 in virtual participation // Return whether a skipped submission has no // wrong answer/run time error/memory limit exceeded/etc. test case, excluding hacks (can // happen when the user cheated in the contest). Example // https://codeforces.com/contest/1221/submission/60879848. /* let data=$.post('//codeforces.com/data/submitSource',{ submissionId: submissionId, csrf_token:Codeforces.getCsrfToken() }) */ console.log('isValid',submissionId) const options={ url: '//codeforces.com/data/submitSource', type:'post', data: {submissionId: submissionId}, headers: {'X-Csrf-Token': csrf_token}, dataType: 'json', } let data try{ console.log('url = ',options) data=await $.ajax(options) }catch(e){ /* if(e.status===403){ const match=location.href.match('://codeforces.com/contest/(\\d+)') const url=match ? '://codeforces.com/contest/'+match[1]+'/my?force_get=1' : undefined document.body.innerHTML=( 'Failed to load. If you are taking part in a virtual contest, please open ' + (match? `<a href="${url}">${url}</a>`: url) + ' in an incognito window, then reload this page.' ) } */ console.log('trying again with new csrf_token | error =',e) options.headers['X-Csrf-Token']=csrf_token=parseCsrfToken(await $.get('/')) console.log('new csrf=',csrf_token) try{ data=await $.ajax(options) }catch(e){ console.log('trying again after logging out | error =',e) options.headers['X-Csrf-Token']=csrf_token=parseCsrfToken( await $.ajax({ type:'get', url:document.querySelector('[href$="logout"]').href, headers:{'X-Csrf-Token': csrf_token}, })) logged_out=true console.log('new csrf=',csrf_token) try{ data=await $.ajax(options) }catch(e){ console.log('??? | error =',e) throw e } } } return Object.keys(data).filter( x=>x.startsWith('verdict#')&&data[x]!='OK' ).length==0 } function pretestCountFetched(contestId){ const key='pretest_count_'+contestId return GM_getValue(key)!==undefined } async function getPretestCount(contestId){ if(searchParams.has('mock_pretest_count')) return new Proxy({}, { get: _=>[10, 20] }) const key='pretest_count_'+contestId { const stored_result=GM_getValue(key) if(stored_result!==undefined) return JSON.parse(stored_result) } console.log('aaa') const data=await (async function(url){ // cache the GET requests for development purposes let data //data=GM_getValue('stored_api_get_'+url) //if(data!==undefined) // return JSON.parse(data) data=await $.get(url) // compress data data.result=data.result.filter(x=>x.author.participantType=="CONTESTANT") console.log('length=',data.result.length) // NOT WORK - always exceed the quota //GM_setValue('stored_api_get_'+url, JSON.stringify(data)) return data })('//codeforces.com/api/contest.status?contestId='+contestId+'&from=1&count=100000000') console.log('bbb') let result={} // {problemIndex /* A/B/C/... */: [minPretestCount, maxPretestCount]} for(const problemIndex of new Set(data.result.map(x=>x.problem.index))){ let problemResult=data.result.filter(x=> x.problem.index==problemIndex&& x.author.participantType=="CONTESTANT" ) let minPretestCount,maxPretestCount s1=problemResult.filter(x=> pretest_passed_verdicts[x.verdict]&&x.passedTestCount!=0 // it's possible for SKIPPED submissions to have 0 tests passed when // the user submits the second solution before the first one is judged ) s1.sort((a,b)=>b.passedTestCount-a.passedTestCount) if(s1.length!=0&&(await isValid(s1[0].id))){ minPretestCount=maxPretestCount=s1[0].passedTestCount }else{ minPretestCount=1+Math.max(... problemResult.filter( // x=>x.testset=="PRETESTS"&&wrong_verdicts[x.verdict] // cannot be "skipped" -> must fail on pretest x=>x.testset=="PRETESTS" ).map(getPassedTestCount) ) maxPretestCount=Math.min(... problemResult.filter( x=>x.testset=="TESTS" ).map(getPassedTestCount) ) } result[problemIndex]=[minPretestCount,maxPretestCount] } GM_setValue(key,JSON.stringify(result)) console.log(result) if(logged_out){ document.body.innerHTML='You are logged out. Please refresh the page.' location.reload() } return result } /* function getContestId(){ return location.pathname.match('^/contest/(\\d+)')[1] } const contestId=getContestId() */ let searchParams=new URL(location).searchParams // always_show, reset_button, mock_pretest_count, force_get let cache={} // problemId -> result let cacheSubmissions={} // submissionId -> item let participantId // assume participantId is fixed /* function get_csrf_token(){ // use Codeforces.getCsrfToken() return csrf_token } */ if(location.href.match('://codeforces.com/contestRegistration/\\d*/virtual/true')){ const contestId=location.href.match('://codeforces.com/contestRegistration/(\\d*)/virtual/true')[1] if(pretestCountFetched(contestId)) return window.addEventListener('load',function(){ if(!confirm('Do you want to prefetch the pretest count of this contest?')){ alert("Note: the pretest will still be fetched inside the contest, and that may log you out. "+ "If you don't want that to happen, you should disable the script.") return } const registerButton=document.querySelector('[value="Register for virtual participation"]') if(registerButton===null){ $.jGrowl('Something unexpected happened. Please wait until "Done" is displayed before registering.') }else{ registerButton.disabled=true registerButton.value="Fetching pretest data..." } getPretestCount(contestId).then(function(){ $.jGrowl('Done! You can register now.') location.reload() }) }) }else if(searchParams.has('force_get')&&location.href.match('://codeforces.com/contest/\\d*/my')){ console.log('force_get') window.addEventListener('load',function(){ const contestId=location.href.match('://codeforces.com/contest/(\\d*)/my')[1] getPretestCount(contestId).then(function(){ let url=new URL(location) url.searchParams.delete('force_get') document.body.innerHTML=`Done. Redirecting to <a href="${url}">${url}</a>...` location.href=url }) }) }else if(searchParams.has('always_show')||location.href.match('://codeforces.com/contest/\\d*/my')){ console.log('start') function restoreAll(){ observer.disconnect() document.querySelectorAll('span').forEach(function(t){ if(t.__oldTextContent!==undefined){ t.textContent=t.__oldTextContent t.className=t.__oldClassName } }) } let button function clickResetButton(){ restoreAll() if(searchParams.has('reset_button')){ document.body.removeChild(button) button=undefined } } function createResetButton(){ if(button===undefined){ if(searchParams.has('reset_button')){ button=document.createElement('button') button.innerHTML='Reset' button.onclick=clickResetButton document.body.appendChild(button) } } } let pendingNodes=[] let getPretestCountRunning=false let pretestCount={} function processPendingNodes(){ let oldPendingNodes=pendingNodes pendingNodes=[] oldPendingNodes.forEach(processSpan) } const loadingText='Loading...' function processSpan(t){ if(t.textContent==='Running') // before any test return console.log('processSpan',t.textContent) let modified=t.textContent===loadingText||t.textContent==='Pretest passed'||t.textContent.includes(' pretest ') if(!modified){ t.__oldTextContent=t.textContent t.__oldClassName=t.className } let contestId,problemIndex { let tableRow=t console.log('tableRow=',tableRow) while(tableRow.tagName!='TR'){ tableRow=tableRow.parentNode console.log('tableRow=',tableRow) if(tableRow===null){ console.log('??? not added to document?') return } } const problemUrl=tableRow.children[3].children[0].href const match=problemUrl.match('/contest/(\\d*)/problem/(.*)$$$')||problemUrl.match('/problemset/problem/(\\d*)/(.*)$$$') // the second format is only used in problemset status page (when always_show is on) contestId=match[1] problemIndex=match[2] } if(pretestCount[contestId]===undefined||pretestCount[contestId]==='running'){ t.textContent=loadingText t.className='' pendingNodes.push(t) if(pretestCount[contestId]!=='running'){ getPretestCount(contestId).then(function(result){ pretestCount[contestId]=result processPendingNodes() // for pages different from contest/my this may cause the span to be push back to pendingNodes list }) pretestCount[contestId]='running' } return } if(['Accepted','Happy New Year!'].includes(t.__oldTextContent)){ createResetButton() t.textContent='Pretest passed' t.className=t.__oldClassName return } t.classList.replace('verdict-accepted','verdict-rejected') if(!( t.__oldClassName.match(/\bverdict-rejected\b/)|| t.__oldClassName.match(/\bverdict-waiting\b/) )) throw new Error try{ let wrongTestIndex=t.__oldTextContent.match(/ on test (\d+)$/)[1] if(wrongTestIndex<=pretestCount[contestId][problemIndex][0]){ t.textContent=t.__oldTextContent.replace('on test','on pretest') t.className=t.__oldClassName // rejected || waiting }else if(wrongTestIndex<=pretestCount[contestId][problemIndex][1]){ t.textContent='???' t.className='' }else{ t.textContent='Pretest passed' t.className='verdict-accepted' } }catch(e){ console.log(t.__oldTextContent,e) } //let problemId=tableRow.children[3].getAttribute('data-problemId') // int-parseable string //let submissionId=parseInt(tableRow.children[0].textContent) //if(participantId===undefined) // participantId=parseInt(tableRow.children[2].getAttribute('data-participantId')) } let observer=new MutationObserver(function(mutations, observer){ for (let r of mutations){ for (let t of r.addedNodes){ // t must be in local scope if(t.tagName==='SPAN'){ // t.classList.contains('contest-state-phase') // 'Contest is running' | 'Finished' if(t.classList.contains('contest-state-regular')&&!t.classList.contains('countdown')){ console.log('contest-state = ',t,t.textContent) if( t.querySelector('.toggle-favourite')===null&& t.textContent!=='Virtual Participation' // | 'Practice' | '???' &&!searchParams.has('always_show') ){ console.log('bad state') clickResetButton() observer.disconnect() return } }else if(t.classList.contains('verdict-accepted')||t.classList.contains('verdict-rejected')||t.classList.contains('verdict-waiting')){ processSpan(t) } }else if(t.tagName==='DIV'){ // jGrowl if(t.classList.contains('jGrowl-notification')){ let z=t.getElementsByClassName('message') if(z.length!==0&& z[0].textContent.match(/^Accepted$| on test \d+$/) ) z[0].textContent='???' } } } } }) observer.observe(document,{ childList:true, subtree:true, attributes:true }); } })() // TODO incomplete (rewrite standings table) // vim: set ts=4 sw=4 fdm=indent: