Skip to content

Commit a2ea0fa

Browse files
committed
get user statistics from leetcode
1 parent 745e8ba commit a2ea0fa

File tree

6 files changed

+237
-29
lines changed

6 files changed

+237
-29
lines changed

jupyterlab_leetcode/handlers/__init__.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,17 @@
22

33
from .base_handler import BaseHandler
44
from .cookie_handler import GetCookieHandler
5-
from .leetcode_handler import LeetCodeProfileHandler
5+
from .leetcode_handler import LeetCodeProfileHandler, LeetCodeStatisticsHandler
66

77

88
def setup_handlers(web_app):
99
host_pattern = ".*$"
1010
base_url = web_app.settings["base_url"]
11-
handlers: list[type[BaseHandler]] = [GetCookieHandler, LeetCodeProfileHandler]
11+
handlers: list[type[BaseHandler]] = [
12+
GetCookieHandler,
13+
LeetCodeProfileHandler,
14+
LeetCodeStatisticsHandler,
15+
]
1216

1317
web_app.add_handlers(
1418
host_pattern,

jupyterlab_leetcode/handlers/cookie_handler.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ class GetCookieHandler(BaseHandler):
3030

3131
@tornado.web.authenticated
3232
def get(self):
33-
self.log.info("Loading all cookies for LeetCode...")
33+
self.log.debug("Loading all cookies for LeetCode...")
3434
browser = self.get_query_argument("browser", "", strip=True)
3535
if not browser:
3636
self.set_status(400)
@@ -62,7 +62,6 @@ def get(self):
6262
else 3600 * 24 * 14
6363
)
6464
self.set_cookie("leetcode_browser", browser, max_age=max_age)
65-
leetcode_ua = self.request.headers.get("user-agent")
6665
self.settings.update(
6766
leetcode_browser=browser,
6867
leetcode_cookiejar=cj,
@@ -79,8 +78,9 @@ def get(self):
7978
),
8079
}
8180
),
82-
leetcode_ua=leetcode_ua,
8381
)
84-
AsyncHTTPClient.configure(None, defaults=dict(user_agent=leetcode_ua))
82+
AsyncHTTPClient.configure(
83+
None, defaults=dict(user_agent=self.request.headers.get("user-agent"))
84+
)
8585

8686
self.finish(json.dumps(resp))
Lines changed: 152 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,176 @@
11
import json
2+
from typing import cast
23

34
import tornado
4-
from tornado.httpclient import AsyncHTTPClient, HTTPRequest
5+
from tornado.gen import multi
6+
from tornado.httpclient import AsyncHTTPClient, HTTPRequest, HTTPResponse
57
from tornado.httputil import HTTPHeaders
68

79
from .base_handler import BaseHandler
810

911
LEETCODE_GRAPHQL_URL = "https://leetcode.com/graphql"
1012

13+
type QueryType = dict[str, str | dict[str, str]]
1114

12-
class LeetCodeProfileHandler(BaseHandler):
13-
route = r"leetcode/profile"
1415

15-
@tornado.web.authenticated
16-
async def get(self):
16+
class LeetCodeHandler(BaseHandler):
17+
"""Base handler for LeetCode-related requests."""
18+
19+
async def graphql(self, name: str, query: QueryType) -> None:
20+
self.log.debug(f"Fetching LeetCode {name} data...")
1721
client = AsyncHTTPClient()
18-
headers = HTTPHeaders(self.settings.get("leetcode_headers", {}))
1922
req = HTTPRequest(
2023
url=LEETCODE_GRAPHQL_URL,
2124
method="POST",
22-
headers=headers,
23-
body=json.dumps(
24-
{
25-
"operationName": "globalData",
26-
"variables": {},
27-
"query": "query globalData { userStatus { isSignedIn username realName avatar } }",
28-
}
29-
),
25+
headers=HTTPHeaders(self.settings.get("leetcode_headers", {})),
26+
body=json.dumps(query),
3027
)
3128

3229
try:
3330
resp = await client.fetch(req)
3431
except Exception as e:
35-
self.log.error(f"Error fetching LeetCode profile: {e}")
32+
self.log.error(f"Error fetching LeetCode {name}: {e}")
3633
self.set_status(500)
37-
self.finish(json.dumps({"message": "Failed to fetch LeetCode profile"}))
34+
self.finish(json.dumps({"message": f"Failed to fetch LeetCode {name}"}))
3835
return
3936
else:
4037
self.finish(resp.body)
38+
39+
async def graphql_multi(
40+
self, name: str, queries: dict[str, QueryType]
41+
) -> dict[str, HTTPResponse]:
42+
self.log.debug(f"Fetching LeetCode {name} data...")
43+
client = AsyncHTTPClient()
44+
request_futures = dict(
45+
map(
46+
lambda kv: (
47+
kv[0],
48+
client.fetch(
49+
HTTPRequest(
50+
url=LEETCODE_GRAPHQL_URL,
51+
method="POST",
52+
headers=HTTPHeaders(
53+
self.settings.get("leetcode_headers", {})
54+
),
55+
body=json.dumps(kv[1]),
56+
),
57+
),
58+
),
59+
queries.items(),
60+
)
61+
)
62+
63+
try:
64+
responses = await multi(request_futures)
65+
except Exception as e:
66+
self.log.error(f"Error fetching LeetCode {name}: {e}")
67+
self.set_status(500)
68+
self.finish(json.dumps({"message": f"Failed to fetch LeetCode {name}"}))
69+
return {}
70+
else:
71+
return cast("dict[str, HTTPResponse]", responses)
72+
73+
74+
class LeetCodeProfileHandler(LeetCodeHandler):
75+
route = r"leetcode/profile"
76+
77+
@tornado.web.authenticated
78+
async def get(self):
79+
await self.graphql(
80+
name="profile",
81+
query={
82+
"query": """query globalData {
83+
userStatus {
84+
isSignedIn
85+
username
86+
realName
87+
avatar
88+
}
89+
}"""
90+
},
91+
)
92+
93+
94+
class LeetCodeStatisticsHandler(LeetCodeHandler):
95+
route = r"leetcode/statistics"
96+
97+
@tornado.web.authenticated
98+
async def get(self):
99+
username = self.get_query_argument("username", "", strip=True)
100+
if not username:
101+
self.set_status(400)
102+
self.finish(json.dumps({"message": "Username parameter is required"}))
103+
return
104+
105+
responses = await self.graphql_multi(
106+
name="statistics",
107+
queries={
108+
"userSessionProgress": {
109+
"query": """query userSessionProgress($username: String!) {
110+
allQuestionsCount {
111+
difficulty
112+
count
113+
}
114+
matchedUser(username: $username) {
115+
submitStats {
116+
acSubmissionNum {
117+
difficulty
118+
count
119+
}
120+
totalSubmissionNum {
121+
difficulty
122+
count
123+
}
124+
}
125+
}
126+
}""",
127+
"variables": {"username": username},
128+
},
129+
"userProfileUserQuestionProgressV2": {
130+
"query": """query userProfileUserQuestionProgressV2($userSlug: String!) {
131+
userProfileUserQuestionProgressV2(userSlug: $userSlug) {
132+
numAcceptedQuestions {
133+
count
134+
difficulty
135+
}
136+
numFailedQuestions {
137+
count
138+
difficulty
139+
}
140+
numUntouchedQuestions {
141+
count
142+
difficulty
143+
}
144+
userSessionBeatsPercentage {
145+
difficulty
146+
percentage
147+
}
148+
totalQuestionBeatsPercentage
149+
}
150+
}""",
151+
"variables": {"userSlug": username},
152+
},
153+
"userPublicProfile": {
154+
"query": """query userPublicProfile($username: String!) {
155+
matchedUser(username: $username) {
156+
username
157+
profile {
158+
ranking
159+
}
160+
}
161+
}""",
162+
"variables": {"username": username},
163+
},
164+
},
165+
)
166+
167+
if not responses:
168+
return
169+
170+
res = dict(
171+
map(
172+
lambda kv: (kv[0], json.loads(kv[1].body).get("data", {})),
173+
responses.items(),
174+
)
175+
)
176+
self.finish(res)

src/components/LeetCode.tsx

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,39 @@
11
import React, { useEffect, useState } from 'react';
2-
import { getProfile } from '../services/leetcode';
2+
import { getProfile, getStatistics } from '../services/leetcode';
3+
import { LeetCodeProfile } from '../types/leetcode';
34

45
const LeetCode = () => {
5-
const [username, setUsername] = useState('');
6+
const [profile, setProfile] = useState<LeetCodeProfile | null>(null);
67

78
useEffect(() => {
89
getProfile().then(profile => {
910
if (!profile || !profile.isSignedIn) {
1011
alert('Please sign in to LeetCode.');
1112
return;
1213
}
13-
setUsername(profile.username);
14+
setProfile(profile);
1415
});
1516
}, []);
1617

17-
return (
18+
useEffect(() => {
19+
if (!profile) {
20+
return;
21+
}
22+
getStatistics(profile.username).then(d => {
23+
console.log('LeetCode Statistics:', d);
24+
});
25+
}, [profile]);
26+
27+
return profile ? (
1828
<div>
19-
<p>Welcome {username}</p>
29+
<p>Welcome {profile.username}</p>
30+
<img
31+
src={profile.avatar}
32+
alt="Avatar"
33+
style={{ width: '100px', height: '100px' }}
34+
/>
2035
</div>
21-
);
36+
) : null;
2237
};
2338

2439
export default LeetCode;

src/services/leetcode.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { LeetCodeProfile } from '../types/leetcode';
1+
import { LeetCodeProfile, LeetCodeStatistics } from '../types/leetcode';
22
import { requestAPI } from './handler';
33

44
export async function getProfile(): Promise<LeetCodeProfile | null> {
@@ -8,3 +8,11 @@ export async function getProfile(): Promise<LeetCodeProfile | null> {
88
.then(d => d.data.userStatus)
99
.catch(() => null);
1010
}
11+
12+
export async function getStatistics(
13+
username: string
14+
): Promise<LeetCodeStatistics | null> {
15+
return requestAPI<LeetCodeStatistics>(
16+
`/leetcode/statistics?username=${username}`
17+
).catch(() => null);
18+
}

src/types/leetcode.d.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,48 @@ export type LeetCodeProfile = {
44
realName: string;
55
username: string;
66
};
7+
8+
export type LeetCodePublicProfile = {
9+
matchedUser: {
10+
profile: {
11+
ranking: number;
12+
};
13+
username: string;
14+
};
15+
};
16+
17+
export type LeetCodeQuestionStatistic = {
18+
count: number;
19+
difficulty: string;
20+
};
21+
22+
export type LeetCodeBeatsPercentage = {
23+
percentage: number;
24+
difficulty: string;
25+
};
26+
27+
export type LeetCodeQuestionProgress = {
28+
totalQuestionBeatsPercentage: number;
29+
numAcceptedQuestions: LeetCodeQuestionStatistic[];
30+
numFailedQuestions: LeetCodeQuestionStatistic[];
31+
numUntouchedQuestions: LeetCodeQuestionStatistic[];
32+
userSessionBeatsPercentage: LeetCodeBeatsPercentage[];
33+
};
34+
35+
export type LeetCodeSessionProgress = {
36+
allQuestionsCount: LeetCodeQuestionStatistic[];
37+
matchedUser: {
38+
submitStats: {
39+
acSubmissionNum: LeetCodeQuestionStatistic[];
40+
totalSubmissionNum: LeetCodeQuestionStatistic[];
41+
};
42+
};
43+
};
44+
45+
export type LeetCodeStatistics = {
46+
userPublicProfile: LeetCodePublicProfile;
47+
userSessionProgress: LeetCodeSessionProgress;
48+
userProfileUserQuestionProgressV2: {
49+
userProfileUserQuestionProgressV2: LeetCodeQuestionProgress;
50+
};
51+
};

0 commit comments

Comments
 (0)