Skip to content

Commit 4868d67

Browse files
authored
feat: change contest source to CList (#26)
* feat: change contest source to CList * test: add tests for getting contests from BOJ and CList * ci: update node.js.yml * ci: update node.js.yml * ci/cd: update node.js.yml publish-deploy.yml
1 parent 8739fbb commit 4868d67

File tree

6 files changed

+147
-151
lines changed

6 files changed

+147
-151
lines changed

.github/workflows/node.js.yml

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ jobs:
1919
node-version: [ 23.x ]
2020
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
2121

22+
2223
steps:
2324
- uses: actions/checkout@v4
2425
- name: Use Node.js ${{ matrix.node-version }}
@@ -34,13 +35,15 @@ jobs:
3435
echo "BOJ_USER_AGENT=$BOJ_USER_AGENT" >> .env
3536
echo "MONGODB_URI=$MONGODB_URI" >> .env
3637
echo "SOLVEDAC_TOKEN=$SOLVEDAC_TOKEN" >> .env
38+
echo "CLIST_API_KEY=$CLIST_API_KEY" >> .env
3739
env:
38-
PORT: ${{ secrets.ENV_PORT }}
39-
BOJ_AUTO_LOGIN: ${{ secrets.ENV_BOJ_AUTO_LOGIN }}
40-
BOJ_ONLINE_JUDGE: ${{ secrets.ENV_BOJ_ONLINE_JUDGE }}
41-
BOJ_USER_AGENT: ${{ secrets.ENV_BOJ_USER_AGENT }}
42-
MONGODB_URI: ${{ secrets.ENV_MONGODB_URI }}
43-
SOLVEDAC_TOKEN: ${{ secrets.ENV_SOLVEDAC_TOKEN }}
40+
PORT: ${{ secrets.PORT }}
41+
BOJ_AUTO_LOGIN: ${{ secrets.BOJ_AUTO_LOGIN }}
42+
BOJ_ONLINE_JUDGE: ${{ secrets.BOJ_ONLINE_JUDGE }}
43+
BOJ_USER_AGENT: ${{ secrets.BOJ_USER_AGENT }}
44+
MONGODB_URI: ${{ secrets.MONGODB_URI }}
45+
SOLVEDAC_TOKEN: ${{ secrets.SOLVEDAC_TOKEN }}
46+
CLIST_API_KEY: ${{ secrets.CLIST_API_KEY }}
4447
- run: npm ci
4548
- run: npm run build --if-present
4649
- run: npm test

.github/workflows/publish-deploy.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ jobs:
6666
- name: executing remote ssh commands using password
6767
uses: appleboy/ssh-action@v1.0.3
6868
env:
69-
PORT: ${{ secrets.ENV_PORT }}
69+
PORT: ${{ secrets.PORT }}
7070
with:
7171
host: ${{ secrets.SSH_HOST }}
7272
port: ${{ secrets.SSH_PORT }}

src/entities/contest.entity.ts

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -41,23 +41,42 @@ export class Contest {
4141
venue: string,
4242
name: string,
4343
url: string,
44-
startTime: string,
45-
endTime: string,
44+
startTime: Date,
45+
endTime: Date,
4646
badge?: string,
4747
background?: string,
4848
) {
4949
this.venue = venue;
5050
this.name = name;
5151
this.url = url;
52-
this.startTime = startTime;
53-
this.endTime = endTime;
52+
this.startTime = startTime.toISOString();
53+
this.endTime = endTime.toISOString();
5454
this.badge = badge;
5555
this.background = background;
5656
}
57+
58+
static fromCList(data: { event: string; start: string; end: string; href: string; resource_id: number }) {
59+
return new Contest(
60+
clistMap[data.resource_id] ?? 'Unknown',
61+
data.event,
62+
data.href,
63+
new Date(data.start + '.000Z'),
64+
new Date(data.end + '.000Z'),
65+
);
66+
}
5767
}
5868

69+
const clistMap: Record<number, string> = {
70+
1: 'Codeforces',
71+
25: 'USACO',
72+
86: 'ICPC',
73+
141: 'ICPC',
74+
93: 'AtCoder',
75+
102: 'LeetCode',
76+
};
77+
5978
export class ContestList {
60-
ended: Contest[];
61-
ongoing: Contest[];
62-
upcoming: Contest[];
79+
ended: Contest[] = [];
80+
ongoing: Contest[] = [];
81+
upcoming: Contest[] = [];
6382
}

src/modules/boj/repository.spec.ts

Lines changed: 24 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { Contest } from '@entities/contest.entity';
21
import { HttpModule } from '@nestjs/axios';
32
import { ConfigModule } from '@nestjs/config';
43
import { Test } from '@nestjs/testing';
@@ -17,6 +16,30 @@ describe('BojRepository', () => {
1716
repository = module.get<BojRepository>(BojRepository);
1817
});
1918

19+
describe('Get BOJ contests', () => {
20+
it('should return ContestList', async () => {
21+
const contests = await repository.getContestsFromBoj();
22+
expect(contests).toHaveProperty('ended');
23+
expect(contests.ended).toBeInstanceOf(Array);
24+
expect(contests).toHaveProperty('upcoming');
25+
expect(contests.upcoming).toBeInstanceOf(Array);
26+
expect(contests).toHaveProperty('ongoing');
27+
expect(contests.ongoing).toBeInstanceOf(Array);
28+
}, 10000);
29+
});
30+
31+
describe('Get CList contests', () => {
32+
it('should return ContestList', async () => {
33+
const contests = await repository.getContestsFromCList();
34+
expect(contests).toHaveProperty('ended');
35+
expect(contests.ended).toBeInstanceOf(Array);
36+
expect(contests).toHaveProperty('upcoming');
37+
expect(contests.upcoming).toBeInstanceOf(Array);
38+
expect(contests).toHaveProperty('ongoing');
39+
expect(contests.ongoing).toBeInstanceOf(Array);
40+
}, 10000);
41+
});
42+
2043
describe('getUserProblems', () => {
2144
it('should return problems', async () => {
2245
const problems = await repository.getUserProblems('w8385');
@@ -29,36 +52,6 @@ describe('BojRepository', () => {
2952
});
3053
});
3154

32-
describe('getEndedContests', () => {
33-
it('should return contests', async () => {
34-
const contests = await repository.getEndedContests();
35-
expect(contests).toBeInstanceOf(Array<Contest>);
36-
contests.forEach((contest) => {
37-
expect(contest).toBeInstanceOf(Contest);
38-
});
39-
});
40-
});
41-
42-
describe('getOngoingContests', () => {
43-
it('should return contests', async () => {
44-
const contests = await repository.getOngoingContests();
45-
expect(contests).toBeInstanceOf(Array<Contest>);
46-
contests.forEach((contest) => {
47-
expect(contest).toBeInstanceOf(Contest);
48-
});
49-
});
50-
});
51-
52-
describe('getUpcomingContests', () => {
53-
it('should return contests', async () => {
54-
const contests = await repository.getUpcomingContests();
55-
expect(contests).toBeInstanceOf(Array<Contest>);
56-
contests.forEach((contest) => {
57-
expect(contest).toBeInstanceOf(Contest);
58-
});
59-
});
60-
});
61-
6255
describe('getSSUInfo', () => {
6356
it('should return SSU info', async () => {
6457
const ssuInfo = await repository.getSSUInfo();

src/modules/boj/repository.ts

Lines changed: 61 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
1-
import { Contest } from '@entities/contest.entity';
1+
import { Contest, ContestList } from '@entities/contest.entity';
22
import { HttpService } from '@nestjs/axios';
33
import { Injectable } from '@nestjs/common';
44
import { ConfigService } from '@nestjs/config';
55
import * as cheerio from 'cheerio';
6-
import { Cheerio } from 'cheerio';
7-
import { AnyNode } from 'domhandler';
86
import * as process from 'node:process';
97

108
@Injectable()
@@ -56,62 +54,94 @@ export class BojRepository {
5654
return problems;
5755
}
5856

59-
async getEndedContests(): Promise<Contest[]> {
60-
const endedUrl = 'https://www.acmicpc.net/contest/official/list';
57+
async getContestsFromBoj(): Promise<ContestList> {
58+
const url = 'https://www.acmicpc.net/contest/official/list';
6159

6260
const response = cheerio.load(
6361
await this.httpService.axiosRef
64-
.get<string>(endedUrl, {
62+
.get<string>(url, {
6563
headers: {
6664
'User-Agent': this.configService.get<string>('BOJ_USER_AGENT'),
6765
},
6866
})
69-
7067
.then((res) => res.data),
7168
);
7269

73-
const contests: Contest[] = [];
70+
const contests: ContestList = new ContestList();
7471

7572
const rows = response(
7673
'body > div.wrapper > div.container.content > div.row > div:nth-child(2) > div > table > tbody > tr',
7774
);
7875
for (let i = 0; i < rows.length; i++) {
79-
if (rows.eq(i).find('td:nth-child(6)').text() !== '종료') {
80-
continue;
81-
}
82-
8376
const venue = 'BOJ Open';
8477
const name = rows.eq(i).find('td:nth-child(1) > a').text();
8578
const url = 'https://www.acmicpc.net' + rows.eq(i).find('td:nth-child(1) > a').attr('href');
8679
const startDate = new Date(
8780
1000 * parseInt(<string>rows.eq(i).find('td:nth-child(4) > span').attr('data-timestamp')),
88-
).toISOString();
81+
);
8982
const endDate = new Date(
9083
1000 * parseInt(<string>rows.eq(i).find('td:nth-child(5) > span').attr('data-timestamp')),
91-
).toISOString();
92-
93-
contests.push(new Contest(venue, name, url, startDate, endDate));
84+
);
85+
86+
const contest = new Contest(venue, name, url, startDate, endDate);
87+
if (endDate < new Date()) {
88+
contests.ended.push(contest);
89+
} else if (new Date() < startDate) {
90+
contests.upcoming.push(contest);
91+
} else {
92+
contests.ongoing.push(contest);
93+
}
9494
}
9595

9696
return contests;
9797
}
9898

99-
async getOngoingContests(): Promise<Contest[]> {
100-
const response = await this.otherResponse();
101-
102-
if (response('.col-md-12').length < 6) {
103-
return [];
104-
}
105-
106-
return this.contestsFromOther(response, 3);
107-
}
99+
async getContestsFromCList(): Promise<ContestList> {
100+
const url = 'https://clist.by/api/v4/contest/';
101+
const headers = {
102+
Authorization: `${process.env.CLIST_API_KEY}`,
103+
};
104+
const params = {
105+
resource_id__in: '1, 25, 86, 141, 93, 102',
106+
order_by: '-start',
107+
};
108108

109-
async getUpcomingContests(): Promise<Contest[]> {
110-
const response = await this.otherResponse();
109+
const response = await this.httpService.axiosRef.get<{
110+
objects: {
111+
event: string;
112+
start: string;
113+
end: string;
114+
href: string;
115+
resource_id: number;
116+
}[];
117+
}>(url, {
118+
headers: headers,
119+
params: params,
120+
});
111121

112-
const rowIndex = response('.col-md-12').length === 6 ? 5 : 3;
122+
const clist: {
123+
event: string;
124+
start: string;
125+
end: string;
126+
href: string;
127+
resource_id: number;
128+
}[] = response.data.objects;
129+
130+
const contests: ContestList = new ContestList();
131+
for (const contest of clist) {
132+
const startDate = new Date(contest.start);
133+
const endDate = new Date(contest.end);
134+
135+
if (endDate < new Date()) {
136+
contests.ended.push(Contest.fromCList(contest));
137+
} else if (new Date() < startDate) {
138+
contests.upcoming.push(Contest.fromCList(contest));
139+
} else {
140+
contests.ongoing.push(Contest.fromCList(contest));
141+
}
142+
}
113143

114-
return this.contestsFromOther(response, rowIndex);
144+
return contests;
115145
}
116146

117147
async getSSUInfo() {
@@ -181,10 +211,10 @@ export class BojRepository {
181211
return ranking;
182212
}
183213

184-
async getBaechu() {
214+
async getBaechu(): Promise<Record<string, { badge: string; background: string }>> {
185215
const url = 'https://raw.githubusercontent.com/kiwiyou/baechu/main/db.json';
186216

187-
const data: Record<string, Record<string, string>> = {};
217+
const data: Record<string, { badge: string; background: string }> = {};
188218
await this.httpService.axiosRef.get<Record<string, { badge: string; background: string }>>(url).then((res) => {
189219
for (const contestId in res.data) {
190220
const contest: Record<string, string> = res.data[contestId];
@@ -197,43 +227,4 @@ export class BojRepository {
197227

198228
return data;
199229
}
200-
201-
private async otherResponse() {
202-
const otherUrl = 'https://www.acmicpc.net/contest/other/list';
203-
204-
return cheerio.load(
205-
await this.httpService.axiosRef
206-
.get<string>(otherUrl, {
207-
headers: {
208-
'User-Agent': this.configService.get<string>('BOJ_USER_AGENT'),
209-
Cookie: 'bojautologin=' + process.env.BOJ_AUTO_LOGIN + ';',
210-
},
211-
})
212-
.then((res) => res.data),
213-
);
214-
}
215-
216-
private contestsFromOther(response: any, rowIndex: number): Contest[] {
217-
const contests: Contest[] = [];
218-
219-
const rows = response(
220-
`body > div.wrapper > div.container.content > div.row > div:nth-child(${rowIndex}) > div > table > tbody > tr`,
221-
) as Cheerio<AnyNode>;
222-
223-
for (let i = 0; i < rows.length; i++) {
224-
const venue = rows.eq(i).find('td:nth-child(1)').text().trim();
225-
const name = rows.eq(i).find('td:nth-child(2)').text().trim();
226-
const url = rows.eq(i).find('td:nth-child(2) > a').attr('href') ?? ''; // `null` 방지
227-
const startTime = new Date(
228-
1000 * Number(rows.eq(i).find('td:nth-child(3) > span').attr('data-timestamp') ?? 0),
229-
).toISOString();
230-
const endTime = new Date(
231-
1000 * Number(rows.eq(i).find('td:nth-child(4) > span').attr('data-timestamp') ?? 0),
232-
).toISOString();
233-
234-
contests.push(new Contest(venue, name, url, startTime, endTime));
235-
}
236-
237-
return contests;
238-
}
239230
}

0 commit comments

Comments
 (0)