1+ import os
2+ import json
3+ import platform
4+ from typing import (
5+ Optional ,
6+ Tuple ,
7+ Union
8+ )
9+ from http .cookiejar import CookieJar
10+ import requests
11+ import browser_cookie3
12+ from tabulate import tabulate
13+ from pathlib import Path
14+ from leetcli .utils .req import (
15+ _req_header ,
16+ _req_cookies ,
17+ )
18+ from leetcli .utils .query import (
19+ _req_user_progress_v2_query
20+ )
21+ from leetcli .utils .variable import (
22+ _req_user_progress_variable
23+ )
24+
25+ LEETCODE_URL = "https://leetcode.com/graphql"
26+ USERINFO_DIR = Path (os .path .expanduser ("~/.leetcode-cli" ))
27+ USERINFO_FILE = USERINFO_DIR / "session.json"
28+ LEETCODE_DOMAIN = "leetcode.com"
29+
30+
31+ class UserInfoManager :
32+ def __init__ (self ):
33+ pass
34+
35+ def _get_userinfo (
36+ self ,
37+ url
38+ ) -> Optional [Tuple [str , bool ]]:
39+ try :
40+ with open (USERINFO_FILE , "r" ) as f :
41+ cookies_dict = json .load (f )
42+ session = requests .Session ()
43+ for k , v in cookies_dict .items ():
44+ session .cookies .set (k , v )
45+ username , is_active = self ._test_userinfo (url , session )
46+ return username , is_active
47+
48+ except FileNotFoundError as e :
49+ raise e
50+
51+ except ConnectionError as e :
52+ raise e
53+
54+ except Exception as e :
55+ raise e
56+
57+ def _create_userinfo (
58+ self
59+ ) -> Optional [Union [CookieJar , str ]]:
60+ """
61+ Create New Session from Leetcode
62+ """
63+ cookies = None
64+ current_os = platform .system ()
65+ browsers_support = {
66+ "chrome" : ["Linux" , "Darwin" , "Windows" ],
67+ "firefox" : ["Linux" , "Darwin" , "Windows" ],
68+ "librewolf" : ["Linux" , "Darwin" , "Windows" ],
69+ "opera" : ["Linux" , "Darwin" , "Windows" ],
70+ "opera_gx" : ["Darwin" , "Windows" ],
71+ "edge" : ["Linux" , "Darwin" , "Windows" ],
72+ "chromium" : ["Linux" , "Darwin" , "Windows" ],
73+ "brave" : ["Linux" , "Darwin" , "Windows" ],
74+ "vivaldi" : ["Linux" , "Darwin" , "Windows" ],
75+ "w3m" : ["Linux" ],
76+ "lynx" : ["Linux" ],
77+ "safari" : ["Darwin" ],
78+ }
79+
80+ for browser_name , os_list in browsers_support .items ():
81+ if current_os not in os_list :
82+ continue
83+ try :
84+ func = getattr (browser_cookie3 , browser_name )
85+ except AttributeError :
86+ continue
87+
88+ try :
89+ cookies = func (domain_name = "leetcode.com" )
90+ if cookies :
91+ break
92+ except Exception as e :
93+ continue
94+ return cookies
95+
96+ def _save_userinfo (
97+ self ,
98+ cookies ,
99+ default_language : str | None ,
100+ LEETCODE_LANGUAGES
101+ ) -> None :
102+ dir_path = os .path .dirname (USERINFO_FILE )
103+ os .makedirs (dir_path , exist_ok = True )
104+ important_info = {}
105+ for cookie in cookies :
106+ if cookie .name in ("LEETCODE_SESSION" , "csrftoken" ):
107+ important_info [cookie .name ] = cookie .value
108+ if default_language and LEETCODE_LANGUAGES :
109+ lang_key = default_language .strip ().lower ()
110+ if lang_key not in LEETCODE_LANGUAGES :
111+ raise ValueError (
112+ f"Unsupported language: { default_language } \n "
113+ f"Available options: { ', ' .join (sorted (set (LEETCODE_LANGUAGES .values ())))} "
114+ )
115+ important_info ["language" ] = LEETCODE_LANGUAGES [lang_key ]
116+ with open (USERINFO_FILE , "w" ) as f :
117+ json .dump (important_info , f , indent = 2 )
118+
119+ def _test_userinfo (
120+ self ,
121+ url ,
122+ session
123+ ) -> Tuple [bool , Optional [str ]]:
124+ try :
125+ headers = {
126+ "Referer" : "https://leetcode.com/" ,
127+ "Content-Type" : "application/json" ,
128+ "x-csrftoken" : session .cookies .get ("csrftoken" ),
129+ "User-Agent" : "Mozilla/5.0" ,
130+ }
131+
132+ query = {
133+ "operationName" : "globalData" ,
134+ "variables" : {},
135+ "query" : """
136+ query globalData {
137+ userStatus {
138+ isSignedIn
139+ username
140+ }
141+ }
142+ """
143+ }
144+ resp = session .post (url , headers = headers , json = query )
145+ data = resp .json ()
146+ user_status = data .get ("data" , {}).get ("userStatus" , {})
147+ is_signed_in = user_status .get ("isSignedIn" , False )
148+ username = user_status .get ("username" )
149+
150+ if is_signed_in and username :
151+ return True , username
152+ else :
153+ return False , None
154+
155+ except Exception as e :
156+ raise ConnectionError
157+
158+ def _delete_userinfo (
159+ self
160+ ) -> bool :
161+ try :
162+ if USERINFO_FILE .exists ():
163+ USERINFO_FILE .unlink ()
164+ return True
165+ else :
166+ return False
167+ except Exception as e :
168+ return False
169+
170+ def _get_csrftoken (
171+ self
172+ ) -> str :
173+ with open (USERINFO_FILE , "r" ) as f :
174+ userinfo_dict = json .load (f )
175+ return userinfo_dict ['csrftoken' ]
176+
177+ def _get_session (
178+ self
179+ ) -> str :
180+ with open (USERINFO_FILE , "r" ) as f :
181+ userinfo_dict = json .load (f )
182+ return userinfo_dict ['LEETCODE_SESSION' ]
183+
184+ def _set_lang (
185+ self ,
186+ language ,
187+ LEETCODE_LANGUAGES
188+ ) -> None :
189+ if not USERINFO_FILE .exists ():
190+ raise FileNotFoundError ("User info file not found." )
191+ lang_key = language .strip ().lower ()
192+ if lang_key not in LEETCODE_LANGUAGES :
193+ raise ValueError (
194+ f"Unsupported language: { language } \n "
195+ f"Available options: { ', ' .join (sorted (set (LEETCODE_LANGUAGES .values ())))} "
196+ )
197+ selected_lang = LEETCODE_LANGUAGES [lang_key ]
198+ with open (USERINFO_FILE , "r" ) as f :
199+ data = json .load (f )
200+ data ["language" ] = selected_lang
201+
202+ with open (USERINFO_FILE , "w" ) as f :
203+ json .dump (data , f , indent = 2 )
204+
205+ return selected_lang
206+
207+ def _get_lang (
208+ self
209+ ) -> str :
210+ try :
211+ with open (USERINFO_FILE , "r" ) as f :
212+ userinfo_dict = json .load (f )
213+ return userinfo_dict ['language' ]
214+ except Exception as e :
215+ raise FileNotFoundError ("User info file not found." )
216+
217+ def _get_user_progress (
218+ self ,
219+ csrftoken ,
220+ session ,
221+ userinfo
222+ ):
223+ try :
224+ result_table = []
225+ headers = _req_header (csrftoken )
226+ cookies = _req_cookies (
227+ session ,
228+ csrftoken ,
229+ )
230+ query = _req_user_progress_v2_query ()
231+ variables = _req_user_progress_variable (userinfo [1 ])
232+
233+ response = requests .post (
234+ LEETCODE_URL ,
235+ headers = headers ,
236+ cookies = cookies ,
237+ data = json .dumps ({"query" : query , "variables" : variables })
238+ )
239+
240+ data = json .loads (response .content )
241+ progress_result = data ["data" ]["userProfileUserQuestionProgressV2" ]
242+
243+ accepted = {x ["difficulty" ]: x ["count" ] for x in progress_result ["numAcceptedQuestions" ]}
244+ failed = {x ["difficulty" ]: x ["count" ] for x in progress_result ["numFailedQuestions" ]}
245+ untouched = {x ["difficulty" ]: x ["count" ] for x in progress_result ["numUntouchedQuestions" ]}
246+ beats = {x ["difficulty" ]: x ["percentage" ] for x in progress_result ["userSessionBeatsPercentage" ]}
247+
248+ for diff in ["EASY" , "MEDIUM" , "HARD" ]:
249+ result_table .append ([
250+ diff .capitalize (),
251+ accepted .get (diff , 0 ),
252+ failed .get (diff , 0 ),
253+ untouched .get (diff , 0 ),
254+ f"{ beats .get (diff ):.2f} " if beats .get (diff ) is not None else "-"
255+ ])
256+ return tabulate (result_table , headers = ["Difficulty" , "Accepted" , "Failed" , "Untouched" , "Beats (%)" ], tablefmt = "fancy_grid" )
257+
258+ except Exception as e :
259+ raise e
0 commit comments