1use reqwest::Error as HttpError;
2use serde::Deserialize;
3use std::{borrow::Borrow, fmt};
4
5pub mod client;
6pub use client::Client;
7
8#[cfg(test)]
9mod test;
10
11#[derive(Debug, Deserialize)]
12#[serde(bound(deserialize = "T: for<'t> Deserialize<'t>"))]
13struct CFResult<T: for<'t> Deserialize<'t>> {
14 result: Option<T>,
15 comment: Option<String>,
16}
17
18impl<T: for<'t> Deserialize<'t>> From<CFResult<T>> for Result<T> {
19 fn from(c: CFResult<T>) -> Self {
20 match c.result {
21 Some(v) => Ok(v),
22 None => Err(Error::Codeforces(
23 c.comment.unwrap_or("Unknown error".to_owned()),
24 )),
25 }
26 }
27}
28
29#[derive(Debug)]
31pub enum Error {
32 Http(HttpError),
34 Decode(serde_json::Error),
36 Codeforces(String),
38}
39
40impl fmt::Display for Error {
41 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
42 match self {
43 Error::Http(ref e) => write!(f, "HTTP: {}", e),
44 Error::Decode(ref e) => write!(f, "Decode: {}", e),
45 Error::Codeforces(ref s) => write!(f, "Codeforces: {}", s),
46 }
47 }
48}
49
50impl std::error::Error for Error {
51 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
52 match self {
53 Error::Http(ref e) => Some(e),
54 Error::Decode(ref e) => Some(e),
55 Error::Codeforces(_) => None,
56 }
57 }
58}
59
60impl From<HttpError> for Error {
61 fn from(e: HttpError) -> Self {
62 Error::Http(e)
63 }
64}
65
66impl From<serde_json::Error> for Error {
67 fn from(e: serde_json::Error) -> Self {
68 Error::Decode(e)
69 }
70}
71
72pub type Result<T> = std::result::Result<T, Error>;
74
75#[derive(Debug, Deserialize, Clone)]
77#[serde(rename_all = "camelCase")]
78pub struct User {
79 pub handle: String,
80 pub email: Option<String>,
81 pub first_name: Option<String>,
82 pub last_name: Option<String>,
83 pub country: Option<String>,
84 pub organization: Option<String>,
85 pub city: Option<String>,
86 pub contribution: i64,
87 pub rank: Option<String>,
88 pub max_rank: Option<String>,
89 pub rating: Option<i64>,
90 pub max_rating: Option<i64>,
91 pub last_online_time_seconds: u64,
92 pub registration_time_seconds: u64,
93 pub friend_of_count: u64,
94 pub avatar: String,
95 pub title_photo: String,
96}
97
98impl User {
99 pub fn profile_url(&self) -> String {
101 format!("https://codeforces.com/profile/{}", self.handle)
102 }
103
104 pub fn color(&self) -> u64 {
106 match self.rating {
107 None => 0x000000,
108 Some(rating) => {
109 if rating < 1200 {
110 0x808080
111 } else if rating < 1400 {
112 0x008000
113 } else if rating < 1600 {
114 0x03a89e
115 } else if rating < 1900 {
116 0x0000ff
117 } else if rating < 2100 {
118 0xaa00aa
119 } else if rating < 2300 {
120 0xbbbb00
121 } else if rating < 2400 {
122 0xff8c00
123 } else {
124 0xff0000
125 }
126 }
127 }
128 }
129}
130
131#[derive(Debug, Deserialize, Clone)]
133#[serde(rename_all = "camelCase")]
134pub struct RatingChange {
135 pub contest_id: u64,
136 pub contest_name: String,
137 pub handle: String,
138 pub rank: u64,
139 pub rating_update_time_seconds: u64,
140 pub old_rating: i64,
141 pub new_rating: i64,
142}
143
144#[derive(Debug, Deserialize, Clone, Copy, PartialEq, Eq)]
146pub enum ContestType {
147 CF,
148 IOI,
149 ICPC,
150}
151
152impl fmt::Display for ContestType {
153 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
154 match self {
155 ContestType::CF => write!(f, "Codeforces"),
156 ContestType::IOI => write!(f, "IOI-based"),
157 ContestType::ICPC => write!(f, "ACM ICPC-based"),
158 }
159 }
160}
161
162#[derive(Debug, Deserialize, Clone, Copy, PartialEq, Eq)]
164#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
165pub enum ContestPhase {
166 Before,
167 Coding,
168 PendingSystemTest,
169 SystemTest,
170 Finished,
171}
172
173impl fmt::Display for ContestPhase {
174 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
175 use ContestPhase::*;
176 write!(
177 f,
178 "{}",
179 match self {
180 Before => "Contest hasn't started",
181 Coding => "Contest is currently running",
182 PendingSystemTest => "Pending system test",
183 SystemTest => "System test running",
184 Finished => "Finished",
185 }
186 )
187 }
188}
189
190#[derive(Debug, Deserialize, Clone)]
192#[serde(rename_all = "camelCase")]
193pub struct Contest {
194 pub id: u64,
195 pub name: String,
196 #[serde(rename = "type")]
197 pub contest_type: ContestType,
198 pub phase: ContestPhase,
199 pub frozen: bool,
200 pub duration_seconds: u64,
201 pub start_time_seconds: Option<u64>,
202 pub relative_time_seconds: Option<i64>,
203 pub prepared_by: Option<String>,
204 pub website_url: Option<String>,
205 pub description: Option<String>,
206 pub difficulty: Option<u8>,
207 pub kind: Option<String>,
208 pub icpc_region: Option<String>,
209 pub country: Option<String>,
210 pub city: Option<String>,
211 pub season: Option<String>,
212}
213
214impl Contest {
215 pub fn url(&self) -> String {
217 format!("https://codeforces.com/contests/{}", self.id)
218 }
219}
220
221#[derive(Debug, Deserialize, Clone, Copy, PartialEq, Eq)]
223#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
224pub enum ProblemType {
225 Programming,
226 Question,
227}
228
229impl fmt::Display for ProblemType {
230 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
231 match self {
232 ProblemType::Programming => write!(f, "Programming"),
233 ProblemType::Question => write!(f, "Question"),
234 }
235 }
236}
237
238#[derive(Debug, Deserialize, Clone)]
240#[serde(rename_all = "camelCase")]
241pub struct Problem {
242 pub contest_id: Option<u64>,
243 pub problemset_name: Option<String>,
244 pub index: String,
245 pub name: String,
246 #[serde(rename = "type")]
247 pub problem_type: ProblemType,
248 pub points: Option<f64>,
249 pub rating: Option<u64>,
250 pub tags: Vec<String>,
251}
252
253#[derive(Debug, Deserialize, Clone)]
255#[serde(rename_all = "camelCase")]
256pub struct TeamMember {
257 pub handle: String,
258}
259
260#[derive(Debug, Deserialize, Clone, Copy, PartialEq, Eq)]
261#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
262pub enum ParticipantType {
263 Contestant,
264 Practice,
265 Virtual,
266 Manager,
267 OutOfCompetition,
268}
269
270impl fmt::Display for ParticipantType {
271 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
272 use ParticipantType::*;
273 write!(
274 f,
275 "{}",
276 match self {
277 Contestant => "Contestant",
278 Practice => "Practice",
279 Virtual => "Virtual",
280 Manager => "Manager",
281 OutOfCompetition => "OutOfCompetition",
282 }
283 )
284 }
285}
286
287#[derive(Debug, Deserialize, Clone)]
289#[serde(rename_all = "camelCase")]
290pub struct Party {
291 pub contest_id: Option<u64>,
292 pub members: Vec<TeamMember>,
293 pub participant_type: ParticipantType,
294 pub team_id: Option<u64>,
295 pub team_name: Option<String>,
296 pub ghost: bool,
297 pub room: Option<u64>,
298 pub start_time_seconds: Option<u64>,
299}
300
301#[derive(Debug, Deserialize, Clone, Copy, PartialEq, Eq)]
303#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
304pub enum ProblemResultType {
305 Preliminary,
306 Final,
307}
308
309impl fmt::Display for ProblemResultType {
310 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
311 write!(
312 f,
313 "{}",
314 match self {
315 ProblemResultType::Preliminary => "Preliminary",
316 ProblemResultType::Final => "Final",
317 }
318 )
319 }
320}
321
322#[derive(Debug, Deserialize, Clone, PartialEq)]
324#[serde(rename_all = "camelCase")]
325pub struct ProblemResult {
326 pub points: f64,
327 pub penalty: Option<u64>,
328 pub rejected_attempt_count: u64,
329 #[serde(rename = "type")]
330 pub result_type: ProblemResultType,
331 pub best_submission_time_seconds: Option<u64>,
332}
333
334#[derive(Debug, Deserialize, Clone)]
336#[serde(rename_all = "camelCase")]
337pub struct RanklistRow {
338 pub party: Party,
339 pub rank: u64,
340 pub points: f64,
341 pub penalty: u64,
342 pub successful_hack_count: u64,
343 pub unsuccessful_hack_count: u64,
344 pub problem_results: Vec<ProblemResult>,
345 pub last_submission_time_seconds: Option<u64>,
346}
347
348#[derive(Debug, Deserialize, Clone, Copy, PartialEq, Eq)]
349#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
350pub enum Verdict {
351 Failed,
352 Ok,
353 Partial,
354 CompilationError,
355 RuntimeError,
356 WrongAnswer,
357 PresentationError,
358 TimeLimitExceeded,
359 MemoryLimitExceeded,
360 IdlenessLimitExceeded,
361 SecurityViolated,
362 Crashed,
363 InputPreparationCrashed,
364 Challenged,
365 Skipped,
366 Testing,
367 Rejected,
368}
369
370impl fmt::Display for Verdict {
371 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
372 use Verdict::*;
373 write!(
374 f,
375 "{}",
376 match self {
377 Failed => "Failed",
378 Ok => "Ok",
379 Partial => "Partial",
380 CompilationError => "Compilation Error",
381 RuntimeError => "Runtime Error",
382 WrongAnswer => "Wrong Answer",
383 PresentationError => "Presentation Error",
384 TimeLimitExceeded => "Time Limit Exceeded",
385 MemoryLimitExceeded => "Memory Limit Exceeded",
386 IdlenessLimitExceeded => "Idleness Limit Exceeded",
387 SecurityViolated => "Security Violated",
388 Crashed => "Crashed",
389 InputPreparationCrashed => "Input Preparation Crashed",
390 Challenged => "Challenged",
391 Skipped => "Skipped",
392 Testing => "Testing",
393 Rejected => "Rejected",
394 }
395 )
396 }
397}
398
399#[derive(Debug, Deserialize, Clone, Copy, PartialEq, Eq)]
400#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
401pub enum SubmissionTestSet {
402 Samples,
403 Pretests,
404 Tests,
405 Challenges,
406 #[serde(rename = "TESTS1")]
407 TestSet1,
408 #[serde(rename = "TESTS2")]
409 TestSet2,
410 #[serde(rename = "TESTS3")]
411 TestSet3,
412 #[serde(rename = "TESTS4")]
413 TestSet4,
414 #[serde(rename = "TESTS5")]
415 TestSet5,
416 #[serde(rename = "TESTS6")]
417 TestSet6,
418 #[serde(rename = "TESTS7")]
419 TestSet7,
420 #[serde(rename = "TESTS8")]
421 TestSet8,
422 #[serde(rename = "TESTS9")]
423 TestSet9,
424 #[serde(rename = "TESTS10")]
425 TestSet10,
426}
427
428impl fmt::Display for SubmissionTestSet {
429 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
430 use SubmissionTestSet::*;
431 write!(
432 f,
433 "{}",
434 match self {
435 Samples => "Samples",
436 Pretests => "Pretests",
437 Tests => "Tests",
438 Challenges => "Challenges",
439 TestSet1 => "Test Set 1",
440 TestSet2 => "Test Set 2",
441 TestSet3 => "Test Set 3",
442 TestSet4 => "Test Set 4",
443 TestSet5 => "Test Set 5",
444 TestSet6 => "Test Set 6",
445 TestSet7 => "Test Set 7",
446 TestSet8 => "Test Set 8",
447 TestSet9 => "Test Set 9",
448 TestSet10 => "Test Set 10",
449 }
450 )
451 }
452}
453
454#[derive(Debug, Deserialize, Clone)]
458#[serde(rename_all = "camelCase")]
459pub struct Submission {
460 pub id: u64,
461 pub contest_id: Option<u64>,
462 pub creation_time_seconds: u64,
463 pub relative_time_seconds: Option<u64>,
464 pub problem: Problem,
465 pub author: Party,
466 pub programming_language: String,
467 pub verdict: Option<Verdict>,
468 #[serde(rename = "testset")]
469 pub test_set: SubmissionTestSet,
470 pub passed_test_count: u64,
471 pub time_consumed_millis: u64,
472 pub memory_consumed_bytes: u64,
473}
474
475impl User {
477 pub async fn info<T>(client: &Client, handles: &[T]) -> Result<Vec<User>>
481 where
482 T: Borrow<str>,
483 {
484 let users: CFResult<_> = client
485 .borrow()
486 .await
487 .get("https://codeforces.com/api/user.info")
488 .query(&[("handles", handles.join(";"))])
489 .send()
490 .await?
491 .json()
492 .await?;
493 users.into()
494 }
495
496 pub async fn rated_list(client: &Client, active_only: bool) -> Result<Vec<User>> {
502 let users: CFResult<_> = client
503 .borrow()
504 .await
505 .get("https://codeforces.com/api/user.ratedList")
506 .query(&[("activeOnly", active_only)])
507 .send()
508 .await?
509 .json()
510 .await?;
511 users.into()
512 }
513
514 pub async fn rating(client: &Client, handle: &str) -> Result<Vec<RatingChange>> {
518 let changes: CFResult<_> = client
519 .borrow()
520 .await
521 .get("https://codeforces.com/api/user.rating")
522 .query(&[("handle", handle)])
523 .send()
524 .await?
525 .json()
526 .await?;
527 changes.into()
528 }
529
530 pub async fn status(
534 client: &Client,
535 handle: &str,
536 from: u64,
537 count: u64,
538 ) -> Result<Vec<Submission>> {
539 let submissions: CFResult<_> = client
540 .borrow()
541 .await
542 .get("https://codeforces.com/api/user.status")
543 .query(&[("handle", handle)])
544 .query(&[("from", from.max(1)), ("count", count.min(1))])
545 .send()
546 .await?
547 .json()
548 .await?;
549 submissions.into()
550 }
551}
552
553#[derive(Debug, Default)]
555pub struct ContestRankingsBuilder {
556 from: Option<u64>,
557 count: Option<u64>,
558 handles: Option<Vec<String>>,
559 room: Option<u64>,
560 allow_unofficial: bool,
561}
562
563impl ContestRankingsBuilder {
564 pub fn limit(&mut self, from: u64, count: u64) -> &mut Self {
566 self.from = Some(from);
567 self.count = Some(count);
568 self
569 }
570
571 pub fn handles(&mut self, handles: Vec<String>) -> &mut Self {
573 self.handles = Some(handles);
574 self
575 }
576
577 pub fn room(&mut self, room: u64) -> &mut Self {
579 self.room = Some(room);
580 self
581 }
582
583 pub fn allow_unofficial(&mut self, value: bool) -> &mut Self {
585 self.allow_unofficial = value;
586 self
587 }
588}
589
590impl From<ContestRankingsBuilder> for Vec<(&'static str, String)> {
592 fn from(c: ContestRankingsBuilder) -> Self {
593 vec![
594 Some(("allowOfficial", c.allow_unofficial.to_string())),
595 c.from.map(|v| ("from", v.to_string())),
596 c.count.map(|v| ("count", v.to_string())),
597 c.handles.map(|v| ("handles", v.join(";"))),
598 c.room.map(|v| ("room", v.to_string())),
599 ]
600 .into_iter()
601 .filter_map(|v| v)
602 .collect()
603 }
604}
605
606impl Contest {
608 pub async fn list(client: &Client, with_gym: bool) -> Result<Vec<Contest>> {
610 let v: CFResult<_> = client
611 .borrow()
612 .await
613 .get("https://codeforces.com/api/contest.list")
614 .query(&[("gym", with_gym)])
615 .send()
616 .await?
617 .json()
618 .await?;
619 v.into()
620 }
621
622 pub async fn standings(
626 client: &Client,
627 contest_id: u64,
628 opts: impl FnOnce(&mut ContestRankingsBuilder) -> &mut ContestRankingsBuilder,
629 ) -> Result<(Contest, Vec<Problem>, Vec<RanklistRow>)> {
630 #[derive(Deserialize)]
631 struct Middle {
632 contest: Contest,
633 problems: Vec<Problem>,
634 rows: Vec<RanklistRow>,
635 }
636
637 let mut b = ContestRankingsBuilder::default();
638 opts(&mut b);
639
640 let v: CFResult<Middle> = client
641 .borrow()
642 .await
643 .get("https://codeforces.com/api/contest.standings")
644 .query(&[("contestId", contest_id)])
645 .query(&Vec::<(&'static str, String)>::from(b))
646 .send()
647 .await?
648 .json()
649 .await?;
650 let v: Middle = Result::<_>::from(v)?;
651
652 Ok((v.contest, v.problems, v.rows))
653 }
654}
655
656impl User {
658 pub async fn rating_changes(&self, client: &Client) -> Result<Vec<RatingChange>> {
660 Self::rating(client, &self.handle).await
661 }
662
663 pub async fn submissions(
665 &self,
666 client: &Client,
667 from: u64,
668 count: u64,
669 ) -> Result<Vec<Submission>> {
670 Self::status(client, &self.handle, from, count).await
671 }
672}
673
674impl Contest {
676 pub async fn get_standings(
678 &self,
679 client: &Client,
680 opts: impl FnOnce(&mut ContestRankingsBuilder) -> &mut ContestRankingsBuilder,
681 ) -> Result<(Vec<Problem>, Vec<RanklistRow>)> {
682 let (_, problems, rows) = Self::standings(client, self.id, opts).await?;
683 Ok((problems, rows))
684 }
685}