CPH Submit with Submission History

Codeforces Submit add-on for CPH. Now with persistent submission history.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         CPH Submit with Submission History
// @namespace    http://tampermonkey.net/
// @version      2.0.0
// @description  Codeforces Submit add-on for CPH. Now with persistent submission history.
// @author       Sam5440
// @match        *://codeforces.com/*
// @match        *://codeforces.ml/*
// @connect      localhost
// @grant        GM_xmlhttpRequest
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_addStyle
// @grant        GM_openInTab
// @run-at       document-idle
// @license MIT
// ==/UserScript==

(function() {
    'use strict';

    // --- Config and storage keys ---
    const CPH_SERVER_ENDPOINT = 'http://localhost:27121/getSubmit';
    const GM_STORAGE_KEY_SUBMISSION = 'cph_submission_data';
    const GM_STORAGE_KEY_SETTINGS = 'cph_settings_data';
    const GM_STORAGE_KEY_HISTORY = 'cph_submission_history'; // Dedicated key for submission history
    const MAX_HISTORY_ENTRIES = 30; // Maximum 30 submissions saved

    // --- State variables ---
    let settings = {
        pollingEnabled: true,
        loopTimeout: 3000,
        debug: false // Debug logs off by default
    };
    let pollingIntervalId = null;

    // --- Internal debug log function ---
    const debugLog = (...args) => {
        if (settings.debug) {
            const message = args.map(arg => typeof arg === 'object' ? JSON.stringify(arg) : arg).join(' ');
            console.log('[cph-submit]', message);
        }
    };

    // --- Log submission history ---
    const logSubmission = async (submissionData) => {
        const historyEntry = {
            timestamp: new Date().toISOString(),
            problemName: submissionData.problemName,
            url: submissionData.url,
            languageId: submissionData.languageId,
            sourceCode: submissionData.sourceCode
        };

        try {
            const historyString = await GM_getValue(GM_STORAGE_KEY_HISTORY, '[]');
            let history = JSON.parse(historyString);
            history.unshift(historyEntry); // Add to front, latest first

            if (history.length > MAX_HISTORY_ENTRIES) {
                history = history.slice(0, MAX_HISTORY_ENTRIES);
            }
            await GM_setValue(GM_STORAGE_KEY_HISTORY, JSON.stringify(history));
            debugLog('Submission logged to history.');
        } catch (e) {
            console.error('[cph-submit] Failed to write to submission history:', e);
        }
    };

    // --- Helper functions ---
    const isContestProblem = (problemUrl) => problemUrl.includes('/contest/');
    const getSubmitUrl = (problemUrl) => {
        if (!isContestProblem(problemUrl)) return 'https://codeforces.com/problemset/submit';
        try {
            const url = new URL(problemUrl);
            const contestNumber = url.pathname.split('/')[2];
            return `https://codeforces.com/contest/${contestNumber}/submit`;
        } catch (e) {
            debugLog('Error parsing problem URL:', e);
            return 'https://codeforces.com/problemset/submit';
        }
    };

    // --- Form filling logic ---
    const fillAndSubmitForm = async () => {
        const dataString = await GM_getValue(GM_STORAGE_KEY_SUBMISSION, null);
        if (!dataString) return;

        await GM_setValue(GM_STORAGE_KEY_SUBMISSION, null);

        try {
            const data = JSON.parse(dataString);
            debugLog('Handling submit data', data);

            const langEl = document.querySelector('select[name="programTypeId"]');
            const sourceEl = document.querySelector('textarea[name="source"]');
            if (!langEl || !sourceEl) {
                console.error('[cph-submit] Could not find form elements.');
                return;
            }

            sourceEl.value = data.sourceCode;
            sourceEl.dispatchEvent(new Event('input', { bubbles: true }));
            langEl.value = data.languageId.toString();
            langEl.dispatchEvent(new Event('change', { bubbles: true }));

            if (!isContestProblem(data.url)) {
                const problemEl = document.querySelector('input[name="submittedProblemCode"]');
                if (problemEl) {
                    problemEl.value = data.problemName;
                    problemEl.dispatchEvent(new Event('input', { bubbles: true }));
                }
            } else {
                const problemIndexEl = document.querySelector('select[name="submittedProblemIndex"]');
                if (problemIndexEl) {
                    problemIndexEl.value = data.url.split('/problem/')[1];
                    problemIndexEl.dispatchEvent(new Event('change', { bubbles: true }));
                }
            }

            debugLog('Form filled. Submitting...');
            const submitBtn = document.querySelector('input.submit');
            if (submitBtn) {
                submitBtn.disabled = false;
                submitBtn.click();
            } else {
                console.error('[cph-submit] Could not find submit button.');
            }
        } catch (e) {
            console.error('[cph-submit] Error while processing submission:', e);
        }
    };

    // --- Polling logic ---
    const pollCphServer = () => {
        debugLog('Polling CPH server...');
        GM_xmlhttpRequest({
            method: 'GET',
            url: CPH_SERVER_ENDPOINT,
            headers: { 'cph-submit': 'true' },
            timeout: settings.loopTimeout - 500,
            onload: async (response) => {
                if (response.status !== 200) {
                    debugLog('Error response from CPH server:', response.status, response.statusText);
                    return;
                }
                try {
                    const data = JSON.parse(response.responseText);
                    if (data.empty) {
                        debugLog('Got empty response from CPH.');
                        return;
                    }
                    if (data.problemName && data.languageId && data.sourceCode && data.url) {
                        debugLog('Got valid response from CPH:', data.problemName);
                        // Log submission here
                        await logSubmission(data);
                        await GM_setValue(GM_STORAGE_KEY_SUBMISSION, JSON.stringify(data));
                        GM_openInTab(getSubmitUrl(data.url), { active: true });
                    } else {
                        debugLog('Received invalid data from CPH:', data);
                    }
                } catch (e) {
                    debugLog('Error parsing JSON from CPH server:', e.toString());
                }
            },
            onerror: () => debugLog('Could not connect to CPH server. Is cph (VS Code) running?'),
            ontimeout: () => debugLog('Request to CPH server timed out.')
        });
    };

    // --- Settings UI & logic ---
    const createSettingsUI = () => {
        GM_addStyle(`
            #cph-settings-panel { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 600px; max-height: 80vh; background-color: #f9f9f9; border: 1px solid #ccc; border-radius: 8px; z-index: 9999; box-shadow: 0 4px 8px rgba(0,0,0,0.2); display: none; font-family: sans-serif; flex-direction: column; }
            #cph-settings-panel .header { padding: 10px 15px; background-color: #ececec; font-weight: bold; border-bottom: 1px solid #ccc; cursor: move; border-top-left-radius: 8px; border-top-right-radius: 8px; }
            #cph-settings-panel .header .close-btn { float: right; cursor: pointer; font-weight: bold; }
            #cph-settings-panel .content { padding: 15px; overflow-y: auto; }
            #cph-settings-panel .setting { margin-bottom: 10px; display: flex; align-items: center; justify-content: space-between; }
            #cph-settings-panel .setting label { margin-right: 10px; }
            #cph-settings-panel .setting input[type="number"] { width: 80px; }
            #cph-submission-history { margin-top: 10px; }
            .submission-entry { background-color: #fff; border: 1px solid #ddd; border-radius: 4px; padding: 10px; margin-bottom: 10px; }
            .submission-entry .meta { font-size: 13px; color: #555; margin-bottom: 8px; }
            .submission-entry .meta a { color: #007bff; text-decoration: none; }
            .submission-entry .meta a:hover { text-decoration: underline; }
            .submission-entry pre { max-height: 200px; overflow-y: auto; background-color: #fdfdfd; border: 1px solid #eee; padding: 8px; border-radius: 4px; white-space: pre-wrap; word-wrap: break-word; }
            .submission-entry code { font-family: "Courier New", Courier, monospace; font-size: 12px; color: #333; }
            #cph-settings-panel .footer { padding: 10px; text-align: right; border-top: 1px solid #ccc; background-color: #ececec; }
        `);

        const panel = document.createElement('div');
        panel.id = 'cph-settings-panel';
        panel.innerHTML = `
            <div class="header">CPH Submit Settings<span class="close-btn">&times;</span></div>
            <div class="content">
                <div class="setting"><label for="cph-polling-enabled">Enable Polling</label><input type="checkbox" id="cph-polling-enabled"></div>
                <div class="setting"><label for="cph-debug-mode">Enable Console Logs (Debug)</label><input type="checkbox" id="cph-debug-mode"></div>
                <div class="setting"><label for="cph-loop-timeout">Polling Interval (ms)</label><input type="number" id="cph-loop-timeout" min="1000" step="500"></div>
                <hr>
                <strong>Submission History:</strong>
                <div id="cph-submission-history"><p>Loading history...</p></div>
            </div>
            <div class="footer">
                 <button id="cph-clear-history">Clear History</button>
                 <button id="cph-refresh-history">Refresh History</button>
            </div>
        `;
        document.body.appendChild(panel);

        // Event listeners
        panel.querySelector('.close-btn').addEventListener('click', () => panel.style.display = 'none');
        panel.querySelector('#cph-polling-enabled').addEventListener('change', (e) => saveAndUpdateSettings({ pollingEnabled: e.target.checked }));
        panel.querySelector('#cph-debug-mode').addEventListener('change', (e) => saveAndUpdateSettings({ debug: e.target.checked }));
        panel.querySelector('#cph-loop-timeout').addEventListener('change', (e) => saveAndUpdateSettings({ loopTimeout: parseInt(e.target.value, 10) }));
        panel.querySelector('#cph-refresh-history').addEventListener('click', updateSubmissionHistoryDisplay);
        panel.querySelector('#cph-clear-history').addEventListener('click', async () => {
            if (confirm('Are you sure you want to clear all submission history?')) {
                await GM_setValue(GM_STORAGE_KEY_HISTORY, '[]');
                updateSubmissionHistoryDisplay();
            }
        });

        // Drag functionality
        let pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0;
        const header = panel.querySelector('.header');
        header.onmousedown = (e) => {
            e.preventDefault(); pos3 = e.clientX; pos4 = e.clientY;
            document.onmouseup = () => { document.onmouseup = null; document.onmousemove = null; };
            document.onmousemove = (e) => {
                e.preventDefault(); pos1 = pos3 - e.clientX; pos2 = pos4 - e.clientY;
                pos3 = e.clientX; pos4 = e.clientY;
                panel.style.top = `${panel.offsetTop - pos2}px`;
                panel.style.left = `${panel.offsetLeft - pos1}px`;
            };
        };
    };

    const updateSubmissionHistoryDisplay = async () => {
        const historyContainer = document.getElementById('cph-submission-history');
        if (!historyContainer) return;

        const historyString = await GM_getValue(GM_STORAGE_KEY_HISTORY, '[]');
        const history = JSON.parse(historyString);

        if (history.length === 0) {
            historyContainer.innerHTML = '<p>No submissions recorded yet.</p>';
            return;
        }

        historyContainer.innerHTML = history.map(entry => `
            <div class="submission-entry">
                <div class="meta">
                    <strong>Time:</strong> ${new Date(entry.timestamp).toLocaleString()}<br>
                    <strong>Problem:</strong> <a href="${entry.url}" target="_blank">${entry.problemName}</a>
                </div>
                <pre><code>${entry.sourceCode.replace(/</g, "&lt;").replace(/>/g, "&gt;")}</code></pre>
            </div>
        `).join('');
    };

    const updateSettingsUI = () => {
        document.getElementById('cph-polling-enabled').checked = settings.pollingEnabled;
        document.getElementById('cph-debug-mode').checked = settings.debug;
        document.getElementById('cph-loop-timeout').value = settings.loopTimeout;
    };

    const loadSettings = async () => {
        const savedSettings = await GM_getValue(GM_STORAGE_KEY_SETTINGS, null);
        if (savedSettings) {
            settings = { ...settings, ...JSON.parse(savedSettings) };
        }
    };

    const saveAndUpdateSettings = async (newSettings) => {
        settings = { ...settings, ...newSettings };
        await GM_setValue(GM_STORAGE_KEY_SETTINGS, JSON.stringify(settings));
        debugLog('Settings saved:', settings);
        updateSettingsUI();
        startOrStopPolling();
    };

    const startOrStopPolling = () => {
        if (pollingIntervalId) {
            clearInterval(pollingIntervalId);
            pollingIntervalId = null;
        }
        if (settings.pollingEnabled) {
            pollingIntervalId = setInterval(pollCphServer, settings.loopTimeout);
            debugLog(`Polling started with interval ${settings.loopTimeout}ms.`);
        } else {
            debugLog('Polling stopped.');
        }
    };

    const injectMenuButton = () => {
        const ratingLink = document.querySelector('.menu-list a[href="/ratings"]');
        if (ratingLink) {
            const parentLi = ratingLink.parentElement;
            const settingsLi = document.createElement('li');
            settingsLi.innerHTML = '<a href="#" id="cph-settings-btn" style="color: #00aaff;">CPH Settings</a>';
            parentLi.insertAdjacentElement('afterend', settingsLi);

            document.getElementById('cph-settings-btn').addEventListener('click', (e) => {
                e.preventDefault();
                const panel = document.getElementById('cph-settings-panel');
                if (panel.style.display === 'flex') {
                    panel.style.display = 'none';
                } else {
                    updateSettingsUI();
                    updateSubmissionHistoryDisplay();
                    panel.style.display = 'flex';
                }
            });
        }
    };

    // --- Main execution ---
    async function main() {
        await loadSettings();
        createSettingsUI();
        injectMenuButton();

        if (window.location.href.includes('/submit')) {
            debugLog('On a submit page, attempting to fill form.');
            setTimeout(fillAndSubmitForm, 500);
        }

        startOrStopPolling();
        debugLog('Script loaded and initialized.');
    }

    main();
})();