🤖

AtCoder用Discord Botを作ってみた

2024/12/02に公開

この記事は 朝活部 Advent Calendar 2024 2日目の記事です

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

ユーザーを登録しておくと毎日0時にACした問題を報告してくれるBotです。

もともとPythonで書かれていたのですが、Rustで書き直してみました。

Rustにしてよかったところ

Embedの生成周りをイテレータを使ってきれいにできたのがうれしかったです。
仕様としては、

  • Embedのタイトルは「(ユーザー名)さんが昨日ACした問題」にし、ユーザーへのリンクにする
  • Embedのフィールドに解いた問題のタイトル、Difficulty、色、言語、提出リンクなどを列挙する
  • Embed左側の色は、解いた問題の中で最も高いDifficultyの色にし、解いた問題のDifficultyがすべて不明だった場合は黒色にする(APG4bなど一部の問題はDifficultyがnullになることがある)
  • フィールドが25個以上になった場合はEmbedを分割する(Discordの仕様で、25個以上のフィールドを送信することはできないため)

という感じです。


(Inlay Hintsを表示したかったのでスクリーンショットを貼っています)

Rustでは.chunks(25)を使って配列を25個区切りのイテレータにできるので、indexを持って剰余を計算して区切っていく必要がなくなりました。
また、問題をフィールドに変換する部分も問題情報のstructのimplに逃し、こちらではp.to_field()とすることで実装をスッキリさせることができました。

impl側の実装
    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,
            )
        }
    }

さらに、.mapを使うことで、Noneの可能性があるDifficulty値に対してNoneNoneを保ったまま計算を適応していくことができてかなりうれしかったです。Color(色のenum)にOrdトレイトをderiveすることでimpl Iterator<Item = Color>に対してmaxを取れるのも良いです。(こうすることで、Color::Blackは最下位なので、すべての問題のDifficultyが不明でないと選択されなくなります)

Rustにして大変だったところ

APIレスポンスの型定義がめんどくさい

APIレスポンスの結果をstructにするために型を定義する必要があり、少しでも間違えているとパースに失敗するので辛かったです。AtCoder ProblemsのAPIにはレスポンスの型がわかるようなドキュメントが(おそらく)ないので、nullableかどうか調べるのが大変でした。
後で知ったのですが、serde_json::Valueならすべてのjsonを表現できるらしいので、それを使えばもう少し楽だったかもしれません。
ただ、この型定義を書くことで実行時間の情報がnullableであることに気づけたので、意味がないこともないのかなとは思います。

おわり

おわり

Discussion