iTranslated by AI

The content below is an AI-generated translation. This is an experimental feature, and may contain errors. View original article
🤖

Building a Discord Bot for AtCoder

に公開

This article is for Day 2 of the Asakatsu-bu Advent Calendar 2024.

https://github.com/1STEP621/atcoder-bot-rs

This is a bot that reports problems solved (AC) by registered users every day at midnight.

It was originally written in Python, but I decided to rewrite it in Rust.

What was good about using Rust

I was happy that I could use iterators to cleanly handle the generation of Embeds.
The specifications are as follows:

  • The title of the Embed is "(Username)'s AC Problems from Yesterday," which serves as a link to the user.
  • The Embed fields list the solved problem's title, Difficulty, color, language, submission link, etc.
  • The color on the left side of the Embed is set to the color of the highest Difficulty among the solved problems; if the Difficulty of all solved problems is unknown, it is set to black (some problems, such as APG4b, may have a null Difficulty).
  • If there are 25 or more fields, the Embed is split (this is because Discord's specifications do not allow sending more than 25 fields).


(I'm posting a screenshot because I wanted to show the Inlay Hints.)

In Rust, since you can use .chunks(25) to turn an array into an iterator with 25-item chunks, there's no longer a need to maintain an index and calculate remainders to split them.
Also, by moving the part that converts problems into fields to an impl of the problem information struct and calling p.to_field(), I was able to keep the implementation clean.

Implementation on the impl side
    impl ProblemDetail {
        fn to_field(&self) -> (String, String, bool) {
            (
                self.title.clone(),
                format!(
                    "{} | {} | [提出]({})",
                    self.difficulty
                        .map(|d| {
                            let diff = difficulty::normalize(d);
                            format!("{}({})", diff, difficulty::Color::from(diff))
                        })
                        .unwrap_or("不明".into()),
                    self.language,
                    self.submission_url
                ),
                false,
            )
        }
    }

Furthermore, by using .map, I could apply calculations to Difficulty values that might be None while keeping None as None, which was very satisfying. It is also great that by deriving the Ord trait for the Color enum, I can call max on impl Iterator<Item = Color>. (By doing this, since Color::Black is the lowest rank, it won't be selected unless the Difficulty of all problems is unknown.)

What was challenging about using Rust

Defining types for API responses is tedious.

It was tough because I had to define types to turn API response results into structs, and if even a small part was incorrect, parsing would fail. Since there is (likely) no documentation for the AtCoder Problems API that clarifies the response types, it was difficult to investigate which fields were nullable.

I later learned that serde_json::Value can represent any JSON, so using that might have been a bit easier.

However, I think it wasn't entirely meaningless, as writing these type definitions allowed me to realize that execution time information can be nullable.

Conclusion

That's it.

Discussion