codeforces/
lib.rs

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/// The error returned.
30#[derive(Debug)]
31pub enum Error {
32    /// Occurred from within reqwest.
33    Http(HttpError),
34    /// Decoding error,
35    Decode(serde_json::Error),
36    /// Sent back from codeforces.
37    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
72/// The result type.
73pub type Result<T> = std::result::Result<T, Error>;
74
75/// A codeforces user.
76#[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    /// URL to the profile of the user.
100    pub fn profile_url(&self) -> String {
101        format!("https://codeforces.com/profile/{}", self.handle)
102    }
103
104    /// The color of their username.
105    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/// An user's rating change.
132#[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/// The scoring type of a contest.
145#[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/// The current phase of the contest.
163#[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/// A single contest.
191#[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    /// URL to the contest.
216    pub fn url(&self) -> String {
217        format!("https://codeforces.com/contests/{}", self.id)
218    }
219}
220
221/// The type of a problem.
222#[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/// Represents a problem.
239#[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/// A team member.
254#[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/// A group of participants.
288#[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/// Either the result is Preliminary or Final
302#[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/// One party's result on a particular problem.
323#[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/// A row in the scoreboard.
335#[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/// Represents a submission.
455///
456/// https://codeforces.com/apiHelp/objects#Submission
457#[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
475/// API methods described on Codeforces API page.
476impl User {
477    /// Returns information about one or several users.
478    ///
479    /// https://codeforces.com/apiHelp/methods#user.info
480    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    /// Returns the list users who have participated in at least one rated contest.
497    ///
498    /// The return list of Users are sorted by decreasing order of rating.
499    ///
500    /// https://codeforces.com/apiHelp/methods#user.ratedList
501    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    /// Returns rating history of the specified user.
515    ///
516    /// https://codeforces.com/apiHelp/methods#user.rating
517    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    /// Returns submissions of specified user.
531    ///
532    /// https://codeforces.com/apiHelp/methods#user.status
533    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/// Build a contest ranking request.
554#[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    /// Put a limit on the number of records returned.
565    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    /// Set a list of handles.
572    pub fn handles(&mut self, handles: Vec<String>) -> &mut Self {
573        self.handles = Some(handles);
574        self
575    }
576
577    /// Set a specific room.
578    pub fn room(&mut self, room: u64) -> &mut Self {
579        self.room = Some(room);
580        self
581    }
582
583    /// Allow unofficial contestants.
584    pub fn allow_unofficial(&mut self, value: bool) -> &mut Self {
585        self.allow_unofficial = value;
586        self
587    }
588}
589
590/// Consumes self and return a query list.
591impl 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
606/// API methods described on Codeforces API page.
607impl Contest {
608    /// Gets a list of all contests.
609    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    /// Gets the standings of a contest.
623    ///
624    /// https://codeforces.com/apiHelp/methods#contest.standings
625    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
656/// APIs provided as methods.
657impl User {
658    /// Gets a list of rating changes of the current user.
659    pub async fn rating_changes(&self, client: &Client) -> Result<Vec<RatingChange>> {
660        Self::rating(client, &self.handle).await
661    }
662
663    /// Gets a list of most recent submissions.
664    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
674/// APIs provided as methods.
675impl Contest {
676    /// Get the standings of the current contest.
677    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}