diff --git a/manifest-chrome.partial.json b/manifest-chrome.partial.json index 85ef12c..56c9679 100644 --- a/manifest-chrome.partial.json +++ b/manifest-chrome.partial.json @@ -5,6 +5,8 @@ "http://localhost/*", "https://cses.fi/*", "https://codeforces.com/*", + "*://*.codechef.com/*", + "*://*.atcoder.jp/*", "https://maang.in/*" ], "background": { diff --git a/manifest-firefox.partial.json b/manifest-firefox.partial.json index ca092cf..6d3e99d 100644 --- a/manifest-firefox.partial.json +++ b/manifest-firefox.partial.json @@ -8,6 +8,8 @@ "*://localhost/*", "https://cses.fi/*", "*://codeforces.com/*", + "*://*.codechef.com/*", + "*://*.atcoder.jp/*", "*://maang.in/*", "webNavigation" ], diff --git a/readme.md b/readme.md index d38ce65..c08aa55 100644 --- a/readme.md +++ b/readme.md @@ -1,7 +1,8 @@ # ![ICON](icon-48.png) cph-submit -Browser add-on that enables direct submission on Codeforces,CSES and Maang.in -with[Competitive Programming Helper](https://github.com/agrawal-d/cph). +Browser add-on that enables direct submission on Codeforces,CodeChef,AtCoder +CSES and Maang.in with +[Competitive Programming Helper](https://github.com/agrawal-d/cph). Available for Firefox and Google Chrome. diff --git a/src/atcoderInjectedScript.ts b/src/atcoderInjectedScript.ts new file mode 100644 index 0000000..84aed72 --- /dev/null +++ b/src/atcoderInjectedScript.ts @@ -0,0 +1,86 @@ +import { ContentScriptData } from './types'; +import log from './log'; + +declare const ace: any; + +log('cph-submit atcoder script injected'); + +const idToAtcoderLanguage: Record = { + 91: 'C++23 (GCC 15.2.0)', + 89: 'C++ IOI-Style(GNU++20) (GCC 14.2.0)', + 52: 'C++23 (Clang 21.1.0)', + 43: 'C23 (GCC 14.2.0)', + 79: 'C# 13.0 (.NET 9.0.8)', + 28: 'D (DMD 2.111.0)', + 87: 'Java24 (OpenJDK 24.0.2)', + 88: 'Kotlin (Kotlin/JVM 2.2.10)', + 19: 'OCaml (ocamlopt 5.3.0)', + 4: 'Pascal (fpc 3.2.2)', + 13: 'Perl (perl 5.38.2)', + 6: 'PHP (PHP 8.4.12)', + 55: 'JavaScript (Node.js 22.19.0)', + 31: 'Python (CPython 3.13.7)', + 70: 'Python (PyPy 3.11-v7.3.20)', + 67: 'Ruby 3.4 (ruby 3.4.5)', + 32: 'Go (go 1.25.1)', + 98: 'Rust (rustc 1.89.0)', + 20: 'Scala (Dotty 3.7.2)', + 12: 'Haskell (GHC 9.8.4)', +}; + +chrome.runtime.onMessage.addListener(async (message) => { + if (message.type !== 'cph-submit-Atcoder') return; + + const { sourceCode, languageId } = message; + const languageText = idToAtcoderLanguage[languageId]; + + if (!languageText) { + alert('The following language is not supported'); + return; + } + + const selectBox = document.querySelector( + 'select[name="data.LanguageId"]', + ) as HTMLSelectElement; + + if (selectBox) { + const options = Array.from(selectBox.options); + const targetOption = options.find( + (opt) => opt.text.trim() === languageText, + ); + + if (targetOption) { + const atcoderValue = targetOption.value; + selectBox.value = atcoderValue; + selectBox.dispatchEvent(new Event('change', { bubbles: true })); + log( + `Successfully set language to ${languageText} (Value: ${atcoderValue})`, + ); + } else { + log( + `Error: Could not find an option matching the text "${languageText}" in the dropdown.`, + ); + } + } else { + log( + 'Error: Could not find the AtCoder language select box on this page.', + ); + } + await new Promise((r) => setTimeout(r, 1500)); + /* there is no cloudfare verification during live contests*/ + const tokenInput = document.querySelector( + 'input[name="cf-turnstile-response"]', + ) as HTMLInputElement | null; + + if (!tokenInput) { + document.getElementById('submit')?.click(); + return; + } + + const cloudFareVerificationChecker = setInterval(() => { + if (tokenInput && tokenInput.value) { + clearInterval(cloudFareVerificationChecker); + document.getElementById('submit')?.click(); + } + }, 250); +}); diff --git a/src/codeChefInjectedScript.ts b/src/codeChefInjectedScript.ts new file mode 100644 index 0000000..e35c744 --- /dev/null +++ b/src/codeChefInjectedScript.ts @@ -0,0 +1,109 @@ +import { ContentScriptData } from './types'; +import log from './log'; + +declare const ace: any; + +log(`cph-submit Codechef script injected`); + +const idToCodechefLanguage: Record = { + 42: 'C++', + 50: 'C++', + 54: 'C++', + 61: 'C++', + 89: 'C++', + 91: 'C++', + 52: 'C++', + 43: 'C', + 36: 'Java', + 60: 'Java', + 87: 'Java', + 31: 'Python3', + 7: 'Python3', + 40: 'PyPy 3', + 41: 'PyPy 3', + 70: 'PyPy 3', + 9: 'C#', + 65: 'C#', + 79: 'C#', + 34: 'JavaScript', + 55: 'JavaScript', + 32: 'Go', + 6: 'PHP', + 83: 'Kotlin', + 88: 'Kotlin', + 75: 'Rust', + 98: 'Rust', +}; + +const languageMap: Record = { + 'C++': 'C++', + Python3: 'PYTH 3', + C: 'C', + Java: 'JAVA', + 'PyPy 3': 'PYPY3', + 'C#': 'C#', + JavaScript: 'NODEJS', + Go: 'GO', + PHP: 'PHP', + Kotlin: 'KTLN', + Rust: 'rust', +}; + +const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); + +const simulateHumanClick = (element: HTMLElement) => { + ['mouseover', 'mousedown', 'mouseup', 'click'].forEach((eventType) => { + element.dispatchEvent( + new MouseEvent(eventType, { + bubbles: true, + cancelable: true, + view: window, + buttons: 1, + }), + ); + }); +}; + +chrome.runtime.onMessage.addListener((message) => { + if (message.type !== 'cph-submit-codechef') return false; + + const execute = async () => { + try { + const languageName = idToCodechefLanguage[message.languageId]; + if (!languageName) { + alert('Current Language is not supported'); + return; + } + + const languageSelectBtn = document.querySelector( + '#language-select', + ) as HTMLElement | null; + if (languageSelectBtn) { + simulateHumanClick(languageSelectBtn); + await sleep(300); + + const options = Array.from( + document.querySelectorAll('[role="option"]'), + ) as HTMLElement[]; + const targetOption = options.find((opt) => { + const val = opt.getAttribute('data-value'); + return ( + val === languageMap[languageName] || + opt.textContent?.trim() === languageName + ); + }); + + if (targetOption) { + simulateHumanClick(targetOption); + await sleep(400); + } + } + } catch (error) { + log(error); + } + await new Promise((r) => setTimeout(r, 1500)); + document.getElementById('submit_btn')?.click(); + }; + + execute(); +}); diff --git a/src/handleSubmit.ts b/src/handleSubmit.ts index 81685a0..224f6f2 100644 --- a/src/handleSubmit.ts +++ b/src/handleSubmit.ts @@ -2,6 +2,7 @@ import config from './config'; import log from './log'; declare const browser: any; +declare const ace: any; //Ace Editor if (typeof browser !== 'undefined') { self.chrome = browser; @@ -100,6 +101,22 @@ export const isCSESProblem = (problemUrl: string) => { return false; } }; +export const isCodeChefProblem = (problemUrl: string) => { + try { + const url = new URL(problemUrl); + return url.hostname.endsWith('codechef.com'); + } catch { + return false; + } +}; +export const isAtcoderProblem = (problemUrl: string) => { + try { + const url = new URL(problemUrl); + return url.hostname.endsWith('atcoder.jp'); + } catch { + return false; + } +}; export const handleAlgoZenithSubmit = async ( problemName: string, @@ -202,6 +219,106 @@ export const handleCSESSubmit = async ( }, ); }; +export const handleCodeChefSubmit = async ( + problemName: string, + languageId: number, + sourceCode: string, + problemUrl: string, +) => { + const tab = await chrome.tabs.create({ active: true, url: problemUrl }); + const tabId = tab.id as number; + chrome.windows.update(tab.windowId, { focused: true }); + + chrome.tabs.onUpdated.addListener( + function listener(updatedTabId, changeInfo) { + if (updatedTabId === tabId && changeInfo.status === 'complete') { + chrome.tabs.onUpdated.removeListener(listener); + + setTimeout(async () => { + await chrome.scripting.executeScript({ + target: { tabId, allFrames: false }, + files: ['/dist/codeChefInjectedScript.js'], + }); + + chrome.tabs.sendMessage(tabId, { + type: 'cph-submit-codechef', + languageId, + }); + + /**ChromeV3 don't Allow us to inject script due to CSP policy**/ + setTimeout(async () => { + await chrome.scripting.executeScript({ + target: { tabId, allFrames: false }, + world: 'MAIN', + func: (code) => { + const w = window as any; + if (w.ace) + w.ace + .edit( + document.querySelector( + '.ace_editor', + ), + ) + .setValue(code, -1); + }, + args: [sourceCode], + }); + }, 1000); + }, 2000); + } + }, + ); +}; +export const handleAtcoderSubmit = async ( + problemName: string, + languageId: number, + sourceCode: string, + problemUrl: string, +) => { + const tab = await chrome.tabs.create({ active: true, url: problemUrl }); + const tabId = tab.id as number; + chrome.windows.update(tab.windowId, { focused: true }); + + chrome.tabs.onUpdated.addListener( + function listener(updatedTabId, changeInfo) { + if (updatedTabId === tabId && changeInfo.status === 'complete') { + chrome.tabs.onUpdated.removeListener(listener); + + setTimeout(async () => { + await chrome.scripting.executeScript({ + target: { tabId, allFrames: false }, + files: ['/dist/atcoderInjectedScript.js'], + }); + + chrome.tabs.sendMessage(tabId, { + type: 'cph-submit-Atcoder', + languageId, + }); + + /**ChromeV3 don't Allow us to inject script due to CSP policy**/ + setTimeout(async () => { + await chrome.scripting.executeScript({ + target: { tabId, allFrames: false }, + world: 'MAIN', + func: (code) => { + const w = window as any; + if (w.ace) + w.ace + .edit( + document.querySelector( + '.ace_editor', + ), + ) + .setValue(code, -1); + }, + args: [sourceCode], + }); + }, 1000); + }, 2000); + } + }, + ); +}; export const handleSubmit = async ( problemName: string, languageId: number, @@ -229,6 +346,22 @@ export const handleSubmit = async ( problemUrl, ); } + if (isCodeChefProblem(problemUrl)) { + return handleCodeChefSubmit( + problemName, + languageId, + sourceCode, + problemUrl, + ); + } + if (isAtcoderProblem(problemUrl)) { + return handleAtcoderSubmit( + problemName, + languageId, + sourceCode, + problemUrl, + ); + } const submitTarget = resolveSubmitTarget(problemUrl); diff --git a/webpack.config.js b/webpack.config.js index d53f0e1..967f677 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -7,6 +7,8 @@ module.exports = { injectedScript: './src/injectedScript.ts', algoZenithInjectedScript: './src/algoZenithInjectedScript.ts', csesInjectedScript: './src/csesInjectedScript.ts', + codeChefInjectedScript: './src/codeChefInjectedScript.ts', + atcoderInjectedScript: './src/atcoderInjectedScript.ts', offscreen: './src/offscreen.ts', }, output: {