Skip to content

Repository files navigation

cfapp

English | 简体中文

A Codeforces client with a polished web UI: contest/problem browser, server-rendered LaTeX (KaTeX), inline AI translation with persistent annotations, optional auto-translate (full / section / paragraph) with rate limits, statement export and image copy, a syntax-highlighted code editor with autosave, native standings and submission-history pages, an interactive stats dashboard, per-role fonts (including custom uploads), color themes and translation-annotation palettes, and an embedded Codeforces verification flow for real submissions.

What's new in 1.3.7

  • Contest start times on the homepage: Settings now offers off, upcoming (default), and all. Upcoming contests show their local start date and time; all also shows the start date for contests that have begun or finished.
  • Upcoming contests stay together: future contests are placed before historical contests and sorted by the nearest start time, while historical contests remain newest-first.
  • Compact local dates: contest times use the local timezone and omit the year and leading zeroes, for example 8-17 22:35; historical contests use a compact date such as 8-14.
  • Cleaner native submit editor: the non-interactive syntax-highlight layer no longer paints a duplicate scrollbar; editing and synchronized scrolling are unchanged.

Previous highlights (1.3.6)

  • Reliable contest submissions: all submit flows now open Codeforces' contest-scoped /contest/{id}/submit endpoint, which works consistently during active contests as well as after they finish. The selected problem is preserved for both native API mode and the persistent webview.
  • No premature My submissions jump: the app no longer treats any generic status URL as proof of success. A native submission completes only after the current attempt has filled the form, dispatched the real Codeforces submit click, and reached that contest's exact My submissions page.
  • Safer verdict tracking: submission watches start at the real click time, are isolated by attempt ID, survive closing the verifier after dispatch, and ignore delayed events from canceled or superseded attempts.
  • Submission URL hardening: strict parsed URL checks reject wrong contests, status pages, lookalike hosts, and query/fragment bait. Focused Bun tests cover direct Codeforces and same-origin /cf/ proxy URLs.

Previous highlights (1.3.5)

  • Hierarchical navigation: the topbar Back『‹』button steps up the page structure level by level (problem statement → problem list → contest list), and the Home『⌂』button jumps straight to the contest list from any depth. When you open Settings or Stats from a problem, Back returns you to that problem — never to a Standings / Submit / My-subs tab you merely passed through. The bottom "Main" tab still pops the most recent content page from history.
  • Statement sharing: copy a rendered statement directly to the image clipboard, save it as PNG, or open a zoomable PDF preview before saving. Image export keeps the active light/dark theme and translation-annotation palette, expands collapsed annotations, and locks page scrolling while rendering without imposing a total-pixel cap.
  • Persistent zoom: the app window and each embedded Codeforces tab remember their own zoom level across restarts. Ctrl+0, Ctrl++, Ctrl+-, and Ctrl+wheel continue to work.
  • VSCode problem import: optionally send the open problem and samples to the Codeforces Judge / Competitive Companion listener from the statement toolbar. Enable it and configure the localhost port in Settings.
  • More resilient sessions: login state is tied to Codeforces account cookies instead of anonymous session cookies. Temporary network or Cloudflare validation failures no longer make a valid account appear logged out.

Three runtime modes, all backed by the same Bun HTTP server (src/server.ts in dev, src/server-prod.ts in packaged builds):

Mode Command What it is
Electron app (recommended) bun start Frameless desktop window, and the only mode that can submit solutions. Embeds real CF pages via <webview> under a persistent persist:cf partition — that's where cf_clearance lives, auto-refreshed by a background Turnstile solver.
Launcher bash ./bin/cfapp Picks an installed Chromium, opens it in --app mode with an isolated profile. No Electron dependency — but cannot submit (see below).
Dev bun run dev Bun's HMR server on http://localhost:3000, open in any browser. Cannot submit (see below); use this for UI work.

Submitting requires the Electron app. Codeforces guards its submit form with Cloudflare Turnstile, and a Turnstile sitekey only validates on the domains its owner registered — codeforces.com. The Launcher and Dev modes serve CF pages from localhost through the /cf/ reverse proxy, so the widget refuses to issue a token no matter what. Both modes therefore replace the submit form with an explanation. Browsing, statements, translation, standings, and My submissions all work fine everywhere; only the submit step is affected.

bun start spawns the Electron binary directly (scripts/start-electron.ts) so it works on a machine that has Bun but no Node. electron . still works too, but it goes through a #!/usr/bin/env node shim and needs Node installed.

Quick start

bun install
bun run dev            # terminal prints the URL — browsing/translation only

For real CF login and to submit anything, use the Electron app:

bun start

The first time, click Login in the top bar — a CF webview opens, you log in normally, and cookies sync back to the app config dir (mode 0600 on Unix) so the server-side API calls share the session.

In Settings → Submit / standings / my submissions, choose one of two bottom-tab modes:

Mode Behaviour
Codeforces web pages Embeds the original CF Submit / Standings / My submissions pages.
Native / API Uses native cached pages for standings and submission history. Submission stays in the native editor, then opens only the real CF Turnstile widget in a modal. Completing verification submits automatically and opens My submissions, polling every 3 seconds until the new verdict is final.

The selected mode is persisted in both config.json and browser localStorage. Native submit drafts, standings, and submission history survive tab switches; standings and history also have persistent cache entries for instant display after restart.

Platforms (Linux / macOS / Windows)

Runtime data is stored in the OS-native application directories (see src/paths.ts):

Linux macOS Windows
Config (settings, cookies, custom fonts, drafts) ~/.config/cfapp/ ~/Library/Application Support/cfapp/ %APPDATA%\cfapp\
Cache (API, bundled fonts) ~/.cache/cfapp/ ~/Library/Caches/cfapp/ %LOCALAPPDATA%\cfapp\Cache\

Cross-platform behaviour that is already wired:

  • User-Agent matches the host OS (Linux / macOS / Windows) so Cloudflare cf_clearance matches the Electron webview + curl replay.
  • Frameless window — the main window is frame: false on all three platforms and draws its own top bar; drag that bar to move the window. There is deliberately no titlebar and no traffic-light / minimize-maximize-close buttons, so quit with Cmd+Q (macOS) or Alt+F4 (Linux / Windows), or use your compositor's own window controls. On macOS, Cmd+W only closes the window — the process stays alive and reopens a window on reactivate.
  • Built-in fonts (including the open “Georgia” = Gelasio under font-family: Georgia) download into the cache dir on first run. Mirrors include jsDelivr and npmmirror for better reach in China; once cached, loads are local (/fonts/…).
  • Custom fonts — upload TTF / OTF / WOFF / WOFF2 in Settings → 字体; stored under <configDir>/custom-fonts/. Body / statement / display roles can each pick a custom family.
  • Microsoft Georgia (real TTF, optional): auto-imported into the custom-font library when present — Windows Fonts\georgia.ttf, macOS Supplemental, Linux msttcorefonts / Wine / Proton. Distinct from the bundled Gelasio “Georgia”.
  • Legacy migration — if you previously used ~/.config/cfapp on macOS/Windows, data is copied into the OS-native config dir when the new dir is empty.
  • Packaging: bun run build:linux / build:mac / build:win. Ship a matching Bun binary under .bun-packaged for the target OS/arch before packaging (cross-building needs that target’s Bun from bun releases).

Dev on any platform:

bun install
bun start          # Electron + Bun server — needs no Node, and can submit
# or
bun run dev        # browser-only UI work — cannot submit

Configuration

<configDir>/config.json (see table above; on Linux that is ~/.config/cfapp/config.json):

{
  "handle": "your_handle",          // for rankings / my-status
  "apiKey": "...",                  // CF API key (settings → API)
  "apiSecret": "...",
  "password": "...",                // only needed for web-form submit fallback
  "proxy": "http://127.0.0.1:7890", // optional, applies to CF + AI calls
  "verifySsl": true,
  "cfTabsMode": "webview",          // webview | api (native standings/submissions + verification modal)
  "vscodeJudge": {                  // optional Competitive Companion-compatible listener
    "enabled": false,
    "port": 27121                   // Codeforces Judge / CPH default port
  },
  "ai": {                           // OpenAI-compatible endpoint for translation
    "baseUrl": "",                  // e.g. https://api.example.com/v1  (empty by default)
    "apiKey": "",
    "model": "",                    // empty until you pick one in Settings
    "targetLang": "中文",             // language AI translation renders into (preset or custom)
    "promptTemplate": "",            // system prompt; {lang}/{source_text} substituted (blank = built-in)
    "stream": true,                  // stream translation token-by-token
    // Auto-translate (problem page). "off" keeps manual selection-only behaviour.
    "autoMode": "off",               // off | full | section | paragraph
    "autoTrigger": "manual",         // manual button | onopen (start when the statement loads)
    "rpm": 5,                        // rolling 60s start budget; 0 = unlimited
    "concurrency": 2,                // max in-flight translation requests
    "requestIntervalMs": 200,        // min gap between request *starts*; also enforced after a finish before the next start
    "autoCollapse": false            // full-mode: collapse translation cards by default
  }
}

Sensitive fields are stored in plaintext; the file is written with mode 0600. Native submission uses the logged-in Codeforces webview session and the real CF form; the official Codeforces API does not provide a solution-submission endpoint.

AI Base URL and model default to empty — fill them (or use Settings → AI 翻译) for whichever OpenAI-compatible provider you use. No vendor endpoint is pre-filled.

Settings UI fields autosave (debounced); there is no separate Save button.

Two different "languages":

Setting Where Stored in Meaning
App language Settings → 语言 localStorage (cfapp:lang) UI chrome: English / 中文 / bilingual
AI target language Settings → AI 翻译 config.jsonai.targetLang Language the model translates into
CF bottom-tab mode Settings → Submit / standings / my submissions config.jsoncfTabsMode and localStorage (cfapp:cf-tabs-mode) Original CF web pages, or native/API-backed pages

Client-only appearance (also localStorage):

Setting Key Options
Color theme cfapp:color-theme Leather Book (default), Cool Gray, Forest, Rose Gold, Violet, Obsidian
Translation annotation style cfapp:tr-theme Amber 赭黄 (default), Ink 青墨, Indigo 靛蓝, Cinnabar 朱砂, Plum 紫藤
Per-role fonts cfapp:font-{body,statement,display} Built-in stacks, custom family, or explicit default
Homepage contest times cfapp:contest-time-display off, upcoming (default), or all; local timezone, no year or leading zeroes
App zoom cfapp:zoom:host Persisted host-window zoom factor
Embedded CF zoom cfapp:zoom:frame:<tab> Persisted independently for each Codeforces tab

How the Cloudflare bypass works

codeforces.com sits behind Cloudflare, which fingerprints the TLS handshake (JA3). Bun's fetch — like Node's and Deno's — has a recognizable non-Chrome JA3 and gets the "Just a moment" challenge even with a valid cf_clearance cookie. curl's JA3 happens to pass.

So in src/api/cookie.ts, every request to *.codeforces.com is routed through a curl subprocess (jarFetchViaCurl) that preserves real-Chrome header casing and replays the session cookie jar. Everything else (AI translate, font downloads) goes through plain fetch.

The Electron main process (electron/main.cjs) additionally:

  • pins the persist:cf partition's User-Agent to a Chrome major that matches the bundled Chromium, so cf_clearance is issued under a UA the server can replay;
  • treats only a top-level Codeforces navigation 403 as an expired edge-verification token, then purges cf_clearance and opens a small background window so Turnstile can re-issue it; subresource failures do not log the user out;
  • syncs the partition's cookies to disk so the Bun server's curl calls see the same session.

Why this doesn't extend to submitting from a browser

The curl/JA3 path above solves reading Codeforces from anywhere. Submitting is a separate problem, and it has no equivalent workaround.

The submit form carries a Cloudflare Turnstile widget. A Turnstile sitekey is registered against a hostname allowlist by whoever owns it — for codeforces.com, that's Codeforces. When the widget runs on any other origin it declines to issue a token, and without a token the form is rejected server-side. This is the anti- automation property Turnstile exists to provide, so there is no legitimate way around it and this project does not attempt one.

That leaves a clean split:

  • Electron loads codeforces.com for real inside a <webview>. The widget runs on its own domain, verification succeeds, and the app auto-submits afterwards.
  • Any plain browser (Launcher mode, bun run dev, or the packaged web bundle) reaches CF through the same-origin /cf/ reverse proxy on localhost. The origin is wrong, so verification can never complete. The Submit tab detects this (isElectronApp in src/web/shared.ts) and shows an explanation instead of a form that would silently fail.

Standings and My submissions are unaffected in every mode — they are read-only and go through the server-side cookie jar rather than Turnstile.

Project layout

src/
  server.ts              Bun HTTP server (dev / HMR): REST API, fonts, translation
  server-prod.ts         Production entry used by packaged Electron builds
  server-lib/            Shared server helpers (JSON body, config sanitize, drafts)
  api.ts                 re-export shim (backward compat)
  api/                   CF + AI backend modules
    html.ts              barrel for statement HTML parsing
    html-parts/          math (KaTeX / CF $$$$), statement, text, page-detect
    translate-prompt.ts  language-parameterized, injection-resistant system prompt
    translate-stream.ts  SSE streaming translation (throttled KaTeX, stall salvage)
    ai-probe.ts          list models / chat test / rate-limit probe for Settings
    cookie.ts            curl JA3 path + cookie jar
    avatar-cache.ts      proxy + disk cache for CF avatars
    …
  paths.ts               OS-native config/cache dirs + path allowlists
  custom-fonts.ts        user-uploaded font library under <configDir>/custom-fonts/
  config.ts              config.json load/save + migrations
  ac-store.ts            per-handle contest AC verdict persistence
  fonts.ts               multi-mirror font cache under <cacheDir>/fonts/
  web/
    app.tsx              root router + layout chrome
    auto-translate.ts    problem-page auto-translate orchestration
    rate-limiter.ts      dual limiter: request interval + rolling 60s RPM
    themes.ts            color themes, tr-annotation palettes, font-role metadata
    chrome.tsx           Topbar / BottomBar barrel
    chrome/              AuthIndicator, PersistentCfFrame
    pages/
      SettingsPage.tsx   account, language, AI, theme, fonts
      settings/          AI / auto-translate / model / font / custom-font UI
      ProblemPage.tsx    statement + editor + translation/export UI
      NativeCfTabs.tsx   native submit editor, standings, cached submissions + verdict polling
      problem/           selection toolbar, translation, export, VSCode import
      StatsPage.tsx      dashboard shell
      stats/             charts, heatmap, avatar, cards
      ContestsPage.tsx / ProblemsPage.tsx
    styles/              base, problem, native CF tabs, stats, settings, themes
    i18n.ts              app UI language (en / zh / mix) + dictionary
    shared.ts            Route, UserMe, AppConfig, …
    zoom.ts              persistent host and embedded-tab zoom helpers
electron/
  main.cjs               app shell, CF cookie sync, Turnstile solver
  preload.cjs            logout IPC bridge
bin/cfapp                standalone launcher (Chromium --app mode)
scripts/
  auth-codeforces.ts     headless CF login helper
  build-web.ts           bundle web UI into dist-web/
  electron-path.ts       locate the Electron binary without needing Node
  start-electron.ts      `bun start` entry — spawns Electron directly
  seed-config.ts         write a starter config.json (dev convenience)

Development

bun run typecheck    # tsc --noEmit
bun run test         # bun test
bun run build:web    # produce dist-web/ for Electron packaging
bun run build        # web bundle + electron-builder (Linux)

Features

  • Contest & Problem browser — paginated contest list with per-contest "x/y solved" badges synced from your full submission history in one pull; problem statements with server-rendered LaTeX (KaTeX), including Codeforces $$$…$$$ math
  • Inline AI translation — OpenAI-compatible endpoint translates problem text with persistent annotations; streams token-by-token with LaTeX and inline code preserved. Target language is configurable (中文 / English / 日本語 / 한국어 / … or custom); the system prompt is fully editable — the built-in one is injection-resistant so problem imperatives like "determine" or "output YES" are translated as text instead of obeyed. Base URL and model start empty until you configure them.
  • Auto-translate — optional full / section / paragraph modes; manual trigger or start on open; dual rate limit (min gap between starts, and after a finish before the next start under concurrency) + rolling RPM; full-mode cards can default collapsed
  • Rate-limit probe — Settings → auto-translate can fire a short burst with the current concurrency / interval / RPM; if the provider returns 429, that knobs set is too aggressive (manual check, not auto-tuning)
  • Bilingual UI — English, 中文, or mixed bilingual chrome, instantly and per-client (localStorage)
  • Offline-tolerant & instant — contest lists, problem statements, native standings, submission history, stats, and avatars are persisted locally and painted immediately on launch (show-cached-then-refresh), so the app stays usable when the network / VPN drops; avatars are proxied through the app's network path and byte-cached to disk
  • Native CF tabs — optional native standings and My submissions pages; pages remain mounted across tab switches, use persistent caches after restart, and show standard verdict abbreviations (AC, WA, TLE, MLE, RE, CE, …)
  • Code editor & submission — syntax-highlighted autosaving editor. Native mode opens the correct /contest/{id}/submit form, extracts only Turnstile into a centered modal, injects the selected problem/language/source, auto-submits after verification, then polls the new submission every 3 seconds through queue/testing to the final verdict
  • Statement export — copy a statement as an image without saving, save a full-resolution PNG, or preview a vector PDF with adjustable zoom. Exports preserve the current light/dark theme and translation-annotation colors.
  • VSCode integration — send the current problem, limits, URL, and samples to a local Codeforces Judge / Competitive Companion-compatible extension from the problem page.
  • Persistent zoom — host UI and embedded Codeforces tabs retain independent zoom levels between launches.
  • Stats dashboard — interactive rating history (tier bands, hover tooltips, drag-to-zoom + time-range presets), submission heatmap, verdict/language distribution, pure CSS + inline SVG (zero chart deps). Solve data is scoped per handle.
  • Fonts & themes — body / statement / display roles from a multi-mirror local font cache; optional custom font library; six color themes; five translation-annotation palettes (赭黄 / 青墨 / 靛蓝 / 朱砂 / 紫藤) via data-* flips
  • Cloudflare/session handling — curl-based JA3 path for server requests, persistent Electron persist:cf cookies, login-cookie fingerprinting that survives transient verification failures, and an in-app real Turnstile verification modal for submissions
  • Cross-platform — Linux / macOS / Windows data dirs, UA, packaging targets, and optional Microsoft Georgia import
  • Modular codebase — large surfaces split into focused modules under api/, web/pages/, server-lib/, with thin barrels for stable import paths

About

No description, website, or topics provided.

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages