Блог пользователя PROSENJIT_BISWAS

Автор PROSENJIT_BISWAS, 7 часов назад, По-английски

See your friends' last online time directly on the codeforces.com/friends page — with a green dot for anyone active in the last 5 minutes.

MOTIVATION: Seeing my friends online motivates me to open a problem and start coding.

Preview:

  • (green dot)Online now — active within 5 minutes
  • ⬜ 3 hour(s) ago — last seen time
  • ⬜ 2 day(s) ago

How to Install

  1. Install Tampermonkey(Extension) → Chrome | Firefox
  2. Click the Tampermonkey icon → Create new script
  3. Delete everything and paste the code below
  4. Press Ctrl+S to save
  5. Visit codeforces.com/friends

Code

// ==UserScript==
// @name         Codeforces Friends Online Status (Final Stable)
// @namespace    cf-friends-status
// @version      2.3
// @description  Shows last online status of Codeforces friends with online indicator
// @match        https://mirror.codeforces.com/friends
// @grant        GM_xmlhttpRequest
// @connect      codeforces.com
// @connect      api.codeforces.com
// ==/UserScript==
(function () {
    const ONLINE_THRESHOLD = 5 * 60; // 5 minutes = considered "online now"

    function timeText(lastOnline) {
        let diff = Math.floor(Date.now() / 1000) - lastOnline;
        let min = Math.floor(diff / 60);
        let hr = Math.floor(min / 60);
        let day = Math.floor(hr / 24);
        if (diff < ONLINE_THRESHOLD) return "Online now";
        if (day > 0) return `${day} day(s) ago`;
        if (hr > 0) return `${hr} hour(s) ago`;
        return `${min} min(s) ago`;
    }

    function makeStatusCell(lastOnlineSeconds) {
        let diff = Math.floor(Date.now() / 1000) - lastOnlineSeconds;
        let isOnline = diff < ONLINE_THRESHOLD;

        let wrapper = document.createElement('span');
        wrapper.style.display = 'flex';
        wrapper.style.alignItems = 'center';
        wrapper.style.gap = '6px';

        if (isOnline) {
            //  Pulsing green dot
            let dot = document.createElement('span');
            dot.style.cssText = `
                display: inline-block;
                width: 10px;
                height: 10px;
                border-radius: 50%;
                background: #22c55e;
                flex-shrink: 0;
                box-shadow: 0 0 0 0 rgba(34,197,94,0.6);
                animation: cf-pulse 1.5s infinite;
            `;
            wrapper.appendChild(dot);
        }

        let label = document.createElement('span');
        label.innerText = timeText(lastOnlineSeconds);
        label.style.color = isOnline ? '#16a34a' : '#555';
        label.style.fontWeight = isOnline ? 'bold' : 'normal';
        wrapper.appendChild(label);

        return wrapper;
    }

    // Inject pulse animation once
    function injectStyle() {
        if (document.getElementById('cf-online-style')) return;
        let style = document.createElement('style');
        style.id = 'cf-online-style';
        style.textContent = `
            @keyframes cf-pulse {
                0%   { box-shadow: 0 0 0 0 rgba(34,197,94,0.6); }
                70%  { box-shadow: 0 0 0 7px rgba(34,197,94,0); }
                100% { box-shadow: 0 0 0 0 rgba(34,197,94,0); }
            }
        `;
        document.head.appendChild(style);
    }

    window.addEventListener('load', function () {
        console.log("CF Friends script started");
        injectStyle();

        // Pick the largest table that has profile links = main friends list
        let allTables = [...document.querySelectorAll('table')]
            .filter(t => t.querySelector('a[href*="/profile/"]'));

        if (allTables.length === 0) {
            console.log(" No tables with profile links found");
            return;
        }

        let table = allTables.reduce((biggest, t) =>
            t.rows.length > biggest.rows.length ? t : biggest
        );

        console.log(" Targeting table with", table.rows.length, "rows");

        let rows = table.rows;
        if (rows.length <= 1) {
            console.log(" No friend rows found");
            return;
        }

        // Header cell
        let headCell = rows[0].insertCell(-1);
        headCell.innerText = "Last Online";
        headCell.style.fontWeight = "bold";

        let handles = [];

        for (let i = 1; i < rows.length; i++) {
            let row = rows[i];
            let profileLink = row.querySelector('a[href*="/profile/"]');
            let handle = profileLink
                ? profileLink.href.split('/profile/')[1].trim()
                : null;

            if (!handle) continue;
            handles.push({ handle, rowIndex: i });

            let cell = row.insertCell(-1);
            cell.innerText = "Loading...";
            cell.style.color = "#aaa";
            cell.style.fontSize = "0.9em";
        }

        console.log("Handles found:", handles.map(h => h.handle));
        if (handles.length === 0) return;

        const BATCH = 500;
        for (let b = 0; b < handles.length; b += BATCH) {
            let batch = handles.slice(b, b + BATCH);
            let url = "https://mirror.codeforces.com/api/user.info?handles="
                    + batch.map(h => h.handle).join(";");

            GM_xmlhttpRequest({
                method: "GET",
                url: url,
                onload: function (res) {
                    try {
                        let data = JSON.parse(res.responseText);
                        if (data.status !== "OK") {
                            console.log(" API error:", data.comment);
                            return;
                        }

                        let map = {};
                        data.result.forEach(user => {
                            map[user.handle.toLowerCase()] = user.lastOnlineTimeSeconds;
                        });

                        batch.forEach(({ handle, rowIndex }) => {
                            let ts = map[handle.toLowerCase()];
                            let row = rows[rowIndex];
                            let cell = row?.cells[row.cells.length - 1];
                            if (cell) {
                                cell.innerText = ''; // clear "Loading..."
                                if (ts) {
                                    cell.appendChild(makeStatusCell(ts));
                                } else {
                                    cell.innerText = "N/A";
                                    cell.style.color = "#aaa";
                                }
                            }
                        });

                        console.log(" Updated", batch.length, "friends");
                    } catch (e) {
                        console.log(" Parse error:", e);
                    }
                },
                onerror: function (err) {
                    console.log(" Request failed:", err);
                }
            });
        }
    });
})();

How it works

  • Uses the official Codeforces API (user.info) — one batch call for all friends, no scraping
  • Picks the correct friends table automatically
  • Matches results by handle name, not index — so no mismatches
  • Active within last 5 minutes → (green dot)Online now

Полный текст и комментарии »

  • Проголосовать: нравится
  • -1
  • Проголосовать: не нравится