From 125ba8b3200e22dae1ede4563e60a0e922bbab13 Mon Sep 17 00:00:00 2001 From: Cameron Custer Date: Sun, 13 Apr 2025 20:13:41 -0700 Subject: [PATCH 1/2] import user solves --- sql/auth/user_platform_usernames.sql | 32 ++ sql/init.sql | 1 + src/lib/components/Header.svelte | 38 +- src/lib/services/userPlatforms.ts | 379 ++++++++++++++++++ .../api/codeforces/user-solves/+server.ts | 66 +++ src/routes/api/kattis/user-solves/+server.ts | 60 +++ src/routes/import/+page.svelte | 266 ++++++++++++ 7 files changed, 834 insertions(+), 8 deletions(-) create mode 100644 sql/auth/user_platform_usernames.sql create mode 100644 src/lib/services/userPlatforms.ts create mode 100644 src/routes/api/codeforces/user-solves/+server.ts create mode 100644 src/routes/api/kattis/user-solves/+server.ts create mode 100644 src/routes/import/+page.svelte diff --git a/sql/auth/user_platform_usernames.sql b/sql/auth/user_platform_usernames.sql new file mode 100644 index 0000000..4aa3153 --- /dev/null +++ b/sql/auth/user_platform_usernames.sql @@ -0,0 +1,32 @@ +-- Create user_platform_usernames table to store user's platform usernames +CREATE TABLE IF NOT EXISTS user_platform_usernames ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, + codeforces_username TEXT, + kattis_username TEXT, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + UNIQUE(user_id) +); + +-- Create RLS policies for user_platform_usernames table +ALTER TABLE user_platform_usernames ENABLE ROW LEVEL SECURITY; + +-- Users can read their own platform usernames +CREATE POLICY "Users can read their own platform usernames" ON user_platform_usernames FOR +SELECT USING (auth.uid() = user_id); + +-- Users can insert their own platform usernames +CREATE POLICY "Users can insert their own platform usernames" ON user_platform_usernames FOR +INSERT WITH CHECK (auth.uid() = user_id); + +-- Users can update their own platform usernames +CREATE POLICY "Users can update their own platform usernames" ON user_platform_usernames FOR +UPDATE USING (auth.uid() = user_id); + +-- Users can delete their own platform usernames +CREATE POLICY "Users can delete their own platform usernames" ON user_platform_usernames FOR +DELETE USING (auth.uid() = user_id); + +-- Grant access to authenticated users +GRANT ALL ON user_platform_usernames TO authenticated; diff --git a/sql/init.sql b/sql/init.sql index b80c45c..15faaa7 100644 --- a/sql/init.sql +++ b/sql/init.sql @@ -10,6 +10,7 @@ CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; -- Authentication and user management \i auth/user_roles.sql \i auth/user_triggers.sql +\i auth/user_platform_usernames.sql -- Problem-related tables and functions \i problems/problems.sql diff --git a/src/lib/components/Header.svelte b/src/lib/components/Header.svelte index 701a4ff..7c79041 100644 --- a/src/lib/components/Header.svelte +++ b/src/lib/components/Header.svelte @@ -208,18 +208,31 @@ $: if ($page) { >About - {#if $user && isUserAdmin} + {#if $user}
  • SubmitImport
  • + {#if isUserAdmin} +
  • + Submit +
  • + {/if} {/if}
    About - {#if $user && isUserAdmin} + {#if $user}
  • SubmitImport
  • + {#if isUserAdmin} +
  • + Submit +
  • + {/if} {/if}
    { + const currentUser = get(user); + + if (!currentUser) { + return null; + } + + try { + const { data, error } = await supabase + .from('user_platform_usernames') + .select('*') + .eq('user_id', currentUser.id) + .single(); + + if (error) { + if (error.code === 'PGRST116') { + // No record found, which is fine for new users + return null; + } + console.error('Error fetching user platform usernames:', error); + return null; + } + + return data as UserPlatformUsernames; + } catch (err) { + console.error('Failed to fetch user platform usernames:', err); + return null; + } +} + +/** + * Saves the user's platform usernames + * @param codeforcesUsername - Codeforces username + * @param kattisUsername - Kattis username + * @returns Success flag and message + */ +export async function saveUserPlatformUsernames( + codeforcesUsername?: string, + kattisUsername?: string +): Promise<{ success: boolean; message?: string }> { + const currentUser = get(user); + + if (!currentUser) { + return { + success: false, + message: 'You must be logged in to save platform usernames' + }; + } + + try { + // Check if the user already has platform usernames + const existingData = await fetchUserPlatformUsernames(); + + if (existingData) { + // Update existing record + const { error } = await supabase + .from('user_platform_usernames') + .update({ + codeforces_username: codeforcesUsername || null, + kattis_username: kattisUsername || null, + updated_at: new Date().toISOString() + }) + .eq('user_id', currentUser.id); + + if (error) { + console.error('Error updating user platform usernames:', error); + return { + success: false, + message: `Error updating platform usernames: ${error.message}` + }; + } + } else { + // Insert new record + const { error } = await supabase.from('user_platform_usernames').insert({ + user_id: currentUser.id, + codeforces_username: codeforcesUsername || null, + kattis_username: kattisUsername || null + }); + + if (error) { + console.error('Error inserting user platform usernames:', error); + return { + success: false, + message: `Error saving platform usernames: ${error.message}` + }; + } + } + + return { + success: true + }; + } catch (err) { + console.error('Failed to save user platform usernames:', err); + return { + success: false, + message: err instanceof Error ? err.message : 'Unknown error saving platform usernames' + }; + } +} + +/** + * Imports user's solved problems from Codeforces + * @param codeforcesUsername - Codeforces username + * @returns Success flag, message, and counts + */ +export async function importCodeforcesSolves(codeforcesUsername: string): Promise<{ + success: boolean; + message?: string; + totalSolved?: number; + importedCount?: number; +}> { + const currentUser = get(user); + + if (!currentUser) { + return { + success: false, + message: 'You must be logged in to import solved problems' + }; + } + + if (!codeforcesUsername) { + return { + success: false, + message: 'Codeforces username is required' + }; + } + + try { + // Fetch user's solved problems from Codeforces API + const response = await fetch(`/api/codeforces/user-solves?username=${codeforcesUsername}`); + const data = await response.json(); + + if (!response.ok) { + return { + success: false, + message: data.error || 'Failed to fetch solved problems from Codeforces' + }; + } + + // Get all problems from our database + const { data: problems, error: problemsError } = await supabase.from('problems').select('*'); + + if (problemsError) { + console.error('Error fetching problems:', problemsError); + return { + success: false, + message: `Error fetching problems: ${problemsError.message}` + }; + } + + // Map problems by URL for easy lookup + const problemsByUrl = new Map(); + for (const problem of problems) { + problemsByUrl.set(problem.url, problem); + } + + // Match solved problems with our database + const solvedProblems = data.solvedProblems || []; + const matchedProblems = []; + + for (const solvedProblem of solvedProblems) { + const problem = problemsByUrl.get(solvedProblem.url); + if (problem) { + matchedProblems.push({ + user_id: currentUser.id, + problem_id: problem.id, + solved_at: solvedProblem.solvedAt || new Date().toISOString() + }); + } + } + + // Insert matched problems into user_solved_problems table + if (matchedProblems.length > 0) { + // First, get existing solved problems to avoid duplicates + const { data: existingSolved, error: existingError } = await supabase + .from('user_solved_problems') + .select('problem_id') + .eq('user_id', currentUser.id); + + if (existingError) { + console.error('Error fetching existing solved problems:', existingError); + return { + success: false, + message: `Error fetching existing solved problems: ${existingError.message}` + }; + } + + // Filter out already solved problems + const existingSolvedIds = new Set(existingSolved.map((item) => item.problem_id)); + const newSolvedProblems = matchedProblems.filter( + (item) => !existingSolvedIds.has(item.problem_id) + ); + + if (newSolvedProblems.length > 0) { + const { error: insertError } = await supabase + .from('user_solved_problems') + .insert(newSolvedProblems); + + if (insertError) { + console.error('Error inserting solved problems:', insertError); + return { + success: false, + message: `Error inserting solved problems: ${insertError.message}` + }; + } + } + + return { + success: true, + totalSolved: solvedProblems.length, + importedCount: newSolvedProblems.length + }; + } + + return { + success: true, + totalSolved: solvedProblems.length, + importedCount: 0 + }; + } catch (err) { + console.error('Failed to import Codeforces solves:', err); + return { + success: false, + message: err instanceof Error ? err.message : 'Unknown error importing Codeforces solves' + }; + } +} + +/** + * Imports user's solved problems from Kattis + * @param kattisUsername - Kattis username + * @returns Success flag, message, and counts + */ +export async function importKattisSolves(kattisUsername: string): Promise<{ + success: boolean; + message?: string; + totalSolved?: number; + importedCount?: number; +}> { + const currentUser = get(user); + + if (!currentUser) { + return { + success: false, + message: 'You must be logged in to import solved problems' + }; + } + + if (!kattisUsername) { + return { + success: false, + message: 'Kattis username is required' + }; + } + + try { + // Fetch user's solved problems from Kattis API + const response = await fetch(`/api/kattis/user-solves?username=${kattisUsername}`); + const data = await response.json(); + + if (!response.ok) { + return { + success: false, + message: data.error || 'Failed to fetch solved problems from Kattis' + }; + } + + // Get all problems from our database + const { data: problems, error: problemsError } = await supabase.from('problems').select('*'); + + if (problemsError) { + console.error('Error fetching problems:', problemsError); + return { + success: false, + message: `Error fetching problems: ${problemsError.message}` + }; + } + + // Map problems by URL for easy lookup + const problemsByUrl = new Map(); + for (const problem of problems) { + problemsByUrl.set(problem.url, problem); + } + + // Match solved problems with our database + const solvedProblems = data.solvedProblems || []; + const matchedProblems = []; + + for (const solvedProblem of solvedProblems) { + const problem = problemsByUrl.get(solvedProblem.url); + if (problem) { + matchedProblems.push({ + user_id: currentUser.id, + problem_id: problem.id, + solved_at: solvedProblem.solvedAt || new Date().toISOString() + }); + } + } + + // Insert matched problems into user_solved_problems table + if (matchedProblems.length > 0) { + // First, get existing solved problems to avoid duplicates + const { data: existingSolved, error: existingError } = await supabase + .from('user_solved_problems') + .select('problem_id') + .eq('user_id', currentUser.id); + + if (existingError) { + console.error('Error fetching existing solved problems:', existingError); + return { + success: false, + message: `Error fetching existing solved problems: ${existingError.message}` + }; + } + + // Filter out already solved problems + const existingSolvedIds = new Set(existingSolved.map((item) => item.problem_id)); + const newSolvedProblems = matchedProblems.filter( + (item) => !existingSolvedIds.has(item.problem_id) + ); + + if (newSolvedProblems.length > 0) { + const { error: insertError } = await supabase + .from('user_solved_problems') + .insert(newSolvedProblems); + + if (insertError) { + console.error('Error inserting solved problems:', insertError); + return { + success: false, + message: `Error inserting solved problems: ${insertError.message}` + }; + } + } + + return { + success: true, + totalSolved: solvedProblems.length, + importedCount: newSolvedProblems.length + }; + } + + return { + success: true, + totalSolved: solvedProblems.length, + importedCount: 0 + }; + } catch (err) { + console.error('Failed to import Kattis solves:', err); + return { + success: false, + message: err instanceof Error ? err.message : 'Unknown error importing Kattis solves' + }; + } +} diff --git a/src/routes/api/codeforces/user-solves/+server.ts b/src/routes/api/codeforces/user-solves/+server.ts new file mode 100644 index 0000000..98066c5 --- /dev/null +++ b/src/routes/api/codeforces/user-solves/+server.ts @@ -0,0 +1,66 @@ +import { json } from '@sveltejs/kit'; +import type { RequestHandler } from './$types'; + +export const GET: RequestHandler = async ({ url }) => { + const username = url.searchParams.get('username'); + if (!username) { + return json({ error: 'No username provided' }, { status: 400 }); + } + + try { + // Fetch user's submissions from Codeforces API + const response = await fetch(`https://codeforces.com/api/user.status?handle=${username}`); + const data = await response.json(); + + if (data.status !== 'OK') { + return json( + { error: data.comment || 'Failed to fetch submissions from Codeforces' }, + { status: 500 } + ); + } + + // Filter for accepted submissions only + const acceptedSubmissions = data.result.filter( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (submission: any) => submission.verdict === 'OK' + ); + + // Extract unique problem URLs and solved timestamps + const solvedProblems = new Map(); + + for (const submission of acceptedSubmissions) { + const problem = submission.problem; + const contestId = problem.contestId; + const index = problem.index; + + // Handle gym problems + const isGym = contestId >= 100000; + + // Create normalized URL + const url = isGym + ? `https://codeforces.com/gym/${contestId}/problem/${index}` + : `https://codeforces.com/contest/${contestId}/problem/${index}`; + + // Only keep the earliest solve for each problem + if (!solvedProblems.has(url) || submission.creationTimeSeconds < solvedProblems.get(url).solvedAtTimestamp) { + solvedProblems.set(url, { + url, + name: problem.name, + solvedAt: new Date(submission.creationTimeSeconds * 1000).toISOString(), + solvedAtTimestamp: submission.creationTimeSeconds + }); + } + } + + return json({ + success: true, + solvedProblems: Array.from(solvedProblems.values()) + }); + } catch (error) { + console.error('Error fetching Codeforces submissions:', error); + return json( + { error: 'Failed to fetch submissions from Codeforces' }, + { status: 500 } + ); + } +}; diff --git a/src/routes/api/kattis/user-solves/+server.ts b/src/routes/api/kattis/user-solves/+server.ts new file mode 100644 index 0000000..76683ac --- /dev/null +++ b/src/routes/api/kattis/user-solves/+server.ts @@ -0,0 +1,60 @@ +import { json } from '@sveltejs/kit'; +import type { RequestHandler } from './$types'; + +export const GET: RequestHandler = async ({ url }) => { + const username = url.searchParams.get('username'); + if (!username) { + return json({ error: 'No username provided' }, { status: 400 }); + } + + try { + // Fetch user's profile page from Kattis + const response = await fetch(`https://open.kattis.com/users/${username}`); + + if (!response.ok) { + return json( + { error: `Failed to fetch user profile from Kattis: ${response.statusText}` }, + { status: response.status } + ); + } + + const html = await response.text(); + + // Parse the HTML to extract solved problems + // Kattis doesn't have an official API, so we need to scrape the user's profile page + + // Extract problem IDs from the HTML + // The solved problems are listed in a table with links like /problems/problemid + const problemRegex = /\/problems\/([a-z0-9]+)/g; + const matches = html.matchAll(problemRegex); + + const solvedProblems = new Map(); + + for (const match of matches) { + const problemId = match[1]; + const url = `https://open.kattis.com/problems/${problemId}`; + + // We don't have exact solve timestamps from Kattis, so we use the current time + // This means all problems will be considered solved at the same time + if (!solvedProblems.has(url)) { + solvedProblems.set(url, { + url, + problemId, + // We don't have the exact solve time, so we use the current time + solvedAt: new Date().toISOString() + }); + } + } + + return json({ + success: true, + solvedProblems: Array.from(solvedProblems.values()) + }); + } catch (error) { + console.error('Error fetching Kattis submissions:', error); + return json( + { error: 'Failed to fetch submissions from Kattis' }, + { status: 500 } + ); + } +}; diff --git a/src/routes/import/+page.svelte b/src/routes/import/+page.svelte new file mode 100644 index 0000000..66e9b69 --- /dev/null +++ b/src/routes/import/+page.svelte @@ -0,0 +1,266 @@ + + + + AlgoHub | Import Solved Problems + + +
    +

    Import Solved Problems

    + + {#if loading} +
    +
    +
    + {:else} + {#if error} +
    +

    {error}

    +
    + {/if} + + {#if success} +
    +

    {success}

    +
    + {/if} + +
    +

    Platform Usernames

    +

    + Enter your Codeforces and Kattis usernames to import your solved problems. We'll match your + solved problems with the problems in our database and mark them as solved for you. +

    + +
    +
    + + +
    + +
    + + +
    + +
    + + + +
    +
    +
    + + {#if codeforcesImportResult || kattisImportResult} +
    +

    Import Results

    + + {#if codeforcesImportResult} +
    +

    Codeforces

    +

    + Found {codeforcesImportResult.totalSolved} solved problems, imported {codeforcesImportResult.importedCount} new solves. +

    +
    + {/if} + + {#if kattisImportResult} +
    +

    Kattis

    +

    + Found {kattisImportResult.totalSolved} solved problems, imported {kattisImportResult.importedCount} new solves. +

    +
    + {/if} + +

    + Note: Only problems that exist in our database can be imported. If you've solved problems that aren't in our database, they won't be counted. +

    +
    + {/if} + {/if} +
    From 4aa7b1ad29494b630a17500e4daf53731ef2ec0d Mon Sep 17 00:00:00 2001 From: Cameron Custer Date: Sun, 13 Apr 2025 21:05:22 -0700 Subject: [PATCH 2/2] diable kattis --- src/lib/services/userPlatforms.ts | 101 ++++++++++- .../api/codeforces/user-solves/+server.ts | 28 ++- src/routes/api/kattis/user-solves/+server.ts | 171 +++++++++++++++--- src/routes/import/+page.svelte | 164 +++++++++++------ 4 files changed, 368 insertions(+), 96 deletions(-) diff --git a/src/lib/services/userPlatforms.ts b/src/lib/services/userPlatforms.ts index 079bfda..58167ce 100644 --- a/src/lib/services/userPlatforms.ts +++ b/src/lib/services/userPlatforms.ts @@ -131,6 +131,7 @@ export async function importCodeforcesSolves(codeforcesUsername: string): Promis success: boolean; message?: string; totalSolved?: number; + matchedCount?: number; importedCount?: number; }> { const currentUser = get(user); @@ -150,17 +151,44 @@ export async function importCodeforcesSolves(codeforcesUsername: string): Promis } try { + console.log(`Fetching solved problems for Codeforces user: ${codeforcesUsername}`); + // Fetch user's solved problems from Codeforces API - const response = await fetch(`/api/codeforces/user-solves?username=${codeforcesUsername}`); + const response = await fetch( + `/api/codeforces/user-solves?username=${encodeURIComponent(codeforcesUsername)}` + ); const data = await response.json(); if (!response.ok) { + console.error('Error response from Codeforces API:', data); + + // Check if the error is related to user not found + if (data.error && data.error.includes('not found')) { + return { + success: false, + message: `User "${codeforcesUsername}" not found on Codeforces. Please check that you've entered your Codeforces username correctly (case-sensitive). You can verify your username by checking if this link works: https://codeforces.com/profile/${codeforcesUsername}` + }; + } + return { success: false, message: data.error || 'Failed to fetch solved problems from Codeforces' }; } + // Check if we got a valid response with solvedProblems array + if (!data.solvedProblems) { + console.error('Invalid response from Codeforces API:', data); + return { + success: false, + message: 'Invalid response from Codeforces API' + }; + } + + console.log( + `Found ${data.solvedProblems.length} solved problems for Codeforces user: ${codeforcesUsername}` + ); + // Get all problems from our database const { data: problems, error: problemsError } = await supabase.from('problems').select('*'); @@ -172,6 +200,8 @@ export async function importCodeforcesSolves(codeforcesUsername: string): Promis }; } + console.log(`Found ${problems.length} problems in our database`); + // Map problems by URL for easy lookup const problemsByUrl = new Map(); for (const problem of problems) { @@ -193,6 +223,8 @@ export async function importCodeforcesSolves(codeforcesUsername: string): Promis } } + console.log(`Matched ${matchedProblems.length} problems with our database`); + // Insert matched problems into user_solved_problems table if (matchedProblems.length > 0) { // First, get existing solved problems to avoid duplicates @@ -215,6 +247,8 @@ export async function importCodeforcesSolves(codeforcesUsername: string): Promis (item) => !existingSolvedIds.has(item.problem_id) ); + console.log(`Found ${newSolvedProblems.length} new problems to mark as solved`); + if (newSolvedProblems.length > 0) { const { error: insertError } = await supabase .from('user_solved_problems') @@ -232,6 +266,7 @@ export async function importCodeforcesSolves(codeforcesUsername: string): Promis return { success: true, totalSolved: solvedProblems.length, + matchedCount: matchedProblems.length, importedCount: newSolvedProblems.length }; } @@ -239,6 +274,7 @@ export async function importCodeforcesSolves(codeforcesUsername: string): Promis return { success: true, totalSolved: solvedProblems.length, + matchedCount: matchedProblems.length, importedCount: 0 }; } catch (err) { @@ -260,6 +296,7 @@ export async function importKattisSolves(kattisUsername: string): Promise<{ message?: string; totalSolved?: number; importedCount?: number; + matchedCount?: number; }> { const currentUser = get(user); @@ -278,19 +315,69 @@ export async function importKattisSolves(kattisUsername: string): Promise<{ } try { + console.log(`Fetching solved problems for Kattis user: ${kattisUsername}`); + // Fetch user's solved problems from Kattis API - const response = await fetch(`/api/kattis/user-solves?username=${kattisUsername}`); + const response = await fetch( + `/api/kattis/user-solves?username=${encodeURIComponent(kattisUsername)}` + ); const data = await response.json(); if (!response.ok) { + console.error('Error response from Kattis API:', data); return { success: false, message: data.error || 'Failed to fetch solved problems from Kattis' }; } + // Check if we got a valid response + if (!data.success) { + console.error('Error response from Kattis API:', data); + + // Provide a more helpful error message for user not found + if (data.error && data.error.includes('not found on Kattis')) { + return { + success: false, + message: `User "${kattisUsername}" not found on Kattis. Please check that you've entered your Kattis username exactly as it appears in your profile URL. You can verify your username by checking if this link works: https://open.kattis.com/users/${kattisUsername}` + }; + } + + return { + success: false, + message: data.error || 'Failed to fetch solved problems from Kattis' + }; + } + + // Check if solvedProblems array exists (it should, but just to be safe) + if (!data.solvedProblems) { + console.error('Invalid response from Kattis API:', data); + return { + success: false, + message: 'Invalid response from Kattis API' + }; + } + + // If the user has no solved problems, return early with success + if (data.solvedProblems.length === 0) { + console.log(`User ${kattisUsername} has no solved problems on Kattis`); + return { + success: true, + totalSolved: 0, + matchedCount: 0, + importedCount: 0 + }; + } + + console.log( + `Found ${data.solvedProblems.length} solved problems for Kattis user: ${kattisUsername}` + ); + // Get all problems from our database - const { data: problems, error: problemsError } = await supabase.from('problems').select('*'); + const { data: problems, error: problemsError } = await supabase + .from('problems') + .select('*') + .like('url', 'https://open.kattis.com/problems/%'); if (problemsError) { console.error('Error fetching problems:', problemsError); @@ -300,6 +387,8 @@ export async function importKattisSolves(kattisUsername: string): Promise<{ }; } + console.log(`Found ${problems.length} Kattis problems in our database`); + // Map problems by URL for easy lookup const problemsByUrl = new Map(); for (const problem of problems) { @@ -321,6 +410,8 @@ export async function importKattisSolves(kattisUsername: string): Promise<{ } } + console.log(`Matched ${matchedProblems.length} problems with our database`); + // Insert matched problems into user_solved_problems table if (matchedProblems.length > 0) { // First, get existing solved problems to avoid duplicates @@ -343,6 +434,8 @@ export async function importKattisSolves(kattisUsername: string): Promise<{ (item) => !existingSolvedIds.has(item.problem_id) ); + console.log(`Found ${newSolvedProblems.length} new problems to mark as solved`); + if (newSolvedProblems.length > 0) { const { error: insertError } = await supabase .from('user_solved_problems') @@ -360,6 +453,7 @@ export async function importKattisSolves(kattisUsername: string): Promise<{ return { success: true, totalSolved: solvedProblems.length, + matchedCount: matchedProblems.length, importedCount: newSolvedProblems.length }; } @@ -367,6 +461,7 @@ export async function importKattisSolves(kattisUsername: string): Promise<{ return { success: true, totalSolved: solvedProblems.length, + matchedCount: matchedProblems.length, importedCount: 0 }; } catch (err) { diff --git a/src/routes/api/codeforces/user-solves/+server.ts b/src/routes/api/codeforces/user-solves/+server.ts index 98066c5..ea2be7e 100644 --- a/src/routes/api/codeforces/user-solves/+server.ts +++ b/src/routes/api/codeforces/user-solves/+server.ts @@ -8,11 +8,23 @@ export const GET: RequestHandler = async ({ url }) => { } try { + console.log(`Fetching submissions for Codeforces user: ${username}`); + // Fetch user's submissions from Codeforces API const response = await fetch(`https://codeforces.com/api/user.status?handle=${username}`); const data = await response.json(); + console.log(`Codeforces API response status: ${data.status}`); if (data.status !== 'OK') { + console.error('Codeforces API error:', data); + } + + if (data.status !== 'OK') { + // Check for specific error messages + if (data.comment && data.comment.includes('not found')) { + return json({ error: `User ${username} not found on Codeforces` }, { status: 404 }); + } + return json( { error: data.comment || 'Failed to fetch submissions from Codeforces' }, { status: 500 } @@ -32,17 +44,20 @@ export const GET: RequestHandler = async ({ url }) => { const problem = submission.problem; const contestId = problem.contestId; const index = problem.index; - + // Handle gym problems const isGym = contestId >= 100000; - + // Create normalized URL const url = isGym ? `https://codeforces.com/gym/${contestId}/problem/${index}` : `https://codeforces.com/contest/${contestId}/problem/${index}`; - + // Only keep the earliest solve for each problem - if (!solvedProblems.has(url) || submission.creationTimeSeconds < solvedProblems.get(url).solvedAtTimestamp) { + if ( + !solvedProblems.has(url) || + submission.creationTimeSeconds < solvedProblems.get(url).solvedAtTimestamp + ) { solvedProblems.set(url, { url, name: problem.name, @@ -58,9 +73,6 @@ export const GET: RequestHandler = async ({ url }) => { }); } catch (error) { console.error('Error fetching Codeforces submissions:', error); - return json( - { error: 'Failed to fetch submissions from Codeforces' }, - { status: 500 } - ); + return json({ error: 'Failed to fetch submissions from Codeforces' }, { status: 500 }); } }; diff --git a/src/routes/api/kattis/user-solves/+server.ts b/src/routes/api/kattis/user-solves/+server.ts index 76683ac..574a158 100644 --- a/src/routes/api/kattis/user-solves/+server.ts +++ b/src/routes/api/kattis/user-solves/+server.ts @@ -2,59 +2,172 @@ import { json } from '@sveltejs/kit'; import type { RequestHandler } from './$types'; export const GET: RequestHandler = async ({ url }) => { - const username = url.searchParams.get('username'); + let username = url.searchParams.get('username'); if (!username) { return json({ error: 'No username provided' }, { status: 400 }); } + // Normalize username - Kattis usernames are typically lowercase with hyphens + // If the username contains uppercase letters or spaces, convert them + const originalUsername = username; + username = username.trim().toLowerCase(); + + // Replace spaces with hyphens (common mistake) + if (username.includes(' ')) { + console.log(`Converting spaces to hyphens in username: "${username}"`); + username = username.replace(/ /g, '-'); + } + + if (username !== originalUsername) { + console.log(`Normalized username from "${originalUsername}" to "${username}"`); + } + try { // Fetch user's profile page from Kattis const response = await fetch(`https://open.kattis.com/users/${username}`); - + if (!response.ok) { return json( { error: `Failed to fetch user profile from Kattis: ${response.statusText}` }, { status: response.status } ); } - + const html = await response.text(); - + // Parse the HTML to extract solved problems // Kattis doesn't have an official API, so we need to scrape the user's profile page - - // Extract problem IDs from the HTML - // The solved problems are listed in a table with links like /problems/problemid - const problemRegex = /\/problems\/([a-z0-9]+)/g; - const matches = html.matchAll(problemRegex); - + + // First, check if the user exists - log more details for debugging + console.log(`Checking if user exists in HTML. Username: "${username}"`); + console.log(`HTML contains >${username}<: ${html.includes(`>${username}<`)}`); + console.log(`HTML contains >${username}${username}${username}<`, + `>${username}${username.toLowerCase()}<`, + `>${username.toUpperCase()}<` + ]; + + let userFound = false; + for (const variation of usernameVariations) { + if (html.includes(variation)) { + console.log(`Found username variation: ${variation}`); + userFound = true; + break; + } + } + + if (!userFound) { + // Let's check if the page is a 404 or contains error messages + if (html.includes('Page not found') || html.includes('does not exist')) { + console.log('Page indicates user not found'); + return json({ error: `User ${username} not found on Kattis` }, { status: 404 }); + } + + // If we're here, the page exists but we couldn't find the username + // Let's try a more lenient approach - check if the URL path is in the HTML + if (html.includes(`/users/${username}`)) { + console.log(`Found username in URL path: /users/${username}`); + // User likely exists, continue processing + } else { + console.log('Username not found in any expected format'); + return json({ error: `User ${username} not found on Kattis` }, { status: 404 }); + } + } + + // Check if the user has no solved problems + if (html.includes('has not solved any problems')) { + console.log(`User ${username} exists but has not solved any problems`); + return json({ + success: true, + solvedProblems: [] + }); + } + + // Log the HTML for debugging (truncated) + console.log(`HTML length: ${html.length}`); + console.log(`HTML snippet: ${html.substring(0, 200)}...`); + + // Try different approaches to find problem links + console.log('Starting to extract problem links'); const solvedProblems = new Map(); - - for (const match of matches) { - const problemId = match[1]; - const url = `https://open.kattis.com/problems/${problemId}`; - - // We don't have exact solve timestamps from Kattis, so we use the current time - // This means all problems will be considered solved at the same time - if (!solvedProblems.has(url)) { - solvedProblems.set(url, { - url, - problemId, - // We don't have the exact solve time, so we use the current time - solvedAt: new Date().toISOString() - }); + + // Try multiple approaches to find problem links + const approaches = [ + // 1. Look for problems in the solved problems section with full HTML structure + { + regex: /]*>([^<]+)<\/a>/g, + name: 'specific HTML structure' + }, + + // 2. Look for any links to problems + { regex: /href="\/problems\/([a-z0-9]+)"/g, name: 'general problem links' }, + + // 3. Look for problem IDs in the URL path + { regex: /\/problems\/([a-z0-9]+)/g, name: 'URL paths' }, + + // 4. Look for problem IDs in a different format + { regex: /problem\/([a-z0-9]+)/g, name: 'alternative format' } + ]; + + // Try each approach until we find some problems + for (const approach of approaches) { + console.log(`Trying approach: ${approach.name}`); + const matches = html.matchAll(approach.regex); + let matchCount = 0; + + for (const match of matches) { + matchCount++; + const problemId = match[1]; + const url = `https://open.kattis.com/problems/${problemId}`; + + // Get problem name if available (from first approach), otherwise use problem ID + const problemName = match.length > 2 ? match[2].trim() : problemId; + + if (!solvedProblems.has(url)) { + solvedProblems.set(url, { + url, + problemId, + name: problemName, + solvedAt: new Date().toISOString() + }); + } + } + + console.log(`Found ${matchCount} matches with approach: ${approach.name}`); + + // If we found some problems, we can stop trying other approaches + if (solvedProblems.size > 0) { + console.log( + `Successfully found ${solvedProblems.size} problems with approach: ${approach.name}` + ); + break; } } - + + // 3. If we still didn't find any problems but the user exists, they might have no solved problems + if (solvedProblems.size === 0) { + console.log(`No problems found for user ${username}, but user exists`); + return json({ + success: true, + solvedProblems: [] + }); + } + + // Log the number of problems found for debugging + console.log(`Found ${solvedProblems.size} solved problems for Kattis user ${username}`); + return json({ success: true, solvedProblems: Array.from(solvedProblems.values()) }); } catch (error) { console.error('Error fetching Kattis submissions:', error); - return json( - { error: 'Failed to fetch submissions from Kattis' }, - { status: 500 } - ); + return json({ error: 'Failed to fetch submissions from Kattis' }, { status: 500 }); } }; diff --git a/src/routes/import/+page.svelte b/src/routes/import/+page.svelte index 66e9b69..8c61461 100644 --- a/src/routes/import/+page.svelte +++ b/src/routes/import/+page.svelte @@ -2,14 +2,12 @@ import { onMount } from 'svelte'; import { goto } from '$app/navigation'; import { user } from '$lib/services/auth'; -import { - fetchUserPlatformUsernames, +import { + fetchUserPlatformUsernames, saveUserPlatformUsernames, - importCodeforcesSolves, - importKattisSolves + importCodeforcesSolves } from '$lib/services/userPlatforms'; import type { UserPlatformUsernames } from '$lib/services/userPlatforms'; -import type { Unsubscriber } from 'svelte/store'; // Form state let codeforcesUsername = ''; @@ -22,11 +20,19 @@ let success: string | null = null; let userPlatforms: UserPlatformUsernames | null = null; // Import results -let codeforcesImportResult: { totalSolved?: number; importedCount?: number } | null = null; -let kattisImportResult: { totalSolved?: number; importedCount?: number } | null = null; +let codeforcesImportResult: { + totalSolved?: number; + matchedCount?: number; + importedCount?: number; +} | null = null; +let kattisImportResult: { + totalSolved?: number; + matchedCount?: number; + importedCount?: number; + message?: string; +} | null = null; // Auth state -let userUnsubscribe: Unsubscriber | undefined; // Initialize auth state onMount(() => { @@ -53,10 +59,10 @@ onMount(() => { async function loadUserPlatforms() { loading = true; error = null; - + try { userPlatforms = await fetchUserPlatformUsernames(); - + if (userPlatforms) { codeforcesUsername = userPlatforms.codeforces_username || ''; kattisUsername = userPlatforms.kattis_username || ''; @@ -74,10 +80,10 @@ async function handleSave() { saving = true; error = null; success = null; - + try { const result = await saveUserPlatformUsernames(codeforcesUsername, kattisUsername); - + if (result.success) { success = 'Your platform usernames have been saved successfully.'; await loadUserPlatforms(); @@ -99,23 +105,24 @@ async function handleImport() { success = null; codeforcesImportResult = null; kattisImportResult = null; - + try { // Save usernames first const saveResult = await saveUserPlatformUsernames(codeforcesUsername, kattisUsername); - + if (!saveResult.success) { error = saveResult.message || 'Failed to save your platform usernames. Please try again.'; return; } - + // Import from Codeforces if username is provided if (codeforcesUsername) { const cfResult = await importCodeforcesSolves(codeforcesUsername); - + if (cfResult.success) { codeforcesImportResult = { totalSolved: cfResult.totalSolved, + matchedCount: cfResult.matchedCount, importedCount: cfResult.importedCount }; } else { @@ -123,22 +130,16 @@ async function handleImport() { return; } } - - // Import from Kattis if username is provided - if (kattisUsername) { - const kattisResult = await importKattisSolves(kattisUsername); - - if (kattisResult.success) { - kattisImportResult = { - totalSolved: kattisResult.totalSolved, - importedCount: kattisResult.importedCount - }; - } else { - error = kattisResult.message || 'Failed to import Kattis solves. Please try again.'; - return; - } - } - + + // Kattis import is disabled because profiles are private + // We'll just show a message about this + kattisImportResult = { + totalSolved: 0, + matchedCount: 0, + importedCount: 0, + message: 'Kattis profiles are private, so automatic import is not available.' + }; + // Show success message if at least one platform was imported if (codeforcesImportResult || kattisImportResult) { success = 'Your solved problems have been imported successfully.'; @@ -160,60 +161,97 @@ async function handleImport() {

    Import Solved Problems

    - + {#if loading}
    -
    +
    {:else} {#if error}
    -

    {error}

    +

    {@html error}

    {/if} - + {#if success} -
    +

    {success}

    {/if} - -
    + +

    Platform Usernames

    -

    +

    Enter your Codeforces and Kattis usernames to import your solved problems. We'll match your solved problems with the problems in our database and mark them as solved for you.

    - +

    + Important: Make sure to enter your usernames exactly as they appear in your + profile URLs. Usernames are case-sensitive and must match exactly what's shown on the respective + platforms. +

    +
    - +
    +

    + Note: Kattis profiles are not publicly accessible, so automatic import + is not possible. +
    + To mark Kattis problems as solved, please use the "Solved" button on individual problem pages. +

    - +
    - +
    - + {#if codeforcesImportResult || kattisImportResult} -
    +

    Import Results

    - + {#if codeforcesImportResult}

    Codeforces

    - Found {codeforcesImportResult.totalSolved} solved problems, imported {codeforcesImportResult.importedCount} new solves. + Found {codeforcesImportResult.totalSolved} solved problems, matched {codeforcesImportResult.matchedCount} + with our database, imported {codeforcesImportResult.importedCount} + new solves.

    {/if} - + {#if kattisImportResult}

    Kattis

    - Found {kattisImportResult.totalSolved} solved problems, imported {kattisImportResult.importedCount} new solves. + {#if kattisImportResult.message} + {kattisImportResult.message} + {:else} + Found {kattisImportResult.totalSolved} solved problems, matched {kattisImportResult.matchedCount} + with our database, imported {kattisImportResult.importedCount} + new solves. + {/if}

    {/if} - +

    - Note: Only problems that exist in our database can be imported. If you've solved problems that aren't in our database, they won't be counted. + Note: Only problems that exist in our database can be imported. If you've solved problems + that aren't in our database, they won't be counted. +

    +

    + For Kattis problems, you'll need to manually mark them as solved on each problem page.

    {/if}