Skip to content
This repository was archived by the owner on Oct 27, 2025. It is now read-only.

Commit 0cbadbd

Browse files
authored
Refractor codeforces codebase. (#27)
* Add NewPage functionality. (cherry picked from commit 2bfac50) * Complete refractoring. * Fix infinite loop. * More changes. Check if page redirected and return error. Fix formatting errors in getSubmissions. * Improve tests.
1 parent 7bd6155 commit 0cbadbd

File tree

15 files changed

+1306
-1298
lines changed

15 files changed

+1306
-1298
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,5 @@
1515
vendor/
1616

1717
.env
18+
19+
tmp/

codeforces/codeforces.go

Lines changed: 38 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@ package codeforces
22

33
import (
44
"fmt"
5+
"os"
6+
"path/filepath"
57
"regexp"
8+
"strconv"
69
"strings"
710

811
"github.com/cp-tools/cpt-lib/v2/util"
@@ -20,6 +23,10 @@ type (
2023
Class string
2124
Group string
2225
}
26+
27+
page struct {
28+
*rod.Page
29+
}
2330
)
2431

2532
// Class type of contest.
@@ -31,8 +38,7 @@ const (
3138

3239
// Errors returned by library.
3340
var (
34-
ErrInvalidSpecifier = fmt.Errorf("invalid specifier data")
35-
errInvalidCredentials = fmt.Errorf("invalid login credentials")
41+
ErrInvalidSpecifier = fmt.Errorf("invalid specifier data")
3642
)
3743

3844
var (
@@ -42,14 +48,6 @@ var (
4248
Browser *rod.Browser
4349
)
4450

45-
// Start initiates the automated browser to use.
46-
func Start(headless bool, userDataDir, bin string) error {
47-
bs, err := util.NewBrowser(headless, userDataDir, bin)
48-
Browser = bs
49-
50-
return err
51-
}
52-
5351
func (arg Args) String() (str string) {
5452
if arg == (Args{}) {
5553
return ""
@@ -66,9 +64,20 @@ func (arg Args) String() (str string) {
6664
return strings.Join(strings.Fields(str), " ")
6765
}
6866

69-
// loginPage returns link to login page.
70-
func loginPage() string {
71-
return fmt.Sprintf("%v/enter", hostURL)
67+
// Start initiates the automated browser to use.
68+
func Start(headless bool, userDataDir, bin string) error {
69+
cacheDir, _ := os.UserCacheDir()
70+
cacheDir = filepath.Join(cacheDir, "cp-tools", "cpt-lib")
71+
72+
return StartWithCacheDir(headless, userDataDir, bin, cacheDir)
73+
}
74+
75+
// StartWithCacheDir is the same as Start, only allows to set cacheDir to use.
76+
func StartWithCacheDir(headless bool, userDataDir, bin, cacheDir string) error {
77+
bs, err := util.NewBrowser(headless, userDataDir, bin, cacheDir)
78+
Browser = bs
79+
80+
return err
7281
}
7382

7483
// Parse passed in specifier string to new Args struct.
@@ -121,75 +130,24 @@ func Parse(str string) (Args, error) {
121130
Class: result["class"],
122131
Group: result["group"],
123132
}
124-
arg.setContestClass()
133+
134+
// Set contest class.
135+
switch {
136+
case arg.Class != "":
137+
break
138+
case len(arg.Group) == 10:
139+
arg.Class = ClassGroup
140+
default:
141+
switch val, _ := strconv.Atoi(arg.Contest); {
142+
case val <= 1e5:
143+
arg.Class = ClassContest
144+
default:
145+
arg.Class = ClassGym
146+
}
147+
}
148+
125149
return arg, nil
126150
}
127151
}
128152
return Args{}, ErrInvalidSpecifier
129153
}
130-
131-
// login tries logging into codeforces using credentials passed.
132-
// Checks if any active session exists before logging in.
133-
//
134-
// If login is successful, returns user handle of logged in session.
135-
// Otherwise, if login fails, returns ErrInvalidCredentials.
136-
//
137-
// By default, option 'remember me' is checked, ensuring the session
138-
// has expiry period of one month from date of last login.
139-
func login(usr, passwd string) (string, error) {
140-
link := loginPage()
141-
page, msg, err := loadPage(link, selCSSFooter)
142-
if err != nil {
143-
return "", err
144-
}
145-
defer page.Close()
146-
147-
if msg != "" {
148-
// there shouldn't be any notification
149-
return "", fmt.Errorf(msg)
150-
}
151-
152-
// check if current user sesion is logged in
153-
if elm := page.MustElements(selCSSHandle).First(); elm != nil {
154-
return clean(elm.MustText()), nil
155-
}
156-
// otherwise, login
157-
158-
// check if username/password are valid
159-
if usr == "" || passwd == "" {
160-
return "", errInvalidCredentials
161-
}
162-
163-
page.MustElement("#handleOrEmail").Input(usr)
164-
page.MustElement("#password").Input(passwd)
165-
if page.MustElement("#remember").MustProperty("checked").Bool() == false {
166-
page.MustElement("#remember").MustClick()
167-
}
168-
page.MustElement(".submit").MustClick()
169-
170-
page.MustElement(selCSSError, selCSSHandle)
171-
if elm := page.MustElements(selCSSHandle); !elm.Empty() {
172-
return clean(elm.First().MustText()), nil
173-
}
174-
175-
return "", errInvalidCredentials
176-
}
177-
178-
func logout() error {
179-
page, msg, err := loadPage(hostURL, selCSSFooter)
180-
if err != nil {
181-
return err
182-
}
183-
defer page.Close()
184-
185-
if msg != "" {
186-
return fmt.Errorf(msg)
187-
}
188-
189-
if page.MustHasR("a", "Logout") {
190-
page.MustElementR("a", "Logout").MustClick()
191-
// page gives a notification on logout
192-
page.Element(selCSSNotif)
193-
}
194-
return nil
195-
}

codeforces/codeforces_test.go

Lines changed: 93 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,40 @@ import (
99
"github.com/joho/godotenv"
1010
)
1111

12+
func login(usr, passwd string) (string, error) {
13+
p, err := loadPage(fmt.Sprintf("%v/enter", hostURL))
14+
if err != nil {
15+
return "", err
16+
}
17+
defer p.Close()
18+
19+
if _, err := p.Race().Element(`#jGrowl .message`).Handle(handleErrMsg).
20+
Element(`#footer`).Do(); err != nil {
21+
return "", err
22+
}
23+
24+
// Check if current user sesion is logged in.
25+
if elm := p.MustElements(`#header a[href^="/profile/"]`).First(); elm != nil {
26+
return elm.MustText(), nil
27+
}
28+
29+
// Otherwise, login.
30+
p.MustElement("#handleOrEmail").Input(usr)
31+
p.MustElement("#password").Input(passwd)
32+
if p.MustElement("#remember").MustProperty("checked").Bool() == false {
33+
p.MustElement("#remember").MustClick()
34+
}
35+
p.MustElement(".submit").MustClick().WaitInvisible()
36+
37+
elm, err := p.Race().Element(`.error`).Handle(handleErrMsg).
38+
Element(`#header a[href^="/profile/"]`).Do()
39+
if err != nil {
40+
return "", err
41+
}
42+
43+
return elm.MustText(), nil
44+
}
45+
1246
func getLoginCredentials() (string, string) {
1347
// setup login access to use
1448
usr := os.Getenv("CODEFORCES_USERNAME")
@@ -22,19 +56,76 @@ func TestMain(m *testing.M) {
2256

2357
_, browserHeadless := os.LookupEnv("BROWSER_HEADLESS")
2458
browserBin := os.Getenv("BROWSER_BINARY")
25-
Start(browserHeadless, "", browserBin)
59+
if err := StartWithCacheDir(browserHeadless, "", browserBin, "tmp"); err != nil {
60+
fmt.Println("Failed to start browser:", err)
61+
os.Exit(1)
62+
}
2663

27-
if _, err := login(getLoginCredentials()); err != nil {
64+
if handle, err := login(getLoginCredentials()); err != nil {
2865
fmt.Println("Login failed:", err)
2966
Browser.Close()
3067
os.Exit(1)
68+
} else {
69+
fmt.Println("Logged in user:", handle)
3170
}
3271
exitCode := m.Run()
3372

3473
Browser.Close()
74+
3575
os.Exit(exitCode)
3676
}
3777

78+
func TestArgs_String(t *testing.T) {
79+
tests := []struct {
80+
name string
81+
arg Args
82+
want string
83+
}{
84+
{
85+
name: "Test #1",
86+
arg: Args{"1234", "", "contest", ""},
87+
want: "1234 (contest)",
88+
},
89+
{
90+
name: "Test #2",
91+
arg: Args{"1234", "b", "contest", ""},
92+
want: "1234 b (contest)",
93+
},
94+
{
95+
name: "Test #3",
96+
arg: Args{"100522", "", "gym", ""},
97+
want: "100522 (gym)",
98+
},
99+
{
100+
name: "Test #4",
101+
arg: Args{"100522", "f1", "gym", ""},
102+
want: "100522 f1 (gym)",
103+
},
104+
{
105+
name: "Test #5",
106+
arg: Args{"201468", "", "group", "Qvv4lz52cT"},
107+
want: "201468 (group/Qvv4lz52cT)",
108+
},
109+
{
110+
name: "Test #6",
111+
arg: Args{"201468", "c1", "group", "Qvv4lz52cT"},
112+
want: "201468 c1 (group/Qvv4lz52cT)",
113+
},
114+
{
115+
name: "Test #7",
116+
arg: Args{},
117+
want: "",
118+
},
119+
}
120+
for _, tt := range tests {
121+
t.Run(tt.name, func(t *testing.T) {
122+
if got := tt.arg.String(); got != tt.want {
123+
t.Errorf("Args.String() = %v, want %v", got, tt.want)
124+
}
125+
})
126+
}
127+
}
128+
38129
func TestParse(t *testing.T) {
39130
type args struct {
40131
str string
@@ -161,98 +252,3 @@ func TestParse(t *testing.T) {
161252
})
162253
}
163254
}
164-
165-
func TestArgs_String(t *testing.T) {
166-
tests := []struct {
167-
name string
168-
arg Args
169-
want string
170-
}{
171-
{
172-
name: "Test #1",
173-
arg: Args{"1234", "", "contest", ""},
174-
want: "1234 (contest)",
175-
},
176-
{
177-
name: "Test #2",
178-
arg: Args{"1234", "b", "contest", ""},
179-
want: "1234 b (contest)",
180-
},
181-
{
182-
name: "Test #3",
183-
arg: Args{"100522", "", "gym", ""},
184-
want: "100522 (gym)",
185-
},
186-
{
187-
name: "Test #4",
188-
arg: Args{"100522", "f1", "gym", ""},
189-
want: "100522 f1 (gym)",
190-
},
191-
{
192-
name: "Test #5",
193-
arg: Args{"201468", "", "group", "Qvv4lz52cT"},
194-
want: "201468 (group/Qvv4lz52cT)",
195-
},
196-
{
197-
name: "Test #6",
198-
arg: Args{"201468", "c1", "group", "Qvv4lz52cT"},
199-
want: "201468 c1 (group/Qvv4lz52cT)",
200-
},
201-
{
202-
name: "Test #7",
203-
arg: Args{},
204-
want: "",
205-
},
206-
}
207-
for _, tt := range tests {
208-
t.Run(tt.name, func(t *testing.T) {
209-
if got := tt.arg.String(); got != tt.want {
210-
t.Errorf("Args.String() = %v, want %v", got, tt.want)
211-
}
212-
})
213-
}
214-
}
215-
216-
func Test_login(t *testing.T) {
217-
logout()
218-
219-
type args struct {
220-
usr string
221-
passwd string
222-
}
223-
tests := []struct {
224-
name string
225-
args args
226-
want string
227-
wantErr bool
228-
}{
229-
{
230-
name: "Test #1",
231-
args: args{"cp-tools", "PleaseTryAgain"},
232-
want: "",
233-
wantErr: true,
234-
},
235-
{
236-
name: "Test #2",
237-
args: args{"", ""},
238-
want: "",
239-
wantErr: true,
240-
},
241-
}
242-
for _, tt := range tests {
243-
t.Run(tt.name, func(t *testing.T) {
244-
got, err := login(tt.args.usr, tt.args.passwd)
245-
if (err != nil) != tt.wantErr {
246-
t.Errorf("login() error = %v, wantErr %v", err, tt.wantErr)
247-
return
248-
}
249-
if got != tt.want {
250-
t.Errorf("login() = %v, want %v", got, tt.want)
251-
}
252-
})
253-
}
254-
255-
// hope nothing goes wrong.
256-
logout()
257-
login(getLoginCredentials())
258-
}

0 commit comments

Comments
 (0)